<template>
  <div class="perm-page" v-loading="pageLoading">

    <!-- ══════════════════════════════════════
         项目访问控制
    ══════════════════════════════════════ -->
    <template v-if="activeTab === 'project'">
      <div class="perm-header">
        <div class="perm-header-left">
          <h2 class="perm-title">项目访问控制</h2>
        </div>
        <div class="perm-header-actions">
          <div class="project-selector">
            <svg class="project-selector-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
            <el-select
              v-model="selectedStudyId"
              placeholder="选择项目"
              filterable
              popper-class="project-selector-dropdown"
              style="width: 240px"
              @change="onStudyChange"
            >
              <el-option
                v-for="s in studies"
                :key="s.id"
                :label="`${s.code} · ${s.name}`"
                :value="s.id"
              />
            </el-select>
          </div>
        </div>
      </div>

      <div class="perm-body">
        <div v-if="!selectedStudyId" class="perm-empty">
          <div class="empty-state">
            <div class="empty-icon">
              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
            </div>
            <h3>选择项目</h3>
            <p>选择项目后即可管理角色、权限策略与项目成员</p>
          </div>
        </div>
        <template v-else>
          <el-tabs v-model="projectSubTab" class="project-sub-tabs">
            <el-tab-pane label="权限策略" name="api">
              <div class="sub-tab-toolbar">
                <div class="sub-tab-info">
                  <span class="sub-tab-project-name" v-if="selectedStudy">{{ selectedStudy.code }} · {{ selectedStudy.name }}</span>
                </div>
                <div class="sub-tab-actions">
                  <el-radio-group v-model="permissionView" size="small" class="permission-view-switch">
                    <el-radio-button value="roles">角色配置</el-radio-button>
                    <el-radio-button value="matrix">权限对比</el-radio-button>
                  </el-radio-group>
                  <el-button @click="templateDrawerVisible = true">
                    <el-icon><Files /></el-icon>
                    角色与权限
                  </el-button>
                </div>
              </div>
              <PermissionTemplateSelector
                v-if="permissionView === 'roles'"
                :study-id="selectedStudyId"
                :current-permissions="currentPermissionsForTemplate"
                :active-roles="activeRolesInStudy"
                :refresh-key="templateRefreshKey"
                @edit-role="openRoleEditor"
              />
              <ApiEndpointPermissions
                v-else
                :project="selectedStudy"
                :matrix="apiMatrix"
              />
            </el-tab-pane>
            <el-tab-pane label="项目成员" name="members">
              <div class="sub-tab-toolbar">
                <div class="sub-tab-info">
                  <span class="sub-tab-project-name" v-if="selectedStudy">{{ selectedStudy.code }} · {{ selectedStudy.name }}</span>
                  <el-tag v-if="memberRows.length" effect="plain" size="small" round>{{ memberRows.length }} 名成员</el-tag>
                  <el-tag v-for="item in memberRoleSummary" :key="item.role" effect="plain" size="small" type="info" round>
                    {{ roleLabel(item.role) }} {{ item.count }}
                  </el-tag>
                </div>
                <div class="sub-tab-actions">
                  <el-button v-if="canAddProjectMember" type="primary" @click="openAddMember">
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 14px; height: 14px; margin-right: 4px"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="8.5" cy="7" r="4"/><line x1="20" y1="8" x2="20" y2="14"/><line x1="23" y1="11" x2="17" y2="11"/></svg>
                    添加成员
                  </el-button>
                </div>
              </div>
              <div class="members-filter-bar">
                <div class="members-filter-controls">
                  <el-input
                    v-model="memberSearch"
                    clearable
                    placeholder="搜索姓名或邮箱"
                    :prefix-icon="Search"
                    style="width: 240px"
                  />
                  <el-select v-model="memberRoleFilter" style="width: 150px">
                    <el-option label="全部项目角色" value="" />
                    <el-option v-for="role in memberFilterRoles" :key="role" :label="roleLabel(role)" :value="role" />
                  </el-select>
                  <el-select v-model="memberStatusFilter" style="width: 140px">
                    <el-option label="全部状态" value="" />
                    <el-option label="有效" value="ACTIVE" />
                    <el-option label="项目内停用" value="DISABLED" />
                    <el-option label="账号已停用" value="DISABLED_GLOBAL" />
                  </el-select>
                  <el-button :disabled="!hasMemberFilters" @click="resetMemberFilters">重置</el-button>
                </div>
                <div class="members-filter-meta">
                  <span class="members-filter-result">显示 {{ filteredMemberRows.length }} / {{ memberRows.length }} 名成员</span>
                  <span v-if="lastMemberRoleChange" class="member-undo">
                    已调整 {{ lastMemberRoleChange.fullName }} 的角色
                    <el-button link type="primary" size="small" @click="undoMemberRoleChange">撤销</el-button>
                  </span>
                </div>
              </div>
              <div class="members-table-wrapper">
                <el-table :data="filteredMemberRows" v-loading="membersLoading" class="members-table" empty-text="未找到匹配的项目成员">
                  <el-table-column prop="full_name" label="姓名" min-width="140">
                    <template #default="{ row }">
                      <div class="member-name-cell">
                        <span class="member-name">{{ row.full_name }}</span>
                        <span v-if="row.email" class="member-email">{{ row.email }}</span>
                      </div>
                    </template>
                  </el-table-column>
                  <el-table-column label="项目角色" min-width="140">
                    <template #default="{ row }">
                      <el-select
                        :model-value="row.role_in_study"
                        size="small"
                        style="width: 130px"
                        @change="(val: string) => confirmMemberRoleChange(row, val)"
                        :disabled="!canUpdateProjectMember(row) || !row.is_active || row.effectiveStatus === 'DISABLED_GLOBAL'"
                      >
                        <el-option
                          v-for="(label, val) in memberRoleLabels(row)"
                          :key="val"
                          :label="label"
                          :value="val"
                        />
                      </el-select>
                    </template>
                  </el-table-column>
                  <el-table-column label="状态" width="120">
                    <template #default="{ row }">
                      <el-tooltip v-if="row.effectiveStatus === 'DISABLED_GLOBAL'" content="请前往账号管理启用该账号" placement="top">
                        <el-tag type="danger" effect="plain" round size="small">账号已停用</el-tag>
                      </el-tooltip>
                      <el-tag v-else :type="row.is_active ? 'success' : 'warning'" effect="plain" round size="small">
                        {{ row.is_active ? "有效" : "项目内停用" }}
                      </el-tag>
                    </template>
                  </el-table-column>
                  <el-table-column prop="added_at" label="加入时间" min-width="160">
                    <template #default="{ row }">
                      <span class="time-text">{{ displayDateTime(row.added_at) }}</span>
                    </template>
                  </el-table-column>
                  <el-table-column label="操作" width="160" align="center">
                    <template #default="{ row }">
                      <el-button
                        link
                        :type="row.is_active ? 'warning' : 'primary'"
                        size="small"
                        :disabled="!canUpdateProjectMember(row) || row.effectiveStatus === 'DISABLED_GLOBAL'"
                        @click="toggleMemberActive(row)"
                      >
                        {{ row.is_active ? "停用" : "启用" }}
                      </el-button>
                      <el-button link type="danger" size="small" :disabled="!canDeleteProjectMember(row)" @click="deleteMember(row)">
                        移除
                      </el-button>
                    </template>
                  </el-table-column>
                </el-table>
              </div>
            </el-tab-pane>
          </el-tabs>
        </template>
      </div>
    </template>

    <!-- ══════════════════════════════════════
         系统级权限
    ══════════════════════════════════════ -->
    <template v-else-if="activeTab === 'system'">
      <div class="perm-body perm-body--system">
        <div v-if="systemLoading" class="perm-empty">
          <el-skeleton :rows="8" animated />
        </div>
        <div v-else-if="!systemPermissions.length" class="perm-empty">
          <el-empty description="暂无系统级权限数据" />
        </div>
        <div v-else class="system-perm-container">
          <div class="system-stats" aria-label="系统级权限概览">
            <div class="system-stat-item system-stat-item--modules">
              <span class="system-stat-label">权限模块</span>
              <div class="system-stat-value">
                <strong>{{ Object.keys(systemPermissionsByModule).length }}</strong>
                <span>个</span>
              </div>
            </div>
            <div class="system-stat-item system-stat-item--permissions">
              <span class="system-stat-label">权限总数</span>
              <div class="system-stat-value">
                <strong>{{ systemPermissions.length }}</strong>
                <span>项</span>
              </div>
            </div>
            <div class="system-stat-item system-stat-item--pm">
              <span class="system-stat-label">PM 可访问</span>
              <div class="system-stat-value">
                <strong>{{ systemPermissions.filter(p => p.roles.includes('PM')).length }}</strong>
                <span>项</span>
              </div>
            </div>
          </div>
          <div class="system-permissions-card">
            <section v-for="(items, moduleKey) in systemPermissionsByModule" :key="moduleKey" class="system-module-block">
              <div class="system-module-header">
                <div class="system-module-icon" :class="`module-icon--${moduleKey}`">
                  <svg v-if="moduleKey === 'system_users'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="8.5" cy="7" r="4"/><line x1="20" y1="8" x2="20" y2="14"/><line x1="23" y1="11" x2="17" y2="11"/></svg>
                  <svg v-else-if="moduleKey === 'system_projects'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
                  <svg v-else-if="moduleKey === 'system_permissions'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
                  <svg v-else-if="moduleKey === 'system_monitoring'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
                  <svg v-else-if="moduleKey === 'system_audit'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
                  <svg v-else viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
                </div>
                <div class="system-module-info">
                  <span class="system-module-title">{{ systemModuleLabels[moduleKey] || moduleKey }}</span>
                  <span class="system-module-count">{{ items.length }} 项操作</span>
                </div>
                <div class="system-module-roles-summary">
                  <el-tag v-if="items.some(i => i.roles.includes('PM'))" size="small" type="warning" effect="plain" round>PM 可访问</el-tag>
                  <el-tag v-else size="small" type="info" effect="plain" round>仅管理员</el-tag>
                </div>
              </div>
              <div class="system-perm-list">
                <div class="system-perm-columns" aria-hidden="true">
                  <span>权限操作</span>
                  <span>权限标识</span>
                  <span>可访问角色</span>
                </div>
                <div v-for="row in items" :key="row.permission_key" class="system-perm-row">
                  <div class="system-perm-main">
                    <el-tag :type="opTagType({ operation_key: row.permission_key, action: row.action })" effect="plain" size="small" class="system-perm-action">
                      {{ opActionLabel({ operation_key: row.permission_key, action: row.action }) }}
                    </el-tag>
                    <span class="system-perm-desc" :title="row.description">{{ row.description }}</span>
                  </div>
                  <code class="perm-key" :title="row.permission_key">{{ row.permission_key }}</code>
                  <div class="system-perm-roles">
                    <el-tag
                      v-for="r in row.roles"
                      :key="r"
                      size="small"
                      :effect="r === 'ADMIN' ? 'dark' : 'plain'"
                      :type="r === 'ADMIN' ? '' : 'warning'"
                      round
                      class="role-chip"
                    >{{ roleLabel(r) }}</el-tag>
                  </div>
                </div>
              </div>
            </section>
          </div>
        </div>
      </div>
    </template>

    <!-- ══════════════════════════════════════
         角色管理抽屉
    ══════════════════════════════════════ -->
    <el-drawer
      v-model="templateDrawerVisible"
      title="角色与权限"
      size="720px"
      direction="rtl"
      :close-on-click-modal="true"
      :before-close="handleTemplateDrawerBeforeClose"
      @open="onTemplateDrawerOpen"
    >
      <div class="drawer-tabs-shell">
        <el-tabs v-model="templateDrawerTab" class="drawer-tabs">

          <!-- 标签页：角色列表 -->
          <el-tab-pane label="角色定义" name="list">
            <el-table :data="sortedTemplates" v-loading="templatesLoading" stripe class="template-table">
              <el-table-column prop="name" label="角色名称" min-width="140">
                <template #default="{ row }">
                  <span class="template-name">{{ roleDisplayName(row) }}</span>
                </template>
              </el-table-column>
              <el-table-column prop="description" label="描述" min-width="160" show-overflow-tooltip />
              <el-table-column prop="template_type" label="类型" width="100">
                <template #default="{ row }">
                  <el-tag :type="typeTagType(row.template_type)" size="small">{{ typeLabel(row.template_type) }}</el-tag>
                </template>
              </el-table-column>
              <el-table-column label="权限数" width="80">
                <template #default="{ row }">{{ countPermissions(row) }}</template>
              </el-table-column>
              <el-table-column v-if="isAdmin" label="操作" width="120" fixed="right">
                <template #default="{ row }">
                  <el-button v-if="isAdmin" link type="primary" @click="openEditTemplate(row)">编辑</el-button>
                  <el-button v-if="isAdmin" link type="danger" :disabled="row.is_system" @click="confirmDeleteTemplate(row)">删除</el-button>
                </template>
              </el-table-column>
            </el-table>
          </el-tab-pane>

          <!-- 标签页：生效管理 -->
          <el-tab-pane label="项目角色" name="active" :disabled="!selectedStudyId">
            <div class="active-roles-desc">
              <el-alert
                title="生效的角色将映射到成员管理的项目角色中，未生效的角色不可在成员管理中分配。"
                type="info"
                :closable="false"
                show-icon
                style="margin-bottom: 16px"
              />
            </div>
            <div class="active-roles-list" v-loading="activeRolesLoading">
              <div v-for="role in roleOptions" :key="role.key" class="active-role-row">
                <div class="active-role-info">
                  <span class="active-role-icon">{{ role.icon }}</span>
                  <div>
                    <div class="active-role-name">{{ role.label }}</div>
                    <div class="active-role-desc">{{ role.desc }}</div>
                    <div v-if="roleActiveStatusLabel(role.key)" class="active-role-meta">
                      {{ roleActiveStatusLabel(role.key) }}
                    </div>
                  </div>
                </div>
                <el-tooltip
                  :disabled="!roleActiveDisableReason(role.key)"
                  :content="roleActiveDisableReason(role.key)"
                  placement="top"
                >
                  <span>
                    <el-switch
                      :model-value="activeRolesDraft.includes(role.key)"
                      :disabled="!canToggleActiveRole(role.key)"
                      @change="(v: boolean) => toggleActiveRole(role.key, v)"
                    />
                  </span>
                </el-tooltip>
              </div>
            </div>
          </el-tab-pane>

          <!-- 标签页：编辑权限 -->
          <el-tab-pane label="权限配置" name="edit-role" :disabled="!selectedStudyId">
            <div class="role-editor-layout" v-loading="roleEditorLoading">
              <!-- 左侧角色列表 -->
              <div class="role-editor-sidebar">
                <button
                  v-for="role in roleOptions"
                  :key="role.key"
                  type="button"
                  class="role-sidebar-item"
                  :class="{ active: editingRole === role.key }"
                  :aria-pressed="editingRole === role.key"
                  @click="selectRoleForEdit(role.key)"
                >
                  <span class="role-sidebar-icon">{{ role.icon }}</span>
                  <div class="role-sidebar-info">
                    <div class="role-sidebar-name">{{ role.label }}</div>
                    <div class="role-sidebar-count" v-if="editingRole === role.key && Object.keys(roleEditorDraft).length">
                      {{ Object.values(roleEditorDraft).filter(Boolean).length }} 项已授权
                    </div>
                  </div>
                  <el-icon v-if="editingRole === role.key" class="role-sidebar-arrow"><ArrowRight /></el-icon>
                </button>
              </div>

              <!-- 右侧权限编辑区 -->
              <div class="role-editor-content">
                <template v-if="editingRole">
                  <div v-if="!canEditSelectedRole" class="role-editor-notice">
                    <el-alert :title="editingRole === 'PM' ? '仅系统管理员可修改项目负责人权限' : '该角色权限不可修改'" type="info" :closable="false" show-icon />
                  </div>
                  <div class="role-editor-toolbar">
                    <el-input v-model="roleEditorSearch" placeholder="搜索权限..." clearable class="role-editor-search">
                      <template #prefix><el-icon><Search /></el-icon></template>
                    </el-input>
                    <el-select v-model="roleEditorScope" class="role-editor-scope" style="width: 140px">
                      <el-option label="全部权限" value="all" />
                      <el-option label="仅已授权" value="granted" />
                      <el-option label="仅本次变更" value="changed" />
                    </el-select>
                    <div class="role-editor-change-summary" :class="{ 'role-editor-change-summary--dirty': roleEditorChangeCount > 0 }">
                      <template v-if="roleEditorChangeCount">
                        <span>新增授权 {{ roleEditorGrantedCount }} 项</span>
                        <span>取消授权 {{ roleEditorRevokedCount }} 项</span>
                      </template>
                      <span v-else>暂无变更</span>
                    </div>
                  </div>
                  <div class="role-editor-modules">
                    <div v-for="(sections, mod) in roleEditorSectionGroups" :key="mod" class="role-editor-module">
                      <div class="role-editor-module-header">
                        <div class="role-editor-module-heading">
                          <span>{{ roleEditorModuleLabel(mod) }}</span>
                          <span>{{ roleEditorEnabledCount(roleEditorModuleOps(sections)) }} / {{ roleEditorModuleCount(sections) }} 已授权</span>
                        </div>
                        <div v-if="canEditSelectedRole" class="role-editor-module-actions">
                          <el-button link type="primary" size="small" @click="setRoleEditorPermissions(roleEditorModuleOps(sections), true)">全部授权</el-button>
                          <el-button link size="small" @click="setRoleEditorPermissions(roleEditorModuleOps(sections), false)">全部取消</el-button>
                        </div>
                      </div>
                      <el-collapse class="role-editor-collapse">
                        <el-collapse-item
                          v-for="section in sections"
                          :key="`${mod}:${section.label}`"
                          :name="`${mod}:${section.label}`"
                          class="role-editor-section"
                        >
                          <template #title>
                            <span class="role-editor-section-title">{{ section.label }}</span>
                            <span class="role-editor-section-summary">{{ roleEditorEnabledCount(section.ops) }} / {{ section.ops.length }} 已授权</span>
                            <span v-if="canEditSelectedRole" class="role-editor-section-actions" @click.stop>
                              <el-button link type="primary" size="small" @click="setRoleEditorPermissions(section.ops, true)">全选</el-button>
                              <el-button link size="small" @click="setRoleEditorPermissions(section.ops, false)">清空</el-button>
                            </span>
                          </template>
                          <div class="role-editor-ops">
                            <div v-for="op in section.ops" :key="op.operation_key" class="role-editor-op-row">
                              <el-checkbox
                                :model-value="roleEditorDraft[op.operation_key] ?? false"
                                :disabled="!canEditSelectedRole"
                                @change="(v: boolean) => handleRolePermissionChange(op, v)"
                              />
                              <el-tag :type="opTagType(op)" size="small" class="op-action-tag" effect="light">{{ opActionLabel(op) }}</el-tag>
                              <span class="op-desc">{{ op.description || op.operation_key }}</span>
                            </div>
                          </div>
                        </el-collapse-item>
                      </el-collapse>
                    </div>
                    <el-empty v-if="Object.keys(roleEditorSectionGroups).length === 0" description="没有符合当前筛选条件的权限" />
                  </div>
                </template>
                <el-empty v-else description="请从左侧选择角色" style="padding: 80px 0" />
              </div>
            </div>
          </el-tab-pane>

        </el-tabs>
        <div class="drawer-tab-actions">
          <el-button v-if="templateDrawerTab === 'list' && isAdmin" type="primary" @click="openCreateTemplate">创建角色</el-button>
          <el-button v-else-if="templateDrawerTab === 'active'" type="primary" :loading="activeRolesSaving" @click="saveActiveRoles">
            保存
          </el-button>
          <template v-else-if="templateDrawerTab === 'edit-role' && editingRole">
            <el-button @click="cancelRoleEditor">取消</el-button>
            <el-button
              type="primary"
              :loading="roleEditorSaving"
              :disabled="!canEditSelectedRole || !roleEditorDirtyGuard.isDirty.value"
              @click="saveRoleEditor"
            >
              保存 {{ roleLabel(editingRole) }} 权限
            </el-button>
          </template>
        </div>
      </div>
    </el-drawer>

    <!-- 新增/编辑角色对话框 -->
    <el-dialog v-model="templateDialogVisible" :title="editingTemplateId ? '编辑角色' : '创建角色'" width="520px" @close="resetTemplateForm">
      <el-form :model="templateForm" :rules="templateRules" ref="templateFormRef" label-width="90px">
        <el-form-item label="角色类型" prop="template_type">
          <el-select v-model="templateForm.template_type" style="width: 100%" :disabled="Boolean(editingTemplateId)">
            <el-option label="预设角色" value="ROLE" :disabled="!editingTemplateId" />
            <el-option label="自定义角色" value="CUSTOM" :disabled="Boolean(editingTemplateId)" />
          </el-select>
        </el-form-item>
        <el-form-item label="角色名称" prop="name">
          <el-input v-model="templateForm.name" placeholder="请输入角色名称" />
        </el-form-item>
        <el-form-item label="描述">
          <el-input v-model="templateForm.description" type="textarea" :rows="2" placeholder="可选描述" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="templateDialogVisible = false">取消</el-button>
        <el-button type="primary" :loading="templateSaving" @click="submitTemplateForm">保存</el-button>
      </template>
    </el-dialog>

    <!-- 新增成员对话框 -->
    <el-dialog v-model="addMemberVisible" width="480px" class="member-form-dialog" :show-close="false" @close="resetAddMemberForm">
      <div class="member-form-panel">
        <div class="member-form-panel__header">
          <h2>新增成员</h2>
          <button type="button" class="member-form-panel__close" aria-label="关闭" @click="addMemberVisible = false">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 6l12 12M18 6L6 18" /></svg>
          </button>
        </div>
        <div class="member-form-panel__body">
          <el-form :model="addMemberForm" :rules="addMemberRules" ref="addMemberFormRef" label-width="80px">
            <el-form-item label="用户" prop="user_id">
              <el-select v-model="addMemberForm.user_id" filterable placeholder="请选择用户" style="width: 100%">
                <el-option
                  v-for="u in availableCandidates"
                  :key="u.id"
                  :label="u.full_name"
                  :value="u.id"
                  :disabled="!u.is_active"
                />
              </el-select>
            </el-form-item>
            <el-form-item label="项目角色" prop="role_in_study">
              <el-select v-model="addMemberForm.role_in_study" style="width: 100%">
                <el-option v-for="(label, val) in assignableRoleLabels" :key="val" :label="label" :value="val" />
              </el-select>
            </el-form-item>
          </el-form>
        </div>
        <div class="member-form-panel__footer">
          <el-button @click="addMemberVisible = false">取消</el-button>
          <el-button type="primary" :loading="addMemberSaving" @click="submitAddMember">添加</el-button>
        </div>
      </div>
    </el-dialog>

    <!-- 权限校验 Dialog -->
    <el-dialog
      v-model="prereqCheckVisible"
      width="520px"
      :close-on-click-modal="false"
      :show-close="prereqCheckStep === 'failed'"
      class="prereq-check-dialog"
      title="权限依赖校验"
      align-center
    >
      <!-- 校验步骤动画 -->
      <div class="prereq-check-body">
        <div class="prereq-check-steps">
          <div
            v-for="(item, i) in prereqCheckItems"
            :key="i"
            class="prereq-check-step"
            :class="`prereq-check-step--${item.status}`"
          >
            <span class="prereq-step-icon">
              <svg v-if="item.status === 'ok'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>
              <svg v-else-if="item.status === 'fail'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
              <svg v-else-if="item.status === 'warn'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
              <span v-else class="prereq-step-spinner" />
            </span>
            <span class="prereq-step-label">{{ item.label }}</span>
          </div>
        </div>

        <!-- 通过状态 -->
        <transition name="prereq-result">
          <div v-if="prereqCheckStep === 'passed'" class="prereq-result prereq-result--pass">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
            <span>所有权限依赖校验通过</span>
          </div>
        </transition>

        <!-- 失败：缺少前置权限 -->
        <transition name="prereq-result">
          <div v-if="prereqCheckStep === 'failed'" class="prereq-issues">
            <div class="prereq-issues-header">
              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
              <span>发现 {{ prereqMissingItems.length + prereqRemoveItems.length }} 项依赖冲突，需要处理后才能保存</span>
            </div>

            <!-- 缺少前置权限 -->
            <div v-if="prereqMissingItems.length" class="prereq-section">
              <div class="prereq-section-title">
                <span class="prereq-section-badge prereq-section-badge--add">需补充前置权限</span>
                <el-button size="small" type="primary" @click="grantAllPrereqs" class="prereq-grant-all-btn">
                  一键授权全部前置
                </el-button>
              </div>
              <div class="prereq-item-list">
                <div v-for="(item, i) in prereqMissingItems" :key="i" class="prereq-item prereq-item--missing">
                  <div class="prereq-item-info">
                    <div class="prereq-item-row">
                      <span class="prereq-item-chip prereq-item-chip--enabled">已启用</span>
                      <span class="prereq-item-name">{{ rolePermissionDisplayLabel(item.dependentKey) }}</span>
                    </div>
                    <div class="prereq-item-arrow">依赖</div>
                    <div class="prereq-item-row">
                      <span class="prereq-item-chip prereq-item-chip--missing">缺少</span>
                      <span class="prereq-item-name">{{ rolePermissionDisplayLabel(item.prereqKey) }}</span>
                    </div>
                  </div>
                  <el-button
                    size="small"
                    type="success"
                    :disabled="roleEditorDraft[item.prereqKey] === true"
                    @click="grantSinglePrereq(item.prereqKey)"
                  >授权</el-button>
                </div>
              </div>
            </div>

            <!-- 需取消的下游权限 -->
            <div v-if="prereqRemoveItems.length" class="prereq-section">
              <div class="prereq-section-title">
                <span class="prereq-section-badge prereq-section-badge--remove">受影响的下游权限</span>
              </div>
              <div class="prereq-item-list">
                <div v-for="(item, i) in prereqRemoveItems" :key="i" class="prereq-item prereq-item--remove">
                  <div class="prereq-item-info">
                    <div class="prereq-item-row">
                      <span class="prereq-item-chip prereq-item-chip--disabled">已禁用</span>
                      <span class="prereq-item-name">{{ rolePermissionDisplayLabel(item.prereqKey) }}</span>
                    </div>
                    <div class="prereq-item-arrow">影响</div>
                    <div class="prereq-item-row">
                      <span class="prereq-item-chip prereq-item-chip--will-remove">将同步取消</span>
                      <span class="prereq-item-name">{{ rolePermissionDisplayLabel(item.dependentKey) }}</span>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </transition>
      </div>

      <template #footer>
        <div v-if="prereqCheckStep === 'failed'" class="prereq-dialog-footer">
          <el-button @click="prereqCheckVisible = false">取消保存</el-button>
          <el-button
            v-if="prereqRemoveItems.length && !prereqMissingItems.length"
            type="primary"
            :loading="prereqSaving"
            @click="confirmPrereqAndSave"
          >确认调整并保存</el-button>
          <el-button
            v-if="prereqMissingItems.length === 0 && prereqRemoveItems.length === 0"
            type="primary"
            :loading="prereqSaving"
            @click="confirmPrereqAndSave"
          >已处理，保存</el-button>
        </div>
      </template>
    </el-dialog>

    <!-- 角色权限编辑抽屉已合并至角色管理抽屉 -->
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, watch, reactive } from "vue";
import { useRoute, useRouter } from "vue-router";
import { ElMessage, ElMessageBox } from "element-plus";
import { Files, Search, ArrowRight } from "@element-plus/icons-vue";
import type { FormInstance, FormRules } from "element-plus";
import type {
  ApiEndpointPermissionsResponse,
  Study,
  StudyMember,
  UserInfo,
} from "@/types/api";
import {
  fetchApiEndpointPermissions,
  updateApiEndpointPermissions,
  fetchApiOperations,
  fetchPermissionTemplates,
  createPermissionTemplate,
  updatePermissionTemplate,
  deletePermissionTemplate,
  fetchSystemPermissions,
  fetchActiveRoles,
  updateActiveRoles,
  type PermissionTemplate,
  type PermissionTemplateCreate,
  type SystemPermissionItem,
} from "@/api/projectPermissions";
import { addMember, listMemberCandidates, listMembers, removeMember, updateMember } from "@/api/members";
import { fetchStudies } from "@/api/studies";
import { useAuthStore } from "@/store/auth";
import { displayDateTime } from "@/utils/display";
import { isApiPermissionAllowed } from "@/utils/apiPermissionValue";
import { isSystemAdmin } from "@/utils/roles";
import { useDrawerDirtyGuard } from "@/utils/drawerDirtyGuard";
import {
  compareProjectPermissionModules,
  projectPermissionModuleLabel,
} from "@/utils/projectPermissionModules";
import { useRoleTemplateMeta } from "@/composables/useRoleTemplateMeta";
import ApiEndpointPermissions from "@/components/ApiEndpointPermissions.vue";
import PermissionTemplateSelector from "@/components/PermissionTemplateSelector.vue";

