@didaoktv/didaoui-uniapp 1.0.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 (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +89 -0
  3. package/components/dd-action-sheet/dd-action-sheet.vue +207 -0
  4. package/components/dd-alert/dd-alert.vue +240 -0
  5. package/components/dd-avatar/dd-avatar.vue +168 -0
  6. package/components/dd-backtop/dd-backtop.vue +75 -0
  7. package/components/dd-badge/dd-badge.vue +150 -0
  8. package/components/dd-button/dd-button.vue +215 -0
  9. package/components/dd-capsule-button/dd-capsule-button.vue +109 -0
  10. package/components/dd-card/dd-card.vue +155 -0
  11. package/components/dd-cell/dd-cell.vue +201 -0
  12. package/components/dd-cell-group/dd-cell-group.vue +56 -0
  13. package/components/dd-champion-card/dd-champion-card.vue +258 -0
  14. package/components/dd-checkbox/dd-checkbox.vue +145 -0
  15. package/components/dd-collapse/dd-collapse.vue +63 -0
  16. package/components/dd-collapse-item/dd-collapse-item.vue +154 -0
  17. package/components/dd-count-down/dd-count-down.vue +163 -0
  18. package/components/dd-date-picker/dd-date-picker.vue +253 -0
  19. package/components/dd-dialog/dd-dialog.vue +264 -0
  20. package/components/dd-divider/dd-divider.vue +104 -0
  21. package/components/dd-drawer/dd-drawer.vue +214 -0
  22. package/components/dd-dropdown-item/dd-dropdown-item.vue +203 -0
  23. package/components/dd-dropdown-menu/dd-dropdown-menu.vue +177 -0
  24. package/components/dd-empty-state/dd-empty-state.vue +140 -0
  25. package/components/dd-feature-grid/dd-feature-grid.vue +139 -0
  26. package/components/dd-field/dd-field.vue +240 -0
  27. package/components/dd-icon/dd-icon.vue +464 -0
  28. package/components/dd-image/dd-image.vue +158 -0
  29. package/components/dd-input/dd-input.vue +172 -0
  30. package/components/dd-list-cell/dd-list-cell.vue +152 -0
  31. package/components/dd-loading/dd-loading.vue +187 -0
  32. package/components/dd-loadmore/dd-loadmore.vue +152 -0
  33. package/components/dd-mini-program-navbar/dd-mini-program-navbar.vue +245 -0
  34. package/components/dd-modal/dd-modal.vue +286 -0
  35. package/components/dd-navigation/dd-navigation.vue +134 -0
  36. package/components/dd-overlay/dd-overlay.vue +114 -0
  37. package/components/dd-picker/dd-picker.vue +203 -0
  38. package/components/dd-popover/dd-popover.vue +190 -0
  39. package/components/dd-popover-item/dd-popover-item.vue +88 -0
  40. package/components/dd-popup/dd-popup.vue +243 -0
  41. package/components/dd-progress/dd-progress.vue +255 -0
  42. package/components/dd-pull-refresh/dd-pull-refresh.vue +232 -0
  43. package/components/dd-radio/dd-radio.vue +129 -0
  44. package/components/dd-rate/dd-rate.vue +133 -0
  45. package/components/dd-room-card/dd-room-card.vue +309 -0
  46. package/components/dd-search-bar/dd-search-bar.vue +176 -0
  47. package/components/dd-segmented-tab/dd-segmented-tab.vue +187 -0
  48. package/components/dd-skeleton/dd-skeleton.vue +184 -0
  49. package/components/dd-slider/dd-slider.vue +227 -0
  50. package/components/dd-stat-card/dd-stat-card.vue +174 -0
  51. package/components/dd-step/dd-step.vue +188 -0
  52. package/components/dd-stepper/dd-stepper.vue +171 -0
  53. package/components/dd-steps/dd-steps.vue +61 -0
  54. package/components/dd-sticky/dd-sticky.vue +132 -0
  55. package/components/dd-swipe/dd-swipe.vue +146 -0
  56. package/components/dd-swipe-action/dd-swipe-action.vue +248 -0
  57. package/components/dd-swipe-item/dd-swipe-item.vue +27 -0
  58. package/components/dd-swipeable-tab/dd-swipeable-tab.vue +202 -0
  59. package/components/dd-switch/dd-switch.vue +106 -0
  60. package/components/dd-tabbar/dd-tabbar.vue +112 -0
  61. package/components/dd-tabbar-item/dd-tabbar-item.vue +122 -0
  62. package/components/dd-tag/dd-tag.vue +178 -0
  63. package/components/dd-toast/dd-toast.vue +195 -0
  64. package/components/dd-top-navbar/dd-top-navbar.vue +184 -0
  65. package/components/dd-upload/dd-upload.vue +187 -0
  66. package/index.ts +127 -0
  67. package/libs/utils.ts +106 -0
  68. package/package.json +58 -0
  69. package/scss/_functions.scss +68 -0
  70. package/scss/_mixins.scss +154 -0
  71. package/scss/_reset.scss +30 -0
  72. package/scss/_variables.scss +317 -0
  73. package/uni.scss +4 -0
@@ -0,0 +1,152 @@
1
+ <template>
2
+ <view
3
+ class="dd-loadmore"
4
+ :class="{ 'dd-loadmore--clickable': status === 'loadmore' }"
5
+ :style="containerStyle"
6
+ @click="onClick"
7
+ >
8
+ <slot>
9
+ <!-- loading 态 -->
10
+ <template v-if="status === 'loading'">
11
+ <slot name="loading">
12
+ <view
13
+ v-if="icon"
14
+ class="dd-loadmore__spinner"
15
+ :style="spinnerStyle"
16
+ ></view>
17
+ <text class="dd-loadmore__text" :style="textStyle">{{ loadingText }}</text>
18
+ </slot>
19
+ </template>
20
+
21
+ <!-- nomore 态 -->
22
+ <template v-else-if="status === 'nomore'">
23
+ <slot name="nomore">
24
+ <view v-if="isDot" class="dd-loadmore__dot" :style="dotStyle"></view>
25
+ <text v-else class="dd-loadmore__text" :style="textStyle">{{ nomoreText }}</text>
26
+ </slot>
27
+ </template>
28
+
29
+ <!-- loadmore 态 -->
30
+ <template v-else>
31
+ <text class="dd-loadmore__text" :style="textStyle">{{ loadmoreText }}</text>
32
+ </template>
33
+ </slot>
34
+ </view>
35
+ </template>
36
+
37
+ <script setup lang="ts">
38
+ import { computed } from 'vue'
39
+
40
+ type Status = 'loadmore' | 'loading' | 'nomore'
41
+
42
+ interface Props {
43
+ status?: Status
44
+ loadmoreText?: string
45
+ loadingText?: string
46
+ nomoreText?: string
47
+ icon?: boolean
48
+ iconSize?: number // rpx
49
+ fontSize?: number // rpx
50
+ color?: string
51
+ iconColor?: string
52
+ bgColor?: string
53
+ isDot?: boolean
54
+ marginTop?: number // rpx
55
+ marginBottom?: number // rpx
56
+ height?: number | string // rpx
57
+ }
58
+
59
+ const props = withDefaults(defineProps<Props>(), {
60
+ status: 'loadmore',
61
+ loadmoreText: '加载更多',
62
+ loadingText: '正在加载...',
63
+ nomoreText: '没有更多了',
64
+ icon: true,
65
+ iconSize: 32,
66
+ fontSize: 24,
67
+ color: '#9E9E9E', // $dd-text-tertiary
68
+ iconColor: '#F5A623',
69
+ bgColor: 'transparent',
70
+ isDot: false,
71
+ marginTop: 16,
72
+ marginBottom: 16,
73
+ height: 'auto',
74
+ })
75
+
76
+ const emit = defineEmits<{
77
+ (e: 'loadmore'): void
78
+ }>()
79
+
80
+ const containerStyle = computed(() => {
81
+ const h = typeof props.height === 'number' ? `${props.height}rpx` : props.height
82
+ return {
83
+ height: h,
84
+ marginTop: `${props.marginTop}rpx`,
85
+ marginBottom: `${props.marginBottom}rpx`,
86
+ backgroundColor: props.bgColor,
87
+ }
88
+ })
89
+
90
+ const textStyle = computed(() => ({
91
+ fontSize: `${props.fontSize}rpx`,
92
+ color: props.color,
93
+ }))
94
+
95
+ const spinnerStyle = computed(() => ({
96
+ width: `${props.iconSize}rpx`,
97
+ height: `${props.iconSize}rpx`,
98
+ borderColor: props.color,
99
+ borderTopColor: props.iconColor,
100
+ }))
101
+
102
+ const dotStyle = computed(() => ({
103
+ width: '8rpx',
104
+ height: '8rpx',
105
+ backgroundColor: props.color,
106
+ }))
107
+
108
+ function onClick() {
109
+ if (props.status === 'loadmore') emit('loadmore')
110
+ }
111
+ </script>
112
+
113
+ <style lang="scss" scoped>
114
+ @import '../../scss/variables';
115
+ @import '../../scss/mixins';
116
+
117
+ .dd-loadmore {
118
+ @include dd-flex-center;
119
+ width: 100%;
120
+ min-height: 80rpx;
121
+ gap: 12rpx;
122
+ box-sizing: border-box;
123
+
124
+ &--clickable {
125
+ cursor: pointer;
126
+
127
+ &:active {
128
+ opacity: 0.7;
129
+ }
130
+ }
131
+
132
+ &__spinner {
133
+ border: 4rpx solid $dd-border-default;
134
+ border-radius: 50%;
135
+ box-sizing: border-box;
136
+ animation: dd-lm-spin 0.8s linear infinite;
137
+ }
138
+
139
+ &__text {
140
+ line-height: 1.5;
141
+ }
142
+
143
+ &__dot {
144
+ border-radius: 50%;
145
+ opacity: 0.6;
146
+ }
147
+ }
148
+
149
+ @keyframes dd-lm-spin {
150
+ to { transform: rotate(360deg); }
151
+ }
152
+ </style>
@@ -0,0 +1,245 @@
1
+ <template>
2
+ <view
3
+ class="dd-mp-navbar"
4
+ :class="[`dd-mp-navbar--${variant}`, { 'dd-mp-navbar--transparent': transparent }]"
5
+ :style="navStyle"
6
+ >
7
+ <!-- 状态栏占位 -->
8
+ <view class="dd-mp-navbar__status" :style="{ height: statusBarPx + 'px' }"></view>
9
+ <!-- 内容行 -->
10
+ <view class="dd-mp-navbar__content" :style="{ height: contentHeightPx + 'px' }">
11
+ <!-- 左侧:返回 -->
12
+ <view class="dd-mp-navbar__left">
13
+ <view v-if="showBack && variant !== 'search'" class="dd-mp-navbar__back" @click="onBack">
14
+ <view class="dd-mp-navbar__arrow"></view>
15
+ <text v-if="backText" class="dd-mp-navbar__back-text">{{ backText }}</text>
16
+ </view>
17
+ </view>
18
+ <!-- 中间:标题 / 搜索 / 自定义 -->
19
+ <view class="dd-mp-navbar__center">
20
+ <template v-if="variant === 'search'">
21
+ <view class="dd-mp-navbar__search">
22
+ <dd-icon name="search" class="dd-mp-navbar__search-icon" />
23
+ <input
24
+ class="dd-mp-navbar__search-input"
25
+ :value="searchValue"
26
+ :placeholder="searchPlaceholder"
27
+ :placeholder-style="placeholderStyle"
28
+ confirm-type="search"
29
+ @input="onSearchInput"
30
+ @confirm="onSearchConfirm"
31
+ />
32
+ </view>
33
+ </template>
34
+ <template v-else-if="variant === 'custom'">
35
+ <slot></slot>
36
+ </template>
37
+ <template v-else>
38
+ <text v-if="title" class="dd-mp-navbar__title">{{ title }}</text>
39
+ </template>
40
+ </view>
41
+ <!-- 右侧:87px 胶囊占位 -->
42
+ <view class="dd-mp-navbar__right" :style="{ width: capsuleWidthPx + 'px' }"></view>
43
+ </view>
44
+ <!-- custom 变体:内容行下方扩展槽 -->
45
+ <view v-if="variant === 'custom' && $slots.below" class="dd-mp-navbar__below">
46
+ <slot name="below"></slot>
47
+ </view>
48
+ </view>
49
+ </template>
50
+
51
+ <script setup lang="ts">
52
+ import { computed, onMounted, ref } from 'vue'
53
+ import DdIcon from '../dd-icon/dd-icon.vue'
54
+
55
+ interface Props {
56
+ variant?: 'default' | 'search' | 'custom'
57
+ transparent?: boolean
58
+ showBack?: boolean
59
+ title?: string
60
+ backText?: string
61
+ searchValue?: string
62
+ searchPlaceholder?: string
63
+ }
64
+
65
+ const props = withDefaults(defineProps<Props>(), {
66
+ variant: 'default',
67
+ transparent: false,
68
+ showBack: true,
69
+ title: '',
70
+ backText: '',
71
+ searchValue: '',
72
+ searchPlaceholder: '搜索',
73
+ })
74
+
75
+ const emit = defineEmits<{
76
+ (e: 'back'): void
77
+ (e: 'update:searchValue', val: string): void
78
+ (e: 'search', val: string): void
79
+ }>()
80
+
81
+ const statusBarPx = ref(20)
82
+ const contentHeightPx = ref(44)
83
+ const capsuleWidthPx = ref(87)
84
+
85
+ onMounted(() => {
86
+ // 状态栏高度 (跨端)
87
+ try {
88
+ // #ifdef MP-WEIXIN || APP-PLUS
89
+ const info = uni.getWindowInfo()
90
+ statusBarPx.value = info.statusBarHeight || 20
91
+ // #endif
92
+ // #ifdef H5
93
+ statusBarPx.value = 20
94
+ // #endif
95
+ } catch {
96
+ statusBarPx.value = 20
97
+ }
98
+
99
+ // 微信小程序:用胶囊按钮真实 rect 对齐导航栏内容高度与右侧占位
100
+ // #ifdef MP-WEIXIN
101
+ try {
102
+ const rect = uni.getMenuButtonBoundingClientRect()
103
+ if (rect && rect.height) {
104
+ // 内容行高度 = (胶囊 top - 状态栏) * 2 + 胶囊高度,使胶囊垂直居中
105
+ contentHeightPx.value = (rect.top - statusBarPx.value) * 2 + rect.height
106
+ capsuleWidthPx.value = rect.width + (rect.right ? (info_windowWidth() - rect.right) : 10) // 右侧留白
107
+ }
108
+ } catch {
109
+ contentHeightPx.value = 44
110
+ capsuleWidthPx.value = 87
111
+ }
112
+ // #endif
113
+ })
114
+
115
+ // 仅 MP-WEIXIN 需要:取屏宽算右侧留白
116
+ function info_windowWidth(): number {
117
+ try {
118
+ return uni.getWindowInfo().windowWidth
119
+ } catch {
120
+ return 375
121
+ }
122
+ }
123
+
124
+ const navStyle = computed(() => ({}))
125
+
126
+ const placeholderStyle = 'color: #9E9E9E; font-size: 14px;'
127
+
128
+ function onBack() {
129
+ emit('back')
130
+ }
131
+ function onSearchInput(e: any) {
132
+ const val = e?.detail?.value ?? ''
133
+ emit('update:searchValue', val)
134
+ }
135
+ function onSearchConfirm(e: any) {
136
+ emit('search', e?.detail?.value ?? '')
137
+ }
138
+ </script>
139
+
140
+ <style lang="scss" scoped>
141
+ @import '../../scss/variables';
142
+ @import '../../scss/mixins';
143
+
144
+ .dd-mp-navbar {
145
+ width: 100%;
146
+ @include dd-glass;
147
+ border-bottom: 1px solid $dd-glass-border;
148
+ position: relative;
149
+ z-index: $dd-z-index-fixed;
150
+
151
+ &--transparent {
152
+ background: transparent !important;
153
+ border-bottom: 0;
154
+ /* #ifdef H5 */
155
+ backdrop-filter: none;
156
+ -webkit-backdrop-filter: none;
157
+ /* #endif */
158
+ }
159
+
160
+ &__status {
161
+ width: 100%;
162
+ }
163
+
164
+ &__content {
165
+ display: flex;
166
+ align-items: center;
167
+ padding: 0 12rpx;
168
+ width: 100%;
169
+ }
170
+
171
+ &__left {
172
+ flex-shrink: 0;
173
+ min-width: 64rpx;
174
+ }
175
+
176
+ &__back {
177
+ display: flex;
178
+ align-items: center;
179
+ gap: 6rpx;
180
+ padding: 8rpx 12rpx;
181
+ border-radius: $dd-radius-md;
182
+ }
183
+
184
+ // CSS 返回箭头 "<" (无 SVG,MP 兼容)
185
+ &__arrow {
186
+ width: 24rpx;
187
+ height: 24rpx;
188
+ border-left: 4rpx solid $dd-primary-400;
189
+ border-bottom: 4rpx solid $dd-primary-400;
190
+ transform: rotate(45deg);
191
+ }
192
+
193
+ &__back-text {
194
+ font-size: $dd-font-size-body;
195
+ font-weight: 500;
196
+ color: $dd-primary-400;
197
+ }
198
+
199
+ &__center {
200
+ flex: 1;
201
+ @include dd-flex-center;
202
+ overflow: hidden;
203
+ }
204
+
205
+ &__title {
206
+ font-size: $dd-font-size-h4;
207
+ font-weight: 600;
208
+ color: $dd-text-primary;
209
+ max-width: 60vw;
210
+ @include dd-ellipsis(1);
211
+ text-align: center;
212
+ }
213
+
214
+ &__search {
215
+ width: 100%;
216
+ height: 64rpx;
217
+ background: $dd-surface-container;
218
+ border-radius: $dd-radius-full;
219
+ display: flex;
220
+ align-items: center;
221
+ padding: 0 24rpx;
222
+ gap: 12rpx;
223
+ }
224
+
225
+ &__search-icon {
226
+ font-size: 28rpx;
227
+ line-height: 1;
228
+ }
229
+
230
+ &__search-input {
231
+ flex: 1;
232
+ font-size: $dd-font-size-body;
233
+ color: $dd-text-primary;
234
+ }
235
+
236
+ &__right {
237
+ flex-shrink: 0;
238
+ // 87px 胶囊占位,实际宽度由 MP-WEIXIN rect 覆盖
239
+ }
240
+
241
+ &__below {
242
+ width: 100%;
243
+ }
244
+ }
245
+ </style>
@@ -0,0 +1,286 @@
1
+ <script lang="ts">
2
+ // 模块级单例:同时只允许一个 modal 打开
3
+ let activeModalClose: (() => void) | null = null
4
+ </script>
5
+
6
+ <template>
7
+ <view class="dd-modal">
8
+ <view
9
+ v-if="visible"
10
+ class="dd-modal__mask"
11
+ :class="{ 'dd-modal__mask--show': showClass }"
12
+ @click="onMaskClick"
13
+ ></view>
14
+
15
+ <!-- dialog -->
16
+ <view
17
+ v-if="visible && type === 'dialog'"
18
+ class="dd-modal__dialog"
19
+ :class="{ 'dd-modal__dialog--show': showClass }"
20
+ :style="{ width: widthValue }"
21
+ >
22
+ <view v-if="$slots.header || title" class="dd-modal__header">
23
+ <slot name="header">
24
+ <text class="dd-modal__title">{{ title }}</text>
25
+ </slot>
26
+ <view v-if="showClose" class="dd-modal__close" @click="onClose">
27
+ <dd-icon name="cross" />
28
+ </view>
29
+ </view>
30
+ <view class="dd-modal__body">
31
+ <slot></slot>
32
+ </view>
33
+ <view v-if="$slots.footer" class="dd-modal__footer">
34
+ <slot name="footer"></slot>
35
+ </view>
36
+ </view>
37
+
38
+ <!-- bottom-sheet -->
39
+ <view
40
+ v-else-if="visible"
41
+ class="dd-modal__sheet"
42
+ :class="{ 'dd-modal__sheet--show': showClass }"
43
+ >
44
+ <view class="dd-modal__handle"></view>
45
+ <view v-if="$slots.header || title" class="dd-modal__header">
46
+ <slot name="header">
47
+ <text class="dd-modal__title">{{ title }}</text>
48
+ </slot>
49
+ </view>
50
+ <view class="dd-modal__body">
51
+ <slot></slot>
52
+ </view>
53
+ <view v-if="$slots.footer" class="dd-modal__footer">
54
+ <slot name="footer"></slot>
55
+ </view>
56
+ <view class="dd-modal__safe-bottom"></view>
57
+ </view>
58
+ </view>
59
+ </template>
60
+
61
+ <script setup lang="ts">
62
+ import { computed, ref, watch, onUnmounted, nextTick } from 'vue'
63
+ import DdIcon from '../dd-icon/dd-icon.vue'
64
+
65
+ interface Props {
66
+ modelValue?: boolean
67
+ type?: 'dialog' | 'bottom-sheet'
68
+ title?: string
69
+ width?: string | number
70
+ showClose?: boolean
71
+ closeOnClickModal?: boolean
72
+ }
73
+
74
+ const props = withDefaults(defineProps<Props>(), {
75
+ modelValue: false,
76
+ type: 'dialog',
77
+ title: '',
78
+ width: '600rpx',
79
+ showClose: true,
80
+ closeOnClickModal: true,
81
+ })
82
+
83
+ const emit = defineEmits<{
84
+ (e: 'update:modelValue', val: boolean): void
85
+ (e: 'close'): void
86
+ (e: 'open'): void
87
+ (e: 'opened'): void
88
+ (e: 'closed'): void
89
+ }>()
90
+
91
+ const widthValue = computed(() =>
92
+ typeof props.width === 'number' ? `${props.width}rpx` : props.width
93
+ )
94
+
95
+ const animDuration = computed(() =>
96
+ props.type === 'bottom-sheet' ? 300 : 250
97
+ )
98
+
99
+ // 两阶段控制:visible 控制挂载,showClass 控制动画
100
+ const visible = ref(false)
101
+ const showClass = ref(false)
102
+ let myClose: (() => void) | null = null
103
+ let timer: ReturnType<typeof setTimeout> | null = null
104
+
105
+ watch(
106
+ () => props.modelValue,
107
+ async (val) => {
108
+ if (timer) {
109
+ clearTimeout(timer)
110
+ timer = null
111
+ }
112
+
113
+ if (val) {
114
+ // 互斥:关闭其他打开的 modal
115
+ if (activeModalClose) activeModalClose()
116
+ myClose = () => emit('update:modelValue', false)
117
+ activeModalClose = myClose
118
+
119
+ // Phase 1: 挂载元素到 DOM(初始态:opacity:0 / scale(0.9))
120
+ visible.value = true
121
+ emit('open')
122
+
123
+ // Phase 2: 双 RAF 确保浏览器已 paint 初始帧,再加 show class
124
+ await nextTick()
125
+ await new Promise<void>(r => requestAnimationFrame(() => r()))
126
+ await new Promise<void>(r => requestAnimationFrame(() => r()))
127
+ showClass.value = true
128
+ timer = setTimeout(() => emit('opened'), animDuration.value)
129
+ } else {
130
+ if (!visible.value) return
131
+
132
+ // 关闭:先移除 show class 触发退出过渡,等动画结束再卸载
133
+ showClass.value = false
134
+ if (activeModalClose === myClose) {
135
+ activeModalClose = null
136
+ }
137
+ myClose = null
138
+ emit('close')
139
+ timer = setTimeout(() => {
140
+ visible.value = false
141
+ emit('closed')
142
+ }, animDuration.value)
143
+ }
144
+ },
145
+ { immediate: true }
146
+ )
147
+
148
+ function onMaskClick() {
149
+ if (!props.closeOnClickModal) return
150
+ onClose()
151
+ }
152
+
153
+ function onClose() {
154
+ emit('update:modelValue', false)
155
+ }
156
+
157
+ onUnmounted(() => {
158
+ if (timer) clearTimeout(timer)
159
+ if (activeModalClose === myClose) {
160
+ activeModalClose = null
161
+ }
162
+ })
163
+ </script>
164
+
165
+ <style lang="scss" scoped>
166
+ @import '../../scss/variables';
167
+ @import '../../scss/mixins';
168
+
169
+ .dd-modal {
170
+ &__mask {
171
+ position: fixed;
172
+ inset: 0;
173
+ background: $dd-glass-bg;
174
+ backdrop-filter: blur(8px);
175
+ -webkit-backdrop-filter: blur(8px);
176
+ opacity: 0;
177
+ z-index: 1000;
178
+ transition: opacity 0.25s ease;
179
+
180
+ &--show {
181
+ opacity: 1;
182
+ }
183
+ }
184
+
185
+ // dialog: 实心 elevated 背景 (非 glass)
186
+ &__dialog {
187
+ position: fixed;
188
+ top: 50%;
189
+ left: 50%;
190
+ transform: translate(-50%, -50%) scale(0.9);
191
+ opacity: 0;
192
+ z-index: 1001;
193
+ max-width: 720rpx; // 360px 上限
194
+ max-height: 80vh;
195
+ background: $dd-bg-elevated;
196
+ border: 1px solid $dd-border-default;
197
+ border-radius: $dd-radius-xl;
198
+ box-shadow: $dd-shadow-4;
199
+ padding: $dd-space-4 $dd-space-5;
200
+ display: flex;
201
+ flex-direction: column;
202
+ transition: transform 0.25s ease, opacity 0.25s ease;
203
+
204
+ &--show {
205
+ transform: translate(-50%, -50%) scale(1);
206
+ opacity: 1;
207
+ }
208
+ }
209
+
210
+ // bottom-sheet
211
+ &__sheet {
212
+ position: fixed;
213
+ left: 0;
214
+ right: 0;
215
+ bottom: 0;
216
+ transform: translateY(100%);
217
+ z-index: 1001;
218
+ max-height: 85vh;
219
+ background: $dd-bg-elevated;
220
+ border: 1px solid $dd-border-default;
221
+ border-radius: $dd-radius-xl $dd-radius-xl 0 0;
222
+ box-shadow: $dd-shadow-4;
223
+ display: flex;
224
+ flex-direction: column;
225
+ transition: transform 0.3s ease-out;
226
+
227
+ &--show {
228
+ transform: translateY(0);
229
+ }
230
+ }
231
+
232
+ &__handle {
233
+ width: 72rpx;
234
+ height: 8rpx;
235
+ border-radius: $dd-radius-full;
236
+ background: $dd-neutral-700;
237
+ margin: 16rpx auto 8rpx;
238
+ flex-shrink: 0;
239
+ }
240
+
241
+ &__header {
242
+ position: relative;
243
+ display: flex;
244
+ align-items: center;
245
+ justify-content: center;
246
+ padding: $dd-space-3 0;
247
+ flex-shrink: 0;
248
+ }
249
+
250
+ &__title {
251
+ font-size: $dd-font-size-h4;
252
+ font-weight: $dd-font-weight-h4;
253
+ color: $dd-text-primary;
254
+ }
255
+
256
+ &__close {
257
+ position: absolute;
258
+ right: 0;
259
+ top: 50%;
260
+ transform: translateY(-50%);
261
+ width: 64rpx;
262
+ height: 64rpx;
263
+ border-radius: $dd-radius-full;
264
+ background: $dd-gradient-primary;
265
+ color: $dd-neutral-900;
266
+ font-size: 36rpx;
267
+ @include dd-flex-center;
268
+ }
269
+
270
+ &__body {
271
+ flex: 1;
272
+ overflow-y: auto;
273
+ padding: $dd-space-2 0;
274
+ }
275
+
276
+ &__footer {
277
+ flex-shrink: 0;
278
+ padding-top: $dd-space-3;
279
+ }
280
+
281
+ &__safe-bottom {
282
+ @include dd-safe-area-bottom;
283
+ flex-shrink: 0;
284
+ }
285
+ }
286
+ </style>