适配ClassworksKV
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-dialog
|
||||
v-model="visible"
|
||||
persistent
|
||||
transition="dialog-bottom-transition"
|
||||
>
|
||||
<v-card
|
||||
class="kvinit-card"
|
||||
elevation="8"
|
||||
title="初始化云端存储授权"
|
||||
subtitle="请完成授权以启用云端存储功能"
|
||||
prepend-icon="mdi-cloud-lock"
|
||||
>
|
||||
<v-card-actions class="justify-end">
|
||||
<v-btn
|
||||
text
|
||||
class="me-3"
|
||||
@click="useLocalMode"
|
||||
>
|
||||
使用本地模式
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="flat"
|
||||
:loading="loading"
|
||||
@click="goToAuthorize"
|
||||
>
|
||||
前往授权
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<div>
|
||||
<div
|
||||
v-if="loading"
|
||||
class="d-flex align-center"
|
||||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
size="20"
|
||||
width="2"
|
||||
class="me-2"
|
||||
/>
|
||||
<span class="body-2"> 正在检查授权状态… </span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="error"
|
||||
class="body-2 text-error"
|
||||
>
|
||||
检查出错:{{ error }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { getSetting,setSetting } from "@/utils/settings";
|
||||
import { kvServerProvider } from "@/utils/providers/kvServerProvider";
|
||||
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const error = ref("");
|
||||
const route = useRoute();
|
||||
|
||||
// allow external components to reopen the dialog via an event
|
||||
const onExternalOpen = () => {
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
// Guard key to avoid infinite redirect loops across reloads
|
||||
const REDIRECT_GUARD_KEY = "kvinit.redirecting";
|
||||
|
||||
const isKvProvider = (provider) =>
|
||||
provider === "kv-server" || provider === "classworkscloud";
|
||||
|
||||
const shouldInitialize = () => {
|
||||
const provider = getSetting("server.provider");
|
||||
if (!isKvProvider(provider)) return false;
|
||||
if (route.path === "/authorize") return false; // don't run during callback
|
||||
const kvToken = getSetting("server.kvToken");
|
||||
return kvToken === "" || kvToken == null;
|
||||
};
|
||||
|
||||
const goToAuthorize = () => {
|
||||
const authDomain = getSetting("server.authDomain");
|
||||
const appId = import.meta.env.VITE_APP_ID || "1";
|
||||
const currentDomain = window.location.origin;
|
||||
const callbackUrl = encodeURIComponent(`${currentDomain}/authorize`);
|
||||
|
||||
const uuid =
|
||||
getSetting("device.uuid") || "00000000-0000-4000-8000-000000000000";
|
||||
let authorizeUrl = `${authDomain}/authorize?app_id=${appId}&mode=callback&callback_url=${callbackUrl}`;
|
||||
|
||||
// 如果UUID不是默认值,附加编码后的 uuid 参数用于迁移
|
||||
if (uuid !== "00000000-0000-4000-8000-000000000000") {
|
||||
authorizeUrl += `&uuid=${encodeURIComponent(uuid)}`;
|
||||
}
|
||||
|
||||
// set a short-lived guard to prevent immediate re-redirect
|
||||
try {
|
||||
const guardObj = { ts: Date.now() };
|
||||
sessionStorage.setItem(REDIRECT_GUARD_KEY, JSON.stringify(guardObj));
|
||||
} catch (err) {
|
||||
// sessionStorage may be unavailable in some environments
|
||||
console.debug("sessionStorage set failed", err);
|
||||
}
|
||||
|
||||
window.location.href = authorizeUrl;
|
||||
};
|
||||
|
||||
const tryLoadNamespace = async () => {
|
||||
try {
|
||||
await kvServerProvider.loadNamespaceInfo();
|
||||
} catch (err) {
|
||||
console.error("加载命名空间信息失败:", err);
|
||||
// not fatal, show non-blocking error
|
||||
error.value = err && err.message ? err.message : String(err);
|
||||
}
|
||||
};
|
||||
const useLocalMode = () => {
|
||||
// Switch to local provider and hide dialog
|
||||
setSetting("server.provider", "kv-local");
|
||||
visible.value = false;
|
||||
// Reload to let app re-evaluate
|
||||
location.reload();
|
||||
};
|
||||
onMounted(async () => {
|
||||
const provider = getSetting("server.provider");
|
||||
|
||||
// If not using kv provider, hide component immediately
|
||||
if (!isKvProvider(provider)) {
|
||||
visible.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// First try loading namespace info (safe operation) so the app can continue if already authorized
|
||||
loading.value = true;
|
||||
await tryLoadNamespace();
|
||||
loading.value = false;
|
||||
|
||||
// Decide whether we must show initialization UI / redirect
|
||||
if (shouldInitialize()) {
|
||||
// If there's a guard in sessionStorage and it's recent, don't auto-redirect to avoid loops
|
||||
let guarded = false;
|
||||
try {
|
||||
const raw = sessionStorage.getItem(REDIRECT_GUARD_KEY);
|
||||
if (raw) {
|
||||
const obj = JSON.parse(raw);
|
||||
// guard valid for 30 seconds
|
||||
if (obj && obj.ts && Date.now() - obj.ts < 30000) guarded = true;
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore parse errors but log for debugging
|
||||
console.debug("sessionStorage parse guard failed", err);
|
||||
}
|
||||
|
||||
visible.value = true;
|
||||
// Only auto-redirect if UUID is non-default (we have a device to migrate)
|
||||
const uuid =
|
||||
getSetting("device.uuid") || "00000000-0000-4000-8000-000000000000";
|
||||
const isDefaultUuid = uuid === "00000000-0000-4000-8000-000000000000";
|
||||
|
||||
if (!guarded && !isDefaultUuid) {
|
||||
// auto-redirect to authorize for better UX
|
||||
goToAuthorize();
|
||||
} else {
|
||||
// if guarded or uuid is default, stay on the init UI and let user click button
|
||||
// clear guard so subsequent attempts can redirect
|
||||
try {
|
||||
sessionStorage.removeItem(REDIRECT_GUARD_KEY);
|
||||
} catch (err) {
|
||||
console.debug("sessionStorage remove failed", err);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// not initializing: hide component
|
||||
visible.value = false;
|
||||
}
|
||||
});
|
||||
// add/remove listener in lifecycle hooks
|
||||
if (typeof window !== "undefined") {
|
||||
window.addEventListener('kvinit:open', onExternalOpen);
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
window.removeEventListener('kvinit:open', onExternalOpen);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -1,223 +0,0 @@
|
||||
<template>
|
||||
<div class="namespace-access" v-if="shouldShowAccess">
|
||||
<!-- 只读状态显示 -->
|
||||
<v-chip
|
||||
v-if="isReadOnly"
|
||||
color="warning"
|
||||
prepend-icon="mdi-lock-outline"
|
||||
>
|
||||
只读
|
||||
</v-chip>
|
||||
<v-btn
|
||||
v-if="isReadOnly"
|
||||
color="primary"
|
||||
class="rounded-xl"
|
||||
prepend-icon="mdi-lock-open-variant"
|
||||
@click="openPasswordDialog"
|
||||
:disabled="loading"
|
||||
>
|
||||
启用编辑
|
||||
</v-btn>
|
||||
|
||||
<!-- 密码输入对话框 -->
|
||||
<v-dialog v-model="dialog" max-width="400" persistent>
|
||||
<v-card class="rounded-xl" border hover>
|
||||
<v-card-title class="text-h6">输入访问密码</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="password"
|
||||
label="密码"
|
||||
variant="outlined"
|
||||
:error="!!error"
|
||||
:error-messages="error"
|
||||
@keyup.enter="checkPassword"
|
||||
@click:append-inner="showPassword = !showPassword"
|
||||
:disabled="loading"
|
||||
autofocus
|
||||
/>
|
||||
|
||||
<p v-if="passwordHint">密码提示:{{ passwordHint }}</p>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="grey"
|
||||
variant="text"
|
||||
class="rounded-xl"
|
||||
@click="dialog = false"
|
||||
:disabled="loading"
|
||||
>
|
||||
取消
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
class="rounded-xl"
|
||||
variant="tonal"
|
||||
|
||||
@click="checkPassword"
|
||||
:loading="loading"
|
||||
:disabled="!password"
|
||||
>
|
||||
确认
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getSetting, setSetting } from "@/utils/settings";
|
||||
import axios from "@/axios/axios";
|
||||
|
||||
export default {
|
||||
name: "NamespaceAccess",
|
||||
data() {
|
||||
return {
|
||||
dialog: false,
|
||||
password: "",
|
||||
error: "",
|
||||
loading: false,
|
||||
showPassword: false,
|
||||
isReadOnly: false,
|
||||
accessType: "PUBLIC", // 默认为公开访问
|
||||
passwordHint: null, // 密码提示
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
shouldShowAccess() {
|
||||
const provider = getSetting("server.provider");
|
||||
return provider === "kv-server" || provider === "classworkscloud";
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
if (this.shouldShowAccess) {
|
||||
await this.checkAccess();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async checkAccess() {
|
||||
if (!this.shouldShowAccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取命名空间访问类型
|
||||
const response = await axios.get(
|
||||
`${getSetting("server.domain")}/${getSetting("device.uuid")}/_info`
|
||||
);
|
||||
|
||||
if (
|
||||
response.data &&
|
||||
response.data.accessType &&
|
||||
["PRIVATE", "PROTECTED", "PUBLIC"].includes(response.data.accessType)
|
||||
) {
|
||||
this.accessType = response.data.accessType;
|
||||
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是私有或受保护的命名空间,检查密码
|
||||
if (this.accessType === "PRIVATE" || this.accessType === "PROTECTED") {
|
||||
const storedPassword = getSetting("namespace.password");
|
||||
if (storedPassword) {
|
||||
await this.verifyPassword(storedPassword);
|
||||
} else if (this.accessType === "PRIVATE") {
|
||||
// 如果是私有且没有密码,立即打开密码对话框
|
||||
this.openPasswordDialog();
|
||||
} else {
|
||||
// 如果是受保护的且没有密码,设置为只读
|
||||
this.setReadOnly(true);
|
||||
}
|
||||
}
|
||||
const passwordHintresponse = await axios.get(
|
||||
`${getSetting("server.domain")}/${getSetting("device.uuid")}/_hint`
|
||||
);
|
||||
if (passwordHintresponse.data && passwordHintresponse.data.passwordHint) {
|
||||
this.passwordHint = passwordHintresponse.data.passwordHint || null;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
// 处理403错误
|
||||
if (error.response && error.response.status === 403) {
|
||||
this.accessType = "PRIVATE";
|
||||
this.setReadOnly(true);
|
||||
this.openPasswordDialog();
|
||||
} else {
|
||||
console.error("访问检查失败:", error);
|
||||
this.$message?.error("访问检查失败");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async verifyPassword(password) {
|
||||
try {
|
||||
const uuid = getSetting("device.uuid");
|
||||
const response = await axios.post(
|
||||
`${getSetting("server.domain")}/${uuid}/_checkpassword`,
|
||||
{ password }
|
||||
);
|
||||
|
||||
if (response.status != 200) {
|
||||
throw new Error(response.data?.error?.message || "密码错误");
|
||||
}
|
||||
|
||||
// 密码验证成功
|
||||
setSetting("namespace.password", password);
|
||||
this.setReadOnly(false);
|
||||
this.dialog = false;
|
||||
this.$message?.success("验证成功", "已启用编辑功能");
|
||||
} catch (error) {
|
||||
// 密码验证失败
|
||||
setSetting("namespace.password", "");
|
||||
this.setReadOnly(true);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
openPasswordDialog() {
|
||||
this.password = "";
|
||||
this.error = "";
|
||||
this.dialog = true;
|
||||
},
|
||||
|
||||
async checkPassword() {
|
||||
if (!this.password) {
|
||||
this.error = "请输入密码";
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.error = "";
|
||||
|
||||
try {
|
||||
await this.verifyPassword(this.password);
|
||||
} catch (error) {
|
||||
console.error("密码验证失败:", error);
|
||||
this.error = "密码验证失败";
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
setReadOnly(value) {
|
||||
this.isReadOnly = value;
|
||||
setSetting("namespace.accessType", value ? "readonly" : "readwrite");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.namespace-access {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.password-hint {
|
||||
max-width: 100%;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<v-card class="my-4" :loading="loading" :disabled="!hasNamespaceInfo">
|
||||
<template #loader>
|
||||
<v-progress-linear v-if="loading" indeterminate color="primary" />
|
||||
</template>
|
||||
|
||||
<v-card-title>
|
||||
<v-icon class="me-2"> mdi-cloud-check </v-icon>
|
||||
设备信息
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text v-if="hasNamespaceInfo">
|
||||
<!-- 用户信息与头像 -->
|
||||
<div v-if="namespaceInfo.account" class="d-flex align-center mb-4">
|
||||
<v-card
|
||||
border hover
|
||||
class="w-100"
|
||||
variant="tonal"
|
||||
:prepend-avatar="namespaceInfo.account.avatarUrl"
|
||||
:title="namespaceInfo.account.name || '未命名用户'"
|
||||
:subtitle="
|
||||
'此设备由贵校管理 管理员账号 ID: ' + namespaceInfo.account.id
|
||||
"
|
||||
>
|
||||
<v-card-text
|
||||
>此设备由贵校或贵单位管理,该管理员系此空间所有者,如有疑问请咨询他,对于恶意绑定、滥用行为请反馈。</v-card-text
|
||||
>
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<!-- 设备信息卡片 -->
|
||||
<v-card v-if="namespaceInfo.device" variant="tonal" class="mb-4" border hover>
|
||||
<v-card-title class="pb-1"> 设备信息 </v-card-title>
|
||||
<v-card-text>
|
||||
<div class="d-flex flex-column gap-1">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="small" class="me-2"> mdi-tag </v-icon>
|
||||
<span class="font-weight-medium me-2">设备名称:</span>
|
||||
<span>{{ namespaceInfo.device.name || "未命名设备" }}</span>
|
||||
</div>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="small" class="me-2"> mdi-identifier </v-icon>
|
||||
<span class="font-weight-medium me-2">设备 ID:</span>
|
||||
<span>{{ namespaceInfo.device.id }}</span>
|
||||
</div>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="small" class="me-2"> mdi-uuid </v-icon>
|
||||
<span class="font-weight-medium me-2">UUID:</span>
|
||||
<span class="text-truncate">{{
|
||||
namespaceInfo.device.uuid || "未知"
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="small" class="me-2"> mdi-calendar </v-icon>
|
||||
<span class="font-weight-medium me-2">创建时间:</span>
|
||||
<span>{{ formatDate(namespaceInfo.device.createdAt) }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="namespaceInfo.device.updatedAt"
|
||||
class="d-flex align-center"
|
||||
>
|
||||
<v-icon size="small" class="me-2"> mdi-calendar-clock </v-icon>
|
||||
<span class="font-weight-medium me-2">更新时间:</span>
|
||||
<span>{{ formatDate(namespaceInfo.device.updatedAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text> </v-card
|
||||
><v-card title="Classworks KV" subtitle="云原生键值数据库" border hover
|
||||
><v-card-text
|
||||
>Classworks KV
|
||||
是厚浪云推出的云原生键值数据库,其是一个开放的云应用平台,为各种应用提供存储服务。此设备正在使用其服务,如果您希望管理设备信息,请前往
|
||||
Classworks KV
|
||||
的网站,如果您在服务推出前就在使用 Classworks,您的数据已被自动迁移。
|
||||
<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"
|
||||
class="text-none"
|
||||
append-icon="mdi-open-in-new"
|
||||
target="_blank"
|
||||
>前往 Classworks KV</v-btn
|
||||
></v-card-actions
|
||||
></v-card
|
||||
>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text v-else>
|
||||
<v-alert type="info" variant="tonal">
|
||||
<v-alert-title>未获取到设备信息</v-alert-title>
|
||||
<p>您尚未完成云端存储授权或连接失败,请点击下方按钮进行初始化。</p>
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
:loading="loading"
|
||||
@click="reloadInfo"
|
||||
>
|
||||
刷新设备信息
|
||||
</v-btn>
|
||||
|
||||
<v-btn color="primary" @click="reinitializeCloudStorage">
|
||||
重新初始化云端存储
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { kvServerProvider } from "@/utils/providers/kvServerProvider";
|
||||
|
||||
export default {
|
||||
name: "CloudNamespaceInfoCard",
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
namespaceInfo: {},
|
||||
loading: false,
|
||||
hasNamespaceInfo: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
visible(newVal) {
|
||||
if (newVal === true) {
|
||||
this.fetchNamespaceInfo();
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.visible) {
|
||||
this.fetchNamespaceInfo();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return "未知";
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString("zh-CN");
|
||||
} catch {
|
||||
return dateString;
|
||||
}
|
||||
},
|
||||
async fetchNamespaceInfo() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await kvServerProvider.loadNamespaceInfo();
|
||||
|
||||
this.namespaceInfo = response;
|
||||
this.hasNamespaceInfo = true;
|
||||
this.loading = false;
|
||||
} catch (e) {
|
||||
console.error("获取命名空间信息失败:", e);
|
||||
this.hasNamespaceInfo = false;
|
||||
this.namespaceInfo = {};
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async reloadInfo() {
|
||||
await this.fetchNamespaceInfo();
|
||||
},
|
||||
reinitializeCloudStorage() {
|
||||
// 触发 KvInitialize 组件的重新初始化
|
||||
try {
|
||||
window.dispatchEvent(new CustomEvent("kvinit:open"));
|
||||
} catch (e) {
|
||||
console.error("重新初始化云端存储失败:", e);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,729 +0,0 @@
|
||||
<template>
|
||||
<settings-card
|
||||
v-if="shouldShowCard"
|
||||
title="命名空间设置"
|
||||
icon="mdi-database-lock"
|
||||
:loading="loading"
|
||||
>
|
||||
<namespace-access ref="namespaceAccess" />
|
||||
<!-- 命名空间标识符 -->
|
||||
<v-card variant="tonal" class="rounded-lg mb-4">
|
||||
<v-card-item>
|
||||
<template #prepend>
|
||||
<v-icon
|
||||
icon="mdi-identifier"
|
||||
size="large"
|
||||
class="mr-3"
|
||||
color="primary"
|
||||
/>
|
||||
</template>
|
||||
<v-card-title>命名空间标识符</v-card-title>
|
||||
<v-card-subtitle>
|
||||
<div class="d-flex align-center mt-2">
|
||||
<code class="text-body-1">{{ namespaceInfo.uuid }}</code>
|
||||
<v-btn
|
||||
icon="mdi-content-copy"
|
||||
variant="text"
|
||||
size="small"
|
||||
class="ml-2"
|
||||
@click="copyUuid"
|
||||
/>
|
||||
</div>
|
||||
</v-card-subtitle>
|
||||
</v-card-item>
|
||||
</v-card>
|
||||
|
||||
<!-- 命名空间信息表单 -->
|
||||
<v-card variant="tonal" class="rounded-lg mb-4">
|
||||
<v-card-item>
|
||||
<template #prepend>
|
||||
<v-icon
|
||||
icon="mdi-form-textbox"
|
||||
size="large"
|
||||
class="mr-3"
|
||||
color="primary"
|
||||
/>
|
||||
</template>
|
||||
<v-card-title>命名空间信息</v-card-title>
|
||||
</v-card-item>
|
||||
|
||||
<v-card-text>
|
||||
<v-form ref="form" @submit.prevent="saveNamespaceInfo">
|
||||
<v-text-field
|
||||
v-model="namespaceForm.name"
|
||||
label="命名空间名称"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
class="mb-4"
|
||||
:loading="loading"
|
||||
:rules="[(v) => !!v || '请输入命名空间名称']"
|
||||
>
|
||||
<template #prepend-inner>
|
||||
<v-icon icon="mdi-tag-text" />
|
||||
</template>
|
||||
</v-text-field>
|
||||
|
||||
<v-select
|
||||
v-model="namespaceForm.accessType"
|
||||
:items="accessTypeOptions"
|
||||
label="访问权限"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
class="mb-6"
|
||||
:loading="loading"
|
||||
>
|
||||
<template #prepend-inner>
|
||||
<v-icon icon="mdi-shield-lock" />
|
||||
</template>
|
||||
</v-select>
|
||||
|
||||
<div class="d-flex justify-end">
|
||||
<v-btn
|
||||
color="primary"
|
||||
:loading="loading"
|
||||
:disabled="!isFormChanged"
|
||||
@click="saveNamespaceInfo"
|
||||
>
|
||||
保存更改
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-content-save" />
|
||||
</template>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 访问密码设置 -->
|
||||
<v-card variant="tonal" class="rounded-lg">
|
||||
<v-card-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-key" size="large" class="mr-3" color="primary" />
|
||||
</template>
|
||||
<v-card-title>访问密码</v-card-title>
|
||||
<v-card-subtitle class="mt-2">
|
||||
设置访问密码以保护数据安全,可以将老师、电教的名字、学号等作为密码
|
||||
</v-card-subtitle>
|
||||
</v-card-item>
|
||||
<v-card-text>
|
||||
<v-form ref="passwordForm" @submit.prevent="savePassword">
|
||||
<v-text-field
|
||||
v-if="namespaceInfo.hasPassword"
|
||||
v-model="passwordForm.oldPassword"
|
||||
label="当前密码"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
class="mb-4"
|
||||
:loading="passwordLoading"
|
||||
:rules="[(v) => !!v || '请输入当前密码']"
|
||||
>
|
||||
<template #prepend-inner>
|
||||
<v-icon icon="mdi-lock" />
|
||||
</template>
|
||||
</v-text-field><v-text-field
|
||||
v-model="passwordForm.newPassword"
|
||||
label="新密码"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
class="mb-4"
|
||||
:loading="passwordLoading"
|
||||
>
|
||||
<template #prepend-inner>
|
||||
<v-icon icon="mdi-lock-plus" />
|
||||
</template>
|
||||
</v-text-field>
|
||||
|
||||
|
||||
|
||||
<v-text-field
|
||||
v-model="passwordForm.confirmPassword"
|
||||
label="确认新密码"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
class="mb-4"
|
||||
:loading="passwordLoading"
|
||||
:rules="[
|
||||
(v) =>
|
||||
!passwordForm.newPassword ||
|
||||
v === passwordForm.newPassword ||
|
||||
'两次输入的密码不一致',
|
||||
]"
|
||||
>
|
||||
<template #prepend-inner>
|
||||
<v-icon icon="mdi-lock-check" />
|
||||
</template>
|
||||
</v-text-field>
|
||||
|
||||
<div class="d-flex justify-space-between align-center">
|
||||
<div>
|
||||
<v-btn
|
||||
v-if="namespaceInfo.hasPassword"
|
||||
color="error"
|
||||
variant="tonal"
|
||||
:loading="passwordLoading"
|
||||
@click="confirmDeletePassword"
|
||||
class="mr-2"
|
||||
>
|
||||
删除密码
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-lock-remove" />
|
||||
</template>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="namespaceInfo.hasPassword"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
:loading="hintLoading"
|
||||
@click="openHintDialog"
|
||||
class="mr-2"
|
||||
>
|
||||
设置密码提示
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-lightbulb-outline" />
|
||||
</template>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
@click="modifyLocalPassword"
|
||||
>
|
||||
修改本地密码
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-key-variant" />
|
||||
</template>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-btn
|
||||
color="primary"
|
||||
:loading="passwordLoading"
|
||||
:disabled="!isPasswordFormValid"
|
||||
@click="savePassword"
|
||||
>
|
||||
保存密码
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-content-save" />
|
||||
</template>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-form>
|
||||
<!--<setting-item
|
||||
setting-key="namespace.password"
|
||||
title="访问密码"
|
||||
></setting-item>-->
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 密码提示设置对话框 -->
|
||||
<v-dialog v-model="showHintDialog" max-width="400">
|
||||
<v-card>
|
||||
<v-card-item>
|
||||
<v-card-title>设置密码提示</v-card-title>
|
||||
<v-card-subtitle class="mt-2">
|
||||
设置一个提示帮助记忆密码
|
||||
</v-card-subtitle>
|
||||
</v-card-item>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="passwordHintForm.hint"
|
||||
label="密码提示"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
class="mb-4"
|
||||
:loading="hintLoading"
|
||||
placeholder="例如:我的生日"
|
||||
>
|
||||
<template #prepend-inner>
|
||||
<v-icon icon="mdi-lightbulb-outline" />
|
||||
</template>
|
||||
</v-text-field>
|
||||
<div class="text-caption text-grey">
|
||||
当前提示:{{ namespaceInfo.passwordHint || "未设置" }}
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="grey-darken-1"
|
||||
variant="text"
|
||||
@click="showHintDialog = false"
|
||||
:disabled="hintLoading"
|
||||
>
|
||||
取消
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="text"
|
||||
:loading="hintLoading"
|
||||
@click="savePasswordHint"
|
||||
>
|
||||
保存
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 删除密码确认对话框 -->
|
||||
<v-dialog v-model="showDeleteConfirm" max-width="400">
|
||||
<v-card>
|
||||
<v-card-item>
|
||||
<v-card-title>确认删除密码</v-card-title>
|
||||
<v-card-text class="mt-4">
|
||||
删除密码后,任何人都可以访问和修改此命名空间的数据。确定要继续吗?
|
||||
</v-card-text>
|
||||
</v-card-item>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="grey-darken-1"
|
||||
variant="text"
|
||||
@click="showDeleteConfirm = false"
|
||||
>
|
||||
取消
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="error"
|
||||
variant="text"
|
||||
:loading="passwordLoading"
|
||||
@click="deletePassword"
|
||||
>
|
||||
确认删除
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 密码验证对话框 -->
|
||||
<v-dialog v-model="showVerifyDialog" max-width="400" persistent>
|
||||
<v-card>
|
||||
<v-card-item>
|
||||
<v-card-title>验证密码</v-card-title>
|
||||
<v-card-subtitle class="mt-2">
|
||||
请输入当前密码以继续操作
|
||||
</v-card-subtitle>
|
||||
</v-card-item>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="verifyForm.password"
|
||||
label="当前密码"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
class="mb-4"
|
||||
:loading="verifyLoading"
|
||||
:error="!!verifyForm.error"
|
||||
:error-messages="verifyForm.error"
|
||||
@keyup.enter="verifyPassword"
|
||||
>
|
||||
<template #prepend-inner>
|
||||
<v-icon icon="mdi-lock" />
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="grey-darken-1"
|
||||
variant="text"
|
||||
@click="cancelVerify"
|
||||
:disabled="verifyLoading"
|
||||
>
|
||||
取消
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="text"
|
||||
:loading="verifyLoading"
|
||||
:disabled="!verifyForm.password"
|
||||
@click="verifyPassword"
|
||||
>
|
||||
确认
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-snackbar
|
||||
v-model="showSnackbar"
|
||||
:timeout="3000"
|
||||
:color="snackbarColor"
|
||||
location="top"
|
||||
>
|
||||
{{ snackbarText }}
|
||||
<template #actions>
|
||||
<v-btn variant="text" @click="showSnackbar = false"> 关闭 </v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</settings-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettingsCard from "@/components/SettingsCard.vue";
|
||||
import NamespaceAccess from "@/components/NamespaceAccess.vue";
|
||||
import { kvServerProvider } from "@/utils/providers/kvServerProvider";
|
||||
import { getSetting } from "@/utils/settings";
|
||||
import axios from "@/axios/axios";
|
||||
|
||||
// Helper function to get request headers
|
||||
const getHeaders = () => {
|
||||
const headers = { Accept: "application/json" };
|
||||
const siteKey = getSetting("server.siteKey");
|
||||
const password = getSetting("namespace.password");
|
||||
|
||||
if (siteKey) {
|
||||
headers["x-site-key"] = siteKey;
|
||||
}
|
||||
if (password) {
|
||||
headers["x-namespace-password"] = password;
|
||||
}
|
||||
|
||||
return headers;
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "NamespaceSettingsCard",
|
||||
components: {
|
||||
SettingsCard,
|
||||
NamespaceAccess
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
passwordLoading: false,
|
||||
hintLoading: false,
|
||||
verifyLoading: false,
|
||||
showSnackbar: false,
|
||||
showDeleteConfirm: false,
|
||||
showHintDialog: false,
|
||||
showVerifyDialog: false,
|
||||
snackbarText: "",
|
||||
snackbarColor: "success",
|
||||
namespaceInfo: {
|
||||
uuid: "",
|
||||
name: "",
|
||||
accessType: "PUBLIC",
|
||||
hasPassword: false,
|
||||
passwordHint: null,
|
||||
},
|
||||
namespaceForm: {
|
||||
name: "",
|
||||
accessType: "PUBLIC",
|
||||
},
|
||||
passwordForm: {
|
||||
newPassword: "",
|
||||
oldPassword: "",
|
||||
confirmPassword: "",
|
||||
},
|
||||
passwordHintForm: {
|
||||
hint: "",
|
||||
},
|
||||
verifyForm: {
|
||||
password: "",
|
||||
error: "",
|
||||
action: null, // 'delete' | 'hint'
|
||||
onSuccess: null,
|
||||
},
|
||||
originalForm: {
|
||||
name: "",
|
||||
accessType: "PUBLIC",
|
||||
},
|
||||
accessTypeOptions: [
|
||||
{
|
||||
title: "公开(无需密码)",
|
||||
value: "PUBLIC",
|
||||
icon: "mdi-lock-open",
|
||||
},
|
||||
{
|
||||
title: "受保护(需要密码写入)",
|
||||
value: "PROTECTED",
|
||||
icon: "mdi-lock",
|
||||
},
|
||||
{
|
||||
title: "私有(需要密码读写)",
|
||||
value: "PRIVATE",
|
||||
icon: "mdi-lock-alert",
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
shouldShowCard() {
|
||||
const provider = getSetting("server.provider");
|
||||
return provider === "kv-server" || provider === "classworkscloud";
|
||||
},
|
||||
deviceUuid() {
|
||||
return this.namespaceInfo.uuid;
|
||||
},
|
||||
isFormChanged() {
|
||||
return (
|
||||
this.namespaceForm.name !== this.originalForm.name ||
|
||||
this.namespaceForm.accessType !== this.originalForm.accessType
|
||||
);
|
||||
},
|
||||
isPasswordFormValid() {
|
||||
if (!this.passwordForm.newPassword) {
|
||||
return true; // 允许清空密码
|
||||
}
|
||||
const isConfirmMatch = this.passwordForm.newPassword === this.passwordForm.confirmPassword;
|
||||
if (this.namespaceInfo.hasPassword) {
|
||||
return isConfirmMatch && !!this.passwordForm.oldPassword;
|
||||
}
|
||||
return isConfirmMatch;
|
||||
},
|
||||
},
|
||||
|
||||
async created() {
|
||||
if (this.shouldShowCard) {
|
||||
await this.loadNamespaceInfo();
|
||||
await this.loadPasswordHint();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async loadNamespaceInfo() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await kvServerProvider.loadNamespaceInfo();
|
||||
if (response.status == 200 && response.data) {
|
||||
this.namespaceInfo = response.data;
|
||||
this.namespaceForm.name = response.data.name;
|
||||
this.namespaceForm.accessType = response.data.accessType;
|
||||
this.passwordForm.passwordHint = response.data.passwordHint || "";
|
||||
// 保存原始值用于比较
|
||||
this.originalForm = { ...this.namespaceForm };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载命名空间信息失败:", error);
|
||||
this.showError("加载命名空间信息失败");
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async saveNamespaceInfo() {
|
||||
if (!this.isFormChanged) return;
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await kvServerProvider.updateNamespaceInfo({
|
||||
name: this.namespaceForm.name,
|
||||
accessType: this.namespaceForm.accessType,
|
||||
});
|
||||
console.log(response);
|
||||
if (response.status == 200) {
|
||||
this.originalForm = { ...this.namespaceForm };
|
||||
this.showSuccess("命名空间信息已更新");
|
||||
} else {
|
||||
throw new Error(response.error.message || "保存失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("保存命名空间信息失败:", error);
|
||||
this.showError(error.message || "保存命名空间信息失败");
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async copyUuid() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(this.namespaceInfo.uuid);
|
||||
this.showSuccess("命名空间标识符已复制到剪贴板");
|
||||
} catch (error) {
|
||||
console.error("复制失败:", error);
|
||||
this.showError("复制失败");
|
||||
}
|
||||
},
|
||||
|
||||
async savePassword() {
|
||||
if (!this.isPasswordFormValid) return;
|
||||
|
||||
this.passwordLoading = true;
|
||||
try {
|
||||
const response = await kvServerProvider.updatePassword(
|
||||
this.passwordForm.newPassword || null,
|
||||
this.passwordForm.oldPassword || null
|
||||
);
|
||||
|
||||
if (response.status === 200) {
|
||||
this.namespaceInfo.hasPassword = !!this.passwordForm.newPassword;
|
||||
this.passwordForm = {
|
||||
newPassword: "",
|
||||
oldPassword: "",
|
||||
confirmPassword: "",
|
||||
};
|
||||
this.showSuccess("密码已更新");
|
||||
this.$router.push("/");
|
||||
} else {
|
||||
throw new Error(response.error?.message || "保存失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("保存密码失败:", error);
|
||||
this.showError(error.response?.data?.message || "保存密码失败");
|
||||
} finally {
|
||||
this.passwordLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async confirmDeletePassword() {
|
||||
this.verifyForm = {
|
||||
password: "",
|
||||
error: "",
|
||||
action: "delete",
|
||||
onSuccess: () => {
|
||||
this.showDeleteConfirm = true;
|
||||
},
|
||||
};
|
||||
this.showVerifyDialog = true;
|
||||
},
|
||||
|
||||
async deletePassword() {
|
||||
this.passwordLoading = true;
|
||||
try {
|
||||
const response = await kvServerProvider.deletePassword();
|
||||
|
||||
if (response.status === 200) {
|
||||
this.namespaceInfo.hasPassword = false;
|
||||
this.namespaceInfo.passwordHint = null;
|
||||
this.passwordForm = {
|
||||
newPassword: "",
|
||||
oldPassword: "",
|
||||
confirmPassword: "",
|
||||
};
|
||||
this.showDeleteConfirm = false;
|
||||
this.showSuccess("密码已删除");
|
||||
} else {
|
||||
throw new Error(response.error?.message || "删除失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("删除密码失败:", error);
|
||||
this.showError(error.response?.data?.message || "删除密码失败");
|
||||
} finally {
|
||||
this.passwordLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async loadPasswordHint() {
|
||||
try {
|
||||
const serverUrl = getSetting("server.domain");
|
||||
const machineId = getSetting("device.uuid");
|
||||
const response = await axios.get(
|
||||
`${serverUrl}/${machineId}/_hint`,
|
||||
{ headers: getHeaders() }
|
||||
);
|
||||
|
||||
if (response.data && response.data.passwordHint !== undefined) {
|
||||
this.namespaceInfo.passwordHint = response.data.passwordHint;
|
||||
this.passwordHintForm.hint = response.data.passwordHint || "";
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载密码提示失败:", error);
|
||||
this.showError("加载密码提示失败");
|
||||
}
|
||||
},
|
||||
|
||||
async savePasswordHint() {
|
||||
this.hintLoading = true;
|
||||
try {
|
||||
const serverUrl = getSetting("server.domain");
|
||||
const machineId = getSetting("device.uuid");
|
||||
const response = await axios.put(
|
||||
`${serverUrl}/${machineId}/_hint`,
|
||||
{ hint: this.passwordHintForm.hint || null },
|
||||
{ headers: getHeaders() }
|
||||
);
|
||||
|
||||
if (response.data) {
|
||||
this.namespaceInfo.passwordHint = response.data.passwordHint;
|
||||
this.showSuccess("密码提示已更新");
|
||||
this.showHintDialog = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("保存密码提示失败:", error);
|
||||
this.showError(error.response?.data?.message || "保存密码提示失败");
|
||||
} finally {
|
||||
this.hintLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async openHintDialog() {
|
||||
this.verifyForm = {
|
||||
password: "",
|
||||
error: "",
|
||||
action: "hint",
|
||||
onSuccess: () => {
|
||||
this.showHintDialog = true;
|
||||
},
|
||||
};
|
||||
this.showVerifyDialog = true;
|
||||
},
|
||||
|
||||
cancelVerify() {
|
||||
this.showVerifyDialog = false;
|
||||
this.verifyForm = {
|
||||
password: "",
|
||||
error: "",
|
||||
action: null,
|
||||
onSuccess: null,
|
||||
};
|
||||
},
|
||||
|
||||
async verifyPassword() {
|
||||
if (!this.verifyForm.password) return;
|
||||
|
||||
this.verifyLoading = true;
|
||||
this.verifyForm.error = "";
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${getSetting("server.domain")}/${getSetting("device.uuid")}/_checkpassword`,
|
||||
{ password: this.verifyForm.password },
|
||||
{ headers: getHeaders() }
|
||||
);
|
||||
|
||||
if (response.status == 200) {
|
||||
// 验证成功,执行对应操作
|
||||
this.showVerifyDialog = false;
|
||||
if (this.verifyForm.onSuccess) {
|
||||
this.verifyForm.onSuccess();
|
||||
}
|
||||
} else {
|
||||
this.verifyForm.error = "密码错误";
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("密码验证失败:", error);
|
||||
this.verifyForm.error = "密码验证失败";
|
||||
} finally {
|
||||
this.verifyLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
showSuccess(message) {
|
||||
this.snackbarColor = "success";
|
||||
this.snackbarText = message;
|
||||
this.showSnackbar = true;
|
||||
},
|
||||
|
||||
showError(message) {
|
||||
this.snackbarColor = "error";
|
||||
this.snackbarText = message;
|
||||
this.showSnackbar = true;
|
||||
},
|
||||
|
||||
modifyLocalPassword() {
|
||||
// 获取NamespaceAccess组件实例并调用方法
|
||||
const namespaceAccess = this.$refs.namespaceAccess;
|
||||
if (namespaceAccess) {
|
||||
namespaceAccess.openPasswordDialog();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,9 +1,33 @@
|
||||
<template>
|
||||
<settings-card title="数据源设置" icon="mdi-database" :loading="loading">
|
||||
<settings-card
|
||||
title="数据源设置"
|
||||
icon="mdi-database"
|
||||
:loading="loading"
|
||||
>
|
||||
<v-form>
|
||||
<setting-item setting-key="server.provider" title="数据提供者" />
|
||||
<!-- 使用双向绑定来替代 setting-key -->
|
||||
<v-select
|
||||
v-model="serverSettings.provider"
|
||||
:items="[
|
||||
{ title: 'Classworks云端存储', value: 'classworkscloud' },
|
||||
{ title: 'KV本地存储', value: 'kv-local' },
|
||||
{ title: 'KV远程服务器', value: 'kv-server' }
|
||||
]"
|
||||
label="数据提供者"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
item-title="title"
|
||||
item-value="value"
|
||||
prepend-icon="mdi-database"
|
||||
class="mb-3"
|
||||
/>
|
||||
|
||||
<v-alert v-if="isKvProvider" type="info" variant="tonal" class="my-2">
|
||||
<v-alert
|
||||
v-if="isKvProvider"
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="my-2"
|
||||
>
|
||||
<v-alert-title>KV 存储系统</v-alert-title>
|
||||
<p>KV存储系统使用本机唯一标识符(UUID)来区分不同设备的数据。</p>
|
||||
<p v-if="currentProvider === 'kv-server'">
|
||||
@@ -12,49 +36,113 @@
|
||||
</p>
|
||||
</v-alert>
|
||||
|
||||
<v-alert v-if="isClassworksCloud" type="info" color="success" variant="tonal" class="my-2">
|
||||
<v-alert
|
||||
v-if="isClassworksCloud"
|
||||
type="info"
|
||||
color="success"
|
||||
variant="tonal"
|
||||
class="my-2"
|
||||
>
|
||||
<v-alert-title>Classworks云端存储</v-alert-title>
|
||||
<p>Classworks云端存储是官方提供的存储解决方案,自动配置了最优的访问设置。</p>
|
||||
<p>使用此选项时,服务器域名和网站令牌将自动配置,无需手动设置。</p>
|
||||
</v-alert>
|
||||
|
||||
<v-divider class="my-2" />
|
||||
<setting-item setting-key="server.domain" title="服务器域名" :disabled="isClassworksCloud" />
|
||||
<v-divider class="my-2" />
|
||||
<setting-item setting-key="server.classNumber" title="班号" />
|
||||
<v-divider class="my-2" />
|
||||
<setting-item setting-key="server.siteKey" title="网站令牌" :disabled="isClassworksCloud">
|
||||
<template #description>
|
||||
用于后端验证请求的安全令牌。如需要,请从系统管理员获取。
|
||||
</template>
|
||||
</setting-item>
|
||||
<v-alert v-if="useServer" type="info" variant="tonal" class="my-2">
|
||||
<v-icon icon="mdi-information-outline" class="mr-2"></v-icon>
|
||||
<span>网站令牌将作为 <code>x-site-key</code> 请求头发送给服务器,用于验证请求的合法性。如果您的服务器需要此验证,请在上方输入有效的令牌。</span>
|
||||
</v-alert>
|
||||
<v-divider class="my-2" />
|
||||
<setting-item setting-key="device.uuid" title="设备UUID" />
|
||||
<v-divider
|
||||
class="my-2"
|
||||
/>
|
||||
|
||||
<!-- For classworkscloud show kv token and namespace info card -->
|
||||
<div v-if="isClassworksCloud">
|
||||
<v-text-field
|
||||
v-model="serverSettings.kvToken"
|
||||
label="KV 授权令牌"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
prepend-icon="mdi-shield-key"
|
||||
class="mb-2"
|
||||
hint="令牌用于云端存储授权"
|
||||
persistent-hint
|
||||
/>
|
||||
|
||||
|
||||
|
||||
<cloud-namespace-info-card
|
||||
:visible="isClassworksCloud"
|
||||
class="mt-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- For kv-server show domain + kv token -->
|
||||
<div v-else-if="currentProvider === 'kv-server'">
|
||||
<v-text-field
|
||||
v-model="serverSettings.domain"
|
||||
label="服务器域名"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
prepend-icon="mdi-web"
|
||||
class="mb-2"
|
||||
hint="例如: https://example.com (不需要路径)"
|
||||
persistent-hint
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="serverSettings.kvToken"
|
||||
label="KV 授权令牌"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
prepend-icon="mdi-shield-key"
|
||||
class="mb-2"
|
||||
hint="令牌用于服务器验证"
|
||||
persistent-hint
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- For kv-local show only class number -->
|
||||
<div v-else-if="currentProvider === 'kv-local'">
|
||||
<v-text-field
|
||||
v-model="serverSettings.classNumber"
|
||||
label="班级编号"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
prepend-icon="mdi-account-group"
|
||||
class="mb-2"
|
||||
hint="例如: 高三八班"
|
||||
persistent-hint
|
||||
/>
|
||||
</div>
|
||||
</v-form>
|
||||
</settings-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettingsCard from "@/components/SettingsCard.vue";
|
||||
import SettingItem from "@/components/settings/SettingItem.vue";
|
||||
import { getSetting } from "@/utils/settings";
|
||||
import CloudNamespaceInfoCard from "./CloudNamespaceInfoCard.vue";
|
||||
import { getSetting, setSetting, watchSettings } from "@/utils/settings";
|
||||
|
||||
export default {
|
||||
name: "ServerSettingsCard",
|
||||
components: { SettingsCard, SettingItem },
|
||||
components: { SettingsCard, CloudNamespaceInfoCard },
|
||||
props: {
|
||||
loading: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
return {
|
||||
unwatch: null,
|
||||
// 保存所有相关设置
|
||||
serverSettings: {
|
||||
provider: getSetting("server.provider"),
|
||||
domain: getSetting("server.domain"),
|
||||
classNumber: getSetting("server.classNumber"),
|
||||
kvToken: getSetting("server.kvToken"),
|
||||
},
|
||||
// 用于监听设置变化时刷新 UI
|
||||
settingsChangeTimeout: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
currentProvider() {
|
||||
return getSetting("server.provider");
|
||||
return this.serverSettings.provider;
|
||||
},
|
||||
isKvProvider() {
|
||||
return this.currentProvider === 'kv-local' || this.currentProvider === 'kv-server';
|
||||
@@ -65,6 +153,70 @@ export default {
|
||||
useServer() {
|
||||
return this.currentProvider === 'server' || this.currentProvider === 'kv-server' || this.currentProvider === 'classworkscloud';
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 监视 serverSettings 的深层变化
|
||||
serverSettings: {
|
||||
handler() {
|
||||
// 使用防抖处理,避免频繁刷新
|
||||
if (this.settingsChangeTimeout) {
|
||||
clearTimeout(this.settingsChangeTimeout);
|
||||
}
|
||||
// 延迟保存,提供更好的用户体验
|
||||
this.settingsChangeTimeout = setTimeout(() => {
|
||||
this.saveAllSettings();
|
||||
}, 100);
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 加载所有设置
|
||||
this.loadAllSettings();
|
||||
|
||||
// 订阅全局设置变更事件
|
||||
this.unwatch = watchSettings(() => {
|
||||
// 当设置从其他地方(如其他标签页、其他组件)改变时,刷新本地状态
|
||||
this.loadAllSettings();
|
||||
// 可选:强制重新渲染组件
|
||||
this.$forceUpdate && this.$forceUpdate();
|
||||
});
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.unwatch) this.unwatch();
|
||||
},
|
||||
methods: {
|
||||
// 从全局设置加载所有设置到本地
|
||||
loadAllSettings() {
|
||||
this.serverSettings = {
|
||||
provider: getSetting("server.provider"),
|
||||
domain: getSetting("server.domain"),
|
||||
classNumber: getSetting("server.classNumber"),
|
||||
kvToken: getSetting("server.kvToken"),
|
||||
};
|
||||
},
|
||||
|
||||
// 保存所有本地设置到全局
|
||||
saveAllSettings() {
|
||||
Object.entries(this.serverSettings).forEach(([key, value]) => {
|
||||
const settingKey = `server.${key}`;
|
||||
const currentValue = getSetting(settingKey);
|
||||
|
||||
// 只有当值发生变化时才进行设置
|
||||
if (value !== currentValue) {
|
||||
const success = setSetting(settingKey, value);
|
||||
if (success) {
|
||||
console.log(`设置已更新: ${settingKey} = ${value}`);
|
||||
} else {
|
||||
console.error(`设置失败: ${settingKey}`);
|
||||
// 如果设置失败,恢复值
|
||||
this.serverSettings[key] = currentValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user