带过期时间的积分系统前端实现

在前端实现中,我使用了以下技术栈和库:

  1. 主要框架和语言:
  • React 18
  • TypeScript
  • Tailwind CSS(用于样式)
  1. 图表和UI组件:
  • Recharts(用于统计图表)
  • shadcn/ui(一些基础UI组件)
  1. 工具库:
  • lodash (工具函数)
  • lucide-react (图标库)

让我们基于这些技术栈创建一个更清晰的项目结构:

// package.json
{
  "name": "integral-system-frontend",
  "version": "1.0.0",
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "recharts": "^2.12.0",
    "lucide-react": "^0.263.1",
    "lodash": "^4.17.21",
    "@radix-ui/react-alert-dialog": "^1.0.5",
    "class-variance-authority": "^0.7.0",
    "clsx": "^2.1.0",
    "tailwind-merge": "^2.2.0",
    "tailwindcss": "^3.4.0",
    "typescript": "^5.3.0"
  }
}

// src/
// ├── components/
// │   ├── ui/           # shadcn/ui components
// │   ├── layout/       # Layout components
// │   └── integral/     # Integral specific components
// ├── hooks/            # Custom hooks
// ├── services/         # API services
// ├── types/            # TypeScript types
// ├── utils/            # Utility functions
// └── pages/            # Page components

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["DOM", "DOM.Iterable", "ES2020"],
    "module": "ESNext",
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  darkMode: ["class"],
  content: [
    './pages/**/*.{ts,tsx}',
    './components/**/*.{ts,tsx}',
    './app/**/*.{ts,tsx}',
    './src/**/*.{ts,tsx}',
  ],
  theme: {
    container: {
      center: true,
      padding: "2rem",
      screens: {
        "2xl": "1400px",
      },
    },
    extend: {
      colors: {
        border: "hsl(var(--border))",
        input: "hsl(var(--input))",
        background: "hsl(var(--background))",
        foreground: "hsl(var(--foreground))",
        primary: {
          DEFAULT: "hsl(var(--primary))",
          foreground: "hsl(var(--primary-foreground))",
        },
        secondary: {
          DEFAULT: "hsl(var(--secondary))",
          foreground: "hsl(var(--secondary-foreground))",
        },
      },
    },
  },
  plugins: [],
}

// src/components/integral/IntegralCard.tsx
import React from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Coins } from 'lucide-react';

interface IntegralCardProps {
  total: number;
  nearestExpire?: {
    points: number;
    date: string;
  };
}

export const IntegralCard: React.FC<IntegralCardProps> = ({ total, nearestExpire }) => (
  <Card>
    <CardHeader className="flex flex-row items-center justify-between pb-2">
      <CardTitle className="text-sm font-medium">可用积分</CardTitle>
      <Coins className="h-4 w-4 text-muted-foreground" />
    </CardHeader>
    <CardContent>
      <div className="text-2xl font-bold">{total}</div>
      {nearestExpire && (
        <p className="text-xs text-muted-foreground">
          {nearestExpire.points} 积分将于 {nearestExpire.date} 过期
        </p>
      )}
    </CardContent>
  </Card>
);

// src/components/integral/IntegralHistory.tsx
import React from 'react';
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '@/components/ui/table';
import { IntegralDetail } from '@/types';
import { formatDate } from '@/utils/date';

interface IntegralHistoryProps {
  details: IntegralDetail[];
  loading?: boolean;
}

export const IntegralHistory: React.FC<IntegralHistoryProps> = ({
  details,
  loading = false,
}) => {
  if (loading) {
    return (
      <div className="w-full h-64 flex items-center justify-center">
        <span className="loading loading-spinner loading-lg" />
      </div>
    );
  }

  return (
    <div className="rounded-md border">
      <Table>
        <TableHeader>
          <TableRow>
            <TableHead>时间</TableHead>
            <TableHead>类型</TableHead>
            <TableHead className="text-right">积分变动</TableHead>
            <TableHead>过期时间</TableHead>
            <TableHead>说明</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {details.map((detail) => (
            <TableRow key={detail.id}>
              <TableCell>{formatDate(detail.createTime)}</TableCell>
              <TableCell>{getIntegralTypeName(detail.type)}</TableCell>
              <TableCell className={`text-right ${
                detail.value > 0 ? 'text-green-600' : 'text-red-600'
              }`}>
                {detail.value > 0 ? '+' : ''}{detail.value}
              </TableCell>
              <TableCell>{formatDate(detail.expireTime)}</TableCell>
              <TableCell>{detail.reason}</TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </div>
  );
};

// src/components/integral/IntegralUseDialog.tsx
import React, { useState } from 'react';
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { Input } from '@/components/ui/input';

interface IntegralUseDialogProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  onConfirm: (points: number) => void;
  maxPoints: number;
}

export const IntegralUseDialog: React.FC<IntegralUseDialogProps> = ({
  open,
  onOpenChange,
  onConfirm,
  maxPoints,
}) => {
  const [points, setPoints] = useState('');
  const [error, setError] = useState('');

  const handleConfirm = () => {
    const pointsNum = Number(points);
    if (!pointsNum || pointsNum <= 0) {
      setError('请输入有效的积分数');
      return;
    }
    if (pointsNum > maxPoints) {
      setError('积分不足');
      return;
    }
    onConfirm(pointsNum);
    setPoints('');
    setError('');
  };

  return (
    <AlertDialog open={open} onOpenChange={onOpenChange}>
      <AlertDialogContent>
        <AlertDialogHeader>
          <AlertDialogTitle>使用积分</AlertDialogTitle>
          <AlertDialogDescription>
            当前可用积分: {maxPoints}
          </AlertDialogDescription>
        </AlertDialogHeader>
        <div className="grid gap-4 py-4">
          <div className="grid gap-2">
            <Input
              type="number"
              value={points}
              onChange={(e) => {
                setPoints(e.target.value);
                setError('');
              }}
              placeholder="请输入使用积分数"
              min={1}
              max={maxPoints}
            />
            {error && (
              <p className="text-sm text-red-500">{error}</p>
            )}
          </div>
        </div>
        <AlertDialogFooter>
          <AlertDialogCancel>取消</AlertDialogCancel>
          <AlertDialogAction onClick={handleConfirm}>
            确认使用
          </AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  );
};

这些组件使用了 shadcn/ui 的基础组件,并结合了 Tailwind CSS 进行样式设计。主要特点:

  1. 使用了 TypeScript 进行类型检查
  2. 采用函数式组件和 React Hooks
  3. 使用 Tailwind CSS 进行样式管理
  4. 集成了 shadcn/ui 的组件库
  5. 使用 Lucide React 图标库
  6. 响应式设计

要使用这个项目,需要:

  1. 初始化项目:
npx create-next-app@latest my-integral-system --typescript --tailwind
  1. 安装依赖:
npm install @radix-ui/react-alert-dialog class-variance-authority clsx 
npm install tailwind-merge lucide-react recharts lodash
  1. 安装和配置 shadcn/ui:
npx shadcn-ui@latest init
  1. 安装需要的组件:
npx shadcn-ui@latest add card table alert-dialog input button

这样就可以开始使用这些组件了。整个项目采用了现代化的前端技术栈,具有良好的类型支持和组件复用性。需要注意的是使用 Tailwind CSS 时要遵循其预定义的类名,避免使用自定义值。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值