dsh-design-studio 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.en.md +6 -4
  2. package/README.md +6 -4
  3. package/assets/demo.gif +0 -0
  4. package/assets/hero-sm.jpg +0 -0
  5. package/cordis.patch.yml +5 -0
  6. package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/_shots/menu.jpg +0 -0
  7. package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/app.css +64 -0
  8. package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/cart.html +120 -0
  9. package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/design.json +56 -0
  10. package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/menu.html +200 -0
  11. package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/order.html +135 -0
  12. package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/tokens.css +43 -0
  13. package/gallery-seed/d/orderg-x__autumn-sale/08e9e40834f9/_shots/email.jpg +0 -0
  14. package/gallery-seed/d/orderg-x__autumn-sale/08e9e40834f9/design.json +20 -0
  15. package/gallery-seed/d/orderg-x__autumn-sale/08e9e40834f9/email.html +268 -0
  16. package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/_shots/courses.jpg +0 -0
  17. package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/courses.html +364 -0
  18. package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/design.json +49 -0
  19. package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/lesson.html +196 -0
  20. package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/tokens.css +55 -0
  21. package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/ui.css +255 -0
  22. package/gallery-seed/d/orderg-x__moon-fair/08e9e40834f9/_shots/poster.jpg +0 -0
  23. package/gallery-seed/d/orderg-x__moon-fair/08e9e40834f9/design.json +21 -0
  24. package/gallery-seed/d/orderg-x__moon-fair/08e9e40834f9/poster.html +165 -0
  25. package/gallery-seed/d/orderg-x__moon-fair/08e9e40834f9/tokens.css +35 -0
  26. package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/_shots/home.jpg +0 -0
  27. package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/app.css +155 -0
  28. package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/booking.html +232 -0
  29. package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/design.json +57 -0
  30. package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/health.html +221 -0
  31. package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/home.html +225 -0
  32. package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/tokens.css +54 -0
  33. package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/_shots/overview.jpg +0 -0
  34. package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/app.css +79 -0
  35. package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/design.json +51 -0
  36. package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/entries.html +161 -0
  37. package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/icons.svg +30 -0
  38. package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/overview.html +181 -0
  39. package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/tokens.css +50 -0
  40. package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/_shots/home.jpg +0 -0
  41. package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/book.html +279 -0
  42. package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/design.json +21 -0
  43. package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/home.html +348 -0
  44. package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/style.css +297 -0
  45. package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/tokens.css +49 -0
  46. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/01-cover.html +133 -0
  47. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/02-highlights.html +67 -0
  48. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/03-growth.html +89 -0
  49. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/04-milestones.html +92 -0
  50. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/05-next.html +99 -0
  51. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/06-thanks.html +61 -0
  52. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/_shots/01-cover.jpg +0 -0
  53. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/deck.css +67 -0
  54. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/design.json +25 -0
  55. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/tokens.css +30 -0
  56. package/gallery-seed/index.json +1 -0
  57. package/gallery-seed/thumbs/orderg-x__alley-noodles.webp +0 -0
  58. package/gallery-seed/thumbs/orderg-x__autumn-sale.webp +0 -0
  59. package/gallery-seed/thumbs/orderg-x__little-painter.webp +0 -0
  60. package/gallery-seed/thumbs/orderg-x__moon-fair.webp +0 -0
  61. package/gallery-seed/thumbs/orderg-x__paw-diary.webp +0 -0
  62. package/gallery-seed/thumbs/orderg-x__pocket-ledger.webp +0 -0
  63. package/gallery-seed/thumbs/orderg-x__time-bookshop.webp +0 -0
  64. package/gallery-seed/thumbs/orderg-x__year-review.webp +0 -0
  65. package/lib/browser.js +23 -9
  66. package/lib/bundle.js +12 -3
  67. package/lib/client.js +3106 -111
  68. package/lib/core.js +44 -11
  69. package/lib/gallery/css.js +230 -0
  70. package/lib/gallery/html.js +313 -0
  71. package/lib/gallery/index.js +22 -0
  72. package/lib/gallery/market-routes.js +306 -0
  73. package/lib/gallery/market.js +623 -0
  74. package/lib/gallery/pack.js +311 -0
  75. package/lib/gallery/policy.js +1560 -0
  76. package/lib/gallery/qrfind.js +181 -0
  77. package/lib/gallery/render.js +148 -0
  78. package/lib/gallery/routes.js +360 -0
  79. package/lib/gallery/tar.js +118 -0
  80. package/lib/gallery/use.js +125 -0
  81. package/lib/gallery/zip.js +212 -0
  82. package/lib/handoff.js +291 -29
  83. package/lib/history.js +3 -1
  84. package/lib/index.js +118 -24
  85. package/lib/inspire.js +142 -0
  86. package/lib/offline.js +17 -0
  87. package/lib/picker.js +425 -32
  88. package/lib/serve.js +88 -11
  89. package/lib/works.js +238 -0
  90. package/locale/en.json +2 -1
  91. package/locale/offline/en.json +6 -0
  92. package/locale/offline/zh.json +6 -0
  93. package/locale/zh.json +2 -1
  94. package/package.json +10 -3
  95. package/skills/ui-design/SKILL.md +9 -0
package/lib/handoff.js CHANGED
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import fs from 'node:fs'
13
+ import os from 'node:os'
13
14
  import path from 'node:path'
14
15
  import { pathToFileURL } from 'node:url'
15
16
  import { prepare } from './look.js'
@@ -26,26 +27,135 @@ export const FRAMEWORKS = {
26
27
  vue: { label: 'Vue 3 + Vite(TypeScript)', create: 'npm create vite@latest {name} -- --template vue-ts', router: 'vue-router' },
27
28
  next: { label: 'Next.js(App Router,TypeScript)', create: 'npx create-next-app@latest {name} --ts --app --use-npm', router: '' },
28
29
  html: { label: '纯 HTML / CSS(不用框架)', create: '', router: '' },
30
+ // 小程序:uni-app 一份代码出微信小程序和网页版(网页版能用 compare_design 比);原生写 WXML / WXSS(没有脚手架命令,手搭骨架)
31
+ uniapp: { label: 'uni-app(Vue 3 + Vite,TypeScript;出微信小程序和网页版)', create: 'npx degit dcloudio/uni-preset-vue#vite-ts {name}', router: '' },
32
+ weapp: { label: '微信原生小程序(WXML / WXSS)', create: '', router: '' },
29
33
  }
30
34
 
31
- const FW_DEPS = [['next', 'next', 'Next.js'], ['nuxt', 'vue', 'Nuxt'], ['@sveltejs/kit', 'svelte', 'SvelteKit'], ['svelte', 'svelte', 'Svelte'], ['astro', 'astro', 'Astro'], ['@angular/core', 'angular', 'Angular'], ['solid-js', 'solid', 'Solid'], ['vue', 'vue', 'Vue'], ['react', 'react', 'React']]
35
+ /** 做成小程序的写法(交接说明按小程序的规矩写:rpx、page 上的变量、view / text、pages.json / app.json 登记页面) */
36
+ const MINI = new Set(['uniapp', 'weapp', 'taro'])
32
37
 
