mirror of
https://github.com/Funkoala14/CreatorCenter_OOIN.git
synced 2025-06-07 21:48:14 +08:00
[dev]database filter
This commit is contained in:
parent
0a74e9a59c
commit
d9e0750441
10177
package-lock.json
generated
10177
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -17,9 +17,11 @@
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"bootstrap": "^5.3.3",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.508.0",
|
||||
"react": "^19.1.0",
|
||||
"react-bootstrap": "^2.10.1",
|
||||
"react-bootstrap-range-slider": "^3.0.8",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^7.6.0"
|
||||
|
260
src/components/DatabaseFilter.jsx
Normal file
260
src/components/DatabaseFilter.jsx
Normal file
@ -0,0 +1,260 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Row, Col, Form, InputGroup, Button } from 'react-bootstrap';
|
||||
import { Eye } from 'lucide-react';
|
||||
import RangeSlider from './RangeSlider';
|
||||
import '../styles/DatabaseFilter.scss';
|
||||
|
||||
export default function DatabaseFilter() {
|
||||
// 类别选项数据
|
||||
const categories = [
|
||||
'Phones & Electronics',
|
||||
'Homes Supplies',
|
||||
'Kitchenware',
|
||||
'Textiles & Soft Furnishings',
|
||||
'Household Appliances',
|
||||
];
|
||||
|
||||
// 电商评级数据
|
||||
const ecommerceRatings = ['L1', 'L2', 'L3', 'L4', 'L5', 'L6', 'L7'];
|
||||
|
||||
// 曝光评级数据
|
||||
const exposureRatings = ['KOC-1', 'KOC-2', 'KOL-1', 'KOL-2', 'KOL-3'];
|
||||
|
||||
// GMV 范围选项
|
||||
const gmvRanges = [
|
||||
{ label: '$0 - $5k', min: 0, max: 5000 },
|
||||
{ label: '$5k - $25k', min: 5000, max: 25000 },
|
||||
{ label: '$25k - $60k', min: 25000, max: 60000 },
|
||||
{ label: '$60k - $150k', min: 60000, max: 150000 },
|
||||
{ label: '$150k - $400k', min: 150000, max: 400000 },
|
||||
{ label: '$400k - $1500k', min: 400000, max: 1500000 },
|
||||
{ label: '$1500k+', min: 1500000, max: Number.MAX_SAFE_INTEGER },
|
||||
];
|
||||
|
||||
// 预定义的离散点值
|
||||
const discreteValues = [0, 100, 1000, 10000, 100000, 250000, 500000];
|
||||
|
||||
// 找到最接近的离散值索引
|
||||
const findClosestDiscreteIndex = (value) => {
|
||||
let closestIndex = 0;
|
||||
let minDiff = Math.abs(discreteValues[0] - value);
|
||||
|
||||
for (let i = 1; i < discreteValues.length; i++) {
|
||||
const diff = Math.abs(discreteValues[i] - value);
|
||||
if (diff < minDiff) {
|
||||
minDiff = diff;
|
||||
closestIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
return closestIndex;
|
||||
};
|
||||
|
||||
// 存储选择的状态
|
||||
const [selectedCategory, setSelectedCategory] = useState('Homes Supplies');
|
||||
const [selectedEcommerceRatings, setSelectedEcommerceRatings] = useState(['L2', 'L3']);
|
||||
const [selectedExposureRatings, setSelectedExposureRatings] = useState([]);
|
||||
const [selectedGmvRanges, setSelectedGmvRanges] = useState(['$5k - $25k', '$25k - $60k']);
|
||||
|
||||
// 视频观看量过滤器数值范围 - 使用离散值初始化
|
||||
const [viewsRange, setViewsRange] = useState([0, 100000]);
|
||||
const [minViews, setMinViews] = useState(0);
|
||||
const [maxViews, setMaxViews] = useState(100000);
|
||||
|
||||
// 处理类别选择
|
||||
const handleCategorySelect = (category) => {
|
||||
setSelectedCategory(category);
|
||||
};
|
||||
|
||||
// 处理电商评级选择
|
||||
const handleEcommerceRatingSelect = (rating) => {
|
||||
if (selectedEcommerceRatings.includes(rating)) {
|
||||
setSelectedEcommerceRatings(selectedEcommerceRatings.filter((r) => r !== rating));
|
||||
} else {
|
||||
setSelectedEcommerceRatings([...selectedEcommerceRatings, rating]);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理曝光评级选择
|
||||
const handleExposureRatingSelect = (rating) => {
|
||||
if (selectedExposureRatings.includes(rating)) {
|
||||
setSelectedExposureRatings(selectedExposureRatings.filter((r) => r !== rating));
|
||||
} else {
|
||||
setSelectedExposureRatings([...selectedExposureRatings, rating]);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理GMV范围选择
|
||||
const handleGmvRangeSelect = (range) => {
|
||||
if (selectedGmvRanges.includes(range)) {
|
||||
setSelectedGmvRanges(selectedGmvRanges.filter((r) => r !== range));
|
||||
} else {
|
||||
setSelectedGmvRanges([...selectedGmvRanges, range]);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理视图范围更新
|
||||
const handleViewsRangeChange = (newRange) => {
|
||||
setViewsRange(newRange);
|
||||
setMinViews(newRange[0]);
|
||||
setMaxViews(newRange[1]);
|
||||
};
|
||||
|
||||
// 处理min input变更
|
||||
const handleMinViewsChange = (e) => {
|
||||
const inputValue = parseInt(e.target.value) || 0;
|
||||
setMinViews(inputValue);
|
||||
};
|
||||
|
||||
// 处理max input变更
|
||||
const handleMaxViewsChange = (e) => {
|
||||
const inputValue = parseInt(e.target.value) || 0;
|
||||
setMaxViews(inputValue);
|
||||
};
|
||||
|
||||
// 当输入框失去焦点时,转换为最接近的离散值
|
||||
const handleInputBlur = (type) => {
|
||||
if (type === 'min') {
|
||||
// 找到最接近的离散值
|
||||
const closestIndex = findClosestDiscreteIndex(minViews);
|
||||
const discreteValue = discreteValues[closestIndex];
|
||||
|
||||
// 确保最小值不超过最大值
|
||||
const finalValue = Math.min(discreteValue, maxViews);
|
||||
|
||||
setMinViews(finalValue);
|
||||
setViewsRange([finalValue, viewsRange[1]]);
|
||||
} else {
|
||||
// 找到最接近的离散值
|
||||
const closestIndex = findClosestDiscreteIndex(maxViews);
|
||||
const discreteValue = discreteValues[closestIndex];
|
||||
|
||||
// 确保最大值不小于最小值
|
||||
const finalValue = Math.max(discreteValue, minViews);
|
||||
|
||||
setMaxViews(finalValue);
|
||||
setViewsRange([viewsRange[0], finalValue]);
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化显示值的函数
|
||||
const formatValue = (value) => {
|
||||
if (value >= 1000000) {
|
||||
return `${(value / 1000000).toFixed(1)}M`;
|
||||
}
|
||||
if (value >= 1000) {
|
||||
return `${(value / 1000).toFixed(0)}k`;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className='shadow-sm mb-4 filter-card'>
|
||||
<Card.Body>
|
||||
<h3 className='mb-4'>Filter</h3>
|
||||
|
||||
{/* 类别筛选 */}
|
||||
<div className='filter-item'>
|
||||
<h5 className='filter-title'>Category</h5>
|
||||
<div className='filter-options'>
|
||||
{categories.map((category) => (
|
||||
<Button
|
||||
key={category}
|
||||
variant={selectedCategory === category ? 'primary' : 'light'}
|
||||
className='rounded-pill'
|
||||
onClick={() => handleCategorySelect(category)}
|
||||
>
|
||||
{category}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 电商评级筛选 */}
|
||||
<div className='filter-item'>
|
||||
<h5 className='filter-title'>E-commerce Rating</h5>
|
||||
<div className='filter-options'>
|
||||
{ecommerceRatings.map((rating) => (
|
||||
<Button
|
||||
key={rating}
|
||||
variant={selectedEcommerceRatings.includes(rating) ? 'primary' : 'light'}
|
||||
className='rounded-pill'
|
||||
onClick={() => handleEcommerceRatingSelect(rating)}
|
||||
>
|
||||
{rating}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 曝光评级筛选 */}
|
||||
<div className='filter-item'>
|
||||
<h5 className='filter-title'>Exposure Rating</h5>
|
||||
<div className='filter-options'>
|
||||
{exposureRatings.map((rating) => (
|
||||
<Button
|
||||
key={rating}
|
||||
variant={selectedExposureRatings.includes(rating) ? 'primary' : 'light'}
|
||||
className='rounded-pill'
|
||||
onClick={() => handleExposureRatingSelect(rating)}
|
||||
>
|
||||
{rating}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* GMV筛选 */}
|
||||
<div className='filter-item'>
|
||||
<h5 className='filter-title'>GMV</h5>
|
||||
<div className='filter-options'>
|
||||
{gmvRanges.map((range) => (
|
||||
<Button
|
||||
key={range.label}
|
||||
variant={selectedGmvRanges.includes(range.label) ? 'primary' : 'light'}
|
||||
className='rounded-pill'
|
||||
onClick={() => handleGmvRangeSelect(range.label)}
|
||||
>
|
||||
{range.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 视频观看量筛选 */}
|
||||
<div className='filter-item'>
|
||||
<h5 className='filter-title'>Views</h5>
|
||||
<div className='filter-options filter-views'>
|
||||
<RangeSlider min={0} max={500000} value={viewsRange} onChange={handleViewsRangeChange} />
|
||||
|
||||
<div className='range-input'>
|
||||
<InputGroup>
|
||||
<InputGroup.Text>
|
||||
<Eye size={16} />
|
||||
</InputGroup.Text>
|
||||
<Form.Control
|
||||
type='number'
|
||||
value={minViews}
|
||||
onChange={handleMinViewsChange}
|
||||
onBlur={() => handleInputBlur('min')}
|
||||
/>
|
||||
</InputGroup>
|
||||
<span>-</span>
|
||||
<InputGroup>
|
||||
<InputGroup.Text>
|
||||
<Eye size={16} />
|
||||
</InputGroup.Text>
|
||||
<Form.Control
|
||||
type='number'
|
||||
value={maxViews}
|
||||
onChange={handleMaxViewsChange}
|
||||
onBlur={() => handleInputBlur('max')}
|
||||
/>
|
||||
</InputGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
);
|
||||
}
|
4
src/components/DatabaseList.jsx
Normal file
4
src/components/DatabaseList.jsx
Normal file
@ -0,0 +1,4 @@
|
||||
export default function DatabaseList() {
|
||||
return <div>DatabaseList</div>;
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import { Button } from 'react-bootstrap';
|
||||
import { List } from 'lucide-react';
|
||||
import Sidebar from './Sidebar';
|
||||
|
||||
export default function Layout() {
|
||||
export default function MainLayout() {
|
||||
const [showSidebar, setShowSidebar] = useState(true);
|
||||
|
||||
const toggleSidebar = () => {
|
188
src/components/RangeSlider.jsx
Normal file
188
src/components/RangeSlider.jsx
Normal file
@ -0,0 +1,188 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import '../styles/RangeSlider.scss';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
export default function RangeSlider({ min = 0, max = 100, value, onChange }) {
|
||||
// 预定义的离散值点
|
||||
const discreteValues = [0, 100, 1000, 10000, 100000, 250000, 500000];
|
||||
const marks = ['0', '100', '1k', '10k', '100k', '250k', '500k+'];
|
||||
|
||||
// 使用索引位置作为滑块的实际值,以实现等分
|
||||
const getIndexForValue = (val) => {
|
||||
for (let i = 0; i < discreteValues.length; i++) {
|
||||
if (val <= discreteValues[i]) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return discreteValues.length - 1;
|
||||
};
|
||||
|
||||
// 找到最接近的离散值索引
|
||||
const findClosestDiscreteIndex = (value) => {
|
||||
let closestIndex = 0;
|
||||
let minDiff = Math.abs(discreteValues[0] - value);
|
||||
|
||||
for (let i = 1; i < discreteValues.length; i++) {
|
||||
const diff = Math.abs(discreteValues[i] - value);
|
||||
if (diff < minDiff) {
|
||||
minDiff = diff;
|
||||
closestIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
return closestIndex;
|
||||
};
|
||||
|
||||
// 初始化使用给定值的索引位置
|
||||
const initialMinIndex = findClosestDiscreteIndex(value[0]);
|
||||
const initialMaxIndex = findClosestDiscreteIndex(value[1]);
|
||||
|
||||
const [minValIndex, setMinValIndex] = useState(initialMinIndex);
|
||||
const [maxValIndex, setMaxValIndex] = useState(initialMaxIndex);
|
||||
const minValIndexRef = useRef(initialMinIndex);
|
||||
const maxValIndexRef = useRef(initialMaxIndex);
|
||||
const range = useRef(null);
|
||||
|
||||
// 将索引转换为百分比位置
|
||||
const getPercent = (index) => {
|
||||
return Math.round((index / (discreteValues.length - 1)) * 100);
|
||||
};
|
||||
|
||||
const debouncedOnChange = useRef(
|
||||
debounce((minIndex, maxIndex) => {
|
||||
onChange([discreteValues[minIndex], discreteValues[maxIndex]]);
|
||||
}, 300)
|
||||
).current;
|
||||
|
||||
// 更新内部状态以匹配外部传入的值
|
||||
useEffect(() => {
|
||||
const newMinIndex = findClosestDiscreteIndex(value[0]);
|
||||
const newMaxIndex = findClosestDiscreteIndex(value[1]);
|
||||
|
||||
setMinValIndex(newMinIndex);
|
||||
setMaxValIndex(newMaxIndex);
|
||||
minValIndexRef.current = newMinIndex;
|
||||
maxValIndexRef.current = newMaxIndex;
|
||||
}, [value]);
|
||||
|
||||
// 设置上限和下限的宽度
|
||||
useEffect(() => {
|
||||
if (range.current) {
|
||||
const minPercent = getPercent(minValIndex);
|
||||
const maxPercent = getPercent(maxValIndexRef.current);
|
||||
|
||||
range.current.style.left = `${minPercent}%`;
|
||||
range.current.style.width = `${maxPercent - minPercent}%`;
|
||||
}
|
||||
}, [minValIndex]);
|
||||
|
||||
// 设置下限和区间的宽度
|
||||
useEffect(() => {
|
||||
if (range.current) {
|
||||
const minPercent = getPercent(minValIndexRef.current);
|
||||
const maxPercent = getPercent(maxValIndex);
|
||||
|
||||
range.current.style.width = `${maxPercent - minPercent}%`;
|
||||
}
|
||||
}, [maxValIndex]);
|
||||
|
||||
// 当值改变时通知父组件,但避免循环更新
|
||||
useEffect(() => {
|
||||
const minVal = discreteValues[minValIndex];
|
||||
const maxVal = discreteValues[maxValIndex];
|
||||
|
||||
// 判断值是否与外部传入的值不同
|
||||
if (minVal !== value[0] || maxVal !== value[1]) {
|
||||
debouncedOnChange(minValIndex, maxValIndex);
|
||||
}
|
||||
}, [minValIndex, maxValIndex]);
|
||||
|
||||
// 格式化显示的值(如1k, 10k, 100k等)
|
||||
const formatValue = (value) => {
|
||||
if (value >= 1000000) {
|
||||
return `${(value / 1000000).toFixed(1)}M`;
|
||||
}
|
||||
if (value >= 1000) {
|
||||
return `${(value / 1000).toFixed(0)}k`;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const handleSliderChange = (which, val) => {
|
||||
// 将输入值转换为索引(0-6)
|
||||
const index = Math.round(val);
|
||||
|
||||
if (which === 'min') {
|
||||
// 确保最小值不超过最大值
|
||||
const newIndex = Math.min(index, maxValIndex - 1);
|
||||
setMinValIndex(newIndex);
|
||||
minValIndexRef.current = newIndex;
|
||||
} else {
|
||||
// 确保最大值不小于最小值
|
||||
const newIndex = Math.max(index, minValIndex + 1);
|
||||
setMaxValIndex(newIndex);
|
||||
maxValIndexRef.current = newIndex;
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
onChange([discreteValues[minValIndex], discreteValues[maxValIndex]]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='range-slider slider-container'>
|
||||
<input
|
||||
type='range'
|
||||
min={0}
|
||||
max={discreteValues.length - 1}
|
||||
step={1}
|
||||
value={minValIndex}
|
||||
onChange={(e) => handleSliderChange('min', +e.target.value)}
|
||||
onMouseUp={handleMouseUp}
|
||||
onTouchEnd={handleMouseUp}
|
||||
className='thumb thumb--left'
|
||||
style={{ zIndex: minValIndex > discreteValues.length - 3 ? 5 : 4 }}
|
||||
/>
|
||||
<input
|
||||
type='range'
|
||||
min={0}
|
||||
max={discreteValues.length - 1}
|
||||
step={1}
|
||||
value={maxValIndex}
|
||||
onChange={(e) => handleSliderChange('max', +e.target.value)}
|
||||
onMouseUp={handleMouseUp}
|
||||
onTouchEnd={handleMouseUp}
|
||||
className='thumb thumb--right'
|
||||
/>
|
||||
|
||||
<div className='slider'>
|
||||
<div className='slider__track'></div>
|
||||
<div ref={range} className='slider__range'></div>
|
||||
{/* 添加离散标记点 */}
|
||||
<div className='slider__steps'>
|
||||
{discreteValues.map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`slider__step ${index >= minValIndex && index <= maxValIndex ? 'active' : ''}`}
|
||||
style={{ left: `${getPercent(index)}%` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className='range-slider-marks'>
|
||||
{marks.map((mark, index) => (
|
||||
<span key={index}>{mark}</span>
|
||||
))}
|
||||
</div>
|
||||
{/* 显示当前选中的值 */}
|
||||
{/* <div className='slider__values'>
|
||||
<div className='slider__value-left' style={{ left: `${getPercent(minValIndex)}%` }}>
|
||||
{formatValue(discreteValues[minValIndex])}
|
||||
</div>
|
||||
<div className='slider__value-right' style={{ left: `${getPercent(maxValIndex)}%` }}>
|
||||
{formatValue(discreteValues[maxValIndex])}
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import '../src/styles/custom-theme.scss';
|
||||
import './styles/global.scss';
|
||||
import './index.css';
|
||||
import App from './App.jsx';
|
||||
|
||||
|
16
src/pages/Database.jsx
Normal file
16
src/pages/Database.jsx
Normal file
@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import DatabaseFilter from '../components/DatabaseFilter';
|
||||
import DatabaseList from '../components/DatabaseList';
|
||||
import { Link } from 'react-router-dom';
|
||||
export default function Database({ path }) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className='breadcrumb'>
|
||||
<div className='breadcrumb-item'>Creator Database</div>
|
||||
<div className='breadcrumb-item'>{path}</div>
|
||||
</div>
|
||||
<DatabaseFilter />
|
||||
<DatabaseList />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
||||
import Home from '../pages/Home';
|
||||
import BootstrapLayout from '../components/Layout';
|
||||
import Database from '../pages/Database';
|
||||
import MainLayout from '../components/MainLayout';
|
||||
|
||||
// Routes configuration object
|
||||
const routes = [
|
||||
@ -17,19 +18,19 @@ const routes = [
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
element: <Home />,
|
||||
element: <Database />,
|
||||
},
|
||||
{
|
||||
path: 'tiktok',
|
||||
element: <Home />,
|
||||
element: <Database path='tiktok' />,
|
||||
},
|
||||
{
|
||||
path: 'instagram',
|
||||
element: <Home />,
|
||||
element: <Database path='instagram' />,
|
||||
},
|
||||
{
|
||||
path: 'youtube',
|
||||
element: <Home />,
|
||||
element: <Database path='youtube' />,
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -59,7 +60,7 @@ const routes = [
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: '/',
|
||||
element: <BootstrapLayout />,
|
||||
element: <MainLayout />,
|
||||
children: routes,
|
||||
},
|
||||
]);
|
||||
|
106
src/styles/DatabaseFilter.scss
Normal file
106
src/styles/DatabaseFilter.scss
Normal file
@ -0,0 +1,106 @@
|
||||
@import './custom-theme.scss';
|
||||
|
||||
.filter-card {
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
overflow: hidden;
|
||||
background-color: #fff;
|
||||
|
||||
.card-body {
|
||||
padding: 1.5rem;
|
||||
|
||||
.filter-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
.filter-title {
|
||||
width: 170px;
|
||||
margin: 0;
|
||||
color: $gray-500;
|
||||
}
|
||||
|
||||
.filter-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
flex: 1;
|
||||
|
||||
&.filter-views {
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
gap: 1rem;
|
||||
.range-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.input-group {
|
||||
flex: 1;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
span {
|
||||
padding: 0 5px;
|
||||
color: $gray-600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-weight: 600;
|
||||
font-size: 1.5rem;
|
||||
color: $dark;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
color: $dark;
|
||||
}
|
||||
|
||||
.btn-light {
|
||||
background-color: #f5f5f5;
|
||||
border-color: transparent;
|
||||
color: $gray-800;
|
||||
font-size: 0.875rem;
|
||||
|
||||
&:hover {
|
||||
border-color: transparent;
|
||||
background-color: #eee;
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border-color: #e2e8f0;
|
||||
|
||||
&:focus {
|
||||
border-color: $primary;
|
||||
box-shadow: 0 0 0 0.25rem rgba($primary, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
.input-group-text {
|
||||
background-color: #f8f9fa;
|
||||
border-color: #e2e8f0;
|
||||
}
|
||||
}
|
||||
|
||||
// 圆角标签样式
|
||||
.rounded-pill {
|
||||
padding: 0.375rem 1rem;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.btn-primary {
|
||||
box-shadow: 0 2px 5px rgba($primary, 0.3);
|
||||
}
|
||||
}
|
214
src/styles/RangeSlider.scss
Normal file
214
src/styles/RangeSlider.scss
Normal file
@ -0,0 +1,214 @@
|
||||
@import './custom-theme.scss';
|
||||
|
||||
$primary: #6366f1; // 使用与主题一致的颜色
|
||||
|
||||
.range-slider {
|
||||
width: 70%;
|
||||
position: relative;
|
||||
padding: 0.5rem 10px 0;
|
||||
|
||||
.range-slider-marks {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 5px;
|
||||
margin-top: 5px;
|
||||
font-size: 0.75rem;
|
||||
color: #6c757d;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.slider-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 5px;
|
||||
|
||||
&__track {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 5px;
|
||||
border-radius: 3px;
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
&__range {
|
||||
position: absolute;
|
||||
height: 5px;
|
||||
border-radius: 3px;
|
||||
background-color: #6366f1;
|
||||
}
|
||||
|
||||
&__steps {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 5px;
|
||||
|
||||
.slider__step {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background-color: #e9ecef;
|
||||
top: -2.5px;
|
||||
margin-left: -5px;
|
||||
z-index: 1;
|
||||
border: 1px solid #adb5bd;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.active {
|
||||
background-color: #6366f1;
|
||||
border-color: #6366f1;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__values {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 25px;
|
||||
|
||||
.slider__value-left,
|
||||
.slider__value-right {
|
||||
position: absolute;
|
||||
transform: translateX(-50%);
|
||||
font-size: 0.75rem;
|
||||
color: white;
|
||||
background-color: #6366f1;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-bottom: 5px solid #6366f1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.thumb {
|
||||
position: absolute;
|
||||
height: 0;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
z-index: 3;
|
||||
appearance: none;
|
||||
pointer-events: none;
|
||||
left: 0;
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
pointer-events: all;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background-color: white;
|
||||
border: 2px solid #6366f1;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #6366f1;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
|
||||
transform: scale(1.15);
|
||||
}
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
appearance: none;
|
||||
pointer-events: all;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background-color: white;
|
||||
border: 2px solid #6366f1;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #6366f1;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
|
||||
transform: scale(1.15);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Removing the default appearance */
|
||||
.thumb,
|
||||
.thumb::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.thumb {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
height: 0;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.thumb--left {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.thumb--right {
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
/* For Chrome browsers */
|
||||
.thumb::-webkit-slider-thumb {
|
||||
background-color: #fff;
|
||||
border: 2px solid $primary;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 3px rgba(0, 0, 0, 0.2);
|
||||
cursor: pointer;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
margin-top: 4px;
|
||||
pointer-events: all;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* For Firefox browsers */
|
||||
.thumb::-moz-range-thumb {
|
||||
background-color: #fff;
|
||||
border: 2px solid $primary;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 3px rgba(0, 0, 0, 0.2);
|
||||
cursor: pointer;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
margin-top: 4px;
|
||||
pointer-events: all;
|
||||
position: relative;
|
||||
}
|
@ -29,6 +29,27 @@ $box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
// 导入Bootstrap
|
||||
@import 'bootstrap/scss/bootstrap';
|
||||
|
||||
:root {
|
||||
--bs-breadcrumb-font-size: 1.5rem;
|
||||
--bs-body-color: #171A1FFF;
|
||||
--bs-btn-color: white;
|
||||
--bs-btn-hover-color: white;
|
||||
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
--bs-btn-color: white !important;
|
||||
--bs-btn-hover-color: white !important;
|
||||
}
|
||||
|
||||
|
||||
#root {
|
||||
background-color: #f5f3ff;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none !important;
|
||||
&:hover {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
}
|
||||
|
7
src/styles/global.scss
Normal file
7
src/styles/global.scss
Normal file
@ -0,0 +1,7 @@
|
||||
@import 'custom-theme.scss';
|
||||
|
||||
.breadcrumb {
|
||||
a {
|
||||
color: var(--bs-body-color) !important;
|
||||
}
|
||||
}
|
@ -103,7 +103,7 @@
|
||||
|
||||
// Adjust main content when sidebar is present
|
||||
.main-content {
|
||||
padding: 1rem;
|
||||
padding: 1.5rem;
|
||||
margin: 1rem;
|
||||
margin-left: 220px;
|
||||
height: 100vh;
|
||||
|
Loading…
Reference in New Issue
Block a user