ParallelogramButton
This commit is contained in:
parent
963790e355
commit
5539a89dcc
|
|
@ -9,6 +9,8 @@ import { observer } from 'mobx-react-lite'
|
|||
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { ActivityIndicator, RefreshControl, TextInput } from 'react-native'
|
||||
|
||||
import ParallelogramButton from '@/components/ParallelogramButton'
|
||||
|
||||
import BannerSection from '@/components/BannerSection'
|
||||
import { useTemplateActions } from '@/hooks/actions/use-template-actions'
|
||||
import { useTemplates } from '@/hooks/data'
|
||||
|
|
@ -35,7 +37,7 @@ type MediaItem = {
|
|||
title?: string
|
||||
authorName?: string
|
||||
}
|
||||
type ActiveTab = 'gen' | '' | 'new' | 'like'
|
||||
type ActiveTab = 'gen' | 'HOT' | 'NEW' | 'LIKE'
|
||||
|
||||
/** =========================
|
||||
* Entry page
|
||||
|
|
@ -50,7 +52,7 @@ const Index = observer(function Index() {
|
|||
|
||||
/** ================= 状态 ================= */
|
||||
|
||||
const [activeTab, setActiveTab] = useState<ActiveTab>('')
|
||||
const [activeTab, setActiveTab] = useState<ActiveTab>('HOT')
|
||||
const [isSearchOpen, setIsSearchOpen] = useState(false)
|
||||
|
||||
const [allItems, setAllItems] = useState<MediaItem[]>([])
|
||||
|
|
@ -126,7 +128,7 @@ const Index = observer(function Index() {
|
|||
const { page, pageSize, search, tab } = queryRef.current
|
||||
let newItems: MediaItem[] = []
|
||||
|
||||
if (tab === 'like') {
|
||||
if (tab === 'LIKE') {
|
||||
console.log('加载收藏列表,isAuthenticated=', isAuthenticated)
|
||||
if (!isAuthenticated) {
|
||||
setHasMore(false)
|
||||
|
|
@ -139,7 +141,7 @@ const Index = observer(function Index() {
|
|||
.map((f) => transformTemplateToMediaItem(f.template as TemplateData)) || []
|
||||
}
|
||||
} else {
|
||||
const sortBy = tab === 'new' ? 'createdAt' : 'likeCount'
|
||||
const sortBy = tab === 'NEW' ? 'createdAt' : 'likeCount'
|
||||
const { data } = await loadTemplates({
|
||||
page,
|
||||
limit: pageSize,
|
||||
|
|
@ -342,7 +344,7 @@ const Index = observer(function Index() {
|
|||
}
|
||||
|
||||
const renderListEmpty = () => {
|
||||
if (activeTab === 'like' && !isAuthenticated) {
|
||||
if (activeTab === 'LIKE' && !isAuthenticated) {
|
||||
return (
|
||||
<Block className="mt-[40px] items-center justify-center gap-[16px] py-[60px]">
|
||||
<Block className="size-[80px] items-center justify-center rounded-full border-4 border-white/20 bg-white/10">
|
||||
|
|
@ -648,29 +650,32 @@ type FilterSectionProps = {
|
|||
const FilterSection = memo<FilterSectionProps>(function FilterSection({ activeTab, onChange }) {
|
||||
const tabs = useMemo(
|
||||
() => [
|
||||
{ label: '最热', state: '' as const },
|
||||
{ label: '最新', state: 'new' as const },
|
||||
{ label: '喜欢', state: 'like' as const },
|
||||
{ label: '最热', state: 'HOT' as const },
|
||||
{ label: '最新', state: 'NEW' as const },
|
||||
{ label: '喜欢', state: 'LIKE' as const },
|
||||
],
|
||||
[],
|
||||
)
|
||||
|
||||
return (
|
||||
<Block className="mt-[12px] flex-row items-end justify-between">
|
||||
<Block className="flex-row gap-[8px]">
|
||||
<Block className="flex-row gap-[4px]">
|
||||
{tabs.map(({ label, state }) => {
|
||||
const isActive = activeTab === state
|
||||
// 估算宽度:根据文本长度动态计算
|
||||
// 中文字符大约 13px(字体13px),padding 左右各 16px
|
||||
const buttonWidth = Math.max(label.length * 13 + 12, 66) // 最小宽度 70,适应13px字体
|
||||
const buttonHeight = 30 // 稍微增加高度以适应13px字体
|
||||
return (
|
||||
<Block
|
||||
className={`border-2 border-black px-[16px] py-[4px] ${isActive ? 'bg-accent' : 'bg-white'}`}
|
||||
<ParallelogramButton
|
||||
key={state}
|
||||
onClick={() => onChange(state)}
|
||||
style={{
|
||||
transform: [{ skewX: '-6deg' }],
|
||||
}}
|
||||
>
|
||||
<Text className={`text-[10px] font-[900] ${isActive ? 'text-black' : 'text-gray-500'}`}>{label}</Text>
|
||||
</Block>
|
||||
label={label}
|
||||
state={state}
|
||||
isActive={isActive}
|
||||
onPress={() => onChange(state)}
|
||||
width={buttonWidth}
|
||||
height={buttonHeight}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Block>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,182 @@
|
|||
import React, { memo, useMemo } from 'react'
|
||||
import { Pressable } from 'react-native'
|
||||
import { Canvas, Group, Paragraph, Path, Skia, useFonts } from '@shopify/react-native-skia'
|
||||
|
||||
export type ParallelogramButtonProps = {
|
||||
label: string
|
||||
state: string
|
||||
isActive: boolean
|
||||
onPress: () => void
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
const ParallelogramButton = memo<ParallelogramButtonProps>(function ParallelogramButton({
|
||||
label,
|
||||
state,
|
||||
isActive,
|
||||
onPress,
|
||||
width,
|
||||
height,
|
||||
}) {
|
||||
const fontMgr = useFonts({
|
||||
System: [],
|
||||
})
|
||||
|
||||
// 创建英文 state 标签段落(白色字体,黑色描边,字重 700,激活时 #FAE307,未激活时白色)
|
||||
const stateParagraph = useMemo(() => {
|
||||
if (!fontMgr) return null
|
||||
const para = Skia.ParagraphBuilder.Make({}, fontMgr)
|
||||
.pushStyle({
|
||||
color: isActive ? Skia.Color('#FAE307') : Skia.Color('#FFFFFF'),
|
||||
fontSize: 7,
|
||||
fontFamilies: ['System'],
|
||||
fontStyle: { weight: 700 },
|
||||
shadows: [
|
||||
// 黑色描边效果(略微偏移,避免过粗)
|
||||
{ color: Skia.Color('#000000'), blurRadius: 0, offset: { x: -0.5, y: -0.5 } },
|
||||
{ color: Skia.Color('#000000'), blurRadius: 0, offset: { x: 0.5, y: -0.5 } },
|
||||
{ color: Skia.Color('#000000'), blurRadius: 0, offset: { x: -0.5, y: 0.5 } },
|
||||
{ color: Skia.Color('#000000'), blurRadius: 0, offset: { x: 0.5, y: 0.5 } },
|
||||
{ color: Skia.Color('#000000'), blurRadius: 0, offset: { x: -0.5, y: 0 } },
|
||||
{ color: Skia.Color('#000000'), blurRadius: 0, offset: { x: 0.5, y: 0 } },
|
||||
{ color: Skia.Color('#000000'), blurRadius: 0, offset: { x: 0, y: -0.5 } },
|
||||
{ color: Skia.Color('#000000'), blurRadius: 0, offset: { x: 0, y: 0.5 } },
|
||||
],
|
||||
})
|
||||
.addText(state)
|
||||
.build()
|
||||
para.layout(40)
|
||||
return para
|
||||
}, [fontMgr, state, isActive])
|
||||
|
||||
// 创建白色字体带黑色描边的中文文本段落
|
||||
const paragraph = useMemo(() => {
|
||||
if (!fontMgr) return null
|
||||
const para = Skia.ParagraphBuilder.Make({}, fontMgr)
|
||||
.pushStyle({
|
||||
color: Skia.Color('#FFFFFF'),
|
||||
fontSize: 13,
|
||||
fontFamilies: ['System'],
|
||||
fontStyle: { weight: 900 },
|
||||
shadows: [
|
||||
{ color: Skia.Color('#000000'), blurRadius: 0, offset: { x: -1, y: -1 } },
|
||||
{ color: Skia.Color('#000000'), blurRadius: 0, offset: { x: 1, y: -1 } },
|
||||
{ color: Skia.Color('#000000'), blurRadius: 0, offset: { x: -1, y: 1 } },
|
||||
{ color: Skia.Color('#000000'), blurRadius: 0, offset: { x: 1, y: 1 } },
|
||||
{ color: Skia.Color('#000000'), blurRadius: 0, offset: { x: -1, y: 0 } },
|
||||
{ color: Skia.Color('#000000'), blurRadius: 0, offset: { x: 1, y: 0 } },
|
||||
{ color: Skia.Color('#000000'), blurRadius: 0, offset: { x: 0, y: -1 } },
|
||||
{ color: Skia.Color('#000000'), blurRadius: 0, offset: { x: 0, y: 1 } },
|
||||
],
|
||||
})
|
||||
.addText(label)
|
||||
.build()
|
||||
para.layout(width - 12) // 减去左右 padding
|
||||
return para
|
||||
}, [fontMgr, label, width])
|
||||
|
||||
const skewOffset = 8 // 倾斜偏移量(像素)
|
||||
const padding = 15 // 增加 padding,防止旋转后的文本被裁剪
|
||||
|
||||
// 创建平行四边形路径:只有左右边倾斜,上下边保持水平
|
||||
const parallelogramPath = useMemo(() => {
|
||||
const path = Skia.Path.Make()
|
||||
// 四个顶点:左上、右上、右下、左下(加上 padding 偏移)
|
||||
path.moveTo(skewOffset + padding, padding)
|
||||
path.lineTo(width + padding, padding)
|
||||
path.lineTo(width - skewOffset + padding, height + padding)
|
||||
path.lineTo(padding, height + padding)
|
||||
path.close()
|
||||
return path
|
||||
}, [width, height, skewOffset, padding])
|
||||
|
||||
// 创建边框路径
|
||||
const borderPath = useMemo(() => {
|
||||
const path = Skia.Path.Make()
|
||||
path.moveTo(skewOffset + padding + 1, padding + 1)
|
||||
path.lineTo(width + padding - 1, padding + 1)
|
||||
path.lineTo(width - skewOffset + padding - 1, height + padding - 1)
|
||||
path.lineTo(padding + 1, height + padding - 1)
|
||||
path.close()
|
||||
return path
|
||||
}, [width, height, skewOffset, padding])
|
||||
|
||||
// 计算实际画布尺寸(包含 padding)
|
||||
const canvasWidth = width + padding * 2
|
||||
const canvasHeight = height + padding * 2
|
||||
|
||||
// 注意:布局占位仍然使用原始 width,避免改变 tab 间距
|
||||
return (
|
||||
<Pressable onPress={onPress} style={{ width, height: canvasHeight }}>
|
||||
<Canvas
|
||||
style={{
|
||||
width: canvasWidth,
|
||||
height: canvasHeight,
|
||||
// 向左偏移 padding,让额外画布空间不参与横向布局计算
|
||||
marginLeft: -padding,
|
||||
}}
|
||||
>
|
||||
{/* 背景 - 平行四边形 */}
|
||||
<Path
|
||||
path={parallelogramPath}
|
||||
color={isActive ? Skia.Color('#FFE500') : Skia.Color('#FFFFFF')}
|
||||
/>
|
||||
|
||||
{/* 边框 - 平行四边形 */}
|
||||
<Path path={borderPath} color={Skia.Color('#000000')} style="stroke" strokeWidth={2} />
|
||||
|
||||
{/* 英文 state 标签 - 左上角,以左下角为中心,逆时针旋转 15 度 */}
|
||||
{stateParagraph && (() => {
|
||||
const stateTextX = 8 + skewOffset / 2 + 10 + padding
|
||||
const stateTextY = 4 + padding
|
||||
const rotationAngle = (-15 * Math.PI) / 180
|
||||
const centerX = padding
|
||||
const centerY = height + padding
|
||||
|
||||
return (
|
||||
<Group
|
||||
transform={[
|
||||
{ translateX: centerX },
|
||||
{ translateY: centerY },
|
||||
{ rotate: rotationAngle },
|
||||
{ translateX: -centerX + stateTextX },
|
||||
{ translateY: -centerY + stateTextY },
|
||||
]}
|
||||
>
|
||||
<Paragraph paragraph={stateParagraph} x={0} y={0} width={40} />
|
||||
</Group>
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* 中文文本 - 以左下角为中心,逆时针旋转 15 度 */}
|
||||
{paragraph && (() => {
|
||||
const textX = 16 + skewOffset / 2 + 2 + padding
|
||||
const textY = height - paragraph.getHeight() + 1 + padding
|
||||
const rotationAngle = (-15 * Math.PI) / 180
|
||||
const centerX = padding
|
||||
const centerY = height + padding
|
||||
|
||||
return (
|
||||
<Group
|
||||
transform={[
|
||||
{ translateX: centerX },
|
||||
{ translateY: centerY },
|
||||
{ rotate: rotationAngle },
|
||||
{ translateX: -centerX + textX },
|
||||
{ translateY: -centerY + textY },
|
||||
]}
|
||||
>
|
||||
<Paragraph paragraph={paragraph} x={0} y={0} width={width - 32 - skewOffset} />
|
||||
</Group>
|
||||
)
|
||||
})()}
|
||||
</Canvas>
|
||||
</Pressable>
|
||||
)
|
||||
})
|
||||
|
||||
ParallelogramButton.displayName = 'ParallelogramButton'
|
||||
|
||||
export default ParallelogramButton
|
||||
|
||||
Loading…
Reference in New Issue