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
12 changes: 6 additions & 6 deletions ui/src/components/COMPONENT_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -357,12 +357,12 @@ Dialog、Drawer、Popover、嵌套区域等其他大、小表格均禁止开启

手动导入 `@/components/mk-edit-avatar/index.vue`,不参与全局注册。字符串 `v-model` 为当前
自定义头像 URL,空字符串表示使用默认图标;不再接收 `defaultIcon`。
`size` 默认为 `32`(px),控制触发区传给插槽的尺寸;弹层预览和上传区域固定为 80px。
`size` 默认为 `32`(px),只设置 `referenceRef` 触发容器的宽高;弹层预览和上传区域固定为 80px。
`editable` 默认为 `true`,设为 `false` 时仅展示头像。

必填默认插槽同时渲染触发区和默认 Logo 预览,透出 `{ icon, size }`。触发区传入当前头像
(空值转为 `undefined`)和 `props.size`;默认预览始终传入 `icon: undefined`、固定的 `size: 80`,不读取组件的
`props.size`。插槽参数由调用方按需使用,不会自动覆盖插槽内组件的尺寸。
必填默认插槽同时渲染触发区和默认 Logo 预览,只透出 `{ icon }`。触发区传入当前头像
(空值转为 `undefined`);默认预览始终传入 `icon: undefined`。两处容器通过样式让插槽的
直接子元素填满各自宽高,不向插槽传递 `size`,也不修改插槽组件的 Props。
调用方必须使用插槽的 `icon`,由 `PortalIcon`、`ToolIcon` 等资源组件自行展示默认图标,
不要在插槽内直接绑定外部头像值。
触发区由 `MkEditAvatar` 的 `span` 容器承载,统一处理悬停,插槽内只放展示内容。
Expand All @@ -384,8 +384,8 @@ const avatar = ref('')

<template>
<MkEditAvatar v-model="avatar">
<template #default="{ icon, size: previewSize }">
<PortalIcon :icon="icon" :size="previewSize" />
<template #default="{ icon }">
<PortalIcon :icon="icon" />
</template>
</MkEditAvatar>
</template>
Expand Down
18 changes: 12 additions & 6 deletions ui/src/components/mk-edit-avatar/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const props = withDefaults(defineProps<{ editable?: boolean; size?: number }>(),
})
const avatar = defineModel<string>({ default: '' })
const emit = defineEmits<{ change: [icon: string, file: File | null] }>()
defineSlots<{ default(props: { icon: string | undefined; size: number }): unknown }>()
defineSlots<{ default(props: { icon: string | undefined }): unknown }>()

// 头像编辑草稿:确认前不修改外部值。
const visible = ref(false)
Expand Down Expand Up @@ -101,8 +101,14 @@ onBeforeUnmount(() => fileReader?.abort())
<el-popover :visible="visible" :width="414" placement="bottom-start" @after-leave="clearDraft">
<template #reference>
<!-- 悬停修改头像 -->
<span ref="referenceRef" class="inline-flex" :class="{ 'cursor-pointer': props.editable }" @mouseenter="open">
<slot :icon="avatar || undefined" :size="props.size" />
<span
ref="referenceRef"
class="inline-flex shrink-0 [&>*]:h-full! [&>*]:w-full!"
:class="{ 'cursor-pointer': props.editable }"
:style="{ width: `${props.size}px`, height: `${props.size}px` }"
@mouseenter="open"
>
<slot :icon="avatar || undefined" />
</span>
</template>

Expand All @@ -112,8 +118,8 @@ onBeforeUnmount(() => fileReader?.abort())
<el-radio value="default">默认 Logo</el-radio>
<el-radio value="custom">自定义上传</el-radio>
</el-radio-group>
<div v-if="logoMode === 'default'">
<slot :icon="undefined" :size="80" />
<div v-if="logoMode === 'default'" class="h-20 w-20 [&>*]:h-full! [&>*]:w-full!">
<slot :icon="undefined" />
</div>

<div v-else>
Expand All @@ -128,7 +134,7 @@ onBeforeUnmount(() => fileReader?.abort())
:on-change="selectImage"
>
<el-avatar v-if="draftIcon" :size="80" shape="square">
<img :src="draftIcon" alt="自定义头像预览" class="object-cover" />
<img :src="draftIcon" class="object-cover" />
</el-avatar>

<div v-else class="flex-center h-20 w-20 rounded-md border border-dashed bg-N200 hover:border-primary">
Expand Down
4 changes: 3 additions & 1 deletion ui/src/views/VIEW_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,9 @@ Dialog。新增或重命名文件时,应同步更新所有导入和页面功
## 备注要求

