dsh-design-studio 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +20 -104
- package/README.md +20 -184
- package/cordis.patch.yml +2 -2
- package/lib/client.js +169 -37
- package/lib/core.js +4 -3
- package/lib/handoff.js +7 -0
- package/lib/index.js +5 -5
- package/lib/look.js +24 -13
- package/lib/picker.js +576 -33
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +2 -2
- package/skills/ui-design/SKILL.md +13 -6
- package/skills/ui-design/references/miniprogram.md +27 -0
package/lib/client.js
CHANGED
|
@@ -83,6 +83,7 @@ window.__ModuleLoader__.load({
|
|
|
83
83
|
kind: '类型',
|
|
84
84
|
kindWeb: '网页',
|
|
85
85
|
kindApp: 'App',
|
|
86
|
+
kindMini: '小程序',
|
|
86
87
|
kindTablet: '平板',
|
|
87
88
|
kindPoster: '海报',
|
|
88
89
|
kindSlides: '幻灯片',
|
|
@@ -139,6 +140,7 @@ window.__ModuleLoader__.load({
|
|
|
139
140
|
ck_tight: '离固定栏太近',
|
|
140
141
|
ck_jserr: '页面脚本报错',
|
|
141
142
|
ck_missing: '文件没载出来',
|
|
143
|
+
ck_capsule: '被小程序胶囊挡住',
|
|
142
144
|
cd_hscroll: '伸出屏幕 {px}px',
|
|
143
145
|
cd_clipped: '被切掉 {px}px',
|
|
144
146
|
cd_overflow: '伸出框 {px}px',
|
|
@@ -155,6 +157,7 @@ window.__ModuleLoader__.load({
|
|
|
155
157
|
cd_tight: '离{edgeName} {bar} 只有 {px}px(留 16px 以上)',
|
|
156
158
|
cd_jserr: '{msg}{at}',
|
|
157
159
|
cd_missing: '{what} {src} 载不出来(路径写错了,或者文件不在)',
|
|
160
|
+
cd_capsule: '伸进右上角胶囊 {px}px(胶囊是微信画的、盖不住:顶栏右边从 x={x} 起空着)',
|
|
158
161
|
ckEdge_top: '顶栏',
|
|
159
162
|
ckEdge_bottom: '底栏',
|
|
160
163
|
ckWhen_top: '没滚时',
|
|
@@ -254,7 +257,10 @@ window.__ModuleLoader__.load({
|
|
|
254
257
|
stylesTitle: '换个风格',
|
|
255
258
|
stylesSwap: '点一个,让模型把现在的设计换成这个风格(先改 tokens.css,布局和文案不动)',
|
|
256
259
|
styleText: '〔风格〕{name}——{desc}\n配色:{colors}\n字体:{fonts}\n形状:{shape}\n要点:{notes}\n{ask}',
|
|
257
|
-
styleColors: '底色 {bg},卡片 {surface},文字 {text},次要文字 {muted},分隔线 {line},主色 {primary}(上面的字 {on}),点缀 {accent}
|
|
260
|
+
styleColors: '底色 {bg},卡片 {surface},文字 {text},次要文字 {muted},分隔线 {line},主色 {primary}(上面的字 {on}),点缀 {accent}({accUse}),链接和小字强调 {link}{bgImage}',
|
|
261
|
+
accBig: '图形、大字',
|
|
262
|
+
accBlock: '只做图形和色块,别拿来写字;色块上的字用 {text}',
|
|
263
|
+
accShape: '只做图形,别拿来写字',
|
|
258
264
|
styleBgImage: ';底色上叠 {v}',
|
|
259
265
|
styleFonts: '标题 {head};正文 {body}',
|
|
260
266
|
styleShape: '圆角 {r}px;阴影 {s}',
|
|
@@ -396,6 +402,7 @@ window.__ModuleLoader__.load({
|
|
|
396
402
|
kind: 'Type',
|
|
397
403
|
kindWeb: 'Web',
|
|
398
404
|
kindApp: 'App',
|
|
405
|
+
kindMini: 'Mini program',
|
|
399
406
|
kindTablet: 'Tablet',
|
|
400
407
|
kindPoster: 'Poster',
|
|
401
408
|
kindSlides: 'Slides',
|
|
@@ -452,6 +459,7 @@ window.__ModuleLoader__.load({
|
|
|
452
459
|
ck_tight: 'Too close to a fixed bar',
|
|
453
460
|
ck_jserr: 'Script error',
|
|
454
461
|
ck_missing: 'File failed to load',
|
|
462
|
+
ck_capsule: 'Under the mini program capsule',
|
|
455
463
|
cd_hscroll: 'sticks out {px}px',
|
|
456
464
|
cd_clipped: '{px}px cut off',
|
|
457
465
|
cd_overflow: 'sticks out {px}px',
|
|
@@ -468,6 +476,7 @@ window.__ModuleLoader__.load({
|
|
|
468
476
|
cd_tight: 'only {px}px from the {edgeName} {bar} (leave 16px or more)',
|
|
469
477
|
cd_jserr: '{msg}{at}',
|
|
470
478
|
cd_missing: '{what} {src} did not load (wrong path, or the file is missing)',
|
|
479
|
+
cd_capsule: '{px}px under the top-right capsule (WeChat draws it on top; keep the top bar clear from x={x})',
|
|
471
480
|
ckEdge_top: 'top bar',
|
|
472
481
|
ckEdge_bottom: 'bottom bar',
|
|
473
482
|
ckWhen_top: 'before scrolling, ',
|
|
@@ -567,7 +576,10 @@ window.__ModuleLoader__.load({
|
|
|
567
576
|
stylesTitle: 'Change the style',
|
|
568
577
|
stylesSwap: 'Pick one and the model restyles the current design (tokens.css first; layout and copy stay)',
|
|
569
578
|
styleText: '[Style] {name}: {desc}\nColors: {colors}\nFonts: {fonts}\nShape: {shape}\nNotes: {notes}\n{ask}',
|
|
570
|
-
styleColors: 'background {bg}, cards {surface}, text {text}, muted text {muted}, lines {line}, primary {primary} (text on it {on}), accent {accent} (
|
|
579
|
+
styleColors: 'background {bg}, cards {surface}, text {text}, muted text {muted}, lines {line}, primary {primary} (text on it {on}), accent {accent} ({accUse}), links and small emphasis {link}{bgImage}',
|
|
580
|
+
accBig: 'graphics and large text only',
|
|
581
|
+
accBlock: 'graphics and color blocks only, not for text; text on those blocks uses {text}',
|
|
582
|
+
accShape: 'graphics only, not for text',
|
|
571
583
|
styleBgImage: '; on the background: {v}',
|
|
572
584
|
styleFonts: 'headings {head}; body {body}',
|
|
573
585
|
styleShape: 'radius {r}px; shadow {s}',
|
|
@@ -727,8 +739,8 @@ window.__ModuleLoader__.load({
|
|
|
727
739
|
.ddp-frame.cur{opacity:1;z-index:2;pointer-events:auto;transition:opacity .32s ease}
|
|
728
740
|
.ddp-dev{position:relative;background:#fff;box-shadow:0 0 0 .5px rgba(0,0,0,.14),0 18px 48px rgba(0,0,0,.2)}
|
|
729
741
|
.ddp-dev.fill{position:absolute;inset:0;box-shadow:none;border-radius:0}
|
|
730
|
-
.ddp-dev.k-app,.ddp-dev.k-tablet{background:#0e0e12;box-shadow:inset 0 0 0 1.5px #2b2b33,0 0 0 1px rgba(0,0,0,.45),0 24px 60px rgba(0,0,0,.3)}
|
|
731
|
-
.ddp-dev.fill.k-app,.ddp-dev.fill.k-tablet{background:#fff;box-shadow:none}
|
|
742
|
+
.ddp-dev.k-app,.ddp-dev.k-miniprogram,.ddp-dev.k-tablet{background:#0e0e12;box-shadow:inset 0 0 0 1.5px #2b2b33,0 0 0 1px rgba(0,0,0,.45),0 24px 60px rgba(0,0,0,.3)}
|
|
743
|
+
.ddp-dev.fill.k-app,.ddp-dev.fill.k-miniprogram,.ddp-dev.fill.k-tablet{background:#fff;box-shadow:none}
|
|
732
744
|
.ddp-vp{position:absolute;overflow:hidden;background:#fff;isolation:isolate}
|
|
733
745
|
.ddp-vp:has(.ddp-scr.dk),.ddp-scr.dk .ddp-frame{background:#111}
|
|
734
746
|
.ddp-chrome{position:absolute;left:0;right:0;top:0;align-items:center;gap:7px;padding:0 13px;background:#ebebef;border-bottom:1px solid rgba(0,0,0,.07);border-radius:inherit;border-bottom-left-radius:0;border-bottom-right-radius:0;overflow:hidden}
|
|
@@ -737,6 +749,14 @@ window.__ModuleLoader__.load({
|
|
|
737
749
|
.ddp-chrome i:nth-child(3){background:#28c840}
|
|
738
750
|
.ddp-chrome span{flex:1;min-width:0;margin:0 60px 0 18px;height:20px;border-radius:6px;background:rgba(0,0,0,.05);color:#6b6b74;font:12px/20px -apple-system,BlinkMacSystemFont,"PingFang SC",sans-serif;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
739
751
|
.ddp-island{position:absolute;left:50%;width:118px;height:34px;margin-left:-59px;border-radius:20px;background:#000;z-index:4;pointer-events:none}
|
|
752
|
+
.ddp-cap{position:absolute;width:87px;height:32px;z-index:4;pointer-events:none;overflow:visible;fill:#000}
|
|
753
|
+
.ddp-cap .pl{fill:rgba(255,255,255,.6);stroke:rgba(0,0,0,.08);stroke-width:.5}
|
|
754
|
+
.ddp-cap .sp{stroke:rgba(0,0,0,.15);stroke-width:.5}
|
|
755
|
+
.ddp-cap .rg{fill:none;stroke:#000;stroke-width:2}
|
|
756
|
+
.ddp-cap.w{fill:#fff}
|
|
757
|
+
.ddp-cap.w .pl{fill:rgba(0,0,0,.15);stroke:rgba(255,255,255,.25)}
|
|
758
|
+
.ddp-cap.w .sp{stroke:rgba(255,255,255,.25)}
|
|
759
|
+
.ddp-cap.w .rg{stroke:#fff}
|
|
740
760
|
.ddp-body{position:absolute;inset:0}
|
|
741
761
|
.ddp-body.vb{top:46px}
|
|
742
762
|
.ddp-single{position:absolute;inset:0;overflow:hidden}
|
|
@@ -1050,6 +1070,7 @@ window.__ModuleLoader__.load({
|
|
|
1050
1070
|
single: () => svg([h('rect', { key: 1, x: 5, y: 3.5, width: 14, height: 17, rx: 2.2 })]),
|
|
1051
1071
|
web: () => svg([h('rect', { key: 1, x: 3, y: 4.5, width: 18, height: 15, rx: 2.2 }), h('path', { key: 2, d: 'M3 9h18' }), h('path', { key: 3, d: 'M6 6.8h.01M8.5 6.8h.01' })]),
|
|
1052
1072
|
app: () => svg([h('rect', { key: 1, x: 7, y: 2.5, width: 10, height: 19, rx: 2.4 }), h('path', { key: 2, d: 'M10.5 5h3' })]),
|
|
1073
|
+
miniprogram: () => svg([h('rect', { key: 1, x: 6, y: 2.5, width: 12, height: 19, rx: 2.4 }), h('rect', { key: 2, x: 11.5, y: 5.2, width: 4.5, height: 2.4, rx: 1.2 })]),
|
|
1053
1074
|
tablet: () => svg([h('rect', { key: 1, x: 4.5, y: 3, width: 15, height: 18, rx: 2.2 }), h('path', { key: 2, d: 'M11 18h2' })]),
|
|
1054
1075
|
poster: () => svg([h('rect', { key: 1, x: 5, y: 3, width: 14, height: 18, rx: 1.8 }), h('path', { key: 2, d: 'M5 16l4-4 3 3 2-2 5 5' }), h('circle', { key: 3, cx: 14.5, cy: 8, r: 1.6 })]),
|
|
1055
1076
|
slides: () => svg([h('rect', { key: 1, x: 2.5, y: 4, width: 19, height: 12, rx: 1.8 }), h('path', { key: 2, d: 'M12 16v3M8.5 20.5h7' }), h('path', { key: 3, d: 'M6.5 8h6M6.5 11h4' })]),
|
|
@@ -1312,6 +1333,7 @@ window.__ModuleLoader__.load({
|
|
|
1312
1333
|
const SANS = '"PingFang SC", "HarmonyOS Sans SC", "Microsoft YaHei", system-ui, sans-serif'
|
|
1313
1334
|
const SERIF = '"Songti SC", "Noto Serif SC", "Source Han Serif SC", serif'
|
|
1314
1335
|
const ROUND = '"SF Pro Rounded", "PingFang SC", "HarmonyOS Sans SC", system-ui, sans-serif'
|
|
1336
|
+
const MONO = '"JetBrains Mono", "SF Mono", Menlo, Consolas, monospace'
|
|
1315
1337
|
const STYLES = [
|
|
1316
1338
|
{ id: 'minimal', zh: '极简白', en: 'Minimal', dzh: '大留白、细线、黑白加一点蓝', den: 'Lots of space, hairlines, black and white with a touch of blue',
|
|
1317
1339
|
c: { bg: '#FAFAF9', surface: '#FFFFFF', text: '#18181B', muted: '#6B6B73', line: '#E4E4E7', primary: '#18181B', accent: '#2563EB', on: '#FFFFFF', link: '#2563EB' },
|
|
@@ -1359,8 +1381,50 @@ window.__ModuleLoader__.load({
|
|
|
1359
1381
|
head: ROUND, w: 700, radius: 18, shadow: '6px 6px 14px #C8CED6, -6px -6px 14px #FFFFFF',
|
|
1360
1382
|
nzh: '卡片和底同色,靠一亮一暗两道阴影凸起,按下用内阴影;容易看不清,所以主按钮一定用主色实底、文字够深;别整屏都是凸起',
|
|
1361
1383
|
nen: 'Cards share the background color and rise with a light and a dark shadow; pressed states use inset shadows; it gets low-contrast fast, so the main button is solid primary with strong text; don\'t make everything raised' },
|
|
1384
|
+
{ id: 'guochao', zh: '国潮', en: 'Guochao', dzh: '中国红配金、粗宋体、传统纹样', den: 'Chinese red and gold, bold Song type, traditional patterns',
|
|
1385
|
+
c: { bg: '#FBF3E4', surface: '#FFFAF0', text: '#2A1410', muted: '#6F4A3A', line: '#E8D3B5', primary: '#B8202A', accent: '#C8952E', on: '#FFF7E6', link: '#A31D26' },
|
|
1386
|
+
head: SERIF, w: 800, radius: 6, shadow: '0 6px 18px rgba(140,30,20,.12)',
|
|
1387
|
+
nzh: '中国红的大色块配金色细线;标题用粗宋体,可以竖排;祥云、回纹、印章这类纹样用 SVG 画,只做点缀别铺满;底色是米黄纸色,不用纯白',
|
|
1388
|
+
nen: 'Big Chinese-red blocks with fine gold lines; bold Song headings, vertical text allowed; clouds, fret borders and seal stamps drawn in SVG as accents only; rice-paper background, not pure white' },
|
|
1389
|
+
{ id: 'ink', zh: '中式雅致', en: 'Ink & paper', dzh: '宣纸底、墨色字、朱砂点缀', den: 'Paper white, ink-black type, a touch of cinnabar',
|
|
1390
|
+
c: { bg: '#F5F2EC', surface: '#FBF9F5', text: '#1E1E1C', muted: '#5F5B54', line: '#DAD4C8', primary: '#2F3B37', accent: '#8C2F2B', on: '#F5F2EC', link: '#8C2F2B' },
|
|
1391
|
+
head: SERIF, w: 500, radius: 4, shadow: 'none',
|
|
1392
|
+
nzh: '大量留白、细线分隔;标题细宋体、字距放开;朱砂红只像印章一样点一两处;插画用浅灰的水墨晕染(SVG 模糊色块),不用照片堆满',
|
|
1393
|
+
nen: 'Lots of space and hairline dividers; light Song headings with open tracking; cinnabar red only in one or two seal-like spots; illustrations as pale ink washes (blurred SVG shapes), not photo walls' },
|
|
1394
|
+
{ id: 'cyber', zh: '赛博朋克', en: 'Cyberpunk', dzh: '近黑底、品红青色霓虹、等宽字', den: 'Near-black, magenta and cyan neon, monospace',
|
|
1395
|
+
c: { bg: '#0B0B16', surface: '#151528', text: '#E9E6FF', muted: '#A09CC8', line: '#2E2B52', primary: '#FF2E88', accent: '#22E1FF', on: '#0B0B16', link: '#22E1FF' },
|
|
1396
|
+
head: MONO, w: 700, radius: 2, shadow: '0 0 0 1px #FF2E88, 0 0 18px rgba(255,46,136,.45)',
|
|
1397
|
+
nzh: '标题用等宽字、全大写,可以加一点故障错位(两层彩色 text-shadow);边框是 1px 霓虹线加外发光,发光只给主按钮和关键数字;背景可以叠细网格线;正文照样要清楚',
|
|
1398
|
+
nen: 'Monospace, all-caps headings with a slight glitch (two colored text-shadows); 1px neon borders with glow, glow only on the main button and key numbers; a fine grid on the background is fine; body text stays readable' },
|
|
1399
|
+
{ id: 'cute', zh: '可爱卡通', en: 'Cute', dzh: '粉黄糖果色、大圆角、圆体字', den: 'Candy pink and yellow, big radii, rounded type',
|
|
1400
|
+
c: { bg: '#FFF4F6', surface: '#FFFFFF', text: '#3A2540', muted: '#7A5E80', line: '#F3D3DD', primary: '#FF7AA8', accent: '#FFC93C', on: '#3A2540', link: '#C2366F' },
|
|
1401
|
+
head: ROUND, w: 800, radius: 24, shadow: '0 6px 0 #F3C4D3',
|
|
1402
|
+
nzh: '圆角很大、字胖胖的;按钮像糖果,底下一道同色系的厚阴影;配小动物或角色插画(SVG 简单几何画);粉色和黄色两种就够,主色按钮上用深色字',
|
|
1403
|
+
nen: 'Very round corners and chubby type; candy-like buttons with a thick same-hue shadow underneath; small animal or mascot illustrations (simple SVG shapes); pink and yellow are enough; dark text on the primary button' },
|
|
1404
|
+
{ id: 'retro', zh: '复古', en: 'Retro', dzh: '焦橙芥末黄墨绿、粗衬线、颗粒感', den: 'Burnt orange, mustard and teal, fat serif, grain',
|
|
1405
|
+
c: { bg: '#F3E6CF', surface: '#FBF2E2', text: '#2E1F14', muted: '#6A5240', line: '#DDC7A4', primary: '#B5471D', accent: '#E0A526', on: '#FFF6E8', link: '#2F5D50' },
|
|
1406
|
+
head: SERIF, w: 800, radius: 14, shadow: '0 4px 0 #2E1F14',
|
|
1407
|
+
nzh: '焦橙、芥末黄、墨绿三色;粗衬线大标题;彩虹条纹、圆形徽章做装饰;底色叠一层很淡的颗粒噪点(SVG feTurbulence);图片调成褪色暖调',
|
|
1408
|
+
nen: 'Burnt orange, mustard and teal; fat serif headlines; rainbow stripes and round badges as decoration; a very light grain over the background (SVG feTurbulence); faded warm photos' },
|
|
1409
|
+
{ id: 'sale', zh: '电商促销', en: 'Sale', dzh: '红橙主色、大价格、优惠券', den: 'Red-orange, big prices, coupons',
|
|
1410
|
+
c: { bg: '#FFF5F0', surface: '#FFFFFF', text: '#1F1A17', muted: '#6B5E57', line: '#F5DDD2', primary: '#CF2E0E', accent: '#FFB800', on: '#FFFFFF', link: '#C2290B' },
|
|
1411
|
+
head: SANS, w: 800, radius: 12, shadow: '0 4px 12px rgba(207,46,14,.12)',
|
|
1412
|
+
nzh: '价格用大号粗体数字(¥ 小一号),原价灰色划线;「限时」「包邮」这类小标签用色块;优惠券用金黄色、带锯齿边;倒计时醒目;信息多但分区清楚,主按钮通栏',
|
|
1413
|
+
nen: 'Prices in big bold numerals (smaller currency sign), old price struck through in gray; small colored tags like "Limited" and "Free shipping"; gold coupons with notched edges; a prominent countdown; dense but clearly sectioned, full-width main button' },
|
|
1362
1414
|
]
|
|
1363
1415
|
|
|
1416
|
+
/** 两个 #RRGGBB 的对比度(WCAG) */
|
|
1417
|
+
function hexRatio(a, b) {
|
|
1418
|
+
const l = (hex) => { const v = hex.slice(1).match(/../g).map((x) => parseInt(x, 16) / 255).map((c) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4)); return 0.2126 * v[0] + 0.7152 * v[1] + 0.0722 * v[2] }
|
|
1419
|
+
const [x, y] = [l(a), l(b)].sort((p, q) => q - p)
|
|
1420
|
+
return (x + 0.05) / (y + 0.05)
|
|
1421
|
+
}
|
|
1422
|
+
/** 点缀色能怎么用:和底色、卡片都 ≥ 3:1 能写大字;不然只做色块(上面的字用正文色,要 ≥ 4.5:1)或只做图形 */
|
|
1423
|
+
function accentUse(c) {
|
|
1424
|
+
if (Math.min(hexRatio(c.accent, c.bg), hexRatio(c.accent, c.surface)) >= 3) return 'accBig'
|
|
1425
|
+
return hexRatio(c.text, c.accent) >= 4.5 ? 'accBlock' : 'accShape'
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1364
1428
|
/** 选了一个风格 → 输入框里的一段字(restyle:给已经打开的设计换风格) */
|
|
1365
1429
|
function formatStyle(t, pre, restyle) {
|
|
1366
1430
|
const en = t('lang') === 'en'
|
|
@@ -1369,7 +1433,7 @@ window.__ModuleLoader__.load({
|
|
|
1369
1433
|
return t('styleText', {
|
|
1370
1434
|
name: en ? pre.en : pre.zh,
|
|
1371
1435
|
desc: en ? pre.den : pre.dzh,
|
|
1372
|
-
colors: t('styleColors', { ...c, bgImage: pre.bgImage ? t('styleBgImage', { v: pre.bgImage }) : '' }),
|
|
1436
|
+
colors: t('styleColors', { ...c, accUse: t(accentUse(c), c), bgImage: pre.bgImage ? t('styleBgImage', { v: pre.bgImage }) : '' }),
|
|
1373
1437
|
fonts,
|
|
1374
1438
|
shape: t('styleShape', { r: pre.radius, s: pre.shadow }),
|
|
1375
1439
|
notes: en ? pre.nen : pre.nzh,
|
|
@@ -1533,9 +1597,9 @@ window.__ModuleLoader__.load({
|
|
|
1533
1597
|
|
|
1534
1598
|
// ── 导出图片:设备外框、整套展示图 ───────────────────────────────────────
|
|
1535
1599
|
|
|
1536
|
-
const SHOW_COLS = { app: 4, tablet: 3, web: 2, poster: 3, slides: 2, email: 3 }
|
|
1600
|
+
const SHOW_COLS = { app: 4, miniprogram: 4, tablet: 3, web: 2, poster: 3, slides: 2, email: 3 }
|
|
1537
1601
|
const CANVAS_FONT = '-apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif'
|
|
1538
|
-
const FRAMED = { app: 1, tablet: 1, web: 1 }
|
|
1602
|
+
const FRAMED = { app: 1, miniprogram: 1, tablet: 1, web: 1 }
|
|
1539
1603
|
|
|
1540
1604
|
function loadImg(blob) {
|
|
1541
1605
|
return new Promise((resolve, reject) => {
|
|
@@ -1560,13 +1624,13 @@ window.__ModuleLoader__.load({
|
|
|
1560
1624
|
}
|
|
1561
1625
|
|
|
1562
1626
|
/** 在 (x, y) 画一个带外框的屏:w × ht 是屏幕本身;img 按宽铺进去,高出来的裁掉 */
|
|
1563
|
-
function drawDevice(ctx, kind, img, x, y, w, ht, label) {
|
|
1627
|
+
function drawDevice(ctx, kind, img, x, y, w, ht, label, capWhite) {
|
|
1564
1628
|
const f = frameOf(kind, w, ht)
|
|
1565
1629
|
const pad = f.pad || 0
|
|
1566
1630
|
const top = f.top || 0
|
|
1567
1631
|
const r = f.r || 0
|
|
1568
1632
|
const [W, H] = frameSize(kind, w, ht)
|
|
1569
|
-
const dark = kind === 'app' || kind === 'tablet'
|
|
1633
|
+
const dark = kind === 'app' || kind === 'miniprogram' || kind === 'tablet'
|
|
1570
1634
|
ctx.save()
|
|
1571
1635
|
ctx.shadowColor = 'rgba(15, 18, 30, .22)'
|
|
1572
1636
|
ctx.shadowBlur = 48
|
|
@@ -1618,6 +1682,47 @@ window.__ModuleLoader__.load({
|
|
|
1618
1682
|
ctx.fillStyle = '#000'
|
|
1619
1683
|
ctx.fill()
|
|
1620
1684
|
}
|
|
1685
|
+
// 胶囊颜色用页面截图那一刻自己量的(和画布上一致);老页面没报就看截图
|
|
1686
|
+
if (f.capsule) drawCapsule(ctx, x + pad + w - CAP.right - CAP.w, y + pad + CAP.top, typeof capWhite === 'boolean' ? capWhite : !!img && capDarkOf(img, w))
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
/** 截图里胶囊那块是不是深色(导出时没有页面可问,看图) */
|
|
1690
|
+
function capDarkOf(img, w) {
|
|
1691
|
+
try {
|
|
1692
|
+
const k = img.width / w
|
|
1693
|
+
const c = document.createElement('canvas')
|
|
1694
|
+
c.width = Math.max(1, Math.round(CAP.w * k))
|
|
1695
|
+
c.height = Math.max(1, Math.round(CAP.h * k))
|
|
1696
|
+
const g = c.getContext('2d')
|
|
1697
|
+
g.drawImage(img, (w - CAP.right - CAP.w) * k, CAP.top * k, CAP.w * k, CAP.h * k, 0, 0, c.width, c.height)
|
|
1698
|
+
const d = g.getImageData(0, 0, c.width, c.height).data
|
|
1699
|
+
let sum = 0
|
|
1700
|
+
for (let i = 0; i < d.length; i += 4) sum += 0.299 * d[i] + 0.587 * d[i + 1] + 0.114 * d[i + 2]
|
|
1701
|
+
return sum / (d.length / 4) < 140
|
|
1702
|
+
} catch { return false }
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
/** 小程序右上角的胶囊(「···」和关闭圈),和画布上 .ddp-cap 一样 */
|
|
1706
|
+
function drawCapsule(ctx, x, y, white) {
|
|
1707
|
+
const fg = white ? '#FFFFFF' : '#000000'
|
|
1708
|
+
ctx.save()
|
|
1709
|
+
rrect(ctx, x, y, CAP.w, CAP.h, CAP.h / 2)
|
|
1710
|
+
ctx.fillStyle = white ? 'rgba(0,0,0,.15)' : 'rgba(255,255,255,.6)'
|
|
1711
|
+
ctx.fill()
|
|
1712
|
+
rrect(ctx, x + 0.25, y + 0.25, CAP.w - 0.5, CAP.h - 0.5, CAP.h / 2)
|
|
1713
|
+
ctx.strokeStyle = white ? 'rgba(255,255,255,.25)' : 'rgba(0,0,0,.08)'
|
|
1714
|
+
ctx.lineWidth = 0.5
|
|
1715
|
+
ctx.stroke()
|
|
1716
|
+
ctx.fillStyle = fg
|
|
1717
|
+
;[[20.5, 2.2], [28, 3.1], [35.5, 2.2]].forEach(([cx, r]) => { ctx.beginPath(); ctx.arc(x + cx, y + 16, r, 0, Math.PI * 2); ctx.fill() })
|
|
1718
|
+
ctx.fillStyle = white ? 'rgba(255,255,255,.25)' : 'rgba(0,0,0,.15)'
|
|
1719
|
+
ctx.fillRect(x + 43.25, y + 9, 0.5, 14)
|
|
1720
|
+
ctx.strokeStyle = fg
|
|
1721
|
+
ctx.lineWidth = 2
|
|
1722
|
+
ctx.beginPath(); ctx.arc(x + 65, y + 16, 7.5, 0, Math.PI * 2); ctx.stroke()
|
|
1723
|
+
ctx.fillStyle = fg
|
|
1724
|
+
ctx.beginPath(); ctx.arc(x + 65, y + 16, 2.6, 0, Math.PI * 2); ctx.fill()
|
|
1725
|
+
ctx.restore()
|
|
1621
1726
|
}
|
|
1622
1727
|
|
|
1623
1728
|
function canvasBlob(canvas) {
|
|
@@ -1625,7 +1730,7 @@ window.__ModuleLoader__.load({
|
|
|
1625
1730
|
}
|
|
1626
1731
|
|
|
1627
1732
|
/** 一屏带外框(四周留出阴影的地方,背景透明) */
|
|
1628
|
-
async function composeOne(kind, size, img, label) {
|
|
1733
|
+
async function composeOne(kind, size, img, label, top) {
|
|
1629
1734
|
const [w, ht] = size
|
|
1630
1735
|
const [W, H] = frameSize(kind, w, ht)
|
|
1631
1736
|
const m = 56
|
|
@@ -1635,7 +1740,7 @@ window.__ModuleLoader__.load({
|
|
|
1635
1740
|
c.height = Math.round((H + m * 2) * S)
|
|
1636
1741
|
const ctx = c.getContext('2d')
|
|
1637
1742
|
ctx.scale(S, S)
|
|
1638
|
-
drawDevice(ctx, kind, img, m, m, w, ht, label)
|
|
1743
|
+
drawDevice(ctx, kind, img, m, m, w, ht, label, top)
|
|
1639
1744
|
return canvasBlob(c)
|
|
1640
1745
|
}
|
|
1641
1746
|
|
|
@@ -1850,7 +1955,7 @@ window.__ModuleLoader__.load({
|
|
|
1850
1955
|
const off = ((cols - inRow) * (DW + gap)) / 2 // 最后一行不满就居中
|
|
1851
1956
|
const x = pad + off + col * (DW + gap)
|
|
1852
1957
|
const yy = y + row * (DH + labH + gap * 0.6)
|
|
1853
|
-
drawDevice(ctx, kind, s.img, x, yy, w, ht, s.label)
|
|
1958
|
+
drawDevice(ctx, kind, s.img, x, yy, w, ht, s.label, s.top)
|
|
1854
1959
|
ctx.fillStyle = muted
|
|
1855
1960
|
ctx.font = `500 ${16 * u}px ${CANVAS_FONT}`
|
|
1856
1961
|
ctx.textAlign = 'center'
|
|
@@ -1927,15 +2032,16 @@ window.__ModuleLoader__.load({
|
|
|
1927
2032
|
|
|
1928
2033
|
// ── 设计类型:画幅和外框 ─────────────────────────────────────────────────
|
|
1929
2034
|
|
|
1930
|
-
const KIND_LIST = ['web', 'app', 'tablet', 'poster', 'slides', 'email']
|
|
1931
|
-
const KIND_KEY = { web: 'kindWeb', app: 'kindApp', tablet: 'kindTablet', poster: 'kindPoster', slides: 'kindSlides', email: 'kindEmail' }
|
|
2035
|
+
const KIND_LIST = ['web', 'app', 'miniprogram', 'tablet', 'poster', 'slides', 'email']
|
|
2036
|
+
const KIND_KEY = { web: 'kindWeb', app: 'kindApp', miniprogram: 'kindMini', tablet: 'kindTablet', poster: 'kindPoster', slides: 'kindSlides', email: 'kindEmail' }
|
|
1932
2037
|
const GHOST_MS = 90000 // 这么久没出新屏就撤掉「生成中…」占位
|
|
1933
2038
|
const EDIT_GRACE_MS = 3000 // 退出改字后这么久里,页面交上来的最后一处改动还收
|
|
1934
|
-
const KIND_SIZE = { web: [1280, 800], app: [390, 844], tablet: [820, 1180], poster: [1080, 1350], slides: [1280, 720], email: [640, 960] }
|
|
1935
|
-
// 外框:pad 边框厚度、top 顶上一条(浏览器栏)、r 外圆角、ir 屏幕圆角、island
|
|
2039
|
+
const KIND_SIZE = { web: [1280, 800], app: [390, 844], miniprogram: [375, 812], tablet: [820, 1180], poster: [1080, 1350], slides: [1280, 720], email: [640, 960] }
|
|
2040
|
+
// 外框:pad 边框厚度、top 顶上一条(浏览器栏)、r 外圆角、ir 屏幕圆角、island 灵动岛离顶的距离、capsule 小程序右上角的胶囊
|
|
1936
2041
|
const FRAME = {
|
|
1937
2042
|
web: { pad: 0, top: 34, r: 10, ir: 0 },
|
|
1938
2043
|
app: { pad: 11, top: 0, r: 55, ir: 44, island: 11 },
|
|
2044
|
+
miniprogram: { pad: 11, top: 0, r: 55, ir: 44, island: 11, capsule: 1 },
|
|
1939
2045
|
tablet: { pad: 14, top: 0, r: 34, ir: 21 },
|
|
1940
2046
|
poster: { pad: 0, top: 0, r: 6, ir: 6 },
|
|
1941
2047
|
slides: { pad: 0, top: 0, r: 8, ir: 8 },
|
|
@@ -1947,7 +2053,9 @@ window.__ModuleLoader__.load({
|
|
|
1947
2053
|
const f = FRAME[kind] || FRAME.web
|
|
1948
2054
|
return f.island && w && ht / w < 1.9 ? { ...f, island: 0 } : f
|
|
1949
2055
|
}
|
|
1950
|
-
|
|
2056
|
+
// 小程序胶囊:离屏幕顶 48(状态栏 44 + 4)、离右边 7、87×32(和页面体检 picker.js 的 CAP 一样)
|
|
2057
|
+
const CAP = { top: 48, right: 7, w: 87, h: 32 }
|
|
2058
|
+
const META_ALIAS = { phone: 'app', mobile: 'app', ios: 'app', android: 'app', mini: 'miniprogram', miniapp: 'miniprogram', weapp: 'miniprogram', wechat: 'miniprogram', 小程序: 'miniprogram', pad: 'tablet', ipad: 'tablet', social: 'poster', image: 'poster', slide: 'slides', deck: 'slides', ppt: 'slides', mail: 'email' }
|
|
1951
2059
|
|
|
1952
2060
|
/** 页面里 <meta name="dsh-design" content="app"> / "poster 1080x1920" → { kind, size } */
|
|
1953
2061
|
function parseMeta(content) {
|
|
@@ -2023,6 +2131,7 @@ window.__ModuleLoader__.load({
|
|
|
2023
2131
|
const SIZE_SETS = {
|
|
2024
2132
|
web: [['szPhone', 'app', [390, 844]], ['szTablet', 'tablet', [820, 1180]], ['szDesktop', 'web', [1440, 900]]],
|
|
2025
2133
|
app: [['szSmall', 'app', [375, 667]], ['szStd', 'app', [393, 852]], ['szLarge', 'app', [430, 932]]],
|
|
2134
|
+
miniprogram: [['szSmall', 'miniprogram', [375, 667]], ['szStd', 'miniprogram', [375, 812]], ['szLarge', 'miniprogram', [414, 896]]],
|
|
2026
2135
|
tablet: [['szSmall', 'tablet', [744, 1133]], ['szStd', 'tablet', [820, 1180]], ['szLarge', 'tablet', [1032, 1376]]],
|
|
2027
2136
|
}
|
|
2028
2137
|
|
|
@@ -2414,10 +2523,10 @@ window.__ModuleLoader__.load({
|
|
|
2414
2523
|
}
|
|
2415
2524
|
|
|
2416
2525
|
/**
|
|
2417
|
-
* 外框:网页是浏览器窗口,App / 平板是带圆角边框的设备(App
|
|
2418
|
-
* fill = 铺满舞台(单页看网页)。不管哪种,里面那屏都在同一个位置(换类型不重载页面)。
|
|
2526
|
+
* 外框:网页是浏览器窗口,App / 平板是带圆角边框的设备(App 还有灵动岛,小程序再加右上角的胶囊),海报 / 幻灯片 / 邮件是一张卡片。
|
|
2527
|
+
* fill = 铺满舞台(单页看网页)。不管哪种,里面那屏都在同一个位置(换类型不重载页面)。top:页面顶栏右边是深色(胶囊画白的)
|
|
2419
2528
|
*/
|
|
2420
|
-
function Device({ kind, w, h: ht, fill, label, children }) {
|
|
2529
|
+
function Device({ kind, w, h: ht, fill, label, top, children }) {
|
|
2421
2530
|
const f = fill ? { pad: 0, top: 0, r: 0, ir: 0 } : frameOf(kind, w, ht)
|
|
2422
2531
|
return h('div', {
|
|
2423
2532
|
className: `ddp-dev k-${kind}` + (fill ? ' fill' : ''),
|
|
@@ -2425,7 +2534,16 @@ window.__ModuleLoader__.load({
|
|
|
2425
2534
|
},
|
|
2426
2535
|
h('div', { className: 'ddp-chrome', style: { display: f.top ? 'flex' : 'none', height: f.top } }, h('i'), h('i'), h('i'), h('span', null, label || '')),
|
|
2427
2536
|
h('div', { className: 'ddp-vp', style: fill ? { inset: 0 } : { left: f.pad, top: f.pad + f.top, width: w, height: ht, borderRadius: f.ir } }, children),
|
|
2428
|
-
h('div', { className: 'ddp-island', style: { display: f.island ? 'block' : 'none', top: (f.pad || 0) + (f.island || 0) } })
|
|
2537
|
+
h('div', { className: 'ddp-island', style: { display: f.island ? 'block' : 'none', top: (f.pad || 0) + (f.island || 0) } }),
|
|
2538
|
+
f.capsule ? h(Capsule, { white: !!top, style: { top: (f.pad || 0) + CAP.top, right: (f.pad || 0) + CAP.right } }) : null)
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
function Capsule({ white, style }) {
|
|
2542
|
+
return h('svg', { className: 'ddp-cap' + (white ? ' w' : ''), style, viewBox: `0 0 ${CAP.w} ${CAP.h}`, 'aria-hidden': true },
|
|
2543
|
+
h('rect', { className: 'pl', x: 0.25, y: 0.25, width: CAP.w - 0.5, height: CAP.h - 0.5, rx: CAP.h / 2 - 0.25 }),
|
|
2544
|
+
h('circle', { cx: 20.5, cy: 16, r: 2.2 }), h('circle', { cx: 28, cy: 16, r: 3.1 }), h('circle', { cx: 35.5, cy: 16, r: 2.2 }),
|
|
2545
|
+
h('path', { className: 'sp', d: 'M43.5 9v14' }),
|
|
2546
|
+
h('circle', { className: 'rg', cx: 65, cy: 16, r: 7.5 }), h('circle', { cx: 65, cy: 16, r: 2.6 }))
|
|
2429
2547
|
}
|
|
2430
2548
|
|
|
2431
2549
|
/**
|
|
@@ -2462,7 +2580,7 @@ window.__ModuleLoader__.load({
|
|
|
2462
2580
|
const rem = remOf(props)
|
|
2463
2581
|
const wins = useRef(new Map())
|
|
2464
2582
|
const life = useRef(null)
|
|
2465
|
-
if (!life.current) life.current = { next: 1, loading: 0, again: false, file: '', seeds: new Map(), seedOf: new Map(), readied: new Set(), snapTimer: null, gen: 0, at: '', deps: null }
|
|
2583
|
+
if (!life.current) life.current = { next: 1, loading: 0, again: false, file: '', seeds: new Map(), seedOf: new Map(), readied: new Set(), tops: new Map(), snapTimer: null, gen: 0, at: '', deps: null }
|
|
2466
2584
|
const curRef = useRef(0)
|
|
2467
2585
|
const P = useRef(props)
|
|
2468
2586
|
P.current = props
|
|
@@ -2571,10 +2689,12 @@ window.__ModuleLoader__.load({
|
|
|
2571
2689
|
if (p.demo) postTo(id, { type: 'demo', on: true })
|
|
2572
2690
|
if (p.textEdit) postTo(id, { type: 'edit', on: true, hint: false })
|
|
2573
2691
|
if (p.styleEdit) styleTo(id, p)
|
|
2692
|
+
if (L.tops.has(id) && p.onTop) p.onTop(L.tops.get(id))
|
|
2574
2693
|
if (first && p.onShown) p.onShown()
|
|
2575
2694
|
if (first) { if (build && p.onBuilt) p.onBuilt(L.title || '') } else if (p.onNote) p.onNote({ changes, file: L.file, build })
|
|
2576
2695
|
L.file = ''
|
|
2577
2696
|
setTimeout(() => setFrames((fs) => fs.filter((f) => f.id >= curRef.current || f.id > id)), 450)
|
|
2697
|
+
for (const k of [...L.tops.keys()]) if (k < id) L.tops.delete(k)
|
|
2578
2698
|
if (L.again) {
|
|
2579
2699
|
L.again = false
|
|
2580
2700
|
postTo(id, { type: 'snapshot' })
|
|
@@ -2588,6 +2708,7 @@ window.__ModuleLoader__.load({
|
|
|
2588
2708
|
L.again = false
|
|
2589
2709
|
L.seeds.clear()
|
|
2590
2710
|
L.seedOf.clear()
|
|
2711
|
+
L.tops.clear()
|
|
2591
2712
|
L.gen++
|
|
2592
2713
|
L.at = ''
|
|
2593
2714
|
L.deps = null
|
|
@@ -2639,6 +2760,11 @@ window.__ModuleLoader__.load({
|
|
|
2639
2760
|
case 'links':
|
|
2640
2761
|
if (p.onLinks && Array.isArray(d.links)) p.onLinks(d.links)
|
|
2641
2762
|
break
|
|
2763
|
+
case 'top':
|
|
2764
|
+
// 每帧记着自己的;只有正显示的那帧算数(预载的换上来时再用,旧帧晚到的不管)
|
|
2765
|
+
L.tops.set(id, !!d.dark)
|
|
2766
|
+
if (isCur && p.onTop) p.onTop(!!d.dark)
|
|
2767
|
+
break
|
|
2642
2768
|
case 'changed':
|
|
2643
2769
|
if (isCur) { L.file = d.file || L.file; load(d.seed) }
|
|
2644
2770
|
break
|
|
@@ -2939,9 +3065,10 @@ window.__ModuleLoader__.load({
|
|
|
2939
3065
|
return h('div', { className: 'ddp-styles' }, ...STYLES.map((x) => h('button', { key: x.id, className: 'ddp-sc', title: en ? x.den : x.dzh, onClick: () => onPick(x) },
|
|
2940
3066
|
h('span', { className: 'pv', style: { background: x.c.bg, backgroundImage: x.bgImage || 'none', color: x.c.text } },
|
|
2941
3067
|
h('b', { style: { fontFamily: x.head, fontWeight: x.w } }, en ? 'Aa' : '字'),
|
|
2942
|
-
h('i', { className: 'bt', style: { background: x.c.primary, borderRadius: Math.min(x.radius, 6), boxShadow: x.id === 'playful' ? `1.5px 1.5px 0 ${x.c.line}` : 'none', outline: x.id === 'playful' ? `1px solid ${x.c.line}` : 'none' } }),
|
|
3068
|
+
h('i', { className: 'bt', style: { background: x.c.primary, borderRadius: Math.min(x.radius, 6), boxShadow: x.id === 'playful' ? `1.5px 1.5px 0 ${x.c.line}` : x.id === 'cyber' ? `0 0 6px ${x.c.primary}` : x.id === 'retro' ? `0 1.5px 0 ${x.c.text}` : 'none', outline: x.id === 'playful' ? `1px solid ${x.c.line}` : 'none' } }),
|
|
2943
3069
|
h('i', { className: 'cd', style: { background: x.id === 'glass' ? 'rgba(255,255,255,.6)' : x.c.surface, borderRadius: Math.min(x.radius, 8) / 2 + 2,
|
|
2944
|
-
boxShadow: x.id === 'soft' ? '2px 2px 5px #C8CED6, -2px -2px 5px #FFFFFF' : x.id === 'playful' ? `2px 2px 0 ${x.c.line}` : x.id === 'editorial' ? `inset 0 1px 0 ${x.c.line}`
|
|
3070
|
+
boxShadow: x.id === 'soft' ? '2px 2px 5px #C8CED6, -2px -2px 5px #FFFFFF' : x.id === 'playful' ? `2px 2px 0 ${x.c.line}` : x.id === 'editorial' ? `inset 0 1px 0 ${x.c.line}`
|
|
3071
|
+
: x.id === 'guochao' ? `inset 0 0 0 1px ${x.c.accent}` : x.id === 'retro' ? `0 2px 0 ${x.c.text}` : `0 0 0 .5px ${x.c.line}` } },
|
|
2945
3072
|
h('i', { className: 'ln', style: { background: x.c.text } }), h('i', { className: 'ln s', style: { background: x.c.muted } }))),
|
|
2946
3073
|
h('span', { className: 'nm' }, en ? x.en : x.zh))))
|
|
2947
3074
|
}
|
|
@@ -3427,6 +3554,8 @@ window.__ModuleLoader__.load({
|
|
|
3427
3554
|
const [flash, setFlash] = useState('')
|
|
3428
3555
|
const [reloadSeq, setReloadSeq] = useState(0)
|
|
3429
3556
|
const [info, setInfo] = useState({}) // 目录|屏 → { meta, title }(页面报上来的)
|
|
3557
|
+
const [tops, setTops] = useState({}) // 屏 → 顶栏右边是不是深色(小程序胶囊画白的)
|
|
3558
|
+
const setTop = (k, dk) => setTops((cur) => (cur[k] === dk ? cur : { ...cur, [k]: dk }))
|
|
3430
3559
|
const [sel, setSel] = useState('')
|
|
3431
3560
|
const [panel, setPanel] = useState(false)
|
|
3432
3561
|
const [styles, setStyles] = useState(false) // 换风格面板
|
|
@@ -3829,7 +3958,7 @@ window.__ModuleLoader__.load({
|
|
|
3829
3958
|
else change({ view: 'single' })
|
|
3830
3959
|
}
|
|
3831
3960
|
|
|
3832
|
-
const renderScreen = (sc, z, fill) => h(Device, { kind, w: size[0], h: size[1], fill, label: labelOf(sc) },
|
|
3961
|
+
const renderScreen = (sc, z, fill) => h(Device, { kind, w: size[0], h: size[1], fill, label: labelOf(sc), top: tops[ik(sc)] },
|
|
3833
3962
|
h(Screen, { t, remote,
|
|
3834
3963
|
src: sc.url,
|
|
3835
3964
|
label: labelOf(sc),
|
|
@@ -3856,6 +3985,7 @@ window.__ModuleLoader__.load({
|
|
|
3856
3985
|
onNote: (n) => note(n, sc),
|
|
3857
3986
|
onMeta: (m, title, toks, dk, links) => remember(ik(sc), m, title, toks, dk, links),
|
|
3858
3987
|
onLinks: (links) => rememberLinks(ik(sc), links),
|
|
3988
|
+
onTop: (dk) => setTop(ik(sc), dk),
|
|
3859
3989
|
onBuilt: (title) => onBuilt(sc, title),
|
|
3860
3990
|
}))
|
|
3861
3991
|
|
|
@@ -3874,9 +4004,8 @@ window.__ModuleLoader__.load({
|
|
|
3874
4004
|
const captureOf = async (sc, full) => {
|
|
3875
4005
|
const api = await apiOf(sc)
|
|
3876
4006
|
const r = await api.capture({ full: !!full, scale: 2 })
|
|
3877
|
-
return { img: await loadImg(r.png), w: r.w, h: r.h, links: Array.isArray(r.links) ? r.links : [] }
|
|
4007
|
+
return { img: await loadImg(r.png), w: r.w, h: r.h, links: Array.isArray(r.links) ? r.links : [], top: typeof r.top === 'boolean' ? r.top : null }
|
|
3878
4008
|
}
|
|
3879
|
-
const shotOf = async (sc, full) => (await captureOf(sc, full)).img
|
|
3880
4009
|
|
|
3881
4010
|
const saveImage = async (name, blob) => {
|
|
3882
4011
|
const png = await blobB64(blob)
|
|
@@ -3920,7 +4049,7 @@ window.__ModuleLoader__.load({
|
|
|
3920
4049
|
if (mode === 'show') {
|
|
3921
4050
|
const shots = []
|
|
3922
4051
|
for (const sc of shown) {
|
|
3923
|
-
try { shots.push({ img:
|
|
4052
|
+
try { const c = await captureOf(sc, false); shots.push({ img: c.img, top: c.top, title: labelOf(sc) }) } catch (e) { console.warn('[dsh-design-studio] 截图失败', sc.key, e) }
|
|
3924
4053
|
}
|
|
3925
4054
|
if (!shots.length) throw new Error('no screens')
|
|
3926
4055
|
const sp = splitTitles(shots.map((x) => x.title))
|
|
@@ -3928,7 +4057,7 @@ window.__ModuleLoader__.load({
|
|
|
3928
4057
|
const blob = await composeShowcase({
|
|
3929
4058
|
kind,
|
|
3930
4059
|
size,
|
|
3931
|
-
shots: shots.map((x, i) => ({ img: x.img, label: sp.labels[i] || x.title })),
|
|
4060
|
+
shots: shots.map((x, i) => ({ img: x.img, top: x.top, label: sp.labels[i] || x.title })),
|
|
3932
4061
|
title: sp.brand || (st && st.target && st.target.kind === 'file' ? String(st.target.file).split(/[\\/]/).slice(-2, -1)[0] : st.label) || '',
|
|
3933
4062
|
sub: t('showSub', { n: shots.length, kind: t(KIND_KEY[kind]), date: ymd(new Date()) }),
|
|
3934
4063
|
bg: colors.bg,
|
|
@@ -3942,8 +4071,8 @@ window.__ModuleLoader__.load({
|
|
|
3942
4071
|
const sc = shown.find((x) => x.key === k) || shown[0]
|
|
3943
4072
|
if (!sc) throw new Error('no screen')
|
|
3944
4073
|
const base = prefix + sc.key.replace(/\.html?$/i, '') + suffix
|
|
3945
|
-
const img = await
|
|
3946
|
-
if (mode === 'frame') await saveImage(base, await composeOne(kind, size, img, labelOf(sc)))
|
|
4074
|
+
const { img, top } = await captureOf(sc, mode === 'long')
|
|
4075
|
+
if (mode === 'frame') await saveImage(base, await composeOne(kind, size, img, labelOf(sc), top))
|
|
3947
4076
|
else {
|
|
3948
4077
|
const c = document.createElement('canvas')
|
|
3949
4078
|
c.width = img.width
|
|
@@ -4317,7 +4446,7 @@ window.__ModuleLoader__.load({
|
|
|
4317
4446
|
return h('div', { key: l.id, className: cls },
|
|
4318
4447
|
h('div', { className: 'ddp-single' },
|
|
4319
4448
|
h('div', { className: 'ddp-fit' + (fill ? ' fill' : ''), style: fill ? undefined : { width: dw, height: dh, transform: `translate(-50%, -50%) scale(${z})` } },
|
|
4320
|
-
h(Device, { kind, w: size[0], h: size[1], fill, label },
|
|
4449
|
+
h(Device, { kind, w: size[0], h: size[1], fill, label, top: tops[l.key] },
|
|
4321
4450
|
h(Screen, { t, remote,
|
|
4322
4451
|
src: urlOfKey(l.key),
|
|
4323
4452
|
label,
|
|
@@ -4334,6 +4463,7 @@ window.__ModuleLoader__.load({
|
|
|
4334
4463
|
onShown: () => demoShown(l.id),
|
|
4335
4464
|
onMeta: (m, title, toks, dk, links) => remember(l.key, m, title, toks, dk, links),
|
|
4336
4465
|
onLinks: (links) => rememberLinks(l.key, links),
|
|
4466
|
+
onTop: (dk) => setTop(l.key, dk),
|
|
4337
4467
|
})))))
|
|
4338
4468
|
}))
|
|
4339
4469
|
} else if (inMulti) {
|
|
@@ -4352,7 +4482,7 @@ window.__ModuleLoader__.load({
|
|
|
4352
4482
|
picking: picking || textEdit,
|
|
4353
4483
|
sel: '',
|
|
4354
4484
|
labelOf: (sc) => sc.label,
|
|
4355
|
-
render: (sc, z, i) => h(Device, { kind: items[i].kind, w: items[i].size[0], h: items[i].size[1], label: labelOf(base) },
|
|
4485
|
+
render: (sc, z, i) => h(Device, { kind: items[i].kind, w: items[i].size[0], h: items[i].size[1], label: labelOf(base), top: tops[ik(base) + '|' + i] },
|
|
4356
4486
|
h(Screen, { t, remote,
|
|
4357
4487
|
src: base.url,
|
|
4358
4488
|
label: labelOf(base),
|
|
@@ -4370,6 +4500,7 @@ window.__ModuleLoader__.load({
|
|
|
4370
4500
|
onEdited: (d) => onEdited(base, d),
|
|
4371
4501
|
onEditDone: () => setTextEdit(false),
|
|
4372
4502
|
onNote: (n) => note(n, base),
|
|
4503
|
+
onTop: (dk) => setTop(ik(base) + '|' + i, dk),
|
|
4373
4504
|
})),
|
|
4374
4505
|
})
|
|
4375
4506
|
} else if (ver && cmp) {
|
|
@@ -4382,12 +4513,13 @@ window.__ModuleLoader__.load({
|
|
|
4382
4513
|
const [dw, dh] = deviceBox(kind, size)
|
|
4383
4514
|
const z = fill ? 1 : Math.max(0.05, Math.min(1, (box.w - 32) / dw, (box.h - 32 - (ver ? 46 : 0)) / dh))
|
|
4384
4515
|
// 两边的小标签不跟着画面缩小(缩了看不清);手机、平板躲开状态栏
|
|
4385
|
-
const tagAt = (side) => ({ top: kind === 'app' || kind === 'tablet' ? 60 : 10, transform: `scale(${1 / z})`, transformOrigin: 'top ' + side })
|
|
4516
|
+
const tagAt = (side) => ({ top: kind === 'app' || kind === 'miniprogram' || kind === 'tablet' ? 60 : 10, transform: `scale(${1 / z})`, transformOrigin: 'top ' + side })
|
|
4386
4517
|
const screenOf = (sc) => h(Screen, { t, remote, src: sc.url, label: labelOf(sc), busy: false, born: false, picking: false, dark, zoom: z, reloadSeq,
|
|
4387
|
-
apis: apis.current, apiKey: 'cmp|' + ik(sc), onMeta: (m, title, toks, dk) => remember(ik(sc), m, title, toks, dk) })
|
|
4518
|
+
apis: apis.current, apiKey: 'cmp|' + ik(sc), onMeta: (m, title, toks, dk) => remember(ik(sc), m, title, toks, dk), onTop: (dk) => setTop(ik(sc), dk) })
|
|
4388
4519
|
body = h('div', { className: 'ddp-single' },
|
|
4389
4520
|
h('div', { className: 'ddp-fit' + (fill ? ' fill' : ''), style: fill ? undefined : { width: dw, height: dh, transform: `translate(-50%, -50%) scale(${z})` } },
|
|
4390
|
-
|
|
4521
|
+
// 胶囊那块露的是哪一边(把手拖过胶囊中间就是左边那一版),胶囊就跟哪一边的顶栏
|
|
4522
|
+
now ? h(Device, { kind, w: size[0], h: size[1], fill, label: labelOf(now), top: tops[ik(has && (cx / 100) * size[0] > size[0] - CAP.right - CAP.w / 2 ? old : now)] },
|
|
4391
4523
|
h('div', { className: 'ddp-cmp' + (cmpDrag ? ' drag' : ''), ref: cmpRef },
|
|
4392
4524
|
h('div', { className: 'ly' }, has ? screenOf(old) : h('div', { className: 'miss' }, t('notInVer', { n: ver.n }))),
|
|
4393
4525
|
// 右边一层只露出把手右边那块:外层裁宽度,里面整屏往左挪(iframe 不变大小、不重排;clip-path 盖 iframe 有的内核画不出来)
|
|
@@ -4556,7 +4688,7 @@ window.__ModuleLoader__.load({
|
|
|
4556
4688
|
// 注意:这里是客户端**服务名**(短的),不是包名
|
|
4557
4689
|
exports.inject = INJECT
|
|
4558
4690
|
// 测试用
|
|
4559
|
-
exports._test = { STYLES, formatStyle, padInsert, pageOf, pxOf, styleChanges, formatStyleAsk, takesOf, formatCheckTool, poller, frameOf, paletteOf, formatRef, layoutRow, keyOfPath, flowsOf, flowLines, flowMarks, formatPick, whereOf, parseMeta, sizeOf, layoutGrid, fitCam, tokenKind, formatToken, splitTitles, pickColors, hhmm }
|
|
4691
|
+
exports._test = { STYLES, formatStyle, accentUse, padInsert, pageOf, pxOf, styleChanges, formatStyleAsk, takesOf, formatCheckTool, poller, frameOf, paletteOf, formatRef, layoutRow, keyOfPath, flowsOf, flowLines, flowMarks, formatPick, whereOf, parseMeta, sizeOf, layoutGrid, fitCam, tokenKind, formatToken, splitTitles, pickColors, hhmm }
|
|
4560
4692
|
return module.exports
|
|
4561
4693
|
},
|
|
4562
4694
|
})
|
package/lib/core.js
CHANGED
|
@@ -31,11 +31,12 @@ const JOB_READ_TOOL = /^(job_output|bash_output|read_job|job_read)$/i
|
|
|
31
31
|
const BG_JOB = /\bbackground job\s+`?([\w.:-]{3,})/i
|
|
32
32
|
const MAX_SCREENS = 24
|
|
33
33
|
|
|
34
|
-
//
|
|
35
|
-
export const KINDS = ['web', 'app', 'tablet', 'poster', 'slides', 'email']
|
|
34
|
+
// 设计类型:决定右侧用什么画幅显示(网页框、手机、小程序、平板、海报、幻灯片、邮件)
|
|
35
|
+
export const KINDS = ['web', 'app', 'miniprogram', 'tablet', 'poster', 'slides', 'email']
|
|
36
36
|
const KIND_ALIAS = {
|
|
37
37
|
website: 'web', site: 'web', page: 'web', landing: 'web', desktop: 'web', dashboard: 'web',
|
|
38
|
-
phone: 'app', mobile: 'app', ios: 'app', android: 'app',
|
|
38
|
+
phone: 'app', mobile: 'app', ios: 'app', android: 'app',
|
|
39
|
+
mini: 'miniprogram', miniapp: 'miniprogram', weapp: 'miniprogram', wechat: 'miniprogram', 小程序: 'miniprogram',
|
|
39
40
|
pad: 'tablet', ipad: 'tablet',
|
|
40
41
|
social: 'poster', image: 'poster', banner: 'poster', card: 'poster', flyer: 'poster', cover: 'poster',
|
|
41
42
|
slide: 'slides', deck: 'slides', ppt: 'slides', presentation: 'slides',
|
package/lib/handoff.js
CHANGED
|
@@ -199,6 +199,13 @@ export function handoffMd({ root, rows, kind, size, framework, dest, project, sy
|
|
|
199
199
|
lines.push(`- 框架:${fwLabel}`)
|
|
200
200
|
if (system) lines.push(`- 设计系统:${system}(设计用的是它的 tokens.css / components.css,项目里也照它)`)
|
|
201
201
|
lines.push(`- 生成时间:${now.toISOString().slice(0, 16).replace('T', ' ')}`, '')
|
|
202
|
+
if (kind === 'miniprogram') {
|
|
203
|
+
lines.push('## 小程序要点', '')
|
|
204
|
+
lines.push(`- 这是微信小程序的设计(${size[0]} 宽)。要做成真小程序:原生写 WXML / WXSS,或用 Taro、uni-app;框架选的「${fwLabel}」是做成网页的写法,用户要的是哪种先问清。`)
|
|
205
|
+
lines.push(`- 尺寸换成 rpx:设计稿 1px = ${Math.round((750 / size[0]) * 100) / 100}rpx(整屏宽 750rpx)。`)
|
|
206
|
+
lines.push('- 设计稿顶上画的状态栏不用做,微信自带;右上角的胶囊也是微信画的。导航栏用 app.json / 页面 json 的默认导航栏,或 `navigationStyle: custom` 自己画——自己画时状态栏高用 `wx.getWindowInfo().statusBarHeight`、导航栏高度和右边让开的宽度按 `wx.getMenuButtonBoundingClientRect()` 算(设计稿的 44 / 88 / 101px 只是预览手机的)。顶栏是深色的(预览里胶囊是白的),页面配置写 `navigationBarTextStyle: white`。')
|
|
207
|
+
lines.push('- 底部标签栏写进 app.json 的 `tabBar`(样式特殊就用自定义 tabBar),底部留出安全区 `env(safe-area-inset-bottom)`。', '')
|
|
208
|
+
}
|
|
202
209
|
|
|
203
210
|
lines.push('## 屏幕和路由', '', '| 屏 | 设计文件 | 建议路由 | 截图(handoff/shots/) | 跳到 |', '| --- | --- | --- | --- | --- |')
|
|
204
211
|
for (const r of rows) {
|
package/lib/index.js
CHANGED
|
@@ -117,7 +117,7 @@ export const previewTool = {
|
|
|
117
117
|
name: 'preview_page',
|
|
118
118
|
description:
|
|
119
119
|
'在右侧栏「设计工作台」里实时打开一个页面,用户能边看边点选要改的地方。' +
|
|
120
|
-
'做网页、App
|
|
120
|
+
'做网页、App 界面、小程序、海报、幻灯片、邮件这类界面设计时用:写好第一个 HTML 文件、或启动开发服务器后调一次就行,之后改文件会自动刷新,不用再调。' +
|
|
121
121
|
'只是把回答做成一页(对比、表格、清单、图表、计算器)给用户看或用时,有 show_page 工具就用它,不用这里。' +
|
|
122
122
|
'本地 HTML 写文件路径;开发服务器写地址或端口(如 http://localhost:5173)。只能预览本机页面。' +
|
|
123
123
|
'kind 说明是哪类设计,右侧按它的画幅显示(App 套手机外框、幻灯片 16:9 …)。' +
|
|
@@ -130,7 +130,7 @@ export const previewTool = {
|
|
|
130
130
|
kind: {
|
|
131
131
|
type: 'string',
|
|
132
132
|
enum: KINDS,
|
|
133
|
-
description: '设计类型:web 网页(默认)、app 手机 App 界面(390×844)、tablet 平板、poster 海报 / 社媒图(1080×1350)、slides 幻灯片(16:9)、email 邮件',
|
|
133
|
+
description: '设计类型:web 网页(默认)、app 手机 App 界面(390×844)、miniprogram 微信小程序(375×812,外框带右上角胶囊)、tablet 平板、poster 海报 / 社媒图(1080×1350)、slides 幻灯片(16:9)、email 邮件',
|
|
134
134
|
},
|
|
135
135
|
size: { type: 'string', description: '画幅尺寸,可选,如 1080x1920;不填按类型的默认尺寸' },
|
|
136
136
|
screens: {
|
|
@@ -244,7 +244,7 @@ export const checkTool = {
|
|
|
244
244
|
// 严重问题的局部截图(截不成就不附;附件库用不了的老版本 DSH 不截,清单照样给)
|
|
245
245
|
let crops = []
|
|
246
246
|
if (a.images !== false && ctx && typeof ctx.get === 'function' && ctx.get('attachments')) {
|
|
247
|
-
try { crops = await cropIssues(b, rows, { screens: pick.list, dark: !!a.dark }) } catch (e) { studio.log(e) }
|
|
247
|
+
try { crops = await cropIssues(b, rows, { screens: pick.list, dark: !!a.dark, kind }) } catch (e) { studio.log(e) }
|
|
248
248
|
}
|
|
249
249
|
const kept = crops.length ? await keepImages(ctx, id, crops) : null
|
|
250
250
|
if (kept && !kept.images) crops = [] // 存进附件库失败:不提图
|
|
@@ -368,9 +368,9 @@ export const lookTool = {
|
|
|
368
368
|
const at = ['top', 'bottom', 'full'].includes(a.at) ? a.at : 'top'
|
|
369
369
|
const pick = pickScreens(snap, a.screens, MAX_SHOTS)
|
|
370
370
|
if (!pick.list.length) return { text: `没找到要截的屏:${pick.missing.join('、')}。现在有:${(snap.screens || []).map((x) => x.key).join('、') || snap.focus}` }
|
|
371
|
-
const shots = await shootAll(b, { screens: pick.list, size: want, at, selector: a.selector, dark: !!a.dark, max: MAX_SHOTS })
|
|
371
|
+
const shots = await shootAll(b, { screens: pick.list, size: want, at, selector: a.selector, dark: !!a.dark, kind, max: MAX_SHOTS })
|
|
372
372
|
const good = shots.filter((s) => s.jpg).slice(0, MAX_SHOTS)
|
|
373
|
-
const text = formatShots(shots, { where: whereOf(snap), missing: pick.missing, more: pick.more })
|
|
373
|
+
const text = formatShots(shots, { where: whereOf(snap), missing: pick.missing, more: pick.more, kind })
|
|
374
374
|
if (!good.length) return { text }
|
|
375
375
|
const kept = await keepImages(ctx, id, good, true)
|
|
376
376
|
if (kept.images) return { text, images: kept.images }
|