feat: 前端UI标题栏优化与配置更新
- 更新.gitignore,添加frontend/.next/忽略 - 优化RecordModal、index、recurring、search、stats页面标题栏样式 - 新增frontend/lib/utils.js工具函数 - 更新useCategories.js钩子 - 添加代码优化计划文档 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ node_modules/
|
|||||||
# Build output
|
# Build output
|
||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
|
frontend/.next/
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.idea/
|
.idea/
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -24,11 +24,8 @@ export default function RecordModal({
|
|||||||
return (
|
return (
|
||||||
<div className="modal-overlay modal-overlay--fullscreen" onClick={onClose}>
|
<div className="modal-overlay modal-overlay--fullscreen" onClick={onClose}>
|
||||||
<div className="modal modal--fullscreen" onClick={(e) => e.stopPropagation()}>
|
<div className="modal modal--fullscreen" onClick={(e) => e.stopPropagation()}>
|
||||||
<div className="modal-header">
|
<div className="modal-header" style={{minHeight: 56, height: 56, padding: 'calc(var(--safe-top) + 8px) 16px 8px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', boxSizing: 'border-box'}}>
|
||||||
<div>
|
<h2 style={{margin: 0, fontSize: '1.1rem'}}>{editingRecord ? '编辑记录' : '新增记录'}</h2>
|
||||||
<p className="eyebrow">{editingRecord ? '编辑记录' : '新增记录'}</p>
|
|
||||||
{editingRecord ? <h2>更新账目</h2> : null}
|
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="icon-button icon-button--ghost"
|
className="icon-button icon-button--ghost"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { DEFAULT_CATEGORIES } from '../lib/categories';
|
|||||||
export function useCategories() {
|
export function useCategories() {
|
||||||
const [categories, setCategories] = useState(DEFAULT_CATEGORIES);
|
const [categories, setCategories] = useState(DEFAULT_CATEGORIES);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoggedIn()) {
|
if (!isLoggedIn()) {
|
||||||
@@ -33,6 +34,7 @@ export function useCategories() {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
setError(err);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -41,5 +43,5 @@ export function useCategories() {
|
|||||||
loadCategories();
|
loadCategories();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return { categories, loading };
|
return { categories, loading, error };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// Date utilities
|
||||||
|
export const getMonthStr = (date) =>
|
||||||
|
`${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
||||||
|
|
||||||
|
export const formatMonth = (date) =>
|
||||||
|
`${date.getFullYear()}年${date.getMonth() + 1}月`;
|
||||||
|
|
||||||
|
export const formatMoney = (num) => (num || 0).toFixed(2);
|
||||||
|
|
||||||
|
// Parse date-time string 'YYYY-MM-DD HH:MM' into date and time parts
|
||||||
|
export const parseDateTime = (dateTimeStr) => {
|
||||||
|
if (!dateTimeStr) return { datePart: '', timePart: '00:00' };
|
||||||
|
const [datePart, timePart] = dateTimeStr.split(' ');
|
||||||
|
return { datePart, timePart: timePart || '00:00' };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract date part from 'YYYY-MM-DD HH:MM'
|
||||||
|
export const getDatePart = (dateTimeStr) => {
|
||||||
|
if (!dateTimeStr) return '';
|
||||||
|
return dateTimeStr.split(' ')[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Transaction type constants
|
||||||
|
export const TRANSACTION_TYPES = {
|
||||||
|
EXPENSE: 'expense',
|
||||||
|
INCOME: 'income',
|
||||||
|
ALL: 'all',
|
||||||
|
};
|
||||||
+33
-27
@@ -1,12 +1,13 @@
|
|||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { isLoggedIn, getRecords, getStats, createRecord, updateRecord, deleteRecord, getCategories } from '../lib/api';
|
import { isLoggedIn, getRecords, getStats, createRecord, updateRecord, deleteRecord } from '../lib/api';
|
||||||
import { DEFAULT_CATEGORIES } from '../lib/categories';
|
|
||||||
import AppShell from '../components/AppShell';
|
import AppShell from '../components/AppShell';
|
||||||
import { Icon } from '../components/icons';
|
import { Icon } from '../components/icons';
|
||||||
import RecordModal from '../components/RecordModal';
|
import RecordModal from '../components/RecordModal';
|
||||||
import { useToast } from '../components/ToastProvider';
|
import { useToast } from '../components/ToastProvider';
|
||||||
import { useConfirm } from '../components/ConfirmProvider';
|
import { useConfirm } from '../components/ConfirmProvider';
|
||||||
|
import { useCategories } from '../hooks/useCategories';
|
||||||
|
import { getMonthStr, formatMoney, formatMonth, parseDateTime, getDatePart } from '../lib/utils';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -22,7 +23,14 @@ export default function Home() {
|
|||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
const [formError, setFormError] = useState('');
|
const [formError, setFormError] = useState('');
|
||||||
const [categories, setCategories] = useState(DEFAULT_CATEGORIES);
|
const { categories, error: categoriesError } = useCategories();
|
||||||
|
|
||||||
|
// 显示分类加载错误
|
||||||
|
useEffect(() => {
|
||||||
|
if (categoriesError) {
|
||||||
|
showToast('分类加载失败', { tone: 'error' });
|
||||||
|
}
|
||||||
|
}, [categoriesError]);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
type: 'expense',
|
type: 'expense',
|
||||||
amount: '',
|
amount: '',
|
||||||
@@ -40,9 +48,6 @@ export default function Home() {
|
|||||||
loadData();
|
loadData();
|
||||||
}, [router, currentMonth, typeFilter]);
|
}, [router, currentMonth, typeFilter]);
|
||||||
|
|
||||||
const getMonthStr = (date) => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
|
||||||
const formatMoney = (num) => (num || 0).toFixed(2);
|
|
||||||
const formatMonth = (date) => `${date.getFullYear()}年${date.getMonth() + 1}月`;
|
|
||||||
|
|
||||||
const resetForm = () => ({
|
const resetForm = () => ({
|
||||||
type: 'expense',
|
type: 'expense',
|
||||||
@@ -53,27 +58,26 @@ export default function Home() {
|
|||||||
note: '',
|
note: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Memoized category lookup map for O(1) icon retrieval
|
||||||
|
const categoryIconMap = useMemo(() => {
|
||||||
|
const icons = {};
|
||||||
|
for (const type of ['expense', 'income']) {
|
||||||
|
for (const cat of categories[type] || []) {
|
||||||
|
icons[`${type}:${cat.name}`] = cat.icon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return icons;
|
||||||
|
}, [categories]);
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const [recordsData, statsData, categoriesData] = await Promise.all([
|
const [recordsData, statsData] = await Promise.all([
|
||||||
getRecords(getMonthStr(currentMonth), typeFilter),
|
getRecords(getMonthStr(currentMonth), typeFilter),
|
||||||
getStats(getMonthStr(currentMonth)),
|
getStats(getMonthStr(currentMonth)),
|
||||||
getCategories(),
|
|
||||||
]);
|
]);
|
||||||
setRecords(recordsData);
|
setRecords(recordsData);
|
||||||
setStats(statsData);
|
setStats(statsData);
|
||||||
// 合并默认分类和自定义分类
|
|
||||||
setCategories({
|
|
||||||
expense: [
|
|
||||||
...DEFAULT_CATEGORIES.expense,
|
|
||||||
...(categoriesData.expense || []).filter(c => !DEFAULT_CATEGORIES.expense.some(d => d.name === c.name))
|
|
||||||
],
|
|
||||||
income: [
|
|
||||||
...DEFAULT_CATEGORIES.income,
|
|
||||||
...(categoriesData.income || []).filter(c => !DEFAULT_CATEGORIES.income.some(d => d.name === c.name))
|
|
||||||
],
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
showToast(err.message, { tone: 'error' });
|
showToast(err.message, { tone: 'error' });
|
||||||
@@ -106,14 +110,14 @@ export default function Home() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openEditModal = (record) => {
|
const openEditModal = (record) => {
|
||||||
const [datePart, timePart] = record.date.split(' ');
|
const { datePart, timePart } = parseDateTime(record.date);
|
||||||
setEditingRecord(record);
|
setEditingRecord(record);
|
||||||
setFormData({
|
setFormData({
|
||||||
type: record.type,
|
type: record.type,
|
||||||
amount: record.amount.toString(),
|
amount: record.amount.toString(),
|
||||||
category: record.category,
|
category: record.category,
|
||||||
date: datePart,
|
date: datePart,
|
||||||
time: timePart || '00:00',
|
time: timePart,
|
||||||
note: record.note || '',
|
note: record.note || '',
|
||||||
});
|
});
|
||||||
setFormError('');
|
setFormError('');
|
||||||
@@ -146,14 +150,16 @@ export default function Home() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (editingRecord) {
|
if (editingRecord) {
|
||||||
|
const updated = { ...editingRecord, ...data, amount: parseFloat(data.amount) };
|
||||||
await updateRecord(editingRecord.id, data);
|
await updateRecord(editingRecord.id, data);
|
||||||
|
setRecords(prev => prev.map(r => r.id === editingRecord.id ? updated : r));
|
||||||
showToast('记录已更新', { tone: 'success' });
|
showToast('记录已更新', { tone: 'success' });
|
||||||
} else {
|
} else {
|
||||||
await createRecord(data);
|
const newRecord = await createRecord(data);
|
||||||
|
setRecords(prev => [...prev, newRecord]);
|
||||||
showToast('记录已添加', { tone: 'success' });
|
showToast('记录已添加', { tone: 'success' });
|
||||||
}
|
}
|
||||||
closeModal();
|
closeModal();
|
||||||
await loadData();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setFormError(err.message);
|
setFormError(err.message);
|
||||||
showToast(err.message, { tone: 'error' });
|
showToast(err.message, { tone: 'error' });
|
||||||
@@ -174,9 +180,9 @@ export default function Home() {
|
|||||||
setDeleting(true);
|
setDeleting(true);
|
||||||
try {
|
try {
|
||||||
await deleteRecord(editingRecord.id);
|
await deleteRecord(editingRecord.id);
|
||||||
|
setRecords(prev => prev.filter(r => r.id !== editingRecord.id));
|
||||||
closeModal();
|
closeModal();
|
||||||
showToast('记录已删除', { tone: 'success' });
|
showToast('记录已删除', { tone: 'success' });
|
||||||
await loadData();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setFormError(err.message);
|
setFormError(err.message);
|
||||||
showToast(err.message, { tone: 'error' });
|
showToast(err.message, { tone: 'error' });
|
||||||
@@ -187,7 +193,7 @@ export default function Home() {
|
|||||||
|
|
||||||
const groupedRecords = useMemo(() => {
|
const groupedRecords = useMemo(() => {
|
||||||
return records.reduce((groups, record) => {
|
return records.reduce((groups, record) => {
|
||||||
const date = record.date.split(' ')[0];
|
const date = getDatePart(record.date);
|
||||||
if (!groups[date]) groups[date] = [];
|
if (!groups[date]) groups[date] = [];
|
||||||
groups[date].push(record);
|
groups[date].push(record);
|
||||||
return groups;
|
return groups;
|
||||||
@@ -293,11 +299,11 @@ export default function Home() {
|
|||||||
<span className="day-expense">支出 {formatMoney(getDayExpense(date))}</span>
|
<span className="day-expense">支出 {formatMoney(getDayExpense(date))}</span>
|
||||||
</div>
|
</div>
|
||||||
{groupedRecords[date].map((record) => {
|
{groupedRecords[date].map((record) => {
|
||||||
const cat = categories[record.type]?.find((item) => item.name === record.category) || { icon: '📝' };
|
const icon = categoryIconMap[`${record.type}:${record.category}`] || '📝';
|
||||||
return (
|
return (
|
||||||
<button key={record.id} type="button" className="record-item" onClick={() => openEditModal(record)}>
|
<button key={record.id} type="button" className="record-item" onClick={() => openEditModal(record)}>
|
||||||
<div className="record-left">
|
<div className="record-left">
|
||||||
<div className="record-icon">{cat.icon}</div>
|
<div className="record-icon">{icon}</div>
|
||||||
<div className="record-info">
|
<div className="record-info">
|
||||||
<div className="record-category">{record.category}</div>
|
<div className="record-category">{record.category}</div>
|
||||||
{record.note && <div className="record-note">{record.note}</div>}
|
{record.note && <div className="record-note">{record.note}</div>}
|
||||||
|
|||||||
@@ -7,11 +7,20 @@ import { useToast } from '../components/ToastProvider';
|
|||||||
import { useConfirm } from '../components/ConfirmProvider';
|
import { useConfirm } from '../components/ConfirmProvider';
|
||||||
import { useCategories } from '../hooks/useCategories';
|
import { useCategories } from '../hooks/useCategories';
|
||||||
|
|
||||||
|
const DAYS = Array.from({ length: 28 }, (_, i) => i + 1);
|
||||||
|
|
||||||
export default function Recurring() {
|
export default function Recurring() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
const { confirm } = useConfirm();
|
const { confirm } = useConfirm();
|
||||||
const { categories } = useCategories();
|
const { categories, error: categoriesError } = useCategories();
|
||||||
|
|
||||||
|
// 显示分类加载错误
|
||||||
|
useEffect(() => {
|
||||||
|
if (categoriesError) {
|
||||||
|
showToast('分类加载失败', { tone: 'error' });
|
||||||
|
}
|
||||||
|
}, [categoriesError]);
|
||||||
const [bills, setBills] = useState([]);
|
const [bills, setBills] = useState([]);
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const [editingBill, setEditingBill] = useState(null);
|
const [editingBill, setEditingBill] = useState(null);
|
||||||
@@ -170,8 +179,6 @@ export default function Recurring() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const days = Array.from({ length: 28 }, (_, index) => index + 1);
|
|
||||||
|
|
||||||
const actions = (
|
const actions = (
|
||||||
<button type="button" className="icon-button" aria-label="设置" onClick={() => router.push('/settings')}>
|
<button type="button" className="icon-button" aria-label="设置" onClick={() => router.push('/settings')}>
|
||||||
<Icon name="settings" size={18} />
|
<Icon name="settings" size={18} />
|
||||||
@@ -322,7 +329,7 @@ export default function Recurring() {
|
|||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="bill-day">日期(每月几号)</label>
|
<label htmlFor="bill-day">日期(每月几号)</label>
|
||||||
<select id="bill-day" value={formData.day} onChange={(e) => setFormData({ ...formData, day: e.target.value })} disabled={submitting || deleting}>
|
<select id="bill-day" value={formData.day} onChange={(e) => setFormData({ ...formData, day: e.target.value })} disabled={submitting || deleting}>
|
||||||
{days.map((day) => (
|
{DAYS.map((day) => (
|
||||||
<option key={day} value={day}>{day} 日</option>
|
<option key={day} value={day}>{day} 日</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -7,12 +7,20 @@ import RecordModal from '../components/RecordModal';
|
|||||||
import { useToast } from '../components/ToastProvider';
|
import { useToast } from '../components/ToastProvider';
|
||||||
import { useConfirm } from '../components/ConfirmProvider';
|
import { useConfirm } from '../components/ConfirmProvider';
|
||||||
import { useCategories } from '../hooks/useCategories';
|
import { useCategories } from '../hooks/useCategories';
|
||||||
|
import { formatMoney, parseDateTime, getDatePart } from '../lib/utils';
|
||||||
|
|
||||||
export default function Search() {
|
export default function Search() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const { categories } = useCategories();
|
const { categories, error: categoriesError } = useCategories();
|
||||||
|
|
||||||
|
// 显示分类加载错误
|
||||||
|
useEffect(() => {
|
||||||
|
if (categoriesError) {
|
||||||
|
showToast('分类加载失败', { tone: 'error' });
|
||||||
|
}
|
||||||
|
}, [categoriesError]);
|
||||||
const [keyword, setKeyword] = useState('');
|
const [keyword, setKeyword] = useState('');
|
||||||
const [results, setResults] = useState([]);
|
const [results, setResults] = useState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -40,6 +48,8 @@ export default function Search() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (router.isReady && router.query.q) {
|
if (router.isReady && router.query.q) {
|
||||||
const q = decodeURIComponent(router.query.q);
|
const q = decodeURIComponent(router.query.q);
|
||||||
|
// Skip if already have results for this keyword (prevents double API call)
|
||||||
|
if (keyword === q && results.length > 0) return;
|
||||||
setKeyword(q);
|
setKeyword(q);
|
||||||
// 自动搜索
|
// 自动搜索
|
||||||
const doSearch = async () => {
|
const doSearch = async () => {
|
||||||
@@ -56,7 +66,7 @@ export default function Search() {
|
|||||||
};
|
};
|
||||||
doSearch();
|
doSearch();
|
||||||
}
|
}
|
||||||
}, [router.isReady, router.query.q]);
|
}, [router.isReady, router.query.q, keyword, results]);
|
||||||
|
|
||||||
const handleSearch = async (e) => {
|
const handleSearch = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -81,7 +91,7 @@ export default function Search() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openEditModal = (record) => {
|
const openEditModal = (record) => {
|
||||||
const [datePart, timePart] = record.date.split(' ');
|
const { datePart, timePart } = parseDateTime(record.date);
|
||||||
setEditingRecord(record);
|
setEditingRecord(record);
|
||||||
setFormData({
|
setFormData({
|
||||||
type: record.type,
|
type: record.type,
|
||||||
@@ -165,12 +175,10 @@ export default function Search() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatMoney = (num) => (num || 0).toFixed(2);
|
|
||||||
|
|
||||||
const groupedResults = useMemo(() => {
|
const groupedResults = useMemo(() => {
|
||||||
if (!Array.isArray(results)) return {};
|
if (!Array.isArray(results)) return {};
|
||||||
return results.reduce((groups, record) => {
|
return results.reduce((groups, record) => {
|
||||||
const date = record.date.split(' ')[0];
|
const date = getDatePart(record.date);
|
||||||
if (!groups[date]) groups[date] = [];
|
if (!groups[date]) groups[date] = [];
|
||||||
groups[date].push(record);
|
groups[date].push(record);
|
||||||
return groups;
|
return groups;
|
||||||
|
|||||||
+20
-5
@@ -1,12 +1,30 @@
|
|||||||
import { useState, useEffect, useRef, useMemo } from 'react';
|
import { useState, useEffect, useRef, useMemo } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { isLoggedIn, getTrendStats, getCategoryStats } from '../lib/api';
|
import { isLoggedIn, getTrendStats, getCategoryStats } from '../lib/api';
|
||||||
import { Chart, registerables } from 'chart.js';
|
import {
|
||||||
|
Chart,
|
||||||
|
LineController,
|
||||||
|
LineElement,
|
||||||
|
PointElement,
|
||||||
|
LinearScale,
|
||||||
|
CategoryScale,
|
||||||
|
Filler,
|
||||||
|
Legend,
|
||||||
|
} from 'chart.js';
|
||||||
import AppShell from '../components/AppShell';
|
import AppShell from '../components/AppShell';
|
||||||
import { Icon } from '../components/icons';
|
import { Icon } from '../components/icons';
|
||||||
|
import { getMonthStr, formatMonth } from '../lib/utils';
|
||||||
import { useToast } from '../components/ToastProvider';
|
import { useToast } from '../components/ToastProvider';
|
||||||
|
|
||||||
Chart.register(...registerables);
|
Chart.register(
|
||||||
|
LineController,
|
||||||
|
LineElement,
|
||||||
|
PointElement,
|
||||||
|
LinearScale,
|
||||||
|
CategoryScale,
|
||||||
|
Filler,
|
||||||
|
Legend
|
||||||
|
);
|
||||||
|
|
||||||
export default function Stats() {
|
export default function Stats() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -35,9 +53,6 @@ export default function Stats() {
|
|||||||
loadCategoryData();
|
loadCategoryData();
|
||||||
}, [router, currentMonth, categoryType]);
|
}, [router, currentMonth, categoryType]);
|
||||||
|
|
||||||
const getMonthStr = (date) => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
|
||||||
const formatMonth = (date) => `${date.getFullYear()}年${date.getMonth() + 1}月`;
|
|
||||||
|
|
||||||
const loadTrendData = async () => {
|
const loadTrendData = async () => {
|
||||||
setTrendLoading(true);
|
setTrendLoading(true);
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user