mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 03:55:09 +08:00
refactor(approval-grants-ui): strip Element Plus, reuse project list+pagination
This commit is contained in:
parent
6a754f5bfa
commit
b34de4e078
@ -3732,6 +3732,7 @@ export default {
|
|||||||
manage: 'Manage auto-approve rules...',
|
manage: 'Manage auto-approve rules...',
|
||||||
},
|
},
|
||||||
chipLabel: 'Auto-approve active ({count})',
|
chipLabel: 'Auto-approve active ({count})',
|
||||||
|
chipShort: 'Auto-approve {count}',
|
||||||
menu: 'Auto-approve',
|
menu: 'Auto-approve',
|
||||||
title: 'Auto-approve rules',
|
title: 'Auto-approve rules',
|
||||||
desc: 'Rules let specific tool calls skip manual approval. Safety-floor patterns (rm -rf /, pipe-to-shell, etc.) always apply, and CRITICAL severity always falls back to human approval.',
|
desc: 'Rules let specific tool calls skip manual approval. Safety-floor patterns (rm -rf /, pipe-to-shell, etc.) always apply, and CRITICAL severity always falls back to human approval.',
|
||||||
|
|||||||
@ -3824,6 +3824,7 @@ export default {
|
|||||||
manage: '管理自动批准策略...',
|
manage: '管理自动批准策略...',
|
||||||
},
|
},
|
||||||
chipLabel: '自动批准已启用 ({count})',
|
chipLabel: '自动批准已启用 ({count})',
|
||||||
|
chipShort: '自动批准 {count}',
|
||||||
menu: '自动批准',
|
menu: '自动批准',
|
||||||
title: '自动批准策略',
|
title: '自动批准策略',
|
||||||
desc: '策略让特定工具调用跳过人审。地板规则(如 rm -rf /、pipe-to-shell)始终生效,CRITICAL 严重度永远人审。',
|
desc: '策略让特定工具调用跳过人审。地板规则(如 rm -rf /、pipe-to-shell)始终生效,CRITICAL 严重度永远人审。',
|
||||||
|
|||||||
@ -16,17 +16,12 @@
|
|||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<div class="section-header__title">
|
<div class="section-header__title">
|
||||||
<h2 class="section-title">{{ t('approval.grant.title') }}</h2>
|
<h2 class="section-title">{{ t('approval.grant.title') }}</h2>
|
||||||
<el-tag
|
<!-- Inline summary pill — same "danger-tinted info" tokens as the
|
||||||
v-if="total > 0"
|
sidebar chip so the two surfaces feel like one design language.
|
||||||
type="warning"
|
No el-tag here: keeps the page entirely native + token-driven. -->
|
||||||
size="small"
|
<span v-if="total > 0" class="summary-pill">
|
||||||
effect="light"
|
|
||||||
round
|
|
||||||
disable-transitions
|
|
||||||
class="active-summary"
|
|
||||||
>
|
|
||||||
{{ t('approval.grant.chipLabel', { count: total }) }}
|
{{ t('approval.grant.chipLabel', { count: total }) }}
|
||||||
</el-tag>
|
</span>
|
||||||
<p class="section-desc">{{ t('approval.grant.desc') }}</p>
|
<p class="section-desc">{{ t('approval.grant.desc') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-header__actions">
|
<div class="section-header__actions">
|
||||||
@ -54,113 +49,93 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="config-card">
|
<!--
|
||||||
<el-table
|
Table + pagination — native HTML, no Element Plus components. Mirrors
|
||||||
v-loading="loading"
|
the ToolGuard / AuditLogs pattern (rules-table-wrapper + .rules-table
|
||||||
:data="rows"
|
+ .severity-badge / .action-btn from shared.css) so this page reads
|
||||||
:empty-text="t('approval.grant.empty')"
|
like any other Security sub-view. Pagination uses the shared
|
||||||
size="small"
|
McPagination component, which is also fully native (no el-pagination).
|
||||||
stripe
|
-->
|
||||||
>
|
<div class="rules-table-wrapper">
|
||||||
<el-table-column :label="t('approval.grant.columns.scope')" min-width="180">
|
<table class="rules-table">
|
||||||
<template #default="{ row }">
|
<thead>
|
||||||
<el-tag
|
<tr>
|
||||||
:type="scopeTagType(row.scopeType)"
|
<th>{{ t('approval.grant.columns.scope') }}</th>
|
||||||
size="small"
|
<th>{{ t('approval.grant.columns.tool') }}</th>
|
||||||
effect="light"
|
<th>{{ t('approval.grant.columns.rule') }}</th>
|
||||||
disable-transitions
|
<th>{{ t('approval.grant.columns.severity') }}</th>
|
||||||
>
|
<th>{{ t('approval.grant.columns.kind') }}</th>
|
||||||
{{ t(`approval.grant.scope.${scopeI18nKey(row.scopeType)}`) }}
|
<th>{{ t('approval.grant.columns.expire') }}</th>
|
||||||
</el-tag>
|
<th>{{ t('approval.grant.columns.grantedBy') }}</th>
|
||||||
<span class="scope-id">{{ row.scopeId }}</span>
|
<th>{{ t('approval.grant.columns.note') }}</th>
|
||||||
</template>
|
<th class="col-actions">{{ t('approval.grant.columns.actions') }}</th>
|
||||||
</el-table-column>
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="row in rows"
|
||||||
|
:key="row.id"
|
||||||
|
:class="{ 'row-revoked': row.revoked === 1 }"
|
||||||
|
>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
class="scope-badge"
|
||||||
|
:class="`scope-${scopeI18nKey(row.scopeType)}`"
|
||||||
|
>
|
||||||
|
{{ t(`approval.grant.scope.${scopeI18nKey(row.scopeType)}`) }}
|
||||||
|
</span>
|
||||||
|
<div class="scope-id" :title="row.scopeId">{{ row.scopeId }}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<code v-if="row.toolName" class="mono">{{ row.toolName }}</code>
|
||||||
|
<span v-else class="severity-badge severity-high">∗ any</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<code v-if="row.ruleId" class="mono">{{ row.ruleId }}</code>
|
||||||
|
<span v-else class="muted">∗</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
class="severity-badge"
|
||||||
|
:class="`severity-${row.maxSeverity?.toLowerCase()}`"
|
||||||
|
>
|
||||||
|
{{ row.maxSeverity }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ t(`approval.grant.kind.${kindI18nKey(row.grantKind)}`) }}</td>
|
||||||
|
<td class="muted">{{ formatDate(row.expireAt) }}</td>
|
||||||
|
<td>{{ row.grantedBy }}</td>
|
||||||
|
<td class="note-cell" :title="row.note || ''">{{ row.note }}</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
v-if="row.revoked === 0"
|
||||||
|
class="action-btn danger"
|
||||||
|
:title="t('approval.grant.revokeBtn')"
|
||||||
|
@click="confirmRevoke(row)"
|
||||||
|
>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none"
|
||||||
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="3 6 5 6 21 6"/>
|
||||||
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<span v-else class="muted">{{ t('common.revoked') }}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div v-if="loading" class="empty-state">{{ t('common.loading') }}</div>
|
||||||
|
<div v-else-if="!rows.length" class="empty-state">{{ t('approval.grant.empty') }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-table-column
|
<div v-if="total > 0" class="grants-pagination-row">
|
||||||
:label="t('approval.grant.columns.tool')"
|
<McPagination
|
||||||
prop="toolName"
|
:page="currentPage"
|
||||||
min-width="140"
|
:size="pageSize"
|
||||||
>
|
|
||||||
<template #default="{ row }">
|
|
||||||
<code v-if="row.toolName" class="mono">{{ row.toolName }}</code>
|
|
||||||
<el-tag v-else type="danger" size="small" effect="dark">∗ any</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
<el-table-column :label="t('approval.grant.columns.rule')" min-width="140">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<code v-if="row.ruleId" class="mono">{{ row.ruleId }}</code>
|
|
||||||
<span v-else class="muted">∗</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
<el-table-column
|
|
||||||
:label="t('approval.grant.columns.severity')"
|
|
||||||
prop="maxSeverity"
|
|
||||||
width="110"
|
|
||||||
>
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag :type="severityTagType(row.maxSeverity)" size="small" disable-transitions>
|
|
||||||
{{ row.maxSeverity }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
<el-table-column :label="t('approval.grant.columns.kind')" width="140">
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ t(`approval.grant.kind.${kindI18nKey(row.grantKind)}`) }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
<el-table-column :label="t('approval.grant.columns.expire')" width="160">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<span class="muted">{{ formatDate(row.expireAt) }}</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
<el-table-column
|
|
||||||
:label="t('approval.grant.columns.grantedBy')"
|
|
||||||
prop="grantedBy"
|
|
||||||
width="120"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<el-table-column :label="t('approval.grant.columns.note')" min-width="160">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<span :title="row.note || ''" class="note-cell">{{ row.note }}</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
<el-table-column :label="t('approval.grant.columns.actions')" width="100" fixed="right">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-button
|
|
||||||
v-if="row.revoked === 0"
|
|
||||||
:icon="Delete"
|
|
||||||
type="danger"
|
|
||||||
size="small"
|
|
||||||
text
|
|
||||||
@click="confirmRevoke(row)"
|
|
||||||
>
|
|
||||||
{{ t('approval.grant.revokeBtn') }}
|
|
||||||
</el-button>
|
|
||||||
<el-tag v-else type="info" size="small" effect="plain" disable-transitions>
|
|
||||||
{{ t('common.revoked') }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
|
|
||||||
<el-pagination
|
|
||||||
v-if="total > 0"
|
|
||||||
class="grants-pagination"
|
|
||||||
v-model:current-page="currentPage"
|
|
||||||
v-model:page-size="pageSize"
|
|
||||||
:total="total"
|
:total="total"
|
||||||
:page-sizes="[10, 20, 50, 100]"
|
:sizes="[10, 20, 50, 100]"
|
||||||
layout="total, sizes, prev, pager, next, jumper"
|
@update:page="onPageChange"
|
||||||
background
|
@update:size="onSizeChange"
|
||||||
small
|
|
||||||
@size-change="loadGrants"
|
|
||||||
@current-change="loadGrants"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -312,6 +287,7 @@ import {
|
|||||||
WarningFilled,
|
WarningFilled,
|
||||||
} from '@element-plus/icons-vue'
|
} from '@element-plus/icons-vue'
|
||||||
import { approvalApi } from '@/api'
|
import { approvalApi } from '@/api'
|
||||||
|
import McPagination from '@/components/common/McPagination.vue'
|
||||||
import type {
|
import type {
|
||||||
ApprovalGrant,
|
ApprovalGrant,
|
||||||
CreateGrantPayload,
|
CreateGrantPayload,
|
||||||
@ -365,6 +341,16 @@ const requiresPassword = computed(() => {
|
|||||||
return noTool && (form.scopeType === 'WORKSPACE' || form.scopeType === 'AGENT')
|
return noTool && (form.scopeType === 'WORKSPACE' || form.scopeType === 'AGENT')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function onPageChange(p: number) {
|
||||||
|
currentPage.value = p
|
||||||
|
loadGrants()
|
||||||
|
}
|
||||||
|
function onSizeChange(s: number) {
|
||||||
|
pageSize.value = s
|
||||||
|
currentPage.value = 1
|
||||||
|
loadGrants()
|
||||||
|
}
|
||||||
|
|
||||||
async function loadGrants() {
|
async function loadGrants() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@ -471,23 +457,6 @@ function scopeI18nKey(scope: GrantScope): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function scopeTagType(scope: GrantScope): 'primary' | 'success' | 'warning' | 'danger' | 'info' {
|
|
||||||
switch (scope) {
|
|
||||||
case 'CONVERSATION': return 'primary'
|
|
||||||
case 'AGENT': return 'warning'
|
|
||||||
case 'USER': return 'success'
|
|
||||||
case 'WORKSPACE': return 'danger'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function severityTagType(sev: GrantSeverity): 'success' | 'warning' | 'danger' {
|
|
||||||
switch (sev) {
|
|
||||||
case 'LOW': return 'success'
|
|
||||||
case 'MEDIUM': return 'warning'
|
|
||||||
case 'HIGH': return 'danger'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function kindI18nKey(kind: GrantKind): string {
|
function kindI18nKey(kind: GrantKind): string {
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case 'ALWAYS': return 'always'
|
case 'ALWAYS': return 'always'
|
||||||
@ -523,8 +492,19 @@ onMounted(loadGrants)
|
|||||||
flex-basis: 100%;
|
flex-basis: 100%;
|
||||||
margin: 4px 0 0;
|
margin: 4px 0 0;
|
||||||
}
|
}
|
||||||
.active-summary {
|
/* Summary pill — reuses the same danger-tinted tokens as the sidebar chip
|
||||||
|
so the two surfaces feel like one design language; no el-tag here. */
|
||||||
|
.summary-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--mc-danger-bg, rgba(192, 57, 43, 0.12));
|
||||||
|
color: var(--mc-danger, #C0392B);
|
||||||
|
border: 1px solid var(--mc-danger-border, rgba(192, 57, 43, 0.4));
|
||||||
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Header actions: matches McpServers / ToolGuard layout (.section-header__actions
|
/* Header actions: matches McpServers / ToolGuard layout (.section-header__actions
|
||||||
@ -567,17 +547,36 @@ onMounted(loadGrants)
|
|||||||
color: #991b1b;
|
color: #991b1b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Scope badge — same family as .severity-badge in shared.css, 4 token-driven
|
||||||
|
color variants for CONVERSATION / AGENT / USER / WORKSPACE. */
|
||||||
|
.scope-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.scope-conversation { background: rgba(59, 130, 246, 0.12); color: #3b82f6; }
|
||||||
|
.scope-agent { background: rgba(245, 158, 11, 0.12); color: #f59e0b; }
|
||||||
|
.scope-user { background: rgba(16, 185, 129, 0.12); color: #10b981; }
|
||||||
|
.scope-workspace { background: var(--mc-danger-bg, rgba(192, 57, 43, 0.12));
|
||||||
|
color: var(--mc-danger, #C0392B); }
|
||||||
.scope-id {
|
.scope-id {
|
||||||
margin-left: 8px;
|
margin-top: 2px;
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
color: var(--mc-text-tertiary, #94a3b8);
|
color: var(--mc-text-tertiary, #94a3b8);
|
||||||
|
max-width: 180px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mono {
|
.mono {
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
background: var(--mc-surface-tertiary, #f1f5f9);
|
background: var(--mc-surface-tertiary, var(--mc-bg-muted, rgba(0, 0, 0, 0.04)));
|
||||||
padding: 1px 6px;
|
padding: 1px 6px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
@ -596,8 +595,29 @@ onMounted(loadGrants)
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grants-pagination {
|
/* Revoked rows: dim out so live rows stay visually dominant. */
|
||||||
margin-top: 16px;
|
.row-revoked { opacity: 0.55; }
|
||||||
|
.row-revoked .scope-badge { filter: grayscale(0.5); }
|
||||||
|
|
||||||
|
/* Actions column header sits right-aligned, matches ToolGuard. */
|
||||||
|
.col-actions { text-align: center; width: 80px; }
|
||||||
|
|
||||||
|
/* This page has more columns than ToolGuard / AuditLogs, so the wrapper needs
|
||||||
|
to allow horizontal scroll on narrow viewports — shared.css uses
|
||||||
|
overflow:hidden which would chop the rightmost columns off. Scoped override
|
||||||
|
only affects this view; the shared style stays untouched. */
|
||||||
|
.rules-table-wrapper {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.rules-table {
|
||||||
|
min-width: 900px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pagination row — right-aligned beneath the table. McPagination provides
|
||||||
|
its own pill background, so just lay it out. */
|
||||||
|
.grants-pagination-row {
|
||||||
|
margin-top: 14px;
|
||||||
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -80,11 +80,34 @@
|
|||||||
<!-- 底部 -->
|
<!-- 底部 -->
|
||||||
<div class="sidebar-footer">
|
<div class="sidebar-footer">
|
||||||
<template v-if="!sidebarCollapsed || isMobile">
|
<template v-if="!sidebarCollapsed || isMobile">
|
||||||
<!-- Doctor 健康指示器 -->
|
<!--
|
||||||
<button class="health-indicator" :class="healthStatus" @click="showDoctor = true" :title="t('doctor.title')">
|
Status row: two side-by-side status cards. Doctor health on the left
|
||||||
<span class="health-dot"></span>
|
stays always visible; the auto-approve chip on the right only renders
|
||||||
<span class="health-label">{{ t('doctor.title') }}</span>
|
when at least one grant is active. When the chip is hidden, flex
|
||||||
</button>
|
naturally lets the doctor card expand to full width again — so the
|
||||||
|
row never wastes vertical space the way a stacked banner did.
|
||||||
|
-->
|
||||||
|
<div class="footer-status-row">
|
||||||
|
<button
|
||||||
|
class="health-indicator"
|
||||||
|
:class="[healthStatus, { 'is-half': autoApproveSummary && autoApproveSummary.count > 0 }]"
|
||||||
|
@click="showDoctor = true"
|
||||||
|
:title="t('doctor.title')"
|
||||||
|
>
|
||||||
|
<span class="health-dot"></span>
|
||||||
|
<span class="health-label">{{ t('doctor.title') }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="autoApproveSummary && autoApproveSummary.count > 0"
|
||||||
|
class="auto-approve-chip"
|
||||||
|
@click="goAutoApproveSettings"
|
||||||
|
:title="t('approval.grant.title')"
|
||||||
|
>
|
||||||
|
<span class="auto-approve-chip__dot"></span>
|
||||||
|
<el-icon :size="13"><Unlock /></el-icon>
|
||||||
|
<span class="auto-approve-chip__label">{{ t('approval.grant.chipShort', { count: autoApproveSummary.count }) }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="sidebar-utility-card">
|
<div class="sidebar-utility-card">
|
||||||
<div class="compact-utility-row">
|
<div class="compact-utility-row">
|
||||||
@ -133,18 +156,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Auto-approve chip — red on purpose: security-reducing setting needs
|
|
||||||
persistent visibility, not a friendly green badge. -->
|
|
||||||
<button
|
|
||||||
v-if="autoApproveSummary && autoApproveSummary.count > 0"
|
|
||||||
class="auto-approve-chip"
|
|
||||||
@click="goAutoApproveSettings"
|
|
||||||
:title="t('approval.grant.title')"
|
|
||||||
>
|
|
||||||
<el-icon :size="14"><Unlock /></el-icon>
|
|
||||||
<span>{{ t('approval.grant.chipLabel', { count: autoApproveSummary.count }) }}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="shortcuts-hint" :title="shortcutsHintText">
|
<div class="shortcuts-hint" :title="shortcutsHintText">
|
||||||
<kbd>Ctrl+K</kbd>
|
<kbd>Ctrl+K</kbd>
|
||||||
<span>{{ t('nav.shortcutAgents') }}</span>
|
<span>{{ t('nav.shortcutAgents') }}</span>
|
||||||
@ -805,25 +816,93 @@ watch(() => workspaceStore.currentWorkspaceId, () => {
|
|||||||
backdrop-filter: blur(14px);
|
backdrop-filter: blur(14px);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.health-indicator { display: flex; align-items: center; gap: 8px; width: 100%; padding: 8px 10px; border: 1px solid var(--mc-border-light); background: var(--mc-bg-muted); border-radius: 12px; cursor: pointer; color: var(--mc-text-secondary); font-size: 12px; margin-bottom: 8px; }
|
/* Status row: doctor health + (optional) auto-approve chip side by side, so
|
||||||
|
the footer never gives up a whole banner-row for a single state badge. When
|
||||||
|
the chip is hidden, .health-indicator naturally expands back to full width. */
|
||||||
|
.footer-status-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.health-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid var(--mc-border-light);
|
||||||
|
background: var(--mc-bg-muted);
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--mc-text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
.health-indicator:hover { background: var(--mc-bg-sunken); }
|
.health-indicator:hover { background: var(--mc-bg-sunken); }
|
||||||
|
.health-indicator .health-label {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
/* When the chip is showing, the health card yields half of the row so the
|
||||||
|
two states stay visually balanced. */
|
||||||
|
.health-indicator.is-half { flex: 1 1 50%; }
|
||||||
|
|
||||||
/* Auto-approve chip — red border + red text, the persistent reminder that
|
/*
|
||||||
this workspace currently has active auto-approve rules. Clicking takes
|
Auto-approve chip — persistent indicator that this workspace currently has
|
||||||
the user to Security > 自动批准策略 so they can review or revoke. */
|
at least one active auto-approve rule. Designed to be informative, not
|
||||||
|
alarming: a soft danger-tinted pill with a steady pulse on the dot, sized
|
||||||
|
to fit beside the doctor indicator on one row. Colors come from the
|
||||||
|
mateclaw token system (`var(--mc-danger-*)`), so dark mode picks up the
|
||||||
|
appropriate dim variants automatically.
|
||||||
|
*/
|
||||||
.auto-approve-chip {
|
.auto-approve-chip {
|
||||||
display: flex; align-items: center; gap: 6px;
|
display: inline-flex;
|
||||||
width: 100%; padding: 6px 10px;
|
align-items: center;
|
||||||
border: 1px solid #ef4444;
|
gap: 6px;
|
||||||
background: #fef2f2;
|
flex: 1 1 50%;
|
||||||
color: #b91c1c;
|
min-width: 0;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid var(--mc-danger-border, rgba(192, 57, 43, 0.4));
|
||||||
|
background: var(--mc-danger-bg, rgba(192, 57, 43, 0.12));
|
||||||
|
color: var(--mc-danger, #C0392B);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 8px;
|
line-height: 1.2;
|
||||||
|
transition: background-color 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
.auto-approve-chip:hover {
|
||||||
|
background: var(--mc-danger-bg, rgba(192, 57, 43, 0.18));
|
||||||
|
border-color: var(--mc-danger, #C0392B);
|
||||||
|
}
|
||||||
|
.auto-approve-chip__label {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
/* Small live-status dot that gently pulses to suggest "this is active right
|
||||||
|
now" without being aggressive about it. The animation pauses when the user
|
||||||
|
prefers reduced motion. */
|
||||||
|
.auto-approve-chip__dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--mc-danger, #C0392B);
|
||||||
|
box-shadow: 0 0 0 0 var(--mc-danger, #C0392B);
|
||||||
|
animation: auto-approve-pulse 2.4s ease-in-out infinite;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
@keyframes auto-approve-pulse {
|
||||||
|
0% { box-shadow: 0 0 0 0 var(--mc-danger, #C0392B); opacity: 1; }
|
||||||
|
60% { box-shadow: 0 0 0 6px transparent; opacity: 0.6; }
|
||||||
|
100% { box-shadow: 0 0 0 0 transparent; opacity: 1; }
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.auto-approve-chip__dot { animation: none; }
|
||||||
}
|
}
|
||||||
.auto-approve-chip:hover { background: #fee2e2; }
|
|
||||||
.health-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
.health-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
||||||
.health-indicator.healthy .health-dot { background: var(--mc-success); }
|
.health-indicator.healthy .health-dot { background: var(--mc-success); }
|
||||||
.health-indicator.warning .health-dot { background: var(--mc-primary); }
|
.health-indicator.warning .health-dot { background: var(--mc-primary); }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user