const route = useRoute();
const router = useRouter();
const auth = useAuthStore();
const isAdmin = computed(() => isSystemAdmin(auth.user));
const selectedProjectRole = computed(() => selectedStudy.value?.role_in_study || "");
const isSelectedProjectPm = computed(() => selectedProjectRole.value === "PM");
const canManageSelectedProject = computed(() => isAdmin.value || isSelectedProjectPm.value);
const selectedProjectPermissionAllowed = (operationKey: string) => {
  if (isAdmin.value) return true;
  const role = selectedProjectRole.value;
  if (!role || !apiMatrix.value) return false;
  return isApiPermissionAllowed(apiMatrix.value?.[role]?.[operationKey]);
};
const canListProjectMembers = computed(() => selectedProjectPermissionAllowed("project_members:read"));
const canListProjectMemberCandidates = computed(() => selectedProjectPermissionAllowed("project_members:read"));
const canCreateProjectMember = computed(() => selectedProjectPermissionAllowed("project_members:create"));
const canUpdateProjectMembers = computed(() => selectedProjectPermissionAllowed("project_members:update"));
const canDeleteProjectMembers = computed(() => selectedProjectPermissionAllowed("project_members:delete"));
const canAddProjectMember = computed(() => canCreateProjectMember.value && canListProjectMemberCandidates.value);

