perf: 首屏 JS 减少 2/3、随机评分查询提速,WebSocket 防刷和登录限流
Deploy / deploy (build, debian, 22) (push) Canceled after 0s
Deploy / deploy (build, debian, 22) (push) Canceled after 0s
- prettier 点“整理”时才加载,编辑器页面按路由懒加载:首屏 JS 2345 KB → 766 KB - 随机评分先随机抽 64 个 rowid 挑选,抽不中再退回全表随机:6 万条提交时 50ms → 1ms - WebSocket 消息最大 64 KB;AI 回复中再发来的消息直接拒绝,不再排队 - 同一用户名 10 分钟失败 10 次、同一 IP 失败 100 次后暂停登录(返回 429,登录框显示提示) - 作品展的 iframe 加 loading="lazy" - 新增 ws.test.ts(正常生成、回复中再发消息、中途断开、握手失败)和随机评分、登录限流的测试 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bd84e3f8d1
commit
325474bdb7
10 files changed
+269
-37
No files matched your search
@@ -48,9 +48,9 @@
|
||||
/>
|
||||
<n-alert
|
||||
type="error"
|
||||
v-if="showStudentError"
|
||||
v-if="studentError"
|
||||
class="message"
|
||||
title="登录失败,请检查密码"
|
||||
:title="studentError"
|
||||
/>
|
||||
<n-button
|
||||
block
|
||||
@@ -79,9 +79,9 @@
|
||||
</n-form-item>
|
||||
<n-alert
|
||||
type="error"
|
||||
v-if="showAdminError"
|
||||
v-if="adminError"
|
||||
class="message"
|
||||
title="登录失败,请检查用户名和密码"
|
||||
:title="adminError"
|
||||
/>
|
||||
<n-button
|
||||
block
|
||||
@@ -113,7 +113,7 @@ const selectedClass = ref<string | null>(null)
|
||||
const selectedUsername = ref<string | null>(null)
|
||||
const studentPassword = ref("")
|
||||
const studentLoading = ref(false)
|
||||
const showStudentError = ref(false)
|
||||
const studentError = ref("")
|
||||
|
||||
// Classes data
|
||||
const classes = ref<string[]>([])
|
||||
@@ -135,7 +135,7 @@ const nameOptions = computed(() =>
|
||||
const adminName = ref("")
|
||||
const adminPassword = ref("")
|
||||
const adminLoading = ref(false)
|
||||
const showAdminError = ref(false)
|
||||
const adminError = ref("")
|
||||
|
||||
// Load classes on mount (cached — not reloaded on tab switch)
|
||||
onMounted(async () => {
|
||||
@@ -155,13 +155,13 @@ function onTabChange() {
|
||||
selectedClass.value = null
|
||||
selectedUsername.value = null
|
||||
studentPassword.value = ""
|
||||
showStudentError.value = false
|
||||
studentError.value = ""
|
||||
namesError.value = false
|
||||
names.value = []
|
||||
// Reset admin tab state
|
||||
adminName.value = ""
|
||||
adminPassword.value = ""
|
||||
showAdminError.value = false
|
||||
adminError.value = ""
|
||||
}
|
||||
|
||||
async function onClassChange(classname: string) {
|
||||
@@ -178,10 +178,15 @@ async function onClassChange(classname: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// 失败次数太多被暂时禁止登录时,显示服务端给的提示
|
||||
function loginError(err: any, fallback: string) {
|
||||
return err.response?.status === 429 ? err.response.data.detail : fallback
|
||||
}
|
||||
|
||||
async function submitStudent() {
|
||||
if (!selectedUsername.value || !studentPassword.value) return
|
||||
studentLoading.value = true
|
||||
showStudentError.value = false
|
||||
studentError.value = ""
|
||||
try {
|
||||
const data = await Account.login(
|
||||
selectedUsername.value,
|
||||
@@ -194,8 +199,8 @@ async function submitStudent() {
|
||||
router.replace(
|
||||
window.location.pathname + window.location.search + window.location.hash,
|
||||
)
|
||||
} catch {
|
||||
showStudentError.value = true
|
||||
} catch (err: any) {
|
||||
studentError.value = loginError(err, "登录失败,请检查密码")
|
||||
} finally {
|
||||
studentLoading.value = false
|
||||
}
|
||||
@@ -204,7 +209,7 @@ async function submitStudent() {
|
||||
async function submitAdmin() {
|
||||
if (!adminName.value || !adminPassword.value) return
|
||||
adminLoading.value = true
|
||||
showAdminError.value = false
|
||||
adminError.value = ""
|
||||
try {
|
||||
const data = await Account.login(adminName.value, adminPassword.value)
|
||||
user.username = data.username
|
||||
@@ -214,8 +219,8 @@ async function submitAdmin() {
|
||||
router.replace(
|
||||
window.location.pathname + window.location.search + window.location.hash,
|
||||
)
|
||||
} catch {
|
||||
showAdminError.value = true
|
||||
} catch (err: any) {
|
||||
adminError.value = loginError(err, "登录失败,请检查用户名和密码")
|
||||
} finally {
|
||||
adminLoading.value = false
|
||||
}
|
||||
|
||||
@@ -79,11 +79,6 @@
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { Icon } from "@iconify/vue"
|
||||
import prettier from "prettier/standalone"
|
||||
import * as htmlParser from "prettier/parser-html"
|
||||
import * as cssParser from "prettier/parser-postcss"
|
||||
import * as babelParser from "prettier/parser-babel"
|
||||
import * as estreeParser from "prettier/plugins/estree"
|
||||
import Editor from "./Editor.vue"
|
||||
import Toolbar from "./Toolbar.vue"
|
||||
import { html, css, js, tab, size, reset } from "../../store/editors"
|
||||
@@ -105,6 +100,15 @@ function changeSize(num: number) {
|
||||
}
|
||||
|
||||
async function formatCode() {
|
||||
// prettier 加上各语言插件近 1MB,只在点“格式化”时才加载
|
||||
const [prettier, htmlParser, cssParser, babelParser, estreeParser] =
|
||||
await Promise.all([
|
||||
import("prettier/standalone"),
|
||||
import("prettier/parser-html"),
|
||||
import("prettier/parser-postcss"),
|
||||
import("prettier/parser-babel"),
|
||||
import("prettier/plugins/estree"),
|
||||
])
|
||||
const [htmlFormatted, cssFormatted, jsFormatted] = await Promise.all([
|
||||
prettier.format(html.value, {
|
||||
parser: "html",
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
<iframe
|
||||
:srcdoc="buildSrcdoc(item)"
|
||||
sandbox="allow-scripts"
|
||||
loading="lazy"
|
||||
scrolling="no"
|
||||
class="preview-iframe"
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createWebHistory, createRouter } from "vue-router"
|
||||
import { loginModal } from "./store/modal"
|
||||
|
||||
import Workspace from "./pages/Workspace.vue"
|
||||
// 编辑器(CodeMirror 等)比较大,用到时才加载;几个路由共用同一个加载函数,切换时不会重新挂载
|
||||
const Workspace = () => import("./pages/Workspace.vue")
|
||||
import { hasLogin } from "./token"
|
||||
import { authed } from "./store/user"
|
||||
|
||||
|
||||
Reference in new issue
Block a user