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
VERSION=1.0.2
VERSION=1.0.3
BIN=answer
DIR_SRC=./cmd/answer
DOCKER_CMD=docker
@ -39,6 +39,6 @@ install-ui-packages:
@corepack prepare pnpm@v7.12.2 --activate
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

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)
controller_adminReportController := controller_admin.NewReportController(reportAdminService)
userAdminRepo := user.NewUserAdminRepo(dataData, authRepo)
userAdminService := user_admin.NewUserAdminService(userAdminRepo, userRoleRelService, authService, userCommon)
userAdminService := user_admin.NewUserAdminService(userAdminRepo, userRoleRelService, authService, userCommon, userActiveActivityRepo)
userAdminController := controller_admin.NewUserAdminController(userAdminService)
reasonRepo := reason.NewReasonRepo(configRepo)
reasonService := reason2.NewReasonService(reasonRepo)

View File

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

View File

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

View File

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

4
go.mod
View File

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

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-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
@ -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.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
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 v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
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/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
@ -476,6 +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/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg=
github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=

View File

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

View File

@ -509,6 +509,8 @@ ui:
label: Revision
answer:
label: Answer
feedback:
characters: content must be at least 6 characters in length.
edit_summary:
label: Edit Summary
placeholder: >-
@ -748,7 +750,7 @@ ui:
text: User interface language. It will change when you refresh the page.
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_content: Are you sure you want to remove this login from your account?
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
edit link to refine and improve your existing answer, instead.</p>
empty: Answer cannot be empty.
characters: content must be at least 6 characters in length.
reopen:
title: Reopen this post
content: Are you sure you want to reopen?
@ -1012,7 +1015,11 @@ ui:
db_failed: Database connection failed
db_failed_desc: >-
This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your hosts database server is down.
counts:
views: views
votes: votes
answers: answers
accepted: Accepted
page_404:
desc: "Unfortunately, this page doesn't exist."
back_home: Back to homepage
@ -1050,7 +1057,7 @@ ui:
title: Admin
dashboard:
title: Dashboard
welcome: Welcome to {{site_name}} Admin!
welcome: Welcome to Answer Admin!
site_statistics: Site Statistics
questions: "Questions:"
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:
name:
user:
other: "User"
other: "Utente"
admin:
other: "Admin"
other: "Amministratore"
moderator:
other: "Moderator"
other: "Moderatore"
description:
user:
other: "Default with no special access."
other: "Predefinito senza alcun accesso speciale."
admin:
other: "Have the full power to access the site."
other: "Avere il pieno potere di accedere al sito."
moderator:
other: "Has access to all posts except admin settings."
other: "Ha accesso a tutti i post tranne le impostazioni di amministratore."
email:
other: "email"
password:
@ -40,9 +40,9 @@ backend:
not_found:
other: "Risposta non trovata"
cannot_deleted:
other: "No permission to delete."
other: "Permesso per cancellare mancante."
cannot_update:
other: "No permission to update."
other: "Nessun permesso per l'aggiornamento."
comment:
edit_without_permission:
other: "Non si hanno di privilegi sufficienti per modificare il commento"
@ -81,11 +81,11 @@ backend:
not_found:
other: "domanda non trovata"
cannot_deleted:
other: "No permission to delete."
other: "Permesso per cancellare mancante."
cannot_close:
other: "No permission to close."
other: "Nessun permesso per chiudere."
cannot_update:
other: "No permission to update."
other: "Nessun permesso per l'aggiornamento."
rank:
fail_to_meet_the_condition:
other: "Condizioni non valide per il grado"
@ -98,23 +98,26 @@ backend:
not_found:
other: "Etichetta non trovata"
recommend_tag_not_found:
other: "Recommend Tag is not exist."
other: "Il Tag consigliato non esiste."
recommend_tag_enter:
other: "Please enter at least one required tag."
other: "Inserisci almeno un tag."
not_contain_synonym_tags:
other: "Should not contain synonym tags."
other: "Non deve contenere tag sinonimi."
cannot_update:
other: "No permission to update."
other: "Nessun permesso per l'aggiornamento."
cannot_set_synonym_as_itself:
other: "You cannot set the synonym of the current tag as itself."
other: "Non puoi impostare il sinonimo del tag corrente come se stesso."
smtp:
config_from_name_cannot_be_email:
other: "The From Name cannot be a email address."
theme:
not_found:
other: "tema non trovato"
revision:
review_underway:
other: "Can't edit currently, there is a version in the review queue."
other: "Non è possibile modificare al momento, c'è una versione nella coda di revisione."
no_permission:
other: "No permission to Revision."
other: "Nessun permesso per la revisione."
user:
email_or_password_wrong:
other:
@ -128,75 +131,82 @@ backend:
username_duplicate:
other: "utente già in uso"
set_avatar:
other: "Avatar set failed."
config:
read_config_failed:
other: "Read config failed"
database:
connection_failed:
other: "Database connection failed"
create_table_failed:
other: "Create table failed"
install:
create_config_failed:
other: "Cant create the config.yaml file."
other: "Inserimento dell'Avatar non riuscito."
cannot_update_your_role:
other: "You cannot modify your role."
other: "Non puoi modificare il tuo ruolo."
not_allowed_registration:
other: "Currently the site is not open for registration"
other: "Al momento il sito non è aperto per la registrazione"
config:
read_config_failed:
other: "Read config failed"
database:
connection_failed:
other: "Database connection failed"
create_table_failed:
other: "Create table failed"
install:
create_config_failed:
other: "Cant create the config.yaml file."
report:
spam:
name:
other: "spam"
other: "posta indesiderata"
desc:
other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic."
other: "Questo articolo è una pubblicità o vandalismo. Non è utile o rilevante all'argomento corrente."
rude:
name:
other: "scortese o violento"
desc:
other: "A reasonable person would find this content inappropriate for respectful discourse."
other: "Una persona ragionevole trova questo contenuto inappropriato a un discorso rispettoso."
duplicate:
name:
other: "duplicato"
desc:
other: "This question has been asked before and already has an answer."
other: "Questa domanda è già stata posta e ha già una risposta."
not_answer:
name:
other: "non è una risposta"
desc:
other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether."
other: "Questo è stato pubblicato come una risposta, ma non tenta di rispondere alla domanda. Dovrebbe forse essere una modifica, un commento, un'altra domanda, o cancellata del tutto."
not_need:
name:
other: "non più necessario"
desc:
other: "This comment is outdated, conversational or not relevant to this post."
other: "Questo commento è obsoleto, conversazionale o non pertinente per questo post."
other:
name:
other: "altro"
desc:
other: "This post requires staff attention for another reason not listed above."
other: "Questo articolo richiede una supervisione dello staff per altre ragioni non listate sopra."
question:
close:
duplicate:
name:
other: "spam"
other: "posta indesiderata"
desc:
other: "This question has been asked before and already has an answer."
other: "Questa domanda è già stata posta e ha già una risposta."
guideline:
name:
other: "motivo legato alla community"
desc:
other: "This question doesn't meet a community guideline."
other: "Questa domanda non soddisfa le linee guida della comunità."
multiple:
name:
other: "richiede maggiori dettagli o chiarezza"
desc:
other: "This question currently includes multiple questions in one. It should focus on one problem only."
other: "Questa domanda attualmente include più domande in uno. Dovrebbe concentrarsi su un solo problema."
other:
name:
other: "altro"
desc:
other: "This post requires another reason not listed above."
other: "Questo articolo richiede un'altro motivo non listato sopra."
operation_type:
asked:
other: "asked"
answered:
other: "answered"
modified:
other: "modified"
notification:
action:
update_question:
@ -206,7 +216,7 @@ backend:
update_answer:
other: "risposta aggiornata"
accept_answer:
other: "risposta accepted"
other: "risposta accettata"
comment_question:
other: "domanda commentata"
comment_answer:
@ -226,21 +236,21 @@ backend:
#The following fields are used for interface presentation(Front-end)
ui:
how_to_format:
title: How to Format
title: Come formattare
desc: >-
<ul class="mb-0"><li><p class="mb-2">to make links</p><pre class="mb-2"><code>&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:
prev: Prev
next: Next
prev: Prec
next: Successivo
page_title:
question: Question
questions: Questions
question: Domanda
questions: Domande
tag: Tag
tags: Tags
tag_wiki: tag wiki
edit_tag: Edit Tag
ask_a_question: Add Question
edit_question: Edit Question
edit_tag: Modifica Tag
ask_a_question: Aggiungi una domanda
edit_question: Modifica Domanda
edit_answer: Edit Answer
search: Search
posts_containing: Posts containing
@ -801,6 +811,11 @@ ui:
confirm_new_email: Your email has been updated.
confirm_new_email_invalid: >-
Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?
unsubscribe:
page_title: Unsubscribe
success_title: Unsubscribe Successful
success_desc: You have been successfully removed from this subscriber list and wont receive any further emails from us.
link: Change settings
question:
following_tags: Following Tags
edit: Edit
@ -991,6 +1006,7 @@ ui:
answer_links: Answer Links
documents: Documents
feedback: Feedback
support: Support
review: Review
config: Config
update_to: Update to

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -246,9 +246,9 @@ type QuestionPageReq struct {
}
const (
QuestionPageRespOperationTypeAsked = "question.operation_type.asked"
QuestionPageRespOperationTypeAnswered = "question.operation_type.answered"
QuestionPageRespOperationTypeModified = "question.operation_type.modified"
QuestionPageRespOperationTypeAsked = "asked"
QuestionPageRespOperationTypeAnswered = "answered"
QuestionPageRespOperationTypeModified = "modified"
)
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
type SiteBrandingReq struct {
Logo string `validate:"required,gt=0,lte=512" form:"logo" json:"logo"`
Logo string `validate:"omitempty,gt=0,lte=512" form:"logo" json:"logo"`
MobileLogo string `validate:"omitempty,gt=0,lte=512" form:"mobile_logo" json:"mobile_logo"`
SquareIcon string `validate:"required,gt=0,lte=512" form:"square_icon" json:"square_icon"`
SquareIcon string `validate:"omitempty,gt=0,lte=512" form:"square_icon" json:"square_icon"`
Favicon string `validate:"omitempty,gt=0,lte=512" form:"favicon" json:"favicon"`
}
@ -134,7 +134,7 @@ type SiteThemeResp struct {
func (s *SiteThemeResp) TrTheme(ctx context.Context) {
la := handler.GetLangByCtx(ctx)
for _, option := range s.ThemeOptions {
tr := translator.GlobalTrans.Tr(la, option.Value)
tr := translator.Tr(la, option.Value)
// if tr is equal the option value means not found translation, so use the original label
if tr != option.Value {
option.Label = tr

View File

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

View File

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

View File

@ -6,9 +6,7 @@ import (
"time"
"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/translator"
"github.com/answerdev/answer/internal/service/activity_common"
"github.com/answerdev/answer/internal/service/activity_queue"
"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(
ctx context.Context, questionList []*entity.Question, loginUserID string, orderCond string) (
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)
questionIDs := make([]string, 0)
userIDs := make([]string, 0)
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 orderCond == schema.QuestionOrderCondNewest || (!haveEdited && !haveAnswered) {
t.OperationType = askedOp
t.OperationType = schema.QuestionPageRespOperationTypeAsked
t.OperatedAt = questionInfo.CreatedAt.Unix()
t.Operator = &schema.QuestionPageRespOperator{ID: questionInfo.UserID}
} else {
// if no one
if haveEdited {
t.OperationType = modifiedOp
t.OperationType = schema.QuestionPageRespOperationTypeModified
t.OperatedAt = questionInfo.UpdatedAt.Unix()
t.Operator = &schema.QuestionPageRespOperator{ID: questionInfo.LastEditUserID}
}
if haveAnswered {
if t.LastAnsweredAt.Unix() > t.OperatedAt {
t.OperationType = answeredOp
t.OperationType = schema.QuestionPageRespOperationTypeAnswered
t.OperatedAt = t.LastAnsweredAt.Unix()
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()
}
for _, t := range resp {
t.Name = translator.GlobalTrans.Tr(lang, t.Name)
t.Description = translator.GlobalTrans.Tr(lang, t.Description)
t.Name = translator.Tr(lang, t.Name)
t.Description = translator.Tr(lang, t.Description)
}
return resp, err
}
@ -163,7 +163,7 @@ func (qs *QuestionService) CheckAddQuestion(ctx context.Context, req *schema.Que
errorlist := make([]*validator.FormErrorField, 0)
errorlist = append(errorlist, &validator.FormErrorField{
ErrorField: "tags",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), reason.TagNotFound),
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.TagNotFound),
})
err = errors.BadRequest(reason.RecommendTagEnter)
return errorlist, err
@ -176,7 +176,7 @@ func (qs *QuestionService) CheckAddQuestion(ctx context.Context, req *schema.Que
errorlist := make([]*validator.FormErrorField, 0)
errorlist = append(errorlist, &validator.FormErrorField{
ErrorField: "tags",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter),
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter),
})
err = errors.BadRequest(reason.RecommendTagEnter)
return errorlist, err
@ -213,7 +213,7 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
errorlist := make([]*validator.FormErrorField, 0)
errorlist = append(errorlist, &validator.FormErrorField{
ErrorField: "tags",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), reason.TagNotFound),
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.TagNotFound),
})
err = errors.BadRequest(reason.RecommendTagEnter)
return errorlist, err
@ -226,7 +226,7 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
errorlist := make([]*validator.FormErrorField, 0)
errorlist = append(errorlist, &validator.FormErrorField{
ErrorField: "tags",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter),
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter),
})
err = errors.BadRequest(reason.RecommendTagEnter)
return errorlist, err
@ -539,7 +539,7 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest
errorlist := make([]*validator.FormErrorField, 0)
errorlist = append(errorlist, &validator.FormErrorField{
ErrorField: "tags",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter),
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter),
})
err = errors.BadRequest(reason.RecommendTagEnter)
return errorlist, err

