feat(权限): 新增 v-permission 按钮级权限指令并移除前端权限别名
This commit is contained in:
@@ -177,6 +177,8 @@ API 模块与页面按业务域对应。工作台页面使用 `api/workbench`
|
||||
|
||||
路由、侧栏和页面操作均按后端授权资源控制;导航每 15 秒及窗口重新聚焦时刷新,角色、菜单资源或员工角色绑定变更后也会主动刷新当前授权。角色权限从菜单资源树配置目录、菜单和按钮,员工角色绑定通过独立用户角色接口保存。菜单资源维护使用真实接口;系统参数卡片仍是演示数据,最终授权始终以后端校验为准。
|
||||
|
||||
按钮级权限统一用全局指令 `v-permission` 声明(实现见 `src/directives/permission.ts`):`v-permission="'doc:template:edit'"` 无权限时隐藏,`v-permission.disable="'doc:template:edit'"` 无权限时置灰;权限码必须与菜单资源树里 BUTTON 节点的 `code` 完全一致,前端不再维护别名映射。指令只负责体验,按钮背后的处理函数仍要用同一套权限码自行校验。文档库管理(`views/management/documents`)是本轮先落地的页面,其余页面沿用旧的 `hasPermission` 写法,待逐个统计按钮后迁移。
|
||||
|
||||
签署工作台已接入的真实接口包括:
|
||||
|
||||
- `GET /api/v1/patients`、`GET /api/v1/patients/{id}`:患者定位;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { App } from 'vue'
|
||||
|
||||
import permission from './permission'
|
||||
|
||||
export function registerDirectives(app: App) {
|
||||
app.directive('permission', permission)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { effectScope, ref, watchEffect, type Directive, type EffectScope, type Ref } from 'vue'
|
||||
|
||||
import { useLoginStore } from '@/stores/login'
|
||||
|
||||
/**
|
||||
* 指令值:单个权限码,或需要同时具备的一组权限码。
|
||||
* 数组按「且」判断,与 stores/login 的 hasPermission 口径保持一致。
|
||||
*/
|
||||
export type PermissionBindingValue = string | string[]
|
||||
|
||||
interface PermissionBindingState {
|
||||
value: Ref<PermissionBindingValue | undefined>
|
||||
scope: EffectScope
|
||||
}
|
||||
|
||||
const bindingStates = new WeakMap<HTMLElement, PermissionBindingState>()
|
||||
|
||||
function isAllowed(value: PermissionBindingValue | undefined): boolean {
|
||||
if (!value || (Array.isArray(value) && value.length === 0)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return useLoginStore().hasPermission(value)
|
||||
}
|
||||
|
||||
function applyPermission(el: HTMLElement, allowed: boolean, disableOnly: boolean) {
|
||||
if (disableOnly) {
|
||||
el.toggleAttribute('disabled', !allowed)
|
||||
el.setAttribute('aria-disabled', String(!allowed))
|
||||
return
|
||||
}
|
||||
|
||||
el.style.display = allowed ? '' : 'none'
|
||||
}
|
||||
|
||||
/**
|
||||
* 按钮级权限指令。
|
||||
*
|
||||
* - `v-permission="'doc:template:edit'"`:没有权限时隐藏元素;
|
||||
* - `v-permission.disable="'doc:template:edit'"`:没有权限时置灰,保留位置,
|
||||
* 用于「必须让操作人看到、但当前不可执行」的弹窗主按钮;
|
||||
* - 传数组时按「且」判断,与 hasPermission 一致。
|
||||
*
|
||||
* 使用约定:
|
||||
* - 权限码必须与菜单资源树里 BUTTON 节点的 code 完全一致,指令内部不做任何别名兜底;
|
||||
* - 隐藏或置灰只影响体验,后端校验仍是最终边界,因此提交接口的处理函数仍需自行校验;
|
||||
* - 与 v-if 可同时使用:隐藏走 display,不删除 DOM,不会和 v-if 的节点复用打架。
|
||||
*/
|
||||
const permission: Directive<HTMLElement, PermissionBindingValue> = {
|
||||
mounted(el, binding) {
|
||||
const value = ref<PermissionBindingValue | undefined>(binding.value)
|
||||
const disableOnly = Boolean(binding.modifiers.disable)
|
||||
const scope = effectScope()
|
||||
|
||||
// 用 watchEffect 追踪授权数据:角色、菜单资源或员工角色变更后,
|
||||
// refreshAuthorization 会让按钮自动恢复或隐藏,不必刷新页面。
|
||||
scope.run(() => {
|
||||
watchEffect(() => {
|
||||
applyPermission(el, isAllowed(value.value), disableOnly)
|
||||
})
|
||||
})
|
||||
|
||||
bindingStates.set(el, { value, scope })
|
||||
},
|
||||
updated(el, binding) {
|
||||
const state = bindingStates.get(el)
|
||||
if (state) {
|
||||
state.value.value = binding.value
|
||||
}
|
||||
},
|
||||
unmounted(el) {
|
||||
bindingStates.get(el)?.scope.stop()
|
||||
bindingStates.delete(el)
|
||||
},
|
||||
}
|
||||
|
||||
declare module 'vue' {
|
||||
interface GlobalDirectives {
|
||||
vPermission: typeof permission
|
||||
}
|
||||
}
|
||||
|
||||
export default permission
|
||||
@@ -4,10 +4,13 @@ import 'element-plus/dist/index.css'
|
||||
import { createApp } from 'vue'
|
||||
|
||||
import App from './App.vue'
|
||||
import { registerDirectives } from './directives'
|
||||
import router from './router'
|
||||
import './styles/index.css'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(pinia).use(router).use(ElementPlus).mount('#app')
|
||||
app.use(pinia).use(router).use(ElementPlus)
|
||||
registerDirectives(app)
|
||||
app.mount('#app')
|
||||
|
||||
@@ -42,32 +42,10 @@ export const useLoginStore = defineStore('login', () => {
|
||||
}
|
||||
|
||||
const permissions = authorization.value?.permissions ?? user.value?.permissions ?? []
|
||||
const legacyAliases: Record<string, string> = {
|
||||
'sign:task:query': 'sign:task:view',
|
||||
'sign:template:query': 'doc:template:view',
|
||||
'sign:template:create': 'doc:template:create',
|
||||
'sign:template:update': 'doc:template:edit',
|
||||
'sign:template:publish': 'doc:template:publish',
|
||||
'report:signing:query': 'report:sign:view',
|
||||
'report:signing:export': 'report:sign:export',
|
||||
'system:user:query': 'system:org:view',
|
||||
'system:user:create': 'system:org:emp',
|
||||
'system:user:update': 'system:org:emp',
|
||||
'system:user:disable': 'system:org:emp',
|
||||
'system:user:password:reset': 'system:org:pwd',
|
||||
'system:role:permission:query': 'system:role-permission:view',
|
||||
'system:role:permission:update': 'system:role-permission:edit',
|
||||
'audit:log:query': 'audit:log:view',
|
||||
}
|
||||
|
||||
return requiredPermissions.every((permission) => {
|
||||
const alias = legacyAliases[permission]
|
||||
return (
|
||||
permissions.includes(permission) ||
|
||||
(alias !== undefined && permissions.includes(alias)) ||
|
||||
permissions.includes('*')
|
||||
)
|
||||
})
|
||||
return requiredPermissions.every(
|
||||
(permission) => permissions.includes(permission) || permissions.includes('*'),
|
||||
)
|
||||
}
|
||||
|
||||
const accessibleMenus = computed(() =>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { DOC_TEMPLATE_PERMISSIONS } from '../permissions'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
modelValue: string
|
||||
total: number
|
||||
/** 只在演示模式下展示「重置演示数据」:真实环境不该出现这个自测入口 */
|
||||
showReset?: boolean
|
||||
canCreate?: boolean
|
||||
canExport?: boolean
|
||||
/** 模板包导出接口尚未接入,先只在演示模式展示入口;功能上线后放开这里即可 */
|
||||
showExport?: boolean
|
||||
}>(),
|
||||
{ showReset: false, canCreate: false, canExport: false },
|
||||
{ showReset: false, showExport: false },
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -42,11 +44,22 @@ function handleInput(event: Event) {
|
||||
aria-label="搜索模板名称、编号或科室"
|
||||
@input="handleInput"
|
||||
/>
|
||||
<button v-if="canCreate" type="button" class="tool-button primary" @click="emit('add')">
|
||||
<button
|
||||
v-permission="DOC_TEMPLATE_PERMISSIONS.create"
|
||||
type="button"
|
||||
class="tool-button primary"
|
||||
@click="emit('add')"
|
||||
>
|
||||
<span aria-hidden="true">+</span>
|
||||
新增文档
|
||||
</button>
|
||||
<button v-if="canExport" type="button" class="tool-button secondary" @click="emit('export')">
|
||||
<button
|
||||
v-if="showExport"
|
||||
v-permission="DOC_TEMPLATE_PERMISSIONS.export"
|
||||
type="button"
|
||||
class="tool-button secondary"
|
||||
@click="emit('export')"
|
||||
>
|
||||
导出模板包
|
||||
</button>
|
||||
<button
|
||||
|
||||
@@ -5,13 +5,10 @@ import type { TemplateVersionAction } from '@/api/management/documents'
|
||||
|
||||
import type { DocumentTemplate } from '../types'
|
||||
import { readStatusLabel, readStatusTone } from '../mock'
|
||||
import { DOC_TEMPLATE_PERMISSIONS, workflowPermission } from '../permissions'
|
||||
|
||||
const props = defineProps<{
|
||||
template: DocumentTemplate
|
||||
canEdit: boolean
|
||||
canInitiate: boolean
|
||||
canWorkflow: (action: TemplateVersionAction) => boolean
|
||||
canRemove: boolean
|
||||
/** 演示模式下「删除」是真删;真实模式下语义是下架(模板无 DELETE 接口,走归档) */
|
||||
isMock: boolean
|
||||
}>()
|
||||
@@ -31,16 +28,10 @@ const statusLabel = computed(() => readStatusLabel(props.template))
|
||||
const statusTone = computed(() => readStatusTone(props.template))
|
||||
|
||||
/** 只有已发布 / 待复审的模板可以发起签署;草稿与已归档不可用 */
|
||||
const canInitiateTemplate = computed(
|
||||
() => props.canInitiate && Boolean(props.template.currentVersionId || props.isMock),
|
||||
)
|
||||
const hasInitiableVersion = computed(() => Boolean(props.template.currentVersionId || props.isMock))
|
||||
|
||||
const initiateTitle = computed(() =>
|
||||
canInitiateTemplate.value
|
||||
? '用此模板发起签署'
|
||||
: props.canInitiate
|
||||
? '仅已发布或待复审的模板可发起签署'
|
||||
: '当前账号没有发起签署任务的权限',
|
||||
hasInitiableVersion.value ? '用此模板发起签署' : '仅已发布或待复审的模板可发起签署',
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -78,7 +69,7 @@ const primaryAction = computed<{ key: TemplateVersionAction; label: string } | n
|
||||
*/
|
||||
const secondaryActions = computed<{ key: TemplateVersionAction; label: string; title: string }[]>(
|
||||
() => {
|
||||
if (props.template.backendStatus === 'PENDING_REVIEW' && props.canWorkflow('reject')) {
|
||||
if (props.template.backendStatus === 'PENDING_REVIEW') {
|
||||
return [
|
||||
{
|
||||
key: 'reject',
|
||||
@@ -94,7 +85,7 @@ const secondaryActions = computed<{ key: TemplateVersionAction; label: string; t
|
||||
|
||||
/**
|
||||
* 本次交付暂不包含「版本历史」与「删除 / 下架」入口,先隐藏对应按钮。
|
||||
* 相关 emit、页面处理函数与接口调用保持原样,需要时把开关改回 true 即可恢复。
|
||||
* 权限码已按 doc:template:hist / doc:template:stop 接好,需要恢复时把开关改回 true 即可。
|
||||
*/
|
||||
const showVersionAction = false
|
||||
const showRemoveAction = false
|
||||
@@ -124,11 +115,17 @@ const removeTitle = computed(() => (props.isMock ? '删除此文书' : '下架
|
||||
</span>
|
||||
<div class="template-actions">
|
||||
<button type="button" class="card-button" @click="emit('preview', template)">预览</button>
|
||||
<button v-if="canEdit" type="button" class="card-button" @click="emit('edit', template)">
|
||||
<button
|
||||
v-permission="DOC_TEMPLATE_PERMISSIONS.edit"
|
||||
type="button"
|
||||
class="card-button"
|
||||
@click="emit('edit', template)"
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
v-if="showVersionAction && canInitiate"
|
||||
v-if="showVersionAction"
|
||||
v-permission="DOC_TEMPLATE_PERMISSIONS.history"
|
||||
type="button"
|
||||
class="card-button"
|
||||
title="查看该文书的版本历史"
|
||||
@@ -137,7 +134,8 @@ const removeTitle = computed(() => (props.isMock ? '删除此文书' : '下架
|
||||
版本
|
||||
</button>
|
||||
<button
|
||||
v-if="canInitiateTemplate"
|
||||
v-if="hasInitiableVersion"
|
||||
v-permission="DOC_TEMPLATE_PERMISSIONS.initiate"
|
||||
type="button"
|
||||
class="card-button primary"
|
||||
:title="initiateTitle"
|
||||
@@ -147,7 +145,8 @@ const removeTitle = computed(() => (props.isMock ? '删除此文书' : '下架
|
||||
</button>
|
||||
<!-- 还不能签署时,把「当前该做的下一步」放在同一位置,避免按钮越堆越多 -->
|
||||
<button
|
||||
v-if="primaryAction && canWorkflow(primaryAction.key)"
|
||||
v-if="primaryAction"
|
||||
v-permission="workflowPermission(primaryAction.key)"
|
||||
type="button"
|
||||
class="card-button primary"
|
||||
:title="`${primaryAction.label}后才能发起签署`"
|
||||
@@ -158,6 +157,7 @@ const removeTitle = computed(() => (props.isMock ? '删除此文书' : '下架
|
||||
<button
|
||||
v-for="action in secondaryActions"
|
||||
:key="action.key"
|
||||
v-permission="workflowPermission(action.key)"
|
||||
type="button"
|
||||
class="card-button"
|
||||
:title="action.title"
|
||||
@@ -166,7 +166,8 @@ const removeTitle = computed(() => (props.isMock ? '删除此文书' : '下架
|
||||
{{ action.label }}
|
||||
</button>
|
||||
<button
|
||||
v-if="showRemoveAction && canRemove"
|
||||
v-if="showRemoveAction"
|
||||
v-permission="DOC_TEMPLATE_PERMISSIONS.stop"
|
||||
type="button"
|
||||
class="card-button danger"
|
||||
:title="removeTitle"
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from '@/utils/signing-fields'
|
||||
|
||||
import EditorWangPane from './editors/EditorWangPane.vue'
|
||||
import { DOC_TEMPLATE_PERMISSIONS } from '../permissions'
|
||||
import type { DocumentTemplate, TemplateEditorForm } from '../types'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -30,6 +31,11 @@ const emit = defineEmits<{
|
||||
save: [form: TemplateEditorForm]
|
||||
}>()
|
||||
|
||||
/** 新增文书用创建权限,编辑已有文书用编辑权限;后端仍会再校验一次 */
|
||||
const savePermission = computed(() =>
|
||||
props.template ? DOC_TEMPLATE_PERMISSIONS.edit : DOC_TEMPLATE_PERMISSIONS.create,
|
||||
)
|
||||
|
||||
// Word 自维护方案:科室把文书从 Word/WPS 直接粘贴进来(保留表格与基本格式),
|
||||
// 或上传 .docx 由 mammoth 转换;签署时由交互管线识别"勾选"表并转为签署域。
|
||||
// 编辑引擎固定为 WangEditor(MIT)。更换引擎只需新增一个实现 EditorPaneHandle 的面板组件,
|
||||
@@ -389,7 +395,14 @@ function save() {
|
||||
<footer class="dialog-footer">
|
||||
<p>保存后进入文书库;签署时自动填充占位符,"勾选"表转为交互勾选。</p>
|
||||
<button type="button" class="footer-button secondary" @click="emit('close')">取消</button>
|
||||
<button type="button" class="footer-button primary" @click="save">保存文书</button>
|
||||
<button
|
||||
v-permission.disable="savePermission"
|
||||
type="button"
|
||||
class="footer-button primary"
|
||||
@click="save"
|
||||
>
|
||||
保存文书
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
</div>
|
||||
@@ -716,6 +729,14 @@ function save() {
|
||||
filter: brightness(0.96);
|
||||
}
|
||||
|
||||
.footer-button:disabled {
|
||||
color: #aebbc0;
|
||||
cursor: not-allowed;
|
||||
background: #f1f5f6;
|
||||
border-color: #dce5e8;
|
||||
filter: none;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.meta-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
readBackendStatus,
|
||||
saveDocumentLibrary,
|
||||
} from './mock'
|
||||
import { DOC_TEMPLATE_PERMISSIONS, workflowPermission } from './permissions'
|
||||
import type {
|
||||
DepartmentTab,
|
||||
DocumentBackendStatus,
|
||||
@@ -72,42 +73,24 @@ const appStore = useAppStore()
|
||||
const loginStore = useLoginStore()
|
||||
|
||||
const useMockLibrary = mockFlags.documents
|
||||
const canCreateTemplate = computed(
|
||||
() =>
|
||||
!loginStore.isReadOnly &&
|
||||
(useMockLibrary ||
|
||||
loginStore.hasWritePermission(['sign:template:create', 'sign:template:version:create'])),
|
||||
)
|
||||
const canEditTemplate = computed(
|
||||
() =>
|
||||
!loginStore.isReadOnly &&
|
||||
(useMockLibrary ||
|
||||
loginStore.hasWritePermission(['sign:template:update', 'sign:template:version:create'])),
|
||||
)
|
||||
const canArchiveTemplate = computed(
|
||||
() =>
|
||||
!loginStore.isReadOnly &&
|
||||
(useMockLibrary || loginStore.hasWritePermission('sign:template:archive')),
|
||||
)
|
||||
const canInitiateSigning = computed(
|
||||
() =>
|
||||
!loginStore.isReadOnly && (useMockLibrary || loginStore.hasWritePermission('sign:task:create')),
|
||||
)
|
||||
|
||||
const workflowPermissions: Record<TemplateVersionAction, string> = {
|
||||
'submit-review': 'sign:template:review:submit',
|
||||
reject: 'sign:template:review:reject',
|
||||
approve: 'sign:template:review:approve',
|
||||
publish: 'sign:template:publish',
|
||||
disable: 'sign:template:disable',
|
||||
archive: 'sign:template:archive',
|
||||
/**
|
||||
* 页面操作的服务端校验兜底。
|
||||
*
|
||||
* 按钮是否显示由模板上的 v-permission 决定,这里必须用同一套权限码再判一次:
|
||||
* 快捷键、回车提交和程序化调用不会经过按钮,只靠隐藏按钮挡不住。
|
||||
*/
|
||||
function canWriteDocument(permission: string) {
|
||||
return !loginStore.isReadOnly && (useMockLibrary || loginStore.hasWritePermission(permission))
|
||||
}
|
||||
|
||||
const canCreateTemplate = computed(() => canWriteDocument(DOC_TEMPLATE_PERMISSIONS.create))
|
||||
const canEditTemplate = computed(() => canWriteDocument(DOC_TEMPLATE_PERMISSIONS.edit))
|
||||
const canArchiveTemplate = computed(() => canWriteDocument(DOC_TEMPLATE_PERMISSIONS.stop))
|
||||
const canInitiateSigning = computed(() => canWriteDocument(DOC_TEMPLATE_PERMISSIONS.initiate))
|
||||
|
||||
function canRunTemplateWorkflow(action: TemplateVersionAction) {
|
||||
return (
|
||||
!loginStore.isReadOnly &&
|
||||
(useMockLibrary || loginStore.hasWritePermission(workflowPermissions[action]))
|
||||
)
|
||||
return canWriteDocument(workflowPermission(action))
|
||||
}
|
||||
/** 真实模式下的加载状态:区分"正在加载"与"确实没有模板" */
|
||||
const libraryLoading = ref(false)
|
||||
@@ -959,8 +942,7 @@ onMounted(() => {
|
||||
v-model="filters.keyword"
|
||||
:total="filteredTemplates.length"
|
||||
:show-reset="useMockLibrary"
|
||||
:can-create="canCreateTemplate"
|
||||
:can-export="useMockLibrary"
|
||||
:show-export="useMockLibrary"
|
||||
@add="showAddMessage"
|
||||
@export="showExportMessage"
|
||||
@reset="resetMockData"
|
||||
@@ -985,10 +967,6 @@ onMounted(() => {
|
||||
v-for="template in filteredTemplates"
|
||||
:key="template.id"
|
||||
:template="template"
|
||||
:can-edit="canEditTemplate"
|
||||
:can-initiate="canInitiateSigning"
|
||||
:can-workflow="canRunTemplateWorkflow"
|
||||
:can-remove="canArchiveTemplate"
|
||||
:is-mock="useMockLibrary"
|
||||
@preview="handlePreview"
|
||||
@edit="showEditMessage"
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { TemplateVersionAction } from '@/api/management/documents'
|
||||
|
||||
/**
|
||||
* 文档库管理的权限码。
|
||||
*
|
||||
* 必须与后端菜单资源树里 BUTTON 节点的 code 完全一致(当前为 doc:template:* 这一套)。
|
||||
* 前端不再维护别名映射:码对不上就等于没有权限,不做兜底。
|
||||
*/
|
||||
export const DOC_TEMPLATE_PERMISSIONS = {
|
||||
view: 'doc:template:view',
|
||||
index: 'doc:template:index',
|
||||
create: 'doc:template:create',
|
||||
edit: 'doc:template:edit',
|
||||
export: 'doc:template:export',
|
||||
history: 'doc:template:hist',
|
||||
props: 'doc:template:props',
|
||||
publish: 'doc:template:publish',
|
||||
stop: 'doc:template:stop',
|
||||
/** 发起签署复用签署任务的新增权限 */
|
||||
initiate: 'sign:task:create',
|
||||
} as const
|
||||
|
||||
/**
|
||||
* 版本工作流的权限码映射。
|
||||
*
|
||||
* 后端当前没有独立的「提交审核 / 审核通过 / 驳回」按钮码,因此按职责归并:
|
||||
* - 提交审核 / 重新提交归入编辑权限:改稿并送审是编辑流程的一步;
|
||||
* - 审核通过 / 驳回 / 发布 / 重新启用归入发布权限:决定文书能否上线;
|
||||
* - 停用 / 归档归入停用权限。
|
||||
* 后端补齐独立按钮码后只需改这张表,页面不需要调整。
|
||||
*/
|
||||
export const TEMPLATE_WORKFLOW_PERMISSIONS: Record<TemplateVersionAction, string> = {
|
||||
'submit-review': DOC_TEMPLATE_PERMISSIONS.edit,
|
||||
reject: DOC_TEMPLATE_PERMISSIONS.publish,
|
||||
approve: DOC_TEMPLATE_PERMISSIONS.publish,
|
||||
publish: DOC_TEMPLATE_PERMISSIONS.publish,
|
||||
disable: DOC_TEMPLATE_PERMISSIONS.stop,
|
||||
archive: DOC_TEMPLATE_PERMISSIONS.stop,
|
||||
}
|
||||
|
||||
export function workflowPermission(action: TemplateVersionAction): string {
|
||||
return TEMPLATE_WORKFLOW_PERMISSIONS[action]
|
||||
}
|
||||
@@ -82,7 +82,7 @@ const loginStore = useLoginStore()
|
||||
const canExportReport = computed(
|
||||
() =>
|
||||
!loginStore.isReadOnly &&
|
||||
(isReportsMockEnabled || loginStore.hasWritePermission('report:signing:export')),
|
||||
(isReportsMockEnabled || loginStore.hasWritePermission('report:sign:export')),
|
||||
)
|
||||
|
||||
let reportRequestId = 0
|
||||
|
||||
Reference in New Issue
Block a user