diff --git a/back_end/saolei/config/global_settings.py b/back_end/saolei/config/global_settings.py index 932e72a4..86e0dfbf 100644 --- a/back_end/saolei/config/global_settings.py +++ b/back_end/saolei/config/global_settings.py @@ -18,7 +18,7 @@ class MaxSizes: password = 20 # 密码 signature = 4095 # 个性签名的长度,考虑了一些比较啰嗦的语言。 software = 1 - username = 255 # 用户名,考虑了一些名字特别长的文化。 + username = 30 # 用户名,行业习惯的上限 videofile = 5*1024*1024 # 录像文件 # 默认修改个人资料的次数 diff --git a/back_end/saolei/userprofile/forms.py b/back_end/saolei/userprofile/forms.py index 8e9ef92a..bf2e2553 100644 --- a/back_end/saolei/userprofile/forms.py +++ b/back_end/saolei/userprofile/forms.py @@ -19,6 +19,8 @@ class UserLoginForm(forms.Form): # 获取邮箱验证码时的表单,检查邮箱格式用 class EmailForm(forms.Form): email = forms.EmailField(max_length=MaxSizes.email, required=True,error_messages = FormErrors.email) + captcha = forms.CharField(required=True) + hashkey = forms.CharField(required=True) # 注册表单 diff --git a/back_end/saolei/userprofile/urls.py b/back_end/saolei/userprofile/urls.py index cc671d3f..e8671d0a 100644 --- a/back_end/saolei/userprofile/urls.py +++ b/back_end/saolei/userprofile/urls.py @@ -16,7 +16,7 @@ path('get_email_captcha/',views.get_email_captcha), path('get/',views.get_userProfile), path('set/',views.set_userProfile), - + path('checkcollision/',views.check_collision), # path('captcha/captcha', views.captcha, name='captcha'), # path('edit//', views.profile_edit, name='edit'), diff --git a/back_end/saolei/userprofile/views.py b/back_end/saolei/userprofile/views.py index 47eeb4e8..b8c92ff4 100644 --- a/back_end/saolei/userprofile/views.py +++ b/back_end/saolei/userprofile/views.py @@ -1,7 +1,7 @@ import logging logger = logging.getLogger('userprofile') from django.contrib.auth import authenticate, login, logout -from django.http import HttpResponse, JsonResponse, HttpResponseForbidden, HttpResponseNotFound +from django.http import HttpResponse, JsonResponse, HttpResponseForbidden, HttpResponseNotFound, HttpResponseBadRequest from .forms import UserLoginForm, UserRegisterForm, UserRetrieveForm, EmailForm from captcha.models import CaptchaStore import json @@ -24,34 +24,29 @@ # 用账号、密码登录 # 此处要分成两个,密码容易碰撞,hash难碰撞 def user_login(request): - user_login_form = UserLoginForm(data=request.POST) - if not user_login_form.is_valid(): - return JsonResponse({'status': 106, 'msg': "表单错误!"}) - data = user_login_form.cleaned_data - + data = request.POST + if not UserLoginForm(data=data).is_valid(): + return HttpResponseBadRequest() capt = data["captcha"] # 用户提交的验证码 key = data["hashkey"] # 验证码hash username = data["username"] - response = {'status': 100, 'msg': None} if not judge_captcha(capt, key): logger.info(f'用户 {username} 验证码错误') - return JsonResponse({'status': 104, 'msg': "验证码错误!"}) + return JsonResponse({'type': 'error', 'object': 'login', 'category': 'captcha'}) # 检验账号、密码是否正确匹配数据库中的某个用户 # 如果均匹配则返回这个 user 对象 user = authenticate( username=username, password=data['password']) if not user: logger.info(f'用户 {username} 账密错误') - return JsonResponse({'status': 105, 'msg': "账号或密码输入有误。请重新输入~"}) + return JsonResponse({'type': 'error', 'object': 'login', 'category': 'password'}) # 将用户数据保存在 session 中,即实现了登录动作 login(request, user) - response['msg'] = { - "id": user.id, "username": user.username, - "realname": user.realname, "is_banned": user.is_banned, "is_staff": user.is_staff} + userdata = {"id": user.id, "username": user.username, "realname": user.realname, "is_banned": user.is_banned, "is_staff": user.is_staff} if 'user_id' in data and data['user_id'] != str(user.id): # 检测到小号 - logger.warning(f'{data["user_id"][:50]} is diffrent from {str(user.id)}.') - return JsonResponse(response) + logger.warning(f'{data["user_id"][:50]} is different from {str(user.id)}.') + return JsonResponse({'type': 'success', 'user': userdata}) @require_GET @@ -63,7 +58,7 @@ def user_login_auto(request): def user_logout(request): logout(request) - return JsonResponse({'status': 100, 'msg': None}) + return HttpResponse() # 用户找回密码 @@ -72,26 +67,24 @@ def user_logout(request): def user_retrieve(request): user_retrieve_form = UserRetrieveForm(data=request.POST) if not user_retrieve_form.is_valid(): - return JsonResponse({'status': 101, 'msg': user_retrieve_form.errors.\ - as_text().split("*")[-1]}) + return HttpResponseBadRequest() emailHashkey = request.POST.get("email_key") email_captcha = request.POST.get("email_captcha") email = request.POST.get("email") - if judge_email_verification(email, email_captcha, emailHashkey): - user = UserProfile.objects.filter(email=user_retrieve_form.cleaned_data['email']).first() - if not user: - return JsonResponse({'status': 109, 'msg': "该邮箱尚未注册,请先注册!"}) - # 设置密码(哈希) - user.set_password( - user_retrieve_form.cleaned_data['password']) - user.save() - # 保存好数据后立即登录 - login(request, user) - logger.info(f'用户 {user.username}#{user.id} 邮箱找回密码') - EmailVerifyRecord.objects.filter(hashkey=emailHashkey).delete() - return JsonResponse({'status': 100, 'msg': user.realname}) - else: - return JsonResponse({'status': 102, 'msg': "邮箱验证码不正确或已过期!"}) + if not judge_email_verification(email, email_captcha, emailHashkey): + return JsonResponse({'type': 'error', 'object': 'emailcode'}) + user = UserProfile.objects.filter(email=user_retrieve_form.cleaned_data['email']).first() + if not user: + return HttpResponseNotFound() # 前端已经查过重了,理论上不应该进到这里 + # 设置密码(哈希) + user.set_password(user_retrieve_form.cleaned_data['password']) + user.save() + # 保存好数据后立即登录 + login(request, user) + logger.info(f'用户 {user.username}#{user.id} 邮箱找回密码') + EmailVerifyRecord.objects.filter(hashkey=emailHashkey).delete() + userdata = {"id": user.id, "username": user.username, "realname": user.realname, "is_banned": user.is_banned, "is_staff": user.is_staff} + return JsonResponse({'type': 'success', 'user': userdata}) # 用户注册 @@ -104,7 +97,8 @@ def user_register(request): emailHashkey = request.POST.get("email_key") email_captcha = request.POST.get("email_captcha") email = request.POST.get("email") - if EMAIL_SKIP or judge_email_verification(email, email_captcha, emailHashkey): + print(email, email_captcha, emailHashkey) + if judge_email_verification(email, email_captcha, emailHashkey): new_user = user_register_form.save(commit=False) # 设置密码(哈希) new_user.set_password( @@ -119,12 +113,12 @@ def user_register(request): logger.info(f'用户 {new_user.username}#{new_user.id} 注册') # 顺手把过期的验证码删了 EmailVerifyRecord.objects.filter(hashkey=emailHashkey).delete() - return JsonResponse({'status': 100, 'msg': { + return JsonResponse({'type': 'success', 'user': { "id": new_user.id, "username": new_user.username, - "realname": new_user.realname, "is_banned": False} + "realname": new_user.realname, "is_banned": new_user.is_banned, "is_staff": new_user.is_staff} }) else: - return JsonResponse({'status': 102, 'msg': "邮箱验证码不正确或已过期!"}) + return JsonResponse({'type': 'error', 'object': 'emailcode'}) else: if "email" not in user_register_form.cleaned_data or "username" not in user_register_form.cleaned_data: # 可能发生前端验证正确,而后端验证不正确(后端更严格),此时clean会直接删除email字段 @@ -136,6 +130,19 @@ def user_register(request): return JsonResponse({'status': 101, 'msg': user_register_form.errors.\ as_text().split("*")[-1]}) +@require_GET +def check_collision(request): + user = None + if request.GET.get('username'): + print(request.GET.get('username')) + user = UserProfile.objects.filter(username=request.GET.get('username')).first() + elif request.GET.get('email'): + user = UserProfile.objects.filter(email=request.GET.get('email')).first() + else: + return HttpResponseBadRequest() + if not user: + return HttpResponse(False) + return HttpResponse(True) # 【站长】任命解除管理员 # http://127.0.0.1:8000/userprofile/set_staff/?id=1&is_staff=True @@ -197,27 +204,24 @@ def refresh_captcha(request): @ratelimit(key='ip', rate='20/h') @require_POST def get_email_captcha(request): - email_form = EmailForm(data=request.POST) - if email_form.is_valid(): - capt = request.POST.get("captcha", None) # 用户提交的验证码 - key = request.POST.get("hashkey", None) # 验证码hash - response = {'status': 100, 'msg': None, "hashkey": None} - if judge_captcha(capt, key): - hashkey = send_email(request.POST.get("email", None), request.POST.get("type", None)) - if hashkey: - response['hashkey'] = hashkey - return JsonResponse(response) - else: - response['status'] = 103 - response['msg'] = "发送邮件失败" - return JsonResponse(response) - else: - response['status'] = 104 - response['msg'] = "验证码错误" - return JsonResponse(response) - else: - return JsonResponse({'status': 110, 'msg': email_form.errors.\ - as_text().split("*")[-1]}) + data = request.POST + email_form = EmailForm(data=data) + if not email_form.is_valid(): # 正常工作的前端不应当发出的请求 + return HttpResponseBadRequest() + capt = data.get("captcha") + key = data.get("hashkey") + if not judge_captcha(capt, key): # 图形验证码不对 + return JsonResponse({'type': 'error', 'object': 'captcha'}) + if EMAIL_SKIP: + code, hashkey = send_email(data.get("email"), data.get("type")) + return JsonResponse({'type': 'success', 'code': code, 'hashkey': hashkey}) + hashkey = send_email(data.get("email"), data.get("type")) + if hashkey: # 邮件发送成功 + return JsonResponse({'type': 'success', 'hashkey': hashkey}) + else: # 邮件发送失败 + return JsonResponse({'type': 'error', 'object': 'email'}) + + # 管理员使用的操作接口,调用方式见前端的StaffView.vue get_userProfile_fields = ["id", "userms__identifiers", "userms__video_num_limit", "username", "first_name", "last_name", "email", "realname", "signature", "country", "left_realname_n", "left_avatar_n", "left_signature_n", "is_banned"] # 可获取的域列表 diff --git a/back_end/saolei/utils/__init__.py b/back_end/saolei/utils/__init__.py index ebeef93f..338a6ac5 100644 --- a/back_end/saolei/utils/__init__.py +++ b/back_end/saolei/utils/__init__.py @@ -6,7 +6,7 @@ from django.http import HttpResponse, JsonResponse, FileResponse from django.shortcuts import render, redirect import requests -from config.flags import BAIDU_VERIFY_SKIP +from config.flags import BAIDU_VERIFY_SKIP, EMAIL_SKIP def generate_code(code_len): """ @@ -34,7 +34,9 @@ def send_email(email, send_type='register'): # email_record.send_type = send_type email_record.save() -# 验证码保存之后,我们就要把带有验证码的链接发送到注册时的邮箱! + # 验证码保存之后,我们就要把带有验证码的链接发送到注册时的邮箱! + if EMAIL_SKIP: + return code, hashkey if send_type == 'register': email_title = '元扫雷网邮箱注册验证码' email_body = f'欢迎您注册元扫雷网,您的邮箱验证码为:{code}(一小时内有效)。' diff --git a/front_end/package.json b/front_end/package.json index d9411707..6768d7f2 100644 --- a/front_end/package.json +++ b/front_end/package.json @@ -10,6 +10,7 @@ "build:openms": "vite build --mode openms" }, "dependencies": { + "@chenfengyuan/vue-countdown": "^2.1.2", "@element-plus/icons-vue": "^2.1.0", "@mdit/plugin-abbr": "^0.10.0", "@mdit/plugin-align": "^0.10.0", @@ -22,7 +23,7 @@ "@vueuse/core": "^10.11.0", "axios": "^1.7.2", "echarts": "^5.5.0", - "element-plus": "^2.7.0", + "element-plus": "^2.8.0", "flag-icon-css": "^4.1.7", "highlight.js": "^11.9.0", "image-conversion": "^2.1.1", @@ -32,10 +33,12 @@ "markdown-it-highlightjs": "^4.0.1", "markdown-it-mathjax3": "^4.3.2", "ms-toollib": "^1.4.10", + "out-of-character": "^1.2.2", "pinia": "^2.1.7", "pinia-plugin-persistedstate": "^3.2.1", "uuid": "^9.0.0", - "vue": "^3.4.0", + "validator": "^13.12.0", + "vue": "^3.5.12", "vue-echarts": "^6.7.1", "vue-i18n": "^9.13.1", "vue-router": "^4.0.3" diff --git a/front_end/src/App.vue b/front_end/src/App.vue index b5a566a5..d2b2dc57 100644 --- a/front_end/src/App.vue +++ b/front_end/src/App.vue @@ -57,9 +57,7 @@ import Login from "./components/Login.vue"; import Footer from "./components/Footer.vue"; import PlayerDialog from "./components/PlayerDialog.vue"; import useCurrentInstance from "@/utils/common/useCurrentInstance"; -import { useLocalStore, useUserStore } from "./store"; -const store = useUserStore(); -const local = useLocalStore(); +import { store, local } from "./store"; import { useI18n } from "vue-i18n"; const t = useI18n(); diff --git a/front_end/src/components/AccountLinkManager.vue b/front_end/src/components/AccountLinkManager.vue index 2e874781..4b4619b3 100644 --- a/front_end/src/components/AccountLinkManager.vue +++ b/front_end/src/components/AccountLinkManager.vue @@ -82,8 +82,8 @@ import { computed, onMounted, reactive, ref, watch } from 'vue'; import useCurrentInstance from '@/utils/common/useCurrentInstance'; -import { useLocalStore, useUserStore } from '@/store'; -import { ElMessageBox, ElNotification } from 'element-plus'; +import { store, local } from '@/store'; +import { ElMessageBox } from 'element-plus'; import { Platform, platformlist } from '@/utils/common/accountLinkPlatforms' import PlatformIcon from './widgets/PlatformIcon.vue'; import AccountLinkGuide from './dialogs/AccountLinkGuide.vue' @@ -101,8 +101,6 @@ interface AccountLink { const { proxy } = useCurrentInstance(); const t = useI18n(); -const store = useUserStore(); -const local = useLocalStore(); const accountlinks = ref([]); const formvisible = ref(false); const form = reactive({ diff --git a/front_end/src/components/Filters/BBBVFilter.vue b/front_end/src/components/Filters/BBBVFilter.vue index 777eb843..7aefac00 100644 --- a/front_end/src/components/Filters/BBBVFilter.vue +++ b/front_end/src/components/Filters/BBBVFilter.vue @@ -1,10 +1,11 @@ \ No newline at end of file diff --git a/front_end/src/components/Login.vue b/front_end/src/components/Login.vue index 88bc24ff..d11e6c39 100644 --- a/front_end/src/components/Login.vue +++ b/front_end/src/components/Login.vue @@ -1,165 +1,41 @@ diff --git a/front_end/src/components/Notifications.ts b/front_end/src/components/Notifications.ts new file mode 100644 index 00000000..62c91d37 --- /dev/null +++ b/front_end/src/components/Notifications.ts @@ -0,0 +1,54 @@ +import { local } from "@/store"; +import { ElNotification } from "element-plus"; +import i18n from "@/i18n"; + +// @ts-ignore +const { t } = i18n.global; + +const notificationMessage: { [code: number]: string } = { + 200: 'common.response.OK', + 400: 'common.response.BadRequest', + 403: 'common.response.Forbidden', + 404: 'common.response.NotFound', + 413: 'common.response.PayloadTooLarge', + 415: 'common.response.UnsupportedMediaType', + 429: 'common.response.TooManyRequests', + 500: 'common.response.InternalServerError', +}; + +export const httpErrorNotification = (error: any) => { + let status = error.response.status + if (status in notificationMessage) { + ElNotification({ + title: t('msg.actionFail'), + message: t(notificationMessage[status]), + type: 'error', + duration: local.notification_duration, + }) + } else { + unknownErrorNotification(error) + } + +} + +export const successNotification = (response: any) => { + if (response.status == 200) { + ElNotification({ + title: t('msg.actionSuccess'), + type: 'success', + duration: local.notification_duration, + }) + } else { + unknownErrorNotification(response) + } + +} + +export const unknownErrorNotification = (error: any) => { + ElNotification({ + title: t('msg.unknownError'), + message: error, + type: 'error', + duration: local.notification_duration, + }) +} \ No newline at end of file diff --git a/front_end/src/components/PlayerDialog.vue b/front_end/src/components/PlayerDialog.vue index 3fa8a631..81fce450 100644 --- a/front_end/src/components/PlayerDialog.vue +++ b/front_end/src/components/PlayerDialog.vue @@ -8,14 +8,11 @@ \ No newline at end of file diff --git a/front_end/src/components/dialogs/RegisterDialog.vue b/front_end/src/components/dialogs/RegisterDialog.vue new file mode 100644 index 00000000..afd8b292 --- /dev/null +++ b/front_end/src/components/dialogs/RegisterDialog.vue @@ -0,0 +1,143 @@ + + + \ No newline at end of file diff --git a/front_end/src/components/dialogs/RetrieveDialog.vue b/front_end/src/components/dialogs/RetrieveDialog.vue new file mode 100644 index 00000000..79c05ff3 --- /dev/null +++ b/front_end/src/components/dialogs/RetrieveDialog.vue @@ -0,0 +1,93 @@ + + + \ No newline at end of file diff --git a/front_end/src/components/formItems/emailCodeBlock.vue b/front_end/src/components/formItems/emailCodeBlock.vue new file mode 100644 index 00000000..60e495ae --- /dev/null +++ b/front_end/src/components/formItems/emailCodeBlock.vue @@ -0,0 +1,155 @@ + + + + \ No newline at end of file diff --git a/front_end/src/components/formItems/emailFormItem.vue b/front_end/src/components/formItems/emailFormItem.vue new file mode 100644 index 00000000..d1889c1b --- /dev/null +++ b/front_end/src/components/formItems/emailFormItem.vue @@ -0,0 +1,53 @@ + + + \ No newline at end of file diff --git a/front_end/src/components/formItems/passwordConfirmBlock.vue b/front_end/src/components/formItems/passwordConfirmBlock.vue new file mode 100644 index 00000000..9f347df4 --- /dev/null +++ b/front_end/src/components/formItems/passwordConfirmBlock.vue @@ -0,0 +1,47 @@ + + + \ No newline at end of file diff --git a/front_end/src/components/staff/StaffAccountLink.vue b/front_end/src/components/staff/StaffAccountLink.vue index b00e903e..ddf27d42 100644 --- a/front_end/src/components/staff/StaffAccountLink.vue +++ b/front_end/src/components/staff/StaffAccountLink.vue @@ -22,6 +22,7 @@ import { platformlist } from '@/utils/common/accountLinkPlatforms'; import useCurrentInstance from '@/utils/common/useCurrentInstance'; import { reactive } from 'vue'; +import { httpErrorNotification } from '../Notifications'; const { proxy } = useCurrentInstance(); @@ -42,7 +43,7 @@ const verify = () => { form.id = 0; form.platform = ''; form.identifier = ''; - }) + }).catch(httpErrorNotification) } const unverify = () => { @@ -56,7 +57,7 @@ const unverify = () => { form.id = 0; form.platform = ''; form.identifier = ''; - }) + }).catch(httpErrorNotification) } \ No newline at end of file diff --git a/front_end/src/components/widgets/IconMenuItem.vue b/front_end/src/components/widgets/IconMenuItem.vue index 1c944710..4015caba 100644 --- a/front_end/src/components/widgets/IconMenuItem.vue +++ b/front_end/src/components/widgets/IconMenuItem.vue @@ -15,9 +15,7 @@ diff --git a/front_end/src/components/widgets/LanguagePicker.vue b/front_end/src/components/widgets/LanguagePicker.vue index 3fdbb1a0..68c59f25 100644 --- a/front_end/src/components/widgets/LanguagePicker.vue +++ b/front_end/src/components/widgets/LanguagePicker.vue @@ -23,14 +23,13 @@ import i18n from "@/i18n"; import { useI18n } from "vue-i18n"; const t = useI18n(); -import { useLocalStore } from "@/store"; -const local = useLocalStore(); +import { local } from "@/store"; const options = [ { lang: "zh-cn", text: "简体中文" }, - { lang: "en", text: "English" }, - { lang: "de", text: "name" }, - { lang: "pl", text: "polski" }, + { lang: "en", text: "English (98%)" }, + { lang: "de", text: "Deutsch (16%)" }, + { lang: "pl", text: "polski (21%)" }, { lang: "dev", text: "dev" }, ]; diff --git a/front_end/src/components/widgets/LazyTooltip.vue b/front_end/src/components/widgets/LazyTooltip.vue index 2672cf1e..59c5d571 100644 --- a/front_end/src/components/widgets/LazyTooltip.vue +++ b/front_end/src/components/widgets/LazyTooltip.vue @@ -10,11 +10,9 @@ \ No newline at end of file diff --git a/front_end/src/components/widgets/PlatformIcon.vue b/front_end/src/components/widgets/PlatformIcon.vue index b196ceb7..02c9b612 100644 --- a/front_end/src/components/widgets/PlatformIcon.vue +++ b/front_end/src/components/widgets/PlatformIcon.vue @@ -1,5 +1,7 @@ diff --git a/front_end/src/i18n/index.ts b/front_end/src/i18n/index.ts index 359ba112..51a4a4dc 100644 --- a/front_end/src/i18n/index.ts +++ b/front_end/src/i18n/index.ts @@ -1,9 +1,9 @@ import { createI18n } from 'vue-i18n' -import { dev } from '@/i18n/locales/dev' -import { zhCn } from '@/i18n/locales/zh-cn' -import { de } from '@/i18n/locales/de' -import { en } from './locales/en' -import { pl } from './locales/pl' +import dev from '@/i18n/locales/dev' +import zhCn from '@/i18n/locales/zh-cn' +import de from '@/i18n/locales/de' +import en from './locales/en' +import pl from './locales/pl' /** * 获取所有语言 diff --git a/front_end/src/i18n/locales/de.ts b/front_end/src/i18n/locales/de.ts index 16abfd0f..feadd893 100644 --- a/front_end/src/i18n/locales/de.ts +++ b/front_end/src/i18n/locales/de.ts @@ -1,27 +1,25 @@ - -import { LocaleConfig } from '@/i18n/config' - -export const de = { +export default { local: 'de', - name: 'name', - forgetPassword: { - title: 'Titel', - email: 'E-Mail', + name: 'Deutsch', + form: { captcha: 'Captcha', - getEmailCode: 'E-Mail Code anfordern', + confirmPassword: 'Passwort bestätigen', + email: 'E-Mail', emailCode: 'E-Mail Code', password: 'Passwort', - confirmPassword: 'Passwort bestätigen', - confirm: 'bestätigen' + username: 'Benutzername', }, login: { - title: 'Titel', - username: 'Benutzername', - password: 'Passwort', - captcha: 'Captcha', + agreeTAC1: 'Zustimmen', + agreeTAC2: 'Nutzungsbedingungen', forgetPassword: 'Passwort vergessen?', keepMeLoggedIn: 'eingeloggt bleiben', - confirm: 'Login' + loginConfirm: 'Login', + loginTitle: 'Titel', + registerConfirm: 'bestätigen', + registerTitle: 'Titel', + retrieveConfirm: 'bestätigen', + retrieveTitle: 'Titel', }, menu: { ranking: 'Ranking', @@ -44,17 +42,4 @@ export const de = { confirmChange: 'Änderungen bestätigen', cancelChange: 'abbrechen', }, - register: { - title: 'Titel', - username: 'Benutzername', - email: 'E-mail', - captcha: 'Captcha', - getEmailCode: 'E-mail Code erhalten', - emailCode: 'E-Mail Code', - password: 'Passwort', - confirmPassword: 'Passwort', - agreeTo: 'Zustimmen', - termsAndConditions: 'Nutzungsbedingungen', - confirm: 'bestätigen' - }, } diff --git a/front_end/src/i18n/locales/dev.ts b/front_end/src/i18n/locales/dev.ts index ff9dc712..95a7ea49 100644 --- a/front_end/src/i18n/locales/dev.ts +++ b/front_end/src/i18n/locales/dev.ts @@ -1,6 +1,4 @@ -import { LocaleConfig } from '@/i18n/config' - -export const dev = { +export default { local: 'dev', common: { prop: { diff --git a/front_end/src/i18n/locales/en.ts b/front_end/src/i18n/locales/en.ts index 6506a6ac..82df57ae 100644 --- a/front_end/src/i18n/locales/en.ts +++ b/front_end/src/i18n/locales/en.ts @@ -1,6 +1,4 @@ -import { LocaleConfig } from '@/i18n/config' - -export const en = { +export default { local: 'en', name: 'English', common: { @@ -17,6 +15,7 @@ export const en = { button: { cancel: 'Cancel', confirm: 'Confirm', + send: 'Send', }, filter: 'Filter', hide: 'Hide', @@ -37,19 +36,9 @@ export const en = { actionFail: 'Failed to {0}', actionSuccess: 'Succeed to {0}', agreeTAC: 'Please agree to Terms and Conditions!', - confirmPasswordFail: 'Passwords do not match!', connectionFail: 'Connection Fails!', contactDeveloper: 'Please contact the developers.', - emailCodeSent: 'The email code has been sent. Please check your inbox!', - emptyEmail: 'Please enter your email!', - emptyEmailCode: 'Please enter 6-char email code!', - emptyPassword: 'Please enter your password!', - emptyUsername: 'Please enter your username!', fileTooLarge: 'Maximum file size is {0}', - invalidEmail: 'Invalid email!', - invalidEmailCode: 'Email code is invalid! Please re-sent your email code.', - invalidPassword: 'The length of password should be from 6 to 20!。', - invalidUsername: 'The length of username cannot exceed 20!', logoutFail: 'Failed to log out!', logoutSuccess: 'Log out success!', realNameRequired: 'Real name required', @@ -90,6 +79,7 @@ export const en = { download_link: 'Resource', metasweeper: 'MetaSweeper', metasweeper_int: '', + metasweeper_int2: '', arbiter_int: '', }, state: { @@ -144,16 +134,14 @@ export const en = { links: 'Links', about: 'About', }, - forgetPassword: { - title: 'Reset password', + form: { captcha: 'captcha', - confirm: 'Reset', confirmPassword: 'confirm password', email: 'email', - emailCode: 'one-time password', - getEmailCode: 'Send one-time password', - password: 'new password', - success: 'Password reset complete!' + emailCode: 'Email code', + imageCaptcha: 'Image captcha', + password: 'Password', + username: 'Username', }, guide: { announcement: 'Announcements', @@ -176,13 +164,16 @@ export const en = { notFound: 'You do not have any video of the identifier', }, login: { - title: 'Login', - username: 'username', - password: 'password', - captcha: 'captcha', + agreeTAC1: 'Agree to', + agreeTAC2: 'Terms & Conditions', forgetPassword: 'Forget password?', keepMeLoggedIn: 'Keep me logged in', - confirm: 'Log in' + loginConfirm: 'Log in', + loginTitle: 'Login', + registerConfirm: 'Register', + registerTitle: 'Register', + retrieveConfirm: 'Update password', + retrieveTitle: 'Account Recovery', }, menu: { ranking: 'Ranking', @@ -197,6 +188,39 @@ export const en = { register: 'Register', setting: 'Settings', }, + msg: { + actionFail: 'Action failed', + actionSuccess: 'Action success', + captchaFail: 'Invalid captcha. Please input again', + captchaRefresh: 'Please input again', + captchaRequired: 'Captcha required', + confirmPasswordMismatch: 'Mismatches password', + connectionFail: 'Connection failed. Please try again', + emailCodeInvalid: 'Email code is invalid or expired', + emailCodeRequired: 'Email code required', + emailCollision: 'Email already exists', + emailInvalid: 'Invalid email address', + emailNoCollision: 'Email does not exist', + emailRequired: 'Email required', + emailSendFailMsg: 'Please re-enter captcha. If this happens again, please contact the developers.', + emailSendFailTitle: 'Failed to send email', + emailSendSuccessMsg: 'Please check your email inbox', + emailSendSuccessTitle: 'Email is sent', + fail: 'Failed: ', + illegalCharacter: 'Illegal character', + passwordChanged: 'Password updated', + passwordMinimum: 'Password requires at least 6 characters', + passwordRequired: 'Password required', + pleaseWait: 'Please wait', + pleaseSeeEmail: 'Please check your email', + registerSuccess: 'Successfully registered!', + success: 'Success: ', + unknownError: 'An unexpected error occurred. Please contact the developers. {0}', + usernameCollision: 'Username already exists', + usernameInvalid: 'Username must contain visible characters', + usernamePasswordInvalid: 'Invalid username or password', + usernameRequired: 'Username required', + }, news: { breakRecordTo: ' breaks their {mode} {level} {stat} record with ', }, @@ -247,19 +271,6 @@ export const en = { } } }, - register: { - title: 'Register', - username: 'username', - email: 'email', - captcha: 'captcha', - getEmailCode: 'Send email code', - emailCode: 'email code', - password: 'password', - confirmPassword: 'confirm password', - agreeTo: 'Agree to ', - termsAndConditions: 'Terms & Conditions', - confirm: 'Register', - }, setting: { appearance: 'Appearance', colorscheme: { diff --git a/front_end/src/i18n/locales/pl.ts b/front_end/src/i18n/locales/pl.ts index 909681c6..5e19deff 100644 --- a/front_end/src/i18n/locales/pl.ts +++ b/front_end/src/i18n/locales/pl.ts @@ -1,4 +1,4 @@ -export const pl = { +export default { local: 'pl', name: 'polski', common: { @@ -8,31 +8,30 @@ export const pl = { e: 'ekspert', }, mode: { - standard: 'podstawowy', - noFlag: 'bez flag', - noGuess: 'bez zgadywania', - recursive: 'rekurencyjny akord' + std: 'podstawowy', + nf: 'bez flag', + ng: 'bez zgadywania', + dg: 'rekurencyjny akord' + }, + prop: { + time: 'czas', }, - time: 'czas' }, - forgetPassword: { - title: 'resetuj Hasło', - email: 'email', + form: { captcha: 'captcha', - getEmailCode: 'wyślij hasło pojedyńczego użytku', - emailCode: 'hasło pojedyńczego użytku', - password: 'nowe hasło', confirmPassword: 'potwierdź hasło', - confirm: 'Potwierdź' + email: 'email', + emailCode: 'hasło pojedyńczego użytku', + password: 'hasło', + username: 'nazwa użytkownika', }, login: { - title: 'login', - username: 'nazwa użytkownika', - password: 'hasło', - captcha: 'captcha', forgetPassword: 'zapomniałeś hasła?', keepMeLoggedIn: 'utrzym', - confirm: 'zaloguj' + loginConfirm: 'zaloguj', + loginTitle: 'login', + retrieveConfirm: 'Potwierdź', + retrieveTitle: 'resetuj Hasło', }, menu: { ranking: 'ranking', diff --git a/front_end/src/i18n/locales/zh-cn.ts b/front_end/src/i18n/locales/zh-cn.ts index fa5a8c9e..531e6472 100644 --- a/front_end/src/i18n/locales/zh-cn.ts +++ b/front_end/src/i18n/locales/zh-cn.ts @@ -1,6 +1,4 @@ -import { LocaleConfig } from '@/i18n/config' - -export const zhCn = { +export default { local: 'zh-cn', name: '简体中文', common: { @@ -17,6 +15,7 @@ export const zhCn = { button: { cancel: '取消', confirm: '确认', + send: '发送', }, filter: '筛选', hide: '隐藏', @@ -37,19 +36,9 @@ export const zhCn = { actionFail: '{0}失败!', actionSuccess: '{0}成功', agreeTAC: '请同意用户协议!', - confirmPasswordFail: '两次输入的密码不一致!', connectionFail: '无法连接到服务器!', contactDeveloper: '请联系开发者', - emailCodeSent: '获取验证码成功,请至邮箱查看!', - emptyEmail: '请输入邮箱!', - emptyEmailCode: '请输入6位邮箱验证码!', - emptyPassword: '请输入密码!', - emptyUsername: '请输入用户名!', fileTooLarge: '文件大小不能超过{0}', - invalidEmail: '邮箱格式不正确!', - invalidEmailCode: '邮箱验证码格式不正确!请点击邮箱验证码并打开邮箱查收。', - invalidPassword: '密码格式不正确!长度应该为6-20位。', - invalidUsername: '用户名格式不正确!长度不超过20位。', logoutFail: '退出失败!', logoutSuccess: '退出成功!', realNameRequired: '请修改为实名', @@ -145,16 +134,14 @@ export const zhCn = { links: '友链', about: '关于我们', }, - forgetPassword: { - title: '找回密码', + form: { captcha: '验证码', - confirm: '确认修改密码', - confirmPassword: '请输入确认密码', - email: '请输入邮箱', - emailCode: '请输入邮箱验证码', - getEmailCode: '获取邮箱验证码', - password: '请输入新的6-20位密码', - success: '修改密码成功!', + confirmPassword: '确认密码', + email: '邮箱', + emailCode: '邮箱验证码', + imageCaptcha: '图形验证码', + password: '密码', + username: '用户名', }, guide: { announcement: '公告', @@ -177,13 +164,16 @@ export const zhCn = { notFound: '你没有该标识的录像', }, login: { - title: '欢迎登录', - username: '用户名', - password: '密码', - captcha: '验证码', - forgetPassword: '(找回密码)', + agreeTAC1: '已阅读并同意', + agreeTAC2: '开源扫雷网用户协议', + forgetPassword: '忘记密码', keepMeLoggedIn: '记住我', - confirm: '登录' + loginConfirm: '登录', + loginTitle: '用户登录', + registerConfirm: '注册', + registerTitle: '用户注册', + retrieveConfirm: '确认', + retrieveTitle: '修改密码', }, menu: { ranking: '排行榜', @@ -198,6 +188,39 @@ export const zhCn = { register: '注册', setting: '设置', }, + msg: { + actionFail: '操作失败', + actionSuccess: '操作成功', + captchaFail: '验证码不正确,请重新输入', + captchaRefresh: '请重新输入', + captchaRequired: '请输入图形验证码', + confirmPasswordMismatch: '密码和确认密码不一致', + connectionFail: '无法连接到服务器,请重试', + emailCodeInvalid: '邮箱验证码过期或不正确', + emailCodeRequired: '请输入邮箱验证码', + emailCollision: '邮箱已存在', + emailInvalid: '邮箱格式错误', + emailNoCollision: '邮箱未注册', + emailRequired: '请输入邮箱', + emailSendFailMsg: '请重新输入图形验证码并尝试。如果该情况反复发生,请联系开发者。', + emailSendFailTitle: '邮件发送失败', + emailSendSuccessMsg: '请至邮箱查看', + emailSendSuccessTitle: '邮件发送成功', + fail: '失败:', + illegalCharacter: '非法字符', + passwordChanged: '修改密码成功', + passwordMinimum: '密码至少6位', + passwordRequired: '请输入密码', + pleaseWait: '请稍候', + pleaseSeeEmail: '请查看邮箱', + registerSuccess: '注册成功!', + success: '成功:', + unknownError: '发生未知错误,请联系开发者。', + usernameCollision: '用户名已存在', + usernameInvalid: '用户名必须包含至少一个可见字符', + usernamePasswordInvalid: '用户名或密码不正确', + usernameRequired: '请输入用户名', + }, news: { breakRecordTo: '将{mode}{level}{stat}纪录刷新为', }, @@ -248,19 +271,6 @@ export const zhCn = { } } }, - register: { - title: '用户注册', - username: '请输入用户昵称(唯一、登录凭证、无法修改)', - email: '请输入邮箱(唯一)', - captcha: '验证码', - getEmailCode: '获取邮箱验证码', - emailCode: '请输入邮箱验证码', - password: '请输入6-20位密码', - confirmPassword: '请输入确认密码', - agreeTo: '已阅读并同意', - termsAndConditions: '开源扫雷网用户协议', - confirm: '注册', - }, setting: { appearance: '外观设置', colorscheme: { diff --git a/front_end/src/main.ts b/front_end/src/main.ts index 93d92188..f75605a9 100644 --- a/front_end/src/main.ts +++ b/front_end/src/main.ts @@ -3,9 +3,6 @@ import * as ELIcons from '@element-plus/icons-vue'; import App from './App.vue' import router from './router' -// import store from './store' -import { createPinia } from 'pinia' -import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' import i18n from '@/i18n' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' @@ -15,11 +12,10 @@ import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; import 'highlight.js/styles/stackoverflow-light.css' import 'element-plus/theme-chalk/dark/css-vars.css' +import { pinia } from './store/create'; const app = createApp(App) -const pinia = createPinia() -pinia.use(piniaPluginPersistedstate) app.config.globalProperties.$axios = $axios; diff --git a/front_end/src/store/create.ts b/front_end/src/store/create.ts new file mode 100644 index 00000000..7153b261 --- /dev/null +++ b/front_end/src/store/create.ts @@ -0,0 +1,4 @@ +import { createPinia } from 'pinia' +import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' + +export const pinia = createPinia().use(piniaPluginPersistedstate); \ No newline at end of file diff --git a/front_end/src/store/index.ts b/front_end/src/store/index.ts index 15e37fd9..91183b99 100644 --- a/front_end/src/store/index.ts +++ b/front_end/src/store/index.ts @@ -1,7 +1,8 @@ import { LoginStatus } from "@/utils/common/structInterface" import { defineStore } from 'pinia' +import { pinia } from "./create" -export const useUserStore = defineStore('user', { +export const store = defineStore('user', { state: () => ({ user: { // 此id为用户的id从1开始,而不是数据库的自增id @@ -27,16 +28,16 @@ export const useUserStore = defineStore('user', { new_identifier: false, // 是否有新标识录像 } ), -}) +})(pinia) -export const useVideoPlayerStore = defineStore('videoplayer', { +export const videoplayerstore = defineStore('videoplayer', { state: () => ({ visible: false, id: 0, }), -}) +})(pinia) -export const useLocalStore = defineStore('local', { +export const local = defineStore('local', { state: () => ({ darkmode: false, language: (navigator.language).toLocaleLowerCase(), @@ -48,9 +49,9 @@ export const useLocalStore = defineStore('local', { tooltip_show: true, }), persist: true, -}) +})(pinia) -export const useVideoFilter = defineStore('videofilter', { +export const videofilter = defineStore('videofilter', { state: () => ({ pagesize: 100, level: 'e', @@ -62,4 +63,4 @@ export const useVideoFilter = defineStore('videofilter', { } }), persist: true -}) \ No newline at end of file +})(pinia) \ No newline at end of file diff --git a/front_end/src/utils/common/PlayerDialog.ts b/front_end/src/utils/common/PlayerDialog.ts index ed32b8b1..78a0970f 100644 --- a/front_end/src/utils/common/PlayerDialog.ts +++ b/front_end/src/utils/common/PlayerDialog.ts @@ -1,6 +1,4 @@ -import { useVideoPlayerStore } from "@/store"; - -const videoplayerstore = useVideoPlayerStore(); +import { videoplayerstore } from "@/store"; export const preview = (id: number | undefined) => { if (!id) { diff --git a/front_end/src/utils/common/elFormValidate.ts b/front_end/src/utils/common/elFormValidate.ts new file mode 100644 index 00000000..d31051be --- /dev/null +++ b/front_end/src/utils/common/elFormValidate.ts @@ -0,0 +1,9 @@ + +export function validateSuccess(elFormItemRef: any) { + elFormItemRef.value!.validateMessage=''; + elFormItemRef.value!.validateState='success'; +} +export function validateError(elFormItemRef: any, msg: string) { + elFormItemRef.value!.validateMessage=msg; + elFormItemRef.value!.validateState='error'; +} \ No newline at end of file diff --git a/front_end/src/utils/strings.ts b/front_end/src/utils/strings.ts new file mode 100644 index 00000000..2288503e --- /dev/null +++ b/front_end/src/utils/strings.ts @@ -0,0 +1,3 @@ +// \u2028: Line Separator +// \u2029: Paragraph Separator +export const containsControl = /[\x00-\x1F\x7F-\x9F\u2028\u2029]/; diff --git a/front_end/src/utils/system/status.ts b/front_end/src/utils/system/status.ts deleted file mode 100644 index a7a52457..00000000 --- a/front_end/src/utils/system/status.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { ElNotification } from "element-plus" - -const notificationType = ['', '', 'success', '', 'error', 'error']; -const notificationTitle = ['', '', 'common.msg.actionSuccess', '', 'common.msg.actionFail', 'common.msg.actionFail']; -const notificationMessage: { [code: number]: string} = { - 200: 'common.response.OK', - 400: 'common.response.BadRequest', - 403: 'common.response.Forbidden', - 404: 'common.response.NotFound', - 413: 'common.response.PayloadTooLarge', - 415: 'common.response.UnsupportedMediaType', - 429: 'common.response.TooManyRequests', - 500: 'common.response.InternalServerError', -}; - -export function generalNotification(t: any, status: number, action: string) { - let type = Math.floor(status / 100); - ElNotification({ - title: t.t(notificationTitle[type], [action]), - message: t.t(notificationMessage[status]), - type: notificationType[type], - duration: JSON.parse(localStorage.getItem('local')).notification_duration, - }) -} - -export function unknownErrorNotification(t: any) { - ElNotification({ - title: t.t('common.msg.unknownError'), - message: t.t('common.msg.contactDeveloper'), - type: 'error', - duration: JSON.parse(localStorage.getItem('local')).notification_duration, - }) -} diff --git a/front_end/src/views/HomeView.vue b/front_end/src/views/HomeView.vue index b8c95fdc..6adbb888 100644 --- a/front_end/src/views/HomeView.vue +++ b/front_end/src/views/HomeView.vue @@ -115,10 +115,7 @@ import FriendlyLink from "@/components/dialogs/FriendlyLinks.vue"; import Downloads from "@/components/dialogs/Downloads.vue"; import Thanks from "@/components/dialogs/Thanks.vue"; import Groups from "@/components/dialogs/Groups.vue"; - - -import { useUserStore } from '../store' -const store = useUserStore() +import { store } from '../store' import { useI18n } from 'vue-i18n'; const t = useI18n(); diff --git a/front_end/src/views/PlayerRecordView.vue b/front_end/src/views/PlayerRecordView.vue index ccf36e96..ba630cfd 100644 --- a/front_end/src/views/PlayerRecordView.vue +++ b/front_end/src/views/PlayerRecordView.vue @@ -47,19 +47,14 @@ \ No newline at end of file diff --git a/front_end/src/views/StaffView.vue b/front_end/src/views/StaffView.vue index 6ef30909..f077de6c 100644 --- a/front_end/src/views/StaffView.vue +++ b/front_end/src/views/StaffView.vue @@ -47,13 +47,10 @@ // 管理员操作接口,通过'/staff'访问 import useCurrentInstance from '@/utils/common/useCurrentInstance'; -import { generalNotification } from '@/utils/system/status'; import { ref } from 'vue'; - -import { useI18n } from 'vue-i18n'; import { preview } from '@/utils/common/PlayerDialog'; import StaffAccountLink from '@/components/staff/StaffAccountLink.vue'; -const t = useI18n(); +import { httpErrorNotification, successNotification } from '@/components/Notifications'; const { proxy } = useCurrentInstance(); @@ -68,18 +65,16 @@ const getUser = () => { function (response: any) { userprofile.value = response.data; } - ).catch(error => { - generalNotification(t, error.response.status, t.t('common.action.getUserProfile')) - }) + ).catch(httpErrorNotification) } const setUser = (id: number, field: string, value: string) => { proxy.$axios.post('userprofile/set/', {id: id, field: field, value: value}).then( function (response: any) { - generalNotification(t, response.status, t.t('common.action.setUserProfile')); + successNotification(response); getUser(); } - ) + ).catch(httpErrorNotification) } const videoid = ref(0); @@ -93,18 +88,16 @@ const getVideo = () => { function (response: any) { videomodel.value = response.data; } - ).catch(error => { - generalNotification(t, error.response.status, t.t('common.action.getUserProfile')) - }) + ).catch(httpErrorNotification) } const setVideo = (id: number, field: string, value: string) => { proxy.$axios.post('video/set/', {id: id, field: field, value: value}).then( function (response: any) { - generalNotification(t, response.status, t.t('common.action.setUserProfile')); + successNotification(response); getVideo(); } - ) + ).catch(httpErrorNotification) } \ No newline at end of file diff --git a/front_end/src/views/UploadView.vue b/front_end/src/views/UploadView.vue index 09d9df88..ee750684 100644 --- a/front_end/src/views/UploadView.vue +++ b/front_end/src/views/UploadView.vue @@ -71,8 +71,7 @@ import useCurrentInstance from "@/utils/common/useCurrentInstance"; const { proxy } = useCurrentInstance(); import type { UploadInstance, UploadProps, UploadUserFile, UploadRawFile, UploadFile, UploadFiles, UploadRequestOptions } from 'element-plus' // import img_arbiter from '@/assets/img/img_arbiter.png' -import { useUserStore } from '../store' -const store = useUserStore() +import { store } from '../store' import { ms_to_s, to_fixed_n } from "@/utils" import { useI18n } from 'vue-i18n'; diff --git a/front_end/src/views/VideoView.vue b/front_end/src/views/VideoView.vue index 3f9018a9..0200c014 100644 --- a/front_end/src/views/VideoView.vue +++ b/front_end/src/views/VideoView.vue @@ -64,11 +64,10 @@ import { ms_to_s } from "@/utils"; import { preview } from '@/utils/common/PlayerDialog'; import { useI18n } from 'vue-i18n'; -import { generalNotification } from '@/utils/system/status'; const t = useI18n(); -import { useVideoFilter } from '@/store'; -const videofilter = useVideoFilter(); +import { videofilter } from '@/store'; +import { httpErrorNotification } from '@/components/Notifications'; const level_tag_selected = ref("EXPERT"); const mode_tag_selected = ref("STD"); @@ -85,7 +84,6 @@ const state = reactive({ }); // const test = reactive({v: 5}); -const videoData = reactive([]); const videoList = reactive([]); // 带下划线与不带的至少存在一个 interface Video { @@ -260,7 +258,9 @@ const request_videos = () => { params["r"] = state.ReverseOrder; params["ps"] = videofilter.pagesize; params["page"] = state.CurrentPage; + // @ts-expect-error params["bmin"] = videofilter.bbbv_range[level_tags[level_tag_selected.value].key][0]; + // @ts-expect-error params["bmax"] = videofilter.bbbv_range[level_tags[level_tag_selected.value].key][1]; if (![0,4].includes(videofilter.filter_state.length)) { params['s'] = videofilter.filter_state; @@ -274,9 +274,7 @@ const request_videos = () => { videoList.splice(0, videoList.length); videoList.push(...data.videos); state.VideoCount = data.count; - }).catch((error: any) => { - generalNotification(t, error.response.status, t.t('common.action.videoQuery')) - }) + }).catch(httpErrorNotification) } const index_select = (key: string | number, value: NameKeyReverse) => {