Explorar el Código

feat: 个人画像

lixiangrui hace 1 mes
padre
commit
ca70149e0d

BIN
src/assets/dataBigScreen/bg.png


BIN
src/assets/dataBigScreen/img01.png


BIN
src/assets/dataBigScreen/img02.png


BIN
src/assets/dataBigScreen/img03.png


BIN
src/assets/dataBigScreen/img04.png


BIN
src/assets/dataBigScreen/img05.png


BIN
src/assets/dataBigScreen/img06.png


BIN
src/assets/dataBigScreen/logo.png


BIN
src/assets/dataBigScreen/title_icon01.png


+ 114 - 0
src/views/portraitManagement/components/SearchBar.vue

@@ -0,0 +1,114 @@
1
+<template>
2
+  <div class="ep-topbar" @click="visible = false">
3
+    <div class="time-btns">
4
+      <el-button size="small" :type="currentTime==='week'?'primary':'default'" @click="selectTime('week')">近一周</el-button>
5
+      <el-button size="small" :type="currentTime==='month'?'primary':'default'" @click="selectTime('month')">近一月</el-button>
6
+      <el-button size="small" :type="currentTime==='quarter'?'primary':'default'" @click="selectTime('quarter')">近三月</el-button>
7
+      <el-button size="small" :type="currentTime==='year'?'primary':'default'" @click="selectTime('year')">近一年</el-button>
8
+      <el-button size="small" :type="currentTime==='custom'?'primary':'default'" @click="selectTime('custom')">自定义时间范围</el-button>
9
+      <el-date-picker
10
+        v-if="currentTime==='custom'"
11
+        v-model="dateRange"
12
+        type="daterange"
13
+        range-separator="至"
14
+        start-placeholder="开始"
15
+        end-placeholder="结束"
16
+        size="small"
17
+        style="margin-left:8px;width:240px"
18
+        @change="() => searchHandler()" />
19
+    </div>
20
+    <el-popover title="" :visible="visible" placement="bottom-end" trigger="click" width="60vw">
21
+      <template #reference>
22
+        <el-button type="primary" @click.stop="visible = !visible">组织架构/模糊搜索</el-button>
23
+      </template>
24
+      <div style="width:100%; padding: 15px;">
25
+        <div>
26
+          <el-autocomplete
27
+            v-model="personName"
28
+            :fetch-suggestions="queryUsers"
29
+            placeholder="搜索员工/团队画像"
30
+            style="width:320px"
31
+            clearable>
32
+            <template #suffix
33
+              ><el-icon><Search /></el-icon
34
+            ></template>
35
+            <template #default="{ item }">
36
+              <span>{{ item.nickName }}</span>
37
+              <span v-if="item.deptName" style="font-size:12px;color:#999;margin-left:8px">{{ item.deptName }}</span>
38
+            </template>
39
+          </el-autocomplete>
40
+          <el-button type="primary" style="margin-left: 30px;" @click="() => searchHandler()">模糊搜索</el-button>
41
+        </div>
42
+        <div style="margin-top: 20px;">
43
+          <el-tree :data="departments" :props="{ value: 'id', showPrefix: false }" accordion @node-click="handleNodeClick" />
44
+        </div>
45
+      </div>
46
+    </el-popover>
47
+  </div>
48
+</template>
49
+
50
+<script setup>
51
+const visible = defineModel('visible', false)
52
+const emit = defineEmits(['search'])
53
+const currentTime = ref('year')
54
+const personName = ref('')
55
+
56
+const queryUsers = async (query, cb) => {
57
+  if (!query?.trim()) { cb([]); return }
58
+  try {
59
+    const res = await searchPortraitUsers(query.trim())
60
+    cb((res.data || []).map(u => ({ ...u, value: u.nickName })))
61
+  } catch (_) { cb([]) }
62
+}
63
+
64
+const getTimeRange = () => {
65
+  if (currentTime.value === 'custom') {
66
+    if (dateRange.value?.length === 2) {
67
+      return { beginTime: formatDate(dateRange.value[0]), endTime: formatDate(dateRange.value[1]) }
68
+    }
69
+    return {}
70
+  }
71
+  const end = new Date(), begin = new Date()
72
+  if (currentTime.value === 'week') begin.setDate(end.getDate()-7)
73
+  else if (currentTime.value === 'month') begin.setMonth(end.getMonth()-1)
74
+  else if (currentTime.value === 'quarter') begin.setMonth(end.getMonth()-3)
75
+  else begin.setFullYear(end.getFullYear()-1)
76
+  return { beginTime: formatDate(begin), endTime: formatDate(end) }
77
+}
78
+
79
+const selectTime = (t) => {
80
+  currentTime.value = t
81
+  if (t !== 'custom') searchHandler()
82
+}
83
+
84
+const searchHandler = (query = {}) => {
85
+  emit('search', { ...getTimeRange(), personName: personName.value }, ...query)
86
+}
87
+
88
+const handleNodeClick = (node) => {
89
+  if (node.nodeType === 'user') {
90
+    searchHandler({ personName: node.label })
91
+  }
92
+}
93
+</script>
94
+
95
+<style lang="scss" scoped>
96
+.ep-topbar {
97
+  display: flex;
98
+  align-items: center;
99
+  justify-content: flex-start;
100
+  height: 90px;
101
+  padding: 15px;
102
+  box-sizing: border-box;
103
+  gap: 12px;
104
+  flex-wrap: wrap;
105
+
106
+  .time-btns {
107
+    display: flex;
108
+    align-items: center;
109
+    gap: 6px;
110
+    flex-wrap: wrap;
111
+    margin-right: 40px;
112
+  }
113
+}
114
+</style>

