diff --git a/src/api/auth.ts b/src/api/auth.ts index a85c1218..82232394 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -8,6 +8,7 @@ export interface LoginPayload { export interface LoginResult { token: string + roleType: string raw: unknown } @@ -22,7 +23,15 @@ function getTokenFromResponse(data: unknown): string { return directToken } - const nested = record.data + const tokens = record.tokens + if (tokens && typeof tokens === 'object') { + const accessToken = (tokens as Record).access + if (typeof accessToken === 'string') { + return accessToken + } + } + + const nested = record.data || record.result || record.payload if (nested && typeof nested === 'object') { return getTokenFromResponse(nested) } @@ -30,6 +39,29 @@ function getTokenFromResponse(data: unknown): string { return '' } +function getRoleTypeFromResponse(data: unknown): string { + if (!data || typeof data !== 'object') { + return '' + } + + const record = data as Record + const roleType = record.role_type || record.roleType + if (typeof roleType === 'string') { + return roleType + } + + if (typeof roleType === 'number') { + return String(roleType) + } + + const nested = record.user || record.profile || record.data || record.result || record.payload + if (nested && typeof nested === 'object') { + return getRoleTypeFromResponse(nested) + } + + return '' +} + function getMessageFromResponse(data: unknown): string { if (!data || typeof data !== 'object') { return '' @@ -75,6 +107,7 @@ export async function login(payload: LoginPayload): Promise { return { token: getTokenFromResponse(data), + roleType: getRoleTypeFromResponse(data), raw: data } } diff --git a/src/api/departments.ts b/src/api/departments.ts new file mode 100644 index 00000000..56abc9e9 --- /dev/null +++ b/src/api/departments.ts @@ -0,0 +1,311 @@ +export interface DepartmentListParams { + token: string + search?: string + category?: string + page?: number +} + +export interface DepartmentListItem { + id: number + name: string + category: string + createdAt: string + updatedAt: string +} + +export interface DepartmentListResult { + departments: DepartmentListItem[] + total: number +} + +export interface DepartmentPayload { + name?: string + category?: string +} + +export interface CreateDepartmentPayload extends DepartmentPayload { + name: string +} + +export interface CreateDepartmentParams { + token: string + payload: CreateDepartmentPayload +} + +export interface UpdateDepartmentParams { + token: string + id: number + payload: DepartmentPayload +} + +export interface DisableDepartmentParams { + token: string + id: number +} + +export interface ImportDepartmentsParams { + token: string + file: File +} + +export interface ExportDepartmentsParams extends DepartmentListParams {} + +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 getString(record: Record, key: string): string { + const value = record[key] + if (typeof value === 'string') { + return value + } + if (typeof value === 'number') { + return String(value) + } + + return '' +} + +function normalizeDepartment(item: unknown): DepartmentListItem { + const record = item && typeof item === 'object' ? (item as Record) : {} + const id = record.id + + return { + id: typeof id === 'number' ? id : Number(id) || 0, + name: getString(record, 'name'), + category: getString(record, 'category'), + createdAt: getString(record, 'created_at'), + updatedAt: getString(record, 'updated_at') + } +} + +function getTotal(data: unknown, fallback: number): number { + if (!data || typeof data !== 'object') { + return fallback + } + + const record = data as Record + const total = record.count || record.total + if (typeof total === 'number') { + return total + } + if (typeof total === 'string' && Number.isFinite(Number(total))) { + return Number(total) + } + + return fallback +} + +function getResults(data: unknown): unknown[] { + if (Array.isArray(data)) { + return data + } + if (!data || typeof data !== 'object') { + return [] + } + + const record = data as Record + if (Array.isArray(record.results)) { + return record.results + } + if (Array.isArray(record.data)) { + return record.data + } + if (record.data && typeof record.data === 'object') { + return getResults(record.data) + } + + return [] +} + +function createDepartmentQuery(params: Partial, includePage = true) { + const query = new URLSearchParams() + if (params.search?.trim()) { + query.set('search', params.search.trim()) + } + if (params.category?.trim()) { + query.set('category', params.category.trim()) + } + if (includePage && 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 +} + +function getFilenameFromDisposition(disposition: string | null, fallback: string) { + if (!disposition) { + return fallback + } + + const utf8Match = disposition.match(/filename\*=UTF-8''([^;]+)/i) + if (utf8Match?.[1]) { + return decodeURIComponent(utf8Match[1]) + } + + const filenameMatch = disposition.match(/filename="?([^"]+)"?/i) + return filenameMatch?.[1] || fallback +} + +async function downloadResponse(response: Response, fallbackFilename: string, fallbackMessage: string) { + if (!response.ok) { + const text = await response.text() + const data = parseResponseText(text) + const message = getMessageFromResponse(data) || fallbackMessage + throw new Error(message) + } + + const blob = await response.blob() + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = getFilenameFromDisposition(response.headers.get('content-disposition'), fallbackFilename) + document.body.appendChild(link) + link.click() + link.remove() + URL.revokeObjectURL(url) +} + +export async function fetchDepartments(params: DepartmentListParams): Promise { + const query = createDepartmentQuery(params) + const url = `/server/api/cms/departments/${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 results = getResults(data) + + return { + departments: results.map(normalizeDepartment), + total: getTotal(data, results.length) + } +} + +export async function createDepartment(params: CreateDepartmentParams): Promise { + const response = await fetch('/server/api/cms/departments/', { + 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 updateDepartment(params: UpdateDepartmentParams): Promise { + const response = await fetch(`/server/api/cms/departments/${params.id}/update/`, { + 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 disableDepartment(params: DisableDepartmentParams): Promise { + const response = await fetch(`/server/api/cms/departments/${params.id}/disable/`, { + method: 'POST', + headers: { + Accept: 'application/json', + Authorization: createAuthorization(params.token) + } + }) + + return parseMutationResponse(response, '停用科室失败') +} + +export async function importDepartments(params: ImportDepartmentsParams): Promise { + const formData = new FormData() + formData.append('file', params.file) + + const response = await fetch('/server/api/cms/departments/import/', { + method: 'POST', + headers: { + Accept: 'application/json', + Authorization: createAuthorization(params.token) + }, + body: formData + }) + + return parseMutationResponse(response, '导入科室失败') +} + +export async function exportDepartments(params: ExportDepartmentsParams): Promise { + const query = createDepartmentQuery(params, false) + const url = `/server/api/cms/departments/export/${query.toString() ? `?${query.toString()}` : ''}` + const response = await fetch(url, { + method: 'GET', + headers: { + Authorization: createAuthorization(params.token) + } + }) + + await downloadResponse(response, 'departments.xlsx', '导出科室失败') +} + +export async function downloadDepartmentImportTemplate(token: string): Promise { + const response = await fetch('/server/api/cms/departments/import-template/', { + method: 'GET', + headers: { + Authorization: createAuthorization(token) + } + }) + + await downloadResponse(response, 'departments-import-template.xlsx', '下载导入模板失败') +} diff --git a/src/api/institutions.ts b/src/api/institutions.ts new file mode 100644 index 00000000..2c0bc6c3 --- /dev/null +++ b/src/api/institutions.ts @@ -0,0 +1,332 @@ +export interface InstitutionListParams { + token: string + search?: string + type?: string + province?: string + city?: string + page?: number +} + +export interface InstitutionListItem { + id: number + code: string + name: string + type: string + level: string + province: string + city: string + bannerUrl: string + createdAt: string + updatedAt: string +} + +export interface InstitutionListResult { + institutions: InstitutionListItem[] + total: number +} + +export interface InstitutionPayload { + code?: string + name?: string + type?: string + level?: string + province?: string + city?: string +} + +export interface CreateInstitutionPayload extends InstitutionPayload { + code: string + name: string +} + +export interface InstitutionMutationParams { + token: string + payload: InstitutionPayload +} + +export interface UpdateInstitutionParams extends InstitutionMutationParams { + id: number +} + +export interface DisableInstitutionParams { + token: string + id: number +} + +export interface ImportInstitutionsParams { + token: string + file: File +} + +export interface ExportInstitutionsParams extends InstitutionListParams {} + +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 getString(record: Record, key: string): string { + const value = record[key] + if (typeof value === 'string') { + return value + } + if (typeof value === 'number') { + return String(value) + } + + return '' +} + +function normalizeInstitution(item: unknown): InstitutionListItem { + const record = item && typeof item === 'object' ? (item as Record) : {} + const id = record.id + + return { + id: typeof id === 'number' ? id : Number(id) || 0, + code: getString(record, 'code'), + name: getString(record, 'name'), + type: getString(record, 'type'), + level: getString(record, 'level'), + province: getString(record, 'province'), + city: getString(record, 'city'), + bannerUrl: getString(record, 'banner_url'), + createdAt: getString(record, 'created_at'), + updatedAt: getString(record, 'updated_at') + } +} + +function getTotal(data: unknown, fallback: number): number { + if (!data || typeof data !== 'object') { + return fallback + } + + const record = data as Record + const total = record.count || record.total + if (typeof total === 'number') { + return total + } + if (typeof total === 'string' && Number.isFinite(Number(total))) { + return Number(total) + } + + return fallback +} + +function getResults(data: unknown): unknown[] { + if (Array.isArray(data)) { + return data + } + if (!data || typeof data !== 'object') { + return [] + } + + const record = data as Record + if (Array.isArray(record.results)) { + return record.results + } + if (Array.isArray(record.data)) { + return record.data + } + if (record.data && typeof record.data === 'object') { + return getResults(record.data) + } + + return [] +} + +function createInstitutionQuery(params: Partial) { + const query = new URLSearchParams() + if (params.search?.trim()) { + query.set('search', params.search.trim()) + } + if (params.type) { + query.set('type', params.type) + } + if (params.province?.trim()) { + query.set('province', params.province.trim()) + } + if (params.city?.trim()) { + query.set('city', params.city.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 +} + +function getFilenameFromDisposition(disposition: string | null, fallback: string) { + if (!disposition) { + return fallback + } + + const utf8Match = disposition.match(/filename\*=UTF-8''([^;]+)/i) + if (utf8Match?.[1]) { + return decodeURIComponent(utf8Match[1]) + } + + const filenameMatch = disposition.match(/filename="?([^"]+)"?/i) + return filenameMatch?.[1] || fallback +} + +async function downloadResponse(response: Response, fallbackFilename: string, fallbackMessage: string) { + if (!response.ok) { + const text = await response.text() + const data = parseResponseText(text) + const message = getMessageFromResponse(data) || fallbackMessage + throw new Error(message) + } + + const blob = await response.blob() + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = getFilenameFromDisposition(response.headers.get('content-disposition'), fallbackFilename) + document.body.appendChild(link) + link.click() + link.remove() + URL.revokeObjectURL(url) +} + +export async function fetchInstitutions(params: InstitutionListParams): Promise { + const query = createInstitutionQuery(params) + const url = `/server/api/cms/institutions/${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 results = getResults(data) + + return { + institutions: results.map(normalizeInstitution), + total: getTotal(data, results.length) + } +} + +export async function createInstitution(params: InstitutionMutationParams): Promise { + const response = await fetch('/server/api/cms/institutions/', { + 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 updateInstitution(params: UpdateInstitutionParams): Promise { + const response = await fetch(`/server/api/cms/institutions/${params.id}/update/`, { + 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 disableInstitution(params: DisableInstitutionParams): Promise { + const response = await fetch(`/server/api/cms/institutions/${params.id}/disable/`, { + method: 'POST', + headers: { + Accept: 'application/json', + Authorization: createAuthorization(params.token) + } + }) + + return parseMutationResponse(response, '停用机构失败') +} + +export async function importInstitutions(params: ImportInstitutionsParams): Promise { + const formData = new FormData() + formData.append('file', params.file) + + const response = await fetch('/server/api/cms/institutions/import/', { + method: 'POST', + headers: { + Accept: 'application/json', + Authorization: createAuthorization(params.token) + }, + body: formData + }) + + return parseMutationResponse(response, '导入机构失败') +} + +export async function exportInstitutions(params: ExportInstitutionsParams): Promise { + const query = createInstitutionQuery(params) + const url = `/server/api/cms/institutions/export/${query.toString() ? `?${query.toString()}` : ''}` + const response = await fetch(url, { + method: 'GET', + headers: { + Authorization: createAuthorization(params.token) + } + }) + + await downloadResponse(response, 'institutions.xlsx', '导出机构失败') +} + +export async function downloadInstitutionImportTemplate(token: string): Promise { + const response = await fetch('/server/api/cms/institutions/import-template/', { + method: 'GET', + headers: { + Authorization: createAuthorization(token) + } + }) + + await downloadResponse(response, 'institutions-import-template.xlsx', '下载导入模板失败') +} diff --git a/src/api/teacherStudentRelations.ts b/src/api/teacherStudentRelations.ts new file mode 100644 index 00000000..5cf920b0 --- /dev/null +++ b/src/api/teacherStudentRelations.ts @@ -0,0 +1,358 @@ +export interface TeacherStudentRelationListParams { + token: string + teacher?: string + student?: string + status?: string + search?: string + page?: number +} + +export interface TeacherStudentRelationItem { + id: number + teacherId: string + teacherName: string + teacherPhone: string + studentId: string + studentName: string + studentPhone: string + relationType: string + status: 0 | 1 + createdAt: string + updatedAt: string +} + +export interface TeacherStudentRelationListResult { + relations: TeacherStudentRelationItem[] + total: number +} + +export interface TeacherStudentRelationPayload { + teacher?: number + student?: number + relation_type?: string + status?: 0 | 1 +} + +export interface CreateTeacherStudentRelationPayload extends TeacherStudentRelationPayload { + teacher: number + student: number +} + +export interface CreateTeacherStudentRelationParams { + token: string + payload: CreateTeacherStudentRelationPayload +} + +export interface UpdateTeacherStudentRelationParams { + token: string + id: number + payload: TeacherStudentRelationPayload +} + +export interface DisableTeacherStudentRelationParams { + token: string + id: number +} + +export interface ImportTeacherStudentRelationsParams { + token: string + file: File +} + +export interface ExportTeacherStudentRelationsParams extends TeacherStudentRelationListParams {} + +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 getString(record: Record, keys: string[], fallback = ''): string { + for (const key of keys) { + const value = record[key] + if (typeof value === 'string') { + return value + } + if (typeof value === 'number') { + return String(value) + } + } + + return fallback +} + +function getRelatedUser(value: unknown, fallbackRecord: Record, prefix: 'teacher' | 'student') { + if (value && typeof value === 'object') { + const record = value as Record + return { + id: getString(record, ['id', 'user_id', 'userId']), + name: getString(record, ['real_name', 'realName', 'name', 'username']), + phone: getString(record, ['phone', 'mobile']) + } + } + + return { + id: typeof value === 'number' || typeof value === 'string' ? String(value) : getString(fallbackRecord, [`${prefix}_id`, `${prefix}Id`]), + name: getString(fallbackRecord, [`${prefix}_name`, `${prefix}Name`, `${prefix}_real_name`, `${prefix}RealName`]), + phone: getString(fallbackRecord, [`${prefix}_phone`, `${prefix}Phone`]) + } +} + +function normalizeRelation(item: unknown): TeacherStudentRelationItem { + const record = item && typeof item === 'object' ? (item as Record) : {} + const id = record.id + const teacher = getRelatedUser(record.teacher, record, 'teacher') + const student = getRelatedUser(record.student, record, 'student') + const status = record.status === 0 || record.status === '0' ? 0 : 1 + + return { + id: typeof id === 'number' ? id : Number(id) || 0, + teacherId: teacher.id, + teacherName: teacher.name, + teacherPhone: teacher.phone, + studentId: student.id, + studentName: student.name, + studentPhone: student.phone, + relationType: getString(record, ['relation_type', 'relationType', 'type']), + status, + createdAt: getString(record, ['created_at', 'createdAt']), + updatedAt: getString(record, ['updated_at', 'updatedAt']) + } +} + +function getTotal(data: unknown, fallback: number): number { + if (!data || typeof data !== 'object') { + return fallback + } + + const record = data as Record + const total = record.count || record.total + if (typeof total === 'number') { + return total + } + if (typeof total === 'string' && Number.isFinite(Number(total))) { + return Number(total) + } + + return fallback +} + +function getResults(data: unknown): unknown[] { + if (Array.isArray(data)) { + return data + } + if (!data || typeof data !== 'object') { + return [] + } + + const record = data as Record + if (Array.isArray(record.results)) { + return record.results + } + if (Array.isArray(record.data)) { + return record.data + } + if (record.data && typeof record.data === 'object') { + return getResults(record.data) + } + + return [] +} + +function createRelationQuery(params: Partial, includePage = true) { + const query = new URLSearchParams() + if (params.teacher?.trim()) { + query.set('teacher', params.teacher.trim()) + } + if (params.student?.trim()) { + query.set('student', params.student.trim()) + } + if (params.status) { + query.set('status', params.status) + } + if (params.search?.trim()) { + query.set('search', params.search.trim()) + } + if (includePage && 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 +} + +function getFilenameFromDisposition(disposition: string | null, fallback: string) { + if (!disposition) { + return fallback + } + + const utf8Match = disposition.match(/filename\*=UTF-8''([^;]+)/i) + if (utf8Match?.[1]) { + return decodeURIComponent(utf8Match[1]) + } + + const filenameMatch = disposition.match(/filename="?([^"]+)"?/i) + return filenameMatch?.[1] || fallback +} + +async function downloadResponse(response: Response, fallbackFilename: string, fallbackMessage: string) { + if (!response.ok) { + const text = await response.text() + const data = parseResponseText(text) + const message = getMessageFromResponse(data) || fallbackMessage + throw new Error(message) + } + + const blob = await response.blob() + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = getFilenameFromDisposition(response.headers.get('content-disposition'), fallbackFilename) + document.body.appendChild(link) + link.click() + link.remove() + URL.revokeObjectURL(url) +} + +export async function fetchTeacherStudentRelations( + params: TeacherStudentRelationListParams +): Promise { + const query = createRelationQuery(params) + const url = `/server/api/cms/teacher-student-relations/${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 results = getResults(data) + + return { + relations: results.map(normalizeRelation), + total: getTotal(data, results.length) + } +} + +export async function createTeacherStudentRelation(params: CreateTeacherStudentRelationParams): Promise { + const response = await fetch('/server/api/cms/teacher-student-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 updateTeacherStudentRelation(params: UpdateTeacherStudentRelationParams): Promise { + const response = await fetch(`/server/api/cms/teacher-student-relations/${params.id}/update/`, { + 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 disableTeacherStudentRelation(params: DisableTeacherStudentRelationParams): Promise { + const response = await fetch(`/server/api/cms/teacher-student-relations/${params.id}/disable/`, { + method: 'POST', + headers: { + Accept: 'application/json', + Authorization: createAuthorization(params.token) + } + }) + + return parseMutationResponse(response, '停用师生关系失败') +} + +export async function importTeacherStudentRelations(params: ImportTeacherStudentRelationsParams): Promise { + const formData = new FormData() + formData.append('file', params.file) + + const response = await fetch('/server/api/cms/teacher-student-relations/import/', { + method: 'POST', + headers: { + Accept: 'application/json', + Authorization: createAuthorization(params.token) + }, + body: formData + }) + + return parseMutationResponse(response, '导入师生关系失败') +} + +export async function exportTeacherStudentRelations(params: ExportTeacherStudentRelationsParams): Promise { + const query = createRelationQuery(params, false) + const url = `/server/api/cms/teacher-student-relations/export/${query.toString() ? `?${query.toString()}` : ''}` + const response = await fetch(url, { + method: 'GET', + headers: { + Authorization: createAuthorization(params.token) + } + }) + + await downloadResponse(response, 'teacher-student-relations.xlsx', '导出师生关系失败') +} + +export async function downloadTeacherStudentRelationImportTemplate(token: string): Promise { + const response = await fetch('/server/api/cms/teacher-student-relations/import-template/', { + method: 'GET', + headers: { + Authorization: createAuthorization(token) + } + }) + + await downloadResponse(response, 'teacher-student-relations-import-template.xlsx', '下载导入模板失败') +} diff --git a/src/api/teacherStudents.ts b/src/api/teacherStudents.ts new file mode 100644 index 00000000..10e65ea9 --- /dev/null +++ b/src/api/teacherStudents.ts @@ -0,0 +1,214 @@ +export interface TeacherStudentListParams { + token: string + search?: string + gender?: string + status?: string + page?: number +} + +export interface TeacherStudentItem { + id: string + username: string + realName: string + phone: string + gender: 0 | 1 | 2 + status: 0 | 1 + major: string + trainingStage: string + institutionName: string + createdAt: string + updatedAt: string +} + +export interface TeacherStudentListResult { + students: TeacherStudentItem[] + total: number +} + +const listKeys = ['results', 'list', 'rows', 'items', 'records', 'students'] +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 getString(record: Record, keys: string[], fallback = ''): string { + for (const key of keys) { + const value = record[key] + if (typeof value === 'string') { + return value + } + if (typeof value === 'number') { + return String(value) + } + } + + return fallback +} + +function getGender(record: Record): 0 | 1 | 2 { + const value = record.gender + if (value === 1 || value === '1') return 1 + if (value === 2 || value === '2') return 2 + return 0 +} + +function getStatus(record: Record): 0 | 1 { + const value = record.status + if (value === 0 || value === '0' || value === false || value === '禁用') { + return 0 + } + + return 1 +} + +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 findStudentPayload(data: unknown): { items: unknown[]; total: number } { + if (Array.isArray(data)) { + return { items: data, total: data.length } + } + if (!data || typeof data !== 'object') { + return { items: [], total: 0 } + } + + const record = data as Record + 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 = findStudentPayload(record.data) + return { + items: nested.items, + total: nested.total || getTotal(record, nested.items.length) + } + } + + return { items: [], total: getTotal(record, 0) } +} + +function normalizeStudent(item: unknown): TeacherStudentItem { + const record = item && typeof item === 'object' ? (item as Record) : {} + + return { + id: getString(record, ['id', 'user_id', 'userId']), + username: getString(record, ['username', 'account']), + realName: getString(record, ['real_name', 'realName', 'name', 'nickname']), + phone: getString(record, ['phone', 'mobile']), + gender: getGender(record), + status: getStatus(record), + major: getString(record, ['major']), + trainingStage: getString(record, ['training_stage', 'trainingStage']), + institutionName: getString(record, ['institution_name', 'institutionName', 'hospital_name', 'hospitalName']), + createdAt: getString(record, ['created_at', 'createdAt']), + updatedAt: getString(record, ['updated_at', 'updatedAt']) + } +} + +function createStudentQuery(params: Partial) { + const query = new URLSearchParams() + if (params.search?.trim()) { + query.set('search', params.search.trim()) + } + if (params.gender) { + query.set('gender', params.gender) + } + if (params.status) { + query.set('status', params.status) + } + if (params.page) { + query.set('page', String(params.page)) + } + + return query +} + +export async function fetchTeacherStudents(params: TeacherStudentListParams): Promise { + const query = createStudentQuery(params) + const response = await fetch(`/server/api/cms/students/${query.toString() ? `?${query.toString()}` : ''}`, { + 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 = findStudentPayload(data) + + return { + students: payload.items.map(normalizeStudent), + total: payload.total + } +} + +export async function fetchTeacherStudentDetail(token: string, id: string | number): Promise { + const response = await fetch(`/server/api/cms/students/${id}/`, { + 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 record = data && typeof data === 'object' && 'data' in data ? (data as Record).data : data + return normalizeStudent(record) +} diff --git a/src/api/users.ts b/src/api/users.ts new file mode 100644 index 00000000..5a1b8363 --- /dev/null +++ b/src/api/users.ts @@ -0,0 +1,432 @@ +export interface UserListParams { + token: string + roleType: string + page: number + size?: number + search?: string + institution?: string + status?: string + gender?: string +} + +export interface UserListItem { + id: string + name: string + account: string + role: string + roleType: string + org: string + lastLogin: string + enabled: boolean + phone: string + realName: string + gender: 0 | 1 | 2 + titleName: string + major: string + trainingStage: string + status: 0 | 1 + institutionId: string +} + +export interface UserListResult { + users: UserListItem[] + total: number +} + +export interface CreateUserPayload { + phone: string + real_name: string + role_type: string + institution?: number + gender?: 0 | 1 | 2 + title_name?: string + major?: string + training_stage?: string + status?: 0 | 1 +} + +export interface UpdateUserPayload { + real_name?: string + phone?: string + role_type?: string + institution?: number + gender?: 0 | 1 | 2 + title_name?: string + major?: string + training_stage?: string + status?: 0 | 1 +} + +export interface CreateUserParams { + token: string + payload: CreateUserPayload +} + +export interface UpdateUserParams { + token: string + id: string | number + payload: UpdateUserPayload +} + +export interface DisableUserParams { + token: string + id: string | number +} + +export interface ResetUserPasswordParams { + token: string + id: string | number + password?: string +} + +export interface ImportUsersParams { + token: string + file: File +} + +export interface ExportUsersParams extends UserListParams {} + +const listKeys = ['results', 'list', 'rows', 'items', 'records', 'users'] +const totalKeys = ['count', 'total', 'total_count', 'totalCount'] + +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 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 getBoolean(record: Record, keys: string[], fallback = true): boolean { + for (const key of keys) { + const value = record[key] + if (typeof value === 'boolean') { + return value + } + if (typeof value === 'number') { + return value === 1 + } + if (typeof value === 'string') { + return !['0', 'false', 'disabled', 'inactive', '禁用', '停用', '冻结'].includes(value.toLowerCase()) + } + } + + return fallback +} + +function getGender(record: Record): 0 | 1 | 2 { + const value = record.gender + if (value === 1 || value === '1') return 1 + if (value === 2 || value === '2') return 2 + return 0 +} + +function getStatus(record: Record): 0 | 1 { + const value = record.status + if (value === 0 || value === '0' || value === false || value === '禁用') { + return 0 + } + + return 1 +} + +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 findUserPayload(data: unknown): { items: unknown[]; total: number } { + if (Array.isArray(data)) { + return { items: data, total: data.length } + } + + if (!data || typeof data !== 'object') { + return { items: [], total: 0 } + } + + const record = data as Record + + 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 = findUserPayload(record.data) + return { + items: nested.items, + total: nested.total || getTotal(record, nested.items.length) + } + } + + return { items: [], total: getTotal(record, 0) } +} + +function normalizeUser(item: unknown, index: number): UserListItem { + const record = item && typeof item === 'object' ? (item as Record) : {} + const name = getString(record, ['name', 'real_name', 'realName', 'nickname', 'username'], `用户${index + 1}`) + const roleType = getString(record, ['role_type', 'roleType', 'role'], '') + const status = getStatus(record) + + return { + id: getString(record, ['id', 'uuid', 'user_id', 'userId'], `${index}`), + name, + account: getString(record, ['account', 'username', 'phone', 'mobile', 'email'], name), + role: getString(record, ['role_name', 'roleName', 'role', 'role_type', 'roleType']), + roleType, + org: getString(record, ['org', 'organization', 'organization_name', 'organizationName', 'hospital', 'hospital_name', 'hospitalName']), + lastLogin: getString(record, ['last_login', 'lastLogin', 'login_time', 'loginTime']), + enabled: getBoolean(record, ['enabled', 'is_active', 'isActive', 'status']), + phone: getString(record, ['phone', 'mobile', 'account', 'username'], ''), + realName: getString(record, ['real_name', 'realName', 'name', 'nickname'], name), + gender: getGender(record), + titleName: getString(record, ['title_name', 'titleName']), + major: getString(record, ['major']), + trainingStage: getString(record, ['training_stage', 'trainingStage']), + status, + institutionId: getString(record, ['institution', 'institution_id', 'institutionId']) + } +} + +function createAuthorization(token: string) { + return /^Bearer\s+/i.test(token) ? token : `Bearer ${token}` +} + +function createUserQuery(params: Partial, includePage = true) { + const query = new URLSearchParams() + if (params.roleType) { + query.set('role_type', params.roleType) + } + if (params.search?.trim()) { + query.set('search', params.search.trim()) + } + if (params.institution?.trim()) { + query.set('institution', params.institution.trim()) + } + if (params.status) { + query.set('status', params.status) + } + if (params.gender) { + query.set('gender', params.gender) + } + if (includePage && params.page) { + query.set('page', String(params.page)) + } + if (includePage && params.size) { + query.set('size', String(params.size)) + } + + 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 +} + +function getFilenameFromDisposition(disposition: string | null, fallback: string) { + if (!disposition) { + return fallback + } + + const utf8Match = disposition.match(/filename\*=UTF-8''([^;]+)/i) + if (utf8Match?.[1]) { + return decodeURIComponent(utf8Match[1]) + } + + const filenameMatch = disposition.match(/filename="?([^"]+)"?/i) + return filenameMatch?.[1] || fallback +} + +async function downloadResponse(response: Response, fallbackFilename: string, fallbackMessage: string) { + if (!response.ok) { + const text = await response.text() + const data = parseResponseText(text) + const message = getMessageFromResponse(data) || fallbackMessage + throw new Error(message) + } + + const blob = await response.blob() + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = getFilenameFromDisposition(response.headers.get('content-disposition'), fallbackFilename) + document.body.appendChild(link) + link.click() + link.remove() + URL.revokeObjectURL(url) +} + +export async function fetchUsers(params: UserListParams): Promise { + const query = createUserQuery(params) + const authorization = createAuthorization(params.token) + + const response = await fetch(`/server/api/cms/users/?${query.toString()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: authorization + } + }) + const text = await response.text() + const data = parseResponseText(text) + + if (!response.ok) { + const message = getMessageFromResponse(data) || '获取用户列表失败' + throw new Error(message) + } + + const payload = findUserPayload(data) + + return { + users: payload.items.map(normalizeUser), + total: payload.total + } +} + +export async function createUser(params: CreateUserParams): Promise { + const response = await fetch('/server/api/cms/users/', { + 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 updateUser(params: UpdateUserParams): Promise { + const response = await fetch(`/server/api/cms/users/${params.id}/update/`, { + 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 disableUser(params: DisableUserParams): Promise { + const response = await fetch(`/server/api/cms/users/${params.id}/disable/`, { + method: 'POST', + headers: { + Accept: 'application/json', + Authorization: createAuthorization(params.token) + } + }) + + return parseMutationResponse(response, '停用用户失败') +} + +export async function resetUserPassword(params: ResetUserPasswordParams): Promise { + const payload = params.password ? { password: params.password } : {} + const response = await fetch(`/server/api/cms/users/${params.id}/reset-password/`, { + method: 'POST', + headers: { + Accept: 'application/json', + Authorization: createAuthorization(params.token), + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload) + }) + + return parseMutationResponse(response, '重置密码失败') +} + +export async function importUsers(params: ImportUsersParams): Promise { + const formData = new FormData() + formData.append('file', params.file) + + const response = await fetch('/server/api/cms/users/import/', { + method: 'POST', + headers: { + Accept: 'application/json', + Authorization: createAuthorization(params.token) + }, + body: formData + }) + return parseMutationResponse(response, '导入用户失败') +} + +export async function exportUsers(params: ExportUsersParams): Promise { + const query = createUserQuery(params, false) + const url = `/server/api/cms/users/export/${query.toString() ? `?${query.toString()}` : ''}` + const response = await fetch(url, { + method: 'GET', + headers: { + Authorization: createAuthorization(params.token) + } + }) + + await downloadResponse(response, 'users.xlsx', '导出用户失败') +} + +export async function downloadUserImportTemplate(token: string): Promise { + const response = await fetch('/server/api/cms/users/import-template/', { + method: 'GET', + headers: { + Authorization: createAuthorization(token) + } + }) + + await downloadResponse(response, 'users-import-template.xlsx', '下载导入模板失败') +} diff --git a/src/assets/styles/main.scss b/src/assets/styles/main.scss index 53edaa33..bd8e0267 100644 --- a/src/assets/styles/main.scss +++ b/src/assets/styles/main.scss @@ -544,6 +544,21 @@ p { padding: 18px; } +.table-pagination { + display: flex; + justify-content: flex-end; + padding-top: 16px; +} + +.department-form, +.institution-form, +.user-create-form { + .el-select, + .el-input-number { + width: 100%; + } +} + .section-header { display: flex; align-items: center; @@ -588,6 +603,29 @@ p { gap: 12px; } +.admin-user-filter { + grid-template-columns: minmax(220px, 1fr) 150px 150px 130px 130px 96px 96px; +} + +.institution-filter { + grid-template-columns: minmax(240px, 1fr) 150px 150px 150px 96px 96px; +} + +.role-user-filter { + grid-template-columns: minmax(260px, 1fr) 150px 150px 96px 96px; +} + +.relation-filter { + grid-template-columns: minmax(240px, 1fr) 140px 140px 130px 96px 96px; +} + +.table-subtext { + display: block; + margin-top: 4px; + color: var(--muted); + font-size: 12px; +} + .kanban-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); diff --git a/src/mock/navigation.ts b/src/mock/navigation.ts index ac2484bf..0fbdba4a 100644 --- a/src/mock/navigation.ts +++ b/src/mock/navigation.ts @@ -151,7 +151,22 @@ export const pageTitles = Object.values(roleMenus).reduce return acc }, {}) +const directPagePaths: Record = { + 'content-admin-list': '/users/content-admins', + 'department-list': '/departments', + 'doctor-list': '/users/doctors', + 'hospital-list': '/institutions', + 'my-students': '/my-students', + 'student-list': '/users/students', + 'teacher-list': '/teacher-student-relations', + 'user-list': '/users' +} + export function getPagePath(page: string) { + if (directPagePaths[page]) { + return directPagePaths[page] + } + return page === 'dashboard' ? '/dashboard' : `/module/${page}` } diff --git a/src/router/index.ts b/src/router/index.ts index 47466778..c90a3e4e 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -16,9 +16,23 @@ const routes: RouteRecordRaw[] = [ { path: 'dashboard', name: 'Dashboard', component: () => import('@/views/DashboardView.vue'), meta: { title: '数据驾驶舱' } }, { path: 'cases', name: 'Cases', component: () => import('@/views/CasesView.vue'), meta: { title: '病例中心' } }, { path: 'training', name: 'Training', component: () => import('@/views/TrainingView.vue'), meta: { title: '训练管理' } }, - { path: 'institutions', name: 'Institutions', component: () => import('@/views/InstitutionsView.vue'), meta: { title: '机构管理' } }, - { path: 'users', name: 'Users', component: () => import('@/views/UsersView.vue'), meta: { title: '用户权限' } }, + { 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: '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' } }, + { path: 'my-students', name: 'MyStudents', component: () => import('@/views/MyStudentsView.vue'), meta: { title: '我的学生' } }, + { path: 'teacher-student-relations', name: 'TeacherStudentRelations', component: () => import('@/views/TeacherStudentRelationsView.vue'), meta: { title: '师生关系管理' } }, { path: 'settings', name: 'Settings', component: () => import('@/views/SettingsView.vue'), meta: { title: '系统配置' } }, + { path: 'module/content-admin-list', redirect: '/users/content-admins' }, + { path: 'module/department-list', redirect: '/departments' }, + { path: 'module/doctor-list', redirect: '/users/doctors' }, + { path: 'module/hospital-list', redirect: '/institutions' }, + { path: 'module/my-students', redirect: '/my-students' }, + { path: 'module/student-list', redirect: '/users/students' }, + { path: 'module/teacher-list', redirect: '/teacher-student-relations' }, + { path: 'module/user-list', redirect: '/users' }, { path: 'module/knowledge-base', name: 'KnowledgeBase', component: () => import('@/views/KnowledgeBaseView.vue'), meta: { title: '知识库管理' } }, { path: 'module/:page', name: 'Module', component: () => import('@/views/ModuleView.vue'), meta: { title: '业务模块' } } ] diff --git a/src/stores/app.ts b/src/stores/app.ts index 90eadf48..5fa90286 100644 --- a/src/stores/app.ts +++ b/src/stores/app.ts @@ -23,6 +23,7 @@ function getStoredRole(): RoleKey { export const useAppStore = defineStore('app', () => { const token = ref(localStorage.getItem('mediai-token') || '') + const roleType = ref(localStorage.getItem('mediai-role-type') || '') const collapsed = ref(false) const darkMode = ref(false) const storedRole = getStoredRole() @@ -35,10 +36,12 @@ export const useAppStore = defineStore('app', () => { const roleLabel = computed(() => roleOptions.find(item => item.value === user.value.role)?.label || '管理员') const isLoggedIn = computed(() => Boolean(token.value)) - function login(username: string, role: LoginRole, authToken: string) { + function login(username: string, role: LoginRole, authToken: string, apiRoleType: string) { const appRole = loginRoleMap[role] - token.value = authToken || `session-${username}` + token.value = authToken + roleType.value = apiRoleType || role localStorage.setItem('mediai-token', token.value) + localStorage.setItem('mediai-role-type', roleType.value) localStorage.setItem('mediai-role', appRole) user.value.role = appRole user.value.name = username === 'admin' ? '张管理员' : username @@ -47,7 +50,9 @@ export const useAppStore = defineStore('app', () => { function logout() { token.value = '' + roleType.value = '' localStorage.removeItem('mediai-token') + localStorage.removeItem('mediai-role-type') localStorage.removeItem('mediai-role') } @@ -64,6 +69,7 @@ export const useAppStore = defineStore('app', () => { return { token, + roleType, collapsed, darkMode, user, diff --git a/src/views/DepartmentsView.vue b/src/views/DepartmentsView.vue new file mode 100644 index 00000000..ffb7af6c --- /dev/null +++ b/src/views/DepartmentsView.vue @@ -0,0 +1,403 @@ + + + diff --git a/src/views/InstitutionsView.vue b/src/views/InstitutionsView.vue index 53266058..91e83b44 100644 --- a/src/views/InstitutionsView.vue +++ b/src/views/InstitutionsView.vue @@ -2,33 +2,504 @@
-

机构管理

-

管理医院、医学院、科室组织及账号配额。

+

医院管理

+

维护医院列表,支持按名称、编码、类型和地区查询。

+
+
+ 导入模板 + 导入机构 + 导出机构 + 新增机构
- 新增机构
-
-
-
-
-

{{ item.name }}

- {{ item.type }} -
- {{ item.status }} -
-
-
{{ item.teachers }}带教老师
-
{{ item.students }}学生
-
{{ item.activeRate }}%活跃率
-
- -
+
+ + + + + + + + + 查询 + 重置
+ +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
拖拽文件到此处,或点击选择
+ +
+ +
diff --git a/src/views/LoginView.vue b/src/views/LoginView.vue index 6b763518..4851adc7 100644 --- a/src/views/LoginView.vue +++ b/src/views/LoginView.vue @@ -92,7 +92,10 @@ async function handleLogin() { password: form.password, role: form.role }) - appStore.login(form.account, form.role, result.token) + if (!result.token) { + throw new Error('登录接口未返回访问令牌') + } + appStore.login(form.account, form.role, result.token, result.roleType || form.role) loading.value = false const defaultPath = getPagePath(getFirstPage(appStore.getRoleByLoginRole(form.role))) router.push((route.query.redirect as string) || defaultPath) diff --git a/src/views/MyStudentsView.vue b/src/views/MyStudentsView.vue new file mode 100644 index 00000000..b1822b41 --- /dev/null +++ b/src/views/MyStudentsView.vue @@ -0,0 +1,193 @@ + + + diff --git a/src/views/RoleUsersView.vue b/src/views/RoleUsersView.vue new file mode 100644 index 00000000..ef49be77 --- /dev/null +++ b/src/views/RoleUsersView.vue @@ -0,0 +1,603 @@ + + + diff --git a/src/views/TeacherStudentRelationsView.vue b/src/views/TeacherStudentRelationsView.vue new file mode 100644 index 00000000..c0b5b23b --- /dev/null +++ b/src/views/TeacherStudentRelationsView.vue @@ -0,0 +1,458 @@ + + + diff --git a/src/views/UsersView.vue b/src/views/UsersView.vue index f7d90d18..1620fea3 100644 --- a/src/views/UsersView.vue +++ b/src/views/UsersView.vue @@ -2,46 +2,607 @@
-

用户权限

-

维护管理员、内容运营、带教医生与学生账号。

+

用户列表

+

维护平台用户账号,支持按角色、机构、状态和性别筛选。

- 导入用户 - 新增用户 + 导入模板 + 导入用户 + 导出用户 + 新增用户
+
+ + + + + + + + + + + + + + + 查询 + 重置 +
+
- - - - - - - + + + + - -