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 React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { ActivityIndicator, RefreshControl, TextInput } from 'react-native'
|
import { ActivityIndicator, RefreshControl, TextInput } from 'react-native'
|
||||||
|
|
||||||
|
import ParallelogramButton from '@/components/ParallelogramButton'
|
||||||
|
|
||||||
import BannerSection from '@/components/BannerSection'
|
import BannerSection from '@/components/BannerSection'
|
||||||
import { useTemplateActions } from '@/hooks/actions/use-template-actions'
|
import { useTemplateActions } from '@/hooks/actions/use-template-actions'
|
||||||
import { useTemplates } from '@/hooks/data'
|
import { useTemplates } from '@/hooks/data'
|
||||||
|
|
@ -35,7 +37,7 @@ type MediaItem = {
|
||||||
title?: string
|
title?: string
|
||||||
authorName?: string
|
authorName?: string
|
||||||
}
|
}
|
||||||
type ActiveTab = 'gen' | '' | 'new' | 'like'
|
type ActiveTab = 'gen' | 'HOT' | 'NEW' | 'LIKE'
|
||||||
|
|
||||||
/** =========================
|
/** =========================
|
||||||
* Entry page
|
* 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 [isSearchOpen, setIsSearchOpen] = useState(false)
|
||||||
|
|
||||||
const [allItems, setAllItems] = useState<MediaItem[]>([])
|
const [allItems, setAllItems] = useState<MediaItem[]>([])
|
||||||
|
|
@ -126,7 +128,7 @@ const Index = observer(function Index() {
|
||||||
const { page, pageSize, search, tab } = queryRef.current
|
const { page, pageSize, search, tab } = queryRef.current
|
||||||
let newItems: MediaItem[] = []
|
let newItems: MediaItem[] = []
|
||||||
|
|
||||||
if (tab === 'like') {
|
if (tab === 'LIKE') {
|
||||||
console.log('加载收藏列表,isAuthenticated=', isAuthenticated)
|
console.log('加载收藏列表,isAuthenticated=', isAuthenticated)
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
setHasMore(false)
|
setHasMore(false)
|
||||||
|
|
@ -139,7 +141,7 @@ const Index = observer(function Index() {
|
||||||
.map((f) => transformTemplateToMediaItem(f.template as TemplateData)) || []
|
.map((f) => transformTemplateToMediaItem(f.template as TemplateData)) || []
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const sortBy = tab === 'new' ? 'createdAt' : 'likeCount'
|
const sortBy = tab === 'NEW' ? 'createdAt' : 'likeCount'
|
||||||
const { data } = await loadTemplates({
|
const { data } = await loadTemplates({
|
||||||
page,
|
page,
|
||||||
limit: pageSize,
|
limit: pageSize,
|
||||||
|
|
@ -342,7 +344,7 @@ const Index = observer(function Index() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderListEmpty = () => {
|
const renderListEmpty = () => {
|
||||||
if (activeTab === 'like' && !isAuthenticated) {
|
if (activeTab === 'LIKE' && !isAuthenticated) {
|
||||||
return (
|
return (
|
||||||
<Block className="mt-[40px] items-center justify-center gap-[16px] py-[60px]">
|
<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">
|
<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 FilterSection = memo<FilterSectionProps>(function FilterSection({ activeTab, onChange }) {
|
||||||
const tabs = useMemo(
|
const tabs = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{ label: '最热', state: '' as const },
|
{ label: '最热', state: 'HOT' as const },
|
||||||
{ label: '最新', state: 'new' as const },
|
{ label: '最新', state: 'NEW' as const },
|
||||||
{ label: '喜欢', state: 'like' as const },
|
{ label: '喜欢', state: 'LIKE' as const },
|
||||||
],
|
],
|
||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Block className="mt-[12px] flex-row items-end justify-between">
|
<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 }) => {
|
{tabs.map(({ label, state }) => {
|
||||||
const isActive = activeTab === 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 (
|
return (
|
||||||
<Block
|
<ParallelogramButton
|
||||||
className={`border-2 border-black px-[16px] py-[4px] ${isActive ? 'bg-accent' : 'bg-white'}`}
|
|
||||||
key={state}
|
key={state}
|
||||||
onClick={() => onChange(state)}
|
label={label}
|
||||||
style={{
|
state={state}
|
||||||
transform: [{ skewX: '-6deg' }],
|
isActive={isActive}
|
||||||
}}
|
onPress={() => onChange(state)}
|
||||||
>
|
width={buttonWidth}
|
||||||
<Text className={`text-[10px] font-[900] ${isActive ? 'text-black' : 'text-gray-500'}`}>{label}</Text>
|
height={buttonHeight}
|
||||||
</Block>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</Block>
|
</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