@uxda/appkit 4.0.12 → 4.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uxda/appkit",
3
- "version": "4.0.12",
3
+ "version": "4.0.14",
4
4
  "description": "小程序应用开发包",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.ts",
@@ -22,18 +22,18 @@
22
22
  <div class="btn confirm" @click="onOkClick">确定</div>
23
23
  </div>
24
24
  <div class="bottom"></div>
25
+ <nut-popup v-model:visible="datePickerOpen" position="bottom">
26
+ <nut-date-picker
27
+ v-model="focusedDate"
28
+ :min-date="minDate"
29
+ :max-date="maxDate"
30
+ :is-show-chinese="false"
31
+ :three-dimensional="false"
32
+ @cancel="datePickerOpen = false"
33
+ @confirm="onDatePickerComplete"
34
+ ></nut-date-picker>
35
+ </nut-popup>
25
36
  </div>
26
- <nut-popup v-model:visible="datePickerOpen" position="bottom">
27
- <nut-date-picker
28
- v-model="focusedDate"
29
- :min-date="minDate"
30
- :max-date="maxDate"
31
- :is-show-chinese="false"
32
- :three-dimensional="false"
33
- @cancel="datePickerOpen = false"
34
- @confirm="onDatePickerComplete"
35
- ></nut-date-picker>
36
- </nut-popup>
37
37
  </template>
38
38
 
39
39
  <script lang="ts" setup>