门户设置的 `components/ButtonEditPortal.vue`、`ButtonPortalAuthSetting.vue` 和
`ButtonPortalCorsSetting.vue` 分别封装对应按钮、浮层及表单状态。认证组件使用 `MkDrawer`,
`ButtonPortalCorsSetting.vue` 分别封装对应按钮、浮层及表单状态,通过 `disabled` 接收页面统一的
编辑限制。页面使用 `perm.system.portal.edit()` 判断门户编辑权限,无权限时禁用配置、复制按钮、
访问开关及浮层表单和按钮,保存入口同步校验权限。认证组件使用 `MkDrawer`,
打开时与系统 `LoginSetting` 共用 `AuthSettingApi.getLoginSetting()`,通过返回的 `login_methods`
生成登录方式选项,复用登录方式标签,初始保留账号登录。提供默认登录方式及验证码与账号锁定设置;默认方式
仅可选已勾选项,取消勾选当前默认项时回退到首个可选项。认证组件暴露 `open()` 供首次开启认证
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import LogoIcon from '@/components/mk-logo/LogoIcon.vue'
import { copyText } from '@/utils/clipboard'
import { resetUrl } from '@/utils/icon'
import { MsgSuccess } from '@/utils/message'
import { perm } from '@/permission'
import PortalPreview from './components/PortalPreview.vue'
import ButtonEditPortal from './components/ButtonEditPortal.vue'
import ButtonPortalAuthSetting from './components/ButtonPortalAuthSetting.vue'
Expand Down Expand Up @@ -49,6 +50,8 @@ function loadPortalSetting() {

/* 保存门户配置:接口返回完整配置,成功后整体回填作为页面唯一数据来源 */
function savePortalSetting(payload: PortalSettingPayload | FormData): Promise<void> {
if (!canEditPortal.value || saving.value) return Promise.resolve()

saving.value = true
return PortalApi.putPortalSetting(payload)
.then((setting) => {
Expand All @@ -61,7 +64,7 @@ function savePortalSetting(payload: PortalSettingPayload | FormData): Promise<vo
}

function handleAccessChange(field: AccessField, value: string | number | boolean) {
if (saving.value) return
if (!canEditPortal.value || saving.value) return
if (field === 'enable_auth' && value && !portalSetting.auth_config.login_value?.length) {
handleOpenAuthSetting(true)
return
Expand All @@ -81,6 +84,8 @@ function savePortalCorsSetting(origins: string[]) {
}

function handleOpenAuthSetting(enableAfterSave = false) {
if (!canEditPortal.value || saving.value) return

authSettingButtonRef.value?.open(enableAfterSave)
}

Expand All @@ -97,6 +102,9 @@ const previewName = computed(() => portalSetting.name)
const previewLogo = computed(() => resetUrl(portalSetting.logo))

onMounted(() => loadPortalSetting())

// 权限 控制
const canEditPortal = computed(() => perm.system.portal.edit())
</script>

<template>
Expand All @@ -111,7 +119,7 @@ onMounted(() => loadPortalSetting())
<h4 class="truncate" :title="previewName">{{ previewName }}</h4>
</div>
<!-- 编辑门户名称与 Logo -->
<ButtonEditPortal :setting="portalSetting" :save="savePortalSetting" />
<ButtonEditPortal :setting="portalSetting" :saving="saving" :disabled="!canEditPortal" :save="savePortalSetting" />
</div>
</el-card>
<el-card shadow="never">
Expand All @@ -122,15 +130,16 @@ onMounted(() => loadPortalSetting())
<div class="flex-between mb-2 gap-2">
<span>门户公开访问链接</span>
<el-switch
:disabled="!canEditPortal || saving"
:model-value="portalSetting.enable_public_access"
size="small"
@change="handleAccessChange('enable_public_access', $event)"
/>
</div>
<el-input :model-value="portalAccessUrl">
<el-input :model-value="portalAccessUrl" readonly>
<template #suffix>
<!-- 复制门户公开访问链接 -->
<el-button text @click="copyText(portalAccessUrl)" class="-mr-1">
<el-button text :disabled="!canEditPortal" @click="copyText(portalAccessUrl)" class="-mr-1">
<MkIcon name="icon_copy_outlined" class="text-N600" />
</el-button>
</template>
Expand All @@ -140,12 +149,17 @@ onMounted(() => loadPortalSetting())
<div>
<div class="flex-between mb-2 gap-2">
<span>智能体后端 API 访问</span>
<el-switch :model-value="portalSetting.enable_api" size="small" @change="handleAccessChange('enable_api', $event)" />
<el-switch
:disabled="!canEditPortal || saving"
:model-value="portalSetting.enable_api"
size="small"
@change="handleAccessChange('enable_api', $event)"
/>
</div>
<el-input :model-value="portalApiUrl">
<el-input :model-value="portalApiUrl" readonly>
<template #suffix>
<!-- 复制智能体后端 API 访问 -->
<el-button text @click="copyText(portalApiUrl)" class="-mr-1">
<el-button text :disabled="!canEditPortal" @click="copyText(portalApiUrl)" class="-mr-1">
<MkIcon name="icon_copy_outlined" class="text-N600" />
</el-button>
</template>
Expand All @@ -156,15 +170,16 @@ onMounted(() => loadPortalSetting())
<div class="flex-between mb-2 gap-2">
<span>知识库后端 API 检索</span>
<el-switch
:disabled="!canEditPortal || saving"
:model-value="portalSetting.enable_knowledge_base_api"
size="small"
@change="handleAccessChange('enable_knowledge_base_api', $event)"
/>
</div>
<el-input :model-value="portalApiUrl">
<el-input :model-value="portalApiUrl" readonly>
<template #suffix>
<!-- 复制知识库后端 API 检索 -->
<el-button text @click="copyText(portalApiUrl)" class="-mr-1">
<el-button text :disabled="!canEditPortal" @click="copyText(portalApiUrl)" class="-mr-1">
<MkIcon name="icon_copy_outlined" class="text-N600" />
</el-button>
</template>
Expand All @@ -174,16 +189,32 @@ onMounted(() => loadPortalSetting())
<span>身份认证</span>
<div class="flex-align-center gap-2">
<!-- 配置身份认证 -->
<ButtonPortalAuthSetting ref="authSettingButtonRef" :setting="portalSetting" :saving="saving" :save="savePortalAuthSetting" />
<el-switch :model-value="portalSetting.enable_auth" size="small" @change="handleAccessChange('enable_auth', $event)" />
<ButtonPortalAuthSetting
ref="authSettingButtonRef"
:setting="portalSetting"
:saving="saving"
:disabled="!canEditPortal"
:save="savePortalAuthSetting"
/>
<el-switch
:disabled="!canEditPortal || saving"
:model-value="portalSetting.enable_auth"
size="small"
@change="handleAccessChange('enable_auth', $event)"
/>
</div>
</div>
<div class="flex-between gap-2">
<span>跨域设置</span>
<div class="flex-align-center gap-2">
<!-- 配置跨域地址 -->
<ButtonPortalCorsSetting v-model="portalCorsOrigins" :saving="saving" :save="savePortalCorsSetting" />
<el-switch :model-value="portalSetting.enable_cors" size="small" @change="handleAccessChange('enable_cors', $event)" />
<ButtonPortalCorsSetting v-model="portalCorsOrigins" :saving="saving" :disabled="!canEditPortal" :save="savePortalCorsSetting" />
<el-switch
:disabled="!canEditPortal || saving"
:model-value="portalSetting.enable_cors"
size="small"
@change="handleAccessChange('enable_cors', $event)"
/>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { FormInstance, FormRules } from 'element-plus'
import type { PortalSetting } from '@/api/types'
import MkEditAvatar from '@/components/mk-edit-avatar/index.vue'

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

/* 门户名称与 Logo */
const editVisible = ref(false)
Expand All @@ -17,6 +17,8 @@ const portalRules: FormRules = {
}

function handleOpenPortalEdit() {
if (props.disabled || props.saving) return

handleClosePortalEdit()
portalForm.name = props.setting.name
portalForm.logo = props.setting.logo || ''
Expand All @@ -31,8 +33,9 @@ function handleLogoChange(icon: string, file: File | null) {
}

function handleSavePortalInfo() {
if (props.disabled || props.saving) return
portalFormRef.value?.validate((valid) => {
if (!valid) return
if (!valid || props.disabled || props.saving) return
const payload = new FormData()
payload.append('name', portalForm.name.trim())
if (logoChanged.value) payload.append('logo', logoFile.value || '')
Expand All @@ -53,14 +56,22 @@ function handleClosePortalEdit() {

<template>
<!-- 编辑门户名称与 Logo -->
<el-button text @click="handleOpenPortalEdit">
<el-button text :disabled="disabled || saving" @click="handleOpenPortalEdit">
<MkIcon name="icon_edit_outlined" />
</el-button>
<MkDialog v-model="editVisible" title="编辑" @closed="handleClosePortalEdit">
<el-form ref="portalFormRef" :model="portalForm" :rules="portalRules" label-position="top" require-asterisk-position="right" @submit.prevent>
<el-form
v-loading="saving"
ref="portalFormRef"
:model="portalForm"
:rules="portalRules"
label-position="top"
require-asterisk-position="right"
@submit.prevent
>
<el-form-item label="名称" prop="name">
<div class="flex-align-center w-full gap-3">
<MkEditAvatar v-model="portalForm.logo" @change="handleLogoChange" :size="32">
<MkEditAvatar :editable="!saving" v-model="portalForm.logo" @change="handleLogoChange" :size="32">
<template #default="{ icon }">
<PortalIcon :icon="icon" />
</template>
Expand All @@ -71,9 +82,9 @@ function handleClosePortalEdit() {
</el-form>
<template #footer>
<!-- 取消编辑 -->
<el-button plain @click="editVisible = false">取消</el-button>
<el-button plain :disabled="saving" @click="editVisible = false">取消</el-button>
<!-- 保存门户信息 -->
<el-button type="primary" @click="handleSavePortalInfo">保存</el-button>
<el-button type="primary" :loading="saving" @click="handleSavePortalInfo">保存</el-button>
</template>
</MkDialog>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import AuthSettingApi from '@/api/admin/system/settings/auth-setting'
import type { LoginMethod, PortalSetting, PortalSettingPayload } from '@/api/types'
import { LOGIN_METHOD_LABELS } from '@/constants/auth'

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

/* 登录方式与默认登录方式 */
const loginMethodOptions = ref<string[]>([LOGIN_METHOD.LOCAL])
Expand Down Expand Up @@ -37,6 +37,8 @@ const authRules: FormRules<typeof authForm> = {

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

enableAuthAfterSave.value = enableAfterSave
const config = props.setting.auth_config
Object.assign(authForm, {
Expand Down Expand Up @@ -66,8 +68,10 @@ function handleClosed() {

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

authFormRef.value?.validate((valid) => {
if (!valid || props.saving || loginMethodsLoading.value) return
if (!valid || props.disabled || props.saving || loginMethodsLoading.value) return
return props
.save({
...(enableAuthAfterSave.value ? { enable_auth: true } : {}),
Expand All @@ -90,12 +94,12 @@ defineExpose({ open })

<template>
<!-- 配置身份认证 -->
<el-button text type="primary" title="身份认证设置" :disabled="saving" @click="handleOpenAuthSetting">
<el-button text type="primary" title="身份认证设置" :disabled="disabled || saving" @click="handleOpenAuthSetting">
<MkIcon name="icon_setting" />
</el-button>
<MkDrawer v-model="authVisible" title="身份认证设置" @closed="handleClosed">
<el-form
v-loading="loginMethodsLoading"
v-loading="loginMethodsLoading || saving"
ref="authFormRef"
:model="authForm"
:rules="authRules"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref } from 'vue'

const props = defineProps<{ saving?: boolean; save: (origins: string[]) => Promise<void> }>()
const props = defineProps<{ disabled: boolean; saving?: boolean; save: (origins: string[]) => Promise<void> }>()
const allowedOrigins = defineModel<string[]>({ default: () => [] })

/* 跨域地址草稿,取消不修改已保存配置。 */
Expand All @@ -11,6 +11,8 @@ const corsAddressPlaceholder =
'请输入允许的跨域地址,开启后不输入跨域地址则不限制。\n跨域地址一行一个,如:\nhttp://127.0.0.1:5678\nhttps://dataease.io'

function handleOpenCorsSetting() {
if (props.disabled || props.saving) return

corsAddressDraft.value = allowedOrigins.value.join('\n')
corsVisible.value = true
}
Expand All @@ -25,7 +27,7 @@ function parseCorsOrigins(value: string) {
}

function handleSaveCorsSetting() {
if (props.saving) return
if (props.disabled || props.saving) return
return props.save(parseCorsOrigins(corsAddressDraft.value)).then(() => {
corsVisible.value = false
})
Expand All @@ -38,7 +40,7 @@ function handleClosed() {

<template>
<!-- 配置跨域地址 -->
<el-button text type="primary" title="跨域设置" @click="handleOpenCorsSetting">
<el-button text type="primary" title="跨域设置" :disabled="disabled || saving" @click="handleOpenCorsSetting">
<MkIcon name="icon_setting" />
</el-button>
<MkDialog v-model="corsVisible" title="跨域设置" @closed="handleClosed">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ defineProps<{ name: string; logo: string | null }>()
<el-avatar :size="32" class="shrink-0 bg-primary-gradient!">
<img src="@/assets/mk_icon_user_gradient.svg" alt="" style="width: 54%" />
</el-avatar>
<span>用户名</span>
<span>姓名</span>
</div>
</div>
<div class="min-w-0 flex-1 p-6">
Expand Down
Loading
Loading