wangxx 4 kuukautta sitten
vanhempi
commit
d3ad120a83

+ 39 - 0
src/components/HeadTitle.vue

@@ -0,0 +1,39 @@
1
+<template>
2
+  <div class="head-title">
3
+    {{ title }}
4
+    <div v-if="subTitle" class="sub-title">{{ subTitle }}</div>
5
+  </div>
6
+</template>
7
+
8
+<script>
9
+export default {
10
+  props: {
11
+    title: {
12
+      type: String,
13
+      default: ''
14
+    },
15
+    subTitle: {
16
+      type: String,
17
+      default: ''
18
+    }
19
+  }
20
+}
21
+</script>
22
+
23
+<style lang="scss" scoped>
24
+.head-title {
25
+  padding-top: 20rpx;
26
+  font-weight: bold;
27
+  font-size: 32rpx;
28
+  color: #3D3D3D;
29
+  line-height: 44rpx;
30
+
31
+  .sub-title {
32
+    margin: 8rpx 0 4rpx;
33
+    font-weight: 400;
34
+    font-size: 24rpx;
35
+    color: #999999;
36
+    line-height: 28rpx;
37
+  }
38
+}
39
+</style>

+ 96 - 0
src/components/HomeContainer.vue

@@ -0,0 +1,96 @@
1
+<template>
2
+  <div class="home-container" :style="customHomeStyle" ref="containerRef">
3
+    <div class="container" :style="customStyle">
4
+      <slot></slot>
5
+    </div>
6
+  </div>
7
+</template>
8
+
9
+<script>
10
+export default {
11
+  props: {
12
+    customHomeStyle: {
13
+      type: Object,
14
+      default: () => ({})
15
+    },
16
+    customStyle: {
17
+      type: Object,
18
+      default: () => ({})
19
+    }
20
+  },
21
+  data() {
22
+    return {
23
+      scrollTimer: null
24
+    }
25
+  },
26
+  mounted() {
27
+    this.$nextTick(() => {
28
+      this.addScrollListener()
29
+    })
30
+  },
31
+  beforeDestroy() {
32
+    this.removeScrollListener()
33
+  },
34
+  methods: {
35
+    addScrollListener() {
36
+      const container = this.$refs.containerRef
37
+      if (container) {
38
+        container.addEventListener('scroll', this.handleScroll)
39
+      }
40
+    },
41
+    removeScrollListener() {
42
+      const container = this.$refs.containerRef
43
+      if (container) {
44
+        container.removeEventListener('scroll', this.handleScroll)
45
+      }
46
+      if (this.scrollTimer) {
47
+        clearTimeout(this.scrollTimer)
48
+      }
49
+    },
50
+    handleScroll() {
51
+      // 触发滚动事件
52
+      this.$emit('scroll')
53
+      
54
+      // 清除之前的定时器
55
+      if (this.scrollTimer) {
56
+        clearTimeout(this.scrollTimer)
57
+      }
58
+      
59
+      // 设置新的定时器,停止滚动后触发停止事件
60
+      this.scrollTimer = setTimeout(() => {
61
+        this.$emit('scroll-end')
62
+      }, 200)
63
+    },
64
+    
65
+    // 滚动到顶部方法
66
+    scrollToTop() {
67
+      const container = this.$refs.containerRef
68
+      if (container) {
69
+        container.scrollTo({
70
+          top: 0,
71
+          behavior: 'smooth'
72
+        })
73
+      }
74
+    }
75
+  }
76
+}
77
+</script>
78
+
79
+<style lang="scss" scoped>
80
+.home-container {
81
+  height: 100%;
82
+  overflow: auto;
83
+  background: #FFFFFF;
84
+
85
+  .container {
86
+    width: 100%;
87
+    padding: 20rpx;
88
+    min-height: calc(100vh - 80rpx);
89
+    overflow: auto;
90
+    box-sizing: border-box;
91
+    background: url("../static/images/home-bg.png") no-repeat;
92
+    background-size: 100% auto;
93
+    
94
+  }
95
+}
96
+</style>

+ 52 - 0
src/components/UserInfo.vue

@@ -0,0 +1,52 @@
1
+<template>
2
+  <div>
3
+    <div class="user-info">
4
+      亲爱的,{{ name }}
5
+    </div>
6
+
7
+    <div v-if="subTitle || $scopedSlots.default" class="sub-title">
8
+      <slot>{{ subTitle }}</slot>
9
+    </div>
10
+  </div>
11
+</template>
12
+
13
+<script>
14
+import userIcon from "@/static/images/user-icon.png";
15
+
16
+export default {
17
+  props: {
18
+    name: {
19
+      type: String,
20
+      default: ""
21
+    },
22
+    subTitle: {
23
+      type: String,
24
+      default: ""
25
+    }
26
+  },
27
+  data() {
28
+    return {
29
+      userIcon,
30
+    }
31
+  },
32
+}
33
+</script>
34
+
35
+<style lang="scss" scoped>
36
+.user-info {
37
+  padding: 4rpx 0 4rpx 130rpx;
38
+  font-weight: 400;
39
+  font-size: 48rpx;
40
+  color: #FFFFFF;
41
+  background: url("../static/images/user-icon.png") no-repeat;
42
+  background-size: auto 100%;
43
+}
44
+
45
+.sub-title {
46
+  padding: 28rpx 0;
47
+  font-weight: 400;
48
+  font-size: 28rpx;
49
+  color: rgba(255, 255, 255, 0.7);
50
+  line-height: 40rpx;
51
+}
52
+</style>

+ 149 - 0
src/components/approve-history/approve-history.vue

@@ -0,0 +1,149 @@
1
+<template>
2
+  <view class="approval-history">
3
+    <view v-for="(item, index) in historyList" :key="index" class="approval-item">
4
+      <view class="timeline">
5
+        <view class="circle" :class="{ 'circle-submit': item.action !== 'PENDING' }"></view>
6
+        <view class="line" v-if="index < historyList.length - 1" :class="{ 'line-submit': item.action !== 'PENDING' }">
7
+        </view>
8
+      </view>
9
+      <view class="content">
10
+        <view class="header">
11
+          <text class="name">{{ item.operatorName }}</text>
12
+          <text class="date">{{ item.operationTime }}</text>
13
+        </view>
14
+        <view v-if="item.comment" class="approval-content">{{ item.action === 'REJECT' ? `审批驳回:${item.comment}`
15
+          : `${item.comment}` }}</view>
16
+      </view>
17
+    </view>
18
+
19
+    <!-- 空状态 -->
20
+    <view v-if="historyList.length === 0" class="empty-state">
21
+      <text class="empty-text">暂无审批记录</text>
22
+    </view>
23
+  </view>
24
+</template>
25
+
26
+<script>
27
+export default {
28
+  name: 'ApproveHistory',
29
+  props: {
30
+    // 审批历史数据列表
31
+    historyList: {
32
+      type: Array,
33
+      default: () => []
34
+    },
35
+    // 是否显示空状态
36
+    showEmpty: {
37
+      type: Boolean,
38
+      default: true
39
+    }
40
+  },
41
+  data() {
42
+    return {}
43
+  },
44
+  computed: {
45
+    // 格式化后的历史数据
46
+    formattedHistory() {
47
+      return this.historyList.map(item => ({
48
+        operatorName: item.operatorName || '未知用户',
49
+        operationTime: item.operationTime || '',
50
+        comment: item.comment || '',
51
+        status: item.status || '',
52
+        nodeName: item.nodeName || ''
53
+      }))
54
+    }
55
+  },
56
+  methods: {}
57
+}
58
+</script>
59
+
60
+<style lang="scss" scoped>
61
+.approval-history {
62
+  padding: 15px;
63
+
64
+  .approval-item {
65
+    display: flex;
66
+    margin-bottom: 1rpx;
67
+
68
+    &:last-child {
69
+      margin-bottom: 0;
70
+    }
71
+
72
+    .timeline {
73
+      display: flex;
74
+      flex-direction: column;
75
+      align-items: center;
76
+      margin-right: 12px;
77
+
78
+      .circle {
79
+        width: 12px;
80
+        height: 12px;
81
+        border-radius: 50%;
82
+        background-color: #409EFF;
83
+        border: 2px solid #fff;
84
+        box-shadow: 0 0 0 2px #409EFF;
85
+      }
86
+
87
+      .circle-submit {
88
+        background-color: #909399 !important;
89
+        box-shadow: 0 0 0 2px #909399 !important;
90
+      }
91
+
92
+      .line {
93
+        width: 2px;
94
+        flex: 1;
95
+        background-color: #409EFF;
96
+        margin-top: 4px;
97
+      }
98
+
99
+      .line-submit {
100
+        background-color: #909399 !important;
101
+      }
102
+    }
103
+
104
+    .content {
105
+      flex: 1;
106
+      position: relative;
107
+      bottom: 8rpx;
108
+
109
+      .header {
110
+        display: flex;
111
+        justify-content: space-between;
112
+        align-items: center;
113
+        margin-bottom: 8px;
114
+
115
+        .name {
116
+          font-size: 14px;
117
+          font-weight: 500;
118
+          color: #333;
119
+        }
120
+
121
+        .date {
122
+          font-size: 12px;
123
+          color: #999;
124
+        }
125
+      }
126
+
127
+      .approval-content {
128
+        font-size: 13px;
129
+        color: #666;
130
+        border: 1px solid #DDDDDD;
131
+        line-height: 1.5;
132
+        background-color: #f8f9fa;
133
+        padding: 10px;
134
+        border-radius: 6px;
135
+      }
136
+    }
137
+  }
138
+
139
+  .empty-state {
140
+    text-align: center;
141
+    padding: 40px 0;
142
+
143
+    .empty-text {
144
+      font-size: 14px;
145
+      color: #999;
146
+    }
147
+  }
148
+}
149
+</style>

