223bb530dd
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m20s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m28s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m21s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m49s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m18s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m41s
CI/CD Pipeline / Integration Tests (push) Successful in 2m25s
CI/CD Pipeline / Unit Tests (push) Failing after 10m1s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 12m34s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 30s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 33s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m37s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Failing after 5m1s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
137 lines
4.2 KiB
TypeScript
137 lines
4.2 KiB
TypeScript
import React, { useState } from "react"
|
|
import type { StickerType, TextStickerPreset } from "../../../types"
|
|
import { TEXT_STICKER_PRESET_LABELS } from "../../../types"
|
|
import { EMOJI_LIST, TEXT_PRESET_STYLES } from "../constants"
|
|
|
|
export interface StickerTabsProps {
|
|
activeTab: StickerType
|
|
onTabChange: (tab: StickerType) => void
|
|
textInput: string
|
|
onTextInputChange: (val: string) => void
|
|
onAddEmoji: (emoji: string) => void
|
|
onAddImage: (url: string) => void
|
|
onAddText: (text: string) => void
|
|
}
|
|
|
|
export const StickerTabs: React.FC<StickerTabsProps> = ({
|
|
activeTab,
|
|
onTabChange,
|
|
textInput,
|
|
onTextInputChange,
|
|
onAddEmoji,
|
|
onAddImage,
|
|
onAddText,
|
|
}) => {
|
|
const [imageUrl, setImageUrl] = useState("")
|
|
|
|
const handleImageAdd = () => {
|
|
if (imageUrl.trim()) {
|
|
onAddImage(imageUrl.trim())
|
|
setImageUrl("")
|
|
}
|
|
}
|
|
|
|
const handleTextAdd = () => {
|
|
if (textInput.trim()) {
|
|
onAddText(textInput.trim())
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{/* 类型 Tab */}
|
|
<div className="sticker-tabs">
|
|
{(["emoji", "image", "text"] as StickerType[]).map((t) => (
|
|
<button
|
|
key={t}
|
|
className={`sticker-tab${activeTab === t ? " active" : ""}`}
|
|
onClick={() => onTabChange(t)}
|
|
>
|
|
{t === "emoji" ? "表情贴纸" : t === "image" ? "图片贴纸" : "文字花字"}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Tab 内容区 */}
|
|
<div className="sticker-tab-content">
|
|
{/* Emoji 素材库 */}
|
|
{activeTab === "emoji" && (
|
|
<div className="sticker-emoji-grid">
|
|
{EMOJI_LIST.map((emoji) => (
|
|
<button key={emoji} className="sticker-emoji-btn" onClick={() => onAddEmoji(emoji)}>
|
|
{emoji}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* 图片贴纸 */}
|
|
{activeTab === "image" && (
|
|
<div className="sticker-image-input">
|
|
<input
|
|
type="text"
|
|
className="sticker-url-input"
|
|
placeholder="输入图片 URL 添加贴纸..."
|
|
value={imageUrl}
|
|
onChange={(e) => setImageUrl(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter" && imageUrl.trim()) {
|
|
onAddImage(imageUrl.trim())
|
|
setImageUrl("")
|
|
}
|
|
}}
|
|
/>
|
|
<button className="sticker-url-add-btn" onClick={handleImageAdd}>
|
|
添加
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* 文字花字 */}
|
|
{activeTab === "text" && (
|
|
<div className="sticker-text-section">
|
|
<div className="sticker-text-input-row">
|
|
<input
|
|
type="text"
|
|
className="sticker-text-input"
|
|
placeholder="输入文字内容..."
|
|
value={textInput}
|
|
onChange={(e) => onTextInputChange(e.target.value)}
|
|
/>
|
|
<button
|
|
className="sticker-text-add-btn"
|
|
disabled={!textInput.trim()}
|
|
onClick={handleTextAdd}
|
|
>
|
|
添加
|
|
</button>
|
|
</div>
|
|
<div className="sticker-text-presets">
|
|
<div className="sticker-preset-title">花字预设预览</div>
|
|
<div className="sticker-preset-grid">
|
|
{(Object.keys(TEXT_STICKER_PRESET_LABELS) as TextStickerPreset[]).map((p) => (
|
|
<div
|
|
key={p}
|
|
className="sticker-preset-preview"
|
|
style={{
|
|
background:
|
|
p === "bubble"
|
|
? "rgba(0,0,0,0.5)"
|
|
: p === "gradient"
|
|
? "linear-gradient(90deg,#f093fb,#f5576c)"
|
|
: "#1a1a2e",
|
|
}}
|
|
>
|
|
<span style={TEXT_PRESET_STYLES[p]}>示例</span>
|
|
<div className="sticker-preset-name">{TEXT_STICKER_PRESET_LABELS[p]}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
)
|
|
}
|