@lingfan-engine/create 0.0.0-stage → 0.1.4

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 (122) hide show
  1. package/LICENSE +201 -0
  2. package/bin/create.mjs +18 -0
  3. package/package.json +44 -3
  4. package/src/cli/cli.ts +302 -0
  5. package/src/cli/prompts.ts +115 -0
  6. package/src/generate/dotfiles.ts +27 -0
  7. package/src/generate/index.ts +20 -0
  8. package/src/generate/project.ts +279 -0
  9. package/src/generate/replace.ts +91 -0
  10. package/src/index.ts +46 -0
  11. package/src/overlay/index.ts +10 -0
  12. package/src/overlay/merge.ts +143 -0
  13. package/src/registry/index.ts +15 -0
  14. package/src/registry/template-meta.ts +107 -0
  15. package/src/registry/versions.ts +109 -0
  16. package/src/shared/guards.ts +85 -0
  17. package/src/shared/index.ts +7 -0
  18. package/src/shared/texts.ts +55 -0
  19. package/template/README.md +138 -0
  20. package/template/__PROJECT__/Resources/Audio/.gitkeep +0 -0
  21. package/template/__PROJECT__/Resources/Fonts/README.md +10 -0
  22. package/template/__PROJECT__/Resources/Images/.gitkeep +0 -0
  23. package/template/__PROJECT__/Resources/Lang/.gitkeep +0 -0
  24. package/template/__PROJECT__/Resources/Stories/title/title_main.story +24 -0
  25. package/template/__PROJECT__/Resources/Video/.gitkeep +0 -0
  26. package/template/__PROJECT__/Resources/project.json +7 -0
  27. package/template/__PROJECT__/gitignore +8 -0
  28. package/template/__PROJECT__/index.html +34 -0
  29. package/template/__PROJECT__/package.json +22 -0
  30. package/template/__PROJECT__/pnpm-workspace.yaml +8 -0
  31. package/template/__PROJECT__/src/host/boot.ts +181 -0
  32. package/template/__PROJECT__/src/host/dialogue-view.ts +235 -0
  33. package/template/__PROJECT__/src/host/effects.ts +68 -0
  34. package/template/__PROJECT__/src/host/engine-wiring.ts +206 -0
  35. package/template/__PROJECT__/src/host/frame-loop.ts +42 -0
  36. package/template/__PROJECT__/src/host/index.ts +27 -0
  37. package/template/__PROJECT__/src/host/input-map.ts +51 -0
  38. package/template/__PROJECT__/src/host/layer-z.ts +50 -0
  39. package/template/__PROJECT__/src/host/save-slots.ts +38 -0
  40. package/template/__PROJECT__/src/host/stage-dom.ts +74 -0
  41. package/template/__PROJECT__/src/host/theme-fonts.ts +60 -0
  42. package/template/__PROJECT__/src/host/toast.ts +29 -0
  43. package/template/__PROJECT__/src/main.ts +61 -0
  44. package/template/__PROJECT__/src/styles/stage.css +183 -0
  45. package/template/__PROJECT__/src/vite-env.d.ts +14 -0
  46. package/template/__PROJECT__/tsconfig.json +19 -0
  47. package/template/__PROJECT__/tsconfig.node.json +14 -0
  48. package/template/__PROJECT__/vite.config.ts +64 -0
  49. package/template/overlays/README.md +70 -0
  50. package/template/overlays/miniprogram/README.md +104 -0
  51. package/template/overlays/miniprogram/index.html +15 -0
  52. package/template/overlays/miniprogram/package.patch.json +40 -0
  53. package/template/overlays/miniprogram/scripts/serve-project.mjs +273 -0
  54. package/template/overlays/miniprogram/src/App.vue +23 -0
  55. package/template/overlays/miniprogram/src/components/ChoiceLayer.vue +208 -0
  56. package/template/overlays/miniprogram/src/components/DialogueLayer.vue +183 -0
  57. package/template/overlays/miniprogram/src/components/NodeTree.vue +44 -0
  58. package/template/overlays/miniprogram/src/components/NoticeLayer.vue +87 -0
  59. package/template/overlays/miniprogram/src/components/SaveLoadBar.vue +168 -0
  60. package/template/overlays/miniprogram/src/components/StageView.vue +50 -0
  61. package/template/overlays/miniprogram/src/env.d.ts +1 -0
  62. package/template/overlays/miniprogram/src/main.ts +12 -0
  63. package/template/overlays/miniprogram/src/manifest.json +21 -0
  64. package/template/overlays/miniprogram/src/mp/frame.ts +51 -0
  65. package/template/overlays/miniprogram/src/mp/guards.ts +28 -0
  66. package/template/overlays/miniprogram/src/mp/host.ts +252 -0
  67. package/template/overlays/miniprogram/src/mp/index.ts +9 -0
  68. package/template/overlays/miniprogram/src/mp/platform.ts +210 -0
  69. package/template/overlays/miniprogram/src/mp/ports.ts +84 -0
  70. package/template/overlays/miniprogram/src/mp/project-source.ts +77 -0
  71. package/template/overlays/miniprogram/src/mp/slots.ts +64 -0
  72. package/template/overlays/miniprogram/src/mp/surface.ts +239 -0
  73. package/template/overlays/miniprogram/src/mp/video-surface.ts +104 -0
  74. package/template/overlays/miniprogram/src/mp/view.ts +91 -0
  75. package/template/overlays/miniprogram/src/pages/index/index.vue +299 -0
  76. package/template/overlays/miniprogram/src/pages.json +18 -0
  77. package/template/overlays/miniprogram/tsconfig.json +20 -0
  78. package/template/overlays/miniprogram/vite.config.ts +87 -0
  79. package/template/overlays/react/index.html +16 -0
  80. package/template/overlays/react/package.patch.json +18 -0
  81. package/template/overlays/react/src/App.tsx +32 -0
  82. package/template/overlays/react/src/main.tsx +74 -0
  83. package/template/overlays/react/src/vite-env.d.ts +14 -0
  84. package/template/overlays/react/tsconfig.json +20 -0
  85. package/template/overlays/react/vite.config.ts +67 -0
  86. package/template/overlays/tauri/README.md +59 -0
  87. package/template/overlays/tauri/package.patch.json +14 -0
  88. package/template/overlays/tauri/src/main.ts +58 -0
  89. package/template/overlays/tauri/src-tauri/Cargo.toml +16 -0
  90. package/template/overlays/tauri/src-tauri/build.rs +6 -0
  91. package/template/overlays/tauri/src-tauri/capabilities/default.json +7 -0
  92. package/template/overlays/tauri/src-tauri/icons/128x128.png +0 -0
  93. package/template/overlays/tauri/src-tauri/icons/128x128@2x.png +0 -0
  94. package/template/overlays/tauri/src-tauri/icons/32x32.png +0 -0
  95. package/template/overlays/tauri/src-tauri/icons/Square107x107Logo.png +0 -0
  96. package/template/overlays/tauri/src-tauri/icons/Square142x142Logo.png +0 -0
  97. package/template/overlays/tauri/src-tauri/icons/Square150x150Logo.png +0 -0
  98. package/template/overlays/tauri/src-tauri/icons/Square284x284Logo.png +0 -0
  99. package/template/overlays/tauri/src-tauri/icons/Square30x30Logo.png +0 -0
  100. package/template/overlays/tauri/src-tauri/icons/Square310x310Logo.png +0 -0
  101. package/template/overlays/tauri/src-tauri/icons/Square44x44Logo.png +0 -0
  102. package/template/overlays/tauri/src-tauri/icons/Square71x71Logo.png +0 -0
  103. package/template/overlays/tauri/src-tauri/icons/Square89x89Logo.png +0 -0
  104. package/template/overlays/tauri/src-tauri/icons/StoreLogo.png +0 -0
  105. package/template/overlays/tauri/src-tauri/icons/icon.icns +0 -0
  106. package/template/overlays/tauri/src-tauri/icons/icon.ico +0 -0
  107. package/template/overlays/tauri/src-tauri/icons/icon.png +0 -0
  108. package/template/overlays/tauri/src-tauri/src/lib.rs +26 -0
  109. package/template/overlays/tauri/src-tauri/src/main.rs +7 -0
  110. package/template/overlays/tauri/src-tauri/src/preferences.rs +50 -0
  111. package/template/overlays/tauri/src-tauri/src/save.rs +178 -0
  112. package/template/overlays/tauri/src-tauri/tauri.conf.json +35 -0
  113. package/template/overlays/vue/index.html +16 -0
  114. package/template/overlays/vue/package.patch.json +16 -0
  115. package/template/overlays/vue/src/App.vue +30 -0
  116. package/template/overlays/vue/src/main.ts +68 -0
  117. package/template/overlays/vue/src/vite-env.d.ts +24 -0
  118. package/template/overlays/vue/tsconfig.json +19 -0
  119. package/template/overlays/vue/vite.config.ts +67 -0
  120. package/template/pack-template.ps1 +65 -0
  121. package/template/template-meta.json +40 -0
  122. package/README.md +0 -3