+ 150 - 0
src/components/custom-tabbar.vue

@@ -0,0 +1,150 @@
1
+<template>
2
+  <view class="custom-tabbar">
3
+    <view v-for="(item, index) in list" :key="index" @click="switchTab(item)" class="tabbar-item">
4
+      <image :src="currentPage === item.pagePath ? item.selectedIconPath : item.iconPath"
5
+        style="width: 24px; height: 24px;" />
6
+      <text style="font-size: 12px; margin-top: 4px;">{{ item.text }}</text>
7
+    </view>
8
+  </view>
9
+</template>
10
+
11
+<script>
12
+export default {
13
+  data() {
14
+    return {
15
+      list: [
16
+        {
17
+          pagePath: "pages/home/index",
18
+          iconPath: "/static/images/tabbar/home.png",
19
+          selectedIconPath: "/static/images/tabbar/home_.png",
20
+          text: "首页"
21
+        },
22
+        {
23
+          pagePath: "pages/myToDoList/index",
24
+          iconPath: "/static/images/tabbar/message.png",
25
+          selectedIconPath: "/static/images/tabbar/message_.png",
26
+          text: "消息"
27
+        },
28
+        {
29
+          pagePath: "pages/work/index",
30
+          iconPath: "/static/images/tabbar/work.png",
31
+          selectedIconPath: "/static/images/tabbar/work_.png",
32
+          text: "工作台"
33
+        },
34
+        {
35
+          pagePath: "pages/mine/index",
36
+          iconPath: "/static/images/tabbar/mine.png",
37
+          selectedIconPath: "/static/images/tabbar/mine_.png",
38
+          text: "我的"
39
+        }
40
+      ],
41
+      currentPage: ""
42
+    };
43
+  },
44
+  mounted() {
45
+    this.updateCurrentPage();
46
+  },
47
+  methods: {
48
+    switchTab(item) {
49
+      uni.switchTab({
50
+        url: `/${item.pagePath}` // 确保路径以 "/" 开头
51
+      });
52
+    },
53
+    updateCurrentPage() {
54
+      const pages = getCurrentPages();
55
+      if (pages.length > 0) {
56
+        let route = pages[pages.length - 1].route;
57
+        // 统一路径格式(如去掉 "/" 前缀)
58
+        this.currentPage = route.startsWith('/') ? route.substring(1) : route;
59
+      }
60
+    },
61
+    
62
+    // 获取未读消息数量
63
+    async getUnreadMessageCount() {
64
+      try {
65
+        // 这里需要调用实际的获取未读消息数量的接口
66
+        // 示例接口:假设是获取待办列表的接口
67
+        const { listCheckPendingTasks } = require('@/api/myToDoList/myToDoList.js');
68
+        const response = await listCheckPendingTasks({ pageNum: 1, pageSize: 1 });
69
+        
70
+        // 假设接口返回的total字段是未读消息总数
71
+        const unreadCount = response.total || 0;
72
+        
73
+        // 更新消息tab的未读数量
74
+        const messageTab = this.list.find(item => item.text === '消息');
75
+        if (messageTab) {
76
+          messageTab.unreadCount = unreadCount;
77
+        }
78
+      } catch (error) {
79
+        console.error('获取未读消息数量失败:', error);
80
+      }
81
+    }
82
+  }
83
+};
84
+</script>
85
+
86
+<style>
87
+.custom-tabbar {
88
+  position: fixed;
89
+  bottom: 0;
90
+  left: 0;
91
+  right: 0;
92
+  z-index: 9999;
93
+  display: flex;
94
+  justify-content: space-around;
95
+  background: #fff;
96
+  padding: 0;
97
+  box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
98
+  transform: translateZ(0);
99
+  -webkit-transform: translateZ(0);
100
+  /* 移除重复的padding设置,改为在特定条件下应用 */
101
+}
102
+
103
+.tabbar-item {
104
+  display: flex;
105
+  flex-direction: column;
106
+  align-items: center;
107
+  padding: 8px 0;
108
+}
109
+
110
+.icon-container {
111
+  position: relative;
112
+  width: 24px;
113
+  height: 24px;
114
+}
115
+
116
+.badge {
117
+  position: absolute;
118
+  top: -4px;
119
+  right: -4px;
120
+  width: 12px;
121
+  height: 12px;
122
+  background-color: #F55B5D;
123
+  border-radius: 50%;
124
+  z-index: 10;
125
+}
126
+
127
+/* 为iOS Safari浏览器添加安全区域支持 */
128
+/* 仅在独立浏览器环境中应用,避免在App嵌套环境中造成双重padding */
129
+@media screen and (max-width: 768px) and (orientation: portrait), 
130
+       screen and (max-width: 1024px) and (orientation: landscape) {
131
+  /* 检测移动设备环境 */
132
+  @supports (padding-bottom: env(safe-area-inset-bottom)) {
133
+    /* 确保只在独立浏览器而非App WebView中应用 */
134
+    .custom-tabbar {
135
+      padding-bottom: env(safe-area-inset-bottom, 0px);
136
+    }
137
+  }
138
+}
139
+
140
+/* 兼容性处理 */
141
+@supports (-webkit-touch-callout: none) and (not (min-device-width: 768px)) {
142
+  /* 针对移动设备的WebView特殊处理 */
143
+  .custom-tabbar {
144
+    /* App内嵌时,使用绝对定位确保贴合底部 */
145
+    position: absolute;
146
+    bottom: 0;
147
+    width: 100%;
148
+  }
149
+}
150
+</style>

+ 237 - 0
src/components/fuzzy-select/fuzzy-select.vue

@@ -0,0 +1,237 @@
1
+<template>
2
+  <view class="fuzzy-select-container">
3
+    <!-- 搜索输入框 -->
4
+    <uni-easyinput ref="inputRef" v-model="searchKeyword" :placeholder="placeholder" :clearable="true"
5
+      :disabled="disabled" @input="handleInput" @focus="showDropdown = true" @blur="handleBlur" />
6
+
7
+    <!-- 下拉选项列表 -->
8
+    <view v-if="showDropdown && filteredOptions.length > 0" class="dropdown-list">
9
+      <scroll-view scroll-y="true" class="dropdown-scroll">
10
+        <view v-for="(option, index) in filteredOptions" :key="index" class="dropdown-item"
11
+          @click="selectOption(option)">
12
+          {{ option[dataText] }}
13
+        </view>
14
+      </scroll-view>
15
+    </view>
16
+
17
+    <!-- 无匹配结果提示 -->
18
+    <view v-if="showDropdown && filteredOptions.length === 0 && searchKeyword" class="no-result">
19
+      无匹配结果
20
+    </view>
21
+  </view>
22
+</template>
23
+
24
+<script>
25
+export default {
26
+  name: "FuzzySelect",
27
+  props: {
28
+    // 绑定值
29
+    value: {
30
+      type: [String, Number],
31
+      default: ''
32
+    },
33
+    // 选项列表
34
+    options: {
35
+      type: Array,
36
+      default: () => []
37
+    },
38
+    // 占位符
39
+    placeholder: {
40
+      type: String,
41
+      default: '请输入搜索'
42
+    },
43
+    // 值字段名
44
+    dataValue: {
45
+      type: String,
46
+      default: 'value'
47
+    },
48
+    // 显示文本字段名
49
+    dataText: {
50
+      type: String,
51
+      default: 'text'
52
+    },
53
+    // 是否开启搜索
54
+    filterable: {
55
+      type: Boolean,
56
+      default: true
57
+    },
58
+    // 是否禁用
59
+    disabled: {
60
+      type: Boolean,
61
+      default: false
62
+    }
63
+  },
64
+  data() {
65
+    return {
66
+      searchKeyword: '',
67
+      showDropdown: false,
68
+      filteredOptions: this.options,
69
+      selectedOption: null
70
+    }
71
+  },
72
+  watch: {
73
+    options(newOptions) {
74
+      this.filterOptions(this.searchKeyword, newOptions)
75
+      // 当options更新后,需要重新根据当前value设置选中项
76
+      this.updateSelectedOption(this.value)
77
+    },
78
+    value(newValue) {
79
+      // 只有当value确实变化时才更新选中项
80
+      if (newValue !== this.selectedOption?.[this.dataValue]) {
81
+        this.updateSelectedOption(newValue)
82
+      }
83
+    }
84
+  },
85
+  mounted() {
86
+    this.updateSelectedOption(this.value)
87
+  },
88
+  methods: {
89
+    // 根据value值更新选中的选项
90
+    updateSelectedOption(value) {
91
+      
92
+      if (!value) {
93
+        this.searchKeyword = ''
94
+        this.selectedOption = null
95
+        return
96
+      }
97
+
98
+      const foundOption = this.options.find(option =>
99
+        String(option[this.dataValue]) === String(value)
100
+      )
101
+
102
+      if (foundOption) {
103
+        
104
+        this.selectedOption = foundOption
105
+        this.searchKeyword = foundOption[this.dataText]
106
+        // 确保filteredOptions包含当前选中的选项
107
+        if (!this.filteredOptions.some(opt => String(opt[this.dataValue]) === String(value))) {
108
+          this.filteredOptions = this.options
109
+        }
110
+      } else {
111
+        this.searchKeyword = ''
112
+        this.selectedOption = null
113
+      }
114
+    },
115
+    // 处理输入
116
+    handleInput(value) {
117
+      if (this.disabled) return;
118
+      this.searchKeyword = value
119
+      this.filterOptions(value)
120
+      this.showDropdown = true
121
+    },
122
+
123
+    // 过滤选项
124
+    filterOptions(keyword = '', options = this.options) {
125
+      if (!this.filterable || !keyword.trim()) {
126
+        this.filteredOptions = options
127
+        return
128
+      }
129
+
130
+      const lowerKeyword = keyword.toLowerCase()
131
+      this.filteredOptions = options.filter(option => {
132
+        const text = String(option[this.dataText] || '').toLowerCase()
133
+        const value = String(option[this.dataValue] || '').toLowerCase()
134
+        return text.includes(lowerKeyword) || value.includes(lowerKeyword)
135
+      })
136
+    },
137
+
138
+    // 选择选项
139
+    selectOption(option) {
140
+      if (this.disabled) return;
141
+      this.selectedOption = option
142
+      this.searchKeyword = option[this.dataText]
143
+      this.$emit('input', option[this.dataValue])
144
+      this.$emit('change', option[this.dataValue])
145
+      this.showDropdown = false
146
+    },
147
+
148
+    // 处理失焦
149
+    handleBlur() {
150
+      // 延迟隐藏下拉框,确保点击选项能触发
151
+      setTimeout(() => {
152
+        this.showDropdown = false
153
+      }, 200)
154
+    },
155
+
156
+    // 清空搜索
157
+    clearSearch() {
158
+      this.searchKeyword = ''
159
+      this.filteredOptions = this.options
160
+      this.selectedOption = null
161
+      this.$emit('input', '')
162
+      this.$emit('change', '')
163
+    },
164
+
165
+    // 聚焦输入框
166
+    focus() {
167
+      if (this.$refs.inputRef) {
168
+        this.$refs.inputRef.focus()
169
+      }
170
+    },
171
+
172
+    // 失焦输入框
173
+    blur() {
174
+      if (this.$refs.inputRef) {
175
+        this.$refs.inputRef.blur()
176
+      }
177
+    },
178
+  }
179
+}
180
+</script>
181
+
182
+<style scoped>
183
+.fuzzy-select-container {
184
+  position: relative;
185
+  width: 100%;
186
+}
187
+
188
+.dropdown-list {
189
+  position: absolute;
190
+  top: 100%;
191
+  left: 0;
192
+  right: 0;
193
+  max-height: 200px;
194
+  background-color: #fff;
195
+  border: 1px solid #e5e5e5;
196
+  border-radius: 4px;
197
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
198
+  z-index: 999;
199
+  margin-top: 4px;
200
+}
201
+
202
+.dropdown-scroll {
203
+  max-height: 200px;
204
+}
205
+
206
+.dropdown-item {
207
+  padding: 12px 16px;
208
+  font-size: 14px;
209
+  color: #333;
210
+  border-bottom: 1px solid #f0f0f0;
211
+  cursor: pointer;
212
+}
213
+
214
+.dropdown-item:hover {
215
+  background-color: #f5f5f5;
216
+}
217
+
218
+.dropdown-item:last-child {
219
+  border-bottom: none;
220
+}
221
+
222
+.no-result {
223
+  position: absolute;
224
+  top: 100%;
225
+  left: 0;
226
+  right: 0;
227
+  padding: 12px 16px;
228
+  background-color: #fff;
229
+  border: 1px solid #e5e5e5;
230
+  border-radius: 4px;
231
+  font-size: 14px;
232
+  color: #999;
233
+  text-align: center;
234
+  z-index: 999;
235
+  margin-top: 4px;
236
+}
237
+</style>