View File

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

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) {
switch role.Name {
case roleUserName:
role.Name = translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), trRoleNameUser)
role.Description = translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionUser)
role.Name = translator.Tr(handler.GetLangByCtx(ctx), trRoleNameUser)
role.Description = translator.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionUser)
case roleAdminName:
role.Name = translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), trRoleNameAdmin)
role.Description = translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionAdmin)
role.Name = translator.Tr(handler.GetLangByCtx(ctx), trRoleNameAdmin)
role.Description = translator.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionAdmin)
case roleModeratorName:
role.Name = translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), trRoleNameModerator)
role.Description = translator.GlobalTrans.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionModerator)
role.Name = translator.Tr(handler.GetLangByCtx(ctx), trRoleNameModerator)
role.Description = translator.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionModerator)
}
}

View File

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

View File

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

View File

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

View File

@ -3,22 +3,26 @@ package converter
import (
"bytes"
"github.com/microcosm-cc/bluemonday"
"github.com/segmentfault/pacman/log"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/renderer"
goldmarkHTML "github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
)
// Markdown2HTML convert markdown to html
func Markdown2HTML(source string) string {
mdConverter := goldmark.New(
goldmark.WithExtensions(extension.GFM),
goldmark.WithExtensions(&DangerousHTMLFilterExtension{}, extension.GFM),
goldmark.WithParserOptions(
parser.WithAutoHeadingID(),
),
goldmark.WithRendererOptions(
html.WithHardWraps(),
goldmarkHTML.WithHardWraps(),
),
)
var buf bytes.Buffer
@ -28,3 +32,56 @@ func Markdown2HTML(source string) string {
}
return buf.String()
}
type DangerousHTMLFilterExtension struct {
}
func (e *DangerousHTMLFilterExtension) Extend(m goldmark.Markdown) {
m.Renderer().AddOptions(renderer.WithNodeRenderers(
util.Prioritized(&DangerousHTMLRenderer{
Config: goldmarkHTML.NewConfig(),
Filter: bluemonday.UGCPolicy(),
}, 1),
))
}
type DangerousHTMLRenderer struct {
goldmarkHTML.Config
Filter *bluemonday.Policy
}
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
func (r *DangerousHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindHTMLBlock, r.renderHTMLBlock)
reg.Register(ast.KindRawHTML, r.renderRawHTML)
}
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",
"dayjs": "^1.11.5",
"diff": "^5.1.0",
"dompurify": "^2.4.3",
"emoji-regex": "^10.2.1",
"html-react-parser": "^3.0.8",
"i18next": "^21.9.0",
"katex": "^0.16.2",
"lodash": "^4.17.21",
@ -51,6 +53,7 @@
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^13.5.0",
"@types/color": "^3.0.3",
"@types/dompurify": "^2.4.0",
"@types/jest": "^27.5.2",
"@types/lodash": "^4.14.184",
"@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 name="viewport" content="width=device-width, initial-scale=1" />
<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" />
</head>
<body>

