
> 由于开发项目页面多且复杂,所以选择使用到 iconify 图标库。使用到的是 nextjs,由于 iconify 在ssr中的一些缺陷,只能通过 json 去将图标 ssr 化,导致搭配 vscode 插件 Iconify IntelliSense 预览图标和书写体验不佳,所以进行封装改善开发体验。 ## 使用: ```typescript <AxIcon icon="line-md:close-circle-twotone" className="mr-2" size={48} /> ``` ## 完整代码 ```typescript "use client"; import { cn } from "@/lib/utils"; import { icons as entypoIcons, IconifyJSON } from "@iconify-json/entypo"; import { icons as lineMDIcons } from "@iconify-json/line-md"; import { icons as phIcons } from "@iconify-json/ph"; import { icons as tablerIcons } from "@iconify-json/tabler"; import { replaceIDs } from "@iconify/react"; import { getIconData, iconToHTML, iconToSVG } from "@iconify/utils"; interface AxIconProps { icon: string; size?: string | number; className?: string; } export default function AxIcon({ icon, size = 16, className }: AxIconProps) { const [brand, iconName] = icon.split(":"); const brandIcons: Record<string, IconifyJSON> = { tabler: tablerIcons, entypo: entypoIcons, ph: phIcons, "line-md": lineMDIcons }; if (!brandIcons[brand]) return; const iconData = getIconData(brandIcons[brand], iconName); if (!iconData) return; const renderData = iconToSVG(iconData, { height: size, width: size }); const svgStr = iconToHTML(replaceIDs(renderData.body), { ...renderData.attributes, class: cn("!w-auto !h-auto", className) }); return <div dangerouslySetInnerHTML={{ __html: svgStr }} />; } ```



