diff --git a/src/app/(main)/character/[id]/page.tsx b/src/app/(main)/character/[id]/page.tsx index fd5a643..3d345b5 100644 --- a/src/app/(main)/character/[id]/page.tsx +++ b/src/app/(main)/character/[id]/page.tsx @@ -5,6 +5,8 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { IconButton } from '@/components/ui/button'; import Link from 'next/link'; import { getTranslations } from 'next-intl/server'; +import LikedIcon from '@/components/features/LikedIcon'; +import { LikeTargetType } from '@/services/editor/type'; export default async function Page({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; @@ -18,7 +20,11 @@ export default async function Page({ params }: { params: Promise<{ id: string }> - + {/* 内容区 */}
diff --git a/src/app/(main)/chat/[id]/Drawer/index.tsx b/src/app/(main)/chat/[id]/Drawer/index.tsx index 33b1014..fcf3d5e 100644 --- a/src/app/(main)/chat/[id]/Drawer/index.tsx +++ b/src/app/(main)/chat/[id]/Drawer/index.tsx @@ -22,6 +22,9 @@ import { useStreamChatStore } from '../stream-chat'; import IconFont from '@/components/ui/iconFont'; import MaskCreate from './MaskCreate'; import { useTranslations } from 'next-intl'; +import { useParams } from 'next/navigation'; +import LikedIcon from '@/components/features/LikedIcon'; +import { LikeTargetType } from '@/services/editor/type'; type SettingProps = { open: boolean; @@ -42,6 +45,8 @@ export default function SettingDialog({ open, onOpenChange }: SettingProps) { const t = useTranslations('chat.drawer'); const [activeTab, setActiveTab] = useState('profile'); const updateUserChatSetting = useStreamChatStore((store) => store.updateUserChatSetting); + const { id } = useParams<{ id: string }>(); + const characterId = id.split('-')[2]; const titleMap = { mask_list: t('maskedIdentityMode'), @@ -66,7 +71,7 @@ export default function SettingDialog({ open, onOpenChange }: SettingProps) { {activeTab === 'profile' ? ( - + ) : ( titleMap[activeTab] )} diff --git a/src/components/features/LikedIcon.tsx b/src/components/features/LikedIcon.tsx new file mode 100644 index 0000000..70e2609 --- /dev/null +++ b/src/components/features/LikedIcon.tsx @@ -0,0 +1,32 @@ +'use client'; + +import { LikeTargetType, LikeType } from '@/services/editor/type'; +import { IconButton } from '../ui/button'; +import { useThmubObject } from '@/hooks/services/common'; +import React from 'react'; + +type LikedIconProps = { + objectId: string; + objectType: LikeTargetType; + iconProps?: React.ComponentProps; +}; + +const LikedIcon = React.memo((props: LikedIconProps) => { + const { objectId, objectType, iconProps } = props; + const { thumb, handleThumb, loading } = useThmubObject({ objectId, objectType }); + + return ( + { + if (loading) return; + handleThumb(thumb === LikeType.Liked ? LikeType.Canceled : LikeType.Liked); + }} + /> + ); +}); + +export default LikedIcon; diff --git a/src/components/features/abandon-creation-dialog.tsx b/src/components/features/abandon-creation-dialog.tsx deleted file mode 100644 index af5bd7b..0000000 --- a/src/components/features/abandon-creation-dialog.tsx +++ /dev/null @@ -1,73 +0,0 @@ -'use client' - -import React, { useLayoutEffect, useRef } from 'react' -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog' -import { useRouter } from 'next/navigation' - -interface AbandonCreationDialogProps { - open: boolean - onOpenChange: (open: boolean) => void - onCancel: () => void - onRegenerate: () => void - actionType?: 'exit' | 'regenerate' -} - -const AbandonCreationDialog = ({ - open, - onOpenChange, - onCancel, - onRegenerate, - actionType = 'regenerate', -}: AbandonCreationDialogProps) => { - const router = useRouter() - // 保存对话框打开时的 actionType,确保关闭动画期间按钮文案不变 - const actionTypeRef = useRef(actionType) - const prevOpenRef = useRef(open) - - useLayoutEffect(() => { - // 当对话框打开时,持续更新 ref 为当前的 actionType - // 这样可以确保即使 actionType 在对话框打开后才更新,也能被捕获 - if (open) { - actionTypeRef.current = actionType - } - prevOpenRef.current = open - }, [open, actionType]) - - const handleConfirm = () => { - onRegenerate() - } - - // 对话框打开时使用当前的 actionType(确保显示最新值),关闭动画期间使用 ref 保持文案不变 - const displayActionType = open ? actionType : actionTypeRef.current - - return ( - - - - Discard Generation - - - If you choose to exit or regenerate, the current images will be lost and one generation - attempt will be used. - - - cancel - - {displayActionType === 'exit' ? 'Discard' : 'Regenerate'} - - - - - ) -} - -export default AbandonCreationDialog diff --git a/src/components/features/ai-generate-button.tsx b/src/components/features/ai-generate-button.tsx deleted file mode 100644 index 6ecdce8..0000000 --- a/src/components/features/ai-generate-button.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { cn } from '@/lib/utils' -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '../ui/alert-dialog' -import { useState } from 'react' -import { toast } from 'sonner' - -const AIGenerateButton = ({ - loading, - showAlert, - onConfirm, - className, - getNickName, - disabled, -}: { - loading?: boolean - showAlert?: boolean - onConfirm: (() => Promise | void) | (() => void) - className?: string - getNickName?: () => string - disabled?: boolean -}) => { - const [open, setOpen] = useState(false) - const [isLoading, setIsLoading] = useState(false) - - const handleConfirm = async () => { - if (disabled) return - if (getNickName && getNickName() === '') { - toast.error('Please enter nickname') - return - } - if (showAlert) { - setOpen(true) - return - } - setIsLoading(true) - try { - await onConfirm() - } catch (error) { - console.error(error) - } finally { - setIsLoading(false) - } - } - - const handleContinue = async () => { - setIsLoading(true) - try { - setOpen(false) - await onConfirm() - } catch (error) { - console.error(error) - } finally { - setIsLoading(false) - } - } - - if (loading || isLoading) { - return ( -
- - Generating... -
- ) - } - - return ( - <> -
- AI Generate -
- - - - Warning - - - Using AI generation will overwrite the content you have already entered. Do you want to - continue? - - - Cancel - Continue - - - - - ) -} -export default AIGenerateButton diff --git a/src/components/features/album-delete-alert.tsx b/src/components/features/album-delete-alert.tsx deleted file mode 100644 index e908d18..0000000 --- a/src/components/features/album-delete-alert.tsx +++ /dev/null @@ -1,75 +0,0 @@ -'use client' - -import { useDeleteAlbumImage } from '@/hooks/aiUser' -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from '../ui/alert-dialog' -import { Button } from '../ui/button' -import { useState } from 'react' - -const AlbumDeleteAlert = ({ - aiId, - albumId, - isDefaultImage, - onDeleted, - children, -}: { - aiId: number - albumId: number - isDefaultImage: boolean - onDeleted?: () => void - children: React.ReactNode -}) => { - const [isOpen, setIsOpen] = useState(false) - const deleteMutation = useDeleteAlbumImage() - - const handleDelete = async () => { - try { - await deleteMutation.mutateAsync({ aiId, albumId }) - onDeleted?.() - setIsOpen(false) - } catch (error) { - console.error(error) - } finally { - setIsOpen(false) - } - } - - return ( - - {children} - - - Delete Image - - - {isDefaultImage - ? 'The default cover image cannot be deleted. It will appear as your profile header, card cover, and chat background.' - : `Once deleted, the image cannot be restored. Users who have already unlocked this image will still be able to view it in the character's album.`} - - {isDefaultImage ? ( - - setIsOpen(false)}>Got it - - ) : ( - - Cancel - - - )} - - - ) -} - -export default AlbumDeleteAlert diff --git a/src/components/features/album-price-setting.tsx b/src/components/features/album-price-setting.tsx deleted file mode 100644 index 47fbc89..0000000 --- a/src/components/features/album-price-setting.tsx +++ /dev/null @@ -1,274 +0,0 @@ -'use client'; -import { useEffect, useState } from 'react'; -import { useForm } from 'react-hook-form'; -import { - AlertDialog, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from '../ui/alert-dialog'; -import { IconButton, Button } from '../ui/button'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'; -import { Input } from '../ui/input'; -import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../ui/form'; -import Image from 'next/image'; -import { z } from 'zod'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'; -import Decimal from 'decimal.js'; - -export interface AlbumPriceFormData { - unlockMethod: 'pay' | 'free'; - price?: string; -} - -const schema = z - .object({ - unlockMethod: z.enum(['pay', 'free']), - price: z.string().optional(), - }) - .refine( - (data) => { - // 如果选择了 "pay",price 必须填写且有效 - if (data.unlockMethod === 'pay') { - if (!data.price || data.price.trim() === '') { - return false; - } - if (Number(data.price) < 20) { - return false; - } - } - return true; - }, - { - message: 'At least 20 diamonds', - path: ['price'], - } - ); - -interface AlbumPriceSettingProps { - defaultUnlockPrice?: number; - onConfirm: (data: AlbumPriceFormData) => Promise; - children?: React.ReactNode; -} - -const AlbumPriceSetting = ({ defaultUnlockPrice, onConfirm, children }: AlbumPriceSettingProps) => { - const [isOpen, setIsOpen] = useState(false); - const [isLoading, setIsLoading] = useState(false); - - console.log('defaultUnlockPrice', defaultUnlockPrice); - - useEffect(() => { - if (isOpen) { - if (defaultUnlockPrice === undefined) { - form.setValue('unlockMethod', 'pay'); - form.setValue('price', '20'); - return; - } - if (defaultUnlockPrice) { - form.setValue('unlockMethod', 'pay'); - form.setValue('price', new Decimal(defaultUnlockPrice).div(100).toString()); - } else { - form.setValue('unlockMethod', 'free'); - form.setValue('price', ''); - } - } - }, [isOpen, defaultUnlockPrice]); - - const form = useForm({ - resolver: zodResolver(schema), - defaultValues: { - unlockMethod: 'pay', - price: '', - }, - }); - const unlockMethod = form.watch('unlockMethod'); - - useEffect(() => { - if (unlockMethod === 'free') { - const priceValue = form.getValues('price'); - if (!priceValue) { - form.setValue('price', '20'); - } - } - }, [unlockMethod]); - - const handleSubmit = async (data: AlbumPriceFormData) => { - setIsLoading(true); - try { - console.log('保存数据:', data); - const result = { - unlockMethod: data.unlockMethod, - price: data.unlockMethod === 'pay' ? data.price : undefined, - }; - await onConfirm(result); - setIsOpen(false); - form.reset(); - } catch (error) { - console.error('保存失败:', error); - } finally { - setIsLoading(false); - } - }; - - const handleCancel = () => { - setIsOpen(false); - form.reset(); - }; - - return ( - - - {children || ( - - - - )} - - - - - How to Unlock - - - -
- - -
- {/* 方法选择器 */} - ( - - -
-
How to Unlock
- - - - - -
-

- Users can unlock your character's images, giving you a share of - the revenue. -

-

- Spicyxx.AI keeps 20% of each image's sales as a service fee. -

-

- Offering a few free images can encourage users to interact with - your character. -

-
-
-
-
-
- - - - -
- )} - /> - - {/* 价格输入框 */} - {form.watch('unlockMethod') === 'pay' && ( - ( - - - Unlock Price - - - { - const target = e.target as HTMLInputElement; - // 只允许输入数字 - let value = target.value.replace(/[^0-9]/g, ''); - - // 如果为空,允许继续输入 - if (value === '') { - target.value = ''; - field.onChange(''); - return; - } - - // 转换为数字并限制范围 - const numValue = parseInt(value); - if (numValue > 99999) { - value = '99999'; - } - - target.value = value; - field.onChange(value); - }} - prefixIcon={ -
- Diamond -
- } - error={!!form.formState.errors.price} - /> -
- -
- )} - /> - )} -
-
- - - - - -
- -
-
- ); -}; - -export default AlbumPriceSetting; diff --git a/src/components/features/device-info.tsx b/src/components/features/device-info.tsx deleted file mode 100644 index efc5544..0000000 --- a/src/components/features/device-info.tsx +++ /dev/null @@ -1,103 +0,0 @@ -'use client' - -import { useEffect, useState } from 'react' -import { tokenManager } from '@/lib/auth/token' -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { Button } from '@/components/ui/button' - -export function DeviceInfo() { - const [deviceId, setDeviceId] = useState('') - const [token, setToken] = useState('') - - useEffect(() => { - // 获取当前的设备ID和token - const currentDeviceId = tokenManager.getDeviceId() - const currentToken = tokenManager.getToken() || '' - - setDeviceId(currentDeviceId) - setToken(currentToken) - }, []) - - const refreshDeviceId = () => { - // 生成新的设备ID - const newDeviceId = tokenManager.getDeviceId() - setDeviceId(newDeviceId) - } - - const clearAllData = () => { - tokenManager.clearAll() - setDeviceId('') - setToken('') - // 重新生成设备ID - setTimeout(() => { - const newDeviceId = tokenManager.getDeviceId() - setDeviceId(newDeviceId) - }, 100) - } - - return ( - - - 📱 设备信息 - - -
-

Device ID (sd) Device ID (sd)

-
- {deviceId || '未生成'} -
-

- Stored in a cookie, field named'sd ', sent as AUTH_DID' in the request header -

-
- -
-

- Authentication token (st) Authentication token (st) -

-
- {token || '未登录'} -
-

- Stored in a cookie, field named'st ', sent as AUTH_TK in the request header -

-
- -
- - -
- -
-
💡 设备ID说明
-
    -
  • - Automatically generated when a user visits for the first time Automatically generated - when a user visits for the first time -
  • -
  • - • Contains timestamp, random string and browser information • Contains timestamp, - random string and browser information -
  • -
  • - Store in a cookie with a valid period of 365 days Store in a cookie with a valid - period of 365 days -
  • -
  • - • For device identification and security verification • For device identification and - security verification -
  • -
  • - • Will not be cleared when logging out (only clearAll will be cleared) • Will not be - cleared when logging out (only clearAll will be cleared) -
  • -
-
-
-
- ) -} diff --git a/src/components/features/im-reconnect-status.tsx b/src/components/features/im-reconnect-status.tsx deleted file mode 100644 index 9561913..0000000 --- a/src/components/features/im-reconnect-status.tsx +++ /dev/null @@ -1,99 +0,0 @@ -'use client' - -import { useAtomValue } from 'jotai' -import { imReconnectStatusAtom, IMReconnectStatus } from '@/atoms/im' -import { cn } from '@/lib/utils' -import { useEffect, useState } from 'react' - -interface IMReconnectStatusProps { - className?: string -} - -const IMReconnectStatusBar: React.FC = ({ className }) => { - const reconnectStatus = useAtomValue(imReconnectStatusAtom) - const [isVisible, setIsVisible] = useState(false) - - // 控制状态条的显示/隐藏 - useEffect(() => { - if (reconnectStatus === IMReconnectStatus.CONNECTED) { - // 连接成功后,延迟隐藏状态条 - const timer = setTimeout(() => { - setIsVisible(false) - }, 2000) - return () => clearTimeout(timer) - } else if (reconnectStatus !== IMReconnectStatus.DISCONNECTED) { - // 断线、重连中、失败时立即显示 - setIsVisible(true) - } - }, [reconnectStatus]) - - // 不显示时返回null - if (!isVisible) { - return null - } - - const getStatusConfig = () => { - switch (reconnectStatus) { - case IMReconnectStatus.DISCONNECTED: - return { - text: '连接已断开', - bgColor: 'bg-error-default', - textColor: 'text-white', - icon: 'icon-status-error', - } - case IMReconnectStatus.RECONNECTING: - return { - text: '正在重连...', - bgColor: 'bg-warning-default', - textColor: 'text-white', - icon: 'icon-loading', - animate: true, - } - case IMReconnectStatus.FAILED: - return { - text: '重连失败,请刷新页面', - bgColor: 'bg-error-default', - textColor: 'text-white', - icon: 'icon-status-error', - } - case IMReconnectStatus.CONNECTED: - return { - text: '连接已恢复', - bgColor: 'bg-success-default', - textColor: 'text-white', - icon: 'icon-check', - } - default: - return null - } - } - - const statusConfig = getStatusConfig() - - if (!statusConfig) return null - - return ( -
-
- - {statusConfig.text} -
-
- ) -} - -export default IMReconnectStatusBar diff --git a/src/components/ui/text-md.tsx b/src/components/ui/text-md.tsx index b3a46f2..87d6582 100644 --- a/src/components/ui/text-md.tsx +++ b/src/components/ui/text-md.tsx @@ -1,17 +1,19 @@ -import React from 'react' +import React from 'react'; +import { useStreamChatStore } from '@/app/(main)/chat/[id]/stream-chat'; +import { cn } from '@/lib/utils'; // import './index.css' type AITextRenderProps = { - text: string -} + text: string; +} & React.HTMLAttributes; // 定义解析后的节点类型 -type TextStyle = 'dialogue' | 'emphasis' | 'emphasis2' | 'supplement' +type TextStyle = 'dialogue' | 'emphasis' | 'emphasis2' | 'supplement'; type TextNode = { - styles: TextStyle[] - content: string -} + styles: TextStyle[]; + content: string; +}; /** * 解析文本为节点数组 @@ -19,129 +21,129 @@ type TextNode = { * @returns 解析后的节点数组 */ const parseText = (text: string): TextNode[] => { - const nodes: TextNode[] = [] - const activeStyles = new Set() - let buffer = '' + const nodes: TextNode[] = []; + const activeStyles = new Set(); + let buffer = ''; const flush = () => { - if (!buffer) return - nodes.push({ styles: Array.from(activeStyles), content: buffer }) - buffer = '' - } + if (!buffer) return; + nodes.push({ styles: Array.from(activeStyles), content: buffer }); + buffer = ''; + }; - let i = 0 + let i = 0; while (i < text.length) { - const char = text[i] + const char = text[i]; // 换行符: \n if (char === '\n') { - flush() - nodes.push({ styles: [], content: '\n' }) - i += 1 - continue + flush(); + nodes.push({ styles: [], content: '\n' }); + i += 1; + continue; } // 一级强调: * // 不保留标记符号 if (char === '*') { - flush() + flush(); if (activeStyles.has('emphasis')) { - activeStyles.delete('emphasis') + activeStyles.delete('emphasis'); } else { - activeStyles.add('emphasis') + activeStyles.add('emphasis'); } - i += 1 - continue + i += 1; + continue; } // 二级强调: ^ // 不保留标记符号 if (char === '^') { - flush() + flush(); if (activeStyles.has('emphasis2')) { - activeStyles.delete('emphasis2') + activeStyles.delete('emphasis2'); } else { - activeStyles.add('emphasis2') + activeStyles.add('emphasis2'); } - i += 1 - continue + i += 1; + continue; } // 对话: "" (英文双引号) if (char === '"') { if (activeStyles.has('dialogue')) { // Closing: include quote, flush, remove style - buffer += char - flush() - activeStyles.delete('dialogue') + buffer += char; + flush(); + activeStyles.delete('dialogue'); } else { // Opening: flush prev, add style, include quote - flush() - activeStyles.add('dialogue') - buffer += char + flush(); + activeStyles.add('dialogue'); + buffer += char; } - i += 1 - continue + i += 1; + continue; } // 对话: “” (中文双引号) if (char === '“') { - flush() - activeStyles.add('dialogue') - buffer += char - i += 1 - continue + flush(); + activeStyles.add('dialogue'); + buffer += char; + i += 1; + continue; } if (char === '”') { - buffer += char - flush() - activeStyles.delete('dialogue') - i += 1 - continue + buffer += char; + flush(); + activeStyles.delete('dialogue'); + i += 1; + continue; } // 对话:「」 if (char === '「') { - flush() - activeStyles.add('dialogue') - buffer += char - i += 1 - continue + flush(); + activeStyles.add('dialogue'); + buffer += char; + i += 1; + continue; } if (char === '」') { - buffer += char - flush() - activeStyles.delete('dialogue') - i += 1 - continue + buffer += char; + flush(); + activeStyles.delete('dialogue'); + i += 1; + continue; } // 补充: () or () if (char === '(' || char === '(') { - flush() - activeStyles.add('supplement') - buffer += char - i += 1 - continue + flush(); + activeStyles.add('supplement'); + buffer += char; + i += 1; + continue; } if (char === ')' || char === ')') { - buffer += char - flush() - activeStyles.delete('supplement') - i += 1 - continue + buffer += char; + flush(); + activeStyles.delete('supplement'); + i += 1; + continue; } - buffer += char - i += 1 + buffer += char; + i += 1; } - flush() + flush(); - return nodes -} + return nodes; +}; /** * 渲染单个节点 @@ -150,55 +152,65 @@ const parseText = (text: string): TextNode[] => { * @returns React 元素 */ const renderNode = (node: TextNode, index: number): React.ReactNode => { - const { styles, content } = node + const { styles, content } = node; // 处理换行符 if (content === '\n') { - return
+ return
; } - let className = '' + let className = ''; if (styles.length === 0) { - return {content} + return {content}; } // 叠加样式 if (styles.includes('dialogue')) { - className += ' text-[rgba(255,236,159,1)]' + className += ' text-[rgba(255,236,159,1)]'; } if (styles.includes('emphasis')) { - className += ' font-semibold' + className += ' font-semibold'; } if (styles.includes('emphasis2')) { - className += ' underline' + className += ' underline'; } if (styles.includes('supplement')) { - className += ' text-[rgba(181,177,207,1)]' + className += ' text-[rgba(181,177,207,1)]'; } return ( {content} - ) -} + ); +}; /** * AI 文本渲染组件 */ -const AITextRender = React.memo(({ text }: AITextRenderProps) => { +const AITextRender = React.memo(({ text, ...props }: AITextRenderProps) => { // 1. 解析文本 - const nodes = parseText(text) + const nodes = parseText(text); + const font = useStreamChatStore((store) => store.chatSetting.font); + const fontClass = + { + 12: 'text-xs', + 14: 'text-sm', + 16: 'text-base', + 18: 'text-lg', + 20: 'text-xl', + }[font] || 'text-base'; // 2. 渲染节点 - // 这里的 className "ai-text-render" 对应 CSS 中的根类,用于控制全局字体、行高等 return ( - {nodes.map((node, index) => renderNode(node, index))} - ) -}) + + {nodes.map((node, index) => renderNode(node, index))} + + ); +}); -export default AITextRender +export default AITextRender; diff --git a/src/hooks/services/common.ts b/src/hooks/services/common.ts new file mode 100644 index 0000000..817d2e6 --- /dev/null +++ b/src/hooks/services/common.ts @@ -0,0 +1,36 @@ +import { getLikeStatus, thmubObject } from '@/services/editor'; +import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useState } from 'react'; +import { authKeys } from '@/lib/query-keys'; +import { LikeObjectParamsType, LikeType } from '@/services/editor/type'; +import { useAsyncFn } from '../tools'; + +export function useThmubObject(props: Pick) { + const queryClient = useQueryClient(); + const [thumb, setThumb] = useState(); + const user = queryClient.getQueryData(authKeys.currentUser()) as any; + + useQuery({ + queryKey: ['likeStatus', props.objectId, user.userId], + enabled: !!props.objectId && !!user.userId, + queryFn: () => { + return getLikeStatus({ objectId: props.objectId, userId: user.userId }); + }, + }); + + const { run: handleThumb, loading } = useAsyncFn(async (likeType: LikeType) => { + setThumb(likeType); + const user = queryClient.getQueryData(authKeys.currentUser()) as any; + const { data } = await thmubObject({ ...props, likeType, userId: user?.userId }); + if (data.code === 200) { + } else { + setThumb(undefined); + } + }); + + return { + thumb, + loading, + handleThumb, + }; +} diff --git a/src/services/editor/index.ts b/src/services/editor/index.ts index e26c8a7..e8d6f96 100644 --- a/src/services/editor/index.ts +++ b/src/services/editor/index.ts @@ -1,4 +1,5 @@ import { editorRequest } from '@/lib/client'; +import { LikeObjectParamsType } from './type'; export async function fetchCharacters({ index, limit, query }: any) { const { data } = await editorRequest('/api/character/list', { @@ -16,3 +17,12 @@ export async function fetchCharacter(params: any) { export async function fetchCharacterTags(params: any = {}) { return editorRequest('/api/tag/list', { method: 'POST', data: params }); } + +export async function thmubObject(params: LikeObjectParamsType) { + return editorRequest('/api/like/likeOrCancel', { method: 'POST', data: params }); +} + +export async function getLikeStatus(params: Pick) { + const { data } = await editorRequest('/api/like/getLikeStatus', { method: 'POST', data: params }); + return data; +} diff --git a/src/services/editor/type.d.ts b/src/services/editor/type.ts similarity index 61% rename from src/services/editor/type.d.ts rename to src/services/editor/type.ts index c958cdd..87e8545 100644 --- a/src/services/editor/type.d.ts +++ b/src/services/editor/type.ts @@ -21,3 +21,18 @@ export type CharacterType = { }[]; [x: string]: any; }; + +export enum LikeTargetType { + Character = 0, + Story = 1, +} +export enum LikeType { + Liked = 0, + Canceled = 1, +} +export type LikeObjectParamsType = { + objectId?: string; + objectType?: LikeTargetType; + userId?: number; + likeType?: LikeType; +};