From db48e55b053d0f849f3263a8e8d922872db4e0f5 Mon Sep 17 00:00:00 2001 From: juguohong Date: Sun, 17 Aug 2025 18:36:43 +0800 Subject: [PATCH] =?UTF-8?q?=E7=AD=9B=E9=80=89=E7=BB=84=E4=BB=B6=E5=BC=80?= =?UTF-8?q?=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/index.ts | 4 +- src/app.config.ts | 4 +- src/components/Bubble/BubbleItem.tsx | 18 +- ...bubbleItem.scss => bubbleItem.module.scss} | 14 +- .../Bubble/{index.scss => index.module.scss} | 19 +- src/components/Bubble/index.tsx | 56 ++-- src/components/CityFilter/example.tsx | 18 ++ src/components/CityFilter/index.module.scss | 59 +++++ src/components/CityFilter/index.tsx | 64 +++++ src/components/Menu/example.tsx | 18 ++ src/components/Menu/index.module.scss | 45 ++++ src/components/Menu/index.tsx | 37 +++ src/components/Range/index.module.scss | 81 ++++++ src/components/Range/index.scss | 246 ------------------ src/components/Range/index.tsx | 20 +- src/pages/list/index.tsx | 8 + types/css-modules.d.ts | 9 + 17 files changed, 406 insertions(+), 314 deletions(-) rename src/components/Bubble/{bubbleItem.scss => bubbleItem.module.scss} (89%) rename src/components/Bubble/{index.scss => index.module.scss} (51%) create mode 100644 src/components/CityFilter/example.tsx create mode 100644 src/components/CityFilter/index.module.scss create mode 100644 src/components/CityFilter/index.tsx create mode 100644 src/components/Menu/example.tsx create mode 100644 src/components/Menu/index.module.scss create mode 100644 src/components/Menu/index.tsx create mode 100644 src/components/Range/index.module.scss delete mode 100644 src/components/Range/index.scss create mode 100644 types/css-modules.d.ts diff --git a/config/index.ts b/config/index.ts index 3eb418b..2bc2144 100644 --- a/config/index.ts +++ b/config/index.ts @@ -46,7 +46,7 @@ export default defineConfig<'webpack5'>(async (merge, { command, mode }) => { } }, cssModules: { - enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true + enable: true, // 默认为 false,如需使用 css modules 功能,则设为 true config: { namingPattern: 'module', // 转换模式,取值为 global/module generateScopedName: '[name]__[local]___[hash:base64:5]' @@ -75,7 +75,7 @@ export default defineConfig<'webpack5'>(async (merge, { command, mode }) => { config: {} }, cssModules: { - enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true + enable: true, // 默认为 false,如需使用 css modules 功能,则设为 true config: { namingPattern: 'module', // 转换模式,取值为 global/module generateScopedName: '[name]__[local]___[hash:base64:5]' diff --git a/src/app.config.ts b/src/app.config.ts index 5d3f7cb..ea3393a 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -1,7 +1,7 @@ export default defineAppConfig({ pages: [ - 'pages/index/index', - 'pages/list/index' + 'pages/list/index', + 'pages/index/index' ], window: { backgroundTextStyle: 'light', diff --git a/src/components/Bubble/BubbleItem.tsx b/src/components/Bubble/BubbleItem.tsx index 300c8e9..0ede586 100644 --- a/src/components/Bubble/BubbleItem.tsx +++ b/src/components/Bubble/BubbleItem.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { BubbleOption } from './index'; -import './bubbleItem.scss'; +import styles from './bubbleItem.module.scss'; export interface BubbleItemProps { option: BubbleOption; @@ -8,6 +8,7 @@ export interface BubbleItemProps { size: 'small' | 'medium' | 'large'; disabled: boolean; onClick: (option: BubbleOption) => void; + itemClassName?: string; } const BubbleItem: React.FC = ({ @@ -15,19 +16,20 @@ const BubbleItem: React.FC = ({ isSelected, size, disabled, - onClick + onClick, + itemClassName }) => { return ( ); }; diff --git a/src/components/Bubble/bubbleItem.scss b/src/components/Bubble/bubbleItem.module.scss similarity index 89% rename from src/components/Bubble/bubbleItem.scss rename to src/components/Bubble/bubbleItem.module.scss index e1a1dbb..8c032ea 100644 --- a/src/components/Bubble/bubbleItem.scss +++ b/src/components/Bubble/bubbleItem.module.scss @@ -1,4 +1,4 @@ -.bubble-option { +.bubbleOption { position: relative; border: 1px solid #e5e5e5; outline: none; // 移除浏览器默认的outline @@ -14,11 +14,11 @@ justify-content: center; gap: 8px; white-space: nowrap; - padding: 0; font-size: 14px; border-radius: 28px; margin: 0; - height: 36px; + width: 116px; + height: 28px; // 移除浏览器默认样式 &:focus { @@ -38,8 +38,6 @@ // 尺寸变体 &.small { - padding: 8px 12px; - min-height: 32px; font-size: 12px; } @@ -71,7 +69,7 @@ } // 图标样式 - .bubble-icon { + .bubbleIcon { display: flex; align-items: center; justify-content: center; @@ -83,12 +81,12 @@ } // 标签样式 - .bubble-label { + .bubbleLabel { font-weight: 500; } // 描述样式 - .bubble-description { + .bubbleDescription { font-size: 12px; opacity: 0.7; font-weight: 400; diff --git a/src/components/Bubble/index.scss b/src/components/Bubble/index.module.scss similarity index 51% rename from src/components/Bubble/index.scss rename to src/components/Bubble/index.module.scss index aa88fbb..52b676d 100644 --- a/src/components/Bubble/index.scss +++ b/src/components/Bubble/index.module.scss @@ -1,34 +1,23 @@ -.bubble-container { +.bubbleContainer { width: 100%; // 水平布局 - .bubble-horizontal { + .bubbleHorizontal { display: flex; gap: 12px; flex-wrap: wrap; justify-content: space-between; - - .bubble-option { - $gap: 8px; - $count: 3; - flex-basis: calc(100% / $count - $gap * 2); - gap: $gap; - } } // 垂直布局 - .bubble-vertical { + .bubbleVertical { display: flex; flex-direction: column; gap: 12px; - - .bubble-option { - width: 100%; - } } // 网格布局 - .bubble-grid { + .bubbleGrid { display: grid; width: 100%; } diff --git a/src/components/Bubble/index.tsx b/src/components/Bubble/index.tsx index 2c118d3..fe56b6d 100644 --- a/src/components/Bubble/index.tsx +++ b/src/components/Bubble/index.tsx @@ -1,6 +1,6 @@ -import React, { useState, useEffect } from 'react'; -import './index.scss'; -import BubbleItem from './BubbleItem'; +import React, { useState, useEffect } from "react"; +import styles from "./index.module.scss"; +import BubbleItem from "./BubbleItem"; export interface BubbleOption { id: string | number; @@ -14,12 +14,16 @@ export interface BubbleOption { export interface BubbleProps { options: BubbleOption[]; value?: string | number | (string | number)[]; - onChange?: (value: string | number | (string | number)[], option: BubbleOption | BubbleOption[]) => void; + onChange?: ( + value: string | number | (string | number)[], + option: BubbleOption | BubbleOption[] + ) => void; multiple?: boolean; - layout?: 'horizontal' | 'vertical' | 'grid'; + layout?: "horizontal" | "vertical" | "grid"; columns?: number; - size?: 'small' | 'medium' | 'large'; + size?: "small" | "medium" | "large"; className?: string; + itemClassName?: string; style?: React.CSSProperties; disabled?: boolean; } @@ -29,12 +33,13 @@ const Bubble: React.FC = ({ value, onChange, multiple = false, - layout = 'horizontal', + layout = "horizontal", columns = 3, - size = 'small', - className = '', + size = "small", + className = "", + itemClassName = "", style = {}, - disabled = false + disabled = false, }) => { const [selectedValues, setSelectedValues] = useState<(string | number)[]>([]); @@ -53,7 +58,7 @@ const Bubble: React.FC = ({ if (multiple) { if (selectedValues.includes(option.value)) { - newSelectedValues = selectedValues.filter(v => v !== option.value); + newSelectedValues = selectedValues.filter((v) => v !== option.value); } else { newSelectedValues = [...selectedValues, option.value]; } @@ -62,11 +67,13 @@ const Bubble: React.FC = ({ } setSelectedValues(newSelectedValues); - + // 调用onChange回调,传递选中的值和对应的选项 if (onChange) { if (multiple) { - const selectedOptions = options.filter(opt => newSelectedValues.includes(opt.value)); + const selectedOptions = options.filter((opt) => + newSelectedValues.includes(opt.value) + ); onChange(newSelectedValues, selectedOptions); } else { onChange(option.value, option); @@ -79,7 +86,7 @@ const Bubble: React.FC = ({ }; const renderHorizontalLayout = () => ( -
+
{options.map((option) => ( = ({ size={size} disabled={disabled} onClick={handleOptionClick} + itemClassName={itemClassName} /> ))}
); const renderVerticalLayout = () => ( -
+
{options.map((option) => ( = ({ size={size} disabled={disabled} onClick={handleOptionClick} + itemClassName={itemClassName} /> ))}
); const renderGridLayout = () => ( -
{options.map((option) => ( @@ -124,6 +133,7 @@ const Bubble: React.FC = ({ size={size} disabled={disabled} onClick={handleOptionClick} + itemClassName={itemClassName} /> ))}
@@ -131,11 +141,11 @@ const Bubble: React.FC = ({ const renderLayout = () => { switch (layout) { - case 'horizontal': + case "horizontal": return renderHorizontalLayout(); - case 'vertical': + case "vertical": return renderVerticalLayout(); - case 'grid': + case "grid": return renderGridLayout(); default: return renderHorizontalLayout(); @@ -143,7 +153,7 @@ const Bubble: React.FC = ({ }; return ( -
+
{renderLayout()}
); diff --git a/src/components/CityFilter/example.tsx b/src/components/CityFilter/example.tsx new file mode 100644 index 0000000..022297a --- /dev/null +++ b/src/components/CityFilter/example.tsx @@ -0,0 +1,18 @@ +import { useState } from "react"; +import MenuComponent from "./index"; + +export default function Example() { + const [value, setValue] = useState("a"); + const options = [ + { text: "默认排序", value: "a" }, + { text: "好评排序", value: "b" }, + { text: "销量排序", value: "c" }, + ]; + return ( + setValue(val)} + /> + ); +} diff --git a/src/components/CityFilter/index.module.scss b/src/components/CityFilter/index.module.scss new file mode 100644 index 0000000..2be87e2 --- /dev/null +++ b/src/components/CityFilter/index.module.scss @@ -0,0 +1,59 @@ +.menuWrap { + padding: 5px 20px 10px; + .menuItem { + left: 0; + border-bottom-left-radius: 30px; + border-bottom-right-radius: 30px; + } + &.active { + .nut-menu-bar { + background-color: #000000; + color: #ffffff; + } + } + :global(.nut-menu-bar) { + color: #000000; + line-height: 1; + box-shadow: unset; + min-height: 28px; + min-width: 80px; + border-radius: 28px; + border: 1px solid #e5e5e5; + line-height: 28px; + font-size: 14px; + width: max-content; + .nut-menu-title { + color: inherit !important; + font-weight: 600; + } + + .nut-menu-title-text { + padding-left: 0; + } + } + + .positionWrap { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + margin-bottom: 16px; + } + + .title { + font-size: 14px; + font-weight: 600; + } + + .cityName { + font-size: 13px; + font-weight: 400; + } + .distanceWrap { + margin-bottom: 16px; + width: 100%; + } + .distanceBubbleItem { + width: auto; + } +} diff --git a/src/components/CityFilter/index.tsx b/src/components/CityFilter/index.tsx new file mode 100644 index 0000000..db40f86 --- /dev/null +++ b/src/components/CityFilter/index.tsx @@ -0,0 +1,64 @@ +import { Menu } from "@nutui/nutui-react-taro"; +import styles from "./index.module.scss"; +import { useState, useRef } from "react"; +import Bubble, { BubbleOption } from "../Bubble"; + +interface IProps { + options: BubbleOption[]; + value: string; + onChange: (value: string) => void; + wrapperClassName?: string; + itemClassName?: string; +} + +const MenuComponent = (props: IProps) => { + const { value, onChange, wrapperClassName, itemClassName } = props; + const [isChange, setIsChange] = useState(false); + const itemRef = useRef(null); + + const handleChange = (value: string) => { + console.log("===value", value); + setIsChange(true); + onChange && onChange(value); + }; + + const options: BubbleOption[] = [ + { id: 0, label: "全城", value: "0" }, + { id: 1, label: "3km", value: "3" }, + { id: 2, label: "5km", value: "5" }, + { id: 3, label: "10km", value: "10" }, + ]; + + return ( + + +
+

当前位置

+

上海市

+
+
+ +
+
+
+ ); +}; + +export default MenuComponent; diff --git a/src/components/Menu/example.tsx b/src/components/Menu/example.tsx new file mode 100644 index 0000000..022297a --- /dev/null +++ b/src/components/Menu/example.tsx @@ -0,0 +1,18 @@ +import { useState } from "react"; +import MenuComponent from "./index"; + +export default function Example() { + const [value, setValue] = useState("a"); + const options = [ + { text: "默认排序", value: "a" }, + { text: "好评排序", value: "b" }, + { text: "销量排序", value: "c" }, + ]; + return ( + setValue(val)} + /> + ); +} diff --git a/src/components/Menu/index.module.scss b/src/components/Menu/index.module.scss new file mode 100644 index 0000000..0867087 --- /dev/null +++ b/src/components/Menu/index.module.scss @@ -0,0 +1,45 @@ +.menuWrap { + padding: 5px 20px 10px; + .menuItem { + left: 0; + border-bottom-left-radius: 30px; + border-bottom-right-radius: 30px; + } + &.active { + :global(.nut-menu-bar) { + background-color: #000000; + color: #ffffff; + } + } + :global(.nut-menu-bar) { + color: #000000; + line-height: 1; + box-shadow: unset; + min-height: 28px; + min-width: 94px; + border-radius: 28px; + border: 1px solid #e5e5e5; + line-height: 28px; + font-size: 14px; + width: max-content; + + .nut-menu-title-text { + padding-left: 0; + } + } + + :global(.nut-menu-title) { + color: inherit !important; + font-weight: 600; + } + + :global(.nut-menu-container-item) { + color: #3c3c43; + font-weight: 600; + font-size: 14px; + } + :global(.nut-menu-container-item.active) { + flex-direction: row-reverse; + justify-content: space-between; + } +} diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx new file mode 100644 index 0000000..0f3d435 --- /dev/null +++ b/src/components/Menu/index.tsx @@ -0,0 +1,37 @@ +import { Menu } from "@nutui/nutui-react-taro"; +import styles from "./index.module.scss"; +import { useState } from "react"; + +interface IProps { + options: { text: string; value: string }[]; + value: string; + onChange: (value: string) => void; + wrapperClassName?: string; + itemClassName?: string; +} + +const MenuComponent = (props: IProps) => { + const { options, value, onChange, wrapperClassName, itemClassName } = props; + const [isChange, setIsChange] = useState(false); + + const handleChange = (value: string) => { + setIsChange(true); + onChange && onChange(value); + }; + + return ( + + + + ); +}; + +export default MenuComponent; diff --git a/src/components/Range/index.module.scss b/src/components/Range/index.module.scss new file mode 100644 index 0000000..571e63d --- /dev/null +++ b/src/components/Range/index.module.scss @@ -0,0 +1,81 @@ + + +// 全局NutUI样式覆盖 +.nutRange { + width: 100% !important; + height: 100% !important; + + // .nut-range__bar-box { + // margin: 0 !important; + // padding: 0 !important; + // } + + // .nut-range__bar { + // margin: 0 !important; + // } +} + +.nutRangeHeader { + line-height: 20px; + display: flex; + align-items: center; + justify-content: space-between; + .nutRangeHeaderLeft { + display: flex; + align-items: center; + gap: 8px; + } + .nutRangeHeaderTitle { + font-weight: 600; + font-size: 14px; + color: #000000; + } + + .nutRangeHeaderContent { + font-weight: 400; + font-size: 14px; + color: #3c3c34; + } +} + +.rangeWrapper { + display: flex; + align-items: center; + justify-content: space-between; + gap: 5px; + height: 44px; + border-radius: 12px; + border: 1px solid #e0e0e0; + padding: 0 10px; + .rangeHandle { + :global(.nut-range-mark) { + padding-top: 28px; + left: 8px; + } + :global(.nut-range-bar) { + background: #000000; + height: 6px; + } + :global(.nut-range-button) { + border: none; + box-shadow: 0 6px 13px 0 rgba(0, 0, 0, 0.12); + } + + :global(.nut-range-tick) { + background: #3c3c3c; + height: 4px !important; + width: 4px !important; + } + } + + span { + font-size: 12px; + color: #999; + } + .rangeWrapperMin, + .rangeWrapperMax { + flex-shrink: 0; + font-size: 12px; + color: #000000; + } +} diff --git a/src/components/Range/index.scss b/src/components/Range/index.scss deleted file mode 100644 index 00bd829..0000000 --- a/src/components/Range/index.scss +++ /dev/null @@ -1,246 +0,0 @@ -.ntrp-range { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - user-select: none; - - &__header { - display: flex; - align-items: center; - margin-bottom: 16px; - } - - &__icon { - margin-right: 8px; - display: flex; - align-items: center; - - svg { - width: 20px; - height: 20px; - } - } - - &__title { - margin: 0; - font-size: 16px; - font-weight: 500; - color: #000; - line-height: 1.2; - } - - &__slider-container { - position: relative; - } - - &__labels { - display: flex; - justify-content: space-between; - margin-bottom: 12px; - } - - &__label { - font-size: 14px; - color: #000; - font-weight: 400; - } - - &__track-container { - position: relative; - padding: 0 10px; - } - - &__track { - position: relative; - height: 40px; - background: #fff; - border: 1px solid #e0e0e0; - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); - display: flex; - align-items: center; - padding: 0 20px; - } - - &__markers { - position: absolute; - width: 100%; - height: 100%; - pointer-events: none; - z-index: 1; - } - - &__marker { - position: absolute; - width: 6px; - height: 6px; - background: #e0e0e0; - border-radius: 50%; - top: 50%; - transform: translate(-50%, -50%); - } - - &__nutui-wrapper { - position: relative; - width: 100%; - height: 100%; - z-index: 2; - - // 隐藏NutUI的默认标签 - .nut-range__label { - display: none !important; - } - - // 隐藏默认的轨道背景 - .nut-range__bar-box { - background: transparent !important; - height: 6px !important; - border-radius: 3px !important; - margin: 0 !important; - padding: 0 !important; - } - - // 隐藏默认的轨道 - .nut-range__bar { - background: transparent !important; - height: 0 !important; - margin: 0 !important; - } - - // 自定义活动轨道(黑色填充条) - .nut-range__bar--active { - background: #000 !important; - height: 6px !important; - border-radius: 3px !important; - margin: 0 !important; - } - - // 自定义滑块手柄样式 - .nut-range__button { - width: 24px !important; - height: 24px !important; - background: #fff !important; - border: 2px solid #000 !important; - border-radius: 50% !important; - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15) !important; - top: 50% !important; - transform: translate(-50%, -50%) !important; - transition: all 0.1s ease !important; - - &:hover { - transform: translate(-50%, -50%) scale(1.1) !important; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2) !important; - } - - &:active { - transform: translate(-50%, -50%) scale(1.15) !important; - box-shadow: 0 6px 16px rgba(0, 0, 0, 0.25) !important; - } - } - - // 确保滑块在正确位置 - .nut-range__button-wrapper { - top: 50% !important; - transform: translateY(-50%) !important; - } - } - - // 响应式设计 - @media (max-width: 480px) { - &__track { - padding: 0 15px; - } - - &__nutui-wrapper { - .nut-range__button { - width: 20px !important; - height: 20px !important; - } - } - - &__title { - font-size: 14px; - } - - &__label { - font-size: 12px; - } - } -} - -// 全局NutUI样式覆盖 -.nut-range { - width: 100% !important; - height: 100% !important; - - .nut-range__bar-box { - margin: 0 !important; - padding: 0 !important; - } - - .nut-range__bar { - margin: 0 !important; - } -} - -.ntrp-range__header { - line-height: 20px; - display: flex; - align-items: center; - justify-content: space-between; - .ntrp-range__header-left { - display: flex; - align-items: center; - gap: 8px; - } - .ntrp-range__title { - font-weight: 600; - font-size: 14px; - color: #000000; - } - - .ntrp-range__content { - font-weight: 400; - font-size: 14px; - color: #3c3c34; - } -} - -.rangeWrapper { - display: flex; - align-items: center; - justify-content: space-between; - gap: 5px; - height: 44px; - border-radius: 12px; - border: 1px solid #e0e0e0; - padding: 0 10px; - .rangeHandle { - .nut-range-mark { - padding-top: 28px; - } - .nut-range-bar { - background: #000000; - height: 6px; - } - .nut-range-button { - border: none; - box-shadow: 0 6px 13px 0 rgba(0, 0, 0, 0.12); - } - - .nut-range-tick { - background: #3c3c3c; - height: 4px !important; - width: 4px !important; - } - } - - span { - font-size: 12px; - color: #999; - } - .rangeWrapper__min, - .rangeWrapper__max { - flex-shrink: 0; - font-size: 12px; - color: #000000; - } -} diff --git a/src/components/Range/index.tsx b/src/components/Range/index.tsx index 0523235..b1ddbf6 100644 --- a/src/components/Range/index.tsx +++ b/src/components/Range/index.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useMemo } from "react"; import { Range } from "@nutui/nutui-react-taro"; -import "./index.scss"; +import styles from "./index.module.scss"; interface RangeProps { min?: number; @@ -48,18 +48,18 @@ const NtrpRange: React.FC = ({ }, [currentValue, min, max]); return ( -
-
-
+
+
+
icon
-

NTRP水平区间

+

NTRP水平区间

-

{rangContent}

+

{rangContent}

-
- {min} +
+ {min} = ({ onChange={handleChange} disabled={disabled} defaultValue={[min, max]} - className="rangeHandle" + className={styles.rangeHandle} maxDescription={null} minDescription={null} currentDescription={null} marks={marks} style={{ color: "gold" }} /> - {max} + {max}
diff --git a/src/pages/list/index.tsx b/src/pages/list/index.tsx index 14b6563..bc5f426 100644 --- a/src/pages/list/index.tsx +++ b/src/pages/list/index.tsx @@ -2,6 +2,8 @@ import ListItem from "../../components/ListItem"; import List from "../../components/List"; import Bubble from "../../components/Bubble/example"; import Range from "../../components/Range/example"; +import Menu from "../../components/Menu/example"; +import CityFilter from "../../components/CityFilter/example"; import "./index.scss"; import { useEffect } from "react"; import Taro from "@tarojs/taro"; @@ -160,6 +162,12 @@ const ListPage = () => {
)} + {/* 全城筛选 */} + + + {/* 菜单 */} + + {/* 范围选择 */} diff --git a/types/css-modules.d.ts b/types/css-modules.d.ts new file mode 100644 index 0000000..01d9fe6 --- /dev/null +++ b/types/css-modules.d.ts @@ -0,0 +1,9 @@ +declare module '*.module.scss' { + const classes: { [key: string]: string }; + export default classes; +} + +declare module '*.module.css' { + const classes: { [key: string]: string }; + export default classes; +}