1721 lines
63 KiB
Vue
1721 lines
63 KiB
Vue
<template>
|
||
<div class="app-container">
|
||
<!-- 顶部静态布局:标题栏 -->
|
||
<div class="page-topbar">
|
||
<div class="topbar-left">
|
||
<h2 class="topbar-title">教师信息</h2>
|
||
<span class="topbar-sub">在册 {{ total }} 人 </span>
|
||
</div>
|
||
<div class="topbar-actions">
|
||
<el-button icon="el-icon-download" size="small" @click="handleExport">导出教师台账</el-button>
|
||
<el-button type="primary" icon="el-icon-plus" size="small" @click="handleAdd">新增教师</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 顶部静态布局:筛选卡片 -->
|
||
<div class="topbar-filter">
|
||
<el-form v-show="showSearch" ref="queryForm" :model="queryParams" size="small" :inline="true" label-width="68px" class="filter-form">
|
||
<el-form-item label="教师姓名" prop="tName">
|
||
<el-input
|
||
v-model="queryParams.tName"
|
||
placeholder="请输入教师姓名"
|
||
clearable
|
||
@keyup.enter.native="handleQuery"
|
||
/>
|
||
</el-form-item>
|
||
<!-- <el-form-item label="人员编号" prop="personNo">
|
||
<el-input
|
||
v-model="queryParams.personNo"
|
||
placeholder="请输入人员编号"
|
||
clearable
|
||
@keyup.enter.native="handleQuery"
|
||
/>
|
||
</el-form-item>-->
|
||
<el-form-item label="所属单位" prop="deptName">
|
||
<!-- <el-input
|
||
v-model="queryParams.deptName"
|
||
placeholder="请输入所属单位"
|
||
clearable
|
||
@keyup.enter.native="handleQuery"
|
||
/>-->
|
||
<treeselect style="width: 180px;" v-model="queryParams.deptName" :options="deptOptions" :normalizer="normalizer" :show-count="true" placeholder="请选择归属部门" />
|
||
</el-form-item>
|
||
<!-- <el-form-item label="职务职称" prop="jobTitle">
|
||
<el-input
|
||
v-model="queryParams.jobTitle"
|
||
placeholder="请输入职务/职称"
|
||
clearable
|
||
@keyup.enter.native="handleQuery"
|
||
/>
|
||
</el-form-item>-->
|
||
<el-form-item label="专业方向" prop="major">
|
||
<el-input
|
||
v-model="queryParams.major"
|
||
placeholder="请输入专业方向"
|
||
clearable
|
||
@keyup.enter.native="handleQuery"
|
||
/>
|
||
</el-form-item>
|
||
<!-- <el-form-item label="主讲课程" prop="courses">
|
||
<el-input
|
||
v-model="queryParams.courses"
|
||
placeholder="请输入主讲课程"
|
||
clearable
|
||
@keyup.enter.native="handleQuery"
|
||
/>
|
||
</el-form-item>-->
|
||
<!-- <el-form-item label="联系电话" prop="tPhone">
|
||
<el-input
|
||
v-model="queryParams.tPhone"
|
||
placeholder="请输入联系电话"
|
||
clearable
|
||
@keyup.enter.native="handleQuery"
|
||
/>
|
||
</el-form-item>-->
|
||
<el-form-item label="状态" prop="status">
|
||
<el-select v-model="queryParams.status" placeholder="教师状态" clearable>
|
||
<el-option label="正常" :value="1" />
|
||
<el-option label="停用" :value="0" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item class="filter-actions">
|
||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
|
||
|
||
|
||
<!-- <el-row :gutter="10" class="mb8">
|
||
<el-col :span="1.5">
|
||
<el-button
|
||
v-hasPermi="['train:teacher:add']"
|
||
type="primary"
|
||
plain
|
||
icon="el-icon-plus"
|
||
size="mini"
|
||
@click="handleAdd"
|
||
>新增</el-button>
|
||
</el-col>
|
||
<el-col :span="1.5">
|
||
<el-button
|
||
v-hasPermi="['train:teacher:edit']"
|
||
type="success"
|
||
plain
|
||
icon="el-icon-edit"
|
||
size="mini"
|
||
:disabled="single"
|
||
@click="handleUpdate"
|
||
>修改</el-button>
|
||
</el-col>
|
||
<right-toolbar :show-search.sync="showSearch" @queryTable="getList" />
|
||
</el-row>-->
|
||
|
||
<el-table v-loading="loading" :data="teacherList">
|
||
<!-- @selection-change="handleSelectionChange"-->
|
||
<!-- <el-table-column type="selection" width="55" align="center" />-->
|
||
<el-table-column label="头像" align="center" width="80">
|
||
<template slot-scope="scope">
|
||
<el-image
|
||
:src="avatarUrl(scope.row.avatar)"
|
||
:preview-src-list="[avatarUrl(scope.row.avatar)]"
|
||
fit="cover"
|
||
class="teacher-avatar"
|
||
>
|
||
<div slot="error" class="image-slot">
|
||
<i class="el-icon-picture-outline" />
|
||
</div>
|
||
</el-image>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="教师姓名" align="center" prop="tName" min-width="120" />
|
||
<!-- <el-table-column label="人员编号" align="center" prop="personNo" min-width="120" show-overflow-tooltip />-->
|
||
<el-table-column label="性别" align="center" prop="tGender" width="80">
|
||
<template slot-scope="scope">
|
||
<span>{{ formatGender(scope.row.tGender) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<!-- <el-table-column label="出生日期" align="center" prop="tBirth" width="120">
|
||
<template slot-scope="scope">
|
||
<span>{{ parseTime(scope.row.tBirth, '{y}-{m}-{d}') }}</span>
|
||
</template>
|
||
</el-table-column>-->
|
||
<el-table-column label="所属单位" align="center" prop="deptName" :show-overflow-tooltip="true" />
|
||
<el-table-column label="职务/职称" align="center" prop="jobTitle" :show-overflow-tooltip="true" />
|
||
<!-- <el-table-column label="专业方向" align="center" prop="major" :show-overflow-tooltip="true" />
|
||
<el-table-column label="主讲课程" align="center" prop="courses" :show-overflow-tooltip="true" />-->
|
||
<!-- <el-table-column label="联系电话" align="center" prop="tPhone" width="130" />
|
||
<el-table-column label="邮箱" align="center" prop="tEmail" :show-overflow-tooltip="true" />-->
|
||
<!-- <el-table-column label="状态" align="center" prop="status" width="90">
|
||
<template slot-scope="scope">
|
||
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
|
||
{{ scope.row.status === 1 ? '正常' : '停用' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>-->
|
||
<!-- <el-table-column label="关联系统账号" align="center" min-width="150">
|
||
<template slot-scope="scope">
|
||
<div v-if="scope.row.accountBindingStatus === 1 || scope.row.userName">
|
||
<el-tag type="success" size="mini">已关联</el-tag>
|
||
<div style="font-size: 12px; color: #606266; margin-top: 2px;">
|
||
{{ scope.row.userName }}
|
||
<span v-if="scope.row.nickName">({{ scope.row.nickName }})</span>
|
||
</div>
|
||
</div>
|
||
<el-tag v-else type="info" size="mini">未关联</el-tag>
|
||
</template>
|
||
</el-table-column>-->
|
||
<!-- <el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />-->
|
||
<!-- 住宿安排:教师宿舍与班级无关,房间/床位下拉 + 安排宿舍、退宿操作(同一行展示,不换行) -->
|
||
<el-table-column label="住宿安排" align="center" min-width="480">
|
||
<template slot-scope="scope">
|
||
<div class="dorm-cell-line">
|
||
<el-select
|
||
:value="accTeacherOf(scope.row).dormId"
|
||
placeholder="宿舍房间号"
|
||
filterable
|
||
size="mini"
|
||
:disabled="!accTeacherOf(scope.row).editing"
|
||
style="width: 150px"
|
||
@change="val => handleAccDormChange(scope.row, val)"
|
||
>
|
||
<el-option
|
||
v-for="dorm in accDormOptions"
|
||
:key="dorm.dormId"
|
||
:label="accDormLabel(dorm)"
|
||
:value="dorm.dormId"
|
||
:disabled="isAccDormUnavailable(dorm, accTeacherOf(scope.row).dormId)"
|
||
/>
|
||
</el-select>
|
||
<el-select
|
||
:value="accTeacherOf(scope.row).bedNo"
|
||
placeholder="床位"
|
||
size="mini"
|
||
:disabled="!accTeacherOf(scope.row).editing || !accTeacherOf(scope.row).dormId"
|
||
style="width: 88px"
|
||
@change="val => handleAccBedChange(scope.row, val)"
|
||
>
|
||
<el-option v-for="bed in accBedOptions(scope.row)" :key="bed" :label="bed" :value="bed" />
|
||
</el-select>
|
||
<el-button
|
||
v-hasPermi="['train:teacher:edit']"
|
||
size="mini"
|
||
type="text"
|
||
:icon="accTeacherOf(scope.row).editing ? 'el-icon-close' : 'el-icon-office-building'"
|
||
:loading="accTeacherOf(scope.row).saving"
|
||
@click="handleToggleAccAdjust(scope.row)"
|
||
>{{ accTeacherOf(scope.row).editing ? '取消' : (hasAccAssignment(scope.row) ? '调整宿舍' : '安排宿舍') }}</el-button>
|
||
<el-button
|
||
v-hasPermi="['train:teacher:edit']"
|
||
size="mini"
|
||
type="text"
|
||
icon="el-icon-remove-outline"
|
||
style="color: #E6A23C;"
|
||
:disabled="!hasAccAssignment(scope.row)"
|
||
@click="handleTeacherAccCheckOut(scope.row)"
|
||
>退宿</el-button>
|
||
<!-- 授权结果:lockResult 经字典 train_accommodation_lock_result 翻译为中文,以图标+文案展示 -->
|
||
<el-tooltip :content="lockResultCell(scope.row).tip" placement="top">
|
||
<span class="dorm-lock-result" :style="{ color: lockResultCell(scope.row).color }">
|
||
<i :class="lockResultCell(scope.row).icon"></i>
|
||
<span class="dorm-lock-text">{{ lockResultCell(scope.row).text }}</span>
|
||
</span>
|
||
</el-tooltip>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="260">
|
||
<template slot-scope="scope">
|
||
<el-button
|
||
v-hasPermi="['train:teacher:query']"
|
||
size="mini"
|
||
type="text"
|
||
icon="el-icon-view"
|
||
@click="handleDetail(scope.row)"
|
||
>详情</el-button>
|
||
<el-button
|
||
v-hasPermi="['train:teacher:edit']"
|
||
size="mini"
|
||
type="text"
|
||
icon="el-icon-edit"
|
||
@click="handleUpdate(scope.row)"
|
||
>修改</el-button>
|
||
<!-- <el-button
|
||
v-if="!scope.row.userName && !scope.row.userId"
|
||
v-hasPermi="['train:teacher:bindUser']"
|
||
size="mini"
|
||
type="text"
|
||
icon="el-icon-link"
|
||
@click="handleOpenBinding(scope.row)"
|
||
>关联账号</el-button>
|
||
<el-button
|
||
v-else
|
||
v-hasPermi="['train:teacher:unbindUser']"
|
||
size="mini"
|
||
type="text"
|
||
icon="el-icon-circle-close"
|
||
style="color: #F56C6C;"
|
||
@click="handleUnbind(scope.row)"
|
||
>解绑账号</el-button>-->
|
||
<el-button
|
||
v-hasPermi="['train:teacher:remove']"
|
||
size="mini"
|
||
type="text"
|
||
icon="el-icon-delete"
|
||
style="color: #F56C6C;"
|
||
@click="handleDelete(scope.row)"
|
||
>删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<pagination
|
||
v-show="total>0"
|
||
:total="total"
|
||
:page.sync="queryParams.pageNum"
|
||
:limit.sync="queryParams.pageSize"
|
||
@pagination="getList"
|
||
/>
|
||
|
||
<!-- 添加或修改教师信息对话框 -->
|
||
<el-dialog :title="title" :visible.sync="open" width="900px" append-to-body>
|
||
<el-form ref="form" :model="form" :rules="rules" label-width="90px">
|
||
<el-row :gutter="20">
|
||
<el-col :span="24">
|
||
<el-form-item label="教师头像" prop="avatar">
|
||
<div class="avatar-edit">
|
||
<studentAvatar ref="studentAvatar" :user="user" @success="handleAvatarSuccess" />
|
||
<div class="avatar-edit-actions">
|
||
<el-button size="mini" type="primary" icon="el-icon-picture" @click="handleSelectAvatar">选择图片</el-button>
|
||
<el-button size="mini" icon="el-icon-camera" :loading="cameraStarting" @click="openCamera">拍照</el-button>
|
||
</div>
|
||
</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="教师姓名" prop="tName">
|
||
<el-input v-model="form.tName" placeholder="请输入教师姓名" maxlength="50" show-word-limit />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="人员编号" prop="personNo">
|
||
<el-input v-model="form.personNo" placeholder="请输入人员编号" maxlength="50" show-word-limit />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="性别" prop="tGender">
|
||
<el-radio-group v-model="form.tGender">
|
||
<el-radio label="男">男</el-radio>
|
||
<el-radio label="女">女</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="出生日期" prop="tBirth">
|
||
<el-date-picker
|
||
v-model="form.tBirth"
|
||
clearable
|
||
type="date"
|
||
value-format="yyyy-MM-dd"
|
||
placeholder="请选择出生日期"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="所属单位" prop="deptName">
|
||
<!-- <el-input v-model="form.deptName" placeholder="请输入所属单位/部门" maxlength="100" />-->
|
||
<treeselect v-model="form.deptName" :options="deptOptions" :normalizer="normalizer" :show-count="true" placeholder="请选择所属单位" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="职务/职称" prop="jobTitle">
|
||
<el-input v-model="form.jobTitle" placeholder="请输入职务/职称" maxlength="50" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="专业方向" prop="major">
|
||
<el-input v-model="form.major" placeholder="请输入专业方向" maxlength="100" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="主讲课程" prop="courses">
|
||
<el-input v-model="form.courses" placeholder="请输入主讲课程,多门课程可用逗号分隔" maxlength="200" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="从教时间" prop="startTeaching">
|
||
<el-date-picker
|
||
v-model="form.startTeaching"
|
||
clearable
|
||
type="date"
|
||
value-format="yyyy-MM-dd"
|
||
placeholder="请选择从教时间"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="联系电话" prop="tPhone">
|
||
<el-input v-model="form.tPhone" placeholder="请输入11位手机号码" maxlength="20" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="邮箱" prop="tEmail">
|
||
<el-input v-model="form.tEmail" placeholder="请输入电子邮箱" maxlength="100" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="状态" prop="status">
|
||
<el-radio-group v-model="form.status">
|
||
<el-radio :label="1">正常</el-radio>
|
||
<el-radio :label="0">停用</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="24">
|
||
<el-form-item label="备注" prop="remark">
|
||
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注信息" maxlength="500" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</el-form>
|
||
<div slot="footer" class="dialog-footer">
|
||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||
<el-button @click="cancel">取 消</el-button>
|
||
</div>
|
||
</el-dialog>
|
||
|
||
<!-- 拍照:调用浏览器摄像头拍摄教师头像 -->
|
||
<el-dialog title="拍照" :visible.sync="cameraOpen" width="640px" append-to-body @opened="handleCameraOpened" @close="closeCamera">
|
||
<div class="camera-wrap">
|
||
<video v-show="!cameraPhoto" ref="cameraVideo" class="camera-video" autoplay playsinline muted />
|
||
<canvas ref="cameraCanvas" style="display: none;" />
|
||
<img v-if="cameraPhoto" :src="cameraPhoto" class="camera-photo" alt="拍摄照片" />
|
||
<div v-if="cameraError" class="camera-error">{{ cameraError }}</div>
|
||
</div>
|
||
<div slot="footer" class="dialog-footer">
|
||
<el-button size="mini" icon="el-icon-refresh" :disabled="!!cameraPhoto" @click="switchCamera">切换摄像头</el-button>
|
||
<el-button v-if="!cameraPhoto" type="primary" size="mini" icon="el-icon-camera-solid" @click="takePhoto">拍 摄</el-button>
|
||
<el-button v-if="cameraPhoto" size="mini" icon="el-icon-refresh-left" @click="retakePhoto">重 拍</el-button>
|
||
<el-button v-if="cameraPhoto" type="primary" size="mini" :loading="cameraUploading" @click="submitCameraPhoto">使用该照片</el-button>
|
||
<el-button size="mini" @click="cameraOpen = false">取 消</el-button>
|
||
</div>
|
||
</el-dialog>
|
||
|
||
<!-- 关联系统账号对话框 -->
|
||
<el-dialog title="关联系统账号 (教师业务档案)" :visible.sync="bindingOpen" width="700px" append-to-body>
|
||
<el-alert
|
||
title="关联说明"
|
||
type="info"
|
||
description="将教师业务档案与系统用户账号关联后,教师可凭该系统账号登录并使用“教师工作台”。系统仅允许选择正常启用且已分配教师角色(train_teacher)的账号。"
|
||
show-icon
|
||
:closable="false"
|
||
style="margin-bottom: 15px;"
|
||
/>
|
||
<div style="margin-bottom: 15px; font-weight: bold; color: #303133;">
|
||
当前教师:{{ currentTeacher.tName }} (工号: {{ currentTeacher.tId }})
|
||
</div>
|
||
<el-form size="small" :inline="true" @submit.native.prevent>
|
||
<el-form-item label="查找用户">
|
||
<el-input
|
||
v-model="bindingKeyword"
|
||
placeholder="请输入用户名或姓名搜索"
|
||
clearable
|
||
style="width: 240px;"
|
||
@keyup.enter.native="loadBindableUsers"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" icon="el-icon-search" size="mini" @click="loadBindableUsers">搜索</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
<el-table v-loading="bindingLoading" :data="bindableUsers" max-height="300" highlight-current-row @current-change="handleSelectedUserChange">
|
||
<el-table-column width="55" align="center">
|
||
<template slot-scope="scope">
|
||
<el-radio v-model="selectedUserId" :label="scope.row.userId" :disabled="!scope.row.canBind">{{ '' }}</el-radio>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="系统账号" prop="userName" min-width="110" />
|
||
<el-table-column label="姓名/昵称" prop="nickName" min-width="110" />
|
||
<el-table-column label="状态" width="80" align="center">
|
||
<template slot-scope="scope">
|
||
<el-tag :type="scope.row.status === '0' ? 'success' : 'danger'" size="mini">
|
||
{{ scope.row.status === '0' ? '正常' : '停用' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="分配角色" min-width="120" show-overflow-tooltip>
|
||
<template slot-scope="scope">
|
||
<span>{{ (scope.row.roleKeys || []).join(', ') }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="可否关联" min-width="150">
|
||
<template slot-scope="scope">
|
||
<el-tag v-if="scope.row.canBind" type="success" size="mini">可选</el-tag>
|
||
<span v-else style="color: #F56C6C; font-size: 12px;">{{ scope.row.reason }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div slot="footer" class="dialog-footer">
|
||
<el-button type="primary" :disabled="!selectedUserId" :loading="bindingSubmitting" @click="submitBinding">确 定 关 联</el-button>
|
||
<el-button @click="bindingOpen = false">取 消</el-button>
|
||
</div>
|
||
</el-dialog>
|
||
|
||
<!-- 教师详情弹窗 -->
|
||
<el-dialog
|
||
title="教师详情"
|
||
:visible.sync="detailOpen"
|
||
:width="detailDialogWidth"
|
||
append-to-body
|
||
:close-on-click-modal="true"
|
||
@closed="handleDetailClosed"
|
||
>
|
||
<div v-loading="detailLoading" class="teacher-detail-body">
|
||
<el-empty v-if="!detailLoading && !detail.tId" description="暂无教师详细信息" />
|
||
<el-descriptions v-else-if="detail.tId" :column="detailColumns" border>
|
||
<el-descriptions-item :span="detailColumns">
|
||
<template slot="label"><span class="teacher-desc-label">教师头像</span></template>
|
||
<el-image
|
||
:src="avatarUrl(detail.avatar)"
|
||
:preview-src-list="[avatarUrl(detail.avatar)]"
|
||
fit="cover"
|
||
class="detail-avatar"
|
||
>
|
||
<div slot="error" class="image-slot">
|
||
<i class="el-icon-picture-outline" />
|
||
</div>
|
||
</el-image>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item>
|
||
<template slot="label"><span class="teacher-desc-label">教师姓名</span></template>
|
||
{{ detail.tName || '-' }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item>
|
||
<template slot="label"><span class="teacher-desc-label">人员编号</span></template>
|
||
{{ detail.personNo || '-' }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item>
|
||
<template slot="label"><span class="teacher-desc-label">性别</span></template>
|
||
{{ formatGender(detail.tGender) }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item>
|
||
<template slot="label"><span class="teacher-desc-label">出生日期</span></template>
|
||
{{ fmtDate(detail.tBirth) }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item>
|
||
<template slot="label"><span class="teacher-desc-label">所属单位</span></template>
|
||
{{ detail.deptName || '-' }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item>
|
||
<template slot="label"><span class="teacher-desc-label">职务/职称</span></template>
|
||
{{ detail.jobTitle || '-' }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item>
|
||
<template slot="label"><span class="teacher-desc-label">专业方向</span></template>
|
||
{{ detail.major || '-' }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item>
|
||
<template slot="label"><span class="teacher-desc-label">主讲课程</span></template>
|
||
{{ detail.courses || '-' }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item>
|
||
<template slot="label"><span class="teacher-desc-label">从教时间</span></template>
|
||
{{ fmtDate(detail.startTeaching) }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item>
|
||
<template slot="label"><span class="teacher-desc-label">联系电话</span></template>
|
||
{{ detail.tPhone || '-' }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item>
|
||
<template slot="label"><span class="teacher-desc-label">邮箱</span></template>
|
||
{{ detail.tEmail || '-' }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item>
|
||
<template slot="label"><span class="teacher-desc-label">状态</span></template>
|
||
<el-tag :type="detail.status === 1 ? 'success' : 'danger'">
|
||
{{ detail.status === 1 ? '正常' : '停用' }}
|
||
</el-tag>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item>
|
||
<template slot="label"><span class="teacher-desc-label">创建时间</span></template>
|
||
{{ fmtDate(detail.createTime, true) }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item :span="detailColumns">
|
||
<template slot="label"><span class="teacher-desc-label">备注</span></template>
|
||
{{ detail.remark || '-' }}
|
||
</el-descriptions-item>
|
||
</el-descriptions>
|
||
</div>
|
||
<div slot="footer" class="dialog-footer">
|
||
<el-button @click="detailOpen = false">关 闭</el-button>
|
||
</div>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { listTeacher, getTeacher, delTeacher, addTeacher, updateTeacher, getTeacherBindableUsers, bindTeacherUser, unbindTeacherUser } from '@/api/train/teacher'
|
||
import {deptTreeSelect} from "@/api/system/user";
|
||
import Treeselect from "@riophae/vue-treeselect";
|
||
import "@riophae/vue-treeselect/dist/vue-treeselect.css";
|
||
import {listDept} from "@/api/system/dept";
|
||
// 复用学员头像上传组件(通用:props.user 传入 {avatar},上传成功后抛出 success 事件)
|
||
import studentAvatar from '@/views/train/student/studentAvatar'
|
||
import request from '@/utils/request'
|
||
import { getTeacherAvatar } from '@/utils/avatar'
|
||
// 教师住宿安排(与学生住宿逻辑隔离,不关联班级)
|
||
import { listDormitory } from '@/api/train/dormitory'
|
||
import {
|
||
listTeacherAccommodation,
|
||
getTeacherAccommodation,
|
||
assignTeacherAccommodation,
|
||
checkOutTeacherAccommodation
|
||
} from '@/api/train/teacherAccommodation'
|
||
|
||
/**
|
||
* 门禁授权结果图标规则:字典值(train_accommodation_lock_result) → 图标 + 颜色。
|
||
* 与学员列表住宿安排列保持完全一致的取值口径与展示逻辑。
|
||
* 1 / unauthorized / pending 未授权 el-icon-time 灰色
|
||
* 2 / authorized / success 已授权 el-icon-circle-check 绿色
|
||
* 3 / expired 已过期 el-icon-warning-outline 橙色
|
||
* failed / revoked 失败/撤销 el-icon-circle-close 红色
|
||
*/
|
||
const LOCK_RESULT_ICON_RULES = {
|
||
1: { icon: 'el-icon-time', color: '#909399' },
|
||
2: { icon: 'el-icon-circle-check', color: '#67C23A' },
|
||
3: { icon: 'el-icon-warning-outline', color: '#E6A23C' },
|
||
unauthorized: { icon: 'el-icon-time', color: '#909399' },
|
||
pending: { icon: 'el-icon-time', color: '#909399' },
|
||
waiting: { icon: 'el-icon-time', color: '#909399' },
|
||
authorized: { icon: 'el-icon-circle-check', color: '#67C23A' },
|
||
success: { icon: 'el-icon-circle-check', color: '#67C23A' },
|
||
expired: { icon: 'el-icon-warning-outline', color: '#E6A23C' },
|
||
failed: { icon: 'el-icon-circle-close', color: '#F56C6C' },
|
||
fail: { icon: 'el-icon-circle-close', color: '#F56C6C' },
|
||
revoked: { icon: 'el-icon-remove-outline', color: '#909399' },
|
||
canceled: { icon: 'el-icon-remove-outline', color: '#909399' }
|
||
}
|
||
|
||
/** 授权结果默认图标:空值与字典未匹配时使用(灰色问号) */
|
||
const LOCK_RESULT_ICON_DEFAULT = { icon: 'el-icon-question', color: '#C0C4CC' }
|
||
|
||
export default {
|
||
name: 'Teacher',
|
||
// 字典注入:train_accommodation_lock_result 门禁授权结果(住宿安排列图标文案)
|
||
dicts: ['train_accommodation_lock_result'],
|
||
components: { Treeselect, studentAvatar },
|
||
data() {
|
||
return {
|
||
// 遮罩层
|
||
loading: true,
|
||
// 选中数组
|
||
ids: [],
|
||
// 非单个禁用
|
||
single: true,
|
||
// 显示搜索条件
|
||
showSearch: true,
|
||
// 总条数
|
||
total: 0,
|
||
// 教师信息表格数据
|
||
teacherList: [],
|
||
// ===== 教师住宿安排(与学生住宿逻辑隔离) =====
|
||
// 宿舍房间选项(全量,含停用房间,保证已分配教师的宿舍能正确回显)
|
||
accDormOptions: [],
|
||
// 每个教师行的住宿编辑态(key = tId)
|
||
accTeacherState: {},
|
||
// 弹出层标题
|
||
title: '',
|
||
// 是否显示弹出层
|
||
open: false,
|
||
// 账号关联弹窗
|
||
bindingOpen: false,
|
||
bindingLoading: false,
|
||
bindingSubmitting: false,
|
||
bindingKeyword: '',
|
||
bindableUsers: [],
|
||
currentTeacher: {},
|
||
selectedUserId: null,
|
||
// 教师详情弹窗
|
||
detailOpen: false,
|
||
detailLoading: false,
|
||
detail: {},
|
||
// 当前窗口宽度(用于详情弹窗与描述列数自适应)
|
||
screenWidth: typeof window !== 'undefined' ? window.innerWidth : 1280,
|
||
// 查询参数
|
||
queryParams: {
|
||
pageNum: 1,
|
||
pageSize: 10,
|
||
tName: null,
|
||
deptName: null,
|
||
personNo: null,
|
||
jobTitle: null,
|
||
tPhone: null,
|
||
status: null
|
||
},
|
||
// 部门树选项
|
||
deptOptions: undefined,
|
||
deptArr: {},
|
||
// 头像上传组件用的教师对象(avatar 为入库的相对地址)
|
||
user: { avatar: '' },
|
||
// 拍照弹窗
|
||
cameraOpen: false,
|
||
// 当前摄像头视频流
|
||
cameraStream: null,
|
||
// 摄像头是否正在启动
|
||
cameraStarting: false,
|
||
// 拍摄后的照片 DataURL
|
||
cameraPhoto: '',
|
||
// 摄像头前置/后置
|
||
cameraFacing: 'user',
|
||
// 摄像头不可用时的提示
|
||
cameraError: '',
|
||
// 拍摄照片上传中
|
||
cameraUploading: false,
|
||
form: {},
|
||
// 表单校验
|
||
rules: {
|
||
tName: [
|
||
{ required: true, message: '教师姓名不能为空', trigger: 'blur' },
|
||
{ max: 50, message: '教师姓名长度不能超过50个字符', trigger: 'blur' }
|
||
],
|
||
personNo: [
|
||
{ max: 50, message: '人员编号长度不能超过50个字符', trigger: 'blur' }
|
||
],
|
||
tPhone: [
|
||
{ required: true, message: '联系电话不能为空', trigger: 'blur' },
|
||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的11位手机号码', trigger: 'blur' }
|
||
],
|
||
tEmail: [
|
||
{ type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }
|
||
],
|
||
major: [
|
||
{ max: 100, message: '专业方向长度不能超过100个字符', trigger: 'blur' }
|
||
],
|
||
courses: [
|
||
{ max: 200, message: '主讲课程长度不能超过200个字符', trigger: 'blur' }
|
||
]
|
||
}
|
||
}
|
||
},
|
||
computed: {
|
||
/** 详情描述列数:窄屏单列、宽屏双列 */
|
||
detailColumns() {
|
||
return this.screenWidth < 768 ? 1 : 2
|
||
},
|
||
/** 详情弹窗宽度:窄屏接近满宽、宽屏固定 */
|
||
detailDialogWidth() {
|
||
return this.screenWidth < 768 ? '92%' : '820px'
|
||
}
|
||
},
|
||
created() {
|
||
this.mapDept()
|
||
this.loadAccDormOptions()
|
||
this.getList()
|
||
this.getDeptTree()
|
||
},
|
||
mounted() {
|
||
window.addEventListener('resize', this.handleResize)
|
||
},
|
||
beforeDestroy() {
|
||
window.removeEventListener('resize', this.handleResize)
|
||
},
|
||
methods: {
|
||
/** 窗口尺寸变化:同步宽度,驱动详情弹窗自适应 */
|
||
handleResize() {
|
||
this.screenWidth = window.innerWidth
|
||
},
|
||
/** 日期格式化:空值显示 - */
|
||
fmtDate(value, withTime) {
|
||
if (!value) return '-'
|
||
const pattern = withTime ? '{y}-{m}-{d} {h}:{i}:{s}' : '{y}-{m}-{d}'
|
||
return this.parseTime ? this.parseTime(value, pattern) : value
|
||
},
|
||
/** 打开教师详情弹窗并拉取当前行完整信息 */
|
||
handleDetail(row) {
|
||
if (!row || row.tId === null || row.tId === undefined) {
|
||
this.$modal.msgWarning('暂无该教师的可用信息')
|
||
return
|
||
}
|
||
this.detail = {}
|
||
this.detailOpen = true
|
||
this.detailLoading = true
|
||
getTeacher(row.tId).then(res => {
|
||
this.detail = (res && res.data) || {}
|
||
}).catch(() => {
|
||
// 请求失败:错误已由请求拦截器提示,这里保持弹窗并展示空态
|
||
}).finally(() => {
|
||
this.detailLoading = false
|
||
})
|
||
},
|
||
/** 详情弹窗关闭后清理数据,避免再次打开闪现旧数据 */
|
||
handleDetailClosed() {
|
||
this.detail = {}
|
||
this.detailLoading = false
|
||
},
|
||
/** 查询教师信息列表 */
|
||
getList() {
|
||
this.loading = true
|
||
listTeacher(this.queryParams).then(response => {
|
||
this.teacherList = response.rows
|
||
this.total = response.total
|
||
this.teacherList.forEach(stud => {
|
||
let studOrgName = this.formatOrgName(stud.deptName);
|
||
//console.log(stud.deptName +"--"+ studOrgName);
|
||
this.$set(stud, "deptName", studOrgName);
|
||
});
|
||
// 回显教师当前住宿安排(独立接口,不影响教师列表本身)
|
||
this.loadTeacherAccommodations();
|
||
}).finally(() => {
|
||
this.loading = false
|
||
})
|
||
},
|
||
/** 查询部门下拉树结构 */
|
||
getDeptTree() {
|
||
deptTreeSelect().then(response => {
|
||
this.deptOptions = response.data;
|
||
});
|
||
},
|
||
// 筛选节点
|
||
filterNode(value, data) {
|
||
if (!value) return true;
|
||
return data.label.indexOf(value) !== -1;
|
||
},
|
||
// 节点单击事件
|
||
handleNodeClick(data) {
|
||
this.createForm.deptName = data.id;
|
||
this.handleQuery();
|
||
},
|
||
// 数据格式化函数,把后端返回部门树形转成treeselect需要格式
|
||
normalizer(node) {
|
||
return {
|
||
id: node.id,
|
||
label: node.label,
|
||
children: node.children
|
||
}
|
||
},
|
||
mapDept() {
|
||
listDept().then(response => {
|
||
let deptList = response.data || [];
|
||
let deptArr = {};
|
||
deptList.forEach(dept => {
|
||
if (dept && dept.deptId !== undefined && dept.deptId !== null) {
|
||
deptArr[dept.deptId] = dept.deptName;
|
||
}
|
||
});
|
||
this.deptArr = deptArr;
|
||
});
|
||
},
|
||
|
||
// 取消按钮
|
||
cancel() {
|
||
this.open = false
|
||
this.reset()
|
||
},
|
||
// 表单重置
|
||
reset() {
|
||
this.form = {
|
||
tId: null,
|
||
avatar: null,
|
||
tName: null,
|
||
personNo: null,
|
||
tGender: '男',
|
||
tBirth: null,
|
||
deptName: null,
|
||
jobTitle: null,
|
||
major: null,
|
||
courses: null,
|
||
startTeaching: null,
|
||
tPhone: null,
|
||
tEmail: null,
|
||
status: 1,
|
||
remark: null
|
||
}
|
||
// 重置头像组件预览
|
||
this.user = { avatar: '' }
|
||
this.resetForm('form')
|
||
},
|
||
/*
|
||
* “选择图片”:与点击头像图片效果一致,打开头像裁剪/上传弹窗
|
||
* (弹窗内的“选择”按钮可挑选本地图片,提交后写入 form.avatar)
|
||
*/
|
||
handleSelectAvatar() {
|
||
if (this.$refs.studentAvatar) {
|
||
this.$refs.studentAvatar.editCropper()
|
||
}
|
||
},
|
||
/** 打开拍照弹窗 */
|
||
openCamera() {
|
||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||
this.$modal.msgError('当前浏览器不支持拍照功能,请使用 Chrome/Edge 等现代浏览器,并确保以 https 或 localhost 访问')
|
||
return
|
||
}
|
||
this.cameraPhoto = ''
|
||
this.cameraError = ''
|
||
this.cameraFacing = 'user'
|
||
this.cameraOpen = true
|
||
},
|
||
/** 拍照弹窗打开后启动摄像头 */
|
||
handleCameraOpened() {
|
||
this.startCamera()
|
||
},
|
||
/** 启动摄像头并播放视频流 */
|
||
startCamera() {
|
||
this.stopCameraStream()
|
||
this.cameraStarting = true
|
||
this.cameraError = ''
|
||
const constraints = {
|
||
audio: false,
|
||
video: {
|
||
facingMode: this.cameraFacing,
|
||
width: { ideal: 1280 },
|
||
height: { ideal: 720 }
|
||
}
|
||
}
|
||
navigator.mediaDevices.getUserMedia(constraints).then(stream => {
|
||
this.cameraStream = stream
|
||
const video = this.$refs.cameraVideo
|
||
if (!video) {
|
||
this.stopCameraStream()
|
||
return
|
||
}
|
||
video.srcObject = stream
|
||
const playPromise = video.play()
|
||
if (playPromise && playPromise.catch) {
|
||
playPromise.catch(() => {})
|
||
}
|
||
}).catch(error => {
|
||
const name = error && error.name
|
||
if (name === 'NotAllowedError' || name === 'SecurityError') {
|
||
this.cameraError = '摄像头权限被拒绝,请在浏览器地址栏允许摄像头权限后重试'
|
||
} else if (name === 'NotFoundError' || name === 'OverconstrainedError') {
|
||
this.cameraError = '未检测到可用的摄像头设备'
|
||
} else {
|
||
this.cameraError = '摄像头打开失败:' + ((error && (error.message || name)) || '未知错误')
|
||
}
|
||
}).finally(() => {
|
||
this.cameraStarting = false
|
||
})
|
||
},
|
||
/** 切换前置/后置摄像头 */
|
||
switchCamera() {
|
||
this.cameraFacing = this.cameraFacing === 'user' ? 'environment' : 'user'
|
||
this.startCamera()
|
||
},
|
||
/** 拍摄当前画面 */
|
||
takePhoto() {
|
||
const video = this.$refs.cameraVideo
|
||
const canvas = this.$refs.cameraCanvas
|
||
if (!video || !canvas) {
|
||
this.$modal.msgError('摄像头尚未就绪,请稍候重试')
|
||
return
|
||
}
|
||
if (!video.videoWidth || !video.videoHeight) {
|
||
this.$modal.msgWarning('摄像头画面尚未加载完成,请稍候再试')
|
||
return
|
||
}
|
||
// 取正方形区域,避免头像被拉伸变形
|
||
const size = Math.min(video.videoWidth, video.videoHeight)
|
||
canvas.width = size
|
||
canvas.height = size
|
||
const context = canvas.getContext('2d')
|
||
const sx = (video.videoWidth - size) / 2
|
||
const sy = (video.videoHeight - size) / 2
|
||
context.drawImage(video, sx, sy, size, size, 0, 0, size, size)
|
||
this.cameraPhoto = canvas.toDataURL('image/png')
|
||
// 拍摄完成即释放摄像头
|
||
this.stopCameraStream()
|
||
},
|
||
/** 重新拍摄 */
|
||
retakePhoto() {
|
||
this.cameraPhoto = ''
|
||
this.cameraError = ''
|
||
this.startCamera()
|
||
},
|
||
/** 使用拍摄照片:上传到 /common/upload 并回填教师头像 */
|
||
submitCameraPhoto() {
|
||
if (!this.cameraPhoto) {
|
||
this.$modal.msgWarning('请先拍摄照片')
|
||
return
|
||
}
|
||
const blob = this.dataURLtoBlob(this.cameraPhoto)
|
||
if (!blob) {
|
||
this.$modal.msgError('照片解析失败,请重新拍摄')
|
||
return
|
||
}
|
||
const formData = new FormData()
|
||
formData.append('file', new File([blob], 'avatar.png', { type: 'image/png' }))
|
||
this.cameraUploading = true
|
||
request({
|
||
url: '/common/upload',
|
||
method: 'post',
|
||
data: formData,
|
||
headers: { 'Content-Type': 'multipart/form-data' }
|
||
}).then(response => {
|
||
const imgUrl = response.fileName || response.url
|
||
if (!imgUrl) {
|
||
this.$modal.msgError('上传失败,未获取到文件地址')
|
||
return
|
||
}
|
||
// 与头像上传组件走同一回填逻辑,只入库相对地址
|
||
this.handleAvatarSuccess(imgUrl)
|
||
this.$modal.msgSuccess('拍照上传成功')
|
||
this.cameraOpen = false
|
||
}).catch(() => {}).finally(() => {
|
||
this.cameraUploading = false
|
||
})
|
||
},
|
||
/** DataURL -> Blob */
|
||
dataURLtoBlob(dataURL) {
|
||
if (!dataURL) {
|
||
return null
|
||
}
|
||
const parts = dataURL.split(',')
|
||
if (parts.length < 2) {
|
||
return null
|
||
}
|
||
const mime = (parts[0].match(/:(.*?);/) || [])[1] || 'image/png'
|
||
const binary = window.atob(parts[1])
|
||
const buffer = new Uint8Array(binary.length)
|
||
for (let i = 0; i < binary.length; i++) {
|
||
buffer[i] = binary.charCodeAt(i)
|
||
}
|
||
return new Blob([buffer], { type: mime })
|
||
},
|
||
/** 释放摄像头视频流 */
|
||
stopCameraStream() {
|
||
if (this.cameraStream) {
|
||
this.cameraStream.getTracks().forEach(track => track.stop())
|
||
this.cameraStream = null
|
||
}
|
||
const video = this.$refs.cameraVideo
|
||
if (video) {
|
||
video.srcObject = null
|
||
}
|
||
},
|
||
/** 关闭拍照弹窗:释放摄像头并清理临时照片 */
|
||
closeCamera() {
|
||
this.stopCameraStream()
|
||
this.cameraPhoto = ''
|
||
this.cameraError = ''
|
||
},
|
||
/** 头像上传成功回调:相对地址写入表单,随教师信息一并提交 */
|
||
handleAvatarSuccess(imgUrl) {
|
||
if (!imgUrl) {
|
||
return
|
||
}
|
||
this.$set(this.form, 'avatar', imgUrl)
|
||
// 同步更新头像组件的预览
|
||
this.user = Object.assign({}, this.user, { avatar: imgUrl })
|
||
if (this.$refs.form) {
|
||
this.$refs.form.clearValidate('avatar')
|
||
}
|
||
},
|
||
/** 头像地址解析:补全 baseApi,空值使用专业讲师默认头像 */
|
||
avatarUrl(avatar) {
|
||
return getTeacherAvatar(avatar)
|
||
},
|
||
/** 搜索按钮操作 */
|
||
handleQuery() {
|
||
this.queryParams.pageNum = 1
|
||
this.getList()
|
||
},
|
||
/** 重置按钮操作 */
|
||
resetQuery() {
|
||
this.resetForm('queryForm')
|
||
this.handleQuery()
|
||
},
|
||
// 多选框选中数据
|
||
handleSelectionChange(selection) {
|
||
this.ids = selection.map(item => item.tId)
|
||
this.single = selection.length !== 1
|
||
},
|
||
/** 新增按钮操作 */
|
||
handleAdd() {
|
||
this.reset()
|
||
this.open = true
|
||
this.title = '添加教师信息'
|
||
},
|
||
/** 修改按钮操作 */
|
||
handleUpdate(row) {
|
||
this.reset()
|
||
const tId = row.tId || this.ids[0]
|
||
getTeacher(tId).then(response => {
|
||
this.form = response.data
|
||
// 回填头像预览
|
||
this.user = { avatar: (this.form && this.form.avatar) || '' }
|
||
this.open = true
|
||
this.title = '修改教师信息'
|
||
})
|
||
},
|
||
/** 提交按钮 */
|
||
submitForm() {
|
||
this.$refs['form'].validate(valid => {
|
||
if (valid) {
|
||
if (this.form.tId != null) {
|
||
updateTeacher(this.form).then(response => {
|
||
this.$modal.msgSuccess('修改成功')
|
||
this.open = false
|
||
this.getList()
|
||
})
|
||
} else {
|
||
addTeacher(this.form).then(response => {
|
||
this.$modal.msgSuccess('新增成功')
|
||
this.open = false
|
||
this.getList()
|
||
})
|
||
}
|
||
}
|
||
})
|
||
},
|
||
/** 打开关联系统账号对话框 */
|
||
handleOpenBinding(row) {
|
||
this.currentTeacher = row
|
||
this.selectedUserId = null
|
||
this.bindingKeyword = ''
|
||
this.bindableUsers = []
|
||
this.bindingOpen = true
|
||
this.loadBindableUsers()
|
||
},
|
||
/** 加载候选用户列表 */
|
||
loadBindableUsers() {
|
||
this.bindingLoading = true
|
||
getTeacherBindableUsers(this.bindingKeyword).then(response => {
|
||
this.bindableUsers = response.data || []
|
||
this.bindingLoading = false
|
||
}).catch(() => {
|
||
this.bindingLoading = false
|
||
})
|
||
},
|
||
/** 单击行选中候选账号 */
|
||
handleSelectedUserChange(row) {
|
||
if (row && row.canBind) {
|
||
this.selectedUserId = row.userId
|
||
}
|
||
},
|
||
/** 提交账号关联 */
|
||
submitBinding() {
|
||
if (!this.selectedUserId) {
|
||
this.$modal.msgWarning('请选择要关联的系统账号')
|
||
return
|
||
}
|
||
const selectedUser = this.bindableUsers.find(u => u.userId === this.selectedUserId)
|
||
const userName = selectedUser ? selectedUser.userName : this.selectedUserId
|
||
this.$modal.confirm('确定要将教师档案【' + this.currentTeacher.tName + '】关联到账号【' + userName + '】吗?').then(() => {
|
||
this.bindingSubmitting = true
|
||
return bindTeacherUser(this.currentTeacher.tId, this.selectedUserId)
|
||
}).then(() => {
|
||
this.$modal.msgSuccess('关联成功')
|
||
this.bindingOpen = false
|
||
this.bindingSubmitting = false
|
||
this.getList()
|
||
}).catch(() => {
|
||
this.bindingSubmitting = false
|
||
})
|
||
},
|
||
/** 解除账号关联 */
|
||
handleUnbind(row) {
|
||
this.$modal.confirm('确定要解除教师【' + row.tName + '】与系统账号【' + (row.userName || row.userId) + '】的关联吗?').then(() => {
|
||
return unbindTeacherUser(row.tId)
|
||
}).then(() => {
|
||
this.$modal.msgSuccess('解除关联成功')
|
||
this.getList()
|
||
})
|
||
},
|
||
/** 删除按钮操作 */
|
||
handleDelete(row) {
|
||
const tId = row.tId;
|
||
const tName = row.tName;
|
||
this.$modal.confirm(`确定删除教师【${tName}】吗?`).then(() => {
|
||
return delTeacher(tId);
|
||
}).then(() => {
|
||
this.getList();
|
||
this.$modal.msgSuccess("删除成功");
|
||
}).catch(() => {});
|
||
},
|
||
// 所属单位:deptName 为数字或 deptArr 中存在映射时取 deptArr 对应值,否则显示 deptName 本身
|
||
formatOrgName(deptName) {
|
||
if (deptName === null || deptName === undefined || deptName === '') return '-'
|
||
const map = this.deptArr || {}
|
||
const mapped = map[deptName]
|
||
const isNumber = /^\d+$/.test(String(deptName))
|
||
if (isNumber || mapped !== undefined) {
|
||
return (mapped !== undefined && mapped !== null && mapped !== '') ? mapped : deptName
|
||
}
|
||
return deptName
|
||
},
|
||
formatGender(gender) {
|
||
if (gender === '0' || gender === 0 || gender === '男') return '男'
|
||
if (gender === '1' || gender === 1 || gender === '女') return '女'
|
||
return gender || '未知'
|
||
},
|
||
/** 导出按钮操作 */
|
||
handleExport() {
|
||
this.download('train/teacher/export', {
|
||
...this.queryParams
|
||
}, `teacher_${new Date().getTime()}.xlsx`)
|
||
},
|
||
// ==================== 教师住宿安排(与学生住宿逻辑隔离) ====================
|
||
/** 取某教师行的住宿编辑态 */
|
||
accTeacherOf(row) {
|
||
const state = row && this.accTeacherState[row.tId]
|
||
return state || { editing: false, saving: false, accommodationId: null, dormId: null, bedNo: null }
|
||
},
|
||
/** 加载全量宿舍房间(含停用房间),保证已分配教师的宿舍能正确回显 */
|
||
loadAccDormOptions() {
|
||
return listDormitory({ pageNum: 1, pageSize: 1000 }).then(res => {
|
||
this.accDormOptions = res.rows || []
|
||
this.initAccTeacherState()
|
||
}).catch(() => {
|
||
this.accDormOptions = []
|
||
})
|
||
},
|
||
/** 批量回显教师当前住宿安排(key 为教师表主键 id,返回记录内为 enrollmentId) */
|
||
loadTeacherAccommodations() {
|
||
const teacherIds = (this.teacherList || [])
|
||
.map(row => row.id)
|
||
.filter(id => id !== null && id !== undefined && id !== '')
|
||
if (!teacherIds.length) {
|
||
this.initAccTeacherState()
|
||
return Promise.resolve()
|
||
}
|
||
return listTeacherAccommodation(teacherIds).then(res => {
|
||
const list = (res && res.data) || []
|
||
const byTeacherId = {}
|
||
list.forEach(acc => {
|
||
byTeacherId[String(acc.enrollmentId)] = acc
|
||
})
|
||
this.teacherList.forEach(row => {
|
||
const acc = byTeacherId[String(row.id)]
|
||
this.$set(row, 'accommodationId', acc ? acc.accommodationId : null)
|
||
this.$set(row, 'accommodationStatus', acc ? acc.accommodationStatus : null)
|
||
this.$set(row, 'dormId', acc ? acc.dormId : null)
|
||
this.$set(row, 'dormBuilding', acc ? acc.dormBuilding : '')
|
||
this.$set(row, 'dormRoom', acc ? acc.dormRoom : '')
|
||
this.$set(row, 'bedNo', acc ? acc.bedNo : null)
|
||
this.$set(row, 'lockApiBizNo', acc ? acc.lockApiBizNo : null)
|
||
this.$set(row, 'lockResult', acc ? acc.lockResult : null)
|
||
})
|
||
this.initAccTeacherState()
|
||
}).catch(() => {
|
||
this.initAccTeacherState()
|
||
})
|
||
},
|
||
/** 依据当前列表初始化每行住宿编辑态(下拉框默认禁用) */
|
||
initAccTeacherState() {
|
||
const state = {}
|
||
;(this.teacherList || []).forEach(row => {
|
||
const matched = this.matchAccDorm(row)
|
||
state[row.tId] = {
|
||
editing: false,
|
||
saving: false,
|
||
accommodationId: row.accommodationId || null,
|
||
dormId: matched ? matched.dormId : null,
|
||
bedNo: row.bedNo || null,
|
||
originDormId: matched ? matched.dormId : null,
|
||
originBedNo: row.bedNo || null
|
||
}
|
||
})
|
||
this.accTeacherState = state
|
||
},
|
||
/** 按楼栋+房间号匹配宿舍记录 */
|
||
matchAccDorm(row) {
|
||
if (!row || (!row.dormBuilding && !row.dormRoom)) return null
|
||
return this.accDormOptions.find(d =>
|
||
String(d.dormBuilding || '') === String(row.dormBuilding || '') &&
|
||
String(d.dormRoom || '') === String(row.dormRoom || '')
|
||
) || null
|
||
},
|
||
/** 是否已有「当前有效住宿」:后端仅返回 0/1 的住宿字段,这里再兜底排除已退宿(2) */
|
||
hasAccAssignment(row) {
|
||
return !!(row && row.accommodationStatus !== 2 && (row.dormBuilding || row.dormRoom))
|
||
},
|
||
/**
|
||
* 「住宿安排」列授权结果单元:lockResult → 字典 train_accommodation_lock_result 文案 + 图标。
|
||
* 与学员列表住宿安排列保持一致:
|
||
* - 空值:灰色问号图标 + 「未授权」,tooltip 提示暂无授权结果
|
||
* - 字典未匹配:灰色问号图标 + 「未知状态」,tooltip 展示原始值
|
||
* - 匹配成功:按 LOCK_RESULT_ICON_RULES 取图标与颜色,文案取字典 label
|
||
*/
|
||
lockResultCell(row) {
|
||
const raw = row ? row.lockResult : null
|
||
const isEmpty = raw === null || raw === undefined || String(raw).trim() === ''
|
||
if (isEmpty) {
|
||
return {
|
||
icon: LOCK_RESULT_ICON_DEFAULT.icon,
|
||
color: LOCK_RESULT_ICON_DEFAULT.color,
|
||
text: '未授权',
|
||
tip: '暂无门禁授权结果'
|
||
}
|
||
}
|
||
const options = (this.dict && this.dict.type && this.dict.type.train_accommodation_lock_result) || []
|
||
const matchedItem = options.find(item => String(item.value) === String(raw))
|
||
if (!matchedItem) {
|
||
return {
|
||
icon: LOCK_RESULT_ICON_DEFAULT.icon,
|
||
color: LOCK_RESULT_ICON_DEFAULT.color,
|
||
text: '未知状态',
|
||
tip: `未匹配到字典项:${raw}`
|
||
}
|
||
}
|
||
const rule = LOCK_RESULT_ICON_RULES[String(raw).trim().toLowerCase()] ||
|
||
LOCK_RESULT_ICON_RULES[String(raw).trim()] ||
|
||
LOCK_RESULT_ICON_DEFAULT
|
||
return {
|
||
icon: rule.icon,
|
||
color: rule.color,
|
||
text: matchedItem.label,
|
||
tip: matchedItem.label
|
||
}
|
||
},
|
||
/** 已分配人数 */
|
||
accUsedBeds(dorm) {
|
||
return dorm && dorm.usedCount != null ? Number(dorm.usedCount) || 0 : 0
|
||
},
|
||
/** 剩余床位数 */
|
||
accFreeBeds(dorm) {
|
||
const remaining = dorm && dorm.remainingBeds != null
|
||
? dorm.remainingBeds
|
||
: (dorm && dorm.capacity != null ? Number(dorm.capacity) - this.accUsedBeds(dorm) : 0)
|
||
return Number(remaining) || 0
|
||
},
|
||
/** 房间下拉展示文案(教师住宿跳过性别校验,不展示性别限制) */
|
||
accDormLabel(dorm) {
|
||
const free = this.accFreeBeds(dorm)
|
||
const capacity = dorm.capacity != null ? dorm.capacity : '-'
|
||
return `${dorm.dormBuilding || ''} ${dorm.dormRoom || ''}(余位:${free}/${capacity})`
|
||
},
|
||
/** 房间是否不可选:停用或已满(已选中的房间仍可展示) */
|
||
isAccDormUnavailable(dorm, currentDormId) {
|
||
if (String(dorm.dormId) === String(currentDormId)) return false
|
||
if (dorm.dormStatus === 0) return true
|
||
return this.accFreeBeds(dorm) <= 0
|
||
},
|
||
/** 床位选项:按所选房间核定容量生成 */
|
||
accBedOptions(row) {
|
||
const state = this.accTeacherOf(row)
|
||
const dorm = this.accDormOptions.find(d => String(d.dormId) === String(state.dormId))
|
||
const capacity = dorm && dorm.capacity != null ? Number(dorm.capacity) : 0
|
||
const options = []
|
||
for (let i = 1; i <= capacity; i++) {
|
||
options.push(`${i}号床`)
|
||
}
|
||
return options
|
||
},
|
||
handleAccDormChange(row, dormId) {
|
||
const state = this.accTeacherState[row.tId]
|
||
if (!state || !state.editing) return
|
||
this.$set(state, 'dormId', dormId)
|
||
// 换房间后床位需重新选择
|
||
this.$set(state, 'bedNo', null)
|
||
},
|
||
handleAccBedChange(row, bedNo) {
|
||
const state = this.accTeacherState[row.tId]
|
||
if (!state || !state.editing) return
|
||
this.$set(state, 'bedNo', bedNo)
|
||
this.saveTeacherAccommodation(row)
|
||
},
|
||
/** 房间与床位均已选择后提交(新增安排 / 调整宿舍) */
|
||
saveTeacherAccommodation(row) {
|
||
const state = this.accTeacherState[row.tId]
|
||
if (!state || !state.editing || state.saving) return
|
||
if (!state.dormId || !state.bedNo) return
|
||
const dorm = this.accDormOptions.find(d => String(d.dormId) === String(state.dormId)) || {}
|
||
const location = `${dorm.dormBuilding || ''} ${dorm.dormRoom || ''} ${state.bedNo}`.trim()
|
||
const isAdjust = !!state.accommodationId
|
||
this.$modal.confirm(`确认将教师【${row.tName || ''}】的宿舍安排为【${location}】吗?`)
|
||
.then(() => {
|
||
state.saving = true
|
||
return assignTeacherAccommodation({
|
||
teacherId: row.id,
|
||
dormId: state.dormId,
|
||
bedNo: state.bedNo
|
||
})
|
||
})
|
||
.then(res => {
|
||
// 后端开锁授权门禁:住宿已保存,但人脸不可用时拒绝开锁,需要明确告知原因
|
||
const result = (res && res.data) || {}
|
||
if (result.lockAuthorized === false && result.lockStatus === 'SKIPPED') {
|
||
this.$modal.msgWarning(`宿舍安排已保存,但未开通门锁:${result.lockMessage || '教师人脸照片不可用'}`)
|
||
} else if (result.lockAuthorized === false && result.lockStatus === 'FAILED') {
|
||
this.$modal.msgWarning(`宿舍安排已保存,门锁授权下发失败:${result.lockMessage || '请稍后重试'}`)
|
||
} else {
|
||
this.$modal.msgSuccess(isAdjust ? '教师宿舍调整成功' : '教师宿舍安排成功')
|
||
}
|
||
this.$set(state, 'editing', false)
|
||
// 刷新房间余位与列表回显
|
||
this.loadAccDormOptions().then(() => this.getList())
|
||
})
|
||
.catch(err => {
|
||
// 取消或失败均回滚,保持原住宿安排
|
||
this.$set(state, 'dormId', state.originDormId)
|
||
this.$set(state, 'bedNo', state.originBedNo)
|
||
if (err !== 'cancel') {
|
||
this.$modal.msgError(this.accErrorText(err, '教师宿舍安排失败'))
|
||
}
|
||
})
|
||
.finally(() => {
|
||
state.saving = false
|
||
})
|
||
},
|
||
/** 取教师当前有效住宿记录(供调整/退宿使用) */
|
||
resolveTeacherActiveAccommodation(row, state, fillSelection = false) {
|
||
if (!state || state.accommodationId) return Promise.resolve(state)
|
||
return getTeacherAccommodation(row.id).then(res => {
|
||
const acc = (res && res.data) || null
|
||
if (!acc || !acc.accommodationId) return state
|
||
this.$set(state, 'accommodationId', acc.accommodationId)
|
||
if (fillSelection) {
|
||
if (acc.dormId) {
|
||
this.$set(state, 'dormId', acc.dormId)
|
||
this.$set(state, 'originDormId', acc.dormId)
|
||
}
|
||
if (acc.bedNo) {
|
||
this.$set(state, 'bedNo', acc.bedNo)
|
||
this.$set(state, 'originBedNo', acc.bedNo)
|
||
}
|
||
}
|
||
return state
|
||
}).catch(() => state)
|
||
},
|
||
/** 安排/调整宿舍:激活或取消下拉框编辑 */
|
||
handleToggleAccAdjust(row) {
|
||
const state = this.accTeacherState[row.tId]
|
||
if (!state) return
|
||
if (state.editing) {
|
||
// 再次点击视为取消,回滚为原住宿安排并禁用下拉框
|
||
this.$set(state, 'editing', false)
|
||
this.$set(state, 'dormId', state.originDormId)
|
||
this.$set(state, 'bedNo', state.originBedNo)
|
||
return
|
||
}
|
||
this.$set(state, 'editing', true)
|
||
if (!this.hasAccAssignment(row)) return
|
||
// 已安排住宿:取有效住宿记录,供调房接口使用
|
||
this.resolveTeacherActiveAccommodation(row, state, true)
|
||
},
|
||
/** 退宿:后端统一办理门锁退宿撤权 + 人脸删除(不清空教师头像) */
|
||
handleTeacherAccCheckOut(row) {
|
||
if (!this.hasAccAssignment(row)) {
|
||
this.$modal.msgWarning('该教师暂无有效住宿记录,无需退宿')
|
||
return
|
||
}
|
||
const state = this.accTeacherState[row.tId]
|
||
this.$modal.confirm(`确认办理教师【${row.tName || ''}】的退宿吗?退宿后将解除门锁入住授权并删除人脸数据,教师头像不受影响。`)
|
||
.then(() => {
|
||
const accommodationIdPromise = state && state.accommodationId
|
||
? Promise.resolve(state.accommodationId)
|
||
: getTeacherAccommodation(row.id).then(res => (res && res.data && res.data.accommodationId) || null)
|
||
return accommodationIdPromise.then(accommodationId => {
|
||
if (!accommodationId) {
|
||
this.$modal.msgWarning('未查询到该教师的有效住宿记录(可能已退宿),无法办理退宿')
|
||
return
|
||
}
|
||
return checkOutTeacherAccommodation(accommodationId).then(() => {
|
||
this.$modal.msgSuccess('退宿办理成功')
|
||
if (state) {
|
||
this.$set(state, 'editing', false)
|
||
this.$set(state, 'accommodationId', null)
|
||
}
|
||
this.loadAccDormOptions().then(() => this.getList())
|
||
})
|
||
})
|
||
})
|
||
.catch(err => {
|
||
if (err !== 'cancel') {
|
||
this.$modal.msgError(this.accErrorText(err, '退宿办理失败'))
|
||
}
|
||
})
|
||
},
|
||
/** 统一描述住宿接口异常:业务错误带错误码与详情,超时/网络异常给出对应提示 */
|
||
accErrorText(err, fallbackMsg) {
|
||
const fallback = fallbackMsg || '操作失败'
|
||
if (!err) return fallback
|
||
if (typeof err === 'string') {
|
||
return err === 'cancel' || err === 'error' ? fallback : err
|
||
}
|
||
const response = err.response || {}
|
||
const body = response.data || err.data || {}
|
||
const code = err.code !== undefined && err.code !== null
|
||
? err.code
|
||
: (body && body.code !== undefined && body.code !== null ? body.code : response.status)
|
||
const detail = (body && (body.msg || body.message)) || err.msg || err.message || ''
|
||
if (/timeout/i.test(detail) || code === 'ECONNABORTED') {
|
||
return `${fallback}:请求超时,请检查网络后重试`
|
||
}
|
||
if (/network\s*error/i.test(detail)) {
|
||
return `${fallback}:网络异常,请检查网络连接后重试`
|
||
}
|
||
if (!detail) return code ? `${fallback}(错误码:${code})` : fallback
|
||
return code ? `${fallback}(错误码:${code}):${detail}` : `${fallback}:${detail}`
|
||
},
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.teacher-avatar {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 50%;
|
||
vertical-align: middle;
|
||
background: #f5f7fa;
|
||
}
|
||
|
||
.teacher-avatar .image-slot {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 100%;
|
||
height: 100%;
|
||
color: #c0c4cc;
|
||
font-size: 18px;
|
||
}
|
||
|
||
/* 住宿安排列:房间、床位下拉与安排/退宿按钮同行展示,强制不换行 */
|
||
.dorm-cell-line {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.dorm-cell-line ::v-deep .el-button {
|
||
padding: 0;
|
||
margin-left: 0;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* 住宿安排列:授权结果图标 + 字典文案,固定不收缩,避免挤压房间/床位下拉 */
|
||
.dorm-lock-result {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 3px;
|
||
flex-shrink: 0;
|
||
font-size: 12px;
|
||
line-height: 1;
|
||
white-space: nowrap;
|
||
cursor: default;
|
||
}
|
||
|
||
.dorm-lock-result i {
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* 响应式:中等屏幕起只保留图标,中文文案由 tooltip 承载,保证列宽不溢出 */
|
||
@media (max-width: 1500px) {
|
||
.dorm-lock-text {
|
||
display: none;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 1200px) {
|
||
.dorm-lock-result {
|
||
margin-left: 0;
|
||
}
|
||
}
|
||
|
||
/* 教师详情弹窗 */
|
||
.teacher-detail-body {
|
||
min-height: 120px;
|
||
}
|
||
|
||
/* 详情属性名列:按名称长度自适应列宽,禁止折行;超长名称限制上限并省略 */
|
||
.teacher-detail-body ::v-deep .el-descriptions-item__label {
|
||
white-space: nowrap;
|
||
word-break: keep-all;
|
||
}
|
||
|
||
.teacher-desc-label {
|
||
display: inline-block;
|
||
min-width: 64px;
|
||
max-width: 140px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.detail-avatar {
|
||
width: 64px;
|
||
height: 64px;
|
||
border-radius: 6px;
|
||
background: #f5f7fa;
|
||
}
|
||
|
||
.detail-avatar .image-slot {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 100%;
|
||
height: 100%;
|
||
color: #c0c4cc;
|
||
font-size: 20px;
|
||
}
|
||
|
||
.avatar-edit {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.avatar-edit-actions {
|
||
display: flex;
|
||
flex-direction: column;
|
||
margin-left: 20px;
|
||
|
||
.el-button + .el-button {
|
||
margin-left: 0;
|
||
margin-top: 8px;
|
||
}
|
||
}
|
||
|
||
.camera-wrap {
|
||
width: 100%;
|
||
text-align: center;
|
||
}
|
||
|
||
.camera-video,
|
||
.camera-photo {
|
||
width: 100%;
|
||
max-height: 420px;
|
||
background: #000;
|
||
border-radius: 4px;
|
||
display: inline-block;
|
||
}
|
||
|
||
.camera-error {
|
||
margin-top: 12px;
|
||
color: #F56C6C;
|
||
font-size: 13px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
/* 顶部静态布局样式 */
|
||
.page-topbar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 16px;
|
||
|
||
.topbar-left {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 12px;
|
||
}
|
||
|
||
.topbar-title {
|
||
margin: 0;
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: #1f2d3d;
|
||
}
|
||
|
||
.topbar-sub {
|
||
font-size: 13px;
|
||
color: #8a94a6;
|
||
}
|
||
}
|
||
|
||
.topbar-filter {
|
||
background: #fff;
|
||
border: 1px solid #eef0f5;
|
||
border-radius: 10px;
|
||
padding: 16px 20px;
|
||
margin-bottom: 16px;
|
||
|
||
.filter-item {
|
||
.filter-label {
|
||
font-size: 12px;
|
||
color: #8a94a6;
|
||
margin-bottom: 6px;
|
||
}
|
||
}
|
||
|
||
.filter-keyword {
|
||
flex: 1;
|
||
min-width: 260px;
|
||
}
|
||
|
||
.filter-buttons {
|
||
display: flex;
|
||
gap: 8px;
|
||
|
||
.el-button + .el-button {
|
||
margin-left: 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 查询区:查询条件与按钮同一行、水平垂直对齐,窄屏自动换行 */
|
||
.topbar-filter ::v-deep .filter-form {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 12px 16px;
|
||
}
|
||
|
||
/* 清除 element-ui 默认的表单下/右外边距,由 gap 统一控制间距,保证水平对齐 */
|
||
.topbar-filter ::v-deep .filter-form .el-form-item {
|
||
display: flex;
|
||
align-items: center;
|
||
margin: 0;
|
||
}
|
||
|
||
.topbar-filter ::v-deep .filter-form .el-form-item__label {
|
||
padding-right: 8px;
|
||
line-height: 32px;
|
||
}
|
||
|
||
.topbar-filter ::v-deep .filter-form .el-form-item__content {
|
||
display: flex;
|
||
align-items: center;
|
||
line-height: 32px;
|
||
}
|
||
|
||
/* 统一输入控件宽度,保持整体样式一致 */
|
||
.topbar-filter ::v-deep .filter-form .el-input,
|
||
.topbar-filter ::v-deep .filter-form .el-select {
|
||
width: 180px;
|
||
}
|
||
|
||
/* 按钮组:宽屏靠右,与查询项同一行 */
|
||
.topbar-filter ::v-deep .filter-form .el-form-item.filter-actions {
|
||
margin-left: auto;
|
||
}
|
||
|
||
.topbar-filter ::v-deep .filter-form .filter-actions .el-button + .el-button {
|
||
margin-left: 8px;
|
||
}
|
||
|
||
/* 窄屏适配:缩小间距与控件宽度,按钮不再强制靠右 */
|
||
@media (max-width: 768px) {
|
||
.topbar-filter ::v-deep .filter-form {
|
||
gap: 10px 12px;
|
||
}
|
||
.topbar-filter ::v-deep .filter-form .el-form-item.filter-actions {
|
||
margin-left: 0;
|
||
}
|
||
.topbar-filter ::v-deep .filter-form .el-input,
|
||
.topbar-filter ::v-deep .filter-form .el-select {
|
||
width: 160px;
|
||
}
|
||
}
|
||
</style>
|