fix(auth): 完善权限控制与强制改密流程

This commit is contained in:
xy
2026-09-17 16:33:09 +08:00
parent 81922d774f
commit 61b4b19f55
22 changed files with 618 additions and 59 deletions
@@ -98,7 +98,10 @@ const canViewPolicy = computed(
() => isPermissionMockEnabled || hasAnyPermission(policyReadPermissions),
)
const canEditPolicy = computed(
() => isPermissionMockEnabled || hasAnyPermission(policyWritePermissions),
() =>
!loginStore.isReadOnly &&
(isPermissionMockEnabled ||
policyWritePermissions.some((code) => loginStore.hasWritePermission(code))),
)
const canViewTemplateExceptions = computed(() => canViewPolicy.value)
const canEditTemplateExceptions = computed(() => canEditPolicy.value)
@@ -5,8 +5,10 @@ withDefaults(
total: number
/** 只在演示模式下展示「重置演示数据」:真实环境不该出现这个自测入口 */
showReset?: boolean
canCreate?: boolean
canExport?: boolean
}>(),
{ showReset: false },
{ showReset: false, canCreate: false, canExport: false },
)
const emit = defineEmits<{
@@ -40,11 +42,11 @@ function handleInput(event: Event) {
aria-label="搜索模板名称、编号或科室"
@input="handleInput"
/>
<button type="button" class="tool-button primary" @click="emit('add')">
<button v-if="canCreate" type="button" class="tool-button primary" @click="emit('add')">
<span aria-hidden="true">+</span>
新增文档
</button>
<button type="button" class="tool-button secondary" @click="emit('export')">
<button v-if="canExport" type="button" class="tool-button secondary" @click="emit('export')">
导出模板包
</button>
<button
@@ -6,6 +6,9 @@ import { documentStatusLabels } from '../mock'
const props = defineProps<{
template: DocumentTemplate
canEdit: boolean
canInitiate: boolean
canRemove: boolean
}>()
const emit = defineEmits<{
@@ -18,12 +21,18 @@ const emit = defineEmits<{
const statusLabel = computed(() => documentStatusLabels[props.template.status])
/** 只有已发布 / 待复审的模板可以发起签署;草稿与已归档不可用 */
const canInitiate = computed(
() => props.template.status === 'published' || props.template.status === 'review',
const canInitiateTemplate = computed(
() =>
props.canInitiate &&
(props.template.status === 'published' || props.template.status === 'review'),
)
const initiateTitle = computed(() =>
canInitiate.value ? '用此模板发起签署' : '仅已发布或待复审的模板可发起签署',
canInitiateTemplate.value
? '用此模板发起签署'
: props.canInitiate
? '仅已发布或待复审的模板可发起签署'
: '当前账号没有发起签署任务的权限',
)
</script>
@@ -49,17 +58,21 @@ const initiateTitle = computed(() =>
</span>
<div class="template-actions">
<button type="button" class="card-button" @click="emit('preview', template)">预览</button>
<button type="button" class="card-button" @click="emit('edit', template)">编辑</button>
<button v-if="canEdit" type="button" class="card-button" @click="emit('edit', template)">
编辑
</button>
<button
v-if="canInitiate"
type="button"
class="card-button primary"
:disabled="!canInitiate"
:disabled="!canInitiateTemplate"
:title="initiateTitle"
@click="emit('initiate', template)"
>
发起
</button>
<button
v-if="canRemove"
type="button"
class="card-button danger"
title="删除此文书"
@@ -21,6 +21,7 @@ import type {
import type { SigningTaskRecord, SigningTemplate } from '@/api/workbench/types'
import NewSigningTaskDialog from '@/components/signing/NewSigningTaskDialog.vue'
import { useAppStore } from '@/stores/app'
import { useLoginStore } from '@/stores/login'
import { demoCampuses, demoDepartments } from '@/utils/mock-document-library'
import { mockFlags } from '@/utils/mock-flags'
import { clearMockState } from '@/utils/mock-storage'
@@ -53,8 +54,29 @@ import type {
*/
const router = useRouter()
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: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 libraryLoading = ref(false)
const libraryError = ref('')
@@ -320,6 +342,10 @@ async function handlePreview(template: DocumentTemplate) {
}
function showAddMessage() {
if (!canCreateTemplate.value) {
ElMessage.warning('当前账号没有创建文书模板的权限')
return
}
// Word 自维护方案:新增文书直接进入 Word 编辑器(粘贴/导入 .docx)。
wordEditingTemplate.value = null
wordEditorVisible.value = true
@@ -341,6 +367,11 @@ function closeWordEditor() {
* 保存后还会把模板正文冲掉。
*/
async function showEditMessage(template: DocumentTemplate) {
if (!canEditTemplate.value) {
ElMessage.warning('当前账号没有创建模板版本的权限')
return
}
if (useMockLibrary || template.contentHtml) {
wordEditingTemplate.value = template
wordEditorVisible.value = true
@@ -448,6 +479,11 @@ function toVersionPayload(form: TemplateEditorForm) {
}
async function saveTemplate(form: TemplateEditorForm) {
if ((!form.id && !canCreateTemplate.value) || (form.id && !canEditTemplate.value)) {
ElMessage.warning('当前账号没有保存此文书的权限')
return
}
if (useMockLibrary) {
if (form.id) {
updateLocalTemplate(form)
@@ -490,6 +526,11 @@ async function saveTemplate(form: TemplateEditorForm) {
* 语义上等于下架,已签署文书仍可追溯。
*/
async function removeTemplate(template: DocumentTemplate) {
if (!canArchiveTemplate.value) {
ElMessage.warning('当前账号没有下架文书的权限')
return
}
const confirmed = await ElMessageBox.confirm(
useMockLibrary
? `确定删除文书「${template.name}」?删除后不可恢复。`
@@ -571,6 +612,11 @@ async function toSigningTemplate(template: DocumentTemplate): Promise<SigningTem
}
async function openSigningDialog(template: DocumentTemplate) {
if (!canInitiateSigning.value) {
ElMessage.warning('当前账号没有发起签署任务的权限')
return
}
closePreview()
try {
@@ -660,6 +706,8 @@ onMounted(() => {
v-model="filters.keyword"
:total="filteredTemplates.length"
:show-reset="useMockLibrary"
:can-create="canCreateTemplate"
:can-export="useMockLibrary"
@add="showAddMessage"
@export="showExportMessage"
@reset="resetMockData"
@@ -684,6 +732,9 @@ onMounted(() => {
v-for="template in filteredTemplates"
:key="template.id"
:template="template"
:can-edit="canEditTemplate"
:can-initiate="canInitiateSigning"
:can-remove="canArchiveTemplate"
@preview="handlePreview"
@edit="showEditMessage"
@initiate="openSigningDialog"
@@ -23,6 +23,7 @@ defineProps<{
methodOptions: ReportSelectOption<ReportMethodFilter>[]
exportLoading: boolean
exportStatus: string
canExport: boolean
}>()
const emit = defineEmits<{
@@ -149,7 +150,13 @@ function handleKeywordChange(event: Event) {
<span class="filter-spacer" />
<span v-if="exportStatus" class="export-status" role="status">{{ exportStatus }}</span>
<button type="button" class="export-button" :disabled="exportLoading" @click="emit('export')">
<button
v-if="canExport"
type="button"
class="export-button"
:disabled="exportLoading"
@click="emit('export')"
>
{{ exportLoading ? '导出中…' : '导出' }}
</button>
</div>
@@ -21,6 +21,7 @@ import type {
ReportTaskResponseDto,
ReportTasksQueryDto,
} from '@/api/management/types'
import { useLoginStore } from '@/stores/login'
import DepartmentDistributionChart from './components/DepartmentDistributionChart.vue'
import ReportDetailTable from './components/ReportDetailTable.vue'
@@ -77,6 +78,12 @@ const exportDownloadLoading = ref(false)
const exportJob = ref<ReportExportJobResponseDto | null>(null)
const exportErrorMessage = ref('')
const router = useRouter()
const loginStore = useLoginStore()
const canExportReport = computed(
() =>
!loginStore.isReadOnly &&
(isReportsMockEnabled || loginStore.hasWritePermission('report:signing:export')),
)
let reportRequestId = 0
let taskRequestId = 0
@@ -850,6 +857,11 @@ function handleRetryTasks() {
}
async function handleExport() {
if (!canExportReport.value) {
ElMessage.warning('当前账号没有导出签署报表的权限')
return
}
if (exportLoading.value) {
return
}
@@ -1056,6 +1068,7 @@ onUnmounted(() => {
:method-options="methodOptions"
:export-loading="exportLoading"
:export-status="exportStatusText"
:can-export="canExportReport"
@update:period="updatePeriod"
@update:campus="updateCampus"
@update:department="updateDepartment"
@@ -80,19 +80,29 @@ const rolePermissionDrawerVisible = ref(false)
let userRequestId = 0
const canEditUsers = computed(
() => isUserMockEnabled || loginStore.hasPermission('system:user:update'),
() =>
!loginStore.isReadOnly &&
(isUserMockEnabled || loginStore.hasWritePermission('system:user:update')),
)
const canCreateUsers = computed(
() => isUserMockEnabled || loginStore.hasPermission('system:user:create'),
() =>
!loginStore.isReadOnly &&
(isUserMockEnabled || loginStore.hasWritePermission('system:user:create')),
)
const canDeactivateUsers = computed(
() => isUserMockEnabled || loginStore.hasPermission('system:user:disable'),
() =>
!loginStore.isReadOnly &&
(isUserMockEnabled || loginStore.hasWritePermission('system:user:disable')),
)
const canResetPasswords = computed(
() => isUserMockEnabled || loginStore.hasPermission('system:user:password:reset'),
() =>
!loginStore.isReadOnly &&
(isUserMockEnabled || loginStore.hasWritePermission('system:user:password:reset')),
)
const canImportUsers = computed(
() => isUserMockEnabled || loginStore.hasPermission('system:user:import'),
() =>
!loginStore.isReadOnly &&
(isUserMockEnabled || loginStore.hasWritePermission('system:user:import')),
)
const canViewRoleApiPermissions = computed(
() =>
@@ -100,7 +110,7 @@ const canViewRoleApiPermissions = computed(
loginStore.hasPermission(['system:permission:query', 'system:role:permission:query']),
)
const canEditRoleApiPermissions = computed(
() => !isUserMockEnabled && loginStore.hasPermission('system:role:permission:update'),
() => !isUserMockEnabled && loginStore.hasWritePermission('system:role:permission:update'),
)
const mockCampuses: CampusRecord[] = [