+ 138 - 0
src/components/h-collapse-item/h-collapse-item.vue

@@ -0,0 +1,138 @@
1
+<template>
2
+    <uni-collapse-item :name="name" :show-animation="showAnimation" :disabled="disabled" ref="collapseItem" @change="handleCollapseChange">
3
+        <template #title>
4
+            <view class="h-collapse-item-title">
5
+                <view v-if="iconUrl" class="title-icon"
6
+                    :style="{ backgroundImage: iconUrl ? 'url(' + iconUrl + ')' : '' }"></view>
7
+                <text class="title-text">{{ title }}</text>
8
+            </view>
9
+        </template>
10
+        <view ref="contentSlot">
11
+            <slot></slot>
12
+        </view>
13
+    </uni-collapse-item>
14
+</template>
15
+
16
+<script>
17
+export default {
18
+    name: 'eikonCollapseItem',
19
+    props: {
20
+        // 折叠面板名称
21
+        name: {
22
+            type: String,
23
+            default: ''
24
+        },
25
+        // 标题文本
26
+        title: {
27
+            type: String,
28
+            default: ''
29
+        },
30
+        // 是否显示动画
31
+        showAnimation: {
32
+            type: Boolean,
33
+            default: true
34
+        },
35
+        // 是否禁用
36
+        disabled: {
37
+            type: Boolean,
38
+            default: false
39
+        },
40
+        // 图标URL地址(可选)
41
+        iconUrl: {
42
+            type: String,
43
+            default: ''
44
+        },
45
+        
46
+    },
47
+    mounted() {
48
+       
49
+    },
50
+    methods: {
51
+        handleCollapseChange(e) {
52
+            // 当折叠面板状态变化时,如果是展开状态,重新计算高度
53
+            if (e.detail.show) {
54
+                this.$nextTick(() => {
55
+                    this.updateCollapseHeight();
56
+                });
57
+            }
58
+        },
59
+      
60
+        updateCollapseHeight() {
61
+            
62
+            // 强制重新渲染折叠面板
63
+            if (this.$refs.collapseItem && this.$refs.collapseItem.$el) {
64
+                // 触发uni-collapse-item的重新渲染
65
+                const collapseItem = this.$refs.collapseItem;
66
+                if (collapseItem.isOpen) {
67
+                    // 如果是展开状态,强制重新计算高度
68
+                    this.$nextTick(() => {
69
+                        if (collapseItem.resize) {
70
+                            collapseItem.resize();
71
+                        } else {
72
+                            // 如果resize方法不存在,使用uni-app的节点查询获取高度
73
+                            const query = uni.createSelectorQuery().in(this);
74
+                            query.select('.uni-collapse-item__wrap').boundingClientRect(data => {
75
+                                debugger
76
+                                if (data) {
77
+                                    // 获取内容区域的实际高度 - 使用ref而不是ID选择器
78
+                                    const contentQuery = uni.createSelectorQuery().in(this);
79
+                                    contentQuery.select('.uni-collapse-item__wrap-content').boundingClientRect(contentData => {
80
+                                        if (contentData && contentData.height > 0) {
81
+                                            // 设置折叠面板高度为内容高度
82
+                                            const wrapElement = collapseItem.$el.querySelector('.uni-collapse-item__wrap');
83
+                                            if (wrapElement) {
84
+                                                wrapElement.style.height = contentData.height + 'px';
85
+                                            }
86
+                                        }
87
+                                    }).exec();
88
+                                }
89
+                            }).exec();
90
+                        }
91
+                    });
92
+                }
93
+            }
94
+        }
95
+    },
96
+    beforeDestroy() {
97
+        if (this.contentObserver) {
98
+            this.contentObserver.disconnect();
99
+        }
100
+    }
101
+}
102
+</script>
103
+
104
+<style lang="scss" scoped>
105
+::v-deep .uni-collapse-item__title {
106
+    height: 96rpx !important;
107
+    padding-left: 28rpx;
108
+}
109
+
110
+// 确保内容区域在展开时高度自适应
111
+::v-deep .uni-collapse-item__wrap {
112
+    // 保持原有行为,不设置固定高度
113
+}
114
+
115
+::v-deep .uni-collapse-item__content {
116
+    // 移除任何可能影响动画的高度限制
117
+    // 让uni-collapse-item自己管理高度
118
+}
119
+
120
+.h-collapse-item-title {
121
+    display: flex;
122
+    align-items: center;
123
+
124
+    .title-icon {
125
+        width: 40rpx;
126
+        height: 40rpx;
127
+        margin-right: 8px;
128
+        background-size: contain;
129
+        background-repeat: no-repeat;
130
+        background-position: center;
131
+    }
132
+
133
+    .title-text {
134
+        font-size: 14px;
135
+        color: #333;
136
+    }
137
+}
138
+</style>

+ 74 - 0
src/components/h-legend/h-legend.vue

@@ -0,0 +1,74 @@
1
+<template>
2
+  <view class="h-legend-container">
3
+    <view v-for="(item, index) in legendData" :key="index" class="legend-item"
4
+      :style="{ marginRight: index === legendData.length - 1 ? '0' : itemSpacing + 'rpx' }">
5
+      <view class="legend-color" :style="{ backgroundColor: item.color }"></view>
6
+      <text class="legend-text" :style="{ fontSize: textSize + 'rpx', color: textColor }">
7
+        {{ item.name }}
8
+      </text>
9
+    </view>
10
+  </view>
11
+</template>
12
+
13
+<script>
14
+export default {
15
+  name: 'HLegend',
16
+  props: {
17
+    // 图例数据数组
18
+    legendData: {
19
+      type: Array,
20
+      default: () => []
21
+    },
22
+    // 图例颜色块大小
23
+    colorSize: {
24
+      type: Number,
25
+      default: 24
26
+    },
27
+    // 文字大小
28
+    textSize: {
29
+      type: Number,
30
+      default: 24
31
+    },
32
+    // 文字颜色
33
+    textColor: {
34
+      type: String,
35
+      default: '#333333'
36
+    },
37
+    // 图例间距
38
+    itemSpacing: {
39
+      type: Number,
40
+      default: 32
41
+    }
42
+  }
43
+}
44
+</script>
45
+
46
+<style lang="scss" scoped>
47
+.h-legend-container {
48
+  display: flex;
49
+  flex-wrap: nowrap;
50
+  align-items: center;
51
+  overflow-x: auto;
52
+  padding: 16rpx 0;
53
+}
54
+
55
+.legend-item {
56
+  display: flex;
57
+  align-items: center;
58
+  flex-shrink: 0;
59
+}
60
+
61
+.legend-color {
62
+  border-radius: 4rpx;
63
+  margin-right: 12rpx;
64
+  margin-top: 2rpx;
65
+  flex-shrink: 0;
66
+  width: 45rpx;
67
+  height: 22rpx;
68
+}
69
+
70
+.legend-text {
71
+  font-weight: 400;
72
+  white-space: nowrap;
73
+}
74
+</style>

