SearchBar.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. <template>
  2. <div class="ep-topbar" @click="visible = false">
  3. <div class="ep-topbar-content">
  4. <div class="time-btns">
  5. <div :class="currentTime === 'week' ? 'primary' : 'default'" @click="selectTime('week')">近一周</div>
  6. <div :class="currentTime === 'month' ? 'primary' : 'default'" @click="selectTime('month')">近一月</div>
  7. <div :class="currentTime === 'quarter' ? 'primary' : 'default'" @click="selectTime('quarter')">近三月</div>
  8. <div :class="currentTime === 'year' ? 'primary' : 'default'" @click="selectTime('year')">近一年</div>
  9. <div :class="currentTime === 'custom' ? 'primary' : 'default'" @click="selectTime('custom')">自定义时间范围</div>
  10. <div class="custom-style-date-picker-wrap" v-if="currentTime === 'custom'">
  11. <el-date-picker class="custom-style-date-picker" v-model="dateRange" type="daterange" range-separator="至"
  12. start-placeholder="开始" end-placeholder="结束" style="width:320px" @change="() => searchHandler()" />
  13. </div>
  14. </div>
  15. <el-popover class="popover" title="" :visible="visible" placement="bottom-start" trigger="click" width="45vw">
  16. <template #reference>
  17. <div class="primary" style="border-radius: 6px;" @click.stop="visible = !visible">{{ props.deptType === 'user' ?
  18. '组织架构/模糊搜索' : '部门选择' }}</div>
  19. </template>
  20. <div class="custom-el-style">
  21. <div>
  22. <el-autocomplete v-model="personName" :fetch-suggestions="props.deptType === 'user' ? queryUsers : queryDept"
  23. :placeholder="props.deptType === 'user' ? '搜索员工/团队画像' : '搜索部门'" style="width:320px;" clearable>
  24. <template #suffix><el-icon>
  25. <Search />
  26. </el-icon></template>
  27. <template #default="{ item }">
  28. <span>{{ item.nickName || item.deptName }}</span>
  29. <span v-if="item.deptName && item.nickName" style="font-size:12px;color:#999;margin-left:8px">{{
  30. item.deptName }}</span>
  31. </template>
  32. </el-autocomplete>
  33. <div class="primary" style="border-radius: 6px;" @click="() => searchHandler()">模糊搜索</div>
  34. </div>
  35. <div style="margin-top: 20px;">
  36. <el-tree :data="departments" :default-expanded-keys="[100]" :props="{ value: 'id', showPrefix: false }"
  37. accordion @node-click="handleNodeClick" />
  38. </div>
  39. </div>
  40. </el-popover>
  41. </div>
  42. </div>
  43. </template>
  44. <script setup>
  45. import { onMounted, onUnmounted } from 'vue'
  46. import { searchPortraitUsers } from '@/api/score/index'
  47. import { getDeptList,getDeptUserTree } from '@/api/item/items'
  48. import { listDept } from '@/api/system/dept'
  49. const props = defineProps({
  50. deptType: {
  51. type: String,
  52. default: 'user'
  53. }
  54. })
  55. const visible = defineModel('visible', false)
  56. const emit = defineEmits(['search'])
  57. const currentTime = ref('year')
  58. const personName = ref('')
  59. const departments = ref([])
  60. const dateRange = ref([])
  61. const queryUsers = async (query, cb) => {
  62. if (!query?.trim()) { cb([]); return }
  63. try {
  64. const res = await searchPortraitUsers(query.trim())
  65. cb((res.data || []).map(u => ({ ...u, value: u.nickName })))
  66. } catch (_) { cb([]) }
  67. }
  68. const queryDept = async (query, cb) => {
  69. if (!query?.trim()) { cb([]); return }
  70. try {
  71. const res = await listDept({ deptName: query.trim(), deptType: props.deptType })
  72. cb((res.data || []).map(d => ({ ...d, value: d.deptName })))
  73. } catch (_) { cb([]) }
  74. }
  75. const filterDeptTree = (data, targetType) => {
  76. const filterNode = (node) => {
  77. if (node.deptType === targetType) {
  78. return { ...node, children: undefined }
  79. }
  80. if (node.children && node.children.length > 0) {
  81. const filteredChildren = node.children.map(child => filterNode(child)).filter(Boolean)
  82. if (filteredChildren.length > 0) {
  83. return { ...node, children: filteredChildren }
  84. }
  85. }
  86. return null
  87. }
  88. return data.map(node => filterNode(node)).filter(Boolean)
  89. }
  90. const getTimeRange = () => {
  91. if (currentTime.value === 'custom') {
  92. if (dateRange.value?.length === 2) {
  93. if(props.deptType === 'user'){
  94. return { beginTime: formatDate(dateRange.value[0]), endTime: formatDate(dateRange.value[1]) }
  95. }else{
  96. return { startDate: formatDate(dateRange.value[0]), endDate: formatDate(dateRange.value[1]) }
  97. }
  98. }
  99. return {}
  100. }
  101. const end = new Date(), begin = new Date()
  102. if (currentTime.value === 'week') begin.setDate(end.getDate() - 7)
  103. else if (currentTime.value === 'month') begin.setMonth(end.getMonth() - 1)
  104. else if (currentTime.value === 'quarter') begin.setMonth(end.getMonth() - 3)
  105. else begin.setFullYear(end.getFullYear() - 1)
  106. if(props.deptType === 'user'){
  107. return { beginTime: formatDate(begin), endTime: formatDate(end) }
  108. }else{
  109. return { startDate: formatDate(begin), endDate: formatDate(end) }
  110. }
  111. }
  112. const formatDate = (d) => {
  113. const dt = new Date(d)
  114. return `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, '0')}-${String(dt.getDate()).padStart(2, '0')}`
  115. }
  116. const selectTime = (t) => {
  117. currentTime.value = t
  118. if (t !== 'custom') searchHandler()
  119. }
  120. const searchHandler = (query = {}) => {
  121. const queryParams = { ...getTimeRange(), ...query }
  122. if (props.deptType === 'user') {
  123. if (queryParams.personName) {
  124. emit('search', queryParams)
  125. }
  126. } else {
  127. emit('search', queryParams)
  128. visible.value = false
  129. }
  130. }
  131. const handleNodeClick = (node) => {
  132. if (props.deptType === 'user') {
  133. if (node.nodeType === 'user') {
  134. personName.value = node.label
  135. searchHandler({ personName: node.label })
  136. }
  137. } else {
  138. if (node.deptType === props.deptType) {
  139. personName.value = node.deptName || node.name || node.label;
  140. let obj = {}
  141. if (props.deptType === 'BRIGADE'||props.deptType === 'STATION') {
  142. obj = { deptId: node.deptId || node.id }
  143. }
  144. if (props.deptType === 'MANAGER') {
  145. obj = { teamId: node.deptId || node.id, deptId: node.deptId || node.id }
  146. }
  147. if (props.deptType === 'TEAMS') {
  148. obj = { groupId: node.deptId || node.id, deptId: node.deptId || node.id }
  149. }
  150. searchHandler(obj)
  151. }
  152. }
  153. }
  154. const setStyle = () => {
  155. const root = document.querySelector(':root')
  156. root.style.setProperty('--el-bg-color-overlay', '#1c1936')
  157. root.style.setProperty('--el-border-color-extra-light', '#5c676d')
  158. root.style.setProperty('--el-border-color-light', 'transparent')
  159. root.style.setProperty('--el-popover-padding', '0px')
  160. root.style.setProperty('--el-text-color-regular', '#fff')
  161. root.style.setProperty('--el-fill-color-light', '#5c676d')
  162. }
  163. const resetStyle = () => {
  164. const root = document.querySelector(':root')
  165. root.style.setProperty('--el-bg-color-overlay', '#ffffff')
  166. root.style.setProperty('--el-border-color-extra-light', '#f2f6fc')
  167. root.style.setProperty('--el-border-color-light', '#e4e7ed')
  168. root.style.setProperty('--el-popover-padding', '12px')
  169. root.style.setProperty('--el-text-color-regular', '#606266')
  170. root.style.setProperty('--el-fill-color-light', '#f5f7fa')
  171. }
  172. onMounted(async () => {
  173. setStyle()
  174. if (props.deptType === 'user') {
  175. const res = await getDeptUserTree()
  176. departments.value = res.data
  177. } else {
  178. const res = await getDeptList({ deptType: props.deptType })
  179. departments.value = filterDeptTree(res.data, props.deptType)
  180. }
  181. })
  182. onUnmounted(() => {
  183. resetStyle()
  184. })
  185. defineExpose({
  186. getDefQuery() {
  187. return {
  188. ...getTimeRange(),
  189. }
  190. }
  191. })
  192. </script>
  193. <style lang="scss" scoped>
  194. .ep-topbar {
  195. height: 60px;
  196. position: relative;
  197. .ep-topbar-content {
  198. position: absolute;
  199. padding: 0 15px;
  200. inset: 0;
  201. display: flex;
  202. align-items: center;
  203. justify-content: flex-start;
  204. box-sizing: border-box;
  205. gap: 12px;
  206. flex-wrap: wrap;
  207. z-index: 1000;
  208. }
  209. .time-btns {
  210. display: flex;
  211. align-items: center;
  212. gap: 10px;
  213. flex-wrap: wrap;
  214. margin-right: 40px;
  215. &>div {
  216. padding: 8px 24px;
  217. border-radius: 28px;
  218. font-weight: 500;
  219. }
  220. }
  221. .primary {
  222. background: linear-gradient(125deg, #2AB3E6, #9605FC);
  223. color: #fff;
  224. padding: 8px 24px;
  225. }
  226. .default {
  227. background: #1C1936;
  228. color: #4C4C93;
  229. position: relative;
  230. &::before {
  231. content: '';
  232. position: absolute;
  233. inset: -1px;
  234. border-radius: 29px;
  235. background: linear-gradient(150deg, #0f46fa 0%, #bd03fb 100%);
  236. filter: brightness(2) contrast(150%);
  237. z-index: -1;
  238. opacity: 0.5;
  239. }
  240. }
  241. .custom-style-date-picker-wrap {
  242. position: relative;
  243. border-radius: 19px;
  244. height: 38px;
  245. padding: 0 !important;
  246. --el-text-color-primary: #fff;
  247. &::before {
  248. content: '';
  249. position: absolute;
  250. inset: -1px;
  251. border-radius: 19px;
  252. background: linear-gradient(150deg, #0f46fa 0%, #bd03fb 100%);
  253. filter: brightness(2) contrast(150%);
  254. z-index: -1;
  255. opacity: 0.5;
  256. }
  257. }
  258. }
  259. </style>
  260. <style lang="scss">
  261. .custom-el-style {
  262. width: 100%;
  263. padding: 10px;
  264. position: relative;
  265. background: #1c1936;
  266. border-radius: 8px;
  267. --el-text-color-regular: #fff;
  268. --el-fill-color-blank: #17122A;
  269. --el-border-color: linear-gradient(150deg, #0f46fa 0%, #bd03fb 100%);
  270. --el-border-color-hover: linear-gradient(150deg, #0f46fa 0%, #bd03fb 100%);
  271. .primary {
  272. background: linear-gradient(125deg, #2AB3E6, #9605FC);
  273. color: #fff;
  274. padding: 6px 20px;
  275. display: inline-block;
  276. margin-left: 15px;
  277. }
  278. &::before {
  279. content: '';
  280. position: absolute;
  281. inset: -2px;
  282. border-radius: 10px;
  283. background: linear-gradient(150deg, #0f46fa 0%, #0f46fa 10%, #020E15, #020E15, #020E15, #bd03fb 90%, #bd03fb 100%);
  284. filter: brightness(2) contrast(150%);
  285. z-index: -2;
  286. opacity: 0.5;
  287. }
  288. .el-input__wrapper {}
  289. .el-tree {
  290. background-color: #1c1936;
  291. color: #fff;
  292. .el-tree-node__content {
  293. --el-tree-node-hover-bg-color: #5c676d;
  294. }
  295. }
  296. }
  297. .el-autocomplete-suggestion {
  298. background: #17122A;
  299. }
  300. .custom-style-date-picker {
  301. background: #1C1936 !important;
  302. box-shadow: none !important;
  303. height: 38px !important;
  304. border-radius: 19px;
  305. }
  306. .el-popper {
  307. --el-popover-padding: 0px;
  308. }
  309. </style>