+ 93 - 0
src/views/portraitManagement/components/card.vue

@@ -0,0 +1,93 @@
1
+<template>
2
+  <div class="info-card">
3
+    <div class="card-inner">
4
+      <div class="card-header" v-if="title">
5
+        <div class="gradient-dot"></div>
6
+        <div class="card-title">{{ title }}</div>
7
+      </div>
8
+      <div class="card-content">
9
+        <slot></slot>
10
+      </div>
11
+    </div>
12
+  </div>
13
+</template>
14
+
15
+<script setup>
16
+const props = defineProps({
17
+  title: {
18
+    type: String
19
+  }
20
+})
21
+
22
+</script>
23
+
24
+<style lang="scss" scoped>
25
+
26
+  /* 卡片容器 - 实现渐变边框 */
27
+  .info-card {
28
+    position: relative;
29
+    width: 100%;
30
+    border-radius: 40px;
31
+    overflow: hidden;
32
+    filter: opacity(0.9);
33
+  }
34
+
35
+  /* 渐变边框伪元素 */
36
+  .info-card::before {
37
+    content: '';
38
+    position: absolute;
39
+    inset: 0;
40
+    border-radius: 40px;
41
+    background: linear-gradient(150deg, #0f46fa 0%, #0f46fa 10%, #020E15, #020E15, #020E15, #bd03fb 90%, #bd03fb 100%);
42
+    filter: brightness(2) contrast(150%);
43
+    z-index: -2;
44
+    opacity: 0.5;
45
+  }
46
+  /* 渐变边框伪元素 */
47
+  .info-card::after {
48
+    content: '';
49
+    position: absolute;
50
+    inset: 1px;
51
+    border-radius: 40px;
52
+    background: #21213a;
53
+    z-index: -1;
54
+  }
55
+
56
+  /* 卡片内部内容区 */
57
+  .card-inner {
58
+    border-radius: 39px;
59
+    padding: 15px 20px;
60
+    height: 100%;
61
+  }
62
+
63
+  /* 标题区域 */
64
+  .card-header {
65
+    display: flex;
66
+    align-items: center;
67
+    opacity: 1;
68
+    height: 32px;
69
+  }
70
+
71
+  .gradient-dot {
72
+    width: 25px;
73
+    height: 25px;
74
+    border-radius: 50%;
75
+    background: url('@/assets/dataBigScreen/title_icon01.png');
76
+  }
77
+
78
+  /* 标题文字 */
79
+  .card-title {
80
+    font-size: 24px;
81
+    font-weight: normal;
82
+    color: #ffffff;
83
+    margin-left: 10px;
84
+    line-height: 30px;
85
+  }
86
+
87
+  /* 内容文字 */
88
+  .card-content {
89
+    color: #fff;
90
+    font-size: 16px;
91
+    height: 100%;
92
+  }
93
+</style>

+ 72 - 0
src/views/portraitManagement/components/page.vue

@@ -0,0 +1,72 @@
1
+<template>
2
+  <div class="bg">
3
+    <div class="page">
4
+      <div class="title">{{ title }}</div>
5
+      <div class="content">
6
+        <slot></slot>
7
+      </div>
8
+    </div>
9
+  </div>
10
+
11
+  
12
+</template>
13
+
14
+<script setup>
15
+const props = defineProps({
16
+  title: {
17
+    type: String,
18
+    default: '安检人事管理可视化大屏'
19
+  }
20
+})
21
+</script>
22
+
23
+<style lang="scss" scoped>
24
+.bg {
25
+  height: 100vh;
26
+  width: 100%;
27
+  position: relative;
28
+  background-image: url('@/assets/dataBigScreen/bg.png');
29
+  &::before {
30
+    content: '';
31
+    position: absolute;
32
+    inset: 0;
33
+    background: linear-gradient(45deg, #231A43, #1c2d3e) ;
34
+    opacity: 0.8;
35
+    z-index: 1;
36
+  }
37
+  .page {
38
+    content: '';
39
+    position: absolute;
40
+    inset: 0;
41
+    z-index: 2;
42
+    display: flex;
43
+    flex-direction: column;
44
+  }
45
+  .title {
46
+    height: 88px;
47
+    line-height: 88px;
48
+    text-align: center;
49
+    color: #fff;
50
+    font-size: 48px;
51
+    background: #2b394d86;
52
+    position: relative;
53
+    margin-bottom: 2px;
54
+    font-weight: bold;
55
+    &::before {
56
+      content: '';
57
+      position: absolute;
58
+      top: 0;
59
+      left: 0;
60
+      right: 0;
61
+      bottom: -2px;
62
+      margin: auto;
63
+      background: linear-gradient(90deg, transparent, #0f46facd 40%, #8a06e8cd 60%, transparent);
64
+      filter: brightness(1.5);
65
+      z-index: -1;
66
+    }
67
+  }
68
+  .content {
69
+    flex: 1;
70
+  }
71
+}
72
+</style>

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 51 - 1270
src/views/portraitManagement/employeeProfile/index.vue


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1306 - 0
src/views/portraitManagement/employeeProfile/index1.vue