+ 200 - 0
src/components/h-line/h-line.vue

@@ -0,0 +1,200 @@
1
+<template>
2
+  <view class="h-line-container">
3
+    <!-- 单个长条容器 -->
4
+    <view class="single-line-container" :style="{
5
+      height: lineHeight + 'rpx'
6
+    }">
7
+      <!-- 背景条 -->
8
+      <view class="line-background" :style="{
9
+        borderRadius: borderRadius + 'rpx',
10
+        backgroundColor: backgroundColor
11
+      }"></view>
12
+
13
+      <!-- 背景条 -->
14
+      <view class="line-background" :style="{
15
+        borderRadius: borderRadius + 'rpx',
16
+        backgroundColor: backgroundColor
17
+      }"></view>
18
+
19
+      <!-- 分段进度条 -->
20
+      <view v-for="(item, index) in data" :key="index" class="segment-progress" :style="{
21
+        width: calculateSegmentWidth(item) + '%',
22
+        left: calculateSegmentLeft(index) + '%',
23
+        backgroundColor: item.color || defaultColor,
24
+        borderRadius: getSegmentBorderRadius(index) + 'rpx'
25
+      }">
26
+        <!-- 每个分段的数值显示 -->
27
+        <text v-if="item.value > 0" class="segment-text">
28
+          {{ item.value }}
29
+        </text>
30
+      </view>
31
+    </view>
32
+  </view>
33
+</template>
34
+
35
+<script>
36
+export default {
37
+  name: 'HLine',
38
+  props: {
39
+    // 数据数组
40
+    data: {
41
+      type: Array,
42
+      default: () => []
43
+    },
44
+    // 总数量(用于计算百分比)
45
+    total: {
46
+      type: Number,
47
+      default: 0
48
+    },
49
+    // 条宽度
50
+    itemWidth: {
51
+      type: Number,
52
+      default: 200
53
+    },
54
+    // 条高度
55
+    lineHeight: {
56
+      type: String,
57
+      default: '40'
58
+    },
59
+    // 条间距
60
+    itemSpacing: {
61
+      type: Number,
62
+      default: 32
63
+    },
64
+    // 圆角大小
65
+    borderRadius: {
66
+      type: Number,
67
+      default: 20
68
+    },
69
+    // 背景颜色
70
+    backgroundColor: {
71
+      type: String,
72
+      default: '#f5f5f5'
73
+    },
74
+    // 默认进度条颜色
75
+    defaultColor: {
76
+      type: String,
77
+      default: '#4873E3'
78
+    },
79
+    // 是否显示百分比
80
+    showPercentage: {
81
+      type: Boolean,
82
+      default: true
83
+    },
84
+ 
85
+  
86
+    // 百分比文字大小
87
+    percentageSize: {
88
+      type: Number,
89
+      default: 24
90
+    },
91
+    // 百分比文字颜色
92
+    percentageColor: {
93
+      type: String,
94
+      default: '#666666'
95
+    }
96
+  },
97
+  computed: {
98
+    // 计算总数量(如果未传入total,则使用data中所有值的和)
99
+    computedTotal() {
100
+      if (this.total > 0) {
101
+        return this.total;
102
+      }
103
+      return this.data.reduce((sum, item) => sum + (item.value || 0), 0);
104
+    }
105
+  },
106
+  methods: {
107
+    // 计算每个分段的宽度百分比
108
+    calculateSegmentWidth(item) {
109
+      if (this.computedTotal === 0) return 0;
110
+      const percentage = (item.value / this.computedTotal) * 100;
111
+      // 确保最小宽度为1%,以便显示
112
+      return Math.max(1, percentage);
113
+    },
114
+
115
+    // 计算每个分段的左侧位置
116
+    calculateSegmentLeft(index) {
117
+      if (index === 0) return 0;
118
+      let left = 0;
119
+      for (let i = 0; i < index; i++) {
120
+        left += this.calculateSegmentWidth(this.data[i]);
121
+      }
122
+      return left;
123
+    },
124
+
125
+    // 获取分段圆角样式
126
+    getSegmentBorderRadius(index) {
127
+      if (this.data.length === 1) {
128
+        return this.borderRadius;
129
+      }
130
+      if (index === 0) {
131
+        return `${this.borderRadius} 0 0 ${this.borderRadius}`;
132
+      }
133
+      if (index === this.data.length - 1) {
134
+        return `0 ${this.borderRadius} ${this.borderRadius} 0`;
135
+      }
136
+      return 0;
137
+    },
138
+
139
+    // 计算总百分比
140
+    calculateTotalPercentage() {
141
+      if (this.computedTotal === 0) return 0;
142
+      return Math.round((this.computedTotal / this.total) * 100);
143
+    }
144
+  }
145
+}
146
+</script>
147
+
148
+<style lang="scss" scoped>
149
+.h-line-container {
150
+  display: flex;
151
+  align-items: center;
152
+  padding: 8rpx 0;
153
+  width: 100%;
154
+}
155
+
156
+.single-line-container {
157
+  position: relative;
158
+  display: flex;
159
+  width: 100%;
160
+  align-items: center;
161
+  justify-content: center;
162
+  overflow: hidden;
163
+  border-radius: 20rpx;
164
+}
165
+
166
+.line-background {
167
+  position: absolute;
168
+  top: 0;
169
+  left: 0;
170
+  width: 100%;
171
+  height: 100%;
172
+  z-index: 1;
173
+}
174
+
175
+.segment-progress {
176
+  position: absolute;
177
+  top: 0;
178
+  height: 100%;
179
+  z-index: 2;
180
+  transition: width 0.3s ease;
181
+}
182
+
183
+.segment-progress {
184
+  position: absolute;
185
+  top: 0;
186
+  height: 100%;
187
+  z-index: 2;
188
+  transition: width 0.3s ease;
189
+  display: flex;
190
+  align-items: center;
191
+  justify-content: center;
192
+}
193
+
194
+.segment-text {
195
+  font-size: 23rpx;
196
+  font-weight: 500;
197
+  color: #ffffff;
198
+  text-shadow: 0 0 2rpx rgba(0, 0, 0, 0.5);
199
+}
200
+</style>

+ 104 - 0
src/components/h-rank-line/h-rank-line.vue

