feat(miniapp): 知识库查看页
This commit is contained in:
@@ -0,0 +1,34 @@
|
|||||||
|
import { get } from './request';
|
||||||
|
|
||||||
|
export interface Disease {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
category: string;
|
||||||
|
pathogen?: string;
|
||||||
|
transmission?: string;
|
||||||
|
medium?: string;
|
||||||
|
incubation?: string;
|
||||||
|
symptoms?: string;
|
||||||
|
highRiskStage?: string;
|
||||||
|
highRiskCondition?: string;
|
||||||
|
lethalTime?: string;
|
||||||
|
spreadTrend?: string;
|
||||||
|
recurrence?: string;
|
||||||
|
detection?: string;
|
||||||
|
prevention?: string;
|
||||||
|
sortOrder?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KnowledgeArticle {
|
||||||
|
id: string;
|
||||||
|
kind: string;
|
||||||
|
title: string;
|
||||||
|
summary?: string;
|
||||||
|
content?: string;
|
||||||
|
sortOrder?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listDiseases = () => get<Disease[]>('/knowledge/diseases');
|
||||||
|
export const getDisease = (id: string) => get<Disease>(`/knowledge/diseases/${id}`);
|
||||||
|
export const listAIGuides = () => get<KnowledgeArticle[]>('/knowledge/articles', { kind: 'ai_guide' });
|
||||||
|
export const getArticle = (id: string) => get<KnowledgeArticle>(`/knowledge/articles/${id}`);
|
||||||
@@ -11,6 +11,8 @@ export default defineAppConfig({
|
|||||||
'pages/video/index',
|
'pages/video/index',
|
||||||
'pages/video/player/index',
|
'pages/video/player/index',
|
||||||
'pages/settings/index',
|
'pages/settings/index',
|
||||||
|
'pages/knowledge/index',
|
||||||
|
'pages/knowledge/detail/index',
|
||||||
],
|
],
|
||||||
window: {
|
window: {
|
||||||
backgroundTextStyle: 'dark',
|
backgroundTextStyle: 'dark',
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '知识详情',
|
||||||
|
});
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
@use '@/styles/variables.scss' as *;
|
||||||
|
|
||||||
|
.detailPage {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: $color-bg-page;
|
||||||
|
padding-bottom: $spacing-xl;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
background: linear-gradient(135deg, $color-primary 0%, $color-primary-dark 100%);
|
||||||
|
padding: $spacing-xl $spacing-lg;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $spacing-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heroName {
|
||||||
|
font-size: $font-size-xxl;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
color: $color-text-white;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heroTag {
|
||||||
|
font-size: $font-size-xs;
|
||||||
|
color: $color-text-white;
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
padding: 4rpx $spacing-sm;
|
||||||
|
border-radius: $radius-xs;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionList {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $spacing-md;
|
||||||
|
padding: $spacing-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionCard {
|
||||||
|
background: $color-bg-card;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
padding: $spacing-lg;
|
||||||
|
box-shadow: $shadow-card;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionLabel {
|
||||||
|
display: block;
|
||||||
|
font-size: $font-size-xs;
|
||||||
|
color: $color-text-tertiary;
|
||||||
|
margin-bottom: $spacing-xs;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionValue {
|
||||||
|
display: block;
|
||||||
|
font-size: $font-size-md;
|
||||||
|
color: $color-text-primary;
|
||||||
|
line-height: $line-height-loose;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.articleCard {
|
||||||
|
margin: $spacing-lg;
|
||||||
|
background: $color-bg-card;
|
||||||
|
border-radius: $radius-lg;
|
||||||
|
padding: $spacing-lg;
|
||||||
|
box-shadow: $shadow-card;
|
||||||
|
}
|
||||||
|
|
||||||
|
.articleTitle {
|
||||||
|
display: block;
|
||||||
|
font-size: $font-size-xl;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
color: $color-text-primary;
|
||||||
|
margin-bottom: $spacing-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.articleSummary {
|
||||||
|
display: block;
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
color: $color-text-secondary;
|
||||||
|
margin-bottom: $spacing-md;
|
||||||
|
line-height: $line-height-normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.articleContent {
|
||||||
|
display: block;
|
||||||
|
font-size: $font-size-md;
|
||||||
|
color: $color-text-primary;
|
||||||
|
line-height: $line-height-loose;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyText {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
color: $color-text-tertiary;
|
||||||
|
padding: $spacing-xl 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { ScrollView, Text, View } from '@tarojs/components';
|
||||||
|
import Taro, { useRouter } from '@tarojs/taro';
|
||||||
|
import styles from './index.module.scss';
|
||||||
|
import {
|
||||||
|
getArticle,
|
||||||
|
getDisease,
|
||||||
|
type Disease,
|
||||||
|
type KnowledgeArticle,
|
||||||
|
} from '@/api/knowledge';
|
||||||
|
|
||||||
|
const CATEGORY_LABELS: Record<string, string> = {
|
||||||
|
viral: '病毒性',
|
||||||
|
fungal: '真菌性',
|
||||||
|
bacterial: '细菌性',
|
||||||
|
protozoan: '原虫性',
|
||||||
|
other: '其他',
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildSections = (d: Disease) =>
|
||||||
|
[
|
||||||
|
{ label: '病原', value: d.pathogen },
|
||||||
|
{ label: '传播途径', value: d.transmission },
|
||||||
|
{ label: '传染媒介', value: d.medium },
|
||||||
|
{ label: '潜伏期', value: d.incubation },
|
||||||
|
{ label: '主要症状', value: d.symptoms },
|
||||||
|
{ label: '高发阶段', value: d.highRiskStage },
|
||||||
|
{ label: '高发条件', value: d.highRiskCondition },
|
||||||
|
{ label: '致死时间', value: d.lethalTime },
|
||||||
|
{ label: '传播趋势', value: d.spreadTrend },
|
||||||
|
{ label: '连发性特征', value: d.recurrence },
|
||||||
|
{ label: '检测方法', value: d.detection },
|
||||||
|
{ label: '防治指南', value: d.prevention },
|
||||||
|
].filter((s) => s.value && s.value !== '—');
|
||||||
|
|
||||||
|
const DetailPage: React.FC = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const kind = router.params.kind || 'disease';
|
||||||
|
const id = router.params.id || '';
|
||||||
|
const [disease, setDisease] = useState<Disease | null>(null);
|
||||||
|
const [article, setArticle] = useState<KnowledgeArticle | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
const fetchData = useCallback(async () => {
|
||||||
|
if (!id) {
|
||||||
|
setError('缺少内容ID');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (kind === 'ai_guide') {
|
||||||
|
setArticle(await getArticle(id));
|
||||||
|
} else {
|
||||||
|
setDisease(await getDisease(id));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[KnowledgeDetail] 加载失败:', err);
|
||||||
|
setError(err instanceof Error ? err.message : '数据加载失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [id, kind]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, [fetchData]);
|
||||||
|
|
||||||
|
if (article) {
|
||||||
|
return (
|
||||||
|
<ScrollView className={styles.detailPage} scrollY>
|
||||||
|
<View className={styles.articleCard}>
|
||||||
|
<Text className={styles.articleTitle}>{article.title}</Text>
|
||||||
|
{article.summary ? <Text className={styles.articleSummary}>{article.summary}</Text> : null}
|
||||||
|
<Text className={styles.articleContent}>{article.content}</Text>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disease) {
|
||||||
|
const sections = buildSections(disease);
|
||||||
|
return (
|
||||||
|
<ScrollView className={styles.detailPage} scrollY>
|
||||||
|
<View className={styles.hero}>
|
||||||
|
<Text className={styles.heroName}>{disease.name}</Text>
|
||||||
|
<Text className={styles.heroTag}>{CATEGORY_LABELS[disease.category] || disease.category}</Text>
|
||||||
|
</View>
|
||||||
|
<View className={styles.sectionList}>
|
||||||
|
{sections.map((s) => (
|
||||||
|
<View key={s.label} className={styles.sectionCard}>
|
||||||
|
<Text className={styles.sectionLabel}>{s.label}</Text>
|
||||||
|
<Text className={styles.sectionValue}>{s.value}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className={styles.detailPage}>
|
||||||
|
<Text className={styles.emptyText}>{loading ? '加载中...' : error || '内容不存在'}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DetailPage;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '知识库',
|
||||||
|
enablePullDownRefresh: true,
|
||||||
|
});
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
@use '@/styles/variables.scss' as *;
|
||||||
|
|
||||||
|
.knowledgePage {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: $color-bg-page;
|
||||||
|
padding: $spacing-md $spacing-lg;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guideCard {
|
||||||
|
background: $color-bg-card;
|
||||||
|
border-radius: $radius-lg;
|
||||||
|
padding: $spacing-lg;
|
||||||
|
box-shadow: $shadow-card;
|
||||||
|
margin-bottom: $spacing-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guideHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: $spacing-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guideBadge {
|
||||||
|
font-size: $font-size-xs;
|
||||||
|
color: $color-primary;
|
||||||
|
background: rgba(16, 185, 129, 0.1);
|
||||||
|
padding: 4rpx $spacing-sm;
|
||||||
|
border-radius: $radius-round;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guideArrow {
|
||||||
|
font-size: $font-size-lg;
|
||||||
|
color: $color-text-tertiary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guideTitle {
|
||||||
|
display: block;
|
||||||
|
font-size: $font-size-lg;
|
||||||
|
font-weight: $font-weight-semibold;
|
||||||
|
color: $color-text-primary;
|
||||||
|
margin-bottom: $spacing-xs;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guideSummary {
|
||||||
|
display: block;
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
color: $color-text-secondary;
|
||||||
|
line-height: $line-height-normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
display: block;
|
||||||
|
font-size: $font-size-md;
|
||||||
|
font-weight: $font-weight-semibold;
|
||||||
|
color: $color-text-primary;
|
||||||
|
margin: $spacing-lg 0 $spacing-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diseaseList {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $spacing-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diseaseCard {
|
||||||
|
background: $color-bg-card;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
padding: $spacing-lg;
|
||||||
|
box-shadow: $shadow-card;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diseaseHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: $spacing-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diseaseName {
|
||||||
|
font-size: $font-size-md;
|
||||||
|
font-weight: $font-weight-medium;
|
||||||
|
color: $color-text-primary;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
@include text-ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diseaseTag {
|
||||||
|
font-size: $font-size-xs;
|
||||||
|
color: $color-primary;
|
||||||
|
background: rgba(16, 185, 129, 0.1);
|
||||||
|
padding: 4rpx $spacing-sm;
|
||||||
|
border-radius: $radius-round;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: $spacing-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diseaseBrief {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
color: $color-text-secondary;
|
||||||
|
line-height: $line-height-normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyText {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
color: $color-text-tertiary;
|
||||||
|
padding: $spacing-xl 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { ScrollView, Text, View } from '@tarojs/components';
|
||||||
|
import Taro, { usePullDownRefresh } from '@tarojs/taro';
|
||||||
|
import styles from './index.module.scss';
|
||||||
|
import {
|
||||||
|
listAIGuides,
|
||||||
|
listDiseases,
|
||||||
|
type Disease,
|
||||||
|
type KnowledgeArticle,
|
||||||
|
} from '@/api/knowledge';
|
||||||
|
|
||||||
|
const CATEGORY_LABELS: Record<string, string> = {
|
||||||
|
viral: '病毒性',
|
||||||
|
fungal: '真菌性',
|
||||||
|
bacterial: '细菌性',
|
||||||
|
protozoan: '原虫性',
|
||||||
|
other: '其他',
|
||||||
|
};
|
||||||
|
|
||||||
|
const KnowledgePage: React.FC = () => {
|
||||||
|
const [diseases, setDiseases] = useState<Disease[]>([]);
|
||||||
|
const [guides, setGuides] = useState<KnowledgeArticle[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const fetchData = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const [diseaseRes, guideRes] = await Promise.all([
|
||||||
|
listDiseases().catch(() => [] as Disease[]),
|
||||||
|
listAIGuides().catch(() => [] as KnowledgeArticle[]),
|
||||||
|
]);
|
||||||
|
setDiseases(diseaseRes);
|
||||||
|
setGuides(guideRes);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Knowledge] 数据加载失败:', err);
|
||||||
|
Taro.showToast({
|
||||||
|
title: err instanceof Error ? err.message : '数据加载失败',
|
||||||
|
icon: 'none',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, [fetchData]);
|
||||||
|
|
||||||
|
usePullDownRefresh(() => {
|
||||||
|
fetchData().then(() => Taro.stopPullDownRefresh());
|
||||||
|
});
|
||||||
|
|
||||||
|
const openGuide = (a: KnowledgeArticle) => {
|
||||||
|
Taro.navigateTo({ url: `/pages/knowledge/detail/index?kind=ai_guide&id=${a.id}` });
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDisease = (d: Disease) => {
|
||||||
|
Taro.navigateTo({ url: `/pages/knowledge/detail/index?kind=disease&id=${d.id}` });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView className={styles.knowledgePage} scrollY>
|
||||||
|
{guides.map((a) => (
|
||||||
|
<View key={a.id} className={styles.guideCard} onClick={() => openGuide(a)}>
|
||||||
|
<View className={styles.guideHeader}>
|
||||||
|
<Text className={styles.guideBadge}>AI 解读</Text>
|
||||||
|
<Text className={styles.guideArrow}>›</Text>
|
||||||
|
</View>
|
||||||
|
<Text className={styles.guideTitle}>{a.title}</Text>
|
||||||
|
{a.summary ? <Text className={styles.guideSummary}>{a.summary}</Text> : null}
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Text className={styles.sectionTitle}>蚕病百科</Text>
|
||||||
|
<View className={styles.diseaseList}>
|
||||||
|
{diseases.map((d) => (
|
||||||
|
<View key={d.id} className={styles.diseaseCard} onClick={() => openDisease(d)}>
|
||||||
|
<View className={styles.diseaseHeader}>
|
||||||
|
<Text className={styles.diseaseName}>{d.name}</Text>
|
||||||
|
<Text className={styles.diseaseTag}>{CATEGORY_LABELS[d.category] || d.category}</Text>
|
||||||
|
</View>
|
||||||
|
<Text className={styles.diseaseBrief}>{d.symptoms || '暂无症状描述'}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
{!loading && diseases.length === 0 ? (
|
||||||
|
<Text className={styles.emptyText}>暂无蚕病百科内容</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KnowledgePage;
|
||||||
@@ -47,6 +47,9 @@ const SettingsPage: React.FC = () => {
|
|||||||
case 'thresholds':
|
case 'thresholds':
|
||||||
Taro.navigateTo({ url: '/pages/thresholds/index' });
|
Taro.navigateTo({ url: '/pages/thresholds/index' });
|
||||||
break;
|
break;
|
||||||
|
case 'knowledge':
|
||||||
|
Taro.navigateTo({ url: '/pages/knowledge/index' });
|
||||||
|
break;
|
||||||
case 'about':
|
case 'about':
|
||||||
Taro.showModal({
|
Taro.showModal({
|
||||||
title: '关于',
|
title: '关于',
|
||||||
@@ -158,6 +161,11 @@ const SettingsPage: React.FC = () => {
|
|||||||
<Text className={styles.menuLabel}>阈值管理</Text>
|
<Text className={styles.menuLabel}>阈值管理</Text>
|
||||||
<Text className={styles.menuArrow}>›</Text>
|
<Text className={styles.menuArrow}>›</Text>
|
||||||
</View>
|
</View>
|
||||||
|
<View className={styles.menuItem} onClick={() => handleMenuTap('knowledge')}>
|
||||||
|
<Text className={styles.menuIcon}>📖</Text>
|
||||||
|
<Text className={styles.menuLabel}>知识库</Text>
|
||||||
|
<Text className={styles.menuArrow}>›</Text>
|
||||||
|
</View>
|
||||||
<View className={styles.menuItem} onClick={() => handleMenuTap('wsStatus')}>
|
<View className={styles.menuItem} onClick={() => handleMenuTap('wsStatus')}>
|
||||||
<Text className={styles.menuIcon}>🔗</Text>
|
<Text className={styles.menuIcon}>🔗</Text>
|
||||||
<Text className={styles.menuLabel}>连接状态</Text>
|
<Text className={styles.menuLabel}>连接状态</Text>
|
||||||
|
|||||||
@@ -180,6 +180,34 @@ export interface DashboardStats {
|
|||||||
openAlarmCount: number;
|
openAlarmCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Disease {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
category: string;
|
||||||
|
pathogen?: string;
|
||||||
|
transmission?: string;
|
||||||
|
medium?: string;
|
||||||
|
incubation?: string;
|
||||||
|
symptoms?: string;
|
||||||
|
highRiskStage?: string;
|
||||||
|
highRiskCondition?: string;
|
||||||
|
lethalTime?: string;
|
||||||
|
spreadTrend?: string;
|
||||||
|
recurrence?: string;
|
||||||
|
detection?: string;
|
||||||
|
prevention?: string;
|
||||||
|
sortOrder?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KnowledgeArticle {
|
||||||
|
id: string;
|
||||||
|
kind: string;
|
||||||
|
title: string;
|
||||||
|
summary?: string;
|
||||||
|
content?: string;
|
||||||
|
sortOrder?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ApiResponse<T> {
|
export interface ApiResponse<T> {
|
||||||
data: T;
|
data: T;
|
||||||
message?: string;
|
message?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user