From 1d093c95890709d0c39af035227d1f835cd8c0e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=A4=A9=E9=AA=84?= <5307576@qq.com> Date: Fri, 12 Jun 2026 18:10:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=81=94=E8=B0=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/cases.ts | 572 ++++++++++++++ src/assets/styles/main.scss | 138 ++++ src/layouts/AdminLayout.vue | 14 +- src/mock/navigation.ts | 16 +- src/router/index.ts | 7 + src/types/index.ts | 1 + src/views/AiCaseGenerateView.vue | 177 +++++ src/views/CaseReviewView.vue | 308 ++++++++ src/views/CasesView.vue | 1126 ++++++++++++++++++++++++++-- src/views/ContentDashboardView.vue | 2 +- src/views/LoginView.vue | 2 +- src/views/TeacherDashboardView.vue | 2 +- 12 files changed, 2309 insertions(+), 56 deletions(-) create mode 100644 src/api/cases.ts create mode 100644 src/views/AiCaseGenerateView.vue create mode 100644 src/views/CaseReviewView.vue diff --git a/src/api/cases.ts b/src/api/cases.ts new file mode 100644 index 00000000..4f05437c --- /dev/null +++ b/src/api/cases.ts @@ -0,0 +1,572 @@ +export type DraftCaseType = 'traditional' | 'teaching' +export type CasePublishStatus = 0 | 1 | 2 + +export interface CaseListParams { + token: string + search?: string + case_type?: string + publish_status?: CasePublishStatus | '' + institution?: string + department?: string + status?: string + osce_enabled?: boolean | string | '' + ordering?: string + page?: number +} + +export interface CaseListItem { + id: string + title: string + caseType: string + publishStatus: CasePublishStatus | null + institutionId: string + institutionName: string + departmentId: string + departmentName: string + difficulty: string + chiefComplaint: string + tags: string[] + icdCodes: string[] + estimatedMinutes: number | null + updatedAt: string + createdAt: string +} + +export interface CaseListResult { + cases: CaseListItem[] + total: number +} + +export interface CaseScoringRulePayload { + dimension: string + score_weight: number + description?: string + ai_auto_score?: boolean + scoring_standard?: string +} + +export interface CaseExamItemPayload { + item_code: string + item_name?: string + item_type?: string +} + +export interface CreateCaseDraftPayload { + title: string + case_type: DraftCaseType + institution_id?: number + department_name?: string + traditional?: Record + teaching?: Record + scoring_rules: CaseScoringRulePayload[] + exam_items?: CaseExamItemPayload[] + 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 CreateCaseDraftParams { + token: string + payload: CreateCaseDraftPayload +} + +export interface AiGenerateCasePayload { + prompt: string + case_type: DraftCaseType +} + +export interface AiGenerateCaseParams { + token: string + payload: AiGenerateCasePayload +} + +export interface AiCaseUsage { + promptTokens: number | null + completionTokens: number | null +} + +export interface AiGeneratedCaseData extends Record { + title?: string + case_type?: string + chief_complaint?: string + department_name?: string + patient_age?: number + patient_gender?: string +} + +export interface AiGenerateCaseResult { + parseId: string + caseType: string + aiUsage: AiCaseUsage + promptVersion: string + parsingSeconds: number | null + generatingSeconds: number | null + data: AiGeneratedCaseData + raw: unknown +} + +export interface ImportCasePdfParams { + token: string + file: File +} + +export interface UpdateCaseRelationsPayload { + institution_id?: number | null + department_id?: number | null + department_name?: string +} + +export interface UpdateCaseRelationsParams { + token: string + id: string | number + payload: UpdateCaseRelationsPayload +} + +export interface CaseActionParams { + token: string + id: string | number +} + +const listKeys = ['results', 'list', 'rows', 'items', 'records', 'cases'] +const totalKeys = ['count', 'total', 'total_count', 'totalCount'] + +function createAuthorization(token: string) { + return /^Bearer\s+/i.test(token) ? token : `Bearer ${token}` +} + +function parseResponseText(text: string): unknown { + if (!text) { + return null + } + + try { + return JSON.parse(text) + } catch { + return null + } +} + +function getMessageFromResponse(data: unknown): string { + if (!data || typeof data !== 'object') { + return '' + } + + const record = data as Record + const message = record.message || record.msg || record.detail + if (typeof message === 'string') { + return message + } + + return getMessageFromResponse(record.data) +} + +function getRecord(value: unknown): Record { + return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record) : {} +} + +function getFirst(record: Record, keys: string[]): unknown { + for (const key of keys) { + if (record[key] !== undefined && record[key] !== null) { + return record[key] + } + } + + return undefined +} + +function getString(record: Record, keys: string[], fallback = ''): string { + for (const key of keys) { + const value = record[key] + if (typeof value === 'string' && value.trim()) { + return value + } + if (typeof value === 'number') { + return String(value) + } + } + + return fallback +} + +function getNumber(record: Record, keys: string[]): number | null { + const value = getFirst(record, keys) + if (typeof value === 'number' && Number.isFinite(value)) { + return value + } + if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) { + return Number(value) + } + + return null +} + +function getStringList(record: Record, keys: string[]): string[] { + const value = getFirst(record, keys) + if (Array.isArray(value)) { + return value + .map(item => { + if (typeof item === 'string' || typeof item === 'number') { + return String(item) + } + const itemRecord = getRecord(item) + return getString(itemRecord, ['name', 'title', 'code', 'value']) + }) + .map(item => item.trim()) + .filter(Boolean) + } + + if (typeof value === 'string') { + return value + .split(/[,\n,;;]/) + .map(item => item.trim()) + .filter(Boolean) + } + + return [] +} + +function normalizePublishStatus(value: unknown): CasePublishStatus | null { + if (value === 0 || value === 1 || value === 2) { + return value + } + + if (typeof value === 'string') { + const normalized = value.trim() + const numeric = Number(normalized) + if (numeric === 0 || numeric === 1 || numeric === 2) { + return numeric + } + if (normalized.includes('草稿') || normalized.toLowerCase() === 'draft') { + return 0 + } + if (normalized.includes('发布') || normalized.toLowerCase() === 'published') { + return 2 + } + if (normalized.includes('正常') || normalized.includes('启用') || normalized.toLowerCase() === 'active') { + return 1 + } + } + + return null +} + +function getTotal(record: Record, fallback: number): number { + for (const key of totalKeys) { + const value = record[key] + if (typeof value === 'number') { + return value + } + if (typeof value === 'string' && Number.isFinite(Number(value))) { + return Number(value) + } + } + + return fallback +} + +function findCasePayload(data: unknown): { items: unknown[]; total: number } { + if (Array.isArray(data)) { + return { items: data, total: data.length } + } + + const record = getRecord(data) + if (!Object.keys(record).length) { + return { items: [], total: 0 } + } + + for (const key of listKeys) { + const value = record[key] + if (Array.isArray(value)) { + return { items: value, total: getTotal(record, value.length) } + } + } + + if (Array.isArray(record.data)) { + return { items: record.data, total: getTotal(record, record.data.length) } + } + + if (record.data && typeof record.data === 'object') { + const nested = findCasePayload(record.data) + return { + items: nested.items, + total: nested.total || getTotal(record, nested.items.length) + } + } + + return { items: [], total: getTotal(record, 0) } +} + +function getRelatedId(record: Record, directKeys: string[], objectKeys: string[]): string { + const direct = getString(record, directKeys) + if (direct) { + return direct + } + + for (const key of objectKeys) { + const nested = getRecord(record[key]) + const value = getString(nested, ['id', 'pk', 'value']) + if (value) { + return value + } + } + + return '' +} + +function getRelatedName(record: Record, directKeys: string[], objectKeys: string[]): string { + const direct = getString(record, directKeys) + if (direct) { + return direct + } + + for (const key of objectKeys) { + const nested = getRecord(record[key]) + const value = getString(nested, ['name', 'title', 'label']) + if (value) { + return value + } + } + + return '' +} + +function normalizeCase(item: unknown, index: number): CaseListItem { + const record = getRecord(item) + const publishStatus = normalizePublishStatus(getFirst(record, ['publish_status', 'publishStatus', 'status'])) + const title = getString(record, ['title', 'name', 'case_title', 'caseTitle'], `病例${index + 1}`) + + return { + id: getString(record, ['id', 'uuid', 'case_id', 'caseId'], `${index}`), + title, + caseType: getString(record, ['case_type', 'caseType', 'type']), + publishStatus, + institutionId: getRelatedId(record, ['institution_id', 'institutionId'], ['institution', 'institution_info', 'institutionInfo']), + institutionName: getRelatedName(record, ['institution_name', 'institutionName', 'organization_name', 'organizationName'], ['institution', 'institution_info', 'institutionInfo']), + departmentId: getRelatedId(record, ['department_id', 'departmentId'], ['department', 'department_info', 'departmentInfo']), + departmentName: getRelatedName(record, ['department_name', 'departmentName'], ['department', 'department_info', 'departmentInfo']), + difficulty: getString(record, ['difficulty']), + chiefComplaint: getString(record, ['chief_complaint', 'chiefComplaint']), + tags: getStringList(record, ['tags', 'tag_list', 'tagList']), + icdCodes: getStringList(record, ['icd_codes', 'icdCodes', 'icd_list', 'icdList']), + estimatedMinutes: getNumber(record, ['estimated_minutes', 'estimatedMinutes']), + updatedAt: getString(record, ['updated_at', 'updatedAt', 'modified_at', 'modifiedAt']), + createdAt: getString(record, ['created_at', 'createdAt']) + } +} + +function normalizeAiGenerateResult(data: unknown): AiGenerateCaseResult { + const root = getRecord(data) + const payload = root.parse_id || root.parseId ? root : getRecord(root.data) + const usage = getRecord(getFirst(payload, ['ai_usage', 'aiUsage'])) + const generatedData = getRecord(payload.data) + + return { + parseId: getString(payload, ['parse_id', 'parseId']), + caseType: getString(payload, ['case_type', 'caseType']), + aiUsage: { + promptTokens: getNumber(usage, ['prompt_tokens', 'promptTokens']), + completionTokens: getNumber(usage, ['completion_tokens', 'completionTokens']) + }, + promptVersion: getString(payload, ['prompt_version', 'promptVersion']), + parsingSeconds: getNumber(payload, ['parsing_seconds', 'parsingSeconds']), + generatingSeconds: getNumber(payload, ['generating_seconds', 'generatingSeconds']), + data: generatedData as AiGeneratedCaseData, + raw: data + } +} + +function createCaseQuery(params: Partial) { + const query = new URLSearchParams() + if (params.search?.trim()) { + query.set('search', params.search.trim()) + } + if (params.case_type) { + query.set('case_type', params.case_type) + } + if (params.publish_status !== undefined && params.publish_status !== '') { + query.set('publish_status', String(params.publish_status)) + } + if (params.institution?.trim()) { + query.set('institution', params.institution.trim()) + } + if (params.department?.trim()) { + query.set('department', params.department.trim()) + } + if (params.status?.trim()) { + query.set('status', params.status.trim()) + } + if (params.osce_enabled !== undefined && params.osce_enabled !== '') { + query.set('osce_enabled', String(params.osce_enabled)) + } + if (params.ordering?.trim()) { + query.set('ordering', params.ordering.trim()) + } + if (params.page) { + query.set('page', String(params.page)) + } + return query +} + +async function parseMutationResponse(response: Response, fallbackMessage: string): Promise { + const text = await response.text() + const data = parseResponseText(text) + + if (!response.ok) { + const message = getMessageFromResponse(data) || fallbackMessage + throw new Error(message) + } + + return data +} + +export async function fetchCases(params: CaseListParams): Promise { + const query = createCaseQuery(params) + const url = `/server/api/cms/cases/${query.toString() ? `?${query.toString()}` : ''}` + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: createAuthorization(params.token) + } + }) + const text = await response.text() + const data = parseResponseText(text) + + if (!response.ok) { + const message = getMessageFromResponse(data) || '获取病例列表失败' + throw new Error(message) + } + + const payload = findCasePayload(data) + + return { + cases: payload.items.map(normalizeCase), + total: payload.total + } +} + +export async function createCaseDraft(params: CreateCaseDraftParams): Promise { + const response = await fetch('/server/api/cms/cases/', { + 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 { + const response = await fetch('/server/api/cms/cases/ai-generate/', { + method: 'POST', + headers: { + Accept: 'application/json', + Authorization: createAuthorization(params.token), + 'Content-Type': 'application/json' + }, + body: JSON.stringify(params.payload) + }) + const text = await response.text() + const data = parseResponseText(text) + + if (!response.ok) { + const message = getMessageFromResponse(data) || 'AI 生成病例失败' + throw new Error(message) + } + + return normalizeAiGenerateResult(data) +} + +export async function importCasePdf(params: ImportCasePdfParams): Promise { + const formData = new FormData() + formData.append('file', params.file) + + const response = await fetch('/server/api/cms/cases/import-pdf/', { + method: 'POST', + headers: { + Accept: 'application/json', + Authorization: createAuthorization(params.token) + }, + body: formData + }) + + return parseMutationResponse(response, '导入 PDF 病例失败') +} + +export async function updateCaseRelations(params: UpdateCaseRelationsParams): Promise { + const response = await fetch(`/server/api/cms/cases/${params.id}/relations/`, { + 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 submitCase(params: CaseActionParams): Promise { + const response = await fetch(`/server/api/cms/cases/${params.id}/submit/`, { + method: 'POST', + headers: { + Accept: 'application/json', + Authorization: createAuthorization(params.token) + } + }) + + return parseMutationResponse(response, '提交病例失败') +} + +export async function disableCase(params: CaseActionParams): Promise { + const response = await fetch(`/server/api/cms/cases/${params.id}/disable/`, { + method: 'POST', + headers: { + Accept: 'application/json', + Authorization: createAuthorization(params.token) + } + }) + + return parseMutationResponse(response, '停用病例失败') +} + +export async function publishCase(params: CaseActionParams): Promise { + const response = await fetch(`/server/api/cms/cases/${params.id}/publish/`, { + method: 'POST', + headers: { + Accept: 'application/json', + Authorization: createAuthorization(params.token) + } + }) + + return parseMutationResponse(response, '发布病例失败') +} + +export async function fetchCaseFull(params: CaseActionParams): Promise { + const response = await fetch(`/server/api/cms/cases/${params.id}/full/`, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: createAuthorization(params.token) + } + }) + const text = await response.text() + const data = parseResponseText(text) + + if (!response.ok) { + const message = getMessageFromResponse(data) || '获取病例详情失败' + throw new Error(message) + } + + return data +} diff --git a/src/assets/styles/main.scss b/src/assets/styles/main.scss index 3f7405a8..71ac6c9e 100644 --- a/src/assets/styles/main.scss +++ b/src/assets/styles/main.scss @@ -316,6 +316,16 @@ p { color: #fff; background: rgb(37 99 235 / 42%); } + + &.disabled { + color: rgb(189 208 231 / 42%); + cursor: not-allowed; + + &:hover { + color: rgb(189 208 231 / 42%); + background: transparent; + } + } } .sidebar-footer { @@ -757,6 +767,18 @@ p { grid-template-columns: minmax(240px, 1fr) 140px 140px 130px 96px 96px; } +.case-filter { + grid-template-columns: minmax(260px, 1fr) 160px 140px 150px 96px 96px; +} + +.content-case-filter { + grid-template-columns: minmax(220px, 1fr) 140px 130px 120px 120px 120px 140px 96px 96px; +} + +.case-review-filter { + grid-template-columns: minmax(260px, 1fr) 150px 96px 96px; +} + .table-subtext { display: block; margin-top: 4px; @@ -764,6 +786,77 @@ p { font-size: 12px; } +.case-tag-list { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; +} + +.case-create-form, +.case-relations-form { + .el-select, + .el-input-number { + width: 100%; + } +} + +.case-form-section { + display: grid; + gap: 12px; + padding: 14px 0; + border-top: 1px solid var(--border); + + &:first-child { + padding-top: 0; + border-top: 0; + } + + h3 { + margin: 0; + font-size: 15px; + } +} + +.case-section-title { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.scoring-rule-row, +.exam-item-row { + display: grid; + align-items: center; + gap: 10px; +} + +.scoring-rule-row { + grid-template-columns: minmax(120px, 0.7fr) 100px 120px minmax(180px, 1fr) 36px; +} + +.exam-item-row { + grid-template-columns: minmax(150px, 0.8fr) minmax(160px, 1fr) minmax(120px, 0.8fr) 36px; +} + +.case-empty-line { + padding: 10px 12px; + border-radius: 8px; + color: var(--muted); + font-size: 13px; + background: var(--panel-soft); +} + +.relation-input { + margin-top: 12px; +} + +.case-detail-drawer { + display: grid; + gap: 16px; +} + .kanban-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); @@ -871,6 +964,50 @@ p { gap: 18px; } +.ai-case-workbench { + display: grid; + grid-template-columns: minmax(420px, 0.82fr) minmax(0, 1.18fr); + gap: 18px; +} + +.ai-case-form { + .el-radio-group, + .el-textarea { + width: 100%; + } +} + +.ai-result-section { + display: grid; + align-content: start; + gap: 16px; +} + +.ai-result-kpis { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; + + div { + display: grid; + gap: 6px; + min-height: 76px; + padding: 12px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--panel-soft); + } + + span { + color: var(--muted); + font-size: 12px; + } + + strong { + font-size: 20px; + } +} + .profile-layout { display: grid; grid-template-columns: 320px minmax(0, 1fr); @@ -923,6 +1060,7 @@ p { .dashboard-grid, .content-grid, .ai-workbench, + .ai-case-workbench, .profile-layout, .settings-grid { grid-template-columns: 1fr; diff --git a/src/layouts/AdminLayout.vue b/src/layouts/AdminLayout.vue index 75db50ba..dfa6867e 100644 --- a/src/layouts/AdminLayout.vue +++ b/src/layouts/AdminLayout.vue @@ -13,10 +13,16 @@ diff --git a/src/mock/navigation.ts b/src/mock/navigation.ts index 0fbdba4a..dbe2596a 100644 --- a/src/mock/navigation.ts +++ b/src/mock/navigation.ts @@ -50,7 +50,7 @@ export const roleMenus: Record = { title: '内容管理', items: [ { page: 'case-list', icon: Collection, title: '病例库' }, - { page: 'tag-category', icon: PriceTag, title: '标签分类' }, + { page: 'tag-category', icon: PriceTag, title: '标签分类', disabled: true }, { page: 'ai-case', icon: Cpu, title: 'AI病例生成' } ] }, @@ -70,10 +70,10 @@ export const roleMenus: Record = { { title: '数据分析', items: [ - { page: 'data-user', icon: UserFilled, title: '用户分析' }, - { page: 'data-case', icon: Files, title: '病例分析' }, - { page: 'data-hospital', icon: OfficeBuilding, title: '医院分析' }, - { page: 'data-retention', icon: Refresh, title: '留存分析' } + { page: 'data-user', icon: UserFilled, title: '用户分析', disabled: true }, + { page: 'data-case', icon: Files, title: '病例分析', disabled: true }, + { page: 'data-hospital', icon: OfficeBuilding, title: '医院分析', disabled: true }, + { page: 'data-retention', icon: Refresh, title: '留存分析', disabled: true } ] }, ], @@ -90,6 +90,7 @@ export const roleMenus: Record = { { title: '运营数据', items: [ + { page: 'case-review', icon: DocumentChecked, title: '病例审核' }, { page: 'hospital-data', icon: DataAnalysis, title: '运营数据' }, { page: 'case-usage', icon: Collection, title: '病例使用' }, { page: 'training-stats', icon: Memo, title: '训练统计' }, @@ -152,6 +153,11 @@ export const pageTitles = Object.values(roleMenus).reduce }, {}) const directPagePaths: Record = { + 'ai-case': '/cases/ai-generate', + 'ai-generate': '/cases/ai-generate', + 'case-library': '/cases', + 'case-list': '/cases', + 'case-review': '/cases/review', 'content-admin-list': '/users/content-admins', 'department-list': '/departments', 'doctor-list': '/users/doctors', diff --git a/src/router/index.ts b/src/router/index.ts index f616ee29..ea0d23fd 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -19,6 +19,8 @@ const routes: RouteRecordRaw[] = [ { path: 'institutions', name: 'Institutions', component: () => import('@/views/InstitutionsView.vue'), meta: { title: '医院管理' } }, { path: 'departments', name: 'Departments', component: () => import('@/views/DepartmentsView.vue'), meta: { title: '科室管理' } }, { path: 'users', name: 'Users', component: () => import('@/views/UsersView.vue'), meta: { title: '用户列表' } }, + { path: 'cases/ai-generate', name: 'AiCaseGenerate', component: () => import('@/views/AiCaseGenerateView.vue'), meta: { title: 'AI病例生成' } }, + { path: 'cases/review', name: 'CaseReview', component: () => import('@/views/CaseReviewView.vue'), meta: { title: '病例审核' } }, { path: 'users/doctors', name: 'DoctorUsers', component: () => import('@/views/RoleUsersView.vue'), meta: { title: '医生管理', roleType: 'doctor' } }, { path: 'users/students', name: 'StudentUsers', component: () => import('@/views/RoleUsersView.vue'), meta: { title: '医学生管理', roleType: 'student' } }, { path: 'users/content-admins', name: 'ContentAdminUsers', component: () => import('@/views/RoleUsersView.vue'), meta: { title: '内容管理员', roleType: 'content_admin' } }, @@ -28,6 +30,11 @@ const routes: RouteRecordRaw[] = [ { path: 'module/hospital-dashboard', name: 'HospitalDashboard', component: () => import('@/views/HospitalDashboardView.vue'), meta: { title: '医院驾驶舱' } }, { path: 'module/content-dashboard', name: 'ContentDashboard', component: () => import('@/views/ContentDashboardView.vue'), meta: { title: '内容概览' } }, { path: 'module/teacher-dashboard', name: 'TeacherDashboard', component: () => import('@/views/TeacherDashboardView.vue'), meta: { title: '教学概览' } }, + { path: 'module/case-list', redirect: '/cases' }, + { path: 'module/case-library', redirect: '/cases' }, + { path: 'module/case-review', redirect: '/cases/review' }, + { path: 'module/ai-case', redirect: '/cases/ai-generate' }, + { path: 'module/ai-generate', redirect: '/cases/ai-generate' }, { path: 'module/content-admin-list', redirect: '/users/content-admins' }, { path: 'module/department-list', redirect: '/departments' }, { path: 'module/doctor-list', redirect: '/users/doctors' }, diff --git a/src/types/index.ts b/src/types/index.ts index 2680cc71..5368529d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -17,6 +17,7 @@ export interface MenuItem { page: string title: string icon: Component + disabled?: boolean } export interface MenuSection { diff --git a/src/views/AiCaseGenerateView.vue b/src/views/AiCaseGenerateView.vue new file mode 100644 index 00000000..3f18d23c --- /dev/null +++ b/src/views/AiCaseGenerateView.vue @@ -0,0 +1,177 @@ + + + diff --git a/src/views/CaseReviewView.vue b/src/views/CaseReviewView.vue new file mode 100644 index 00000000..5fc486bd --- /dev/null +++ b/src/views/CaseReviewView.vue @@ -0,0 +1,308 @@ + + + diff --git a/src/views/CasesView.vue b/src/views/CasesView.vue index 08accf29..f4c1eb25 100644 --- a/src/views/CasesView.vue +++ b/src/views/CasesView.vue @@ -2,75 +2,1113 @@
-

病例中心

-

维护 AI 问诊病例、难度、科室和发布状态。

+

病例库

+

{{ pageDescription }}

- 批量导入 - 新建病例 + PDF 导入 + 新增草稿
-
- - - - - - +
+ + + + - - - - + + + + - 重置 + + + 查询 + 重置
- - - - - + + + - - - + - - diff --git a/src/views/ContentDashboardView.vue b/src/views/ContentDashboardView.vue index 71cff3c3..af33527e 100644 --- a/src/views/ContentDashboardView.vue +++ b/src/views/ContentDashboardView.vue @@ -57,7 +57,7 @@

内容质量处理队列

-

可直接用于后续接入病例列表接口

+

集中跟进低通过率和待优化内容

diff --git a/src/views/LoginView.vue b/src/views/LoginView.vue index 4851adc7..0982b9a1 100644 --- a/src/views/LoginView.vue +++ b/src/views/LoginView.vue @@ -93,7 +93,7 @@ async function handleLogin() { role: form.role }) if (!result.token) { - throw new Error('登录接口未返回访问令牌') + throw new Error('登录失败,请稍后重试') } appStore.login(form.account, form.role, result.token, result.roleType || form.role) loading.value = false diff --git a/src/views/TeacherDashboardView.vue b/src/views/TeacherDashboardView.vue index 28c60343..ebb5bd47 100644 --- a/src/views/TeacherDashboardView.vue +++ b/src/views/TeacherDashboardView.vue @@ -67,7 +67,7 @@

任务列表

-

展示已下发任务情况,后续可接老师创建任务后的统计接口

+

展示已下发任务的完成进度与训练情况

新建任务