@hlw-uni/mp-vue 1.0.35 → 1.1.1

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.
@@ -0,0 +1,99 @@
1
+ <template>
2
+ <view v-if="!closed" class="hlw-notice" :style="{ color, background }">
3
+ <view v-if="leftIcon" :class="leftIcon" class="hlw-notice-left-icon" />
4
+ <view v-else class="hlw-notice-left-icon i-fa6-solid-bullhorn" />
5
+ <view class="hlw-notice-wrap" @tap="$emit('click')">
6
+ <view v-if="scrollable" class="hlw-notice-scroll" :style="animStyle">
7
+ <text class="hlw-notice-text">{{ text }}</text>
8
+ </view>
9
+ <text v-else class="hlw-notice-text hlw-notice-text--ellipsis">{{ text }}</text>
10
+ </view>
11
+ <view v-if="closable" class="hlw-notice-close i-fa6-solid-xmark" @tap="onClose" />
12
+ </view>
13
+ </template>
14
+
15
+ <script setup lang="ts">
16
+ import { computed, ref } from "vue";
17
+
18
+ interface Props {
19
+ text?: string;
20
+ scrollable?: boolean;
21
+ closable?: boolean;
22
+ color?: string;
23
+ background?: string;
24
+ speed?: number;
25
+ leftIcon?: string;
26
+ }
27
+
28
+ const props = withDefaults(defineProps<Props>(), {
29
+ text: "",
30
+ scrollable: true,
31
+ closable: false,
32
+ color: "#ed6a0c",
33
+ background: "#fffbe8",
34
+ speed: 60,
35
+ leftIcon: "",
36
+ });
37
+
38
+ defineEmits<{ close: []; click: [] }>();
39
+
40
+ const closed = ref(false);
41
+ const duration = computed(() => Math.max(3, (props.text.length * 20) / props.speed));
42
+ const animStyle = computed(() => ({ animationDuration: `${duration.value}s` }));
43
+
44
+ function onClose() {
45
+ closed.value = true;
46
+ }
47
+ </script>
48
+
49
+ <style lang="scss" scoped>
50
+ .hlw-notice {
51
+ display: flex;
52
+ align-items: center;
53
+ padding: 16rpx 24rpx;
54
+ gap: 12rpx;
55
+ font-size: var(--font-sm, 24rpx);
56
+ }
57
+
58
+ .hlw-notice-left-icon {
59
+ flex-shrink: 0;
60
+ font-size: var(--font-base, 28rpx);
61
+ }
62
+
63
+ .hlw-notice-wrap {
64
+ flex: 1;
65
+ overflow: hidden;
66
+ white-space: nowrap;
67
+ }
68
+
69
+ .hlw-notice-scroll {
70
+ display: inline-block;
71
+ white-space: nowrap;
72
+ animation: hlw-notice-scroll linear infinite;
73
+ padding-left: 100%;
74
+ }
75
+
76
+ .hlw-notice-text {
77
+ display: inline;
78
+ }
79
+
80
+ .hlw-notice-text--ellipsis {
81
+ display: block;
82
+ overflow: hidden;
83
+ text-overflow: ellipsis;
84
+ white-space: nowrap;
85
+ }
86
+
87
+ .hlw-notice-close {
88
+ flex-shrink: 0;
89
+ font-size: 28rpx;
90
+ line-height: 1;
91
+ opacity: 0.6;
92
+ padding: 4rpx;
93
+ }
94
+
95
+ @keyframes hlw-notice-scroll {
96
+ 0% { transform: translateX(0); }
97
+ 100% { transform: translateX(-100%); }
98
+ }
99
+ </style>
@@ -26,6 +26,30 @@
26
26
  </template>
27
27
 
28
28
  <script setup lang="ts">
