初步添加ea2
This commit is contained in:
Binary file not shown.
@@ -1073,6 +1073,8 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 确认删除配置
|
||||
*/
|
||||
|
||||
@@ -225,7 +225,7 @@
|
||||
|
||||
<script>
|
||||
import { openDB } from "idb";
|
||||
import axios from "@/axios/axios";
|
||||
import axios from "@/assets/fonts/axios/axios";
|
||||
import { getSetting, setSetting } from "@/utils/settings";
|
||||
|
||||
export default {
|
||||
@@ -237,7 +237,7 @@ export default {
|
||||
migrationType: "server",
|
||||
serverUrl: "",
|
||||
targetStorage: "kv-server",
|
||||
targetServerUrl: "https://kv.wuyuan.dev",
|
||||
targetServerUrl: "https://kv-service.wuyuan.dev",
|
||||
startDate: this.getDateString(
|
||||
new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
|
||||
), // 30 days ago
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
<br/><br/>Classworks KV 的全域管理员是 <a href="https://wuyuan.dev" target="_blank">孙悟元</a></v-card-text
|
||||
><v-card-actions
|
||||
><v-btn
|
||||
href="https://kv.wuyuan.dev"
|
||||
href="https://kv.houlang.cloud"
|
||||
class="text-none"
|
||||
append-icon="mdi-open-in-new"
|
||||
target="_blank"
|
||||
|
||||
+4
-1
@@ -15,6 +15,9 @@ import GlobalMessage from '@/components/GlobalMessage.vue'
|
||||
import { createApp } from 'vue'
|
||||
import Clarity from '@microsoft/clarity';
|
||||
const projectId = "rhp8uqoc3l"
|
||||
import TDesign from 'tdesign-vue-next'
|
||||
import 'tdesign-vue-next/es/style/index.css'
|
||||
import '@examaware-cs/player/dist/player.css'
|
||||
|
||||
Clarity.init(projectId);
|
||||
import messageService from './utils/message';
|
||||
@@ -22,7 +25,7 @@ import messageService from './utils/message';
|
||||
const app = createApp(App)
|
||||
|
||||
registerPlugins(app)
|
||||
|
||||
app.use(TDesign)
|
||||
app.use(messageService);
|
||||
|
||||
app.component('GlobalMessage', GlobalMessage)
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<v-container class="pa-0">
|
||||
<v-app-bar elevation="1">
|
||||
<template #prepend>
|
||||
<v-btn
|
||||
icon="mdi-arrow-left"
|
||||
variant="text"
|
||||
@click="$router.back()"
|
||||
/>
|
||||
</template>
|
||||
<v-app-bar-title class="text-h6">
|
||||
编辑考试配置
|
||||
</v-app-bar-title>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="success"
|
||||
variant="outlined"
|
||||
prepend-icon="mdi-content-save"
|
||||
:loading="saving"
|
||||
@click="save"
|
||||
>
|
||||
保存
|
||||
</v-btn>
|
||||
</v-app-bar>
|
||||
|
||||
<v-container
|
||||
class="py-4"
|
||||
style="max-width: 1200px"
|
||||
>
|
||||
<ExamConfigEditor
|
||||
v-if="id"
|
||||
ref="editor"
|
||||
:config-id="id"
|
||||
@saved="onSaved"
|
||||
@error="onError"
|
||||
/>
|
||||
</v-container>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ExamConfigEditor from '@/components/ExamConfigEditor.vue'
|
||||
|
||||
export default {
|
||||
name: 'ExamEditorPage',
|
||||
components: { ExamConfigEditor },
|
||||
data() {
|
||||
return {
|
||||
id: this.$route.params.id,
|
||||
saving: false,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route.params.id'(val) {
|
||||
this.id = val
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async save() {
|
||||
if (!this.$refs.editor) return
|
||||
this.saving = true
|
||||
try {
|
||||
await this.$refs.editor.saveConfig()
|
||||
} finally {
|
||||
this.saving = false
|
||||
}
|
||||
},
|
||||
onSaved() {
|
||||
// 轻提示
|
||||
this.$toast?.success?.('保存成功')
|
||||
},
|
||||
onError(msg) {
|
||||
this.$toast?.error?.(msg || '保存失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,216 @@
|
||||
<template>
|
||||
<v-alert
|
||||
v-if="error"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
border="start"
|
||||
class="mb-4"
|
||||
closable
|
||||
@click:close="error = ''"
|
||||
>
|
||||
{{ error }}
|
||||
</v-alert>
|
||||
|
||||
<v-skeleton-loader v-if="loading" type="article" />
|
||||
|
||||
<div v-else-if="!config">
|
||||
<v-alert type="warning" variant="tonal" border="start">
|
||||
缺少配置,请通过 URL 参数 id 或 url 传入配置。
|
||||
</v-alert>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="player" ref="playerRef">
|
||||
<ExamPlayer
|
||||
v-model:room-number="roomNumberLocal"
|
||||
:exam-config="config"
|
||||
:config="playerConfigObj"
|
||||
:show-action-bar="true"
|
||||
:time-sync-status="'电脑时间'"
|
||||
@exit="exit()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed, onMounted, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import dataProvider from "@/utils/dataProvider";
|
||||
import { ExamPlayer } from "@examaware-cs/player";
|
||||
|
||||
export default {
|
||||
name: "ExamPlayerPage",
|
||||
components: { ExamPlayer },
|
||||
setup() {
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const loading = ref(true);
|
||||
const error = ref("");
|
||||
const config = ref(null);
|
||||
|
||||
// 播放器配置:支持从查询参数读取考场号
|
||||
const roomNumber = computed(() => {
|
||||
return (route.query.roomNumber || route.query.room || "01") + "";
|
||||
});
|
||||
const roomNumberLocal = ref(roomNumber.value);
|
||||
const playerConfigObj = computed(() => ({
|
||||
roomNumber: roomNumberLocal.value,
|
||||
timeSync: true,
|
||||
refreshInterval: 1000,
|
||||
fullscreen: false,
|
||||
}));
|
||||
|
||||
async function loadConfig() {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
config.value = null;
|
||||
try {
|
||||
const url = route.query.url || route.query.configUrl;
|
||||
const id = route.query.id;
|
||||
|
||||
if (url) {
|
||||
const resp = await fetch(url);
|
||||
if (!resp.ok) throw new Error("拉取配置失败: " + resp.status);
|
||||
const json = await resp.json();
|
||||
config.value = normalizeConfig(json);
|
||||
} else if (id) {
|
||||
const data = await dataProvider.loadData(`es_${id}`);
|
||||
if (!data) throw new Error("未找到该配置");
|
||||
config.value = normalizeConfig(data);
|
||||
} else {
|
||||
// 没有参数
|
||||
config.value = null;
|
||||
}
|
||||
// ExamPlayer 组件会自行响应传入的 exam-config
|
||||
} catch (e) {
|
||||
error.value = e?.message || String(e);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function exit() {
|
||||
// 返回上一页
|
||||
router.push("/");
|
||||
}
|
||||
|
||||
function normalizeConfig(raw) {
|
||||
// 保障字段存在与正确类型
|
||||
return {
|
||||
examName: raw?.examName || "未命名考试",
|
||||
message: raw?.message || "",
|
||||
// ExamAware 需要 examInfos: { name, start, end, alertTime? }
|
||||
examInfos: Array.isArray(raw?.examInfos)
|
||||
? raw.examInfos.map((i) => ({
|
||||
name: i?.name || "未命名科目",
|
||||
start: i?.start || "",
|
||||
end: i?.end || "",
|
||||
alertTime: typeof i?.alertTime === "number" ? i.alertTime : 15,
|
||||
}))
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
onMounted(loadConfig);
|
||||
watch(
|
||||
() => [
|
||||
route.query.id,
|
||||
route.query.url,
|
||||
route.query.configUrl,
|
||||
route.query.room,
|
||||
route.query.roomNumber,
|
||||
],
|
||||
loadConfig
|
||||
);
|
||||
watch(roomNumber, (v) => {
|
||||
roomNumberLocal.value = v;
|
||||
});
|
||||
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
config,
|
||||
roomNumberLocal,
|
||||
playerConfigObj,
|
||||
exit,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
/* 自定义字体定义 */
|
||||
@font-face {
|
||||
font-family: "TCloudNumber";
|
||||
src: url("../assets/fonts/TCloudNumberVF.ttf") format("truetype-variations"),
|
||||
url("../assets/fonts/TCloudNumberVF.ttf") format("truetype");
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: "MiSans", MiSans, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans",
|
||||
"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
|
||||
/* 设置主题为深色 */
|
||||
html {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--td-bg-color-page, #0a0a0a);
|
||||
color: var(--td-text-color-primary, #ffffff);
|
||||
}
|
||||
|
||||
/* 通用样式 */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 确保 TDesign 组件样式正常工作 */
|
||||
.t-button {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
/* 自定义滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--td-bg-color-container, #1a1a1a);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--td-bg-color-component, #333333);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--td-bg-color-component-hover, #444444);
|
||||
}
|
||||
</style>
|
||||
+1
-1
@@ -44,4 +44,4 @@ export const getNamespaceInfo = async () => {
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || "获取命名空间信息失败");
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -23,7 +23,6 @@ export const kvServerProvider = {
|
||||
async loadNamespaceInfo() {
|
||||
try {
|
||||
// 使用 Classworks Cloud 或者用户配置的服务器域名
|
||||
const provider = getSetting("server.provider");
|
||||
const serverUrl = getSetting("server.domain");
|
||||
|
||||
const res = await axios.get(`${serverUrl}/kv/_info`, {
|
||||
|
||||
Reference in New Issue
Block a user