@@ -0,0 +1,208 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * 选择层:菜单与输入两形态互斥(同一张卡片,`choices.options` 或 `choices.input` 二选一)。
4
+ *
5
+ * 层根拦住点击:选项点完不该再穿到舞台层推进一次对话。输入框的值暂存在页面状态里,
6
+ * 提交时一次性交回核心——这样「打字中的字」不会污染叙事状态。
7
+ */
8
+ import type { MiniprogramChoicesData } from "../mp/index.ts";
9
+
10
+ defineOptions({ name: "ChoiceLayer" });
11
+
12
+ defineProps<{
13
+ /** 选择层数据:菜单、输入或两者皆空的空壳 */
14
+ choices: MiniprogramChoicesData;
15
+ /** 输入框当前值(受控) */
16
+ inputValue: string;
17
+ /** 层序(核心下发的 z 表) */
18
+ zIndex: number;
19
+ }>();
20
+
21
+ const emit = defineEmits<{
22
+ choose: [target: string];
23
+ input: [value: string];
24
+ submit: [];
25
+ }>();
26
+
27
+ /** 选项点击:带回核心给的列 id */
28
+ function onChoose(target: string): void {
29
+ emit("choose", target);
30
+ }
31
+
32
+ /** 输入事件:小程序把值放在 `event.detail.value`,Web 形态放在 `target.value` */
33
+ function onInput(event: unknown): void {
34
+ const detail = (event as { detail?: { value?: unknown } }).detail;
35
+ if (typeof detail?.value === "string") {
36
+ emit("input", detail.value);
37
+ return;
38
+ }
39
+ const target = (event as { target?: { value?: unknown } }).target;
40
+ if (typeof target?.value === "string") emit("input", target.value);
41
+ }
42
+
43
+ /** 提交:回车或点按钮都走这里 */
44
+ function onSubmit(): void {
45
+ emit("submit");
46
+ }
47
+ </script>
48
+
49
+ <template>
50
+ <view
51
+ class="layer choices"
52
+ :class="choices.rootClass"
53
+ :style="{ zIndex }"
54
+ @tap.stop
55
+ >
56
+ <view v-if="choices.promptHtml" class="layer-prompt">
57
+ <rich-text :nodes="choices.promptHtml" />
58
+ </view>
59
+ <view v-if="choices.options.length" class="choice-row">
60
+ <view
61
+ v-for="option in choices.options"
62
+ :key="option.key"
63
+ class="option"
64
+ @tap="onChoose(option.target)"
65
+ >
66
+ <rich-text :nodes="option.html" />
67
+ </view>
68
+ </view>
69
+ <view v-if="choices.input" class="input-row">
70
+ <input
71
+ class="input-box"
72
+ :value="inputValue"
73
+ :placeholder="choices.input.prompt"
74
+ @input="onInput"
75
+ @confirm="onSubmit"
76
+ />
77
+ <view class="submit" @tap="onSubmit">
78
+ {{ choices.input.submitText }}
79
+ </view>
80
+ </view>
81
+ </view>
82
+ </template>
83
+
84
+ <style>
85
+ .layer {
86
+ position: absolute;
87
+ left: 0;
88
+ top: 0;
89
+ width: 100%;
90
+ }
91
+
92
+ /* 选择层:与对话层同一张卡片形态,菜单或输入二选一 */
93
+ .choices {
94
+ top: auto;
95
+ bottom: 24rpx;
96
+ left: 24rpx;
97
+ right: 24rpx;
98
+ width: auto;
99
+ padding: 32rpx 40rpx;
100
+ box-sizing: border-box;
101
+ border: 2rpx solid rgba(68, 68, 68, 0.27);
102
+ border-radius: 24rpx;
103
+ background: rgba(30, 30, 46, 0.92);
104
+ color: #e6e6f0;
105
+ }
106
+
107
+ .layer-prompt {
108
+ margin: 0 0 20rpx;
109
+ color: #9aa5ce;
110
+ text-align: center;
111
+ font-size: 30rpx;
112
+ }
113
+
114
+ .choice-row {
115
+ display: flex;
116
+ flex-wrap: wrap;
117
+ gap: 24rpx;
118
+ justify-content: center;
119
+ }
120
+
121
+ .option {
122
+ padding: 16rpx 32rpx;
123
+ border: 2rpx solid rgba(122, 162, 247, 0.4);
124
+ border-radius: 20rpx;
125
+ background: rgba(30, 30, 46, 0.8);
126
+ color: #e6e6f0;
127
+ font-size: 30rpx;
128
+ text-align: center;
129
+ }
130
+
131
+ .option:active {
132
+ background: rgba(60, 80, 130, 0.72);
133
+ }
134
+
135
+ .input-row {
136
+ display: flex;
137
+ align-items: center;
138
+ gap: 20rpx;
139
+ }
140
+
141
+ .input-box {
142
+ flex: 1;
143
+ padding: 16rpx 24rpx;
144
+ border: 2rpx solid rgba(122, 162, 247, 0.4);
145
+ border-radius: 20rpx;
146
+ background: rgba(30, 30, 46, 0.8);
147
+ color: #e6e6f0;
148
+ font-size: 30rpx;
149
+ }
150
+
151
+ .submit {
152
+ padding: 16rpx 32rpx;
153
+ border: 2rpx solid rgba(122, 162, 247, 0.4);
154
+ border-radius: 20rpx;
155
+ background: rgba(30, 30, 46, 0.8);
156
+ color: #e6e6f0;
157
+ font-size: 30rpx;
158
+ }
159
+
160
+ /* 编号选项皮肤:整行靠左(序号由模板正文自带) */
161
+ .choices.tpl-choice-numbered .option {
162
+ text-align: left;
163
+ }
164
+
165
+ /* 横屏:rpx 的基准是窗口宽度,横过来同一串 rpx 会被放大,方向相关的排版改用 px 定值 */
166
+ @media (orientation: landscape) {
167
+ .choices {
168
+ bottom: 12px;
169
+ left: 12px;
170
+ right: 12px;
171
+ padding: 12px 16px;
172
+ border-width: 1px;
173
+ border-radius: 12px;
174
+ }
175
+
176
+ .layer-prompt,
177
+ .option,
178
+ .input-box,
179
+ .submit {
180
+ font-size: 16px;
181
+ }
182
+
183
+ .choice-row {
184
+ gap: 10px;
185
+ }
186
+
187
+ .layer-prompt {
188
+ margin-bottom: 10px;
189
+ }
190
+
191
+ .option,
192
+ .submit {
193
+ padding: 8px 16px;
194
+ border-width: 1px;
195
+ border-radius: 10px;
196
+ }
197
+
198
+ .input-row {
199
+ gap: 10px;
200
+ }
201
+
202
+ .input-box {
203
+ padding: 8px 12px;
204
+ border-width: 1px;
205
+ border-radius: 10px;
206
+ }
207
+ }
208
+ </style>
@@ -0,0 +1,183 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * 对话层:普通形态三段式(说话人 / 正文 / 推进提示),NVL 形态抬升为阅读区。
4
+ *
5
+ * 两种形态共用同一个容器类(`.dialogue`),NVL 时补 `.nvl-mode` 把高度抬高并改成
6
+ * 纵向排布——与桌面面板同构,差别只在单位(这里用 rpx,横屏在样式表尾部改 px)。
7
+ *
8
+ * 点击整层推进对话:面板本体吃掉点击,不让它穿到舞台层去触发第二次推进。
9
+ */
10
+ import type {
11
+ MiniprogramDialogueData,
12
+ MiniprogramNvlData,
13
+ } from "../mp/index.ts";
14
+
15
+ defineOptions({ name: "DialogueLayer" });
16
+
17
+ defineProps<{
18
+ /** 普通对话形态的数据 */
19
+ dialogue: MiniprogramDialogueData;
20
+ /** NVL 累积形态的数据;非 null 时优先于普通形态 */
21
+ nvl: MiniprogramNvlData | null;
22
+ /** 层序(核心下发的 z 表) */
23
+ zIndex: number;
24
+ }>();
25
+
26
+ const emit = defineEmits<{ advance: [] }>();
27
+
28
+ function onAdvance(): void {
29
+ emit("advance");
30
+ }
31
+ </script>
32
+
33
+ <template>
34
+ <view
35
+ class="layer dialogue"
36
+ :class="nvl ? 'nvl-mode' : dialogue.rootClass"
37
+ :style="{ zIndex }"
38
+ @tap="onAdvance"
39
+ >
40
+ <!-- NVL 形态:累积行自上而下一行行铺开,最后一行是正在打字的当前行 -->
41
+ <view v-if="nvl" class="nvl-body">
42
+ <view v-for="(html, index) in nvl.pastLines" :key="index" class="text">
43
+ <rich-text :nodes="html" />
44
+ </view>
45
+ <view class="text">
46
+ <rich-text :nodes="nvl.typingHtml" />
47
+ </view>
48
+ </view>
49
+ <template v-else>
50
+ <view
51
+ v-if="dialogue.speakerHtml"
52
+ class="speaker"
53
+ :style="{ color: dialogue.speakerColor }"
54
+ >
55
+ <rich-text :nodes="dialogue.speakerHtml" />
56
+ </view>
57
+ <view
58
+ class="text"
59
+ :style="
60
+ dialogue.bodyFont ? { fontFamily: dialogue.bodyFont } : undefined
61
+ "
62
+ >
63
+ <rich-text :nodes="dialogue.bodyHtml" />
64
+ </view>
65
+ <view v-if="dialogue.hintHtml" class="advance-hint">
66
+ <rich-text :nodes="dialogue.hintHtml" />
67
+ </view>
68
+ </template>
69
+ </view>
70
+ </template>
71
+
72
+ <style>
73
+ .layer {
74
+ position: absolute;
75
+ left: 0;
76
+ top: 0;
77
+ width: 100%;
78
+ }
79
+
80
+ /* 对话层:贴底居中的卡片。
81
+ 高度按视口百分比给下限:竖屏约等于桌面的 104~150px,横屏不会因为 rpx 基准换成大宽度而撑破屏。 */
82
+ .dialogue {
83
+ top: auto;
84
+ bottom: 24rpx;
85
+ left: 24rpx;
86
+ right: 24rpx;
87
+ width: auto;
88
+ min-height: 16%;
89
+ padding: 32rpx 40rpx;
90
+ box-sizing: border-box;
91
+ border: 2rpx solid rgba(68, 68, 68, 0.27);
92
+ border-radius: 24rpx;
93
+ background: rgba(30, 30, 46, 0.8);
94
+ color: #e6e6f0;
95
+ }
96
+
97
+ /* NVL 累积层形态:容器抬升为阅读区(累积文本自上而下滚动) */
98
+ .dialogue.nvl-mode {
99
+ min-height: 46%;
100
+ display: flex;
101
+ flex-direction: column;
102
+ }
103
+
104
+ .nvl-body {
105
+ flex: 1;
106
+ display: flex;
107
+ flex-direction: column;
108
+ gap: 12rpx;
109
+ overflow-y: auto;
110
+ min-height: 0;
111
+ }
112
+
113
+ .speaker {
114
+ margin: 0 0 16rpx;
115
+ font-weight: 600;
116
+ font-size: 30rpx;
117
+ letter-spacing: 1rpx;
118
+ }
119
+
120
+ .text {
121
+ margin: 0;
122
+ font-size: 34rpx;
123
+ line-height: 1.7;
124
+ white-space: pre-wrap;
125
+ }
126
+
127
+ /* 推进指示器:右下角闪烁(桌面对应 `.advance-hint`) */
128
+ .advance-hint {
129
+ position: absolute;
130
+ right: 24rpx;
131
+ bottom: 16rpx;
132
+ font-size: 26rpx;
133
+ color: #7aa2f7;
134
+ opacity: 0.9;
135
+ animation: blink 1.2s infinite;
136
+ }
137
+
138
+ /* 对话模板皮肤:居中独白形态(与桌面的 .tpl-center 同名同色) */
139
+ .dialogue.tpl-center {
140
+ display: flex;
141
+ flex-direction: column;
142
+ justify-content: center;
143
+ align-items: center;
144
+ text-align: center;
145
+ font-size: 1.15em;
146
+ background: rgba(22, 22, 30, 0.8);
147
+ border-color: rgba(122, 162, 247, 0.27);
148
+ }
149
+
150
+ /* 横屏:rpx 的基准是窗口宽度,横过来同一串 rpx 会被放大,方向相关的排版改用 px 定值 */
151
+ @media (orientation: landscape) {
152
+ .dialogue {
153
+ bottom: 12px;
154
+ left: 12px;
155
+ right: 12px;
156
+ padding: 12px 16px;
157
+ border-width: 1px;
158
+ border-radius: 12px;
159
+ }
160
+
161
+ .speaker {
162
+ margin: 0 0 6px;
163
+ font-size: 15px;
164
+ letter-spacing: 0;
165
+ }
166
+
167
+ .text {
168
+ font-size: 16px;
169
+ }
170
+
171
+ .advance-hint {
172
+ right: 12px;
173
+ bottom: 8px;
174
+ font-size: 13px;
175
+ }
176
+ }
177
+
178
+ @keyframes blink {
179
+ 50% {
180
+ opacity: 0.2;
181
+ }
182
+ }
183
+ </style>
@@ -0,0 +1,44 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * 递归节点树:把 `mp/view.ts` 算出的描述树铺成节点。
4
+ *
5
+ * 小程序没有 DOM,渲染就是「数据 + 模板」:这一层只做映射,不拼样式(行内样式串已在数据侧
6
+ * 算好)。组件自带样式——小程序组件默认样式隔离,页面样式不会穿透进来。自引用靠文件名。
7
+ */
8
+ import type { MiniNode } from "../mp/view.ts";
9
+
10
+ defineProps<{ nodes: MiniNode[] }>();
11
+ </script>
12
+
13
+ <template>
14
+ <template v-for="node in nodes" :key="node.key">
15
+ <image
16
+ v-if="node.kind === 'image'"
17
+ class="node node-image"
18
+ :class="node.className"
19
+ :style="node.style"
20
+ :src="node.src"
21
+ mode="widthFix"
22
+ />
23
+ <view
24
+ v-else
25
+ class="node"
26
+ :class="[`node-${node.kind}`, node.className]"
27
+ :style="node.style"
28
+ >
29
+ <text v-if="node.text">{{ node.text }}</text>
30
+ <rich-text v-else-if="node.html" :nodes="node.html" />
31
+ <NodeTree v-if="node.children" :nodes="node.children" />
32
+ </view>
33
+ </template>
34
+ </template>
35
+
36
+ <style>
37
+ .node {
38
+ position: absolute;
39
+ }
40
+
41
+ .node-image {
42
+ display: block;
43
+ }
44
+ </style>
@@ -0,0 +1,87 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * 通知层:贴右上角的逐条气泡,不吃点击(挡住下面的层会让舞台点不动)。
4
+ *
5
+ * 气泡的皮肤由 `rootClass` 决定(`tpl-notify-warning` / `tpl-notify-error`),
6
+ * 配色与桌面那份同名同色——两份实现同一套视觉语言。
7
+ */
8
+ import type { MiniprogramNoticeData } from "../mp/index.ts";
9
+
10
+ defineOptions({ name: "NoticeLayer" });
11
+
12
+ defineProps<{
13
+ /** 当前显示的通知列表(新通知在末尾) */
14
+ notices: MiniprogramNoticeData[];
15
+ /** 层序(核心下发的 z 表) */
16
+ zIndex: number;
17
+ }>();
18
+ </script>
19
+
20
+ <template>
21
+ <view class="layer notices" :style="{ zIndex }">
22
+ <view
23
+ v-for="notice in notices"
24
+ :key="notice.key"
25
+ class="notice"
26
+ :class="notice.rootClass"
27
+ >
28
+ <rich-text :nodes="notice.bodyHtml" />
29
+ </view>
30
+ </view>
31
+ </template>
32
+
33
+ <style>
34
+ .layer {
35
+ position: absolute;
36
+ left: 0;
37
+ top: 0;
38
+ width: 100%;
39
+ }
40
+
41
+ /* 通知层:贴右上角的一条条气泡(桌面对应 NotificationLayer),不吃点击 */
42
+ .notices {
43
+ top: 24rpx;
44
+ left: auto;
45
+ right: 24rpx;
46
+ width: auto;
47
+ max-width: 560rpx;
48
+ display: flex;
49
+ flex-direction: column;
50
+ align-items: flex-end;
51
+ gap: 16rpx;
52
+ pointer-events: none;
53
+ }
54
+
55
+ .notice {
56
+ padding: 16rpx 28rpx;
57
+ border-radius: 16rpx;
58
+ background: rgba(42, 47, 69, 0.87);
59
+ color: #9ece6a;
60
+ font-size: 26rpx;
61
+ }
62
+
63
+ /* 通知层皮肤:按 tone 换色(与桌面的 .tpl-notify-* 同名同色) */
64
+ .notice.tpl-notify-warning {
65
+ color: #e0af68;
66
+ }
67
+
68
+ .notice.tpl-notify-error {
69
+ color: #f7768e;
70
+ }
71
+
72
+ /* 横屏:rpx 的基准是窗口宽度,横过来同一串 rpx 会被放大,方向相关的排版改用 px 定值 */
73
+ @media (orientation: landscape) {
74
+ .notices {
75
+ top: 12px;
76
+ right: 12px;
77
+ max-width: 320px;
78
+ gap: 8px;
79
+ }
80
+
81
+ .notice {
82
+ padding: 8px 14px;
83
+ border-radius: 8px;
84
+ font-size: 13px;
85
+ }
86
+ }
87
+ </style>
@@ -0,0 +1,168 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * 存/读入口:左上角两颗按钮 + 打开后的槽位面板。
4
+ *
5
+ * 组件只管渲染与转发:槽位视图由页面在打开时从宿主取(`loadHostSlotViews`),点选判据与
6
+ * 存读命令都在宿主侧(`chooseHostSlot`),这里不判断空槽、不碰引擎——「谁能点」与「点了
7
+ * 做什么」各只有一处实现。
8
+ *
9
+ * 面板形态与桌面宿主的槽位面板同构(列表 + 标题/时间),移动端换成 rpx 与更大的点击面。
10
+ */
11
+ import {
12
+ canChooseSlot,
13
+ type SlotPanelMode,
14
+ type SlotView,
15
+ } from "../mp/slots.ts";
16
+
17
+ const props = defineProps<{
18
+ /** 面板模式:null = 收起(只留入口条) */
19
+ mode: SlotPanelMode | null;
20
+ /** 逐槽视图(打开面板时由页面拉取) */
21
+ views: SlotView[];
22
+ }>();
23
+
24
+ const emit = defineEmits<{
25
+ /** 点入口条:打开对应模式的面板 */
26
+ open: [mode: SlotPanelMode];
27
+ /** 点槽位:交给宿主执行存/读 */
28
+ choose: [view: SlotView];
29
+ /** 关闭面板 */
30
+ close: [];
31
+ }>();
32
+
33
+ /** 槽位能否点:判据与桌面面板同一份 */
34
+ function pickable(view: SlotView): boolean {
35
+ return canChooseSlot(view, props.mode);
36
+ }
37
+
38
+ /** 槽位上的一行说明:有档给标题,没档写「空槽」 */
39
+ function slotHint(view: SlotView): string {
40
+ if (view.empty) return "空槽";
41
+ const title = (view.title ?? "").trim();
42
+ return title === "" ? "已有存档" : title;
43
+ }
44
+
45
+ /** 点槽位:不可点(读档模式的空槽)就什么都不做 */
46
+ function pick(view: SlotView): void {
47
+ if (pickable(view)) emit("choose", view);
48
+ }
49
+ </script>
50
+
51
+ <template>
52
+ <view class="save-load">
53
+ <view class="entries">
54
+ <view class="entry" @tap.stop="emit('open', 'save')">存</view>
55
+ <view class="entry" @tap.stop="emit('open', 'load')">读</view>
56
+ </view>
57
+
58
+ <view v-if="mode" class="sheet">
59
+ <view class="sheet-title">{{
60
+ mode === "save" ? "写入存档" : "读取存档"
61
+ }}</view>
62
+ <view class="slots">
63
+ <view
64
+ v-for="view in views"
65
+ :key="view.id"
66
+ class="slot"
67
+ :class="{ 'slot-disabled': !pickable(view) }"
68
+ @tap.stop="pick(view)"
69
+ >
70
+ <view class="slot-label">{{ view.label }}</view>
71
+ <view class="slot-hint">{{ slotHint(view) }}</view>
72
+ </view>
73
+ </view>
74
+ <view class="close" @tap.stop="emit('close')">关闭</view>
75
+ </view>
76
+ </view>
77
+ </template>
78
+
79
+ <style>
80
+ /* 入口与面板都在左上:右上角归通知层,底部归对话与选择层 */
81
+ .save-load {
82
+ position: absolute;
83
+ left: 24rpx;
84
+ top: 24rpx;
85
+ pointer-events: auto;
86
+ }
87
+
88
+ .entries {
89
+ display: flex;
90
+ gap: 16rpx;
91
+ }
92
+
93
+ .entry,
94
+ .close {
95
+ padding: 12rpx 28rpx;
96
+ border: 2rpx solid rgba(122, 162, 247, 0.4);
97
+ border-radius: 16rpx;
98
+ background: rgba(30, 30, 46, 0.72);
99
+ color: #e6e6f0;
100
+ font-size: 26rpx;
101
+ text-align: center;
102
+ }
103
+
104
+ .entry:active,
105
+ .close:active {
106
+ background: rgba(60, 80, 130, 0.72);
107
+ }
108
+
109
+ /* 面板:卡片形态与对话层同源(同一批色值),点击面按移动端放大 */
110
+ .sheet {
111
+ margin-top: 16rpx;
112
+ width: 520rpx;
113
+ padding: 24rpx 24rpx 16rpx;
114
+ box-sizing: border-box;
115
+ border: 2rpx solid rgba(68, 68, 68, 0.27);
116
+ border-radius: 24rpx;
117
+ background: rgba(30, 30, 46, 0.92);
118
+ color: #e6e6f0;
119
+ }
120
+
121
+ .sheet-title {
122
+ margin: 0 0 16rpx;
123
+ color: #9aa5ce;
124
+ font-size: 26rpx;
125
+ text-align: center;
126
+ }
127
+
128
+ .slots {
129
+ display: flex;
130
+ flex-direction: column;
131
+ gap: 12rpx;
132
+ max-height: 520rpx;
133
+ overflow-y: auto;
134
+ }
135
+
136
+ .slot {
137
+ display: flex;
138
+ align-items: baseline;
139
+ justify-content: space-between;
140
+ gap: 16rpx;
141
+ padding: 16rpx 20rpx;
142
+ border: 2rpx solid rgba(122, 162, 247, 0.28);
143
+ border-radius: 16rpx;
144
+ background: rgba(30, 30, 46, 0.8);
145
+ }
146
+
147
+ .slot:active {
148
+ background: rgba(60, 80, 130, 0.72);
149
+ }
150
+
151
+ /* 空槽在读档模式下置灰:引擎读不到档必失败,点了只会多一条提示 */
152
+ .slot-disabled {
153
+ opacity: 0.45;
154
+ }
155
+
156
+ .slot-label {
157
+ font-size: 28rpx;
158
+ }
159
+
160
+ .slot-hint {
161
+ color: #9aa5ce;
162
+ font-size: 24rpx;
163
+ }
164
+
165
+ .close {
166
+ margin-top: 16rpx;
167
+ }
168
+ </style>