Browse Source

经销商加入数据库

luoyangwei 8 months ago
parent
commit
238ffe876a

+ 3 - 0
package.json

@@ -23,6 +23,7 @@
     "@radix-ui/react-toast": "^1.2.4",
     "@types/react-cookies": "^0.1.4",
     "ahooks": "^3.8.4",
+    "ali-oss": "^6.x",
     "class-variance-authority": "^0.7.1",
     "clsx": "^2.1.1",
     "date-fns": "^4.1.0",
@@ -35,6 +36,7 @@
     "next-nprogress-bar": "^2.4.3",
     "next-themes": "^0.4.4",
     "node-xlsx": "^0.24.0",
+    "proxy-agent": "^6.5.0",
     "react": "^19.0.0",
     "react-cookies": "^0.1.1",
     "react-day-picker": "8.10.1",
@@ -50,6 +52,7 @@
   },
   "devDependencies": {
     "@faker-js/faker": "^9.3.0",
+    "@types/ali-oss": "^6.16.11",
     "@types/jest": "^29.5.14",
     "@types/jsonwebtoken": "^9.0.7",
     "@types/node": "^20.17.10",

File diff suppressed because it is too large
+ 526 - 0
pnpm-lock.yaml


+ 2 - 2
src/app/(module)/LeftSlider.tsx

@@ -66,7 +66,7 @@ export default function LeftSlider() {
                     <>
                         {menuData.menu.map((item, index) => (
                             <button onClick={() => handleMenuChecked(item)} key={index}
-                                className={`${item.name === menuChecked.name ? "bg-hma text-white hover:bg-hma/90" : "hover:bg-gray-100"} size-[var(--slider-menu-item-size)] rounded-lg flex-center-center text-gray-700 transition-all`}>
+                                className={`${item.name === menuChecked.name ? "bg-primary text-white hover:bg-primary/90" : "hover:bg-gray-100"} size-[var(--slider-menu-item-size)] rounded-lg flex-center-center text-gray-700 transition-all`}>
                                 <Icon name={item.icon} size={24} />
                             </button>
                         ))}
@@ -195,7 +195,7 @@ function MenuItem({ item, active, onChange }: MenuItemProps) {
         <button data-id={item.link} onClick={() => handleMenuItemClick(item)}
             className={"w-full bg-slate-50 p-4 rounded-lg overflow-hidden flex items-center justify-between gap-4 text-left"}>
             <div
-                className={`size-12 flex-center-center  rounded-lg transition-all ${!active ? "bg-blue-50" : "bg-hma text-white"}`}>
+                className={`size-12 flex-center-center  rounded-lg transition-all ${!active ? "bg-blue-50" : "bg-primary text-white"}`}>
                 <Icon name={item.icon} size={22} />
             </div>
             <div className={"flex-1"}>

+ 5 - 2
src/app/(module)/sale/dealer/Avatar.tsx

@@ -1,11 +1,11 @@
 'use client'
 
+import { AspectRatio } from "@/components/ui/aspect-ratio";
 import { Button } from "@/components/ui/button";
-import { AspectRatio } from "@/components/ui/aspect-ratio"
 import Image from "next/image";
 import { ChangeEvent, useState } from "react";
 
-export function Avatar({ initSrc }: { initSrc?: string }) {
+export function Avatar({ initSrc, onChange }: { initSrc?: string, onChange?: (src: string) => void }) {
     const [src, setSrc] = useState<string>(initSrc || "");
 
     function handleInputAvatarChange(e: ChangeEvent<HTMLInputElement>) {
@@ -14,6 +14,9 @@ export function Avatar({ initSrc }: { initSrc?: string }) {
             const reader = new FileReader();
             reader.onload = (e) => {
                 setSrc(e.target?.result as string);
+                if (onChange) {
+                    onChange(e.target?.result as string);
+                }
             }
             reader.readAsDataURL(e.target.files[0]);
         }

+ 2 - 37
src/app/(module)/sale/dealer/DataTable.tsx

@@ -1,13 +1,7 @@
 'use client'
 
+import Pagination from "@/components/Pagination";
 import { Button } from "@/components/ui/button";
-import {
-    Pagination,
-    PaginationContent,
-    PaginationItem,
-    PaginationNext,
-    PaginationPrevious
-} from "@/components/ui/pagination";
 import dayjs from "dayjs";
 import { Filter, Settings } from "lucide-react";
 import { DealerAndCompanyCombination } from "./types";
@@ -50,9 +44,6 @@ export function DataTable(props: DataTableProps) {
                 </Button>
             </div>
             <table className="table-fixed table-layout">
-                {/*<caption className="caption-bottom">*/}
-                {/*    Table 3.1: Professional wrestlers and their signature moves.*/}
-                {/*</caption>*/}
                 <thead>
                     <tr>
                         <th align="left" className="px-6" style={{ width: 280 }}>经销商</th>
@@ -88,34 +79,8 @@ export function DataTable(props: DataTableProps) {
                 </tbody>
             </table>
 
-            {/* 数据分页 */}
-            <Pagination className={"w-full py-8"}>
-                <PaginationContent className={"w-full"}>
-                    <PaginationItem>
-                        <PaginationPrevious href={"#"} />
-                    </PaginationItem>
-                    <div className="flex-1 flex-center-center gap-2">
-{/* 
-                        {[...Array(totalPages)].keys().map((item, index) => (
-                            <PdaginationItem key={index}>
-                                <PaginationLink href="#">{item}</PaginationLink>
-                            </PdaginationItem>
-                        ))} */}
 
-                        {/* {totalPages} {props.count} */}
-                        {/* {pages.map((item, index) =>
-                            <PaginationItem key={index} className={"size-10 bg-slate-50 text-center rounded-lg"}>
-                                {item.type === PageType.INDEX
-                                    ? <PaginationLink href="#">1</PaginationLink>
-                                    : <PaginationEllipsis />}
-                            </PaginationItem>
-                        )} */}
-                    </div>
-                    <PaginationItem>
-                        <PaginationNext />
-                    </PaginationItem>
-                </PaginationContent>
-            </Pagination>
+            <Pagination size={10} count={100} />
         </>
     )
 }

+ 50 - 9
src/app/(module)/sale/dealer/FormInput.tsx

@@ -1,17 +1,21 @@
 'use client'
 
+import { DealerAddRequest } from "@/app/api/v1/(request)/dealerRequest"
+import { Response } from "@/app/api/v1/(response)/response"
+import CitySelector, { SelectorItem } from "@/components/CitySelector"
 import { Button } from "@/components/ui/button"
+import { Calendar } from "@/components/ui/calendar"
 import { Input } from "@/components/ui/input"
-import { Textarea } from "@/components/ui/textarea"
-import { Controller, useForm } from "react-hook-form"
-import { AnimatePresence, motion } from "motion/react"
 import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
-import { Calendar1Icon } from "lucide-react"
-import { Calendar } from "@/components/ui/calendar"
+import { Textarea } from "@/components/ui/textarea"
 import dayjs from "dayjs"
-import CitySelector, { SelectorItem } from "@/components/CitySelector"
+import { Calendar1Icon, Loader2 } from "lucide-react"
+import { AnimatePresence, motion } from "motion/react"
+import { useState } from "react"
+import { Controller, useForm } from "react-hook-form"
 
 interface FormInputPorps {
+    avatar?: string
 }
 
 export type { FormInputPorps }
@@ -32,12 +36,45 @@ interface FormType {
     company_avatar: string
 }
 
-
-export function FormInput() {
+export function FormInput(props: FormInputPorps) {
+    const [loading, setLoading] = useState<boolean>(false);
     const { register, control, handleSubmit, formState: { errors } } = useForm<FormType>({})
 
+    const addDealer = async (body: DealerAddRequest) => {
+        const response = await fetch("/api/v1/sale/dealer/add",
+            { method: "POST", body: JSON.stringify(body) })
+        if (!response.ok) {
+            throw new Error("添加经销商失败")
+        }
+        return await response.json() as Response<undefined>;
+    }
+
     function onSubmit(data: FormType) {
         console.log(data)
+        setLoading(true);
+        addDealer({
+            company_name: data.company_name,
+            company_avatar: props.avatar || "",
+            dealer_link_name: data.dealer_link_name,
+            dealer_link_number: data.dealer_link_number,
+            dealer_address: data.dealer_address,
+            cooperated_date_start: dayjs(data.cooperated_date_start).format("YYYY-MM-DD"),
+            cooperated_date_end: dayjs(data.cooperated_date_end).format("YYYY-MM-DD"),
+            province: data.area.provinceCode,
+            city: data.area.cityCode,
+            remark: data.remark
+        })
+            .then(response => {
+                if (response.code === 0) {
+                    handleSubmitSuccessful()
+                }
+            })
+            .catch(console.error)
+            .finally(() => setLoading(false))
+    }
+
+    function handleSubmitSuccessful() {
+        console.log("添加成功")
     }
 
     return (
@@ -249,6 +286,7 @@ export function FormInput() {
                     <div className="flex items-center gap-4 py-2 col-span-2">
                         <label className="flex w-[100px] items-start justify-end shrink">详细地址:</label>
                         <Textarea
+                            {...register("dealer_address", { required: "详细地址必填" })}
                             className={`flex-1 ${errors.dealer_address && "border-red-500 text-red-500 focus-visible:ring-red-500"}`}
                             placeholder="请输入详细地址" />
                     </div>
@@ -290,7 +328,10 @@ export function FormInput() {
                 </div>
 
                 <div className="col-span-2 text-right py-4">
-                    <Button type="submit">添加</Button>
+                    <Button type="submit">
+                        {loading && <Loader2 className="animate-spin" />}
+                        添加
+                    </Button>
                 </div>
             </div>
         </form >

+ 23 - 0
src/app/(module)/sale/dealer/Info.tsx

@@ -0,0 +1,23 @@
+'use client'
+
+import { useState } from "react"
+import { Avatar } from "./Avatar"
+import { FormInput } from "./FormInput"
+
+export default function Info() {
+    const [avatar, setAvatar] = useState<string>()
+    return (
+        <>
+            <div className="grid grid-cols-12">
+                <div className="col-span-3">
+                    <div className="w-full flex flex-col items-center gap-4 py-10">
+                        <Avatar onChange={setAvatar} />
+                    </div>
+                </div>
+                <div className="col-span-9 py-8">
+                    <FormInput avatar={avatar} />
+                </div>
+            </div>
+        </>
+    )
+}

+ 2 - 46
src/app/(module)/sale/dealer/add/page.tsx

@@ -1,7 +1,6 @@
 import PageTitle from "@/components/PageTitle";
 import { HousePlus, SquareChartGantt } from "lucide-react";
-import { Avatar } from "../Avatar";
-import { FormInput } from "../FormInput";
+import Info from "../Info";
 
 export default function AddDealer() {
     return (
@@ -16,50 +15,7 @@ export default function AddDealer() {
                         <h2 className="text-lg font-bold text-gray-800">个人基本信息</h2>
                     </header>
 
-                    <div className="grid grid-cols-12">
-                        <div className="col-span-3">
-                            <div className="w-full flex flex-col items-center gap-4 py-10">
-                                <Avatar />
-                            </div>
-                        </div>
-                        <div className="col-span-9 py-8">
-                            {/* <div className="grid grid-cols-2 px-4"> */}
-                            <FormInput />
-                            {/* <div>
-                                    <FormItem label="单位名称:" >
-                                        <Input placeholder="请输入单位名称" />
-                                    </FormItem>
-                                    <FormItem label="所在地:" >
-                                        <Input placeholder="请输入单位名称" />
-                                    </FormItem>
-                                    <FormItem label="合作开始:" >
-                                        <Input placeholder="请输入单位名称" />
-                                    </FormItem>
-                                </div>
-                                <div>
-                                    <FormItem label="联系人:" >
-                                        <Input placeholder="请输入单位名称" />
-                                    </FormItem>
-                                    <FormItem label="联系电话:" >
-                                        <Input placeholder="请输入单位名称" />
-                                    </FormItem>
-                                    <FormItem label="合作到期:" >
-                                        <Input placeholder="请输入单位名称" />
-                                    </FormItem>
-                                </div>
-                                <div className="col-span-2">
-                                    <FormItem label="详细地址:" >
-                                        <Input placeholder="请输入单位名称" className="min-h-14" />
-                                    </FormItem>
-                                </div>
-                                <div className="col-span-2">
-                                    <FormItem label="备注:" >
-                                        <Input placeholder="请输入单位名称" className="min-h-14" />
-                                    </FormItem>
-                                </div> */}
-                            {/* </div> */}
-                        </div>
-                    </div>
+                    <Info />
                 </div>
 
                 {/* 名下订阅的设备列表 */}

+ 15 - 0
src/app/api/v1/(request)/dealerRequest.ts

@@ -0,0 +1,15 @@
+interface DealerAddRequest {
+    company_name: string
+    company_avatar: string
+
+    dealer_link_name: string;
+    dealer_link_number: string;
+    cooperated_date_start: string;
+    cooperated_date_end: string;
+    province: string;
+    city: string;
+    dealer_address: string;
+    remark: string;
+}
+
+export type { DealerAddRequest };

+ 8 - 0
src/app/api/v1/sale/dealer/[id]/route.ts

@@ -0,0 +1,8 @@
+import { NextRequest } from "next/server";
+
+/**
+ * 详情查询
+ */
+export async function GET(request: NextRequest) {
+
+}

+ 88 - 0
src/app/api/v1/sale/dealer/add/route.ts

@@ -0,0 +1,88 @@
+import crypto from "crypto";
+
+import { DatabasePool } from "@/lib/mysql";
+import { oss } from "@/lib/oss";
+import { ResultSetHeader } from "mysql2/promise";
+import { NextRequest, NextResponse } from "next/server";
+import { DealerAddRequest } from "../../../(request)/dealerRequest";
+import { response } from "../../../(response)/response";
+
+/**
+ * 添加经销商
+ */
+export async function POST(request: NextRequest) {
+    const requestBody = await request.json() as DealerAddRequest;
+    console.log("🚀 ~ POST ~ requestBody:", requestBody)
+
+    // 保存经销商信息
+    const connection = await DatabasePool.getConnection();
+    await connection.beginTransaction();
+
+    try {
+
+        // 如果有头像,就上传头像到 OSS
+        if (requestBody.company_avatar) {
+            const buf = Buffer.from(requestBody.company_avatar, 'base64')
+            const ossResult = await oss.put(`company/${Date.now()}.png`, buf)
+            console.log("🚀 ~ POST ~ ossResult:", ossResult)
+            requestBody.company_avatar = ossResult.url
+        }
+
+        // 新建经销商公司信息
+        const [companyResult] = await connection.execute<ResultSetHeader>(
+            `insert into tb_admin_company (company_name, company_avatar) VALUES (?, ?)`,
+            [requestBody.company_name, requestBody.company_avatar]);
+        const companyId = companyResult.insertId
+        console.log("🚀 ~ POST ~ companyId:", companyId)
+
+        // 新建经销商登录信息
+        const defaultLoginName = requestBody.dealer_link_number;
+        // 默认密码进行 MD5 加密
+        const hash = crypto.createHash("md5");
+        hash.update("tecanswer");
+        const defaultPassword = hash.digest("hex").toUpperCase();
+
+        const [userResult] = await connection.execute<ResultSetHeader>(
+            `insert into tb_admin_user (login_name, password, real_name, avatar, company_id, status) VALUES (?, ?, ?, ?, ?, 1)`,
+            [defaultLoginName, defaultPassword, requestBody.dealer_link_name, requestBody.company_avatar, companyId]);
+        const userId = userResult.insertId
+        console.log("🚀 ~ POST ~ userId:", userId)
+
+        // 新建经销商信息
+        console.log("Params", userId, companyId,
+            requestBody.dealer_link_name,
+            requestBody.dealer_link_number,
+            requestBody.dealer_address,
+            requestBody.cooperated_date_start,
+            requestBody.cooperated_date_end,
+            requestBody.province,
+            requestBody.city,
+            requestBody.remark);
+        const [dealerResult] = await connection.execute<ResultSetHeader>(
+            `insert into tb_admin_dealer (user_id, company_id, dealer_link_name, dealer_link_number, dealer_address,
+                                        cooperated_date_start, cooperated_date_end, province, city, remark)
+             values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
+            userId, companyId,
+            requestBody.dealer_link_name,
+            requestBody.dealer_link_number,
+            requestBody.dealer_address,
+            requestBody.cooperated_date_start,
+            requestBody.cooperated_date_end,
+            requestBody.province,
+            requestBody.city,
+            requestBody.remark
+        ])
+        const dealerId = dealerResult.insertId
+        console.log("🚀 ~ POST ~ dealerId:", dealerId)
+
+    } catch (error) {
+        await connection.rollback();
+        console.error(error);
+    }
+
+
+    connection.commit();
+    connection.release();
+
+    return NextResponse.json(response.success());
+}

+ 10 - 0
src/app/api/v1/sale/dealer/route.ts

@@ -1,6 +1,16 @@
 import { NextRequest, NextResponse } from "next/server";
 import { response } from "../../(response)/response";
 
+/**
+ * 列表总数统计
+ */
 export async function GET(request: NextRequest) {
     return NextResponse.json(response.success("Hello World"))
 }
+
+/**
+ * 经销商列表
+ */
+export async function POST(request: NextRequest) {
+
+}

+ 0 - 0
src/app/api/v1/sale/route.ts


+ 1 - 0
src/app/api/v1/user/sign-in/route.ts

@@ -28,6 +28,7 @@ export async function POST(request: NextRequest) {
 
     const secretKey = process.env.SECRET_KEY;
     if (!secretKey) {
+        connection.release();
         throw new Error("Secret key is not set");
     }
 

+ 2 - 2
src/components/PageTitle.tsx

@@ -1,5 +1,5 @@
-import { ReactNode } from "react";
 import { Search } from "lucide-react";
+import { ReactNode } from "react";
 
 interface PageTitleProps {
     className?: string,
@@ -12,7 +12,7 @@ export default function PageTitle(props: PageTitleProps) {
     return (
         <header className={"flex-center-between pt-9 pb-4 px-6 sticky top-0 z-50 bg-gray-100/80 backdrop-blur-lg"}>
             <div className={"flex-center-center gap-6"}>
-                <div className={"size-9 rounded-lg bg-hma text-white flex-center-center"}>
+                <div className={"size-9 rounded-lg bg-primary text-white flex-center-center"}>
                     {props.icon}
                 </div>
                 <h2 className={"text-xl font-bold"}>{props.title}</h2>

+ 2 - 1
src/components/pagination.css

@@ -5,6 +5,7 @@
     justify-content: center;
     align-items: center;
     gap: 1rem;
+    @apply text-sm;
 }
 
 .paginate>li:first-child {
@@ -52,5 +53,5 @@
     min-height: 40px;
     color: #333;
 
-    @apply bg-muted transition-all;
+    @apply bg-muted;
 }

+ 11 - 0
src/lib/oss.ts

@@ -0,0 +1,11 @@
+import OSS from "ali-oss";
+
+export const oss = new OSS({
+    // Specify the region in which the bucket is located. For example, if the bucket is located in the China (Hangzhou) region, set the region to oss-cn-hangzhou. 
+    region: 'oss-cn-shanghai',
+    // Obtain access credentials from environment variables. Before you run the sample code, make sure that you have configured environment variables OSS_ACCESS_KEY_ID and OSS_ACCESS_KEY_SECRET. 
+    accessKeyId: process.env.OSS_ACCESS_KEY_ID || "",
+    accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET || "",
+    // Specify the name of the bucket. Example: examplebucket. 
+    bucket: 'wa04-files',  // 临时使用 wa04 的存储空间
+})

Some files were not shown because too many files changed in this diff