Enhance App.vue, axios.js, MigrationTool.vue, index.vue, settings.vue, and related utilities to support namespace management and improve configuration handling. Add namespace password to axios request headers, load namespace info on App mount, and update settings structure to include namespace settings. Refactor dataProvider to utilize new kvLocalProvider and kvServerProvider for data operations, ensuring better separation of concerns and improved error handling.

This commit is contained in:
SunWuyuan
2025-05-18 10:54:39 +08:00
parent 6d6e4a27a1
commit c42c878ac8
12 changed files with 1249 additions and 441 deletions
+3 -3
View File
@@ -681,7 +681,7 @@ export default {
// 批量导入配置数据
const configResponse = await axios.post(
`${this.targetServerUrl}/${this.machineId}/import/batch-import`,
`${this.targetServerUrl}/${this.machineId}/_batchimport`,
batchData,
{
headers: this.getRequestHeaders(),
@@ -798,7 +798,7 @@ export default {
// 批量导入配置数据
const configResponse = await axios.post(
`${this.targetServerUrl}/${this.machineId}/import/batch-import`,
`${this.targetServerUrl}/${this.machineId}/_batchimport`,
batchData,
{
headers: this.getRequestHeaders(),
@@ -890,7 +890,7 @@ export default {
// 发送批量请求
if (Object.keys(batchPayload).length > 0) {
const response = await axios.post(
`${this.targetServerUrl}/${this.machineId}/import/batch-import`,
`${this.targetServerUrl}/${this.machineId}/_batchimport`,
batchPayload,
{
headers: this.getRequestHeaders(),
+187
View File
@@ -0,0 +1,187 @@
<template>
<div class="namespace-access">
<!-- 只读状态显示 -->
<v-chip
v-if="isReadOnly"
color="warning"
size="small"
class="mr-2"
prepend-icon="mdi-lock-outline"
>
只读
</v-chip>
<v-btn
v-if="isReadOnly"
color="primary"
size="small"
prepend-icon="mdi-lock-open-variant"
@click="openPasswordDialog"
:disabled="loading"
>
启用编辑
</v-btn>
<!-- 密码输入对话框 -->
<v-dialog v-model="dialog" max-width="400" persistent>
<v-card>
<v-card-title class="text-h6">输入访问密码</v-card-title>
<v-card-text>
<v-text-field
v-model="password"
:type="showPassword ? 'text' : 'password'"
label="密码"
variant="outlined"
:error="!!error"
:error-messages="error"
@keyup.enter="checkPassword"
:append-inner-icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
@click:append-inner="showPassword = !showPassword"
:disabled="loading"
autofocus
/>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
color="grey"
variant="text"
@click="dialog = false"
:disabled="loading"
>
取消
</v-btn>
<v-btn
color="primary"
@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", // 默认为公开访问
};
},
async created() {
await this.checkAccess();
},
methods: {
async checkAccess() {
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 {
//this.$router.push("/settings");
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);
}
}
} 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}/_check`,
{ password }
);
if (!response.data || response.data.success === false) {
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>
@@ -0,0 +1,427 @@
<template>
<settings-card
title="命名空间设置"
icon="mdi-database-lock"
:loading="loading"
>
<!-- 命名空间标识符 -->
<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-model="passwordForm.newPassword"
label="新密码"
variant="outlined"
density="comfortable"
hide-details="auto"
class="mb-4"
type="password"
: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-6"
type="password"
: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">
<v-btn
v-if="namespaceInfo.hasPassword"
color="error"
variant="tonal"
:loading="passwordLoading"
@click="showDeleteConfirm = true"
>
删除密码
<template #prepend>
<v-icon icon="mdi-lock-remove" />
</template>
</v-btn>
<v-spacer v-else />
<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="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-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 { kvServerProvider } from "@/utils/providers/kvServerProvider";
import { getSetting } from "@/utils/settings";
import SettingItem from "@/components/settings/SettingItem.vue";
export default {
name: "NamespaceSettingsCard",
components: { SettingsCard, SettingItem },
data() {
return {
loading: false,
passwordLoading: false,
showSnackbar: false,
showDeleteConfirm: false,
snackbarText: "",
snackbarColor: "success",
namespaceInfo: {
uuid: "",
name: "",
accessType: "PUBLIC",
hasPassword: false,
},
namespaceForm: {
name: "",
accessType: "PUBLIC",
},
passwordForm: {
newPassword: "",
confirmPassword: "",
},
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: {
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; // 允许清空密码
}
return (
this.passwordForm.newPassword === this.passwordForm.confirmPassword
);
},
},
async created() {
await this.loadNamespaceInfo();
},
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.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 oldPassword = getSetting("namespace.password");
const response = await kvServerProvider.updatePassword(
this.passwordForm.newPassword || null, // 如果为空字符串则发送null
oldPassword
);
if (response.status === 200) {
this.namespaceInfo.hasPassword = !!this.passwordForm.newPassword;
this.passwordForm = {
newPassword: "",
confirmPassword: "",
};
this.showSuccess("密码已更新");
this.$router.push("/");
} else {
console.log(response);
throw new Error(response.error.message || "保存失败 #1");
}
} catch (error) {
console.error("保存密码失败:", error);
this.showError(error.message || "保存密码失败");
} finally {
this.passwordLoading = false;
}
},
async deletePassword() {
this.passwordLoading = true;
try {
const response = await kvServerProvider.deletePassword();
if (response.status == 200) {
this.namespaceInfo.hasPassword = false;
this.passwordForm = {
newPassword: "",
confirmPassword: "",
};
this.showDeleteConfirm = false;
this.showSuccess("密码已删除");
} else {
throw new Error(response.error.message || "删除失败");
}
} catch (error) {
console.error("删除密码失败:", error);
this.showError(error.message || "删除密码失败");
} finally {
this.passwordLoading = false;
}
},
showSuccess(message) {
this.snackbarColor = "success";
this.snackbarText = message;
this.showSnackbar = true;
},
showError(message) {
this.snackbarColor = "error";
this.snackbarText = message;
this.showSnackbar = true;
},
},
};
</script>