按角色授权
@@ -81,6 +216,24 @@ onMounted(loadPermissions)
+
+
+
+
模板权限
+ 按模板配置角色、科室和用户的可见/可用/可维护权限
+
+
+
diff --git a/clinical-web/src/views/management/document-permissions/types.ts b/clinical-web/src/views/management/document-permissions/types.ts
index 9df1cb0..dbde5ce 100644
--- a/clinical-web/src/views/management/document-permissions/types.ts
+++ b/clinical-web/src/views/management/document-permissions/types.ts
@@ -24,3 +24,16 @@ export interface DepartmentPermissionRow {
commonDocuments: DepartmentDocumentAccess
otherDocuments: DepartmentDocumentAccess
}
+
+export interface PermissionSubjectOption {
+ id: string
+ type: 'ROLE' | 'DEPARTMENT' | 'USER'
+ label: string
+}
+
+export interface TemplatePermissionForm {
+ subjectType: PermissionSubjectOption['type']
+ subjectId: string
+ permissionLevel: 'VIEW' | 'USE' | 'MAINTAIN'
+ effect: 'ALLOW' | 'DENY'
+}
diff --git a/clinical-web/src/views/management/documents/components/DocumentPreviewDialog.vue b/clinical-web/src/views/management/documents/components/DocumentPreviewDialog.vue
index 4624197..415ddc0 100644
--- a/clinical-web/src/views/management/documents/components/DocumentPreviewDialog.vue
+++ b/clinical-web/src/views/management/documents/components/DocumentPreviewDialog.vue
@@ -7,6 +7,8 @@ import type { DocumentTemplate } from '../types'
const props = defineProps<{
visible: boolean
template: DocumentTemplate | null
+ loading: boolean
+ error: boolean
}>()
const emit = defineEmits<{
@@ -49,7 +51,18 @@ const statusLabel = computed(() => {
-
+ 正在加载模板内容…
+
+ 模板内容加载失败,请关闭后重试。
+
+
+
{{ template.name }}
模板编号:{{ template.code }} · 版本 {{ template.version }}
@@ -84,9 +97,21 @@ const statusLabel = computed(() => {
@@ -194,6 +219,26 @@ const statusLabel = computed(() => {
overflow-y: auto;
}
+.preview-state {
+ display: grid;
+ min-height: 420px;
+ color: var(--mut);
+ font-size: 13px;
+ place-items: center;
+}
+
+.preview-state--error {
+ color: var(--err);
+}
+
+.preview-frame {
+ display: block;
+ width: 100%;
+ min-height: 560px;
+ background: #fff;
+ border: 1px solid #eef4f6;
+}
+
.preview-paper {
min-height: 420px;
padding: 26px 34px;
@@ -299,6 +344,14 @@ const statusLabel = computed(() => {
filter: brightness(0.96);
}
+.footer-button:disabled {
+ color: #aebbc0;
+ background: #f1f5f6;
+ border-color: #dce5e8;
+ cursor: not-allowed;
+ filter: none;
+}
+
@media (max-width: 680px) {
.preview-paper {
padding: 22px 18px;
diff --git a/clinical-web/src/views/management/documents/components/DocumentTemplateCard.vue b/clinical-web/src/views/management/documents/components/DocumentTemplateCard.vue
index 016ba1a..82b6d31 100644
--- a/clinical-web/src/views/management/documents/components/DocumentTemplateCard.vue
+++ b/clinical-web/src/views/management/documents/components/DocumentTemplateCard.vue
@@ -40,7 +40,13 @@ const statusLabel = computed(() => documentStatusLabels[props.template.status])
-
@@ -207,6 +213,17 @@ const statusLabel = computed(() => documentStatusLabels[props.template.status])
background: var(--brand-d);
}
+.card-button:disabled {
+ color: #aebbc0;
+ background: #f1f5f6;
+ border-color: #dce5e8;
+ cursor: not-allowed;
+}
+
+.card-button.primary:disabled:hover {
+ background: #f1f5f6;
+}
+
@media (max-width: 680px) {
.template-footer {
align-items: flex-start;
diff --git a/clinical-web/src/views/management/documents/components/TemplateEditorDialog.vue b/clinical-web/src/views/management/documents/components/TemplateEditorDialog.vue
new file mode 100644
index 0000000..a97e3e2
--- /dev/null
+++ b/clinical-web/src/views/management/documents/components/TemplateEditorDialog.vue
@@ -0,0 +1,498 @@
+
+
+
+
+
+
+
+
+
diff --git a/clinical-web/src/views/management/documents/index.vue b/clinical-web/src/views/management/documents/index.vue
index 219e5fc..6739f21 100644
--- a/clinical-web/src/views/management/documents/index.vue
+++ b/clinical-web/src/views/management/documents/index.vue
@@ -2,6 +2,18 @@
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
+import { getCampuses, getDepartments } from '@/api/management/organization'
+import {
+ createTemplate,
+ createTemplateVersion,
+ getDocumentDetail,
+ getDocuments,
+ getTemplateVersionDetail,
+ isDocumentMockEnabled,
+ updateTemplateVersionStatus,
+} from '@/api/management/documents'
+import type { TemplateVersionAction } from '@/api/management/documents'
+import type { CampusRecord, DepartmentRecord, DocumentRecord } from '@/api/management/types'
import NewSigningTaskDialog from '@/components/signing/NewSigningTaskDialog.vue'
import type { SigningTemplate } from '@/api/workbench/types'
@@ -9,20 +21,53 @@ import DocumentFilterTabs from './components/DocumentFilterTabs.vue'
import DocumentLibraryHeader from './components/DocumentLibraryHeader.vue'
import DocumentPreviewDialog from './components/DocumentPreviewDialog.vue'
import DocumentTemplateCard from './components/DocumentTemplateCard.vue'
+import TemplateEditorDialog from './components/TemplateEditorDialog.vue'
import { documentLibrary, documentStatusOptions } from './mock'
import type {
DepartmentTab,
+ DocumentStatus,
DocumentDepartment,
DocumentFilterForm,
DocumentTemplate,
StatusTab,
+ TemplateEditorForm,
} from './types'
const loading = ref(true)
const library = ref
([])
const selectedTemplate = ref(null)
const signingTemplate = ref(null)
+const editorTemplate = ref(null)
+const campuses = ref([])
+const departments = ref([])
const signingDialogVisible = ref(false)
+const editorVisible = ref(false)
+const editorSaving = ref(false)
+const workflowSaving = ref(false)
+const previewLoading = ref(false)
+const previewError = ref(false)
+let previewRequestId = 0
+
+const mockCampuses: CampusRecord[] = [
+ { id: 'mock-main', code: 'MAIN', name: '本部院区', status: 'ENABLED', sortNo: 1 },
+ { id: 'mock-east', code: 'EAST', name: '东院区', status: 'ENABLED', sortNo: 2 },
+ { id: 'mock-west', code: 'WEST', name: '西院区', status: 'ENABLED', sortNo: 3 },
+]
+
+const mockDepartments: DepartmentRecord[] = [
+ '放射科',
+ '心内科',
+ '骨科',
+ '健康管理中心',
+ '全院通用',
+].map((name, index) => ({
+ id: `mock-template-department-${index + 1}`,
+ campusId: index === 3 ? 'mock-east' : 'mock-main',
+ code: `MOCK-TPL-${index + 1}`,
+ name,
+ status: 'ENABLED',
+ sortNo: index + 1,
+}))
const filters = reactive({
keyword: '',
@@ -76,39 +121,362 @@ const filteredTemplates = computed(() => {
async function loadLibrary() {
loading.value = true
- await Promise.resolve()
- library.value = documentLibrary
- loading.value = false
+ try {
+ if (isDocumentMockEnabled) {
+ campuses.value = mockCampuses.map((campus) => ({ ...campus }))
+ departments.value = mockDepartments.map((department) => ({ ...department }))
+ library.value = documentLibrary.map((department) => ({
+ ...department,
+ categories: department.categories.map((category) => ({
+ ...category,
+ documents: category.documents.map((document) => ({ ...document })),
+ })),
+ }))
+ return
+ }
+
+ const [documentsResult, campusesResult, departmentsResult] = await Promise.allSettled([
+ getDocuments({ page: 1, pageSize: 200, status: 'all' }),
+ getCampuses(),
+ getDepartments(),
+ ])
+
+ if (documentsResult.status === 'rejected') {
+ throw documentsResult.reason
+ }
+
+ campuses.value = campusesResult.status === 'fulfilled' ? campusesResult.value : []
+ departments.value = departmentsResult.status === 'fulfilled' ? departmentsResult.value : []
+ library.value = buildDocumentLibrary(
+ documentsResult.value.records,
+ new Map(departments.value.map((department) => [department.id, department.name])),
+ new Map(campuses.value.map((campus) => [campus.id, campus.name])),
+ )
+ } catch {
+ library.value = []
+ ElMessage.error('文档模板加载失败,请稍后重试')
+ } finally {
+ loading.value = false
+ }
}
-function handlePreview(template: DocumentTemplate) {
+function mapDocumentStatus(status: DocumentRecord['status']): DocumentStatus {
+ if (status === 'published') {
+ return 'enabled'
+ }
+
+ if (status === 'archived') {
+ return 'archived'
+ }
+
+ if (status === 'review') {
+ return 'review'
+ }
+
+ return 'draft'
+}
+
+function buildDocumentLibrary(
+ records: DocumentRecord[],
+ departmentNames = new Map(),
+ campusNames = new Map(),
+): DocumentDepartment[] {
+ const departments = new Map()
+
+ records.forEach((record) => {
+ const departmentName =
+ record.departmentName || departmentNames.get(record.departmentId ?? '') || '全院通用'
+ const departmentKey = record.departmentId || departmentName
+ const campusName = record.campusName || campusNames.get(record.campusId ?? '')
+ const department = departments.get(departmentKey) ?? {
+ key: departmentKey,
+ name: departmentName,
+ campus:
+ campusName === '本部院区' || campusName === '东院区' || campusName === '西院区'
+ ? campusName
+ : 'all',
+ categories: [],
+ }
+ const category =
+ department.categories.find((item) => item.name === record.category) ??
+ (() => {
+ const nextCategory = { name: record.category, documents: [] }
+ department.categories.push(nextCategory)
+ return nextCategory
+ })()
+
+ category.documents.push({
+ id: record.id,
+ name: record.name,
+ code: record.code ?? record.id,
+ version: record.version,
+ status: mapDocumentStatus(record.status),
+ backendStatus: record.backendStatus,
+ description: record.description ?? '',
+ versionId: record.versionId,
+ departmentId: record.departmentId,
+ campusId: record.campusId,
+ contentHtml: record.contentHtml,
+ contentSha256: record.contentSha256,
+ })
+ departments.set(departmentKey, department)
+ })
+
+ return [...departments.values()]
+}
+
+async function handlePreview(template: DocumentTemplate) {
selectedTemplate.value = template
+ previewError.value = false
+
+ if (isDocumentMockEnabled) {
+ previewLoading.value = false
+ return
+ }
+
+ const requestId = ++previewRequestId
+ previewLoading.value = true
+
+ try {
+ const detail = await getDocumentDetail(template.id)
+
+ if (!detail) {
+ throw new Error('模板不存在')
+ }
+
+ let contentHtml = detail.contentHtml
+ const versionId = detail.versionId ?? template.versionId
+
+ if (!contentHtml && versionId) {
+ const version = await getTemplateVersionDetail(template.id, versionId)
+ contentHtml = version?.contentHtml ?? version?.content ?? undefined
+ }
+
+ if (requestId !== previewRequestId) {
+ return
+ }
+
+ selectedTemplate.value = {
+ ...template,
+ code: detail.code ?? template.code,
+ version: detail.version || template.version,
+ versionId,
+ description: detail.description ?? template.description,
+ contentHtml,
+ contentSha256: detail.contentSha256,
+ }
+ } catch {
+ if (requestId === previewRequestId) {
+ previewError.value = true
+ ElMessage.error('模板内容加载失败,请稍后重试')
+ }
+ } finally {
+ if (requestId === previewRequestId) {
+ previewLoading.value = false
+ }
+ }
}
function closePreview() {
+ previewRequestId += 1
selectedTemplate.value = null
+ previewLoading.value = false
+ previewError.value = false
}
function showAddMessage() {
- ElMessage.info('原型演示:新增文档模板(类 Word 在线编辑器)')
+ editorTemplate.value = null
+ editorVisible.value = true
}
function showExportMessage() {
ElMessage.info('原型演示:导出模板包(ZIP)')
}
-function showEditMessage(template: DocumentTemplate) {
- ElMessage.info('原型演示:编辑模板《' + template.name + '》')
+async function showEditMessage(template: DocumentTemplate) {
+ editorTemplate.value = template
+ editorVisible.value = true
+
+ if (isDocumentMockEnabled || template.contentHtml) {
+ return
+ }
+
+ try {
+ const detail = await getDocumentDetail(template.id)
+ const versionId = detail?.versionId ?? template.versionId
+ let contentHtml = detail?.contentHtml
+
+ if (!contentHtml && versionId) {
+ const version = await getTemplateVersionDetail(template.id, versionId)
+ contentHtml = version?.contentHtml ?? version?.content ?? undefined
+ }
+
+ if (editorVisible.value && editorTemplate.value?.id === template.id) {
+ editorTemplate.value = {
+ ...template,
+ versionId,
+ description: detail?.description ?? template.description,
+ contentHtml,
+ }
+ }
+ } catch {
+ ElMessage.warning('模板内容加载失败,可先关闭后重试')
+ }
+}
+
+function closeEditor() {
+ editorVisible.value = false
+ editorTemplate.value = null
+}
+
+function addMockTemplate(form: TemplateEditorForm) {
+ const department = departments.value.find((item) => item.id === form.departmentId)
+ const departmentName = department?.name ?? '全院通用'
+ const departmentKey = department?.id ?? 'mock-general'
+ let departmentGroup = library.value.find((item) => item.key === departmentKey)
+
+ if (!departmentGroup) {
+ departmentGroup = {
+ key: departmentKey,
+ name: departmentName,
+ campus: 'all',
+ categories: [],
+ }
+ library.value.push(departmentGroup)
+ }
+
+ let category = departmentGroup.categories.find((item) => item.name === form.category)
+
+ if (!category) {
+ category = { name: form.category, documents: [] }
+ departmentGroup.categories.push(category)
+ }
+
+ const timestamp = Date.now()
+ category.documents.push({
+ id: `mock-template-${timestamp}`,
+ name: form.name,
+ code: form.templateCode,
+ version: form.versionNo || 'v1',
+ status: 'draft',
+ description: form.description,
+ versionId: `mock-version-${timestamp}`,
+ departmentId: form.departmentId || null,
+ campusId: form.campusId,
+ contentHtml: form.contentHtml,
+ })
+}
+
+async function saveTemplate(form: TemplateEditorForm) {
+ editorSaving.value = true
+
+ try {
+ if (isDocumentMockEnabled) {
+ if (form.id) {
+ library.value.forEach((department) =>
+ department.categories.forEach((category) =>
+ category.documents.forEach((document) => {
+ if (document.id === form.id) {
+ document.version = form.versionNo || document.version
+ document.contentHtml = form.contentHtml
+ document.description = form.description
+ }
+ }),
+ ),
+ )
+ } else {
+ addMockTemplate(form)
+ }
+ } else if (form.id) {
+ await createTemplateVersion(form.id, {
+ versionNo: form.versionNo || undefined,
+ contentHtml: form.contentHtml,
+ })
+ await loadLibrary()
+ } else {
+ const template = await createTemplate({
+ templateCode: form.templateCode,
+ name: form.name,
+ description: form.description || undefined,
+ campusId: form.campusId,
+ departmentId: form.departmentId || null,
+ category: form.category || undefined,
+ })
+ await createTemplateVersion(template.id, {
+ versionNo: form.versionNo || undefined,
+ contentHtml: form.contentHtml,
+ })
+ await loadLibrary()
+ }
+
+ closeEditor()
+ ElMessage.success(form.id ? '模板新版本已保存' : '模板已创建')
+ } catch {
+ ElMessage.error(form.id ? '模板版本保存失败,请稍后重试' : '模板创建失败,请稍后重试')
+ } finally {
+ editorSaving.value = false
+ }
+}
+
+function updateMockTemplateStatus(templateId: string, action: TemplateVersionAction) {
+ const nextStatus =
+ action === 'submit-review' || action === 'reject'
+ ? 'review'
+ : action === 'disable' || action === 'archive'
+ ? 'archived'
+ : 'enabled'
+
+ library.value.forEach((department) =>
+ department.categories.forEach((category) =>
+ category.documents.forEach((document) => {
+ if (document.id === templateId) {
+ document.status = nextStatus
+ }
+ }),
+ ),
+ )
+}
+
+async function handleWorkflow(action: TemplateVersionAction, comment?: string) {
+ const template = editorTemplate.value
+ const versionId = template?.versionId
+
+ if (!template || (!versionId && !isDocumentMockEnabled)) {
+ ElMessage.error('当前模板没有可操作的版本')
+ return
+ }
+
+ workflowSaving.value = true
+
+ try {
+ if (isDocumentMockEnabled) {
+ updateMockTemplateStatus(template.id, action)
+ } else if (versionId) {
+ await updateTemplateVersionStatus(template.id, versionId, action, comment)
+ await loadLibrary()
+ } else {
+ throw new Error('当前模板没有可操作的版本')
+ }
+
+ closeEditor()
+ ElMessage.success('模板版本状态已更新')
+ } catch {
+ ElMessage.error('模板版本操作失败,请稍后重试')
+ } finally {
+ workflowSaving.value = false
+ }
}
function toSigningTemplate(template: DocumentTemplate): SigningTemplate {
return {
id: template.id,
- versionId: template.id,
+ versionId: template.versionId ?? template.id,
name: template.name,
code: template.code,
version: template.version,
department: template.department,
+ departmentId: template.departmentId,
+ campusId: template.campusId ?? undefined,
category: template.category,
description: template.description,
supportedMethods: ['pad', 'sms'],
@@ -191,6 +559,8 @@ onMounted(loadLibrary)
@@ -199,6 +569,17 @@ onMounted(loadLibrary)
:initial-template="signingTemplate"
@close="closeSigningDialog"
/>
+
diff --git a/clinical-web/src/views/management/documents/types.ts b/clinical-web/src/views/management/documents/types.ts
index 687d82c..f15acde 100644
--- a/clinical-web/src/views/management/documents/types.ts
+++ b/clinical-web/src/views/management/documents/types.ts
@@ -10,7 +10,13 @@ export interface LibraryDocument {
code: string
version: string
status: DocumentStatus
+ backendStatus?: string
description: string
+ versionId?: string
+ departmentId?: string | null
+ campusId?: string | null
+ contentHtml?: string
+ contentSha256?: string
}
export interface DocumentCategory {
@@ -31,6 +37,18 @@ export interface DocumentTemplate extends LibraryDocument {
category: string
}
+export interface TemplateEditorForm {
+ id?: string
+ templateCode: string
+ name: string
+ description: string
+ campusId: string
+ departmentId: string
+ category: string
+ versionNo: string
+ contentHtml: string
+}
+
export interface DocumentFilterForm {
keyword: string
department: string | null
diff --git a/clinical-web/src/views/management/reports/index.vue b/clinical-web/src/views/management/reports/index.vue
index 724337a..fbc854a 100644
--- a/clinical-web/src/views/management/reports/index.vue
+++ b/clinical-web/src/views/management/reports/index.vue
@@ -1,6 +1,11 @@
+
+
+
+
+
+
+
+
diff --git a/clinical-web/src/views/management/users/index.vue b/clinical-web/src/views/management/users/index.vue
index c9be25e..935fba7 100644
--- a/clinical-web/src/views/management/users/index.vue
+++ b/clinical-web/src/views/management/users/index.vue
@@ -2,21 +2,64 @@
import { computed, onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus'
+import { getCampuses, getDepartments } from '@/api/management/organization'
+import { getRoles } from '@/api/management/roles'
+import { getUserDetail, getUsers, isUserMockEnabled } from '@/api/management/users'
+import { createUser, updateUser } from '@/api/management/users'
+import type {
+ CampusRecord,
+ DepartmentRecord,
+ RoleRecord,
+ UserRecord as ApiUserRecord,
+} from '@/api/management/types'
+import { useAppStore } from '@/stores/app'
+
import RoleSummaryCards from './components/RoleSummaryCards.vue'
import UserTable from './components/UserTable.vue'
+import UserEditorDialog from './components/UserEditorDialog.vue'
import { roleColors, roleSummaries, users } from './mock'
-import type { RoleSummary, UserTableRow } from './types'
+import type { RoleSummary, UserEditorForm, UserTableRow } from './types'
+const appStore = useAppStore()
const loading = ref(true)
const keyword = ref('')
const roleRows = ref([])
const userRows = ref([])
+const roleColorMap = ref>(roleColors)
+const campuses = ref([])
+const departments = ref([])
+const editorVisible = ref(false)
+const editorInitial = ref(null)
+const editorSaving = ref(false)
+
+const mockCampuses: CampusRecord[] = [
+ { id: 'mock-main', code: 'MAIN', name: '本部院区', status: 'ENABLED', sortNo: 1 },
+ { id: 'mock-east', code: 'EAST', name: '东院区', status: 'ENABLED', sortNo: 2 },
+ { id: 'mock-west', code: 'WEST', name: '西院区', status: 'ENABLED', sortNo: 3 },
+]
+
+const mockDepartments: DepartmentRecord[] = [
+ '医务处',
+ '放射科',
+ '心内科',
+ '骨科',
+ '健康管理中心',
+ '信息科',
+ '纪检监察室',
+].map((name, index) => ({
+ id: `mock-department-${index + 1}`,
+ campusId: index === 3 || index === 4 ? 'mock-east' : 'mock-main',
+ code: `MOCK-${index + 1}`,
+ name,
+ status: 'ENABLED',
+ sortNo: index + 1,
+}))
const visibleUsers = computed(() => {
const normalizedKeyword = keyword.value.trim().toLowerCase()
return userRows.value.filter((user) => {
- const matchesCampus = user.campus === '本部院区'
+ const matchesCampus = user.campus === appStore.selectedCampus
const matchesKeyword =
!normalizedKeyword ||
[user.name, user.employeeNo].join(' ').toLowerCase().includes(normalizedKeyword)
@@ -27,26 +70,233 @@ const visibleUsers = computed(() => {
async function loadUsers() {
loading.value = true
- await Promise.resolve()
- roleRows.value = roleSummaries.map((role) => ({ ...role }))
- userRows.value = users.map((user) => ({ ...user }))
- loading.value = false
+ try {
+ if (isUserMockEnabled) {
+ campuses.value = mockCampuses.map((campus) => ({ ...campus }))
+ departments.value = mockDepartments.map((department) => ({ ...department }))
+ roleRows.value = roleSummaries.map((role) => ({ ...role }))
+ userRows.value = users.map((user) => ({ ...user }))
+ roleColorMap.value = roleColors
+ return
+ }
+
+ const [usersResult, rolesResult, campusesResult, departmentsResult] = await Promise.allSettled([
+ getUsers({ page: 1, pageSize: 200, status: 'all' }),
+ getRoles(),
+ getCampuses(),
+ getDepartments(),
+ ])
+
+ if (usersResult.status === 'rejected') {
+ throw usersResult.reason
+ }
+
+ const campusNames = new Map(
+ campusesResult.status === 'fulfilled'
+ ? campusesResult.value.map((campus) => [campus.id, campus.name])
+ : [],
+ )
+ const departmentNames = new Map(
+ departmentsResult.status === 'fulfilled'
+ ? departmentsResult.value.map((department) => [department.id, department.name])
+ : [],
+ )
+ campuses.value = campusesResult.status === 'fulfilled' ? campusesResult.value : []
+ departments.value = departmentsResult.status === 'fulfilled' ? departmentsResult.value : []
+ const apiUsers = usersResult.value.records
+
+ userRows.value = apiUsers.map((user) => mapUserRow(user, campusNames, departmentNames))
+
+ const roles = rolesResult.status === 'fulfilled' ? rolesResult.value : []
+ roleRows.value = roles.map((role) => mapRoleSummary(role, apiUsers))
+ roleColorMap.value = Object.fromEntries(
+ roles.map((role) => [role.name, role.color ?? '#8a9aa3']),
+ )
+ } catch {
+ roleRows.value = []
+ userRows.value = []
+ ElMessage.error('用户与角色加载失败,请稍后重试')
+ } finally {
+ loading.value = false
+ }
+}
+
+function mapUserRow(
+ user: ApiUserRecord,
+ campusNames: Map,
+ departmentNames: Map,
+): UserTableRow {
+ const dataScopeLabels: Record = {
+ ALL: '全院',
+ CAMPUS: '院区',
+ DEPARTMENT: '本科室',
+ READ_ONLY_ALL: '全院只读',
+ }
+
+ return {
+ id: user.id,
+ name: user.name,
+ account: user.account ?? '',
+ employeeNo: user.employeeNo ?? '—',
+ phone: user.phone ?? '',
+ email: user.email ?? '',
+ campus: campusNames.get(user.campusId ?? '') ?? '未指定院区',
+ campusId: user.campusId,
+ department: departmentNames.get(user.departmentId ?? '') ?? user.department ?? '未指定科室',
+ departmentId: user.departmentId,
+ role: user.role,
+ dataScope: dataScopeLabels[user.dataScope ?? ''] ?? user.dataScope ?? '—',
+ status: user.status,
+ }
+}
+
+function mapRoleSummary(role: RoleRecord, users: ApiUserRecord[]): RoleSummary {
+ const userCount =
+ role.userCount ??
+ users.filter((user) => user.roleNames?.some((roleName) => roleName === role.name)).length
+
+ return {
+ id: role.id,
+ name: role.name,
+ color: role.color ?? '#8a9aa3',
+ description: role.description,
+ userCount,
+ }
}
function showAddUserMessage() {
- ElMessage.info('原型演示:新增用户')
+ editorInitial.value = null
+ editorVisible.value = true
}
function showImportMessage() {
ElMessage.info('原型演示:批量导入')
}
-function editUser(user: UserTableRow) {
- ElMessage.info(`原型演示:编辑用户《${user.name}》`)
+async function editUser(row: UserTableRow) {
+ try {
+ const user = isUserMockEnabled
+ ? userRows.value.find((item) => item.id === row.id)
+ : await getUserDetail(row.id)
+
+ if (!user) {
+ ElMessage.error('用户不存在或已被删除')
+ return
+ }
+
+ editorInitial.value = {
+ id: user.id,
+ username: user.account ?? '',
+ password: '',
+ displayName: user.name,
+ employeeNo: user.employeeNo ?? '',
+ phone: user.phone ?? '',
+ email: user.email ?? '',
+ campusId:
+ user.campusId ?? campuses.value.find((campus) => campus.name === row.campus)?.id ?? '',
+ departmentId:
+ user.departmentId ??
+ departments.value.find((department) => department.name === row.department)?.id ??
+ '',
+ status: user.status,
+ dataScope: toBackendDataScope(user.dataScope),
+ }
+ editorVisible.value = true
+ } catch {
+ ElMessage.error('用户详情加载失败,请稍后重试')
+ }
}
function resetPassword(user: UserTableRow) {
- ElMessage.success(`已重置${user.name}的密码`)
+ ElMessage.info(
+ isUserMockEnabled
+ ? `原型演示:重置${user.name}的密码`
+ : '当前接口未提供重置密码能力,请由认证系统或管理员流程处理',
+ )
+}
+
+function toBackendDataScope(value: string | undefined) {
+ const map: Record = {
+ 全院: 'ALL',
+ 院区: 'CAMPUS',
+ 本科室: 'DEPARTMENT',
+ 全院只读: 'READ_ONLY_ALL',
+ }
+
+ return map[value ?? ''] ?? value ?? 'DEPARTMENT'
+}
+
+function getCampusName(id: string) {
+ return campuses.value.find((campus) => campus.id === id)?.name ?? '未指定院区'
+}
+
+function getDepartmentName(id: string) {
+ return departments.value.find((department) => department.id === id)?.name ?? '未指定科室'
+}
+
+async function saveUser(form: UserEditorForm) {
+ editorSaving.value = true
+
+ try {
+ if (isUserMockEnabled) {
+ const row: UserTableRow = {
+ id: form.id ?? `mock-user-${Date.now()}`,
+ name: form.displayName,
+ account: form.username,
+ employeeNo: form.employeeNo || '—',
+ phone: form.phone,
+ email: form.email,
+ campus: getCampusName(form.campusId),
+ campusId: form.campusId,
+ department: getDepartmentName(form.departmentId),
+ departmentId: form.departmentId || null,
+ role: form.id
+ ? (userRows.value.find((item) => item.id === form.id)?.role ?? '未分配角色')
+ : '未分配角色',
+ dataScope: form.dataScope,
+ status: form.status,
+ }
+ const index = userRows.value.findIndex((item) => item.id === row.id)
+ if (index === -1) {
+ userRows.value.unshift(row)
+ } else {
+ userRows.value[index] = row
+ }
+ } else if (form.id) {
+ await updateUser(form.id, {
+ displayName: form.displayName,
+ employeeNo: form.employeeNo || undefined,
+ phone: form.phone || undefined,
+ email: form.email || undefined,
+ campusId: form.campusId,
+ departmentId: form.departmentId || null,
+ status: form.status.toUpperCase(),
+ dataScope: form.dataScope,
+ })
+ await loadUsers()
+ } else {
+ await createUser({
+ username: form.username,
+ password: form.password,
+ displayName: form.displayName,
+ employeeNo: form.employeeNo || undefined,
+ phone: form.phone || undefined,
+ email: form.email || undefined,
+ campusId: form.campusId,
+ departmentId: form.departmentId || null,
+ status: form.status.toUpperCase(),
+ dataScope: form.dataScope,
+ })
+ await loadUsers()
+ }
+
+ editorVisible.value = false
+ ElMessage.success(form.id ? '用户已更新' : '用户已创建')
+ } catch {
+ ElMessage.error(form.id ? '用户更新失败,请稍后重试' : '用户创建失败,请稍后重试')
+ } finally {
+ editorSaving.value = false
+ }
}
onMounted(loadUsers)
@@ -77,12 +327,22 @@ onMounted(loadUsers)