const { roleLabel, roleDescription, roleIcon, compareRolesByTemplateOrder, loadRoleTemplates } = useRoleTemplateMeta();
const ROLE_RANK: Record<string, number> = {
  ADMIN: 100, PM: 80, QA: 60, PV: 50, CRA: 40, CTA: 40,
};
type MemberRoleRow = { user_id: string; role_in_study: string; user?: any };
const canAssignProjectRole = (role: string) => isAdmin.value || (ROLE_RANK[role] ?? 0) < ROLE_RANK.PM;
const assignableRoleLabels = computed(() => {
  const result: Record<string, string> = {};
  for (const role of activeRolesInStudy.value) {
    if (canAssignProjectRole(role)) result[role] = roleLabel(role);
  }
  return result;
});
const memberRoleLabels = (row: MemberRoleRow) => {
  if (canUpdateProjectMember(row)) return assignableRoleLabels.value;
  return {
    ...assignableRoleLabels.value,
    [row.role_in_study]: roleLabel(row.role_in_study),
  };
};

// ── 顶层标签 ──
const tabFromPath = (): "project" | "system" => {
  const p = route.path;
  if (p.endsWith("/system")) return "system";
  return "project";
};
const activeTab = computed({
  get: () => tabFromPath(),
  set: (tab: "project" | "system") => {
    const pathMap = { system: "/admin/permissions/system", project: "/admin/permissions/project" };
    router.push({ path: pathMap[tab], query: route.query });
  },
});
const pageLoading = ref(false);

// ═══════════════════════════════════════════
// 项目权限配置
// ═══════════════════════════════════════════
const studies = ref<Study[]>([]);
const selectedStudyId = ref<string>("");
const selectedStudy = computed(() => studies.value.find((s) => s.id === selectedStudyId.value) ?? null);
const projectSubTab = ref<"api" | "members">("api");
const permissionView = ref<"roles" | "matrix">("roles");

// 接口级权限
const apiMatrix = ref<ApiEndpointPermissionsResponse | null>(null);

const currentPermissionsForTemplate = computed(() => {
  if (!apiMatrix.value) return undefined;
  const result: Record<string, Record<string, boolean>> = {};
  for (const [role, endpoints] of Object.entries(apiMatrix.value)) {
    result[role] = {};
    for (const [key, val] of Object.entries(endpoints)) result[role][key] = isApiPermissionAllowed(val);
  }
  return result;
});

const loadStudies = async () => {
  try {
    const res = await fetchStudies();
    const items = res.data.items ?? [];
    studies.value = isAdmin.value ? items : items.filter((item: Study) => item.role_in_study === "PM");
  } catch {
    ElMessage.error("加载项目列表失败");
  }
};

const loadPermissionData = async () => {
  if (!selectedStudyId.value) return;
  pageLoading.value = true;
  try {
    const res = await fetchApiEndpointPermissions(selectedStudyId.value);
    apiMatrix.value = res.data;
  } catch {
    ElMessage.error("加载权限数据失败");
  } finally {
    pageLoading.value = false;
  }
};

const onStudyChange = async (id: string) => {
  const study = studies.value.find((s) => s.id === id);
  if (!isAdmin.value && study?.role_in_study !== "PM") {
    ElMessage.error("仅项目 PM 可配置该项目");
    selectedStudyId.value = "";
    return;
  }
  apiMatrix.value = null;
  members.value = [];
  lastMemberRoleChange.value = null;
  candidates.value = [];
  activeRolesDraft.value = [];
  router.replace({ query: { ...route.query, projectId: id } });
  await loadPermissionData();
  await Promise.all([loadMembers(), loadActiveRoles()]);
  loadCandidates();
};

const activeRolesInStudy = computed(() => activeRolesDraft.value);

const flattenRolePermissions = (
  role: string,
  permissions: Record<string, boolean>
): Record<string, Record<string, boolean>> => ({
  [role]: { ...permissions },
});

// 成员管理
const members = ref<StudyMember[]>([]);
const candidates = ref<UserInfo[]>([]);
const membersLoading = ref(false);
const lastMemberRoleChange = ref<{ memberId: string; fullName: string; previousRole: string; currentRole: string } | null>(null);
const memberSearch = ref("");
const memberRoleFilter = ref("");
const memberStatusFilter = ref("");
const addMemberVisible = ref(false);
const addMemberSaving = ref(false);
const addMemberFormRef = ref<FormInstance>();
const addMemberForm = reactive({ user_id: "", role_in_study: "PM" });
const addMemberRules: FormRules = {
  user_id: [{ required: true, message: "请选择用户", trigger: "change" }],
  role_in_study: [{ required: true, message: "请选择角色", trigger: "change" }],
};

const canMutateProjectMember = (row: MemberRoleRow) => {
  if (row.user_id === auth.user?.id) return false;
  if (row.user?.is_admin) return false;
  if (!isAdmin.value && (ROLE_RANK[row.role_in_study] ?? 0) >= ROLE_RANK.PM) return false;
  return true;
};
const canUpdateProjectMember = (row: MemberRoleRow) => canUpdateProjectMembers.value && canMutateProjectMember(row);
const canDeleteProjectMember = (row: MemberRoleRow) => canDeleteProjectMembers.value && canMutateProjectMember(row);

