teacherList.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  1. <template>
  2. <div class="teacher_list_container">
  3. <!-- 顶部导航 -->
  4. <div class="header">
  5. <el-button class="back_button" @click="GoBack">
  6. <i class="iconfont icon_xiangzuo back_icon" ></i>
  7. 返回
  8. </el-button>
  9. <div class="school_name">{{ schoolName }}</div>
  10. </div>
  11. <!-- 筛选区域 -->
  12. <div class="filter_area">
  13. <TabSearch.default
  14. :tabList="tabList"
  15. tabType="box"
  16. :defaultTabKey="currentTab"
  17. @tabChange="ChangeTab"
  18. />
  19. <div class="search_area">
  20. <el-input
  21. v-model="searchKeyword"
  22. placeholder="请输入账号/手机号"
  23. @input="HandleSearch"
  24. >
  25. <template #append>
  26. <el-button :icon="Search" @click="HandleSearch" />
  27. </template>
  28. </el-input>
  29. </div>
  30. </div>
  31. <!-- 表格区域 -->
  32. <div class="page_table">
  33. <el-table
  34. :data="tableData"
  35. stripe
  36. v-loading="tableLoading"
  37. element-loading-text="拼命加载中……"
  38. :height="tableHeight"
  39. >
  40. <el-table-column type="index" label="序号" width="60" align="center" :index="GetTableIndex" />
  41. <el-table-column prop="userAccount" label="账号" min-width="100" align="center" show-overflow-tooltip />
  42. <el-table-column prop="userName" label="姓名" min-width="100" align="center">
  43. <template #default="{ row }">
  44. <span class="name_text" @click="GoEnter(row)">{{ row.userName }}</span>
  45. </template>
  46. </el-table-column>
  47. <el-table-column prop="wxName" label="微信名" min-width="100" align="center">
  48. <template #default="{ row }">
  49. {{ row.wxName || '-' }}
  50. </template>
  51. </el-table-column>
  52. <el-table-column prop="phone" label="手机号" min-width="120" align="center">
  53. <template #default="{ row }">
  54. {{ row.phone || '-' }}
  55. </template>
  56. </el-table-column>
  57. <el-table-column prop="gradeName" label="年级" width="160" align="center">
  58. <template #default="{ row }">
  59. {{ row.gradeName || '-' }}
  60. </template>
  61. </el-table-column>
  62. <el-table-column prop="subjectName" label="科目" width="160" align="center">
  63. <template #default="{ row }">
  64. {{ row.subjectName || '-' }}
  65. </template>
  66. </el-table-column>
  67. <el-table-column prop="textbookVersions" label="教材版本" width="180" align="center">
  68. <template #default="{ row }">
  69. <el-input
  70. v-if="IsEditing(row, 'textbookVersions')"
  71. v-model="editingCell!.value"
  72. size="small"
  73. placeholder="请输入教材版本"
  74. @click.stop
  75. >
  76. <template #suffix>
  77. <el-icon
  78. class="save_icon"
  79. :class="{ saving: savingField === GetEditKey(row, 'textbookVersions') }"
  80. @click.stop="SaveEdit(row, 'textbookVersions')"
  81. >
  82. <Check />
  83. </el-icon>
  84. </template>
  85. </el-input>
  86. <span
  87. v-else
  88. class="editable_text"
  89. @click="StartEdit(row, 'textbookVersions')"
  90. >
  91. {{ row.textbookVersions || '-' }}
  92. </span>
  93. </template>
  94. </el-table-column>
  95. <el-table-column prop="textbook" label="册别" width="180" align="center">
  96. <template #default="{ row }">
  97. <el-input
  98. v-if="IsEditing(row, 'textbook')"
  99. v-model="editingCell!.value"
  100. size="small"
  101. placeholder="请输入册别"
  102. @click.stop
  103. >
  104. <template #suffix>
  105. <el-icon
  106. class="save_icon"
  107. :class="{ saving: savingField === GetEditKey(row, 'textbook') }"
  108. @click.stop="SaveEdit(row, 'textbook')"
  109. >
  110. <Check />
  111. </el-icon>
  112. </template>
  113. </el-input>
  114. <span
  115. v-else
  116. class="editable_text"
  117. @click="StartEdit(row, 'textbook')"
  118. >
  119. {{ row.textbook || '-' }}
  120. </span>
  121. </template>
  122. </el-table-column>
  123. <el-table-column prop="binXKW" label="是否绑定学科网" width="160" align="center">
  124. <template #default="{ row }">
  125. {{ row.binXKW === 1 ? '是' : '否' }}
  126. </template>
  127. </el-table-column>
  128. <!-- <el-table-column prop="xkwId" label="学科网" width="160" align="center">
  129. <template #default="{ row }">
  130. {{ row.xkwId || '-' }}
  131. </template>
  132. </el-table-column> -->
  133. <el-table-column prop="status" label="状态" width="80" align="center">
  134. <template #default="{ row }">
  135. <div class="status_container">
  136. <span
  137. class="status_dot"
  138. :class="row.status === 1 ? 'active' : 'disabled'"
  139. ></span>
  140. <span :class="row.status === 1 ? 'status_active' : 'status_disabled'">
  141. {{ row.status === 1 ? '启用' : '禁用' }}
  142. </span>
  143. </div>
  144. </template>
  145. </el-table-column>
  146. <el-table-column label="操作" width="80" align="center" fixed="right">
  147. <template #default="{ row }">
  148. <span class="enter_button" @click="GoEnter(row)">进入</span>
  149. </template>
  150. </el-table-column>
  151. </el-table>
  152. </div>
  153. <!-- 分页器 -->
  154. <div class="page_pagination">
  155. <el-pagination
  156. v-model:current-page="pageNum"
  157. v-model:page-size="pageSize"
  158. :page-sizes="[20, 50, 100]"
  159. background
  160. layout="sizes, prev, pager, next"
  161. :total="total"
  162. @current-change="HandleCurrentChange"
  163. @size-change="HandleSizeChange"
  164. />
  165. </div>
  166. </div>
  167. </template>
  168. <script lang="ts" setup>
  169. import { ref, onMounted, onUnmounted, nextTick } from 'vue'
  170. import { useRoute, useRouter } from 'vue-router'
  171. import { Search, Check } from '@element-plus/icons-vue'
  172. import { ElMessage } from 'element-plus'
  173. import { throttle } from 'lodash'
  174. import * as TabSearch from '@/baseComponents/TabSearch.vue'
  175. import { getAllTeacher, getTicket, updateTeacherTextbook } from '@/api/school'
  176. type EditField = 'textbookVersions' | 'textbook'
  177. const route = useRoute()
  178. const router = useRouter()
  179. // 学校信息
  180. const schoolName = ref('')
  181. const schoolId = ref('')
  182. // Tab列表
  183. interface TabItem {
  184. label: string
  185. key: string
  186. prefix?: string
  187. prefixLight?: string
  188. suffix?: string | number
  189. }
  190. const tabList = ref<TabItem[]>([
  191. { label: '全部', key: 'all' },
  192. { label: '已绑定学科网', key: 'bound' },
  193. { label: '未绑定学科网', key: 'unbound' },
  194. ])
  195. // Tab切换
  196. const currentTab = ref('all')
  197. // 搜索关键字
  198. const searchKeyword = ref('')
  199. // 表格数据
  200. interface Teacher {
  201. userId: number | string
  202. userAccount: string
  203. userName: string
  204. wxName: string
  205. phone: string
  206. status: number
  207. binXKW: number
  208. textbookVersions?: string
  209. textbook?: string
  210. [key: string]: any
  211. }
  212. const tableData = ref<Teacher[]>([])
  213. const allData = ref<Teacher[]>([])
  214. const tableLoading = ref(false)
  215. const pageNum = ref(1)
  216. const pageSize = ref(20)
  217. const total = ref(0)
  218. const tableHeight = ref(0)
  219. const API_PAGE_SIZE = 7000
  220. const editingCell = ref<{
  221. userId: number | string
  222. field: EditField
  223. value: string
  224. } | null>(null)
  225. const savingField = ref('')
  226. const GetEditKey = (row: Teacher, field: EditField) => `${row.userId}_${field}`
  227. const IsEditing = (row: Teacher, field: EditField) => {
  228. return editingCell.value?.userId === row.userId && editingCell.value?.field === field
  229. }
  230. const StartEdit = (row: Teacher, field: EditField) => {
  231. editingCell.value = {
  232. userId: row.userId,
  233. field,
  234. value: row[field] || '',
  235. }
  236. }
  237. const UpdateTeacherField = (userId: number | string, field: EditField, value: string) => {
  238. const updateItem = (item: Teacher) => {
  239. if (item.userId === userId) {
  240. item[field] = value
  241. }
  242. }
  243. allData.value.forEach(updateItem)
  244. tableData.value.forEach(updateItem)
  245. }
  246. const GetTextbookPayload = (row: Teacher, field: EditField, value: string) => {
  247. const textbookVersions = (field === 'textbookVersions' ? value : (row.textbookVersions || '')).trim()
  248. const textbook = (field === 'textbook' ? value : (row.textbook || '')).trim()
  249. return { textbookVersions, textbook }
  250. }
  251. const SaveEdit = async (row: Teacher, field: EditField) => {
  252. if (!editingCell.value || !IsEditing(row, field)) return
  253. const value = editingCell.value.value.trim()
  254. const editKey = GetEditKey(row, field)
  255. if (savingField.value === editKey) return
  256. const { textbookVersions, textbook } = GetTextbookPayload(row, field, value)
  257. savingField.value = editKey
  258. try {
  259. const res = await updateTeacherTextbook(row.userId, {
  260. schoolId: schoolId.value,
  261. textbookVersions,
  262. textbook,
  263. })
  264. if (res.code === 200) {
  265. UpdateTeacherField(row.userId, 'textbookVersions', textbookVersions)
  266. UpdateTeacherField(row.userId, 'textbook', textbook)
  267. editingCell.value = null
  268. ElMessage.success('保存成功')
  269. } else {
  270. ElMessage.error(res.msg || '保存失败')
  271. }
  272. } catch (error) {
  273. console.error('保存教材信息失败', error)
  274. ElMessage.error('保存失败')
  275. } finally {
  276. savingField.value = ''
  277. }
  278. }
  279. // 跨页连续序号
  280. const GetTableIndex = (index: number) => {
  281. return (pageNum.value - 1) * pageSize.value + index + 1
  282. }
  283. // 前端 Tab 筛选 + 分页
  284. const FilterAndPaginate = () => {
  285. let filteredData = [...allData.value]
  286. if (currentTab.value === 'bound') {
  287. filteredData = filteredData.filter(item => item.binXKW === 1)
  288. } else if (currentTab.value === 'unbound') {
  289. filteredData = filteredData.filter(item => item.binXKW === 0)
  290. }
  291. total.value = filteredData.length
  292. const start = (pageNum.value - 1) * pageSize.value
  293. const end = start + pageSize.value
  294. tableData.value = filteredData.slice(start, end)
  295. }
  296. // 获取教师列表(接口一次拉取,前端分页)
  297. const GetTeacherList = async () => {
  298. tableLoading.value = true
  299. try {
  300. const params: Record<string, any> = {
  301. schoolId: schoolId.value,
  302. pageNum: 1,
  303. pageSize: API_PAGE_SIZE,
  304. }
  305. if (searchKeyword.value) {
  306. params.word = searchKeyword.value
  307. }
  308. const res = await getAllTeacher(params)
  309. if (res.code === 200 && res.data) {
  310. allData.value = res.data.records || []
  311. FilterAndPaginate()
  312. } else {
  313. allData.value = []
  314. tableData.value = []
  315. total.value = 0
  316. }
  317. } catch (error) {
  318. console.error('获取教师列表失败', error)
  319. allData.value = []
  320. tableData.value = []
  321. total.value = 0
  322. } finally {
  323. tableLoading.value = false
  324. }
  325. }
  326. // 切换Tab(前端筛选)
  327. const ChangeTab = (tab: string) => {
  328. currentTab.value = tab
  329. pageNum.value = 1
  330. FilterAndPaginate()
  331. }
  332. // 搜索(接口模糊查询)
  333. const HandleSearch = () => {
  334. pageNum.value = 1
  335. GetTeacherList()
  336. }
  337. // 分页操作(前端分页)
  338. const HandleCurrentChange = (val: number) => {
  339. pageNum.value = val
  340. FilterAndPaginate()
  341. }
  342. const HandleSizeChange = (val: number) => {
  343. pageSize.value = val
  344. pageNum.value = 1
  345. FilterAndPaginate()
  346. }
  347. // 返回
  348. const GoBack = () => {
  349. router.back()
  350. }
  351. // 进入
  352. const GoEnter = (row: Teacher) => {
  353. // 禁用状态禁止进入
  354. if (row.status === 0) {
  355. ElMessage.error('该教师已禁用,无法进入')
  356. return
  357. }
  358. const params = {
  359. id: row.userId,
  360. }
  361. getTicket(params).then((res: any) => {
  362. const ticket = res.data
  363. let url = `https://www.k12100.com/#/otherLogin?type=1&schoolId=${schoolId.value}&id=${row.userId}&ticket=${ticket}`
  364. if (import.meta.env.VITE_API_BASE_URL === 'https://admin.k12100.com/admin/') {
  365. url = `https://www.k12100.com/#/otherLogin?type=1&schoolId=${schoolId.value}&id=${row.userId}&ticket=${ticket}`
  366. } else {
  367. url = `https://index.k12100.net/#/otherLogin?type=1&schoolId=${schoolId.value}&id=${row.userId}&ticket=${ticket}`
  368. }
  369. window.open(url)
  370. }).catch(() => {})
  371. }
  372. // 计算表格高度
  373. const SetTableHeight = () => {
  374. const containerHeight = document.documentElement.clientHeight
  375. const headerHeight = 60 // 顶部导航
  376. const filterHeight = 100 // 筛选区域
  377. const paginationHeight = 100 // 分页器
  378. const marginHeight = 40 // 边距
  379. tableHeight.value = containerHeight - headerHeight - filterHeight - paginationHeight - marginHeight
  380. }
  381. // 监听窗口大小变化
  382. const HandleResize = throttle(() => {
  383. SetTableHeight()
  384. }, 500)
  385. onMounted(() => {
  386. // 获取路由参数
  387. schoolName.value = route.query.schoolName as string || ''
  388. schoolId.value = route.query.schoolId as string || ''
  389. nextTick(() => {
  390. SetTableHeight()
  391. GetTeacherList()
  392. })
  393. window.addEventListener('resize', HandleResize)
  394. })
  395. onUnmounted(() => {
  396. window.removeEventListener('resize', HandleResize)
  397. })
  398. </script>
  399. <style lang="scss" scoped>
  400. .teacher_list_container {
  401. margin: 20px;
  402. box-sizing: border-box;
  403. background:#fff;
  404. border-radius: 10px;
  405. padding:20px;
  406. width: 100%;
  407. overflow: hidden;
  408. }
  409. // 顶部导航
  410. .header {
  411. display: flex;
  412. align-items: center;
  413. gap: 20px;
  414. margin-bottom: 20px;
  415. .back_button {
  416. width: 82px;
  417. height: 36px;
  418. background: #FFFFFF !important;
  419. border-radius: 4px;
  420. border: 1px solid #DCDFE6 !important;
  421. color: #606266;
  422. font-size: 14px;
  423. padding: 0;
  424. .back_icon {
  425. width: 14px;
  426. height: 14px;
  427. font-size: 14px;
  428. vertical-align: middle;
  429. color: #333;
  430. margin-right: 5px;
  431. }
  432. }
  433. .school_name {
  434. font-size: 16px;
  435. font-weight: 550;
  436. color: #333;
  437. }
  438. }
  439. // 筛选区域
  440. .filter_area {
  441. display: flex;
  442. align-items: center;
  443. gap: 20px;
  444. border-radius: 8px;
  445. margin-bottom: 16px;
  446. .search_area {
  447. flex: 1;
  448. max-width: 217px;
  449. }
  450. // TabSearch 组件样式穿透
  451. :deep(.box_tab) {
  452. .un_selected {
  453. color: #BBBBBB !important;
  454. }
  455. }
  456. }
  457. .page_table {
  458. width: 100%;
  459. min-width: 0;
  460. overflow: hidden;
  461. }
  462. // 表格内自定义样式
  463. .name_text {
  464. color: #2E64FA;
  465. cursor: pointer;
  466. &:hover {
  467. text-decoration: underline;
  468. }
  469. }
  470. // 状态样式
  471. .status_container {
  472. display: flex;
  473. align-items: center;
  474. justify-content: center;
  475. gap: 4px;
  476. }
  477. .status_dot {
  478. width: 8px;
  479. height: 8px;
  480. border-radius: 50%;
  481. &.active {
  482. background: #2BC644;
  483. }
  484. &.disabled {
  485. background: #F56C6C;
  486. }
  487. }
  488. .status_active {
  489. color: #2BC644;
  490. }
  491. .status_disabled {
  492. color: #F56C6C;
  493. }
  494. .enter_button {
  495. color: #2E64FA;
  496. cursor: pointer;
  497. &:hover {
  498. text-decoration: underline;
  499. }
  500. }
  501. .editable_text {
  502. color: #2E64FA;
  503. cursor: pointer;
  504. &:hover {
  505. text-decoration: underline;
  506. }
  507. }
  508. .save_icon {
  509. cursor: pointer;
  510. color: #2E64FA;
  511. font-size: 16px;
  512. &:hover {
  513. color: #1d4ed8;
  514. }
  515. &.saving {
  516. cursor: not-allowed;
  517. opacity: 0.5;
  518. }
  519. }
  520. </style>