index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  1. <template>
  2. <view class="dimension-page">
  3. <!-- 页面标题 -->
  4. <view class="page-header">
  5. <view class="header-title">员工维度评分明细</view>
  6. </view>
  7. <!-- 筛选栏 -->
  8. <view class="filter-bar">
  9. <scroll-view scroll-x class="time-scroll">
  10. <view class="time-tags">
  11. <view v-for="(tag, index) in timeTags" :key="index"
  12. :class="['time-tag', { active: selectedTimeTag === index }]" @click="onTimeTagClick(index)">
  13. {{ tag }}
  14. </view>
  15. </view>
  16. </scroll-view>
  17. <view class="date-range-picker">
  18. <picker mode="date" :value="beginTime" @change="onBeginTimeChange" class="date-picker-half">
  19. <view class="date-input" :class="{ filled: beginTime }">
  20. {{ beginTime || '开始日期' }}
  21. </view>
  22. </picker>
  23. <text class="date-separator">至</text>
  24. <picker mode="date" :value="endTime" @change="onEndTimeChange" class="date-picker-half">
  25. <view class="date-input" :class="{ filled: endTime }">
  26. {{ endTime || '结束日期' }}
  27. </view>
  28. </picker>
  29. </view>
  30. <view class="filter-row">
  31. <view class="filter-select" @click="showOrgPicker = true">
  32. <text class="filter-select-text">{{ selectedOrgName || '组织架构/员工' }}</text>
  33. <u-icon name="arrow-down" size="14" color="#999"></u-icon>
  34. </view>
  35. <view class="filter-select" @click="showDimPicker = true">
  36. <text class="filter-select-text">{{ selectedDimName || '维度' }}</text>
  37. <u-icon name="arrow-down" size="14" color="#999"></u-icon>
  38. </view>
  39. </view>
  40. <view class="filter-actions">
  41. <view class="btn-search" @click="handleSearch">
  42. <u-icon name="search" size="16" color="#fff"></u-icon>
  43. <text>搜索</text>
  44. </view>
  45. <view class="btn-reset" @click="handleReset">重置</view>
  46. </view>
  47. </view>
  48. <!-- 数据表格 -->
  49. <view class="section-area">
  50. <view class="section-header">
  51. <text class="section-title">员工维度评分明细</text>
  52. <text class="section-badge">评分依据:员工配分表</text>
  53. </view>
  54. <view class="table-wrapper">
  55. <statistic-table :columns="tableColumns" :data="tableData" />
  56. <view class="loading-mask" v-if="loading">
  57. <u-loading-icon size="28"></u-loading-icon>
  58. <text class="loading-text">加载中...</text>
  59. </view>
  60. </view>
  61. <view class="pagination-wrapper" v-if="total > 0">
  62. <uni-pagination :current="pageNum" :total="total" :page-size="pageSize" @change="onPageChange" />
  63. </view>
  64. </view>
  65. <!-- 组织架构选择弹窗 -->
  66. <u-popup :show="showOrgPicker" mode="bottom" :round="16" :mask-close-able="true"
  67. @close="showOrgPicker = false">
  68. <view class="picker-popup">
  69. <view class="picker-header">
  70. <text class="picker-title">选择组织架构/员工</text>
  71. <u-icon name="close" size="20" @click="showOrgPicker = false"></u-icon>
  72. </view>
  73. <view class="search-box">
  74. <u-input v-model="orgSearchKeyword" placeholder="搜索" @input="onOrgSearch"></u-input>
  75. </view>
  76. <scroll-view v-if="!orgSearchKeyword.trim()" scroll-y class="tree-list">
  77. <employee-tree-node v-for="(node, index) in deptTreeData" :key="node.id" :node="node" :expanded-ids="expandedDeptIds"
  78. :selected-id="selectedOrgId" :selectable="true" @toggle="toggleDeptExpand" @select="onOrgSelect" />
  79. </scroll-view>
  80. <scroll-view v-else scroll-y class="org-list">
  81. <view class="org-item" v-for="item in filteredOrgList" :key="item.userId"
  82. @click="onOrgSelect(item)">
  83. <text class="org-item-name">{{ item.nickName }}</text>
  84. <u-icon v-if="item.userId === selectedOrgId" name="checkmark" color="#34D399"
  85. size="18"></u-icon>
  86. </view>
  87. </scroll-view>
  88. </view>
  89. </u-popup>
  90. <!-- 维度选择弹窗 -->
  91. <u-popup :show="showDimPicker" mode="bottom" :round="16" :mask-close-able="true"
  92. @close="showDimPicker = false">
  93. <view class="dim-popup">
  94. <view class="picker-header">
  95. <text class="picker-title">选择维度</text>
  96. <u-icon name="close" size="20" @click="showDimPicker = false"></u-icon>
  97. </view>
  98. <view class="dim-list">
  99. <view class="dim-item" :class="{ active: selectedDimension === '' }" @click="selectDimension('')">
  100. <text>全部</text>
  101. </view>
  102. <view class="dim-item" v-for="item in dimensionOptions" :key="item.value"
  103. :class="{ active: selectedDimension === item.value }" @click="selectDimension(item.value)">
  104. <text>{{ item.label }}</text>
  105. </view>
  106. </view>
  107. </view>
  108. </u-popup>
  109. </view>
  110. </template>
  111. <script>
  112. import StatisticTable from '@/components/statistic-table/statistic-table.vue'
  113. import EmployeeTreeNode from '@/pages/components/EmployeeTreeNode.vue'
  114. import { getEmployeeDimensionDetails, getDimensionAll } from '@/api/warningManage/index'
  115. import { getDeptUserTree } from '@/api/system/user'
  116. export default {
  117. name: 'EmployeeDimensionDetails',
  118. components: {
  119. StatisticTable,
  120. EmployeeTreeNode
  121. },
  122. data() {
  123. return {
  124. selectedTimeTag: 1,
  125. timeTags: ['近一周', '近一月', '近三月', '近一年'],
  126. beginTime: '',
  127. endTime: '',
  128. selectedOrgId: null,
  129. selectedOrgName: '',
  130. selectedDeptType: '',
  131. selectedDimension: '',
  132. selectedDimName: '全部',
  133. showOrgPicker: false,
  134. showDimPicker: false,
  135. orgSearchKeyword: '',
  136. deptTreeData: [],
  137. expandedDeptIds: [],
  138. orgList: [],
  139. dimensionOptions: [],
  140. tableColumns: [
  141. { props: 'userId', title: '员工ID' },
  142. { props: 'personName', title: '姓名' },
  143. { props: 'deptName', title: '部门' },
  144. { props: 'teamName', title: '班组' },
  145. { props: 'groupName', title: '小组' },
  146. { props: 'dimensionName', title: '维度名称' },
  147. { props: 'dimensionScore', title: '维度分值' }
  148. ],
  149. tableData: [],
  150. allTableData: [],
  151. pageNum: 1,
  152. pageSize: 10,
  153. total: 0,
  154. loading: false
  155. }
  156. },
  157. computed: {
  158. filteredOrgList() {
  159. const keyword = this.orgSearchKeyword.trim().toLowerCase()
  160. if (!keyword) return this.orgList
  161. return this.orgList.filter(item =>
  162. (item.nickName || '').toLowerCase().includes(keyword)
  163. )
  164. }
  165. },
  166. mounted() {
  167. this.loadDeptTree()
  168. this.loadDimensionOptions()
  169. this.fetchData()
  170. },
  171. methods: {
  172. formatDate(date) {
  173. const y = date.getFullYear()
  174. const m = String(date.getMonth() + 1).padStart(2, '0')
  175. const d = String(date.getDate()).padStart(2, '0')
  176. return `${y}-${m}-${d}`
  177. },
  178. getDateRange() {
  179. const now = new Date()
  180. let start = new Date(now)
  181. switch (this.selectedTimeTag) {
  182. case 0: start.setDate(now.getDate() - 7); break
  183. case 1: start.setMonth(now.getMonth() - 1); break
  184. case 2: start.setMonth(now.getMonth() - 3); break
  185. case 3: start.setFullYear(now.getFullYear() - 1); break
  186. default: break
  187. }
  188. return { startDate: this.formatDate(start), endDate: this.formatDate(now) }
  189. },
  190. onTimeTagClick(index) {
  191. this.selectedTimeTag = index
  192. },
  193. onBeginTimeChange(e) {
  194. this.beginTime = e.detail.value
  195. },
  196. onEndTimeChange(e) {
  197. this.endTime = e.detail.value
  198. },
  199. flattenDeptTree(tree) {
  200. const result = []
  201. const traverse = (nodes) => {
  202. nodes.forEach(node => {
  203. const hasChildren = node.children && node.children.length > 0
  204. if (!hasChildren && node.nodeType !== 'dept') {
  205. result.push({
  206. nodeType: node.nodeType || '',
  207. userId: node.userId || node.id,
  208. nickName: node.nickName || node.label || node.userName || ''
  209. })
  210. }
  211. if (hasChildren) {
  212. traverse(node.children)
  213. }
  214. })
  215. }
  216. traverse(tree)
  217. return result
  218. },
  219. expandAllDepts(nodes) {
  220. this.expandedDeptIds = []
  221. const traverse = (list) => {
  222. list.forEach(node => {
  223. const hasChildren = node.children && node.children.length > 0
  224. if (hasChildren || node.nodeType === 'dept') {
  225. this.expandedDeptIds.push(node.id)
  226. if (hasChildren) {
  227. traverse(node.children)
  228. }
  229. }
  230. })
  231. }
  232. traverse(nodes)
  233. },
  234. loadDeptTree() {
  235. getDeptUserTree({}).then(res => {
  236. if (res.code === 200) {
  237. this.deptTreeData = res.data || []
  238. this.orgList = this.flattenDeptTree(this.deptTreeData).filter(u => u.nodeType === 'user')
  239. this.expandAllDepts(this.deptTreeData)
  240. }
  241. }).catch(() => { })
  242. },
  243. toggleDeptExpand(id) {
  244. const index = this.expandedDeptIds.indexOf(id)
  245. if (index > -1) {
  246. this.expandedDeptIds.splice(index, 1)
  247. } else {
  248. this.expandedDeptIds.push(id)
  249. }
  250. },
  251. onOrgSelect(item) {
  252. this.selectedOrgId = item.userId
  253. this.selectedOrgName = item.nickName
  254. this.selectedDeptType = item.deptType || ''
  255. this.showOrgPicker = false
  256. },
  257. onOrgSearch() { },
  258. selectDimension(value) {
  259. this.selectedDimension = value
  260. const item = this.dimensionOptions.find(d => d.value === value)
  261. this.selectedDimName = value === '' ? '全部' : (item ? item.label : '')
  262. this.showDimPicker = false
  263. },
  264. handleSearch() {
  265. this.pageNum = 1
  266. this.fetchData()
  267. },
  268. handleReset() {
  269. this.selectedTimeTag = 1
  270. this.beginTime = ''
  271. this.endTime = ''
  272. this.selectedOrgId = null
  273. this.selectedOrgName = ''
  274. this.selectedDeptType = ''
  275. this.selectedDimension = ''
  276. this.selectedDimName = '全部'
  277. this.pageNum = 1
  278. this.loadDimensionOptions()
  279. this.fetchData()
  280. },
  281. onPageChange(e) {
  282. this.pageNum = e.current
  283. this.updatePagedData()
  284. },
  285. async fetchData() {
  286. this.loading = true
  287. let params = this.getQueryParams()
  288. try {
  289. const res = await getEmployeeDimensionDetails(params)
  290. if (res.code === 200 && res.data) {
  291. const data = res.data
  292. this.allTableData = data.list || data || []
  293. this.pageNum = 1
  294. this.updatePagedData()
  295. }
  296. } catch (e) { } finally {
  297. this.loading = false
  298. }
  299. },
  300. updatePagedData() {
  301. const data = this.allTableData
  302. this.total = data.length
  303. const start = (this.pageNum - 1) * this.pageSize
  304. const end = start + this.pageSize
  305. this.tableData = data.slice(start, end)
  306. },
  307. getQueryParams() {
  308. let params = {}
  309. if (this.beginTime && this.endTime) {
  310. params.startDate = this.beginTime
  311. params.endDate = this.endTime
  312. } else {
  313. const range = this.getDateRange()
  314. params.startDate = range.startDate
  315. params.endDate = range.endDate
  316. }
  317. if (this.selectedOrgId) {
  318. const key = this.getOrgParamKey()
  319. if (key){
  320. params[key] = this.selectedOrgId
  321. }
  322. }
  323. if (this.selectedDimension) {
  324. params.dimensionId = this.selectedDimension
  325. }
  326. return params
  327. },
  328. getOrgParamKey() {
  329. const deptType = this.selectedDeptType
  330. if (deptType === 'BRIGADE') return 'deptId'
  331. if (deptType === 'MANAGER') return 'teamId'
  332. if (deptType === 'TEAMS') return 'groupId'
  333. if (deptType === 'user') return 'userId'
  334. return ''
  335. },
  336. async loadDimensionOptions() {
  337. try {
  338. const res = await getDimensionAll({ pageNum: 1, pageSize: 100, org: 4 })
  339. if (res.code === 200 && res.data) {
  340. this.dimensionOptions = (res.data || []).map(item => ({
  341. label: item.name || item.label || item.dimensionName || '',
  342. value: item.id != null ? String(item.id) : (item.value || item.name || '')
  343. }))
  344. }
  345. } catch (e) { }
  346. }
  347. }
  348. }
  349. </script>
  350. <style lang="scss" scoped>
  351. .dimension-page {
  352. min-height: 100vh;
  353. background: #f5f7fa;
  354. padding-bottom: 40rpx;
  355. }
  356. .page-header {
  357. background: #fff;
  358. padding: 24rpx 32rpx;
  359. border-bottom: 1rpx solid #eee;
  360. }
  361. .header-title {
  362. font-size: 36rpx;
  363. font-weight: bold;
  364. color: #1e3c72;
  365. }
  366. .filter-bar {
  367. background: #fff;
  368. margin: 20rpx;
  369. border-radius: 16rpx;
  370. padding: 20rpx;
  371. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
  372. }
  373. .time-scroll {
  374. width: 100%;
  375. margin-bottom: 16rpx;
  376. }
  377. .time-tags {
  378. display: flex;
  379. gap: 0;
  380. width: 100%;
  381. }
  382. .time-tag {
  383. flex: 1;
  384. text-align: center;
  385. padding: 12rpx 0;
  386. background: #f8fafc;
  387. border: 1rpx solid #e2e8f0;
  388. font-size: 24rpx;
  389. color: #333;
  390. &.active {
  391. background: #2563eb;
  392. border-color: #2563eb;
  393. color: #fff;
  394. }
  395. }
  396. .date-range-picker {
  397. display: flex;
  398. align-items: center;
  399. width: 100%;
  400. margin-top: 16rpx;
  401. margin-bottom: 16rpx;
  402. }
  403. .date-picker-half {
  404. flex: 1;
  405. }
  406. .date-input {
  407. width: 100%;
  408. padding: 12rpx 16rpx;
  409. border-radius: 8rpx;
  410. background: #f5f5f5;
  411. font-size: 24rpx;
  412. color: #999;
  413. text-align: center;
  414. box-sizing: border-box;
  415. &.filled {
  416. color: #333;
  417. }
  418. }
  419. .date-separator {
  420. font-size: 24rpx;
  421. color: #999;
  422. flex-shrink: 0;
  423. padding: 0 12rpx;
  424. }
  425. .filter-row {
  426. display: flex;
  427. gap: 12rpx;
  428. margin-bottom: 16rpx;
  429. }
  430. .filter-select {
  431. flex: 1;
  432. display: flex;
  433. align-items: center;
  434. justify-content: space-between;
  435. padding: 16rpx 20rpx;
  436. background: #f5f5f5;
  437. border-radius: 8rpx;
  438. border: 1rpx solid #e0e0e0;
  439. }
  440. .filter-select-text {
  441. font-size: 26rpx;
  442. color: #333;
  443. }
  444. .filter-actions {
  445. display: flex;
  446. gap: 16rpx;
  447. }
  448. .btn-search {
  449. flex: 1;
  450. display: flex;
  451. align-items: center;
  452. justify-content: center;
  453. gap: 8rpx;
  454. padding: 16rpx;
  455. background: #2563eb;
  456. border-radius: 8rpx;
  457. color: #fff;
  458. font-size: 28rpx;
  459. }
  460. .btn-reset {
  461. padding: 16rpx 32rpx;
  462. background: #f5f5f5;
  463. border-radius: 8rpx;
  464. color: #666;
  465. font-size: 28rpx;
  466. text-align: center;
  467. }
  468. .section-area {
  469. margin: 0 20rpx;
  470. }
  471. .section-header {
  472. display: flex;
  473. align-items: center;
  474. gap: 16rpx;
  475. margin-bottom: 16rpx;
  476. }
  477. .section-title {
  478. font-size: 32rpx;
  479. font-weight: bold;
  480. color: #dc2626;
  481. }
  482. .section-badge {
  483. font-size: 22rpx;
  484. background: #eef2ff;
  485. padding: 6rpx 20rpx;
  486. border-radius: 30rpx;
  487. color: #666;
  488. }
  489. .table-wrapper {
  490. background: #fff;
  491. border-radius: 16rpx;
  492. overflow-x: auto;
  493. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
  494. position: relative;
  495. min-height: 200rpx;
  496. }
  497. .loading-mask {
  498. position: absolute;
  499. top: 0;
  500. left: 0;
  501. right: 0;
  502. bottom: 0;
  503. background: rgba(255, 255, 255, 0.8);
  504. display: flex;
  505. flex-direction: column;
  506. align-items: center;
  507. justify-content: center;
  508. gap: 16rpx;
  509. z-index: 10;
  510. }
  511. .loading-text {
  512. font-size: 24rpx;
  513. color: #999;
  514. }
  515. .pagination-wrapper {
  516. display: flex;
  517. justify-content: center;
  518. padding: 20rpx 0;
  519. }
  520. .picker-popup,
  521. .dim-popup {
  522. background: #fff;
  523. border-radius: 16rpx 16rpx 0 0;
  524. max-height: 70vh;
  525. display: flex;
  526. flex-direction: column;
  527. }
  528. .picker-header {
  529. display: flex;
  530. justify-content: space-between;
  531. align-items: center;
  532. padding: 24rpx 32rpx;
  533. border-bottom: 1rpx solid #eee;
  534. }
  535. .picker-title {
  536. font-size: 32rpx;
  537. font-weight: 600;
  538. color: #333;
  539. }
  540. .search-box {
  541. padding: 16rpx 32rpx;
  542. }
  543. .tree-list,
  544. .org-list {
  545. flex: 1;
  546. padding: 0 32rpx;
  547. height: 0;
  548. overflow: hidden;
  549. }
  550. .org-item {
  551. display: flex;
  552. align-items: center;
  553. justify-content: space-between;
  554. padding: 24rpx 0;
  555. border-bottom: 1rpx solid #f0f0f0;
  556. }
  557. .org-item-name {
  558. font-size: 28rpx;
  559. color: #333;
  560. }
  561. .dim-list {
  562. padding: 16rpx 32rpx;
  563. }
  564. .dim-item {
  565. padding: 24rpx 0;
  566. border-bottom: 1rpx solid #f0f0f0;
  567. font-size: 28rpx;
  568. color: #333;
  569. text-align: center;
  570. &.active {
  571. color: #2563eb;
  572. font-weight: 500;
  573. }
  574. }
  575. </style>