const memberRows = computed(() =>
  members.value.map((m) => {
    const user = candidates.value.find((u) => u.id === m.user_id) || m.user;
    const effectiveStatus = user && (user as any).is_active === false ? "DISABLED_GLOBAL" : m.is_active ? "ACTIVE" : "DISABLED";
    return {
      ...m,
      full_name: (user as any)?.full_name || m.user_id,
      email: (user as any)?.email || "",
      effectiveStatus,
    };
  })
);

const memberFilterRoles = computed(() =>
  [...new Set(memberRows.value.map((row) => row.role_in_study).filter(Boolean))].sort(compareRolesByTemplateOrder)
);

const memberRoleSummary = computed(() => memberFilterRoles.value.map((role) => ({
  role,
  count: memberRows.value.filter((row) => row.role_in_study === role).length,
})));

const filteredMemberRows = computed(() => {
  const search = memberSearch.value.trim().toLowerCase();
  return memberRows.value.filter((row) => {
    const matchesSearch = !search || row.full_name.toLowerCase().includes(search) || row.email.toLowerCase().includes(search);
    const matchesRole = !memberRoleFilter.value || row.role_in_study === memberRoleFilter.value;
    const matchesStatus = !memberStatusFilter.value || row.effectiveStatus === memberStatusFilter.value;
    return matchesSearch && matchesRole && matchesStatus;
  });
});

const hasMemberFilters = computed(() => Boolean(memberSearch.value || memberRoleFilter.value || memberStatusFilter.value));

const resetMemberFilters = () => {
  memberSearch.value = "";
  memberRoleFilter.value = "";
  memberStatusFilter.value = "";
};

const availableCandidates = computed(() => {
  const ids = new Set(members.value.map((m) => m.user_id));
  return candidates.value.filter((u) => !u.is_admin && !ids.has(u.id));
});

const loadMembers = async () => {
  if (!canListProjectMembers.value || !selectedStudyId.value) return;
  membersLoading.value = true;
  try {
    const { data } = await listMembers(selectedStudyId.value, { limit: 500, include_inactive: true });
    members.value = Array.isArray(data) ? data : (data as any).items ?? [];
  } catch {
    ElMessage.error("加载成员失败");
  } finally {
    membersLoading.value = false;
  }
};

const loadCandidates = async () => {
  if (!canListProjectMemberCandidates.value || !selectedStudyId.value) {
    candidates.value = [];
    return;
  }
  try {
    const { data } = await listMemberCandidates(selectedStudyId.value, { limit: 500 });
    candidates.value = data ?? [];
  } catch {
    candidates.value = [];
  }
};

const openAddMember = () => {
  if (!canAddProjectMember.value) return;
  addMemberForm.user_id = "";
  addMemberForm.role_in_study = Object.keys(assignableRoleLabels.value)[0] || "";
  addMemberVisible.value = true;
};

const resetAddMemberForm = () => { addMemberFormRef.value?.clearValidate(); };

const submitAddMember = async () => {
  if (!canAddProjectMember.value) {
    ElMessage.warning("无权限添加成员");
    return;
  }
  const valid = await addMemberFormRef.value?.validate().catch(() => false);
  if (!valid) return;
  if (!availableCandidates.value.some((candidate) => candidate.id === addMemberForm.user_id)) {
    ElMessage.warning("请选择可添加的项目成员");
    return;
  }
  addMemberSaving.value = true;
  try {
    await addMember(selectedStudyId.value, addMemberForm);
    ElMessage.success("成员已添加");
    addMemberVisible.value = false;
    await Promise.all([loadMembers(), loadCandidates()]);
  } catch (e: any) {
    ElMessage.error(e?.response?.data?.message || "添加失败");
  } finally {
    addMemberSaving.value = false;
  }
};

const updateMemberRole = async (row: any, role: string, { trackChange = true }: { trackChange?: boolean } = {}) => {
  const member = members.value.find((item) => item.id === row.id);
  if (!member || !canUpdateProjectMember(member)) {
    ElMessage.warning("无权限更新成员");
    loadMembers();
    return false;
  }
  const previousRole = member.role_in_study;
  try {
    await updateMember(selectedStudyId.value, member.id, { role_in_study: role });
    member.role_in_study = role;
    members.value = [...members.value];
    if (trackChange && previousRole !== role) {
      lastMemberRoleChange.value = {
        memberId: member.id,
        fullName: row.full_name || member.user_id,
        previousRole,
        currentRole: role,
      };
    }
    ElMessage.success("项目角色已更新");
    return true;
  } catch (e: any) {
    ElMessage.error(e?.response?.data?.message || "更新失败");
    loadMembers();
    return false;
  }
};

const confirmMemberRoleChange = async (row: any, role: string) => {
  if (!role || role === row.role_in_study) return;
  const confirmed = await ElMessageBox.confirm(
    `确认将 ${row.full_name} 的项目角色由“${roleLabel(row.role_in_study)}”调整为“${roleLabel(role)}”？`,
    "调整项目角色",
    { type: "warning", confirmButtonText: "确认调整", cancelButtonText: "取消" }
  ).catch(() => false);
  if (!confirmed) {
    members.value = [...members.value];
    return;
  }
  await updateMemberRole(row, role);
};

const undoMemberRoleChange = async () => {
  const change = lastMemberRoleChange.value;
  if (!change) return;
  const row = memberRows.value.find((item) => item.id === change.memberId);
  if (!row) {
    lastMemberRoleChange.value = null;
    return;
  }
  const restored = await updateMemberRole(row, change.previousRole, { trackChange: false });
  if (restored) lastMemberRoleChange.value = null;
};

const toggleMemberActive = async (row: any) => {
  if (!canUpdateProjectMember(row)) {
    ElMessage.warning("无权限更新成员");
    return;
  }
  if (row.is_active) {
    const ok = await ElMessageBox.confirm("确认停用该成员？", "停用成员", { type: "warning" }).catch(() => null);
    if (!ok) return;
  }
  try {
    await updateMember(selectedStudyId.value, row.id, { is_active: !row.is_active });
    ElMessage.success(row.is_active ? "已停用" : "已启用");
    loadMembers();
  } catch (e: any) {
    ElMessage.error(e?.response?.data?.message || "操作失败");
  }
};

const deleteMember = async (row: any) => {
  if (!canDeleteProjectMember(row)) {
    ElMessage.warning("无权限移除成员");
    return;
  }
  const ok = await ElMessageBox.confirm("确认将该账号从本项目移除？账号本身不会被删除。", "移除成员", {
    type: "warning",
  }).catch(() => null);
  if (!ok) return;
  try {
    await removeMember(selectedStudyId.value, row.id);
    ElMessage.success("已移除");
    await Promise.all([loadMembers(), loadCandidates()]);
  } catch (e: any) {
    ElMessage.error(e?.response?.data?.message || "移除失败");
  }
};

// ═══════════════════════════════════════════
// 角色管理（抽屉）
// ═══════════════════════════════════════════
const templateDrawerVisible = ref(false);
const templateDrawerTab = ref<"list" | "active" | "edit-role">("list");
const templates = ref<PermissionTemplate[]>([]);
const templatesLoading = ref(false);
const templateRefreshKey = ref(0);
const templateDialogVisible = ref(false);
const editingTemplateId = ref<string | null>(null);
const templateFormRef = ref<FormInstance>();
const templateSaving = ref(false);

const activeRolesDraft = ref<string[]>([]);
const activeRolesLoading = ref(false);
const activeRolesSaving = ref(false);
const activeRolesDirtyGuard = useDrawerDirtyGuard(() => activeRolesDraft.value);

const sortedTemplates = computed(() =>
  [...templates.value].sort((a, b) => {
    if (a.category && b.category) return compareRolesByTemplateOrder(a.category, b.category);
    if (a.category) return -1;
    if (b.category) return 1;
    return 0;
  })
);

const roleOptions = computed(() => {
  const options = templates.value.filter((template) => template.category).map((template) => ({
    key: template.category!,
    label: template.name,
    icon: roleIcon(template.category),
    desc: template.description || roleDescription(template.category),
  }));
  const known = new Set(options.map((role) => role.key));
  for (const role of activeRolesDraft.value) {
    if (!known.has(role)) {
      options.push({ key: role, label: roleLabel(role), icon: roleIcon(role), desc: roleDescription(role) });
      known.add(role);
    }
  }
  options.sort((a, b) => compareRolesByTemplateOrder(a.key, b.key));
  return options;
});

const onTemplateDrawerOpen = async () => {
  await loadTemplates();
  if (selectedStudyId.value) {
    await Promise.all([loadActiveRoles(), ensureOperationsLoaded()]);
  }
};

const loadActiveRoles = async () => {
  activeRolesLoading.value = true;
  try {
    const res = await fetchActiveRoles(selectedStudyId.value);
    activeRolesDraft.value = protectPmActiveRole([...res.data.active_roles]);
    activeRolesDirtyGuard.syncBaseline();
  } catch {
    ElMessage.error("加载生效角色失败");
  } finally {
    activeRolesLoading.value = false;
  }
};

const activeRoleMemberCounts = computed(() => {
  const counts: Record<string, number> = {};
  for (const member of members.value) {
    if (!member.is_active) continue;
    const role = member.role_in_study;
    if (!role) continue;
    counts[role] = (counts[role] ?? 0) + 1;
  }
  return counts;
});

const activeRoleMemberCount = (role: string) => activeRoleMemberCounts.value[role] ?? 0;
const isActiveRoleInUse = (role: string) => activeRolesDraft.value.includes(role) && activeRoleMemberCount(role) > 0;
const canToggleActiveRole = (role: string) => {
  if (!isAdmin.value && role === "PM") return false;
  if (isActiveRoleInUse(role)) return false;
  return true;
};

const roleActiveStatusLabel = (role: string) => {
  if (role === "PM" && !isAdmin.value) return "项目负责人必需";
  const count = activeRoleMemberCount(role);
  return count > 0 ? `${count} 名成员使用中` : "";
};

const roleActiveDisableReason = (role: string) => {
  if (role === "PM" && !isAdmin.value) return "PM 为项目必需角色，项目 PM 不可停用";
  if (isActiveRoleInUse(role)) return "该角色仍有关联成员，需先调整成员角色后才能停用";
  return "";
};

const protectPmActiveRole = (roles: string[]) => {
  if (!isAdmin.value && !roles.includes("PM")) roles.unshift("PM");
  return roles;
};

const toggleActiveRole = (role: string, active: boolean) => {
  if (!canToggleActiveRole(role)) return;
  if (active) {
    if (!activeRolesDraft.value.includes(role)) activeRolesDraft.value.push(role);
  } else {
    activeRolesDraft.value = activeRolesDraft.value.filter((r) => r !== role);
  }
};

const saveActiveRoles = async () => {
  if (!canManageSelectedProject.value) return;
  activeRolesSaving.value = true;
  try {
    const roles = protectPmActiveRole([...activeRolesDraft.value]);
    await updateActiveRoles(selectedStudyId.value, roles);
    activeRolesDraft.value = roles;
    activeRolesDirtyGuard.syncBaseline();
    await loadPermissionData();
    ElMessage.success("生效角色已保存");
  } catch {
    ElMessage.error("保存失败");
  } finally {
    activeRolesSaving.value = false;
  }
};

const templateForm = ref({
  name: "", description: "", template_type: "CUSTOM" as "ROLE" | "SCENARIO" | "CUSTOM",
  category: "", recommended_roles: "", tags: "",
});
const templateRules: FormRules = {
  name: [{ required: true, message: "请输入角色名称", trigger: "blur" }],
  template_type: [{ required: true, message: "请选择角色类型", trigger: "change" }],
};

const typeLabel = (t: string) => ({ ROLE: "预设角色", SCENARIO: "场景角色", CUSTOM: "自定义角色" }[t] ?? t);
const typeTagType = (t: string) =>
  ({ ROLE: "primary", SCENARIO: "warning", CUSTOM: "success" } as Record<string, any>)[t] ?? "info";
const roleDisplayName = (row: PermissionTemplate) => row.name;
const countEnabledPermissions = (permissions: Record<string, boolean>) =>
  Object.values(permissions).filter(Boolean).length;