View File

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

View File

@ -1,5 +1,5 @@
import { useState, useEffect, memo } from 'react';
import { Button } from 'react-bootstrap';
import { Button, Form } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
@ -7,7 +7,7 @@ import classNames from 'classnames';
import { TextArea, Mentions } from '@/components';
import { usePageUsers } from '@/hooks';
const Form = ({
const Index = ({
className = '',
value: initialValue = '',
onSendReply,
@ -18,7 +18,7 @@ const Form = ({
const [value, setValue] = useState('');
const pageUsers = usePageUsers();
const { t } = useTranslation('translation', { keyPrefix: 'comment' });
const [validationErrorMsg, setValidationErrorMsg] = useState('');
useEffect(() => {
if (!initialValue) {
return;
@ -32,6 +32,13 @@ const Form = ({
const handleSelected = (val) => {
setValue(val);
};
const handleSendReply = () => {
onSendReply(value).catch((ex) => {
if (ex.isError) {
setValidationErrorMsg(ex.msg);
}
});
};
return (
<div
className={classNames(
@ -39,17 +46,27 @@ const Form = ({
className,
)}>
<div>
<Mentions pageUsers={pageUsers.getUsers()} onSelected={handleSelected}>
<TextArea size="sm" value={value} onChange={handleChange} />
</Mentions>
<div className="form-text">{t(`tip_${mode}`)}</div>
<div
className={classNames('custom-form-control', {
'is-invalid': validationErrorMsg,
})}>
<Mentions
pageUsers={pageUsers.getUsers()}
onSelected={handleSelected}>
<TextArea size="sm" value={value} onChange={handleChange} />
</Mentions>
<div className="form-text">{t(`tip_${mode}`)}</div>
</div>
<Form.Control.Feedback type="invalid">
{validationErrorMsg}
</Form.Control.Feedback>
</div>
{type === 'edit' ? (
<div className="d-flex flex-row flex-md-column ms-0 ms-md-2 mt-2 mt-md-0">
<Button
size="sm"
className="text-nowrap "
onClick={() => onSendReply(value)}>
onClick={() => handleSendReply()}>
{t('btn_save_edits')}
</Button>
<Button
@ -64,7 +81,7 @@ const Form = ({
<Button
size="sm"
className="text-nowrap ms-0 ms-md-2 mt-2 mt-md-0"
onClick={() => onSendReply(value)}>
onClick={() => handleSendReply()}>
{t('btn_add_comment')}
</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 { Button } from 'react-bootstrap';
import { Button, Form } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
import { TextArea, Mentions } from '@/components';
import { usePageUsers } from '@/hooks';
const Form = ({ userName, onSendReply, onCancel, mode }) => {
const Index = ({ userName, onSendReply, onCancel, mode }) => {
const [value, setValue] = useState('');
const pageUsers = usePageUsers();
const { t } = useTranslation('translation', { keyPrefix: 'comment' });
const [validationErrorMsg, setValidationErrorMsg] = useState('');
const handleChange = (e) => {
setValue(e.target.value);
};
const handleSelected = (val) => {
setValue(val);
};
const handleSendReply = () => {
onSendReply(value).catch((ex) => {
if (ex.isError) {
setValidationErrorMsg(ex.msg);
}
});
};
return (
<div className="mb-2">
@ -24,18 +33,26 @@ const Form = ({ userName, onSendReply, onCancel, mode }) => {
</div>
<div className="d-flex mb-1 align-items-start flex-column flex-md-row">
<div>
<Mentions
pageUsers={pageUsers.getUsers()}
onSelected={handleSelected}>
<TextArea size="sm" value={value} onChange={handleChange} />
</Mentions>
<div className="form-text">{t(`tip_${mode}`)}</div>
<div
className={classNames('custom-form-control', {
'is-invalid': validationErrorMsg,
})}>
<Mentions
pageUsers={pageUsers.getUsers()}
onSelected={handleSelected}>
<TextArea size="sm" value={value} onChange={handleChange} />
</Mentions>
<div className="form-text">{t(`tip_${mode}`)}</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">
<Button
size="sm"
className="text-nowrap"
onClick={() => onSendReply(value)}>
onClick={() => handleSendReply()}>
{t('btn_add_comment')}
</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 { Modal } from '@/components';
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 {
useQueryComments,
@ -43,7 +48,7 @@ const Comment = ({ objectId, mode, commentId }) => {
const scrollCallback = useCallback((el, co) => {
if (pageIndex === 0 && co.comment_id === commentId) {
setTimeout(() => {
scrollTop(el);
scrollToElementTop(el);
bgFadeOut(el);
}, 100);
}
@ -102,13 +107,14 @@ const Comment = ({ objectId, mode, commentId }) => {
const handleSendReply = (item) => {
const users = matchedUsers(item.value);
const userNames = unionBy(users.map((user) => user.userName));
const html = marked.parse(parseUserInfo(item.value));
if (!item.value || !html) {
return;
}
const commentMarkDown = parseUserInfo(item.value);
const html = marked.parse(commentMarkDown);
// if (!commentMarkDown || !html) {
// return;
// }
const params = {
object_id: objectId,
original_text: item.value,
original_text: commentMarkDown,
mention_username_list: userNames,
parsed_text: html,
...(item.type === 'reply'
@ -119,7 +125,7 @@ const Comment = ({ objectId, mode, commentId }) => {
};
if (item.type === 'edit') {
updateComment({
return updateComment({
...params,
comment_id: item.comment_id,
}).then(() => {
@ -134,30 +140,29 @@ const Comment = ({ objectId, mode, commentId }) => {
}),
);
});
} else {
addComment(params).then((res) => {
if (item.type === 'reply') {
const index = comments.findIndex(
(comment) => comment.comment_id === item.comment_id,
);
comments[index].showReply = false;
comments.splice(index + 1, 0, res);
setComments([...comments]);
} else {
setComments([
...comments.map((comment) => {
if (comment.comment_id === item.comment_id) {
comment.showReply = false;
}
return comment;
}),
res,
]);
}
setVisibleComment(false);
});
}
return addComment(params).then((res) => {
if (item.type === 'reply') {
const index = comments.findIndex(
(comment) => comment.comment_id === item.comment_id,
);
comments[index].showReply = false;
comments.splice(index + 1, 0, res);
setComments([...comments]);
} else {
setComments([
...comments.map((comment) => {
if (comment.comment_id === item.comment_id) {
comment.showReply = false;
}
return comment;
}),
res,
]);
}
setVisibleComment(false);
});
};
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';
import { markdownToHtml } from '@/services';
import { htmlToReact } from '@/utils';
import { htmlRender } from './utils';
@ -38,6 +39,7 @@ const Index = ({ value }, ref) => {
}
previewRef.current?.scrollTo(0, scrollTop);
htmlRender(previewRef.current);
}, [html]);
useImperativeHandle(ref, () => {
@ -49,9 +51,9 @@ const Index = ({ value }, ref) => {
return (
<div
ref={previewRef}
className="preview-wrap position-relative p-3 bg-light rounded text-break text-wrap mt-2 fmt"
dangerouslySetInnerHTML={{ __html: html }}
/>
className="preview-wrap position-relative p-3 bg-light rounded text-break text-wrap mt-2 fmt">
{htmlToReact(html)}
</div>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@ import classNames from 'classnames';
import { Editor, Modal, TextArea } from '@/components';
import { FormDataType } from '@/common/interface';
import { postAnswer } from '@/services';
import { guard } from '@/utils';
import { guard, handleFormError } from '@/utils';
interface Props {
visible?: boolean;
@ -35,35 +35,60 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
const [focusType, setFocusType] = useState('');
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 = () => {
if (!guard.tryNormalLogged(true)) {
return;
}
if (!formData.content.value) {
setFormData({
content: {
value: '',
isInvalid: true,
errorMsg: t('empty'),
},
});
if (!checkValidated()) {
return;
}
postAnswer({
question_id: data?.qid,
content: formData.content.value,
html: marked.parse(formData.content.value),
}).then((res) => {
setShowEditor(false);
setFormData({
content: {
value: '',
isInvalid: false,
errorMsg: '',
},
})
.then((res) => {
setShowEditor(false);
setFormData({
content: {
value: '',
isInvalid: false,
errorMsg: '',
},
});
callback?.(res.info);
})
.catch((ex) => {
if (ex.isError) {
const stateData = handleFormError(ex, formData);
setFormData({ ...stateData });
}
});
callback?.(res.info);
});
};
const clickBtn = () => {

View File

@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next';
import Pattern from '@/common/pattern';
import { Pagination } from '@/components';
import { loggedUserInfoStore, toastStore } from '@/stores';
import { scrollTop } from '@/utils';
import { scrollToElementTop } from '@/utils';
import { usePageTags, usePageUsers } from '@/hooks';
import type {
ListResult,
@ -80,7 +80,7 @@ const Index = () => {
if (page > 0 || order) {
// scroll into view;
const element = document.getElementById('answerHeader');
scrollTop(element);
scrollToElementTop(element);
}
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 { useParams, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs';
import classNames from 'classnames';
import { handleFormError } from '@/utils';
import { usePageTags } from '@/hooks';
import { pathFactory } from '@/router/pathFactory';
import { Editor, EditorRef, Icon } from '@/components';
@ -19,11 +20,11 @@ import {
import './index.scss';
interface FormDataItem {
answer: Type.FormValue<string>;
content: Type.FormValue<string>;
description: Type.FormValue<string>;
}
const initFormData = {
answer: {
content: {
value: '',
isInvalid: false,
errorMsg: '',
@ -35,7 +36,6 @@ const initFormData = {
},
};
const Index = () => {
const [formData, setFormData] = useState<FormDataItem>(initFormData);
const { aid = '', qid = '' } = useParams();
const [focusType, setForceType] = useState('');
@ -43,6 +43,10 @@ const Index = () => {
const navigate = useNavigate();
const { data } = useQueryAnswerInfo(aid);
const [formData, setFormData] = useState<FormDataItem>(initFormData);
initFormData.content.value = data?.info.content || '';
const { data: revisions = [] } = useQueryRevisions(aid);
const editorRef = useRef<EditorRef>({
@ -51,18 +55,10 @@ const Index = () => {
const questionContentRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!data) {
return;
}
formData.answer.value = data.info.content;
setFormData({ ...formData });
}, [data]);
const handleAnswerChange = (value: string) =>
setFormData({
...formData,
answer: { ...formData.answer, value },
content: { ...formData.content, value },
});
const handleSummaryChange = (evt) => {
const v = evt.currentTarget.value;
@ -74,18 +70,18 @@ const Index = () => {
const checkValidated = (): boolean => {
let bol = true;
const { answer } = formData;
const { content } = formData;
if (!answer.value) {
if (!content.value || Array.from(content.value.trim()).length < 6) {
bol = false;
formData.answer = {
value: '',
formData.content = {
value: content.value,
isInvalid: true,
errorMsg: '标题不能为空',
errorMsg: t('form.fields.answer.feedback.characters'),
};
} else {
formData.answer = {
value: answer.value,
formData.content = {
value: content.value,
isInvalid: false,
errorMsg: '',
};
@ -105,29 +101,36 @@ const Index = () => {
}
const params: Type.AnswerParams = {
content: formData.answer.value,
content: formData.content.value,
html: editorRef.current.getHtml(),
question_id: qid,
id: aid,
edit_summary: formData.description.value,
};
modifyAnswer(params).then((res) => {
navigate(
pathFactory.answerLanding({
questionId: qid,
slugTitle: data?.question?.url_title,
answerId: aid,
}),
{
state: { isReview: res?.wait_for_review },
},
);
});
modifyAnswer(params)
.then((res) => {
navigate(
pathFactory.answerLanding({
questionId: qid,
slugTitle: data?.question?.url_title,
answerId: aid,
}),
{
state: { isReview: res?.wait_for_review },
},
);
})
.catch((ex) => {
if (ex.isError) {
const stateData = handleFormError(ex, formData);
setFormData({ ...stateData });
}
});
};
const handleSelectedRevision = (e) => {
const index = e.target.value;
const revision = revisions[index];
formData.answer.value = revision.content.content;
formData.content.value = revision.content.content;
setFormData({ ...formData });
};
@ -190,7 +193,7 @@ const Index = () => {
<Form.Group controlId="answer" className="mt-3">
<Form.Label>{t('form.fields.answer.label')}</Form.Label>
<Editor
value={formData.answer.value}
value={formData.content.value}
onChange={handleAnswerChange}
className={classNames(
'form-control p-0',
@ -205,14 +208,14 @@ const Index = () => {
ref={editorRef}
/>
<Form.Control
value={formData.answer.value}
value={formData.content.value}
type="text"
isInvalid={formData.answer.isInvalid}
isInvalid={formData.content.isInvalid}
readOnly
hidden
/>
<Form.Control.Feedback type="invalid">
{formData.answer.errorMsg}
{formData.content.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<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 { 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 { escapeRemove } from '@/utils';
@ -51,23 +51,17 @@ const Index: FC<Props> = ({ data }) => {
className="me-3"
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">
<Icon name="hand-thumbs-up-fill me-1" />
<span> {data.object?.vote_count}</span>
</div>
<div
className={`d-flex align-items-center ${
data.object?.accepted ? 'text-success' : ''
}`}>
{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>
<Counts
className="my-2 my-sm-0"
showViews={false}
isAccepted={data.object?.accepted}
data={{
votes: data.object?.vote_count,
answers: data.object?.answer_count,
views: 0,
}}
/>
</div>
{data.object?.excerpt && (

View File

@ -4,7 +4,7 @@ import { Link, useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePageTags } from '@/hooks';
import { loggedUserInfoStore } from '@/stores';
import { loggedUserInfoStore, siteInfoStore } from '@/stores';
import { changeEmailVerify, getLoggedUserInfo } from '@/services';
const Index: FC = () => {
@ -13,6 +13,7 @@ const Index: FC = () => {
const [step, setStep] = useState('loading');
const updateUser = loggedUserInfoStore((state) => state.update);
const siteName = siteInfoStore((state) => state.siteInfo.name);
useEffect(() => {
const code = searchParams.get('code');
@ -38,7 +39,9 @@ const Index: FC = () => {
<Container className="pt-4 mt-2 mb-5">
<Row className="justify-content-center">
<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' && (
<>
<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 { useTranslation } from 'react-i18next';
import { Icon, FormatTime, Tag } from '@/components';
import { FormatTime, Tag, Counts } from '@/components';
import { pathFactory } from '@/router/pathFactory';
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">
<FormatTime
time={item.create_time}
className="me-4"
className="me-3"
preFix={t('answered')}
/>
<div className="d-flex align-items-center me-3">
<Icon name="hand-thumbs-up-fill me-1" />
<span>{item?.vote_count}</span>
</div>
{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>
)}
<Counts
data={{ votes: item?.vote_count, views: 0, answers: 0 }}
showAnswers={false}
showViews={false}
showAccepted={item.accepted === 2}
/>
</div>
<div>
{item.question_info?.tags?.map((tag) => {

View File

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

View File

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

View File

@ -40,7 +40,7 @@ const Index = () => {
return (
<div className="mt-5">
<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">
{data?.map((item) => {

View File

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

View File

@ -48,7 +48,7 @@ class Request {
},
(error) => {
const { status, data: respData } = error.response || {};
const { data = {}, msg = '' } = respData || {};
const { data = {}, msg = '', reason = '' } = respData || {};
if (status === 400) {
// show error message
if (data instanceof Object && data.err_type) {
@ -79,7 +79,13 @@ class Request {
if (data instanceof Array && data.length > 0) {
// 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) {