Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion ui/src/api/types/portal.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import type { Dict } from './common'
import type { Dict, OptionItem } from './common'

export interface PortalAuthConfig extends Dict<unknown> {
type?: string
login_value?: string[]
max_attempts?: number
failed_attempts?: number
lock_time?: number
system_options?: OptionItem<string>[]
auth_types?: OptionItem<string>[]
}

export interface PortalSetting {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,54 +1,64 @@
<script setup lang="ts">
import { reactive, ref, useTemplateRef } from 'vue'
import { computed, reactive, ref, useTemplateRef } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { LOGIN_METHOD } from '@/api/enums'
import AuthSettingApi from '@/api/admin/system/settings/auth-setting'
import type { LoginMethod, PortalSetting, PortalSettingPayload } from '@/api/types'
import type { LoginMethod, OptionItem, PortalSetting, PortalSettingPayload } from '@/api/types'
import { LOGIN_METHOD_LABELS } from '@/constants/auth'

const props = defineProps<{ disabled: boolean; setting: PortalSetting; saving: boolean; save: (payload: PortalSettingPayload) => Promise<void> }>()

/* 登录方式与默认登录方式 */
const loginMethodOptions = ref<string[]>([LOGIN_METHOD.LOCAL])
const loginMethodsLoading = ref(false)
/* 可用登录方式与默认登录方式均取自后端返回的门户 auth_config */
const authConfig = computed(() => props.setting.auth_config ?? {})

const loginMethodOptions = computed<string[]>(() => {
const values = (authConfig.value.system_options ?? []).map((option) => option.value)
return values.length ? values : [LOGIN_METHOD.LOCAL]
})

const defaultLoginMethodOptions = computed<OptionItem<string>[]>(() => {
const authTypes = authConfig.value.auth_types ?? []
return authTypes.length ? authTypes : (authConfig.value.system_options ?? [])
})

function loadLoginMethodOptions() {
loginMethodsLoading.value = true
return AuthSettingApi.getLoginSetting()
.then((setting) => {
loginMethodOptions.value = setting.login_methods ?? [LOGIN_METHOD.LOCAL]
})
.finally(() => {
loginMethodsLoading.value = false
})
}
const authVisible = ref(false)
const enableAuthAfterSave = ref(false)
const authFormRef = useTemplateRef<FormInstance>('authFormRef')
const authForm = reactive({
loginMethods: [LOGIN_METHOD.LOCAL] as string[],
default_value: LOGIN_METHOD.LOCAL as string,
maxAttempts: 1,
failedAttempts: 5,
lockTime: 10,
})
const authRules: FormRules<typeof authForm> = {
loginMethods: [{ type: 'array', required: true, min: 1, message: '请选择至少一种登录方式', trigger: 'change' }],
default_value: [{ required: true, message: '请选择默认登录方式', trigger: 'change' }],
}

/* 登录方式变化时,确保默认登录方式仍为已启用的方式 */
function handleLoginMethodsChange(loginMethods: string[]) {
if (!loginMethods.includes(authForm.default_value)) {
authForm.default_value = loginMethods[0] ?? LOGIN_METHOD.LOCAL
}
}

/* 抽屉回填与重置,草稿不直接修改门户配置 */
function open(enableAfterSave = false) {
if (props.disabled || props.saving) return

enableAuthAfterSave.value = enableAfterSave
const config = props.setting.auth_config
const config = authConfig.value
const loginMethods = [...(config.login_value?.length ? config.login_value : [LOGIN_METHOD.LOCAL])]
const defaultType = config.type ?? LOGIN_METHOD.LOCAL
const default_value = loginMethods.includes(defaultType) ? defaultType : loginMethods[0] ?? LOGIN_METHOD.LOCAL
Object.assign(authForm, {
loginMethods: [...(config.login_value ?? [LOGIN_METHOD.LOCAL])],
loginMethods,
default_value,
maxAttempts: config.max_attempts ?? 1,
failedAttempts: config.failed_attempts ?? 5,
lockTime: config.lock_time ?? 10,
})
authVisible.value = true
loadLoginMethodOptions()
}

function handleOpenAuthSetting() {
Expand All @@ -59,6 +69,7 @@ function handleClosed() {
enableAuthAfterSave.value = false
Object.assign(authForm, {
loginMethods: [LOGIN_METHOD.LOCAL],
default_value: LOGIN_METHOD.LOCAL,
maxAttempts: 1,
failedAttempts: 5,
lockTime: 10,
Expand All @@ -68,16 +79,17 @@ function handleClosed() {

/* 保存认证配置,仅提交后端读取的字段并保留其余配置 */
function handleSaveAuthSetting() {
if (props.disabled || props.saving || loginMethodsLoading.value) return
if (props.disabled || props.saving) return

authFormRef.value?.validate((valid) => {
if (!valid || props.disabled || props.saving || loginMethodsLoading.value) return
if (!valid || props.disabled || props.saving) return
return props
.save({
...(enableAuthAfterSave.value ? { enable_auth: true } : {}),
auth_config: {
...props.setting.auth_config,
login_value: [...authForm.loginMethods],
type: authForm.default_value,
max_attempts: authForm.maxAttempts,
failed_attempts: authForm.failedAttempts,
lock_time: authForm.lockTime,
Expand All @@ -99,7 +111,7 @@ defineExpose({ open })
</el-button>
<MkDrawer v-model="authVisible" title="身份认证设置" @closed="handleClosed">
<el-form
v-loading="loginMethodsLoading || saving"
v-loading="saving"
ref="authFormRef"
:model="authForm"
:rules="authRules"
Expand All @@ -108,12 +120,22 @@ defineExpose({ open })
@submit.prevent
>
<el-form-item label="登录方式" prop="loginMethods">
<el-checkbox-group v-model="authForm.loginMethods" class="flex-wrap">
<el-checkbox-group v-model="authForm.loginMethods" @change="handleLoginMethodsChange" class="flex-wrap">
<template v-for="loginMethod in loginMethodOptions" :key="loginMethod">
<el-checkbox :value="loginMethod" class="w-32">{{ LOGIN_METHOD_LABELS[loginMethod as LoginMethod] || loginMethod }}</el-checkbox>
</template>
</el-checkbox-group>
</el-form-item>
<el-form-item label="默认登录方式" prop="default_value">
<el-select v-model="authForm.default_value">
<el-option
v-for="loginMethodOption in defaultLoginMethodOptions"
:key="loginMethodOption.value"
:value="loginMethodOption.value"
:label="loginMethodOption.label"
/>
</el-select>
</el-form-item>
<el-form-item label="账号登录验证码设置" required>
<div class="mk-gray-card-lg w-full space-y-4">
<div class="flex-align-center gap-2">
Expand All @@ -135,7 +157,7 @@ defineExpose({ open })
<!-- 取消认证设置 -->
<el-button plain :disabled="saving" @click="authVisible = false">取消</el-button>
<!-- 保存认证设置 -->
<el-button type="primary" :loading="saving" :disabled="loginMethodsLoading" @click="handleSaveAuthSetting">保存</el-button>
<el-button type="primary" :loading="saving" :disabled="saving" @click="handleSaveAuthSetting">保存</el-button>
</template>
</MkDrawer>
</template>
4 changes: 3 additions & 1 deletion ui/src/views/system/chat-management/portal-setting/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import PortalPreview from './components/PortalPreview.vue'
import ButtonEditPortal from './components/ButtonEditPortal.vue'
import ButtonPortalAuthSetting from './components/ButtonPortalAuthSetting.vue'
import ButtonPortalCorsSetting from './components/ButtonPortalCorsSetting.vue'
import { useStore } from '@/stores'

type AccessField = 'enable_public_access' | 'enable_api' | 'enable_knowledge_base_api' | 'enable_auth' | 'enable_cors'

Expand All @@ -32,7 +33,7 @@ const portalSetting = reactive<PortalSetting>({
})
const loading = ref(false)
const saving = ref(false)

const { auth } = useStore()
const portalAccessUrl = new URL('/portal', window.location.origin).href
const portalApiUrl = new URL('/api/portal', window.location.origin).href

Expand Down Expand Up @@ -186,6 +187,7 @@ const canEditPortal = computed(() => perm.system.portal.edit())
<div class="flex-align-center gap-2">
<!-- 配置身份认证 -->
<ButtonPortalAuthSetting
v-if="auth.isPE || auth.isEE"
ref="authSettingButtonRef"
:setting="portalSetting"
:saving="saving"
Expand Down
47 changes: 30 additions & 17 deletions ui/src/views/system/home/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,37 +15,50 @@ defineOptions({ name: 'SystemHomeView' })
const { auth } = useStore()

/* 工作空间筛选 */
const selectedWorkspaceId = ref('default')
const ALL_WORKSPACES = '' // 全部空间标识(空串),传给后端视为 None(所有工作空间)
const selectedWorkspaceId = ref(ALL_WORKSPACES)
const workspaceOptions = ref<WorkspaceItem[]>([])
function loadWorkspaceOptions() {
return WorkspaceApi.getSystemWorkspaceList().then((workspaces) => {
workspaceOptions.value = workspaces
if (!workspaces.some(({ id }) => id === selectedWorkspaceId.value)) {
selectedWorkspaceId.value = workspaces[0]?.id ?? 'default'
workspaceOptions.value = [{ name: '全部空间', id: ALL_WORKSPACES } as WorkspaceItem, ...workspaces]
if (!workspaceOptions.value.some(({ id }) => id === selectedWorkspaceId.value)) {
selectedWorkspaceId.value = workspaceOptions.value[0]?.id ?? ALL_WORKSPACES
}
})
}
function handleWorkspaceSelect(workspace: WorkspaceItem) {
selectedWorkspaceId.value = workspace.id ?? 'default'
selectedWorkspaceId.value = workspace.id ?? ALL_WORKSPACES
}

/* 监控智能体筛选 */
const applicationId = ref('all')
const applications = ref<ApplicationDetail[]>([])
const applicationsLoading = ref(false)
const selectedApplication = ref<ApplicationDetail>()
function loadApplications(query = '') {
async function fetchApplications(query: string) {
applicationsLoading.value = true
return SystemApplicationApi.getApplicationPage(
{ currentPage: 1, pageSize: 200 },
{ name: query, workspace_ids: JSON.stringify([selectedWorkspaceId.value]) },
)
.then((result) => {
applications.value = result.records
})
.finally(() => {
applicationsLoading.value = false
})
const workspaceFilter = selectedWorkspaceId.value ? { workspace_ids: JSON.stringify([selectedWorkspaceId.value]) } : {}
try {
const result = await SystemApplicationApi.getApplicationPage(
{ currentPage: 1, pageSize: 200 },
{ name: query, ...workspaceFilter },
)
applications.value = result.records
} finally {
applicationsLoading.value = false
}
}
/* 远程搜索防抖,避免输入时每次按键都发请求 */
let searchTimer: ReturnType<typeof setTimeout> | undefined
function loadApplications(query = '') {
clearTimeout(searchTimer)
searchTimer = setTimeout(() => fetchApplications(query), 300)
}
/* 打开下拉时才按需加载智能体列表,避免进入页面就全量拉取 */
function handleApplicationVisibleChange(visible: boolean) {
if (visible && !applicationsLoading.value && !applications.value.length) {
loadApplications('')
}
}
function handleApplicationChange(value: string) {
selectedApplication.value = applications.value.find((application) => application.id === value)
Expand All @@ -57,7 +70,6 @@ watch(
applicationId.value = 'all'
applications.value = []
selectedApplication.value = undefined
loadApplications()
},
{ immediate: true },
)
Expand Down Expand Up @@ -97,6 +109,7 @@ onMounted(() => {
:loading="applicationsLoading"
:disabled="loading"
@change="handleApplicationChange"
@visible-change="handleApplicationVisibleChange"
>
<el-option label="全部智能体" value="all">
<div class="flex-align-center gap-2">
Expand Down
Loading