@@ -0,0 +1,104 @@
1
+<template>
2
+  <view class="h-rank-line-container">
3
+    <!-- 左侧进度条区域 -->
4
+    <view class="progress-section">
5
+      <!-- 灰条背景 -->
6
+      <view class="gray-bar">
7
+        <!-- 蓝条,根据百分比显示 -->
8
+        <view class="blue-bar" :style="{ width: percentage + '%' }">
9
+          <!-- 蓝条结尾的竖条 -->
10
+          <view class="end-line"></view>
11
+        </view>
12
+      </view>
13
+    </view>
14
+
15
+    <!-- 右侧插槽区域 -->
16
+    <view class="slot-section">
17
+      <slot></slot>
18
+    </view>
19
+  </view>
20
+</template>
21
+
22
+<script>
23
+export default {
24
+  name: 'HRankLine',
25
+  props: {
26
+    // 百分比值,范围0-100
27
+    percentage: {
28
+      type: Number,
29
+      default: 0,
30
+      validator: (value) => {
31
+        return value >= 0 && value <= 100
32
+      }
33
+    }
34
+  },
35
+  data() {
36
+    return {}
37
+  }
38
+}
39
+</script>
40
+
41
+<style lang="scss" scoped>
42
+.h-rank-line-container {
43
+  display: flex;
44
+  align-items: center;
45
+  width: 100%;
46
+  height: 60rpx;
47
+
48
+}
49
+
50
+.progress-section {
51
+  flex: 1;
52
+  margin-right: 20rpx;
53
+}
54
+
55
+.gray-bar {
56
+  position: relative;
57
+  width: 100%;
58
+  height: 24rpx;
59
+  background: #f0f0f0;
60
+  border-radius: 12rpx;
61
+  // overflow: hidden;
62
+}
63
+
64
+.blue-bar {
65
+  position: relative;
66
+  height: 100%;
67
+  background: linear-gradient(90deg, #0098FA 0%, #0098FA 100%);
68
+  border-radius: 12rpx 0 0 12rpx;
69
+  transition: width 0.3s ease;
70
+  display: flex;
71
+  align-items: center;
72
+  justify-content: flex-end;
73
+}
74
+
75
+.end-line {
76
+  width: 4rpx;
77
+  height: 35rpx;
78
+  background: #0098FA;
79
+  border-radius: 2rpx;
80
+  box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
81
+  margin-right: -2rpx;
82
+}
83
+
84
+.slot-section {
85
+  flex-shrink: 0;
86
+  min-width: 100rpx;
87
+  text-align: right;
88
+}
89
+
90
+/* 响应式适配 */
91
+@media (max-width: 768px) {
92
+  .h-rank-line-container {
93
+    height: 50rpx;
94
+  }
95
+
96
+  .gray-bar {
97
+    height: 20rpx;
98
+  }
99
+
100
+  .end-line {
101
+    height: 35rpx;
102
+  }
103
+}
104
+</style>

+ 128 - 0
src/components/h-search/h-search.vue

@@ -0,0 +1,128 @@
1
+<template>
2
+    <u-search v-model="internalValue" :placeholder="placeholder" :showAction="showAction" :actionText="actionText"
3
+        @search="handleSearch" @custom="handleCustom" @clear="handleClear" :disabled="disabled" :maxlength="maxlength"
4
+        :height="height" :bgColor="bgColor" :color="color" :searchIconColor="searchIconColor"
5
+        :clearButton="clearButton" />
6
+</template>
7
+
8
+<script>
9
+export default {
10
+    name: 'HSearch',
11
+    props: {
12
+        // 双向绑定的值
13
+        value: {
14
+            type: String,
15
+            default: ''
16
+        },
17
+        // 占位符文本
18
+        placeholder: {
19
+            type: String,
20
+            default: '请输入关键词'
21
+        },
22
+        // 是否显示搜索按钮
23
+        showAction: {
24
+            type: Boolean,
25
+            default: true
26
+        },
27
+        // 搜索按钮文本
28
+        actionText: {
29
+            type: String,
30
+            default: '搜索'
31
+        },
32
+        // 是否禁用
33
+        disabled: {
34
+            type: Boolean,
35
+            default: false
36
+        },
37
+        // 最大输入长度
38
+        maxlength: {
39
+            type: Number,
40
+            default: 100
41
+        },
42
+        // 搜索框高度
43
+        height: {
44
+            type: Number,
45
+            default: 64
46
+        },
47
+        // 背景颜色
48
+        bgColor: {
49
+            type: String,
50
+            default: '#f5f5f5'
51
+        },
52
+        // 文字颜色
53
+        color: {
54
+            type: String,
55
+            default: '#333333'
56
+        },
57
+        // 搜索图标颜色
58
+        searchIconColor: {
59
+            type: String,
60
+            default: '#999999'
61
+        },
62
+        // 是否显示清除按钮
63
+        clearButton: {
64
+            type: Boolean,
65
+            default: true
66
+        }
67
+    },
68
+    data() {
69
+        return {
70
+            internalValue: this.value
71
+        };
72
+    },
73
+    watch: {
74
+        value(newVal) {
75
+            this.internalValue = newVal;
76
+        },
77
+        internalValue(newVal) {
78
+            this.$emit('input', newVal);
79
+            this.$emit('update:value', newVal);
80
+        }
81
+    },
82
+    methods: {
83
+        // 处理搜索事件
84
+        handleSearch(value) {
85
+            this.$emit('search', value);
86
+        },
87
+        // 处理自定义事件(点击搜索按钮)
88
+        handleCustom(value) {
89
+            this.$emit('custom', value);
90
+        },
91
+        // 处理清除事件
92
+        handleClear() {
93
+            this.internalValue = '';
94
+            this.$emit('clear');
95
+            this.$emit('search', '');
96
+        },
97
+        // 聚焦搜索框
98
+        focus() {
99
+            this.$refs.uSearch && this.$refs.uSearch.focus();
100
+        },
101
+        // 失焦搜索框
102
+        blur() {
103
+            this.$refs.uSearch && this.$refs.uSearch.blur();
104
+        },
105
+        // 清空搜索框
106
+        clear() {
107
+            this.internalValue = '';
108
+            this.handleClear();
109
+        }
110
+    }
111
+};
112
+</script>
113
+
114
+<style lang="scss" scoped>
115
+// 可以在这里添加自定义样式
116
+.h-search-container {
117
+    width: 100%;
118
+}
119
+
120
+::v-deep .u-search__content {
121
+
122
+
123
+    .u-search__content__input {
124
+        height: 80rpx !important;
125
+        
126
+    }
127
+}
128
+</style>

+ 105 - 0
src/components/h-tabs/h-tabs.vue

@@ -0,0 +1,105 @@
1
+<template>
2
+  <view class="h-tabs" :style="{ '--active-color': activeColor }">
3
+    <view v-for="item in tabs" :key="item[valueKey]" :class="{ active: item[valueKey] === currentValue }"
4
+      :style="[item[valueKey] === currentValue ? item.activeStyle : item.style]" class="h-tabs-item"
5
+      @click="handleTabClick(item)">
6
+      {{ item[labelKey] }}
7
+      <!-- 显示红点 -->
8
+      <view v-if="item.unreadCount > 0" class="h-tabs-red-dot"></view>
9
+    </view>
10
+  </view>
11
+</template>
12
+
13
+<script>
14
+export default {
15
+  name: 'HTabs',
16
+  props: {
17
+    tabs: {
18
+      type: Array,
19
+      default: () => []
20
+    },
21
+    value: {
22
+      type: [String, Number],
23
+      default: ''
24
+    },
25
+    valueKey: {
26
+      type: String,
27
+      default: 'type'
28
+    },
29
+    labelKey: {
30
+      type: String,
31
+      default: 'title'
32
+    },
33
+    activeColor: {
34
+      type: String,
35
+      default: '#2A70D1'
36
+    }
37
+  },
38
+  data() {
39
+    return {
40
+      currentValue: this.value
41
+    };
42
+  },
43
+  watch: {
44
+    value(newVal) {
45
+      this.currentValue = newVal;
46
+    }
47
+  },
48
+  methods: {
49
+    handleTabClick(item) {
50
+      this.currentValue = item[this.valueKey];
51
+      this.$emit('input', this.currentValue);
52
+      this.$emit('change', this.currentValue, item);
53
+    }
54
+  }
55
+};
56
+</script>
57
+
58
+<style lang="scss" scoped>
59
+.h-tabs {
60
+  display: flex;
61
+  padding: 0 0 32rpx;
62
+  font-size: 28rpx;
63
+  color: #666666;
64
+  line-height: 32rpx;
65
+  gap: 48rpx;
66
+  width: 100%;
67
+
68
+  // 默认样式
69
+  .h-tabs-item {
70
+    position: relative;
71
+    cursor: pointer;
72
+    padding: 0 8rpx;
73
+
74
+    &.active {
75
+      font-size: 36rpx;
76
+      color: var(--active-color);
77
+      line-height: 42rpx;
78
+
79
+      &:after {
80
+        content: "";
81
+        position: absolute;
82
+        bottom: -12rpx;
83
+        left: 0;
84
+        width: 50%;
85
+        height: 6rpx;
86
+        background: var(--active-color);
87
+        border-radius: 12rpx;
88
+        transition: all 0.3s ease;
89
+      }
90
+    }
91
+  }
92
+
93
+  // 红点样式
94
+  .h-tabs-red-dot {
95
+    position: absolute;
96
+    top: -4rpx;
97
+    right: 0;
98
+    width: 16rpx;
99
+    height: 16rpx;
100
+    background-color: #FF4D4F;
101
+    border-radius: 50%;
102
+    transform: translateX(50%);
103
+  }
104
+}
105
+</style>

+ 81 - 0
src/components/list-card/list-card.vue

@@ -0,0 +1,81 @@
1
+<template>
2
+  <view class="list-card-container" @click="handleClick" :class="containerStyle">
3
+    <slot v-if="showChecked" name="checkbox">
4
+
5
+    </slot>
6
+    <view class="list-card">
7
+      <view class="list-card-header">
8
+        <slot name="title"></slot>
9
+        <slot name="icon"></slot>
10
+      </view>
11
+      <view class="list-card-content">
12
+        <slot></slot>
13
+      </view>
14
+
15
+    </view>
16
+    <slot name="footer"></slot>
17
+  </view>
18
+
19
+</template>
20
+
21
+<script>
22
+export default {
23
+  name: 'ListCard',
24
+  props: {
25
+    showChecked: {
26
+      type: Boolean,
27
+      default: false
28
+    },
29
+    containerStyle: {
30
+      type: Object,
31
+      default: () => ({})
32
+    }
33
+  },
34
+  data() {
35
+    return {
36
+      checked: false
37
+    }
38
+  },
39
+
40
+  methods: {
41
+    handleClick() {
42
+      this.$emit('click');
43
+    }
44
+  }
45
+}
46
+</script>
47
+
48
+<style lang="scss" scoped>
49
+.list-card-container {
50
+  background: #FFFFFF;
51
+  border-radius: 16rpx;
52
+  box-shadow: 0 8rpx 20rpx 0 rgba(0, 0, 0, 0.08);
53
+  border: 2rpx solid #F0F8FF;
54
+  padding: 32rpx;
55
+  margin-bottom: 32rpx;
56
+  display: flex;
57
+  align-items: flex-start;
58
+  position: relative;
59
+
60
+
61
+
62
+  .list-card {
63
+    width: 100%;
64
+
65
+    .list-card-header {
66
+      display: flex;
67
+      justify-content: space-between;
68
+      align-items: center;
69
+      margin-bottom: 24rpx;
70
+
71
+      .list-card-checkbox {
72
+        margin-right: 16rpx;
73
+      }
74
+    }
75
+
76
+    .list-card-content {
77
+      // 默认内容样式
78
+    }
79
+  }
80
+}
81
+</style>

+ 38 - 0
src/components/list-icon/list-icon.vue

@@ -0,0 +1,38 @@
1
+<template>
2
+  <view class="list-icon" :style="divStyle">
3
+    {{ text }}
4
+  </view>
5
+</template>
6
+
7
+<script>
8
+export default {
9
+  name: 'ListIcon',
10
+  props: {
11
+    divStyle: {
12
+      type: Object,
13
+      default: () => ({
14
+        backgroundColor: '#5972C0'
15
+      })
16
+    },
17
+    text: {
18
+      type: String,
19
+      default: ''
20
+    }
21
+  }
22
+}
23
+</script>
24
+
25
+<style lang="scss" scoped>
26
+.list-icon {
27
+  display: inline-flex;
28
+  align-items: center;
29
+  justify-content: center;
30
+  min-width: 60rpx;
31
+  height: 40rpx;
32
+  padding: 0 16rpx;
33
+  border-radius: 8rpx;
34
+  font-size: 24rpx;
35
+  color: #FFFFFF;
36
+  line-height: 1;
37
+}
38
+</style>

+ 134 - 0
src/components/list-user/list-user.vue

@@ -0,0 +1,134 @@
1
+<template>
2
+  <view class="personnel-list" v-if="data && data.length">
3
+    <view class="personnel-item" v-for="(item, index) of data" :key="item.id || index" @click="handleItemClick(item, index)">
4
+      <view class="personnel-img">
5
+        <UserAvatar :userName="item.nickName || item.userName" :avatarLink="item.avatar" />
6
+      </view>
7
+      <view class="personnel-name">{{ item.nickName || item.userName }}</view>
8
+      <view v-if="showDelete" class="delete-icon" @click.stop="handleDelete(index)">
9
+        <uni-icons type="close" size="16" color="#999"></uni-icons>
10
+      </view>
11
+    </view>
12
+  </view>
13
+</template>
14
+
15
+<script>
16
+import UserAvatar from '@/pages/attendance/components/UserAvatar.vue'
17
+
18
+export default {
19
+  name: 'ListUser',
20
+  components: {
21
+    UserAvatar
22
+  },
23
+  props: {
24
+    // 人员数据数组
25
+    data: {
26
+      type: Array,
27
+      default: () => []
28
+    },
29
+    // 是否显示删除图标
30
+    showDelete: {
31
+      type: Boolean,
32
+      default: false
33
+    },
34
+    // 禁用状态
35
+    disabled: {
36
+      type: Boolean,
37
+      default: false
38
+    }
39
+  },
40
+  methods: {
41
+    // 处理删除操作
42
+    handleDelete(index) {
43
+      if (!this.data || !this.data.length || this.disabled) return
44
+
45
+      // 创建新数组(避免直接修改props)
46
+      const newData = [...this.data]
47
+      const deletedItem = newData.splice(index, 1)[0]
48
+
49
+      // 向外传递删除后的数组和删除的项
50
+      this.$emit('update:data', newData)
51
+      this.$emit('delete', {
52
+        deletedItem,
53
+        index,
54
+        newData
55
+      })
56
+
57
+      uni.showToast({
58
+        title: '删除成功',
59
+        icon: 'success',
60
+        duration: 1500
61
+      })
62
+    },
63
+
64
+    // 处理人员项点击
65
+    handleItemClick(item, index) {
66
+      this.$emit('click', {
67
+        clickedItem: item,
68
+        index,
69
+        data: this.data
70
+      })
71
+    }
72
+  }
73
+}
74
+</script>
75
+
76
+<style lang="scss" scoped>
77
+.personnel-list {
78
+  display: flex;
79
+  flex-wrap: wrap;
80
+  gap: 16rpx;
81
+  margin: 20rpx 0;
82
+}
83
+
84
+.personnel-item {
85
+  display: flex;
86
+  align-items: center;
87
+  background: #f8f9fa;
88
+  border-radius: 24rpx;
89
+  padding: 12rpx 20rpx;
90
+  position: relative;
91
+
92
+  .personnel-img {
93
+    width: 60rpx;
94
+    height: 60rpx;
95
+    border-radius: 50%;
96
+    overflow: hidden;
97
+    margin-right: 16rpx;
98
+    flex-shrink: 0;
99
+  }
100
+
101
+  .personnel-name {
102
+    font-size: 28rpx;
103
+    color: #333;
104
+    font-weight: 500;
105
+    flex: 1;
106
+    min-width: 0;
107
+    overflow: hidden;
108
+    text-overflow: ellipsis;
109
+    white-space: nowrap;
110
+  }
111
+
112
+  .delete-icon {
113
+    width: 40rpx;
114
+    height: 40rpx;
115
+    border-radius: 50%;
116
+    background: #f0f0f0;
117
+    display: flex;
118
+    align-items: center;
119
+    justify-content: center;
120
+    margin-left: 16rpx;
121
+    flex-shrink: 0;
122
+    cursor: pointer;
123
+    transition: background-color 0.2s;
124
+
125
+    &:hover {
126
+      background: #e0e0e0;
127
+    }
128
+
129
+    &:active {
130
+      background: #d0d0d0;
131
+    }
132
+  }
133
+}
134
+</style>

+ 75 - 0
src/components/no-data/no-data.vue

@@ -0,0 +1,75 @@
1
+<template>
2
+  <view class="no-data-container">
3
+    <!-- 图标 -->
4
+    <view class="no-data-icon">
5
+      <uni-icons 
6
+        :type="iconType" 
7
+        :size="iconSize" 
8
+        :color="iconColor" 
9
+      />
10
+    </view>
11
+    
12
+    <!-- 提示文字 -->
13
+    <text class="no-data-text">
14
+      {{ text }}
15
+    </text>
16
+  </view>
17
+</template>
18
+
19
+<script>
20
+// 导入uni-ui图标组件
21
+import uniIcons from '@/uni_modules/uni-icons/components/uni-icons/uni-icons.vue';
22
+
23
+export default {
24
+  name: 'NoData',
25
+  components: {
26
+    uniIcons
27
+  },
28
+  props: {
29
+    // 提示文字
30
+    text: {
31
+      type: String,
32
+      default: '暂无数据'
33
+    },
34
+    // 图标类型
35
+    iconType: {
36
+      type: String,
37
+      default: 'search' // 可以使用其他图标如: 'info', 'help', 'minus', etc.
38
+    },
39
+    // 图标大小
40
+    iconSize: {
41
+      type: [Number, String],
42
+      default: 60
43
+    },
44
+    // 图标颜色
45
+    iconColor: {
46
+      type: String,
47
+      default: '#C0C4CC'
48
+    }
49
+  }
50
+}
51
+</script>
52
+
53
+<style lang="scss" scoped>
54
+.no-data-container {
55
+  display: flex;
56
+  flex-direction: column;
57
+  align-items: center;
58
+  justify-content: center;
59
+  width: 100%;
60
+  height: 100%;
61
+  text-align: center;
62
+  padding: 40rpx 0; /* 添加上下padding值 */
63
+  
64
+  .no-data-icon {
65
+    margin-bottom: 20rpx;
66
+    opacity: 0.5;
67
+  }
68
+  
69
+  .no-data-text {
70
+    font-size: 28rpx;
71
+    color: #909399;
72
+    font-weight: 400;
73
+  }
74
+}
75
+</style>

+ 136 - 0
src/components/statistic-table/statistic-table.vue

@@ -0,0 +1,136 @@
1
+<template>
2
+  <div class="table-container">
3
+    <table class="data-table">
4
+      <thead>
5
+        <tr>
6
+          <th v-for="column in columns" :key="column.props">{{ column.title }}</th>
7
+        </tr>
8
+      </thead>
9
+      <tbody>
10
+        <tr v-for="(row, index) in data" :key="index"
11
+          :class="{ 'odd-row': (index + 1) % 2 !== 0, 'even-row': (index + 1) % 2 === 0 }">
12
+          
13
+          <td v-for="column in columns" :key="`${column.props}-${index}`">
14
+            <!-- 如果列配置了slot属性,则使用插槽 -->
15
+            <slot v-if="column.slot" :name="`column-${column.props}`" :row="row" :index="index">
16
+              <!-- 默认插槽内容 -->
17
+              {{ row[column.props] }}
18
+            </slot>
19
+            <!-- 如果没有配置slot属性,则显示普通文本 -->
20
+            <template v-else>
21
+              {{ row[column.props] || '-' }}
22
+            </template>
23
+          </td>
24
+        </tr>
25
+      </tbody>
26
+    </table>
27
+  </div>
28
+</template>
29
+
30
+<script>
31
+export default {
32
+  name: 'TableList',
33
+  props: {
34
+    columns: {
35
+      type: Array,
36
+      default: () => [],
37
+      validator: (value) => {
38
+        return value.every(column => {
39
+          return column.props && column.title;
40
+        });
41
+      }
42
+    },
43
+    data: {
44
+      type: Array,
45
+      default: () => []
46
+    }
47
+  },
48
+  created() {
49
+    console.log(this.columns,this.data);
50
+  }
51
+};
52
+</script>
53
+
54
+<style lang="scss" scoped>
55
+.table-container {
56
+  width: 100%;
57
+  overflow-x: auto;
58
+  -webkit-overflow-scrolling: touch;
59
+  
60
+  /* 强制显示滚动条 */
61
+  scrollbar-width: thin; /* Firefox */
62
+  scrollbar-color: #525861 #f1f1f1; /* Firefox */
63
+  
64
+  /* 自定义滚动条样式 - 更明显的滚动条 */
65
+  &::-webkit-scrollbar {
66
+    height: 12px; /* 增加滚动条高度 */
67
+    width: 12px;
68
+  }
69
+  
70
+  &::-webkit-scrollbar-track {
71
+    background: #f1f1f1;
72
+    border-radius: 6px;
73
+    box-shadow: inset 0 0 5px rgba(0,0,0,0.1);
74
+  }
75
+  
76
+  &::-webkit-scrollbar-thumb {
77
+    background: #525861; /* 使用主题色 */
78
+    border-radius: 6px;
79
+    border: 2px solid #f1f1f1;
80
+  }
81
+  
82
+  &::-webkit-scrollbar-thumb:hover {
83
+    background: #525861; /* 悬停时加深颜色 */
84
+  }
85
+  
86
+  &::-webkit-scrollbar-corner {
87
+    background: #f1f1f1;
88
+  }
89
+}
90
+
91
+.data-table {
92
+  width: auto;
93
+  min-width: 100%;
94
+  border-collapse: collapse;
95
+  border: 2rpx solid #ccc;
96
+  border-radius: 16rpx;
97
+  table-layout: auto;
98
+
99
+  th,
100
+  td {
101
+    padding: 12rpx;
102
+    height: 48rpx;
103
+    text-align: left;
104
+    white-space: nowrap;
105
+    overflow: visible;
106
+    text-overflow: clip;
107
+    // border-bottom: 1rpx solid #ccc;
108
+  }
109
+
110
+  th {
111
+    background-color: #f5f5f5;
112
+    font-weight: bold;
113
+    color: #666;
114
+    position: sticky;
115
+    top: 0;
116
+    z-index: 1;
117
+    
118
+    /* 确保表头内容完全展示 */
119
+    white-space: nowrap;
120
+    overflow: visible;
121
+    text-overflow: clip;
122
+  }
123
+
124
+  .odd-row {
125
+    background-color: #fff !important;
126
+  }
127
+
128
+  .even-row {
129
+    background-color: #f5f5f5 !important;
130
+  }
131
+
132
+  tr:hover {
133
+    background-color: #e0e0e0;
134
+  }
135
+}
136
+</style>

+ 218 - 0
src/components/text-switch/text-switch.vue

@@ -0,0 +1,218 @@
1
+<!-- import textSwitch from "@/components/text-li-switch/text-li-switch.vue" -->
2
+<!-- <textSwitch v-model="prop.must" uncheck_text="非必选" checked_text="必选" @change="switchChange"></textSwitch> -->
3
+<!-- @change可以不写 -->
4
+
5
+
6
+<template>
7
+	<view>
8
+		<view class="switch_box row-b-c" :class="{ 'disabled-switch': disabled }"
9
+			:style="{ backgroundColor: checked ? checkedbg : uncheckedbg, width: width + 'rpx', height: height + 'rpx', borderRadius: '100px' }"
10
+			@click="changeSwitch">
11
+			<view class="switch_text c-white" :class="checked ? ['to_left'] : ['_right', 'c-black']"
12
+				:style="{ fontSize: size + 'rpx', 'padding-right': padding_right }">{{ checked ? checked_text :
13
+					uncheck_text }}
14
+			</view>
15
+			<view class="round" :style="{
16
+				left: checked ? '100%' : '2rpx',
17
+				marginLeft: checked ? '-' + (Number(height) - 6) + 'rpx' : '', width: + height + 'rpx',
18
+				height: + height - 10 + 'rpx',
19
+				width: + height - 10 + 'rpx',
20
+				borderRadius: height / 2 + 'rpx'
21
+			}">
22
+			</view>
23
+			<!-- 禁用遮罩层 -->
24
+			<view v-if="disabled" class="disabled-overlay"></view>
25
+		</view>
26
+	</view>
27
+</template>
28
+
29
+<script>
30
+export default {
31
+	emits: ['click', 'change', 'update:modelValue', 'input'],
32
+	model: {
33
+		prop: 'modelValue',
34
+		event: 'update:modelValue'
35
+	},
36
+
37
+	props: {
38
+		modelValue: [String], // 修改为接收字符串值
39
+		checkedbg: {
40
+			type: String,
41
+			default: '#626FF0'
42
+		},
43
+		uncheckedbg: {
44
+			type: String,
45
+			default: '#F67072'
46
+		},
47
+		width: {
48
+			type: String,
49
+			default: '130'
50
+		},
51
+		height: {
52
+			type: String,
53
+			default: '50'
54
+		},
55
+		uncheck_text: {
56
+			type: String,
57
+			default: ''
58
+		},
59
+		checked_text: {
60
+			type: String,
61
+			default: ''
62
+		},
63
+		size: {
64
+			type: String,
65
+			default: '24'
66
+		},
67
+		checked_value: {
68
+			type: String,
69
+			default: 'QUALIFIED'
70
+		},
71
+		uncheck_value: {
72
+			type: String,
73
+			default: 'UNQUALIFIED'
74
+		},
75
+		disabled: {
76
+			type: Boolean,
77
+			default: false
78
+		}
79
+	},
80
+	data() {
81
+		return {
82
+			checked: false, // 修改为布尔值
83
+			padding_right: '20rpx'
84
+		}
85
+	},
86
+	created() {
87
+		if (this.uncheck_text.length > this.checked_text.length) {
88
+			this.padding_right = '10rpx'
89
+		}
90
+		// 根据传入的值初始化选中状态
91
+		this.updateCheckedState();
92
+	},
93
+	watch: {
94
+		modelValue: {
95
+			handler(newVal) {
96
+				this.updateCheckedState();
97
+			},
98
+			deep: true,
99
+			immediate: true
100
+		},
101
+		checked_value: {
102
+			handler() {
103
+				this.updateCheckedState();
104
+			},
105
+			deep: true
106
+		},
107
+		uncheck_value: {
108
+			handler() {
109
+				this.updateCheckedState();
110
+			},
111
+			deep: true
112
+		}
113
+	},
114
+	methods: {
115
+		// 根据传入的值更新选中状态
116
+		updateCheckedState() {
117
+			if (this.modelValue === this.checked_value) {
118
+				this.checked = true;
119
+			} else if (this.modelValue === this.uncheck_value) {
120
+				this.checked = false;
121
+			}
122
+			// 如果传入的值既不等于checked_value也不等于uncheck_value,保持当前状态
123
+		},
124
+		
125
+		changeSwitch: function (e) {
126
+			// 如果禁用状态,阻止切换
127
+			if (this.disabled) {
128
+				return;
129
+			}
130
+			
131
+			this.checked = !this.checked;
132
+			
133
+			// 根据开关状态确定要传递的值
134
+			const current_value = this.checked ? this.checked_value : this.uncheck_value;
135
+			
136
+			// TODO 兼容 vue2
137
+			this.$emit('input', current_value);
138
+			// TODO 兼容 vue3
139
+			this.$emit('update:modelValue', current_value);
140
+
141
+			this.$emit('change', current_value)
142
+		}
143
+	}
144
+}
145
+</script>
146
+
147
+<style>
148
+.row-c-c {
149
+	display: flex;
150
+	justify-content: center;
151
+	align-items: center;
152
+}
153
+
154
+.c-white {
155
+	color: #FFFFFF;
156
+}
157
+
158
+.c-black {
159
+	color: #FFFFFF;
160
+}
161
+
162
+
163
+/* ---------- */
164
+.switch_box {
165
+	position: relative;
166
+	border: 1rpx solid #EEEEEE;
167
+	background-color: #F6F6F6;
168
+	transition: all 0.4s;
169
+	display: flex;
170
+	align-items: center;
171
+}
172
+
173
+.round {
174
+	position: absolute;
175
+	background-color: #FFFFFF;
176
+	transition: all 0.4s;
177
+}
178
+
179
+.to_left {
180
+	left: 0;
181
+	margin-left: 0;
182
+}
183
+
184
+.to_right {
185
+	left: 100%;
186
+	margin-left: -50rpx;
187
+}
188
+
189
+.switch_text {
190
+	position: absolute;
191
+	padding: 0 20rpx;
192
+}
193
+
194
+._right {
195
+	right: 0;
196
+	border-right: none !important;
197
+}
198
+
199
+/* 禁用状态样式 */
200
+.disabled-switch {
201
+	pointer-events: none;
202
+	user-select: none;
203
+	opacity: 0.6;
204
+	position: relative;
205
+}
206
+
207
+.disabled-overlay {
208
+	position: absolute;
209
+	top: 0;
210
+	left: 0;
211
+	right: 0;
212
+	bottom: 0;
213
+	background: rgba(128, 128, 128, 0.3);
214
+	border-radius: 100px;
215
+	z-index: 10;
216
+	pointer-events: none;
217
+}
218
+</style>

