Merge branch 'feat/ui-1.0.4' into feat/ui-1.1.0

This commit is contained in:
haitao(lj) 2023-02-03 16:49:14 +08:00
commit e8246433a2
68 changed files with 3475 additions and 1856 deletions

View File

@ -1,6 +1,6 @@
.PHONY: build clean ui .PHONY: build clean ui
VERSION=1.0.2 VERSION=1.0.3
BIN=answer BIN=answer
DIR_SRC=./cmd/answer DIR_SRC=./cmd/answer
DOCKER_CMD=docker DOCKER_CMD=docker
@ -39,6 +39,6 @@ install-ui-packages:
@corepack prepare pnpm@v7.12.2 --activate @corepack prepare pnpm@v7.12.2 --activate
ui: ui:
@cd ui && pnpm install && pnpm build && cd - @cd ui && pnpm install && pnpm build && sed -i 's/%AnswerVersion%/'$(VERSION)'/g' ./build/index.html && cd -
all: clean build all: clean build

View File

@ -181,7 +181,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
reportAdminService := report_admin.NewReportAdminService(reportRepo, userCommon, commonRepo, answerRepo, questionRepo, commentCommonRepo, reportHandle, configRepo) reportAdminService := report_admin.NewReportAdminService(reportRepo, userCommon, commonRepo, answerRepo, questionRepo, commentCommonRepo, reportHandle, configRepo)
controller_adminReportController := controller_admin.NewReportController(reportAdminService) controller_adminReportController := controller_admin.NewReportController(reportAdminService)
userAdminRepo := user.NewUserAdminRepo(dataData, authRepo) userAdminRepo := user.NewUserAdminRepo(dataData, authRepo)
userAdminService := user_admin.NewUserAdminService(userAdminRepo, userRoleRelService, authService, userCommon) userAdminService := user_admin.NewUserAdminService(userAdminRepo, userRoleRelService, authService, userCommon, userActiveActivityRepo)
userAdminController := controller_admin.NewUserAdminController(userAdminService) userAdminController := controller_admin.NewUserAdminController(userAdminService)
reasonRepo := reason.NewReasonRepo(configRepo) reasonRepo := reason.NewReasonRepo(configRepo)
reasonService := reason2.NewReasonService(reasonRepo) reasonService := reason2.NewReasonService(reasonRepo)

View File

