This commit is contained in:
fengchuanhn@gmail.com
2026-05-17 21:13:45 +08:00
parent c18176ba60
commit 9dfeaa9e18
10 changed files with 695 additions and 17 deletions

View File

@@ -0,0 +1,238 @@
<script setup>
import { computed, onMounted, ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
const localItems = ref([]);
const mergedDetail = ref([]);
const loading = ref(false);
const saving = ref(false);
const feedback = ref({ severity: "", message: "" });
const dialogVisible = ref(false);
const dialogMode = ref("create");
const editingName = ref("");
const emptyForm = () => ({
name: "",
value: "",
mark: "",
});
const form = ref(emptyForm());
const dialogTitle = computed(() =>
dialogMode.value === "create" ? "添加本地配置" : "编辑本地配置",
);
const isTauri = computed(() => typeof window !== "undefined" && "__TAURI_INTERNALS__" in window);
function showMessage(severity, message) {
feedback.value = { severity, message };
}
function truncate(value, max = 40) {
if (!value) return "—";
if (value.length <= max) return value;
return `${value.slice(0, max)}`;
}
async function loadAll() {
if (!isTauri.value) {
showMessage("warn", "本地配置仅在 Tauri 桌面端可用");
return;
}
loading.value = true;
feedback.value = { severity: "", message: "" };
try {
const [local, detail] = await Promise.all([
invoke("list_local_app_config"),
invoke("get_app_config_detail"),
]);
localItems.value = Array.isArray(local) ? local : [];
mergedDetail.value = Array.isArray(detail) ? detail : [];
} catch (err) {
showMessage("error", String(err));
} finally {
loading.value = false;
}
}
function openCreateDialog() {
dialogMode.value = "create";
editingName.value = "";
form.value = emptyForm();
dialogVisible.value = true;
}
function openEditDialog(row) {
dialogMode.value = "edit";
editingName.value = row.name;
form.value = {
name: row.name,
value: row.value,
mark: row.mark || "",
};
dialogVisible.value = true;
}
async function saveLocal() {
const name = form.value.name.trim();
if (!name) {
showMessage("warn", "键名不能为空");
return;
}
saving.value = true;
try {
await invoke("set_local_app_config", {
name,
value: form.value.value,
mark: form.value.mark.trim() || null,
});
dialogVisible.value = false;
showMessage("success", "本地配置已保存(同名键将覆盖服务端)");
await loadAll();
} catch (err) {
showMessage("error", String(err));
} finally {
saving.value = false;
}
}
async function removeLocal(row) {
const ok = window.confirm(`删除本地配置「${row.name}」?将恢复使用服务端同名配置。`);
if (!ok) return;
try {
await invoke("delete_local_app_config", { name: row.name });
showMessage("success", "已删除");
await loadAll();
} catch (err) {
showMessage("error", String(err));
}
}
onMounted(() => {
loadAll();
});
</script>
<template>
<div class="local-config-page custom-scrollbar p-6">
<div class="mb-4">
<h1 class="gradient-text text-xl font-semibold">本地配置</h1>
<p class="mt-1 text-sm text-text-muted">
保存在本机 SQLite与登录后服务端下发的配置合并<strong class="text-text-secondary">同名键本地优先</strong>并注入 Node 环境变量
<code class="rounded bg-white/10 px-1 font-mono text-xs">AICLIENT_CFG_*</code>
</p>
</div>
<Message
v-if="feedback.message"
:severity="feedback.severity"
:closable="true"
class="mb-3"
@close="feedback.message = ''"
>
{{ feedback.message }}
</Message>
<div class="mb-4 flex flex-wrap items-center gap-2">
<Button label="添加本地配置" :disabled="!isTauri" @click="openCreateDialog" />
<Button label="刷新" severity="secondary" outlined :loading="loading" @click="loadAll" />
</div>
<h2 class="mb-2 text-sm font-medium text-text-muted">本地配置项</h2>
<DataTable
:value="localItems"
:loading="loading"
striped-rows
size="small"
class="admin-users__table mb-6"
data-key="name"
>
<Column field="name" header="键名">
<template #body="{ data }">
<code class="text-xs">{{ data.name }}</code>
</template>
</Column>
<Column field="value" header="值">
<template #body="{ data }">
<span :title="data.value">{{ truncate(data.value) }}</span>
</template>
</Column>
<Column field="mark" header="备注">
<template #body="{ data }">
{{ data.mark || "—" }}
</template>
</Column>
<Column header="操作" style="width: 8rem">
<template #body="{ data }">
<div class="admin-users__actions">
<Button label="编辑" size="small" text @click="openEditDialog(data)" />
<Button label="删除" size="small" severity="danger" text @click="removeLocal(data)" />
</div>
</template>
</Column>
<template #empty>
<p class="py-4 text-center text-sm text-text-muted">暂无本地配置</p>
</template>
</DataTable>
<h2 class="mb-2 text-sm font-medium text-text-muted">当前生效合并后</h2>
<DataTable
:value="mergedDetail"
:loading="loading"
striped-rows
size="small"
class="admin-users__table"
data-key="name"
>
<Column field="name" header="键名">
<template #body="{ data }">
<code class="text-xs">{{ data.name }}</code>
</template>
</Column>
<Column field="value" header="值">
<template #body="{ data }">
<span :title="data.value">{{ truncate(data.value) }}</span>
</template>
</Column>
<Column field="source" header="来源" style="width: 6rem">
<template #body="{ data }">
<Tag
:value="data.source === 'local' ? '本地' : '服务端'"
:severity="data.source === 'local' ? 'warn' : 'info'"
/>
</template>
</Column>
<template #empty>
<p class="py-4 text-center text-sm text-text-muted">尚无配置请登录或添加本地项</p>
</template>
</DataTable>
<Dialog v-model:visible="dialogVisible" :header="dialogTitle" modal :style="{ width: '28rem' }">
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<label class="text-xs text-text-muted uppercase">键名</label>
<InputText
v-model="form.name"
class="w-full font-mono uppercase"
:disabled="dialogMode === 'edit'"
placeholder="如 LLM_API_KEY"
/>
</div>
<div class="flex flex-col gap-2">
<label class="text-xs text-text-muted uppercase"></label>
<Textarea v-model="form.value" rows="5" class="w-full font-mono text-sm" />
</div>
<div class="flex flex-col gap-2">
<label class="text-xs text-text-muted uppercase">备注仅本地记录</label>
<InputText v-model="form.mark" class="w-full" />
</div>
</div>
<template #footer>
<Button label="取消" severity="secondary" text @click="dialogVisible = false" />
<Button label="保存" :loading="saving" @click="saveLocal" />
</template>
</Dialog>
</div>
</template>