+ 113 - 0
src/components/text-tag/text-tag.vue

@@ -0,0 +1,113 @@
1
+<template>
2
+    <view class="text-tag-container">
3
+        <view v-for="(item, index) in localTags" :key="index" class="tag-item" :style="getTagItemStyle(item)">
4
+            <text class="tag-text" :style="getTagTextStyle(item)">{{ item[labelColumn] }}</text>
5
+
6
+            <u-icon v-if="showClose" name="close" color="#666666" size="14" @click="handleRemove(index)" />
7
+        </view>
8
+    </view>
9
+</template>
10
+
11
+<script>
12
+export default {
13
+    name: 'TextTag',
14
+    props: {
15
+        tags: {
16
+            type: Array,
17
+            default: () => []
18
+        },
19
+        showClose: {
20
+            type: Boolean,
21
+            default: true
22
+        },
23
+        labelColumn: {
24
+            type: String,
25
+            default: 'label'
26
+        },
27
+        // 全局tag-item样式配置
28
+        tagItemStyle: {
29
+            type: Object,
30
+            default: () => ({})
31
+        },
32
+        // 全局tag-text样式配置
33
+        tagTextStyle: {
34
+            type: Object,
35
+            default: () => ({})
36
+        }
37
+    },
38
+    data() {
39
+        return {
40
+            localTags: [...this.tags]
41
+        };
42
+    },
43
+    watch: {
44
+        tags(newVal) {
45
+            
46
+            this.localTags = [...newVal];
47
+        }
48
+    },
49
+    methods: {
50
+        handleRemove(index) {
51
+            this.localTags.splice(index, 1);
52
+            this.$emit('remove', [...this.localTags]);
53
+         
54
+        },
55
+        // 获取tag-item样式
56
+        getTagItemStyle(item) {
57
+            const baseStyle = {
58
+                display: 'flex',
59
+                alignItems: 'center',
60
+                padding: '4px 12px',
61
+                backgroundColor: '#f0f0f0',
62
+                borderRadius: '16px',
63
+                fontSize: '14px'
64
+            };
65
+            
66
+            // 合并全局样式和item中的样式
67
+            const mergedStyle = {
68
+                ...baseStyle,
69
+                ...this.tagItemStyle,
70
+                ...(item.tagItemStyle || {})
71
+            };
72
+            
73
+            return mergedStyle;
74
+        },
75
+        // 获取tag-text样式
76
+        getTagTextStyle(item) {
77
+            const baseStyle = {
78
+                marginRight: '8px'
79
+            };
80
+            
81
+            // 合并全局样式和item中的样式
82
+            const mergedStyle = {
83
+                ...baseStyle,
84
+                ...this.tagTextStyle,
85
+                ...(item.tagTextStyle || {})
86
+            };
87
+            
88
+            return mergedStyle;
89
+        }
90
+    }
91
+};
92
+</script>
93
+
94
+<style scoped>
95
+.text-tag-container {
96
+    display: flex;
97
+    flex-wrap: wrap;
98
+    gap: 8px;
99
+}
100
+
101
+.tag-item {
102
+    display: flex;
103
+    align-items: center;
104
+    padding: 4px 12px;
105
+    background-color: #f0f0f0;
106
+    border-radius: 16px;
107
+    font-size: 14px;
108
+}
109
+
110
+.tag-text {
111
+    margin-right: 8px;
112
+}
113
+</style>

