feat(#584): 智能素材匹配前端页面框架 #618
@@ -238,6 +238,21 @@ function getActivePreset(settings: TitleSettings): string | null {
|
||||
return null
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
常量
|
||||
================================================================ */
|
||||
|
||||
const SMART_MATCH_REASONS = [
|
||||
"画面清晰度高,构图专业",
|
||||
"与描述场景高度契合",
|
||||
"时长适中,适合剪辑节奏",
|
||||
"色彩风格统一",
|
||||
"包含关键动作镜头",
|
||||
"镜头运动流畅自然",
|
||||
"光影效果出色",
|
||||
"人物表情生动",
|
||||
]
|
||||
|
||||
/* ================================================================
|
||||
组件
|
||||
================================================================ */
|
||||
@@ -266,6 +281,18 @@ const GeneratePage: React.FC = () => {
|
||||
const [selectedMaterials, setSelectedMaterials] = useState<string[]>([])
|
||||
/* 素材选择模式:手动选择 / 自动匹配 */
|
||||
const [materialMode, setMaterialMode] = useState<"manual" | "auto">("manual")
|
||||
/* 智能素材匹配:用户描述输入 */
|
||||
const [smartMatchInput, setSmartMatchInput] = useState("")
|
||||
/* 智能素材匹配:是否正在匹配中 */
|
||||
const [smartMatching, setSmartMatching] = useState(false)
|
||||
/* 智能素材匹配:推荐结果列表 */
|
||||
const [smartMatchedResults, setSmartMatchedResults] = useState<
|
||||
Array<{ asset: AssetItem; matchScore: number; matchReason: string }>
|
||||
>([])
|
||||
/* 智能素材匹配:已选择的素材ID(auto模式下使用) */
|
||||
const [smartSelectedIds, setSmartSelectedIds] = useState<string[]>([])
|
||||
/* 智能素材匹配:是否已执行过匹配 */
|
||||
const [hasMatched, setHasMatched] = useState(false)
|
||||
|
||||
/* ── 标题设置 ── */
|
||||
const [titleSettings, setTitleSettings] = useState<TitleSettings>(DEFAULT_TITLE_SETTINGS)
|
||||
@@ -471,6 +498,92 @@ const GeneratePage: React.FC = () => {
|
||||
enabled: !!selectedLibraryId,
|
||||
})
|
||||
|
||||
/* ── 智能素材匹配 ── */
|
||||
|
||||
const handleSmartMatch = useCallback(async () => {
|
||||
if (!smartMatchInput.trim()) {
|
||||
message.warning("请先输入视频内容描述")
|
||||
return
|
||||
}
|
||||
if (materials.items.length === 0) {
|
||||
message.warning("当前视频库暂无素材")
|
||||
return
|
||||
}
|
||||
|
||||
setSmartMatching(true)
|
||||
setHasMatched(true)
|
||||
|
||||
// 模拟 AI 匹配延迟
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
|
||||
// 从素材库中随机选取 5-8 个作为推荐结果
|
||||
const shuffled = [...materials.items].sort(() => Math.random() - 0.5)
|
||||
const count = Math.min(shuffled.length, 5 + Math.floor(Math.random() * 4))
|
||||
const picked = shuffled.slice(0, count)
|
||||
|
||||
const results = picked.map((asset, idx) => ({
|
||||
asset,
|
||||
matchScore: Math.round(85 + Math.random() * 14), // 85-99 分
|
||||
matchReason:
|
||||
SMART_MATCH_REASONS[idx % SMART_MATCH_REASONS.length] +
|
||||
(Math.random() > 0.5 ? ",画面质感优秀" : ""),
|
||||
}))
|
||||
|
||||
// 按匹配度从高到低排序
|
||||
results.sort((a, b) => b.matchScore - a.matchScore)
|
||||
|
||||
setSmartMatchedResults(results)
|
||||
// 默认选中匹配度 >= 90 的素材
|
||||
const defaultSelected = results.filter((r) => r.matchScore >= 90).map((r) => r.asset.id)
|
||||
setSmartSelectedIds(
|
||||
defaultSelected.length > 0 ? defaultSelected : results.slice(0, 3).map((r) => r.asset.id),
|
||||
)
|
||||
setSmartMatching(false)
|
||||
}, [smartMatchInput, materials.items])
|
||||
|
||||
const handleToggleSmartSelect = useCallback((assetId: string) => {
|
||||
setSmartSelectedIds((prev) =>
|
||||
prev.includes(assetId) ? prev.filter((id) => id !== assetId) : [...prev, assetId],
|
||||
)
|
||||
}, [])
|
||||
|
||||
const handleRefreshMatch = useCallback(async () => {
|
||||
if (materials.items.length <= 5) {
|
||||
message.info("素材库素材较少,无法换一批")
|
||||
return
|
||||
}
|
||||
setSmartMatching(true)
|
||||
await new Promise((resolve) => setTimeout(resolve, 800))
|
||||
|
||||
const remaining = materials.items.filter(
|
||||
(m) => !smartMatchedResults.some((r) => r.asset.id === m.id),
|
||||
)
|
||||
const shuffled = [...remaining].sort(() => Math.random() - 0.5)
|
||||
const count = Math.min(shuffled.length, 5 + Math.floor(Math.random() * 3))
|
||||
const picked = shuffled.slice(0, count)
|
||||
|
||||
const results = picked.map((asset, idx) => ({
|
||||
asset,
|
||||
matchScore: Math.round(80 + Math.random() * 19),
|
||||
matchReason:
|
||||
SMART_MATCH_REASONS[(idx + 2) % SMART_MATCH_REASONS.length] +
|
||||
(Math.random() > 0.5 ? ",节奏明快" : ""),
|
||||
}))
|
||||
results.sort((a, b) => b.matchScore - a.matchScore)
|
||||
|
||||
setSmartMatchedResults(results)
|
||||
setSmartSelectedIds([])
|
||||
setSmartMatching(false)
|
||||
}, [materials.items, smartMatchedResults])
|
||||
|
||||
const handleSelectAllMatched = useCallback(() => {
|
||||
setSmartSelectedIds(smartMatchedResults.map((r) => r.asset.id))
|
||||
}, [smartMatchedResults])
|
||||
|
||||
const handleClearSmartSelect = useCallback(() => {
|
||||
setSmartSelectedIds([])
|
||||
}, [])
|
||||
|
||||
const handleCloneSuccess = useCallback(
|
||||
(voice: VoiceClone) => {
|
||||
addClone(voice)
|
||||
@@ -700,7 +813,7 @@ const GeneratePage: React.FC = () => {
|
||||
template_id: selectedTemplate,
|
||||
name: titleSettings.title.trim(),
|
||||
config: {
|
||||
asset_ids: selectedMaterials,
|
||||
asset_ids: materialMode === "auto" ? smartSelectedIds : selectedMaterials,
|
||||
title_config: {
|
||||
ai_auto_select: titleSettings.aiAutoSelect,
|
||||
content: titleSettings.title,
|
||||
@@ -905,6 +1018,7 @@ const GeneratePage: React.FC = () => {
|
||||
generateCount,
|
||||
materialMode,
|
||||
coverSettings,
|
||||
smartSelectedIds,
|
||||
])
|
||||
|
||||
/* ── 下载视频 ── */
|
||||
@@ -953,6 +1067,10 @@ const GeneratePage: React.FC = () => {
|
||||
message.warning("请至少选择一个素材")
|
||||
return
|
||||
}
|
||||
if (currentStep === 2 && materialMode === "auto" && smartSelectedIds.length === 0) {
|
||||
message.warning("请先进行智能匹配并选择素材")
|
||||
return
|
||||
}
|
||||
if (currentStep === 4 && !titleSettings.title.trim()) {
|
||||
message.warning("请选择或输入标题")
|
||||
return
|
||||
@@ -960,7 +1078,14 @@ const GeneratePage: React.FC = () => {
|
||||
if (currentStep < 7) {
|
||||
setCurrentStep((s) => s + 1)
|
||||
}
|
||||
}, [currentStep, selectedTemplate, selectedMaterials.length, titleSettings, materialMode])
|
||||
}, [
|
||||
currentStep,
|
||||
selectedTemplate,
|
||||
selectedMaterials.length,
|
||||
titleSettings,
|
||||
materialMode,
|
||||
smartSelectedIds.length,
|
||||
])
|
||||
|
||||
const goPrev = useCallback(() => {
|
||||
if (currentStep > 1) {
|
||||
@@ -1176,40 +1301,157 @@ const GeneratePage: React.FC = () => {
|
||||
|
||||
{/* ── 自动匹配模式 ── */}
|
||||
{materialMode === "auto" && (
|
||||
<div className="xx-auto-match-card">
|
||||
<div className="xx-auto-match-icon">🤖</div>
|
||||
<div className="xx-auto-match-body">
|
||||
<h4 className="xx-auto-match-title">智能素材匹配</h4>
|
||||
<p className="xx-auto-match-desc">
|
||||
系统将根据所选模板和标题,从视频库中自动分析并匹配最合适的素材进行视频生成。
|
||||
无需手动挑选,AI 会综合素材质量、时长、内容相关性等维度进行智能筛选。
|
||||
</p>
|
||||
<div className="xx-auto-match-features">
|
||||
<span className="xx-auto-match-feature">📊 质量评分筛选</span>
|
||||
<span className="xx-auto-match-feature">🎯 内容相关性匹配</span>
|
||||
<span className="xx-auto-match-feature">⏱️ 时长智能分配</span>
|
||||
<div className="xx-smart-match-section">
|
||||
{/* 描述输入区 */}
|
||||
<div className="xx-smart-match-input-area">
|
||||
<label className="xx-smart-match-label">🤖 描述你想要的视频内容</label>
|
||||
<textarea
|
||||
className="xx-smart-match-input"
|
||||
placeholder="例如:一个科技感十足的产品宣传视频,画面要有现代办公场景、团队协作、数据分析图表…"
|
||||
value={smartMatchInput}
|
||||
onChange={(e) => setSmartMatchInput(e.target.value)}
|
||||
rows={3}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
|
||||
handleSmartMatch()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="xx-smart-match-input-footer">
|
||||
<span className="xx-smart-match-tip">
|
||||
{materialsLoading
|
||||
? "扫描视频库中…"
|
||||
: `当前视频库共 ${materials.items.length} 个素材可供匹配`}
|
||||
</span>
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
{hasMatched && (
|
||||
<button
|
||||
type="button"
|
||||
className="xx-btn xx-btn-ghost xx-btn-sm"
|
||||
onClick={handleRefreshMatch}
|
||||
disabled={smartMatching || materialsLoading}
|
||||
>
|
||||
🔄 换一批
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="xx-btn xx-btn-primary xx-btn-sm"
|
||||
onClick={handleSmartMatch}
|
||||
disabled={smartMatching || materialsLoading || !smartMatchInput.trim()}
|
||||
>
|
||||
{smartMatching ? (
|
||||
<>
|
||||
<LoadingOutlined style={{ marginRight: 6 }} />
|
||||
匹配中…
|
||||
</>
|
||||
) : (
|
||||
"✨ 智能匹配"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{materialsLoading ? (
|
||||
<Text
|
||||
style={{
|
||||
color: "var(--text-secondary)",
|
||||
fontSize: 12,
|
||||
marginTop: 8,
|
||||
}}
|
||||
>
|
||||
扫描视频库中…
|
||||
</Text>
|
||||
) : (
|
||||
<Text
|
||||
style={{
|
||||
color: "var(--text-tertiary, #94a3b8)",
|
||||
fontSize: 12,
|
||||
marginTop: 8,
|
||||
}}
|
||||
>
|
||||
当前视频库共 {materials.items.length} 个素材可供匹配
|
||||
</Text>
|
||||
|
||||
{/* 推荐结果区 */}
|
||||
{hasMatched && !smartMatching && smartMatchedResults.length > 0 && (
|
||||
<div className="xx-smart-match-results">
|
||||
<div className="xx-smart-match-results-header">
|
||||
<span className="xx-smart-match-results-title">
|
||||
推荐素材 ({smartMatchedResults.length}个)
|
||||
</span>
|
||||
<div className="xx-smart-match-results-actions">
|
||||
<button type="button" className="xx-link-btn" onClick={handleSelectAllMatched}>
|
||||
全选
|
||||
</button>
|
||||
<span style={{ color: "var(--border-color)" }}>|</span>
|
||||
<button type="button" className="xx-link-btn" onClick={handleClearSmartSelect}>
|
||||
清空
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="xx-smart-match-grid">
|
||||
{smartMatchedResults.map((result) => {
|
||||
const isSelected = smartSelectedIds.includes(result.asset.id)
|
||||
return (
|
||||
<div
|
||||
key={result.asset.id}
|
||||
className={`xx-smart-match-card ${isSelected ? "selected" : ""}`}
|
||||
onClick={() => handleToggleSmartSelect(result.asset.id)}
|
||||
>
|
||||
{/* 缩略图 */}
|
||||
<div className="xx-smart-match-thumb">
|
||||
{result.asset.thumbnail_url ? (
|
||||
<img src={result.asset.thumbnail_url} alt={result.asset.name} />
|
||||
) : (
|
||||
<div className="xx-smart-match-thumb-placeholder">
|
||||
<PlayCircleOutlined style={{ fontSize: 32, opacity: 0.5 }} />
|
||||
</div>
|
||||
)}
|
||||
<div className="xx-smart-match-score">{result.matchScore}%</div>
|
||||
{isSelected && (
|
||||
<div className="xx-smart-match-check">
|
||||
<CheckCircleFilled style={{ fontSize: 20, color: "#fff" }} />
|
||||
</div>
|
||||
)}
|
||||
{result.asset.duration && (
|
||||
<div className="xx-smart-match-duration">
|
||||
{formatDuration(result.asset.duration)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* 信息区 */}
|
||||
<div className="xx-smart-match-info">
|
||||
<div className="xx-smart-match-name" title={result.asset.name}>
|
||||
{result.asset.name}
|
||||
</div>
|
||||
<div className="xx-smart-match-reason">🎯 {result.matchReason}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 匹配中状态 */}
|
||||
{smartMatching && (
|
||||
<div className="xx-smart-match-loading">
|
||||
<LoadingOutlined
|
||||
style={{ fontSize: 32, color: "var(--primary-color)", marginBottom: 12 }}
|
||||
/>
|
||||
<div style={{ color: "var(--text-primary)", fontSize: 14 }}>AI 正在分析素材…</div>
|
||||
<div style={{ color: "var(--text-tertiary)", fontSize: 12, marginTop: 4 }}>
|
||||
正在根据描述从视频库中匹配最合适的素材
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 未匹配状态提示 */}
|
||||
{!hasMatched && !smartMatching && (
|
||||
<div className="xx-smart-match-empty">
|
||||
<div style={{ fontSize: 36, marginBottom: 8 }}>💡</div>
|
||||
<div style={{ color: "var(--text-secondary)", fontSize: 13 }}>
|
||||
输入视频内容描述,点击「智能匹配」让 AI 帮你选素材
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 已选素材汇总 */}
|
||||
{hasMatched && !smartMatching && smartSelectedIds.length > 0 && (
|
||||
<div className="xx-smart-match-summary">
|
||||
<div className="xx-smart-match-summary-header">
|
||||
<span className="xx-pill xx-pill-ok">已选 {smartSelectedIds.length} 个素材</span>
|
||||
<span style={{ color: "var(--text-tertiary)", fontSize: 12 }}>
|
||||
预计总时长约{" "}
|
||||
{smartMatchedResults
|
||||
.filter((r) => smartSelectedIds.includes(r.asset.id))
|
||||
.reduce((sum, r) => sum + (r.asset.duration || 0), 0)
|
||||
.toFixed(0)}{" "}
|
||||
秒
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -1234,7 +1476,9 @@ const GeneratePage: React.FC = () => {
|
||||
<div className="xx-preview-plan-row">
|
||||
<span className="xx-preview-plan-label">素材数量</span>
|
||||
<span className="xx-preview-plan-value">
|
||||
{materialMode === "auto" ? "AI自动匹配" : `${selectedMaterials.length} 个素材`}
|
||||
{materialMode === "auto"
|
||||
? `${smartSelectedIds.length} 个素材(智能匹配)`
|
||||
: `${selectedMaterials.length} 个素材`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="xx-preview-plan-row">
|
||||
@@ -2022,7 +2266,9 @@ const GeneratePage: React.FC = () => {
|
||||
<div className="xx-summary-row">
|
||||
<span className="xx-summary-label">素材</span>
|
||||
<span className="xx-summary-value">
|
||||
{materialMode === "auto" ? "自动匹配" : `${selectedMaterials.length} 个素材`}
|
||||
{materialMode === "auto"
|
||||
? `${smartSelectedIds.length} 个素材(智能匹配)`
|
||||
: `${selectedMaterials.length} 个素材`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="xx-summary-row">
|
||||
|
||||
@@ -1137,6 +1137,251 @@
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
/* ── 智能素材匹配 ── */
|
||||
.xx-smart-match-section {
|
||||
margin-top: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.xx-smart-match-input-area {
|
||||
padding: 16px;
|
||||
background: linear-gradient(135deg, #f0f4ff 0%, #faf5ff 100%);
|
||||
border: 1px solid var(--border-primary, #e2e8f0);
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.xx-smart-match-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #1e293b);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.xx-smart-match-input {
|
||||
width: 100%;
|
||||
min-height: 80px;
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-primary, #1e293b);
|
||||
background: #fff;
|
||||
border: 1px solid var(--border-primary, #e2e8f0);
|
||||
border-radius: 10px;
|
||||
resize: vertical;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.2s;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.xx-smart-match-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color, #4f46e5);
|
||||
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
|
||||
}
|
||||
|
||||
.xx-smart-match-input::placeholder {
|
||||
color: var(--text-tertiary, #94a3b8);
|
||||
}
|
||||
|
||||
.xx-smart-match-input-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.xx-smart-match-tip {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary, #94a3b8);
|
||||
}
|
||||
|
||||
.xx-smart-match-results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.xx-smart-match-results-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.xx-smart-match-results-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #1e293b);
|
||||
}
|
||||
|
||||
.xx-smart-match-results-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.xx-link-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--primary-color, #4f46e5);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.xx-link-btn:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.xx-smart-match-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
max-height: 420px;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.xx-smart-match-card {
|
||||
background: #fff;
|
||||
border: 2px solid var(--border-primary, #e2e8f0);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.xx-smart-match-card:hover {
|
||||
border-color: var(--primary-color, #4f46e5);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.xx-smart-match-card.selected {
|
||||
border-color: var(--primary-color, #4f46e5);
|
||||
background: var(--primary-soft, #eef2ff);
|
||||
}
|
||||
|
||||
.xx-smart-match-thumb {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
background: #f1f5f9;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.xx-smart-match-thumb img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.xx-smart-match-thumb-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-tertiary, #94a3b8);
|
||||
}
|
||||
|
||||
.xx-smart-match-score {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
padding: 2px 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #4f46e5, #7c3aed);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.xx-smart-match-check {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: var(--primary-color, #4f46e5);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.xx-smart-match-duration {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
padding: 2px 6px;
|
||||
font-size: 11px;
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.xx-smart-match-info {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.xx-smart-match-name {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary, #1e293b);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.xx-smart-match-reason {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary, #64748b);
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.xx-smart-match-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 20px;
|
||||
background: #f8fafc;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.xx-smart-match-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 30px 20px;
|
||||
background: #f8fafc;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.xx-smart-match-summary {
|
||||
padding: 12px 16px;
|
||||
background: #f0fdf4;
|
||||
border: 1px solid #bbf7d0;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.xx-smart-match-summary-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
标题设置(选择标题步骤)
|
||||
============================================================ */
|
||||
|
||||
Reference in New Issue
Block a user