const countPermissions = (row: PermissionTemplate) => {
  const role = row.category;
  const currentRolePermissions = role ? currentPermissionsForTemplate.value?.[role] : undefined;
  if (currentRolePermissions) return countEnabledPermissions(currentRolePermissions);

  let n = 0;
  for (const perms of Object.values(row.permissions) as Record<string, boolean>[])
    n += countEnabledPermissions(perms);
  return n;
};

const loadTemplates = async () => {
  templatesLoading.value = true;
  try {
    const res = await fetchPermissionTemplates();
    templates.value = res.data;
    await loadRoleTemplates();
    templateRefreshKey.value += 1;
  } catch {
    ElMessage.error("加载角色失败");
  } finally {
    templatesLoading.value = false;
  }
};

const openCreateTemplate = () => {
  editingTemplateId.value = null;
  resetTemplateForm();
  templateDialogVisible.value = true;
};

const openEditTemplate = (row: PermissionTemplate) => {
  editingTemplateId.value = row.id;
  templateForm.value = {
    name: row.name, description: row.description ?? "",
    template_type: row.template_type, category: row.category ?? "",
    recommended_roles: row.recommended_roles ?? "", tags: row.tags ?? "",
  };
  templateDialogVisible.value = true;
};

const resetTemplateForm = () => {
  templateForm.value = { name: "", description: "", template_type: "CUSTOM", category: "", recommended_roles: "", tags: "" };
  templateFormRef.value?.clearValidate();
};

const buildTemplateUpdatePayload = () => ({
  name: templateForm.value.name,
  description: templateForm.value.description || undefined,
});

const buildTemplateCreatePayload = (): PermissionTemplateCreate => ({
    ...templateForm.value,
    description: templateForm.value.description || undefined,
    category: templateForm.value.category || undefined,
    recommended_roles: templateForm.value.recommended_roles || undefined,
    tags: templateForm.value.tags || undefined,
    permissions: {},
});

const submitTemplateForm = async () => {
  const valid = await templateFormRef.value?.validate().catch(() => false);
  if (!valid) return;
  templateSaving.value = true;
  try {
    if (editingTemplateId.value) {
      const payload = buildTemplateUpdatePayload();
      await updatePermissionTemplate(editingTemplateId.value, payload);
      ElMessage.success("角色已更新");
    } else {
      const payload = buildTemplateCreatePayload();
      await createPermissionTemplate(payload);
      ElMessage.success("角色已创建");
    }
    templateDialogVisible.value = false;
    await loadTemplates();
  } catch {
    ElMessage.error("保存失败");
  } finally {
    templateSaving.value = false;
  }
};

const confirmDeleteTemplate = async (row: PermissionTemplate) => {
  await ElMessageBox.confirm(`确认删除角色「${row.name}」？此操作不可撤销。`, "删除确认", {
    type: "warning", confirmButtonText: "删除", confirmButtonClass: "el-button--danger",
  });
  try {
    await deletePermissionTemplate(row.id);
    ElMessage.success("已删除");
    await loadTemplates();
  } catch {
    ElMessage.error("删除失败");
  }
};

// ═══════════════════════════════════════════
// 系统级权限
// ═══════════════════════════════════════════
const systemPermissions = ref<SystemPermissionItem[]>([]);
const systemModuleLabels = ref<Record<string, string>>({});
const systemLoading = ref(false);

const systemPermissionsByModule = computed(() => {
  const map: Record<string, SystemPermissionItem[]> = {};
  for (const item of systemPermissions.value) {
    if (!map[item.module]) map[item.module] = [];
    map[item.module].push(item);
  }
  return map;
});

const loadSystemPermissions = async () => {
  systemLoading.value = true;
  try {
    const res = await fetchSystemPermissions();
    systemPermissions.value = res.data.permissions;
    systemModuleLabels.value = res.data.module_labels;
  } catch {
    ElMessage.error("加载系统级权限失败");
  } finally {
    systemLoading.value = false;
  }
};

// ═══════════════════════════════════════════
// 角色权限编辑器（抽屉）
// ═══════════════════════════════════════════
interface RoleEditorOp {
  operation_key: string;
  module: string;
  action: string;
  description: string;
  prerequisite_permissions: string[];
}

interface RoleEditorSection {
  label: string;
  ops: RoleEditorOp[];
}

interface RolePrerequisiteAdjustments {
  add: Record<string, string[]>;
  remove: Record<string, string[]>;
}

const editingRole = ref("");
const roleEditorSearch = ref("");
const roleEditorScope = ref<"all" | "granted" | "changed">("all");
const roleEditorDraft = ref<Record<string, boolean>>({});
const roleEditorSaving = ref(false);
const roleEditorLoading = ref(false);
const allOperations = ref<RoleEditorOp[]>([]);
const roleEditorDirtyGuard = useDrawerDirtyGuard(() => roleEditorDraft.value);

const handleTemplateDrawerBeforeClose = (done: () => void) => {
  if (activeRolesDirtyGuard.isDirty.value || (editingRole.value && roleEditorDirtyGuard.isDirty.value)) {
    ElMessage.warning("请先保存或取消编辑");
    return;
  }
  done();
};

const roleEditorModuleLabel = projectPermissionModuleLabel;

const SUBJECT_PERMISSION_SECTION_ORDER = ["基础信息", "病史", "访视", "AE", "PD"];

const PERMISSION_SECTION_LABELS: Record<string, string> = {
  subjects: "基础信息",
  visits: "访视",
  subject_aes: "AE",
  subject_pds: "PD",
  subject_histories: "病史",
  monitoring_issues: "监查访视问题",
  project_milestones: "项目里程碑",
  material_equipments_attachments: "设备管理",
  startup_initiation: "立项",
  startup_initiation_attachments: "立项",
  startup_ethics: "伦理",
  startup_ethics_attachments: "伦理",
  precautions: "注意事项",
  precautions_attachments: "注意事项",
  faq: "医学咨询/FAQ",
  faq_category: "医学咨询/FAQ",
  faq_reply: "医学咨询/FAQ",
  faq_attachments: "医学咨询/FAQ",
};

const getPermissionSectionLabel = (op: RoleEditorOp): string => {
  const prefix = op.operation_key.split(":")[0] || op.module;
  return PERMISSION_SECTION_LABELS[prefix] || roleEditorModuleLabel(op.module);
};

const roleEditorSectionLabel = (op: RoleEditorOp): string => getPermissionSectionLabel(op);

const comparePermissionSectionLabels = (a: string, b: string): number => {
  const aIndex = SUBJECT_PERMISSION_SECTION_ORDER.indexOf(a);
  const bIndex = SUBJECT_PERMISSION_SECTION_ORDER.indexOf(b);
  if (aIndex !== -1 || bIndex !== -1) {
    return (aIndex === -1 ? Number.MAX_SAFE_INTEGER : aIndex) - (bIndex === -1 ? Number.MAX_SAFE_INTEGER : bIndex);
  }
  return a.localeCompare(b, "zh-CN");
};

const getOpVerb = (op: { operation_key: string; action: string }): string => {
  const suffix = op.operation_key.split(":").pop() || "";
  if (suffix === "create") return "create";
  if (suffix === "read" || suffix === "list") return "read";
  if (suffix === "update") return "update";
  if (suffix === "delete") return "delete";
  if (suffix === "export") return "export";
  return op.action;
};

const opActionLabel = (op: { operation_key: string; action: string }) => {
  const verb = getOpVerb(op);
  return ({ create: "创建", read: "读取", update: "更新", delete: "删除", export: "导出", write: "写入" }[verb] ?? verb);
};

const opTagType = (op: { operation_key: string; action: string }) => {
  const verb = getOpVerb(op);
  return (({ create: "success", read: "info", update: "warning", delete: "danger", export: "", write: "success" } as Record<string, string>)[verb] ?? "info");
};

const roleEditorGrouped = computed(() => {
  const search = roleEditorSearch.value.toLowerCase();
  const filtered = allOperations.value.filter((op) => {
    const matchesSearch =
      !search ||
      op.description.toLowerCase().includes(search) ||
      op.operation_key.toLowerCase().includes(search) ||
      roleEditorModuleLabel(op.module).includes(search);
    const currentAllowed = Boolean(roleEditorDraft.value[op.operation_key]);
    const originalAllowed = originalRolePermissionAllowed(op.operation_key);
    const matchesScope =
      roleEditorScope.value === "all" ||
      (roleEditorScope.value === "granted" && currentAllowed) ||
      (roleEditorScope.value === "changed" && currentAllowed !== originalAllowed);
    return matchesSearch && matchesScope;
  });
  const sorted = [...filtered].sort((a, b) => {
    const moduleDiff = compareProjectPermissionModules(a.module, b.module);
    if (moduleDiff !== 0) return moduleDiff;
    return allOperations.value.indexOf(a) - allOperations.value.indexOf(b);
  });
  const map: Record<string, RoleEditorOp[]> = {};
  for (const op of sorted) {
    if (!map[op.module]) map[op.module] = [];
    map[op.module].push(op);
  }
  return map;
});

const roleEditorSectionGroups = computed(() => {
  const result: Record<string, RoleEditorSection[]> = {};
  for (const [mod, ops] of Object.entries(roleEditorGrouped.value)) {
    const sections = new Map<string, RoleEditorOp[]>();
    for (const op of ops) {
      const label = roleEditorSectionLabel(op);
      if (!sections.has(label)) sections.set(label, []);
      sections.get(label)!.push(op);
    }
    result[mod] = [...sections.entries()]
      .sort(([a], [b]) => comparePermissionSectionLabels(a, b))
      .map(([label, sectionOps]) => ({
        label,
        ops: sectionOps,
      }));
  }
  return result;
});

const roleEditorModuleCount = (sections: RoleEditorSection[]): number =>
  sections.reduce((total, section) => total + section.ops.length, 0);

const roleEditorModuleOps = (sections: RoleEditorSection[]): RoleEditorOp[] =>
  sections.flatMap((section) => section.ops);

const roleEditorEnabledCount = (ops: RoleEditorOp[]): number =>
  ops.filter((op) => roleEditorDraft.value[op.operation_key]).length;

const originalRolePermissionAllowed = (operationKey: string): boolean => {
  const permission = apiMatrix.value?.[editingRole.value]?.[operationKey];
  if (typeof permission === "boolean") return permission;
  return Boolean(permission?.allowed);
};

const roleEditorGrantedCount = computed(() =>
  allOperations.value.filter((op) =>
    Boolean(roleEditorDraft.value[op.operation_key]) && !originalRolePermissionAllowed(op.operation_key)
  ).length
);

const roleEditorRevokedCount = computed(() =>
  allOperations.value.filter((op) =>
    !roleEditorDraft.value[op.operation_key] && originalRolePermissionAllowed(op.operation_key)
  ).length
);

const roleEditorChangeCount = computed(() => roleEditorGrantedCount.value + roleEditorRevokedCount.value);

const operationByKey = computed(() => {
  const map: Record<string, RoleEditorOp> = {};
  for (const op of allOperations.value) map[op.operation_key] = op;
  return map;
});

const roleOperationLabel = (operationKey: string): string =>
  operationByKey.value[operationKey]?.description || operationKey;

const roleOperationActionLabel = (operationKey: string): string => {
  const op = operationByKey.value[operationKey];
  if (!op) return "授权";
  const verb = getOpVerb(op);
  if (verb === "read") return "读取";
  if (verb === "delete") return "删除";
  return "写入";
};

const rolePermissionDisplayLabel = (operationKey: string): string =>
  `${roleOperationActionLabel(operationKey)}｜${roleOperationLabel(operationKey)}`;