+ 103 - 0
src/components/timeline-record/TimelineRecord.vue

@@ -0,0 +1,103 @@
1
+
2
+<template>
3
+  <view class="vertical-timeline">
4
+    <uni-section :title="sectionTitle" type="line" padding>
5
+      <view v-if="flatSteps.length > 0">
6
+        <uni-steps
7
+          :options="flatSteps"
8
+          :active="activeStep"
9
+          direction="column"
10
+        />
11
+      </view>
12
+      <view v-else class="no-record">
13
+        <uni-icons type="flag-filled" size="60" color="#ccc"></uni-icons>
14
+        <text>暂无打卡记录</text>
15
+      </view>
16
+    </uni-section>
17
+  </view>
18
+</template>
19
+
20
+<script>
21
+export default {
22
+  name: 'VerticalTimeline',
23
+  props: {
24
+    records: {
25
+      type: Array,
26
+      default: () => []
27
+    },
28
+    sectionTitle: {
29
+      type: String,
30
+      default: '打卡记录'
31
+    },
32
+    statusColorMap: {
33
+      type: Object,
34
+      default: () => ({
35
+        正常: '#67C23A',
36
+        迟到: '#F56C6C',
37
+        早退: '#E6A23C',
38
+        异常: '#909399'
39
+      })
40
+    }
41
+  },
42
+  computed: {
43
+    activeStep() {
44
+      return this.flatSteps.length - 1
45
+    },
46
+    flatSteps() {
47
+      if (!this.records || this.records.length === 0) return []
48
+      
49
+      // 提取items数组并排序
50
+      const items = this.records[0]?.items || []
51
+      const sorted = [...items].sort((a, b) => 
52
+        new Date(a.checkInTime) - new Date(b.checkInTime)
53
+      )
54
+
55
+      return sorted.map(item => ({
56
+        title: this.formatTime(item.checkInTime),
57
+        desc: `${this.getTypeText(item.checkInType)} - ${item.checkInPosition}`,
58
+        color: this.getStatusColor(item)
59
+      }))
60
+    }
61
+  },
62
+  methods: {
63
+    formatTime(timeStr) {
64
+      // return timeStr ? timeStr.split(' ')[1].substr(0,5) : '--:--'
65
+      return timeStr
66
+    },
67
+    getTypeText(type) {
68
+      const map = {
69
+        CLOCK_IN: '上班打卡',
70
+        CLOCK_OUT: '下班打卡'
71
+      }
72
+      return map[type] || type
73
+    },
74
+    getStatusColor(item) {
75
+      // 可根据实际业务逻辑判断状态
76
+      return this.statusColorMap.正常
77
+    }
78
+  }
79
+}
80
+</script>
81
+
82
+<style lang="scss" scoped>
83
+.vertical-timeline {
84
+  background-color: #fff;
85
+  border-radius: 12px;
86
+  padding: 10px 0;
87
+  box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
88
+}
89
+
90
+.no-record {
91
+  display: flex;
92
+  flex-direction: column;
93
+  align-items: center;
94
+  justify-content: center;
95
+  padding: 40px 0;
96
+  color: #999;
97
+
98
+  text {
99
+    margin-top: 8px;
100
+    font-size: 14px;
101
+  }
102
+}
103
+</style>

