mirror of
https://github.com/Funkoala14/CreatorCenter_OOIN.git
synced 2025-06-07 22:58:14 +08:00
[dev]creatorList
This commit is contained in:
parent
443c253014
commit
8dd91b75c8
@ -25,6 +25,7 @@ export default function CreatorList({ path }) {
|
||||
hasMore,
|
||||
isLoadingMore,
|
||||
pagination,
|
||||
error,
|
||||
} = useSelector((state) => state.creators);
|
||||
const { sortBy, sortDirection } = useSelector((state) => state.filters);
|
||||
const observer = useRef();
|
||||
@ -49,7 +50,8 @@ export default function CreatorList({ path }) {
|
||||
}, [path, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
}, [publicCreators]);
|
||||
console.log(publicCreators, status);
|
||||
}, [publicCreators, status]);
|
||||
|
||||
// 处理全选/取消全选
|
||||
const handleSelectAll = (e) => {
|
||||
@ -67,11 +69,13 @@ export default function CreatorList({ path }) {
|
||||
|
||||
// 处理排序
|
||||
const handleSort = (field) => {
|
||||
return;
|
||||
dispatch(setSortBy(field));
|
||||
};
|
||||
|
||||
// 渲染排序图标
|
||||
const renderSortIcon = (field) => {
|
||||
return;
|
||||
if (sortBy === field) {
|
||||
return sortDirection === 'asc' ? <ChevronUp size={14} /> : <ChevronDown size={14} />;
|
||||
}
|
||||
@ -93,7 +97,7 @@ export default function CreatorList({ path }) {
|
||||
};
|
||||
|
||||
// 如果正在加载且没有数据,显示加载中
|
||||
if (status === 'loading' && (!publicCreators || publicCreators.length === 0)) {
|
||||
if (status === 'loading' && !isLoadingMore) {
|
||||
return (
|
||||
<div className='text-center p-5'>
|
||||
<Spinner animation='border' role='status' variant='primary'>
|
||||
@ -105,7 +109,7 @@ export default function CreatorList({ path }) {
|
||||
|
||||
// 如果加载失败,显示错误信息
|
||||
if (status === 'failed') {
|
||||
return <div className='alert alert-danger'>Failed to load creators. Please try again later.</div>;
|
||||
return <div className='alert alert-danger'>{error || 'Failed to load creators. Please try again later.'}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -117,7 +121,9 @@ export default function CreatorList({ path }) {
|
||||
<th className='selector' style={{ width: '40px' }}>
|
||||
<Form.Check
|
||||
type='checkbox'
|
||||
checked={selectedCreators.length === publicCreators.length && publicCreators.length > 0}
|
||||
checked={
|
||||
selectedCreators.length === publicCreators.length && publicCreators.length > 0
|
||||
}
|
||||
onChange={handleSelectAll}
|
||||
/>
|
||||
</th>
|
||||
|
@ -10,8 +10,9 @@ import {
|
||||
toggleGmvRange,
|
||||
setViewsRange,
|
||||
setPricingRange,
|
||||
setPlatform,
|
||||
} from '../store/slices/filtersSlice';
|
||||
import { fetchCreators } from '../store/slices/creatorsSlice';
|
||||
import { fetchCreators, resetCreators } from '../store/slices/creatorsSlice';
|
||||
import '../styles/DatabaseFilter.scss';
|
||||
|
||||
export default function DatabaseFilter({ path, pageType = 'database' }) {
|
||||
@ -63,23 +64,23 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
|
||||
};
|
||||
|
||||
// 本地状态用于表单控制
|
||||
const [minViews, setMinViews] = useState(filters.viewsRange[0]);
|
||||
const [maxViews, setMaxViews] = useState(filters.viewsRange[1]);
|
||||
const [minPricing, setMinPricing] = useState(filters.pricingRange[0]);
|
||||
const [maxPricing, setMaxPricing] = useState(filters.pricingRange[1]);
|
||||
const [minViews, setMinViews] = useState(filters.views_range[0]);
|
||||
const [maxViews, setMaxViews] = useState(filters.views_range[1]);
|
||||
const [minPricing, setMinPricing] = useState(filters.pricing[0]);
|
||||
const [maxPricing, setMaxPricing] = useState(filters.pricing[1]);
|
||||
|
||||
// 监听Redux状态变化,更新本地表单状态
|
||||
useEffect(() => {
|
||||
setMinViews(filters.viewsRange[0]);
|
||||
setMaxViews(filters.viewsRange[1]);
|
||||
setMinPricing(filters.pricingRange[0]);
|
||||
setMaxPricing(filters.pricingRange[1]);
|
||||
}, [filters.viewsRange, filters.pricingRange]);
|
||||
setMinViews(filters.views_range[0]);
|
||||
setMaxViews(filters.views_range[1]);
|
||||
setMinPricing(filters.pricing[0]);
|
||||
setMaxPricing(filters.pricing[1]);
|
||||
}, [filters.views_range, filters.pricing]);
|
||||
|
||||
// 组件加载时获取数据
|
||||
useEffect(() => {
|
||||
|
||||
}, [dispatch, filters, pageType, path]);
|
||||
dispatch(setPlatform(path));
|
||||
}, [dispatch, path]);
|
||||
|
||||
// 处理类别选择
|
||||
const handleCategorySelect = (category) => {
|
||||
@ -104,6 +105,7 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
|
||||
// 处理视图范围更新
|
||||
const handleViewsRangeChange = (newRange) => {
|
||||
dispatch(setViewsRange(newRange));
|
||||
resetAndFetch();
|
||||
};
|
||||
|
||||
// 处理min input变更
|
||||
@ -120,6 +122,7 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
|
||||
|
||||
const handlePricingRangeChange = (newRange) => {
|
||||
dispatch(setPricingRange(newRange));
|
||||
resetAndFetch();
|
||||
};
|
||||
|
||||
// 处理min pricing input变更
|
||||
@ -145,7 +148,7 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
|
||||
const finalValue = Math.min(discreteValue, maxViews);
|
||||
|
||||
setMinViews(finalValue);
|
||||
dispatch(setViewsRange([finalValue, filters.viewsRange[1]]));
|
||||
dispatch(setViewsRange([finalValue, filters.views_range[1]]));
|
||||
} else {
|
||||
// 找到最接近的离散值
|
||||
const closestIndex = findClosestDiscreteIndex(maxViews);
|
||||
@ -155,7 +158,7 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
|
||||
const finalValue = Math.max(discreteValue, minViews);
|
||||
|
||||
setMaxViews(finalValue);
|
||||
dispatch(setViewsRange([filters.viewsRange[0], finalValue]));
|
||||
dispatch(setViewsRange([filters.views_range[0], finalValue]));
|
||||
}
|
||||
};
|
||||
|
||||
@ -169,7 +172,7 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
|
||||
const finalValue = Math.min(discreteValue, maxPricing);
|
||||
|
||||
setMinPricing(finalValue);
|
||||
dispatch(setPricingRange([finalValue, filters.pricingRange[1]]));
|
||||
dispatch(setPricingRange([finalValue, filters.pricing[1]]));
|
||||
} else {
|
||||
const closestIndex = findClosestDiscreteIndex(maxPricing);
|
||||
const discreteValue = discretePricingValues[closestIndex];
|
||||
@ -178,7 +181,7 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
|
||||
const finalValue = Math.max(discreteValue, minPricing);
|
||||
|
||||
setMaxPricing(finalValue);
|
||||
dispatch(setPricingRange([filters.pricingRange[0], finalValue]));
|
||||
dispatch(setPricingRange([filters.pricing[0], finalValue]));
|
||||
}
|
||||
};
|
||||
|
||||
@ -193,6 +196,43 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
|
||||
return value;
|
||||
};
|
||||
|
||||
const resetAndFetch = () => {
|
||||
dispatch(resetCreators());
|
||||
if (pageType === 'private') {
|
||||
dispatch(fetchPrivateCreators({ page: 1 }));
|
||||
} else {
|
||||
dispatch(fetchCreators({ page: 1 }));
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = (e) => {
|
||||
const name = e.target.name;
|
||||
const value = e.target.value;
|
||||
switch (name) {
|
||||
case 'category':
|
||||
handleCategorySelect(value);
|
||||
break;
|
||||
case 'e_commerce_level':
|
||||
handleEcommerceRatingSelect(value);
|
||||
break;
|
||||
case 'exposure_level':
|
||||
handleExposureRatingSelect(value);
|
||||
break;
|
||||
case 'gmv_range':
|
||||
handleGmvRangeSelect(value);
|
||||
break;
|
||||
case 'views_range':
|
||||
handleViewsRangeChange(value);
|
||||
break;
|
||||
case 'pricing':
|
||||
handlePricingRangeChange(value);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
resetAndFetch();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='shadow-xs mb-4 filter-card'>
|
||||
<Card.Body>
|
||||
@ -207,7 +247,9 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
|
||||
key={category}
|
||||
variant={filters.category.includes(category) ? 'primary' : 'light'}
|
||||
className='rounded-pill'
|
||||
onClick={() => handleCategorySelect(category)}
|
||||
onClick={onChange}
|
||||
name='category'
|
||||
value={category}
|
||||
>
|
||||
{category}
|
||||
</Button>
|
||||
@ -222,9 +264,11 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
|
||||
{ecommerceRatings.map((rating) => (
|
||||
<Button
|
||||
key={rating}
|
||||
variant={filters.ecommerceRatings.includes(rating) ? 'primary' : 'light'}
|
||||
variant={filters.e_commerce_level.includes(rating) ? 'primary' : 'light'}
|
||||
className='rounded-pill'
|
||||
onClick={() => handleEcommerceRatingSelect(rating)}
|
||||
onClick={onChange}
|
||||
name='e_commerce_level'
|
||||
value={rating}
|
||||
>
|
||||
{rating}
|
||||
</Button>
|
||||
@ -239,9 +283,11 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
|
||||
{exposureRatings.map((rating) => (
|
||||
<Button
|
||||
key={rating}
|
||||
variant={filters.exposureRatings.includes(rating) ? 'primary' : 'light'}
|
||||
variant={filters.exposure_level.includes(rating) ? 'primary' : 'light'}
|
||||
className='rounded-pill'
|
||||
onClick={() => handleExposureRatingSelect(rating)}
|
||||
onClick={onChange}
|
||||
name='exposure_level'
|
||||
value={rating}
|
||||
>
|
||||
{rating}
|
||||
</Button>
|
||||
@ -256,9 +302,11 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
|
||||
{gmvRanges.map((range) => (
|
||||
<Button
|
||||
key={range.label}
|
||||
variant={filters.gmvRanges.includes(range.label) ? 'primary' : 'light'}
|
||||
variant={filters.gmv_range.includes(range.label) ? 'primary' : 'light'}
|
||||
className='rounded-pill'
|
||||
onClick={() => handleGmvRangeSelect(range.label)}
|
||||
onClick={onChange}
|
||||
name='gmv_range'
|
||||
value={range.label}
|
||||
>
|
||||
{range.label}
|
||||
</Button>
|
||||
@ -273,7 +321,7 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
|
||||
<RangeSlider
|
||||
min={0}
|
||||
max={500000}
|
||||
value={filters.viewsRange}
|
||||
value={filters.views_range}
|
||||
onChange={handleViewsRangeChange}
|
||||
/>
|
||||
|
||||
@ -313,7 +361,7 @@ export default function DatabaseFilter({ path, pageType = 'database' }) {
|
||||
<RangeSlider
|
||||
min={0}
|
||||
max={500000}
|
||||
value={filters.pricingRange}
|
||||
value={filters.pricing}
|
||||
onChange={handlePricingRangeChange}
|
||||
/>
|
||||
|
||||
|
21
src/components/NotificationBar.jsx
Normal file
21
src/components/NotificationBar.jsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { Check, CircleAlert, Info, X } from 'lucide-react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
export default function NotificationBar() {
|
||||
const { message, type, show } = useSelector((state) => state.notificationBar);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`snackbar alert alert-${type} d-flex align-items-center justify-content-between position-fixed top-10 start-50 translate-middle w-50 gap-2`}
|
||||
role='alert'
|
||||
>
|
||||
{type === 'success' && <Check />}
|
||||
{type === 'warning' && <CircleAlert />}
|
||||
{type === 'error' && <X />}
|
||||
{type === 'info' && <Info />}
|
||||
<div className='flex-fill'>{message}</div>
|
||||
<button type='button' className='btn-close flex-end' onClick={handleClose} aria-label='Close'></button>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -15,7 +15,7 @@ import { Link } from 'react-router-dom';
|
||||
|
||||
export default function PrivateCreatorList({ path }) {
|
||||
const dispatch = useDispatch();
|
||||
const { privateCreators, status, selectedCreators, hasMore, isLoadingMore, pagination } = useSelector(
|
||||
const { privateCreators, status, selectedCreators, hasMore, isLoadingMore, pagination, error } = useSelector(
|
||||
(state) => state.creators
|
||||
);
|
||||
const { sortBy, sortDirection } = useSelector((state) => state.filters);
|
||||
@ -60,11 +60,13 @@ export default function PrivateCreatorList({ path }) {
|
||||
|
||||
// 处理排序
|
||||
const handleSort = (field) => {
|
||||
return;
|
||||
dispatch(setSortBy(field));
|
||||
};
|
||||
|
||||
// 渲染排序图标
|
||||
const renderSortIcon = (field) => {
|
||||
return;
|
||||
if (sortBy === field) {
|
||||
return sortDirection === 'asc' ? <ChevronUp size={14} /> : <ChevronDown size={14} />;
|
||||
}
|
||||
@ -86,7 +88,7 @@ export default function PrivateCreatorList({ path }) {
|
||||
};
|
||||
|
||||
// 如果正在加载且没有数据,显示加载中
|
||||
if (status === 'loading' && (!privateCreators || privateCreators.length === 0)) {
|
||||
if (status === 'loading' && !isLoadingMore) {
|
||||
return (
|
||||
<div className='text-center p-5'>
|
||||
<Spinner animation='border' role='status' variant='primary'>
|
||||
@ -98,7 +100,7 @@ export default function PrivateCreatorList({ path }) {
|
||||
|
||||
// 如果加载失败,显示错误信息
|
||||
if (status === 'failed') {
|
||||
return <div className='alert alert-danger'>Failed to load creators. Please try again later.</div>;
|
||||
return <div className='alert alert-danger'>{error || 'Failed to load creators. Please try again later.'}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -216,24 +216,47 @@ const initialState = {
|
||||
isLoadingMore: false,
|
||||
};
|
||||
|
||||
export const fetchCreators = createAsyncThunk('creators/fetchCreators', async ({ path, page = 1 }, { getState }) => {
|
||||
export const fetchCreators = createAsyncThunk(
|
||||
'creators/fetchCreators',
|
||||
async ({ page = 1 }, { getState, rejectWithValue }) => {
|
||||
try {
|
||||
const state = getState();
|
||||
const filters = state.filters;
|
||||
|
||||
const response = await api.get(`/daren_detail/public/creators`, { params: { page } });
|
||||
console.log(response);
|
||||
return response;
|
||||
});
|
||||
const { code, data, message, pagination } = await api.post(
|
||||
`/daren_detail/public/creators/filter/?page=${page}`,
|
||||
filters
|
||||
);
|
||||
if (code === 200) {
|
||||
return { data, pagination };
|
||||
} else {
|
||||
throw new Error(message);
|
||||
}
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const fetchPrivateCreators = createAsyncThunk(
|
||||
'creators/fetchPrivateCreators',
|
||||
async ({ path, page = 1 }, { getState }) => {
|
||||
async ({ page = 1 }, { getState, rejectWithValue, dispatch }) => {
|
||||
try {
|
||||
const state = getState();
|
||||
const filters = state.filters;
|
||||
|
||||
const queryParams = { pool_id: 1, page };
|
||||
const response = await api.get(`/daren_detail/private/pools/creators`, { params: queryParams });
|
||||
return response;
|
||||
const { code, data, message, pagination } = await api.post(
|
||||
`/daren_detail/private/pools/creators/filter/?page=${page}`,
|
||||
{ pool_id: 1, filters }
|
||||
);
|
||||
if (code === 200) {
|
||||
return { data, pagination };
|
||||
} else {
|
||||
throw new Error(message);
|
||||
}
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@ -283,8 +306,8 @@ const creatorsSlice = createSlice({
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(fetchCreators.pending, (state) => {
|
||||
if (state.publicCreators.length === 0) {
|
||||
state.status = 'loading';
|
||||
if (state.publicCreators.length === 0) {
|
||||
} else {
|
||||
state.isLoadingMore = true;
|
||||
}
|
||||
@ -304,7 +327,7 @@ const creatorsSlice = createSlice({
|
||||
.addCase(fetchCreators.rejected, (state, action) => {
|
||||
state.status = 'failed';
|
||||
state.isLoadingMore = false;
|
||||
state.error = action.error.message;
|
||||
state.error = action.payload;
|
||||
})
|
||||
.addCase(fetchPrivateCreators.pending, (state) => {
|
||||
if (state.privateCreators?.length === 0) {
|
||||
@ -330,7 +353,7 @@ const creatorsSlice = createSlice({
|
||||
console.log('fetchPrivateCreators.rejected', action);
|
||||
state.status = 'failed';
|
||||
state.isLoadingMore = false;
|
||||
state.error = action.error.message;
|
||||
state.error = action.payload;
|
||||
})
|
||||
.addCase(fetchCreatorDetail.pending, (state) => {
|
||||
state.status = 'loading';
|
||||
|
@ -2,13 +2,14 @@ import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
const initialState = {
|
||||
category: ['Homes Supplies'],
|
||||
ecommerceRatings: ['L2', 'L3'],
|
||||
exposureRatings: [],
|
||||
gmvRanges: ['$5k - $25k', '$25k - $60k'],
|
||||
viewsRange: [0, 100000],
|
||||
pricingRange: [0, 3000],
|
||||
e_commerce_level: ['L2', 'L3'],
|
||||
exposure_level: [],
|
||||
gmv_range: ['$5k - $25k', '$25k - $60k'],
|
||||
views_range: [0, 100000],
|
||||
pricing: [0, 3000],
|
||||
sortBy: 'followers',
|
||||
sortDirection: 'desc',
|
||||
platform: '',
|
||||
};
|
||||
const filtersSlice = createSlice({
|
||||
name: 'filters',
|
||||
@ -24,33 +25,33 @@ const filtersSlice = createSlice({
|
||||
},
|
||||
toggleEcommerceRating: (state, action) => {
|
||||
const rating = action.payload;
|
||||
if (state.ecommerceRatings.includes(rating)) {
|
||||
state.ecommerceRatings = state.ecommerceRatings.filter((r) => r !== rating);
|
||||
if (state.e_commerce_level.includes(rating)) {
|
||||
state.e_commerce_level = state.e_commerce_level.filter((r) => r !== rating);
|
||||
} else {
|
||||
state.ecommerceRatings.push(rating);
|
||||
state.e_commerce_level.push(rating);
|
||||
}
|
||||
},
|
||||
toggleExposureRating: (state, action) => {
|
||||
const rating = action.payload;
|
||||
if (state.exposureRatings.includes(rating)) {
|
||||
state.exposureRatings = state.exposureRatings.filter((r) => r !== rating);
|
||||
if (state.exposure_level.includes(rating)) {
|
||||
state.exposure_level = state.exposure_level.filter((r) => r !== rating);
|
||||
} else {
|
||||
state.exposureRatings.push(rating);
|
||||
state.exposure_level.push(rating);
|
||||
}
|
||||
},
|
||||
toggleGmvRange: (state, action) => {
|
||||
const range = action.payload;
|
||||
if (state.gmvRanges.includes(range)) {
|
||||
state.gmvRanges = state.gmvRanges.filter((r) => r !== range);
|
||||
if (state.gmv_range.includes(range)) {
|
||||
state.gmv_range = state.gmv_range.filter((r) => r !== range);
|
||||
} else {
|
||||
state.gmvRanges.push(range);
|
||||
state.gmv_range.push(range);
|
||||
}
|
||||
},
|
||||
setViewsRange: (state, action) => {
|
||||
state.viewsRange = action.payload;
|
||||
state.views_range = action.payload;
|
||||
},
|
||||
setPricingRange: (state, action) => {
|
||||
state.pricingRange = action.payload;
|
||||
state.pricing = action.payload;
|
||||
},
|
||||
setSortBy: (state, action) => {
|
||||
// 如果选择了当前已激活的排序项,则切换排序方向
|
||||
@ -62,6 +63,9 @@ const filtersSlice = createSlice({
|
||||
state.sortDirection = 'desc';
|
||||
}
|
||||
},
|
||||
setPlatform: (state, action) => {
|
||||
state.platform = action.payload;
|
||||
},
|
||||
resetFilters: () => {
|
||||
return initialState;
|
||||
},
|
||||
@ -76,6 +80,7 @@ export const {
|
||||
setViewsRange,
|
||||
setPricingRange,
|
||||
setSortBy,
|
||||
setPlatform,
|
||||
resetFilters,
|
||||
} = filtersSlice.actions;
|
||||
|
||||
|
27
src/store/slices/notificationBarSlice.js
Normal file
27
src/store/slices/notificationBarSlice.js
Normal file
@ -0,0 +1,27 @@
|
||||
const initialState = {
|
||||
message: '',
|
||||
show: false,
|
||||
type: 'success', // success, warning, error, info
|
||||
};
|
||||
|
||||
const notificationBarSlice = createSlice({
|
||||
name: 'notificationBar',
|
||||
initialState,
|
||||
reducers: {
|
||||
setNotificationBarMessage: (state, action) => {
|
||||
state.message = action.payload;
|
||||
},
|
||||
setNotificationBarShow: (state, action) => {
|
||||
state.show = action.payload;
|
||||
},
|
||||
resetNotificationBar: (state) => {
|
||||
state.message = '';
|
||||
state.show = false;
|
||||
state.type = 'success';
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setNotificationBarMessage, setNotificationBarShow, resetNotificationBar } = notificationBarSlice.actions;
|
||||
|
||||
export default notificationBarSlice.reducer;
|
Loading…
Reference in New Issue
Block a user