@proteus-vue/create-proteus 0.2.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.md ADDED
@@ -0,0 +1,40 @@
1
+ # @proteus-vue/create-proteus —— Proteus 脚手架
2
+
3
+ > `npm create @proteus-vue/proteus my-app` —— 一键生成 Web + 微信小程序(Skyline)双端工程。
4
+
5
+ ## 使用
6
+
7
+ ```bash
8
+ npm create @proteus-vue/proteus my-app
9
+ cd my-app
10
+ npm install
11
+ npm run dev:web # Web 端(浏览器打开 Vite 提示地址)
12
+ npm run build:mp # 小程序端(微信开发者工具导入 dist/mp-weixin,替换 proteus.config.ts 的 appid)
13
+ ```
14
+
15
+ ## 生成内容(32 个文件)
16
+
17
+ - **编译管线**:`vite.config.ts` + `vite-plugin-mp-transform.ts`(薄适配层,依赖 npm 包 `@proteus-vue/compiler`)+ `scripts/gen-routes.ts`(路由/app.json/page.json 生成器)+ `scripts/mp-entry-stub.ts`
18
+ - **框架本体**(src/ 快照):`router`(Router/RouterView/guards/skyline/presets)+ `platform`(adapter 双端)+ `runtime`(setData 桥接/生命周期/app 骨架)+ `shims`(wx 类型)
19
+ - **应用骨架**:`src/main.ts`(Web 入口)/ `src/main.mp.ts`(★极简入口:不写 App(),骨架自动生成)/ `src/App.vue` / `src/pages/index.vue`(首页,含 `<route>` 块)
20
+ - **配置**:`proteus.config.ts`(platform/skyline/appid/rules 规则覆盖…)
21
+
22
+ ## 模板维护(★与主仓库同步)
23
+
24
+ 模板 = 主仓库快照 + 手写模板,`scripts/snapshot-template.ts` 负责同步:
25
+
26
+ ```bash
27
+ tsx scripts/snapshot-template.ts
28
+ ```
29
+
30
+ - **自动快照**:`src/platform/` `src/runtime/` `src/shims/` `src/router/`(除 auto-routes.ts)`scripts/`(除 snapshot 自身)`vite-plugin-mp-transform.ts`(import 替换为 `@proteus-vue/compiler`)`tsconfig.json` `index.html`(入口替换为 src/main.ts)
31
+ - **手写模板**(改主仓时手动同步):`package.json`(含 `{{name}}` 占位)/ `proteus.config.ts` / `vite.config.ts` / `src/main.ts` / `src/main.mp.ts` / `src/App.vue` / `src/pages/index.vue` / `src/router/auto-routes.ts`(仅首页占位,首次 build 时 gen-routes 重新生成)
32
+
33
+ > ⚠ `@proteus-vue/compiler` 未发布 npm 前,生成工程安装会 404;临时用 `npm install <仓库>/packages/compiler` 或 npm link。
34
+
35
+ ## 发布
36
+
37
+ ```bash
38
+ npm run build -w @proteus-vue/create-proteus # esbuild 单文件(dist/index.js + shebang)
39
+ npm publish # files: dist + templates
40
+ ```
@@ -0,0 +1,4 @@
1
+ /** 复制模板工程到目标目录(纯函数:返回生成的文件列表;templatesDir 可注入便于测试) */
2
+ export declare function copyTemplate(targetDir: string, opts: {
3
+ name: string;
4
+ }, templatesDir?: string): string[];
package/dist/index.js ADDED
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ var TEMPLATES_DIR = process.env.CREATE_PROTEUS_TEMPLATES ?? new URL("../templates/", import.meta.url).pathname;
7
+ var NEXT_STEPS = (name) => `\u2705 \u5DF2\u751F\u6210\u5DE5\u7A0B\uFF1A${name}
8
+
9
+ \u4E0B\u4E00\u6B65\uFF1A
10
+ cd ${name}
11
+ npm install # \u5B89\u88C5\u4F9D\u8D56\uFF08@proteus-vue/compiler \u53D1\u5E03\u540E\u53EF\u6B63\u5E38\u5B89\u88C5\uFF1B\u672A\u53D1\u5E03\u65F6\u7528\u4ED3\u5E93 workspace \u6216 npm link\uFF09
12
+ npm run dev:web # Web \u7AEF\uFF08\u6D4F\u89C8\u5668\u6253\u5F00 Vite \u63D0\u793A\u5730\u5740\uFF09
13
+ npm run build:mp # \u5C0F\u7A0B\u5E8F\u7AEF\uFF08\u5FAE\u4FE1\u5F00\u53D1\u8005\u5DE5\u5177\u5BFC\u5165 dist/mp-weixin\uFF0C\u9700\u66FF\u6362 proteus.config.ts \u7684 appid\uFF09
14
+
15
+ \u63D0\u793A\uFF1A
16
+ - npx proteus explain src/pages/index.vue # \u51B3\u7B56 trace\uFF08\u8BE5\u6587\u4EF6\u89E6\u53D1\u7684\u5168\u90E8\u8F6C\u6362\u89C4\u5219\uFF09
17
+ - npx proteus rules # \u7F16\u8BD1\u5668\u80FD\u529B\u6E05\u5355\uFF0849 \u6761 AI \u8BF4\u660E\u4E66\uFF09
18
+ - \u7F16\u8F91 proteus.config.ts \u7684 rules \u6BB5 # \u5E95\u7EBF\u5FAA\u73AF \u2462\uFF1A\u6539\u914D\u7F6E\u5373\u6539\u53D8\u7F16\u8BD1\u884C\u4E3A`;
19
+ function copyTemplate(targetDir, opts, templatesDir = TEMPLATES_DIR) {
20
+ const generated = [];
21
+ walk(templatesDir, (from, rel) => {
22
+ const to = path.join(targetDir, rel);
23
+ fs.mkdirSync(path.dirname(to), { recursive: true });
24
+ const content = fs.readFileSync(from, "utf-8").replace(/\{\{name\}\}/g, opts.name);
25
+ fs.writeFileSync(to, content);
26
+ generated.push(rel);
27
+ });
28
+ return generated;
29
+ }
30
+ function walk(dir, visit, base = "") {
31
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
32
+ if (entry.name === ".git") continue;
33
+ const full = path.join(dir, entry.name);
34
+ const rel = base ? `${base}/${entry.name}` : entry.name;
35
+ if (entry.isDirectory()) walk(full, visit, rel);
36
+ else visit(full, rel);
37
+ }
38
+ }
39
+ function main() {
40
+ const [nameArg, ...rest] = process.argv.slice(2);
41
+ if (!nameArg) {
42
+ console.error("\u7528\u6CD5\uFF1Anpm create @proteus-vue/proteus <\u9879\u76EE\u540D>\uFF08\u5982 npm create @proteus-vue/proteus my-app\uFF09");
43
+ process.exitCode = 1;
44
+ return;
45
+ }
46
+ if (rest.length) {
47
+ console.error(`\u591A\u4F59\u53C2\u6570\uFF1A${rest.join(" ")}`);
48
+ process.exitCode = 1;
49
+ return;
50
+ }
51
+ const name = nameArg.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "");
52
+ if (!name) {
53
+ console.error("\u9879\u76EE\u540D\u4E0D\u5408\u6CD5\uFF08\u4EC5\u5141\u8BB8\u5B57\u6BCD/\u6570\u5B57/\u8FDE\u5B57\u7B26\uFF09");
54
+ process.exitCode = 1;
55
+ return;
56
+ }
57
+ if (fs.existsSync(nameArg) && fs.readdirSync(nameArg).length > 0) {
58
+ console.error(`\u76EE\u5F55\u5DF2\u5B58\u5728\u4E14\u975E\u7A7A\uFF1A${nameArg}`);
59
+ process.exitCode = 1;
60
+ return;
61
+ }
62
+ const targetDir = nameArg;
63
+ const files = copyTemplate(targetDir, { name });
64
+ console.log(`\u5DF2\u590D\u5236 ${files.length} \u4E2A\u6587\u4EF6\uFF08\u542B\u6A21\u677F\u5FEB\u7167\uFF1A\u6846\u67B6\u672C\u4F53 + \u7F16\u8BD1\u7BA1\u7EBF + \u9996\u9875\uFF09`);
65
+ console.log(NEXT_STEPS(nameArg));
66
+ }
67
+ main();
68
+ export {
69
+ copyTemplate
70
+ };
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@proteus-vue/create-proteus",
3
+ "version": "0.2.0",
4
+ "description": "Proteus 脚手架:npm create @proteus-vue/proteus my-app —— 一键生成 Web + 微信小程序(Skyline)双端工程",
5
+ "type": "module",
6
+ "license": "Apache-2.0",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "bin": {
11
+ "create-proteus": "./dist/index.js"
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "templates",
16
+ "README.md"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc -p tsconfig.build.json --emitDeclarationOnly && esbuild src/index.ts --bundle --format=esm --platform=node --outfile=dist/index.js --banner:js=\"#!/usr/bin/env node\""
20
+ }
21
+ }
@@ -0,0 +1,53 @@
1
+ # AUTO-GENERATED by @proteus-vue/create-proteus 模板(cli-plus G-33 M4)。可自由编辑。
2
+ # 流水线:proteus check(CSS/Style/Router/CLI 四域门禁)→ 双端构建 → 测试(L1-L3 + Web E2E;automator 仅 Mac runner)
3
+ name: Proteus CI
4
+
5
+ on:
6
+ push:
7
+ branches: [main]
8
+ pull_request:
9
+
10
+ jobs:
11
+ check:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-node@v4
16
+ with:
17
+ node-version: '22'
18
+ cache: npm
19
+ - run: npm ci
20
+ - name: proteus check(四域门禁)
21
+ run: npx proteus check
22
+ build:
23
+ needs: check
24
+ runs-on: ubuntu-latest
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+ - uses: actions/setup-node@v4
28
+ with:
29
+ node-version: '22'
30
+ cache: npm
31
+ - run: npm ci
32
+ - run: npx proteus build --target web
33
+ - run: npx proteus build --target skyline
34
+ - uses: actions/upload-artifact@v4
35
+ with:
36
+ name: proteus-dist
37
+ path: |
38
+ dist/
39
+ dist/mp-weixin/
40
+ test:
41
+ needs: build
42
+ runs-on: ubuntu-latest
43
+ steps:
44
+ - uses: actions/checkout@v4
45
+ - uses: actions/setup-node@v4
46
+ with:
47
+ node-version: '22'
48
+ cache: npm
49
+ - run: npm ci
50
+ - name: 单测 + 编译快照(L1-L3)
51
+ run: npx proteus test
52
+ - name: Web E2E(Playwright headless)
53
+ run: npx proteus build --target web && npx proteus test e2e:web
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Proteus</title>
7
+ </head>
8
+ <body>
9
+ <div id="app"></div>
10
+ <script type="module" src="/src/main.ts"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "{{name}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "Proteus 应用:一份标准 Vue 源码 → Web + 微信小程序(Skyline)",
7
+ "scripts": {
8
+ "dev:web": "vite --mode web",
9
+ "build:web": "vue-tsc --noEmit && vite build --mode web",
10
+ "dev:mp": "tsx scripts/gen-routes.ts && vite --mode mp-weixin",
11
+ "build:mp": "tsx scripts/gen-routes.ts && vue-tsc --noEmit && vite build --mode mp-weixin",
12
+ "debug:mp": "PROTEUS_DEBUG=1 npm run build:mp",
13
+ "proteus": "npx proteus"
14
+ },
15
+ "dependencies": {
16
+ "@proteus-vue/router": "^0.2.0-beta.0",
17
+ "@proteus-vue/runtime": "^0.2.0-beta.0",
18
+ "@proteus-vue/shared": "^0.2.0-beta.0",
19
+ "vue": "^3.4.0"
20
+ },
21
+ "devDependencies": {
22
+ "@proteus-vue/compiler": "^0.3.0-beta.0",
23
+ "@proteus-vue/plugin-vite": "^0.2.0-beta.0",
24
+ "@types/node": "^20.0.0",
25
+ "@vitejs/plugin-vue": "^5.0.0",
26
+ "tsx": "^4.0.0",
27
+ "typescript": "^5.4.0",
28
+ "vite": "^5.0.0",
29
+ "vue-tsc": "^2.0.0"
30
+ }
31
+ }
@@ -0,0 +1,35 @@
1
+ // proteus.config.ts —— Proteus 框架统一配置(拆包步骤 7:类型契约来自 @proteus-vue/plugin-vite npm 包)
2
+ import type { ProteusConfig } from '@proteus-vue/plugin-vite'
3
+
4
+ const config: ProteusConfig = {
5
+ platform: 'mp-weixin',
6
+ skyline: true,
7
+ appid: 'wx0000000000', // 替换为真实 AppID
8
+ pagesDir: 'src/pages',
9
+ routesOutput: 'src/router/auto-routes.ts',
10
+ customRoute: {
11
+ registerPresets: true,
12
+ // 内置预设 builders(随 @proteus-vue/router 包发布源码,插件读取后内联进 app.js 注册)
13
+ builders: {
14
+ halfScreen: 'node_modules/@proteus-vue/router/src/presets/halfScreen.ts',
15
+ slideUp: 'node_modules/@proteus-vue/router/src/presets/slideUp.ts',
16
+ scaleDown: 'node_modules/@proteus-vue/router/src/presets/scaleDown.ts',
17
+ },
18
+ },
19
+ // ★底线循环 ①③:规则覆盖(改这里立即改变编译行为)
20
+ rules: {
21
+ disabled: [],
22
+ mapping: {},
23
+ customTags: {}, // 例:{ 'my-widget': 'view' } —— 新增标签映射
24
+ },
25
+ setDataBridge: {
26
+ batchWindow: 16, // ~1 帧
27
+ perComponent: true,
28
+ },
29
+ style: {
30
+ px2rpx: true,
31
+ rpxRatio: 2,
32
+ },
33
+ }
34
+
35
+ export default config
@@ -0,0 +1,7 @@
1
+ // scripts/gen-routes.ts —— 编译期路由表生成器 CLI(拆包步骤 7:逻辑来自 @proteus-vue/plugin-vite npm 包)
2
+ // 运行:tsx scripts/gen-routes.ts(dev:mp / build:mp 前置步骤)
3
+ // 输出:src/router/auto-routes.ts + dist/mp-weixin/{app.json,page.json,component.json}
4
+ import config from '../proteus.config'
5
+ import { runGenRoutes } from '@proteus-vue/plugin-vite'
6
+
7
+ runGenRoutes({ config })
@@ -0,0 +1,5 @@
1
+ // scripts/mp-entry-stub.ts
2
+ // mp 构建的 rollup 占位入口:真实 app.js 由 vite-plugin-mp-transform 在 buildStart 直出为纯文本资产
3
+ // (绕开 rollup 打包——微信 worklet 响应式重执行对打包代码不友好,见 examples/main.mp.ts 注释)
4
+ // 构建后此 chunk 会被插件在 generateBundle 中删除
5
+ export {}
@@ -0,0 +1,18 @@
1
+ <!-- src/App.vue —— Web 端根组件(SPA 壳:RouterView 按当前路由渲染页面) -->
2
+ <script setup lang="ts">
3
+ // RouterView 为应用壳(随应用存放),不再从框架包引子路径(拆包步骤 7)
4
+ import RouterView from './router/RouterView.vue'
5
+ </script>
6
+
7
+ <template>
8
+ <div class="app">
9
+ <RouterView />
10
+ </div>
11
+ </template>
12
+
13
+ <style>
14
+ .app {
15
+ font-family: system-ui, -apple-system, sans-serif;
16
+ color: #1f2328;
17
+ }
18
+ </style>
@@ -0,0 +1,18 @@
1
+ // src/main.mp.ts —— 小程序端入口(★极简模式)
2
+ // 不需要写 App()/onLaunch/调试日志/错误捕获——app 骨架由编译插件自动生成(appSkeleton),
3
+ // 本文件只需写"自定义":自定义路由 builder(覆盖/新增预设,见 proteus.config customRoute)。
4
+ // 若要完全自定义 app 生命周期,写一个含 App() 的完整入口即可(插件尊重原样)。
5
+
6
+ // 示例:自定义路由 builder(halfScreen 半屏转场,微信 RouteBuilder 契约)
7
+ // function myBuilder(customRouteContext: RouteContext): RouteBuilderResult {
8
+ // const primaryAnimation = customRouteContext.primaryAnimation
9
+ // const handlePrimaryAnimation = () => {
10
+ // 'worklet'
11
+ // const t = primaryAnimation.value
12
+ // return { transform: `translateY(${(1 - t) * 100}%)` }
13
+ // }
14
+ // return { opaque: false, barrierDismissible: true, barrierColor: 'rgba(0,0,0,0.6)', handlePrimaryAnimation }
15
+ // }
16
+ // if (typeof wx !== 'undefined' && wx.router) {
17
+ // wx.router.addRouteBuilder('halfScreen', myBuilder)
18
+ // }
@@ -0,0 +1,5 @@
1
+ // src/main.ts —— Web 端入口(Web 原生、零转换:标准 Vue SPA)
2
+ import { createApp } from 'vue'
3
+ import App from './App.vue'
4
+
5
+ createApp(App).mount('#app')
@@ -0,0 +1,33 @@
1
+ <!-- src/pages/index.vue —— 首页(路由页面:编译期由 gen-routes 扫描注册) -->
2
+ <route>
3
+ {
4
+ "meta": {
5
+ "title": "首页",
6
+ "isTab": true
7
+ }
8
+ }
9
+ </route>
10
+ <script setup lang="ts">
11
+ import { ref } from 'vue'
12
+
13
+ const count = ref(0)
14
+
15
+ function handleTap() {
16
+ count.value++
17
+ }
18
+ </script>
19
+
20
+ <template>
21
+ <div class="home">
22
+ <h1>Hello Proteus</h1>
23
+ <p class="tapped-count">tapped {{ count }} times</p>
24
+ <button @click="handleTap">tap</button>
25
+ </div>
26
+ </template>
27
+
28
+ <style>
29
+ .home {
30
+ text-align: center;
31
+ padding: 48px 0;
32
+ }
33
+ </style>
@@ -0,0 +1,378 @@
1
+ <!-- examples/router/RouterView.vue —— Web 端渲染容器(应用壳,仅 Web 构建使用)
2
+ ★ Web 原生转场:routeType 驱动 Vue <Transition>(CSS 动画),与 MP 的 Skyline worklet 转场对应——
3
+ 同一套 routeType API(halfScreen/slideUp → 上滑、scaleDown → 缩放、默认 fade) -->
4
+ <script setup lang="ts">
5
+ import { computed, ref, defineAsyncComponent } from 'vue'
6
+ import type { Component } from 'vue'
7
+ import { routeMap } from './auto-routes'
8
+ import { adapter } from '@proteus-vue/shared'
9
+ import { webTransitionName } from '@proteus-vue/router'
10
+
11
+ // 懒加载全部页面(含分包页):Web 端按页面自动 code-split
12
+ // glob 相对本文件(examples/router/)→ examples/pages 与 examples/subpackages/*/pages
13
+ const modules = import.meta.glob('../**/pages/**/*.vue')
14
+
15
+ // 页面组件缓存 + 预热:异步组件首次挂载会跳过 Transition 动画(真机/浏览器验证"第一次无下沉转场"),
16
+ // 预热使首次导航也同步挂载(chunk 已加载则 defineAsyncComponent 不再是异步 wrapper)
17
+ const pageCache = new Map<string, Component>()
18
+ for (const [key, load] of Object.entries(modules)) {
19
+ ;(load() as Promise<{ default: Component }>).then((mod) => {
20
+ pageCache.set(key, mod.default)
21
+ })
22
+ }
23
+ const current = ref(adapter.getCurrentPages()[0]?.route || 'pages/index')
24
+
25
+ // 路由变化 → 转场名(★透明化:routeType → Vue Transition 映射由框架共享表 webTransitionName 提供,
26
+ // 不再 RouterView 私有硬编码;三端共用同一枚举见 packages/router/src/transforms/transform-transition.ts)
27
+ const transitionName = ref('fade')
28
+ let lastForwardName = 'fade' // 当前页进入时的转场名(后退时取其反向)
29
+ adapter.onPageLoad?.((route, _query, routeType, nav) => {
30
+ current.value = route || 'pages/index'
31
+ if (nav === 'back') {
32
+ // 反向转场:用当前退出页进入时的转场名 + '-back'
33
+ transitionName.value =
34
+ lastForwardName && lastForwardName !== 'fade' ? `${lastForwardName}-back` : 'fade'
35
+ } else if (nav === 'replace' || nav === 'reLaunch' || nav === 'switchTab') {
36
+ lastForwardName = 'fade' // 无堆叠语义,后退按 fade
37
+ transitionName.value = nav === 'replace' ? 'replace' : nav === 'reLaunch' ? 'reset' : 'tab'
38
+ } else {
39
+ lastForwardName = webTransitionName(routeType)
40
+ transitionName.value = lastForwardName
41
+ }
42
+ })
43
+
44
+ const currentRoute = computed(() => current.value || 'pages/index')
45
+
46
+ // 层叠转场(routeType 前进/后退):新旧同屏重叠 → 绝对定位 + default 模式;
47
+ // fade / replace / reset / tab 用 out-in(先退后进)
48
+ const layeredNames = ['halfscreen', 'slide-up', 'scale', 'halfscreen-back', 'slide-up-back', 'scale-back']
49
+ const isLayered = computed(() => layeredNames.includes(transitionName.value))
50
+
51
+ // 遮罩对齐 MP barrierColor:halfScreen 0.4 / scaleDown 0.8(forward + back 都显示);slideUp opaque 无遮罩
52
+ // 遮罩常驻但被停留页(z:2)盖住:forward 时旧页(z:0)暴露在遮罩下淡入,back 时前页(z:0)暴露在遮罩下淡出
53
+ const showBarrier = computed(() => {
54
+ const n = transitionName.value
55
+ return n === 'halfscreen' || n === 'scale' || n === 'halfscreen-back' || n === 'scale-back'
56
+ })
57
+ const isBack = computed(() => transitionName.value.endsWith('-back'))
58
+ const barrierOpacity = computed(() => (transitionName.value.startsWith('halfscreen') ? 0.4 : 0.8))
59
+
60
+ const view = computed<Component | null>(() => {
61
+ // routeMap 以 name 为键,这里收到的是 path,需按 path 回退查找(同 guards.getCurrentFrom)
62
+ const rec =
63
+ routeMap[currentRoute.value] || Object.values(routeMap).find((r) => r.path === currentRoute.value)
64
+ if (!rec) return null
65
+ // rec.component 为相对 examples/router/ 的路径,与 glob 键一致;优先用缓存(同步挂载保证转场)
66
+ const cached = pageCache.get(rec.component)
67
+ if (cached) return cached
68
+ const load = (modules as Record<string, () => Promise<unknown>>)[rec.component]
69
+ return load ? defineAsyncComponent(load as () => Promise<Component>) : null
70
+ })
71
+ </script>
72
+
73
+ <template>
74
+ <!-- routeType 转场全部层叠(default 模式 + 绝对定位重叠 → 新旧同屏):
75
+ halfScreen/slideUp/scaleDown 各自层叠语义;默认 fade 用 out-in 先退后进
76
+ :key 按路由强制重挂载触发过渡 -->
77
+ <div class="router-view" :class="{ layered: isLayered }">
78
+ <!-- 遮罩层(对齐 MP barrierColor):旧页/前页(z:0)之上、停留页(z:2)之下;forward 淡入压暗,back 淡出抬起 -->
79
+ <div
80
+ v-if="showBarrier"
81
+ class="route-barrier"
82
+ :class="{ 'barrier-out': isBack }"
83
+ :style="{ '--barrier-opacity': barrierOpacity }"
84
+ />
85
+ <Transition :name="transitionName" :mode="isLayered ? undefined : 'out-in'">
86
+ <component :is="view" v-if="view" :key="currentRoute" class="page" />
87
+ <div v-else :key="'404'" class="page">404 Not Found</div>
88
+ </Transition>
89
+ </div>
90
+ </template>
91
+
92
+ <style>
93
+ /* 层叠容器:所有 routeType 转场时新旧页同屏绝对定位重叠 */
94
+ .router-view {
95
+ position: relative;
96
+ min-height: 100vh;
97
+ }
98
+ .router-view.layered .page {
99
+ position: absolute;
100
+ inset: 0;
101
+ overflow-y: auto;
102
+ }
103
+
104
+ /* 遮罩层(对齐 MP barrierColor):前/旧页(z:0)之上、停留页(z:2)之下;forward 淡入压暗、back 淡出抬起 */
105
+ .route-barrier {
106
+ position: absolute;
107
+ inset: 0;
108
+ z-index: 1;
109
+ background: #000;
110
+ opacity: 0;
111
+ animation: barrier-in 0.3s ease forwards;
112
+ pointer-events: none;
113
+ }
114
+ @keyframes barrier-in {
115
+ from {
116
+ opacity: 0;
117
+ }
118
+ to {
119
+ opacity: var(--barrier-opacity);
120
+ }
121
+ }
122
+ .route-barrier.barrier-out {
123
+ animation: barrier-out 0.4s ease forwards;
124
+ }
125
+ @keyframes barrier-out {
126
+ from {
127
+ opacity: var(--barrier-opacity);
128
+ }
129
+ to {
130
+ opacity: 0;
131
+ }
132
+ }
133
+
134
+ /* 页面"纸片"基础样式:白色背景 + 撑满视口 + 恒在遮罩(z:1)之上(z:2)
135
+ 层叠转场时旧页被 leave 规则降为 z:0 暴露在遮罩下;停留页始终盖住遮罩 */
136
+ .page {
137
+ background: #fff;
138
+ min-height: 100vh;
139
+ z-index: 2;
140
+ }
141
+
142
+ /* 默认:淡入淡出 */
143
+ .fade-enter-active,
144
+ .fade-leave-active {
145
+ transition: opacity 0.25s ease;
146
+ }
147
+ .fade-enter-from,
148
+ .fade-leave-to {
149
+ opacity: 0;
150
+ }
151
+
152
+ /* halfScreen / slideUp:底部上滑(缓出曲线,与 MP 预设一致) */
153
+ .slide-up-enter-active {
154
+ transition: transform 0.32s cubic-bezier(0.35, 0.91, 0.33, 0.97);
155
+ }
156
+ .slide-up-enter-from {
157
+ transform: translateY(20%);
158
+ }
159
+ .slide-up-leave-active {
160
+ transition: opacity 0.25s ease;
161
+ }
162
+ .slide-up-leave-to {
163
+ opacity: 0;
164
+ }
165
+
166
+ /* scaleDown:层叠缩放——新页从底部滑入覆盖(z 上层),旧页同时下沉缩放(z 下层) */
167
+ .scale-enter-active {
168
+ transition: transform 0.4s cubic-bezier(0.35, 0.91, 0.33, 0.97);
169
+ z-index: 2;
170
+ }
171
+ .scale-enter-from {
172
+ transform: translateY(100%);
173
+ }
174
+ .scale-leave-active {
175
+ transition:
176
+ transform 0.4s cubic-bezier(0.35, 0.91, 0.33, 0.97),
177
+ opacity 0.4s ease,
178
+ border-radius 0.4s ease;
179
+ z-index: 0;
180
+ }
181
+ .scale-leave-to {
182
+ transform: scale(0.92) translateY(4%);
183
+ opacity: 0.8;
184
+ border-radius: 12px;
185
+ }
186
+
187
+ /* slideUp:层叠推入——新页底部推入覆盖(z:2),旧页被推出视口上方(z:1,对应 MP slideUp) */
188
+ .slide-up-enter-active {
189
+ transition: transform 0.32s cubic-bezier(0.35, 0.91, 0.33, 0.97);
190
+ z-index: 2;
191
+ }
192
+ .slide-up-enter-from {
193
+ transform: translateY(100%);
194
+ }
195
+ .slide-up-leave-active {
196
+ transition:
197
+ transform 0.32s cubic-bezier(0.35, 0.91, 0.33, 0.97),
198
+ opacity 0.32s ease;
199
+ z-index: 0;
200
+ }
201
+ .slide-up-leave-to {
202
+ transform: translateY(-20%);
203
+ opacity: 0;
204
+ }
205
+
206
+ /* halfScreen:层叠半屏——新页底部滑入覆盖(z:2),旧页保持原位淡出(对应 MP 半屏前页保持) */
207
+ .halfscreen-enter-active {
208
+ transition: transform 0.3s cubic-bezier(0.35, 0.91, 0.33, 0.97);
209
+ z-index: 2;
210
+ }
211
+ .halfscreen-enter-from {
212
+ transform: translateY(100%);
213
+ }
214
+ .halfscreen-leave-active {
215
+ transition: opacity 0.3s ease;
216
+ z-index: 0;
217
+ }
218
+ .halfscreen-leave-to {
219
+ opacity: 0;
220
+ }
221
+
222
+ /* ===== 反向转场(返回/后退:当前页 B 反向退出滑出底部,前页 A 恢复原态)===== */
223
+ /* scale 返回:B 滑出底部(z:2),A 从下沉态恢复 scale(0.92)→1 */
224
+ .scale-back-enter-active {
225
+ transition:
226
+ transform 0.4s cubic-bezier(0.35, 0.91, 0.33, 0.97),
227
+ opacity 0.4s ease,
228
+ border-radius 0.4s ease;
229
+ z-index: 0; /* 低于遮罩(z:1),恢复过程中被遮罩压暗 */
230
+ }
231
+ .scale-back-enter-from {
232
+ transform: scale(0.92) translateY(4%);
233
+ opacity: 0.8;
234
+ border-radius: 12px;
235
+ }
236
+ .scale-back-leave-active {
237
+ transition: transform 0.4s cubic-bezier(0.35, 0.91, 0.33, 0.97);
238
+ z-index: 2;
239
+ }
240
+ .scale-back-leave-to {
241
+ transform: translateY(100%);
242
+ }
243
+
244
+ /* slideUp 返回:B 滑出底部(z:2),A 从被推出上方恢复 */
245
+ .slide-up-back-enter-active {
246
+ transition:
247
+ transform 0.32s cubic-bezier(0.35, 0.91, 0.33, 0.97),
248
+ opacity 0.32s ease;
249
+ z-index: 1;
250
+ }
251
+ .slide-up-back-enter-from {
252
+ transform: translateY(-20%);
253
+ opacity: 0;
254
+ }
255
+ .slide-up-back-leave-active {
256
+ transition: transform 0.32s cubic-bezier(0.35, 0.91, 0.33, 0.97);
257
+ z-index: 2;
258
+ }
259
+ .slide-up-back-leave-to {
260
+ transform: translateY(100%);
261
+ }
262
+
263
+ /* halfScreen 返回:B 滑出底部(z:2),A 轻微下沉后恢复 */
264
+ .halfscreen-back-enter-active {
265
+ transition: transform 0.3s cubic-bezier(0.35, 0.91, 0.33, 0.97);
266
+ z-index: 0; /* 低于遮罩(z:1) */
267
+ }
268
+ .halfscreen-back-enter-from {
269
+ transform: translateY(8%);
270
+ }
271
+ .halfscreen-back-leave-active {
272
+ transition: transform 0.3s cubic-bezier(0.35, 0.91, 0.33, 0.97);
273
+ z-index: 2;
274
+ }
275
+ .halfscreen-back-leave-to {
276
+ transform: translateY(100%);
277
+ }
278
+
279
+ /* replace(redirectTo):替换当前页——旧页轻微缩小淡出、新页淡入(out-in) */
280
+ .replace-enter-active,
281
+ .replace-leave-active {
282
+ transition:
283
+ opacity 0.2s ease,
284
+ transform 0.2s ease;
285
+ }
286
+ .replace-enter-from {
287
+ opacity: 0;
288
+ transform: scale(0.98);
289
+ }
290
+ .replace-leave-to {
291
+ opacity: 0;
292
+ transform: scale(0.98);
293
+ }
294
+
295
+ /* reLaunch:重置——淡入 */
296
+ .reset-enter-active {
297
+ transition: opacity 0.25s ease;
298
+ }
299
+ .reset-enter-from {
300
+ opacity: 0;
301
+ }
302
+ .reset-leave-active {
303
+ transition: opacity 0.2s ease;
304
+ }
305
+ .reset-leave-to {
306
+ opacity: 0;
307
+ }
308
+
309
+ /* switchTab:tab 切换——淡入淡出 */
310
+ .tab-enter-active {
311
+ transition: opacity 0.2s ease;
312
+ }
313
+ .tab-enter-from {
314
+ opacity: 0;
315
+ }
316
+ .tab-leave-active {
317
+ transition: opacity 0.15s ease;
318
+ }
319
+ .tab-leave-to {
320
+ opacity: 0;
321
+ }
322
+
323
+ /* 无障碍:减少动态偏好时关闭全部转场 */
324
+ @media (prefers-reduced-motion: reduce) {
325
+ .fade-enter-active,
326
+ .fade-leave-active,
327
+ .slide-up-enter-active,
328
+ .slide-up-leave-active,
329
+ .halfscreen-enter-active,
330
+ .halfscreen-leave-active,
331
+ .scale-enter-active,
332
+ .scale-leave-active,
333
+ .slide-up-back-enter-active,
334
+ .slide-up-back-leave-active,
335
+ .halfscreen-back-enter-active,
336
+ .halfscreen-back-leave-active,
337
+ .scale-back-enter-active,
338
+ .scale-back-leave-active,
339
+ .replace-enter-active,
340
+ .replace-leave-active,
341
+ .reset-enter-active,
342
+ .reset-leave-active,
343
+ .tab-enter-active,
344
+ .tab-leave-active {
345
+ transition: none !important;
346
+ }
347
+ .fade-enter-from,
348
+ .fade-leave-to,
349
+ .slide-up-enter-from,
350
+ .slide-up-leave-to,
351
+ .halfscreen-enter-from,
352
+ .halfscreen-leave-to,
353
+ .scale-enter-from,
354
+ .scale-leave-to,
355
+ .slide-up-back-enter-from,
356
+ .slide-up-back-leave-to,
357
+ .halfscreen-back-enter-from,
358
+ .halfscreen-back-leave-to,
359
+ .scale-back-enter-from,
360
+ .scale-back-leave-to,
361
+ .replace-enter-from,
362
+ .replace-leave-to,
363
+ .reset-enter-from,
364
+ .reset-leave-to,
365
+ .tab-enter-from,
366
+ .tab-leave-to {
367
+ transform: none;
368
+ opacity: 1;
369
+ }
370
+ .route-barrier {
371
+ animation: none;
372
+ opacity: var(--barrier-opacity);
373
+ }
374
+ .route-barrier.barrier-out {
375
+ opacity: 0;
376
+ }
377
+ }
378
+ </style>
@@ -0,0 +1,17 @@
1
+ // src/router/auto-routes.ts —— 应用侧路由表(AUTO-GENERATED by scripts/gen-routes.ts,勿手动编辑)
2
+ // ★拆包步骤 4:auto-routes 随应用存放(工厂化后路由表由应用注入 createRouter),不再属于 @proteus-vue/router 包
3
+ import type { RouteRecord } from '@proteus-vue/router/types'
4
+
5
+ export const routes: RouteRecord[] = [
6
+ { name: "index", path: "pages/index", component: "../pages/index.vue", meta: {"title":"首页","isTab":true} },
7
+ ]
8
+
9
+ export const tabRoutes: RouteRecord[] = routes.filter(r => r.meta?.isTab)
10
+ export const routeMap: Record<string, RouteRecord> = routes.reduce((m, r) => { m[r.name] = r; return m }, {} as Record<string, RouteRecord>)
11
+
12
+ // ★ 类型提示:按路由名索引的参数类型表(来源:<route> 块 params 声明)
13
+ declare module '@proteus-vue/router/types' {
14
+ interface RouteParamsByName {
15
+ 'index': { },
16
+ }
17
+ }
@@ -0,0 +1,18 @@
1
+ // examples/router/index.ts —— 应用侧路由单例(拆包步骤 4 工厂化)
2
+ // ★工厂化:路由表不再藏在框架里,由本文件从 gen-routes 生成的 ./auto-routes 注入 createRouter
3
+ // 应用开发者视角:只需这一行单例;换应用换路由表,框架零改动
4
+ import { createRouter } from '@proteus-vue/router'
5
+ import { routes } from './auto-routes'
6
+
7
+ export const router = createRouter(routes)
8
+
9
+ // 类型契约透传(应用页面可直接 import type { PageOnLoad } from './router',无需深路径)
10
+ export type {
11
+ RouteRecord,
12
+ RouteMeta,
13
+ RouteParams,
14
+ RouteParamsByName,
15
+ PageOnLoad,
16
+ NavigateOptions,
17
+ RouterInstance,
18
+ } from '@proteus-vue/router'
@@ -0,0 +1,20 @@
1
+ // src/shims/events.d.ts
2
+ // 事件处理器类型(类型提示全链路步骤 4)—— 全局声明(无 export,页面/组件无需 import)
3
+ // 微信事件对象:detail(自定义数据)/ currentTarget.dataset(元素自定义数据)
4
+ // 用法:function onChange(e: MpInputEvent) { e.detail.value } / function onTap(e: TapEvent) { e.currentTarget.dataset.url }
5
+ // 命名避开 DOM 内置类型(InputEvent/UIEvent 已存在)
6
+
7
+ /** 通用微信事件(TDetail = 事件携带数据,如 input 的 { value: string }) */
8
+ interface MpEvent<TDetail = unknown> {
9
+ type: string
10
+ detail: TDetail
11
+ target: { dataset: Record<string, string>; id?: string }
12
+ currentTarget: { dataset: Record<string, string>; id?: string }
13
+ timeStamp: number
14
+ }
15
+
16
+ /** input/textarea 输入事件(v-model handler 等):e.detail.value: string */
17
+ type MpInputEvent = MpEvent<{ value: string }>
18
+
19
+ /** 点击事件(tap):e.currentTarget.dataset 可读元素自定义数据 */
20
+ type TapEvent = MpEvent<Record<string, never>>
@@ -0,0 +1,112 @@
1
+ // src/shims/mp.d.ts
2
+ // ============================================================
3
+ // 微信小程序 API 类型声明垫片(minimal)
4
+ //
5
+ // 职责:
6
+ // - 让 wx.* / Page() / App() / Component() 在小程序端代码中通过编译
7
+ // - 提供 RouteBuilder / PageOptions 等类型,是 P3(router)、
8
+ // P4(编译产物)、P5(pageLifecycle)的类型依据,务必完整
9
+ //
10
+ // 注意:本文件无 import/export,保持全局声明(ambient)形态
11
+ // ============================================================
12
+
13
+ /** wx 全局对象(minimal:任何属性可用,仅供编译通过) */
14
+ declare const wx: any
15
+
16
+ /** Skyline wx.router 自定义路由上下文(官方 CustomRouteContext) */
17
+ interface RouteContext {
18
+ /** 主转场动画进度 0→1(控制推入页面的进入/退出过渡) */
19
+ primaryAnimation: { value: number }
20
+ /** 主动画控制器状态:0=dismissed 1=forward 2=reverse 3=completed */
21
+ primaryAnimationStatus: { value: number }
22
+ /** 次转场动画进度 0→1(控制栈顶页面的压入/压出过渡) */
23
+ secondaryAnimation: { value: number }
24
+ /** 次动画控制器状态 */
25
+ secondaryAnimationStatus: { value: number }
26
+ /** 当前路由进度是否由手势控制 */
27
+ userGestureInProgress: { value: number }
28
+ /** 手势开始控制路由 */
29
+ startUserGesture: () => void
30
+ /** 手势不再控制路由 */
31
+ stopUserGesture: () => void
32
+ /** 返回上一级(效果同 wx.navigateBack) */
33
+ didPop: () => void
34
+ }
35
+
36
+ /** worklet 返回值('worklet' 指令注释由编译器保留) */
37
+ interface WorkletValue {
38
+ transform?: string
39
+ opacity?: number
40
+ }
41
+
42
+ /** routeBuilder 返回结构(wx.router.addRouteBuilder 的 builder 返回值,字段见官方 CustomRouteConfig) */
43
+ interface RouteBuilderResult {
44
+ /** 是否不透明(false 时前一页可见,如半屏弹窗) */
45
+ opaque?: boolean
46
+ /** 是否保持前一个页面状态 */
47
+ maintainState?: boolean
48
+ /** 页面推入动画时长(ms) */
49
+ transitionDuration?: number
50
+ /** 页面推出动画时长(ms) */
51
+ reverseTransitionDuration?: number
52
+ /** 遮罩层背景色 rgba() / #RRGGBBAA */
53
+ barrierColor?: string
54
+ /** 点击遮罩层返回上一页 */
55
+ barrierDismissible?: boolean
56
+ /** 无障碍语义 */
57
+ barrierLabel?: string
58
+ /** 是否与下一个页面联动(决定当前页 secondaryAnimation 是否生效) */
59
+ canTransitionTo?: boolean
60
+ /** 是否与前一个页面联动(决定前一个页 secondaryAnimation 是否生效;false = 前页保持不动) */
61
+ canTransitionFrom?: boolean
62
+ handlePrimaryAnimation?: () => WorkletValue
63
+ handleSecondaryAnimation?: () => WorkletValue
64
+ /** 控制上一级页面的压入/压出动画(基础库 ≥3.0.0) */
65
+ handlePreviousPageAnimation?: () => WorkletValue
66
+ /** 页面进入时是否采用 snapshot 模式优化动画性能(基础库 ≥3.2.0) */
67
+ allowEnterRouteSnapshotting?: boolean
68
+ /** 页面退出时是否采用 snapshot 模式(基础库 ≥3.2.0) */
69
+ allowExitRouteSnapshotting?: boolean
70
+ /** 右滑返回可拖动范围是否撑满屏幕(基础库 ≥3.2.0,常用于半屏弹窗) */
71
+ fullscreenDrag?: boolean
72
+ /** 返回手势方向(基础库 ≥3.4.0) */
73
+ popGestureDirection?: 'horizontal' | 'vertical' | 'multi'
74
+ }
75
+
76
+ /** 自定义路由 builder(skyline.ts 注册到 wx.router) */
77
+ type RouteBuilder = (ctx: RouteContext) => RouteBuilderResult
78
+
79
+ /** 小程序 Page 构造器配置(pageLifecycle.createPage 的返回类型) */
80
+ interface PageOptions {
81
+ data?: Record<string, unknown> | (() => Record<string, unknown>)
82
+ onLoad?: (options: Record<string, string>) => void
83
+ onReady?: () => void
84
+ onUnload?: () => void
85
+ [key: string]: unknown
86
+ }
87
+
88
+ /** 小程序 Component 构造器配置(pageLifecycle.createComponent 的返回类型) */
89
+ interface ComponentOptions {
90
+ properties?: Record<string, unknown>
91
+ data?: Record<string, unknown> | (() => Record<string, unknown>)
92
+ methods?: Record<string, Function>
93
+ lifetimes?: {
94
+ attached?: () => void
95
+ detached?: () => void
96
+ }
97
+ [key: string]: unknown
98
+ }
99
+
100
+ declare function App(options: Record<string, unknown>): void
101
+ declare function Page(options: PageOptions): void
102
+ declare function Component(options: ComponentOptions): void
103
+
104
+ /** Skyline worklet 渲染线程全局(仅 worklet 函数内可用) */
105
+ declare const screenHeight: number
106
+ declare const windowWidth: number
107
+
108
+ /** 调试开关(PROTEUS_DEBUG=1 构建时由 vite define 注入为 true) */
109
+ declare const __PROTEUS_DEBUG__: boolean
110
+
111
+ /** Skyline 渲染开关(mp 构建时由 vite define 注入 proteus.config.skyline) */
112
+ declare const __PROTEUS_SKYLINE__: boolean
@@ -0,0 +1,8 @@
1
+ // src/shims/vue.d.ts
2
+ // .vue 模块声明 —— 供 .ts 文件 import 组件(如 src/components/index.ts 聚合导出)
3
+ // vue-tsc 对 .vue 文件内部 import .vue 原生支持;.ts 文件需要此声明
4
+ declare module '*.vue' {
5
+ import type { DefineComponent } from 'vue'
6
+ const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>
7
+ export default component
8
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "jsx": "preserve",
8
+ "noEmit": true,
9
+ "isolatedModules": true,
10
+ "esModuleInterop": true,
11
+ "resolveJsonModule": true,
12
+ "sourceMap": true,
13
+ "skipLibCheck": true,
14
+ "allowImportingTsExtensions": true,
15
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
16
+ "types": ["vite/client", "./src/shims/mp.d.ts", "./src/shims/events.d.ts"],
17
+ "baseUrl": ".",
18
+ "paths": {
19
+ "@/*": ["src/*"]
20
+ }
21
+ },
22
+ "include": [
23
+ "src/**/*.ts",
24
+ "src/**/*.d.ts",
25
+ "src/**/*.vue",
26
+ "vite.config.ts",
27
+ "proteus.config.ts",
28
+ "scripts/**/*.ts"
29
+ ],
30
+ "exclude": ["node_modules", "dist"]
31
+ }
@@ -0,0 +1,55 @@
1
+ // vite.config.ts —— Proteus 工程骨架(拆包步骤 7:插件来自 @proteus-vue/plugin-vite npm 包)
2
+ import { defineConfig } from 'vite'
3
+ import type { Plugin } from 'vite'
4
+ import vue from '@vitejs/plugin-vue'
5
+ import { fileURLToPath, URL } from 'node:url'
6
+ import config from './proteus.config'
7
+ import { mpTransform } from '@proteus-vue/plugin-vite'
8
+
9
+ /** 处理 <route> 自定义块虚拟模块(?vue&type=route),保证 Web 构建不报错 */
10
+ function routeBlocksPlugin(): Plugin {
11
+ return {
12
+ name: 'proteus-route-blocks',
13
+ enforce: 'pre',
14
+ transform(code, id) {
15
+ if (id.includes('?vue&type=route')) {
16
+ return { code: `export default ${code}`, map: null }
17
+ }
18
+ return null
19
+ },
20
+ }
21
+ }
22
+
23
+ export default defineConfig(({ mode }) => {
24
+ const platform = mode === 'mp-weixin' || mode === 'web' ? mode : config.platform
25
+ const isMp = platform === 'mp-weixin'
26
+
27
+ return {
28
+ define: {
29
+ __PROTEUS_DEBUG__: process.env.PROTEUS_DEBUG === '1',
30
+ // Skyline 开关注入:mp 构建时 __PROTEUS_SKYLINE__ = config.skyline(router/skyline 解耦 config)
31
+ __PROTEUS_SKYLINE__: isMp && config.skyline,
32
+ },
33
+ plugins: isMp
34
+ ? [
35
+ // 小程序端:独占编译管线(不用 plugin-vue),标准 Vue SFC → wxml/wxss/js
36
+ mpTransform({ config }),
37
+ ]
38
+ : [vue(), routeBlocksPlugin()],
39
+ resolve: {
40
+ alias: [
41
+ { find: '@', replacement: fileURLToPath(new URL('./src', import.meta.url)) },
42
+ ],
43
+ },
44
+ build: {
45
+ target: 'es2018',
46
+ cssCodeSplit: false,
47
+ minify: isMp ? false : undefined,
48
+ outDir: `dist/${platform}`,
49
+ emptyOutDir: !isMp,
50
+ rollupOptions: isMp
51
+ ? { input: 'scripts/mp-entry-stub.ts', output: { entryFileNames: 'mp-entry.js' } }
52
+ : undefined,
53
+ },
54
+ }
55
+ })