xinyu-pro 0.21.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/.env.example +21 -0
- package/README.md +36 -0
- package/app/api/chat/route.ts +84 -0
- package/app/api/generate-svg/route.ts +171 -0
- package/app/api/generate-theme/route.ts +137 -0
- package/app/api/plugins/bindings/route.ts +173 -0
- package/app/api/plugins/export/route.ts +122 -0
- package/app/api/plugins/export-xye/route.ts +156 -0
- package/app/api/plugins/files/route.ts +146 -0
- package/app/api/plugins/files-list/route.ts +168 -0
- package/app/api/plugins/files-upload/route.ts +101 -0
- package/app/api/plugins/files-write/route.ts +272 -0
- package/app/api/plugins/import/route.ts +140 -0
- package/app/api/plugins/import-package/route.ts +231 -0
- package/app/api/plugins/resources/route.ts +109 -0
- package/app/api/plugins/route.ts +308 -0
- package/app/api/plugins/scan/route.ts +280 -0
- package/app/api/plugins/storage/route.ts +146 -0
- package/app/api/sessions/route.ts +165 -0
- package/app/api/settings/route.ts +40 -0
- package/app/api/suggest-fields/route.ts +129 -0
- package/app/api/templates/route.ts +159 -0
- package/app/api/test-api/route.ts +63 -0
- package/app/editor/page.tsx +1466 -0
- package/app/extensions/create/page.tsx +1422 -0
- package/app/extensions/edit/[id]/page.tsx +2342 -0
- package/app/extensions/page.tsx +1572 -0
- package/app/extensions/tutorial/page.tsx +4258 -0
- package/app/favicon.ico +0 -0
- package/app/fonts/GeistMonoVF.woff +0 -0
- package/app/fonts/GeistVF.woff +0 -0
- package/app/game/[id]/page.tsx +996 -0
- package/app/globals.css +3 -0
- package/app/layout.tsx +26 -0
- package/app/loading.tsx +26 -0
- package/app/page.tsx +345 -0
- package/app/settings/page.tsx +1490 -0
- package/bin/cli.js +262 -0
- package/components/ChatInput.tsx +106 -0
- package/components/ChatWindow.tsx +52 -0
- package/components/FullPageLoader.tsx +107 -0
- package/components/LoadingDots.tsx +20 -0
- package/components/MathCurveLoader.tsx +173 -0
- package/components/MessageBubble.tsx +147 -0
- package/components/WorldCardPreview.tsx +98 -0
- package/components/WorldCardUploader.tsx +58 -0
- package/components/ui/ConfirmDialog.tsx +135 -0
- package/components/ui/PageHeader.tsx +99 -0
- package/components/ui/PermissionConflictDialog.tsx +206 -0
- package/components/ui/PluginConfigForm.tsx +192 -0
- package/components/ui/PluginFloatingLayer.tsx +52 -0
- package/components/ui/PluginIcon.tsx +53 -0
- package/components/ui/PluginModalRenderer.tsx +185 -0
- package/components/ui/PluginProvider.tsx +1038 -0
- package/components/ui/PluginSlotRenderer.tsx +76 -0
- package/components/ui/ThemeCustomizer.tsx +174 -0
- package/components/ui/ThemeProvider.tsx +125 -0
- package/components/ui/ThemeSwitcher.tsx +140 -0
- package/components/ui/ToastProvider.tsx +141 -0
- package/lib/builtin-plugins.ts +11 -0
- package/lib/db-init.ts +35 -0
- package/lib/db.ts +244 -0
- package/lib/manifest-parser.ts +185 -0
- package/lib/parseWorldCard.ts +110 -0
- package/lib/plugin-dom-sandbox.ts +327 -0
- package/lib/plugin-events.ts +88 -0
- package/lib/plugin-files.ts +186 -0
- package/lib/plugin-html-sanitizer.ts +79 -0
- package/lib/plugin-resource-tracker.ts +175 -0
- package/lib/plugin-runtime.ts +2287 -0
- package/lib/plugin-security.ts +151 -0
- package/lib/plugin-types.ts +416 -0
- package/lib/prompt-builder.ts +55 -0
- package/lib/router-history.ts +119 -0
- package/lib/storage.ts +381 -0
- package/lib/themes.ts +129 -0
- package/lib/types.ts +117 -0
- package/lib/version.ts +55 -0
- package/next.config.mjs +43 -0
- package/package.json +56 -0
- package/plugins/xinyu.bag-system.xye +0 -0
- package/plugins/xinyu.cache-optimizer.xye +0 -0
- package/plugins/xinyu.dice-arbiter.xye +0 -0
- package/plugins/xinyu.game-auto-start-choices.xye +0 -0
- package/plugins/xinyu.markdown-render.xye +0 -0
- package/plugins/xinyu.slot-ui-beautify.xye +0 -0
- package/plugins/xinyu.world-info.xye +0 -0
- package/postcss.config.mjs +8 -0
- package/public/templates/atlantis.svg +63 -0
- package/public/templates/cyber-city.svg +68 -0
- package/public/templates/jianghu.svg +69 -0
- package/public/templates//351/255/224/346/263/225/344/270/226/347/225/214.svg +137 -0
- package/styles/themes.css +111 -0
- package/tailwind.config.ts +18 -0
- package/tsconfig.json +26 -0
- package/version.json +6 -0
package/app/globals.css
ADDED
package/app/layout.tsx
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import React from "react";
|
|
3
|
+
import './globals.css';
|
|
4
|
+
import '@/styles/themes.css';
|
|
5
|
+
import '@fortawesome/fontawesome-free/css/all.min.css';
|
|
6
|
+
import { ThemeProvider } from '@/components/ui/ThemeProvider';
|
|
7
|
+
import { ToastProvider } from '@/components/ui/ToastProvider';
|
|
8
|
+
|
|
9
|
+
export const metadata: Metadata = {
|
|
10
|
+
title: '星语 · xinyu-pro',
|
|
11
|
+
description: '基于 AI 大模型的沉浸式角色扮演游戏平台',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default function RootLayout({
|
|
15
|
+
children,
|
|
16
|
+
}: Readonly<{
|
|
17
|
+
children: React.ReactNode;
|
|
18
|
+
}>) {
|
|
19
|
+
return (
|
|
20
|
+
<html lang="zh-CN">
|
|
21
|
+
<body className="antialiased min-h-screen">
|
|
22
|
+
<ThemeProvider><ToastProvider><React.Suspense fallback={null}>{children}</React.Suspense></ToastProvider></ThemeProvider>
|
|
23
|
+
</body>
|
|
24
|
+
</html>
|
|
25
|
+
);
|
|
26
|
+
}
|
package/app/loading.tsx
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// app/loading.tsx - 全局路由级加载 UI
|
|
2
|
+
// Next.js App Router 会在页面切换时自动显示此组件
|
|
3
|
+
|
|
4
|
+
import MathCurveLoader from '@/components/MathCurveLoader';
|
|
5
|
+
|
|
6
|
+
export default function Loading() {
|
|
7
|
+
return (
|
|
8
|
+
<div
|
|
9
|
+
className="flex flex-col items-center justify-center gap-4"
|
|
10
|
+
style={{
|
|
11
|
+
minHeight: '100vh',
|
|
12
|
+
backgroundColor: 'var(--color-bg-primary)',
|
|
13
|
+
opacity: 1,
|
|
14
|
+
transition: 'opacity 0.2s ease',
|
|
15
|
+
}}
|
|
16
|
+
>
|
|
17
|
+
<MathCurveLoader size={80} strokeWidthScale={0.8} />
|
|
18
|
+
<p
|
|
19
|
+
className="text-sm select-none"
|
|
20
|
+
style={{ color: 'var(--color-text-muted)' }}
|
|
21
|
+
>
|
|
22
|
+
加载中...
|
|
23
|
+
</p>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
package/app/page.tsx
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { useRouterHistory } from '@/lib/router-history';
|
|
6
|
+
import { WorldCardField, WorldCardTemplate } from '@/lib/types';
|
|
7
|
+
import { WorldCardUploader } from '@/components/WorldCardUploader';
|
|
8
|
+
import { ThemeSwitcher } from '@/components/ui/ThemeSwitcher';
|
|
9
|
+
import ConfirmDialog from '@/components/ui/ConfirmDialog';
|
|
10
|
+
import { getGameSessions, deleteGameSession, getWorldTemplates, deleteWorldTemplate, saveWorldTemplate, saveGameSession, getPlugins, getPluginBindings, upsertPluginBinding, StoredSession } from '@/lib/storage';
|
|
11
|
+
import { parseWorldCardRaw, fieldsToWorldSetting } from '@/lib/parseWorldCard';
|
|
12
|
+
import FullPageLoader from '@/components/FullPageLoader';
|
|
13
|
+
|
|
14
|
+
export default function HomePage() {
|
|
15
|
+
const router = useRouter();
|
|
16
|
+
const { navigate } = useRouterHistory();
|
|
17
|
+
const [sessions, setSessions] = useState<StoredSession[]>([]);
|
|
18
|
+
const [templates, setTemplates] = useState<WorldCardTemplate[]>([]);
|
|
19
|
+
const [availablePlugins, setAvailablePlugins] = useState<Array<{id: string; name: string}>>([]);
|
|
20
|
+
const [loading, setLoading] = useState(true);
|
|
21
|
+
const [fileError, setFileError] = useState<string | null>(null);
|
|
22
|
+
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
23
|
+
const [confirmConfig, setConfirmConfig] = useState<{
|
|
24
|
+
title: string;
|
|
25
|
+
message: string;
|
|
26
|
+
danger?: boolean;
|
|
27
|
+
confirmText?: string;
|
|
28
|
+
cancelText?: string;
|
|
29
|
+
ignoreText?: string;
|
|
30
|
+
onConfirm: () => void;
|
|
31
|
+
onIgnore?: () => void;
|
|
32
|
+
}>({ title: '', message: '', onConfirm: () => {} });
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
(async () => {
|
|
36
|
+
try {
|
|
37
|
+
setSessions(await getGameSessions());
|
|
38
|
+
setTemplates(await getWorldTemplates());
|
|
39
|
+
setAvailablePlugins(await getPlugins());
|
|
40
|
+
} finally {
|
|
41
|
+
setLoading(false);
|
|
42
|
+
}
|
|
43
|
+
})();
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
const handleFileLoaded = async (file: File) => {
|
|
47
|
+
setFileError(null);
|
|
48
|
+
try {
|
|
49
|
+
const svgText = await file.text();
|
|
50
|
+
const rawData = parseWorldCardRaw(svgText);
|
|
51
|
+
const templateId = 'temp-' + Date.now().toString(36);
|
|
52
|
+
// 提取 _plugins 字段
|
|
53
|
+
const plugins = Array.isArray(rawData._plugins)
|
|
54
|
+
? rawData._plugins.filter((p): p is string => typeof p === 'string')
|
|
55
|
+
: [];
|
|
56
|
+
// 过滤掉 _ 开头的内部字段
|
|
57
|
+
const fields: WorldCardField[] = Object.entries(rawData)
|
|
58
|
+
.filter(([key]) => !key.startsWith('_'))
|
|
59
|
+
.map(([key, value], i) => ({
|
|
60
|
+
id: `f${i}`, key, label: key,
|
|
61
|
+
type: Array.isArray(value) ? 'text' : typeof value === 'boolean' ? 'checkbox' : 'text',
|
|
62
|
+
value: Array.isArray(value) ? value.join('、') : String(value),
|
|
63
|
+
editableBeforeGame: true,
|
|
64
|
+
}));
|
|
65
|
+
const template: WorldCardTemplate = {
|
|
66
|
+
id: templateId, name: String(rawData.title || '未命名世界'),
|
|
67
|
+
svgContent: svgText, fields,
|
|
68
|
+
plugins,
|
|
69
|
+
createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
|
|
70
|
+
};
|
|
71
|
+
await saveWorldTemplate(template);
|
|
72
|
+
router.push(`/editor?mode=edit&templateId=${templateId}`);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
setFileError(err instanceof Error ? err.message : '解析世界卡片失败');
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const handleDeleteSession = useCallback((id: string) => {
|
|
79
|
+
setConfirmConfig({
|
|
80
|
+
title: '删除游戏记录',
|
|
81
|
+
message: '确定要删除这条游戏记录吗?',
|
|
82
|
+
danger: true,
|
|
83
|
+
onConfirm: async () => { await deleteGameSession(id); setSessions(await getGameSessions()); setConfirmOpen(false); },
|
|
84
|
+
});
|
|
85
|
+
setConfirmOpen(true);
|
|
86
|
+
}, []);
|
|
87
|
+
const handleTemplateClick = (templateId: string) => { router.push(`/editor?mode=edit&templateId=${templateId}`); };
|
|
88
|
+
|
|
89
|
+
/** 快速开始游戏(从主界面直接开始,携带模板配置的插件) */
|
|
90
|
+
const handleQuickStart = useCallback(async (tpl: WorldCardTemplate, forceStart = false) => {
|
|
91
|
+
const pluginIds = tpl.plugins || [];
|
|
92
|
+
const installedIds = new Set(availablePlugins.map(p => p.id));
|
|
93
|
+
const missingPlugins = pluginIds.filter(pid => !installedIds.has(pid));
|
|
94
|
+
|
|
95
|
+
if (missingPlugins.length > 0 && !forceStart) {
|
|
96
|
+
// 有插件缺失,弹窗提示
|
|
97
|
+
const missingList = missingPlugins.map(pid => `• ${pid}`).join('\n');
|
|
98
|
+
setConfirmConfig({
|
|
99
|
+
title: '插件未安装',
|
|
100
|
+
message: `以下 ${missingPlugins.length} 个插件未安装:\n${missingList}\n\n这些插件的功能将不可用,是否继续?`,
|
|
101
|
+
danger: true,
|
|
102
|
+
confirmText: '前往安装',
|
|
103
|
+
cancelText: '取消',
|
|
104
|
+
ignoreText: '忽略并继续',
|
|
105
|
+
onConfirm: () => {
|
|
106
|
+
setConfirmOpen(false);
|
|
107
|
+
navigate('/extensions');
|
|
108
|
+
},
|
|
109
|
+
onIgnore: () => {
|
|
110
|
+
setConfirmOpen(false);
|
|
111
|
+
handleQuickStart(tpl, true); // 强制开始
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
setConfirmOpen(true);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 开始游戏(过滤掉缺失的插件)
|
|
119
|
+
try {
|
|
120
|
+
const validPlugins = forceStart ? pluginIds.filter(pid => installedIds.has(pid)) : pluginIds;
|
|
121
|
+
const worldSetting = {
|
|
122
|
+
...fieldsToWorldSetting(tpl.fields),
|
|
123
|
+
_plugins: validPlugins,
|
|
124
|
+
};
|
|
125
|
+
const gameId = tpl.id + '-game-' + Date.now().toString(36);
|
|
126
|
+
const session: StoredSession = {
|
|
127
|
+
id: gameId, worldSetting, messages: [],
|
|
128
|
+
createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
|
|
129
|
+
};
|
|
130
|
+
await saveGameSession(session);
|
|
131
|
+
|
|
132
|
+
// 为插件创建世界级绑定(使用全局绑定的配置作为默认值)
|
|
133
|
+
if (validPlugins.length > 0) {
|
|
134
|
+
const globalBindings = await getPluginBindings('global');
|
|
135
|
+
const globalBindingMap = new Map(globalBindings.map(b => [b.extensionId, b]));
|
|
136
|
+
for (const pid of validPlugins) {
|
|
137
|
+
const globalBinding = globalBindingMap.get(pid);
|
|
138
|
+
await upsertPluginBinding({
|
|
139
|
+
extensionId: pid, scope: 'world', worldId: gameId,
|
|
140
|
+
enabled: true, config: globalBinding?.config || {}, sortOrder: 0,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
router.push(`/game/${gameId}`);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.error('快速开始游戏失败:', err);
|
|
148
|
+
}
|
|
149
|
+
}, [router, availablePlugins]);
|
|
150
|
+
|
|
151
|
+
const handleDeleteTemplate = useCallback((id: string) => {
|
|
152
|
+
setConfirmConfig({
|
|
153
|
+
title: '删除模板',
|
|
154
|
+
message: '确定要删除这个世界卡片模板吗?',
|
|
155
|
+
danger: true,
|
|
156
|
+
onConfirm: async () => { await deleteWorldTemplate(id); setTemplates(await getWorldTemplates()); setConfirmOpen(false); },
|
|
157
|
+
});
|
|
158
|
+
setConfirmOpen(true);
|
|
159
|
+
}, []);
|
|
160
|
+
|
|
161
|
+
if (loading) {
|
|
162
|
+
return <FullPageLoader />;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<>
|
|
167
|
+
<div className="min-h-screen flex flex-col" style={{ backgroundColor: 'var(--color-bg-primary)' }}>
|
|
168
|
+
{/* 顶部导航 */}
|
|
169
|
+
<header className="flex items-center justify-between px-4 sm:px-6 py-2.5 border-b shrink-0"
|
|
170
|
+
style={{ borderColor: 'var(--color-border)', backgroundColor: 'var(--color-bg-secondary)' }}>
|
|
171
|
+
<h1 className="text-lg font-bold tracking-wide" style={{ color: 'var(--color-accent)' }}>✦ 星语</h1>
|
|
172
|
+
<div className="flex items-center gap-2">
|
|
173
|
+
<ThemeSwitcher />
|
|
174
|
+
<button onClick={() => navigate('/extensions')}
|
|
175
|
+
className="flex items-center gap-1.5 px-3 py-2 rounded-lg border transition-colors"
|
|
176
|
+
style={{ backgroundColor: 'var(--color-bg-tertiary)', borderColor: 'var(--color-border)', color: 'var(--color-text-primary)' }}
|
|
177
|
+
title="插件管理">
|
|
178
|
+
<i className="fa-solid fa-puzzle-piece" style={{ fontSize: '14px' }} />
|
|
179
|
+
<span className="text-sm hidden sm:inline">插件</span>
|
|
180
|
+
</button>
|
|
181
|
+
<button onClick={() => router.push('/settings')}
|
|
182
|
+
className="flex items-center gap-1.5 px-3 py-2 rounded-lg border transition-colors"
|
|
183
|
+
style={{ backgroundColor: 'var(--color-bg-tertiary)', borderColor: 'var(--color-border)', color: 'var(--color-text-primary)' }}
|
|
184
|
+
title="设置">
|
|
185
|
+
<i className="fa-solid fa-gear" style={{ fontSize: '14px' }} />
|
|
186
|
+
<span className="text-sm hidden sm:inline">设置</span>
|
|
187
|
+
</button>
|
|
188
|
+
</div>
|
|
189
|
+
</header>
|
|
190
|
+
|
|
191
|
+
{/* 操作栏:紧凑一行 */}
|
|
192
|
+
<div className="shrink-0 px-4 sm:px-6 py-3 flex flex-wrap items-center justify-center gap-2"
|
|
193
|
+
style={{ borderBottom: '1px solid var(--color-border)' }}>
|
|
194
|
+
<WorldCardUploader onFileLoaded={handleFileLoaded} />
|
|
195
|
+
<button onClick={() => router.push('/editor?mode=create')}
|
|
196
|
+
className="flex items-center gap-2 px-4 py-2.5 rounded-lg text-sm font-medium border transition-colors"
|
|
197
|
+
style={{ borderColor: 'var(--color-accent)', color: 'var(--color-accent)', backgroundColor: 'transparent' }}>
|
|
198
|
+
<i className="fa-solid fa-plus" style={{ fontSize: '12px' }} />
|
|
199
|
+
创建新的世界卡片
|
|
200
|
+
</button>
|
|
201
|
+
{fileError && (
|
|
202
|
+
<span className="text-xs px-2 py-1 rounded" style={{ color: '#ef4444', backgroundColor: 'rgba(220,38,38,0.1)' }}>
|
|
203
|
+
{fileError}
|
|
204
|
+
</span>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
{/* 主内容区 */}
|
|
209
|
+
<main className="flex-1 overflow-auto px-4 sm:px-6 py-4">
|
|
210
|
+
<div className="flex gap-6 h-full">
|
|
211
|
+
{/* 左侧:模板库 */}
|
|
212
|
+
<div className="flex-1 min-w-0">
|
|
213
|
+
<h3 className="text-sm font-medium mb-3 text-center" style={{ color: 'var(--color-text-muted)' }}>
|
|
214
|
+
模板库
|
|
215
|
+
{templates.length > 0 && (
|
|
216
|
+
<span className="ml-1 px-1.5 py-0.5 rounded text-xs"
|
|
217
|
+
style={{ backgroundColor: 'var(--color-bg-tertiary)', color: 'var(--color-text-muted)' }}>
|
|
218
|
+
{templates.length}
|
|
219
|
+
</span>
|
|
220
|
+
)}
|
|
221
|
+
</h3>
|
|
222
|
+
{templates.length > 0 ? (
|
|
223
|
+
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3">
|
|
224
|
+
{templates.map((tpl) => (
|
|
225
|
+
<div key={tpl.id}
|
|
226
|
+
className="relative rounded-xl border cursor-pointer transition-all group overflow-hidden"
|
|
227
|
+
style={{ backgroundColor: 'var(--color-bg-secondary)', borderColor: 'var(--color-border)' }}
|
|
228
|
+
onClick={() => handleTemplateClick(tpl.id)}>
|
|
229
|
+
{/* SVG 缩略图 */}
|
|
230
|
+
<div className="w-full aspect-[5/7] overflow-hidden relative" style={{ backgroundColor: 'var(--color-bg-tertiary)' }}>
|
|
231
|
+
{tpl.svgContent ? (
|
|
232
|
+
<iframe srcDoc={tpl.svgContent}
|
|
233
|
+
className="w-full h-full pointer-events-none transition-all duration-300 group-hover:blur-sm group-hover:scale-105"
|
|
234
|
+
style={{ border: 'none', display: 'block' }}
|
|
235
|
+
title={tpl.name} />
|
|
236
|
+
) : (
|
|
237
|
+
<div className="w-full h-full flex items-center justify-center">
|
|
238
|
+
<i className="fa-regular fa-image" style={{ color: 'var(--color-text-muted)', fontSize: '24px', opacity: 0.4 }} />
|
|
239
|
+
</div>
|
|
240
|
+
)}
|
|
241
|
+
{/* Hover 覆盖层 */}
|
|
242
|
+
<div className="absolute inset-0 flex flex-col items-center justify-center gap-2 p-3 opacity-0 group-hover:opacity-100 transition-opacity duration-300 z-10"
|
|
243
|
+
style={{ backgroundColor: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(4px)' }}>
|
|
244
|
+
<div className="text-sm font-bold text-white text-center leading-tight">{tpl.name}</div>
|
|
245
|
+
<div className="w-8 h-px" style={{ backgroundColor: 'rgba(255,255,255,0.3)' }} />
|
|
246
|
+
<div className="text-xs text-center" style={{ color: 'rgba(255,255,255,0.7)' }}>
|
|
247
|
+
{tpl.fields.length} 个字段
|
|
248
|
+
</div>
|
|
249
|
+
<div className="text-xs" style={{ color: 'rgba(255,255,255,0.5)' }}>
|
|
250
|
+
{new Date(tpl.updatedAt).toLocaleDateString()}
|
|
251
|
+
</div>
|
|
252
|
+
<div className="flex items-center gap-2 mt-1">
|
|
253
|
+
<button onClick={(e) => { e.stopPropagation(); handleQuickStart(tpl); }}
|
|
254
|
+
className="px-2.5 py-1 rounded-md text-xs font-medium transition-colors"
|
|
255
|
+
style={{ backgroundColor: 'rgba(255,255,255,0.2)', color: '#fff' }}>
|
|
256
|
+
<i className="fa-solid fa-play mr-1" style={{ fontSize: '10px' }} />开始游戏
|
|
257
|
+
</button>
|
|
258
|
+
<button onClick={(e) => { e.stopPropagation(); router.push(`/editor?mode=editTemplate&templateId=${tpl.id}`); }}
|
|
259
|
+
className="px-2.5 py-1 rounded-md text-xs font-medium transition-colors"
|
|
260
|
+
style={{ backgroundColor: 'rgba(255,255,255,0.2)', color: '#fff' }}>
|
|
261
|
+
<i className="fa-solid fa-pen-to-square mr-1" style={{ fontSize: '10px' }} />编辑
|
|
262
|
+
</button>
|
|
263
|
+
<button onClick={(e) => { e.stopPropagation(); handleDeleteTemplate(tpl.id); }}
|
|
264
|
+
className="px-2.5 py-1 rounded-md text-xs font-medium transition-colors"
|
|
265
|
+
style={{ backgroundColor: 'rgba(255,255,255,0.1)', color: 'rgba(255,255,255,0.6)' }}>
|
|
266
|
+
<i className="fa-solid fa-trash-can mr-1" style={{ fontSize: '10px' }} />删除
|
|
267
|
+
</button>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
{/* 底部名称 */}
|
|
272
|
+
<div className="px-2.5 py-2">
|
|
273
|
+
<div className="text-xs font-medium truncate" style={{ color: 'var(--color-text-primary)' }}>{tpl.name}</div>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
))}
|
|
277
|
+
</div>
|
|
278
|
+
) : (
|
|
279
|
+
<div className="flex flex-col items-center justify-center py-16 gap-2 opacity-40">
|
|
280
|
+
<i className="fa-regular fa-folder-open" style={{ color: 'var(--color-text-muted)', fontSize: '32px' }} />
|
|
281
|
+
<p className="text-xs" style={{ color: 'var(--color-text-muted)' }}>暂无模板,上传或创建一个吧</p>
|
|
282
|
+
</div>
|
|
283
|
+
)}
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
{/* 右侧:继续游戏 */}
|
|
287
|
+
<div className="w-64 shrink-0 hidden md:block">
|
|
288
|
+
<h3 className="text-sm font-medium mb-3 text-center" style={{ color: 'var(--color-text-muted)' }}>
|
|
289
|
+
继续游戏
|
|
290
|
+
{sessions.length > 0 && (
|
|
291
|
+
<span className="ml-1 px-1.5 py-0.5 rounded text-xs"
|
|
292
|
+
style={{ backgroundColor: 'var(--color-bg-tertiary)', color: 'var(--color-text-muted)' }}>
|
|
293
|
+
{sessions.length}
|
|
294
|
+
</span>
|
|
295
|
+
)}
|
|
296
|
+
</h3>
|
|
297
|
+
{sessions.length > 0 ? (
|
|
298
|
+
<div className="space-y-1.5">
|
|
299
|
+
{sessions.map((session) => (
|
|
300
|
+
<div key={session.id}
|
|
301
|
+
className="flex items-center justify-between px-3 py-2.5 rounded-lg border cursor-pointer transition-colors group"
|
|
302
|
+
style={{ backgroundColor: 'var(--color-bg-secondary)', borderColor: 'var(--color-border)' }}
|
|
303
|
+
onClick={() => router.push(`/game/${session.id}`)}>
|
|
304
|
+
<div className="min-w-0 flex-1">
|
|
305
|
+
<div className="text-sm font-medium truncate" style={{ color: 'var(--color-text-primary)' }}>
|
|
306
|
+
{session.worldSetting?.title as string}
|
|
307
|
+
</div>
|
|
308
|
+
<div className="text-xs mt-0.5 truncate" style={{ color: 'var(--color-text-muted)' }}>
|
|
309
|
+
· {session.messages.length} 条消息
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
<button onClick={(e) => { e.stopPropagation(); handleDeleteSession(session.id); }}
|
|
313
|
+
className="opacity-0 group-hover:opacity-100 p-1 rounded transition-opacity shrink-0 ml-2"
|
|
314
|
+
style={{ color: 'var(--color-text-muted)' }} title="删除">
|
|
315
|
+
<i className="fa-solid fa-xmark" style={{ fontSize: '10px' }} />
|
|
316
|
+
</button>
|
|
317
|
+
</div>
|
|
318
|
+
))}
|
|
319
|
+
</div>
|
|
320
|
+
) : (
|
|
321
|
+
<div className="flex flex-col items-center justify-center py-16 gap-2 opacity-40">
|
|
322
|
+
<i className="fa-regular fa-clock" style={{ color: 'var(--color-text-muted)', fontSize: '24px' }} />
|
|
323
|
+
<p className="text-xs" style={{ color: 'var(--color-text-muted)' }}>暂无游戏记录</p>
|
|
324
|
+
</div>
|
|
325
|
+
)}
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
</main>
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
<ConfirmDialog
|
|
332
|
+
open={confirmOpen}
|
|
333
|
+
title={confirmConfig.title}
|
|
334
|
+
message={confirmConfig.message}
|
|
335
|
+
danger={confirmConfig.danger}
|
|
336
|
+
confirmText={confirmConfig.confirmText}
|
|
337
|
+
cancelText={confirmConfig.cancelText}
|
|
338
|
+
ignoreText={confirmConfig.ignoreText}
|
|
339
|
+
onConfirm={confirmConfig.onConfirm}
|
|
340
|
+
onIgnore={confirmConfig.onIgnore}
|
|
341
|
+
onCancel={() => setConfirmOpen(false)}
|
|
342
|
+
/>
|
|
343
|
+
</>
|
|
344
|
+
);
|
|
345
|
+
}
|