km-card-layout-component-miniprogram 0.1.41 → 0.1.42

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.
@@ -5,13 +5,13 @@ const icon_map_1 = require("../../icon-map");
5
5
  const normalizeIconName = (name) => {
6
6
  if (!name)
7
7
  return "";
8
- return name.startsWith("icon-") ? name.slice(5) : name;
8
+ return name.startsWith("layout-icon-") ? name.slice(5) : name;
9
9
  };
10
10
  const buildIconClassName = (name) => {
11
11
  const normalized = normalizeIconName(name);
12
12
  if (!normalized)
13
13
  return "";
14
- return `icon-${normalized}`;
14
+ return `layout-icon-${normalized}`;
15
15
  };
16
16
  const buildIconCode = (name) => {
17
17
  const normalized = normalizeIconName(name);
@@ -1,3 +1,3 @@
1
1
  <view class="km-node km-node--icon canvas-item" style="{{wrapperStyle}}">
2
- <view class="km-node__icon icon {{iconClass}} canvas-item" style="{{contentStyle}}" data-icon="{{iconCode}}"></view>
2
+ <view class="km-node__icon layoutIconFont {{iconClass}} canvas-item" style="{{contentStyle}}" data-icon="{{iconCode}}"></view>
3
3
  </view>
@@ -1,9 +1,9 @@
1
1
  @font-face {
2
2
  font-family: "layoutIconFont"; /* Project id 5095635 */
3
- src: url('//at.alicdn.com/t/c/font_5095635_ztv7ro0zm59.woff2?t=1766563440291') format('woff2'),
4
- url('//at.alicdn.com/t/c/font_5095635_ztv7ro0zm59.woff?t=1766563440291') format('woff'),
5
- url('//at.alicdn.com/t/c/font_5095635_ztv7ro0zm59.ttf?t=1766563440291') format('truetype'),
6
- url('//at.alicdn.com/t/c/font_5095635_ztv7ro0zm59.svg?t=1766563440291#layoutIconFont') format('svg');
3
+ src: url('//at.alicdn.com/t/c/font_5095635_0u3b33ft9ub.woff2?t=1770624313420') format('woff2'),
4
+ url('//at.alicdn.com/t/c/font_5095635_0u3b33ft9ub.woff?t=1770624313420') format('woff'),
5
+ url('//at.alicdn.com/t/c/font_5095635_0u3b33ft9ub.ttf?t=1770624313420') format('truetype'),
6
+ url('//at.alicdn.com/t/c/font_5095635_0u3b33ft9ub.svg?t=1770624313420#layoutIconFont') format('svg');
7
7
  }
8
8
 
9
9
  .layoutIconFont {
@@ -14,6 +14,10 @@
14
14
  -moz-osx-font-smoothing: grayscale;
15
15
  }
16
16
 
17
+ .layout-icon-refresh:before {
18
+ content: "\e62b";
19
+ }
20
+
17
21
  .layout-icon-round:before {
18
22
  content: "\e608";
19
23
  }
@@ -13,4 +13,5 @@ exports.ICON_CODE_MAP = {
13
13
  address: "e628",
14
14
  "website-line": "e679",
15
15
  website: "e643",
16
+ refresh: "e62b",
16
17
  };
@@ -7,17 +7,18 @@ const EMPTY_COMPANY_DUTY = {
7
7
  company: "",
8
8
  duty: "",
9
9
  };
10
+ const SWITCH_ANIMATION_MS = 420;
11
+ const SWIPE_MIN_DISTANCE = 40;
12
+ const SWIPE_DIRECTION_RATIO = 1.2;
10
13
  /**
11
- * 规范化 moreCardInfo.company,保证长度始终为 2
14
+ * 规范�?moreCardInfo.company,保证长度始终为 2
12
15
  */
13
16
  function normalizeMoreCompany(data) {
14
17
  var _a, _b;
15
18
  const origin = (_b = (_a = data.user) === null || _a === void 0 ? void 0 : _a.moreCardInfo) === null || _b === void 0 ? void 0 : _b.company;
16
- // 不存在 / 不是数组 → 不处理
17
19
  if (!Array.isArray(origin)) {
18
20
  return data;
19
21
  }
20
- // 长度 === 0 → 不处理
21
22
  if (origin.length === 0) {
22
23
  return data;
23
24
  }
@@ -43,13 +44,22 @@ const pickCardId = (layout, idx) => {
43
44
  return `card-${idx}`;
44
45
  };
45
46
  const buildCards = (layouts) => {
46
- return layouts.map((layout, idx) => ({
47
- id: pickCardId(layouts[idx], idx),
48
- cardStyle: (0, index_1.styleObjectToString)((0, index_1.buildCardStyle)(layout, "rpx"), "rpx"),
49
- backgroundImage: layout.backgroundImage || "",
50
- backgroundStyle: (0, index_1.styleObjectToString)((0, index_1.buildBackgroundStyle)(layout, "rpx"), "rpx"),
51
- elements: layout.children || [],
52
- }));
47
+ return layouts.map((layout, idx) => {
48
+ const baseCardStyle = (0, index_1.buildCardStyle)(layout, "rpx");
49
+ const cardStyle = (0, index_1.styleObjectToString)(baseCardStyle, "rpx");
50
+ const sizeStyle = (0, index_1.styleObjectToString)({
51
+ width: baseCardStyle.width,
52
+ height: baseCardStyle.height,
53
+ }, "rpx");
54
+ return {
55
+ id: pickCardId(layouts[idx], idx),
56
+ cardStyle,
57
+ sizeStyle,
58
+ backgroundImage: layout.backgroundImage || "",
59
+ backgroundStyle: (0, index_1.styleObjectToString)((0, index_1.buildBackgroundStyle)(layout, "rpx"), "rpx"),
60
+ elements: layout.children || [],
61
+ };
62
+ });
53
63
  };
54
64
  const nextTick = () => new Promise((resolve) => {
55
65
  wx.nextTick(() => resolve());
@@ -76,6 +86,20 @@ Component({
76
86
  rootData: {},
77
87
  firstCard: [],
78
88
  shareImage: "",
89
+ activeCard: null,
90
+ canvasCard: null,
91
+ frontCard: null,
92
+ backCard: null,
93
+ activeIndex: 0,
94
+ flipEnabled: false,
95
+ showToggle: false,
96
+ isFlipped: false,
97
+ isSwitching: false,
98
+ switch: {
99
+ name: "refresh",
100
+ size: 14,
101
+ style: "fill",
102
+ },
79
103
  },
80
104
  observers: {
81
105
  layout() {
@@ -100,18 +124,112 @@ Component({
100
124
  return self._fontReady;
101
125
  },
102
126
  rebuild() {
127
+ const self = this;
128
+ if (self._switchTimer) {
129
+ clearTimeout(self._switchTimer);
130
+ self._switchTimer = null;
131
+ }
103
132
  const data = normalizeMoreCompany(this.data.data);
104
133
  const layoutInput = (0, helpers_1.hasCompanyDutyKey)(this.data.layout)
105
134
  ? (0, index_1.processCardLayout)(this.data.layout, data)
106
135
  : this.data.layout;
107
136
  const rootData = (0, index_1.handleSpecialFields)(data);
108
137
  if (!layoutInput.length) {
109
- this.setData({ cards: [], rootData });
138
+ this.setData({
139
+ cards: [],
140
+ rootData,
141
+ activeCard: null,
142
+ canvasCard: null,
143
+ frontCard: null,
144
+ backCard: null,
145
+ activeIndex: 0,
146
+ flipEnabled: false,
147
+ showToggle: false,
148
+ isFlipped: false,
149
+ isSwitching: false,
150
+ });
110
151
  return;
111
152
  }
112
153
  const normalizedLayouts = (0, index_1.normalizeLayout)(layoutInput);
113
154
  const cards = buildCards(normalizedLayouts);
114
- this.setData({ cards, rootData, firstCard: [cards[0]] });
155
+ const activeIndex = 0;
156
+ const flipEnabled = cards.length > 1;
157
+ const activeCard = cards[activeIndex] || null;
158
+ const frontCard = cards[0] || null;
159
+ const backCard = cards[1] || null;
160
+ this.setData({
161
+ cards,
162
+ rootData,
163
+ firstCard: [cards[0]],
164
+ activeIndex,
165
+ activeCard,
166
+ canvasCard: activeCard,
167
+ frontCard,
168
+ backCard,
169
+ flipEnabled,
170
+ showToggle: flipEnabled,
171
+ isFlipped: false,
172
+ isSwitching: false,
173
+ });
174
+ },
175
+ handleToggleCard() {
176
+ const cards = this.data.cards;
177
+ if (!cards || cards.length < 2)
178
+ return;
179
+ if (this.data.isSwitching)
180
+ return;
181
+ const self = this;
182
+ if (self._switchTimer) {
183
+ clearTimeout(self._switchTimer);
184
+ self._switchTimer = null;
185
+ }
186
+ const nextIndex = this.data.activeIndex === 0 ? 1 : 0;
187
+ this.setData({
188
+ activeIndex: nextIndex,
189
+ activeCard: cards[nextIndex],
190
+ canvasCard: cards[nextIndex],
191
+ isFlipped: nextIndex === 1,
192
+ isSwitching: true,
193
+ });
194
+ self._switchTimer = setTimeout(() => {
195
+ self._switchTimer = null;
196
+ this.setData({ isSwitching: false });
197
+ }, SWITCH_ANIMATION_MS);
198
+ },
199
+ onToggleTap() {
200
+ this.handleToggleCard();
201
+ },
202
+ onTouchStart(event) {
203
+ const touch = event.touches && event.touches[0];
204
+ if (!touch)
205
+ return;
206
+ const self = this;
207
+ self._touchStartX = touch.clientX;
208
+ self._touchStartY = touch.clientY;
209
+ },
210
+ onTouchEnd(event) {
211
+ const self = this;
212
+ const touch = event.changedTouches && event.changedTouches[0];
213
+ if (!touch)
214
+ return;
215
+ const startX = self._touchStartX;
216
+ const startY = self._touchStartY;
217
+ self._touchStartX = null;
218
+ self._touchStartY = null;
219
+ if (typeof startX !== "number" || typeof startY !== "number")
220
+ return;
221
+ const dx = touch.clientX - startX;
222
+ const dy = touch.clientY - startY;
223
+ const absDx = Math.abs(dx);
224
+ const absDy = Math.abs(dy);
225
+ if (absDx > SWIPE_MIN_DISTANCE && absDx > absDy * SWIPE_DIRECTION_RATIO) {
226
+ this.handleToggleCard();
227
+ }
228
+ },
229
+ onTouchCancel() {
230
+ const self = this;
231
+ self._touchStartX = null;
232
+ self._touchStartY = null;
115
233
  },
116
234
  async handleDrawCanvas(options) {
117
235
  const self = this;
@@ -138,16 +256,22 @@ Component({
138
256
  self._isDrawing = false;
139
257
  }
140
258
  }
141
- },
142
- async handleDrawShareCanvas() {
259
+ }, async handleDrawShareCanvas() {
143
260
  var _a, _b, _c;
144
261
  const self = this;
145
262
  if (self._isDrawing)
146
263
  return;
147
264
  self._isDrawing = true;
265
+ let previousCanvasCard;
148
266
  try {
149
267
  // wait for setData / component render
150
268
  await nextTick();
269
+ const frontCard = this.data.frontCard || this.data.activeCard;
270
+ previousCanvasCard = this.data.canvasCard;
271
+ if (frontCard && frontCard !== previousCanvasCard) {
272
+ await new Promise((resolve) => this.setData({ canvasCard: frontCard }, resolve));
273
+ await nextTick();
274
+ }
151
275
  const layoutPath = await this.handleDrawCanvas({ waitForReady: false, skipDrawingGuard: true });
152
276
  if (!layoutPath)
153
277
  return;
@@ -165,6 +289,9 @@ Component({
165
289
  return filePath;
166
290
  }
167
291
  finally {
292
+ if (previousCanvasCard && previousCanvasCard !== this.data.canvasCard) {
293
+ this.setData({ canvasCard: previousCanvasCard });
294
+ }
168
295
  self._isDrawing = false;
169
296
  }
170
297
  },
@@ -173,8 +300,7 @@ Component({
173
300
  global: true,
174
301
  family: "layoutIconFont",
175
302
  scopes: ["native"],
176
- // 可以为 https 链接或者 Data URL
177
- source: "https://at.alicdn.com/t/c/font_5095635_ztv7ro0zm59.ttf?t=1766563440291",
303
+ source: "https://at.alicdn.com/t/c/font_5095635_0u3b33ft9ub.ttf?t=1770624313420",
178
304
  });
179
305
  },
180
306
  },
@@ -1,39 +1,63 @@
1
- <template name="layout-template">
2
- <block wx:for="{{renderCards}}" wx:key="id">
3
- <view class="km-card-layout layout-container">
4
- <view class="km-card-layout__item canvas-item">
5
- <view class="km-card canvas-item" style="{{item.cardStyle}}">
6
- <image wx:if="{{item.backgroundImage}}" class="km-card__bg canvas-item" style="{{item.backgroundStyle}}" src="{{item.backgroundImage}}" mode="aspectFill" />
7
- <block wx:for="{{item.elements}}" wx:key="id">
8
- <template wx:if="{{item.visible !== false}}" is="render-element" data="{{el:item, rootData: rootData}}" />
9
- </block>
10
- </view>
1
+ <view class="km-card-layout layout-view-container" bindtouchstart="onTouchStart" bindtouchend="onTouchEnd" bindtouchcancel="onTouchCancel">
2
+ <view class="km-card-layout__item canvas-item">
3
+ <view wx:if="{{flipEnabled}}" class="km-card-layout__flip {{isFlipped ? 'is-flipped' : ''}} {{isSwitching ? 'is-switching' : ''}}" style="{{frontCard.sizeStyle}}">
4
+ <view class="km-card-layout__face km-card-layout__face--front">
5
+ <template is="card-content" data="{{ idPrefix: 'view', card: frontCard, rootData: rootData }}" />
6
+ </view>
7
+ <view class="km-card-layout__face km-card-layout__face--back">
8
+ <template is="card-content" data="{{ idPrefix: 'view', card: backCard, rootData: rootData }}" />
11
9
  </view>
12
10
  </view>
13
- </block>
14
- </template>
15
- <template is="layout-template" data="{{ renderCards: cards, rootData: rootData }}"></template>
16
- <!-- 暂时只绘制第一张卡片 -->
17
- <wxml2canvas id="layout-canvas" container-class="layout-container" item-class="canvas-item"></wxml2canvas>
11
+ <view wx:else class="km-card-layout__single">
12
+ <template is="card-content" data="{{ idPrefix: 'view', card: activeCard, rootData: rootData }}" />
13
+ </view>
14
+ <view wx:if="{{showToggle}}" class="km-card-layout__toggle" catchtap="onToggleTap">
15
+ <icon-element element="{{switch}}" > </icon-element>
16
+ <text class="km-card-layout__toggle-text">{{activeIndex === 0?'反面':'正面'}}</text>
17
+ </view>
18
+ </view>
19
+ </view>
20
+
21
+ <view class="km-card-layout layout-canvas-container" style="{{canvasCard.sizeStyle}}">
22
+ <view class="km-card-layout__item canvas-item">
23
+ <view class="km-card-layout__single">
24
+ <template is="card-content" data="{{ idPrefix: 'canvas', card: canvasCard, rootData: rootData }}" />
25
+ </view>
26
+ </view>
27
+ </view>
28
+
29
+ <wxml2canvas id="layout-canvas" container-class="layout-canvas-container" item-class="canvas-item"></wxml2canvas>
18
30
  <share-canvas id="share-canvas"></share-canvas>
31
+
32
+ <template name="card-content">
33
+ <view wx:if="{{card}}" class="km-card canvas-item" style="{{card.cardStyle}}">
34
+ <image wx:if="{{card.backgroundImage}}" class="km-card__bg canvas-item" style="{{card.backgroundStyle}}" src="{{card.backgroundImage}}" mode="aspectFill" />
35
+ <block wx:for="{{card.elements}}" wx:key="id">
36
+ <template wx:if="{{item.visible !== false}}" is="render-element" data="{{el:item, rootData: rootData, idPrefix: idPrefix}}" />
37
+ </block>
38
+ </view>
39
+ </template>
40
+
19
41
  <template name="render-element">
20
42
  <block wx:if="{{el.type === 'image'}}">
21
- <image-element id="node-{{el.id}}" class="canvas-item" data-component="{{true}}" element="{{el}}" rootData="{{rootData}}" />
43
+ <image-element id="{{idPrefix}}-node-{{el.id}}" class="canvas-item" data-component="{{true}}" element="{{el}}" rootData="{{rootData}}" />
22
44
  </block>
23
45
  <block wx:elif="{{el.type === 'icon'}}">
24
- <icon-element id="node-{{el.id}}" class="canvas-item" data-component="{{true}}" element="{{el}}" rootData="{{rootData}}" />
46
+ <icon-element id="{{idPrefix}}-node-{{el.id}}" class="canvas-item" data-component="{{true}}" element="{{el}}" rootData="{{rootData}}" />
25
47
  </block>
26
48
  <block wx:elif="{{el.type === 'layout-panel'}}">
27
- <layout-panel-element id="node-{{el.id}}" class="canvas-item" data-component="{{true}}" element="{{el}}" rootData="{{rootData}}">
49
+ <layout-panel-element id="{{idPrefix}}-node-{{el.id}}" class="canvas-item" data-component="{{true}}" element="{{el}}" rootData="{{rootData}}">
28
50
  <block wx:for="{{el.children}}" wx:key="id">
29
- <template is="render-element" data="{{el:item, rootData: rootData}}" />
51
+ <template is="render-element" data="{{el:item, rootData: rootData, idPrefix: idPrefix}}" />
30
52
  </block>
31
53
  </layout-panel-element>
32
54
  </block>
33
55
  <block wx:elif="{{el.type === 'custom'}}">
34
- <custom-element id="node-{{el.id}}" class="canvas-item" data-component="{{true}}" element="{{el}}" rootData="{{rootData}}" />
56
+ <custom-element id="{{idPrefix}}-node-{{el.id}}" class="canvas-item" data-component="{{true}}" element="{{el}}" rootData="{{rootData}}" />
35
57
  </block>
36
58
  <block wx:else>
37
- <text-element id="node-{{el.id}}" class="canvas-item" data-component="{{true}}" element="{{el}}" rootData="{{rootData}}" />
59
+ <text-element id="{{idPrefix}}-node-{{el.id}}" class="canvas-item" data-component="{{true}}" element="{{el}}" rootData="{{rootData}}" />
38
60
  </block>
39
- </template>
61
+ </template>
62
+
63
+
@@ -2,10 +2,66 @@
2
2
  display: flex;
3
3
  flex-direction: column;
4
4
  position: relative;
5
+ perspective: 1200rpx;
5
6
  }
6
7
 
7
8
  .km-card-layout__item {
8
9
  width: 100%;
10
+ position: relative;
11
+ }
12
+
13
+ .layout-canvas-container {
14
+ position: fixed;
15
+ left: -10000rpx;
16
+ top: 0;
17
+ opacity: 0;
18
+ pointer-events: none;
19
+ z-index: -1;
20
+ }
21
+
22
+ .km-card-layout__flip {
23
+ position: relative;
24
+ -webkit-transform-style: preserve-3d;
25
+ transform-style: preserve-3d;
26
+ transition: transform 420ms ease;
27
+ transform-origin: center;
28
+ will-change: transform;
29
+ }
30
+
31
+ .km-card-layout__flip.is-flipped {
32
+ transform: rotateY(180deg);
33
+ }
34
+
35
+ .km-card-layout__flip:not(.is-switching) .km-card-layout__face--back {
36
+ visibility: hidden;
37
+ }
38
+
39
+ .km-card-layout__flip.is-flipped:not(.is-switching) .km-card-layout__face--front {
40
+ visibility: hidden;
41
+ }
42
+
43
+ .km-card-layout__flip.is-flipped:not(.is-switching) .km-card-layout__face--back {
44
+ visibility: visible;
45
+ }
46
+
47
+ .km-card-layout__face {
48
+ position: absolute;
49
+ left: 0;
50
+ top: 0;
51
+ width: 100%;
52
+ height: 100%;
53
+ -webkit-backface-visibility: hidden;
54
+ backface-visibility: hidden;
55
+ -webkit-transform-style: preserve-3d;
56
+ transform-style: preserve-3d;
57
+ }
58
+
59
+ .km-card-layout__face--back {
60
+ transform: rotateY(180deg);
61
+ }
62
+
63
+ .km-card-layout__single {
64
+ position: relative;
9
65
  }
10
66
 
11
67
  .km-card {
@@ -15,6 +71,8 @@
15
71
  color: inherit;
16
72
  font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
17
73
  background: transparent;
74
+ -webkit-backface-visibility: hidden;
75
+ backface-visibility: hidden;
18
76
  }
19
77
 
20
78
  .km-card__bg {
@@ -25,3 +83,27 @@
25
83
  height: 100%;
26
84
  object-fit: cover;
27
85
  }
86
+
87
+ .km-card-layout__toggle {
88
+ position: absolute;
89
+ top: 48rpx;
90
+ right: 20rpx;
91
+ z-index: 20;
92
+ padding: 8rpx 16rpx;
93
+ display: flex;
94
+ align-items: center;
95
+ gap: 4rpx;
96
+ border-radius: 30px;
97
+ border: 0.5px solid #FFF;
98
+ color: #fff;
99
+ background: rgba(85, 85, 85, 0.30);
100
+ backdrop-filter: blur(4px);
101
+ -webkit-backdrop-filter: blur(4rpx);
102
+
103
+ }
104
+
105
+ .km-card-layout__toggle-text {
106
+ color: #ffffff;
107
+ font-size: 24rpx;
108
+ line-height: 1;
109
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "km-card-layout-component-miniprogram",
3
- "version": "0.1.41",
3
+ "version": "0.1.42",
4
4
  "description": "",
5
5
  "main": "miniprogram_dist/index.js",
6
6
  "miniprogram": "miniprogram_dist",
@@ -9,13 +9,13 @@ import { ICON_CODE_MAP } from "../../icon-map";
9
9
 
10
10
  const normalizeIconName = (name?: string) => {
11
11
  if (!name) return "";
12
- return name.startsWith("icon-") ? name.slice(5) : name;
12
+ return name.startsWith("layout-icon-") ? name.slice(5) : name;
13
13
  };
14
14
 
15
15
  const buildIconClassName = (name?: string) => {
16
16
  const normalized = normalizeIconName(name);
17
17
  if (!normalized) return "";
18
- return `icon-${normalized}`;
18
+ return `layout-icon-${normalized}`;
19
19
  };
20
20
 
21
21
  const buildIconCode = (name?: string) => {
@@ -1,3 +1,3 @@
1
1
  <view class="km-node km-node--icon canvas-item" style="{{wrapperStyle}}">
2
- <view class="km-node__icon icon {{iconClass}} canvas-item" style="{{contentStyle}}" data-icon="{{iconCode}}"></view>
2
+ <view class="km-node__icon layoutIconFont {{iconClass}} canvas-item" style="{{contentStyle}}" data-icon="{{iconCode}}"></view>
3
3
  </view>
@@ -1,9 +1,9 @@
1
1
  @font-face {
2
2
  font-family: "layoutIconFont"; /* Project id 5095635 */
3
- src: url('//at.alicdn.com/t/c/font_5095635_ztv7ro0zm59.woff2?t=1766563440291') format('woff2'),
4
- url('//at.alicdn.com/t/c/font_5095635_ztv7ro0zm59.woff?t=1766563440291') format('woff'),
5
- url('//at.alicdn.com/t/c/font_5095635_ztv7ro0zm59.ttf?t=1766563440291') format('truetype'),
6
- url('//at.alicdn.com/t/c/font_5095635_ztv7ro0zm59.svg?t=1766563440291#layoutIconFont') format('svg');
3
+ src: url('//at.alicdn.com/t/c/font_5095635_0u3b33ft9ub.woff2?t=1770624313420') format('woff2'),
4
+ url('//at.alicdn.com/t/c/font_5095635_0u3b33ft9ub.woff?t=1770624313420') format('woff'),
5
+ url('//at.alicdn.com/t/c/font_5095635_0u3b33ft9ub.ttf?t=1770624313420') format('truetype'),
6
+ url('//at.alicdn.com/t/c/font_5095635_0u3b33ft9ub.svg?t=1770624313420#layoutIconFont') format('svg');
7
7
  }
8
8
 
9
9
  .layoutIconFont {
@@ -14,6 +14,10 @@
14
14
  -moz-osx-font-smoothing: grayscale;
15
15
  }
16
16
 
17
+ .layout-icon-refresh:before {
18
+ content: "\e62b";
19
+ }
20
+
17
21
  .layout-icon-round:before {
18
22
  content: "\e608";
19
23
  }
@@ -10,4 +10,5 @@ export const ICON_CODE_MAP: Record<string, string> = {
10
10
  address: "e628",
11
11
  "website-line": "e679",
12
12
  website: "e643",
13
+ refresh: "e62b",
13
14
  };
@@ -19,18 +19,20 @@ const EMPTY_COMPANY_DUTY: CompanyDuty = {
19
19
  duty: "",
20
20
  };
21
21
 
22
+ const SWITCH_ANIMATION_MS = 420;
23
+ const SWIPE_MIN_DISTANCE = 40;
24
+ const SWIPE_DIRECTION_RATIO = 1.2;
25
+
22
26
  /**
23
- * 规范化 moreCardInfo.company,保证长度始终为 2
27
+ * 规范�?moreCardInfo.company,保证长度始终为 2
24
28
  */
25
29
  export function normalizeMoreCompany(data: AnyObject): AnyObject {
26
30
  const origin = data.user?.moreCardInfo?.company;
27
31
 
28
- // 不存在 / 不是数组 → 不处理
29
32
  if (!Array.isArray(origin)) {
30
33
  return data;
31
34
  }
32
35
 
33
- // 长度 === 0 → 不处理
34
36
  if (origin.length === 0) {
35
37
  return data;
36
38
  }
@@ -58,6 +60,7 @@ type RenderElement = CardElement;
58
60
  type RenderCard = {
59
61
  id: string;
60
62
  cardStyle: string;
63
+ sizeStyle: string;
61
64
  backgroundImage?: string;
62
65
  backgroundStyle: string;
63
66
  elements: RenderElement[];
@@ -69,13 +72,25 @@ const pickCardId = (layout: any, idx: number) => {
69
72
  };
70
73
 
71
74
  const buildCards = (layouts: CardLayoutSchema[]) => {
72
- return layouts.map((layout, idx) => ({
73
- id: pickCardId(layouts[idx], idx),
74
- cardStyle: styleObjectToString(buildCardStyle(layout, "rpx"), "rpx"),
75
- backgroundImage: layout.backgroundImage || "",
76
- backgroundStyle: styleObjectToString(buildBackgroundStyle(layout, "rpx"), "rpx"),
77
- elements: layout.children || [],
78
- }));
75
+ return layouts.map((layout, idx) => {
76
+ const baseCardStyle = buildCardStyle(layout, "rpx");
77
+ const cardStyle = styleObjectToString(baseCardStyle, "rpx");
78
+ const sizeStyle = styleObjectToString(
79
+ {
80
+ width: baseCardStyle.width,
81
+ height: baseCardStyle.height,
82
+ },
83
+ "rpx",
84
+ );
85
+ return {
86
+ id: pickCardId(layouts[idx], idx),
87
+ cardStyle,
88
+ sizeStyle,
89
+ backgroundImage: layout.backgroundImage || "",
90
+ backgroundStyle: styleObjectToString(buildBackgroundStyle(layout, "rpx"), "rpx"),
91
+ elements: layout.children || [],
92
+ };
93
+ });
79
94
  };
80
95
 
81
96
  const nextTick = () =>
@@ -105,6 +120,20 @@ Component({
105
120
  rootData: {} as Record<string, any>,
106
121
  firstCard: [] as RenderCard[],
107
122
  shareImage: "",
123
+ activeCard: null as RenderCard | null,
124
+ canvasCard: null as RenderCard | null,
125
+ frontCard: null as RenderCard | null,
126
+ backCard: null as RenderCard | null,
127
+ activeIndex: 0,
128
+ flipEnabled: false,
129
+ showToggle: false,
130
+ isFlipped: false,
131
+ isSwitching: false,
132
+ switch: {
133
+ name: "refresh",
134
+ size: 14,
135
+ style: "fill",
136
+ },
108
137
  },
109
138
  observers: {
110
139
  layout() {
@@ -129,18 +158,119 @@ Component({
129
158
  return self._fontReady;
130
159
  },
131
160
  rebuild() {
161
+ const self = this as any;
162
+ if (self._switchTimer) {
163
+ clearTimeout(self._switchTimer);
164
+ self._switchTimer = null;
165
+ }
132
166
  const data = normalizeMoreCompany(this.data.data);
133
167
  const layoutInput = hasCompanyDutyKey(this.data.layout)
134
168
  ? processCardLayout(this.data.layout, data as any)
135
169
  : this.data.layout;
136
170
  const rootData = handleSpecialFields(data as any);
137
171
  if (!layoutInput.length) {
138
- this.setData({ cards: [], rootData });
172
+ this.setData({
173
+ cards: [],
174
+ rootData,
175
+ activeCard: null,
176
+ canvasCard: null,
177
+ frontCard: null,
178
+ backCard: null,
179
+ activeIndex: 0,
180
+ flipEnabled: false,
181
+ showToggle: false,
182
+ isFlipped: false,
183
+ isSwitching: false,
184
+ });
139
185
  return;
140
186
  }
141
187
  const normalizedLayouts = normalizeLayout(layoutInput);
142
188
  const cards = buildCards(normalizedLayouts);
143
- this.setData({ cards, rootData, firstCard: [cards[0]] });
189
+ const activeIndex = 0;
190
+ const flipEnabled = cards.length > 1;
191
+ const activeCard = cards[activeIndex] || null;
192
+ const frontCard = cards[0] || null;
193
+ const backCard = cards[1] || null;
194
+ this.setData({
195
+ cards,
196
+ rootData,
197
+ firstCard: [cards[0]],
198
+ activeIndex,
199
+ activeCard,
200
+ canvasCard: activeCard,
201
+ frontCard,
202
+ backCard,
203
+ flipEnabled,
204
+ showToggle: flipEnabled,
205
+ isFlipped: false,
206
+ isSwitching: false,
207
+ });
208
+ },
209
+
210
+ handleToggleCard() {
211
+ const cards = this.data.cards;
212
+ if (!cards || cards.length < 2) return;
213
+ if (this.data.isSwitching) return;
214
+
215
+ const self = this as any;
216
+ if (self._switchTimer) {
217
+ clearTimeout(self._switchTimer);
218
+ self._switchTimer = null;
219
+ }
220
+
221
+ const nextIndex = this.data.activeIndex === 0 ? 1 : 0;
222
+ this.setData({
223
+ activeIndex: nextIndex,
224
+ activeCard: cards[nextIndex],
225
+ canvasCard: cards[nextIndex],
226
+ isFlipped: nextIndex === 1,
227
+ isSwitching: true,
228
+ });
229
+
230
+ self._switchTimer = setTimeout(() => {
231
+ self._switchTimer = null;
232
+ this.setData({ isSwitching: false });
233
+ }, SWITCH_ANIMATION_MS);
234
+ },
235
+
236
+ onToggleTap() {
237
+ this.handleToggleCard();
238
+ },
239
+
240
+ onTouchStart(event: WechatMiniprogram.TouchEvent) {
241
+ const touch = event.touches && event.touches[0];
242
+ if (!touch) return;
243
+ const self = this as any;
244
+ self._touchStartX = touch.clientX;
245
+ self._touchStartY = touch.clientY;
246
+ },
247
+
248
+ onTouchEnd(event: WechatMiniprogram.TouchEvent) {
249
+ const self = this as any;
250
+ const touch = event.changedTouches && event.changedTouches[0];
251
+ if (!touch) return;
252
+
253
+ const startX = self._touchStartX;
254
+ const startY = self._touchStartY;
255
+ self._touchStartX = null;
256
+ self._touchStartY = null;
257
+
258
+ if (typeof startX !== "number" || typeof startY !== "number") return;
259
+
260
+ const dx = touch.clientX - startX;
261
+ const dy = touch.clientY - startY;
262
+ const absDx = Math.abs(dx);
263
+ const absDy = Math.abs(dy);
264
+
265
+ if (absDx > SWIPE_MIN_DISTANCE && absDx > absDy * SWIPE_DIRECTION_RATIO) {
266
+ this.handleToggleCard();
267
+ }
268
+ },
269
+
270
+ onTouchCancel() {
271
+ const self = this as any;
272
+ self._touchStartX = null;
273
+ self._touchStartY = null;
144
274
  },
145
275
 
146
276
  async handleDrawCanvas(options?: { waitForReady?: boolean; skipDrawingGuard?: boolean }) {
@@ -166,16 +296,22 @@ Component({
166
296
  self._isDrawing = false;
167
297
  }
168
298
  }
169
- },
170
-
171
- async handleDrawShareCanvas() {
299
+ }, async handleDrawShareCanvas() {
172
300
  const self = this as any;
173
301
  if (self._isDrawing) return;
174
302
  self._isDrawing = true;
303
+ let previousCanvasCard: RenderCard | null | undefined;
175
304
  try {
176
305
  // wait for setData / component render
177
306
  await nextTick();
178
307
 
308
+ const frontCard = this.data.frontCard || this.data.activeCard;
309
+ previousCanvasCard = this.data.canvasCard;
310
+ if (frontCard && frontCard !== previousCanvasCard) {
311
+ await new Promise<void>((resolve) => this.setData({ canvasCard: frontCard }, resolve));
312
+ await nextTick();
313
+ }
314
+
179
315
  const layoutPath = await this.handleDrawCanvas({ waitForReady: false, skipDrawingGuard: true });
180
316
  if (!layoutPath) return;
181
317
 
@@ -193,18 +329,24 @@ Component({
193
329
  }
194
330
  return filePath;
195
331
  } finally {
332
+ if (previousCanvasCard && previousCanvasCard !== this.data.canvasCard) {
333
+ this.setData({ canvasCard: previousCanvasCard });
334
+ }
196
335
  self._isDrawing = false;
197
336
  }
198
337
  },
199
-
200
338
  async loadFont() {
201
339
  await wx.loadFontFace({
202
340
  global: true,
203
341
  family: "layoutIconFont",
204
342
  scopes: ["native"],
205
- // 可以为 https 链接或者 Data URL
206
- source: "https://at.alicdn.com/t/c/font_5095635_ztv7ro0zm59.ttf?t=1766563440291",
343
+ source: "https://at.alicdn.com/t/c/font_5095635_0u3b33ft9ub.ttf?t=1770624313420",
207
344
  });
208
345
  },
209
346
  },
210
347
  });
348
+
349
+
350
+
351
+
352
+
@@ -1,39 +1,63 @@
1
- <template name="layout-template">
2
- <block wx:for="{{renderCards}}" wx:key="id">
3
- <view class="km-card-layout layout-container">
4
- <view class="km-card-layout__item canvas-item">
5
- <view class="km-card canvas-item" style="{{item.cardStyle}}">
6
- <image wx:if="{{item.backgroundImage}}" class="km-card__bg canvas-item" style="{{item.backgroundStyle}}" src="{{item.backgroundImage}}" mode="aspectFill" />
7
- <block wx:for="{{item.elements}}" wx:key="id">
8
- <template wx:if="{{item.visible !== false}}" is="render-element" data="{{el:item, rootData: rootData}}" />
9
- </block>
10
- </view>
1
+ <view class="km-card-layout layout-view-container" bindtouchstart="onTouchStart" bindtouchend="onTouchEnd" bindtouchcancel="onTouchCancel">
2
+ <view class="km-card-layout__item canvas-item">
3
+ <view wx:if="{{flipEnabled}}" class="km-card-layout__flip {{isFlipped ? 'is-flipped' : ''}} {{isSwitching ? 'is-switching' : ''}}" style="{{frontCard.sizeStyle}}">
4
+ <view class="km-card-layout__face km-card-layout__face--front">
5
+ <template is="card-content" data="{{ idPrefix: 'view', card: frontCard, rootData: rootData }}" />
6
+ </view>
7
+ <view class="km-card-layout__face km-card-layout__face--back">
8
+ <template is="card-content" data="{{ idPrefix: 'view', card: backCard, rootData: rootData }}" />
11
9
  </view>
12
10
  </view>
13
- </block>
14
- </template>
15
- <template is="layout-template" data="{{ renderCards: cards, rootData: rootData }}"></template>
16
- <!-- 暂时只绘制第一张卡片 -->
17
- <wxml2canvas id="layout-canvas" container-class="layout-container" item-class="canvas-item"></wxml2canvas>
11
+ <view wx:else class="km-card-layout__single">
12
+ <template is="card-content" data="{{ idPrefix: 'view', card: activeCard, rootData: rootData }}" />
13
+ </view>
14
+ <view wx:if="{{showToggle}}" class="km-card-layout__toggle" catchtap="onToggleTap">
15
+ <icon-element element="{{switch}}" > </icon-element>
16
+ <text class="km-card-layout__toggle-text">{{activeIndex === 0?'反面':'正面'}}</text>
17
+ </view>
18
+ </view>
19
+ </view>
20
+
21
+ <view class="km-card-layout layout-canvas-container" style="{{canvasCard.sizeStyle}}">
22
+ <view class="km-card-layout__item canvas-item">
23
+ <view class="km-card-layout__single">
24
+ <template is="card-content" data="{{ idPrefix: 'canvas', card: canvasCard, rootData: rootData }}" />
25
+ </view>
26
+ </view>
27
+ </view>
28
+
29
+ <wxml2canvas id="layout-canvas" container-class="layout-canvas-container" item-class="canvas-item"></wxml2canvas>
18
30
  <share-canvas id="share-canvas"></share-canvas>
31
+
32
+ <template name="card-content">
33
+ <view wx:if="{{card}}" class="km-card canvas-item" style="{{card.cardStyle}}">
34
+ <image wx:if="{{card.backgroundImage}}" class="km-card__bg canvas-item" style="{{card.backgroundStyle}}" src="{{card.backgroundImage}}" mode="aspectFill" />
35
+ <block wx:for="{{card.elements}}" wx:key="id">
36
+ <template wx:if="{{item.visible !== false}}" is="render-element" data="{{el:item, rootData: rootData, idPrefix: idPrefix}}" />
37
+ </block>
38
+ </view>
39
+ </template>
40
+
19
41
  <template name="render-element">
20
42
  <block wx:if="{{el.type === 'image'}}">
21
- <image-element id="node-{{el.id}}" class="canvas-item" data-component="{{true}}" element="{{el}}" rootData="{{rootData}}" />
43
+ <image-element id="{{idPrefix}}-node-{{el.id}}" class="canvas-item" data-component="{{true}}" element="{{el}}" rootData="{{rootData}}" />
22
44
  </block>
23
45
  <block wx:elif="{{el.type === 'icon'}}">
24
- <icon-element id="node-{{el.id}}" class="canvas-item" data-component="{{true}}" element="{{el}}" rootData="{{rootData}}" />
46
+ <icon-element id="{{idPrefix}}-node-{{el.id}}" class="canvas-item" data-component="{{true}}" element="{{el}}" rootData="{{rootData}}" />
25
47
  </block>
26
48
  <block wx:elif="{{el.type === 'layout-panel'}}">
27
- <layout-panel-element id="node-{{el.id}}" class="canvas-item" data-component="{{true}}" element="{{el}}" rootData="{{rootData}}">
49
+ <layout-panel-element id="{{idPrefix}}-node-{{el.id}}" class="canvas-item" data-component="{{true}}" element="{{el}}" rootData="{{rootData}}">
28
50
  <block wx:for="{{el.children}}" wx:key="id">
29
- <template is="render-element" data="{{el:item, rootData: rootData}}" />
51
+ <template is="render-element" data="{{el:item, rootData: rootData, idPrefix: idPrefix}}" />
30
52
  </block>
31
53
  </layout-panel-element>
32
54
  </block>
33
55
  <block wx:elif="{{el.type === 'custom'}}">
34
- <custom-element id="node-{{el.id}}" class="canvas-item" data-component="{{true}}" element="{{el}}" rootData="{{rootData}}" />
56
+ <custom-element id="{{idPrefix}}-node-{{el.id}}" class="canvas-item" data-component="{{true}}" element="{{el}}" rootData="{{rootData}}" />
35
57
  </block>
36
58
  <block wx:else>
37
- <text-element id="node-{{el.id}}" class="canvas-item" data-component="{{true}}" element="{{el}}" rootData="{{rootData}}" />
59
+ <text-element id="{{idPrefix}}-node-{{el.id}}" class="canvas-item" data-component="{{true}}" element="{{el}}" rootData="{{rootData}}" />
38
60
  </block>
39
- </template>
61
+ </template>
62
+
63
+
@@ -2,10 +2,66 @@
2
2
  display: flex;
3
3
  flex-direction: column;
4
4
  position: relative;
5
+ perspective: 1200rpx;
5
6
  }
6
7
 
7
8
  .km-card-layout__item {
8
9
  width: 100%;
10
+ position: relative;
11
+ }
12
+
13
+ .layout-canvas-container {
14
+ position: fixed;
15
+ left: -10000rpx;
16
+ top: 0;
17
+ opacity: 0;
18
+ pointer-events: none;
19
+ z-index: -1;
20
+ }
21
+
22
+ .km-card-layout__flip {
23
+ position: relative;
24
+ -webkit-transform-style: preserve-3d;
25
+ transform-style: preserve-3d;
26
+ transition: transform 420ms ease;
27
+ transform-origin: center;
28
+ will-change: transform;
29
+ }
30
+
31
+ .km-card-layout__flip.is-flipped {
32
+ transform: rotateY(180deg);
33
+ }
34
+
35
+ .km-card-layout__flip:not(.is-switching) .km-card-layout__face--back {
36
+ visibility: hidden;
37
+ }
38
+
39
+ .km-card-layout__flip.is-flipped:not(.is-switching) .km-card-layout__face--front {
40
+ visibility: hidden;
41
+ }
42
+
43
+ .km-card-layout__flip.is-flipped:not(.is-switching) .km-card-layout__face--back {
44
+ visibility: visible;
45
+ }
46
+
47
+ .km-card-layout__face {
48
+ position: absolute;
49
+ left: 0;
50
+ top: 0;
51
+ width: 100%;
52
+ height: 100%;
53
+ -webkit-backface-visibility: hidden;
54
+ backface-visibility: hidden;
55
+ -webkit-transform-style: preserve-3d;
56
+ transform-style: preserve-3d;
57
+ }
58
+
59
+ .km-card-layout__face--back {
60
+ transform: rotateY(180deg);
61
+ }
62
+
63
+ .km-card-layout__single {
64
+ position: relative;
9
65
  }
10
66
 
11
67
  .km-card {
@@ -15,6 +71,8 @@
15
71
  color: inherit;
16
72
  font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
17
73
  background: transparent;
74
+ -webkit-backface-visibility: hidden;
75
+ backface-visibility: hidden;
18
76
  }
19
77
 
20
78
  .km-card__bg {
@@ -25,3 +83,27 @@
25
83
  height: 100%;
26
84
  object-fit: cover;
27
85
  }
86
+
87
+ .km-card-layout__toggle {
88
+ position: absolute;
89
+ top: 48rpx;
90
+ right: 20rpx;
91
+ z-index: 20;
92
+ padding: 8rpx 16rpx;
93
+ display: flex;
94
+ align-items: center;
95
+ gap: 4rpx;
96
+ border-radius: 30px;
97
+ border: 0.5px solid #FFF;
98
+ color: #fff;
99
+ background: rgba(85, 85, 85, 0.30);
100
+ backdrop-filter: blur(4px);
101
+ -webkit-backdrop-filter: blur(4rpx);
102
+
103
+ }
104
+
105
+ .km-card-layout__toggle-text {
106
+ color: #ffffff;
107
+ font-size: 24rpx;
108
+ line-height: 1;
109
+ }