@didaoktv/didaoui-uniapp 1.0.0 → 1.2.0
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/README.md +16 -7
- package/components/dd-action-sheet/dd-action-sheet.vue +3 -3
- package/components/dd-album/album.js +19 -0
- package/components/dd-album/dd-album.vue +345 -0
- package/components/dd-album/props.js +89 -0
- package/components/dd-avatar/avatar.js +19 -0
- package/components/dd-avatar/dd-avatar.vue +172 -160
- package/components/dd-avatar/props.js +84 -0
- package/components/dd-avatar-group/avatarGroup.js +14 -0
- package/components/dd-avatar-group/dd-avatar-group.vue +110 -0
- package/components/dd-avatar-group/props.js +57 -0
- package/components/dd-badge/dd-badge.vue +2 -2
- package/components/dd-barcode/dd-barcode.vue +1030 -0
- package/components/dd-bill-detail/dd-bill-detail.vue +265 -0
- package/components/dd-button/dd-button.vue +17 -17
- package/components/dd-calendar/dd-calendar.vue +357 -0
- package/components/dd-canvas/dd-canvas.vue +1088 -0
- package/components/dd-capsule-button/dd-capsule-button.vue +6 -6
- package/components/dd-car-keyboard/carKeyboard.js +6 -0
- package/components/dd-car-keyboard/dd-car-keyboard.vue +315 -0
- package/components/dd-car-keyboard/props.js +19 -0
- package/components/dd-cascader/dd-cascader.vue +405 -0
- package/components/dd-cell/dd-cell.vue +50 -23
- package/components/dd-cell-group/dd-cell-group.vue +32 -25
- package/components/dd-champion-card/dd-champion-card.vue +4 -4
- package/components/dd-checkbox/dd-checkbox.vue +30 -39
- package/components/dd-checkbox-group/dd-checkbox-group.vue +67 -0
- package/components/dd-copy/dd-copy.vue +74 -0
- package/components/dd-coupon/dd-coupon.vue +368 -0
- package/components/dd-cropper/dd-cropper.vue +1297 -0
- package/components/dd-dialog/dd-dialog.vue +1 -1
- package/components/dd-divider/dd-divider.vue +3 -3
- package/components/dd-dropdown-item/dd-dropdown-item.vue +5 -5
- package/components/dd-empty-state/dd-empty-state.vue +1 -1
- package/components/dd-field/dd-field.vue +531 -106
- package/components/dd-form/dd-form.vue +116 -0
- package/components/dd-form/rules.ts +84 -0
- package/components/dd-goods-sku/dd-goods-sku.vue +434 -0
- package/components/dd-icon/dd-icon.vue +2 -2
- package/components/dd-image/dd-image.vue +4 -4
- package/components/dd-input/dd-input.vue +5 -5
- package/components/dd-keyboard/dd-keyboard.vue +178 -0
- package/components/dd-keyboard/keyboard.js +22 -0
- package/components/dd-keyboard/props.js +89 -0
- package/components/dd-link/dd-link.vue +87 -0
- package/components/dd-link/link.js +18 -0
- package/components/dd-link/props.js +44 -0
- package/components/dd-list-cell/dd-list-cell.vue +4 -4
- package/components/dd-loading/dd-loading.vue +1 -1
- package/components/dd-loadmore/dd-loadmore.vue +2 -2
- package/components/dd-markdown/dd-markdown.vue +344 -0
- package/components/dd-markdown/marked.esm.mjs +7 -0
- package/components/dd-mini-program-navbar/dd-mini-program-navbar.vue +9 -9
- package/components/dd-navigation/dd-navigation.vue +5 -5
- package/components/dd-number-keyboard/dd-number-keyboard.vue +200 -0
- package/components/dd-number-keyboard/numberKeyboard.js +8 -0
- package/components/dd-number-keyboard/props.js +24 -0
- package/components/dd-parse/dd-parse.vue +534 -0
- package/components/dd-parse/node/handler.wxs +26 -0
- package/components/dd-parse/node/node.vue +603 -0
- package/components/dd-parse/parse.js +13 -0
- package/components/dd-parse/parser.js +1400 -0
- package/components/dd-parse/props.js +57 -0
- package/components/dd-popover/dd-popover.vue +10 -10
- package/components/dd-popover-item/dd-popover-item.vue +3 -3
- package/components/dd-popup/dd-popup.vue +5 -5
- package/components/dd-poster/dd-poster.vue +695 -0
- package/components/dd-progress/dd-progress.vue +3 -3
- package/components/dd-pull-refresh/dd-pull-refresh.vue +3 -3
- package/components/dd-qrcode/dd-qrcode.vue +509 -0
- package/components/dd-qrcode/qrcode.js +1283 -0
- package/components/dd-radio/dd-radio.vue +19 -20
- package/components/dd-radio-group/dd-radio-group.vue +53 -0
- package/components/dd-rate/dd-rate.vue +1 -1
- package/components/dd-room-card/dd-room-card.vue +3 -3
- package/components/dd-search-bar/dd-search-bar.vue +8 -8
- package/components/dd-segmented-tab/dd-segmented-tab.vue +6 -6
- package/components/dd-signature/dd-signature.vue +513 -0
- package/components/dd-skeleton/dd-skeleton.vue +1 -1
- package/components/dd-slider/dd-slider.vue +4 -4
- package/components/dd-stat-card/dd-stat-card.vue +5 -5
- package/components/dd-step/dd-step.vue +11 -11
- package/components/dd-stepper/dd-stepper.vue +6 -6
- package/components/dd-swipe-action/dd-swipe-action.vue +1 -1
- package/components/dd-swipeable-tab/dd-swipeable-tab.vue +1 -1
- package/components/dd-tabbar-item/dd-tabbar-item.vue +8 -8
- package/components/dd-tag/dd-tag.vue +17 -1
- package/components/dd-text/dd-text.vue +250 -0
- package/components/dd-text/props.js +120 -0
- package/components/dd-text/text.js +30 -0
- package/components/dd-text/value.js +87 -0
- package/components/dd-toast/dd-toast.vue +2 -2
- package/components/dd-upload/dd-upload.vue +4 -4
- package/components/dd-workorder-card/dd-workorder-card.vue +237 -0
- package/index.ts +50 -9
- package/libs/config/color.js +16 -0
- package/libs/config/config.js +25 -0
- package/libs/config/props.js +185 -0
- package/libs/config/zIndex.js +20 -0
- package/libs/function/digit.js +167 -0
- package/libs/function/index.js +932 -0
- package/libs/function/test.js +327 -0
- package/libs/i18n/index.d.ts +2 -0
- package/libs/i18n/index.js +89 -0
- package/libs/i18n/locales/zh-Hans.js +86 -0
- package/libs/mixin/button.js +18 -0
- package/libs/mixin/mixin.js +44 -0
- package/libs/mixin/mpMixin.js +13 -0
- package/libs/mixin/openType.js +27 -0
- package/libs/utils.ts +462 -0
- package/libs/vue.js +3 -0
- package/package.json +1 -1
- package/scss/_dd-shared.scss +40 -0
- package/scss/_variables.scss +4 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<view class="dd-form">
|
|
3
|
+
<slot></slot>
|
|
4
|
+
</view>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
import { provide, ref } from 'vue'
|
|
9
|
+
import type { DdFieldInstance, DdFieldValidateTrigger, DdFormContext } from './rules'
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
/** 子字段默认校验触发时机(field 的 rule.trigger 可覆盖) */
|
|
13
|
+
validateTrigger?: DdFieldValidateTrigger
|
|
14
|
+
/** 逐项校验,遇错即停(默认并行全量) */
|
|
15
|
+
validateFirst?: boolean
|
|
16
|
+
/** 下发子 field 的标签位置 */
|
|
17
|
+
labelPosition?: 'left' | 'top'
|
|
18
|
+
/** 下发子 field 的标签宽度(如 '160rpx',空 = 子自身默认) */
|
|
19
|
+
labelWidth?: string
|
|
20
|
+
/** 下发禁用全部子 field */
|
|
21
|
+
disabled?: boolean
|
|
22
|
+
/** submit 校验失败时滚动到第一个错误字段 */
|
|
23
|
+
scrollToError?: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
27
|
+
validateTrigger: 'onBlur',
|
|
28
|
+
validateFirst: false,
|
|
29
|
+
labelPosition: 'left',
|
|
30
|
+
labelWidth: '',
|
|
31
|
+
disabled: false,
|
|
32
|
+
scrollToError: false,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const emit = defineEmits<{
|
|
36
|
+
(e: 'submit', values: Record<string, any>): void
|
|
37
|
+
(e: 'failed', errors: { name: string; message: string }[]): void
|
|
38
|
+
}>()
|
|
39
|
+
|
|
40
|
+
const fields = ref<DdFieldInstance[]>([])
|
|
41
|
+
|
|
42
|
+
provide<DdFormContext>('dd-form', {
|
|
43
|
+
props,
|
|
44
|
+
register(field) {
|
|
45
|
+
if (!fields.value.some((f) => f.name === field.name)) fields.value.push(field)
|
|
46
|
+
},
|
|
47
|
+
unregister(name) {
|
|
48
|
+
const i = fields.value.findIndex((f) => f.name === name)
|
|
49
|
+
if (i >= 0) fields.value.splice(i, 1)
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
function getFields(name?: string | string[]): DdFieldInstance[] {
|
|
54
|
+
if (!name) return [...fields.value]
|
|
55
|
+
const arr = Array.isArray(name) ? name : [name]
|
|
56
|
+
return fields.value.filter((f) => arr.includes(f.name))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getValues(): Record<string, any> {
|
|
60
|
+
const values: Record<string, any> = {}
|
|
61
|
+
fields.value.forEach((f) => {
|
|
62
|
+
values[f.name] = f.getValue()
|
|
63
|
+
})
|
|
64
|
+
return values
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function validateField(name: string): Promise<void> {
|
|
68
|
+
const field = fields.value.find((f) => f.name === name)
|
|
69
|
+
if (!field) return
|
|
70
|
+
const message = await field.validate()
|
|
71
|
+
if (message !== undefined) throw [{ name, message }]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function validate(name?: string | string[]): Promise<Record<string, any>> {
|
|
75
|
+
const targets = getFields(name)
|
|
76
|
+
if (props.validateFirst) {
|
|
77
|
+
for (const field of targets) {
|
|
78
|
+
const message = await field.validate()
|
|
79
|
+
if (message !== undefined) throw [{ name: field.name, message }]
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
const results = await Promise.all(
|
|
83
|
+
targets.map(async (f) => ({ name: f.name, message: await f.validate() }))
|
|
84
|
+
)
|
|
85
|
+
const errors = results.filter((r) => r.message !== undefined)
|
|
86
|
+
if (errors.length > 0) throw errors
|
|
87
|
+
}
|
|
88
|
+
return getValues()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function resetValidation(name?: string | string[]): void {
|
|
92
|
+
getFields(name).forEach((f) => f.resetValidation())
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function submit(): Promise<void> {
|
|
96
|
+
try {
|
|
97
|
+
const values = await validate()
|
|
98
|
+
emit('submit', values)
|
|
99
|
+
} catch (e) {
|
|
100
|
+
const errors = e as { name: string; message: string }[]
|
|
101
|
+
emit('failed', errors)
|
|
102
|
+
if (props.scrollToError) {
|
|
103
|
+
fields.value.find((f) => f.name === errors[0]?.name)?.scrollIntoView()
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
defineExpose({ validate, validateField, resetValidation, submit, getValues })
|
|
109
|
+
</script>
|
|
110
|
+
|
|
111
|
+
<style lang="scss" scoped>
|
|
112
|
+
.dd-form {
|
|
113
|
+
display: block;
|
|
114
|
+
width: 100%;
|
|
115
|
+
}
|
|
116
|
+
</style>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dd-form 轻量校验引擎(vant runRules 思想,自研无依赖)
|
|
3
|
+
* 规则分散在各 dd-field 上,dd-form 只做调度。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type DdFieldValidateTrigger = 'onBlur' | 'onChange' | 'onSubmit'
|
|
7
|
+
|
|
8
|
+
/** dd-field 向 dd-form 注册的实例契约 */
|
|
9
|
+
export interface DdFieldInstance {
|
|
10
|
+
name: string
|
|
11
|
+
getValue: () => any
|
|
12
|
+
/** 校验并显示错误;返回错误消息(undefined = 通过) */
|
|
13
|
+
validate: () => Promise<string | undefined>
|
|
14
|
+
resetValidation: () => void
|
|
15
|
+
scrollIntoView: () => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** dd-form 通过 provide 下发的上下文契约 */
|
|
19
|
+
export interface DdFormContext {
|
|
20
|
+
props: {
|
|
21
|
+
validateTrigger: DdFieldValidateTrigger
|
|
22
|
+
labelPosition: 'left' | 'top'
|
|
23
|
+
labelWidth: string
|
|
24
|
+
disabled: boolean
|
|
25
|
+
}
|
|
26
|
+
register: (field: DdFieldInstance) => void
|
|
27
|
+
unregister: (name: string) => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DdFieldRule {
|
|
31
|
+
/** 必填(空值报错) */
|
|
32
|
+
required?: boolean
|
|
33
|
+
/** 错误消息;缺省时按规则类型兜底 */
|
|
34
|
+
message?: string
|
|
35
|
+
/** 正则校验(仅对非空值测试) */
|
|
36
|
+
pattern?: RegExp
|
|
37
|
+
/**
|
|
38
|
+
* 自定义校验:返回 true=通过,false=用 message 报错,
|
|
39
|
+
* 返回 string=直接以该字符串为错误消息(vant 语义);支持 Promise
|
|
40
|
+
*/
|
|
41
|
+
validator?: (value: any) => boolean | string | Promise<boolean | string>
|
|
42
|
+
/** 触发时机;缺省 = 任意触发都执行 */
|
|
43
|
+
trigger?: DdFieldValidateTrigger
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 空值判断:'' / null / undefined / 空数组 */
|
|
47
|
+
export function isEmptyValue(value: any): boolean {
|
|
48
|
+
if (value === '' || value === null || value === undefined) return true
|
|
49
|
+
if (Array.isArray(value) && value.length === 0) return true
|
|
50
|
+
return false
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function runValidator(rule: DdFieldRule, value: any): Promise<string | undefined> {
|
|
54
|
+
const res = await rule.validator!(value)
|
|
55
|
+
if (res === true) return undefined
|
|
56
|
+
if (typeof res === 'string') return res
|
|
57
|
+
return rule.message ?? '校验不通过'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 按触发方式过滤规则后顺序执行,首个失败短路。
|
|
62
|
+
* @returns 错误消息;undefined = 全部通过
|
|
63
|
+
*/
|
|
64
|
+
export async function runRules(
|
|
65
|
+
value: any,
|
|
66
|
+
rules: DdFieldRule[] = [],
|
|
67
|
+
trigger?: DdFieldValidateTrigger
|
|
68
|
+
): Promise<string | undefined> {
|
|
69
|
+
for (const rule of rules) {
|
|
70
|
+
if (trigger && rule.trigger && rule.trigger !== trigger) continue
|
|
71
|
+
|
|
72
|
+
if (rule.required && isEmptyValue(value)) {
|
|
73
|
+
return rule.message ?? '不能为空'
|
|
74
|
+
}
|
|
75
|
+
if (rule.pattern && !isEmptyValue(value) && !rule.pattern.test(String(value))) {
|
|
76
|
+
return rule.message ?? '格式不正确'
|
|
77
|
+
}
|
|
78
|
+
if (rule.validator) {
|
|
79
|
+
const err = await runValidator(rule, value)
|
|
80
|
+
if (err !== undefined) return err
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return undefined
|
|
84
|
+
}
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<view class="dd-goods-sku">
|
|
3
|
+
<view @click="open">
|
|
4
|
+
<slot name="trigger"></slot>
|
|
5
|
+
</view>
|
|
6
|
+
<!-- ponytail: pageInline 仅控制 closeable 与内边距,dd-popup 无内联形态,始终以弹窗呈现 -->
|
|
7
|
+
<dd-popup
|
|
8
|
+
v-model="show"
|
|
9
|
+
position="bottom"
|
|
10
|
+
round
|
|
11
|
+
:closeable="pageInline ? false : closeable"
|
|
12
|
+
@close="close"
|
|
13
|
+
>
|
|
14
|
+
<view class="dd-goods-sku__container" :style="{ padding: pageInline ? '0px' : '' }">
|
|
15
|
+
<view class="dd-goods-sku__header">
|
|
16
|
+
<slot name="header">
|
|
17
|
+
<view class="dd-goods-sku__header__image">
|
|
18
|
+
<image :src="goodsInfo.image || goodsInfo.picture" mode="aspectFill"></image>
|
|
19
|
+
</view>
|
|
20
|
+
<view class="dd-goods-sku__header__info">
|
|
21
|
+
<view class="dd-goods-sku__header__info__price">
|
|
22
|
+
<text class="dd-goods-sku__header__info__price__symbol">¥</text>
|
|
23
|
+
<text class="dd-goods-sku__header__info__price__value">{{ price }}</text>
|
|
24
|
+
</view>
|
|
25
|
+
<view class="dd-goods-sku__header__info__stock">{{ t('dd.goodsSku.stock') }} {{ stock }} {{ t('dd.goodsSku.amount') }}</view>
|
|
26
|
+
<view class="dd-goods-sku__header__info__selected">{{ t('dd.goodsSku.choosed') }}: {{ selectedSkuText }}</view>
|
|
27
|
+
</view>
|
|
28
|
+
</slot>
|
|
29
|
+
</view>
|
|
30
|
+
|
|
31
|
+
<scroll-view class="dd-goods-sku__content" scroll-y>
|
|
32
|
+
<view v-for="(treeItem, index) in skuTree" :key="index" class="dd-goods-sku__content__item">
|
|
33
|
+
<view class="dd-goods-sku__content__item__title">{{ treeItem.label }}</view>
|
|
34
|
+
<view class="dd-goods-sku__content__item__list">
|
|
35
|
+
<view
|
|
36
|
+
v-for="(leafItem, leafIndex) in treeItem.children"
|
|
37
|
+
:key="leafIndex"
|
|
38
|
+
class="dd-goods-sku__content__item__list__item"
|
|
39
|
+
:class="{
|
|
40
|
+
'dd-goods-sku__content__item__list__item--active': isSelected(treeItem.name, leafItem.id),
|
|
41
|
+
'dd-goods-sku__content__item__list__item--disabled': isDisabled(treeItem.name, leafItem.id),
|
|
42
|
+
}"
|
|
43
|
+
@click="onSkuClick(treeItem.name, leafItem)"
|
|
44
|
+
>
|
|
45
|
+
<text>{{ leafItem.name }}</text>
|
|
46
|
+
</view>
|
|
47
|
+
</view>
|
|
48
|
+
</view>
|
|
49
|
+
|
|
50
|
+
<view class="dd-goods-sku__content__count">
|
|
51
|
+
<view class="dd-goods-sku__content__count__title">{{ t('dd.goodsSku.buyAmount') }}</view>
|
|
52
|
+
<view class="dd-goods-sku__content__count__control">
|
|
53
|
+
<dd-stepper
|
|
54
|
+
v-model="buyNum"
|
|
55
|
+
:min="1"
|
|
56
|
+
:max="maxBuyNum"
|
|
57
|
+
:disabled="!canBuy"
|
|
58
|
+
></dd-stepper>
|
|
59
|
+
</view>
|
|
60
|
+
</view>
|
|
61
|
+
</scroll-view>
|
|
62
|
+
|
|
63
|
+
<view class="dd-goods-sku__footer">
|
|
64
|
+
<dd-button type="primary" :disabled="!canBuy" @click="onConfirm">
|
|
65
|
+
{{ confirmText }}
|
|
66
|
+
</dd-button>
|
|
67
|
+
</view>
|
|
68
|
+
</view>
|
|
69
|
+
</dd-popup>
|
|
70
|
+
</view>
|
|
71
|
+
</template>
|
|
72
|
+
|
|
73
|
+
<script setup lang="ts">
|
|
74
|
+
import { computed, reactive, ref } from 'vue'
|
|
75
|
+
import { t } from '../../libs/i18n'
|
|
76
|
+
import DdPopup from '../dd-popup/dd-popup.vue'
|
|
77
|
+
import DdStepper from '../dd-stepper/dd-stepper.vue'
|
|
78
|
+
import DdButton from '../dd-button/dd-button.vue'
|
|
79
|
+
|
|
80
|
+
interface SkuLeaf {
|
|
81
|
+
id: string | number
|
|
82
|
+
name: string
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface SkuTreeItem {
|
|
86
|
+
name: string
|
|
87
|
+
label: string
|
|
88
|
+
children: SkuLeaf[]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface Props {
|
|
92
|
+
/** 商品信息 */
|
|
93
|
+
goodsInfo?: Record<string, any>
|
|
94
|
+
/** SKU树形结构 */
|
|
95
|
+
skuTree?: SkuTreeItem[]
|
|
96
|
+
/** SKU列表 */
|
|
97
|
+
skuList?: Record<string, any>[]
|
|
98
|
+
/** 最大购买数量 */
|
|
99
|
+
maxBuy?: number
|
|
100
|
+
/** 确认按钮文字 */
|
|
101
|
+
confirmText?: string
|
|
102
|
+
/** 是否显示关闭弹窗按钮 */
|
|
103
|
+
closeable?: boolean
|
|
104
|
+
/** 是否页面内联模式 */
|
|
105
|
+
pageInline?: boolean
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
109
|
+
goodsInfo: () => ({}),
|
|
110
|
+
skuTree: () => [],
|
|
111
|
+
skuList: () => [],
|
|
112
|
+
maxBuy: 999,
|
|
113
|
+
confirmText: '确定',
|
|
114
|
+
closeable: true,
|
|
115
|
+
pageInline: false,
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const emit = defineEmits<{
|
|
119
|
+
(e: 'open'): void
|
|
120
|
+
(e: 'confirm', val: { sku: Record<string, any> | null; goodsInfo: Record<string, any>; num: number; selectedText: string }): void
|
|
121
|
+
(e: 'close'): void
|
|
122
|
+
}>()
|
|
123
|
+
|
|
124
|
+
const show = ref(false)
|
|
125
|
+
/** 已选择的SKU */
|
|
126
|
+
const selectedSku = reactive<Record<string, string | number | ''>>({})
|
|
127
|
+
/** 购买数量 */
|
|
128
|
+
const buyNum = ref(1)
|
|
129
|
+
|
|
130
|
+
if (props.pageInline) {
|
|
131
|
+
show.value = true
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 当前价格
|
|
135
|
+
const price = computed(() => {
|
|
136
|
+
const comb = getSelectedSkuComb()
|
|
137
|
+
if (comb) return comb.price || comb.price_fee
|
|
138
|
+
return props.goodsInfo.price || props.goodsInfo.price_fee || 0
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
// 当前库存
|
|
142
|
+
const stock = computed(() => {
|
|
143
|
+
const comb = getSelectedSkuComb()
|
|
144
|
+
if (comb) return comb.stock || comb.quantity
|
|
145
|
+
return props.goodsInfo.stock || props.goodsInfo.quantity || 0
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
// 最大购买数量
|
|
149
|
+
const maxBuyNum = computed(() => {
|
|
150
|
+
return stock.value > props.maxBuy ? props.maxBuy : stock.value
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// 是否可以购买
|
|
154
|
+
const canBuy = computed(() => {
|
|
155
|
+
const selectedSkuCount = Object.keys(selectedSku).filter((key) => selectedSku[key] !== '').length
|
|
156
|
+
return selectedSkuCount === props.skuTree.length && buyNum.value > 0 && stock.value > 0
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
// 已选SKU文字描述
|
|
160
|
+
const selectedSkuText = computed(() => {
|
|
161
|
+
const selected: string[] = []
|
|
162
|
+
Object.keys(selectedSku).forEach((key) => {
|
|
163
|
+
const value = selectedSku[key]
|
|
164
|
+
if (value) {
|
|
165
|
+
props.skuTree.forEach((treeItem) => {
|
|
166
|
+
if (treeItem.name === key) {
|
|
167
|
+
treeItem.children.forEach((leafItem) => {
|
|
168
|
+
if (leafItem.id === value) {
|
|
169
|
+
selected.push(leafItem.name)
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
return selected.join(', ')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
// 判断SKU是否被选中
|
|
180
|
+
function isSelected(skuKey: string, skuValueId: string | number) {
|
|
181
|
+
return selectedSku[skuKey] === skuValueId
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 判断SKU是否禁用
|
|
185
|
+
function isDisabled(skuKey: string, skuValueId: string | number) {
|
|
186
|
+
// 构造一个临时的已选中SKU对象
|
|
187
|
+
const tempSelected: Record<string, any> = { ...selectedSku, [skuKey]: skuValueId }
|
|
188
|
+
|
|
189
|
+
// 检查是否还有未选择的SKU维度
|
|
190
|
+
const selectedCount = Object.keys(tempSelected).filter((key) => tempSelected[key]).length
|
|
191
|
+
const totalSkuCount = props.skuTree.length
|
|
192
|
+
|
|
193
|
+
// 如果所有SKU都已选择,则检查组合是否存在
|
|
194
|
+
if (selectedCount === totalSkuCount) {
|
|
195
|
+
return !getSkuComb(tempSelected)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 检查当前选择的SKU是否会导致无法组成有效组合
|
|
199
|
+
for (let i = 0; i < props.skuList.length; i++) {
|
|
200
|
+
const sku = props.skuList[i]
|
|
201
|
+
let match = true
|
|
202
|
+
|
|
203
|
+
for (const key in tempSelected) {
|
|
204
|
+
if (tempSelected[key] && sku[key] !== tempSelected[key]) {
|
|
205
|
+
match = false
|
|
206
|
+
break
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (match) {
|
|
211
|
+
return false
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return true
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// SKU点击事件
|
|
219
|
+
function onSkuClick(skuKey: string, skuValue: SkuLeaf) {
|
|
220
|
+
// 如果是禁用状态,直接返回
|
|
221
|
+
if (isDisabled(skuKey, skuValue.id)) {
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 如果已选中,则取消选中
|
|
226
|
+
if (selectedSku[skuKey] === skuValue.id) {
|
|
227
|
+
selectedSku[skuKey] = ''
|
|
228
|
+
} else {
|
|
229
|
+
selectedSku[skuKey] = skuValue.id
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// 获取选中的SKU组合
|
|
234
|
+
function getSelectedSkuComb() {
|
|
235
|
+
return getSkuComb(selectedSku)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 根据已选SKU获取组合信息
|
|
239
|
+
function getSkuComb(selectedSkuObj: Record<string, any>) {
|
|
240
|
+
const selected: Record<string, any> = { ...selectedSkuObj }
|
|
241
|
+
|
|
242
|
+
// 过滤掉空值
|
|
243
|
+
Object.keys(selected).forEach((key) => {
|
|
244
|
+
if (!selected[key]) {
|
|
245
|
+
delete selected[key]
|
|
246
|
+
}
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
// 检查是否所有SKU都已选择
|
|
250
|
+
if (Object.keys(selected).length !== props.skuTree.length) {
|
|
251
|
+
return null
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 查找匹配的SKU组合
|
|
255
|
+
for (let i = 0; i < props.skuList.length; i++) {
|
|
256
|
+
const sku = props.skuList[i]
|
|
257
|
+
let match = true
|
|
258
|
+
|
|
259
|
+
for (const key in selected) {
|
|
260
|
+
if (sku[key] !== selected[key]) {
|
|
261
|
+
match = false
|
|
262
|
+
break
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (match) {
|
|
267
|
+
return sku
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return null
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// 重置选择
|
|
275
|
+
function reset() {
|
|
276
|
+
Object.keys(selectedSku).forEach((key) => delete selectedSku[key])
|
|
277
|
+
buyNum.value = 1
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function open() {
|
|
281
|
+
show.value = true
|
|
282
|
+
emit('open')
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 关闭弹窗(修复原版 this.false = true 笔误)
|
|
286
|
+
function close() {
|
|
287
|
+
show.value = false
|
|
288
|
+
emit('close')
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 确认选择
|
|
292
|
+
function onConfirm() {
|
|
293
|
+
if (!canBuy.value) {
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const selectedSkuComb = getSelectedSkuComb()
|
|
298
|
+
emit('confirm', {
|
|
299
|
+
sku: selectedSkuComb,
|
|
300
|
+
goodsInfo: props.goodsInfo,
|
|
301
|
+
num: buyNum.value,
|
|
302
|
+
selectedText: selectedSkuText.value,
|
|
303
|
+
})
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
defineExpose({ open, close, reset })
|
|
307
|
+
</script>
|
|
308
|
+
|
|
309
|
+
<style lang="scss" scoped>
|
|
310
|
+
@import '../../scss/variables';
|
|
311
|
+
@import '../../scss/mixins';
|
|
312
|
+
|
|
313
|
+
.dd-goods-sku {
|
|
314
|
+
background-color: $dd-bg-elevated;
|
|
315
|
+
overflow: hidden;
|
|
316
|
+
|
|
317
|
+
&__container {
|
|
318
|
+
padding: 4rpx 30rpx;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
&__header {
|
|
322
|
+
display: flex;
|
|
323
|
+
flex-direction: row;
|
|
324
|
+
padding: 30rpx 0;
|
|
325
|
+
position: relative;
|
|
326
|
+
|
|
327
|
+
&__image {
|
|
328
|
+
width: 180rpx;
|
|
329
|
+
height: 180rpx;
|
|
330
|
+
border-radius: 10rpx;
|
|
331
|
+
overflow: hidden;
|
|
332
|
+
margin-right: 20rpx;
|
|
333
|
+
|
|
334
|
+
image {
|
|
335
|
+
width: 100%;
|
|
336
|
+
height: 100%;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
&__info {
|
|
341
|
+
flex: 1;
|
|
342
|
+
|
|
343
|
+
&__price {
|
|
344
|
+
display: flex;
|
|
345
|
+
flex-direction: row;
|
|
346
|
+
align-items: baseline;
|
|
347
|
+
margin-bottom: 20rpx;
|
|
348
|
+
|
|
349
|
+
&__symbol {
|
|
350
|
+
font-size: 24rpx;
|
|
351
|
+
color: $dd-error-400;
|
|
352
|
+
margin-right: 4rpx;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
&__value {
|
|
356
|
+
font-size: 36rpx;
|
|
357
|
+
color: $dd-error-400;
|
|
358
|
+
font-weight: bold;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
&__stock {
|
|
363
|
+
font-size: 26rpx;
|
|
364
|
+
color: $dd-text-tertiary;
|
|
365
|
+
margin-bottom: 20rpx;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
&__selected {
|
|
369
|
+
font-size: 26rpx;
|
|
370
|
+
color: $dd-text-primary;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
&__content {
|
|
376
|
+
max-height: 600rpx;
|
|
377
|
+
padding: 0 30rpx 30rpx 0;
|
|
378
|
+
|
|
379
|
+
&__item {
|
|
380
|
+
margin-bottom: 30rpx;
|
|
381
|
+
|
|
382
|
+
&__title {
|
|
383
|
+
font-size: 28rpx;
|
|
384
|
+
color: $dd-text-primary;
|
|
385
|
+
margin-bottom: 20rpx;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
&__list {
|
|
389
|
+
display: flex;
|
|
390
|
+
flex-direction: row;
|
|
391
|
+
flex-wrap: wrap;
|
|
392
|
+
|
|
393
|
+
&__item {
|
|
394
|
+
padding: 10rpx 20rpx;
|
|
395
|
+
border: 2rpx solid $dd-border-default;
|
|
396
|
+
border-radius: 10rpx;
|
|
397
|
+
margin-right: 20rpx;
|
|
398
|
+
margin-bottom: 20rpx;
|
|
399
|
+
font-size: 26rpx;
|
|
400
|
+
color: $dd-text-primary;
|
|
401
|
+
background-color: $dd-bg-elevated;
|
|
402
|
+
|
|
403
|
+
&--active {
|
|
404
|
+
border-color: $dd-error-400;
|
|
405
|
+
color: $dd-error-400;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
&--disabled {
|
|
409
|
+
color: $dd-neutral-500;
|
|
410
|
+
border-color: $dd-border-default;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
&__count {
|
|
417
|
+
display: flex;
|
|
418
|
+
flex-direction: row;
|
|
419
|
+
align-items: center;
|
|
420
|
+
justify-content: space-between;
|
|
421
|
+
margin-top: 20rpx;
|
|
422
|
+
|
|
423
|
+
&__title {
|
|
424
|
+
font-size: 28rpx;
|
|
425
|
+
color: $dd-text-primary;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
&__footer {
|
|
431
|
+
padding: 20rpx 0 40rpx 0;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
</style>
|
|
@@ -136,16 +136,16 @@ watch(
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
.dd-image__spinner {
|
|
139
|
-
width:
|
|
140
|
-
height:
|
|
141
|
-
border:
|
|
139
|
+
width: $dd-size-icon-lg;
|
|
140
|
+
height: $dd-size-icon-lg;
|
|
141
|
+
border: $dd-space-1 solid $dd-border-default;
|
|
142
142
|
border-top-color: $dd-primary-500;
|
|
143
143
|
border-radius: 50%;
|
|
144
144
|
animation: dd-image-spin 0.8s linear infinite;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
.dd-image__error-icon {
|
|
148
|
-
font-size:
|
|
148
|
+
font-size: $dd-size-icon-lg; // 24px
|
|
149
149
|
color: $dd-text-tertiary;
|
|
150
150
|
line-height: 1;
|
|
151
151
|
}
|
|
@@ -112,9 +112,9 @@ function toggleVisible() {
|
|
|
112
112
|
.dd-input {
|
|
113
113
|
display: flex;
|
|
114
114
|
align-items: center;
|
|
115
|
-
gap:
|
|
115
|
+
gap: $dd-space-2;
|
|
116
116
|
height: 88rpx;
|
|
117
|
-
padding: 0
|
|
117
|
+
padding: 0 $dd-space-4;
|
|
118
118
|
background: $dd-neutral-800;
|
|
119
119
|
border: 1px solid $dd-neutral-700;
|
|
120
120
|
border-radius: $dd-radius-md;
|
|
@@ -125,7 +125,7 @@ function toggleVisible() {
|
|
|
125
125
|
flex: 1;
|
|
126
126
|
min-width: 0;
|
|
127
127
|
height: 100%;
|
|
128
|
-
font-size:
|
|
128
|
+
font-size: $dd-font-size-body;
|
|
129
129
|
color: $dd-neutral-100;
|
|
130
130
|
background: transparent;
|
|
131
131
|
border: none;
|
|
@@ -144,12 +144,12 @@ function toggleVisible() {
|
|
|
144
144
|
border-radius: 50%;
|
|
145
145
|
background: $dd-neutral-700;
|
|
146
146
|
color: $dd-neutral-300;
|
|
147
|
-
font-size:
|
|
147
|
+
font-size: $dd-font-size-caption;
|
|
148
148
|
line-height: 1;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
&__toggle {
|
|
152
|
-
font-size:
|
|
152
|
+
font-size: $dd-font-size-h4;
|
|
153
153
|
opacity: 0.7;
|
|
154
154
|
line-height: 1;
|
|
155
155
|
}
|