const previewRolePrerequisiteAdjustments = (draft: Record<string, boolean>): RolePrerequisiteAdjustments => {
  const add: Record<string, string[]> = {};
  const remove: Record<string, string[]> = {};
  const baseline: Record<string, boolean> = {};
  const candidate = { ...draft };
  const explicitlyDenied = new Set<string>();

  const role = editingRole.value;
  if (apiMatrix.value && apiMatrix.value[role]) {
    for (const [key, val] of Object.entries(apiMatrix.value[role] as Record<string, any>)) {
      baseline[key] = typeof val === "boolean" ? val : val.allowed;
    }
  }

  for (const [key, allowed] of Object.entries(draft)) {
    if (baseline[key] !== allowed && !allowed) explicitlyDenied.add(key);
  }

  // 先补充缺失的前置权限（add 优先）
  let changed = true;
  while (changed) {
    changed = false;
    for (const op of allOperations.value) {
      if (!candidate[op.operation_key]) continue;
      for (const prereq of op.prerequisite_permissions) {
        if (candidate[prereq]) continue;
        const prerequisites = add[op.operation_key] || [];
        if (!prerequisites.includes(prereq)) prerequisites.push(prereq);
        add[op.operation_key] = prerequisites;
        candidate[prereq] = true;
        explicitlyDenied.delete(prereq); // 补回来的前置不再是"明确禁用"
        changed = true;
      }
    }
  }

  // 再级联禁用受影响的下游权限（remove）
  changed = true;
  while (changed) {
    changed = false;
    for (const op of allOperations.value) {
      if (!candidate[op.operation_key] || explicitlyDenied.has(op.operation_key)) continue;
      const deniedPrereq = op.prerequisite_permissions.find((key) => explicitlyDenied.has(key));
      if (!deniedPrereq) continue;
      const dependents = remove[deniedPrereq] || [];
      if (!dependents.includes(op.operation_key)) dependents.push(op.operation_key);
      remove[deniedPrereq] = dependents;
      candidate[op.operation_key] = false;
      explicitlyDenied.add(op.operation_key);
      changed = true;
    }
  }

  return { add, remove };
};

const hasPrerequisiteAdjustments = (adjustments: RolePrerequisiteAdjustments): boolean =>
  Object.keys(adjustments.add).length > 0 || Object.keys(adjustments.remove).length > 0;

const handleRolePermissionChange = (op: RoleEditorOp, allowed: boolean) => {
  roleEditorDraft.value = { ...roleEditorDraft.value, [op.operation_key]: allowed };
};

const setRoleEditorPermissions = (ops: RoleEditorOp[], allowed: boolean) => {
  if (!canEditSelectedRole.value) return;
  const next = { ...roleEditorDraft.value };
  for (const op of ops) next[op.operation_key] = allowed;
  roleEditorDraft.value = next;
};

const canEditSelectedRole = computed(() => {
  if (editingRole.value === "ADMIN") return false;
  if (isAdmin.value) return true;
  if (editingRole.value === "PM") return false;
  return true;
});

const selectRoleForEdit = (role: string) => {
  editingRole.value = role;
  roleEditorSearch.value = "";
  roleEditorScope.value = "all";
  initRoleEditor();
};

const openRoleEditor = async (role: string) => {
  templateDrawerTab.value = "edit-role";
  templateDrawerVisible.value = true;
  await ensureOperationsLoaded();
  selectRoleForEdit(role);
};

const ensureOperationsLoaded = async () => {
  if (allOperations.value.length) return;
  roleEditorLoading.value = true;
  try {
    const res = await fetchApiOperations();
    allOperations.value = res.data.operations;
  } catch {
    ElMessage.error("加载权限操作失败");
  } finally {
    roleEditorLoading.value = false;
  }
};

const initRoleEditor = () => {
  const role = editingRole.value;
  const draft: Record<string, boolean> = {};
  if (apiMatrix.value && apiMatrix.value[role]) {
    for (const [key, val] of Object.entries(apiMatrix.value[role] as Record<string, any>)) {
      draft[key] = typeof val === "boolean" ? val : val.allowed;
    }
  }
  roleEditorDraft.value = draft;
  roleEditorDirtyGuard.syncBaseline();
};

const cancelRoleEditor = () => {
  editingRole.value = "";
  roleEditorSearch.value = "";
  roleEditorScope.value = "all";
  roleEditorDraft.value = {};
  roleEditorDirtyGuard.syncBaseline();
};

// ── 权限校验 Dialog ──
const prereqCheckVisible = ref(false);
const prereqCheckStep = ref<"checking" | "passed" | "failed">("checking");
const prereqCheckItems = ref<Array<{ label: string; status: "checking" | "ok" | "fail" | "warn" }>>([]);
const prereqAdjustments = ref<RolePrerequisiteAdjustments>({ add: {}, remove: {} });
const prereqSaving = ref(false);

const prereqCheckLabels = [
  "分析已启用权限依赖关系",
  "检查前置权限完整性",
  "检查下游权限一致性",
];

const openPrereqCheck = async (): Promise<boolean> => {
  prereqCheckVisible.value = true;
  prereqCheckStep.value = "checking";
  prereqCheckItems.value = prereqCheckLabels.map((label) => ({ label, status: "checking" }));
  prereqAdjustments.value = { add: {}, remove: {} };

  // 逐步动画显示校验过程
  for (let i = 0; i < prereqCheckLabels.length; i++) {
    await new Promise((r) => setTimeout(r, 320 + i * 180));
    prereqCheckItems.value[i].status = "ok";
  }

  const adjustments = previewRolePrerequisiteAdjustments(roleEditorDraft.value);
  prereqAdjustments.value = adjustments;

  if (!hasPrerequisiteAdjustments(adjustments)) {
    prereqCheckStep.value = "passed";
    await new Promise((r) => setTimeout(r, 600));
    prereqCheckVisible.value = false;
    return true;
  }

  // 标记状态：有缺失前置 → 红叉；纯下游受影响 → 黄色警告
  const hasAdd = Object.keys(adjustments.add).length > 0;
  for (const item of prereqCheckItems.value) {
    item.status = hasAdd ? "fail" : "warn";
  }
  prereqCheckStep.value = "failed";
  return false;
};

const prereqMissingItems = computed(() => {
  const items: Array<{ dependentKey: string; prereqKey: string }> = [];
  for (const [dependentKey, prereqs] of Object.entries(prereqAdjustments.value.add)) {
    for (const prereqKey of prereqs) {
      items.push({ dependentKey, prereqKey });
    }
  }
  return items;
});

const prereqRemoveItems = computed(() => {
  const items: Array<{ prereqKey: string; dependentKey: string }> = [];
  for (const [prereqKey, dependents] of Object.entries(prereqAdjustments.value.remove)) {
    for (const dependentKey of dependents) {
      items.push({ prereqKey, dependentKey });
    }
  }
  return items;
});

const markAllStepsOk = () => {
  prereqCheckItems.value = prereqCheckItems.value.map((item) => ({ ...item, status: "ok" as const }));
};

const grantSinglePrereq = (prereqKey: string) => {
  roleEditorDraft.value = { ...roleEditorDraft.value, [prereqKey]: true };
  prereqAdjustments.value = previewRolePrerequisiteAdjustments(roleEditorDraft.value);
  if (!hasPrerequisiteAdjustments(prereqAdjustments.value)) {
    markAllStepsOk();
    prereqCheckStep.value = "passed";
    setTimeout(() => confirmPrereqAndSave(), 800);
  }
};

const grantAllPrereqs = () => {
  const next = { ...roleEditorDraft.value };
  for (const prereqs of Object.values(prereqAdjustments.value.add)) {
    for (const p of prereqs) next[p] = true;
  }
  roleEditorDraft.value = next;
  prereqAdjustments.value = previewRolePrerequisiteAdjustments(next);
  markAllStepsOk();
  prereqCheckStep.value = "passed";
  setTimeout(() => confirmPrereqAndSave(), 800);
};

const confirmPrereqAndSave = async () => {
  prereqSaving.value = true;
  try {
    const payload = flattenRolePermissions(editingRole.value, roleEditorDraft.value);
    const res = await updateApiEndpointPermissions(selectedStudyId.value, payload, { confirm_prerequisite_adjustments: true });
    apiMatrix.value = res.data;
    const savedRole = editingRole.value;
    prereqCheckVisible.value = false;
    editingRole.value = "";
    roleEditorDirtyGuard.syncBaseline();
    ElMessage.success(`${roleLabel(savedRole)} 权限已保存`);
  } catch (error: any) {
    ElMessage.error(error?.response?.data?.detail?.message || "保存失败");
  } finally {
    prereqSaving.value = false;
  }
};

const saveRoleEditor = async () => {
  if (!selectedStudyId.value || !apiMatrix.value) return;
  if (!canEditSelectedRole.value) {
    ElMessage.warning("当前角色权限不可修改");
    return;
  }
  const passed = await openPrereqCheck();
  if (!passed) return; // Dialog 保持打开，等用户操作

  // 校验通过，直接保存
  roleEditorSaving.value = true;
  try {
    const payload = flattenRolePermissions(editingRole.value, roleEditorDraft.value);
    const res = await updateApiEndpointPermissions(selectedStudyId.value, payload);
    apiMatrix.value = res.data;
    const savedRole = editingRole.value;
    editingRole.value = "";
    roleEditorDirtyGuard.syncBaseline();
    ElMessage.success(`${roleLabel(savedRole)} 权限已保存`);
  } catch (error: any) {
    if (error === "cancel" || error === "close") return;
    ElMessage.error(error?.response?.data?.detail?.message || "保存失败");
  } finally {
    roleEditorSaving.value = false;
  }
};

// ═══════════════════════════════════════════
// 懒加载 & 初始化
// ═══════════════════════════════════════════
watch(() => route.path, (path) => {
  if (path.endsWith("/system") && !systemPermissions.value.length) loadSystemPermissions();
});

onMounted(async () => {
  await loadStudies();
  await loadTemplates();

  const qProjectId = route.query.projectId as string | undefined;
  const qSub = route.query.sub as string | undefined;
  const initialStudyId = qProjectId || studies.value[0]?.id;
  if (initialStudyId) {
    selectedStudyId.value = initialStudyId;
    if (qSub === "members") projectSubTab.value = "members";
    await loadPermissionData();
    await Promise.all([loadMembers(), loadActiveRoles()]);
    loadCandidates();
  }

  const tab = tabFromPath();
  if (tab === "system" && !systemPermissions.value.length) loadSystemPermissions();
});
</script>

<style scoped>
/* ── 页面容器：铺满，无 padding ── */
.perm-page {
  display: flex;
  flex-direction: column;
  margin: -6px -8px;
  min-height: calc(100vh - 52px);
  min-height: calc(100dvh - 52px);
  background: #f5f7fa;
}

/* ── 顶部标题栏 ── */
.perm-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 14px;
  background: #fff;
  border-bottom: 1px solid #e2e8f0;
  flex-shrink: 0;
  gap: 10px;
}

.perm-header-left {
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.perm-title {
  margin: 0;
  font-size: 15px;
  font-weight: 700;
  color: #1a2332;
  line-height: 1.3;
}

.perm-header-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-shrink: 0;
}

.project-selector {
  display: flex;
  align-items: center;
  gap: 8px;
}

.project-selector :deep(.el-select) {
  --el-select-input-color: #193b5a;
  --el-select-multiple-input-color: #193b5a;
  --el-input-text-color: #193b5a;
}

.project-selector :deep(.el-select__wrapper),
.project-selector :deep(.el-select__selection) {
  color: #193b5a;
}

.project-selector :deep(.el-select__selected-item) {
  color: #193b5a !important;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
  font-size: 13px;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: 0.01em;
  font-variant-numeric: tabular-nums;
}

.project-selector :deep(.el-select__input) {
  color: #193b5a !important;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.01em;
  font-variant-numeric: tabular-nums;
}

.project-selector :deep(.el-select__placeholder) {
  color: #94a3b8;
  font-size: 13px;
}

.project-selector :deep(.el-select__caret) {
  color: #315f7a;
}

.project-selector-icon {
  width: 16px;
  height: 16px;
  color: #315f7a;
  flex-shrink: 0;
}

/* ── 内容区：可滚动 ── */
.perm-body {
  flex: 1;
  overflow-y: auto;
  padding: 0 0 8px;
}

.perm-empty {
  padding: 80px 28px;
  display: flex;
  justify-content: center;
}

.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  text-align: center;
  max-width: 360px;
}