+ 167 - 0
src/components/uni-section/uni-section.vue

@@ -0,0 +1,167 @@
1
+<template>
2
+	<view class="uni-section">
3
+		<view class="uni-section-header" @click="onClick">
4
+				<view class="uni-section-header__decoration" v-if="type" :class="type" />
5
+        <slot v-else name="decoration"></slot>
6
+
7
+        <view class="uni-section-header__content">
8
+          <text :style="{'font-size':titleFontSize,'color':titleColor}" class="uni-section__content-title" :class="{'distraction':!subTitle}">{{ title }}</text>
9
+          <text v-if="subTitle" :style="{'font-size':subTitleFontSize,'color':subTitleColor}" class="uni-section-header__content-sub">{{ subTitle }}</text>
10
+        </view>
11
+
12
+        <view class="uni-section-header__slot-right">
13
+          <slot name="right"></slot>
14
+        </view>
15
+		</view>
16
+
17
+		<view class="uni-section-content" :style="{padding: _padding}">
18
+			<slot />
19
+		</view>
20
+	</view>
21
+</template>
22
+
23
+<script>
24
+
25
+	/**
26
+	 * Section 标题栏
27
+	 * @description 标题栏
28
+	 * @property {String} type = [line|circle|square] 标题装饰类型
29
+	 * 	@value line 竖线
30
+	 * 	@value circle 圆形
31
+	 * 	@value square 正方形
32
+	 * @property {String} title 主标题
33
+	 * @property {String} titleFontSize 主标题字体大小
34
+	 * @property {String} titleColor 主标题字体颜色
35
+	 * @property {String} subTitle 副标题
36
+	 * @property {String} subTitleFontSize 副标题字体大小
37
+	 * @property {String} subTitleColor 副标题字体颜色
38
+	 * @property {String} padding 默认插槽 padding
39
+	 */
40
+
41
+	export default {
42
+		name: 'UniSection',
43
+    emits:['click'],
44
+		props: {
45
+			type: {
46
+				type: String,
47
+				default: ''
48
+			},
49
+			title: {
50
+				type: String,
51
+				required: true,
52
+				default: ''
53
+			},
54
+      titleFontSize: {
55
+        type: String,
56
+        default: '14px'
57
+      },
58
+			titleColor:{
59
+				type: String,
60
+				default: '#333'
61
+			},
62
+			subTitle: {
63
+				type: String,
64
+				default: ''
65
+			},
66
+      subTitleFontSize: {
67
+        type: String,
68
+        default: '12px'
69
+      },
70
+      subTitleColor: {
71
+        type: String,
72
+        default: '#999'
73
+      },
74
+			padding: {
75
+				type: [Boolean, String],
76
+				default: false
77
+			}
78
+		},
79
+    computed:{
80
+      _padding(){
81
+        if(typeof this.padding === 'string'){
82
+          return this.padding
83
+        }
84
+
85
+        return this.padding?'10px':''
86
+      }
87
+    },
88
+		watch: {
89
+			title(newVal) {
90
+				if (uni.report && newVal !== '') {
91
+					uni.report('title', newVal)
92
+				}
93
+			}
94
+		},
95
+    methods: {
96
+			onClick() {
97
+				this.$emit('click')
98
+			}
99
+		}
100
+	}
101
+</script>
102
+<style lang="scss" >
103
+	$uni-primary: #2979ff !default;
104
+
105
+	.uni-section {
106
+		background-color: #fff;
107
+    .uni-section-header {
108
+      position: relative;
109
+      /* #ifndef APP-NVUE */
110
+      display: flex;
111
+      /* #endif */
112
+      flex-direction: row;
113
+      align-items: center;
114
+      padding: 12px 10px;
115
+      font-weight: normal;
116
+
117
+      &__decoration{
118
+        margin-right: 6px;
119
+        background-color: $uni-primary;
120
+        &.line {
121
+          width: 4px;
122
+          height: 12px;
123
+          border-radius: 10px;
124
+        }
125
+
126
+        &.circle {
127
+          width: 8px;
128
+          height: 8px;
129
+          border-top-right-radius: 50px;
130
+          border-top-left-radius: 50px;
131
+          border-bottom-left-radius: 50px;
132
+          border-bottom-right-radius: 50px;
133
+        }
134
+
135
+        &.square {
136
+          width: 8px;
137
+          height: 8px;
138
+        }
139
+      }
140
+
141
+      &__content {
142
+        /* #ifndef APP-NVUE */
143
+        display: flex;
144
+        /* #endif */
145
+        flex-direction: column;
146
+        flex: 1;
147
+        color: #333;
148
+
149
+        .distraction {
150
+          flex-direction: row;
151
+          align-items: center;
152
+        }
153
+        &-sub {
154
+          margin-top: 2px;
155
+        }
156
+      }
157
+
158
+      &__slot-right{
159
+        font-size: 14px;
160
+      }
161
+    }
162
+
163
+    .uni-section-content{
164
+      font-size: 14px;
165
+    }
166
+	}
167
+</style>