@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.
Files changed (114) hide show
  1. package/README.md +16 -7
  2. package/components/dd-action-sheet/dd-action-sheet.vue +3 -3
  3. package/components/dd-album/album.js +19 -0
  4. package/components/dd-album/dd-album.vue +345 -0
  5. package/components/dd-album/props.js +89 -0
  6. package/components/dd-avatar/avatar.js +19 -0
  7. package/components/dd-avatar/dd-avatar.vue +172 -160
  8. package/components/dd-avatar/props.js +84 -0
  9. package/components/dd-avatar-group/avatarGroup.js +14 -0
  10. package/components/dd-avatar-group/dd-avatar-group.vue +110 -0
  11. package/components/dd-avatar-group/props.js +57 -0
  12. package/components/dd-badge/dd-badge.vue +2 -2
  13. package/components/dd-barcode/dd-barcode.vue +1030 -0
  14. package/components/dd-bill-detail/dd-bill-detail.vue +265 -0
  15. package/components/dd-button/dd-button.vue +17 -17
  16. package/components/dd-calendar/dd-calendar.vue +357 -0
  17. package/components/dd-canvas/dd-canvas.vue +1088 -0
  18. package/components/dd-capsule-button/dd-capsule-button.vue +6 -6
  19. package/components/dd-car-keyboard/carKeyboard.js +6 -0
  20. package/components/dd-car-keyboard/dd-car-keyboard.vue +315 -0
  21. package/components/dd-car-keyboard/props.js +19 -0
  22. package/components/dd-cascader/dd-cascader.vue +405 -0
  23. package/components/dd-cell/dd-cell.vue +50 -23
  24. package/components/dd-cell-group/dd-cell-group.vue +32 -25
  25. package/components/dd-champion-card/dd-champion-card.vue +4 -4
  26. package/components/dd-checkbox/dd-checkbox.vue +30 -39
  27. package/components/dd-checkbox-group/dd-checkbox-group.vue +67 -0
  28. package/components/dd-copy/dd-copy.vue +74 -0
  29. package/components/dd-coupon/dd-coupon.vue +368 -0
  30. package/components/dd-cropper/dd-cropper.vue +1297 -0
  31. package/components/dd-dialog/dd-dialog.vue +1 -1
  32. package/components/dd-divider/dd-divider.vue +3 -3
  33. package/components/dd-dropdown-item/dd-dropdown-item.vue +5 -5
  34. package/components/dd-empty-state/dd-empty-state.vue +1 -1
  35. package/components/dd-field/dd-field.vue +531 -106
  36. package/components/dd-form/dd-form.vue +116 -0
  37. package/components/dd-form/rules.ts +84 -0
  38. package/components/dd-goods-sku/dd-goods-sku.vue +434 -0
  39. package/components/dd-icon/dd-icon.vue +2 -2
  40. package/components/dd-image/dd-image.vue +4 -4
  41. package/components/dd-input/dd-input.vue +5 -5
  42. package/components/dd-keyboard/dd-keyboard.vue +178 -0
  43. package/components/dd-keyboard/keyboard.js +22 -0
  44. package/components/dd-keyboard/props.js +89 -0
  45. package/components/dd-link/dd-link.vue +87 -0
  46. package/components/dd-link/link.js +18 -0
  47. package/components/dd-link/props.js +44 -0
  48. package/components/dd-list-cell/dd-list-cell.vue +4 -4
  49. package/components/dd-loading/dd-loading.vue +1 -1
  50. package/components/dd-loadmore/dd-loadmore.vue +2 -2
  51. package/components/dd-markdown/dd-markdown.vue +344 -0
  52. package/components/dd-markdown/marked.esm.mjs +7 -0
  53. package/components/dd-mini-program-navbar/dd-mini-program-navbar.vue +9 -9
  54. package/components/dd-navigation/dd-navigation.vue +5 -5
  55. package/components/dd-number-keyboard/dd-number-keyboard.vue +200 -0
  56. package/components/dd-number-keyboard/numberKeyboard.js +8 -0
  57. package/components/dd-number-keyboard/props.js +24 -0
  58. package/components/dd-parse/dd-parse.vue +534 -0
  59. package/components/dd-parse/node/handler.wxs +26 -0
  60. package/components/dd-parse/node/node.vue +603 -0
  61. package/components/dd-parse/parse.js +13 -0
  62. package/components/dd-parse/parser.js +1400 -0
  63. package/components/dd-parse/props.js +57 -0
  64. package/components/dd-popover/dd-popover.vue +10 -10
  65. package/components/dd-popover-item/dd-popover-item.vue +3 -3
  66. package/components/dd-popup/dd-popup.vue +5 -5
  67. package/components/dd-poster/dd-poster.vue +695 -0
  68. package/components/dd-progress/dd-progress.vue +3 -3
  69. package/components/dd-pull-refresh/dd-pull-refresh.vue +3 -3
  70. package/components/dd-qrcode/dd-qrcode.vue +509 -0
  71. package/components/dd-qrcode/qrcode.js +1283 -0
  72. package/components/dd-radio/dd-radio.vue +19 -20
  73. package/components/dd-radio-group/dd-radio-group.vue +53 -0
  74. package/components/dd-rate/dd-rate.vue +1 -1
  75. package/components/dd-room-card/dd-room-card.vue +3 -3
  76. package/components/dd-search-bar/dd-search-bar.vue +8 -8
  77. package/components/dd-segmented-tab/dd-segmented-tab.vue +6 -6
  78. package/components/dd-signature/dd-signature.vue +513 -0
  79. package/components/dd-skeleton/dd-skeleton.vue +1 -1
  80. package/components/dd-slider/dd-slider.vue +4 -4
  81. package/components/dd-stat-card/dd-stat-card.vue +5 -5
  82. package/components/dd-step/dd-step.vue +11 -11
  83. package/components/dd-stepper/dd-stepper.vue +6 -6
  84. package/components/dd-swipe-action/dd-swipe-action.vue +1 -1
  85. package/components/dd-swipeable-tab/dd-swipeable-tab.vue +1 -1
  86. package/components/dd-tabbar-item/dd-tabbar-item.vue +8 -8
  87. package/components/dd-tag/dd-tag.vue +17 -1
  88. package/components/dd-text/dd-text.vue +250 -0
  89. package/components/dd-text/props.js +120 -0
  90. package/components/dd-text/text.js +30 -0
  91. package/components/dd-text/value.js +87 -0
  92. package/components/dd-toast/dd-toast.vue +2 -2
  93. package/components/dd-upload/dd-upload.vue +4 -4
  94. package/components/dd-workorder-card/dd-workorder-card.vue +237 -0
  95. package/index.ts +50 -9
  96. package/libs/config/color.js +16 -0
  97. package/libs/config/config.js +25 -0
  98. package/libs/config/props.js +185 -0
  99. package/libs/config/zIndex.js +20 -0
  100. package/libs/function/digit.js +167 -0
  101. package/libs/function/index.js +932 -0
  102. package/libs/function/test.js +327 -0
  103. package/libs/i18n/index.d.ts +2 -0
  104. package/libs/i18n/index.js +89 -0
  105. package/libs/i18n/locales/zh-Hans.js +86 -0
  106. package/libs/mixin/button.js +18 -0
  107. package/libs/mixin/mixin.js +44 -0
  108. package/libs/mixin/mpMixin.js +13 -0
  109. package/libs/mixin/openType.js +27 -0
  110. package/libs/utils.ts +462 -0
  111. package/libs/vue.js +3 -0
  112. package/package.json +1 -1
  113. package/scss/_dd-shared.scss +40 -0
  114. 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>
@@ -193,8 +193,8 @@ const iconStyle = computed(() => {
193
193
  &--dot::after {
194
194
  content: '';
195
195
  position: absolute;
196
- top: -4rpx;
197
- right: -4rpx;
196
+ top: -$dd-space-1;
197
+ right: -$dd-space-1;
198
198
  width: 12rpx;
199
199
  height: 12rpx;
200
200
  background: $dd-error-500;
@@ -136,16 +136,16 @@ watch(
136
136
  }
137
137
 
138
138
  .dd-image__spinner {
139
- width: 48rpx;
140
- height: 48rpx;
141
- border: 4rpx solid $dd-border-default;
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: 48rpx; // 24px
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: 16rpx;
115
+ gap: $dd-space-2;
116
116
  height: 88rpx;
117
- padding: 0 32rpx;
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: 28rpx;
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: 24rpx;
147
+ font-size: $dd-font-size-caption;
148
148
  line-height: 1;
149
149
  }
150
150
 
151
151
  &__toggle {
152
- font-size: 32rpx;
152
+ font-size: $dd-font-size-h4;
153
153
  opacity: 0.7;
154
154
  line-height: 1;
155
155
  }