Compare commits

..

2 Commits

Author SHA1 Message Date
aiyimickey b5b59f1c3b feat: 服务表单默认检查 + 至少保留一条 + CHANGELOG 更新 2026-07-02 10:09:19 +08:00
aiyimickey ba7f181501 style: 设计规范新增 cursor-pointer 规则 + 代码同步
- docs: tlyq-design-system.md 新增「光标系统」章节
- dashboard: 服务卡片添加 cursor-pointer
- settings: checkbox label 添加 cursor-pointer
- roles: 权限 checkbox label 添加 cursor-pointer
2026-07-02 09:41:56 +08:00
5 changed files with 28 additions and 7 deletions

View File

@ -1,5 +1,23 @@
# Changelog
## v2026.07.02
### 新增
- **用户管理页面**用户列表、角色编辑admin/editor/viewerAPI 权限守卫
- **角色权限管理页面**11 个权限的开关式管理admin/editor/viewer 三角色独立配置
- **审计日志页面**:分页、筛选(操作类型/对象类型)、真实数据展示
- **容器操作管理**:服务管理页面增加重启/停止/查看日志功能,通过 SSH 执行 Docker 命令,安全边界(不挂载 rw docker.sock所有操作记录审计日志
- **服务 CRUD 表单**:完整的添加/编辑/删除服务对话框,支持 HTTP/Docker 多维度检查配置
- **仪表盘路由**`/` 重定向到 `/dashboard`
### 优化
- **设计规范更新**`tlyq-design-system.md` 新增「光标系统」章节,所有可交互非按钮/链接元素添加 `cursor-pointer`
- **服务表单改进**:新增服务默认包含一条检查方式,至少保留一条检查不允许删除
- **用户管理**:表格显示用户名/显示名/邮箱/角色/状态,支持内联编辑角色
- **审计日志**:支持按操作类型筛选(登录/创建/更新/删除等),按对象类型筛选(认证/服务/告警渠道/用户/角色)
## v2026.07.01
### 新增

View File

@ -107,7 +107,7 @@ export default function RolesPage() {
<span className="text-sm text-slate-400">admin {allPermissions.length} </span>
) : editing === role.role ? (
allPermissions.map(p => (
<label key={p} className="flex items-center gap-2 text-sm py-1">
<label key={p} className="flex items-center gap-2 text-sm py-1 cursor-pointer">
<input type="checkbox" checked={perms.includes(p)} onChange={() => togglePerm(p)}
className="rounded border-slate-300 text-indigo-600" />
{permLabels[p] || p}

View File

@ -70,7 +70,7 @@ export default function DashboardPage() {
{services.map((s) => {
const isAbnormal = s.current_status === 'abnormal'
return (
<div key={String(s.id)} className={`rounded-xl p-5 border transition-all ${isAbnormal ? 'bg-red-50 dark:bg-red-500/10 border-red-600 dark:border-red-400' : 'bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-800 hover:shadow-sm'}`}>
<div key={String(s.id)} className={`rounded-xl p-5 border transition-all cursor-pointer ${isAbnormal ? 'bg-red-50 dark:bg-red-500/10 border-red-600 dark:border-red-400' : 'bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-800 hover:shadow-sm'}`}>
<div className="flex items-center justify-between mb-2">
<span className="flex items-center gap-2 text-sm font-semibold">
<span className={`w-2.5 h-2.5 rounded-full ${isAbnormal ? 'bg-red-600 shadow-[0_0_8px_var(--danger)]' : 'bg-emerald-600'}`} />

View File

@ -6,7 +6,7 @@ import { Plus, RotateCw, Square, ScrollText, X, Save, Trash2, Edit3 } from 'luci
interface Check { type: string; containerName?: string; url?: string }
interface Service { id: number; name: string; category: string; alert_level: string; checks: Check[]; check_interval: number; enabled: number; current_status: string }
const emptyService = { name: '', category: 'endpoint', alert_level: 'warning', checks: [] as Check[], check_interval: 30, check_timeout: 10, enabled: 1 }
const emptyService = { name: '', category: 'endpoint', alert_level: 'warning', checks: [{ type: 'http', url: '' }] as Check[], check_interval: 30, check_timeout: 10, enabled: 1 }
export default function ServicesPage() {
const [services, setServices] = useState<Service[]>([])
@ -63,6 +63,7 @@ export default function ServicesPage() {
}
const removeCheck = (i: number) => {
const checks = [...((editing?.checks as Check[]) || [])]
if (checks.length <= 1) return // 至少保留一条检查方式
checks.splice(i, 1)
update('checks', checks)
}
@ -247,7 +248,9 @@ export default function ServicesPage() {
<input value={c.containerName || ''} onChange={e => updateCheck(i, 'containerName', e.target.value)} placeholder="容器名,如 authelia"
className="flex-1 px-2 py-1.5 rounded border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-xs" />
)}
{((editing.checks as Check[]) || []).length > 1 && (
<button onClick={() => removeCheck(i)} className="text-red-400 hover:text-red-600 p-1"><X size={14} /></button>
)}
</div>
))}
</div>

View File

@ -173,7 +173,7 @@ export default function SettingsPage() {
{ key: 'level_warning', label: 'Warning' },
{ key: 'level_info', label: 'Info' },
].map(l => (
<label key={l.key} className="flex items-center gap-2 text-sm">
<label key={l.key} className="flex items-center gap-2 text-sm cursor-pointer">
<input type="checkbox" checked={!!(editing as Record<string, unknown>)[l.key]} onChange={e => updateField(l.key, e.target.checked ? 1 : 0)}
className="rounded border-slate-300 text-indigo-600 focus:ring-indigo-500" />
{l.label}
@ -182,7 +182,7 @@ export default function SettingsPage() {
</div>
</div>
<div>
<label className="flex items-center gap-2 text-sm text-slate-700 dark:text-slate-300">
<label className="flex items-center gap-2 text-sm text-slate-700 dark:text-slate-300 cursor-pointer">
<input type="checkbox" checked={!!editing.quiet_start} onChange={e => {
if (e.target.checked) {
updateField('quiet_start', '23:00')
@ -210,7 +210,7 @@ export default function SettingsPage() {
</div>
</div>
)}
<label className="flex items-center gap-2 text-sm text-slate-700 dark:text-slate-300">
<label className="flex items-center gap-2 text-sm text-slate-700 dark:text-slate-300 cursor-pointer">
<input type="checkbox" checked={editing.enabled === 1} onChange={e => updateField('enabled', e.target.checked ? 1 : 0)}
className="rounded border-slate-300 text-indigo-600 focus:ring-indigo-500" />