import React, { useState, useEffect, useRef, Suspense } from 'react'
import axios from 'axios'
import { MoonStarsIcon, SunIcon, TuneIcon, CheckIcon, CloseIcon, SparkIcon, RefreshIcon, BuildingIcon } from './Icons'
import { useAuth, hasPermission } from '../auth'
import { useBranding } from '../branding'
/* 这两块只有系统管理账号会看,按需加载:设置本身在外壳里是静态引入的,
跟着首屏一起下发等于给所有人多下载十几 KB。 */
const AIConfigSettings = React.lazy(() => import('./AIConfigSettings'))
const OrgBrandingSettings = React.lazy(() => import('./OrgBrandingSettings'))
/* 设置分类。带 permission 的项只对有该权限点的人显示 ——
智能配置里有接口地址与密钥,组织品牌是系统管理动作,都不该摆在业务员眼前。
数据导入导出已从设置里移出,做成独立页面「数据迁移」(侧栏 · 系统管理),
因为那里要放类别勾选和逐表明细,塞进这个 1000px 的模态框里看不清。 */
const CATEGORIES = [
{ key: 'appearance', label: '外观与主题', icon: TuneIcon },
{ key: 'organization', label: '组织与品牌', icon: BuildingIcon, permission: 'branding.manage' },
{ key: 'ai-config', label: '智能配置', icon: SparkIcon, permission: 'ai.manage' },
{ key: 'system-update', label: '系统与更新', icon: RefreshIcon }
]
function Settings({ isOpen, onClose, theme, onThemeChange, reducedMotion, onReducedMotionChange }) {
const { user } = useAuth()
const [activeCategory, setActiveCategory] = useState('appearance')
const visibleCategories = CATEGORIES.filter(item => !item.permission || hasPermission(user, item.permission))
/* 打开着设置时权限被收回(换了角色),当前分类可能已经不可见,退回第一项 */
const current = visibleCategories.some(item => item.key === activeCategory)
? activeCategory
: visibleCategories[0]?.key
const modalRef = useRef(null)
useEffect(() => {
if (!isOpen) return
const previousFocus = document.activeElement
const previousOverflow = document.body.style.overflow
document.body.style.overflow = 'hidden'
const modal = modalRef.current
const focusable = () => [...modal.querySelectorAll('button, input, select, textarea, a[href], [tabindex="0"]')].filter(el => !el.disabled && el.getClientRects().length)
modal.querySelector('.close-btn')?.focus()
const onKeyDown = event => {
if (event.key === 'Escape') { event.preventDefault(); onClose() }
if (event.key === 'Tab') {
const items = focusable()
const first = items[0], last = items[items.length - 1]
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus() }
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus() }
}
}
modal.addEventListener('keydown', onKeyDown)
return () => {
document.body.style.overflow = previousOverflow
modal.removeEventListener('keydown', onKeyDown)
previousFocus?.focus()
}
}, [isOpen, onClose])
if (!isOpen) return null
return (
}>
{current === 'appearance' &&
}
{current === 'organization' &&
}
{current === 'ai-config' &&
}
{current === 'system-update' &&
}
)
}
function AppearanceSettings({ theme, onThemeChange, reducedMotion, onReducedMotionChange }) {
return (
外观与主题
选择适合工作环境的显示模式,设置会自动保存在本机。
即时生效 · 自动记住您的主题与动效偏好
)
}
/* 数据导入导出已移出设置:见 pages/DataTransfer.jsx(侧栏 · 系统管理 · 数据迁移) */
/* 智能配置已拆到 ./AIConfigSettings(多模型接入 + 用途分配)与 ./OrgBrandingSettings(组织与品牌) */
/* ---------------------------------------------------------------------------
系统与更新
显示本机版本与运行环境,并从 GitHub 仓库检查是否有新版本。
版本号以顶层 package.json 为准;远端取 Release(优先)或 tag。
--------------------------------------------------------------------------- */
function SystemUpdateSettings() {
const { user } = useAuth()
const isAdmin = user?.role === 'admin'
const [job, setJob] = useState(null)
const [updatePassword, setUpdatePassword] = useState('')
const [starting, setStarting] = useState(false)
const [connectionLost, setConnectionLost] = useState(false)
const brand = useBranding()
const [info, setInfo] = useState(null)
const [repo, setRepo] = useState('')
const [loadingInfo, setLoadingInfo] = useState(true)
const [checking, setChecking] = useState(false)
const [result, setResult] = useState(null)
const [error, setError] = useState('')
const [showNotes, setShowNotes] = useState(false)
useEffect(() => {
let alive = true
axios
.get('/api/system/info')
.then(({ data }) => {
if (!alive) return
setInfo(data)
setRepo(data.repo || '')
})
.catch((e) => {
if (alive) setError(e?.response?.data?.error || e.message)
})
.finally(() => {
if (alive) setLoadingInfo(false)
})
return () => {
alive = false
}
}, [])
const check = async () => {
setChecking(true)
setError('')
setResult(null)
setShowNotes(false)
try {
const { data } = await axios.get('/api/system/check-update', { params: { repo, force: 1 } })
setResult(data)
if (!data.ok) setError(data.error || '检查更新失败')
} catch (e) {
setError(e?.response?.data?.error || e.message)
} finally {
setChecking(false)
}
}
const updating = starting || (job && !['idle', 'completed', 'failed'].includes(job.state))
useEffect(() => {
if (!isAdmin) return
let alive = true
let timer
const poll = async () => {
try {
const { data } = await axios.get('/api/system/update/status', { timeout: 8000 })
if (alive) { setJob(data); setConnectionLost(false) }
} catch {
if (alive) setConnectionLost(true)
} finally {
if (alive) timer = setTimeout(poll, 2500)
}
}
poll()
return () => { alive = false; clearTimeout(timer) }
}, [isAdmin])
const install = async event => {
event.preventDefault()
setStarting(true)
setError('')
try {
const { data } = await axios.post('/api/system/update', { current_password: updatePassword }, { timeout: 60000 })
setJob(current => ({ ...current, ...data }))
setUpdatePassword('')
} catch (e) {
setError(e?.response?.data?.error || '未能确认更新任务状态,请查看下方进度后再试')
} finally { setStarting(false) }
}
const fmtTime = (value) => {
if (!value) return ''
try {
return new Date(value).toLocaleString('zh-CN', { hour12: false })
} catch {
return String(value)
}
}
return (
系统与更新
查看当前版本;系统管理员可在此安装更新。
当前版本
{info ? `${brand.name} v${info.version}` : '正在读取版本…'}
{info ? `运行环境 Node ${info.node} · ${info.platform} ${info.arch}` : '读取本机信息中'}
正式版
{info?.startedAt &&
服务启动于 {fmtTime(info.startedAt)}
}
检查更新
{!checking && result && (
上次检查:{fmtTime(result.checkedAt)}
{result.cached ? '(结果来自缓存)' : ''}
)}
{checking && (
正在连接 GitHub,请稍候…
)}
{!checking && error && (
{error}
)}
{!checking && !error && result?.ok && result.hasUpdate && (
<>
发现新版本 v{result.latest},当前 v{result.current}
{result.publishedAt &&
发布于 {fmtTime(result.publishedAt)}
}
{(result.url || result.notes) && (
{result.url && (
在 GitHub 上查看
)}
{result.notes && (
)}
)}
{showNotes && result.notes &&
{result.notes}}
>
)}
{!checking && !error && result?.ok && !result.hasUpdate && (
<>
已是最新版本(v{result.current})
{result.note &&
{result.note}
}
>
)}
{isAdmin && (
安装更新
系统先构建新版本并备份数据库,再切换应用和重启。重启时页面可能短暂断开,进度会自动恢复。
{job?.capability && !job.capability.supported &&
{job.capability.reason}
}
{result?.hasUpdate && job?.capability?.supported && !updating && !(job?.state === 'completed' && job?.version === result.latest) && (
)}
{job && job.state !== 'idle' && (
{updating && }{job.message}
)}
{connectionLost &&
暂时无法连接服务,正在自动重试…
}
{job?.state === 'completed' &&
}
{job?.log &&
更新日志
{job.log}}
)}
)
}
export default Settings