@@ -126,6 +126,7 @@ function switchDateInput(shift: string) {
126
126
  display: flex;
127
127
  flex-direction: column;
128
128
  width: 100%;
129
+ position: relative;
129
130
  .date-filter-header {
130
131
  text-align: center;
131
132
  height: 44px;
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="date-range" @click="openDateFilter" v-show="filtering.to">
2
+ <div class="date-range" @click="openDateRangePicker">
3
3
  <div class="text number">{{ dateRangeDisplay }}</div>
4
4
  <img
5
5
  style="margin-top: -2px"
@@ -10,31 +10,47 @@
10
10
  </template>
11
11
 
12
12
  <script lang="ts" setup>
13
- import { computed, reactive } from 'vue'
13
+ import { computed, PropType, reactive } from 'vue'
14
14
  import { useNutshell } from '@uxda/nutshell/taro'
15
15
  import DateFilter from './DateFilter.vue'
16
16
  import dayjs from 'dayjs'
17
17
 
18
18
  const $n = useNutshell()
19
19
 
20
- const filtering = reactive({
21
- from: dayjs().add(-3, 'M').format('YYYY-MM-DD'),
22
- to: dayjs().format('YYYY-MM-DD'),
20
+ export type DateRangeModel = {
21
+ from: string,
22
+ to: string
23
+ }
24
+
25
+ const model = defineModel({
26
+ type: Object as PropType<DateRangeModel>,
27
+ required: false,
28
+ default: {
29
+ from: dayjs().add(-1, 'M').format('YYYY-MM-DD'),
30
+ to: dayjs().format('YYYY-MM-DD')
31
+ }
23
32
  })
24
33
 
25
- const openDateFilter = () => {
34
+ const openDateRangePicker = () => {
26
35
  $n.sheet({
27
36
  component: DateFilter,
28
37
  props: {
29
- from: '2024-08-01',
30
- to: '2024-08-31',
38
+ from: model.value.from,
39
+ to: model.value.to,
40
+ },
41
+ onComplete (result: any) {
42
+ console.log('===openDateFilter complete', result)
43
+ model.value = {
44
+ from: result.from,
45
+ to: result.to
46
+ }
31
47
  }
32
48
  })
33
49
  }
34
50
 
35
51
  const dateRangeDisplay = computed(() => {
36
- let startTime = (filtering.from || '').replace(/-/g, '.').substring(2)
37
- let endTime = (filtering.to || '').replace(/-/g, '.').substring(2)
52
+ let startTime = (model.value.from || '').replace(/-/g, '.').substring(2)
53
+ let endTime = (model.value.to || '').replace(/-/g, '.').substring(2)
38
54
  return startTime + ' - ' + endTime
39
55
  })
40
56
 
@@ -9,16 +9,40 @@
9
9
  </template>
10
10
 
11
11
  <script lang="ts" setup>
12
- import { computed, reactive } from 'vue'
12
+ import { computed, PropType, reactive } from 'vue'
13
13
  import { useNutshell } from '@uxda/nutshell/taro'
14
14
  import ListFilterPicker from './ListFilterPicker.vue'
15
15
 
16
16
  const $n = useNutshell()
17
17
 
18
+ export type ListFilterValue = {
19
+ type: string,
20
+ }
21
+
22
+ const model = defineModel({
23
+ type: Object as PropType<ListFilterValue>,
24
+ default: {
25
+ type: ''
26
+ },
27
+ required: false,
28
+ })
29
+
30
+ const emit = defineEmits<{
31
+ (e: 'update:modelValue', v: ListFilterValue): void,
32
+ (e: 'change'): void,
33
+ }>()
34
+
18
35
  const openPicker = () => {
19
36
  $n.sheet({
20
37
  component: ListFilterPicker,
21
-
38
+ props: {
39
+ modelValue: model,
40
+ onComplete: (result) => {
41
+ emit('update:modelValue', {
42
+ type: result[0]
43
+ })
44
+ }
45
+ }
22
46
  })
23
47
  }
24
48
  </script>
@@ -1,19 +1,19 @@
1
1
  <template>
2
- <div class="consumption-filter">
3
- <div class="consumption-filter-title">
2
+ <div class="list-filter-picker">
3
+ <div class="list-filter-picker-title">
4
4
  <h3>选择筛选项</h3>
5
5
  </div>
6
6
  <div class="consumption-filter-content">
7
7
  <template v-for="(item, index) in filterSections" :key="index">
8
- <div class="title">{{ item.title }}</div>
8
+ <div class="title">{{ item.label }}</div>
9
9
  <div class="info">
10
10
  <div
11
11
  v-for="(it, i) in item.data"
12
- @click="() => onFilterSectionClick(index, it.code)"
13
- :class="getItemClass(index, it.code)"
12
+ @click="() => onFilterSectionClick(item.name, it.value)"
13
+ :class="getItemClass(item.name, it.value)"
14
14
  class="info-item"
15
15
  :key="i">
16
- {{ typeof it === 'string' ? it : it.name }}
16
+ {{ typeof it === 'string' ? it : it.label }}
17
17
  </div>
18
18
  </div>
19
19
  </template>
@@ -33,21 +33,22 @@ import { UniData, UniDataItem } from '@uxda/nutshell/taro'
33
33
 
34
34
  const $http = useHttp()
35
35
 
36
- interface ConsumptionFilterProps {
37
- modelValue: ConsumptionFilterModelValue
36
+ type ListFilterPickerModel = Record<string, string[]>
37
+
38
+ interface ListFilterPickerProps {
39
+ modelValue: ListFilterPickerModel
38
40
  }
39
41
 
40
- const props = withDefaults(
41
- defineProps<ConsumptionFilterProps>(), {
42
- modelValue: () => ['全部', '全部', '全部', '']
43
- }
44
- )
42
+ const props =
43
+ defineProps<ListFilterPickerProps>()
44
+
45
45
  const emit = defineEmits(['complete'])
46
46
 
47
- const result = reactive<ConsumptionFilterModelValue>(props.modelValue)
47
+ const result = ref<ListFilterPickerModel>(props.modelValue)
48
48
 
49
49
  type FilterSecion = {
50
- title: string,
50
+ label: string,
51
+ name: string,
51
52
  data: UniDataItem[]
52
53
  }
53
54
 
@@ -56,54 +57,45 @@ type FilterSecion = {
56
57
  */
57
58
  const filterSections = ref<FilterSecion[]>([
58
59
  {
59
- title: '类型',
60
- data: consumptionPositions.map(s => ({code: s, name: s})),
61
- },
62
- {
63
- title: '收入/支出',
64
- data: consumptionDirections.map(s => ({code: s, name: s})),
65
- },
66
- {
67
- title: '明细类型',
68
- data: consumptionTypes.map(s => ({code: s, name: s})),
60
+ label: '明细类型',
61
+ name: 'type',
62
+ data: [
63
+ {
64
+ label: '全部',
65
+ value: ''
66
+ },
67
+ {
68
+ label: '提现',
69
+ value: 'withdrawal'
70
+ },
71
+ {
72
+ label: '收益',
73
+ value: 'income'
74
+ },
75
+ {
76
+ label: '兑换',
77
+ value: 'exchange'
78
+ }
79
+ ]
69
80
  },
70
- {
71
- title: '权益类目',
72
- data: []
73
- }
74
81
  ])
75
82
 
76
- const getItemClass = (index: number, value) => result[index] === value ? ['current'] : ['']
83
+ const getItemClass = (name: string, value: string) => result.value[name]?.includes(value) ? ['current'] : ['']
77
84
 
78
- const onFilterSectionClick = (index: number, value: MixteValues) => {
79
- result[index] = value
85
+ const onFilterSectionClick = (index: string, value: string) => {
86
+ result.value[index] = [value]
80
87
  }
81
88
 
82
89
  const reset = () => {
83
- result[0] = '全部'
84
- result[1] = '全部'
85
- result[2] = '全部'
86
- result[3] = ''
90
+ result.value = {}
87
91
  }
88
92
 
89
- const 请求权益类目 = () => {
90
- $http.get<权益类目[]>(endpoints.获取权益类目).then(data => {
91
- filterSections.value[3].data = [
92
- { code: '', name: '全部' },
93
- ...data
94
- ]
95
- })
96
- }
97
-
98
- 请求权益类目()
99
-
100
93
  const onOkClick = () => {
101
94
  emit('complete', result)
102
95
  }
103
96
  </script>
104
97
  <style lang="scss">
105
- .consumption-filter {
106
- height: 100%;
98
+ .list-filter-picker {
107
99
  display: flex;
108
100
  flex-direction: column;
109
101
  width: 100%;
@@ -4,20 +4,33 @@
4
4
  <p class="caption">{{ message || '短信将发送至账号绑定手机号' }}</p>
5
5
  <p class="number">{{ phone }}</p>
6
6
  <ns-form v-model="formData">
7
- <ns-input name="code" placeholder="请输入验证码" variant="solid" :rules="['required', {
7
+ <ns-input
8
+ name="code"
9
+ v-model="formData.code"
10
+ placeholder="请输入验证码"
11
+ variant="solid"
12
+ :rules="['required', {
8
13
  name: 'function',
9
14
  message: '输入错误, 请重新输入',
10
- method: (value: string) => value.length === 4,
11
- }]">
15
+ method: (value: string) => value.length === 6,
16
+ }]"
17
+ >
12
18
  <template #append>
13
- <ns-button v-if="!sent" size="xs" variant="plain" color="primary" @click="send" label="获取验证码" />
19
+ <ns-button
20
+ v-if="!sent"
21
+ size="xs"
22
+ variant="plain"
23
+ color="primary"
24
+ @click="send"
25
+ label="获取验证码"
26
+ />
14
27
  <div class="caption" v-if="sent">{{ countdown }} 秒收重新发送</div>
15
28
  </template>
16
29
  </ns-input>
17
30
  </ns-form>
18
31
  <div class="row buttons">
19
- <ns-button round>取消</ns-button>
20
- <ns-button round gradient="#FFEBC1,#FFD7A7,#FFB875/90">确认</ns-button>
32
+ <ns-button round @click="emits('cancel')">取消</ns-button>
33
+ <ns-button round gradient="#FFEBC1,#FFD7A7,#FFB875/90" @click="onOk">确认</ns-button>
21
34
  </div>
22
35
  </div>
23
36
  </template>
@@ -27,27 +40,29 @@ import { NsForm, NsInput, NsButton } from '@uxda/nutshell/taro'
27
40
  import { reactive, ref } from 'vue'
28
41
 
29
42
  export interface AppVerifyProps {
30
- phone: string,
31
- title?: string,
32
- message?: string,
43
+ phone: string
44
+ title?: string
45
+ message?: string
46
+ onSend?: Function
33
47
  }
34
48
 
35
- const emit = defineEmits<{
36
- (event: 'update:modelValue', value: boolean): void
37
- }>()
49
+ const emits = defineEmits(['complete', 'cancel'])
38
50
 
39
51
  const sent = ref(false),
40
52
  countdown = ref(60)
41
53
 
42
54
  let formData = reactive({
43
- code: ''
55
+ code: '',
44
56
  })
45
57
 
46
58
  const send = () => {
47
59
  sent.value = true
60
+
61
+ props.onSend && props.onSend()
62
+
48
63
  countdown.value = 60
49
64
  let timer = setInterval(() => {
50
- countdown.value --
65
+ countdown.value--
51
66
  if (countdown.value <= 0) {
52
67
  clearInterval(timer)
53
68
  sent.value = false
@@ -55,12 +70,11 @@ const send = () => {
55
70
  }, 1000)
56
71
  }
57
72
 
58
- const onVisibleChange = (value: boolean) => {
59
- emit('update:modelValue', value)
73
+ const onOk = () => {
74
+ emits('complete', { code: formData.code })
60
75
  }
61
76
 
62
- defineProps<AppVerifyProps>()
63
-
77
+ const props = defineProps<AppVerifyProps>()
64
78
  </script>
65
79
 
66
80
  <style lang="scss">
@@ -81,4 +95,4 @@ defineProps<AppVerifyProps>()
81
95
  }
82
96
  }
83
97
  }
84
- </style>
98
+ </style>
@@ -4,3 +4,4 @@ export * from './useCountdown'
4
4
  export * from './useValidator'
5
5
  export * from './useEncode'
6
6
  export * from './useUpload'
7
+ export * from './useCrypto'
@@ -0,0 +1,76 @@
1
+ // 解析脱敏姓名、手机号码等
2
+
3
+ export type CryptoConfig = {
4
+ maskField: string,
5
+ secretField: string,
6
+ }
7
+
8
+ export const defaultCryptoConfig: CryptoConfig = {
9
+ maskField: 'mask',
10
+ secretField: 'secretKey'
11
+ }
12
+
13
+ export type DecodeResult<T extends string> = {
14
+ [key in T | `${T}Secret`]: string
15
+ }
16
+
17
+ export type DefaultDecodeResult = DecodeResult<'masked'>
18
+
19
+ export type DecodeFunction = (encoded: string, key: string) => DecodeResult<`${key}`>
20
+
21
+ /**
22
+ * 解析以及解密脱敏数据
23
+ * @param config
24
+ * @returns
25
+ */
26
+ export function useCrypto (config?: CryptoConfig) {
27
+
28
+ const conf = config || defaultCryptoConfig
29
+
30
+ const parse: DecodeFunction = (data: string, key?: string) => {
31
+ let k = key || 'masked'
32
+ let result: Record<string, any> = {}
33
+ try {
34
+ result = JSON.parse(data)
35
+ result = {
36
+ [k]: result.mask,
37
+ // 字段后加$表示用于解密的 secret
38
+ [`${k}$`]: result.secretKey
39
+ }
40
+ } catch (e) {}
41
+ return result
42
+ }
43
+
44
+ /**
45
+ * 批量解析数组内的数据
46
+ * @param list
47
+ * @param field
48
+ * @returns
49
+ */
50
+ const resolve = (list: any[], field?: string | string[]) => {
51
+ const [first] = list,
52
+ f = field
53
+ ? Array.isArray(field) ? field : [field]
54
+ // 自动探测需要解密的字段
55
+ // '{"secretKey":"","mask":""}' 形式
56
+ : Object.entries(first)
57
+ .filter(([_, v]) => /^\{.*\}$/.test(v as string))
58
+ .map(([k]) => k)
59
+ return list
60
+ .map(item => {
61
+ let result = { ...item }
62
+ f.forEach(r => {
63
+ result = {
64
+ ...result,
65
+ ...parse(item[r], r)
66
+ }
67
+ })
68
+ return result
69
+ })
70
+ }
71
+
72
+ return {
73
+ parse,
74
+ resolve,
75
+ }
76
+ }
@@ -21,21 +21,12 @@
21
21
  <div class="user-entry-bd">
22
22
  <div v-if="!mobile" class="user-entry-bd-bigtxt" @click="toLogin">
23
23
  请登录
24
- <img
25
- class="user-entry-bd-bigtxt-icon"
26
- mode="aspectFit"
27
- src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDEiIHZpZXdCb3g9IjAgMCA0MCA0MSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggb3BhY2l0eT0iMC4wMSIgZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0wIDQwLjM1MTZINDBWMC4zNTE1NjJIMFY0MC4zNTE2WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xNCA4LjM1MTU2TDI2IDIwLjM1MTZMMTQgMzIuMzUxNiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC4wMSIvPgo8cGF0aCBkPSJNMTQgOC4zNTE1NkwyNiAyMC4zNTE2TDE0IDMyLjM1MTYiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+Cg=="
28
- alt=""
29
- />
24
+ <span class="user-entry-bd-arrow">></span>
30
25
  </div>
31
26
  <template v-else>
32
27
  <div @click="toUser" class="user-entry-bd-txt">
33
28
  {{ name }}
34
- <img
35
- style="width: 14px; height: 14px; margin-left: 2px"
36
- src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjgiIGhlaWdodD0iMjgiIHZpZXdCb3g9IjAgMCAyOCAyOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTExLjA4MzUgN0wxOC4wODM1IDE0TDExLjA4MzUgMjEiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMS43NSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPgo="
37
- alt=""
38
- />
29
+ <span class="user-entry-bd-arrow">></span>
39
30
  </div>
40
31
  <div @click="toUser" class="user-entry-bd-smalltxt">{{ encodePhone(mobile) }}</div>
41
32
  </template>
@@ -137,6 +128,10 @@ const emits = defineEmits(['jump', 'login'])
137
128
  font-size: 15px;
138
129
  line-height: 21px;
139
130
  }
131
+ &-arrow {
132
+ font-size: 12px;
133
+ margin-left: 8px;
134
+ }
140
135
  }
141
136
  }
142
137
  </style>