.empty-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 72px;
  height: 72px;
  border-radius: 20px;
  background: linear-gradient(135deg, #eff6ff, #e0f2fe);
  color: #3b82f6;
}

.empty-icon svg {
  width: 36px;
  height: 36px;
}

.empty-state h3 {
  margin: 0;
  font-size: 16px;
  font-weight: 600;
  color: #1a2332;
}

.empty-state p {
  margin: 0;
  font-size: 13px;
  color: #64748b;
  line-height: 1.6;
}

.perm-body-padded {
  padding: 0 28px;
}

/* ── 项目权限：子 tabs ── */
.project-sub-tabs {
  background: #fff;
  border-radius: 0;
  padding: 0 12px 12px;
}

:deep(.project-sub-tabs .el-tabs__header) {
  margin-bottom: 8px;
}

.sub-tab-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 8px;
  padding: 7px 10px;
  background: #f8fafc;
  border-radius: 8px;
  border: 1px solid #f1f5f9;
}

.sub-tab-info {
  display: flex;
  align-items: center;
  gap: 10px;
}

.sub-tab-project-name {
  font-size: 12px;
  font-weight: 600;
  color: #334155;
}

.sub-tab-actions {
  display: flex;
  align-items: center;
  gap: 8px;
}

.permission-view-switch :deep(.el-radio-button__inner) {
  min-width: 78px;
}

.sub-tab-spacer { flex: 1; }

/* ── 成员管理表格 ── */
.members-table-wrapper {
  border-radius: 8px;
  border: 1px solid #e2e8f0;
  overflow: hidden;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
}

.members-table {
  width: 100%;
}

.member-name-cell {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
}

.member-name {
  font-size: 12px;
  font-weight: 500;
  color: #1a2332;
}

.member-email {
  max-width: 260px;
  overflow: hidden;
  color: #81909e;
  font-size: 10.5px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.members-filter-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 8px;
  padding: 6px 8px;
  border: 1px solid #e5ebf0;
  border-radius: 7px;
  background: #fbfcfd;
}

.members-filter-controls {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
}

.members-filter-result {
  flex-shrink: 0;
  color: #718294;
  font-size: 10.5px;
  font-weight: 500;
}

.members-filter-meta {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 10px;
  min-width: 0;
}

.member-undo {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  color: #64788a;
  font-size: 10.5px;
  white-space: nowrap;
}

:global(.member-form-dialog.el-dialog) {
  --el-dialog-bg-color: transparent !important;
  --el-dialog-box-shadow: none !important;
  --el-dialog-border-radius: 0 !important;
  overflow: visible !important;
  margin: 0 auto !important;
  padding: 0 !important;
  border: 0 !important;
  background: transparent !important;
  box-shadow: none !important;
  backdrop-filter: none !important;
}

:global(.member-form-dialog .el-dialog__header) {
  display: none;
}

:global(.member-form-dialog .el-dialog__body) {
  padding: 0 !important;
  background: transparent !important;
}

.member-form-panel {
  overflow: hidden;
  border: 1px solid #dce5eb;
  border-radius: 10px;
  background: #fff;
  box-shadow: 0 18px 48px rgba(15, 23, 42, 0.16);
}

.member-form-panel__header,
.member-form-panel__footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 14px;
}

.member-form-panel__header {
  border-bottom: 1px solid #e8edf1;
}

.member-form-panel__header h2 {
  margin: 0;
  color: #1e293b;
  font-size: 14px;
  font-weight: 700;
}

.member-form-panel__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: #718096;
  cursor: pointer;
}

.member-form-panel__close:hover {
  background: #f1f5f9;
  color: #334155;
}

.member-form-panel__close svg {
  width: 16px;
  height: 16px;
}

.member-form-panel__body {
  padding: 14px 14px 6px;
}

.member-form-panel__footer {
  justify-content: flex-end;
  gap: 8px;
  border-top: 1px solid #e8edf1;
  background: #fafbfc;
}

.time-text {
  font-size: 11px;
  color: #64748b;
}

.full-table { width: 100%; }

:deep(.perm-page .el-input__wrapper),
:deep(.perm-page .el-select__wrapper) {
  min-height: 30px;
}

:deep(.perm-page .el-button) {
  min-height: 28px;
  padding: 5px 9px;
  font-size: 12px;
}

:deep(.members-table .el-table__header-wrapper th.el-table__cell) {
  padding: 6px 0;
  font-size: 11px;
}

:deep(.members-table .el-table__body-wrapper td.el-table__cell) {
  padding: 5px 0;
  font-size: 12px;
}

/* ── 系统级权限：模块块 ── */
.system-perm-container {
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.perm-body--system {
  padding-bottom: 0;
}

.system-stats {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  overflow: hidden;
  border: 1px solid #dfe7f0;
  border-radius: 12px;
  background: #fff;
  box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04);
}

.system-stat-item {
  position: relative;
  display: flex;
  min-width: 0;
  min-height: 46px;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 6px 12px;
  border-left: 1px solid #edf1f6;
}

.system-stat-item:first-child {
  border-left: 0;
}

.system-stat-item::before {
  position: absolute;
  top: 0;
  right: 0;
  left: 0;
  height: 2px;
  background: #3b82f6;
  content: "";
}

.system-stat-item--permissions::before {
  background: #10b981;
}

.system-stat-item--pm::before {
  background: #f59e0b;
}