29
+ /**
30
+ * HlwPage — 页面布局容器
31
+ *
32
+ * 全屏 flex 布局:固定 header + 可滚动 content + 固定 footer。
33
+ * 传入 title/isBack 自动渲染 HlwHeader,也可通过 header 插槽完全自定义。
34
+ *
35
+ * @props
36
+ * title - 页面标题(自动渲染 HlwHeader)
37
+ * isBack - 是否显示返回按钮,默认 false
38
+ * bgClass - header 背景 CSS class
39
+ *
40
+ * @slots
41
+ * header - 自定义顶部(覆盖默认 HlwHeader)
42
+ * default - 主体可滚动内容
43
+ * footer - 固定底部
44
+ *
45
+ * @example
46
+ * ```vue
47
+ * <HlwPage title="首页" bg-class="header-bg">
48
+ * <view>页面内容</view>
49
+ * <template #footer><view>底部栏</view></template>
50
+ * </HlwPage>
51
+ * ```
52
+ */
29
53
  interface Props {
30
54
  title?: string;
31
55
  isBack?: boolean;
@@ -0,0 +1,108 @@
1
+ <template>
2
+ <view v-if="show" class="hlw-popup-mask" @tap.self="onClose" />
3
+ <view class="hlw-popup" :class="[`hlw-popup--${position}`, { 'hlw-popup--show': show, 'hlw-popup--round': round }]">
4
+ <view v-if="title || closable" class="hlw-popup-header">
5
+ <text class="hlw-popup-title">{{ title }}</text>
6
+ <view v-if="closable" class="hlw-popup-close i-fa6-solid-xmark" @tap="onClose" />
7
+ </view>
8
+ <slot />
9
+ </view>
10
+ </template>
11
+
12
+ <script setup lang="ts">
13
+ interface Props {
14
+ show?: boolean;
15
+ position?: "bottom" | "center" | "top";
16
+ round?: boolean;
17
+ closable?: boolean;
18
+ title?: string;
19
+ }
20
+
21
+ withDefaults(defineProps<Props>(), {
22
+ show: false,
23
+ position: "bottom",
24
+ round: true,
25
+ closable: true,
26
+ title: "",
27
+ });
28
+
29
+ const emit = defineEmits<{ "update:show": [value: boolean]; close: [] }>();
30
+
31
+ function onClose() {
32
+ emit("update:show", false);
33
+ emit("close");
34
+ }
35
+ </script>
36
+
37
+ <style lang="scss" scoped>
38
+ .hlw-popup-mask {
39
+ position: fixed;
40
+ inset: 0;
41
+ background: rgba(0, 0, 0, 0.5);
42
+ z-index: 1000;
43
+ animation: hlw-fade-in 0.25s;
44
+ }
45
+
46
+ .hlw-popup {
47
+ position: fixed;
48
+ z-index: 1001;
49
+ background: #fff;
50
+ transition: transform 0.3s ease, opacity 0.3s ease;
51
+
52
+ &--bottom {
53
+ left: 0;
54
+ right: 0;
55
+ bottom: 0;
56
+ max-height: 80vh;
57
+ transform: translateY(100%);
58
+ &.hlw-popup--show { transform: translateY(0); }
59
+ &.hlw-popup--round { border-radius: var(--radius-xl, 32rpx) var(--radius-xl, 32rpx) 0 0; }
60
+ }
61
+
62
+ &--top {
63
+ left: 0;
64
+ right: 0;
65
+ top: 0;
66
+ max-height: 80vh;
67
+ transform: translateY(-100%);
68
+ &.hlw-popup--show { transform: translateY(0); }
69
+ &.hlw-popup--round { border-radius: 0 0 var(--radius-xl, 32rpx) var(--radius-xl, 32rpx); }
70
+ }
71
+
72
+ &--center {
73
+ left: 50%;
74
+ top: 50%;
75
+ width: 80%;
76
+ transform: translate(-50%, -50%) scale(0.9);
77
+ opacity: 0;
78
+ border-radius: var(--radius-xl, 32rpx);
79
+ &.hlw-popup--show { transform: translate(-50%, -50%) scale(1); opacity: 1; }
80
+ }
81
+ }
82
+
83
+ .hlw-popup-header {
84
+ display: flex;
85
+ align-items: center;
86
+ justify-content: space-between;
87
+ padding: 28rpx 32rpx;
88
+ border-bottom: 1rpx solid var(--border-color-light, #f1f5f9);
89
+ }
90
+
91
+ .hlw-popup-title {
92
+ font-size: var(--font-md, 32rpx);
93
+ font-weight: 600;
94
+ color: #1e293b;
95
+ }
96
+
97
+ .hlw-popup-close {
98
+ font-size: 32rpx;
99
+ color: #94a3b8;
100
+ line-height: 1;
101
+ padding: 4rpx;
102
+ }
103
+
104
+ @keyframes hlw-fade-in {
105
+ from { opacity: 0; }
106
+ to { opacity: 1; }
107
+ }
108
+ </style>
@@ -0,0 +1,95 @@
1
+ <template>
2
+ <view class="hlw-search" :style="background ? { background } : {}">
3
+ <view class="hlw-search-box" :class="{ 'hlw-search-box--round': shape === 'round' }">
4
+ <view class="hlw-search-icon i-fa6-solid-magnifying-glass" />
5
+ <input
6
+ class="hlw-search-input"
7
+ type="text"
8
+ :value="modelValue"
9
+ :placeholder="placeholder"
10
+ :disabled="disabled"
11
+ confirm-type="search"
12
+ @input="onInput"
13
+ @confirm="$emit('search', modelValue)"
14
+ @focus="$emit('focus')"
15
+ @blur="$emit('blur')"
16
+ />
17
+ <view v-if="clearable && modelValue" class="hlw-search-clear i-fa6-solid-xmark" @tap="onClear" />
18
+ </view>
19
+ </view>
20
+ </template>
21
+
22
+ <script setup lang="ts">
23
+ interface Props {
24
+ modelValue?: string;
25
+ placeholder?: string;
26
+ disabled?: boolean;
27
+ clearable?: boolean;
28
+ shape?: "square" | "round";
29
+ background?: string;
30
+ }
31
+
32
+ const props = withDefaults(defineProps<Props>(), {
33
+ modelValue: "",
34
+ placeholder: "搜索",
35
+ disabled: false,
36
+ clearable: true,
37
+ shape: "round",
38
+ background: "",
39
+ });
40
+
41
+ const emit = defineEmits<{
42
+ "update:modelValue": [value: string];
43
+ search: [value: string];
44
+ clear: [];
45
+ focus: [];
46
+ blur: [];
47
+ }>();
48
+
49
+ function onInput(e: any) {
50
+ emit("update:modelValue", e?.detail?.value ?? "");
51
+ }
52
+
53
+ function onClear() {
54
+ emit("update:modelValue", "");
55
+ emit("clear");
56
+ }
57
+ </script>
58
+
59
+ <style lang="scss" scoped>
60
+ .hlw-search {
61
+ padding: 16rpx 24rpx;
62
+ }
63
+
64
+ .hlw-search-box {
65
+ display: flex;
66
+ align-items: center;
67
+ gap: 12rpx;
68
+ padding: 12rpx 24rpx;
69
+ background: #f1f5f9;
70
+ border-radius: var(--radius-md, 16rpx);
71
+
72
+ &--round { border-radius: 999rpx; }
73
+ }
74
+
75
+ .hlw-search-icon {
76
+ font-size: var(--font-sm, 24rpx);
77
+ flex-shrink: 0;
78
+ opacity: 0.5;
79
+ }
80
+
81
+ .hlw-search-input {
82
+ flex: 1;
83
+ font-size: var(--font-sm, 24rpx);
84
+ color: #1e293b;
85
+ min-height: 40rpx;
86
+ }
87
+
88
+ .hlw-search-clear {
89
+ font-size: 28rpx;
90
+ color: #94a3b8;
91
+ line-height: 1;
92
+ flex-shrink: 0;
93
+ padding: 4rpx;
94
+ }
95
+ </style>
@@ -0,0 +1,105 @@
1
+ <template>
2
+ <view v-if="loading" class="hlw-skeleton" :class="{ 'hlw-skeleton--animate': animate }">
3
+ <view v-if="avatar" class="hlw-skeleton-avatar" :class="`hlw-skeleton-avatar--${avatarSize}`" />
4
+ <view class="hlw-skeleton-content">
5
+ <view v-if="title" class="hlw-skeleton-title" />
6
+ <view v-for="i in rows" :key="i" class="hlw-skeleton-row" :style="{ width: i === rows ? '60%' : '100%' }" />
7
+ </view>
8
+ </view>
9
+ <slot v-else />
10
+ </template>
11
+
12
+ <script setup lang="ts">
13
+ /**
14
+ * HlwSkeleton — 骨架屏
15
+ *
16
+ * 数据加载占位,支持头像 + 标题 + 多行文字组合。loading 为 false 时显示默认插槽内容。
17
+ *
18
+ * @props
19
+ * loading - 是否显示骨架屏,默认 true
20
+ * rows - 文字行数,默认 3
21
+ * avatar - 是否显示头像占位,默认 false
22
+ * title - 是否显示标题占位,默认 true
23
+ * animate - 是否启用脉冲动画,默认 true
24
+ * avatarSize - 头像尺寸:small / medium / large,默认 medium
25
+ *
26
+ * @slots
27
+ * default - 加载完成后显示的真实内容
28
+ *
29
+ * @example
30
+ * ```vue
31
+ * <HlwSkeleton :loading="loading" avatar :rows="4">
32
+ * <view>真实数据内容</view>
33
+ * </HlwSkeleton>
34
+ * ```
35
+ */
36
+ interface Props {
37
+ loading?: boolean;
38
+ rows?: number;
39
+ avatar?: boolean;
40
+ title?: boolean;
41
+ animate?: boolean;
42
+ avatarSize?: "small" | "medium" | "large";
43
+ }
44
+
45
+ withDefaults(defineProps<Props>(), {
46
+ loading: true,
47
+ rows: 3,
48
+ avatar: false,
49
+ title: true,
50
+ animate: true,
51
+ avatarSize: "medium",
52
+ });
53
+ </script>
54
+
55
+ <style lang="scss" scoped>
56
+ .hlw-skeleton {
57
+ display: flex;
58
+ gap: 24rpx;
59
+ padding: 24rpx;
60
+ }
61
+
62
+ .hlw-skeleton-avatar {
63
+ flex-shrink: 0;
64
+ border-radius: 50%;
65
+ background: #e2e8f0;
66
+
67
+ &--small { width: 56rpx; height: 56rpx; }
68
+ &--medium { width: 80rpx; height: 80rpx; }
69
+ &--large { width: 120rpx; height: 120rpx; }
70
+ }
71
+
72
+ .hlw-skeleton-content {
73
+ flex: 1;
74
+ display: flex;
75
+ flex-direction: column;
76
+ gap: 20rpx;
77
+ }
78
+
79
+ .hlw-skeleton-title {
80
+ width: 40%;
81
+ height: 32rpx;
82
+ border-radius: 6rpx;
83
+ background: #e2e8f0;
84
+ }
85
+
86
+ .hlw-skeleton-row {
87
+ height: 24rpx;
88
+ border-radius: 6rpx;
89
+ background: #e2e8f0;
90
+ }
91
+
92
+ .hlw-skeleton--animate {
93
+ .hlw-skeleton-avatar,
94
+ .hlw-skeleton-title,
95
+ .hlw-skeleton-row {
96
+ animation: hlw-skeleton-pulse 1.5s ease-in-out infinite;
97
+ }
98
+ }
99
+
100
+ @keyframes hlw-skeleton-pulse {
101
+ 0% { opacity: 1; }
102
+ 50% { opacity: 0.4; }
103
+ 100% { opacity: 1; }
104
+ }
105
+ </style>
@@ -0,0 +1,126 @@
1
+ <template>
2
+ <scroll-view class="hlw-tabs" :scroll-x="scrollable" :enhanced="true" :show-scrollbar="false">
3
+ <view class="hlw-tabs-wrap">
4
+ <view
5
+ v-for="(item, index) in items"
6
+ :key="index"
7
+ class="hlw-tab"
8
+ :class="{ 'hlw-tab--active': modelValue === index }"
9
+ @tap="onChange(index)"
10
+ >
11
+ <text class="hlw-tab-text">{{ typeof item === "string" ? item : item.label }}</text>
12
+ <view
13
+ v-if="typeof item !== 'string' && item.badge"
14
+ class="hlw-tab-badge"
15
+ >{{ item.badge }}</view>
16
+ <view v-if="modelValue === index" class="hlw-tab-line" :style="{ width: lineWidth }" />
17
+ </view>
18
+ </view>
19
+ </scroll-view>
20
+ </template>
21
+
22
+ <script setup lang="ts">
23
+ /**
24
+ * HlwTabs — 选项卡
25
+ *
26
+ * 横向选项卡切换,支持文字 / 对象配置、徽标和滚动模式。
27
+ * 激活项跟随 --primary-color 主题色。
28
+ *
29
+ * @props
30
+ * modelValue - 当前选中索引,支持 v-model,默认 0
31
+ * items - 选项列表:string[] 或 { label, badge? }[]
32
+ * scrollable - 超出时是否可横向滚动,默认 false
33
+ * lineWidth - 底部指示线宽度,默认 40rpx
34
+ *
35
+ * @events
36
+ * update:modelValue - 切换时触发
37
+ * change - 切换时触发(携带新索引)
38
+ *
39
+ * @example
40
+ * ```vue
41
+ * <HlwTabs v-model="activeTab" :items="['推荐', '热门', '最新']" />
42
+ * <HlwTabs v-model="tab" :items="[{ label: '消息', badge: '3' }, { label: '关注' }]" />
43
+ * ```
44
+ */
45
+ export interface HlwTabItem {
46
+ label: string;
47
+ badge?: string;
48
+ }
49
+
50
+ interface Props {
51
+ modelValue?: number;
52
+ items?: (string | HlwTabItem)[];
53
+ scrollable?: boolean;
54
+ lineWidth?: string;
55
+ }
56
+
57
+ withDefaults(defineProps<Props>(), {
58
+ modelValue: 0,
59
+ items: () => [],
60
+ scrollable: false,
61
+ lineWidth: "40rpx",
62
+ });
63
+
64
+ const emit = defineEmits<{ "update:modelValue": [index: number]; change: [index: number] }>();
65
+
66
+ function onChange(index: number) {
67
+ emit("update:modelValue", index);
68
+ emit("change", index);
69
+ }
70
+ </script>
71
+
72
+ <style lang="scss" scoped>
73
+ .hlw-tabs {
74
+ background: #fff;
75
+ white-space: nowrap;
76
+ }
77
+
78
+ .hlw-tabs-wrap {
79
+ display: inline-flex;
80
+ min-width: 100%;
81
+ }
82
+
83
+ .hlw-tab {
84
+ position: relative;
85
+ flex: 1;
86
+ display: flex;
87
+ align-items: center;
88
+ justify-content: center;
89
+ padding: 24rpx 28rpx;
90
+ gap: 6rpx;
91
+ transition: color 0.2s;
92
+
93
+ &--active .hlw-tab-text {
94
+ color: var(--primary-color, #3b82f6);
95
+ font-weight: 600;
96
+ }
97
+ }
98
+
99
+ .hlw-tab-text {
100
+ font-size: var(--font-base, 28rpx);
101
+ color: #64748b;
102
+ }
103
+
104
+ .hlw-tab-badge {
105
+ padding: 0 8rpx;
106
+ min-width: 28rpx;
107
+ height: 28rpx;
108
+ line-height: 28rpx;
109
+ font-size: 18rpx;
110
+ text-align: center;
111
+ color: #fff;
112
+ background: #ef4444;
113
+ border-radius: 999rpx;
114
+ }
115
+
116
+ .hlw-tab-line {
117
+ position: absolute;
118
+ bottom: 4rpx;
119
+ left: 50%;
120
+ transform: translateX(-50%);
121
+ height: 6rpx;
122
+ border-radius: 6rpx;
123
+ background: var(--primary-color, #3b82f6);
124
+ transition: width 0.2s;
125
+ }
126
+ </style>
@@ -0,0 +1,76 @@
1
+ <template>
2
+ <view
3
+ class="hlw-tag"
4
+ :class="[`hlw-tag--${type}`, `hlw-tag--${size}`, { 'hlw-tag--plain': plain, 'hlw-tag--round': round }]"
5
+ :style="customStyle"
6
+ @tap="$emit('click')"
7
+ >
8
+ <slot />
9
+ <view v-if="closable" class="hlw-tag-close i-fa6-solid-xmark" @tap.stop="$emit('close')" />
10
+ </view>
11
+ </template>
12
+
13
+ <script setup lang="ts">
14
+ import { computed } from "vue";
15
+
16
+ interface Props {
17
+ type?: "primary" | "success" | "warning" | "danger" | "info";
18
+ plain?: boolean;
19
+ closable?: boolean;
20
+ size?: "small" | "medium";
21
+ round?: boolean;
22
+ color?: string;
23
+ }
24
+
25
+ const props = withDefaults(defineProps<Props>(), {
26
+ type: "primary",
27
+ plain: false,
28
+ closable: false,
29
+ size: "medium",
30
+ round: false,
31
+ color: "",
32
+ });
33
+
34
+ defineEmits<{ click: []; close: [] }>();
35
+
36
+ const customStyle = computed(() => {
37
+ if (!props.color) return {};
38
+ return props.plain
39
+ ? { color: props.color, borderColor: props.color, background: "transparent" }
40
+ : { background: props.color, color: "#fff", borderColor: props.color };
41
+ });
42
+ </script>
43
+
44
+ <style lang="scss" scoped>
45
+ $colors: (
46
+ primary: var(--primary-color, #3b82f6),
47
+ success: #10b981,
48
+ warning: #f59e0b,
49
+ danger: #ef4444,
50
+ info: #64748b,
51
+ );
52
+
53
+ .hlw-tag {
54
+ display: inline-flex;
55
+ align-items: center;
56
+ gap: 4rpx;
57
+ font-weight: 500;
58
+ border: 2rpx solid transparent;
59
+
60
+ &--medium { padding: 4rpx 16rpx; font-size: var(--font-xs, 20rpx); border-radius: var(--radius-sm, 8rpx); }
61
+ &--small { padding: 2rpx 10rpx; font-size: 18rpx; border-radius: 6rpx; }
62
+ &--round { border-radius: 999rpx; }
63
+
64
+ @each $name, $c in $colors {
65
+ &--#{$name} { background: #{$c}; color: #fff; border-color: #{$c}; }
66
+ &--#{$name}.hlw-tag--plain { background: transparent; color: #{$c}; }
67
+ }
68
+ }
69
+
70
+ .hlw-tag-close {
71
+ font-size: 1em;
72
+ line-height: 1;
73
+ margin-left: 2rpx;
74
+ opacity: 0.8;
75
+ }
76
+ </style>
package/src/index.ts CHANGED
@@ -4,13 +4,24 @@
4
4
 
5
5
  export { default as HlwAd } from "./components/hlw-ad/index.vue";
6
6
  export { default as HlwAvatar } from "./components/hlw-avatar/index.vue";
7
+ export { default as HlwButton } from "./components/hlw-button/index.vue";
7
8
  export { default as HlwCard } from "./components/hlw-card/index.vue";
9
+ export { default as HlwCell } from "./components/hlw-cell/index.vue";
10
+ export { default as HlwDivider } from "./components/hlw-divider/index.vue";
8
11
  export { default as HlwEmpty } from "./components/hlw-empty/index.vue";
9
12
  export { default as HlwHeader } from "./components/hlw-header/index.vue";
10
13
  export { default as HlwLoading } from "./components/hlw-loading/index.vue";
11
14
  export { default as HlwMenu } from "./components/hlw-menu/index.vue";
12
15
  export type { HlwMenuItem } from "./components/hlw-menu/types";
16
+ export { default as HlwModal } from "./components/hlw-modal/index.vue";
17
+ export { default as HlwNoticeBar } from "./components/hlw-notice-bar/index.vue";
13
18
  export { default as HlwPage } from "./components/hlw-page/index.vue";
19
+ export { default as HlwPopup } from "./components/hlw-popup/index.vue";
20
+ export { default as HlwSearch } from "./components/hlw-search/index.vue";
21
+ export { default as HlwSkeleton } from "./components/hlw-skeleton/index.vue";
22
+ export { default as HlwTabs } from "./components/hlw-tabs/index.vue";
23
+ export { default as HlwTag } from "./components/hlw-tag/index.vue";
24
+ export type { HlwTabItem } from "./components/hlw-tabs/index.vue";
14
25
  export type { FontScale, FontPreset, ThemeColor } from "./composables/theme";
15
26
  export { FONT_PRESETS, FONT_SCALE_KEY, DEFAULT_THEMES, THEME_COLOR_KEY, THEME_CHANGE_EVENT, getCurrentFontScale, getCurrentFontVars, getCurrentThemeColor, getCurrentThemeVars, buildThemeStyle, useThemePageStyle } from "./composables/theme";
16
27
  export { useThemeStore } from "./stores/theme";