33
- /** 目录是不是个前端项目、用的什么:{ dir, pkg, name, framework, label, tailwind, ts, pm } */
38
+ const FW_DEPS = [['@dcloudio/uni-app', 'uniapp', 'uni-app'], ['@tarojs/taro', 'taro', 'Taro'], ['next', 'next', 'Next.js'], ['nuxt', 'vue', 'Nuxt'], ['@sveltejs/kit', 'svelte', 'SvelteKit'], ['svelte', 'svelte', 'Svelte'], ['astro', 'astro', 'Astro'], ['@angular/core', 'angular', 'Angular'], ['solid-js', 'solid', 'Solid'], ['@builder.io/qwik', 'qwik', 'Qwik'], ['ember-source', 'ember', 'Ember'], ['preact', 'preact', 'Preact'], ['lit', 'lit', 'Lit'], ['vue', 'vue', 'Vue'], ['react-native', 'react-native', 'React Native'], ['react', 'react', 'React']]
39
+
40
+ // 文件夹里只有这些就算空的
41
+ const JUNK = /^\.(DS_Store|git|gitignore)$|^(Thumbs\.db|desktop\.ini)$/i
42
+
43
+ /** 文件夹里有没有正经东西(不存在 / 读不了算没有) */
44
+ function hasStuff(dir) {
45
+ try { return fs.readdirSync(dir).some((f) => !JUNK.test(f)) } catch { return false }
46
+ }
47
+
48
+ // 有这些就算网页入口(没有 package.json 的纯 HTML 站点也算个项目)
49
+ const HTML_ENTRIES = ['index.html', 'src/index.html', 'public/index.html', 'app/index.html']
50
+
51
+ /** workspaces 列表里有本目录以外的(只写 '.' 的不算 monorepo;'!' 开头是排除) */
52
+ const listsOthers = (xs) => xs.some((x) => typeof x === 'string' && !/^!|^\.\/?$/.test(x.trim()))
53
+
54
+ /** 读文本:去掉 BOM、换行统一成 \n(Windows 检出的文件常是 CRLF);读不了返回 null */
55
+ function readText(file) {
56
+ try { return fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n') } catch { return null }
57
+ }
58
+
59
+ /** 像是 monorepo 根目录:package.json 的 workspaces、lerna / rush / Nx 集成式(根上没有 project.json)、pnpm-workspace.yaml 的 packages 里列了别的包(只用来提醒模型放进子包,不拿来改放哪) */
60
+ function monoRoot(dir, pkg) {
61
+ const ws = pkg && pkg.workspaces
62
+ const list = Array.isArray(ws) ? ws : ws && Array.isArray(ws.packages) ? ws.packages : null
63
+ if (list && listsOthers(list)) return true
64
+ const has = (f) => fs.existsSync(path.join(dir, f))
65
+ if (has('lerna.json') || has('rush.json') || (has('nx.json') && !has('project.json'))) return true
66
+ let y = readText(path.join(dir, 'pnpm-workspace.yaml'))
67
+ if (y === null) return false
68
+ y = y.replace(/#.*$/gm, '')
69
+ // packages: ['a/*', 'b'] 或者下面一行一个 - a/*
70
+ const m = y.match(/^packages[ \t]*:(.*(?:\n(?:[ \t]+.*|-.*|[ \t]*))*)/m)
71
+ return !!m && listsOthers([...m[1].matchAll(/['"]?([^'"\s,[\]-][^'"\s,[\]]*)['"]?/g)].map((x) => x[1]))
72
+ }
73
+
74
+ /** 读 JSON 对象:读不了、写坏了、不是对象都返回 null */
75
+ function readObj(file) {
76
+ try { const o = JSON.parse(readText(file)); return o && typeof o === 'object' && !Array.isArray(o) ? o : null } catch { return null }
77
+ }
78
+
79
+ /**
80
+ * 没有 package.json 也认得出的小程序项目:
81
+ * HBuilderX 建的 uni-app(根目录有 manifest.json 和写了 pages 的 pages.json;manifest.json 里 vueVersion 是 3 就是 Vue 3,没写是 Vue 2)、
82
+ * 微信原生小程序(project.config.json,miniprogramRoot 指的目录(默认根目录)里有 app.json)
83
+ */
84
+ /** Taro 的源码目录:config/index 里写死的 sourceRoot(只认项目里面的相对目录),没写或认不出就是默认的 src/ */
85
+ function taroRoot(dir) {
86
+ for (const f of ['index.ts', 'index.js', 'index.mjs', 'index.cjs']) {
87
+ const m = /\bsourceRoot\s*:\s*(['"`])([^'"`$\n]+)\1/.exec(readText(path.join(dir, 'config', f)) || '')
88
+ if (!m) continue
89
+ const r = m[2].trim().replace(/\\/g, '/').replace(/^(\.\/)+|\/+$/g, '')
90
+ return r && r !== '.' && !/(^|\/)\.\.(\/|$)|^\/|^[a-z]:/i.test(r) ? r + '/' : 'src/'
91
+ }
92
+ return 'src/'
93
+ }
94
+
95
+ function miniOf(dir) {
96
+ if (!dir) return null
97
+ const pages = readText(path.join(dir, 'pages.json'))
98
+ if (pages !== null && /"pages"\s*:/.test(pages) && fs.existsSync(path.join(dir, 'manifest.json'))) {
99
+ const v3 = /"vueVersion"\s*:\s*"?3/.test(readText(path.join(dir, 'manifest.json')) || '')
100
+ return { framework: 'uniapp', label: `uni-app(HBuilderX,Vue ${v3 ? 3 : 2})`, hbx: true, pagesAt: '' }
101
+ }
102
+ const cfg = readObj(path.join(dir, 'project.config.json'))
103
+ if (!cfg) return null
104
+ // miniprogramRoot 只认项目里面的相对目录('miniprogram/'),别的当根目录
105
+ const sub = typeof cfg.miniprogramRoot === 'string' && !/(^|[\\/])\.\.([\\/]|$)|^[\\/]|^[a-z]:/i.test(cfg.miniprogramRoot) ? cfg.miniprogramRoot.replace(/\\/g, '/').replace(/^(\.\/)+|\/+$/g, '') : ''
106
+ if (fs.existsSync(path.join(dir, sub, 'app.json'))) return { framework: 'weapp', label: '微信原生小程序', hbx: false, pagesAt: sub ? sub + '/' : '' }
107
+ if (sub && fs.existsSync(path.join(dir, 'app.json'))) return { framework: 'weapp', label: '微信原生小程序', hbx: false, pagesAt: '' }
108
+ return null
109
+ }
110
+
111
+ /**
112
+ * 目录是不是个项目、用的什么:{ dir, pkg, site(有 index.html 入口), empty(不存在或没正经东西), mono(像 monorepo 根目录), name, framework, label, tailwind, ts, pm }
113
+ * 后端、monorepo 根目录、只用 CDN 的纯 HTML 站点也都算项目:是不是前端、页面该放进哪个子包,交接说明里让写代码的模型看清结构自己定(猜不准)
114
+ */
34
115
  export function detectProject(dir) {
35
- const out = { dir, pkg: false, name: path.basename(dir || ''), framework: '', label: '', tailwind: false, ts: false, pm: 'npm' }
36
- let pkg
37
- try { pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')) } catch { return out }
116
+ const out = { dir, pkg: false, site: false, empty: true, mono: false, name: path.basename(dir || ''), framework: '', label: '', tailwind: false, ts: false, pm: 'npm', hbx: false, pagesAt: '' }
117
+ out.site = !!dir && HTML_ENTRIES.some((f) => fs.existsSync(path.join(dir, f)))
118
+ out.empty = !dir || !hasStuff(dir)
119
+ const mini = miniOf(dir)
120
+ const pkg = dir ? readObj(path.join(dir, 'package.json')) : null
121
+ if (!pkg) {
122
+ if (mini) Object.assign(out, mini, { ts: fs.existsSync(path.join(dir, 'tsconfig.json')) })
123
+ return out
124
+ }
38
125
  out.pkg = true
39
126
  if (typeof pkg.name === 'string' && pkg.name) out.name = pkg.name
40
127
  const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) }
41
128
  const hit = FW_DEPS.find(([d]) => deps[d])
42
- if (hit) { out.framework = hit[1]; out.label = hit[2] + (hit[0] === 'react' && deps.vite ? ' + Vite' : hit[0] === 'vue' && deps.vite ? ' + Vite' : '') }
129
+ // HBuilderX 项目、原生小程序也常有 package.json(装 uView、Vant Weapp 这些):没写 uni-app / Taro 的依赖就按目录结构认
130
+ if (mini && !(hit && MINI.has(hit[1]))) Object.assign(out, mini)
131
+ else if (hit && hit[1] === 'uniapp') Object.assign(out, { framework: 'uniapp', label: `uni-app(Vue ${/^\D*2\./.test(String(deps.vue || '')) ? 2 : 3})`, pagesAt: fs.existsSync(path.join(dir, 'src', 'pages.json')) ? 'src/' : '' })
132
+ else if (hit && hit[1] === 'taro') Object.assign(out, { framework: 'taro', label: 'Taro', pagesAt: taroRoot(dir) })
133
+ else if (hit) { out.framework = hit[1]; out.label = hit[2] + (hit[0] === 'react' && deps.vite ? ' + Vite' : hit[0] === 'vue' && deps.vite ? ' + Vite' : '') }
43
134
  out.tailwind = !!deps.tailwindcss
135
+ out.mono = monoRoot(dir, pkg)
44
136
  out.ts = !!deps.typescript || fs.existsSync(path.join(dir, 'tsconfig.json'))
45
137
  out.pm = fs.existsSync(path.join(dir, 'pnpm-lock.yaml')) ? 'pnpm' : fs.existsSync(path.join(dir, 'yarn.lock')) ? 'yarn' : fs.existsSync(path.join(dir, 'bun.lockb')) || fs.existsSync(path.join(dir, 'bun.lock')) ? 'bun' : 'npm'
46
138
  return out
47
139
  }
48
140
 
141
+ /** 是个项目(不用往里面再套一层):有 package.json、index.html 入口、.git,或是认得出的小程序(后端、monorepo 根目录、刚建只有 README 的仓库都算) */
142
+ function isProject(dir) {
143
+ return ['package.json', '.git', ...HTML_ENTRIES].some((f) => fs.existsSync(path.join(dir, f))) || !!miniOf(dir)
144
+ }
145
+
146
+ /** 认得出的现有项目:有 package.json,或者是没有 package.json 的小程序(HBuilderX 的 uni-app、微信原生) */
147
+ export const isCode = (p) => !!(p && (p.pkg || p.framework))
148
+
149
+ /**
150
+ * 不指定框架时用什么:认得出框架的项目就跟它一样,纯 HTML 站点(没有 package.json)就纯 HTML,
151
+ * 别的:小程序的设计用 uni-app(能出微信小程序,网页版还能逐像素比),其余用 React(右侧栏和 handoff_design 都按这个)
152
+ */
153
+ export function defaultFramework(project, kind = '') {
154
+ if (project && project.framework) return 'same'
155
+ if (project && project.site && !project.pkg) return 'html'
156
+ return kind === 'miniprogram' ? 'uniapp' : 'react'
157
+ }
158
+
49
159
  /** 真实路径:不存在的部分接在最近一级存在的上级目录的真实路径后面(新建的目录也能按真实位置比) */
50
160
  export function realish(p) {
51
161
  let cur = path.resolve(p)
@@ -70,16 +180,56 @@ export function within(p, root) {
70
180
  }
71
181
 
72
182
  /**
73
- * 代码默认放哪:会话目录是个前端项目、又不在设计目录里 → 就放会话目录;
183
+ * 代码放进 dest 时算哪个项目:dest 自己就是项目就用它;不是、但已经在某个项目里面(比如选了它的 src/pages)→ 用往上最近的那个
184
+ * (往上找到家目录为止,家目录本身不算:有人在家目录放 package.json;还不存在的目录不往上找:那是要新建的项目)
185
+ * dest 里有 index.html 时:上面的项目认得出框架才算它的(Angular、webpack 项目的 src/index.html);
186
+ * 认不出的(Node 库仓库里的 docs/ 静态站点)还是当纯 HTML 站点
187
+ */
188
+ export function projectFor(dest) {
189
+ const own = detectProject(dest)
190
+ if (isCode(own) || !fs.existsSync(dest)) return own
191
+ const home = fold(realish(os.homedir()))
192
+ for (let cur = path.dirname(realish(dest)); fold(cur) !== home && path.dirname(cur) !== cur; cur = path.dirname(cur)) {
193
+ if (!fs.existsSync(path.join(cur, 'package.json')) && !miniOf(cur)) continue
194
+ const p = detectProject(cur)
195
+ if (isCode(p)) return own.site && !p.framework ? own : p
196
+ }
197
+ return own
198
+ }
199
+
200
+ /**
201
+ * 代码默认放哪:会话目录是个项目(有 package.json,或是认得出的小程序)、又不在设计目录里 → 就放会话目录;
74
202
  * 否则新建 <设计名>-app:设计目录在会话目录下就建在会话目录里,不然建在设计目录旁边(别塞进设计目录)
75
203
  */
76
204
  export function defaultDest(root, cwd, project) {
77
205
  const inside = !!root && within(cwd, root)
78
- if (project && project.pkg && !inside) return cwd
79
- const base = (root ? path.basename(root) : 'app').replace(/[^\w-]+/g, '-').replace(/^-+|-+$/g, '').toLowerCase() || 'app'
80
- const name = base === 'app' || base.endsWith('-app') ? `${base}-code` : `${base}-app`
206
+ if (isCode(project) && !inside) return cwd
81
207
  const under = !!root && !inside && within(root, cwd)
82
- return path.join(root && !under ? path.dirname(root) : cwd, name)
208
+ return path.join(root && !under ? path.dirname(root) : cwd, appDirName(root))
209
+ }
210
+
211
+ /** 新建的代码目录叫什么:<设计名>-app */
212
+ export function appDirName(root) {
213
+ const base = (root ? path.basename(root) : 'app').replace(/[^\w-]+/g, '-').replace(/^-+|-+$/g, '').toLowerCase() || 'app'
214
+ return base === 'app' || base.endsWith('-app') ? `${base}-code` : `${base}-app`
215
+ }
216
+
217
+ /**
218
+ * 用户在右侧栏选了个文件夹 → { dest, why }:
219
+ * 是个项目(有 package.json、index.html 或 .git,后端、monorepo 根目录也算)、在某个项目里面(比如它的 src/pages)、空文件夹、还不存在、
220
+ * 或者本来就叫 <设计名>-app(上回放过的)→ 就放这里(why 空);
221
+ * 别的有东西的文件夹(比如 ~/Projects)→ 在它里面新建 <设计名>-app(why 'nested');
222
+ * 选到设计目录里面了 → 放到设计目录旁边(why 'beside':设计稿要留着做对比,handoff_design 也不收设计目录里的地方)
223
+ */
224
+ export function pickedDest(dir, root) {
225
+ const name = appDirName(root)
226
+ const beside = { dest: path.join(path.dirname(root || dir), name), why: 'beside' }
227
+ if (root && within(dir, root)) return beside
228
+ let out = { dest: dir, why: '' }
229
+ if (!isProject(dir) && !isCode(projectFor(dir)) && path.basename(dir).toLowerCase() !== name) {
230
+ if (hasStuff(dir)) out = { dest: path.join(dir, name), why: 'nested' } // 还不存在 / 读不了 / 空的:就用它
231
+ }
232
+ return root && within(out.dest, root) ? beside : out
83
233
  }
84
234
 
85
235
  /** 文件名 → 建议路由:index / home → /,别的 /名字 */
@@ -88,6 +238,13 @@ export function routeOf(key) {
88
238
  return b === 'index' || b === 'home' ? '/' : '/' + b.replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '')
89
239
  }
90
240
 
241
+ /** 文件名 → 小程序的页面路径(登记进 pages.json / app.json 的,相对它们所在的目录):index / home → pages/index/index,别的 pages/名字/名字(Taro 是 pages/名字/index) */
242
+ export function pagePathOf(key, target = 'uniapp') {
243
+ const r = routeOf(key)
244
+ const b = r === '/' ? 'index' : r.slice(1) || 'page'
245
+ return `pages/${b}/${target === 'taro' ? 'index' : b}`
246
+ }
247
+
91
248
  const CLASS_TAGS = /^(header|nav|footer|aside|section|div|a|button|ul|ol|li|form|article|main|figure)$/
92
249
 
93
250
  /** 几屏共用的 class(≥ 2 屏都有,多半该抽成组件):[{ cls, n, screens }] */
@@ -186,25 +343,85 @@ export function handoffMd({ root, rows, kind, size, framework, dest, project, sy
186
343
  const fwLabel = same ? `${project.label}(跟项目一样)` : fw ? fw.label : framework
187
344
  const name = path.basename(root)
188
345
  const keys = rows.map((r) => r.key)
189
- const route = (k) => routeOf(k)
346
+ // 做成什么:跟项目一样就是项目的框架;小程序的写法(uni-app / 原生 / Taro)按小程序的规矩写
347
+ const target = framework === 'same' ? (project && project.framework) || '' : framework
348
+ const uni = target === 'uniapp'
349
+ const wx = target === 'weapp'
350
+ const taro = target === 'taro'
351
+ const mini = MINI.has(target)
352
+ const known = isCode(project)
353
+ // 目标目录本来就是这种小程序项目(往里加页面,不新建)
354
+ const hasMini = mini && !!project && project.framework === target
355
+ // 目标目录是别的前端项目(React / Vue 网页、别种小程序):不往里硬加,在里面另建子项目
356
+ const mismatch = mini && known && !hasMini && !!project.framework
357
+ // 页面、pages.json 在哪个目录:现有项目照它(命令行建的 uni-app、Taro 在 src/);新建的 uni-app 模板也在 src/
358
+ // Taro 照项目 config/index 的 sourceRoot(默认 src/)
359
+ const at = hasMini ? project.pagesAt || (taro ? 'src/' : '') : uni ? 'src/' : ''
360
+ const cfgFile = uni ? `${at}pages.json` : wx ? `${at}app.json` : `${at}app.config`
361
+ const api = uni ? 'uni' : taro ? 'Taro' : 'wx'
362
+ const route = (k) => (mini ? pagePathOf(k, target) : routeOf(k))
190
363
  const keyOfLink = (p) => String(p || '').replace(/^\/+/, '').split(/[?#]/)[0]
191
364
  const lines = []
192
365
  lines.push(`# 交接:${name} → ${fwLabel}`, '')
193
366
  lines.push(`> 给写代码的模型看:照这份把设计稿做成真页面。设计稿就是源码——每屏一个 HTML,样式在 tokens.css 等样式表里。先读源文件,截图只用来对照,别凭截图猜数值。`, '')
194
367
  lines.push('## 做什么', '')
195
368
  lines.push(`- 设计目录:\`${root}\`(${rows.length} 屏${more ? `,另有 ${more} 屏没列进来` : ''},${kind} ${size[0]}×${size[1]})`)
196
- if (project && project.pkg) lines.push(`- 做到:\`${dest || project.dir}\`(现有项目 ${project.name}:${project.label || '没认出框架'}${project.tailwind ? ' + Tailwind' : ''}${project.ts ? ',TypeScript' : ''},包管理 ${project.pm})`)
369
+ // 目录里已经有别的东西、又不是 Node 项目(别的语言的仓库、后端……):别在原地或旁边跑脚手架
370
+ // (纯 HTML 站点再做成纯 HTML 不算:就加在这个站点里,下面第 1 步提醒别覆盖现有页面)
371
+ const occupied = !!(project && !known && project.empty === false) && !(project.site && framework === 'html')
372
+ if (known) lines.push(`- 做到:\`${dest || project.dir}\`(现有项目 ${project.name}:${project.label || '没认出框架'}${project.tailwind ? ' + Tailwind' : ''}${project.ts ? ',TypeScript' : ''}${project.pkg ? `,包管理 ${project.pm}` : ',没有 package.json'})`)
373
+ else if (occupied) lines.push(`- 做到:\`${dest}\`(目录里已经有别的东西,不是 Node 项目:先看清结构,前端放进它里面新的子目录)`)
197
374
  else if (fw && fw.create) lines.push(`- 做到:\`${dest}\`(新建项目:\`${fw.create.replace('{name}', path.basename(dest || name + '-app'))}\`,在 \`${path.dirname(dest || root)}\` 里跑)`)
198
375
  else lines.push(`- 做到:\`${dest}\``)
199
376
  lines.push(`- 框架:${fwLabel}`)
200
377
  if (system) lines.push(`- 设计系统:${system}(设计用的是它的 tokens.css / components.css,项目里也照它)`)
201
378
  lines.push(`- 生成时间:${now.toISOString().slice(0, 16).replace('T', ' ')}`, '')
202
- if (kind === 'miniprogram') {
379
+ if (kind === 'miniprogram' && !mini) {
380
+ // 小程序的设计、选的是网页框架:做成手机网页,别混进小程序的写法
381
+ lines.push('## 小程序设计做成网页(H5)', '')
382
+ lines.push(`- 这是微信小程序的设计(${size[0]} 宽)。框架选的「${fwLabel}」,就做成手机网页:样式照设计稿写 px(宽 ${size[0]} 的移动端页面,带 \`<meta name="viewport" content="width=device-width, initial-scale=1">\`),不用 rpx、不用 \`wx.*\`。用户其实要真小程序的话,用 handoff_design 的 framework: uniapp(uni-app)或 weapp(原生)重新出交接说明。`)
383
+ lines.push('- 设计稿顶上画的状态栏、右上角的胶囊是微信自己画的,网页里不做;导航栏做成页面自己的顶栏。底部标签栏做成固定在底部的导航,留出安全区 `env(safe-area-inset-bottom)`。', '')
384
+ }
385
+ if (mini) {
386
+ const cfg = uni ? `${cfgFile} 的 \`globalStyle\` / 页面的 \`style\`` : taro ? `\`${cfgFile}\` / 页面的 \`.config\`` : `${cfgFile} / 页面 json`
203
387
  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)`。', '')
388
+ lines.push(taro
389
+ ? `- 尺寸:Taro 会自己把样式里的 px 换成 rpx,按 \`config/index\` 的 \`designWidth\` 算(默认 750)。别改项目的 designWidth(旧页面会跟着变大变小):照它换算——designWidth 是 750 时设计稿 1px 写 ${Math.round((750 / size[0]) * 100) / 100}px,是 ${size[0]} 就照设计稿写。`
390
+ : `- 尺寸换成 rpx:设计稿 1px = ${Math.round((750 / size[0]) * 100) / 100}rpx(整屏宽 750rpx)。`)
391
+ if (kind === 'miniprogram') {
392
+ // 胶囊位置只有小程序里拿得到;uni-app、Taro 的网页版(拿来逐像素比的)要分开写
393
+ const h5Nav = uni ? '(`getMenuButtonBoundingClientRect` 网页版没有:包在 `// #ifdef MP-WEIXIN` … `// #endif` 里,网页版走 `// #ifdef H5`,顶栏高按 44px、状态栏按 0)'
394
+ : taro ? '(`getMenuButtonBoundingClientRect` 网页版拿不到:用 `process.env.TARO_ENV === \'weapp\'` 分开,网页版顶栏高按 44px、状态栏按 0)' : ''
395
+ lines.push(`- 设计稿顶上画的状态栏不用做,微信自带;右上角的胶囊也是微信画的。导航栏用 ${cfg} 里的默认导航栏,或 \`navigationStyle: custom\` 自己画——自己画时状态栏高用 \`${api}.getWindowInfo().statusBarHeight\`、导航栏高度和右边让开的宽度按 \`${api}.getMenuButtonBoundingClientRect()\` 算(设计稿的 44 / 88 / 101px 只是预览手机的)${h5Nav}。顶栏是深色的(预览里胶囊是白的),页面配置写 \`navigationBarTextStyle: white\`。`)
396
+ lines.push(`- 底部标签栏写进 \`${cfgFile}\` 的 \`tabBar\`(样式特殊就用自定义 tabBar),底部留出安全区 \`env(safe-area-inset-bottom)\`。`)
397
+ }
398
+ if (mismatch) lines.push(`- 目标目录 \`${dest || project.dir}\` 是 ${project.label} 项目,不是 ${fwLabel}:按下面第 1 步在里面另建一个子项目,下面说的路径都相对那个子项目。`)
399
+ lines.push('')
400
+ }
401
+ if (uni) {
402
+ lines.push('## 做成 uni-app', '')
403
+ lines.push(`- 页面:每屏一个 \`${at}pages/<名字>/<名字>.vue\`,在 \`${cfgFile}\` 的 \`pages\` 里登记(第一项是首页),标题、导航栏颜色写在那一页的 \`style\` 里。下面表里的「页面路径」就是要登记的 path。`)
404
+ lines.push('- 标签换成 uni-app 的组件:`div` → `view`,文字 → `text`,`img` → `image`(照设计的裁切写 `mode`,比如 `aspectFill`),输入框、按钮用同名组件;点击写 `@tap`;跳转用 `uni.navigateTo`(标签页 `uni.switchTab`),别用 `<a>` 和 vue-router。')
405
+ lines.push('- 设计变量放进 `App.vue` 的全局样式,选择器写 `page`(小程序里没有 `:root`,网页版也认 `page`):`page { --color-primary: …; }`,各处用 `var(--…)`。')
406
+ lines.push('- 别用 `window`、`document`、`localStorage`、`fetch`:换成 `uni.getStorageSync`、`uni.request` 这些。')
407
+ if (hasMini) lines.push(`- 先看清项目现有的写法再动手:Vue ${/Vue 2/.test(project.label) ? '2(选项式)' : '3(`<script setup>` 还是选项式)'}、用没用 uni-ui / uView 这些组件库、\`uni.scss\` 里有没有现成的变量,照它来。`)
408
+ if (hasMini && project.hbx) lines.push(project.pkg ? '- 这是 HBuilderX 建的项目:别跑 npm 脚手架;要的依赖照现有 package.json 装,跑起来让用户在 HBuilderX 里点「运行」。' : '- 这是 HBuilderX 建的项目(没有 package.json):别跑 npm 脚手架、别装依赖;跑起来让用户在 HBuilderX 里点「运行」。')
409
+ lines.push('')
410
+ }
411
+ if (wx) {
412
+ lines.push('## 做成原生小程序', '')
413
+ lines.push(`- 页面:每屏一个目录 \`${at}pages/<名字>/\`,四个文件 \`<名字>.wxml / .wxss / .js / .json\`,在 \`${cfgFile}\` 的 \`pages\` 里登记(第一项是首页);公共样式写 \`${at}app.wxss\`。下面表里的「页面路径」就是要登记的。`)
414
+ if (!hasMini) lines.push('- 这里还没有小程序:先手搭最小骨架——`app.json`、`app.js`(`App({})`)、`app.wxss`、`sitemap.json`,`project.config.json` 里写 `"compileType": "miniprogram"`、`appid` 用户有就填,没有先写 `touristappid`(游客模式)。别找 npm 脚手架。')
415
+ lines.push('- HTML 换成 WXML:`div` → `view`,文字 → `text`,`img` → `image`(照设计的裁切写 `mode`),点击 `bindtap`,列表 `wx:for`,条件 `wx:if`;跳转 `wx.navigateTo`(标签页 `wx.switchTab`);页面数据放 `Page({ data })`,用 `this.setData` 改。设计稿里的脚本照这个改写。')
416
+ lines.push(`- 设计变量写在 \`${at}app.wxss\` 的 \`page { … }\` 上(WXSS 没有 \`:root\`),各页用 \`var(--…)\`。`)
417
+ lines.push('- WXSS 只用常见的选择器(class、id、标签、后代 / 子代、`::before` / `::after`);`*` 通配、属性选择器、`:hover` 别用——按下的样式用组件的 `hover-class`。')
418
+ lines.push('')
419
+ }
420
+ if (taro) {
421
+ lines.push('## 做成 Taro', '')
422
+ lines.push(`- 源码目录是 \`${at}\`(项目 \`config/index\` 里的 \`sourceRoot\`${at === 'src/' ? ',默认 src;那里写的不是 src 就照它' : ''})。页面放 \`${at}pages/<名字>/index\`(\`.tsx\` 还是 \`.vue\` 看项目用 React 还是 Vue),在 \`${cfgFile}\`(\`.ts\` 或 \`.js\`,看项目)的 \`pages\` 里登记;组件用 \`@tarojs/components\` 的 \`View\`、\`Text\`、\`Image\`;跳转 \`Taro.navigateTo\`(标签页 \`Taro.switchTab\`)。`)
423
+ lines.push(`- 设计变量写在 \`${at}app\` 的全局样式里,选择器写 \`page, .taro_page { … }\`:小程序认 \`page\`(没有 \`:root\`),网页版的页面容器是 \`.taro_page\`(\`page\` 在网页版不生效);各处用 \`var(--…)\`。先看清项目现有的写法、组件库(Taro UI、NutUI 之类)再动手。`)
424
+ lines.push('')
208
425
  }
209
426
 
210
427
  if (kind === 'desktop') {
@@ -214,7 +431,7 @@ export function handoffMd({ root, rows, kind, size, framework, dest, project, sy
214
431
  lines.push('- 整个窗口不滚,侧栏、列表、详情各自滚(`overflow: auto`);最小窗口尺寸照设计里最小的那种(多尺寸里的 1024×680)设。', '')
215
432
  }
216
433
 
217
- lines.push('## 屏幕和路由', '', '| 屏 | 设计文件 | 建议路由 | 截图(handoff/shots/) | 跳到 |', '| --- | --- | --- | --- | --- |')
434
+ lines.push(mini ? '## 屏幕和页面' : '## 屏幕和路由', '', `| 屏 | 设计文件 | ${mini ? '页面路径' : '建议路由'} | 截图(handoff/shots/) | 跳到 |`, '| --- | --- | --- | --- | --- |')
218
435
  for (const r of rows) {
219
436
  const to = [...new Set((r.links || []).map((l) => keyOfLink(l.path)).filter((k) => k && k !== r.key))]
220
437
  const shots = r.error ? `没截成(${r.error})` : [...r.shots, r.mobile].filter(Boolean).join('、') + (r.more ? `(还有 ${r.more} 段没截)` : '')
@@ -231,7 +448,9 @@ export function handoffMd({ root, rows, kind, size, framework, dest, project, sy
231
448
  if (tn.length) {
232
449
  const colors = tn.filter((n) => /^--colou?r-/.test(n))
233
450
  lines.push(`\`tokens.css\` 里 ${tn.length} 个变量(颜色 ${colors.length} 个:${colors.slice(0, 10).map((n) => `\`${n}: ${tok[n]}\``).join('、')}${colors.length > 10 ? ' …' : ''})。`, '')
234
- lines.push(project && project.tailwind
451
+ lines.push(mini
452
+ ? `- 把 tokens.css 里的变量搬进${uni ? ' `App.vue` 全局样式' : wx ? ` \`${at}app.wxss\`` : '全局样式'}的 \`${taro ? 'page, .taro_page' : 'page'} { … }\`(小程序没有 \`:root\`);组件样式里一律用 \`var(--…)\`,别写死色值、字号。`
453
+ : project && project.tailwind
235
454
  ? '- 把 tokens.css 原样放进项目的全局样式并在入口引入;Tailwind 的 theme.extend 里把颜色、圆角、字号、阴影映射到这些变量(如 `primary: \'var(--color-primary)\'`),类名里别写死色值。'
236
455
  : '- 把 tokens.css 原样放进项目的全局样式(如 `src/styles/tokens.css`)并在入口引入;组件样式里一律用 `var(--…)`,别写死色值、字号。')
237
456
  } else if (hasTokens) lines.push('- `tokens.css` 的第一段 `:root` 里没读到变量:先打开看看,把各屏用到的颜色、字号收成 `:root` 上的变量再动手。')
@@ -243,12 +462,19 @@ export function handoffMd({ root, rows, kind, size, framework, dest, project, sy
243
462
  // 共用部分
244
463
  const parts = sharedParts(htmlByKey)
245
464
  lines.push('## 几屏共用的部分(做成组件)', '')
246
- if (parts.length) for (const p of parts) lines.push(`- \`.${p.cls}\`:${p.n} 屏都有(${p.screens.map((k) => k.replace(/\.html?$/i, '')).join('、')})→ \`${pascal(p.cls)}\` 组件`)
465
+ // 小程序设计的骨架(技能里的 .mp-status / .mp-nav / .mp-tabbar …)是在模拟微信自己画的东西:照微信的做法,别做成组件
466
+ const tabCfg = `\`${cfgFile}\``
467
+ const chrome = kind !== 'miniprogram' ? {} : mini
468
+ ? { 'mp-status': '预览手机的状态栏,不做(微信自带)', 'mp-top': '状态栏 + 导航栏的外壳:照上面「小程序要点」用默认导航栏,或自己画导航', 'mp-nav': '导航栏:纯色底就用默认导航栏(标题、颜色写进页面配置);渐变、图片、搜索框这种才做自定义导航组件', 'mp-back': '返回按钮:默认导航栏自带', 'mp-tabbar': `底部标签栏:写进 ${tabCfg} 的 \`tabBar\`(样式特殊才做自定义 tabBar)` }
469
+ : { 'mp-status': '预览手机的状态栏,网页里不做', 'mp-top': '→ 页面顶栏组件', 'mp-tabbar': '→ 固定在底部的导航组件' }
470
+ if (parts.length) for (const p of parts) lines.push(chrome[p.cls] ? `- \`.${p.cls}\`:${chrome[p.cls]}` : `- \`.${p.cls}\`:${p.n} 屏都有(${p.screens.map((k) => k.replace(/\.html?$/i, '')).join('、')})→ \`${pascal(p.cls)}\` 组件`)
247
471
  else lines.push('- 没找到几屏共用的结构;按页面拆就行。')
248
472
  if (fs.existsSync(path.join(root, 'icons.svg'))) {
249
473
  let n = 0
250
474
  try { n = (fs.readFileSync(path.join(root, 'icons.svg'), 'utf8').match(/<symbol\b/g) || []).length } catch { n = 0 }
251
- lines.push(`- \`icons.svg\`:${n} 个图标(<symbol id>)→ 一个 \`Icon\` 组件按 id 取,或拆成单个 SVG 组件`)
475
+ lines.push(mini
476
+ ? `- \`icons.svg\`:${n} 个图标(<symbol id>)→ 小程序的模板里不能直接写 \`<svg>\`,本地 .svg 文件用 \`<image>\` 显示也不是每个版本都稳:导出成 PNG(3 倍图)用 \`<image>\`,或者把 SVG 写成 \`data:image/svg+xml\` 放进 \`background-image\`(要换颜色就改里面的 fill),或做成图标字体`
477
+ : `- \`icons.svg\`:${n} 个图标(<symbol id>)→ 一个 \`Icon\` 组件按 id 取,或拆成单个 SVG 组件`)
252
478
  }
253
479
  lines.push('')
254
480
 
@@ -289,16 +515,52 @@ export function handoffMd({ root, rows, kind, size, framework, dest, project, sy
289
515
  }
290
516
  walk(root, '', 0)
291
517
  lines.push('## 图片和字体', '')
292
- lines.push(assets.length ? `- ${assets.slice(0, 30).map((a) => `\`${a}\``).join('、')}${assets.length > 30 ? ' …' : ''}:照原样复制进项目的静态资源目录(public/ 或 src/assets/)` : '- 没有单独的图片、字体文件(图形都写在 HTML / SVG 里)。', '')
518
+ lines.push(assets.length ? `- ${assets.slice(0, 30).map((a) => `\`${a}\``).join('、')}${assets.length > 30 ? ' …' : ''}:照原样复制进项目的静态资源目录(${mini ? (uni ? `\`${at}static/\`` : taro ? `\`${at}assets/\`` : `\`${at}images/\` 之类`) : 'public/ 或 src/assets/'})` : '- 没有单独的图片、字体文件(图形都写在 HTML / SVG 里)。')
519
+ if (mini) lines.push(`- 小程序主包不能超过 2MB:大图放服务器用网址;字体文件要用 \`${api}.loadFontFace\` 从网址加载,加载不了就用系统字体。`)
520
+ lines.push('')
293
521
 
294
522
  // 做法
295
- const create = !(project && project.pkg) && fw && fw.create ? fw.create.replace('{name}', path.basename(dest || name + '-app')) : ''
523
+ // 在认不出前端框架的项目(后端)或 monorepo 里选了个空的新文件夹:就在这里新建前端项目,不用再看结构、另套一层
524
+ const fresh = !!(project && project.pkg && (!project.framework || project.mono) && dest && fold(realish(dest)) !== fold(realish(project.dir)) && !hasStuff(dest))
525
+ // 「跟项目一样」新建子包时照项目的框架;装依赖用项目的包管理器(create-next-app 自己装依赖,换成对应的 --use-xxx)
526
+ // 只认得出是普通 React / Vue / Next.js 时才套现成的脚手架命令(Nuxt 也算 vue,但不能用 Vue + Vite 的命令搭)
527
+ const PLAIN = { react: 'React', vue: 'Vue', next: 'Next.js' }
528
+ const fwNew = fw || (fresh && same && PLAIN[project.framework] === String(project.label || '').replace(/ \+ Vite$/, '') ? FRAMEWORKS[project.framework] : null)
529
+ const pm = fresh && project.pm && project.pm !== 'npm' ? project.pm : ''
530
+ const create = (!known || fresh) && !occupied && fwNew && fwNew.create ? fwNew.create.replace('{name}', path.basename(dest || name + '-app')).replace('--use-npm', pm ? `--use-${pm}` : '--use-npm') : ''
296
531
  lines.push('## 做法和验收', '')
297
- lines.push(`1. 搭骨架:${create ? `新建项目 \`${create}\`,装好依赖${fw.router ? `和 ${fw.router}` : ''}` : framework === 'html' ? '整理成一个静态站点目录(每屏一个页面,公共样式一份)' : `在现有项目里加页面和路由(看清它现有的目录结构、样式写法、组件库再动手)`};把 tokens.css${sheets.length ? '(和公共样式表)' : ''}搬进去。`)
298
- lines.push('2. 一屏一个页面,路由照上面的表;先抽几屏共用的组件,再拼页面。')
299
- lines.push('3. 结构、文案、数字、图标照设计稿的 HTML 搬;class 名可以改成项目的习惯,样式值(间距、字号、颜色、圆角、阴影)一律用设计变量。')
300
- lines.push('4. 每做完一屏:把开发服务器跑起来,调 `compare_design`(screen 填设计文件名,url 填这一屏在开发服务器上的地址),照差异图把不一样的地方改掉,直到差异在 3% 以内,或者剩下的差别说得出理由(比如真实数据、系统字体)。')
301
- lines.push('5. 全部做完:每屏再对一遍;告诉用户做了哪些页面、怎么跑起来(命令和地址)、还有哪里和设计稿不一样。')
532
+ // 现有项目但没认出前端框架(后端、monorepo 根目录、别的语言的仓库)/ 像 monorepo 根目录:页面该放哪让模型看清结构再定,别直接往根目录塞
533
+ const bare = !create && framework !== 'html' && framework !== 'same' && !!(project && ((project.pkg && !project.framework) || occupied))
534
+ const sub = fw && fw.create ? fw.create.replace('{name}', 'web') : ''
535
+ const mono = !create && !fresh && !!(project && project.mono)
536
+ const keep = project && project.empty === false ? ';目录里已经有文件:别覆盖现有的页面,重名的先问用户' : ''
537
+ const tokTail = mini ? `;tokens.css 里的变量搬进全局样式的 \`${taro ? 'page, .taro_page' : 'page'} { }\`${sheets.length ? ',公共样式表也拆进去' : ''}` : `;把 tokens.css${sheets.length ? '(和公共样式表)' : ''}搬进去`
538
+ const step1 = create ? `新建项目 \`${create}\`${fresh ? `(在 \`${path.dirname(dest)}\` 里跑)` : ''},${pm ? `用 ${pm} ` : ''}装好依赖${fwNew.router ? `和 ${fwNew.router}` : ''}${uni ? '(GitHub 连不上就从 gitee 下载模板压缩包 `https://gitee.com/dcloud/uni-preset-vue/repository/archive/vite-ts.zip`,解开当项目目录)' : ''}`
539
+ : framework === 'html' ? `整理成一个静态站点目录(每屏一个页面,公共样式一份)${keep}`
540
+ : fresh ? `在这个空文件夹里新建一个 ${fwLabel.replace('(跟项目一样)', '')} 子项目,照仓库里别的子包怎么搭的来(monorepo 记得加进 workspace)${pm ? `,用 ${pm} 装依赖` : ''}`
541
+ : bare ? `这个目录里已经有东西,但没认出前端框架(可能是后端、monorepo 根目录或别的语言的仓库):先看清它的结构和用途再定放哪——适合直接加页面就加;不适合就在里面另建一个前端(monorepo 放进 apps/ 之类的子包,别的放进新的子目录${sub ? `,比如在里面跑 \`${sub}\`` : wx ? ',比如 `miniprogram/`,照上面搭骨架' : ''}),用 ${fw ? fw.label : framework} 搭,别打乱现有代码`
542
+ : mismatch ? `这个目录是 ${project.label} 项目,不是 ${fwLabel}:别往里面硬加——在里面另建一个子目录(比如 \`${uni ? 'uni-app' : 'miniprogram'}/\`)${uni ? `,在里面跑 \`${FRAMEWORKS.uniapp.create.replace('{name}', 'uni-app')}\` 新建 uni-app 项目再装依赖` : ',照上面「做成原生小程序」搭骨架'},现有代码别动`
543
+ : wx && !hasMini ? `在 \`${dest}\` 里照上面「做成原生小程序」搭好骨架${keep}`
544
+ : mini ? `在现有项目里加页面:放进 \`${at}pages/\`,登记进 \`${cfgFile}\`(看清它现有的目录结构、写法、组件库再动手)`
545
+ : '在现有项目里加页面和路由(看清它现有的目录结构、样式写法、组件库再动手)'
546
+ lines.push(`1. 搭骨架:${step1}${mono && !bare ? '(这像是 monorepo 根目录:先看根目录和 apps/ 之类的子包里有没有现成的前端应用,有就加进去;没有再新建一个子包,别把页面散放在根目录)' : ''}${tokTail}。`)
547
+ lines.push(mini
548
+ ? `2. 一屏一个页面,照上面的表登记页面路径;先抽几屏共用的组件(${wx ? '做成自定义组件,放 `components/`' : '放 `components/`'}),再拼页面。`
549
+ : '2. 一屏一个页面,路由照上面的表;先抽几屏共用的组件,再拼页面。')
550
+ lines.push(`3. 结构、文案、数字、图标照设计稿的 HTML 搬${mini ? `(标签换成${wx ? ' WXML ' : '小程序'}的组件)` : ''};class 名可以改成项目的习惯,样式值(间距、字号、颜色、圆角、阴影)一律用设计变量${mini && !taro ? ',尺寸按上面换成 rpx' : ''}。`)
551
+ if (wx) {
552
+ lines.push('4. 每做完一屏:原生小程序在浏览器里跑不起来,`compare_design` 比不了——让用户用微信开发者工具「导入项目」打开这个目录(没有 AppID 就用测试号 / 游客模式)看这一屏,截图发回来,和 handoff/shots/ 里的截图对照着改。')
553
+ lines.push('5. 全部做完:告诉用户做了哪些页面、在微信开发者工具里导入哪个目录、还有哪里和设计稿不一样。')
554
+ } else if (mini) {
555
+ const h5 = uni && hasMini && project.hbx ? '让用户在 HBuilderX 里点「运行 → 运行到浏览器」' : '后台跑 `npm run dev:h5`'
556
+ // 现有的命令行项目不一定配了网页版
557
+ const noH5 = hasMini && !project.hbx ? `(package.json 里没有 \`dev:h5\` 就是没配网页版:先问用户要不要加;不加就请用户在微信开发者工具里看这一屏,照截图对)` : ''
558
+ lines.push(`4. 每做完一屏:先跑网页版(${h5})${noH5},调 \`compare_design\`(screen 填设计文件名,url 填这一屏在网页版上的地址,比如 \`http://localhost:5173/#/${route(rows[0] ? rows[0].key : 'index')}\`),照差异图把不一样的地方改掉,直到差异在 3% 以内,或者剩下的差别说得出理由(网页版顶上是 ${uni ? 'uni-app' : 'Taro'} 画的导航栏、没有状态栏和胶囊,这块不一样是正常的)。`)
559
+ lines.push(`5. 全部做完:每屏再对一遍;告诉用户做了哪些页面、网页版怎么跑起来、微信小程序怎么出(${uni && hasMini && project.hbx ? 'HBuilderX「运行 → 运行到小程序模拟器 → 微信开发者工具」' : uni ? '`npm run dev:mp-weixin`,再用微信开发者工具打开 `dist/dev/mp-weixin`' : '`npm run dev:weapp`,再用微信开发者工具打开项目目录'})、还有哪里和设计稿不一样。`)
560
+ } else {
561
+ lines.push('4. 每做完一屏:把开发服务器跑起来,调 `compare_design`(screen 填设计文件名,url 填这一屏在开发服务器上的地址),照差异图把不一样的地方改掉,直到差异在 3% 以内,或者剩下的差别说得出理由(比如真实数据、系统字体)。')
562
+ lines.push('5. 全部做完:每屏再对一遍;告诉用户做了哪些页面、怎么跑起来(命令和地址)、还有哪里和设计稿不一样。')
563
+ }
302
564
  lines.push('', '别做:改设计(配色、文案、布局)、加设计稿里没有的功能、引入新的 UI 组件库(项目已经在用的除外)。', '')
303
565
  return lines.join('\n')
304
566
  }
package/lib/history.js CHANGED
@@ -191,7 +191,7 @@ export function createHistory({ base, now = Date.now, log = () => {} }) {
191
191
  * 那一版之后新加的文件不删;落到目录外的(父目录是指到外面的软链接)不写。
192
192
  * @returns { restored: 文件数, saved: 回退前存的那一版 | null }
193
193
  */
194
- function restore(root, n) {
194
+ function restore(root, n, { before = null } = {}) {
195
195
  const v = load(root).find((x) => x && x.n === Number(n))
196
196
  if (!v) throw new Error(`没有第 ${n} 版`)
197
197
  const objDir = path.join(dirOf(root), 'objects')
@@ -203,6 +203,8 @@ export function createHistory({ base, now = Date.now, log = () => {} }) {
203
203
  if (!buf) throw new Error(`第 ${n} 版的文件不全(${rel}),没有回退`)
204
204
  wanted.push([rel, buf])
205
205
  }
206
+ // 写之前先告诉调用方要退回哪些页面(sha256):装好的别人的设计系统要先记下来,写进去的那一刻起就按别人做的预览;它出错就不回
207
+ if (before) before(wanted.filter(([rel]) => /\.html?$/i.test(rel)).map(([, buf]) => crypto.createHash('sha256').update(buf).digest('hex')))
206
208
  let saved
207
209
  try {
208
210
  saved = take(root, '回退前', true)