.system-stat-label {
  overflow: hidden;
  color: #64748b;
  font-size: 12px;
  font-weight: 600;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.system-stat-value {
  display: flex;
  align-items: baseline;
  gap: 3px;
  flex-shrink: 0;
}

.system-stat-value strong {
  color: #172033;
  font-size: 18px;
  font-weight: 720;
  line-height: 1;
}

.system-stat-value span {
  color: #94a3b8;
  font-size: 11px;
}

.system-permissions-card {
  overflow: hidden;
  background: #fff;
  border-radius: 12px;
  border: 1px solid #e2e8f0;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
}

.system-module-block {
  background: #fff;
}

.system-module-block + .system-module-block {
  border-top: 1px solid #dbe4ee;
}

.system-module-header {
  display: flex;
  align-items: center;
  gap: 9px;
  min-height: 42px;
  padding: 6px 12px;
  border-bottom: 1px solid #e8edf3;
  background: #fbfcfe;
}

.system-module-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 7px;
  background: linear-gradient(135deg, #eff6ff, #e0f2fe);
  color: #3b82f6;
  flex-shrink: 0;
}

.system-module-icon.module-icon--system_users {
  background: linear-gradient(135deg, #fef3c7, #fde68a);
  color: #d97706;
}

.system-module-icon.module-icon--system_projects {
  background: linear-gradient(135deg, #ede9fe, #ddd6fe);
  color: #7c3aed;
}

.system-module-icon.module-icon--system_permissions {
  background: linear-gradient(135deg, #ecfdf5, #d1fae5);
  color: #059669;
}

.system-module-icon.module-icon--system_monitoring {
  background: linear-gradient(135deg, #eff6ff, #dbeafe);
  color: #2563eb;
}

.system-module-icon.module-icon--system_audit {
  background: linear-gradient(135deg, #fef2f2, #fecaca);
  color: #dc2626;
}

.system-module-icon svg {
  width: 14px;
  height: 14px;
}

.system-module-info {
  display: flex;
  flex-direction: row;
  align-items: baseline;
  gap: 7px;
  flex: 1;
  min-width: 0;
}

.system-module-title {
  font-size: 13px;
  font-weight: 650;
  color: #1a2332;
}

.system-module-count {
  font-size: 11px;
  color: #94a3b8;
}

.system-module-roles-summary {
  flex-shrink: 0;
}

.system-module-roles-summary :deep(.el-tag) {
  height: 20px;
  padding: 0 7px;
  font-size: 10.5px;
}

.system-perm-list {
  display: flex;
  flex-direction: column;
  gap: 0;
}

.system-perm-columns,
.system-perm-row {
  display: grid;
  grid-template-columns: minmax(240px, 1.2fr) minmax(240px, 1fr) minmax(180px, 0.8fr);
  gap: 10px;
  align-items: center;
}

.system-perm-columns {
  min-height: 26px;
  padding: 4px 12px;
  border-bottom: 1px solid #e8edf3;
  background: #f7f9fc;
  color: #64748b;
  font-size: 10.5px;
  font-weight: 650;
  letter-spacing: 0.02em;
}

.system-perm-row {
  min-height: 36px;
  padding: 4px 12px;
  border-bottom: 1px solid #edf1f5;
  transition: background 0.14s ease;
}

.system-perm-row:last-child {
  border-bottom: 0;
}

.system-perm-row:hover {
  background: #f3f7fb;
}

.system-perm-main {
  display: flex;
  align-items: center;
  gap: 7px;
  min-width: 0;
  justify-self: start;
  width: 100%;
}

.system-perm-action {
  flex-shrink: 0;
  min-width: 38px;
  height: 20px;
  padding: 0 6px;
  text-align: center;
  font-size: 10.5px;
}

.system-perm-desc {
  min-width: 0;
  flex: 1;
  font-size: 12px;
  color: #334155;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.system-perm-roles {
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
  justify-content: flex-start;
  justify-self: start;
}

.role-chip {
  height: 20px;
  padding: 0 7px;
  font-size: 10.5px;
}

.system-perm-table { width: 100%; }

.perm-key {
  display: block;
  overflow: hidden;
  max-width: 100%;
  justify-self: start;
  font-family: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;
  font-size: 10.5px;
  background: #f4f7fa;
  padding: 2px 6px;
  border-radius: 5px;
  color: #475569;
  border: 1px solid #dfe7ef;
  text-overflow: ellipsis;
  white-space: nowrap;
}

@media (max-width: 920px) {
  .system-perm-columns,
  .system-perm-row {
    grid-template-columns: minmax(0, 1fr) auto;
  }

  .system-perm-columns {
    display: none;
  }

  .system-perm-main {
    grid-column: 1;
    grid-row: 1;
  }

  .system-perm-roles {
    grid-column: 2;
    grid-row: 1;
  }

  .perm-key {
    grid-column: 1 / -1;
    grid-row: 2;
  }
}

@media (max-width: 620px) {
  .system-perm-container {
    padding: 0;
  }

  .system-stats {
    grid-template-columns: 1fr;
  }

  .system-stat-item {
    min-height: 42px;
    border-top: 1px solid #edf1f6;
    border-left: 0;
  }

  .system-stat-item:first-child {
    border-top: 0;
  }

  .system-module-header {
    flex-wrap: wrap;
  }

  .system-module-roles-summary {
    width: 100%;
    padding-left: 46px;
  }

  .system-perm-row {
    grid-template-columns: minmax(0, 1fr);
    gap: 7px;
    padding: 10px 12px;
  }

  .system-perm-main,
  .system-perm-roles,
  .perm-key {
    grid-column: 1;
    grid-row: auto;
  }
}

/* ── 抽屉内部 ── */
.drawer-tabs-shell {
  position: relative;
  display: flex;
  flex-direction: column;
  min-height: calc(100vh - 110px);
}

.drawer-tabs {
  flex: 1;
  padding-top: 4px;
}

.drawer-tab-actions {
  position: sticky;
  z-index: 4;
  right: auto;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  width: 100%;
  min-height: 58px;
  margin-top: 12px;
  padding-top: 10px;
  border-top: 1px solid #e8edf2;
  background: #fff;
  box-shadow: 0 -8px 18px rgba(255, 255, 255, 0.92);
}

.template-table {
  margin-top: 12px;
}

/* ── 生效角色列表 ── */
.active-roles-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.active-role-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 16px;
  border-radius: 12px;
  border: 1px solid #e2e8f0;
  background: #fff;
  transition: all 0.15s ease;
}

.active-role-row:hover {
  background: #f8fafc;
  border-color: #cbd5e1;
}

.active-role-info {
  display: flex;
  align-items: center;
  gap: 12px;
}

.active-role-icon {
  font-size: 22px;
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #f8fafc;
  border-radius: 10px;
}

.active-role-name {
  font-size: 14px;
  font-weight: 600;
  color: #1a2332;
}

.active-role-desc {
  font-size: 12px;
  color: #64748b;
  margin-top: 2px;
}

.active-role-meta {
  font-size: 12px;
  color: #b45309;
  margin-top: 4px;
  font-weight: 500;
}

.template-name { font-weight: 600; color: #1a2332; }

/* ── 角色编辑器 ── */
.role-editor-layout {
  display: flex;
  height: calc(100vh - 200px);
  gap: 0;
}

.role-editor-sidebar {
  width: 170px;
  flex-shrink: 0;
  border-right: 1px solid #f1f5f9;
  overflow-y: auto;
  padding: 4px 0;
}

.role-sidebar-item {
  display: flex;
  align-items: center;
  width: calc(100% - 12px);
  gap: 8px;
  padding: 10px 12px;
  cursor: pointer;
  border: 1px solid transparent;
  border-radius: 10px;
  margin: 2px 6px;
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  transition: all 0.15s ease;
}

.role-sidebar-item:hover { background: #f8fafc; }

.role-sidebar-item:focus-visible {
  outline: 3px solid rgba(59, 130, 246, 0.22);
  outline-offset: -1px;
}

.role-sidebar-item.active {
  background: linear-gradient(135deg, #eff6ff, #e0f2fe);
  border: 1px solid #bfdbfe;
}

.role-sidebar-icon {
  font-size: 18px;
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #fff;
  border-radius: 8px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}

.role-sidebar-info { flex: 1; min-width: 0; }

.role-sidebar-name {
  font-size: 13px;
  font-weight: 500;
  color: #334155;
}

.role-sidebar-item.active .role-sidebar-name { color: #1d4ed8; font-weight: 600; }

.role-sidebar-count {
  font-size: 11px;
  color: #10b981;
  margin-top: 2px;
  font-weight: 500;
}

.role-sidebar-arrow {
  font-size: 12px;
  color: #3b82f6;
  flex-shrink: 0;
}

.role-editor-content {
  flex: 1;
  overflow-y: auto;
  padding: 0 16px;
  display: flex;
  flex-direction: column;
}

.role-editor-modules {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.role-editor-toolbar {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 18px;
  padding: 4px;
  background: #fff;
  border-radius: 12px;
  border: 1px solid #e2e8f0;
  box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
}

.role-editor-change-summary {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-shrink: 0;
  padding: 0 10px;
  color: #8795a3;
  font-size: 11.5px;
  font-weight: 600;
}

.role-editor-change-summary--dirty {
  color: #315f7a;
}

.role-editor-change-summary--dirty span + span {
  padding-left: 10px;
  border-left: 1px solid #dbe4ea;
}

.role-editor-search {
  flex: 1;
}

.role-editor-scope {
  flex-shrink: 0;
}

:deep(.role-editor-search .el-input__wrapper) {
  background: transparent;
  border-radius: 8px;
  box-shadow: none;
  padding: 4px 12px;
  transition: background 0.18s ease;
}

:deep(.role-editor-search .el-input__wrapper:hover) {
  background: #f8fafc;
  box-shadow: none;
}

:deep(.role-editor-search .el-input__wrapper.is-focus) {
  background: #eff6ff;
  box-shadow: none;
}

.role-editor-module {
  background: #fff;
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04);
  transition: box-shadow 0.18s ease, border-color 0.18s ease;
}

.role-editor-module:hover {
  border-color: #cbd5e1;
  box-shadow: 0 4px 14px rgba(15, 23, 42, 0.06);
}

.role-editor-module-header {
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  justify-content: space-between;
  font-size: 12px;
  font-weight: 700;
  color: #0f172a;
  padding: 8px 12px 8px 16px;
  background: linear-gradient(135deg, #f1f5ff 0%, #f8fafc 100%);
  border-bottom: 1px solid #e2e8f0;
  letter-spacing: 0.2px;
}

.role-editor-module-heading {
  display: flex;
  align-items: baseline;
  gap: 9px;
}

.role-editor-module-heading > span:last-child {
  color: #718294;
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0;
}

.role-editor-module-actions {
  display: flex;
  align-items: center;
  gap: 2px;
  flex-shrink: 0;
}

.role-editor-module-actions :deep(.el-button + .el-button) {
  margin-left: 2px;
}

.role-editor-module-header::before {
  content: "";
  position: absolute;
  left: 0;
  top: 12px;
  bottom: 12px;
  width: 4px;
  border-radius: 0 4px 4px 0;
  background: linear-gradient(180deg, #3b82f6 0%, #60a5fa 100%);
}

.role-editor-ops {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 4px;
}

.role-editor-collapse {
  border: 0;
  background: #fff;
}

.role-editor-section {
  background: #fff;
}

.role-editor-section-title {
  font-size: 12px;
  font-weight: 600;
  color: #1e293b;
  margin-right: 8px;
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

.role-editor-section-title::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: #94a3b8;
  flex-shrink: 0;
}

.role-editor-section-summary {
  margin-left: auto;
  margin-right: 8px;
  font-size: 10.5px;
  color: #64748b;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.role-editor-section-actions {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  margin-right: 10px;
}

.role-editor-section-actions :deep(.el-button + .el-button) {
  margin-left: 2px;
}

:deep(.role-editor-section .el-collapse-item__header) {
  height: 36px;
  padding: 0 12px;
  background: #fff;
  border-bottom: 1px solid #f1f5f9;
  font-weight: 600;
  transition: background 0.15s ease;
}

:deep(.role-editor-section:last-child .el-collapse-item__header) {
  border-bottom: 0;
}

:deep(.role-editor-section .el-collapse-item__header:hover) {
  background: #f8fafc;
}

:deep(.role-editor-section .el-collapse-item__header.is-active) {
  background: #f8fafc;
  border-bottom-color: #e2e8f0;
}

:deep(.role-editor-section .el-collapse-item__header.is-active .role-editor-section-title::before) {
  background: #3b82f6;
}

:deep(.role-editor-section .el-collapse-item__arrow) {
  margin-right: 0;
  color: #94a3b8;
}

:deep(.role-editor-section .el-collapse-item__content) {
  padding: 2px 4px 5px;
  background: #fafbfc;
}

:deep(.role-editor-section .el-collapse-item__wrap) {
  border-bottom: 0;
  background: #fafbfc;
}

.role-editor-op-row {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 5px 7px;
  border-radius: 5px;
  transition: background 0.15s ease;
}

.role-editor-op-row:hover {
  background: #eef2ff;
}

.op-action-tag {
  flex-shrink: 0;
  width: 42px;
  text-align: center;
  font-weight: 600;
  border-radius: 6px;
}

.op-desc {
  font-size: 12px;
  color: #1e293b;
}

@media (max-width: 720px) {
  .role-editor-toolbar,
  .role-editor-module-header {
    align-items: stretch;
    flex-direction: column;
  }

  .role-editor-change-summary {
    justify-content: space-between;
    padding: 7px 10px;
    border-top: 1px solid #edf1f5;
  }

  .role-editor-module-actions {
    justify-content: flex-end;
  }

  .role-editor-scope {
    width: 100% !important;
  }

  .members-filter-bar {
    align-items: stretch;
    flex-direction: column;
  }

  .members-filter-controls > * {
    width: 100% !important;
  }

  .members-filter-result {
    text-align: right;
  }

  .members-filter-meta {
    align-items: flex-end;
    flex-direction: column;
  }
}
</style>

<style>
.project-selector-dropdown .el-select-dropdown__item {
  color: #1f3954;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.01em;
  font-variant-numeric: tabular-nums;
}

.project-selector-dropdown .el-select-dropdown__item.is-selected {
  color: #123f63;
  background: #f4f8fc;
  font-weight: 600;
}

.project-selector-dropdown .el-select-dropdown__item.is-hovering {
  background: #f1f6fa;
}

/* ══════════════════════════════════════
   权限依赖校验 Dialog
══════════════════════════════════════ */
.prereq-check-body {
  padding: 4px 0 8px;
}

.prereq-check-steps {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-bottom: 20px;
}

.prereq-check-step {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  border-radius: 8px;
  background: #f8fafc;
  border: 1px solid #e8edf3;
  transition: all 0.3s ease;
  font-size: 14px;
  color: #64748b;
}

.prereq-check-step--ok {
  background: #f0fdf4;
  border-color: #bbf7d0;
  color: #15803d;
}

.prereq-check-step--fail {
  background: #fff7ed;
  border-color: #fed7aa;
  color: #c2410c;
}

.prereq-check-step--warn {
  background: #fefce8;
  border-color: #fde68a;
  color: #92400e;
}

.prereq-step-icon {
  width: 20px;
  height: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

.prereq-step-icon svg {
  width: 16px;
  height: 16px;
}

.prereq-check-step--ok .prereq-step-icon svg { stroke: #16a34a; }
.prereq-check-step--fail .prereq-step-icon svg { stroke: #dc2626; }
.prereq-check-step--warn .prereq-step-icon svg { stroke: #d97706; }

.prereq-step-spinner {
  display: block;
  width: 14px;
  height: 14px;
  border: 2px solid #cbd5e1;
  border-top-color: #6366f1;
  border-radius: 50%;
  animation: prereq-spin 0.7s linear infinite;
}

@keyframes prereq-spin {
  to { transform: rotate(360deg); }
}

.prereq-step-label {
  font-weight: 500;
}

/* 通过结果 */
.prereq-result--pass {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 16px 18px;
  background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
  border: 1.5px solid #86efac;
  border-radius: 10px;
  color: #15803d;
  font-weight: 600;
  font-size: 15px;
}

.prereq-result--pass svg {
  width: 22px;
  height: 22px;
  stroke: #16a34a;
  flex-shrink: 0;
}

/* 失败：问题列表 */
.prereq-issues {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.prereq-issues-header {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 14px;
  background: #fff7ed;
  border: 1px solid #fed7aa;
  border-radius: 8px;
  color: #9a3412;
  font-size: 13px;
  font-weight: 500;
}

.prereq-issues-header svg {
  width: 16px;
  height: 16px;
  stroke: #ea580c;
  flex-shrink: 0;
}

.prereq-section {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.prereq-section-title {
  display: flex;
  align-items: center;
  gap: 8px;
}

.prereq-section-badge {
  font-size: 11px;
  font-weight: 600;
  padding: 2px 8px;
  border-radius: 20px;
  letter-spacing: 0.02em;
}

.prereq-section-badge--add {
  background: #eff6ff;
  color: #1d4ed8;
  border: 1px solid #bfdbfe;
}

.prereq-section-badge--remove {
  background: #fef2f2;
  color: #b91c1c;
  border: 1px solid #fecaca;
}

.prereq-grant-all-btn {
  margin-left: auto;
  font-size: 12px;
}

.prereq-item-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.prereq-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border-radius: 8px;
  border: 1px solid;
}

.prereq-item--missing {
  background: #f8faff;
  border-color: #c7d7fe;
}

.prereq-item--remove {
  background: #fff8f8;
  border-color: #fecaca;
}

.prereq-item-info {
  flex: 1;
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  min-width: 0;
}

.prereq-item-row {
  display: flex;
  align-items: center;
  gap: 5px;
}

.prereq-item-arrow {
  font-size: 11px;
  color: #94a3b8;
  padding: 0 2px;
}

.prereq-item-chip {
  font-size: 10px;
  font-weight: 600;
  padding: 1px 6px;
  border-radius: 20px;
  white-space: nowrap;
}

.prereq-item-chip--enabled { background: #dcfce7; color: #15803d; }
.prereq-item-chip--missing { background: #fee2e2; color: #b91c1c; }
.prereq-item-chip--disabled { background: #f1f5f9; color: #64748b; }
.prereq-item-chip--will-remove { background: #fef9c3; color: #854d0e; }

.prereq-item-name {
  font-size: 12px;
  color: #334155;
  font-weight: 500;
}

.prereq-dialog-footer {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}

/* 进入动画 */
.prereq-result-enter-active {
  transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}

.prereq-result-enter-from {
  opacity: 0;
  transform: translateY(8px) scale(0.98);
}
</style>