@ -6948,10 +6948,6 @@ const docTemplate = `{
}, },
"schema.SiteBrandingReq": { "schema.SiteBrandingReq": {
"type": "object", "type": "object",
"required": [
"logo",
"square_icon"
],
"properties": { "properties": {
"favicon": { "favicon": {
"type": "string", "type": "string",
@ -6973,10 +6969,6 @@ const docTemplate = `{
}, },
"schema.SiteBrandingResp": { "schema.SiteBrandingResp": {
"type": "object", "type": "object",
"required": [
"logo",
"square_icon"
],
"properties": { "properties": {
"favicon": { "favicon": {
"type": "string", "type": "string",

View File

@ -6936,10 +6936,6 @@
}, },
"schema.SiteBrandingReq": { "schema.SiteBrandingReq": {
"type": "object", "type": "object",
"required": [
"logo",
"square_icon"
],
"properties": { "properties": {
"favicon": { "favicon": {
"type": "string", "type": "string",
@ -6961,10 +6957,6 @@
}, },
"schema.SiteBrandingResp": { "schema.SiteBrandingResp": {
"type": "object", "type": "object",
"required": [
"logo",
"square_icon"
],
"properties": { "properties": {
"favicon": { "favicon": {
"type": "string", "type": "string",

View File

@ -1241,9 +1241,6 @@ definitions:
square_icon: square_icon:
maxLength: 512 maxLength: 512
type: string type: string
required:
- logo
- square_icon
type: object type: object
schema.SiteBrandingResp: schema.SiteBrandingResp:
properties: properties:
@ -1259,9 +1256,6 @@ definitions:
square_icon: square_icon:
maxLength: 512 maxLength: 512
type: string type: string
required:
- logo
- square_icon
type: object type: object
schema.SiteCustomCssHTMLReq: schema.SiteCustomCssHTMLReq:
properties: properties:

4
go.mod
View File

@ -15,7 +15,6 @@ require (
github.com/go-sql-driver/mysql v1.6.0 github.com/go-sql-driver/mysql v1.6.0
github.com/goccy/go-json v0.9.11 github.com/goccy/go-json v0.9.11
github.com/golang/mock v1.4.4 github.com/golang/mock v1.4.4
github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/google/wire v0.5.0 github.com/google/wire v0.5.0
github.com/gosimple/slug v1.13.1 github.com/gosimple/slug v1.13.1
@ -24,6 +23,7 @@ require (
github.com/jinzhu/now v1.1.5 github.com/jinzhu/now v1.1.5
github.com/lib/pq v1.10.7 github.com/lib/pq v1.10.7
github.com/mattn/go-sqlite3 v1.14.16 github.com/mattn/go-sqlite3 v1.14.16
github.com/microcosm-cc/bluemonday v1.0.21
github.com/mojocn/base64Captcha v1.3.5 github.com/mojocn/base64Captcha v1.3.5
github.com/ory/dockertest/v3 v3.9.1 github.com/ory/dockertest/v3 v3.9.1
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
@ -55,6 +55,7 @@ require (
github.com/Microsoft/go-winio v0.5.2 // indirect github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/andybalholm/brotli v1.0.4 // indirect github.com/andybalholm/brotli v1.0.4 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/containerd/continuity v0.3.0 // indirect github.com/containerd/continuity v0.3.0 // indirect
github.com/docker/cli v20.10.14+incompatible // indirect github.com/docker/cli v20.10.14+incompatible // indirect
@ -71,6 +72,7 @@ require (
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/gosimple/unidecode v1.0.1 // indirect github.com/gosimple/unidecode v1.0.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect github.com/imdario/mergo v0.3.12 // indirect

8
go.sum
View File

@ -82,6 +82,8 @@ github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6l
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
@ -256,8 +258,6 @@ github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8l
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c h1:iyaGYbCmcYK0Ja9a3OUa2Fo+EaN0cbLu0eKpBwPFzc8=
github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@ -301,6 +301,8 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
@ -476,6 +478,8 @@ github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg=
github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=

View File

@ -2,65 +2,65 @@
backend: backend:
base: base:
success: success:
other: "Success." other: "Erfolgreich."
unknown: unknown:
other: "Unknown error." other: "Unbekannter Fehler."
request_format_error: request_format_error:
other: "Request format is not valid." other: "Request format is not valid."
unauthorized_error: unauthorized_error:
other: "Unauthorized." other: "Nicht autorisiert."
database_error: database_error:
other: "Data server error." other: "Data server error."
role: role:
name: name:
user: user:
other: "User" other: "Nutzer"
admin: admin:
other: "Admin" other: "Admin"
moderator: moderator:
other: "Moderator" other: "Moderator"
description: description:
user: user:
other: "Default with no special access." other: "Standard ohne speziellen Zugriff."
admin: admin:
other: "Have the full power to access the site." other: "Have the full power to access the site."
moderator: moderator:
other: "Has access to all posts except admin settings." other: "Hat Zugriff auf alle Beiträge außer Admin-Einstellungen."
email: email:
other: "Email" other: "E-Mail"
password: password:
other: "Password" other: "Passwort"
email_or_password_wrong_error: email_or_password_wrong_error:
other: "Email and password do not match." other: "E-Mail und Password stimmen nicht überein."
error: error:
admin: admin:
email_or_password_wrong: email_or_password_wrong:
other: Email and password do not match. other: E-Mail und Password stimmen nicht überein.
answer: answer:
not_found: not_found:
other: "Answer do not found." other: "Answer do not found."
cannot_deleted: cannot_deleted:
other: "No permission to delete." other: "Keine Berechtigung zum Löschen."
cannot_update: cannot_update:
other: "No permission to update." other: "Keine Berechtigung zum Aktualisieren."
comment: comment:
edit_without_permission: edit_without_permission:
other: "Comment are not allowed to edit." other: "Kommentar kann nicht bearbeitet werden."
not_found: not_found:
other: "Comment not found." other: "Kommentar wurde nicht gefunden."
email: email:
duplicate: duplicate:
other: "Email already exists." other: "E-Mail existiert bereits."
need_to_be_verified: need_to_be_verified:
other: "Email should be verified." other: "E-Mail muss überprüft werden."
verify_url_expired: verify_url_expired:
other: "Email verified URL has expired, please resend the email." other: "Email verified URL has expired, please resend the email."
lang: lang:
not_found: not_found:
other: "Language file not found." other: "Sprachdatei nicht gefunden."
object: object:
captcha_verification_failed: captcha_verification_failed:
other: "Captcha wrong." other: "Captcha ist falsch."
disallow_follow: disallow_follow:
other: "You are not allowed to follow." other: "You are not allowed to follow."
disallow_vote: disallow_vote:
@ -70,22 +70,22 @@ backend:
not_found: not_found:
other: "Object not found." other: "Object not found."
verification_failed: verification_failed:
other: "Verification failed." other: "Verifizierung fehlgeschlagen."
email_or_password_incorrect: email_or_password_incorrect:
other: "Email and password do not match." other: "E-Mail und Password stimmen nicht überein."
old_password_verification_failed: old_password_verification_failed:
other: "The old password verification failed" other: "The old password verification failed"
new_password_same_as_previous_setting: new_password_same_as_previous_setting:
other: "The new password is the same as the previous one." other: "Das neue Passwort ist das gleiche wie das vorherige Passwort."
question: question:
not_found: not_found:
other: "Question not found." other: "Frage nicht gefunden."
cannot_deleted: cannot_deleted:
other: "No permission to delete." other: "Keine Berechtigung zum Löschen."
cannot_close: cannot_close:
other: "No permission to close." other: "No permission to close."
cannot_update: cannot_update:
other: "No permission to update." other: "Keine Berechtigung zum Aktualisieren."
rank: rank:
fail_to_meet_the_condition: fail_to_meet_the_condition:
other: "Rank fail to meet the condition." other: "Rank fail to meet the condition."
@ -96,7 +96,7 @@ backend:
other: "Report not found." other: "Report not found."
tag: tag:
not_found: not_found:
other: "Tag not found." other: "Schlagwort nicht gefunden."
recommend_tag_not_found: recommend_tag_not_found:
other: "Recommend Tag is not exist." other: "Recommend Tag is not exist."
recommend_tag_enter: recommend_tag_enter:
@ -107,6 +107,9 @@ backend:
other: "No permission to update." other: "No permission to update."
cannot_set_synonym_as_itself: cannot_set_synonym_as_itself:
other: "You cannot set the synonym of the current tag as itself." other: "You cannot set the synonym of the current tag as itself."
smtp:
config_from_name_cannot_be_email:
other: "The From Name cannot be a email address."
theme: theme:
not_found: not_found:
other: "Theme not found." other: "Theme not found."
@ -129,6 +132,10 @@ backend:
other: "Username is already in use." other: "Username is already in use."
set_avatar: set_avatar:
other: "Avatar set failed." other: "Avatar set failed."
cannot_update_your_role:
other: "You cannot modify your role."
not_allowed_registration:
other: "Currently the site is not open for registration"
config: config:
read_config_failed: read_config_failed:
other: "Read config failed" other: "Read config failed"
@ -140,10 +147,6 @@ backend:
install: install:
create_config_failed: create_config_failed:
other: "Cant create the config.yaml file." other: "Cant create the config.yaml file."
cannot_update_your_role:
other: "You cannot modify your role."
not_allowed_registration:
other: "Currently the site is not open for registration"
report: report:
spam: spam:
name: name:
@ -181,7 +184,7 @@ backend:
name: name:
other: "spam" other: "spam"
desc: desc:
other: "This question has been asked before and already has an answer." other: "Diese Frage ist bereits gestellt worden und hat bereits eine Antwort."
guideline: guideline:
name: name:
other: "a community-specific reason" other: "a community-specific reason"
@ -197,6 +200,13 @@ backend:
other: "something else" other: "something else"
desc: desc:
other: "This post requires another reason not listed above." other: "This post requires another reason not listed above."
operation_type:
asked:
other: "asked"
answered:
other: "answered"
modified:
other: "modified"
notification: notification:
action: action:
update_question: update_question:
@ -230,24 +240,24 @@ ui:
desc: >- desc: >-
<ul class="mb-0"><li><p class="mb-2">to make links</p><pre class="mb-2"><code>&lt;https://url.com&gt;<br/><br/>[Title](https://url.com)</code></pre></li><li><p class="mb-2">put returns between paragraphs</p></li><li><p class="mb-2"><em>_italic_</em> or **<strong>bold</strong>**</p></li><li><p class="mb-2">indent code by 4 spaces</p></li><li><p class="mb-2">quote by placing <code>&gt;</code> at start of line</p></li><li><p class="mb-2">backtick escapes <code>`like _this_`</code></p></li><li><p class="mb-2">create code fences with backticks <code>`</code></p><pre class="mb-0"><code>```<br/>code here<br/>```</code></pre></li></ul> <ul class="mb-0"><li><p class="mb-2">to make links</p><pre class="mb-2"><code>&lt;https://url.com&gt;<br/><br/>[Title](https://url.com)</code></pre></li><li><p class="mb-2">put returns between paragraphs</p></li><li><p class="mb-2"><em>_italic_</em> or **<strong>bold</strong>**</p></li><li><p class="mb-2">indent code by 4 spaces</p></li><li><p class="mb-2">quote by placing <code>&gt;</code> at start of line</p></li><li><p class="mb-2">backtick escapes <code>`like _this_`</code></p></li><li><p class="mb-2">create code fences with backticks <code>`</code></p><pre class="mb-0"><code>```<br/>code here<br/>```</code></pre></li></ul>
pagination: pagination:
prev: Prev prev: Zurück
next: Next next: Weiter
page_title: page_title:
question: Question question: Frage
questions: Questions questions: Fragen
tag: Tag tag: Schlagwort
tags: Tags tags: Schlagwörter
tag_wiki: tag wiki tag_wiki: tag wiki
edit_tag: Edit Tag edit_tag: Edit Tag
ask_a_question: Add Question ask_a_question: Add Question
edit_question: Edit Question edit_question: Edit Question
edit_answer: Edit Answer edit_answer: Edit Answer
search: Search search: Suche
posts_containing: Posts containing posts_containing: Posts containing
settings: Settings settings: Einstellungen
notifications: Notifications notifications: Notifications
login: Log In login: Anmelden
sign_up: Sign Up sign_up: Registrieren
account_recovery: Account Recovery account_recovery: Account Recovery
account_activation: Account Activation account_activation: Account Activation
confirm_email: Confirm Email confirm_email: Confirm Email
@ -367,7 +377,7 @@ ui:
unordered_list: unordered_list:
text: Bulleted List text: Bulleted List
table: table:
text: Table text: Tabelle
heading: Heading heading: Heading
cell: Cell cell: Cell
close_modal: close_modal:
@ -586,13 +596,13 @@ ui:
empty: Name cannot be empty. empty: Name cannot be empty.
range: Name up to 30 characters. range: Name up to 30 characters.
email: email:
label: Email label: E-Mail
msg: msg:
empty: Email cannot be empty. empty: E-Mail-Feld darf nicht leer sein.
password: password:
label: Password label: Passwort
msg: msg:
empty: Password cannot be empty. empty: Passwort-Feld darf nicht leer sein.
different: The passwords entered on both sides are inconsistent different: The passwords entered on both sides are inconsistent
account_forgot: account_forgot:
page_title: Forgot Your Password page_title: Forgot Your Password
@ -600,7 +610,7 @@ ui:
send_success: >- send_success: >-
If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly. If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly.
email: email:
label: Email label: E-Mail
msg: msg:
empty: Email cannot be empty. empty: Email cannot be empty.
change_email: change_email:
@ -674,24 +684,24 @@ ui:
radio: "Answers to your questions, comments, and more" radio: "Answers to your questions, comments, and more"
account: account:
heading: Account heading: Account
change_email_btn: Change email change_email_btn: E-Mail-Adresse ändern
change_pass_btn: Change password change_pass_btn: Passwort ändern
change_email_info: >- change_email_info: >-
We've sent an email to that address. Please follow the confirmation instructions. We've sent an email to that address. Please follow the confirmation instructions.
email: email:
label: Email label: E-Mail
msg: Email cannot be empty. msg: E-Mail-Feld darf nicht leer sein.
password_title: Password password_title: Passwort
current_pass: current_pass:
label: Current Password label: Aktuelles Passwort
msg: msg:
empty: Current Password cannot be empty. empty: Current Password cannot be empty.
length: The length needs to be between 8 and 32. length: The length needs to be between 8 and 32.
different: The two entered passwords do not match. different: The two entered passwords do not match.
new_pass: new_pass:
label: New Password label: Neues Passwort
pass_confirm: pass_confirm:
label: Confirm New Password label: Neues Passwort bestätigen
interface: interface:
heading: Interface heading: Interface
lang: lang:
@ -747,10 +757,10 @@ ui:
tip_question_deleted: This post has been deleted tip_question_deleted: This post has been deleted
tip_answer_deleted: This answer has been deleted tip_answer_deleted: This answer has been deleted
btns: btns:
confirm: Confirm confirm: Bestätigen
cancel: Cancel cancel: Abbrechen
save: Save save: Speichern
delete: Delete delete: Löschen
login: Log in login: Log in
signup: Sign up signup: Sign up
logout: Log out logout: Log out
@ -801,6 +811,11 @@ ui:
confirm_new_email: Your email has been updated. confirm_new_email: Your email has been updated.
confirm_new_email_invalid: >- confirm_new_email_invalid: >-
Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?
unsubscribe:
page_title: Unsubscribe
success_title: Unsubscribe Successful
success_desc: You have been successfully removed from this subscriber list and wont receive any further emails from us.
link: Change settings
question: question:
following_tags: Following Tags following_tags: Following Tags
edit: Edit edit: Edit
@ -871,7 +886,7 @@ ui:
placeholder: root placeholder: root
msg: Username cannot be empty. msg: Username cannot be empty.
db_password: db_password:
label: Password label: Passwort
placeholder: root placeholder: root
msg: Password cannot be empty. msg: Password cannot be empty.
db_host: db_host:
@ -991,13 +1006,14 @@ ui:
answer_links: Answer Links answer_links: Answer Links
documents: Documents documents: Documents
feedback: Feedback feedback: Feedback
support: Support
review: Review review: Review
config: Config config: Config
update_to: Update to update_to: Update to
latest: Latest latest: Latest
check_failed: Check failed check_failed: Check failed
"yes": "Yes" "yes": "Ja"
"no": "No" "no": "Nein"
not_allowed: Not allowed not_allowed: Not allowed
allowed: Allowed allowed: Allowed
enabled: Enabled enabled: Enabled
@ -1045,7 +1061,7 @@ ui:
users: users:
title: Users title: Users
name: Name name: Name
email: Email email: E-Mail
reputation: Reputation reputation: Reputation
created_at: Created Time created_at: Created Time
delete_at: Deleted Time delete_at: Deleted Time
@ -1184,7 +1200,7 @@ ui:
ssl: SSL ssl: SSL
none: None none: None
smtp_port: smtp_port:
label: SMTP Port label: SMTP-Port
msg: SMTP port must be number 1 ~ 65535. msg: SMTP port must be number 1 ~ 65535.
text: The port to your mail server. text: The port to your mail server.
smtp_username: smtp_username:

View File

@ -509,6 +509,8 @@ ui:
label: Revision label: Revision
answer: answer:
label: Answer label: Answer
feedback:
characters: content must be at least 6 characters in length.
edit_summary: edit_summary:
label: Edit Summary label: Edit Summary
placeholder: >- placeholder: >-
@ -748,7 +750,7 @@ ui:
text: User interface language. It will change when you refresh the page. text: User interface language. It will change when you refresh the page.
my_logins: my_logins:
title: My Logins title: My Logins
lable: Log in or sign up on this site using these accounts. label: Log in or sign up on this site using these accounts.
modal_title: Remove Login modal_title: Remove Login
modal_content: Are you sure you want to remove this login from your account? modal_content: Are you sure you want to remove this login from your account?
modal_confirm_btn: Remove modal_confirm_btn: Remove
@ -790,6 +792,7 @@ ui:
<p>Are you sure you want to add another answer?</p><p>You could use the <p>Are you sure you want to add another answer?</p><p>You could use the
edit link to refine and improve your existing answer, instead.</p> edit link to refine and improve your existing answer, instead.</p>
empty: Answer cannot be empty. empty: Answer cannot be empty.
characters: content must be at least 6 characters in length.
reopen: reopen:
title: Reopen this post title: Reopen this post
content: Are you sure you want to reopen? content: Are you sure you want to reopen?
@ -1012,7 +1015,11 @@ ui:
db_failed: Database connection failed db_failed: Database connection failed
db_failed_desc: >- db_failed_desc: >-
This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your hosts database server is down. This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your hosts database server is down.
counts:
views: views
votes: votes
answers: answers
accepted: Accepted
page_404: page_404:
desc: "Unfortunately, this page doesn't exist." desc: "Unfortunately, this page doesn't exist."
back_home: Back to homepage back_home: Back to homepage
@ -1050,7 +1057,7 @@ ui:
title: Admin title: Admin
dashboard: dashboard:
title: Dashboard title: Dashboard
welcome: Welcome to {{site_name}} Admin! welcome: Welcome to Answer Admin!
site_statistics: Site Statistics site_statistics: Site Statistics
questions: "Questions:" questions: "Questions:"
answers: "Answers:" answers: "Answers:"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -14,18 +14,18 @@ backend:
role: role:
name: name:
user: user:
other: "User" other: "Utente"
admin: admin:
other: "Admin" other: "Amministratore"
moderator: moderator:
other: "Moderator" other: "Moderatore"
description: description:
user: user:
other: "Default with no special access." other: "Predefinito senza alcun accesso speciale."
admin: admin:
other: "Have the full power to access the site." other: "Avere il pieno potere di accedere al sito."
moderator: moderator:
other: "Has access to all posts except admin settings." other: "Ha accesso a tutti i post tranne le impostazioni di amministratore."
email: email:
other: "email" other: "email"
password: password:
@ -40,9 +40,9 @@ backend:
not_found: not_found:
other: "Risposta non trovata" other: "Risposta non trovata"
cannot_deleted: cannot_deleted:
other: "No permission to delete." other: "Permesso per cancellare mancante."
cannot_update: cannot_update:
other: "No permission to update." other: "Nessun permesso per l'aggiornamento."
comment: comment:
edit_without_permission: edit_without_permission:
other: "Non si hanno di privilegi sufficienti per modificare il commento" other: "Non si hanno di privilegi sufficienti per modificare il commento"
@ -81,11 +81,11 @@ backend:
not_found: not_found:
other: "domanda non trovata" other: "domanda non trovata"
cannot_deleted: cannot_deleted:
other: "No permission to delete." other: "Permesso per cancellare mancante."
cannot_close: cannot_close:
other: "No permission to close." other: "Nessun permesso per chiudere."
cannot_update: cannot_update:
other: "No permission to update." other: "Nessun permesso per l'aggiornamento."
rank: rank:
fail_to_meet_the_condition: fail_to_meet_the_condition:
other: "Condizioni non valide per il grado" other: "Condizioni non valide per il grado"
@ -98,23 +98,26 @@ backend:
not_found: not_found:
other: "Etichetta non trovata" other: "Etichetta non trovata"
recommend_tag_not_found: recommend_tag_not_found:
other: "Recommend Tag is not exist." other: "Il Tag consigliato non esiste."
recommend_tag_enter: recommend_tag_enter:
other: "Please enter at least one required tag." other: "Inserisci almeno un tag."
not_contain_synonym_tags: not_contain_synonym_tags:
other: "Should not contain synonym tags." other: "Non deve contenere tag sinonimi."
cannot_update: cannot_update:
other: "No permission to update." other: "Nessun permesso per l'aggiornamento."
cannot_set_synonym_as_itself: cannot_set_synonym_as_itself:
other: "You cannot set the synonym of the current tag as itself." other: "Non puoi impostare il sinonimo del tag corrente come se stesso."
smtp:
config_from_name_cannot_be_email:
other: "The From Name cannot be a email address."
theme: theme:
not_found: not_found:
other: "tema non trovato" other: "tema non trovato"
revision: revision:
review_underway: review_underway:
other: "Can't edit currently, there is a version in the review queue." other: "Non è possibile modificare al momento, c'è una versione nella coda di revisione."
no_permission: no_permission:
other: "No permission to Revision." other: "Nessun permesso per la revisione."
user: user:
email_or_password_wrong: email_or_password_wrong:
other: other:
@ -128,7 +131,11 @@ backend:
username_duplicate: username_duplicate:
other: "utente già in uso" other: "utente già in uso"
set_avatar: set_avatar:
other: "Avatar set failed." other: "Inserimento dell'Avatar non riuscito."
cannot_update_your_role:
other: "Non puoi modificare il tuo ruolo."
not_allowed_registration:
other: "Al momento il sito non è aperto per la registrazione"
config: config:
read_config_failed: read_config_failed:
other: "Read config failed" other: "Read config failed"
@ -140,63 +147,66 @@ backend:
install: install:
create_config_failed: create_config_failed:
other: "Cant create the config.yaml file." other: "Cant create the config.yaml file."
cannot_update_your_role:
other: "You cannot modify your role."
not_allowed_registration:
other: "Currently the site is not open for registration"
report: report:
spam: spam:
name: name:
other: "spam" other: "posta indesiderata"
desc: desc:
other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic." other: "Questo articolo è una pubblicità o vandalismo. Non è utile o rilevante all'argomento corrente."
rude: rude:
name: name:
other: "scortese o violento" other: "scortese o violento"
desc: desc:
other: "A reasonable person would find this content inappropriate for respectful discourse." other: "Una persona ragionevole trova questo contenuto inappropriato a un discorso rispettoso."
duplicate: duplicate:
name: name:
other: "duplicato" other: "duplicato"
desc: desc:
other: "This question has been asked before and already has an answer." other: "Questa domanda è già stata posta e ha già una risposta."
not_answer: not_answer:
name: name:
other: "non è una risposta" other: "non è una risposta"
desc: desc:
other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether." other: "Questo è stato pubblicato come una risposta, ma non tenta di rispondere alla domanda. Dovrebbe forse essere una modifica, un commento, un'altra domanda, o cancellata del tutto."
not_need: not_need:
name: name:
other: "non più necessario" other: "non più necessario"
desc: desc:
other: "This comment is outdated, conversational or not relevant to this post." other: "Questo commento è obsoleto, conversazionale o non pertinente per questo post."
other: other:
name: name:
other: "altro" other: "altro"
desc: desc:
other: "This post requires staff attention for another reason not listed above." other: "Questo articolo richiede una supervisione dello staff per altre ragioni non listate sopra."
question: question:
close: close:
duplicate: duplicate:
name: name:
other: "spam" other: "posta indesiderata"
desc: desc:
other: "This question has been asked before and already has an answer." other: "Questa domanda è già stata posta e ha già una risposta."
guideline: guideline:
name: name:
other: "motivo legato alla community" other: "motivo legato alla community"
desc: desc:
other: "This question doesn't meet a community guideline." other: "Questa domanda non soddisfa le linee guida della comunità."
multiple: multiple:
name: name:
other: "richiede maggiori dettagli o chiarezza" other: "richiede maggiori dettagli o chiarezza"
desc: desc:
other: "This question currently includes multiple questions in one. It should focus on one problem only." other: "Questa domanda attualmente include più domande in uno. Dovrebbe concentrarsi su un solo problema."
other: other:
name: name:
other: "altro" other: "altro"
desc: desc:
other: "This post requires another reason not listed above." other: "Questo articolo richiede un'altro motivo non listato sopra."
operation_type:
asked:
other: "asked"
answered:
other: "answered"
modified:
other: "modified"
notification: notification:
action: action:
update_question: update_question:
@ -206,7 +216,7 @@ backend:
update_answer: update_answer:
other: "risposta aggiornata" other: "risposta aggiornata"
accept_answer: accept_answer:
other: "risposta accepted" other: "risposta accettata"
comment_question: comment_question:
other: "domanda commentata" other: "domanda commentata"
comment_answer: comment_answer:
@ -226,21 +236,21 @@ backend:
#The following fields are used for interface presentation(Front-end) #The following fields are used for interface presentation(Front-end)
ui: ui:
how_to_format: how_to_format:
title: How to Format title: Come formattare
desc: >- desc: >-
<ul class="mb-0"><li><p class="mb-2">to make links</p><pre class="mb-2"><code>&lt;https://url.com&gt;<br/><br/>[Title](https://url.com)</code></pre></li><li><p class="mb-2">put returns between paragraphs</p></li><li><p class="mb-2"><em>_italic_</em> or **<strong>bold</strong>**</p></li><li><p class="mb-2">indent code by 4 spaces</p></li><li><p class="mb-2">quote by placing <code>&gt;</code> at start of line</p></li><li><p class="mb-2">backtick escapes <code>`like _this_`</code></p></li><li><p class="mb-2">create code fences with backticks <code>`</code></p><pre class="mb-0"><code>```<br/>code here<br/>```</code></pre></li></ul> <ul class="mb-0"><li><p class="mb-2">to make links</p><pre class="mb-2"><code>&lt;https://url.com&gt;<br/><br/>[Title](https://url. om)</code></pre></li><li><p class="mb-2">mette i rendimenti tra i paragrafi</p></li><li><p class="mb-2"><em>_italic_</em> o **<strong>grassetto</strong>**</p></li><li><p class="mb-2">trattino di codice per 4 spazi</p></li><li><p class="mb-2">preventivo inserendo <code>&gt;</code> all'inizio della riga</p></li><li><p class="mb-2">backtick escapes <code>`like _this_`</code></p></li><li><p class="mb-2">crea recinzioni di codice con backticks <code>`</code></p><pre class="mb-0">````<code><br/>codice qui<br/>`````</code></pre></li></ul>
pagination: pagination:
prev: Prev prev: Prec
next: Next next: Successivo
page_title: page_title:
question: Question question: Domanda
questions: Questions questions: Domande
tag: Tag tag: Tag
tags: Tags tags: Tags
tag_wiki: tag wiki tag_wiki: tag wiki
edit_tag: Edit Tag edit_tag: Modifica Tag
ask_a_question: Add Question ask_a_question: Aggiungi una domanda
edit_question: Edit Question edit_question: Modifica Domanda
edit_answer: Edit Answer edit_answer: Edit Answer
search: Search search: Search
posts_containing: Posts containing posts_containing: Posts containing
@ -801,6 +811,11 @@ ui:
confirm_new_email: Your email has been updated. confirm_new_email: Your email has been updated.
confirm_new_email_invalid: >- confirm_new_email_invalid: >-
Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?
unsubscribe:
page_title: Unsubscribe
success_title: Unsubscribe Successful
success_desc: You have been successfully removed from this subscriber list and wont receive any further emails from us.
link: Change settings
question: question:
following_tags: Following Tags following_tags: Following Tags
edit: Edit edit: Edit
@ -991,6 +1006,7 @@ ui:
answer_links: Answer Links answer_links: Answer Links
documents: Documents documents: Documents
feedback: Feedback feedback: Feedback
support: Support
review: Review review: Review
config: Config config: Config
update_to: Update to update_to: Update to

View File

@ -107,6 +107,9 @@ backend:
other: "No permission to update." other: "No permission to update."
cannot_set_synonym_as_itself: cannot_set_synonym_as_itself:
other: "You cannot set the synonym of the current tag as itself." other: "You cannot set the synonym of the current tag as itself."
smtp:
config_from_name_cannot_be_email:
other: "The From Name cannot be a email address."
theme: theme:
not_found: not_found:
other: "Theme not found." other: "Theme not found."
@ -129,6 +132,10 @@ backend:
other: "Username is already in use." other: "Username is already in use."
set_avatar: set_avatar:
other: "Avatar set failed." other: "Avatar set failed."
cannot_update_your_role:
other: "You cannot modify your role."
not_allowed_registration:
other: "Currently the site is not open for registration"
config: config:
read_config_failed: read_config_failed:
other: "Read config failed" other: "Read config failed"
@ -140,10 +147,6 @@ backend:
install: install:
create_config_failed: create_config_failed:
other: "Cant create the config.yaml file." other: "Cant create the config.yaml file."
cannot_update_your_role:
other: "You cannot modify your role."
not_allowed_registration:
other: "Currently the site is not open for registration"
report: report:
spam: spam:
name: name:
@ -197,6 +200,13 @@ backend:
other: "something else" other: "something else"
desc: desc:
other: "This post requires another reason not listed above." other: "This post requires another reason not listed above."
operation_type:
asked:
other: "asked"
answered:
other: "answered"
modified:
other: "modified"
notification: notification:
action: action:
update_question: update_question:
@ -801,6 +811,11 @@ ui:
confirm_new_email: Your email has been updated. confirm_new_email: Your email has been updated.
confirm_new_email_invalid: >- confirm_new_email_invalid: >-
Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?
unsubscribe:
page_title: Unsubscribe
success_title: Unsubscribe Successful
success_desc: You have been successfully removed from this subscriber list and wont receive any further emails from us.
link: Change settings
question: question:
following_tags: Following Tags following_tags: Following Tags
edit: Edit edit: Edit
@ -991,6 +1006,7 @@ ui:
answer_links: Answer Links answer_links: Answer Links
documents: Documents documents: Documents
feedback: Feedback feedback: Feedback
support: Support
review: Review review: Review
config: Config config: Config
update_to: Update to update_to: Update to

View File

@ -107,6 +107,9 @@ backend:
other: "No permission to update." other: "No permission to update."
cannot_set_synonym_as_itself: cannot_set_synonym_as_itself:
other: "You cannot set the synonym of the current tag as itself." other: "You cannot set the synonym of the current tag as itself."
smtp:
config_from_name_cannot_be_email:
other: "The From Name cannot be a email address."
theme: theme:
not_found: not_found:
other: "Theme not found." other: "Theme not found."
@ -129,6 +132,10 @@ backend:
other: "Username is already in use." other: "Username is already in use."
set_avatar: set_avatar:
other: "Avatar set failed." other: "Avatar set failed."
cannot_update_your_role:
other: "You cannot modify your role."
not_allowed_registration:
other: "Currently the site is not open for registration"
config: config:
read_config_failed: read_config_failed:
other: "Read config failed" other: "Read config failed"
@ -140,10 +147,6 @@ backend:
install: install:
create_config_failed: create_config_failed:
other: "Cant create the config.yaml file." other: "Cant create the config.yaml file."
cannot_update_your_role:
other: "You cannot modify your role."
not_allowed_registration:
other: "Currently the site is not open for registration"
report: report:
spam: spam:
name: name:
@ -197,6 +200,13 @@ backend:
other: "something else" other: "something else"
desc: desc:
other: "This post requires another reason not listed above." other: "This post requires another reason not listed above."
operation_type:
asked:
other: "asked"
answered:
other: "answered"
modified:
other: "modified"
notification: notification:
action: action:
update_question: update_question:
@ -801,6 +811,11 @@ ui:
confirm_new_email: Your email has been updated. confirm_new_email: Your email has been updated.
confirm_new_email_invalid: >- confirm_new_email_invalid: >-
Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?
unsubscribe:
page_title: Unsubscribe
success_title: Unsubscribe Successful
success_desc: You have been successfully removed from this subscriber list and wont receive any further emails from us.
link: Change settings
question: question:
following_tags: Following Tags following_tags: Following Tags
edit: Edit edit: Edit
@ -991,6 +1006,7 @@ ui:
answer_links: Answer Links answer_links: Answer Links
documents: Documents documents: Documents
feedback: Feedback feedback: Feedback
support: Support
review: Review review: Review
config: Config config: Config
update_to: Update to update_to: Update to

View File

@ -2,15 +2,15 @@
backend: backend:
base: base:
success: success:
other: "Success." other: "Sucesso."
unknown: unknown:
other: "Unknown error." other: "Erro desconhecido."
request_format_error: request_format_error:
other: "Request format is not valid." other: "Formato de solicitação não é válido."
unauthorized_error: unauthorized_error:
other: "Unauthorized." other: "Não autorizado."
database_error: database_error:
other: "Data server error." other: "Erro no servidor de dados."
role: role:
name: name:
user: user:
@ -107,6 +107,9 @@ backend:
other: "No permission to update." other: "No permission to update."
cannot_set_synonym_as_itself: cannot_set_synonym_as_itself:
other: "You cannot set the synonym of the current tag as itself." other: "You cannot set the synonym of the current tag as itself."
smtp:
config_from_name_cannot_be_email:
other: "The From Name cannot be a email address."
theme: theme:
not_found: not_found:
other: "Theme not found." other: "Theme not found."
@ -129,6 +132,10 @@ backend:
other: "Username is already in use." other: "Username is already in use."
set_avatar: set_avatar:
other: "Avatar set failed." other: "Avatar set failed."
cannot_update_your_role:
other: "You cannot modify your role."
not_allowed_registration:
other: "Currently the site is not open for registration"
config: config:
read_config_failed: read_config_failed:
other: "Read config failed" other: "Read config failed"
@ -140,10 +147,6 @@ backend:
install: install:
create_config_failed: create_config_failed:
other: "Cant create the config.yaml file." other: "Cant create the config.yaml file."
cannot_update_your_role:
other: "You cannot modify your role."
not_allowed_registration:
other: "Currently the site is not open for registration"
report: report:
spam: spam:
name: name:
@ -197,6 +200,13 @@ backend:
other: "something else" other: "something else"
desc: desc:
other: "This post requires another reason not listed above." other: "This post requires another reason not listed above."
operation_type:
asked:
other: "asked"
answered:
other: "answered"
modified:
other: "modified"
notification: notification:
action: action:
update_question: update_question:
@ -801,6 +811,11 @@ ui:
confirm_new_email: Your email has been updated. confirm_new_email: Your email has been updated.
confirm_new_email_invalid: >- confirm_new_email_invalid: >-
Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?
unsubscribe:
page_title: Unsubscribe
success_title: Unsubscribe Successful
success_desc: You have been successfully removed from this subscriber list and wont receive any further emails from us.
link: Change settings
question: question:
following_tags: Following Tags following_tags: Following Tags
edit: Edit edit: Edit
@ -991,6 +1006,7 @@ ui:
answer_links: Answer Links answer_links: Answer Links
documents: Documents documents: Documents
feedback: Feedback feedback: Feedback
support: Support
review: Review review: Review
config: Config config: Config
update_to: Update to update_to: Update to

View File

@ -107,6 +107,9 @@ backend:
other: "No permission to update." other: "No permission to update."
cannot_set_synonym_as_itself: cannot_set_synonym_as_itself:
other: "You cannot set the synonym of the current tag as itself." other: "You cannot set the synonym of the current tag as itself."
smtp:
config_from_name_cannot_be_email:
other: "The From Name cannot be a email address."
theme: theme:
not_found: not_found:
other: "Theme not found." other: "Theme not found."
@ -129,6 +132,10 @@ backend:
other: "Username is already in use." other: "Username is already in use."
set_avatar: set_avatar:
other: "Avatar set failed." other: "Avatar set failed."
cannot_update_your_role:
other: "You cannot modify your role."
not_allowed_registration:
other: "Currently the site is not open for registration"
config: config:
read_config_failed: read_config_failed:
other: "Read config failed" other: "Read config failed"
@ -140,10 +147,6 @@ backend:
install: install:
create_config_failed: create_config_failed:
other: "Cant create the config.yaml file." other: "Cant create the config.yaml file."
cannot_update_your_role:
other: "You cannot modify your role."
not_allowed_registration:
other: "Currently the site is not open for registration"
report: report:
spam: spam:
name: name:
@ -197,6 +200,13 @@ backend:
other: "something else" other: "something else"
desc: desc:
other: "This post requires another reason not listed above." other: "This post requires another reason not listed above."
operation_type:
asked:
other: "asked"
answered:
other: "answered"
modified:
other: "modified"
notification: notification:
action: action:
update_question: update_question:
@ -801,6 +811,11 @@ ui:
confirm_new_email: Your email has been updated. confirm_new_email: Your email has been updated.
confirm_new_email_invalid: >- confirm_new_email_invalid: >-
Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?
unsubscribe:
page_title: Unsubscribe
success_title: Unsubscribe Successful
success_desc: You have been successfully removed from this subscriber list and wont receive any further emails from us.
link: Change settings
question: question:
following_tags: Following Tags following_tags: Following Tags
edit: Edit edit: Edit
@ -991,6 +1006,7 @@ ui:
answer_links: Answer Links answer_links: Answer Links
documents: Documents documents: Documents
feedback: Feedback feedback: Feedback
support: Support
review: Review review: Review
config: Config config: Config
update_to: Update to update_to: Update to

View File

@ -107,6 +107,9 @@ backend:
other: "No permission to update." other: "No permission to update."
cannot_set_synonym_as_itself: cannot_set_synonym_as_itself:
other: "You cannot set the synonym of the current tag as itself." other: "You cannot set the synonym of the current tag as itself."
smtp:
config_from_name_cannot_be_email:
other: "The From Name cannot be a email address."
theme: theme:
not_found: not_found:
other: "Theme not found." other: "Theme not found."
@ -129,6 +132,10 @@ backend:
other: "Username is already in use." other: "Username is already in use."
set_avatar: set_avatar:
other: "Avatar set failed." other: "Avatar set failed."
cannot_update_your_role:
other: "You cannot modify your role."
not_allowed_registration:
other: "Currently the site is not open for registration"
config: config:
read_config_failed: read_config_failed:
other: "Read config failed" other: "Read config failed"
@ -140,10 +147,6 @@ backend:
install: install:
create_config_failed: create_config_failed:
other: "Cant create the config.yaml file." other: "Cant create the config.yaml file."
cannot_update_your_role:
other: "You cannot modify your role."
not_allowed_registration:
other: "Currently the site is not open for registration"
report: report:
spam: spam:
name: name:
@ -197,6 +200,13 @@ backend:
other: "something else" other: "something else"
desc: desc:
other: "This post requires another reason not listed above." other: "This post requires another reason not listed above."
operation_type:
asked:
other: "asked"
answered:
other: "answered"
modified:
other: "modified"
notification: notification:
action: action:
update_question: update_question:
@ -801,6 +811,11 @@ ui:
confirm_new_email: Your email has been updated. confirm_new_email: Your email has been updated.
confirm_new_email_invalid: >- confirm_new_email_invalid: >-
Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?
unsubscribe:
page_title: Unsubscribe
success_title: Unsubscribe Successful
success_desc: You have been successfully removed from this subscriber list and wont receive any further emails from us.
link: Change settings
question: question:
following_tags: Following Tags following_tags: Following Tags
edit: Edit edit: Edit
@ -991,6 +1006,7 @@ ui:
answer_links: Answer Links answer_links: Answer Links
documents: Documents documents: Documents
feedback: Feedback feedback: Feedback
support: Support
review: Review review: Review
config: Config config: Config
update_to: Update to update_to: Update to

View File

@ -2,15 +2,15 @@
backend: backend:
base: base:
success: success:
other: "成功" other: "成功"
unknown: unknown:
other: "未知错误" other: "未知错误"
request_format_error: request_format_error:
other: "请求格式错误" other: "请求格式错误"
unauthorized_error: unauthorized_error:
other: "未登录" other: "未授权。"
database_error: database_error:
other: "数据服务异常" other: "数据服务器错误。"
role: role:
name: name:
user: user:
@ -23,38 +23,38 @@ backend:
user: user:
other: "默认没有特殊访问权限。" other: "默认没有特殊访问权限。"
admin: admin:
other: "拥有进入网站的全部权限。" other: "拥有管理网站的全部权限。"
moderator: moderator:
other: "有权访问所有的帖子,无法进入管理员设置页面。" other: "拥有访问除管理员设置以外的所有权限。"
email: email:
other: "邮箱" other: "邮箱"
password: password:
other: "密码" other: "密码"
email_or_password_wrong_error: email_or_password_wrong_error:
other: "邮箱或密码错误" other: "邮箱和密码不匹配。"
error: error:
admin: admin:
email_or_password_wrong: email_or_password_wrong:
other: 邮箱或密码错误 other: 邮箱和密码不匹配。
answer: answer:
not_found: not_found:
other: "答案未找到" other: "没有找到答案。"
cannot_deleted: cannot_deleted:
other: "无删除权限" other: "没有删除权限。"
cannot_update: cannot_update:
other: "无修改权限" other: "没有更新权限。"
comment: comment:
edit_without_permission: edit_without_permission:
other: "不允许编辑评论" other: "不允许编辑评论"
not_found: not_found:
other: "评论未找到" other: "评论未找到"
email: email:
duplicate: duplicate:
other: "邮箱已经存在" other: "邮箱已经存在"
need_to_be_verified: need_to_be_verified:
other: "邮箱需要验证" other: "邮箱需要验证"
verify_url_expired: verify_url_expired:
other: "邮箱验证的网址已过期,请重新发送邮件" other: "邮箱验证的网址已过期,请重新发送邮件"
lang: lang:
not_found: not_found:
other: "语言未找到" other: "语言未找到"
@ -106,7 +106,10 @@ backend:
cannot_update: cannot_update:
other: "没有更新标签权限。" other: "没有更新标签权限。"
cannot_set_synonym_as_itself: cannot_set_synonym_as_itself:
other: "你无法将当前标签的同义词设置为当前标签自己" other: "您不能将当前标签的同义词设置为本身。"
smtp:
config_from_name_cannot_be_email:
other: "发件人名称不能是电子邮件地址。"
theme: theme:
not_found: not_found:
other: "主题未找到" other: "主题未找到"
@ -129,75 +132,81 @@ backend:
other: "用户名已被使用" other: "用户名已被使用"
set_avatar: set_avatar:
other: "头像设置错误" other: "头像设置错误"
cannot_update_your_role:
other: "您不能修改自己的角色。"
not_allowed_registration:
other: "目前该站点未开放注册"
config: config:
read_config_failed: read_config_failed:
other: "读取配置失败" other: "读取配置失败"
database: database:
connection_failed: connection_failed:
other: "数据连接异常!" other: "数据库连接失败"
create_table_failed: create_table_failed:
other: "创建表失败" other: "创建表失败"
install: install:
create_config_failed: create_config_failed:
other: "无法创建配置文件" other: "无法创建 config.yaml 文件。"
cannot_update_your_role:
other: "你无法修改自己的角色"
not_allowed_registration:
other: "目前该网站尚未开放注册"
report: report:
spam: spam:
name: name:
other: "垃圾信息" other: "垃圾信息"
desc: desc:
other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic." other: "这个帖子是一个广告,或是破坏性行为。它对当前的主题没有用处,也不相关。"
rude: rude:
name: name:
other: "粗鲁或辱骂的" other: "粗鲁或辱骂的"
desc: desc:
other: "A reasonable person would find this content inappropriate for respectful discourse." other: "一个有理智的人都会认为这种内容不适合进行尊重性的讨论。"
duplicate: duplicate:
name: name:
other: "重复信息" other: "重复信息"
desc: desc:
other: "This question has been asked before and already has an answer." other: "此问题以前就有人问过,而且已经有了答案。"
not_answer: not_answer:
name: name:
other: "不是答案" other: "不是答案"
desc: desc:
other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether." other: "此帖子是作为一个答案发布的,但它并没有试图回答这个问题。总之,它可能应该是个编辑,评论,另一个问题或者被删除。"
not_need: not_need:
name: name:
other: "不再需要" other: "不再需要"
desc: desc:
other: "This comment is outdated, conversational or not relevant to this post." other: "此评论已过时,对话或与此帖子无关。"
other: other:
name: name:
other: "其他原因" other: "其他原因"
desc: desc:
other: "This post requires staff attention for another reason not listed above." other: "此帖子需要工作人员关注,因为是上述所列以外的其他理由。"
question: question:
close: close:
duplicate: duplicate:
name: name:
other: "垃圾信息" other: "垃圾信息"
desc: desc:
other: "This question has been asked before and already has an answer." other: "此问题以前就有人问过,而且已经有了答案。"
guideline: guideline:
name: name:
other: "社区特定原因" other: "社区特定原因"
desc: desc:
other: "This question doesn't meet a community guideline." other: "此问题不符合社区准则。"
multiple: multiple:
name: name:
other: "需要细节或澄清" other: "需要细节或澄清"
desc: desc:
other: "This question currently includes multiple questions in one. It should focus on one problem only." other: "此问题目前涵盖多个问题。它应该只集中在一个问题上。"
other: other:
name: name:
other: "其他原因" other: "其他原因"
desc: desc:
other: "This post requires another reason not listed above." other: "这个帖子需要上面没有列出的另一个原因。"
operation_type:
asked:
other: "提问于"
answered:
other: "回答于"
modified:
other: "修改于"
notification: notification:
action: action:
update_question: update_question:
@ -207,7 +216,7 @@ backend:
update_answer: update_answer:
other: "更新了答案" other: "更新了答案"
accept_answer: accept_answer:
other: "接受了答案" other: "已接受的回答"
comment_question: comment_question:
other: "评论了问题" other: "评论了问题"
comment_answer: comment_answer:
@ -229,15 +238,7 @@ ui:
how_to_format: how_to_format:
title: 如何设定文本格式 title: 如何设定文本格式
desc: >- desc: >-
<ul class="mb-0"><li><p class="mb-2">添加链接:</p><pre <ul class="mb-0"><li><p class="mb-2">添加链接:</p><pre class="mb-2"><code>&lt;https://url.com&gt;<br/><br/>[标题](https://url.com)</code></pre></li><li><p class="mb-2">段落之间使用空行分隔</p></li><li><p class="mb-2"><em>_斜体_</em> 或者 **<strong>粗体</strong>**</p></li><li><p class="mb-2">使用 4 个空格缩进代码</p></li><li><p class="mb-2">在行首添加<code>&gt;</code>表示引用</p></li><li><p class="mb-2">反引号进行转义 <code>`像 _这样_`</code></p></li><li><p class="mb-2">使用<code>```</code>创建代码块</p><pre class="mb-0"><code>```<br/>// 这是代码<br/>```</code></pre></li></ul>
class="mb-2"><code>&lt;https://url.com&gt;<br/><br/>[标题](https://url.com)</code></pre></li><li><p
class="mb-2">段落之间使用空行分隔</p></li><li><p class="mb-2"><em>_斜体_</em> 或者
**<strong>粗体</strong>**</p></li><li><p class="mb-2">使用 4
个空格缩进代码</p></li><li><p
class="mb-2">在行首添加<code>&gt;</code>表示引用</p></li><li><p class="mb-2">反引号进行转义
<code>`像 _这样_`</code></p></li><li><p
class="mb-2">使用<code>```</code>创建代码块</p><pre class="mb-0"><code>```<br/>//
这是代码<br/>```</code></pre></li></ul>
pagination: pagination:
prev: 上一页 prev: 上一页
next: 下一页 next: 下一页
@ -266,7 +267,7 @@ ui:
install: Answer 安装 install: Answer 安装
upgrade: Answer 升级 upgrade: Answer 升级
maintenance: 网站维护 maintenance: 网站维护
users: Users users: 用户
notifications: notifications:
title: 通知 title: 通知
inbox: 收件箱 inbox: 收件箱
@ -290,7 +291,7 @@ ui:
class_diagram: 类图 class_diagram: 类图
state_diagram: 状态图 state_diagram: 状态图
entity_relationship_diagram: ER 图 entity_relationship_diagram: ER 图
user_defined_diagram: User defined diagram user_defined_diagram: 用户自定义图表
gantt_chart: 甘特图 gantt_chart: 甘特图
pie_chart: 饼图 pie_chart: 饼图
code: code:
@ -339,7 +340,7 @@ ui:
only_image: 只能上传图片文件。 only_image: 只能上传图片文件。
max_size: 图片文件大小不能超过 4 MB。 max_size: 图片文件大小不能超过 4 MB。
desc: desc:
label: 图片描述(可选) label: 描述(可选)
tab_url: 网络图片 tab_url: 网络图片
form_url: form_url:
fields: fields:
@ -410,13 +411,13 @@ ui:
range: 不能超过 35 个字符 range: 不能超过 35 个字符
slug_name: slug_name:
label: URL 固定链接 label: URL 固定链接
desc: '必须由 "a-z", "0-9", "+ # - ." 组成' desc: '必须使用字符集 "a-z"、"0-9"、"+ # - ."'
msg: msg:
empty: 不能为空 empty: 不能为空
range: 不能超过 35 个字符 range: 不能超过 35 个字符
character: 包含非法字符 character: 包含非法字符
desc: desc:
label: 标签描述(可选) label: 描述(可选)
btn_cancel: 取消 btn_cancel: 取消
btn_submit: 提交 btn_submit: 提交
tag_info: tag_info:
@ -564,8 +565,7 @@ ui:
placeholder: 搜索 placeholder: 搜索
footer: footer:
build_on: >- build_on: >-
Built on <1> Answer </1>- the open-source software that powers Q&A 基于<1>Answer</1>--为问答社区提供动力的开源软件。<br />Made with love © {{cc}}.
communities<br />Made with love © 2022 Answer
upload_img: upload_img:
name: 更改图片 name: 更改图片
loading: 加载中... loading: 加载中...
@ -616,7 +616,7 @@ ui:
btn_cancel: 取消 btn_cancel: 取消
btn_update: 更新电子邮件地址 btn_update: 更新电子邮件地址
send_success: >- send_success: >-
If an account matches <strong>{{mail}}</strong>, you should receive an email with instructions on how to reset your password shortly. 如果账户与<strong>{{mail}}</strong>相匹配,您应该很快就会收到一封电子邮件,说明如何重置您的密码。
email: email:
label: 新邮箱 label: 新邮箱
msg: msg:
@ -645,8 +645,8 @@ ui:
account: 账号 account: 账号
interface: 界面 interface: 界面
profile: profile:
heading: Profile heading: 个人资料
btn_name: Save btn_name: 保存
display_name: display_name:
label: 昵称 label: 昵称
msg: 昵称不能为空 msg: 昵称不能为空
@ -664,7 +664,7 @@ ui:
custom: 自定义 custom: 自定义
btn_refresh: 刷新 btn_refresh: 刷新
custom_text: 您可以上传您的图片。 custom_text: 您可以上传您的图片。
default: System default: 系统
msg: 请上传头像 msg: 请上传头像
bio: bio:
label: 关于我 (可选) label: 关于我 (可选)
@ -676,12 +676,12 @@ ui:
label: 位置 (可选) label: 位置 (可选)
placeholder: "城市, 国家" placeholder: "城市, 国家"
notification: notification:
heading: Notifications heading: 通知
email: email:
label: 邮件通知 label: 邮件通知
radio: "你的提问有新的回答,评论,和其他" radio: "你的提问有新的回答,评论,和其他"
account: account:
heading: Account heading: 账号
change_email_btn: 更改邮箱 change_email_btn: 更改邮箱
change_pass_btn: 更改密码 change_pass_btn: 更改密码
change_email_info: >- change_email_info: >-
@ -701,7 +701,7 @@ ui:
pass_confirm: pass_confirm:
label: 确认新密码 label: 确认新密码
interface: interface:
heading: Interface heading: 界面
lang: lang:
label: 界面语言 label: 界面语言
text: 设置用户界面语言,在刷新页面后生效。 text: 设置用户界面语言,在刷新页面后生效。
@ -709,7 +709,7 @@ ui:
update: 更新成功 update: 更新成功
update_password: 更改密码成功。 update_password: 更改密码成功。
flag_success: 感谢您的标记,我们会尽快处理。 flag_success: 感谢您的标记,我们会尽快处理。
forbidden_operate_self: Forbidden to operate on yourself forbidden_operate_self: 禁止自己操作
review: 您的修订将在审核通过后显示。 review: 您的修订将在审核通过后显示。
related_question: related_question:
title: 相关问题 title: 相关问题
@ -735,16 +735,16 @@ ui:
write_answer: write_answer:
title: 你的回答 title: 你的回答
btn_name: 提交你的回答 btn_name: 提交你的回答
add_another_answer: Add another answer add_another_answer: 添加另一个答案
confirm_title: 继续回答 confirm_title: 继续回答
continue: 继续 continue: 继续
confirm_info: >- confirm_info: >-
<p>您确定要提交一个新的回答吗?</p><p>您可以直接编辑和改善您之前的回答的。</p> <p>您确定要提交一个新的回答吗?</p><p>您可以直接编辑和改善您之前的回答的。</p>
empty: 回答内容不能为空。 empty: 回答内容不能为空。
reopen: reopen:
title: Reopen this post title: 重新打开这个帖子
content: Are you sure you want to reopen? content: 确定要重新打开吗?
success: This post has been reopened success: 这个帖子已被重新打开
delete: delete:
title: 删除 title: 删除
question: >- question: >-
@ -808,6 +808,11 @@ ui:
confirm_new_email: 你的电子邮箱已更新 confirm_new_email: 你的电子邮箱已更新
confirm_new_email_invalid: >- confirm_new_email_invalid: >-
抱歉,此验证链接已失效。也许是你的邮箱已经成功更改了? 抱歉,此验证链接已失效。也许是你的邮箱已经成功更改了?
unsubscribe:
page_title: 退订
success_title: 取消订阅成功
success_desc: 您已成功地从此订阅者列表中移除,并且将不会再收到我们的任何电子邮件。
link: 更改设置
question: question:
following_tags: 已关注的标签 following_tags: 已关注的标签
edit: 编辑 edit: 编辑
@ -870,9 +875,9 @@ ui:
done: 完成 done: 完成
config_yaml_error: 无法创建配置文件 config_yaml_error: 无法创建配置文件
lang: lang:
label: Please Choose a Language label: 请选择一种语言
db_type: db_type:
label: Database Engine label: 数据库引擎
db_username: db_username:
label: 用户名 label: 用户名
placeholder: root placeholder: root
@ -887,17 +892,17 @@ ui:
msg: 数据库地址不能为空 msg: 数据库地址不能为空
db_name: db_name:
label: 数据库名 label: 数据库名
placeholder: answer placeholder: 回答
msg: 数据库名称不能为空。 msg: 数据库名称不能为空。
db_file: db_file:
label: Database File label: 数据库文件
placeholder: /data/answer.db placeholder: /data/answer.db
msg: 数据库文件不能为空。 msg: 数据库文件不能为空。
config_yaml: config_yaml:
title: 创建 config.yaml title: 创建 config.yaml
label: 已创建 config.yaml 文件。 label: 已创建 config.yaml 文件。
desc: >- desc: >-
You can create the <1>config.yaml</1> file manually in the <1>/var/wwww/xxx/</1> directory and paste the following text into it. 您可以手动在 <1>/var/wwww/xxx/</1> 目录中创建<1>config.yaml</1> 文件并粘贴以下文本。
info: "完成后,点击“下一步”按钮。" info: "完成后,点击“下一步”按钮。"
site_information: 站点信息 site_information: 站点信息
admin_account: 管理员账户 admin_account: 管理员账户
@ -906,52 +911,52 @@ ui:
msg: 站点名称不能为空。 msg: 站点名称不能为空。
site_url: site_url:
label: 站点地址URL label: 站点地址URL
text: The address of your site. text: 此网站的地址。
msg: msg:
empty: 站点URL不能为空。 empty: 站点URL不能为空。
incorrect: 站点URL格式不正确。 incorrect: 站点URL格式不正确。
contact_email: contact_email:
label: 联系邮箱 label: 联系邮箱
text: Email address of key contact responsible for this site. text: 负责本网站的主要联系人的电子邮件地址。
msg: msg:
empty: Contact Email cannot be empty. empty: 联系人邮箱地址不能为空。
incorrect: Contact Email incorrect format. incorrect: 联系人邮箱地址不正确。
admin_name: admin_name:
label: Name label: 昵称
msg: Name cannot be empty. msg: 昵称不能为空。
admin_password: admin_password:
label: Password label: 密码
text: >- text: >-
You will need this password to log in. Please store it in a secure location. 您需要此密码才能登录。请将其存储在一个安全的位置。
msg: Password cannot be empty. msg: 密码不能为空。
admin_email: admin_email:
label: Email label: 邮箱
text: You will need this email to log in. text: 您需要此电子邮件才能登录。
msg: msg:
empty: Email cannot be empty. empty: 邮箱不能为空。
incorrect: Email incorrect format. incorrect: 邮箱格式不正确。
ready_title: Your Answer is Ready! ready_title: 你的答案已经准备好了!
ready_desc: >- ready_desc: >-
If you ever feel like changing more settings, visit <1>admin section</1>; find it in the site menu. 如果你想改变更多的设置,请访问<1>管理员部分</1>;在网站菜单中找到它。
good_luck: "Have fun, and good luck!" good_luck: "玩得愉快,祝您好运!"
warn_title: Warning warn_title: 警告
warn_desc: >- warn_desc: >-
The file <1>config.yaml</1> already exists. If you need to reset any of the configuration items in this file, please delete it first. 文件<1>config.yaml</1>已存在。如果您需要重置此文件中的任何配置项,请先删除它。
install_now: You may try <1>installing now</1>. install_now: 您可以尝试<1>现在安装</1>。
installed: 已安裝 installed: 已安裝
installed_desc: >- installed_desc: >-
You appear to have already installed. To reinstall please clear your old database tables first. 您似乎已经安装过了。要重新安装,请先清除旧的数据库表。
db_failed: 数据连接异常! db_failed: 数据连接异常!
db_failed_desc: >- db_failed_desc: >-
This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your hosts database server is down. 这或者意味着数据库信息在 <1>config.yaml</1> 文件不正确,或者无法与数据库服务器建立联系。这可能意味着您的主机数据库服务器已关闭。
page_404: page_404:
desc: 页面不存在 desc: "很抱歉,此页面不存在。"
back_home: 回到主页 back_home: 回到主页
page_50X: page_50X:
desc: 服务器遇到了一个错误,无法完成你的请求。 desc: 服务器遇到了一个错误,无法完成你的请求。
back_home: 回到主页 back_home: 回到主页
page_maintenance: page_maintenance:
desc: "We are under maintenance, well be back soon." desc: "我们正在进行维护,我们将很快回来。"
nav_menus: nav_menus:
dashboard: 后台管理 dashboard: 后台管理
contents: 内容管理 contents: 内容管理
@ -969,17 +974,17 @@ ui:
tos: 服务条款 tos: 服务条款
privacy: 隐私政策 privacy: 隐私政策
seo: SEO seo: SEO
customize: Customize customize: 自定义
themes: Themes themes: 主题
css-html: CSS/HTML css-html: CSS/HTML
login: Login login: 登录
website_welcome: 欢迎来到 {{site_name}} website_welcome: 欢迎来到 {{site_name}}
admin: admin:
admin_header: admin_header:
title: 后台管理 title: 后台管理
dashboard: dashboard:
title: 后台管理 title: 后台管理
welcome: 欢迎来到 {{site_name}} 后台管理! welcome: 欢迎来到 Answer 后台管理!
site_statistics: 站点统计 site_statistics: 站点统计
questions: "问题:" questions: "问题:"
answers: "回答:" answers: "回答:"
@ -999,6 +1004,7 @@ ui:
answer_links: 回答链接 answer_links: 回答链接
documents: 文档 documents: 文档
feedback: 用户反馈 feedback: 用户反馈
support: 帮助
review: 审查 review: 审查
config: 配置 config: 配置
update_to: 更新到 update_to: 更新到
@ -1023,11 +1029,11 @@ ui:
btn_cancel: 取消 btn_cancel: 取消
btn_submit: 提交 btn_submit: 提交
normal_name: 正常 normal_name: 正常
normal_desc: 正常状态的用户可以提问和回答。 normal_desc: 普通用户可以提问和回答。
suspended_name: 封禁 suspended_name: 封禁
suspended_desc: 被封禁的用户将无法登录。 suspended_desc: 被封禁的用户将无法登录。
deleted_name: 删除 deleted_name: 删除
deleted_desc: 删除用户的个人信息,认证等等。 deleted_desc: "删除个人资料和身份验证关联。"
inactive_name: 不活跃 inactive_name: 不活跃
inactive_desc: 不活跃的用户必须重新验证邮箱。 inactive_desc: 不活跃的用户必须重新验证邮箱。
confirm_title: 删除此用户 confirm_title: 删除此用户
@ -1038,11 +1044,11 @@ ui:
status_modal: status_modal:
title: "更改 {{ type }} 状态为..." title: "更改 {{ type }} 状态为..."
normal_name: 正常 normal_name: 正常
normal_desc: 所有用户都可以访问 normal_desc: 所有用户都可以访问的普通帖子。
closed_name: 关闭 closed_name: 关闭
closed_desc: 不能回答,但仍然可以编辑、投票和评论。 closed_desc: "关闭的问题不能回答,但仍然可以编辑、投票和评论。"
deleted_name: 删除 deleted_name: 删除
deleted_desc: 所有获得/损失的声望将会恢复。 deleted_desc: 获得和丧失的所有信誉积分将被恢复。
btn_cancel: 取消 btn_cancel: 取消
btn_submit: 提交 btn_submit: 提交
btn_next: 下一步 btn_next: 下一步
@ -1077,32 +1083,32 @@ ui:
change_status: 更改状态 change_status: 更改状态
change_role: 更改角色 change_role: 更改角色
show_logs: 显示日志 show_logs: 显示日志
add_user: Add user add_user: 添加用户
new_password_modal: new_password_modal:
title: Set new password title: 设置新密码
form: form:
fields: fields:
password: password:
label: Password label: 密码
text: The user will be logged out and need to login again. text: 用户将被注销,需要再次登录。
msg: Password must be at 8 - 32 characters in length. msg: 密码的长度必须是8-32个字符。
btn_cancel: Cancel btn_cancel: 取消
btn_submit: Submit btn_submit: 提交
user_modal: user_modal:
title: Add new user title: 添加新用户
form: form:
fields: fields:
display_name: display_name:
label: Display Name label: 昵称
msg: display_name must be at 4 - 30 characters in length. msg: 昵称的长度必须是4-30个字符。
email: email:
label: Email label: 邮箱
msg: Email is not valid. msg: 电子邮箱无效。
password: password:
label: Password label: 密码
msg: Password must be at 8 - 32 characters in length. msg: 密码的长度必须是8-32个字符。
btn_cancel: Cancel btn_cancel: 取消
btn_submit: Submit btn_submit: 提交
questions: questions:
page_title: 问题 page_title: 问题
normal: 正常 normal: 正常
@ -1116,7 +1122,7 @@ ui:
action: 操作 action: 操作
change: 更改 change: 更改
filter: filter:
placeholder: "Filter by title, question:id" placeholder: "按标题过滤,问题:id"
answers: answers:
page_title: 回答 page_title: 回答
normal: 正常 normal: 正常
@ -1128,21 +1134,31 @@ ui:
action: 操作 action: 操作
change: 更改 change: 更改
filter: filter:
placeholder: "Filter by title, answer:id" placeholder: "按标题筛选,答案:id"
general: general:
page_title: 一般 page_title: 一般
name: name:
label: 站点名称 label: 站点名称
msg: 不能为空 msg: 不能为空
text: 站点的名称作为站点的标题HTML 的 title 标签)。 text: "站点的名称作为站点的标题HTML 的 title 标签)。"
site_url:
label: 网站网址
msg: 网站网址不能为空。
validate: 请输入一个有效的 URL。
text: 此网站的地址。
short_desc: short_desc:
label: 简短的站点标语 (可选) label: 简短网站描述(可选)
msg: 不能为空 msg: 简短网站描述不能为空
text: 简短的标语作为网站主页的标题HTML 的 title 标签)。 text: "简短的标语作为网站主页的标题Html 的 title 标签)。"
desc: desc:
label: 网站描述 (可选) label: 网站描述 (可选)
msg: 不能为空 msg: 网站描述不能为空。
text: 使用一句话描述本站作为网站的描述HTML 的 meta 标签)。 text: "使用一句话描述本站作为网站的描述Html 的 meta 标签)。"
contact_email:
label: 联系人邮箱
msg: 联系人邮箱不能为空。
validate: 联系人邮箱无效。
text: 负责本网站的主要联系人的电子邮件地址。
interface: interface:
page_title: 界面 page_title: 界面
logo: logo:
@ -1158,9 +1174,9 @@ ui:
msg: 不能为空 msg: 不能为空
text: 设置用户界面语言,在刷新页面后生效。 text: 设置用户界面语言,在刷新页面后生效。
time_zone: time_zone:
label: Timezone label: 时区
msg: Timezone cannot be empty. msg: 时区不能为空。
text: Choose a city in the same timezone as you. text: 选择一个与您相同时区的城市。
smtp: smtp:
page_title: SMTP page_title: SMTP
from_email: from_email:
@ -1196,97 +1212,97 @@ ui:
text: 提供用于接收测试邮件的邮箱地址。 text: 提供用于接收测试邮件的邮箱地址。
msg: 地址无效 msg: 地址无效
smtp_authentication: smtp_authentication:
label: Enable authentication label: 启用身份验证
title: SMTP Authentication title: SMTP身份验证
msg: 不能为空 msg: 不能为空
"yes": "是" "yes": "是"
"no": "否" "no": "否"
branding: branding:
page_title: Branding page_title: 品牌
logo: logo:
label: Logo label: 图标
msg: Logo cannot be empty. msg: 图标不能为空。
text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. text: 在你的网站左上方的Logo图标。使用一个高度为56长宽比大于3:1的宽长方形图像。如果留空将显示网站标题文本。
mobile_logo: mobile_logo:
label: Mobile Logo (optional) label: 移动端图标(可选)
text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the “logo” setting will be used. text: 在你的网站的移动版上使用的标志。使用一个高度为56的宽矩形图像。如果留空将使用 "Logo"设置中的图像。
square_icon: square_icon:
label: Square Icon label: 方形图标
msg: Square icon cannot be empty. msg: 方形图标不能为空。
text: Image used as the base for metadata icons. Should ideally be larger than 512x512. text: 用作元数据图标的基础的图像。最好是大于512x512。
favicon: favicon:
label: Favicon (optional) label: 收藏夹图标(可选)
text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, “square icon” will be used. text: 网站的图标。要在 CDN 正常工作,它必须是 png。 将调整大小到32x32。如果留空将使用“方形图标”。
legal: legal:
page_title: Legal page_title: 法律条款
terms_of_service: terms_of_service:
label: Terms of Service label: 服务条款
text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." text: "您可以在此添加服务内容的条款。如果您已经在别处托管了文档请在这里提供完整的URL。"
privacy_policy: privacy_policy:
label: Privacy Policy label: 隐私条款
text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." text: "您可以在此添加隐私政策内容。如果您已经在别处托管了文档请在这里提供完整的URL。"
write: write:
page_title: Write page_title: 编辑
recommend_tags: recommend_tags:
label: Recommend Tags label: 推荐标签
text: "Please input tag slug above, one tag per line." text: "请输入以上标签,每行一个标签。"
required_tag: required_tag:
title: Required Tag title: 必需的标签
label: Set recommend tag as required label: 根据需要设置推荐标签
text: "Every new question must have at least one recommend tag." text: "每个新问题必须至少有一个推荐标签。"
reserved_tags: reserved_tags:
label: Reserved Tags label: 保留标签
text: "Reserved tags can only be added to a post by moderator." text: "保留的标签只能由版主添加到一个帖子中。"
seo: seo:
page_title: SEO page_title: 搜索引擎优化
permalink: permalink:
label: Permalink label: 固定链接
text: Custom URL structures can improve the usability, and forward-compatibility of your links. text: 自定义URL结构可以提高可用性以及你的链接的向前兼容性。
robots: robots:
label: robots.txt label: robots.txt
text: This will permanently override any related site settings. text: 这将永久覆盖任何相关的网站设置。
themes: themes:
page_title: Themes page_title: 主题
themes: themes:
label: Themes label: 主题
text: Select an existing theme. text: 选择一个现有主题。
navbar_style: navbar_style:
label: Navbar Style label: 导航栏样式
text: Select an existing theme. text: 选择一个现有主题。
primary_color: primary_color:
label: Primary Color label: 主色调
text: Modify the colors used by your themes text: 修改您主题使用的颜色
css_and_html: css_and_html:
page_title: CSS and HTML page_title: CSS HTML
custom_css: custom_css:
label: Custom CSS label: 自定义CSS
text: This will insert as <link> text: 这将在 <link> 之前插入
head: head:
label: Head label: 头部
text: This will insert before </head> text: 这将在 </head> 之前插入
header: header:
label: Header label: 标题
text: This will insert after <body> text: 这将在 <body> 之前插入
footer: footer:
label: Footer label: 页脚
text: This will insert before </html>. text: 这将在 </html> 之前插入
login: login:
page_title: Login page_title: 登录
membership: membership:
title: Membership title: 会员
label: Allow new registrations label: 允许新注册
text: Turn off to prevent anyone from creating a new account. text: 关闭以防止任何人创建新帐户。
private: private:
title: Private title: 非公开的
label: Login required label: 需要登录
text: Only logged in users can access this community. text: 只有登录用户才能访问这个社区。
form: form:
empty: cannot be empty empty: 不能为空
invalid: is invalid invalid: 是无效的
btn_submit: Save btn_submit: 保存
not_found_props: "Required property {{ key }} not found." not_found_props: "所需属性 {{ key }} 未找到。"
page_review: page_review:
review: Review review: 评论
proposed: 提案 proposed: 提案
question_edit: 问题编辑 question_edit: 问题编辑
answer_edit: 回答编辑 answer_edit: 回答编辑
@ -1324,11 +1340,11 @@ ui:
comment: 评论 comment: 评论
no_data: "空空如也" no_data: "空空如也"
users: users:
title: Users title: 用户
users_with_the_most_reputation: Users with the highest reputation scores users_with_the_most_reputation: 信誉积分最高的用户
users_with_the_most_vote: Users who voted the most users_with_the_most_vote: 投票最多的用户
staffs: Our community staff staffs: 我们的社区工作人员
reputation: reputation reputation: 声望值
votes: votes votes: 投票

View File

@ -4,11 +4,11 @@ backend:
success: success:
other: "成功!" other: "成功!"
unknown: unknown:
other: "Unknown error." other: "未知的錯誤。"
request_format_error: request_format_error:
other: "Request format is not valid." other: "請求的格式無效。"
unauthorized_error: unauthorized_error:
other: "Unauthorized." other: "未授權。"
database_error: database_error:
other: "Data server error." other: "Data server error."
role: role:
@ -29,13 +29,13 @@ backend:
email: email:
other: "Email" other: "Email"
password: password:
other: "Password" other: "密碼"
email_or_password_wrong_error: email_or_password_wrong_error:
other: "Email and password do not match." other: "電子郵箱和密碼不匹配。"
error: error:
admin: admin:
email_or_password_wrong: email_or_password_wrong:
other: Email and password do not match. other: 電子郵箱和密碼不匹配。
answer: answer:
not_found: not_found:
other: "Answer do not found." other: "Answer do not found."
@ -70,22 +70,22 @@ backend:
not_found: not_found:
other: "Object not found." other: "Object not found."
verification_failed: verification_failed:
other: "Verification failed." other: "驗證失敗。"
email_or_password_incorrect: email_or_password_incorrect:
other: "Email and password do not match." other: "電子郵箱和密碼不匹配。"
old_password_verification_failed: old_password_verification_failed:
other: "The old password verification failed" other: "舊密碼驗證失敗"
new_password_same_as_previous_setting: new_password_same_as_previous_setting:
other: "The new password is the same as the previous one." other: "新密碼與先前的一樣。"
question: question:
not_found: not_found:
other: "Question not found." other: "找不到問題。"
cannot_deleted: cannot_deleted:
other: "No permission to delete." other: "沒有刪除的權限。"
cannot_close: cannot_close:
other: "No permission to close." other: "沒有關閉的權限。"
cannot_update: cannot_update:
other: "No permission to update." other: "沒有更新的權限。"
rank: rank:
fail_to_meet_the_condition: fail_to_meet_the_condition:
other: "Rank fail to meet the condition." other: "Rank fail to meet the condition."
@ -107,9 +107,12 @@ backend:
other: "No permission to update." other: "No permission to update."
cannot_set_synonym_as_itself: cannot_set_synonym_as_itself:
other: "You cannot set the synonym of the current tag as itself." other: "You cannot set the synonym of the current tag as itself."
smtp:
config_from_name_cannot_be_email:
other: "The From Name cannot be a email address."
theme: theme:
not_found: not_found:
other: "Theme not found." other: "未找到主題。"
revision: revision:
review_underway: review_underway:
other: "Can't edit currently, there is a version in the review queue." other: "Can't edit currently, there is a version in the review queue."
@ -129,6 +132,10 @@ backend:
other: "Username is already in use." other: "Username is already in use."
set_avatar: set_avatar:
other: "Avatar set failed." other: "Avatar set failed."
cannot_update_your_role:
other: "You cannot modify your role."
not_allowed_registration:
other: "Currently the site is not open for registration"
config: config:
read_config_failed: read_config_failed:
other: "Read config failed" other: "Read config failed"
@ -140,10 +147,6 @@ backend:
install: install:
create_config_failed: create_config_failed:
other: "Cant create the config.yaml file." other: "Cant create the config.yaml file."
cannot_update_your_role:
other: "You cannot modify your role."
not_allowed_registration:
other: "Currently the site is not open for registration"
report: report:
spam: spam:
name: name:
@ -197,6 +200,13 @@ backend:
other: "something else" other: "something else"
desc: desc:
other: "This post requires another reason not listed above." other: "This post requires another reason not listed above."
operation_type:
asked:
other: "asked"
answered:
other: "answered"
modified:
other: "modified"
notification: notification:
action: action:
update_question: update_question:
@ -801,6 +811,11 @@ ui:
confirm_new_email: Your email has been updated. confirm_new_email: Your email has been updated.
confirm_new_email_invalid: >- confirm_new_email_invalid: >-
Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?
unsubscribe:
page_title: Unsubscribe
success_title: Unsubscribe Successful
success_desc: You have been successfully removed from this subscriber list and wont receive any further emails from us.
link: Change settings
question: question:
following_tags: Following Tags following_tags: Following Tags
edit: Edit edit: Edit
@ -991,6 +1006,7 @@ ui:
answer_links: Answer Links answer_links: Answer Links
documents: Documents documents: Documents
feedback: Feedback feedback: Feedback
support: Support
review: Review review: Review
config: Config config: Config
update_to: Update to update_to: Update to

View File

@ -21,7 +21,7 @@ type RespBody struct {
// TrMsg translate the reason cause as a message // TrMsg translate the reason cause as a message
func (r *RespBody) TrMsg(lang i18n.Language) *RespBody { func (r *RespBody) TrMsg(lang i18n.Language) *RespBody {
if len(r.Message) == 0 { if len(r.Message) == 0 {
r.Message = translator.GlobalTrans.Tr(lang, r.Reason) r.Message = translator.Tr(lang, r.Reason)
} }
return r return r
} }

View File

@ -106,3 +106,12 @@ func CheckLanguageIsValid(lang string) bool {
} }
return false return false
} }
// Tr use language to translate data. If this language translation is not available, return default english translation.
func Tr(lang i18n.Language, data string) string {
translation := GlobalTrans.Tr(lang, data)
if translation == data {
return GlobalTrans.Tr(i18n.DefaultLanguage, data)
}
return translation
}

View File

@ -102,7 +102,7 @@ func createDefaultValidator(la i18n.Language) *validator.Validate {
validate.RegisterTagNameFunc(func(fld reflect.StructField) (res string) { validate.RegisterTagNameFunc(func(fld reflect.StructField) (res string) {
defer func() { defer func() {
if len(res) > 0 { if len(res) > 0 {
res = translator.GlobalTrans.Tr(la, res) res = translator.Tr(la, res)
} }
}() }()
if jsonTag := fld.Tag.Get("json"); len(jsonTag) > 0 { if jsonTag := fld.Tag.Get("json"); len(jsonTag) > 0 {
@ -168,7 +168,7 @@ func (m *MyValidator) Check(value interface{}) (errFields []*FormErrorField, err
return nil, nil return nil, nil
} }
for _, errField := range errFields { for _, errField := range errFields {
errField.ErrorMsg = translator.GlobalTrans.Tr(m.Lang, errField.ErrorMsg) errField.ErrorMsg = translator.Tr(m.Lang, errField.ErrorMsg)
} }
return errFields, err return errFields, err
} }

View File

@ -3,7 +3,9 @@ package controller
import ( import (
"github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/uploader" "github.com/answerdev/answer/internal/service/uploader"
"github.com/answerdev/answer/pkg/converter"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/errors"
) )
@ -63,3 +65,21 @@ func (uc *UploadController) UploadFile(ctx *gin.Context) {
} }
handler.HandleResponse(ctx, err, url) handler.HandleResponse(ctx, err, url)
} }
// PostRender render post content
// @Summary render post content
// @Description render post content
// @Tags Upload
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.PostRenderReq true "PostRenderReq"
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/post/render [post]
func (uc *UploadController) PostRender(ctx *gin.Context) {
req := &schema.PostRenderReq{}
if handler.BindAndCheck(ctx, req) {
return
}
handler.HandleResponse(ctx, nil, converter.Markdown2HTML(req.Content))
}

View File

@ -113,7 +113,7 @@ func (uc *UserController) UserEmailLogin(ctx *gin.Context) {
if !captchaPass { if !captchaPass {
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
ErrorField: "captcha_code", ErrorField: "captcha_code",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
}) })
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
return return
@ -124,7 +124,7 @@ func (uc *UserController) UserEmailLogin(ctx *gin.Context) {
_, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeLogin, ctx.ClientIP()) _, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeLogin, ctx.ClientIP())
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
ErrorField: "e_mail", ErrorField: "e_mail",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.EmailOrPasswordWrong), ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.EmailOrPasswordWrong),
}) })
handler.HandleResponse(ctx, errors.BadRequest(reason.EmailOrPasswordWrong), errFields) handler.HandleResponse(ctx, errors.BadRequest(reason.EmailOrPasswordWrong), errFields)
return return
@ -151,7 +151,7 @@ func (uc *UserController) RetrievePassWord(ctx *gin.Context) {
if !captchaPass { if !captchaPass {
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
ErrorField: "captcha_code", ErrorField: "captcha_code",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
}) })
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
return return
@ -236,7 +236,7 @@ func (uc *UserController) UserRegisterByEmail(ctx *gin.Context) {
if !captchaPass { if !captchaPass {
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
ErrorField: "captcha_code", ErrorField: "captcha_code",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
}) })
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
return return
@ -245,7 +245,8 @@ func (uc *UserController) UserRegisterByEmail(ctx *gin.Context) {
resp, errFields, err := uc.userService.UserRegisterByEmail(ctx, req) resp, errFields, err := uc.userService.UserRegisterByEmail(ctx, req)
if len(errFields) > 0 { if len(errFields) > 0 {
for _, field := range errFields { for _, field := range errFields {
field.ErrorMsg = translator.GlobalTrans.Tr(handler.GetLang(ctx), field.ErrorMsg) field.ErrorMsg = translator.
Tr(handler.GetLang(ctx), field.ErrorMsg)
} }
handler.HandleResponse(ctx, err, errFields) handler.HandleResponse(ctx, err, errFields)
} else { } else {
@ -312,7 +313,7 @@ func (uc *UserController) UserVerifyEmailSend(ctx *gin.Context) {
if !captchaPass { if !captchaPass {
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
ErrorField: "captcha_code", ErrorField: "captcha_code",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
}) })
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
return return
@ -350,7 +351,7 @@ func (uc *UserController) UserModifyPassWord(ctx *gin.Context) {
if !oldPassVerification { if !oldPassVerification {
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
ErrorField: "old_pass", ErrorField: "old_pass",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.OldPasswordVerificationFailed), ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.OldPasswordVerificationFailed),
}) })
handler.HandleResponse(ctx, errors.BadRequest(reason.OldPasswordVerificationFailed), errFields) handler.HandleResponse(ctx, errors.BadRequest(reason.OldPasswordVerificationFailed), errFields)
return return
@ -358,7 +359,7 @@ func (uc *UserController) UserModifyPassWord(ctx *gin.Context) {
if req.OldPass == req.Pass { if req.OldPass == req.Pass {
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
ErrorField: "pass", ErrorField: "pass",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.NewPasswordSameAsPreviousSetting), ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.NewPasswordSameAsPreviousSetting),
}) })
handler.HandleResponse(ctx, errors.BadRequest(reason.NewPasswordSameAsPreviousSetting), errFields) handler.HandleResponse(ctx, errors.BadRequest(reason.NewPasswordSameAsPreviousSetting), errFields)
return return
@ -386,7 +387,7 @@ func (uc *UserController) UserUpdateInfo(ctx *gin.Context) {
req.UserID = middleware.GetLoginUserIDFromContext(ctx) req.UserID = middleware.GetLoginUserIDFromContext(ctx)
errFields, err := uc.userService.UpdateInfo(ctx, req) errFields, err := uc.userService.UpdateInfo(ctx, req)
for _, field := range errFields { for _, field := range errFields {
field.ErrorMsg = translator.GlobalTrans.Tr(handler.GetLang(ctx), field.ErrorMsg) field.ErrorMsg = translator.Tr(handler.GetLang(ctx), field.ErrorMsg)
} }
handler.HandleResponse(ctx, err, errFields) handler.HandleResponse(ctx, err, errFields)
} }
@ -491,7 +492,7 @@ func (uc *UserController) UserChangeEmailSendCode(ctx *gin.Context) {
if !captchaPass { if !captchaPass {
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
ErrorField: "captcha_code", ErrorField: "captcha_code",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
}) })
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
return return

View File

@ -77,7 +77,7 @@ type InitBaseInfoReq struct {
SiteName string `validate:"required,gt=0,lte=30" json:"site_name"` SiteName string `validate:"required,gt=0,lte=30" json:"site_name"`
SiteURL string `validate:"required,gt=0,lte=512,url" json:"site_url"` SiteURL string `validate:"required,gt=0,lte=512,url" json:"site_url"`
ContactEmail string `validate:"required,email,gt=0,lte=500" json:"contact_email"` ContactEmail string `validate:"required,email,gt=0,lte=500" json:"contact_email"`
AdminName string `validate:"required,gt=4,lte=30" json:"name"` AdminName string `validate:"required,gt=3,lte=30" json:"name"`
AdminPassword string `validate:"required,gte=8,lte=32" json:"password"` AdminPassword string `validate:"required,gte=8,lte=32" json:"password"`
AdminEmail string `validate:"required,email,gt=0,lte=500" json:"email"` AdminEmail string `validate:"required,email,gt=0,lte=500" json:"email"`
} }

View File

@ -88,7 +88,7 @@ func (ar *ActivityRepo) GetActivity(ctx context.Context, session *xorm.Session,
func (ar *ActivityRepo) GetUserIDObjectIDActivitySum(ctx context.Context, userID, objectID string) (int, error) { func (ar *ActivityRepo) GetUserIDObjectIDActivitySum(ctx context.Context, userID, objectID string) (int, error) {
sum := &entity.ActivityRankSum{} sum := &entity.ActivityRankSum{}
_, err := ar.data.DB.Table(entity.Activity{}.TableName()). _, err := ar.data.DB.Table(entity.Activity{}.TableName()).
Select("sum(rank) as rank"). Select("sum(`rank`) as `rank`").
Where("user_id =?", userID). Where("user_id =?", userID).
And("object_id = ?", objectID). And("object_id = ?", objectID).
And("cancelled =0"). And("cancelled =0").
@ -113,7 +113,7 @@ func (ar *ActivityRepo) AddActivity(ctx context.Context, activity *entity.Activi
func (ar *ActivityRepo) GetUsersWhoHasGainedTheMostReputation( func (ar *ActivityRepo) GetUsersWhoHasGainedTheMostReputation(
ctx context.Context, startTime, endTime time.Time, limit int) (rankStat []*entity.ActivityUserRankStat, err error) { ctx context.Context, startTime, endTime time.Time, limit int) (rankStat []*entity.ActivityUserRankStat, err error) {
rankStat = make([]*entity.ActivityUserRankStat, 0) rankStat = make([]*entity.ActivityUserRankStat, 0)
session := ar.data.DB.Select("user_id, SUM(rank) AS rank_amount").Table("activity") session := ar.data.DB.Select("user_id, SUM(`rank`) AS rank_amount").Table("activity")
session.Where("has_rank = 1 AND cancelled = 0") session.Where("has_rank = 1 AND cancelled = 0")
session.Where("created_at >= ?", startTime) session.Where("created_at >= ?", startTime)
session.Where("created_at <= ?", endTime) session.Where("created_at <= ?", endTime)

View File

@ -60,7 +60,7 @@ func (tr *tagCommonRepo) GetTagListByName(ctx context.Context, name string, hasR
cond := &entity.Tag{} cond := &entity.Tag{}
session := tr.data.DB.Where("") session := tr.data.DB.Where("")
if name != "" { if name != "" {
session.Where("slug_name LIKE ?", name+"%") session.Where("slug_name LIKE ? or display_name LIKE ?", name+"%", name+"%")
} else { } else {
session.UseBool("recommend") session.UseBool("recommend")
cond.Recommend = true cond.Recommend = true

View File

@ -217,6 +217,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
// upload file // upload file
r.POST("/file", a.uploadController.UploadFile) r.POST("/file", a.uploadController.UploadFile)
r.POST("/post/render", a.uploadController.PostRender)
// activity // activity
r.GET("/activity/timeline", a.activityController.GetObjectTimeline) r.GET("/activity/timeline", a.activityController.GetObjectTimeline)

View File

@ -246,9 +246,9 @@ type QuestionPageReq struct {
} }
const ( const (
QuestionPageRespOperationTypeAsked = "question.operation_type.asked" QuestionPageRespOperationTypeAsked = "asked"
QuestionPageRespOperationTypeAnswered = "question.operation_type.answered" QuestionPageRespOperationTypeAnswered = "answered"
QuestionPageRespOperationTypeModified = "question.operation_type.modified" QuestionPageRespOperationTypeModified = "modified"
) )
type QuestionPageResp struct { type QuestionPageResp struct {

View File

@ -0,0 +1,6 @@
package schema
// PostRenderReq post render request
type PostRenderReq struct {
Content string `json:"content"`
}

View File

@ -46,9 +46,9 @@ type SiteInterfaceReq struct {
// SiteBrandingReq site branding request // SiteBrandingReq site branding request
type SiteBrandingReq struct { type SiteBrandingReq struct {
Logo string `validate:"required,gt=0,lte=512" form:"logo" json:"logo"` Logo string `validate:"omitempty,gt=0,lte=512" form:"logo" json:"logo"`
MobileLogo string `validate:"omitempty,gt=0,lte=512" form:"mobile_logo" json:"mobile_logo"` MobileLogo string `validate:"omitempty,gt=0,lte=512" form:"mobile_logo" json:"mobile_logo"`
SquareIcon string `validate:"required,gt=0,lte=512" form:"square_icon" json:"square_icon"` SquareIcon string `validate:"omitempty,gt=0,lte=512" form:"square_icon" json:"square_icon"`
Favicon string `validate:"omitempty,gt=0,lte=512" form:"favicon" json:"favicon"` Favicon string `validate:"omitempty,gt=0,lte=512" form:"favicon" json:"favicon"`
} }
@ -134,7 +134,7 @@ type SiteThemeResp struct {
func (s *SiteThemeResp) TrTheme(ctx context.Context) { func (s *SiteThemeResp) TrTheme(ctx context.Context) {
la := handler.GetLangByCtx(ctx) la := handler.GetLangByCtx(ctx)
for _, option := range s.ThemeOptions { for _, option := range s.ThemeOptions {
tr := translator.GlobalTrans.Tr(la, option.Value) tr := translator.Tr(la, option.Value)
// if tr is equal the option value means not found translation, so use the original label // if tr is equal the option value means not found translation, so use the original label
if tr != option.Value { if tr != option.Value {
option.Label = tr option.Label = tr

View File

@ -228,7 +228,7 @@ type UserEmailLogin struct {
// UserRegisterReq user register request // UserRegisterReq user register request
type UserRegisterReq struct { type UserRegisterReq struct {
// name // name
Name string `validate:"required,gt=4,lte=30" json:"name"` Name string `validate:"required,gt=3,lte=30" json:"name"`
// email // email
Email string `validate:"required,email,gt=0,lte=500" json:"e_mail" ` Email string `validate:"required,email,gt=0,lte=500" json:"e_mail" `
// password // password
@ -277,7 +277,7 @@ type UpdateInfoRequest struct {
// display_name // display_name
DisplayName string `validate:"required,gt=0,lte=30" json:"display_name"` DisplayName string `validate:"required,gt=0,lte=30" json:"display_name"`
// username // username
Username string `validate:"omitempty,gt=0,lte=30" json:"username"` Username string `validate:"omitempty,gt=3,lte=30" json:"username"`
// avatar // avatar
Avatar AvatarInfo `json:"avatar"` Avatar AvatarInfo `json:"avatar"`
// bio // bio
@ -300,12 +300,13 @@ type AvatarInfo struct {
func (u *UpdateInfoRequest) Check() (errFields []*validator.FormErrorField, err error) { func (u *UpdateInfoRequest) Check() (errFields []*validator.FormErrorField, err error) {
if len(u.Username) > 0 { if len(u.Username) > 0 {
errFields := make([]*validator.FormErrorField, 0)
re := regexp.MustCompile(`^[a-z0-9._-]{4,30}$`) re := regexp.MustCompile(`^[a-z0-9._-]{4,30}$`)
match := re.MatchString(u.Username) match := re.MatchString(u.Username)
if !match { if !match {
errField := &validator.FormErrorField{ errField := &validator.FormErrorField{
ErrorField: "username", ErrorField: "username",
ErrorMsg: err.Error(), ErrorMsg: reason.UsernameInvalid,
} }
errFields = append(errFields, errField) errFields = append(errFields, errField)
return errFields, errors.BadRequest(reason.UsernameInvalid) return errFields, errors.BadRequest(reason.UsernameInvalid)

View File

@ -134,7 +134,7 @@ func (ns *NotificationService) GetNotificationPage(ctx context.Context, searchCo
continue continue
} }
lang, _ := ctx.Value(constant.AcceptLanguageFlag).(i18n.Language) lang, _ := ctx.Value(constant.AcceptLanguageFlag).(i18n.Language)
item.NotificationAction = translator.GlobalTrans.Tr(lang, item.NotificationAction) item.NotificationAction = translator.Tr(lang, item.NotificationAction)
item.ID = notificationInfo.ID item.ID = notificationInfo.ID
item.UpdateTime = notificationInfo.UpdatedAt.Unix() item.UpdateTime = notificationInfo.UpdatedAt.Unix()
if notificationInfo.IsRead == schema.NotificationRead { if notificationInfo.IsRead == schema.NotificationRead {

View File

@ -6,9 +6,7 @@ import (
"time" "time"
"github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/service/activity_common" "github.com/answerdev/answer/internal/service/activity_common"
"github.com/answerdev/answer/internal/service/activity_queue" "github.com/answerdev/answer/internal/service/activity_queue"
"github.com/answerdev/answer/internal/service/config" "github.com/answerdev/answer/internal/service/config"
@ -250,13 +248,7 @@ func (qs *QuestionCommon) Info(ctx context.Context, questionID string, loginUser
func (qs *QuestionCommon) FormatQuestionsPage( func (qs *QuestionCommon) FormatQuestionsPage(
ctx context.Context, questionList []*entity.Question, loginUserID string, orderCond string) ( ctx context.Context, questionList []*entity.Question, loginUserID string, orderCond string) (
formattedQuestions []*schema.QuestionPageResp, err error) { formattedQuestions []*schema.QuestionPageResp, err error) {
language := handler.GetLangByCtx(ctx)
askedOp := translator.GlobalTrans.Tr(language, schema.QuestionPageRespOperationTypeAsked)
answeredOp := translator.GlobalTrans.Tr(language, schema.QuestionPageRespOperationTypeAnswered)
modifiedOp := translator.GlobalTrans.Tr(language, schema.QuestionPageRespOperationTypeModified)
formattedQuestions = make([]*schema.QuestionPageResp, 0) formattedQuestions = make([]*schema.QuestionPageResp, 0)
questionIDs := make([]string, 0) questionIDs := make([]string, 0)
userIDs := make([]string, 0) userIDs := make([]string, 0)
for _, questionInfo := range questionList { for _, questionInfo := range questionList {
@ -300,20 +292,20 @@ func (qs *QuestionCommon) FormatQuestionsPage(
// if order condition is newest or nobody edited or nobody answered, only show question author // if order condition is newest or nobody edited or nobody answered, only show question author
if orderCond == schema.QuestionOrderCondNewest || (!haveEdited && !haveAnswered) { if orderCond == schema.QuestionOrderCondNewest || (!haveEdited && !haveAnswered) {
t.OperationType = askedOp t.OperationType = schema.QuestionPageRespOperationTypeAsked
t.OperatedAt = questionInfo.CreatedAt.Unix() t.OperatedAt = questionInfo.CreatedAt.Unix()
t.Operator = &schema.QuestionPageRespOperator{ID: questionInfo.UserID} t.Operator = &schema.QuestionPageRespOperator{ID: questionInfo.UserID}
} else { } else {
// if no one // if no one
if haveEdited { if haveEdited {
t.OperationType = modifiedOp t.OperationType = schema.QuestionPageRespOperationTypeModified
t.OperatedAt = questionInfo.UpdatedAt.Unix() t.OperatedAt = questionInfo.UpdatedAt.Unix()
t.Operator = &schema.QuestionPageRespOperator{ID: questionInfo.LastEditUserID} t.Operator = &schema.QuestionPageRespOperator{ID: questionInfo.LastEditUserID}
} }
if haveAnswered { if haveAnswered {
if t.LastAnsweredAt.Unix() > t.OperatedAt { if t.LastAnsweredAt.Unix() > t.OperatedAt {
t.OperationType = answeredOp t.OperationType = schema.QuestionPageRespOperationTypeAnswered
t.OperatedAt = t.LastAnsweredAt.Unix() t.OperatedAt = t.LastAnsweredAt.Unix()
t.Operator = &schema.QuestionPageRespOperator{ID: t.LastAnsweredUserID} t.Operator = &schema.QuestionPageRespOperator{ID: t.LastAnsweredUserID}
} }

View File

@ -140,8 +140,8 @@ func (qs *QuestionService) CloseMsgList(ctx context.Context, lang i18n.Language)
return nil, errors.InternalServer(reason.UnknownError).WithError(err).WithStack() return nil, errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
} }
for _, t := range resp { for _, t := range resp {
t.Name = translator.GlobalTrans.Tr(lang, t.Name) t.Name = translator.Tr(lang, t.Name)
t.Description = translator.GlobalTrans.Tr(lang, t.Description) t.Description = translator.Tr(lang, t.Description)
} }
return resp, err return resp, err
} }
@ -163,7 +163,7 @@ func (qs *QuestionService) CheckAddQuestion(ctx context.Context, req *schema.Que
errorlist := make([]*validator.FormErrorField, 0) errorlist := make([]*validator.FormErrorField, 0)
errorlist = append(errorlist, &validator.FormErrorField{ errorlist = append(errorlist, &validator.FormErrorField{
ErrorField: "tags", ErrorField: "tags",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), reason.TagNotFound), ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.TagNotFound),
}) })
err = errors.BadRequest(reason.RecommendTagEnter) err = errors.BadRequest(reason.RecommendTagEnter)
return errorlist, err return errorlist, err
@ -176,7 +176,7 @@ func (qs *QuestionService) CheckAddQuestion(ctx context.Context, req *schema.Que
errorlist := make([]*validator.FormErrorField, 0) errorlist := make([]*validator.FormErrorField, 0)
errorlist = append(errorlist, &validator.FormErrorField{ errorlist = append(errorlist, &validator.FormErrorField{
ErrorField: "tags", ErrorField: "tags",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter), ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter),
}) })
err = errors.BadRequest(reason.RecommendTagEnter) err = errors.BadRequest(reason.RecommendTagEnter)
return errorlist, err return errorlist, err
@ -213,7 +213,7 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
errorlist := make([]*validator.FormErrorField, 0) errorlist := make([]*validator.FormErrorField, 0)
errorlist = append(errorlist, &validator.FormErrorField{ errorlist = append(errorlist, &validator.FormErrorField{
ErrorField: "tags", ErrorField: "tags",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), reason.TagNotFound), ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.TagNotFound),
}) })
err = errors.BadRequest(reason.RecommendTagEnter) err = errors.BadRequest(reason.RecommendTagEnter)
return errorlist, err return errorlist, err
@ -226,7 +226,7 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
errorlist := make([]*validator.FormErrorField, 0) errorlist := make([]*validator.FormErrorField, 0)
errorlist = append(errorlist, &validator.FormErrorField{ errorlist = append(errorlist, &validator.FormErrorField{
ErrorField: "tags", ErrorField: "tags",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter), ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter),
}) })
err = errors.BadRequest(reason.RecommendTagEnter) err = errors.BadRequest(reason.RecommendTagEnter)
return errorlist, err return errorlist, err
@ -539,7 +539,7 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest
errorlist := make([]*validator.FormErrorField, 0) errorlist := make([]*validator.FormErrorField, 0)
errorlist = append(errorlist, &validator.FormErrorField{ errorlist = append(errorlist, &validator.FormErrorField{
ErrorField: "tags", ErrorField: "tags",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter), ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter),
}) })
err = errors.BadRequest(reason.RecommendTagEnter) err = errors.BadRequest(reason.RecommendTagEnter)
return errorlist, err return errorlist, err

View File

@ -74,8 +74,8 @@ func (rs *ReportService) GetReportTypeList(ctx context.Context, lang i18n.Langua
err = errors.BadRequest(reason.UnknownError) err = errors.BadRequest(reason.UnknownError)
} }
for _, t := range resp { for _, t := range resp {
t.Name = translator.GlobalTrans.Tr(lang, t.Name) t.Name = translator.Tr(lang, t.Name)
t.Description = translator.GlobalTrans.Tr(lang, t.Description) t.Description = translator.Tr(lang, t.Description)
} }
return resp, err return resp, err
} }

View File

@ -72,13 +72,13 @@ func (rs *RoleService) GetRoleMapping(ctx context.Context) (roleMapping map[int]
func (rs *RoleService) translateRole(ctx context.Context, role *entity.Role) { func (rs *RoleService) translateRole(ctx context.Context, role *entity.Role) {
switch role.Name { switch role.Name {
case roleUserName: case roleUserName:
role.Name = translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), trRoleNameUser) role.Name = translator.Tr(handler.GetLangByCtx(ctx), trRoleNameUser)
role.Description = translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionUser) role.Description = translator.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionUser)
case roleAdminName: case roleAdminName:
role.Name = translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), trRoleNameAdmin) role.Name = translator.Tr(handler.GetLangByCtx(ctx), trRoleNameAdmin)
role.Description = translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionAdmin) role.Description = translator.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionAdmin)
case roleModeratorName: case roleModeratorName:
role.Name = translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), trRoleNameModerator) role.Name = translator.Tr(handler.GetLangByCtx(ctx), trRoleNameModerator)
role.Description = translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionModerator) role.Description = translator.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionModerator)
} }
} }

View File

@ -496,7 +496,7 @@ func (ts *TagCommonService) ObjectChangeTag(ctx context.Context, objectTagData *
thisObjTagNameList := make([]string, 0) thisObjTagNameList := make([]string, 0)
thisObjTagIDList := make([]string, 0) thisObjTagIDList := make([]string, 0)
for _, t := range objectTagData.Tags { for _, t := range objectTagData.Tags {
t.SlugName = strings.ToLower(t.SlugName) // t.SlugName = strings.ToLower(t.SlugName)
thisObjTagNameList = append(thisObjTagNameList, t.SlugName) thisObjTagNameList = append(thisObjTagNameList, t.SlugName)
} }
@ -508,13 +508,13 @@ func (ts *TagCommonService) ObjectChangeTag(ctx context.Context, objectTagData *
tagInDbMapping := make(map[string]*entity.Tag) tagInDbMapping := make(map[string]*entity.Tag)
for _, tag := range tagListInDb { for _, tag := range tagListInDb {
tagInDbMapping[tag.SlugName] = tag tagInDbMapping[strings.ToLower(tag.SlugName)] = tag
thisObjTagIDList = append(thisObjTagIDList, tag.ID) thisObjTagIDList = append(thisObjTagIDList, tag.ID)
} }
addTagList := make([]*entity.Tag, 0) addTagList := make([]*entity.Tag, 0)
for _, tag := range objectTagData.Tags { for _, tag := range objectTagData.Tags {
_, ok := tagInDbMapping[tag.SlugName] _, ok := tagInDbMapping[strings.ToLower(tag.SlugName)]
if ok { if ok {
continue continue
} }

View File

@ -12,6 +12,7 @@ import (
"github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity"
"github.com/answerdev/answer/internal/service/auth" "github.com/answerdev/answer/internal/service/auth"
"github.com/answerdev/answer/internal/service/role" "github.com/answerdev/answer/internal/service/role"
usercommon "github.com/answerdev/answer/internal/service/user_common" usercommon "github.com/answerdev/answer/internal/service/user_common"
@ -38,6 +39,7 @@ type UserAdminService struct {
userRoleRelService *role.UserRoleRelService userRoleRelService *role.UserRoleRelService
authService *auth.AuthService authService *auth.AuthService
userCommonService *usercommon.UserCommon userCommonService *usercommon.UserCommon
userActivity activity.UserActiveActivityRepo
} }
// NewUserAdminService new user admin service // NewUserAdminService new user admin service
@ -46,12 +48,14 @@ func NewUserAdminService(
userRoleRelService *role.UserRoleRelService, userRoleRelService *role.UserRoleRelService,
authService *auth.AuthService, authService *auth.AuthService,
userCommonService *usercommon.UserCommon, userCommonService *usercommon.UserCommon,
userActivity activity.UserActiveActivityRepo,
) *UserAdminService { ) *UserAdminService {
return &UserAdminService{ return &UserAdminService{
userRepo: userRepo, userRepo: userRepo,
userRoleRelService: userRoleRelService, userRoleRelService: userRoleRelService,
authService: authService, authService: authService,
userCommonService: userCommonService, userCommonService: userCommonService,
userActivity: userActivity,
} }
} }
@ -83,7 +87,17 @@ func (us *UserAdminService) UpdateUserStatus(ctx context.Context, req *schema.Up
userInfo.Status = entity.UserStatusAvailable userInfo.Status = entity.UserStatusAvailable
userInfo.MailStatus = entity.EmailStatusAvailable userInfo.MailStatus = entity.EmailStatusAvailable
} }
return us.userRepo.UpdateUserStatus(ctx, userInfo.ID, userInfo.Status, userInfo.MailStatus, userInfo.EMail)
err = us.userRepo.UpdateUserStatus(ctx, userInfo.ID, userInfo.Status, userInfo.MailStatus, userInfo.EMail)
if err != nil {
return err
}
// if user reputation is zero means this user is inactive, so try to activate this user.
if req.IsNormal() && userInfo.Rank == 0 {
return us.userActivity.UserActive(ctx, userInfo.ID)
}
return nil
} }
// UpdateUserRole update user role // UpdateUserRole update user role

View File

@ -510,7 +510,7 @@ func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema.
if exist { if exist {
resp = append([]*validator.FormErrorField{}, &validator.FormErrorField{ resp = append([]*validator.FormErrorField{}, &validator.FormErrorField{
ErrorField: "e_mail", ErrorField: "e_mail",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), reason.EmailDuplicate), ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.EmailDuplicate),
}) })
return resp, errors.BadRequest(reason.EmailDuplicate) return resp, errors.BadRequest(reason.EmailDuplicate)
} }

View File

@ -3,22 +3,26 @@ package converter
import ( import (
"bytes" "bytes"
"github.com/microcosm-cc/bluemonday"
"github.com/segmentfault/pacman/log" "github.com/segmentfault/pacman/log"
"github.com/yuin/goldmark" "github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html" "github.com/yuin/goldmark/renderer"
goldmarkHTML "github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
) )
// Markdown2HTML convert markdown to html // Markdown2HTML convert markdown to html
func Markdown2HTML(source string) string { func Markdown2HTML(source string) string {
mdConverter := goldmark.New( mdConverter := goldmark.New(
goldmark.WithExtensions(extension.GFM), goldmark.WithExtensions(&DangerousHTMLFilterExtension{}, extension.GFM),
goldmark.WithParserOptions( goldmark.WithParserOptions(
parser.WithAutoHeadingID(), parser.WithAutoHeadingID(),
), ),
goldmark.WithRendererOptions( goldmark.WithRendererOptions(
html.WithHardWraps(), goldmarkHTML.WithHardWraps(),
), ),
) )
var buf bytes.Buffer var buf bytes.Buffer
@ -28,3 +32,56 @@ func Markdown2HTML(source string) string {
} }
return buf.String() return buf.String()
} }
type DangerousHTMLFilterExtension struct {
}
func (e *DangerousHTMLFilterExtension) Extend(m goldmark.Markdown) {
m.Renderer().AddOptions(renderer.WithNodeRenderers(
util.Prioritized(&DangerousHTMLRenderer{
Config: goldmarkHTML.NewConfig(),
Filter: bluemonday.UGCPolicy(),
}, 1),
))
}
type DangerousHTMLRenderer struct {
goldmarkHTML.Config
Filter *bluemonday.Policy
}
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
func (r *DangerousHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindHTMLBlock, r.renderHTMLBlock)
reg.Register(ast.KindRawHTML, r.renderRawHTML)
}
func (r *DangerousHTMLRenderer) renderRawHTML(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkSkipChildren, nil
}
n := node.(*ast.RawHTML)
l := n.Segments.Len()
for i := 0; i < l; i++ {
segment := n.Segments.At(i)
_, _ = w.Write(r.Filter.SanitizeBytes(segment.Value(source)))
}
return ast.WalkSkipChildren, nil
}
func (r *DangerousHTMLRenderer) renderHTMLBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.HTMLBlock)
if entering {
l := n.Lines().Len()
for i := 0; i < l; i++ {
line := n.Lines().At(i)
r.Writer.SecureWrite(w, r.Filter.SanitizeBytes(line.Value(source)))
}
} else {
if n.HasClosure() {
closure := n.ClosureLine
r.Writer.SecureWrite(w, closure.Value(source))
}
}
return ast.WalkContinue, nil
}

View File

@ -1 +1 @@
<!doctype html><html><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><link rel="manifest" href="/manifest.json"/><script defer="defer" src="/static/js/main.fde484b3.js"></script><link href="/static/css/main.401dc3ca.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"><div id="spin-mask"><noscript><style>#spin-mask{display:none!important}</style></noscript><style>@keyframes _doc-spin{to{transform:rotate(360deg)}}#spin-mask{position:fixed;top:0;right:0;bottom:0;left:0;background-color:#fff;z-index:9999}#spin-container{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}#spin-container .spinner{box-sizing:border-box;display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25rem solid currentColor;border-right-color:transparent;color:rgba(108,117,125,.75);border-radius:50%;animation:.75s linear infinite _doc-spin}</style><div id="spin-container"><div class="spinner"></div></div></div></div></body></html> <!doctype html><html><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><link rel="manifest" href="/manifest.json"/><script defer="defer" src="/static/js/main.cb9bf782.js"></script><link href="/static/css/main.b8d8739f.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"><div id="spin-mask"><noscript><style>#spin-mask{display:none!important}</style></noscript><style>@keyframes _doc-spin{to{transform:rotate(360deg)}}#spin-mask{position:fixed;top:0;right:0;bottom:0;left:0;background-color:#fff;z-index:9999}#spin-container{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}#spin-container .spinner{box-sizing:border-box;display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25rem solid currentColor;border-right-color:transparent;color:rgba(108,117,125,.75);border-radius:50%;animation:.75s linear infinite _doc-spin}</style><div id="spin-container"><div class="spinner"></div></div></div></div></body></html>

View File

@ -22,7 +22,9 @@
"copy-to-clipboard": "^3.3.2", "copy-to-clipboard": "^3.3.2",
"dayjs": "^1.11.5", "dayjs": "^1.11.5",
"diff": "^5.1.0", "diff": "^5.1.0",
"dompurify": "^2.4.3",
"emoji-regex": "^10.2.1", "emoji-regex": "^10.2.1",
"html-react-parser": "^3.0.8",
"i18next": "^21.9.0", "i18next": "^21.9.0",
"katex": "^0.16.2", "katex": "^0.16.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
@ -51,6 +53,7 @@
"@testing-library/react": "^13.3.0", "@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"@types/color": "^3.0.3", "@types/color": "^3.0.3",
"@types/dompurify": "^2.4.0",
"@types/jest": "^27.5.2", "@types/jest": "^27.5.2",
"@types/lodash": "^4.14.184", "@types/lodash": "^4.14.184",
"@types/marked": "^4.0.6", "@types/marked": "^4.0.6",

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta name="generator" content="Answer %AnswerVersion% - https://github.com/answerdev/answer">
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
</head> </head>
<body> <body>

View File

@ -58,7 +58,7 @@ export interface QuestionParams {
title: string; title: string;
url_title?: string; url_title?: string;
content: string; content: string;
html: string; html?: string;
tags: Tag[]; tags: Tag[];
} }
@ -210,7 +210,7 @@ export interface AnswerItem {
export interface PostAnswerReq { export interface PostAnswerReq {
content: string; content: string;
html: string; html?: string;
question_id: string; question_id: string;
} }

View File

@ -1,5 +1,5 @@
import { useState, useEffect, memo } from 'react'; import { useState, useEffect, memo } from 'react';
import { Button } from 'react-bootstrap'; import { Button, Form } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import classNames from 'classnames'; import classNames from 'classnames';
@ -7,7 +7,7 @@ import classNames from 'classnames';
import { TextArea, Mentions } from '@/components'; import { TextArea, Mentions } from '@/components';
import { usePageUsers } from '@/hooks'; import { usePageUsers } from '@/hooks';
const Form = ({ const Index = ({
className = '', className = '',
value: initialValue = '', value: initialValue = '',
onSendReply, onSendReply,
@ -18,7 +18,7 @@ const Form = ({
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const pageUsers = usePageUsers(); const pageUsers = usePageUsers();
const { t } = useTranslation('translation', { keyPrefix: 'comment' }); const { t } = useTranslation('translation', { keyPrefix: 'comment' });
const [validationErrorMsg, setValidationErrorMsg] = useState('');
useEffect(() => { useEffect(() => {
if (!initialValue) { if (!initialValue) {
return; return;
@ -32,6 +32,13 @@ const Form = ({
const handleSelected = (val) => { const handleSelected = (val) => {
setValue(val); setValue(val);
}; };
const handleSendReply = () => {
onSendReply(value).catch((ex) => {
if (ex.isError) {
setValidationErrorMsg(ex.msg);
}
});
};
return ( return (
<div <div
className={classNames( className={classNames(
@ -39,17 +46,27 @@ const Form = ({
className, className,
)}> )}>
<div> <div>
<Mentions pageUsers={pageUsers.getUsers()} onSelected={handleSelected}> <div
className={classNames('custom-form-control', {
'is-invalid': validationErrorMsg,
})}>
<Mentions
pageUsers={pageUsers.getUsers()}
onSelected={handleSelected}>
<TextArea size="sm" value={value} onChange={handleChange} /> <TextArea size="sm" value={value} onChange={handleChange} />
</Mentions> </Mentions>
<div className="form-text">{t(`tip_${mode}`)}</div> <div className="form-text">{t(`tip_${mode}`)}</div>
</div> </div>
<Form.Control.Feedback type="invalid">
{validationErrorMsg}
</Form.Control.Feedback>
</div>
{type === 'edit' ? ( {type === 'edit' ? (
<div className="d-flex flex-row flex-md-column ms-0 ms-md-2 mt-2 mt-md-0"> <div className="d-flex flex-row flex-md-column ms-0 ms-md-2 mt-2 mt-md-0">
<Button <Button
size="sm" size="sm"
className="text-nowrap " className="text-nowrap "
onClick={() => onSendReply(value)}> onClick={() => handleSendReply()}>
{t('btn_save_edits')} {t('btn_save_edits')}
</Button> </Button>
<Button <Button
@ -64,7 +81,7 @@ const Form = ({
<Button <Button
size="sm" size="sm"
className="text-nowrap ms-0 ms-md-2 mt-2 mt-md-0" className="text-nowrap ms-0 ms-md-2 mt-2 mt-md-0"
onClick={() => onSendReply(value)}> onClick={() => handleSendReply()}>
{t('btn_add_comment')} {t('btn_add_comment')}
</Button> </Button>
)} )}
@ -72,4 +89,4 @@ const Form = ({
); );
}; };
export default memo(Form); export default memo(Index);

View File

@ -1,21 +1,30 @@
import { useState, memo } from 'react'; import { useState, memo } from 'react';
import { Button } from 'react-bootstrap'; import { Button, Form } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
import { TextArea, Mentions } from '@/components'; import { TextArea, Mentions } from '@/components';
import { usePageUsers } from '@/hooks'; import { usePageUsers } from '@/hooks';
const Form = ({ userName, onSendReply, onCancel, mode }) => { const Index = ({ userName, onSendReply, onCancel, mode }) => {
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const pageUsers = usePageUsers(); const pageUsers = usePageUsers();
const { t } = useTranslation('translation', { keyPrefix: 'comment' }); const { t } = useTranslation('translation', { keyPrefix: 'comment' });
const [validationErrorMsg, setValidationErrorMsg] = useState('');
const handleChange = (e) => { const handleChange = (e) => {
setValue(e.target.value); setValue(e.target.value);
}; };
const handleSelected = (val) => { const handleSelected = (val) => {
setValue(val); setValue(val);
}; };
const handleSendReply = () => {
onSendReply(value).catch((ex) => {
if (ex.isError) {
setValidationErrorMsg(ex.msg);
}
});
};
return ( return (
<div className="mb-2"> <div className="mb-2">
@ -24,6 +33,10 @@ const Form = ({ userName, onSendReply, onCancel, mode }) => {
</div> </div>
<div className="d-flex mb-1 align-items-start flex-column flex-md-row"> <div className="d-flex mb-1 align-items-start flex-column flex-md-row">
<div> <div>
<div
className={classNames('custom-form-control', {
'is-invalid': validationErrorMsg,
})}>
<Mentions <Mentions
pageUsers={pageUsers.getUsers()} pageUsers={pageUsers.getUsers()}
onSelected={handleSelected}> onSelected={handleSelected}>
@ -31,11 +44,15 @@ const Form = ({ userName, onSendReply, onCancel, mode }) => {
</Mentions> </Mentions>
<div className="form-text">{t(`tip_${mode}`)}</div> <div className="form-text">{t(`tip_${mode}`)}</div>
</div> </div>
<Form.Control.Feedback type="invalid">
{validationErrorMsg}
</Form.Control.Feedback>
</div>
<div className="d-flex flex-row flex-md-column ms-0 ms-md-2 mt-2 mt-md-0"> <div className="d-flex flex-row flex-md-column ms-0 ms-md-2 mt-2 mt-md-0">
<Button <Button
size="sm" size="sm"
className="text-nowrap" className="text-nowrap"
onClick={() => onSendReply(value)}> onClick={() => handleSendReply()}>
{t('btn_add_comment')} {t('btn_add_comment')}
</Button> </Button>
<Button <Button
@ -51,4 +68,4 @@ const Form = ({ userName, onSendReply, onCancel, mode }) => {
); );
}; };
export default memo(Form); export default memo(Index);

View File

@ -10,7 +10,12 @@ import { marked } from 'marked';
import * as Types from '@/common/interface'; import * as Types from '@/common/interface';
import { Modal } from '@/components'; import { Modal } from '@/components';
import { usePageUsers, useReportModal } from '@/hooks'; import { usePageUsers, useReportModal } from '@/hooks';
import { matchedUsers, parseUserInfo, scrollTop, bgFadeOut } from '@/utils'; import {
matchedUsers,
parseUserInfo,
scrollToElementTop,
bgFadeOut,
} from '@/utils';
import { tryNormalLogged } from '@/utils/guard'; import { tryNormalLogged } from '@/utils/guard';
import { import {
useQueryComments, useQueryComments,
@ -43,7 +48,7 @@ const Comment = ({ objectId, mode, commentId }) => {
const scrollCallback = useCallback((el, co) => { const scrollCallback = useCallback((el, co) => {
if (pageIndex === 0 && co.comment_id === commentId) { if (pageIndex === 0 && co.comment_id === commentId) {
setTimeout(() => { setTimeout(() => {
scrollTop(el); scrollToElementTop(el);
bgFadeOut(el); bgFadeOut(el);
}, 100); }, 100);
} }
@ -102,13 +107,14 @@ const Comment = ({ objectId, mode, commentId }) => {
const handleSendReply = (item) => { const handleSendReply = (item) => {
const users = matchedUsers(item.value); const users = matchedUsers(item.value);
const userNames = unionBy(users.map((user) => user.userName)); const userNames = unionBy(users.map((user) => user.userName));
const html = marked.parse(parseUserInfo(item.value)); const commentMarkDown = parseUserInfo(item.value);
if (!item.value || !html) { const html = marked.parse(commentMarkDown);
return; // if (!commentMarkDown || !html) {
} // return;
// }
const params = { const params = {
object_id: objectId, object_id: objectId,
original_text: item.value, original_text: commentMarkDown,
mention_username_list: userNames, mention_username_list: userNames,
parsed_text: html, parsed_text: html,
...(item.type === 'reply' ...(item.type === 'reply'
@ -119,7 +125,7 @@ const Comment = ({ objectId, mode, commentId }) => {
}; };
if (item.type === 'edit') { if (item.type === 'edit') {
updateComment({ return updateComment({
...params, ...params,
comment_id: item.comment_id, comment_id: item.comment_id,
}).then(() => { }).then(() => {
@ -134,8 +140,8 @@ const Comment = ({ objectId, mode, commentId }) => {
}), }),
); );
}); });
} else { }
addComment(params).then((res) => { return addComment(params).then((res) => {
if (item.type === 'reply') { if (item.type === 'reply') {
const index = comments.findIndex( const index = comments.findIndex(
(comment) => comment.comment_id === item.comment_id, (comment) => comment.comment_id === item.comment_id,
@ -157,7 +163,6 @@ const Comment = ({ objectId, mode, commentId }) => {
setVisibleComment(false); setVisibleComment(false);
}); });
}
}; };
const handleDelete = (id) => { const handleDelete = (id) => {

View File

@ -0,0 +1,77 @@
import { FC, memo } from 'react';
import { useTranslation } from 'react-i18next';
import classname from 'classnames';
import { Icon } from '@/components';
interface Props {
data: {
votes: number;
answers: number;
views: number;
};
showVotes?: boolean;
showAnswers?: boolean;
showViews?: boolean;
showAccepted?: boolean;
isAccepted?: boolean;
className?: string;
}
const Index: FC<Props> = ({
data,
showVotes = true,
showAnswers = true,
showViews = true,
isAccepted = false,
showAccepted = false,
className = '',
}) => {
const { t } = useTranslation('translation', { keyPrefix: 'counts' });
return (
<div className={classname('d-flex align-items-center', className)}>
{showVotes && (
<div className="d-flex align-items-center">
<Icon name="hand-thumbs-up-fill me-1" />
<span>
{data.votes} {t('votes')}
</span>
</div>
)}
{showAccepted && (
<div className="d-flex align-items-center ms-3 text-success">
<Icon name="check-circle-fill me-1" />
<span>{t('accepted')}</span>
</div>
)}
{showAnswers && (
<div
className={`d-flex align-items-center ms-3 ${
isAccepted ? 'text-success' : ''
}`}>
{isAccepted ? (
<Icon name="check-circle-fill me-1" />
) : (
<Icon name="chat-square-text-fill me-1" />
)}
<span>
{data.answers} {t('answers')}
</span>
</div>
)}
{showViews && (
<span className="summary-stat ms-3">
<Icon name="eye-fill" />
<em className="fst-normal ms-1">
{data.views} {t('views')}
</em>
</span>
)}
</div>
);
};
export default memo(Index);

View File

@ -8,6 +8,7 @@ import {
} from 'react'; } from 'react';
import { markdownToHtml } from '@/services'; import { markdownToHtml } from '@/services';
import { htmlToReact } from '@/utils';
import { htmlRender } from './utils'; import { htmlRender } from './utils';
@ -38,6 +39,7 @@ const Index = ({ value }, ref) => {
} }
previewRef.current?.scrollTo(0, scrollTop); previewRef.current?.scrollTo(0, scrollTop);
htmlRender(previewRef.current); htmlRender(previewRef.current);
}, [html]); }, [html]);
useImperativeHandle(ref, () => { useImperativeHandle(ref, () => {
@ -49,9 +51,9 @@ const Index = ({ value }, ref) => {
return ( return (
<div <div
ref={previewRef} ref={previewRef}
className="preview-wrap position-relative p-3 bg-light rounded text-break text-wrap mt-2 fmt" className="preview-wrap position-relative p-3 bg-light rounded text-break text-wrap mt-2 fmt">
dangerouslySetInnerHTML={{ __html: html }} {htmlToReact(html)}
/> </div>
); );
}; };

View File

@ -3,6 +3,8 @@ import { Pagination } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSearchParams, useNavigate, useLocation } from 'react-router-dom'; import { useSearchParams, useNavigate, useLocation } from 'react-router-dom';
import { scrollToDocTop } from '@/utils';
interface Props { interface Props {
currentPage: number; currentPage: number;
pageSize: number; pageSize: number;
@ -49,7 +51,7 @@ const PageItem = ({ page, currentPage, path }: PageItemProps) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
navigate(path); navigate(path);
window.scrollTo(0, 0); scrollToDocTop();
}}> }}>
{page} {page}
</Pagination.Item> </Pagination.Item>
@ -91,7 +93,7 @@ const Index: FC<Props> = ({
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
navigate(handleParams(currentPage - 1)); navigate(handleParams(currentPage - 1));
window.scrollTo(0, 0); scrollToDocTop();
}}> }}>
{t('prev')} {t('prev')}
</Pagination.Prev> </Pagination.Prev>
@ -186,7 +188,7 @@ const Index: FC<Props> = ({
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
navigate(handleParams(currentPage + 1)); navigate(handleParams(currentPage + 1));
window.scrollTo(0, 0); scrollToDocTop();
}}> }}>
{t('next')} {t('next')}
</Pagination.Next> </Pagination.Next>

View File

@ -6,7 +6,6 @@ import { useTranslation } from 'react-i18next';
import { pathFactory } from '@/router/pathFactory'; import { pathFactory } from '@/router/pathFactory';
import type * as Type from '@/common/interface'; import type * as Type from '@/common/interface';
import { import {
Icon,
Tag, Tag,
Pagination, Pagination,
FormatTime, FormatTime,
@ -14,6 +13,7 @@ import {
BaseUserCard, BaseUserCard,
QueryGroup, QueryGroup,
QuestionListLoader, QuestionListLoader,
Counts,
} from '@/components'; } from '@/components';
import { useQuestionList } from '@/services'; import { useQuestionList } from '@/services';
@ -95,29 +95,15 @@ const QuestionList: FC<Props> = ({ source }) => {
preFix={t(li.operation_type)} preFix={t(li.operation_type)}
/> />
</div> </div>
<div className="ms-0 ms-md-3 mt-2 mt-md-0"> <Counts
<span> data={{
<Icon name="hand-thumbs-up-fill" /> votes: li.vote_count,
<em className="fst-normal ms-1">{li.vote_count}</em> answers: li.answer_count,
</span> views: li.view_count,
<span }}
className={`ms-3 ${ isAccepted={li.accepted_answer_id >= 1}
li.accepted_answer_id >= 1 ? 'text-success' : '' className="ms-0 ms-md-3 mt-2 mt-md-0"
}`}>
<Icon
name={
li.accepted_answer_id >= 1
? 'check-circle-fill'
: 'chat-square-text-fill'
}
/> />
<em className="fst-normal ms-1">{li.answer_count}</em>
</span>
<span className="summary-stat ms-3">
<Icon name="eye-fill" />
<em className="fst-normal ms-1">{li.view_count}</em>
</span>
</div>
</div> </div>
<div className="question-tags m-n1"> <div className="question-tags m-n1">
{Array.isArray(li.tags) {Array.isArray(li.tags)
@ -139,7 +125,7 @@ const QuestionList: FC<Props> = ({ source }) => {
currentPage={curPage} currentPage={curPage}
totalSize={count} totalSize={count}
pageSize={pageSize} pageSize={pageSize}
pathname="/questions" pathname={source === 'questions' ? '/questions' : ''}
/> />
</div> </div>
</div> </div>

View File

@ -155,6 +155,7 @@ const TagSelector: FC<IProps> = ({
}; };
const handleKeyDown = (e) => { const handleKeyDown = (e) => {
e.stopPropagation(); e.stopPropagation();
if (!tags) { if (!tags) {
return; return;
} }
@ -166,13 +167,20 @@ const TagSelector: FC<IProps> = ({
if (keyCode === 40 && currentIndex < tags.length - 1) { if (keyCode === 40 && currentIndex < tags.length - 1) {
setCurrentIndex(currentIndex + 1); setCurrentIndex(currentIndex + 1);
} }
if (
keyCode === 13 && if (keyCode === 13 && currentIndex > -1) {
currentIndex > -1 &&
currentIndex <= tags.length - 1
) {
e.preventDefault(); e.preventDefault();
if (tags.length === 0) {
tagModal.onShow(tag);
return;
}
if (currentIndex <= tags.length - 1) {
handleClick(tags[currentIndex]); handleClick(tags[currentIndex]);
if (currentIndex === tags.length - 1 && currentIndex > 0) {
setCurrentIndex(currentIndex - 1);
}
}
} }
}; };
return ( return (

View File

@ -33,6 +33,7 @@ import PageTags from './PageTags';
import QuestionListLoader from './QuestionListLoader'; import QuestionListLoader from './QuestionListLoader';
import TagsLoader from './TagsLoader'; import TagsLoader from './TagsLoader';
import WelcomeTitle from './WelcomeTitle'; import WelcomeTitle from './WelcomeTitle';
import Counts from './Counts';
export { export {
Avatar, Avatar,
@ -72,5 +73,6 @@ export {
QuestionListLoader, QuestionListLoader,
TagsLoader, TagsLoader,
WelcomeTitle, WelcomeTitle,
Counts,
}; };
export type { EditorRef, JSONSchema, UISchema }; export type { EditorRef, JSONSchema, UISchema };

View File

@ -143,21 +143,7 @@ const Ask = () => {
const checkValidated = (): boolean => { const checkValidated = (): boolean => {
const bol = true; const bol = true;
const { title, content, tags, answer } = formData; const { title, content, tags, answer } = formData;
if (!title.value) { if (title.value && Array.from(title.value).length <= 150) {
// bol = false;
// formData.title = {
// value: '',
// isInvalid: true,
// errorMsg: t('form.fields.title.msg.empty'),
// };
} else if (Array.from(title.value).length > 150) {
// bol = false;
// formData.title = {
// value: title.value,
// isInvalid: true,
// errorMsg: t('form.fields.title.msg.range'),
// };
} else {
formData.title = { formData.title = {
value: title.value, value: title.value,
isInvalid: false, isInvalid: false,
@ -165,14 +151,7 @@ const Ask = () => {
}; };
} }
if (!content.value) { if (content.value) {
// bol = false;
// formData.content = {
// value: '',
// isInvalid: true,
// errorMsg: t('form.fields.body.msg.empty'),
// };
} else {
formData.content = { formData.content = {
value: content.value, value: content.value,
isInvalid: false, isInvalid: false,
@ -180,29 +159,16 @@ const Ask = () => {
}; };
} }
if (tags.value.length === 0) { if (Array.isArray(tags.value) && tags.value.length > 0) {
// bol = false;
// formData.tags = {
// value: [],
// isInvalid: true,
// errorMsg: t('form.fields.tags.msg.empty'),
// };
} else {
formData.tags = { formData.tags = {
value: tags.value, value: tags.value,
isInvalid: false, isInvalid: false,
errorMsg: '', errorMsg: '',
}; };
} }
if (checked) { if (checked) {
if (!answer.value) { if (answer.value) {
// bol = false;
// formData.answer = {
// value: '',
// isInvalid: true,
// errorMsg: t('form.fields.answer.msg.empty'),
// };
} else {
formData.answer = { formData.answer = {
value: answer.value, value: answer.value,
isInvalid: false, isInvalid: false,
@ -227,7 +193,6 @@ const Ask = () => {
const params: Type.QuestionParams = { const params: Type.QuestionParams = {
title: formData.title.value, title: formData.title.value,
content: formData.content.value, content: formData.content.value,
html: editorRef.current.getHtml(),
tags: formData.tags.value, tags: formData.tags.value,
}; };
if (isEdit) { if (isEdit) {
@ -261,7 +226,6 @@ const Ask = () => {
postAnswer({ postAnswer({
question_id: id, question_id: id,
content: formData.answer.value, content: formData.answer.value,
html: editorRef2.current.getHtml(),
}) })
.then(() => { .then(() => {
navigate(pathFactory.questionLanding(id, params.url_title)); navigate(pathFactory.questionLanding(id, params.url_title));

View File

@ -12,7 +12,7 @@ import {
FormatTime, FormatTime,
htmlRender, htmlRender,
} from '@/components'; } from '@/components';
import { scrollTop, bgFadeOut } from '@/utils'; import { scrollToElementTop, bgFadeOut } from '@/utils';
import { AnswerItem } from '@/common/interface'; import { AnswerItem } from '@/common/interface';
import { acceptanceAnswer } from '@/services'; import { acceptanceAnswer } from '@/services';
@ -60,7 +60,7 @@ const Index: FC<Props> = ({
if (aid === data.id) { if (aid === data.id) {
setTimeout(() => { setTimeout(() => {
const element = answerRef.current; const element = answerRef.current;
scrollTop(element); scrollToElementTop(element);
if (!searchParams.get('commentId')) { if (!searchParams.get('commentId')) {
bgFadeOut(answerRef.current); bgFadeOut(answerRef.current);
} }

View File

@ -8,7 +8,7 @@ import classNames from 'classnames';
import { Editor, Modal, TextArea } from '@/components'; import { Editor, Modal, TextArea } from '@/components';
import { FormDataType } from '@/common/interface'; import { FormDataType } from '@/common/interface';
import { postAnswer } from '@/services'; import { postAnswer } from '@/services';
import { guard } from '@/utils'; import { guard, handleFormError } from '@/utils';
interface Props { interface Props {
visible?: boolean; visible?: boolean;
@ -35,25 +35,44 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
const [focusType, setFocusType] = useState(''); const [focusType, setFocusType] = useState('');
const [editorFocusState, setEditorFocusState] = useState(false); const [editorFocusState, setEditorFocusState] = useState(false);
const checkValidated = (): boolean => {
let bol = true;
const { content } = formData;
if (!content.value || Array.from(content.value.trim()).length < 6) {
bol = false;
formData.content = {
value: content.value,
isInvalid: true,
errorMsg: t('characters'),
};
} else {
formData.content = {
value: content.value,
isInvalid: false,
errorMsg: '',
};
}
setFormData({
...formData,
});
return bol;
};
const handleSubmit = () => { const handleSubmit = () => {
if (!guard.tryNormalLogged(true)) { if (!guard.tryNormalLogged(true)) {
return; return;
} }
if (!formData.content.value) { if (!checkValidated()) {
setFormData({
content: {
value: '',
isInvalid: true,
errorMsg: t('empty'),
},
});
return; return;
} }
postAnswer({ postAnswer({
question_id: data?.qid, question_id: data?.qid,
content: formData.content.value, content: formData.content.value,
html: marked.parse(formData.content.value), html: marked.parse(formData.content.value),
}).then((res) => { })
.then((res) => {
setShowEditor(false); setShowEditor(false);
setFormData({ setFormData({
content: { content: {
@ -63,6 +82,12 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
}, },
}); });
callback?.(res.info); callback?.(res.info);
})
.catch((ex) => {
if (ex.isError) {
const stateData = handleFormError(ex, formData);
setFormData({ ...stateData });
}
}); });
}; };

View File

@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next';
import Pattern from '@/common/pattern'; import Pattern from '@/common/pattern';
import { Pagination } from '@/components'; import { Pagination } from '@/components';
import { loggedUserInfoStore, toastStore } from '@/stores'; import { loggedUserInfoStore, toastStore } from '@/stores';
import { scrollTop } from '@/utils'; import { scrollToElementTop } from '@/utils';
import { usePageTags, usePageUsers } from '@/hooks'; import { usePageTags, usePageUsers } from '@/hooks';
import type { import type {
ListResult, ListResult,
@ -80,7 +80,7 @@ const Index = () => {
if (page > 0 || order) { if (page > 0 || order) {
// scroll into view; // scroll into view;
const element = document.getElementById('answerHeader'); const element = document.getElementById('answerHeader');
scrollTop(element); scrollToElementTop(element);
} }
res.list.forEach((item) => { res.list.forEach((item) => {

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useRef } from 'react';
import { Container, Row, Col, Form, Button, Card } from 'react-bootstrap'; import { Container, Row, Col, Form, Button, Card } from 'react-bootstrap';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import classNames from 'classnames'; import classNames from 'classnames';
import { handleFormError } from '@/utils';
import { usePageTags } from '@/hooks'; import { usePageTags } from '@/hooks';
import { pathFactory } from '@/router/pathFactory'; import { pathFactory } from '@/router/pathFactory';
import { Editor, EditorRef, Icon } from '@/components'; import { Editor, EditorRef, Icon } from '@/components';
@ -19,11 +20,11 @@ import {
import './index.scss'; import './index.scss';
interface FormDataItem { interface FormDataItem {
answer: Type.FormValue<string>; content: Type.FormValue<string>;
description: Type.FormValue<string>; description: Type.FormValue<string>;
} }
const initFormData = { const initFormData = {
answer: { content: {
value: '', value: '',
isInvalid: false, isInvalid: false,
errorMsg: '', errorMsg: '',
@ -35,7 +36,6 @@ const initFormData = {
}, },
}; };
const Index = () => { const Index = () => {
const [formData, setFormData] = useState<FormDataItem>(initFormData);
const { aid = '', qid = '' } = useParams(); const { aid = '', qid = '' } = useParams();
const [focusType, setForceType] = useState(''); const [focusType, setForceType] = useState('');
@ -43,6 +43,10 @@ const Index = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { data } = useQueryAnswerInfo(aid); const { data } = useQueryAnswerInfo(aid);
const [formData, setFormData] = useState<FormDataItem>(initFormData);
initFormData.content.value = data?.info.content || '';
const { data: revisions = [] } = useQueryRevisions(aid); const { data: revisions = [] } = useQueryRevisions(aid);
const editorRef = useRef<EditorRef>({ const editorRef = useRef<EditorRef>({
@ -51,18 +55,10 @@ const Index = () => {
const questionContentRef = useRef<HTMLDivElement>(null); const questionContentRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!data) {
return;
}
formData.answer.value = data.info.content;
setFormData({ ...formData });
}, [data]);
const handleAnswerChange = (value: string) => const handleAnswerChange = (value: string) =>
setFormData({ setFormData({
...formData, ...formData,
answer: { ...formData.answer, value }, content: { ...formData.content, value },
}); });
const handleSummaryChange = (evt) => { const handleSummaryChange = (evt) => {
const v = evt.currentTarget.value; const v = evt.currentTarget.value;
@ -74,18 +70,18 @@ const Index = () => {
const checkValidated = (): boolean => { const checkValidated = (): boolean => {
let bol = true; let bol = true;
const { answer } = formData; const { content } = formData;
if (!answer.value) { if (!content.value || Array.from(content.value.trim()).length < 6) {
bol = false; bol = false;
formData.answer = { formData.content = {
value: '', value: content.value,
isInvalid: true, isInvalid: true,
errorMsg: '标题不能为空', errorMsg: t('form.fields.answer.feedback.characters'),
}; };
} else { } else {
formData.answer = { formData.content = {
value: answer.value, value: content.value,
isInvalid: false, isInvalid: false,
errorMsg: '', errorMsg: '',
}; };
@ -105,13 +101,14 @@ const Index = () => {
} }
const params: Type.AnswerParams = { const params: Type.AnswerParams = {
content: formData.answer.value, content: formData.content.value,
html: editorRef.current.getHtml(), html: editorRef.current.getHtml(),
question_id: qid, question_id: qid,
id: aid, id: aid,
edit_summary: formData.description.value, edit_summary: formData.description.value,
}; };
modifyAnswer(params).then((res) => { modifyAnswer(params)
.then((res) => {
navigate( navigate(
pathFactory.answerLanding({ pathFactory.answerLanding({
questionId: qid, questionId: qid,
@ -122,12 +119,18 @@ const Index = () => {
state: { isReview: res?.wait_for_review }, state: { isReview: res?.wait_for_review },
}, },
); );
})
.catch((ex) => {
if (ex.isError) {
const stateData = handleFormError(ex, formData);
setFormData({ ...stateData });
}
}); });
}; };
const handleSelectedRevision = (e) => { const handleSelectedRevision = (e) => {
const index = e.target.value; const index = e.target.value;
const revision = revisions[index]; const revision = revisions[index];
formData.answer.value = revision.content.content; formData.content.value = revision.content.content;
setFormData({ ...formData }); setFormData({ ...formData });
}; };
@ -190,7 +193,7 @@ const Index = () => {
<Form.Group controlId="answer" className="mt-3"> <Form.Group controlId="answer" className="mt-3">
<Form.Label>{t('form.fields.answer.label')}</Form.Label> <Form.Label>{t('form.fields.answer.label')}</Form.Label>
<Editor <Editor
value={formData.answer.value} value={formData.content.value}
onChange={handleAnswerChange} onChange={handleAnswerChange}
className={classNames( className={classNames(
'form-control p-0', 'form-control p-0',
@ -205,14 +208,14 @@ const Index = () => {
ref={editorRef} ref={editorRef}
/> />
<Form.Control <Form.Control
value={formData.answer.value} value={formData.content.value}
type="text" type="text"
isInvalid={formData.answer.isInvalid} isInvalid={formData.content.isInvalid}
readOnly readOnly
hidden hidden
/> />
<Form.Control.Feedback type="invalid"> <Form.Control.Feedback type="invalid">
{formData.answer.errorMsg} {formData.content.errorMsg}
</Form.Control.Feedback> </Form.Control.Feedback>
</Form.Group> </Form.Group>
<Form.Group controlId="edit_summary" className="my-3"> <Form.Group controlId="edit_summary" className="my-3">

View File

@ -3,7 +3,7 @@ import { ListGroupItem } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { pathFactory } from '@/router/pathFactory'; import { pathFactory } from '@/router/pathFactory';
import { Icon, Tag, FormatTime, BaseUserCard } from '@/components'; import { Tag, FormatTime, BaseUserCard, Counts } from '@/components';
import type { SearchResItem } from '@/common/interface'; import type { SearchResItem } from '@/common/interface';
import { escapeRemove } from '@/utils'; import { escapeRemove } from '@/utils';
@ -51,23 +51,17 @@ const Index: FC<Props> = ({ data }) => {
className="me-3" className="me-3"
preFix={data.object_type === 'question' ? 'asked' : 'answered'} preFix={data.object_type === 'question' ? 'asked' : 'answered'}
/> />
<div className="d-flex align-items-center my-2 my-sm-0">
<div className="d-flex align-items-center me-3"> <Counts
<Icon name="hand-thumbs-up-fill me-1" /> className="my-2 my-sm-0"
<span> {data.object?.vote_count}</span> showViews={false}
</div> isAccepted={data.object?.accepted}
<div data={{
className={`d-flex align-items-center ${ votes: data.object?.vote_count,
data.object?.accepted ? 'text-success' : '' answers: data.object?.answer_count,
}`}> views: 0,
{data.object?.accepted ? ( }}
<Icon name="check-circle-fill me-1" /> />
) : (
<Icon name="chat-square-text-fill me-1" />
)}
<span>{data.object?.answer_count}</span>
</div>
</div>
</div> </div>
{data.object?.excerpt && ( {data.object?.excerpt && (

View File

@ -4,7 +4,7 @@ import { Link, useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { usePageTags } from '@/hooks'; import { usePageTags } from '@/hooks';
import { loggedUserInfoStore } from '@/stores'; import { loggedUserInfoStore, siteInfoStore } from '@/stores';
import { changeEmailVerify, getLoggedUserInfo } from '@/services'; import { changeEmailVerify, getLoggedUserInfo } from '@/services';
const Index: FC = () => { const Index: FC = () => {
@ -13,6 +13,7 @@ const Index: FC = () => {
const [step, setStep] = useState('loading'); const [step, setStep] = useState('loading');
const updateUser = loggedUserInfoStore((state) => state.update); const updateUser = loggedUserInfoStore((state) => state.update);
const siteName = siteInfoStore((state) => state.siteInfo.name);
useEffect(() => { useEffect(() => {
const code = searchParams.get('code'); const code = searchParams.get('code');
@ -38,7 +39,9 @@ const Index: FC = () => {
<Container className="pt-4 mt-2 mb-5"> <Container className="pt-4 mt-2 mb-5">
<Row className="justify-content-center"> <Row className="justify-content-center">
<Col lg={6}> <Col lg={6}>
<h3 className="text-center mt-3 mb-5">{t('page_title')}</h3> <h3 className="text-center mt-3 mb-5">
{t('page_title', { site_name: siteName })}
</h3>
{step === 'success' && ( {step === 'success' && (
<> <>
<p className="text-center">{t('confirm_new_email')}</p> <p className="text-center">{t('confirm_new_email')}</p>

View File

@ -2,7 +2,7 @@ import { FC, memo } from 'react';
import { ListGroup, ListGroupItem } from 'react-bootstrap'; import { ListGroup, ListGroupItem } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Icon, FormatTime, Tag } from '@/components'; import { FormatTime, Tag, Counts } from '@/components';
import { pathFactory } from '@/router/pathFactory'; import { pathFactory } from '@/router/pathFactory';
interface Props { interface Props {
@ -35,21 +35,16 @@ const Index: FC<Props> = ({ visible, data }) => {
<div className="d-flex align-items-center fs-14 text-secondary mb-2"> <div className="d-flex align-items-center fs-14 text-secondary mb-2">
<FormatTime <FormatTime
time={item.create_time} time={item.create_time}
className="me-4" className="me-3"
preFix={t('answered')} preFix={t('answered')}
/> />
<div className="d-flex align-items-center me-3"> <Counts
<Icon name="hand-thumbs-up-fill me-1" /> data={{ votes: item?.vote_count, views: 0, answers: 0 }}
<span>{item?.vote_count}</span> showAnswers={false}
</div> showViews={false}
showAccepted={item.accepted === 2}
{item.accepted === 2 && ( />
<div className="d-flex align-items-center me-3 text-success">
<Icon name="check-circle-fill me-1" />
<span>{t('accepted')}</span>
</div>
)}
</div> </div>
<div> <div>
{item.question_info?.tags?.map((tag) => { {item.question_info?.tags?.map((tag) => {

View File

@ -2,7 +2,7 @@ import { FC, memo } from 'react';
import { ListGroup, ListGroupItem } from 'react-bootstrap'; import { ListGroup, ListGroupItem } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Icon, FormatTime, Tag, BaseUserCard } from '@/components'; import { FormatTime, Tag, BaseUserCard, Counts } from '@/components';
import { pathFactory } from '@/router/pathFactory'; import { pathFactory } from '@/router/pathFactory';
interface Props { interface Props {
@ -44,35 +44,23 @@ const Index: FC<Props> = ({ visible, tabName, data }) => {
<span className="split-dot" /> <span className="split-dot" />
</> </>
)} )}
<FormatTime <FormatTime
time={item.create_time} time={
tabName === 'bookmarks' ? item.create_time : item.created_at
}
className="me-3" className="me-3"
preFix={t('asked')} preFix={t('asked')}
/> />
<div className="d-flex align-items-center me-3"> <Counts
<Icon name="hand-thumbs-up-fill me-1" /> isAccepted={Number(item.accepted_answer_id) > 0}
<span>{item.vote_count}</span> data={{
</div> votes: item.vote_count,
answers: item.answer_count,
{tabName !== 'answers' && ( views: item.view_count,
<div }}
className={`d-flex align-items-center me-3 ${ />
Number(item.accepted_answer_id) > 0 ? 'text-success' : ''
}`}>
{Number(item.accepted_answer_id) > 0 ? (
<Icon name="check-circle-fill me-1" />
) : (
<Icon name="chat-square-text-fill me-1" />
)}
<span>{item.answer_count}</span>
</div>
)}
<div className="d-flex align-items-center me-3">
<Icon name="eye-fill me-1" />
<span>{item.view_count}</span>
</div>
</div> </div>
<div> <div>
{item.tags?.map((tag) => { {item.tags?.map((tag) => {

View File

@ -32,9 +32,12 @@ const Index: FC<Props> = ({ data, type }) => {
}> }>
{type === 'answer' ? item.question_info.title : item.title} {type === 'answer' ? item.question_info.title : item.title}
</a> </a>
<div className="d-inline-block text-secondary ms-3 fs-14"> <div className="d-inline-block text-secondary ms-3 fs-14">
<Icon name="hand-thumbs-up-fill" /> <Icon name="hand-thumbs-up-fill me-1" />
<span> {item.vote_count}</span> <span>
{item.vote_count} {t('votes', { keyPrefix: 'counts' })}
</span>
</div> </div>
{type === 'question' && ( {type === 'question' && (
<div <div
@ -47,7 +50,10 @@ const Index: FC<Props> = ({ data, type }) => {
<Icon name="chat-square-text-fill" /> <Icon name="chat-square-text-fill" />
)} )}
<span> {item.answer_count}</span> <span>
{' '}
{item.answer_count} {t('answers', { keyPrefix: 'counts' })}
</span>
</div> </div>
)} )}

View File

@ -40,7 +40,7 @@ const Index = () => {
return ( return (
<div className="mt-5"> <div className="mt-5">
<div className="form-label">{t('title')}</div> <div className="form-label">{t('title')}</div>
<small className="form-text mt-0">{t('lable')}</small> <small className="form-text mt-0">{t('label')}</small>
<div className="d-grid gap-2 mt-3"> <div className="d-grid gap-2 mt-3">
{data?.map((item) => { {data?.map((item) => {

View File

@ -1,4 +1,6 @@
import i18next from 'i18next'; import i18next from 'i18next';
import parse from 'html-react-parser';
import * as DOMPurify from 'dompurify';
const Diff = require('diff'); const Diff = require('diff');
@ -21,7 +23,7 @@ function formatCount($num: number): string {
return res; return res;
} }
function scrollTop(element) { function scrollToElementTop(element) {
if (!element) { if (!element) {
return; return;
} }
@ -36,6 +38,15 @@ function scrollTop(element) {
}); });
} }
const scrollToDocTop = () => {
setTimeout(() => {
window.scrollTo({
top: 0,
behavior: 'smooth',
});
});
};
const bgFadeOut = (el) => { const bgFadeOut = (el) => {
if (el && !el.classList.contains('bg-fade-out')) { if (el && !el.classList.contains('bg-fade-out')) {
el.classList.add('bg-fade-out'); el.classList.add('bg-fade-out');
@ -160,14 +171,17 @@ function handleFormError(
) { ) {
if (error.list?.length > 0) { if (error.list?.length > 0) {
error.list.forEach((item) => { error.list.forEach((item) => {
data[item.error_field].isInvalid = true; const errorFieldObject = data[item.error_field];
data[item.error_field].errorMsg = item.error_msg; if (errorFieldObject) {
errorFieldObject.isInvalid = true;
errorFieldObject.errorMsg = item.error_msg;
}
}); });
} }
return data; return data;
} }
function diffText(newText: string, oldText: string): string { function diffText(newText: string, oldText?: string): string {
if (!newText) { if (!newText) {
return ''; return '';
} }
@ -181,8 +195,6 @@ function diffText(newText: string, oldText: string): string {
?.replace(/<input/gi, '&lt;input'); ?.replace(/<input/gi, '&lt;input');
} }
const diff = Diff.diffChars(oldText, newText); const diff = Diff.diffChars(oldText, newText);
console.log(diff);
const result = diff.map((part) => { const result = diff.map((part) => {
if (part.added) { if (part.added) {
if (part.value.replace(/\n/g, '').length <= 0) { if (part.value.replace(/\n/g, '').length <= 0) {
@ -214,10 +226,18 @@ function diffText(newText: string, oldText: string): string {
?.replace(/<input/gi, '&lt;input'); ?.replace(/<input/gi, '&lt;input');
} }
function htmlToReact(html: string) {
const cleanedHtml = DOMPurify.sanitize(html, {
USE_PROFILES: { html: true },
});
return parse(cleanedHtml);
}
export { export {
thousandthDivision, thousandthDivision,
formatCount, formatCount,
scrollTop, scrollToElementTop,
scrollToDocTop,
bgFadeOut, bgFadeOut,
matchedUsers, matchedUsers,
parseUserInfo, parseUserInfo,
@ -228,4 +248,5 @@ export {
labelStyle, labelStyle,
handleFormError, handleFormError,
diffText, diffText,
htmlToReact,
}; };

View File

@ -48,7 +48,7 @@ class Request {
}, },
(error) => { (error) => {
const { status, data: respData } = error.response || {}; const { status, data: respData } = error.response || {};
const { data = {}, msg = '' } = respData || {}; const { data = {}, msg = '', reason = '' } = respData || {};
if (status === 400) { if (status === 400) {
// show error message // show error message
if (data instanceof Object && data.err_type) { if (data instanceof Object && data.err_type) {
@ -79,7 +79,13 @@ class Request {
if (data instanceof Array && data.length > 0) { if (data instanceof Array && data.length > 0) {
// handle form error // handle form error
return Promise.reject({ isError: true, list: data }); return Promise.reject({
code: status,
msg,
reason,
isError: true,
list: data,
});
} }
if (!data || Object.keys(data).length <= 0) { if (!data || Object.keys(data).length <= 0) {