fix: 解决bug
This commit is contained in:
@@ -56,6 +56,7 @@ export interface CreateCaseDraftPayload {
|
||||
title: string
|
||||
case_type: DraftCaseType
|
||||
institution_id?: number
|
||||
department_id?: number
|
||||
department_name?: string
|
||||
traditional?: Record<string, unknown>
|
||||
teaching?: Record<string, unknown>
|
||||
@@ -77,6 +78,30 @@ export interface CreateCaseDraftParams {
|
||||
payload: CreateCaseDraftPayload
|
||||
}
|
||||
|
||||
export interface SaveCaseDraftPayload {
|
||||
title?: string
|
||||
institution_id?: number
|
||||
department_id?: number
|
||||
department_name?: string
|
||||
traditional?: Record<string, unknown>
|
||||
teaching?: Record<string, unknown>
|
||||
exam_items?: CaseExamItemPayload[]
|
||||
scoring_rules?: CaseScoringRulePayload[]
|
||||
difficulty?: string
|
||||
chief_complaint?: string
|
||||
description?: string
|
||||
patient_age?: number
|
||||
patient_gender?: string
|
||||
tags?: string | string[]
|
||||
icd_codes?: string[]
|
||||
estimated_minutes?: number
|
||||
osce_enabled?: boolean
|
||||
}
|
||||
|
||||
export interface SaveCaseDraftParams extends CaseActionParams {
|
||||
payload: SaveCaseDraftPayload
|
||||
}
|
||||
|
||||
export interface AiGenerateCasePayload {
|
||||
prompt: string
|
||||
case_type: DraftCaseType
|
||||
@@ -503,6 +528,20 @@ export async function createCaseDraft(params: CreateCaseDraftParams): Promise<un
|
||||
return parseMutationResponse(response, '新增病例草稿失败')
|
||||
}
|
||||
|
||||
export async function saveCaseDraft(params: SaveCaseDraftParams): Promise<unknown> {
|
||||
const response = await fetch(`/server/api/cms/cases/${params.id}/save-draft/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: createAuthorization(params.token),
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(params.payload)
|
||||
})
|
||||
|
||||
return parseMutationResponse(response, '保存病例草稿失败')
|
||||
}
|
||||
|
||||
export async function generateCaseWithAi(params: AiGenerateCaseParams): Promise<AiGenerateCaseResult> {
|
||||
const response = await fetch('/server/api/cms/cases/ai-generate/', {
|
||||
method: 'POST',
|
||||
|
||||
@@ -33,6 +33,17 @@ export interface UserListResult {
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface InstitutionOption {
|
||||
id: number
|
||||
name: string
|
||||
code: string
|
||||
}
|
||||
|
||||
export interface DepartmentOption {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface CreateUserPayload {
|
||||
phone: string
|
||||
real_name: string
|
||||
@@ -87,6 +98,8 @@ export interface ImportUsersParams {
|
||||
export interface ExportUsersParams extends UserListParams {}
|
||||
|
||||
const listKeys = ['results', 'list', 'rows', 'items', 'records', 'users']
|
||||
const institutionListKeys = ['results', 'list', 'rows', 'items', 'records', 'institutions', 'institution_list', 'institutionList']
|
||||
const departmentListKeys = ['results', 'list', 'rows', 'items', 'records', 'departments', 'department_list', 'departmentList']
|
||||
const totalKeys = ['count', 'total', 'total_count', 'totalCount']
|
||||
|
||||
function parseResponseText(text: string): unknown {
|
||||
@@ -209,6 +222,54 @@ function findUserPayload(data: unknown): { items: unknown[]; total: number } {
|
||||
return { items: [], total: getTotal(record, 0) }
|
||||
}
|
||||
|
||||
function findInstitutionItems(data: unknown): unknown[] {
|
||||
if (Array.isArray(data)) {
|
||||
return data
|
||||
}
|
||||
|
||||
if (!data || typeof data !== 'object') {
|
||||
return []
|
||||
}
|
||||
|
||||
const record = data as Record<string, unknown>
|
||||
for (const key of institutionListKeys) {
|
||||
const value = record[key]
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
if (record.data && typeof record.data === 'object') {
|
||||
return findInstitutionItems(record.data)
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
function findDepartmentItems(data: unknown): unknown[] {
|
||||
if (Array.isArray(data)) {
|
||||
return data
|
||||
}
|
||||
|
||||
if (!data || typeof data !== 'object') {
|
||||
return []
|
||||
}
|
||||
|
||||
const record = data as Record<string, unknown>
|
||||
for (const key of departmentListKeys) {
|
||||
const value = record[key]
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
if (record.data && typeof record.data === 'object') {
|
||||
return findDepartmentItems(record.data)
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
function normalizeUser(item: unknown, index: number): UserListItem {
|
||||
const record = item && typeof item === 'object' ? (item as Record<string, unknown>) : {}
|
||||
const name = getString(record, ['name', 'real_name', 'realName', 'nickname', 'username'], `用户${index + 1}`)
|
||||
@@ -235,6 +296,51 @@ function normalizeUser(item: unknown, index: number): UserListItem {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeInstitutionOption(item: unknown): InstitutionOption | null {
|
||||
if (typeof item === 'number' || (typeof item === 'string' && Number.isFinite(Number(item)))) {
|
||||
const id = Number(item)
|
||||
return { id, name: `机构 ${id}`, code: '' }
|
||||
}
|
||||
|
||||
if (!item || typeof item !== 'object') {
|
||||
return null
|
||||
}
|
||||
|
||||
const record = item as Record<string, unknown>
|
||||
const id = Number(getString(record, ['id', 'institution_id', 'institutionId', 'value', 'pk'], ''))
|
||||
if (!Number.isFinite(id) || id <= 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
name: getString(record, ['name', 'institution_name', 'institutionName', 'hospital_name', 'hospitalName', 'label', 'title'], `机构 ${id}`),
|
||||
code: getString(record, ['code', 'institution_code', 'institutionCode'], '')
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeDepartmentOption(item: unknown): DepartmentOption | null {
|
||||
if (typeof item === 'number' || (typeof item === 'string' && Number.isFinite(Number(item)))) {
|
||||
const id = Number(item)
|
||||
return { id, name: `科室 ${id}` }
|
||||
}
|
||||
|
||||
if (!item || typeof item !== 'object') {
|
||||
return null
|
||||
}
|
||||
|
||||
const record = item as Record<string, unknown>
|
||||
const id = Number(getString(record, ['id', 'department_id', 'departmentId', 'value', 'pk'], ''))
|
||||
if (!Number.isFinite(id) || id <= 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
name: getString(record, ['name', 'department_name', 'departmentName', 'label', 'title'], `科室 ${id}`)
|
||||
}
|
||||
}
|
||||
|
||||
function createAuthorization(token: string) {
|
||||
return /^Bearer\s+/i.test(token) ? token : `Bearer ${token}`
|
||||
}
|
||||
@@ -338,6 +444,66 @@ export async function fetchUsers(params: UserListParams): Promise<UserListResult
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchInstitutionList(token: string): Promise<InstitutionOption[]> {
|
||||
const response = await fetch('/server/api/user/institution_list/', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: createAuthorization(token)
|
||||
}
|
||||
})
|
||||
const text = await response.text()
|
||||
const data = parseResponseText(text)
|
||||
|
||||
if (!response.ok) {
|
||||
const message = getMessageFromResponse(data) || '获取机构列表失败'
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
const seen = new Set<number>()
|
||||
return findInstitutionItems(data)
|
||||
.map(normalizeInstitutionOption)
|
||||
.filter((item): item is InstitutionOption => {
|
||||
if (!item || seen.has(item.id)) {
|
||||
return false
|
||||
}
|
||||
seen.add(item.id)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
export async function fetchMyDepartments(token: string, institutionId?: number): Promise<DepartmentOption[]> {
|
||||
const query = new URLSearchParams()
|
||||
if (institutionId) {
|
||||
query.set('institution_id', String(institutionId))
|
||||
}
|
||||
const response = await fetch(`/server/api/user/my_departments/${query.toString() ? `?${query.toString()}` : ''}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: createAuthorization(token)
|
||||
}
|
||||
})
|
||||
const text = await response.text()
|
||||
const data = parseResponseText(text)
|
||||
|
||||
if (!response.ok) {
|
||||
const message = getMessageFromResponse(data) || '获取科室列表失败'
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
const seen = new Set<number>()
|
||||
return findDepartmentItems(data)
|
||||
.map(normalizeDepartmentOption)
|
||||
.filter((item): item is DepartmentOption => {
|
||||
if (!item || seen.has(item.id)) {
|
||||
return false
|
||||
}
|
||||
seen.add(item.id)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
export async function createUser(params: CreateUserParams): Promise<unknown> {
|
||||
const response = await fetch('/server/api/cms/users/', {
|
||||
method: 'POST',
|
||||
|
||||
@@ -205,20 +205,6 @@
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="case-form-section">
|
||||
<div class="case-section-title">
|
||||
<h3>评分规则</h3>
|
||||
<el-button :icon="Plus" @click="addScoringRule">添加规则</el-button>
|
||||
</div>
|
||||
<div v-for="(rule, index) in draftForm.scoring_rules" :key="index" class="scoring-rule-row">
|
||||
<el-input v-model="rule.dimension" placeholder="维度" />
|
||||
<el-input-number v-model="rule.score_weight" :min="0.01" :max="1" :step="0.05" :precision="2" :controls="false" placeholder="权重" />
|
||||
<el-switch v-model="rule.ai_auto_score" active-text="AI评分" inactive-text="人工" />
|
||||
<el-input v-model="rule.scoring_standard" placeholder="评分标准" />
|
||||
<el-button :icon="Delete" :disabled="draftForm.scoring_rules.length === 1" circle @click="removeScoringRule(index)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="case-form-section">
|
||||
<div class="case-section-title">
|
||||
<h3>检查/检验项目</h3>
|
||||
@@ -469,18 +455,6 @@ function resetDraftForm() {
|
||||
draftForm.exam_items = []
|
||||
}
|
||||
|
||||
function addScoringRule() {
|
||||
draftForm.scoring_rules.push({ dimension: '', score_weight: 1, ai_auto_score: true, scoring_standard: '' })
|
||||
}
|
||||
|
||||
function removeScoringRule(index: number) {
|
||||
if (draftForm.scoring_rules.length <= 1) {
|
||||
return
|
||||
}
|
||||
|
||||
draftForm.scoring_rules.splice(index, 1)
|
||||
}
|
||||
|
||||
function addExamItem() {
|
||||
draftForm.exam_items.push({ item_code: '', item_name: '', item_type: '', result_text: '' })
|
||||
}
|
||||
@@ -544,25 +518,19 @@ function buildCaseStructure(): Record<string, unknown> {
|
||||
|
||||
function normalizeScoringRules(): CaseScoringRulePayload[] {
|
||||
const rules = draftForm.scoring_rules
|
||||
.map(rule => ({
|
||||
dimension: rule.dimension.trim(),
|
||||
score_weight: Number(rule.score_weight),
|
||||
ai_auto_score: rule.ai_auto_score,
|
||||
scoring_standard: rule.scoring_standard.trim()
|
||||
}))
|
||||
.filter(rule => rule.dimension || rule.scoring_standard || Number.isFinite(rule.score_weight))
|
||||
.map((rule, index) => {
|
||||
const scoreWeight = Number(rule.score_weight)
|
||||
return {
|
||||
dimension: rule.dimension.trim() || (rule.scoring_standard.trim() ? `评分维度${index + 1}` : ''),
|
||||
score_weight: Number.isFinite(scoreWeight) && scoreWeight > 0 && scoreWeight <= 1 ? scoreWeight : 1,
|
||||
ai_auto_score: true,
|
||||
scoring_standard: rule.scoring_standard.trim()
|
||||
}
|
||||
})
|
||||
.filter(rule => rule.dimension || rule.scoring_standard)
|
||||
|
||||
if (!rules.length) {
|
||||
throw new Error('至少添加 1 条评分规则')
|
||||
}
|
||||
|
||||
for (const rule of rules) {
|
||||
if (!rule.dimension) {
|
||||
throw new Error('评分规则的维度必填')
|
||||
}
|
||||
if (!Number.isFinite(rule.score_weight) || rule.score_weight <= 0 || rule.score_weight > 1) {
|
||||
throw new Error('评分规则权重必须大于 0 且不超过 1')
|
||||
}
|
||||
return [createDefaultScoringRule()]
|
||||
}
|
||||
|
||||
return rules.map(rule => ({
|
||||
@@ -573,6 +541,15 @@ function normalizeScoringRules(): CaseScoringRulePayload[] {
|
||||
}))
|
||||
}
|
||||
|
||||
function createDefaultScoringRule(): CaseScoringRulePayload {
|
||||
return {
|
||||
dimension: draftForm.case_type === 'teaching' ? '教学目标达成' : '诊断与处置',
|
||||
score_weight: 1,
|
||||
ai_auto_score: true,
|
||||
scoring_standard: '由AI根据病例内容生成评分标准'
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeExamItems(): CaseExamItemPayload[] {
|
||||
const items = draftForm.exam_items
|
||||
.map(item => ({
|
||||
|
||||
+289
-78
@@ -136,13 +136,44 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-if="canManageInstitution" :span="8">
|
||||
<el-form-item label="机构ID">
|
||||
<el-input-number v-model="caseForm.institution_id" :min="1" :precision="0" :controls="false" placeholder="缺省落创建者机构" />
|
||||
<el-form-item label="机构">
|
||||
<el-select
|
||||
v-model="caseForm.institution_id"
|
||||
:loading="loadingInstitutions"
|
||||
clearable
|
||||
filterable
|
||||
placeholder="请选择机构"
|
||||
@change="handleCaseInstitutionChange"
|
||||
@visible-change="handleInstitutionVisibleChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in institutionOptions"
|
||||
:key="item.id"
|
||||
:label="institutionOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="科室名称">
|
||||
<el-input v-model="caseForm.department_name" placeholder="请输入科室名称" />
|
||||
<el-form-item label="科室">
|
||||
<el-select
|
||||
v-model="caseForm.department_id"
|
||||
:disabled="canManageInstitution && !caseForm.institution_id"
|
||||
:loading="loadingDepartments"
|
||||
clearable
|
||||
filterable
|
||||
placeholder="请选择科室"
|
||||
@change="handleCaseDepartmentChange"
|
||||
@visible-change="handleDepartmentVisibleChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in departmentOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
@@ -243,20 +274,6 @@
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="case-form-section">
|
||||
<div class="case-section-title">
|
||||
<h3>评分规则</h3>
|
||||
<el-button :icon="Plus" @click="addScoringRule">添加规则</el-button>
|
||||
</div>
|
||||
<div v-for="(rule, index) in caseForm.scoring_rules" :key="index" class="scoring-rule-row">
|
||||
<el-input v-model="rule.dimension" placeholder="维度" />
|
||||
<el-input-number v-model="rule.score_weight" :min="0.01" :max="1" :step="0.05" :precision="2" :controls="false" placeholder="权重" />
|
||||
<el-switch v-model="rule.ai_auto_score" active-text="AI评分" inactive-text="人工" />
|
||||
<el-input v-model="rule.scoring_standard" placeholder="评分标准" />
|
||||
<el-button :icon="Delete" :disabled="caseForm.scoring_rules.length === 1" circle @click="removeScoringRule(index)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="case-form-section">
|
||||
<div class="case-section-title">
|
||||
<h3>检查/检验项目</h3>
|
||||
@@ -372,6 +389,7 @@
|
||||
class="case-create-form case-detail-form"
|
||||
:model="detailForm"
|
||||
:rules="caseRules"
|
||||
:disabled="!canEditDetailCase"
|
||||
label-position="top"
|
||||
>
|
||||
<div class="case-form-section">
|
||||
@@ -387,20 +405,51 @@
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="病例类型" prop="case_type">
|
||||
<el-select v-model="detailForm.case_type" placeholder="请选择病例类型">
|
||||
<el-select v-model="detailForm.case_type" disabled placeholder="请选择病例类型">
|
||||
<el-option label="传统病例" value="traditional" />
|
||||
<el-option label="教学病例" value="teaching" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-if="canManageInstitution" :span="8">
|
||||
<el-form-item label="机构ID">
|
||||
<el-input-number v-model="detailForm.institution_id" :min="1" :precision="0" :controls="false" placeholder="缺省落创建者机构" />
|
||||
<el-form-item label="机构">
|
||||
<el-select
|
||||
v-model="detailForm.institution_id"
|
||||
:loading="loadingInstitutions"
|
||||
clearable
|
||||
filterable
|
||||
placeholder="请选择机构"
|
||||
@change="handleDetailInstitutionChange"
|
||||
@visible-change="handleInstitutionVisibleChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in institutionOptions"
|
||||
:key="item.id"
|
||||
:label="institutionOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="科室名称">
|
||||
<el-input v-model="detailForm.department_name" placeholder="请输入科室名称" />
|
||||
<el-form-item label="科室">
|
||||
<el-select
|
||||
v-model="detailForm.department_id"
|
||||
:disabled="canManageInstitution && !detailForm.institution_id"
|
||||
:loading="loadingDepartments"
|
||||
clearable
|
||||
filterable
|
||||
placeholder="请选择科室"
|
||||
@change="handleDetailDepartmentChange"
|
||||
@visible-change="handleDepartmentVisibleChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in departmentOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
@@ -501,39 +550,40 @@
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="case-form-section">
|
||||
<div v-if="showPublishedScoringRules" class="case-form-section">
|
||||
<div class="case-section-title">
|
||||
<h3>评分规则</h3>
|
||||
<el-button :icon="Plus" @click="addDetailScoringRule">添加规则</el-button>
|
||||
</div>
|
||||
<div v-for="(rule, index) in detailForm.scoring_rules" :key="`detail-rule-${index}`" class="scoring-rule-row">
|
||||
<el-input v-model="rule.dimension" placeholder="维度" />
|
||||
<el-input-number v-model="rule.score_weight" :min="0.01" :max="1" :step="0.05" :precision="2" :controls="false" placeholder="权重" />
|
||||
<el-switch v-model="rule.ai_auto_score" active-text="AI评分" inactive-text="人工" />
|
||||
<el-input v-model="rule.scoring_standard" placeholder="评分标准" />
|
||||
<el-button :icon="Delete" :disabled="detailForm.scoring_rules.length === 1" circle @click="removeDetailScoringRule(index)" />
|
||||
</div>
|
||||
<el-table :data="visibleScoringRules" border size="small" empty-text="暂无评分规则">
|
||||
<el-table-column prop="dimension" label="维度" min-width="160" />
|
||||
<el-table-column label="权重" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ formatScoreWeight(row.score_weight) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="scoring_standard" label="评分标准" min-width="260" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div class="case-form-section">
|
||||
<div class="case-section-title">
|
||||
<h3>检查/检验项目</h3>
|
||||
<el-button :icon="Plus" @click="addDetailExamItem">添加项目</el-button>
|
||||
<el-button :icon="Plus" :disabled="!canEditDetailCase" @click="addDetailExamItem">添加项目</el-button>
|
||||
</div>
|
||||
<div v-if="!detailForm.exam_items.length" class="case-empty-line">未添加项目,可直接提交。</div>
|
||||
<div v-if="!detailForm.exam_items.length" class="case-empty-line">未添加项目,可直接保存草稿。</div>
|
||||
<div v-for="(item, index) in detailForm.exam_items" :key="`detail-item-${index}`" class="exam-item-row">
|
||||
<el-input v-model="item.item_code" placeholder="项目编码,必须唯一" />
|
||||
<el-input v-model="item.item_name" placeholder="项目名称" />
|
||||
<el-input v-model="item.item_type" placeholder="项目类型" />
|
||||
<el-input v-model="item.result_text" placeholder="结果文本" />
|
||||
<el-button :icon="Delete" circle @click="removeDetailExamItem(index)" />
|
||||
<el-button :icon="Delete" :disabled="!canEditDetailCase" circle @click="removeDetailExamItem(index)" />
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<div class="drawer-form-footer">
|
||||
<el-button @click="detailDrawerVisible = false">取消</el-button>
|
||||
<el-button :loading="submittingDetail" type="primary" @click="submitDetailForm">提交</el-button>
|
||||
<el-button @click="detailDrawerVisible = false">{{ canEditDetailCase ? '取消' : '关闭' }}</el-button>
|
||||
<el-button v-if="canEditDetailCase" :loading="submittingDetail" type="primary" @click="submitDetailForm">保存草稿</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
@@ -551,6 +601,7 @@ import {
|
||||
fetchCaseFull,
|
||||
fetchCases,
|
||||
importCasePdf,
|
||||
saveCaseDraft,
|
||||
submitCase,
|
||||
updateCaseRelations,
|
||||
type CaseExamItemPayload,
|
||||
@@ -560,8 +611,10 @@ import {
|
||||
type CaseScoringRulePayload,
|
||||
type CreateCaseDraftPayload,
|
||||
type DraftCaseType,
|
||||
type SaveCaseDraftPayload,
|
||||
type UpdateCaseRelationsPayload
|
||||
} from '@/api/cases'
|
||||
import { fetchInstitutionList, fetchMyDepartments, type DepartmentOption, type InstitutionOption } from '@/api/users'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
|
||||
type RelationInstitutionMode = 'keep' | 'set' | 'clear'
|
||||
@@ -585,6 +638,7 @@ interface CaseDraftForm {
|
||||
title: string
|
||||
case_type: DraftCaseType
|
||||
institution_id?: number
|
||||
department_id?: number
|
||||
department_name: string
|
||||
difficulty: string
|
||||
chief_complaint: string
|
||||
@@ -612,6 +666,8 @@ const importing = ref(false)
|
||||
const savingRelations = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const submittingDetail = ref(false)
|
||||
const loadingInstitutions = ref(false)
|
||||
const loadingDepartments = ref(false)
|
||||
const caseDialogVisible = ref(false)
|
||||
const importDialogVisible = ref(false)
|
||||
const relationsDialogVisible = ref(false)
|
||||
@@ -623,6 +679,8 @@ const importFile = ref<File | null>(null)
|
||||
const importCaseType = ref<DraftCaseType>('traditional')
|
||||
const importFileList = ref<UploadUserFile[]>([])
|
||||
const cases = ref<CaseListItem[]>([])
|
||||
const institutionOptions = ref<InstitutionOption[]>([])
|
||||
const departmentOptions = ref<DepartmentOption[]>([])
|
||||
const relationsCase = ref<CaseListItem | null>(null)
|
||||
const detailCase = ref<CaseListItem | null>(null)
|
||||
const caseDetail = ref<unknown>(null)
|
||||
@@ -656,6 +714,7 @@ const caseForm = reactive<CaseDraftForm>({
|
||||
title: '',
|
||||
case_type: 'traditional',
|
||||
institution_id: undefined,
|
||||
department_id: undefined,
|
||||
department_name: '',
|
||||
difficulty: '',
|
||||
chief_complaint: '',
|
||||
@@ -680,6 +739,7 @@ const detailForm = reactive<CaseDraftForm>({
|
||||
title: '',
|
||||
case_type: 'traditional',
|
||||
institution_id: undefined,
|
||||
department_id: undefined,
|
||||
department_name: '',
|
||||
difficulty: '',
|
||||
chief_complaint: '',
|
||||
@@ -731,6 +791,13 @@ const pageDescription = computed(() =>
|
||||
const searchPlaceholder = computed(() => (isContentAdmin.value ? '搜索病例' : '搜索标题/主诉/标签/ICD'))
|
||||
const detailDrawerTitle = computed(() => (detailCase.value ? `病例详情:${detailCase.value.title}` : '病例详情'))
|
||||
const detailDisplay = computed(() => createDetailDisplay(detailCase.value, caseDetail.value))
|
||||
const canEditDetailCase = computed(() => detailCase.value?.publishStatus === 0)
|
||||
const showPublishedScoringRules = computed(() => detailCase.value?.publishStatus === 2)
|
||||
const visibleScoringRules = computed(() =>
|
||||
showPublishedScoringRules.value
|
||||
? detailForm.scoring_rules.filter(rule => rule.dimension.trim() || rule.scoring_standard.trim())
|
||||
: []
|
||||
)
|
||||
const relationsCurrentTitle = computed(() => {
|
||||
if (!relationsCase.value) {
|
||||
return ''
|
||||
@@ -802,6 +869,8 @@ function handleSizeChange() {
|
||||
|
||||
function openCreateDialog() {
|
||||
caseDialogVisible.value = true
|
||||
loadInstitutionOptions()
|
||||
loadDepartmentOptions(caseForm.institution_id)
|
||||
}
|
||||
|
||||
function resetCaseForm() {
|
||||
@@ -820,6 +889,7 @@ function resetDraftForm(form: CaseDraftForm) {
|
||||
form.title = ''
|
||||
form.case_type = 'traditional'
|
||||
form.institution_id = undefined
|
||||
form.department_id = undefined
|
||||
form.department_name = ''
|
||||
form.difficulty = ''
|
||||
form.chief_complaint = ''
|
||||
@@ -840,18 +910,6 @@ function resetDraftForm(form: CaseDraftForm) {
|
||||
form.exam_items = []
|
||||
}
|
||||
|
||||
function addScoringRule() {
|
||||
caseForm.scoring_rules.push({ dimension: '', score_weight: 1, ai_auto_score: true, scoring_standard: '' })
|
||||
}
|
||||
|
||||
function removeScoringRule(index: number) {
|
||||
if (caseForm.scoring_rules.length <= 1) {
|
||||
return
|
||||
}
|
||||
|
||||
caseForm.scoring_rules.splice(index, 1)
|
||||
}
|
||||
|
||||
function addExamItem() {
|
||||
caseForm.exam_items.push({ item_code: '', item_name: '', item_type: '', result_text: '' })
|
||||
}
|
||||
@@ -860,26 +918,104 @@ function removeExamItem(index: number) {
|
||||
caseForm.exam_items.splice(index, 1)
|
||||
}
|
||||
|
||||
function addDetailScoringRule() {
|
||||
detailForm.scoring_rules.push({ dimension: '', score_weight: 1, ai_auto_score: true, scoring_standard: '' })
|
||||
}
|
||||
|
||||
function removeDetailScoringRule(index: number) {
|
||||
if (detailForm.scoring_rules.length <= 1) {
|
||||
function addDetailExamItem() {
|
||||
if (!canEditDetailCase.value) {
|
||||
return
|
||||
}
|
||||
|
||||
detailForm.scoring_rules.splice(index, 1)
|
||||
}
|
||||
|
||||
function addDetailExamItem() {
|
||||
detailForm.exam_items.push({ item_code: '', item_name: '', item_type: '', result_text: '' })
|
||||
}
|
||||
|
||||
function removeDetailExamItem(index: number) {
|
||||
if (!canEditDetailCase.value) {
|
||||
return
|
||||
}
|
||||
|
||||
detailForm.exam_items.splice(index, 1)
|
||||
}
|
||||
|
||||
async function loadInstitutionOptions() {
|
||||
if (!appStore.token || !canManageInstitution.value || loadingInstitutions.value || institutionOptions.value.length) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
loadingInstitutions.value = true
|
||||
institutionOptions.value = await fetchInstitutionList(appStore.token)
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '获取机构列表失败')
|
||||
} finally {
|
||||
loadingInstitutions.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDepartmentOptions(institutionId?: number) {
|
||||
if (!appStore.token || loadingDepartments.value) {
|
||||
return
|
||||
}
|
||||
if (canManageInstitution.value && !institutionId) {
|
||||
departmentOptions.value = []
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
loadingDepartments.value = true
|
||||
departmentOptions.value = await fetchMyDepartments(appStore.token, institutionId)
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '获取科室列表失败')
|
||||
} finally {
|
||||
loadingDepartments.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleInstitutionVisibleChange(visible: boolean) {
|
||||
if (visible) {
|
||||
loadInstitutionOptions()
|
||||
}
|
||||
}
|
||||
|
||||
function handleDepartmentVisibleChange(visible: boolean) {
|
||||
if (visible) {
|
||||
loadDepartmentOptions(activeCaseForm().institution_id)
|
||||
}
|
||||
}
|
||||
|
||||
function handleCaseInstitutionChange() {
|
||||
caseForm.department_id = undefined
|
||||
caseForm.department_name = ''
|
||||
loadDepartmentOptions(caseForm.institution_id)
|
||||
}
|
||||
|
||||
function handleDetailInstitutionChange() {
|
||||
detailForm.department_id = undefined
|
||||
detailForm.department_name = ''
|
||||
loadDepartmentOptions(detailForm.institution_id)
|
||||
}
|
||||
|
||||
function handleCaseDepartmentChange(value?: number) {
|
||||
caseForm.department_name = getDepartmentName(value)
|
||||
}
|
||||
|
||||
function handleDetailDepartmentChange(value?: number) {
|
||||
detailForm.department_name = getDepartmentName(value)
|
||||
}
|
||||
|
||||
function activeCaseForm() {
|
||||
return detailDrawerVisible.value ? detailForm : caseForm
|
||||
}
|
||||
|
||||
function getDepartmentName(value?: number) {
|
||||
if (!value) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return departmentOptions.value.find(item => item.id === value)?.name || ''
|
||||
}
|
||||
|
||||
function institutionOptionLabel(item: InstitutionOption) {
|
||||
return item.code ? `${item.name} (${item.code})` : item.name
|
||||
}
|
||||
|
||||
async function submitCaseForm() {
|
||||
if (!appStore.token) {
|
||||
ElMessage.warning('缺少登录信息,请重新登录')
|
||||
@@ -932,6 +1068,7 @@ function buildDraftPayload(form: CaseDraftForm): CreateCaseDraftPayload {
|
||||
|
||||
payload[form.case_type] = structure
|
||||
if (canManageInstitution.value && form.institution_id) payload.institution_id = form.institution_id
|
||||
if (form.department_id) payload.department_id = form.department_id
|
||||
if (form.department_name.trim()) payload.department_name = form.department_name.trim()
|
||||
if (form.difficulty.trim()) payload.difficulty = form.difficulty.trim()
|
||||
if (form.chief_complaint.trim()) payload.chief_complaint = form.chief_complaint.trim()
|
||||
@@ -949,6 +1086,33 @@ function buildDraftPayload(form: CaseDraftForm): CreateCaseDraftPayload {
|
||||
return payload
|
||||
}
|
||||
|
||||
function buildSaveDraftPayload(form: CaseDraftForm): SaveCaseDraftPayload {
|
||||
const structure = buildEditableCaseStructure(form)
|
||||
const scoringRules = normalizeScoringRules(form)
|
||||
const examItems = normalizeExamItems(form)
|
||||
const payload: SaveCaseDraftPayload = {
|
||||
title: form.title.trim(),
|
||||
department_name: form.department_name.trim(),
|
||||
difficulty: form.difficulty.trim(),
|
||||
chief_complaint: form.chief_complaint.trim(),
|
||||
description: form.description.trim(),
|
||||
patient_gender: form.patient_gender,
|
||||
tags: isContentAdmin.value ? form.tags.trim() : parseTextList(form.tags),
|
||||
icd_codes: parseTextList(form.icd_codes),
|
||||
osce_enabled: form.osce_enabled,
|
||||
scoring_rules: scoringRules,
|
||||
exam_items: examItems
|
||||
}
|
||||
|
||||
payload[form.case_type] = structure
|
||||
if (canManageInstitution.value && form.institution_id) payload.institution_id = form.institution_id
|
||||
if (form.department_id) payload.department_id = form.department_id
|
||||
if (form.patient_age !== undefined) payload.patient_age = form.patient_age
|
||||
if (form.estimated_minutes !== undefined) payload.estimated_minutes = form.estimated_minutes
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
function buildCaseStructure(form: CaseDraftForm): Record<string, unknown> {
|
||||
if (form.case_type === 'teaching') {
|
||||
if (!form.teaching_learning_objectives.trim()) {
|
||||
@@ -973,27 +1137,49 @@ function buildCaseStructure(form: CaseDraftForm): Record<string, unknown> {
|
||||
}
|
||||
}
|
||||
|
||||
function buildEditableCaseStructure(form: CaseDraftForm): Record<string, unknown> {
|
||||
if (form.case_type === 'teaching') {
|
||||
if (!form.teaching_learning_objectives.trim()) {
|
||||
throw new Error('请输入教学目标')
|
||||
}
|
||||
|
||||
return {
|
||||
learning_objectives: form.teaching_learning_objectives.trim(),
|
||||
key_points: form.teaching_key_points.trim(),
|
||||
reference_answer: form.teaching_reference_answer.trim()
|
||||
}
|
||||
}
|
||||
|
||||
if (!form.traditional_standard_diagnosis.trim()) {
|
||||
throw new Error('请输入标准诊断')
|
||||
}
|
||||
|
||||
return {
|
||||
standard_diagnosis: form.traditional_standard_diagnosis.trim(),
|
||||
standard_treatment: form.traditional_standard_treatment.trim(),
|
||||
guideline_reference: form.traditional_guideline_reference.trim()
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeScoringRules(form: CaseDraftForm): CaseScoringRulePayload[] {
|
||||
const rules = form.scoring_rules
|
||||
.map(rule => ({
|
||||
dimension: rule.dimension.trim(),
|
||||
score_weight: Number(rule.score_weight),
|
||||
ai_auto_score: rule.ai_auto_score,
|
||||
scoring_standard: rule.scoring_standard.trim()
|
||||
}))
|
||||
.filter(rule => rule.dimension || rule.scoring_standard || Number.isFinite(rule.score_weight))
|
||||
.map((rule, index) => {
|
||||
const scoreWeight = Number(rule.score_weight)
|
||||
return {
|
||||
dimension: rule.dimension.trim() || (rule.scoring_standard.trim() ? `评分维度${index + 1}` : ''),
|
||||
score_weight: Number.isFinite(scoreWeight) && scoreWeight > 0 && scoreWeight <= 1 ? scoreWeight : 1,
|
||||
ai_auto_score: rule.ai_auto_score,
|
||||
scoring_standard: rule.scoring_standard.trim()
|
||||
}
|
||||
})
|
||||
.filter(rule => rule.dimension || rule.scoring_standard)
|
||||
|
||||
if (!rules.length) {
|
||||
throw new Error('至少添加 1 条评分规则')
|
||||
return [createDefaultScoringRule(form)]
|
||||
}
|
||||
|
||||
for (const rule of rules) {
|
||||
if (!rule.dimension) {
|
||||
throw new Error('评分规则的维度必填')
|
||||
}
|
||||
if (!Number.isFinite(rule.score_weight) || rule.score_weight <= 0 || rule.score_weight > 1) {
|
||||
throw new Error('评分规则权重必须大于 0 且不超过 1')
|
||||
}
|
||||
rule.ai_auto_score = true
|
||||
}
|
||||
|
||||
return rules.map(rule => ({
|
||||
@@ -1004,6 +1190,15 @@ function normalizeScoringRules(form: CaseDraftForm): CaseScoringRulePayload[] {
|
||||
}))
|
||||
}
|
||||
|
||||
function createDefaultScoringRule(form: CaseDraftForm): CaseScoringRulePayload {
|
||||
return {
|
||||
dimension: form.case_type === 'teaching' ? '教学目标达成' : '诊断与处置',
|
||||
score_weight: 1,
|
||||
ai_auto_score: true,
|
||||
scoring_standard: '由AI根据病例内容生成评分标准'
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeExamItems(form: CaseDraftForm): CaseExamItemPayload[] {
|
||||
const items = form.exam_items
|
||||
.map(item => ({
|
||||
@@ -1228,6 +1423,7 @@ async function openDetailDrawer(row: CaseListItem) {
|
||||
detailCase.value = row
|
||||
detailDrawerVisible.value = true
|
||||
caseDetail.value = null
|
||||
loadInstitutionOptions()
|
||||
|
||||
try {
|
||||
detailLoading.value = true
|
||||
@@ -1236,6 +1432,7 @@ async function openDetailDrawer(row: CaseListItem) {
|
||||
id: row.id
|
||||
})
|
||||
fillDetailForm(row, caseDetail.value)
|
||||
loadDepartmentOptions(detailForm.institution_id)
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '获取病例详情失败')
|
||||
} finally {
|
||||
@@ -1248,15 +1445,19 @@ async function submitDetailForm() {
|
||||
ElMessage.warning('缺少登录信息,请重新登录')
|
||||
return
|
||||
}
|
||||
if (!canEditDetailCase.value) {
|
||||
ElMessage.warning('只有草稿病例可以保存草稿')
|
||||
return
|
||||
}
|
||||
|
||||
const isValid = await detailFormRef.value?.validate().catch(() => false)
|
||||
if (!isValid) {
|
||||
return
|
||||
}
|
||||
|
||||
let payload: CreateCaseDraftPayload
|
||||
let payload: SaveCaseDraftPayload
|
||||
try {
|
||||
payload = buildDraftPayload(detailForm)
|
||||
payload = buildSaveDraftPayload(detailForm)
|
||||
} catch (error) {
|
||||
ElMessage.warning(error instanceof Error ? error.message : '请检查病例表单')
|
||||
return
|
||||
@@ -1264,16 +1465,16 @@ async function submitDetailForm() {
|
||||
|
||||
try {
|
||||
submittingDetail.value = true
|
||||
await submitCase({
|
||||
await saveCaseDraft({
|
||||
token: appStore.token,
|
||||
id: detailCase.value.id,
|
||||
payload
|
||||
})
|
||||
ElMessage.success('病例已提交')
|
||||
ElMessage.success('病例草稿已保存')
|
||||
detailDrawerVisible.value = false
|
||||
await loadCases()
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '提交病例失败')
|
||||
ElMessage.error(error instanceof Error ? error.message : '保存病例草稿失败')
|
||||
} finally {
|
||||
submittingDetail.value = false
|
||||
}
|
||||
@@ -1299,6 +1500,7 @@ function fillCaseFormFromImportedPdf(result: ImportCasePdfResult) {
|
||||
caseForm.case_type = caseType
|
||||
caseForm.title = getImportString(record, ['title', 'name', 'case_title', 'caseTitle'])
|
||||
caseForm.department_name = getImportString(record, ['department_name', 'departmentName'])
|
||||
caseForm.department_id = getImportNumber(record, ['department_id', 'departmentId']) ?? undefined
|
||||
caseForm.difficulty = getImportString(record, ['difficulty'])
|
||||
caseForm.chief_complaint = getImportString(record, ['chief_complaint', 'chiefComplaint'])
|
||||
caseForm.description = getImportString(record, ['description', 'summary', 'content'])
|
||||
@@ -1334,6 +1536,7 @@ function fillDetailForm(row: CaseListItem, fullData: unknown) {
|
||||
detailForm.title = getImportString(record, ['title', 'name', 'case_title', 'caseTitle'], row.title)
|
||||
detailForm.case_type = normalizeImportCaseType(getImportString(record, ['case_type', 'caseType'], row.caseType), normalizeImportCaseType(row.caseType, 'traditional'))
|
||||
detailForm.institution_id = getImportNumber(record, ['institution_id', 'institutionId']) ?? undefined
|
||||
detailForm.department_id = getImportNumber(record, ['department_id', 'departmentId']) ?? undefined
|
||||
detailForm.department_name = getImportString(record, ['department_name', 'departmentName'], row.departmentName)
|
||||
detailForm.difficulty = getImportString(record, ['difficulty'], row.difficulty)
|
||||
detailForm.chief_complaint = getImportString(record, ['chief_complaint', 'chiefComplaint'], row.chiefComplaint)
|
||||
@@ -1580,5 +1783,13 @@ function formatDateTime(value: string) {
|
||||
return value.replace('T', ' ').slice(0, 19)
|
||||
}
|
||||
|
||||
function formatScoreWeight(value: number) {
|
||||
if (!Number.isFinite(value)) {
|
||||
return '-'
|
||||
}
|
||||
|
||||
return value <= 1 ? `${Math.round(value * 100)}%` : String(value)
|
||||
}
|
||||
|
||||
onMounted(loadCases)
|
||||
</script>
|
||||
|
||||
+68
-20
@@ -98,7 +98,7 @@
|
||||
<el-col :span="12">
|
||||
<el-form-item label="角色" prop="role_type">
|
||||
<el-select v-model="userForm.role_type" placeholder="请选择角色">
|
||||
<el-option v-for="item in roleOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
<el-option v-for="item in formRoleOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -228,6 +228,7 @@ const pagination = reactive({
|
||||
page: 1,
|
||||
total: 0
|
||||
})
|
||||
const roleUserPageSize = 10
|
||||
const userForm = reactive({
|
||||
phone: '',
|
||||
real_name: '',
|
||||
@@ -248,7 +249,7 @@ const roleOptions: Array<{ label: string; value: ManagedRoleType }> = [
|
||||
{ label: '内容管理员', value: 'content_admin' }
|
||||
]
|
||||
const pageConfigs: Record<ManagedRoleType, { title: string; description: string }> = {
|
||||
doctor: { title: '医生管理', description: '维护本院医生账号,支持查询、导入导出、启停和密码重置。' },
|
||||
doctor: { title: '医生管理', description: '维护本院医生和内容管理员账号,支持查询、导入导出、启停和密码重置。' },
|
||||
student: { title: '医学生管理', description: '维护本院医学生账号,支持查询、导入导出、启停和密码重置。' },
|
||||
content_admin: { title: '内容管理员', description: '维护本院内容管理员账号,支持查询、导入导出、启停和密码重置。' }
|
||||
}
|
||||
@@ -258,6 +259,14 @@ const currentRoleType = computed<ManagedRoleType>(() => {
|
||||
return roleType === 'student' || roleType === 'content_admin' ? roleType : 'doctor'
|
||||
})
|
||||
const pageConfig = computed(() => pageConfigs[currentRoleType.value])
|
||||
const listRoleTypes = computed<ManagedRoleType[]>(() =>
|
||||
currentRoleType.value === 'doctor' ? ['doctor', 'content_admin'] : [currentRoleType.value]
|
||||
)
|
||||
const formRoleOptions = computed(() =>
|
||||
currentRoleType.value === 'doctor'
|
||||
? roleOptions.filter(item => item.value === 'doctor' || item.value === 'content_admin')
|
||||
: roleOptions.filter(item => item.value === currentRoleType.value)
|
||||
)
|
||||
const userDialogTitle = computed(() => (userMode.value === 'create' ? '新增用户' : '编辑用户'))
|
||||
const userRules: FormRules = {
|
||||
phone: [
|
||||
@@ -276,16 +285,38 @@ async function loadUsers() {
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
const result = await fetchUsers({
|
||||
token: appStore.token,
|
||||
roleType: currentRoleType.value,
|
||||
search: filters.search,
|
||||
status: filters.status,
|
||||
gender: filters.gender,
|
||||
page: pagination.page
|
||||
})
|
||||
users.value = result.users
|
||||
pagination.total = result.total
|
||||
if (listRoleTypes.value.length === 1) {
|
||||
const result = await fetchUsers({
|
||||
token: appStore.token,
|
||||
roleType: listRoleTypes.value[0],
|
||||
search: filters.search,
|
||||
status: filters.status,
|
||||
gender: filters.gender,
|
||||
page: pagination.page,
|
||||
size: roleUserPageSize
|
||||
})
|
||||
users.value = result.users
|
||||
pagination.total = result.total
|
||||
} else {
|
||||
const fetchSize = pagination.page * roleUserPageSize
|
||||
const results = await Promise.all(
|
||||
listRoleTypes.value.map(roleType =>
|
||||
fetchUsers({
|
||||
token: appStore.token,
|
||||
roleType,
|
||||
search: filters.search,
|
||||
status: filters.status,
|
||||
gender: filters.gender,
|
||||
page: 1,
|
||||
size: fetchSize
|
||||
})
|
||||
)
|
||||
)
|
||||
const mergedUsers = uniqueUsers(results.flatMap(result => result.users))
|
||||
const offset = (pagination.page - 1) * roleUserPageSize
|
||||
users.value = mergedUsers.slice(offset, offset + roleUserPageSize)
|
||||
pagination.total = results.reduce((total, result) => total + result.total, 0)
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '获取人员列表失败')
|
||||
} finally {
|
||||
@@ -541,14 +572,19 @@ async function exportCurrentUsers() {
|
||||
|
||||
try {
|
||||
exporting.value = true
|
||||
await exportUsers({
|
||||
token: appStore.token,
|
||||
roleType: currentRoleType.value,
|
||||
search: filters.search,
|
||||
status: filters.status,
|
||||
gender: filters.gender,
|
||||
page: pagination.page
|
||||
})
|
||||
await Promise.all(
|
||||
listRoleTypes.value.map(roleType =>
|
||||
exportUsers({
|
||||
token: appStore.token,
|
||||
roleType,
|
||||
search: filters.search,
|
||||
status: filters.status,
|
||||
gender: filters.gender,
|
||||
page: pagination.page,
|
||||
size: roleUserPageSize
|
||||
})
|
||||
)
|
||||
)
|
||||
ElMessage.success('用户导出已开始')
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '导出用户失败')
|
||||
@@ -593,6 +629,18 @@ function normalizeManagedRole(value: string): ManagedRoleType {
|
||||
return 'doctor'
|
||||
}
|
||||
|
||||
function uniqueUsers(items: UserListItem[]) {
|
||||
const seen = new Set<string>()
|
||||
return items.filter(item => {
|
||||
const key = item.id || item.phone
|
||||
if (seen.has(key)) {
|
||||
return false
|
||||
}
|
||||
seen.add(key)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
watch(currentRoleType, () => {
|
||||
pagination.page = 1
|
||||
resetUserForm()
|
||||
|
||||
+79
-10
@@ -16,9 +16,23 @@
|
||||
<section class="filter-bar admin-user-filter">
|
||||
<el-input v-model="filters.search" :prefix-icon="Search" clearable placeholder="搜索姓名/手机号" @keyup.enter="loadUsers" />
|
||||
<el-select v-model="filters.roleType" clearable placeholder="角色">
|
||||
<el-option v-for="item in roleTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
<el-option v-for="item in filterRoleTypeOptions" :key="item.value || 'all'" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="filters.institution"
|
||||
:loading="loadingInstitutions"
|
||||
clearable
|
||||
filterable
|
||||
placeholder="机构"
|
||||
@visible-change="handleInstitutionVisibleChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in institutionOptions"
|
||||
:key="item.id"
|
||||
:label="institutionOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input v-model="filters.institution" clearable placeholder="机构ID/编码" @keyup.enter="loadUsers" />
|
||||
<el-select v-model="filters.status" clearable placeholder="状态">
|
||||
<el-option label="正常" value="1" />
|
||||
<el-option label="禁用" value="0" />
|
||||
@@ -100,8 +114,22 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="机构ID" prop="institution">
|
||||
<el-input-number v-model="userForm.institution" :min="1" :precision="0" :controls="false" placeholder="请输入机构ID" />
|
||||
<el-form-item label="机构" prop="institution">
|
||||
<el-select
|
||||
v-model="userForm.institution"
|
||||
:loading="loadingInstitutions"
|
||||
clearable
|
||||
filterable
|
||||
placeholder="请选择机构"
|
||||
@visible-change="handleInstitutionVisibleChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in institutionOptions"
|
||||
:key="item.id"
|
||||
:label="institutionOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
@@ -189,11 +217,13 @@ import {
|
||||
disableUser,
|
||||
downloadUserImportTemplate,
|
||||
exportUsers,
|
||||
fetchInstitutionList,
|
||||
fetchUsers,
|
||||
importUsers,
|
||||
resetUserPassword,
|
||||
updateUser,
|
||||
type CreateUserPayload,
|
||||
type InstitutionOption,
|
||||
type UpdateUserPayload,
|
||||
type UserListItem
|
||||
} from '@/api/users'
|
||||
@@ -206,6 +236,7 @@ const importing = ref(false)
|
||||
const exporting = ref(false)
|
||||
const downloadingTemplate = ref(false)
|
||||
const resettingPassword = ref(false)
|
||||
const loadingInstitutions = ref(false)
|
||||
const userDialogVisible = ref(false)
|
||||
const importDialogVisible = ref(false)
|
||||
const resetPasswordDialogVisible = ref(false)
|
||||
@@ -217,7 +248,14 @@ const resetPasswordUser = ref<UserListItem | null>(null)
|
||||
const importFile = ref<File | null>(null)
|
||||
const importFileList = ref<UploadUserFile[]>([])
|
||||
const users = ref<UserListItem[]>([])
|
||||
const filters = reactive({
|
||||
const institutionOptions = ref<InstitutionOption[]>([])
|
||||
const filters = reactive<{
|
||||
search: string
|
||||
roleType: string
|
||||
institution: number | ''
|
||||
status: string
|
||||
gender: string
|
||||
}>({
|
||||
search: '',
|
||||
roleType: '',
|
||||
institution: '',
|
||||
@@ -251,6 +289,10 @@ const roleTypeOptions = [
|
||||
{ label: '医院管理员 hospital_admin', value: 'hospital_admin' },
|
||||
{ label: '内容管理员 content_admin', value: 'content_admin' }
|
||||
]
|
||||
const filterRoleTypeOptions = [
|
||||
{ label: '全部', value: '' },
|
||||
...roleTypeOptions
|
||||
]
|
||||
|
||||
const userRules: FormRules = {
|
||||
phone: [
|
||||
@@ -259,7 +301,7 @@ const userRules: FormRules = {
|
||||
],
|
||||
real_name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
||||
role_type: [{ required: true, message: '请选择或输入角色码', trigger: 'change' }],
|
||||
institution: [{ required: true, message: '请输入机构ID', trigger: 'blur' }]
|
||||
institution: [{ required: true, message: '请选择机构', trigger: 'change' }]
|
||||
}
|
||||
const userDialogTitle = computed(() => (userMode.value === 'create' ? '新增用户' : '编辑用户'))
|
||||
|
||||
@@ -273,9 +315,9 @@ async function loadUsers() {
|
||||
loading.value = true
|
||||
const result = await fetchUsers({
|
||||
token: appStore.token,
|
||||
roleType: filters.roleType || appStore.roleType,
|
||||
roleType: filters.roleType,
|
||||
search: filters.search,
|
||||
institution: filters.institution,
|
||||
institution: filters.institution === '' ? '' : String(filters.institution),
|
||||
status: filters.status,
|
||||
gender: filters.gender,
|
||||
page: pagination.page,
|
||||
@@ -314,6 +356,7 @@ function handleSizeChange(size: number) {
|
||||
function openCreateDialog() {
|
||||
userMode.value = 'create'
|
||||
userDialogVisible.value = true
|
||||
loadInstitutionOptions()
|
||||
}
|
||||
|
||||
function openEditDialog(row: UserListItem) {
|
||||
@@ -329,6 +372,7 @@ function openEditDialog(row: UserListItem) {
|
||||
userForm.training_stage = row.trainingStage
|
||||
userForm.status = row.status
|
||||
userDialogVisible.value = true
|
||||
loadInstitutionOptions()
|
||||
}
|
||||
|
||||
function openImportDialog() {
|
||||
@@ -372,6 +416,31 @@ function buildCreatePayload(): CreateUserPayload {
|
||||
return payload
|
||||
}
|
||||
|
||||
async function loadInstitutionOptions() {
|
||||
if (!appStore.token || loadingInstitutions.value || institutionOptions.value.length) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
loadingInstitutions.value = true
|
||||
institutionOptions.value = await fetchInstitutionList(appStore.token)
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '获取机构列表失败')
|
||||
} finally {
|
||||
loadingInstitutions.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleInstitutionVisibleChange(visible: boolean) {
|
||||
if (visible) {
|
||||
loadInstitutionOptions()
|
||||
}
|
||||
}
|
||||
|
||||
function institutionOptionLabel(item: InstitutionOption) {
|
||||
return item.code ? `${item.name} (${item.code})` : item.name
|
||||
}
|
||||
|
||||
function buildUpdatePayload(): UpdateUserPayload {
|
||||
if (!editingUser.value) {
|
||||
return {}
|
||||
@@ -561,9 +630,9 @@ async function exportCurrentUsers() {
|
||||
exporting.value = true
|
||||
await exportUsers({
|
||||
token: appStore.token,
|
||||
roleType: filters.roleType || appStore.roleType,
|
||||
roleType: filters.roleType,
|
||||
search: filters.search,
|
||||
institution: filters.institution,
|
||||
institution: filters.institution === '' ? '' : String(filters.institution),
|
||||
status: filters.status,
|
||||
gender: filters.gender,
|
||||
page: pagination.page
|
||||
|
||||
Reference in New Issue
Block a user