fix: 新增接口入参
This commit is contained in:
@@ -32,6 +32,7 @@ export interface InstitutionPayload {
|
||||
level?: string
|
||||
province?: string
|
||||
city?: string
|
||||
banner_url?: string
|
||||
}
|
||||
|
||||
export interface CreateInstitutionPayload extends InstitutionPayload {
|
||||
@@ -53,6 +54,18 @@ export interface DisableInstitutionParams {
|
||||
id: number
|
||||
}
|
||||
|
||||
export interface UploadInstitutionBannerParams {
|
||||
token: string
|
||||
id: number
|
||||
file: File
|
||||
}
|
||||
|
||||
export interface UploadInstitutionBannerResult {
|
||||
message: string
|
||||
bannerUrl: string
|
||||
raw: unknown
|
||||
}
|
||||
|
||||
export interface ImportInstitutionsParams {
|
||||
token: string
|
||||
file: File
|
||||
@@ -102,6 +115,25 @@ function getString(record: Record<string, unknown>, key: string): string {
|
||||
return ''
|
||||
}
|
||||
|
||||
function getBannerUrlFromResponse(data: unknown): string {
|
||||
if (!data || typeof data !== 'object') {
|
||||
return ''
|
||||
}
|
||||
|
||||
const record = data as Record<string, unknown>
|
||||
const bannerUrl = record.banner_url || record.bannerUrl
|
||||
if (typeof bannerUrl === 'string') {
|
||||
return bannerUrl
|
||||
}
|
||||
|
||||
const nested = record.data || record.result || record.payload
|
||||
if (nested && typeof nested === 'object') {
|
||||
return getBannerUrlFromResponse(nested)
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
function normalizeInstitution(item: unknown): InstitutionListItem {
|
||||
const record = item && typeof item === 'object' ? (item as Record<string, unknown>) : {}
|
||||
const id = record.id
|
||||
@@ -291,6 +323,27 @@ export async function disableInstitution(params: DisableInstitutionParams): Prom
|
||||
return parseMutationResponse(response, '停用机构失败')
|
||||
}
|
||||
|
||||
export async function uploadInstitutionBanner(params: UploadInstitutionBannerParams): Promise<UploadInstitutionBannerResult> {
|
||||
const formData = new FormData()
|
||||
formData.append('file', params.file)
|
||||
|
||||
const response = await fetch(`/server/api/cms/institutions/${params.id}/banner/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: createAuthorization(params.token)
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
const data = await parseMutationResponse(response, '上传机构背景图失败')
|
||||
|
||||
return {
|
||||
message: getMessageFromResponse(data),
|
||||
bannerUrl: getBannerUrlFromResponse(data),
|
||||
raw: data
|
||||
}
|
||||
}
|
||||
|
||||
export async function importInstitutions(params: ImportInstitutionsParams): Promise<unknown> {
|
||||
const formData = new FormData()
|
||||
formData.append('file', params.file)
|
||||
|
||||
@@ -101,6 +101,10 @@ export interface HospitalOverview {
|
||||
name?: string
|
||||
logo?: string
|
||||
level?: string
|
||||
province?: string
|
||||
city?: string
|
||||
banner_url?: string
|
||||
bannerUrl?: string
|
||||
cooperation_days?: number | null
|
||||
}
|
||||
summary: {
|
||||
|
||||
@@ -26,22 +26,29 @@ export interface TeacherStudentRelationListResult {
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface TeacherStudentRelationUserOption {
|
||||
id: string
|
||||
name: string
|
||||
phone: string
|
||||
}
|
||||
|
||||
export interface TeacherStudentRelationPayload {
|
||||
teacher_name?: string
|
||||
teacher_phone?: string
|
||||
student_name?: string
|
||||
student_phone?: string
|
||||
relation_type?: string
|
||||
status?: 0 | 1
|
||||
}
|
||||
|
||||
export interface CreateTeacherStudentRelationPayload extends TeacherStudentRelationPayload {
|
||||
teacher_name: string
|
||||
teacher_phone: string
|
||||
student_name: string
|
||||
student_phone: string
|
||||
}
|
||||
|
||||
export interface UpdateTeacherStudentRelationPayload extends TeacherStudentRelationPayload {
|
||||
teacher_phone: string
|
||||
student_phone: string
|
||||
}
|
||||
export interface UpdateTeacherStudentRelationPayload extends TeacherStudentRelationPayload {}
|
||||
|
||||
export interface CreateTeacherStudentRelationParams {
|
||||
token: string
|
||||
@@ -188,6 +195,60 @@ function getResults(data: unknown): unknown[] {
|
||||
return []
|
||||
}
|
||||
|
||||
function getOptionResults(data: unknown, keys: string[]): unknown[] {
|
||||
if (Array.isArray(data)) {
|
||||
return data
|
||||
}
|
||||
if (!data || typeof data !== 'object') {
|
||||
return []
|
||||
}
|
||||
|
||||
const record = data as Record<string, unknown>
|
||||
for (const key of keys) {
|
||||
const value = record[key]
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
if (record.data && typeof record.data === 'object') {
|
||||
return getOptionResults(record.data, keys)
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
function normalizeRelationUserOption(item: unknown): TeacherStudentRelationUserOption | null {
|
||||
if (typeof item === 'string' || typeof item === 'number') {
|
||||
const phone = String(item)
|
||||
return { id: phone, name: '', phone }
|
||||
}
|
||||
if (!item || typeof item !== 'object') {
|
||||
return null
|
||||
}
|
||||
|
||||
const record = item as Record<string, unknown>
|
||||
const phone = getString(record, [
|
||||
'phone',
|
||||
'mobile',
|
||||
'username',
|
||||
'account',
|
||||
'teacher_phone',
|
||||
'teacherPhone',
|
||||
'student_phone',
|
||||
'studentPhone',
|
||||
'value'
|
||||
])
|
||||
if (!phone) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
id: getString(record, ['id', 'user_id', 'userId', 'teacher_id', 'teacherId', 'student_id', 'studentId'], phone),
|
||||
name: getString(record, ['real_name', 'realName', 'name', 'label', 'nickname'], ''),
|
||||
phone
|
||||
}
|
||||
}
|
||||
|
||||
function createRelationQuery(params: Partial<TeacherStudentRelationListParams>, includePage = true) {
|
||||
const query = new URLSearchParams()
|
||||
if (params.teacher?.trim()) {
|
||||
@@ -209,6 +270,34 @@ function createRelationQuery(params: Partial<TeacherStudentRelationListParams>,
|
||||
return query
|
||||
}
|
||||
|
||||
async function fetchRelationUserOptions(url: string, token: string, listKeys: string[], fallbackMessage: string) {
|
||||
const response = await fetch(url, {
|
||||
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) || fallbackMessage
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
const seen = new Set<string>()
|
||||
return getOptionResults(data, listKeys)
|
||||
.map(normalizeRelationUserOption)
|
||||
.filter((item): item is TeacherStudentRelationUserOption => {
|
||||
if (!item || seen.has(item.phone)) {
|
||||
return false
|
||||
}
|
||||
seen.add(item.phone)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
async function parseMutationResponse(response: Response, fallbackMessage: string): Promise<unknown> {
|
||||
const text = await response.text()
|
||||
const data = parseResponseText(text)
|
||||
@@ -282,6 +371,24 @@ export async function fetchTeacherStudentRelations(
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchTeacherStudentRelationDoctors(token: string): Promise<TeacherStudentRelationUserOption[]> {
|
||||
return fetchRelationUserOptions(
|
||||
'/server/api/cms/teacher-student-relations/doctors/',
|
||||
token,
|
||||
['results', 'list', 'rows', 'items', 'records', 'doctors', 'teachers'],
|
||||
'获取带教医生列表失败'
|
||||
)
|
||||
}
|
||||
|
||||
export async function fetchTeacherStudentRelationStudents(token: string): Promise<TeacherStudentRelationUserOption[]> {
|
||||
return fetchRelationUserOptions(
|
||||
'/server/api/cms/teacher-student-relations/students/',
|
||||
token,
|
||||
['results', 'list', 'rows', 'items', 'records', 'students'],
|
||||
'获取学生列表失败'
|
||||
)
|
||||
}
|
||||
|
||||
export async function createTeacherStudentRelation(params: CreateTeacherStudentRelationParams): Promise<unknown> {
|
||||
const response = await fetch('/server/api/cms/teacher-student-relations/', {
|
||||
method: 'POST',
|
||||
|
||||
@@ -78,7 +78,13 @@ export const roleMenus: Record<RoleKey, MenuSection[]> = {
|
||||
},
|
||||
],
|
||||
'hospital-admin': [
|
||||
{ title: '概览', items: [{ page: 'hospital-dashboard', icon: OfficeBuilding, title: '医院驾驶舱' }] },
|
||||
{
|
||||
title: '概览',
|
||||
items: [
|
||||
{ page: 'hospital-dashboard', icon: OfficeBuilding, title: '医院驾驶舱' },
|
||||
{ page: 'hospital-settings', icon: Setting, title: '本院配置' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '人员管理',
|
||||
items: [
|
||||
|
||||
@@ -40,6 +40,7 @@ const routes: RouteRecordRaw[] = [
|
||||
{ path: 'module/dept-analysis', name: 'HospitalDeptAnalysis', component: () => import('@/views/HospitalStatsModuleView.vue'), meta: { title: '科室分析' } },
|
||||
{ path: 'module/ability-trend', name: 'HospitalAbilityTrend', component: () => import('@/views/HospitalStatsModuleView.vue'), meta: { title: '能力趋势' } },
|
||||
{ path: 'module/student-ranking', name: 'HospitalStudentRanking', component: () => import('@/views/HospitalStatsModuleView.vue'), meta: { title: '学生排行' } },
|
||||
{ path: 'module/hospital-settings', name: 'HospitalSettings', component: () => import('@/views/HospitalSettingsView.vue'), meta: { title: '本院配置' } },
|
||||
{ path: 'module/content-dashboard', name: 'ContentDashboard', component: () => import('@/views/ContentDashboardView.vue'), meta: { title: '内容概览' } },
|
||||
{ path: 'module/content-stats', name: 'ContentStats', component: () => import('@/views/ContentStatsView.vue'), meta: { title: '内容统计' } },
|
||||
{ path: 'module/teacher-dashboard', name: 'TeacherDashboard', component: () => import('@/views/TeacherDashboardView.vue'), meta: { title: '教学概览' } },
|
||||
|
||||
@@ -0,0 +1,415 @@
|
||||
<template>
|
||||
<div v-loading="loading" class="page-stack">
|
||||
<section class="page-toolbar">
|
||||
<div>
|
||||
<h1>本院配置</h1>
|
||||
<p>维护本院基础资料和展示背景图。</p>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<el-button :icon="Refresh" :loading="loading" @click="loadHospitalProfile">刷新</el-button>
|
||||
<el-button :loading="saving" type="primary" @click="submitHospitalProfile">保存配置</el-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="data-section hospital-settings-section">
|
||||
<div class="hospital-settings-preview">
|
||||
<div class="hospital-banner-preview">
|
||||
<el-image v-if="hospitalBannerPreview" :src="hospitalBannerPreview" fit="contain" />
|
||||
<span v-else>暂无背景图</span>
|
||||
</div>
|
||||
<div>
|
||||
<h2>{{ profileForm.name || '本院名称' }}</h2>
|
||||
<p>{{ profileSubtitle }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form ref="profileFormRef" class="hospital-settings-form" :model="profileForm" :rules="profileRules" label-position="top">
|
||||
<el-row :gutter="14">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="医院名称" prop="name">
|
||||
<el-input v-model="profileForm.name" placeholder="请输入医院名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="医院级别" prop="level">
|
||||
<el-input v-model="profileForm.level" placeholder="例如:三甲、二甲" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="省" prop="province">
|
||||
<el-input v-model="profileForm.province" placeholder="请输入省份" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="市" prop="city">
|
||||
<el-input v-model="profileForm.city" placeholder="请输入城市" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="背景图" prop="bannerUrl">
|
||||
<div class="hospital-banner-field">
|
||||
<el-input v-model="profileForm.bannerUrl" clearable placeholder="完整 http(s) URL 或静态相对路径" @input="clearLocalBannerPreview" />
|
||||
<div class="hospital-banner-actions">
|
||||
<el-upload
|
||||
ref="bannerUploadRef"
|
||||
v-model:file-list="bannerFileList"
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
:on-change="handleBannerFileChange"
|
||||
:on-exceed="handleBannerExceed"
|
||||
:on-remove="handleBannerFileRemove"
|
||||
accept="image/png,image/jpeg,image/jpg,image/webp"
|
||||
>
|
||||
<el-button :disabled="!institutionId" :icon="Upload">选择图片</el-button>
|
||||
</el-upload>
|
||||
<el-button :icon="Delete" @click="clearHospitalBanner">清空背景图</el-button>
|
||||
</div>
|
||||
<p class="hospital-banner-tip">选择图片后保存配置,会先上传背景图,再更新本院信息;支持 png/jpg/jpeg/webp,最大 5MB。</p>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules, UploadInstance, UploadProps, UploadRawFile, UploadUserFile } from 'element-plus'
|
||||
import { Delete, Refresh, Upload } from '@element-plus/icons-vue'
|
||||
import { fetchHospitalOverview } from '@/api/stats'
|
||||
import { updateInstitution, uploadInstitutionBanner, type InstitutionPayload } from '@/api/institutions'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
|
||||
interface HospitalProfileSnapshot {
|
||||
name: string
|
||||
level: string
|
||||
province: string
|
||||
city: string
|
||||
bannerUrl: string
|
||||
}
|
||||
|
||||
const appStore = useAppStore()
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const institutionId = ref<number | null>(null)
|
||||
const profileFormRef = ref<FormInstance>()
|
||||
const bannerUploadRef = ref<UploadInstance>()
|
||||
const bannerFileList = ref<UploadUserFile[]>([])
|
||||
const bannerFile = ref<File | null>(null)
|
||||
const bannerPreviewUrl = ref('')
|
||||
const originalProfile = ref<HospitalProfileSnapshot>({
|
||||
name: '',
|
||||
level: '',
|
||||
province: '',
|
||||
city: '',
|
||||
bannerUrl: ''
|
||||
})
|
||||
const profileForm = reactive({
|
||||
name: '',
|
||||
level: '',
|
||||
province: '',
|
||||
city: '',
|
||||
bannerUrl: ''
|
||||
})
|
||||
|
||||
const profileRules: FormRules = {
|
||||
name: [{ required: true, message: '请输入医院名称', trigger: 'blur' }]
|
||||
}
|
||||
const hospitalBannerPreview = computed(() => bannerPreviewUrl.value || profileForm.bannerUrl.trim())
|
||||
const profileSubtitle = computed(() => {
|
||||
const region = [profileForm.province, profileForm.city].filter(Boolean).join(' / ')
|
||||
return [profileForm.level || '未设置级别', region || '未设置地区'].join(' | ')
|
||||
})
|
||||
|
||||
function getProfileBannerUrl(profile: Record<string, unknown>) {
|
||||
const value = profile.banner_url || profile.bannerUrl || profile.logo
|
||||
return typeof value === 'string' ? value : ''
|
||||
}
|
||||
|
||||
function getProfileString(profile: Record<string, unknown>, key: string) {
|
||||
const value = profile[key]
|
||||
if (typeof value === 'string') {
|
||||
return value
|
||||
}
|
||||
if (typeof value === 'number') {
|
||||
return String(value)
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
function getInstitutionId(profile: Record<string, unknown>) {
|
||||
const id = profile.institution_id || profile.institutionId || profile.id
|
||||
return typeof id === 'number' ? id : Number(id) || null
|
||||
}
|
||||
|
||||
function applyProfile(profile: Record<string, unknown>) {
|
||||
institutionId.value = getInstitutionId(profile)
|
||||
profileForm.name = getProfileString(profile, 'name')
|
||||
profileForm.level = getProfileString(profile, 'level')
|
||||
profileForm.province = getProfileString(profile, 'province')
|
||||
profileForm.city = getProfileString(profile, 'city')
|
||||
profileForm.bannerUrl = getProfileBannerUrl(profile)
|
||||
originalProfile.value = {
|
||||
name: profileForm.name,
|
||||
level: profileForm.level,
|
||||
province: profileForm.province,
|
||||
city: profileForm.city,
|
||||
bannerUrl: profileForm.bannerUrl
|
||||
}
|
||||
}
|
||||
|
||||
async function loadHospitalProfile() {
|
||||
if (!appStore.token) {
|
||||
ElMessage.warning('缺少登录信息,请重新登录')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
const overview = await fetchHospitalOverview(appStore.token)
|
||||
applyProfile((overview.profile || {}) as Record<string, unknown>)
|
||||
clearPendingBanner()
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '获取本院配置失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function revokeBannerPreview() {
|
||||
if (bannerPreviewUrl.value.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(bannerPreviewUrl.value)
|
||||
}
|
||||
}
|
||||
|
||||
function clearLocalBannerPreview() {
|
||||
revokeBannerPreview()
|
||||
bannerPreviewUrl.value = ''
|
||||
bannerFile.value = null
|
||||
bannerFileList.value = []
|
||||
bannerUploadRef.value?.clearFiles()
|
||||
}
|
||||
|
||||
function clearPendingBanner() {
|
||||
revokeBannerPreview()
|
||||
bannerPreviewUrl.value = ''
|
||||
bannerFile.value = null
|
||||
bannerFileList.value = []
|
||||
bannerUploadRef.value?.clearFiles()
|
||||
}
|
||||
|
||||
function setHospitalBannerFile(file: UploadRawFile) {
|
||||
const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/webp']
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
clearPendingBanner()
|
||||
ElMessage.warning('请选择 png、jpg、jpeg 或 webp 图片')
|
||||
return
|
||||
}
|
||||
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
clearPendingBanner()
|
||||
ElMessage.warning('图片大小不能超过 5MB')
|
||||
return
|
||||
}
|
||||
|
||||
revokeBannerPreview()
|
||||
profileForm.bannerUrl = ''
|
||||
bannerFile.value = file
|
||||
bannerPreviewUrl.value = URL.createObjectURL(file)
|
||||
}
|
||||
|
||||
const handleBannerFileChange: UploadProps['onChange'] = uploadFile => {
|
||||
if (!uploadFile.raw) {
|
||||
return
|
||||
}
|
||||
|
||||
setHospitalBannerFile(uploadFile.raw)
|
||||
}
|
||||
|
||||
const handleBannerFileRemove: UploadProps['onRemove'] = () => {
|
||||
clearPendingBanner()
|
||||
}
|
||||
|
||||
const handleBannerExceed: UploadProps['onExceed'] = files => {
|
||||
const file = files[0] as UploadRawFile | undefined
|
||||
if (!file) {
|
||||
return
|
||||
}
|
||||
|
||||
bannerUploadRef.value?.clearFiles()
|
||||
bannerUploadRef.value?.handleStart(file)
|
||||
}
|
||||
|
||||
function clearHospitalBanner() {
|
||||
clearPendingBanner()
|
||||
profileForm.bannerUrl = ''
|
||||
}
|
||||
|
||||
function buildUpdatePayload(): InstitutionPayload {
|
||||
const current = {
|
||||
name: profileForm.name.trim(),
|
||||
level: profileForm.level.trim(),
|
||||
province: profileForm.province.trim(),
|
||||
city: profileForm.city.trim(),
|
||||
bannerUrl: profileForm.bannerUrl.trim()
|
||||
}
|
||||
const original = originalProfile.value
|
||||
const payload: InstitutionPayload = {}
|
||||
|
||||
if (current.name !== original.name) payload.name = current.name
|
||||
if (current.level !== original.level) payload.level = current.level
|
||||
if (current.province !== original.province) payload.province = current.province
|
||||
if (current.city !== original.city) payload.city = current.city
|
||||
if (current.bannerUrl !== original.bannerUrl) payload.banner_url = current.bannerUrl
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
async function uploadPendingBanner() {
|
||||
if (!bannerFile.value || !institutionId.value || !appStore.token) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const result = await uploadInstitutionBanner({
|
||||
token: appStore.token,
|
||||
id: institutionId.value,
|
||||
file: bannerFile.value
|
||||
})
|
||||
if (!result.bannerUrl) {
|
||||
throw new Error('上传背景图失败,未返回图片地址')
|
||||
}
|
||||
|
||||
return result.bannerUrl
|
||||
}
|
||||
|
||||
async function submitHospitalProfile() {
|
||||
if (!appStore.token) {
|
||||
ElMessage.warning('缺少登录信息,请重新登录')
|
||||
return
|
||||
}
|
||||
if (!institutionId.value) {
|
||||
ElMessage.warning('缺少本院机构 ID,无法保存配置')
|
||||
return
|
||||
}
|
||||
|
||||
const isValid = await profileFormRef.value?.validate().catch(() => false)
|
||||
if (!isValid) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
saving.value = true
|
||||
const uploadedBannerUrl = await uploadPendingBanner()
|
||||
if (uploadedBannerUrl) {
|
||||
profileForm.bannerUrl = uploadedBannerUrl
|
||||
clearPendingBanner()
|
||||
}
|
||||
|
||||
const payload = buildUpdatePayload()
|
||||
if (!Object.keys(payload).length) {
|
||||
ElMessage.warning('没有需要保存的修改')
|
||||
return
|
||||
}
|
||||
|
||||
await updateInstitution({
|
||||
token: appStore.token,
|
||||
id: institutionId.value,
|
||||
payload
|
||||
})
|
||||
ElMessage.success('本院配置已更新')
|
||||
await loadHospitalProfile()
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '保存本院配置失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadHospitalProfile)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.hospital-settings-section {
|
||||
display: grid;
|
||||
grid-template-columns: 320px minmax(0, 1fr);
|
||||
gap: 22px;
|
||||
}
|
||||
|
||||
.hospital-settings-preview {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 14px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 8px;
|
||||
color: var(--muted);
|
||||
}
|
||||
}
|
||||
|
||||
.hospital-banner-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
padding: 12px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
background: var(--panel-soft);
|
||||
|
||||
.el-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(img) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.hospital-settings-form {
|
||||
min-width: 0;
|
||||
|
||||
.el-select,
|
||||
.el-input-number {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.hospital-banner-field {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hospital-banner-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
:deep(.el-upload-list) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.hospital-banner-tip {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
@@ -46,6 +46,12 @@
|
||||
{{ formatRegion(row.province, row.city) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="背景图" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-image v-if="row.bannerUrl" class="institution-banner-thumb" :src="row.bannerUrl" fit="contain" />
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="更新时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.updatedAt) }}
|
||||
@@ -105,6 +111,35 @@
|
||||
<el-input v-model="institutionForm.city" placeholder="请输入城市" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="机构背景图" prop="bannerUrl">
|
||||
<div class="institution-banner-field">
|
||||
<div class="institution-banner-preview">
|
||||
<el-image v-if="institutionBannerPreview" :src="institutionBannerPreview" fit="contain" />
|
||||
<span v-else>暂无背景图</span>
|
||||
</div>
|
||||
<div class="institution-banner-control">
|
||||
<el-input v-model="institutionForm.bannerUrl" clearable placeholder="完整 http(s) URL 或静态相对路径" @input="clearLocalBannerPreview" />
|
||||
<div class="institution-banner-actions">
|
||||
<el-upload
|
||||
ref="bannerUploadRef"
|
||||
v-model:file-list="bannerFileList"
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
:on-change="handleBannerFileChange"
|
||||
:on-exceed="handleBannerExceed"
|
||||
:on-remove="handleBannerFileRemove"
|
||||
accept="image/*"
|
||||
>
|
||||
<el-button :icon="Upload">选择图片</el-button>
|
||||
</el-upload>
|
||||
<el-button :icon="Delete" @click="clearInstitutionBanner">清空背景图</el-button>
|
||||
</div>
|
||||
<p class="institution-banner-tip">编辑时选择图片会先上传背景图,再保存机构信息;支持 png/jpg/jpeg/webp,最大 5MB。</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -143,7 +178,7 @@
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules, UploadInstance, UploadProps, UploadRawFile, UploadUserFile } from 'element-plus'
|
||||
import { Download, Plus, Refresh, Search, Upload, UploadFilled } from '@element-plus/icons-vue'
|
||||
import { Delete, Download, Plus, Refresh, Search, Upload, UploadFilled } from '@element-plus/icons-vue'
|
||||
import {
|
||||
createInstitution,
|
||||
disableInstitution,
|
||||
@@ -152,6 +187,7 @@ import {
|
||||
fetchInstitutions,
|
||||
importInstitutions,
|
||||
updateInstitution,
|
||||
uploadInstitutionBanner,
|
||||
type CreateInstitutionPayload,
|
||||
type InstitutionListItem,
|
||||
type InstitutionPayload
|
||||
@@ -168,10 +204,14 @@ const institutionDialogVisible = ref(false)
|
||||
const importDialogVisible = ref(false)
|
||||
const institutionFormRef = ref<FormInstance>()
|
||||
const importUploadRef = ref<UploadInstance>()
|
||||
const bannerUploadRef = ref<UploadInstance>()
|
||||
const institutionMode = ref<'create' | 'edit'>('create')
|
||||
const editingInstitution = ref<InstitutionListItem | null>(null)
|
||||
const importFile = ref<File | null>(null)
|
||||
const importFileList = ref<UploadUserFile[]>([])
|
||||
const bannerFileList = ref<UploadUserFile[]>([])
|
||||
const bannerPreviewUrl = ref('')
|
||||
const bannerFile = ref<File | null>(null)
|
||||
const institutions = ref<InstitutionListItem[]>([])
|
||||
const filters = reactive({
|
||||
search: '',
|
||||
@@ -189,7 +229,8 @@ const institutionForm = reactive({
|
||||
type: 'hospital',
|
||||
level: '',
|
||||
province: '',
|
||||
city: ''
|
||||
city: '',
|
||||
bannerUrl: ''
|
||||
})
|
||||
|
||||
const institutionTypeOptions = [
|
||||
@@ -209,6 +250,7 @@ const institutionRules = computed<FormRules>(() => {
|
||||
}
|
||||
})
|
||||
const institutionDialogTitle = computed(() => (institutionMode.value === 'create' ? '新增机构' : '编辑机构'))
|
||||
const institutionBannerPreview = computed(() => bannerPreviewUrl.value || institutionForm.bannerUrl.trim())
|
||||
|
||||
async function loadInstitutions() {
|
||||
if (!appStore.token) {
|
||||
@@ -263,22 +305,41 @@ function openEditDialog(row: InstitutionListItem) {
|
||||
institutionForm.level = row.level
|
||||
institutionForm.province = row.province
|
||||
institutionForm.city = row.city
|
||||
institutionForm.bannerUrl = row.bannerUrl
|
||||
bannerPreviewUrl.value = ''
|
||||
institutionDialogVisible.value = true
|
||||
}
|
||||
|
||||
function revokeBannerPreview() {
|
||||
if (bannerPreviewUrl.value.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(bannerPreviewUrl.value)
|
||||
}
|
||||
}
|
||||
|
||||
function clearLocalBannerPreview() {
|
||||
revokeBannerPreview()
|
||||
bannerPreviewUrl.value = ''
|
||||
}
|
||||
|
||||
function resetInstitutionForm() {
|
||||
revokeBannerPreview()
|
||||
institutionForm.code = ''
|
||||
institutionForm.name = ''
|
||||
institutionForm.type = 'hospital'
|
||||
institutionForm.level = ''
|
||||
institutionForm.province = ''
|
||||
institutionForm.city = ''
|
||||
institutionForm.bannerUrl = ''
|
||||
bannerPreviewUrl.value = ''
|
||||
bannerFile.value = null
|
||||
bannerFileList.value = []
|
||||
bannerUploadRef.value?.clearFiles()
|
||||
editingInstitution.value = null
|
||||
institutionFormRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
function buildCreatePayload(): CreateInstitutionPayload {
|
||||
return {
|
||||
const payload: CreateInstitutionPayload = {
|
||||
code: institutionForm.code.trim(),
|
||||
name: institutionForm.name.trim(),
|
||||
type: institutionForm.type.trim() || 'hospital',
|
||||
@@ -286,6 +347,12 @@ function buildCreatePayload(): CreateInstitutionPayload {
|
||||
province: institutionForm.province.trim(),
|
||||
city: institutionForm.city.trim()
|
||||
}
|
||||
const bannerUrl = institutionForm.bannerUrl.trim()
|
||||
if (bannerUrl) {
|
||||
payload.banner_url = bannerUrl
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
function buildUpdatePayload(): InstitutionPayload {
|
||||
@@ -299,7 +366,8 @@ function buildUpdatePayload(): InstitutionPayload {
|
||||
type: institutionForm.type.trim(),
|
||||
level: institutionForm.level.trim(),
|
||||
province: institutionForm.province.trim(),
|
||||
city: institutionForm.city.trim()
|
||||
city: institutionForm.city.trim(),
|
||||
bannerUrl: institutionForm.bannerUrl.trim()
|
||||
}
|
||||
const original = editingInstitution.value
|
||||
const payload: InstitutionPayload = {}
|
||||
@@ -310,10 +378,93 @@ function buildUpdatePayload(): InstitutionPayload {
|
||||
if (current.level !== original.level) payload.level = current.level
|
||||
if (current.province !== original.province) payload.province = current.province
|
||||
if (current.city !== original.city) payload.city = current.city
|
||||
if (current.bannerUrl !== original.bannerUrl) payload.banner_url = current.bannerUrl
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
function setInstitutionBannerFile(file: UploadRawFile) {
|
||||
if (institutionMode.value === 'create') {
|
||||
bannerFileList.value = []
|
||||
bannerFile.value = null
|
||||
bannerUploadRef.value?.clearFiles()
|
||||
ElMessage.warning('请先新增机构,保存后编辑时再上传背景图')
|
||||
return
|
||||
}
|
||||
|
||||
const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/webp']
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
bannerFileList.value = []
|
||||
bannerFile.value = null
|
||||
bannerUploadRef.value?.clearFiles()
|
||||
ElMessage.warning('请选择 png、jpg、jpeg 或 webp 图片')
|
||||
return
|
||||
}
|
||||
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
bannerFileList.value = []
|
||||
bannerFile.value = null
|
||||
bannerUploadRef.value?.clearFiles()
|
||||
ElMessage.warning('图片大小不能超过 5MB')
|
||||
return
|
||||
}
|
||||
|
||||
revokeBannerPreview()
|
||||
institutionForm.bannerUrl = ''
|
||||
bannerFile.value = file
|
||||
bannerPreviewUrl.value = URL.createObjectURL(file)
|
||||
}
|
||||
|
||||
const handleBannerFileChange: UploadProps['onChange'] = uploadFile => {
|
||||
if (!uploadFile.raw) {
|
||||
return
|
||||
}
|
||||
|
||||
setInstitutionBannerFile(uploadFile.raw)
|
||||
}
|
||||
|
||||
const handleBannerFileRemove: UploadProps['onRemove'] = () => {
|
||||
bannerFileList.value = []
|
||||
bannerFile.value = null
|
||||
clearLocalBannerPreview()
|
||||
}
|
||||
|
||||
const handleBannerExceed: UploadProps['onExceed'] = files => {
|
||||
const file = files[0] as UploadRawFile | undefined
|
||||
if (!file) {
|
||||
return
|
||||
}
|
||||
|
||||
bannerUploadRef.value?.clearFiles()
|
||||
bannerUploadRef.value?.handleStart(file)
|
||||
}
|
||||
|
||||
function clearInstitutionBanner() {
|
||||
revokeBannerPreview()
|
||||
institutionForm.bannerUrl = ''
|
||||
bannerPreviewUrl.value = ''
|
||||
bannerFile.value = null
|
||||
bannerFileList.value = []
|
||||
bannerUploadRef.value?.clearFiles()
|
||||
}
|
||||
|
||||
async function uploadPendingBanner() {
|
||||
if (!bannerFile.value || !editingInstitution.value || !appStore.token) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const result = await uploadInstitutionBanner({
|
||||
token: appStore.token,
|
||||
id: editingInstitution.value.id,
|
||||
file: bannerFile.value
|
||||
})
|
||||
if (!result.bannerUrl) {
|
||||
throw new Error('上传背景图失败,未返回图片地址')
|
||||
}
|
||||
|
||||
return result.bannerUrl
|
||||
}
|
||||
|
||||
async function submitInstitutionForm() {
|
||||
if (!appStore.token) {
|
||||
ElMessage.warning('缺少登录信息,请重新登录')
|
||||
@@ -335,6 +486,13 @@ async function submitInstitutionForm() {
|
||||
ElMessage.success('机构已新增')
|
||||
pagination.page = 1
|
||||
} else if (editingInstitution.value) {
|
||||
const uploadedBannerUrl = await uploadPendingBanner()
|
||||
if (uploadedBannerUrl) {
|
||||
institutionForm.bannerUrl = uploadedBannerUrl
|
||||
bannerFile.value = null
|
||||
bannerFileList.value = []
|
||||
clearLocalBannerPreview()
|
||||
}
|
||||
const payload = buildUpdatePayload()
|
||||
if (!Object.keys(payload).length) {
|
||||
ElMessage.warning('没有需要保存的修改')
|
||||
@@ -503,3 +661,76 @@ function formatDateTime(value: string) {
|
||||
|
||||
onMounted(loadInstitutions)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.institution-banner-thumb {
|
||||
width: 72px;
|
||||
height: 40px;
|
||||
padding: 3px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--panel-soft);
|
||||
|
||||
:deep(img) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.institution-banner-field {
|
||||
display: grid;
|
||||
grid-template-columns: 180px minmax(0, 1fr);
|
||||
gap: 14px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.institution-banner-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 180px;
|
||||
height: 104px;
|
||||
padding: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
background: var(--panel-soft);
|
||||
|
||||
.el-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(img) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.institution-banner-control {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.institution-banner-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
:deep(.el-upload-list) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.institution-banner-tip {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -78,13 +78,37 @@
|
||||
<el-form ref="relationFormRef" :model="relationForm" :rules="relationRules" label-position="top">
|
||||
<el-row :gutter="14">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="带教医生手机号" prop="teacher_phone">
|
||||
<el-input v-model="relationForm.teacher_phone" maxlength="11" placeholder="请输入医生手机号" />
|
||||
<el-form-item label="带教医生" prop="teacher_phone">
|
||||
<el-select
|
||||
v-model="relationForm.teacher_phone"
|
||||
:loading="loadingDoctorOptions"
|
||||
placeholder="请选择带教医生"
|
||||
@change="handleTeacherChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in doctorOptions"
|
||||
:key="item.phone"
|
||||
:label="relationUserOptionLabel(item)"
|
||||
:value="item.phone"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="学生手机号" prop="student_phone">
|
||||
<el-input v-model="relationForm.student_phone" maxlength="11" placeholder="请输入学生手机号" />
|
||||
<el-form-item label="学生" prop="student_phone">
|
||||
<el-select
|
||||
v-model="relationForm.student_phone"
|
||||
:loading="loadingStudentOptions"
|
||||
placeholder="请选择学生"
|
||||
@change="handleStudentChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in studentOptions"
|
||||
:key="item.phone"
|
||||
:label="relationUserOptionLabel(item)"
|
||||
:value="item.phone"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
@@ -141,11 +165,14 @@ import {
|
||||
disableTeacherStudentRelation,
|
||||
downloadTeacherStudentRelationImportTemplate,
|
||||
exportTeacherStudentRelations,
|
||||
fetchTeacherStudentRelationDoctors,
|
||||
fetchTeacherStudentRelationStudents,
|
||||
fetchTeacherStudentRelations,
|
||||
importTeacherStudentRelations,
|
||||
updateTeacherStudentRelation,
|
||||
type CreateTeacherStudentRelationPayload,
|
||||
type TeacherStudentRelationItem,
|
||||
type TeacherStudentRelationUserOption,
|
||||
type UpdateTeacherStudentRelationPayload
|
||||
} from '@/api/teacherStudentRelations'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
@@ -156,6 +183,8 @@ const savingRelation = ref(false)
|
||||
const importing = ref(false)
|
||||
const exporting = ref(false)
|
||||
const downloadingTemplate = ref(false)
|
||||
const loadingDoctorOptions = ref(false)
|
||||
const loadingStudentOptions = ref(false)
|
||||
const relationDialogVisible = ref(false)
|
||||
const importDialogVisible = ref(false)
|
||||
const relationFormRef = ref<FormInstance>()
|
||||
@@ -165,6 +194,8 @@ const editingRelation = ref<TeacherStudentRelationItem | null>(null)
|
||||
const importFile = ref<File | null>(null)
|
||||
const importFileList = ref<UploadUserFile[]>([])
|
||||
const relations = ref<TeacherStudentRelationItem[]>([])
|
||||
const doctorOptions = ref<TeacherStudentRelationUserOption[]>([])
|
||||
const studentOptions = ref<TeacherStudentRelationUserOption[]>([])
|
||||
const filters = reactive({
|
||||
teacher: '',
|
||||
student: '',
|
||||
@@ -176,7 +207,9 @@ const pagination = reactive({
|
||||
total: 0
|
||||
})
|
||||
const relationForm = reactive({
|
||||
teacher_name: '',
|
||||
teacher_phone: '',
|
||||
student_name: '',
|
||||
student_phone: '',
|
||||
relation_type: '指导',
|
||||
status: 1 as 0 | 1
|
||||
@@ -184,12 +217,12 @@ const relationForm = reactive({
|
||||
|
||||
const relationRules: FormRules = {
|
||||
teacher_phone: [
|
||||
{ required: true, message: '请输入带教医生手机号', trigger: 'blur' },
|
||||
{ pattern: /^1\d{10}$/, message: '请输入正确的带教医生手机号', trigger: 'blur' }
|
||||
{ required: true, message: '请选择带教医生', trigger: 'change' },
|
||||
{ pattern: /^1\d{10}$/, message: '请选择正确的带教医生手机号', trigger: 'change' }
|
||||
],
|
||||
student_phone: [
|
||||
{ required: true, message: '请输入学生手机号', trigger: 'blur' },
|
||||
{ pattern: /^1\d{10}$/, message: '请输入正确的学生手机号', trigger: 'blur' }
|
||||
{ required: true, message: '请选择学生', trigger: 'change' },
|
||||
{ pattern: /^1\d{10}$/, message: '请选择正确的学生手机号', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
const relationDialogTitle = computed(() => (relationMode.value === 'create' ? '新增师生关系' : '编辑师生关系'))
|
||||
@@ -236,20 +269,27 @@ function resetFilters() {
|
||||
function openCreateDialog() {
|
||||
relationMode.value = 'create'
|
||||
relationDialogVisible.value = true
|
||||
loadRelationUserOptions()
|
||||
}
|
||||
|
||||
function openEditDialog(row: TeacherStudentRelationItem) {
|
||||
relationMode.value = 'edit'
|
||||
editingRelation.value = row
|
||||
relationForm.teacher_name = row.teacherName
|
||||
relationForm.teacher_phone = row.teacherPhone
|
||||
relationForm.student_name = row.studentName
|
||||
relationForm.student_phone = row.studentPhone
|
||||
relationForm.relation_type = row.relationType
|
||||
relationForm.status = row.status
|
||||
relationDialogVisible.value = true
|
||||
ensureCurrentRelationOptions(row)
|
||||
loadRelationUserOptions(true)
|
||||
}
|
||||
|
||||
function resetRelationForm() {
|
||||
relationForm.teacher_name = ''
|
||||
relationForm.teacher_phone = ''
|
||||
relationForm.student_name = ''
|
||||
relationForm.student_phone = ''
|
||||
relationForm.relation_type = '指导'
|
||||
relationForm.status = 1
|
||||
@@ -259,22 +299,162 @@ function resetRelationForm() {
|
||||
|
||||
function buildCreatePayload(): CreateTeacherStudentRelationPayload {
|
||||
return {
|
||||
teacher_name: relationForm.teacher_name.trim(),
|
||||
teacher_phone: relationForm.teacher_phone.trim(),
|
||||
student_name: relationForm.student_name.trim(),
|
||||
student_phone: relationForm.student_phone.trim(),
|
||||
relation_type: relationForm.relation_type.trim(),
|
||||
status: relationForm.status
|
||||
}
|
||||
}
|
||||
|
||||
function ensureOption(options: TeacherStudentRelationUserOption[], option: TeacherStudentRelationUserOption) {
|
||||
if (!option.phone) {
|
||||
return
|
||||
}
|
||||
|
||||
const existing = options.find(item => item.phone === option.phone)
|
||||
if (existing) {
|
||||
existing.id = option.id || existing.id
|
||||
existing.name = option.name || existing.name
|
||||
return
|
||||
}
|
||||
|
||||
options.unshift(option)
|
||||
}
|
||||
|
||||
function mergeRelationUserOptions(
|
||||
currentOptions: TeacherStudentRelationUserOption[],
|
||||
incomingOptions: TeacherStudentRelationUserOption[]
|
||||
) {
|
||||
const currentMap = new Map(currentOptions.map(item => [item.phone, item]))
|
||||
const incomingPhones = new Set(incomingOptions.map(item => item.phone))
|
||||
const missingCurrentOptions = currentOptions.filter(item => item.phone && !incomingPhones.has(item.phone))
|
||||
const mergedIncomingOptions = incomingOptions.map(item => {
|
||||
const current = currentMap.get(item.phone)
|
||||
return {
|
||||
id: item.id || current?.id || item.phone,
|
||||
name: item.name || current?.name || '',
|
||||
phone: item.phone
|
||||
}
|
||||
})
|
||||
|
||||
return [...missingCurrentOptions, ...mergedIncomingOptions]
|
||||
}
|
||||
|
||||
function ensureCurrentRelationOptions(row: TeacherStudentRelationItem) {
|
||||
ensureOption(doctorOptions.value, {
|
||||
id: row.teacherId || row.teacherPhone,
|
||||
name: row.teacherName,
|
||||
phone: row.teacherPhone
|
||||
})
|
||||
ensureOption(studentOptions.value, {
|
||||
id: row.studentId || row.studentPhone,
|
||||
name: row.studentName,
|
||||
phone: row.studentPhone
|
||||
})
|
||||
}
|
||||
|
||||
function buildUpdatePayload(): UpdateTeacherStudentRelationPayload {
|
||||
if (!editingRelation.value) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const current = {
|
||||
teacher_name: relationForm.teacher_name.trim(),
|
||||
teacher_phone: relationForm.teacher_phone.trim(),
|
||||
student_name: relationForm.student_name.trim(),
|
||||
student_phone: relationForm.student_phone.trim(),
|
||||
relation_type: relationForm.relation_type.trim(),
|
||||
status: relationForm.status
|
||||
}
|
||||
const original = editingRelation.value
|
||||
const payload: UpdateTeacherStudentRelationPayload = {}
|
||||
|
||||
return current
|
||||
if (current.teacher_phone !== original.teacherPhone || current.teacher_name !== original.teacherName) {
|
||||
payload.teacher_name = current.teacher_name
|
||||
payload.teacher_phone = current.teacher_phone
|
||||
}
|
||||
if (current.student_phone !== original.studentPhone || current.student_name !== original.studentName) {
|
||||
payload.student_name = current.student_name
|
||||
payload.student_phone = current.student_phone
|
||||
}
|
||||
if (current.relation_type !== original.relationType) payload.relation_type = current.relation_type
|
||||
if (current.status !== original.status) payload.status = current.status
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
function handleTeacherChange(phone: string) {
|
||||
const option = doctorOptions.value.find(item => item.phone === phone)
|
||||
relationForm.teacher_phone = phone
|
||||
relationForm.teacher_name = option?.name || relationForm.teacher_name
|
||||
relationFormRef.value?.validateField('teacher_phone')
|
||||
}
|
||||
|
||||
function handleStudentChange(phone: string) {
|
||||
const option = studentOptions.value.find(item => item.phone === phone)
|
||||
relationForm.student_phone = phone
|
||||
relationForm.student_name = option?.name || relationForm.student_name
|
||||
relationFormRef.value?.validateField('student_phone')
|
||||
}
|
||||
|
||||
function syncSelectedRelationUsers() {
|
||||
const teacherOption = doctorOptions.value.find(item => item.phone === relationForm.teacher_phone)
|
||||
const studentOption = studentOptions.value.find(item => item.phone === relationForm.student_phone)
|
||||
if (teacherOption?.name) {
|
||||
relationForm.teacher_name = teacherOption.name
|
||||
}
|
||||
if (studentOption?.name) {
|
||||
relationForm.student_name = studentOption.name
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRelationUserOptions(force = false) {
|
||||
if (!appStore.token) {
|
||||
ElMessage.warning('缺少登录信息,请重新登录')
|
||||
return
|
||||
}
|
||||
|
||||
const tasks: Promise<void>[] = []
|
||||
if (force || !doctorOptions.value.length) {
|
||||
loadingDoctorOptions.value = true
|
||||
tasks.push(
|
||||
fetchTeacherStudentRelationDoctors(appStore.token)
|
||||
.then(options => {
|
||||
doctorOptions.value = mergeRelationUserOptions(doctorOptions.value, options)
|
||||
syncSelectedRelationUsers()
|
||||
})
|
||||
.catch(error => {
|
||||
ElMessage.error(error instanceof Error ? error.message : '获取带教医生列表失败')
|
||||
})
|
||||
.finally(() => {
|
||||
loadingDoctorOptions.value = false
|
||||
})
|
||||
)
|
||||
}
|
||||
if (force || !studentOptions.value.length) {
|
||||
loadingStudentOptions.value = true
|
||||
tasks.push(
|
||||
fetchTeacherStudentRelationStudents(appStore.token)
|
||||
.then(options => {
|
||||
studentOptions.value = mergeRelationUserOptions(studentOptions.value, options)
|
||||
syncSelectedRelationUsers()
|
||||
})
|
||||
.catch(error => {
|
||||
ElMessage.error(error instanceof Error ? error.message : '获取学生列表失败')
|
||||
})
|
||||
.finally(() => {
|
||||
loadingStudentOptions.value = false
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
await Promise.all(tasks)
|
||||
}
|
||||
|
||||
function relationUserOptionLabel(item: TeacherStudentRelationUserOption) {
|
||||
return [item.name, item.phone].filter(Boolean).join(' / ')
|
||||
}
|
||||
|
||||
async function submitRelationForm() {
|
||||
@@ -291,6 +471,10 @@ async function submitRelationForm() {
|
||||
try {
|
||||
savingRelation.value = true
|
||||
if (relationMode.value === 'create') {
|
||||
if (!relationForm.teacher_name.trim() || !relationForm.student_name.trim()) {
|
||||
ElMessage.warning('请选择包含姓名和手机号的带教医生与学生')
|
||||
return
|
||||
}
|
||||
await createTeacherStudentRelation({
|
||||
token: appStore.token,
|
||||
payload: buildCreatePayload()
|
||||
@@ -298,7 +482,15 @@ async function submitRelationForm() {
|
||||
ElMessage.success('师生关系已新增')
|
||||
pagination.page = 1
|
||||
} else if (editingRelation.value) {
|
||||
if (!relationForm.teacher_name.trim() || !relationForm.student_name.trim()) {
|
||||
ElMessage.warning('请选择包含姓名和手机号的带教医生与学生')
|
||||
return
|
||||
}
|
||||
const payload = buildUpdatePayload()
|
||||
if (!Object.keys(payload).length) {
|
||||
ElMessage.warning('没有需要保存的修改')
|
||||
return
|
||||
}
|
||||
await updateTeacherStudentRelation({
|
||||
token: appStore.token,
|
||||
id: editingRelation.value.id,
|
||||
@@ -447,3 +639,9 @@ function formatDateTime(value: string) {
|
||||
|
||||
onMounted(loadRelations)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.el-dialog .el-select) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user