feat(后台设置): 设置页重排成网站、班级、开关、判题机几块,保存挪到右上角;班级按年级分组不用再拖;判题机写出多久没动静;AI 小助手(MaxKB 挂件)整个删掉
Deploy / deploy (push) Canceled after 0s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-13 14:37:35 +08:00
1 parent 20a6f667d8
commit 0ee5444ede
10 files changed
+304 -273

No files matched your search

-1
View File
@@ -1,4 +1,3 @@
PUBLIC_ENV=xuyue.cc
PUBLIC_MAXKB_URL=https://maxkb.xuyue.cc/chat/api/embed?protocol=https&host=maxkb.xuyue.cc&token=dd37457027c40b39
PUBLIC_CODE_URL=https://code.xuyue.cc
PUBLIC_ICONIFY_URL=https://icon.xuyue.cc
-1
View File
@@ -1,4 +1,3 @@
PUBLIC_ENV=school
PUBLIC_MAXKB_URL=http://10.13.114.114:92/chat/api/embed?protocol=http&host=10.13.114.114:92&token=dd37457027c40b39
PUBLIC_CODE_URL=http://10.13.114.114:82
PUBLIC_ICONIFY_URL=http://10.13.114.114:8098
-1
View File
@@ -1,4 +1,3 @@
PUBLIC_ENV=test
PUBLIC_MAXKB_URL=http://10.13.114.114:92/chat/api/embed?protocol=http&host=10.13.114.114:92&token=dd37457027c40b39
PUBLIC_CODE_URL=http://10.13.114.114:82
PUBLIC_ICONIFY_URL=http://10.13.114.114:8098
+1 -2
View File
@@ -59,7 +59,7 @@ API 调用不按模块分:学生端全在 `oj/api.ts`、后台全在 `admin/ap
Shared logic lives in `shared/`:
- `store/` — Pinia stores: `user` (auth/roles), `config` (site-wide settings), `authModal` (login/signup form state), `collab` (help-request queue + collab room), `achievement` (解锁弹窗队列), `myFlowchart` (题面「你画的流程图」那张 A/S 图的 mermaid 源码)
- `composables/` — `aiStream`, `pagination` (URL-synced), `websocket` (reconnect + heartbeat), `collabDoc` (Yjs binding for the collab channel), `configUpdate` (WS-pushed config sync), `useMermaid` (lazy Mermaid render), `darkTransition` (View Transitions,111 以下走降级分支), `leaveStudents` (课堂看板的「请假」), `chartTheme`, `breakpoints`, `maxkb`, `rarity`
- `composables/` — `aiStream`, `pagination` (URL-synced), `websocket` (reconnect + heartbeat), `collabDoc` (Yjs binding for the collab channel), `configUpdate` (WS-pushed config sync), `useMermaid` (lazy Mermaid render), `darkTransition` (View Transitions,111 以下走降级分支), `leaveStudents` (课堂看板的「请假」), `chartTheme`, `breakpoints`, `rarity`
- `layout/` — `default.vue` and `admin.vue` layout wrappers
- `api.ts` — shared API calls (auth, profile, tags, captcha)
@@ -168,7 +168,6 @@ Env files: `.env`(本机)、`.env.production`(服务器)、`.env.staging
|---|---|
| `PUBLIC_ENV` | 环境角标:`test` → 「测试版」,`dev` → 「开发版」,其余不显示 |
| `PUBLIC_CODE_URL` | 代码分享服务(提交详情、题目页的「分享」) |
| `PUBLIC_MAXKB_URL` | 知识库问答挂件 |
| `PUBLIC_ICONIFY_URL` | 自建 Iconify 图标源,不设则走公共 CDN |
后端地址**不在这里**:`utils/api.ts` 写死 `baseURL: "/api"`,dev 由 `vite.config.ts` 的
-3
View File
@@ -6,9 +6,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>判题狗</title>
<link rel="stylesheet" href="/style.css" />
<script>
window.localStorage.setItem("maxkbMaskTip", true)
</script>
</head>
<body>
<div id="app"></div>
+2 -5
View File
@@ -4,7 +4,6 @@ import "normalize.css"
import "./index.css"
import { useConfigStore } from "shared/store/config"
import { useConfigUpdate } from "shared/composables/configUpdate"
import { useMaxKB } from "shared/composables/maxkb"
import { useUserStore } from "shared/store/user"
import { useCollabStore } from "shared/store/collab"
import CollabHost from "shared/components/CollabHost.vue"
@@ -20,11 +19,9 @@ onMounted(() => {
userStore.getMyProfile()
})
// 配置实时推送 + MaxKB 挂件。阶段 2 时因为后端还没有 /ws/config 而暂时关掉,
// 阶段 3 迁完之后一直没接回来 —— 表现是管理员改站点配置后学生要刷新才生效、
// 知识库挂件根本不出现,且没有任何报错。两者现在都走新后端的 /ws/config。
// 配置实时推送。阶段 2 时因为后端还没有 /ws/config 而暂时关掉,阶段 3 迁完之后一直
// 没接回来 —— 表现是管理员改站点配置后学生要刷新才生效,且没有任何报错。现在走新后端的 /ws/config。
useConfigUpdate()
useMaxKB()
// 课堂求助通道。和 /ws/config 一样是全局常驻的:老师可能正在后台改题时
// 收到求助,学生也要在排队期间一直挂着,所以不放在题目页里起落。
+301 -242
View File
@@ -1,9 +1,10 @@
<script setup lang="ts">
import PageHeader from "admin/components/PageHeader.vue"
import { useThemeVars } from "naive-ui"
import { classLabel } from "oj/submission/utils"
import { errorMessage } from "utils/api"
import { NButton, NTag } from "naive-ui"
import { CLASS_NAME_MAX_DIGITS, CLASS_NAME_MIN_DIGITS, CLASS_NAME_RE } from "utils/constants"
import { parseTime } from "utils/functions"
import { copyToClipboard, parseTime } from "utils/functions"
import type { OrphanTestCase, Server, WebsiteConfig } from "utils/types"
import { useConfigStore } from "shared/store/config"
import {
@@ -14,183 +15,36 @@ import {
listInvalidTestcases,
pruneInvalidTestcases,
} from "../api"
import { useUserStore } from "shared/store/user"
/**
* 后台设置(设计稿「后台首页」页的 ConfAdmin):网站 / 班级 / 开关 / 判题机,有无效测试用例时
* 再多一块。保存在右上角。
*
* 2026-10-13 用户定的:网址、简称、页脚三项删了(前后端从来没读过),AI 小助手连同挂件一起删了;
* 注册、「学生能看到所有人的提交」两个开关留着。班级不再拖动排序 —— 全站的班级下拉早就按年级
* 自动分组排序了,拖出来的顺序没有地方用上
*/
const message = useMessage()
const theme = useThemeVars()
const configStore = useConfigStore()
const userStore = useUserStore()
// 确保只有登录用户才能使用WebSocket
watch(
() => userStore.isAuthed,
(isAuthed) => {
if (!isAuthed) {
// 如果用户未登录,禁用WebSocket功能
console.warn("用户未登录,WebSocket配置更新功能已禁用")
}
},
{ immediate: true },
)
const testcaseColumns: DataTableColumn<OrphanTestCase>[] = [
{ title: "测试用例 ID", key: "id" },
{
title: "选项",
key: "delete",
render: (row) =>
h(NButton, { size: "small", onClick: () => deleteTestcase(row.id) }, () => "删除"),
},
]
const statusMap: {
[key in "normal" | "abnormal"]: { color: "primary" | "error"; label: string }
} = {
normal: { color: "primary", label: "正常" },
abnormal: { color: "error", label: "异常" },
}
const serverColumns: DataTableColumn<Server>[] = [
{
title: "状态",
key: "status",
width: 80,
render: (row) =>
h(
NTag,
{ type: statusMap[row.status].color, size: "small" },
() => statusMap[row.status].label,
),
},
{
title: "选项",
key: "options",
width: 80,
render: (row) =>
h(
NButton,
{
type: "primary",
size: "small",
disabled: row.status === "normal",
onClick: () => delJudgeServer(row.hostname),
},
() => "删除",
),
},
{ title: "主机", key: "hostname", width: 140 },
{
title: "内存占用",
key: "memory_usage",
render: (row) => row.memoryUsage + "%",
width: 100,
},
{ title: "IP", key: "ip", width: 140 },
{ title: "判题机版本", key: "judgerVersion", width: 100 },
{ title: "服务器 URL", key: "serviceUrl", width: 200 },
{
title: "上一次心跳",
key: "lastHeartbeat",
render: (row) => parseTime(row.lastHeartbeat, "YYYY-MM-DD HH:mm:ss"),
width: 120,
},
{
title: "创建时间",
key: "createTime",
render: (row) => parseTime(row.createTime, "YYYY-MM-DD HH:mm:ss"),
width: 120,
},
]
const testcases = ref<OrphanTestCase[]>([])
const token = ref("")
const servers = ref<Server[]>([])
const abnormalServers = computed(() => servers.value.filter((item) => item.status === "abnormal"))
const websiteConfig = reactive<WebsiteConfig>({
websiteBaseUrl: "",
websiteName: "判题狗",
websiteNameShortcut: "判题狗",
websiteFooter: "所有权归属于徐越,感谢青岛大学开源 OJ 系统,感谢开源社区",
allowRegister: true,
allowRegister: false,
submissionListShowAll: true,
classList: [],
enableMaxkb: true,
})
// 班级列表的顺序就是登录页、班级 PK、统计面板下拉框里的顺序,数组原样存进 options 表,
// 所以拖拽只改前端数组,点「保存」生效
const dragFrom = ref<number | null>(null)
const dragOver = ref<number | null>(null)
function moveClass(from: number, to: number) {
const list = [...websiteConfig.classList]
const [moved] = list.splice(from, 1)
list.splice(to, 0, moved)
websiteConfig.classList = list
}
function renderClassTag(tag: string | { label: string }, index: number) {
const label = typeof tag === "string" ? tag : tag.label
return h(
NTag,
{
key: index,
closable: true,
draggable: true,
type: dragOver.value === index ? "primary" : "default",
style: {
cursor: "move",
opacity: dragFrom.value === index ? 0.4 : 1,
},
onClose: () => {
websiteConfig.classList = websiteConfig.classList.filter((_, i) => i !== index)
},
onDragstart: (e: DragEvent) => {
dragFrom.value = index
// Firefox 不 setData 就不会开始拖
e.dataTransfer?.setData("text/plain", label)
if (e.dataTransfer) e.dataTransfer.effectAllowed = "move"
},
onDragover: (e: DragEvent) => {
if (dragFrom.value === null) return
e.preventDefault()
dragOver.value = index
},
onDragleave: () => {
if (dragOver.value === index) dragOver.value = null
},
onDrop: (e: DragEvent) => {
e.preventDefault()
if (dragFrom.value !== null && dragFrom.value !== index) {
moveClass(dragFrom.value, index)
}
dragFrom.value = null
dragOver.value = null
},
onDragend: () => {
dragFrom.value = null
dragOver.value = null
},
},
() => label,
)
}
async function getWebsiteConfig() {
const res = await getWebsite()
websiteConfig.websiteBaseUrl = res.websiteBaseUrl
websiteConfig.websiteName = res.websiteName
websiteConfig.websiteNameShortcut = res.websiteNameShortcut
websiteConfig.websiteFooter = res.websiteFooter
websiteConfig.allowRegister = res.allowRegister
websiteConfig.submissionListShowAll = res.submissionListShowAll
websiteConfig.classList = res.classList
websiteConfig.enableMaxkb = res.enableMaxkb
}
async function saveWebsiteConfig() {
// 班级号要和用户名里 ks 后面那段对得上,位数不对登录页会查不到该班学生。
// 后端 CreateEditWebsiteConfigSerializer 也会拦,这里先报更明确的错
// 班级号要和用户名里 ks 后面那段对得上,位数不对登录页会查不到该班学生。后端也会拦
const invalid = websiteConfig.classList.filter((c) => !CLASS_NAME_RE.test(c))
if (invalid.length) {
message.error(
@@ -201,48 +55,104 @@ async function saveWebsiteConfig() {
try {
await editWebsite(websiteConfig)
} catch (err) {
message.error("保存失败:" + errorMessage(err))
message.error("没保存上:" + errorMessage(err))
return
}
message.success("网站配置保存成功")
message.success("保存好了")
getWebsiteConfig()
// 广播由后端在 POST /admin/website 里做,开着页面的人不用刷新
configStore.getConfig()
// 广播由后端在 POST /admin/website 里做,八个键一个不落。
// 这里原来还从客户端往 /ws/config 推两个键 —— 那条连接从没 connect() 过、
// send() 直接返回 false,而且服务端只认 ping/subscribe,本来就收不下。
}
async function deleteTestcase(id?: string) {
await pruneInvalidTestcases(id)
message.success("删除成功")
getTestcases()
// ---------------------------------------------------------------- 班级
/** 按年级分组,新年级在上,组里按班号排(和全站班级下拉一样) */
const classGroups = computed(() => {
const groups = new Map<string, string[]>()
for (const name of websiteConfig.classList) {
const grade = /^\d{3,}$/.test(name) ? name.slice(0, 2) : "其他"
groups.set(grade, [...(groups.get(grade) ?? []), name])
}
return [...groups.entries()]
.sort(([a], [b]) => b.localeCompare(a))
.map(([grade, list]) => ({
grade,
list: [...list].sort((a, b) => a.localeCompare(b, "zh-CN", { numeric: true })),
}))
})
const newClass = ref("")
function addClass() {
const value = newClass.value.trim()
if (!value) return
if (!CLASS_NAME_RE.test(value)) {
message.error(`班级号要是 ${CLASS_NAME_MIN_DIGITS}~${CLASS_NAME_MAX_DIGITS} 位数字,如 2613`)
return
}
if (websiteConfig.classList.includes(value)) {
message.info(`${classLabel(value)} 已经在里面了`)
return
}
websiteConfig.classList = [...websiteConfig.classList, value]
newClass.value = ""
}
async function getTestcases() {
const res = await listInvalidTestcases()
testcases.value = res
function removeClass(name: string) {
websiteConfig.classList = websiteConfig.classList.filter((c) => c !== name)
}
// ---------------------------------------------------------------- 判题机
const token = ref("")
const tokenShown = ref(false)
const servers = ref<Server[]>([])
const abnormalServers = computed(() => servers.value.filter((item) => item.status === "abnormal"))
async function getJudgeServerData() {
const res = await getJudgeServer()
token.value = res.token
servers.value = res.servers
}
/** 上一次心跳:一分钟内写「刚刚还在」,否则「N 分钟 / 小时 / 天没动静了」 */
function heartbeatText(server: Server) {
const seconds = Math.max(0, (Date.now() - Date.parse(server.lastHeartbeat)) / 1000)
if (seconds < 60) return "刚刚还在"
if (seconds < 3600) return `${Math.floor(seconds / 60)} 分钟没动静了`
if (seconds < 86400) return `${Math.floor(seconds / 3600)} 小时没动静了`
return `${Math.floor(seconds / 86400)} 天没动静了`
}
async function delJudgeServer(hostname: string) {
await deleteJudgeServer(hostname)
message.success("删除成功")
// 不刷新的话删掉的那行还挂在表里,得手动刷新页面才消失(批量删除那支一直是刷的)
message.success("删掉了")
getJudgeServerData()
}
async function deleteAbnormalServers() {
const dels = abnormalServers.value.map((item) => deleteJudgeServer(item.hostname))
await Promise.all(dels)
message.success("删除成功")
await Promise.all(abnormalServers.value.map((item) => deleteJudgeServer(item.hostname)))
message.success("删掉了")
getJudgeServerData()
}
async function copyToken() {
if (await copyToClipboard(token.value)) message.success("复制好了")
}
// ---------------------------------------------------------------- 无效的测试用例
const testcases = ref<OrphanTestCase[]>([])
async function getTestcases() {
testcases.value = await listInvalidTestcases()
}
async function deleteTestcase(id?: string) {
await pruneInvalidTestcases(id)
message.success("删掉了")
getTestcases()
}
onMounted(() => {
getWebsiteConfig()
getTestcases()
@@ -251,83 +161,232 @@ onMounted(() => {
</script>
<template>
<PageHeader title="设置" />
<n-card class="box" title="网站">
<template #header-extra>
<n-button type="primary" size="small" @click="saveWebsiteConfig">保存</n-button>
<PageHeader title="设置">
<template #actions>
<n-button type="primary" @click="saveWebsiteConfig">保存</n-button>
</template>
<n-form inline label-placement="left">
<n-form-item label="网站 URL">
<n-input class="url" v-model:value="websiteConfig.websiteBaseUrl" />
</n-form-item>
<n-form-item label="网站名">
<n-input v-model:value="websiteConfig.websiteName" />
</n-form-item>
<n-form-item label="网站简称">
<n-input v-model:value="websiteConfig.websiteNameShortcut" />
</n-form-item>
</n-form>
<n-form label-placement="left">
<n-form-item label="班级列表">
<n-flex vertical size="small">
<n-dynamic-tags v-model:value="websiteConfig.classList" :render-tag="renderClassTag" />
<n-text depth="3" style="font-size: 12px">
填 {{ CLASS_NAME_MIN_DIGITS }}~{{ CLASS_NAME_MAX_DIGITS }}
位数字,如 251、2510,要和用户名里 ks 后面那段一致;
拖动标签调整顺序(登录页等下拉框按此顺序),改完点保存
</n-text>
</n-flex>
</n-form-item>
</n-form>
<n-flex align="center">
<n-flex align="center">
<span>是否允许注册</span>
<n-switch v-model:value="websiteConfig.allowRegister" />
</n-flex>
<n-flex align="center">
<span>显示所有提交</span>
<n-switch v-model:value="websiteConfig.submissionListShowAll" />
</n-flex>
<n-flex align="center">
<span>启用AI小助手</span>
<n-switch v-model:value="websiteConfig.enableMaxkb" />
</n-flex>
</n-flex>
</n-card>
<n-card class="box" title="判题服务器">
<template #header-extra>
</PageHeader>
<section class="block">
<h3>网站</h3>
<div class="field">
<label for="site-name" class="field-label">网站名</label>
<n-input
v-model:value="websiteConfig.websiteName"
:input-props="{ id: 'site-name' }"
style="width: 240px"
/>
<n-text depth="3" class="hint">顶栏左上角和浏览器标签页上显示的名字</n-text>
</div>
</section>
<section class="block">
<h3>
班级 <span class="count">{{ websiteConfig.classList.length }} 个</span>
</h3>
<div v-for="group in classGroups" :key="group.grade" class="grade">
<span class="grade-label">{{ group.grade === "其他" ? "其他" : `${group.grade} 级` }}</span>
<div class="chips">
<n-tag
v-for="name in group.list"
:key="name"
closable
:bordered="false"
@close="removeClass(name)"
>
{{ classLabel(name) }}
</n-tag>
</div>
</div>
<div class="grade">
<span class="grade-label"></span>
<n-input
v-model:value="newClass"
placeholder="加一个班,如 2613"
style="width: 180px"
@keyup.enter="addClass"
/>
<n-button @click="addClass">加</n-button>
<n-text depth="3" class="hint">批量建号时新班级会自动加进来,这里主要是删掉毕业的班</n-text>
</div>
</section>
<section class="block">
<h3>开关</h3>
<label class="toggle">
<n-switch v-model:value="websiteConfig.allowRegister" />
学生可以自己注册账号
</label>
<label class="toggle">
<n-switch v-model:value="websiteConfig.submissionListShowAll" />
学生能看到所有人的提交
</label>
</section>
<section class="block">
<h3>
判题机
<n-button
v-if="abnormalServers.length"
v-if="abnormalServers.length > 1"
size="small"
type="warning"
style="margin-left: 8px"
@click="deleteAbnormalServers"
>
删除无效服务器
把断了的都删掉
</n-button>
</h3>
<n-empty v-if="!servers.length" description="一台都没有连上" />
<div v-for="server in servers" :key="server.hostname" class="server">
<span class="dot" :class="server.status === 'normal' ? 'ok' : 'bad'"></span>
<b>{{ server.hostname }}</b>
<span class="muted">
{{ server.cpuCore }} 核 · CPU {{ server.cpuUsage }}% · 内存 {{ server.memoryUsage }}% · 版本
{{ server.judgerVersion }}
</span>
<span
:class="server.status === 'normal' ? 'ok-text' : 'bad-text'"
:title="parseTime(server.lastHeartbeat, 'YYYY-MM-DD HH:mm:ss')"
>
{{ heartbeatText(server) }}
</span>
<n-button
v-if="server.status !== 'normal'"
size="small"
@click="delJudgeServer(server.hostname)"
>
删掉
</n-button>
</template>
<div class="box">
接口凭证 <n-tag size="small">{{ token }}</n-tag>
</div>
<n-data-table :single-line="false" striped :columns="serverColumns" :data="servers" />
</n-card>
<n-card class="box" v-if="testcases.length" title="无效的测试用例">
<template #header-extra>
<n-button size="small" type="warning" @click="() => deleteTestcase()">全部删除</n-button>
</template>
<n-data-table striped class="table" :columns="testcaseColumns" :data="testcases" />
</n-card>
<div class="server token">
<span class="muted">接口凭证</span>
<span class="mono">{{ tokenShown ? token : "••••••••••••" }}</span>
<n-button size="small" @click="tokenShown = !tokenShown">
{{ tokenShown ? "藏起来" : "显示" }}
</n-button>
<n-button size="small" @click="copyToken">复制</n-button>
</div>
</section>
<section v-if="testcases.length" class="block">
<h3>
没有题目用的测试数据 <span class="count">{{ testcases.length }} 份</span>
<n-button size="small" style="margin-left: 8px" @click="deleteTestcase()">都删掉</n-button>
</h3>
<n-text depth="3" class="hint">删题时测试数据留在了磁盘上(故意的,删错了还能捞回来)</n-text>
<div class="chips">
<n-tag
v-for="item in testcases"
:key="item.id"
closable
:bordered="false"
@close="deleteTestcase(item.id)"
>
{{ item.id }}
</n-tag>
</div>
</section>
</template>
<style scoped>
.url {
width: 200px;
}
.box {
.block {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 16px;
padding: 16px 18px;
border-radius: 10px;
border: 1px solid v-bind("theme.dividerColor");
}
.table {
width: 40%;
h3 {
display: flex;
align-items: center;
margin: 0;
font-size: 16px;
}
.count {
margin-left: 8px;
font-size: 13px;
font-weight: 400;
color: v-bind("theme.textColor3");
}
.field,
.grade,
.server {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px 12px;
}
.field-label {
width: 60px;
font-size: 14px;
color: v-bind("theme.textColor3");
}
.hint {
font-size: 13px;
}
.grade-label {
width: 50px;
flex: none;
font-size: 13px;
color: v-bind("theme.textColor3");
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.toggle {
display: inline-flex;
align-items: center;
gap: 12px;
font-size: 14px;
cursor: pointer;
}
.server {
font-size: 14px;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.dot.ok {
background: v-bind("theme.successColor");
}
.dot.bad {
background: v-bind("theme.errorColor");
}
.ok-text {
color: v-bind("theme.successColor");
}
.bad-text {
color: v-bind("theme.errorColor");
}
.muted {
color: v-bind("theme.textColor3");
}
.token {
margin-top: 4px;
}
.mono {
font-family: Consolas, Monaco, monospace;
}
</style>
-1
View File
@@ -2,7 +2,6 @@
interface ImportMetaEnv {
readonly PUBLIC_ENV: string
readonly PUBLIC_MAXKB_URL: string
readonly PUBLIC_CODE_URL: string
readonly PUBLIC_ICONIFY_URL: string
}
-7
View File
@@ -3,17 +3,10 @@ import type { WebsiteConfig } from "utils/types"
export const useConfigStore = defineStore("config", () => {
const config = ref<WebsiteConfig>({
websiteBaseUrl: "",
websiteName: "",
websiteNameShortcut: "",
websiteFooter: "",
submissionListShowAll: true,
allowRegister: false,
classList: [],
// 默认 false:这是给 useMaxKB 用的开关,而 MaxKB 是第三方脚本。
// 默认 true 的话,getConfig() 还没回来挂件就已经加载执行了,服务端配置说
// 「关」也只能事后删标签,等于开关失效。宁可晚一个来回出现,也不要关不掉。
enableMaxkb: false,
})
async function getConfig() {
const res = await getWebsiteConfig()
-10
View File
@@ -69,16 +69,6 @@ const polyfills = [
"web.url.parse",
]
// MaxKB 脚本**不要**在这里注入 index.html。
//
// 原来有个 inject-maxkb 插件把 <script src> 写进 head,于是构建产物在 Vue 启动之前
// 就把挂件拉下来执行了 —— 后台那个「启用 MaxKB」开关根本拦不住它,关掉也只是
// 事后把标签和 DOM 删掉,代码早跑完了(表现之一:MaxKB 自带的性能上报在每个页面
// 抛 `Cannot read properties of undefined (reading 'startTime')`,且关不掉)。
//
// 现在只走运行时那一条路:App.vue 的 useMaxKB() 等站点配置回来,
// enableMaxkb 为真才建 script 标签。
export default defineConfig(() => {
// 开发时一律指向本机后端(apps/api,3000)。
// **必须写 IP,不能写 localhost**:api 用 Bun.serve 起,只绑 IPv4 的