Files
xh-medical-sign-web/clinical-web/src/views/management/document-permissions/index.vue
T

962 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import {
getPermissionPolicies,
getTemplatePermissionMatrix,
getTemplatePermissions,
isPermissionMockEnabled,
batchSaveTemplatePermissions,
updateDepartmentPermissionPolicy,
updateRolePermissionPolicy,
} from '@/api/management/document-permissions'
import { getTemplatePermissionConfig } from '@/api/management/documents'
import { getUsers } from '@/api/management/users'
import type {
DepartmentPermissionPolicyRecord,
DepartmentRecord,
DocumentRecord,
PermissionPoliciesRecord,
PermissionPolicyLevel,
RolePermissionPolicyRecord,
RoleRecord,
TemplatePermissionBatchItemRequest,
TemplatePermissionRecord,
UserRecord,
} from '@/api/management/types'
import { useAppStore } from '@/stores/app'
import { useLoginStore } from '@/stores/login'
import DepartmentPermissionDrawer from './components/DepartmentPermissionDrawer.vue'
import DepartmentPermissionTable from './components/DepartmentPermissionTable.vue'
import RolePermissionMatrix from './components/RolePermissionMatrix.vue'
import { permissionColumns } from './mock'
import type {
DepartmentPermissionRow,
PermissionKey,
PermissionSubjectOption,
RolePermissionRow,
TemplatePermissionForm,
} from './types'
const policyReadPermissions = [
'sign:template:permission:query',
'system:template:permission:query',
'system:template:permission:update',
'system:template:permission:manage',
'system:document:permission:query',
'system:document:permission:update',
'system:document:permission:manage',
'system:document-permission:query',
'system:document-permission:update',
'system:document-permission:manage',
] as const
const policyWritePermissions = [
'sign:template:permission:maintain',
'system:template:permission:update',
'system:template:permission:manage',
'system:document:permission:update',
'system:document:permission:manage',
'system:document-permission:update',
'system:document-permission:manage',
] as const
const appStore = useAppStore()
const loginStore = useLoginStore()
const loading = ref(true)
const policyError = ref('')
const policies = ref<PermissionPoliciesRecord | null>(null)
const roleRecords = ref<RoleRecord[]>([])
const departmentRecords = ref<DepartmentRecord[]>([])
const userRecords = ref<UserRecord[]>([])
const roleRows = ref<RolePermissionRow[]>([])
const departmentRows = ref<DepartmentPermissionRow[]>([])
const templateCount = ref(0)
const defaultSavingId = ref('')
const departmentDrawerVisible = ref(false)
const selectedDepartment = ref<DepartmentPermissionRow | null>(null)
const templates = ref<DocumentRecord[]>([])
const templatesLoaded = ref(false)
const templateCatalogLoading = ref(false)
const templateCatalogError = ref('')
const selectedTemplateId = ref('')
const templatePermissions = ref<TemplatePermissionRecord[]>([])
const templatePermissionLoading = ref(false)
const templatePermissionSaving = ref(false)
const templatePermissionError = ref(false)
const templatePermissionErrorMessage = ref('')
const permissionPreviewUserId = ref('')
const subjectsLoaded = ref(false)
let templatePermissionRequestId = 0
let templateCatalogRequest: Promise<void> | null = null
let subjectRequest: Promise<void> | null = null
const canViewPolicy = computed(
() => isPermissionMockEnabled || hasAnyPermission(policyReadPermissions),
)
const canEditPolicy = computed(
() =>
!loginStore.isReadOnly &&
(isPermissionMockEnabled ||
policyWritePermissions.some((code) => loginStore.hasWritePermission(code))),
)
const canViewTemplateExceptions = computed(() => canViewPolicy.value)
const canEditTemplateExceptions = computed(() => canEditPolicy.value)
function hasAnyPermission(codes: readonly string[]) {
return codes.some((code) => loginStore.hasPermission(code))
}
function formatError(error: unknown, fallback: string) {
return error instanceof Error && error.message ? `${fallback}:${error.message}` : fallback
}
function mapRoleRecord(policy: RolePermissionPolicyRecord, index: number): RoleRecord {
return {
id: policy.roleId,
code: policy.code,
name: policy.name,
description: policy.description,
color: ['#6b5bd2', '#0e6e8c', '#0f9d6c', '#d9821f', '#c65d5d'][index % 5],
userCount: policy.userCount ?? undefined,
status: 'ENABLED',
version: policy.version,
}
}
function mapRoleRow(policy: RolePermissionPolicyRecord, index: number): RolePermissionRow {
return {
id: policy.roleId,
name: policy.name,
color: ['#6b5bd2', '#0e6e8c', '#0f9d6c', '#d9821f', '#c65d5d'][index % 5],
description: policy.description,
userCount: policy.userCount ?? 0,
permissions: {
VIEW: policy.view,
USE: policy.use,
MAINTAIN: policy.maintain,
},
permissionStates: {
VIEW: policy.view ? 'direct-allow' : 'none',
USE: policy.use ? 'direct-allow' : 'none',
MAINTAIN: policy.maintain ? 'direct-allow' : 'none',
},
version: policy.version,
}
}
function mapDepartmentRow(policy: DepartmentPermissionPolicyRecord): DepartmentPermissionRow {
const campusName = appStore.campusRecords.find((campus) => campus.id === policy.campusId)?.name
return {
id: policy.departmentId,
code: policy.code,
department: policy.name,
campus:
campusName || (policy.campusId === policies.value?.campusId ? appStore.selectedCampus : ''),
campusId: policy.campusId,
own: policy.own,
common: policy.common,
other: policy.other,
version: policy.version,
}
}
function syncPolicyRows(policy: PermissionPoliciesRecord) {
policies.value = policy
templateCount.value = policy.templateCount
roleRecords.value = policy.roles.map(mapRoleRecord)
departmentRecords.value = policy.departments.map((department) => ({
id: department.departmentId,
campusId: department.campusId || policy.campusId,
code: department.code,
name: department.name,
status: 'ENABLED',
sortNo: 0,
}))
roleRows.value = policy.roles.map(mapRoleRow)
departmentRows.value = policy.departments.map(mapDepartmentRow)
}
async function loadPolicies() {
loading.value = true
policyError.value = ''
try {
if (!isPermissionMockEnabled) {
await loginStore.ensureCurrentUser()
if (!canViewPolicy.value) {
policyError.value = '当前账号没有文档权限配置的查看权限'
return
}
await appStore.loadCampuses()
}
const policy = await getPermissionPolicies(appStore.selectedCampusId || undefined)
syncPolicyRows(policy)
} catch (error: unknown) {
roleRecords.value = []
departmentRecords.value = []
roleRows.value = []
departmentRows.value = []
policyError.value = formatError(error, '默认权限策略加载失败,请稍后重试')
} finally {
loading.value = false
}
}
async function getAllTemplatesForPermission(campusId?: string) {
const firstPage = await getTemplatePermissionConfig({
page: 1,
pageSize: 200,
status: 'all',
campusId,
})
const pageSize = Math.max(firstPage.pageSize, 1)
const pageCount = Math.ceil(firstPage.total / pageSize)
if (pageCount <= 1) {
return firstPage.records
}
const remainingPages = await Promise.all(
Array.from({ length: pageCount - 1 }, (_, index) =>
getTemplatePermissionConfig({
page: index + 2,
pageSize,
status: 'all',
campusId,
}),
),
)
return [firstPage, ...remainingPages].flatMap((page) => page.records)
}
async function loadTemplateCatalog() {
if (templateCatalogRequest) {
return templateCatalogRequest
}
templateCatalogLoading.value = true
templateCatalogError.value = ''
const campusId = appStore.selectedCampusId || policies.value?.campusId || undefined
const request = getAllTemplatesForPermission(campusId)
.then((records) => {
templates.value = records
templatesLoaded.value = true
if (!records.some((template) => template.id === selectedTemplateId.value)) {
selectedTemplateId.value = records[0]?.id ?? ''
templatePermissions.value = []
}
})
.catch((error: unknown) => {
templates.value = []
selectedTemplateId.value = ''
templatesLoaded.value = false
templateCatalogError.value = formatError(error, '模板例外目录加载失败,请稍后重试')
})
.finally(() => {
templateCatalogLoading.value = false
templateCatalogRequest = null
})
templateCatalogRequest = request
return request
}
async function getAllTemplatePermissionMatrix(templateId: string) {
const firstPage = await getTemplatePermissionMatrix({
page: 1,
pageSize: 200,
templateId,
})
const pageSize = Math.max(firstPage.pageSize, 1)
const pageCount = Math.ceil(firstPage.total / pageSize)
if (pageCount <= 1) {
return firstPage.records
}
const remainingPages = await Promise.all(
Array.from({ length: pageCount - 1 }, (_, index) =>
getTemplatePermissionMatrix({
page: index + 2,
pageSize,
templateId,
}),
),
)
return [firstPage, ...remainingPages].flatMap((page) => page.records)
}
async function getAllUsersForPermission() {
const campusId = appStore.selectedCampusId || policies.value?.campusId || undefined
const firstPage = await getUsers({
page: 1,
pageSize: 200,
status: 'all',
...(isPermissionMockEnabled || !campusId ? {} : { campusId }),
})
const pageSize = Math.max(firstPage.pageSize, 1)
const pageCount = Math.ceil(firstPage.total / pageSize)
if (pageCount <= 1) {
return firstPage.records
}
const remainingPages = await Promise.all(
Array.from({ length: pageCount - 1 }, (_, index) =>
getUsers({
page: index + 2,
pageSize,
status: 'all',
...(isPermissionMockEnabled || !campusId ? {} : { campusId }),
}),
),
)
return [firstPage, ...remainingPages].flatMap((page) => page.records)
}
async function loadTemplateSubjects() {
if (subjectsLoaded.value) {
return
}
if (subjectRequest) {
return subjectRequest
}
const request = getAllUsersForPermission()
.then((records) => {
userRecords.value = records
subjectsLoaded.value = true
if (!records.some((user) => user.id === permissionPreviewUserId.value)) {
permissionPreviewUserId.value = ''
}
})
.catch(() => {
userRecords.value = []
ElMessage.warning('用户授权对象加载失败,当前只能配置角色和科室例外')
})
.finally(() => {
subjectRequest = null
})
subjectRequest = request
return request
}
const templateSubjects = computed<PermissionSubjectOption[]>(() => [
...roleRecords.value.map((role) => ({
id: role.id,
type: 'ROLE' as const,
label: `${role.name}(${role.code})`,
})),
...departmentRecords.value.map((department) => ({
id: department.id,
type: 'DEPARTMENT' as const,
label: department.name,
})),
...userRecords.value.map((user) => ({
id: user.id,
type: 'USER' as const,
label: `${user.name}(${user.account})`,
})),
])
function mergeTemplatePermissions(
matrixRecords: TemplatePermissionRecord[],
effectiveRecords: TemplatePermissionRecord[],
) {
const seen = new Set<string>()
return [...matrixRecords, ...effectiveRecords.filter((record) => record.inherited)].filter(
(record) => {
const key = [
record.templateId,
record.subjectType,
record.subjectId ?? '',
record.permissionLevel,
record.effect,
record.inherited,
].join('|')
if (seen.has(key)) {
return false
}
seen.add(key)
return true
},
)
}
async function loadTemplatePermissions() {
const templateId = selectedTemplateId.value
const requestId = ++templatePermissionRequestId
if (!templateId || !templatesLoaded.value || !canViewTemplateExceptions.value) {
templatePermissions.value = []
templatePermissionError.value = false
templatePermissionErrorMessage.value = ''
templatePermissionLoading.value = false
return
}
templatePermissionLoading.value = true
templatePermissionError.value = false
templatePermissionErrorMessage.value = ''
try {
const [matrixResult, effectiveResult] = await Promise.allSettled([
getAllTemplatePermissionMatrix(templateId),
getTemplatePermissions(templateId, permissionPreviewUserId.value || undefined),
])
if (matrixResult.status === 'rejected') {
throw matrixResult.reason
}
if (requestId !== templatePermissionRequestId || templateId !== selectedTemplateId.value) {
return
}
templatePermissions.value = mergeTemplatePermissions(
matrixResult.value,
effectiveResult.status === 'fulfilled' ? effectiveResult.value : [],
)
if (effectiveResult.status === 'rejected') {
ElMessage.warning('继承权限预览加载失败,当前仅展示模板矩阵数据')
}
} catch (error: unknown) {
if (requestId !== templatePermissionRequestId) {
return
}
templatePermissions.value = []
templatePermissionError.value = true
templatePermissionErrorMessage.value = formatError(error, '模板权限加载失败,请稍后重试')
} finally {
if (requestId === templatePermissionRequestId) {
templatePermissionLoading.value = false
}
}
}
async function prepareDepartmentDrawer() {
await Promise.all([loadTemplateCatalog(), loadTemplateSubjects()])
await loadTemplatePermissions()
}
function openDepartmentDrawer(row: DepartmentPermissionRow) {
if (!canEditPolicy.value && !canViewPolicy.value) {
return
}
selectedDepartment.value = row
departmentDrawerVisible.value = true
void prepareDepartmentDrawer()
}
const policyLevelRank: Record<PermissionPolicyLevel, number> = {
NONE: 0,
VIEW: 1,
USE: 2,
MAINTAIN: 3,
}
function policyLevelForPermissions(
permissions: Record<PermissionKey, boolean>,
): PermissionPolicyLevel {
if (permissions.MAINTAIN) return 'MAINTAIN'
if (permissions.USE) return 'USE'
if (permissions.VIEW) return 'VIEW'
return 'NONE'
}
function toggleRolePolicy(roleId: string, permission: PermissionKey) {
if (!canEditPolicy.value || defaultSavingId.value) {
return
}
const row = roleRows.value.find((item) => item.id === roleId)
const source = policies.value?.roles.find((item) => item.roleId === roleId)
if (!row || !source) {
return
}
const nextPermissions = { ...row.permissions }
const permissionIndex: Record<PermissionKey, number> = { VIEW: 0, USE: 1, MAINTAIN: 2 }
const nextIndex = permissionIndex[permission]
const currentLevel = policyLevelForPermissions(nextPermissions)
const currentIndex = policyLevelRank[currentLevel] - 1
const nextLevel: PermissionPolicyLevel = row.permissions[permission]
? nextIndex <= currentIndex
? (['NONE', 'VIEW', 'USE'] as const)[nextIndex]
: currentLevel
: permission
Object.assign(nextPermissions, {
VIEW: policyLevelRank[nextLevel] >= policyLevelRank.VIEW,
USE: policyLevelRank[nextLevel] >= policyLevelRank.USE,
MAINTAIN: policyLevelRank[nextLevel] >= policyLevelRank.MAINTAIN,
})
const payload = {
campusId: source.campusId || policies.value?.campusId || appStore.selectedCampusId,
permissionLevel: nextLevel,
expectedVersion: source.version,
}
defaultSavingId.value = roleId
void updateRolePermissionPolicy(roleId, payload, source)
.then((updated) => {
const nextRoles = (policies.value?.roles ?? []).map((item) =>
item.roleId === roleId ? updated : item,
)
syncPolicyRows({
...(policies.value as PermissionPoliciesRecord),
roles: nextRoles,
})
ElMessage.success('角色默认策略已保存')
})
.catch((error: unknown) => {
ElMessage.error(formatError(error, '角色默认策略保存失败,请稍后重试'))
})
.finally(() => {
defaultSavingId.value = ''
})
}
function mapRowToDepartmentPolicy(row: DepartmentPermissionRow): DepartmentPermissionPolicyRecord {
return {
departmentId: row.id,
code: row.code ?? row.id,
name: row.department,
campusId: row.campusId ?? policies.value?.campusId ?? '',
own: row.own,
common: row.common,
other: row.other,
ownPermission: row.own ? 'USE' : 'NONE',
commonPermission: row.common ? 'USE' : 'NONE',
otherPermission: row.other ? 'USE' : 'NONE',
version: row.version,
}
}
function saveDepartmentPolicy(payload: {
own: boolean
common: boolean
other: boolean
expectedVersion?: number
}) {
const row = selectedDepartment.value
if (!row || !canEditPolicy.value || defaultSavingId.value) {
return
}
defaultSavingId.value = row.id
void updateDepartmentPermissionPolicy(
row.id,
{
campusId: row.campusId ?? policies.value?.campusId ?? appStore.selectedCampusId,
ownPermission: payload.own ? 'USE' : 'NONE',
commonPermission: payload.common ? 'USE' : 'NONE',
otherPermission: payload.other ? 'USE' : 'NONE',
expectedVersion: payload.expectedVersion,
},
mapRowToDepartmentPolicy(row),
)
.then((updated) => {
const nextDepartments = (policies.value?.departments ?? []).map((item) =>
item.departmentId === row.id ? updated : item,
)
const nextPolicy = {
...(policies.value as PermissionPoliciesRecord),
departments: nextDepartments,
}
syncPolicyRows(nextPolicy)
selectedDepartment.value = departmentRows.value.find((item) => item.id === row.id) ?? null
ElMessage.success('科室默认策略已保存')
})
.catch((error: unknown) => {
ElMessage.error(formatError(error, '科室默认策略保存失败,请稍后重试'))
})
.finally(() => {
defaultSavingId.value = ''
})
}
function handleTemplateChange(templateId: string) {
if (!templates.value.some((template) => template.id === templateId)) {
return
}
selectedTemplateId.value = templateId
templatePermissions.value = []
void loadTemplatePermissions()
}
function handlePreviewUserChange(userId: string) {
permissionPreviewUserId.value = userId
void loadTemplatePermissions()
}
function retryTemplatePermissions() {
if (templateCatalogError.value) {
templatesLoaded.value = false
void loadTemplateCatalog().then(loadTemplatePermissions)
return
}
void loadTemplatePermissions()
}
async function addTemplatePermission(form: TemplatePermissionForm) {
if (
!selectedTemplateId.value ||
templatePermissionSaving.value ||
templatePermissionError.value ||
!canEditTemplateExceptions.value
) {
return
}
if (isPermissionMockEnabled) {
ElMessage.info('演示模式不保存模板例外;真实环境会提交权限批量接口')
return
}
templatePermissionSaving.value = true
try {
await batchSaveTemplatePermissions({
operations: [
{
templateId: selectedTemplateId.value,
action: 'ADD',
...form,
},
],
})
await loadTemplatePermissions()
ElMessage.success('模板直接权限已添加')
} catch (error: unknown) {
ElMessage.error(formatError(error, '模板直接权限添加失败,请稍后重试'))
} finally {
templatePermissionSaving.value = false
}
}
async function removeTemplatePermission(permissionId: string) {
if (
!selectedTemplateId.value ||
templatePermissionSaving.value ||
templatePermissionError.value ||
!canEditTemplateExceptions.value
) {
return
}
const permission = templatePermissions.value.find((item) => item.id === permissionId)
if (!permission || permission.inherited) {
ElMessage.warning('继承权限不可直接删除')
return
}
if (isPermissionMockEnabled) {
ElMessage.info('演示模式不保存模板例外;真实环境会提交权限批量接口')
return
}
const operation: TemplatePermissionBatchItemRequest = {
templateId: selectedTemplateId.value,
action: 'REVOKE',
permissionId,
}
templatePermissionSaving.value = true
try {
await batchSaveTemplatePermissions({ operations: [operation] })
await loadTemplatePermissions()
ElMessage.success('模板直接权限已删除')
} catch (error: unknown) {
ElMessage.error(formatError(error, '模板直接权限删除失败,请稍后重试'))
} finally {
templatePermissionSaving.value = false
}
}
function closeDepartmentDrawer() {
departmentDrawerVisible.value = false
}
watch(
() => appStore.selectedCampusId,
(campusId, previousCampusId) => {
if (campusId && previousCampusId && campusId !== previousCampusId) {
departmentDrawerVisible.value = false
templatesLoaded.value = false
templates.value = []
selectedTemplateId.value = ''
templatePermissions.value = []
subjectsLoaded.value = false
userRecords.value = []
void loadPolicies()
}
},
)
onMounted(() => {
void loadPolicies()
})
</script>
<template>
<section class="permissions-page">
<section class="page-card intro-card">
<div class="card-heading">
<h2>文档库权限矩阵</h2>
<span>可见 / 可用 / 可维护分级授权,下级继承上级</span>
</div>
<p>
可见=可浏览模板;可用=可发起签署;可维护=可编辑/停用模板。科室签署员默认继承本科室「可用」权限。调整即时生效并记录审计日志。
</p>
</section>
<section v-if="loading" class="page-card loading-card" aria-label="正在加载权限配置">
<span class="loading-title" />
<span class="loading-row" />
<span class="loading-row" />
<span class="loading-row short" />
</section>
<section v-else-if="policyError" class="page-card state-card error-state">
<span>{{ policyError }}</span>
<button type="button" class="retry-button" @click="loadPolicies">重新加载</button>
</section>
<template v-else>
<section class="page-card">
<div class="card-heading section-heading">
<div>
<h2>按角色授权</h2>
<p>配置当前院区的默认角色策略;模板例外在科室“调整”抽屉中维护。</p>
</div>
<span class="policy-summary">默认策略 · {{ templateCount }} 个模板例外入口</span>
</div>
<RolePermissionMatrix
:rows="roleRows"
:columns="permissionColumns"
:saving="Boolean(defaultSavingId)"
:can-edit="canEditPolicy"
@toggle="toggleRolePolicy"
/>
<p v-if="!canEditPolicy" class="legend">当前账号仅可查看角色默认策略。</p>
</section>
<section class="page-card department-card">
<div class="card-heading section-heading">
<div>
<h2>按科室授权</h2>
<p>科室级别的文档可用范围;调整后即时生效。</p>
</div>
<span class="policy-summary">当前院区:{{ appStore.selectedCampus || '—' }}</span>
</div>
<DepartmentPermissionTable
:rows="departmentRows"
:can-edit="canEditPolicy"
@adjust="openDepartmentDrawer"
/>
</section>
</template>
<DepartmentPermissionDrawer
:visible="departmentDrawerVisible"
:department="selectedDepartment"
:templates="templates"
:selected-template-id="selectedTemplateId"
:permissions="templatePermissions"
:subjects="templateSubjects"
:policy-saving="Boolean(defaultSavingId)"
:template-catalog-loading="templateCatalogLoading"
:template-loading="templatePermissionLoading"
:template-saving="templatePermissionSaving"
:template-error="templatePermissionError"
:template-error-message="templatePermissionErrorMessage"
:template-catalog-error="templateCatalogError"
:preview-user-id="permissionPreviewUserId"
:can-edit-policy="canEditPolicy"
:can-edit-template="canEditTemplateExceptions"
@close="closeDepartmentDrawer"
@save-policy="saveDepartmentPolicy"
@update:selected-template-id="handleTemplateChange"
@update:preview-user-id="handlePreviewUserChange"
@add="addTemplatePermission"
@delete="removeTemplatePermission"
@retry="retryTemplatePermissions"
/>
</section>
</template>
<style scoped>
.permissions-page {
width: 100%;
max-width: 1440px;
margin: 0 auto;
}
.page-card {
padding: 16px 18px;
margin-bottom: 16px;
background: var(--card);
border-radius: var(--r);
box-shadow: var(--sh);
}
.intro-card {
margin-bottom: 14px;
}
.card-heading {
display: flex;
gap: 10px;
align-items: center;
margin-bottom: 14px;
}
.section-heading {
align-items: flex-start;
justify-content: space-between;
}
.card-heading h2 {
margin: 0;
color: var(--ink);
font-size: 14.5px;
line-height: 1.4;
}
.card-heading p {
margin: 5px 0 0;
color: var(--mut);
font-size: 11.5px;
font-weight: 400;
}
.card-heading > span {
color: var(--mut);
font-size: 12px;
}
.intro-card p {
margin: 0;
color: var(--mut);
font-size: 12px;
line-height: 1.8;
}
.policy-summary {
flex-shrink: 0;
padding-top: 4px;
font-size: 11.5px !important;
}
.department-card {
margin-bottom: 0;
}
.loading-card {
display: flex;
flex-direction: column;
gap: 14px;
min-height: 180px;
}
.loading-card span {
display: block;
background: linear-gradient(90deg, #edf3f5 25%, #f7fafb 37%, #edf3f5 63%);
background-size: 400% 100%;
border-radius: 6px;
animation: skeleton-shimmer 1.4s ease infinite;
}
.loading-title {
width: 28%;
height: 20px;
}
.loading-row {
width: 100%;
height: 14px;
}
.loading-row.short {
width: 70%;
}
.state-card {
display: flex;
gap: 10px;
align-items: center;
justify-content: center;
}
.error-state {
color: var(--err);
}
.retry-button {
padding: 4px 10px;
color: var(--brand);
font-size: 12px;
background: #fff;
border: 1px solid var(--brand);
border-radius: 5px;
}
.retry-button:hover {
background: var(--brand-l);
}
.legend {
margin: 10px 0 0;
color: var(--mut);
font-size: 11px;
line-height: 1.6;
}
@keyframes skeleton-shimmer {
0% {
background-position: 100% 0;
}
100% {
background-position: -100% 0;
}
}
@media (max-width: 760px) {
.section-heading {
flex-direction: column;
}
.policy-summary {
padding-top: 0;
}
}
@media (max-width: 600px) {
.card-heading {
align-items: flex-start;
flex-direction: column;
}
}
</style>