diff --git a/frontend/src/components/Studio/StudioBindingPreviewPopup.css b/frontend/src/components/Studio/StudioBindingPreviewPopup.css
new file mode 100644
index 0000000..ef74577
--- /dev/null
+++ b/frontend/src/components/Studio/StudioBindingPreviewPopup.css
@@ -0,0 +1,165 @@
+.studio-binding-preview {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-sm);
+ min-height: 200px;
+}
+
+.studio-binding-preview__tabs {
+ display: flex;
+ gap: 4px;
+ padding-bottom: var(--spacing-xs);
+ border-bottom: 1px solid var(--color-border-light);
+}
+
+.studio-binding-preview__tab {
+ flex: 1;
+ padding: 4px 8px;
+ border: 1px solid transparent;
+ border-radius: var(--radius-sm);
+ background: transparent;
+ color: var(--color-text-muted);
+ font-size: 0.72rem;
+ cursor: pointer;
+}
+
+.studio-binding-preview__tab.is-active {
+ border-color: var(--color-border);
+ background: var(--color-bg-tertiary);
+ color: var(--color-text-primary);
+ font-weight: 600;
+}
+
+.studio-binding-preview__panel {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-sm);
+ max-height: min(50vh, 420px);
+ overflow: auto;
+}
+
+.studio-binding-preview__status {
+ margin: 0;
+ font-size: 0.75rem;
+ color: var(--color-text-muted);
+}
+
+.studio-binding-preview__status--error {
+ color: #f5576c;
+}
+
+.studio-binding-preview__empty {
+ margin: 0;
+ font-size: 0.75rem;
+ color: var(--color-text-muted);
+}
+
+.studio-binding-preview__hero {
+ display: flex;
+ gap: var(--spacing-sm);
+ align-items: center;
+}
+
+.studio-binding-preview__avatar {
+ width: 48px;
+ height: 48px;
+ border-radius: var(--radius-sm);
+ object-fit: cover;
+ flex-shrink: 0;
+ border: 1px solid var(--color-border-light);
+}
+
+.studio-binding-preview__avatar--placeholder {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--color-bg-tertiary);
+ font-size: 1.2rem;
+}
+
+.studio-binding-preview__name {
+ margin: 0;
+ font-size: 0.9rem;
+ color: var(--color-text-primary);
+}
+
+.studio-binding-preview__meta {
+ margin: 2px 0 0;
+ font-size: 0.68rem;
+ color: var(--color-text-muted);
+}
+
+.studio-binding-preview__field-label {
+ margin: 0 0 4px;
+ font-size: 0.68rem;
+ font-weight: 600;
+ color: var(--color-text-muted);
+}
+
+.studio-binding-preview__field-value {
+ margin: 0;
+ padding: var(--spacing-xs);
+ border-radius: var(--radius-sm);
+ border: 1px solid var(--color-border-light);
+ background: var(--color-bg-tertiary);
+ white-space: pre-wrap;
+ font-size: 0.72rem;
+ line-height: 1.45;
+ max-height: 120px;
+ overflow: auto;
+}
+
+.studio-binding-preview__entries {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.studio-binding-preview__entry {
+ border: 1px solid var(--color-border-light);
+ border-radius: var(--radius-sm);
+ overflow: hidden;
+}
+
+.studio-binding-preview__entry-head {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 2px;
+ padding: var(--spacing-xs) var(--spacing-sm);
+ border: none;
+ background: var(--color-bg-tertiary);
+ cursor: pointer;
+ text-align: left;
+}
+
+.studio-binding-preview__entry-head:hover {
+ background: color-mix(in srgb, var(--color-accent) 8%, var(--color-bg-tertiary));
+}
+
+.studio-binding-preview__entry-title {
+ font-size: 0.78rem;
+ color: var(--color-text-primary);
+ font-weight: 500;
+}
+
+.studio-binding-preview__entry-meta {
+ font-size: 0.65rem;
+ color: var(--color-text-muted);
+}
+
+.studio-binding-preview__entry-content {
+ margin: 0;
+ padding: var(--spacing-sm);
+ border-top: 1px solid var(--color-border-light);
+ background: var(--color-bg-secondary);
+ white-space: pre-wrap;
+ font-size: 0.72rem;
+ line-height: 1.45;
+ max-height: 200px;
+ overflow: auto;
+}
diff --git a/frontend/src/components/Studio/StudioBindingPreviewPopup.jsx b/frontend/src/components/Studio/StudioBindingPreviewPopup.jsx
new file mode 100644
index 0000000..31c707b
--- /dev/null
+++ b/frontend/src/components/Studio/StudioBindingPreviewPopup.jsx
@@ -0,0 +1,200 @@
+import React, { useCallback, useEffect, useState } from 'react';
+
+import StudioInsertionPopup from './StudioInsertionPopup';
+
+import './StudioBindingPreviewPopup.css';
+
+const CHARACTER_FIELDS = [
+ { key: 'description', label: '描述' },
+ { key: 'personality', label: '性格' },
+ { key: 'scenario', label: '场景' },
+ { key: 'first_mes', label: '开场白' },
+ { key: 'mes_example', label: '对话示例' },
+];
+
+function StudioBindingPreviewPopup({
+ open,
+ characterName,
+ worldbookName,
+ onClose,
+}) {
+ const [tab, setTab] = useState('character');
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+ const [character, setCharacter] = useState(null);
+ const [entries, setEntries] = useState([]);
+ const [expandedEntryUid, setExpandedEntryUid] = useState(null);
+
+ const loadData = useCallback(async () => {
+ if (!characterName && !worldbookName) return;
+ setLoading(true);
+ setError('');
+ try {
+ const tasks = [];
+
+ if (characterName) {
+ tasks.push(
+ fetch(`/api/characters/${encodeURIComponent(characterName)}`)
+ .then(async (res) => {
+ if (!res.ok) throw new Error('加载角色卡失败');
+ return res.json();
+ })
+ .then(setCharacter)
+ );
+ } else {
+ setCharacter(null);
+ }
+
+ if (worldbookName) {
+ tasks.push(
+ fetch(
+ `/api/worldbooks/${encodeURIComponent(worldbookName)}/entries?page=1&page_size=50`
+ )
+ .then(async (res) => {
+ if (!res.ok) throw new Error('加载世界书条目失败');
+ return res.json();
+ })
+ .then((data) => setEntries(data.entries || []))
+ );
+ } else {
+ setEntries([]);
+ }
+
+ await Promise.all(tasks);
+ } catch (e) {
+ setError(e.message || '加载绑定资源失败');
+ } finally {
+ setLoading(false);
+ }
+ }, [characterName, worldbookName]);
+
+ useEffect(() => {
+ if (open) {
+ setTab('character');
+ setExpandedEntryUid(null);
+ loadData();
+ }
+ }, [open, loadData]);
+
+ const title = characterName ? `绑定预览 · ${characterName}` : '绑定预览';
+
+ return (
+ 加载中…
+ {error}
+ 暂无绑定角色卡 世界书:{worldbookName} 暂无绑定世界书 世界书「{worldbookName}」暂无条目
+ ) : (
+
{character.name}
+ {worldbookName ? (
+ {label}
+ {value}
+
+ {entries.map((entry) => {
+ const expanded = expandedEntryUid === entry.uid;
+ const comment = entry.comment || entry.key?.join?.(', ') || '未命名条目';
+ return (
+
+ )}
+
+ {entry.content || '(空内容)'}
+
+ ) : null}
+