[https://spring-slider.uiinitiative.com/](https://spring-slider.uiinitiative.com/)


最近由于在 nextui 和 shadcn/ui 做切换,遇到些问题,由于 shadcn/ui 灵活性高,导致没有 pagination 动态配置功能,所以基于以下链接封装了pagination组件 AwsomePagination。 [bookmark](https://medium.com/@enayetflweb/implementing-pagination-in-shadcn-ui-a-complete-guide-b7539e34908a) ## 首先 以上链接的 `buildLink` 是用的query的自适应,而对于的的需求都是用 params的形式去设计,所以调整为新的函数 ```typescript const buildLink = useCallback((newPage: number) => { return `${parentPath}/${newPage}`; }, []); ``` 我的component基本运用于服务端组件,所以不需要任何依赖,_parentPath则为 分页页面的父路由。_ ## 然后 renderPageNumbers也做了一些调整 ```typescript const renderPageNumbers = useCallback(() => { const items: ReactNode[] = []; if (totalPageCount <= maxVisiblePages) { for (let i = 1; i <= totalPageCount; i++) { items.push( <PaginationItem key={i}> <PaginationLink href={buildLink(i)} isActive={page === i}> {i} </PaginationLink> </PaginationItem> ); } } else { items.push( <PaginationItem key={1}> <PaginationLink href={buildLink(1)} isActive={page === 1}> 1 </PaginationLink> </PaginationItem> ); if (page > 3) { items.push( <PaginationItem key="ellipsis-start"> <PaginationLink href={buildLink(page - 2)}> <NavigationDots type="left" /> </PaginationLink> </PaginationItem> ); } const start = Math.max(2, page - 1); const end = Math.min(totalPageCount - 1, page + 1); for (let i = start; i <= end; i++) { items.push( <PaginationItem key={i}> <PaginationLink href={buildLink(i)} isActive={page === i}> {i} </PaginationLink> </PaginationItem> ); } if (page < totalPageCount - 2) { items.push( <PaginationItem key="ellipsis-end"> <PaginationLink href={buildLink(page + 2)}> <NavigationDots type="right" /> </PaginationLink> </PaginationItem> ); } items.push( <PaginationItem key={totalPageCount}> <PaginationLink href={buildLink(totalPageCount)} isActive={page === totalPageCount}> {totalPageCount} </PaginationLink> </PaginationItem> ); } return items; }, []); ``` 这里原文章用 pagination的 dots组件展示样式,并没有jump功能,这里实现了 hover 变 jump 功能。指定 buildLink 去 实现对应跳转 ```typescript export function NavigationDots({ type }: { type: "left" | "right" }) { const wrapperRef = useRef<HTMLDivElement>(null); const isHovered = useHover(wrapperRef); const hoverDom = useMemo(() => { if (type === "left") { return <Icon icon={"tabler:arrow-badge-left-filled"} />; } return <Icon icon={"tabler:arrow-badge-right-filled"} />; }, [type]); return ( <div className="cursor-pointer" ref={wrapperRef}> {isHovered ? hoverDom : <Icon icon={"tabler:dots"} />} </div> ); } ``` 这里用到的是 iconify 和 ahooks 去实现 ,hover 图标切换,点击 jump 对应页面 也就是这样用: ```typescript <PaginationItem key="ellipsis-end"> <PaginationLink href={buildLink(page + 2)}> <NavigationDots type="right" /> </PaginationLink> </PaginationItem> ``` ## 最后 使用: ```typescript <AwsomePagination parentPath="/home" pageSize={6} page={1} totalCount={total} /> ``` 源码: ```typescript "use client"; import { Icon } from "@iconify/react/dist/iconify.js"; import { useHover } from "ahooks"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { type ReactNode, useCallback, useMemo, useRef } from "react"; import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from "../ui/pagination"; export interface AwsomePaginationProps { totalCount: number; pageSize: number; page: number; maxVisiblePages?: number; parentPath: string; } export function NavigationDots({ type }: { type: "left" | "right" }) { const wrapperRef = useRef<HTMLDivElement>(null); const isHovered = useHover(wrapperRef); const hoverDom = useMemo(() => { if (type === "left") { return <Icon icon={"tabler:arrow-badge-left-filled"} />; } return <Icon icon={"tabler:arrow-badge-right-filled"} />; }, [type]); return ( <div className="cursor-pointer" ref={wrapperRef}> {isHovered ? hoverDom : <Icon icon={"tabler:dots"} />} </div> ); } export function AwsomePagination({ pageSize, totalCount, page, maxVisiblePages = 5, parentPath }: AwsomePaginationProps) { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); const totalPageCount = Math.ceil(totalCount / pageSize); const buildLink = useCallback((newPage: number) => { return `${parentPath}/${newPage}`; }, []); const renderPageNumbers = useCallback(() => { const items: ReactNode[] = []; if (totalPageCount <= maxVisiblePages) { for (let i = 1; i <= totalPageCount; i++) { items.push( <PaginationItem key={i}> <PaginationLink href={buildLink(i)} isActive={page === i}> {i} </PaginationLink> </PaginationItem> ); } } else { items.push( <PaginationItem key={1}> <PaginationLink href={buildLink(1)} isActive={page === 1}> 1 </PaginationLink> </PaginationItem> ); if (page > 3) { items.push( <PaginationItem key="ellipsis-start"> <PaginationLink href={buildLink(page - 2)}> <NavigationDots type="left" /> </PaginationLink> </PaginationItem> ); } const start = Math.max(2, page - 1); const end = Math.min(totalPageCount - 1, page + 1); for (let i = start; i <= end; i++) { items.push( <PaginationItem key={i}> <PaginationLink href={buildLink(i)} isActive={page === i}> {i} </PaginationLink> </PaginationItem> ); } if (page < totalPageCount - 2) { items.push( <PaginationItem key="ellipsis-end"> <PaginationLink href={buildLink(page + 2)}> <NavigationDots type="right" /> </PaginationLink> </PaginationItem> ); } items.push( <PaginationItem key={totalPageCount}> <PaginationLink href={buildLink(totalPageCount)} isActive={page === totalPageCount}> {totalPageCount} </PaginationLink> </PaginationItem> ); } return items; }, []); return ( <div className="flex flex-col md:flex-row h-9 overflow-y-hidden items-center gap-3 w-full"> <Pagination> <PaginationContent className="max-sm:gap-0"> <PaginationItem> <PaginationPrevious href={buildLink(Math.max(page - 1, 1))} aria-disabled={page === 1} tabIndex={page === 1 ? -1 : undefined} className={page === 1 ? "pointer-events-none opacity-50" : undefined} /> </PaginationItem> {renderPageNumbers()} <PaginationItem> <PaginationNext href={buildLink(Math.min(page + 1, totalPageCount))} aria-disabled={page === totalPageCount} tabIndex={page === totalPageCount ? -1 : undefined} className={page === totalPageCount ? "pointer-events-none opacity-50" : undefined} /> </PaginationItem> </PaginationContent> </Pagination> </div> ); } ``` css ```css .body{ color: #F00; } ``` [bookmark](https://www.a1ex.tech/zh/post/15ed250f8d4d806cb37ed4af14ff0b57)


> 最近在写国际化适配阿拉伯语时,发现在跑翻译的时候,有一些json解析翻译出来有些引号被替换,导致JSON.parse 报错,其中几个页面会直接error但是并没有发现,所以打算写一个测试,测试所有页面是否加载正常。 ### 组件单元测试(vitest) 1. 准备工作 1. vitest.config.mts ```typescript import react from "@vitejs/plugin-react"; import tsconfigPaths from "vite-tsconfig-paths"; import { defineConfig } from "vitest/config"; export default defineConfig({ plugins: [tsconfigPaths(), react()], test: { globals: true, environment: "jsdom", setupFiles: ["./vitest.setup.ts"], include: ["**/*.test.{js,jsx,ts,tsx}"], exclude: ["**/node_modules/**", "**/dist/**"] } }); ``` b. vitest.setup.ts ```typescript import { cleanup } from "@testing-library/react"; import { afterEach, vi } from "vitest"; afterEach(() => { cleanup(); vi.clearAllMocks(); }); ``` 2. 基本使用 1. 判断 渲染组件后关键DOM是否存在 ```typescript import Footer from "@/components/Footer"; import { render, screen } from "@testing-library/react"; import { expect, test } from "vitest"; test("test", () => { render(<Footer />); expect(screen.getByText("[赣ICP备2022002397号]")).toBeDefined(); }); ``` ### e2e测试(playwright) 1. 准备工作 1. playwright.config.ts ```typescript import { defineConfig, devices } from "@playwright/test"; import path from "path"; // Use process.env.PORT by default and fallback to port 3000 const PORT = process.env.PORT || 3000; // Set webServer.url and use.baseURL with the location of the WebServer respecting the correct set port const baseURL = `http://localhost:${PORT}`; // Reference: https://playwright.dev/docs/test-configuration export default defineConfig({ // Timeout per test timeout: 30 * 1000, // Test directory testDir: path.join(__dirname, "e2e"), // If a test fails, retry it additional 2 times retries: 0, // Artifacts folder where screenshots, videos, and traces are stored. outputDir: "test-results/", // Run your local dev server before starting the tests: // https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests webServer: { command: "pnpm run dev", url: baseURL, timeout: 120 * 1000, reuseExistingServer: !process.env.CI }, use: { // Use baseURL so to make navigations relative. // More information: https://playwright.dev/docs/api/class-testoptions#test-options-base-url baseURL, // Retry a test if its failing with enabled tracing. This allows you to analyze the DOM, console logs, network traffic etc. // More information: https://playwright.dev/docs/trace-viewer trace: "retry-with-trace" // All available context options: https://playwright.dev/docs/api/class-browser#browser-new-context // contextOptions: { // ignoreHTTPSErrors: true, // }, }, projects: [ { name: "Desktop Chrome", use: { ...devices["Desktop Chrome"] } } // { // name: 'Desktop Firefox', // use: { // ...devices['Desktop Firefox'], // }, // }, // { // name: 'Desktop Safari', // use: { // ...devices['Desktop Safari'], // }, // }, // Test against mobile viewports. // { // name: "Mobile Chrome", // use: { // ...devices["Pixel 5"] // } // }, // { // name: "Mobile Safari", // use: devices["iPhone 12"] // } ] }); ``` 1. 基本使用 1. 加载所有页面是否正常 svg 过渡变换 并且包含预览 codebox ```typescript import { RouteInfo, scanRoutes } from "@/scripts/scanRoutes"; import { expect, test } from "@playwright/test"; import { filter } from "lodash"; const routes = scanRoutes(); const isStaticRoute = filter(routes, (r: RouteInfo) => !r.isDynamic); const generatePageTestByRoute = (route: RouteInfo) => { test.describe(`<${route.path}> 加载测试`, () => { test(`<${route.path}> 加载成功`, async ({ page }) => { // 检查页面响应状态码 const response = await page.goto(route.path); expect(response?.status()).toBe(200); }); if (route.path.includes("svgTransform")) { test(`<${route.path}> svg 过渡变换 并且包含预览 codebox`, async ({ page }) => { const response = await page.goto(route.path); expect(response?.status()).toBe(200); const htmlContent = await page.content(); const bol = htmlContent.includes("IconTransformProps"); expect(bol).toBe(true); }); } }); }; isStaticRoute.forEach(generatePageTestByRoute); ```


### **微信支付实现(nuxt)** 1. 获取token ```text async asyncData({ $axios, query, n2token }) { // 1. 从query中拿到code const { code } = query // const code = '081ozf300lj2aR1rCI000lRQOc3ozf33' const useRes = await toAuthentication({ code, state: 'sharp' }) let token if (useRes.data.success) { token = useRes.data.data.token } else { token = '' } // 4. 通过token拿到用户套餐 const configRes = await getVipConfig({}) // 支付时用product_id 调用 makeOrder 拿到支付需要的参数并调起微信支付 return { token, payList: configRes.data.data, currType: configRes.data.data[0] } }, ``` 1. 点击支付 ```text async makeOrder() { // 支付时用product_id 调用 makeOrder 拿到支付需要的参数并调起微信支付 const res = await makeMiniH5Order( { product_id: this.currType.params.productId, trade_type: this.isIOS ? 'ios' : 'android' }, { headers: { Authorization: this.token } } ) const openData = res.data.data.open_data this.wxJSPay( { ...openData, appId: res.data.data.weichatpay.appid }, () => { this.$toast('支付成功, 快去小程序体验会员功能吧!~') }, () => { this.$toast('支付失败') } ) }, // 微信环境 wxJSPay(configObj, successCb, failCb) { function onBridgeReady() { WeixinJSBridge.invoke( 'getBrandWCPayRequest', { ...configObj }, function(res) { if (res.err_msg == 'get_brand_wcpay_request:ok') { // 使用以上方式判断前端返回,微信团队郑重提示: // res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。 successCb && successCb() } else { failCb && failCb() } } ) } if (typeof WeixinJSBridge === 'undefined') { if (document.addEventListener) { document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false) } else if (document.attachEvent) { document.attachEvent('WeixinJSBridgeReady', onBridgeReady) document.attachEvent('onWeixinJSBridgeReady', onBridgeReady) } } else { onBridgeReady() } } ```
