feat: 增强知识库索引与设置页面模块化拆分
扩展知识库索引任务和 RAG 检索支持增量入库和文档去重,优 化本体检测和规则匹配精度,前端设置页面拆分为 LLM、邮件 和 Hermes 员工同步子面板并重构样式,新增日志详情组件和 知识入库日志模型,补充单元测试覆盖。
This commit is contained in:
@@ -3,7 +3,7 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { icons } from '../data/icons.js'
|
||||
|
||||
export const appViews = ['overview', 'workbench', 'requests', 'approval', 'archive', 'policies', 'audit', 'logs', 'employees', 'settings']
|
||||
export const appViews = ['overview', 'workbench', 'requests', 'approval', 'archive', 'policies', 'audit', 'employees', 'logs', 'settings']
|
||||
|
||||
export const navItems = [
|
||||
{
|
||||
@@ -62,14 +62,6 @@ export const navItems = [
|
||||
title: '任务规则中心',
|
||||
desc: '集中管理规则文件、外部 MCP 服务与定时任务调度。'
|
||||
},
|
||||
{
|
||||
id: 'logs',
|
||||
label: '日志管理',
|
||||
navHint: '查看 Hermes 调用与系统运行日志',
|
||||
icon: icons.logs,
|
||||
title: '日志管理',
|
||||
desc: '集中查看 Hermes 归纳任务进度、调用明细与系统运行日志。'
|
||||
},
|
||||
{
|
||||
id: 'employees',
|
||||
label: '员工管理',
|
||||
@@ -78,6 +70,14 @@ export const navItems = [
|
||||
title: '员工与组织管理',
|
||||
desc: '维护员工账号、组织结构与角色权限。'
|
||||
},
|
||||
{
|
||||
id: 'logs',
|
||||
label: '日志管理',
|
||||
navHint: '查看 Hermes 调用与系统运行日志',
|
||||
icon: icons.logs,
|
||||
title: '日志管理',
|
||||
desc: '集中查看 Hermes 归纳任务进度、调用明细与系统运行日志。'
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
label: '系统设置',
|
||||
@@ -101,13 +101,34 @@ const viewRouteNames = {
|
||||
settings: 'app-settings'
|
||||
}
|
||||
|
||||
const routeNameViews = Object.fromEntries(
|
||||
Object.entries(viewRouteNames).map(([view, routeName]) => [routeName, view])
|
||||
)
|
||||
|
||||
routeNameViews['app-request-detail'] = 'requests'
|
||||
routeNameViews['app-log-detail'] = 'logs'
|
||||
|
||||
export function resolveAppViewFromRoute(route) {
|
||||
const routeName = String(route?.name || '').trim()
|
||||
if (routeNameViews[routeName]) {
|
||||
return routeNameViews[routeName]
|
||||
}
|
||||
|
||||
const metaView = String(route?.meta?.appView || '').trim()
|
||||
return appViews.includes(metaView) ? metaView : 'overview'
|
||||
}
|
||||
|
||||
export function resolveTargetRouteName(view) {
|
||||
return viewRouteNames[view] || viewRouteNames.overview
|
||||
}
|
||||
|
||||
export function useNavigation() {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const activeView = computed({
|
||||
get() {
|
||||
return route.meta.appView || 'overview'
|
||||
return resolveAppViewFromRoute(route)
|
||||
},
|
||||
set(view) {
|
||||
setView(view)
|
||||
@@ -119,13 +140,13 @@ export function useNavigation() {
|
||||
)
|
||||
|
||||
function setView(view) {
|
||||
const targetName = viewRouteNames[view] || viewRouteNames.overview
|
||||
const targetName = resolveTargetRouteName(view)
|
||||
|
||||
if (route.name === targetName) {
|
||||
return
|
||||
}
|
||||
|
||||
router.push({ name: targetName })
|
||||
router.push({ name: targetName, params: {}, query: {}, hash: '' })
|
||||
}
|
||||
|
||||
return { activeView, currentView, setView, navItems }
|
||||
|
||||
@@ -13,12 +13,12 @@ const EXPENSE_TYPE_LABELS = {
|
||||
ride_ticket: '乘车',
|
||||
travel_allowance: '出差补贴',
|
||||
entertainment: '业务招待费',
|
||||
office: '办公费',
|
||||
office: '办公用品费',
|
||||
meeting: '会务费',
|
||||
training: '培训费',
|
||||
hotel: '住宿费',
|
||||
transport: '交通费',
|
||||
meal: '餐费',
|
||||
meal: '业务招待费',
|
||||
other: '其他费用'
|
||||
}
|
||||
|
||||
|
||||
486
web/src/composables/useSettings.js
Normal file
486
web/src/composables/useSettings.js
Normal file
@@ -0,0 +1,486 @@
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
import { useSystemState } from './useSystemState.js'
|
||||
import { fetchSettings, saveSettings } from '../services/settings.js'
|
||||
import { useToast } from './useToast.js'
|
||||
import {
|
||||
isHermesEmployeeSettingsReady
|
||||
} from '../utils/hermesEmployeeSettingsModel.js'
|
||||
import {
|
||||
LOG_LEVELS,
|
||||
PROVIDER_OPTIONS,
|
||||
SECTION_DEFINITIONS,
|
||||
SESSION_RETENTION_OPTIONS,
|
||||
buildDefaultState,
|
||||
buildLlmPayload,
|
||||
buildRenderPayload,
|
||||
computeSectionStatus,
|
||||
isModelConfigReady,
|
||||
isRenderSecretMask,
|
||||
maskConfiguredModelSecrets,
|
||||
maskConfiguredRenderSecret,
|
||||
mergeState,
|
||||
normalizeValue,
|
||||
persistSettings,
|
||||
readStoredSettings
|
||||
} from '../utils/settingsModelHelper.js'
|
||||
|
||||
export function useSettings() {
|
||||
const { toast } = useToast()
|
||||
const { companyProfile, currentUser, updateCompanyProfilePreview } = useSystemState()
|
||||
|
||||
const buildResolvedDefaults = () => buildDefaultState(companyProfile.value, currentUser.value)
|
||||
const pageState = ref(mergeState(buildResolvedDefaults(), readStoredSettings()))
|
||||
const activeSection = ref('profile')
|
||||
const sessionRetentionPickerOpen = ref(false)
|
||||
const sessionRetentionPickerRef = ref(null)
|
||||
const logoInputRef = ref(null)
|
||||
|
||||
const sections = SECTION_DEFINITIONS
|
||||
const logLevels = LOG_LEVELS
|
||||
const providerOptions = PROVIDER_OPTIONS
|
||||
const sessionRetentionOptions = SESSION_RETENTION_OPTIONS
|
||||
|
||||
const sectionStatus = computed(() => computeSectionStatus(pageState.value))
|
||||
const completedSectionCount = computed(() => Object.values(sectionStatus.value).filter(Boolean).length)
|
||||
const activeSectionConfig = computed(
|
||||
() => sections.find((section) => section.id === activeSection.value) || sections[0]
|
||||
)
|
||||
|
||||
function updateBrandPreviewFromState(state) {
|
||||
updateCompanyProfilePreview({
|
||||
name: normalizeValue(state.companyForm.displayName),
|
||||
code: normalizeValue(state.companyForm.companyCode),
|
||||
adminEmail: normalizeValue(state.adminForm.adminEmail),
|
||||
logo: state.companyForm.logo
|
||||
})
|
||||
}
|
||||
|
||||
function applyLoadedSnapshot(snapshot, options = {}) {
|
||||
const {
|
||||
mergeDraft = false,
|
||||
preserveModelApiKeys = false,
|
||||
preserveAdminPasswords = false,
|
||||
preserveRenderSecret = false,
|
||||
preserveMailPassword = false
|
||||
} = options
|
||||
|
||||
const currentState = pageState.value
|
||||
let nextState = mergeState(buildResolvedDefaults(), snapshot)
|
||||
|
||||
if (mergeDraft) {
|
||||
nextState = mergeState(nextState, readStoredSettings())
|
||||
}
|
||||
|
||||
if (preserveModelApiKeys) {
|
||||
nextState.llmForm.mainApiKey = currentState.llmForm.mainApiKey
|
||||
nextState.llmForm.backupApiKey = currentState.llmForm.backupApiKey
|
||||
nextState.llmForm.embeddingApiKey = currentState.llmForm.embeddingApiKey
|
||||
nextState.llmForm.rerankerApiKey = currentState.llmForm.rerankerApiKey
|
||||
}
|
||||
|
||||
if (preserveAdminPasswords) {
|
||||
nextState.adminForm.newPassword = currentState.adminForm.newPassword
|
||||
nextState.adminForm.confirmPassword = currentState.adminForm.confirmPassword
|
||||
}
|
||||
|
||||
if (preserveRenderSecret) {
|
||||
nextState.renderForm.jwtSecret = currentState.renderForm.jwtSecret
|
||||
}
|
||||
|
||||
if (preserveMailPassword) {
|
||||
nextState.mailForm.password = currentState.mailForm.password
|
||||
}
|
||||
|
||||
pageState.value = maskConfiguredRenderSecret(maskConfiguredModelSecrets(nextState))
|
||||
persistSettings(pageState.value)
|
||||
updateBrandPreviewFromState(pageState.value)
|
||||
}
|
||||
|
||||
async function loadSettingsSnapshot() {
|
||||
try {
|
||||
const snapshot = await fetchSettings()
|
||||
applyLoadedSnapshot(snapshot, { mergeDraft: true })
|
||||
} catch (error) {
|
||||
persistSettings(pageState.value)
|
||||
updateBrandPreviewFromState(pageState.value)
|
||||
toast(error.message || '无法加载已保存设置,继续使用当前会话草稿。')
|
||||
}
|
||||
}
|
||||
|
||||
function buildSettingsPayload() {
|
||||
return {
|
||||
companyForm: { ...pageState.value.companyForm },
|
||||
adminForm: { ...pageState.value.adminForm },
|
||||
sessionForm: { ...pageState.value.sessionForm },
|
||||
llmForm: buildLlmPayload(pageState.value.llmForm),
|
||||
renderForm: buildRenderPayload(pageState.value.renderForm),
|
||||
logForm: { ...pageState.value.logForm },
|
||||
mailForm: { ...pageState.value.mailForm }
|
||||
}
|
||||
}
|
||||
|
||||
async function persistRemoteSettings(successMessage, options = {}) {
|
||||
try {
|
||||
const snapshot = await saveSettings(buildSettingsPayload())
|
||||
applyLoadedSnapshot(snapshot, options)
|
||||
toast(successMessage)
|
||||
return true
|
||||
} catch (error) {
|
||||
toast(error.message || '设置保存失败,请稍后重试。')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function activateSection(sectionId) {
|
||||
sessionRetentionPickerOpen.value = false
|
||||
activeSection.value = sectionId
|
||||
}
|
||||
|
||||
function toggleBoolean(formKey, field) {
|
||||
pageState.value[formKey][field] = !pageState.value[formKey][field]
|
||||
}
|
||||
|
||||
function toggleSessionRetentionPicker() {
|
||||
sessionRetentionPickerOpen.value = !sessionRetentionPickerOpen.value
|
||||
}
|
||||
|
||||
function closeSessionRetentionPicker() {
|
||||
sessionRetentionPickerOpen.value = false
|
||||
}
|
||||
|
||||
function selectSessionRetentionDays(value) {
|
||||
pageState.value.sessionForm.conversationRetentionDays = Number(value)
|
||||
closeSessionRetentionPicker()
|
||||
}
|
||||
|
||||
function handleDocumentPointerDown(event) {
|
||||
if (!sessionRetentionPickerOpen.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const target = event.target
|
||||
if (sessionRetentionPickerRef.value?.contains(target)) {
|
||||
return
|
||||
}
|
||||
|
||||
closeSessionRetentionPicker()
|
||||
}
|
||||
|
||||
function clearRenderSecretMask() {
|
||||
if (isRenderSecretMask(pageState.value.renderForm.jwtSecret)) {
|
||||
pageState.value.renderForm.jwtSecret = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function saveProfileSection() {
|
||||
const companyForm = pageState.value.companyForm
|
||||
|
||||
if (!normalizeValue(companyForm.companyName)) {
|
||||
toast('请输入企业名称。')
|
||||
return
|
||||
}
|
||||
|
||||
if (!normalizeValue(companyForm.displayName)) {
|
||||
toast('请输入系统显示名称。')
|
||||
return
|
||||
}
|
||||
|
||||
if (!normalizeValue(companyForm.copyright)) {
|
||||
toast('请输入版权信息。')
|
||||
return
|
||||
}
|
||||
|
||||
pageState.value.mailForm.senderName = normalizeValue(companyForm.displayName)
|
||||
await persistRemoteSettings('企业信息已保存并应用到当前系统。', {
|
||||
preserveModelApiKeys: true,
|
||||
preserveAdminPasswords: true,
|
||||
preserveRenderSecret: true,
|
||||
preserveMailPassword: true
|
||||
})
|
||||
}
|
||||
|
||||
async function saveAdminSection() {
|
||||
const adminForm = pageState.value.adminForm
|
||||
|
||||
if (!normalizeValue(adminForm.adminAccount)) {
|
||||
toast('请输入管理员账号。')
|
||||
return
|
||||
}
|
||||
|
||||
if (!normalizeValue(adminForm.adminEmail)) {
|
||||
toast('请输入管理员邮箱。')
|
||||
return
|
||||
}
|
||||
|
||||
if (Number(adminForm.sessionTimeout) < 5) {
|
||||
toast('会话超时时间不能少于 5 分钟。')
|
||||
return
|
||||
}
|
||||
|
||||
if (adminForm.newPassword) {
|
||||
if (adminForm.newPassword.length < 5) {
|
||||
toast('管理员密码至少需要 5 位。')
|
||||
return
|
||||
}
|
||||
|
||||
if (adminForm.newPassword !== adminForm.confirmPassword) {
|
||||
toast('两次输入的管理员密码不一致。')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
await persistRemoteSettings('管理员安全设置已保存。', {
|
||||
preserveModelApiKeys: true,
|
||||
preserveAdminPasswords: false,
|
||||
preserveRenderSecret: true,
|
||||
preserveMailPassword: true
|
||||
})
|
||||
}
|
||||
|
||||
function toggleHermesMaster() {
|
||||
pageState.value.hermesForm.masterEnabled = !pageState.value.hermesForm.masterEnabled
|
||||
}
|
||||
|
||||
function toggleHermesFlag(field) {
|
||||
pageState.value.hermesForm[field] = !pageState.value.hermesForm[field]
|
||||
}
|
||||
|
||||
function toggleHermesTask(taskId) {
|
||||
const schedule = pageState.value.hermesForm.schedules[taskId]
|
||||
if (!schedule) {
|
||||
return
|
||||
}
|
||||
|
||||
const enabled = !(pageState.value.hermesForm.capabilities[taskId] && schedule.enabled)
|
||||
pageState.value.hermesForm.capabilities[taskId] = enabled
|
||||
schedule.enabled = enabled
|
||||
}
|
||||
|
||||
function updateHermesTaskTime({ taskId, time }) {
|
||||
const schedule = pageState.value.hermesForm.schedules[taskId]
|
||||
if (!schedule) {
|
||||
return
|
||||
}
|
||||
schedule.time = time
|
||||
}
|
||||
|
||||
function saveHermesSection() {
|
||||
if (!isHermesEmployeeSettingsReady(pageState.value.hermesForm)) {
|
||||
toast('请至少开启一项 Hermes 能力,或关闭总控开关。')
|
||||
return
|
||||
}
|
||||
|
||||
persistSettings(pageState.value)
|
||||
toast('数字员工设置已保存。')
|
||||
}
|
||||
|
||||
async function saveSessionSection() {
|
||||
const sessionForm = pageState.value.sessionForm
|
||||
const retentionDays = Number(sessionForm.conversationRetentionDays)
|
||||
|
||||
if (retentionDays < 1 || retentionDays > 10) {
|
||||
toast('会话保留天数必须在 1 到 10 天之间。')
|
||||
return
|
||||
}
|
||||
|
||||
await persistRemoteSettings('会话设置已保存。', {
|
||||
preserveModelApiKeys: true,
|
||||
preserveAdminPasswords: true,
|
||||
preserveRenderSecret: true,
|
||||
preserveMailPassword: true
|
||||
})
|
||||
}
|
||||
|
||||
async function saveLlmSection() {
|
||||
const llmForm = pageState.value.llmForm
|
||||
const modelConfigs = [
|
||||
['主模型', llmForm.mainProvider, llmForm.mainModel, llmForm.mainEndpoint],
|
||||
['备份模型', llmForm.backupProvider, llmForm.backupModel, llmForm.backupEndpoint],
|
||||
['Embedding 模型', llmForm.embeddingProvider, llmForm.embeddingModel, llmForm.embeddingEndpoint],
|
||||
['Reranker 模型', llmForm.rerankerProvider, llmForm.rerankerModel, llmForm.rerankerEndpoint]
|
||||
]
|
||||
|
||||
for (const [label, provider, model, endpoint] of modelConfigs) {
|
||||
if (!isModelConfigReady(provider, model, endpoint)) {
|
||||
toast(`请完整填写${label}的供应商、模型名称和接口地址。`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
await persistRemoteSettings('模型配置已保存。', {
|
||||
preserveModelApiKeys: true,
|
||||
preserveAdminPasswords: true,
|
||||
preserveRenderSecret: true,
|
||||
preserveMailPassword: true
|
||||
})
|
||||
}
|
||||
|
||||
async function saveRenderingSection() {
|
||||
const renderForm = pageState.value.renderForm
|
||||
|
||||
if (renderForm.enabled && !normalizeValue(renderForm.publicUrl)) {
|
||||
toast('启用 ONLYOFFICE 时请输入服务地址。')
|
||||
return
|
||||
}
|
||||
|
||||
if (renderForm.enabled && !normalizeValue(renderForm.jwtSecret) && !renderForm.jwtSecretConfigured) {
|
||||
toast('启用 ONLYOFFICE 时请输入 JWT 密钥。')
|
||||
return
|
||||
}
|
||||
|
||||
await persistRemoteSettings('文件渲染配置已保存。', {
|
||||
preserveModelApiKeys: true,
|
||||
preserveAdminPasswords: true,
|
||||
preserveRenderSecret: false,
|
||||
preserveMailPassword: true
|
||||
})
|
||||
}
|
||||
|
||||
async function saveLogsSection() {
|
||||
const logForm = pageState.value.logForm
|
||||
|
||||
if (!normalizeValue(logForm.level) || Number(logForm.retentionDays) <= 0) {
|
||||
toast('请填写有效的日志级别和留存天数。')
|
||||
return
|
||||
}
|
||||
|
||||
if (!normalizeValue(logForm.logPath)) {
|
||||
toast('请输入日志路径。')
|
||||
return
|
||||
}
|
||||
|
||||
await persistRemoteSettings('日志策略已保存。', {
|
||||
preserveModelApiKeys: true,
|
||||
preserveAdminPasswords: true,
|
||||
preserveRenderSecret: true,
|
||||
preserveMailPassword: true
|
||||
})
|
||||
}
|
||||
|
||||
async function saveMailSection() {
|
||||
const mailForm = pageState.value.mailForm
|
||||
|
||||
if (!normalizeValue(mailForm.smtpHost) || Number(mailForm.port) <= 0) {
|
||||
toast('请填写有效的 SMTP Host 和端口。')
|
||||
return
|
||||
}
|
||||
|
||||
if (!normalizeValue(mailForm.senderAddress) || !normalizeValue(mailForm.username)) {
|
||||
toast('请填写发件人邮箱和 SMTP 登录账号。')
|
||||
return
|
||||
}
|
||||
|
||||
await persistRemoteSettings('邮箱配置已保存。', {
|
||||
preserveModelApiKeys: true,
|
||||
preserveAdminPasswords: true,
|
||||
preserveRenderSecret: true,
|
||||
preserveMailPassword: false
|
||||
})
|
||||
}
|
||||
|
||||
async function saveActiveSection() {
|
||||
if (activeSection.value === 'profile') {
|
||||
await saveProfileSection()
|
||||
return
|
||||
}
|
||||
|
||||
if (activeSection.value === 'admin') {
|
||||
await saveAdminSection()
|
||||
return
|
||||
}
|
||||
|
||||
if (activeSection.value === 'session') {
|
||||
await saveSessionSection()
|
||||
return
|
||||
}
|
||||
|
||||
if (activeSection.value === 'hermes') {
|
||||
saveHermesSection()
|
||||
return
|
||||
}
|
||||
|
||||
if (activeSection.value === 'llm') {
|
||||
await saveLlmSection()
|
||||
return
|
||||
}
|
||||
|
||||
if (activeSection.value === 'logs') {
|
||||
await saveLogsSection()
|
||||
return
|
||||
}
|
||||
|
||||
if (activeSection.value === 'rendering') {
|
||||
await saveRenderingSection()
|
||||
return
|
||||
}
|
||||
|
||||
await saveMailSection()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (typeof document !== 'undefined') {
|
||||
document.addEventListener('pointerdown', handleDocumentPointerDown)
|
||||
}
|
||||
loadSettingsSnapshot()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (typeof document !== 'undefined') {
|
||||
document.removeEventListener('pointerdown', handleDocumentPointerDown)
|
||||
}
|
||||
})
|
||||
|
||||
function triggerLogoUpload() {
|
||||
logoInputRef.value?.click()
|
||||
}
|
||||
|
||||
function handleLogoUpload(event) {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
if (!file.type.startsWith('image/')) {
|
||||
toast('请上传图片文件。')
|
||||
return
|
||||
}
|
||||
|
||||
if (file.size > 2 * 1024 * 1024) {
|
||||
toast('图片大小不能超过 2MB。')
|
||||
return
|
||||
}
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
pageState.value.companyForm.logo = e.target.result
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
|
||||
return {
|
||||
activeSection,
|
||||
activeSectionConfig,
|
||||
activateSection,
|
||||
clearRenderSecretMask,
|
||||
completedSectionCount,
|
||||
logLevels,
|
||||
logoInputRef,
|
||||
pageState,
|
||||
providerOptions,
|
||||
sessionRetentionOptions,
|
||||
sessionRetentionPickerOpen,
|
||||
sessionRetentionPickerRef,
|
||||
saveActiveSection,
|
||||
sectionStatus,
|
||||
sections,
|
||||
selectSessionRetentionDays,
|
||||
toggleSessionRetentionPicker,
|
||||
closeSessionRetentionPicker,
|
||||
toggleBoolean,
|
||||
toggleHermesFlag,
|
||||
toggleHermesMaster,
|
||||
toggleHermesTask,
|
||||
updateHermesTaskTime,
|
||||
triggerLogoUpload,
|
||||
handleLogoUpload
|
||||
}
|
||||
}
|
||||
@@ -383,7 +383,8 @@ const { toast } = useToast()
|
||||
const companyProfile = computed(() => ({
|
||||
name: bootstrapState.value.company?.name || '',
|
||||
code: bootstrapState.value.company?.code || '',
|
||||
adminEmail: bootstrapState.value.company?.admin_email || ''
|
||||
adminEmail: bootstrapState.value.company?.admin_email || '',
|
||||
logo: bootstrapState.value.company?.logo || ''
|
||||
}))
|
||||
|
||||
function updateCompanyProfilePreview(payload = {}) {
|
||||
@@ -395,7 +396,8 @@ function updateCompanyProfilePreview(payload = {}) {
|
||||
...currentCompany,
|
||||
...(payload.name !== undefined ? { name: payload.name } : {}),
|
||||
...(payload.code !== undefined ? { code: payload.code } : {}),
|
||||
...(payload.adminEmail !== undefined ? { admin_email: payload.adminEmail } : {})
|
||||
...(payload.adminEmail !== undefined ? { admin_email: payload.adminEmail } : {}),
|
||||
...(payload.logo !== undefined ? { logo: payload.logo } : {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user