11
This commit is contained in:
@@ -15,6 +15,7 @@ const baseMenuItems = [
|
||||
{ name: "extract", label: "提取", to: "/extract", icon: "extract" },
|
||||
{ name: "design", label: "设计", to: "/design", icon: "design" },
|
||||
{ name: "model", label: "模型", to: "/model", icon: "model" },
|
||||
{ name: "local-config", label: "本地配置", to: "/local-config", icon: "settings" },
|
||||
{ name: "help", label: "帮助", to: "/help", icon: "help" },
|
||||
];
|
||||
|
||||
@@ -81,6 +82,13 @@ const activeName = computed(() => {
|
||||
<path d="M12 3l7 4v10l-7 4-7-4V7l7-4z" />
|
||||
<path d="M12 3v18M5 7l7 4 7-4" />
|
||||
</svg>
|
||||
<svg v-else-if="item.icon === 'settings'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path
|
||||
d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<svg v-else-if="item.icon === 'help'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<path d="M9.5 9a2.5 2.5 0 1 1 4.2 1.8c-.8.7-1.2 1.2-1.2 2.2M12 17h.01" stroke-linecap="round" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 流水线配置校验:优先使用 Rust 缓存的 desktop_configs(get_app_config)。
|
||||
* 流水线配置校验:Rust 合并服务端 desktop_configs + 本地 SQLite(同名本地优先)。
|
||||
* 实际密钥由 Node 子进程通过环境变量 AICLIENT_CFG_* 读取,无需在前端 params 传递。
|
||||
*/
|
||||
|
||||
@@ -17,7 +17,8 @@ export async function ensureAppConfigReady() {
|
||||
if (!apiKey) {
|
||||
return {
|
||||
ok: false,
|
||||
message: "服务端配置缺少 LLM_API_KEY,请在 desktop_configs 表中维护",
|
||||
message:
|
||||
"缺少 LLM_API_KEY,请在管理后台 desktop_configs 或侧栏「本地配置」中设置",
|
||||
};
|
||||
}
|
||||
return { ok: true };
|
||||
@@ -28,12 +29,12 @@ export async function ensureAppConfigReady() {
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
message: "未获取到应用配置,请先登录并确保 desktop_configs 表有数据",
|
||||
message: "未获取到应用配置,请登录服务端或在「本地配置」中添加 LLM_API_KEY",
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
ok: false,
|
||||
message: "请在 Tauri 桌面端登录后使用(配置由服务端下发)",
|
||||
message: "请在 Tauri 桌面端使用,并登录或在「本地配置」中设置密钥",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +116,18 @@ const mainChildren = [
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
path: "local-config",
|
||||
|
||||
name: "local-config",
|
||||
|
||||
component: () => import("../views/LocalConfigView.vue"),
|
||||
|
||||
meta: { title: "本地配置" },
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
path: "admin",
|
||||
|
||||
238
src/views/LocalConfigView.vue
Normal file
238
src/views/LocalConfigView.vue
Normal 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>
|
||||
Reference in New Issue
Block a user