批量更新

This commit is contained in:
2026-09-20 00:19:08 +08:00
parent fa281363fb
commit c5ed596008
47 changed files with 5385 additions and 1199 deletions
+50
View File
@@ -0,0 +1,50 @@
import request from "@/utils/request";
// 统一认证中心 - 接入应用(OIDC Client)管理
export function getAuthClientList(params) {
return request({
url: "/platform/authClient/list",
method: "get",
params,
});
}
export function getAuthClientDetail(id) {
return request({
url: `/platform/authClient/detail/${id}`,
method: "get",
});
}
export function createAuthClient(data) {
return request({
url: "/platform/authClient/create",
method: "post",
data,
});
}
export function editAuthClient(id, data) {
return request({
url: `/platform/authClient/edit/${id}`,
method: "post",
data,
});
}
/** 重置密钥:返回的新密钥仅此一次显示 */
export function resetAuthClientSecret(id) {
return request({
url: `/platform/authClient/resetSecret/${id}`,
method: "post",
});
}
/** 停用应用(逻辑删除) */
export function deleteAuthClient(id) {
return request({
url: `/platform/authClient/delete/${id}`,
method: "delete",
});
}
+27
View File
@@ -0,0 +1,27 @@
import request from "@/utils/request";
// 统一认证中心 - 租户登录策略配置
export function getAuthConfig(tid) {
return request({
url: "/platform/authConfig/detail",
method: "get",
params: { tid },
});
}
export function saveAuthConfig(data) {
return request({
url: "/platform/authConfig/save",
method: "post",
data,
});
}
/** 恢复默认值(删除该租户的自定义配置) */
export function resetAuthConfig(tid) {
return request({
url: `/platform/authConfig/reset/${tid}`,
method: "delete",
});
}
+43
View File
@@ -0,0 +1,43 @@
import request from "@/utils/request";
// 统一认证中心 - 第三方登录配置(tid=0 全局 / tid>0 租户自带)
export function getAuthIdpList(tid) {
return request({
url: "/platform/authIdp/list",
method: "get",
params: { tid },
});
}
export function createAuthIdp(data) {
return request({
url: "/platform/authIdp/create",
method: "post",
data,
});
}
export function editAuthIdp(id, data) {
return request({
url: `/platform/authIdp/edit/${id}`,
method: "post",
data,
});
}
export function deleteAuthIdp(id) {
return request({
url: `/platform/authIdp/delete/${id}`,
method: "delete",
});
}
/** 获取某平台需要填写的回调地址 */
export function getAuthIdpCallbackHint(provider) {
return request({
url: "/platform/authIdp/callbackHint",
method: "get",
params: { provider },
});
}
+21
View File
@@ -21,6 +21,27 @@ const staticMainChildren = [
component: () => import("@/views/system/email/index.vue"),
meta: { requiresAuth: true, title: "邮箱管理" }
},
// 统一认证中心:接入应用(OIDC Client)注册管理
{
path: "/system/authClient",
name: "AuthClientManage",
component: () => import("@/views/system/authClient/index.vue"),
meta: { requiresAuth: true, title: "应用注册" }
},
// 统一认证中心:第三方登录配置(全局 / 租户自带)
{
path: "/system/authIdp",
name: "AuthIdpManage",
component: () => import("@/views/system/authIdp/index.vue"),
meta: { requiresAuth: true, title: "第三方登录" }
},
// 统一认证中心:租户登录策略(验证方式/会话时长/1号1机)
{
path: "/system/authConfig",
name: "AuthConfigManage",
component: () => import("@/views/system/authConfig/index.vue"),
meta: { requiresAuth: true, title: "登录策略" }
},
{
path: "/tools/passwordStore",
name: "PlatformPasswordStore",
@@ -0,0 +1,313 @@
<template>
<div class="auth-client-page">
<div class="page-header">
<div class="title">接入应用(统一认证)</div>
<el-button type="primary" @click="openCreate">新增应用</el-button>
</div>
<div class="tips">
以后每开发一个新软件,只需在这里注册一条即可接入统一认证:填写 Client ID、应用编码和回跳地址白名单。
</div>
<el-table :data="list" v-loading="loading" border size="default">
<el-table-column prop="client_id" label="Client ID" min-width="150" />
<el-table-column prop="app_code" label="应用编码" width="110" />
<el-table-column prop="name" label="应用名称" min-width="130" />
<el-table-column label="类型" width="100">
<template #default="{ row }">{{ appTypeText(row.app_type) }}</template>
</el-table-column>
<el-table-column prop="realm" label="域" width="90" />
<el-table-column label="回跳地址白名单" min-width="200" show-overflow-tooltip>
<template #default="{ row }">{{ row.redirect_uris || "-" }}</template>
</el-table-column>
<el-table-column label="令牌有效期" width="140">
<template #default="{ row }">
{{ row.access_ttl }}s / {{ Math.floor(row.refresh_ttl / 86400) }}天
</template>
</el-table-column>
<el-table-column label="状态" width="90">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'">
{{ row.status === 1 ? "启用" : "停用" }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button
link
type="warning"
:disabled="row.app_type !== 1"
@click="handleResetSecret(row)"
>重置密钥</el-button
>
<el-button link type="danger" :disabled="row.status !== 1" @click="handleDelete(row)"
>停用</el-button
>
</template>
</el-table-column>
</el-table>
<el-dialog
v-model="dialogVisible"
:title="editing ? '编辑应用' : '新增应用'"
width="640px"
destroy-on-close
>
<el-form :model="form" label-width="130px">
<el-form-item label="Client ID" required>
<el-input
v-model="form.client_id"
placeholder="如 yz-crm"
:disabled="editing"
/>
</el-form-item>
<el-form-item label="应用编码" required>
<el-input v-model="form.app_code" placeholder="如 crm" :disabled="editing" />
</el-form-item>
<el-form-item label="应用名称" required>
<el-input v-model="form.name" placeholder="如 客户关系管理" />
</el-form-item>
<el-form-item label="应用类型">
<el-select v-model="form.app_type">
<el-option :value="1" label="Web 后端(持有密钥)" />
<el-option :value="2" label="单页应用 SPA(PKCE)" />
<el-option :value="3" label="原生 APP(PKCE)" />
<el-option :value="4" label="小程序(PKCE)" />
</el-select>
</el-form-item>
<el-form-item label="域">
<el-select v-model="form.realm">
<el-option value="tenant" label="租户域" />
<el-option value="platform" label="平台域" />
</el-select>
</el-form-item>
<el-form-item label="回跳地址白名单">
<el-input
v-model="form.redirect_uris"
type="textarea"
:rows="3"
placeholder='JSON 数组,如 ["https://back.yunzer.cn/#/auth/callback"]'
/>
</el-form-item>
<el-form-item label="登出回跳地址">
<el-input
v-model="form.post_logout_uris"
type="textarea"
:rows="2"
placeholder='JSON 数组,如 ["https://back.yunzer.cn/#/login"]'
/>
</el-form-item>
<el-form-item label="后端登出通知">
<el-input
v-model="form.backchannel_logout_uri"
placeholder="单点登出回调地址(服务端通知,可留空)"
/>
</el-form-item>
<el-form-item label="访问令牌有效期">
<el-input-number v-model="form.access_ttl" :min="300" :step="300" /> 秒
</el-form-item>
<el-form-item label="刷新令牌有效期">
<el-input-number v-model="form.refresh_ttl" :min="3600" :step="86400" /> 秒
</el-form-item>
<el-form-item label="状态" v-if="editing">
<el-switch v-model="form.statusOn" active-text="启用" inactive-text="停用" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="submit">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import {
getAuthClientList,
createAuthClient,
editAuthClient,
resetAuthClientSecret,
deleteAuthClient,
} from "@/api/authClient";
const list = ref([]);
const loading = ref(false);
const dialogVisible = ref(false);
const editing = ref(false);
const submitting = ref(false);
const currentId = ref(0);
const emptyForm = () => ({
client_id: "",
app_code: "",
name: "",
app_type: 2,
realm: "tenant",
redirect_uris: "",
post_logout_uris: "",
backchannel_logout_uri: "",
access_ttl: 1800,
refresh_ttl: 2592000,
statusOn: true,
});
const form = reactive(emptyForm());
function appTypeText(t) {
return { 1: "Web 后端", 2: "SPA", 3: "原生 APP", 4: "小程序" }[t] || "-";
}
async function loadList() {
loading.value = true;
try {
const res = await getAuthClientList();
list.value = res?.data?.list || [];
} catch (e) {
ElMessage.error(e?.message || "加载失败");
} finally {
loading.value = false;
}
}
function openCreate() {
editing.value = false;
Object.assign(form, emptyForm());
dialogVisible.value = true;
}
function openEdit(row) {
editing.value = true;
currentId.value = row.id;
Object.assign(form, {
client_id: row.client_id || "",
app_code: row.app_code || "",
name: row.name || "",
app_type: row.app_type || 2,
realm: row.realm || "tenant",
redirect_uris: row.redirect_uris || "",
post_logout_uris: row.post_logout_uris || "",
backchannel_logout_uri: row.backchannel_logout_uri || "",
access_ttl: row.access_ttl || 1800,
refresh_ttl: row.refresh_ttl || 2592000,
statusOn: row.status === 1,
});
dialogVisible.value = true;
}
async function submit() {
if (!form.client_id.trim() || !form.app_code.trim() || !form.name.trim()) {
ElMessage.warning("Client ID、应用编码、名称不能为空");
return;
}
submitting.value = true;
try {
const payload = {
name: form.name.trim(),
app_type: form.app_type,
realm: form.realm,
redirect_uris: form.redirect_uris.trim(),
post_logout_uris: form.post_logout_uris.trim(),
backchannel_logout_uri: form.backchannel_logout_uri.trim(),
access_ttl: form.access_ttl,
refresh_ttl: form.refresh_ttl,
};
if (editing.value) {
payload.status = form.statusOn ? 1 : 0;
await editAuthClient(currentId.value, payload);
ElMessage.success("保存成功");
} else {
payload.client_id = form.client_id.trim();
payload.app_code = form.app_code.trim();
const res = await createAuthClient(payload);
ElMessage.success("创建成功");
const secret = res?.data?.client_secret;
if (secret) {
await ElMessageBox.alert(
`Client Secret:${secret}`,
"请立即保存密钥(仅此一次显示)",
{ confirmButtonText: "我已保存" }
);
}
}
dialogVisible.value = false;
loadList();
} catch (e) {
ElMessage.error(e?.message || "保存失败");
} finally {
submitting.value = false;
}
}
async function handleResetSecret(row) {
try {
await ElMessageBox.confirm(
`重置后旧密钥立即失效,使用旧密钥的应用需要更新配置。确定重置「${row.name}」的密钥吗?`,
"重置密钥",
{ type: "warning" }
);
} catch {
return;
}
try {
const res = await resetAuthClientSecret(row.id);
await ElMessageBox.alert(
`新的 Client Secret:${res?.data?.client_secret}`,
"请立即保存密钥(仅此一次显示)",
{ confirmButtonText: "我已保存" }
);
} catch (e) {
ElMessage.error(e?.message || "重置失败");
}
}
async function handleDelete(row) {
try {
await ElMessageBox.confirm(
`停用后该应用将无法登录,确定停用「${row.name}」吗?`,
"停用应用",
{ type: "warning" }
);
} catch {
return;
}
try {
await deleteAuthClient(row.id);
ElMessage.success("已停用");
loadList();
} catch (e) {
ElMessage.error(e?.message || "操作失败");
}
}
onMounted(loadList);
</script>
<style scoped>
.auth-client-page {
padding: 16px;
}
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.title {
font-size: 18px;
font-weight: 600;
}
.tips {
background: #f4f6ff;
border: 1px solid #d9e0ff;
color: #5a6072;
font-size: 13px;
padding: 10px 12px;
border-radius: 6px;
margin-bottom: 12px;
}
</style>
@@ -0,0 +1,256 @@
<template>
<div class="auth-config-page">
<div class="page-header">
<div class="title">租户登录策略</div>
<el-button type="primary" :loading="submitting" @click="submit">保存</el-button>
</div>
<div class="scope-bar">
<span class="label">选择租户</span>
<el-select
v-model="tid"
filterable
style="width: 280px"
placeholder="请选择租户"
@change="loadConfig"
>
<el-option
v-for="t in tenants"
:key="t.id"
:value="t.id"
:label="t.tenant_name"
/>
</el-select>
<el-button link type="danger" :disabled="!tid || !configured" @click="handleReset">
恢复默认
</el-button>
</div>
<div v-if="!tid" class="empty">请先选择一个租户</div>
<el-form v-else v-loading="loading" :model="form" label-width="160px" class="form-body">
<el-divider content-position="left">登录验证</el-divider>
<el-form-item label="开启登录验证">
<el-switch v-model="form.openVerifyOn" active-text="开启" inactive-text="关闭" />
</el-form-item>
<el-form-item label="验证方式">
<el-select v-model="form.verify_type" style="width: 220px">
<el-option value="captcha" label="图形验证码" />
<el-option value="sms" label="短信验证码" />
<el-option value="email" label="邮箱验证码" />
<el-option value="geetest" label="极验人机验证" />
<el-option value="none" label="不开启" />
</el-select>
</el-form-item>
<el-form-item label="开启二次验证(MFA)">
<el-switch v-model="form.mfaOn" active-text="必须" inactive-text="不要求" />
<span class="hint-inline">MFA 功能尚未上线,此配置暂不生效</span>
</el-form-item>
<el-divider content-position="left">密码策略</el-divider>
<el-form-item label="密码最小长度">
<el-input-number v-model="form.pwd_min_len" :min="6" :max="32" />
</el-form-item>
<el-form-item label="密码复杂度">
<el-select v-model="form.pwd_complexity" style="width: 220px">
<el-option :value="0" label="不限制" />
<el-option :value="1" label="字母 + 数字" />
<el-option :value="2" label="大小写 + 数字 + 符号" />
</el-select>
</el-form-item>
<el-divider content-position="left">会话与设备</el-divider>
<el-form-item label="会话有效期">
<el-input-number v-model="form.session_ttl" :min="600" :step="600" /> 秒
<span class="hint-inline">约 {{ Math.floor(form.session_ttl / 3600) }} 小时</span>
</el-form-item>
<el-form-item label="最大同时在线">
<el-input-number v-model="form.max_session" :min="1" :max="10" />
<span class="hint-inline">1 = 一号一机(新登录踢掉旧设备)</span>
</el-form-item>
<el-form-item label="超出限制时">
<el-radio-group v-model="form.kick_strategy">
<el-radio :value="1">踢掉最早的会话</el-radio>
<el-radio :value="2">拒绝新登录</el-radio>
</el-radio-group>
</el-form-item>
<el-divider content-position="left">访问控制</el-divider>
<el-form-item label="IP 白名单">
<el-input
v-model="form.ip_whitelist"
type="textarea"
:rows="2"
placeholder='JSON 数组,如 ["1.2.3.4","8.8.8.8"],留空表示不限制'
/>
</el-form-item>
<el-form-item label="允许的第三方登录">
<el-input
v-model="form.allow_third"
placeholder="逗号分隔,如 wechat,dingtalk;留空表示按全局配置"
/>
</el-form-item>
</el-form>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { getAuthConfig, saveAuthConfig, resetAuthConfig } from "@/api/authConfig";
import { getTenantList } from "@/api/tenant";
const tenants = ref([]);
const tid = ref(0);
const loading = ref(false);
const submitting = ref(false);
const configured = ref(false);
const emptyForm = () => ({
verify_type: "captcha",
openVerifyOn: true,
mfaOn: false,
pwd_min_len: 8,
pwd_complexity: 0,
session_ttl: 7200,
max_session: 1,
kick_strategy: 1,
ip_whitelist: "",
allow_third: "",
});
const form = reactive(emptyForm());
async function loadTenants() {
try {
const res = await getTenantList({ page: 1, pageSize: 200 });
const data = res?.data || res || {};
tenants.value = data.list || data.rows || [];
} catch (e) {
/* 忽略 */
}
}
async function loadConfig() {
if (!tid.value) return;
loading.value = true;
try {
const res = await getAuthConfig(tid.value);
const d = res?.data || {};
configured.value = !!d.configured;
Object.assign(form, {
verify_type: d.verify_type || "captcha",
openVerifyOn: d.open_verify !== 0,
mfaOn: d.mfa_required === 1,
pwd_min_len: d.pwd_min_len || 8,
pwd_complexity: d.pwd_complexity || 0,
session_ttl: d.session_ttl || 7200,
max_session: d.max_session || 1,
kick_strategy: d.kick_strategy || 1,
ip_whitelist: d.ip_whitelist || "",
allow_third: d.allow_third || "",
});
} catch (e) {
ElMessage.error(e?.message || "加载失败");
} finally {
loading.value = false;
}
}
async function submit() {
if (!tid.value) {
ElMessage.warning("请先选择租户");
return;
}
submitting.value = true;
try {
await saveAuthConfig({
tid: tid.value,
verify_type: form.verify_type,
open_verify: form.openVerifyOn ? 1 : 0,
mfa_required: form.mfaOn ? 1 : 0,
pwd_min_len: form.pwd_min_len,
pwd_complexity: form.pwd_complexity,
session_ttl: form.session_ttl,
max_session: form.max_session,
kick_strategy: form.kick_strategy,
ip_whitelist: form.ip_whitelist.trim(),
allow_third: form.allow_third.trim(),
});
ElMessage.success("保存成功");
configured.value = true;
} catch (e) {
ElMessage.error(e?.message || "保存失败");
} finally {
submitting.value = false;
}
}
async function handleReset() {
try {
await ElMessageBox.confirm(
"恢复默认后将删除该租户的自定义策略,使用系统默认值。确定吗?",
"恢复默认",
{ type: "warning" }
);
} catch {
return;
}
try {
await resetAuthConfig(tid.value);
ElMessage.success("已恢复默认");
loadConfig();
} catch (e) {
ElMessage.error(e?.message || "操作失败");
}
}
onMounted(loadTenants);
</script>
<style scoped>
.auth-config-page {
padding: 16px;
}
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.title {
font-size: 18px;
font-weight: 600;
}
.scope-bar {
display: flex;
align-items: center;
gap: 10px;
background: #f4f6ff;
border: 1px solid #d9e0ff;
padding: 10px 12px;
border-radius: 6px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.label {
font-size: 13px;
color: #5a6072;
}
.form-body {
max-width: 720px;
}
.hint-inline {
margin-left: 10px;
font-size: 12px;
color: #8a8fa3;
}
.empty {
text-align: center;
color: #8a8fa3;
padding: 40px 0;
}
</style>
+303
View File
@@ -0,0 +1,303 @@
<template>
<div class="auth-idp-page">
<div class="page-header">
<div class="title">第三方登录配置</div>
<el-button type="primary" @click="openCreate">新增配置</el-button>
</div>
<div class="scope-bar">
<span class="label">配置范围</span>
<el-select v-model="tid" style="width: 260px" @change="loadList">
<el-option :value="0" label="平台全局(所有租户共用)" />
<el-option
v-for="t in tenants"
:key="t.id"
:value="t.id"
:label="`租户:${t.tenant_name}`"
/>
</el-select>
<span class="hint">
全局配置对所有租户生效;租户自带配置仅对该租户生效,且优先级更高。
</span>
</div>
<el-table :data="list" v-loading="loading" border>
<el-table-column prop="provider" label="登录方式" width="120">
<template #default="{ row }">{{ providerName(row.provider) }}</template>
</el-table-column>
<el-table-column prop="name" label="名称" width="130" />
<el-table-column prop="app_id" label="AppID" min-width="180" show-overflow-tooltip />
<el-table-column label="密钥" width="90">
<template #default="{ row }">
<el-tag :type="row.has_secret ? 'success' : 'info'">
{{ row.has_secret ? "已配置" : "未配置" }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="proxy_url" label="代理" min-width="140" show-overflow-tooltip>
<template #default="{ row }">{{ row.proxy_url || "-" }}</template>
</el-table-column>
<el-table-column label="状态" width="90">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'">
{{ row.status === 1 ? "启用" : "停用" }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button link type="info" @click="showCallback(row)">回调地址</el-button>
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog
v-model="dialogVisible"
:title="editing ? '编辑配置' : '新增配置'"
width="620px"
destroy-on-close
>
<el-form :model="form" label-width="120px">
<el-form-item label="登录方式" required>
<el-select v-model="form.provider" :disabled="editing" style="width: 100%">
<el-option v-for="p in providers" :key="p.value" :value="p.value" :label="p.label" />
</el-select>
</el-form-item>
<el-form-item label="显示名称">
<el-input v-model="form.name" placeholder="如 微信登录" />
</el-form-item>
<el-form-item label="AppID / ClientID" required>
<el-input v-model="form.app_id" placeholder="第三方平台申请的 AppID" />
</el-form-item>
<el-form-item label="AppSecret">
<el-input
v-model="form.app_secret"
type="password"
show-password
:placeholder="editing && form.has_secret ? '留空表示不修改' : '第三方平台申请的密钥'"
/>
</el-form-item>
<el-form-item label="代理地址">
<el-input
v-model="form.proxy_url"
placeholder="GitHub/Google 在国内服务器需要,如 http://127.0.0.1:7890"
/>
</el-form-item>
<el-form-item label="授权范围">
<el-input v-model="form.scopes" placeholder="留空使用预设值,多个用逗号分隔" />
</el-form-item>
<el-form-item label="状态" v-if="editing">
<el-switch v-model="form.statusOn" active-text="启用" inactive-text="停用" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="submit">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import {
getAuthIdpList,
createAuthIdp,
editAuthIdp,
deleteAuthIdp,
getAuthIdpCallbackHint,
} from "@/api/authIdp";
import { getTenantList } from "@/api/tenant";
const providers = [
{ value: "wechat", label: "微信" },
{ value: "dingtalk", label: "钉钉" },
{ value: "feishu", label: "飞书" },
{ value: "qq", label: "QQ" },
{ value: "github", label: "GitHub" },
{ value: "google", label: "Google" },
// 租户自带身份源(企业自有)
{ value: "wework", label: "企业微信(租户自带)" },
{ value: "oidc", label: "标准 OIDC(租户自带)" },
{ value: "saml", label: "SAML(租户自带)" },
];
const list = ref([]);
const tenants = ref([]);
const loading = ref(false);
const tid = ref(0);
const dialogVisible = ref(false);
const editing = ref(false);
const submitting = ref(false);
const currentId = ref(0);
const emptyForm = () => ({
provider: "wechat",
name: "",
app_id: "",
app_secret: "",
proxy_url: "",
scopes: "",
statusOn: true,
has_secret: false,
});
const form = reactive(emptyForm());
function providerName(p) {
const hit = providers.find((i) => i.value === p);
return hit ? hit.label : p;
}
async function loadList() {
loading.value = true;
try {
const res = await getAuthIdpList(tid.value);
list.value = res?.data?.list || [];
} catch (e) {
ElMessage.error(e?.message || "加载失败");
} finally {
loading.value = false;
}
}
async function loadTenants() {
try {
const res = await getTenantList({ page: 1, pageSize: 200 });
const data = res?.data || res || {};
tenants.value = data.list || data.rows || [];
} catch (e) {
// 租户列表加载失败不影响主功能
}
}
function openCreate() {
editing.value = false;
Object.assign(form, emptyForm());
dialogVisible.value = true;
}
function openEdit(row) {
editing.value = true;
currentId.value = row.id;
Object.assign(form, {
provider: row.provider || "wechat",
name: row.name || "",
app_id: row.app_id || "",
app_secret: "",
proxy_url: row.proxy_url || "",
scopes: row.scopes || "",
statusOn: row.status === 1,
has_secret: !!row.has_secret,
});
dialogVisible.value = true;
}
async function submit() {
if (!form.provider || !form.app_id.trim()) {
ElMessage.warning("登录方式和 AppID 不能为空");
return;
}
submitting.value = true;
try {
const payload = {
provider: form.provider,
name: form.name.trim(),
app_id: form.app_id.trim(),
proxy_url: form.proxy_url.trim(),
scopes: form.scopes.trim(),
};
if (form.app_secret.trim()) payload.app_secret = form.app_secret.trim();
if (editing.value) {
payload.status = form.statusOn ? 1 : 0;
await editAuthIdp(currentId.value, payload);
} else {
payload.tid = tid.value;
await createAuthIdp(payload);
}
ElMessage.success("保存成功");
dialogVisible.value = false;
loadList();
} catch (e) {
ElMessage.error(e?.message || "保存失败");
} finally {
submitting.value = false;
}
}
async function showCallback(row) {
try {
const res = await getAuthIdpCallbackHint(row.provider);
await ElMessageBox.alert(
res?.data?.callback || "",
`${providerName(row.provider)} 回调地址(填到第三方平台)`,
{ confirmButtonText: "复制好了" }
);
} catch (e) {
ElMessage.error(e?.message || "获取失败");
}
}
async function handleDelete(row) {
try {
await ElMessageBox.confirm(
`确定删除「${providerName(row.provider)}」的配置吗?删除后该登录方式将不可用。`,
"删除配置",
{ type: "warning" }
);
} catch {
return;
}
try {
await deleteAuthIdp(row.id);
ElMessage.success("已删除");
loadList();
} catch (e) {
ElMessage.error(e?.message || "删除失败");
}
}
onMounted(() => {
loadTenants();
loadList();
});
</script>
<style scoped>
.auth-idp-page {
padding: 16px;
}
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.title {
font-size: 18px;
font-weight: 600;
}
.scope-bar {
display: flex;
align-items: center;
gap: 10px;
background: #f4f6ff;
border: 1px solid #d9e0ff;
padding: 10px 12px;
border-radius: 6px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.label {
font-size: 13px;
color: #5a6072;
}
.hint {
font-size: 12px;
color: #8a8fa3;
}
</style>
+4 -2
View File
@@ -28,8 +28,10 @@ export default defineConfig({
},
server: {
host: "127.0.0.1",
port: 4000,
// 开发时前端在 4000,接口走相对路径 /platform/*、/backend/*,转发到本地 Go(当前 httpport=9000)
port: 4400,
// 端口被占用时直接报错,不再自动漂移到其他端口(避免回跳地址与实际端口不一致)
strictPort: true,
// 开发时前端在 4400,接口走相对路径 /platform/*、/backend/*,转发到本地 Go(当前 httpport=9000)
proxy: {
"/platform": {
target: "http://127.0.0.1:9000",