refactor(PostCard, ImageGrid, SmartImage): enhance post editing display and image handling
- Introduced a new method to determine the last edited time for posts, prioritizing content_edited_at for better accuracy in display. - Updated PostCard and PostDetailScreen components to utilize the new editing display logic. - Enhanced ImageGrid and SmartImage components to support additional preview URLs and improve image loading behavior, including lazy loading optimizations for web. - Modified Post and PostDTO interfaces to include contentEditedAt for better tracking of post edits.
This commit is contained in:
@@ -135,12 +135,24 @@ const PostCard: React.FC<PostCardProps> = ({
|
|||||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isPostEdited = (createdAt?: string, updatedAt?: string): boolean => {
|
/** 返回应展示的「最后编辑」时间字符串;优先 content_edited_at(与点赞/评论等统计更新解耦) */
|
||||||
if (!createdAt || !updatedAt) return false;
|
const getPostEditedDisplayAt = (
|
||||||
const created = new Date(createdAt).getTime();
|
createdAt?: string | null,
|
||||||
const updated = new Date(updatedAt).getTime();
|
contentEditedAt?: string | null,
|
||||||
if (Number.isNaN(created) || Number.isNaN(updated)) return false;
|
updatedAt?: string | null,
|
||||||
return updated - created > 1000;
|
): string | null => {
|
||||||
|
if (contentEditedAt) {
|
||||||
|
const c = new Date(createdAt || '').getTime();
|
||||||
|
const e = new Date(contentEditedAt).getTime();
|
||||||
|
if (!Number.isNaN(c) && !Number.isNaN(e) && e - c > 1000) return contentEditedAt;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (createdAt && updatedAt) {
|
||||||
|
const c = new Date(createdAt).getTime();
|
||||||
|
const u = new Date(updatedAt).getTime();
|
||||||
|
if (!Number.isNaN(c) && !Number.isNaN(u) && u - c > 1000) return updatedAt;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTruncatedContent = (content: string | undefined | null, maxLength: number = 100): string => {
|
const getTruncatedContent = (content: string | undefined | null, maxLength: number = 100): string => {
|
||||||
@@ -560,11 +572,18 @@ const PostCard: React.FC<PostCardProps> = ({
|
|||||||
<Text variant="caption" color={colors.text.hint} style={styles.timeText}>
|
<Text variant="caption" color={colors.text.hint} style={styles.timeText}>
|
||||||
发布 {formatDateTime(post.created_at)}
|
发布 {formatDateTime(post.created_at)}
|
||||||
</Text>
|
</Text>
|
||||||
{isPostEdited(post.created_at, post.updated_at) && (
|
{(() => {
|
||||||
|
const editedAt = getPostEditedDisplayAt(
|
||||||
|
post.created_at,
|
||||||
|
post.content_edited_at,
|
||||||
|
post.updated_at,
|
||||||
|
);
|
||||||
|
return editedAt ? (
|
||||||
<Text variant="caption" color={colors.text.hint} style={styles.timeText}>
|
<Text variant="caption" color={colors.text.hint} style={styles.timeText}>
|
||||||
{' · 修改 '}{formatDateTime(post.updated_at)}
|
{' · 修改 '}{formatDateTime(editedAt)}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
) : null;
|
||||||
|
})()}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{post.is_pinned && (
|
{post.is_pinned && (
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
Image,
|
Image,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SmartImage, ImageSource } from './SmartImage';
|
import { SmartImage } from './SmartImage';
|
||||||
import { colors, spacing, borderRadius } from '../../theme';
|
import { colors, spacing, borderRadius } from '../../theme';
|
||||||
import { getPreviewImageUrl, ImageDisplayMode } from '../../utils/imageHelper';
|
import { getPreviewImageUrl, ImageDisplayMode } from '../../utils/imageHelper';
|
||||||
|
|
||||||
@@ -40,6 +40,8 @@ export interface ImageGridItem {
|
|||||||
url?: string;
|
url?: string;
|
||||||
thumbnailUrl?: string;
|
thumbnailUrl?: string;
|
||||||
thumbnail_url?: string;
|
thumbnail_url?: string;
|
||||||
|
preview_url?: string;
|
||||||
|
preview_url_large?: string;
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
}
|
}
|
||||||
@@ -108,6 +110,8 @@ interface SingleImageItemProps {
|
|||||||
maxHeight: number;
|
maxHeight: number;
|
||||||
borderRadiusValue: number;
|
borderRadiusValue: number;
|
||||||
onPress: () => void;
|
onPress: () => void;
|
||||||
|
usePreview: boolean;
|
||||||
|
displayMode: ImageDisplayMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SingleImageItem: React.FC<SingleImageItemProps> = ({
|
const SingleImageItem: React.FC<SingleImageItemProps> = ({
|
||||||
@@ -116,15 +120,26 @@ const SingleImageItem: React.FC<SingleImageItemProps> = ({
|
|||||||
maxHeight,
|
maxHeight,
|
||||||
borderRadiusValue,
|
borderRadiusValue,
|
||||||
onPress,
|
onPress,
|
||||||
|
usePreview,
|
||||||
|
displayMode,
|
||||||
}) => {
|
}) => {
|
||||||
const [aspectRatio, setAspectRatio] = useState<number | null>(null);
|
const [aspectRatio, setAspectRatio] = useState<number | null>(null);
|
||||||
const uri = image.uri || image.url || '';
|
const mainUri = image.uri || image.url || '';
|
||||||
|
const thumbnailUri =
|
||||||
|
image.thumbnail_url ||
|
||||||
|
image.thumbnailUrl ||
|
||||||
|
image.preview_url ||
|
||||||
|
'';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!uri) return;
|
if (image.width && image.height) {
|
||||||
|
setAspectRatio(image.width / image.height);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const probeUri = thumbnailUri || mainUri;
|
||||||
|
if (!probeUri) return;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
// 添加超时处理,防止高分辨率图片加载卡住
|
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
if (!cancelled && aspectRatio === null) {
|
if (!cancelled && aspectRatio === null) {
|
||||||
setAspectRatio(SINGLE_IMAGE_DEFAULT_ASPECT_RATIO);
|
setAspectRatio(SINGLE_IMAGE_DEFAULT_ASPECT_RATIO);
|
||||||
@@ -132,7 +147,7 @@ const SingleImageItem: React.FC<SingleImageItemProps> = ({
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
|
|
||||||
Image.getSize(
|
Image.getSize(
|
||||||
uri,
|
probeUri,
|
||||||
(w, h) => {
|
(w, h) => {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
@@ -150,7 +165,7 @@ const SingleImageItem: React.FC<SingleImageItemProps> = ({
|
|||||||
cancelled = true;
|
cancelled = true;
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
};
|
};
|
||||||
}, [uri]);
|
}, [image.width, image.height, thumbnailUri, mainUri]);
|
||||||
|
|
||||||
const effectiveContainerWidth = containerWidth || SCREEN_WIDTH - DEFAULT_CONTAINER_PADDING;
|
const effectiveContainerWidth = containerWidth || SCREEN_WIDTH - DEFAULT_CONTAINER_PADDING;
|
||||||
|
|
||||||
@@ -189,13 +204,19 @@ const SingleImageItem: React.FC<SingleImageItemProps> = ({
|
|||||||
width = Math.min(SINGLE_IMAGE_MIN_HEIGHT * aspectRatio, effectiveContainerWidth);
|
width = Math.min(SINGLE_IMAGE_MIN_HEIGHT * aspectRatio, effectiveContainerWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const previewForSmart =
|
||||||
|
usePreview && displayMode ? getPreviewImageUrl(image as any, displayMode) : '';
|
||||||
|
const useSmartPreview = !!(usePreview && previewForSmart && mainUri && previewForSmart !== mainUri);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
style={[styles.singleContainer, { width, height, borderRadius: borderRadiusValue }]}
|
style={[styles.singleContainer, { width, height, borderRadius: borderRadiusValue }]}
|
||||||
>
|
>
|
||||||
<SmartImage
|
<SmartImage
|
||||||
source={{ uri }}
|
source={{ uri: mainUri }}
|
||||||
|
previewUrl={useSmartPreview ? previewForSmart : undefined}
|
||||||
|
usePreview={useSmartPreview}
|
||||||
style={styles.fullSize}
|
style={styles.fullSize}
|
||||||
resizeMode="cover"
|
resizeMode="cover"
|
||||||
borderRadius={borderRadiusValue}
|
borderRadius={borderRadiusValue}
|
||||||
@@ -266,11 +287,6 @@ export const ImageGrid: React.FC<ImageGridProps> = ({
|
|||||||
const image = displayImages[0];
|
const image = displayImages[0];
|
||||||
if (!image) return null;
|
if (!image) return null;
|
||||||
|
|
||||||
// 获取预览图 URL
|
|
||||||
const previewUrl = usePreview && displayMode
|
|
||||||
? getPreviewImageUrl(image as any, displayMode)
|
|
||||||
: (image.uri || image.url || '');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SingleImageItem
|
<SingleImageItem
|
||||||
image={image}
|
image={image}
|
||||||
@@ -278,6 +294,8 @@ export const ImageGrid: React.FC<ImageGridProps> = ({
|
|||||||
maxHeight={singleImageMaxHeight}
|
maxHeight={singleImageMaxHeight}
|
||||||
borderRadiusValue={borderRadiusValue}
|
borderRadiusValue={borderRadiusValue}
|
||||||
onPress={() => handleImagePress(0)}
|
onPress={() => handleImagePress(0)}
|
||||||
|
usePreview={usePreview}
|
||||||
|
displayMode={displayMode}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Pressable,
|
Pressable,
|
||||||
StyleProp,
|
StyleProp,
|
||||||
|
Platform,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { Image as ExpoImage } from 'expo-image';
|
import { Image as ExpoImage } from 'expo-image';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
@@ -61,6 +62,8 @@ export interface SmartImageProps {
|
|||||||
usePreview?: boolean;
|
usePreview?: boolean;
|
||||||
/** 是否懒加载 */
|
/** 是否懒加载 */
|
||||||
lazyLoad?: boolean;
|
lazyLoad?: boolean;
|
||||||
|
/** Web 端 IntersectionObserver rootMargin,略提前加载进入视口附近的图 */
|
||||||
|
lazyRootMargin?: string;
|
||||||
/** 缓存策略 */
|
/** 缓存策略 */
|
||||||
cachePolicy?: 'memory' | 'disk' | 'memory-disk' | 'none';
|
cachePolicy?: 'memory' | 'disk' | 'memory-disk' | 'none';
|
||||||
}
|
}
|
||||||
@@ -85,11 +88,13 @@ export const SmartImage: React.FC<SmartImageProps> = ({
|
|||||||
previewUrl,
|
previewUrl,
|
||||||
usePreview = false,
|
usePreview = false,
|
||||||
lazyLoad = false,
|
lazyLoad = false,
|
||||||
|
lazyRootMargin = '160px',
|
||||||
cachePolicy = 'memory-disk',
|
cachePolicy = 'memory-disk',
|
||||||
}) => {
|
}) => {
|
||||||
const [loadState, setLoadState] = useState<ImageLoadState>('loading');
|
const [loadState, setLoadState] = useState<ImageLoadState>('loading');
|
||||||
const [isVisible, setIsVisible] = useState(!lazyLoad);
|
const [isVisible, setIsVisible] = useState(() => !lazyLoad || Platform.OS !== 'web');
|
||||||
const [shouldLoadOriginal, setShouldLoadOriginal] = useState(false);
|
const [shouldLoadOriginal, setShouldLoadOriginal] = useState(false);
|
||||||
|
const lazyTargetRef = useRef<View>(null);
|
||||||
|
|
||||||
// 解析图片源 - 支持 uri 或 url 字段
|
// 解析图片源 - 支持 uri 或 url 字段
|
||||||
const imageUri = typeof source === 'string'
|
const imageUri = typeof source === 'string'
|
||||||
@@ -99,6 +104,39 @@ export const SmartImage: React.FC<SmartImageProps> = ({
|
|||||||
// 解析预览图 URL
|
// 解析预览图 URL
|
||||||
const previewUri = typeof previewUrl === 'string' ? previewUrl : '';
|
const previewUri = typeof previewUrl === 'string' ? previewUrl : '';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setShouldLoadOriginal(false);
|
||||||
|
setLoadState('loading');
|
||||||
|
}, [imageUri]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!lazyLoad) {
|
||||||
|
setIsVisible(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Platform.OS !== 'web') {
|
||||||
|
setIsVisible(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsVisible(false);
|
||||||
|
const node = lazyTargetRef.current as unknown as Element | null;
|
||||||
|
if (!node || typeof IntersectionObserver === 'undefined') {
|
||||||
|
setIsVisible(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
entries => {
|
||||||
|
if (entries.some(e => e.isIntersecting)) {
|
||||||
|
setIsVisible(true);
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ rootMargin: lazyRootMargin }
|
||||||
|
);
|
||||||
|
observer.observe(node);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [lazyLoad, lazyRootMargin, imageUri]);
|
||||||
|
|
||||||
// 智能选择图片 URL
|
// 智能选择图片 URL
|
||||||
const getImageUrl = useCallback((): string => {
|
const getImageUrl = useCallback((): string => {
|
||||||
// 如果应该加载原图(预览图已加载完成或没有预览图)
|
// 如果应该加载原图(预览图已加载完成或没有预览图)
|
||||||
@@ -200,10 +238,12 @@ export const SmartImage: React.FC<SmartImageProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是懒加载且不可见,显示占位
|
// 如果是懒加载且不可见,显示占位(Web 上由 IntersectionObserver 驱动进入视口后再加载)
|
||||||
if (lazyLoad && !isVisible) {
|
if (lazyLoad && !isVisible) {
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
|
ref={Platform.OS === 'web' ? lazyTargetRef : undefined}
|
||||||
|
collapsable={false}
|
||||||
style={[containerStyle, style as ViewStyle, { backgroundColor: '#F5F5F5' }]}
|
style={[containerStyle, style as ViewStyle, { backgroundColor: '#F5F5F5' }]}
|
||||||
testID={testID}
|
testID={testID}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ export interface Post {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
/** 更新时间 */
|
/** 更新时间 */
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
/** 用户编辑内容的时间(与统计类更新解耦) */
|
||||||
|
contentEditedAt?: string;
|
||||||
|
|
||||||
// ==================== 向后兼容字段 (snake_case) ====================
|
// ==================== 向后兼容字段 (snake_case) ====================
|
||||||
// 这些字段用于与旧代码兼容,新代码应使用 camelCase 版本
|
// 这些字段用于与旧代码兼容,新代码应使用 camelCase 版本
|
||||||
@@ -107,6 +109,8 @@ export interface Post {
|
|||||||
created_at?: string;
|
created_at?: string;
|
||||||
/** @deprecated 请使用 updatedAt */
|
/** @deprecated 请使用 updatedAt */
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
|
/** @deprecated 请使用 contentEditedAt */
|
||||||
|
content_edited_at?: string;
|
||||||
/** @deprecated 请使用 communityId */
|
/** @deprecated 请使用 communityId */
|
||||||
community_id?: string;
|
community_id?: string;
|
||||||
/** @deprecated 请使用 status */
|
/** @deprecated 请使用 status */
|
||||||
|
|||||||
@@ -27,7 +27,12 @@ export class PostMapper {
|
|||||||
communityId: response.community_id,
|
communityId: response.community_id,
|
||||||
tags: [],
|
tags: [],
|
||||||
createdAt: new Date(response.created_at || Date.now()),
|
createdAt: new Date(response.created_at || Date.now()),
|
||||||
updatedAt: new Date(response.updated_at || Date.now()),
|
// 空字符串/缺省时用 created_at,避免误用 Date.now() 导致全部显示「刚修改」
|
||||||
|
updatedAt: new Date(
|
||||||
|
response.updated_at ||
|
||||||
|
response.created_at ||
|
||||||
|
Date.now()
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ interface PostApiResponse {
|
|||||||
tags?: string[];
|
tags?: string[];
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
|
content_edited_at?: string;
|
||||||
author?: {
|
author?: {
|
||||||
id: number;
|
id: number;
|
||||||
username: string;
|
username: string;
|
||||||
@@ -122,6 +123,7 @@ export class PostRepository implements IPostRepository {
|
|||||||
tags: model.tags || [],
|
tags: model.tags || [],
|
||||||
createdAt: model.createdAt.toISOString(),
|
createdAt: model.createdAt.toISOString(),
|
||||||
updatedAt: model.updatedAt.toISOString(),
|
updatedAt: model.updatedAt.toISOString(),
|
||||||
|
contentEditedAt: response.content_edited_at,
|
||||||
|
|
||||||
// snake_case 字段(向后兼容)
|
// snake_case 字段(向后兼容)
|
||||||
likes_count: model.likeCount,
|
likes_count: model.likeCount,
|
||||||
@@ -135,6 +137,7 @@ export class PostRepository implements IPostRepository {
|
|||||||
user_id: model.authorId,
|
user_id: model.authorId,
|
||||||
created_at: model.createdAt.toISOString(),
|
created_at: model.createdAt.toISOString(),
|
||||||
updated_at: model.updatedAt.toISOString(),
|
updated_at: model.updatedAt.toISOString(),
|
||||||
|
content_edited_at: response.content_edited_at,
|
||||||
community_id: model.communityId,
|
community_id: model.communityId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -318,12 +318,23 @@ export const PostDetailScreen: React.FC = () => {
|
|||||||
return formatDateTime(dateString);
|
return formatDateTime(dateString);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isPostEdited = (createdAt?: string, updatedAt?: string): boolean => {
|
const getPostEditedDisplayAt = (
|
||||||
if (!createdAt || !updatedAt) return false;
|
createdAt?: string | null,
|
||||||
const created = new Date(createdAt).getTime();
|
contentEditedAt?: string | null,
|
||||||
const updated = new Date(updatedAt).getTime();
|
updatedAt?: string | null,
|
||||||
if (Number.isNaN(created) || Number.isNaN(updated)) return false;
|
): string | null => {
|
||||||
return updated-created > 1000;
|
if (contentEditedAt) {
|
||||||
|
const c = new Date(createdAt || '').getTime();
|
||||||
|
const e = new Date(contentEditedAt).getTime();
|
||||||
|
if (!Number.isNaN(c) && !Number.isNaN(e) && e - c > 1000) return contentEditedAt;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (createdAt && updatedAt) {
|
||||||
|
const c = new Date(createdAt).getTime();
|
||||||
|
const u = new Date(updatedAt).getTime();
|
||||||
|
if (!Number.isNaN(c) && !Number.isNaN(u) && u - c > 1000) return updatedAt;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 格式化数字
|
// 格式化数字
|
||||||
@@ -1081,14 +1092,21 @@ export const PostDetailScreen: React.FC = () => {
|
|||||||
<Text variant="caption" color={colors.text.hint} style={styles.metaInfoText}>
|
<Text variant="caption" color={colors.text.hint} style={styles.metaInfoText}>
|
||||||
发布 {formatRelativeTime(post.created_at)}
|
发布 {formatRelativeTime(post.created_at)}
|
||||||
</Text>
|
</Text>
|
||||||
{isPostEdited(post.created_at, post.updated_at) && (
|
{(() => {
|
||||||
|
const editedAt = getPostEditedDisplayAt(
|
||||||
|
post.created_at,
|
||||||
|
post.content_edited_at,
|
||||||
|
post.updated_at,
|
||||||
|
);
|
||||||
|
return editedAt ? (
|
||||||
<>
|
<>
|
||||||
<Text style={styles.metaInfoDot}>·</Text>
|
<Text style={styles.metaInfoDot}>·</Text>
|
||||||
<Text variant="caption" color={colors.text.hint} style={styles.metaInfoText}>
|
<Text variant="caption" color={colors.text.hint} style={styles.metaInfoText}>
|
||||||
修改 {formatRelativeTime(post.updated_at)}
|
修改 {formatRelativeTime(editedAt)}
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
</>
|
||||||
)}
|
) : null;
|
||||||
|
})()}
|
||||||
{post.views_count !== undefined && post.views_count > 0 && (
|
{post.views_count !== undefined && post.views_count > 0 && (
|
||||||
<>
|
<>
|
||||||
<Text style={styles.metaInfoDot}>·</Text>
|
<Text style={styles.metaInfoDot}>·</Text>
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ export interface PostDTO {
|
|||||||
is_vote: boolean;
|
is_vote: boolean;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
|
/** 用户编辑正文/标题/图片的时间;优先用于「已编辑」展示 */
|
||||||
|
content_edited_at?: string;
|
||||||
author: UserDTO | null;
|
author: UserDTO | null;
|
||||||
is_liked: boolean;
|
is_liked: boolean;
|
||||||
is_favorited: boolean;
|
is_favorited: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user