diff --git a/.dockerignore b/.dockerignore index 977f31f..f27debb 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,8 +1,45 @@ -# This file specifies which files and directories should be ignored by Docker when building images. +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. -.gitignore -.env* -*.log +# Dependencies node_modules -eslint.config.mjs -Dockerfile +.pnp +.pnp.js + +# Local env files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Testing +coverage + +# Turbo +.turbo + +# Vercel +.vercel + +# Build Outputs +.next +out +build +dist + + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Misc +.DS_Store +*.pem + +# IDEs and editors +.vscode +.idea + +# Git +.git diff --git a/.env.example b/.env.example index 67b9906..350344c 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,18 @@ -# DATABASE_URL用于配置Prisma与数据库连接,详情请参考https://www.prisma.io/docs/orm/reference/connection-urls -# APP_KEY用于配置应用的加密密钥,请使用“openssl rand -base64 64 | tr -d '\n'”生成随机密钥 -# EXPRESS_KEY和EXPRESS_SECRET用于配置快递100的API密钥和密钥,详情请参考https://api.kuaidi100.com/manager/v2/myinfo/enterprise - -DATABASE_URL= APP_KEY= +DATABASE_URL= +PUBLIC_APP_URL= + +EMAIL_HOST= +EMAIL_PORT= +EMAIL_USER= +EMAIL_PASSWORD= +EMAIL_FROM= + EXPRESS_KEY= EXPRESS_SECRET= +EXPRESS_CALLBACK= + +NAPCAT_URL= +NAPCAT_TOKEN= + +TRACKING_KEY= diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a55efd6..1779232 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,22 +2,15 @@ name: Build Check on: pull_request: - branches: [ master ] # 替换为你的保护分支名 + branches: [master] jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - name: Checkout code + uses: actions/checkout@v3 - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' # 使用你的Node.js版本 - - - name: Install dependencies - run: npm ci - - - name: Run build - run: npm run build \ No newline at end of file + - name: Docker build test + run: docker build -t myapp:latest . diff --git a/.gitignore b/.gitignore index 0938139..e781499 100644 --- a/.gitignore +++ b/.gitignore @@ -1,44 +1,42 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. -# dependencies -/node_modules -/.pnp -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/versions - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* +# Dependencies +node_modules +.pnp +.pnp.js -# env files (can opt-in for committing if needed) +# Local env files .env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Testing +coverage + +# Turbo +.turbo -# vercel +# Vercel .vercel -# typescript -*.tsbuildinfo -next-env.d.ts +# Build Outputs +.next +out +build +dist + + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Misc +.DS_Store +*.pem +# IDEs and editors +.vscode .idea -prisma/migrations diff --git a/Dockerfile b/Dockerfile index 7306e79..087f26f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,51 +1,35 @@ -# syntax=docker.io/docker/dockerfile:1 - FROM node:alpine AS base -# Install dependencies only when needed -FROM base AS deps -# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. -RUN apk add --no-cache libc6-compat +FROM base AS turbo WORKDIR /app +RUN apk add --no-cache libc6-compat +RUN npm install turbo +COPY . . +RUN npx turbo prune user admin --docker -# Install dependencies based on the preferred package manager -COPY package.json package-lock.json* .npmrc* ./ -RUN npm ci - - -# Rebuild the source code only when needed FROM base AS builder WORKDIR /app -COPY --from=deps /app/node_modules ./node_modules -COPY . . - -# Next.js collects completely anonymous telemetry data about general usage. -# Learn more here: https://nextjs.org/telemetry -# Uncomment the following line in case you want to disable telemetry during the build. -# ENV NEXT_TELEMETRY_DISABLED=1 - -RUN npx prisma generate && npm run build +RUN apk add --no-cache libc6-compat +COPY --from=turbo /app/out/json/ . +RUN npm install +COPY --from=turbo /app/out/full/ . +RUN npx turbo run build -# Production image, copy all the files and run next FROM base AS runner WORKDIR /app - -ENV NODE_ENV=production -# Uncomment the following line in case you want to disable telemetry during runtime. -# ENV NEXT_TELEMETRY_DISABLED=1 - -COPY --from=builder /app/public ./public - -# Automatically leverage output traces to reduce image size -# https://nextjs.org/docs/advanced-features/output-file-tracing -COPY --from=builder /app/.next/standalone ./ -COPY --from=builder /app/.next/static ./.next/static +COPY --from=builder /app/apps/user/.next/standalone/node_modules ./node_modules +COPY --from=builder /app/apps/user/.next/standalone/apps/user ./user +COPY --from=builder /app/apps/user/.next/static ./user/.next/static +COPY --from=builder /app/apps/user/public ./user/public +COPY --from=builder /app/apps/admin/.next/standalone/node_modules ./node_modules +COPY --from=builder /app/apps/admin/.next/standalone/apps/admin ./admin +COPY --from=builder /app/apps/admin/.next/static ./admin/.next/static +COPY --from=builder /app/apps/admin/public ./admin/public +COPY --chmod=755 docker-entrypoint.sh / EXPOSE 3000 - +ENV NODE_ENV=production +ENV HOSTNAME=0.0.0.0 ENV PORT=3000 -# server.js is created by next build from the standalone output -# https://nextjs.org/docs/pages/api-reference/config/next-config-js/output -ENV HOSTNAME="0.0.0.0" -CMD ["node", "server.js"] +ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/README.md b/README.md index b8aea2d..f27292c 100644 --- a/README.md +++ b/README.md @@ -14,67 +14,59 @@ 后端使用TRPC,数据库框架使用Prisma。 -使用精臣标签机API打印标签,快递100 API完成C端寄件。 +使用精臣标签机API打印标签,快递100 API完成C端寄件,17track API完成物流跟踪。 + +目前正在集成Napcat来与群机器人联动。 ## 使用 > 注意:本项目仍在开发中,功能不完善,可能存在bug,仅供学习和参考使用,**不建议用于生产环境**。 -> +> > 如果您对本项目感兴趣,欢迎参与开发和完善,提交issue或PR。 现在已经支持Docker部署,使用Docker可以更方便地进行部署。 ```bash -docker run -d \ - --name nextmyorder \ - -p 3000:3000 \ - -e DATABASE_URL="mysql://user:password@host:port/database" \ - -e API_KEY="your_fast_express_api_key" \ - -e EXPRESS_KEY="your_fast_express_api_secret" \ - -e EXPRESS_SECRET="https://api.fast-express.com" \ - 250king/nextmyorder:beta +docker run -d --name nextmyorder -p 3000:3000 [-e] 250king/nextmyorder:beta [user/admin] +#其中 -e 参数表示使用环境变量配置。user/admin表示指定运行App ``` ### 环境变量说明 -- `DATABASE_URL`: 用于配置Prisma与数据库连接,详情请参考[Prisma文档](https://www.prisma.io/docs/orm/reference/connection-urls)。 -- `API_KEY`: 应用加密密钥,请使用`openssl rand -base64 64 | tr -d '\n'`生成随机密钥 -- `EXPRESS_KEY` `EXPRESS_SECRET`: 快递100 API密钥,请在[快递100官网](https://api.kuaidi100.com/manager/v2/myinfo/enterprise)申请。 + +| 环境变量 | 说明 | +|----------------|--------------------------------------------------------------------------------------------| +| DATABASE_URL | 用于配置Prisma与数据库连接,详情请参考[Prisma文档](https://www.prisma.io/docs/orm/reference/connection-urls) | +| APP_KEY | 应用加密密钥,请使用`openssl rand -base64 64 \| tr -d '\n'`生成随机密钥 | +| PUBLIC_APP_URL | 客户端服务器地址 | +| EMAIL_HOST | 邮件服务器地址 | +| EMAIL_PORT | 邮件服务器端口 | +| EMAIL_USER | 邮件服务器用户名 | +| EMAIL_PASS | 邮件服务器密码 | +| EMAIL_FROM | 邮件发送名称 | +| EXPRESS_KEY | 快递100 API密钥,请在[快递100官网](https://api.kuaidi100.com/manager/v2/myinfo/enterprise)申请。 | +| EXPRESS_SECRET | 快递100 API密钥,请在[快递100官网](https://api.kuaidi100.com/manager/v2/myinfo/enterprise)申请。 | +| CALLBACK_URL | 快递100 API回调地址,需配置为您的服务器地址。 | +| NAPCAT_URL | Napcat API地址 | +| NAPCAT_TOKEN | Napcat API访问令牌 | +| TRACKING_TOKEN | 物流跟踪 API访问令牌 | ### 访问 + 访问`http://localhost:3000`即可访问系统。请注意,首次访问请在设置页面完成应用配置。 基本使用按照正常团购流程进行,创建团购、添加商品、添加订单等。具体教程正在编写中,敬请期待! -## 开发环境搭建 - -1. 克隆仓库 - ```bash - git clone https://github.com/250king/NextMyOrder.git - cd NextMyOrder - ``` -2. 安装依赖 - ```bash - npm install - ``` -3. 配置环境变量。复制`.env.example`为`.env`并根据上面的环境变量说明进行修改 -4. 初始化数据库 - ```bash - npx prisma migrate dev --name init - ``` -5. 启动项目 - ```bash - npm start - ``` - ## ToDo + - [ ] 完善文档 - [ ] 部署支付系统 -- [ ] 部署通知系统 +- [X] 部署通知系统 - [ ] 降低使用门槛 -- [ ] 完善批量处理 +- [X] 完善批量处理 +- [ ] 继续集成Napcat +- [ ] 开发Worker提供高稳定性 - [ ] 添加登录功能 -- [ ] 提供更高的可拓展性 -- [ ] 适配手机端 +- [ ] 开发插件系统提供更高的可拓展性 其中支付系统因为还没通过审核,还在研究中 @@ -82,4 +74,4 @@ docker run -d \ 关于本地化部署,现在也在研究了。 -关于拓展性,目前通过URL解析商品信息是支持自行编写解析过程来适配每个网站。但像标签机目前只能使用精臣的标签机,后续会考虑提供更高的可拓展性,支持为不同的标签机提供不同的API +关于拓展性,目前打算做成插件化的形式,方便后续添加功能。 diff --git a/apps/admin/.dockerignore b/apps/admin/.dockerignore new file mode 100644 index 0000000..f27debb --- /dev/null +++ b/apps/admin/.dockerignore @@ -0,0 +1,45 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# Dependencies +node_modules +.pnp +.pnp.js + +# Local env files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Testing +coverage + +# Turbo +.turbo + +# Vercel +.vercel + +# Build Outputs +.next +out +build +dist + + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Misc +.DS_Store +*.pem + +# IDEs and editors +.vscode +.idea + +# Git +.git diff --git a/apps/admin/eslint.config.mjs b/apps/admin/eslint.config.mjs new file mode 100644 index 0000000..fd683ae --- /dev/null +++ b/apps/admin/eslint.config.mjs @@ -0,0 +1,4 @@ +import eslintConfig from "@repo/config/eslint.js"; + +/** @type {import("eslint").Linter.Config} */ +export default eslintConfig; diff --git a/apps/admin/next-env.d.ts b/apps/admin/next-env.d.ts new file mode 100644 index 0000000..830fb59 --- /dev/null +++ b/apps/admin/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/admin/next.config.js b/apps/admin/next.config.js new file mode 100644 index 0000000..3fd6941 --- /dev/null +++ b/apps/admin/next.config.js @@ -0,0 +1,10 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: "standalone", + reactStrictMode: false, + typescript: { + ignoreBuildErrors: true, + }, +}; + +export default nextConfig; diff --git a/apps/admin/package.json b/apps/admin/package.json new file mode 100644 index 0000000..f439c75 --- /dev/null +++ b/apps/admin/package.json @@ -0,0 +1,39 @@ +{ + "name": "admin", + "version": "0.1.0", + "type": "module", + "private": true, + "scripts": { + "dev": "next dev --port 3000", + "build": "next build", + "start": "next start", + "lint": "next lint --max-warnings 0", + "check-types": "tsc --noEmit" + }, + "devDependencies": { + "@repo/config": "*", + "@types/lodash": "^4.17.20", + "@types/node": "^22.15.3", + "@types/react": "19.1.0", + "@types/react-dom": "19.1.1" + }, + "dependencies": { + "@ant-design/icons": "^6.0.2", + "@ant-design/nextjs-registry": "^1.1.0", + "@ant-design/pro-components": "^2.8.10", + "@ant-design/v5-patch-for-react-19": "^1.0.3", + "@react-email/render": "^1.2.3", + "@repo/schema": "*", + "@repo/component": "*", + "@repo/util": "*", + "@trpc/client": "^11.5.1", + "@trpc/server": "^11.5.1", + "antd": "^5.27.3", + "lodash": "^4.17.21", + "next": "^15.5.0", + "path-to-regexp": "^8.3.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "zod": "^3.24.4" + } +} diff --git a/public/.gitkeep b/apps/admin/public/.gitkeep similarity index 100% rename from public/.gitkeep rename to apps/admin/public/.gitkeep diff --git a/src/app/api/v1.0/[trpc]/route.ts b/apps/admin/src/app/api/v1.0/[trpc]/route.ts similarity index 75% rename from src/app/api/v1.0/[trpc]/route.ts rename to apps/admin/src/app/api/v1.0/[trpc]/route.ts index 40f10f1..9f11962 100644 --- a/src/app/api/v1.0/[trpc]/route.ts +++ b/apps/admin/src/app/api/v1.0/[trpc]/route.ts @@ -1,12 +1,12 @@ -import appRouter from "@/server"; +import appRouter from "@/trpc"; import {fetchRequestHandler} from "@trpc/server/adapters/fetch"; -import {createContext} from "@/server/context"; +import {createContext} from "@/trpc/server"; const handler = (req: Request) => fetchRequestHandler({ endpoint: "/api/v1.0", req, router: appRouter, createContext: createContext, -}) +}); export {handler as GET, handler as POST}; diff --git a/apps/admin/src/app/api/v1.0/webhook/delivery/route.ts b/apps/admin/src/app/api/v1.0/webhook/delivery/route.ts new file mode 100644 index 0000000..2be6e5d --- /dev/null +++ b/apps/admin/src/app/api/v1.0/webhook/delivery/route.ts @@ -0,0 +1,113 @@ +import database from "@repo/util/data/database"; +import crypto from "crypto"; +import {NextRequest, NextResponse} from "next/server"; + +export const POST = async (request: NextRequest) => { + try { + const body = await request.formData(); + const param = body.get("param") || ""; + const sign = body.get("sign") || ""; + const expectedSign = crypto.createHash("md5") + .update(param + process.env.EXPRESS_KEY!) + .digest("hex") + .toUpperCase(); + if (sign !== expectedSign) { + return NextResponse.json({ + message: "签名验证失败", + }, { + status: 401, + }); + } + const delivery = await database.delivery.findUnique({ + where: { + taskId: body.get("taskId")!.toString(), + }, + include: { + orders: { + include: { + order: true, + }, + }, + }, + }); + if (!delivery) { + return NextResponse.json({ + message: "未找到对应的快递记录", + }, { + status: 404, + }); + } + const payload = JSON.parse(param.toString()); + const data: Record = {}; + data.comment = payload.data.status; + data.expressNumber = payload.kuaidinum ?? delivery.expressNumber; + data.expressId = payload.data.orderId ?? delivery.expressId; + data.token = payload.data.pollToken ?? delivery.queryToken; + switch (payload.data.status) { + case 0: + case 1: + case 2: + data.status = "waiting"; + break; + case 10: + case 101: + case 200: + case 166: + case 400: + data.status = "confirmed"; + break; + case 13: + data.status = "finished"; + await database.delivery.update({ + where: { + id: delivery.id, + }, + data: { + status: "finished", + }, + }); + for (const order of delivery.orders) { + await database.order.updateMany({ + where: { + id: order.orderId, + deliveries: { + every: { + delivery: { + status: "finished", + }, + }, + }, + }, + data: { + status: "finished", + }, + }); + } + break; + case 9: + case 99: + case 610: + data.status = "failed"; + break; + default: + data.status = "warning"; + break; + } + await database.delivery.update({ + data, + where: { + id: delivery.id, + }, + }); + return NextResponse.json({ + message: "处理成功", + }); + } catch (error) { + console.error("Webhook处理错误:", error); + return NextResponse.json({ + message: "处理失败", + }, { + status: 500, + }); + } +}; diff --git a/apps/admin/src/app/api/v1.0/webhook/shipping/route.ts b/apps/admin/src/app/api/v1.0/webhook/shipping/route.ts new file mode 100644 index 0000000..9a67390 --- /dev/null +++ b/apps/admin/src/app/api/v1.0/webhook/shipping/route.ts @@ -0,0 +1,76 @@ +import crypto from "crypto"; +import {NextRequest, NextResponse} from "next/server"; +import prisma from "@repo/util/data/database"; + +export const POST = async (request: NextRequest) => { + try { + const body = await request.json(); + const sign = request.headers.get("sign") || ""; + const expectedSign = crypto.createHash("sha256") + .update(JSON.stringify(body) + "/" + process.env.TRACKING_KEY!) + .digest("hex"); + if (sign !== expectedSign) { + return NextResponse.json({ + message: "签名验证失败", + }, { + status: 401, + }); + } + const shipping = await prisma.shipping.findUnique({ + where: { + expressNumber: body.data.number, + }, + }); + if (!shipping) { + return NextResponse.json({ + message: "未找到对应的物流记录", + }, { + status: 404, + }); + } + let status: string; + if (body.event === "TRACKING_UPDATED") { + switch (body.data.track_info.latest_status.status) { + case "InfoReceived": + status = "pending"; + break; + case "InTransit": + case "OutForDelivery": + case "AvailableForPickup": + status = "confirmed"; + break; + case "Delivered": + status = "finished"; + break; + case "DeliveryFailure": + status = "failed"; + break; + default: + status = "warning"; + break; + } + } else { + status = "warning"; + } + const detail = body.data.track_info.latest_status.sub_status_descr? `\n${body.data.track_info.latest_status.sub_status_descr}` : ""; + await prisma.shipping.update({ + where: { + id: shipping.id, + }, + data: { + status: status, + comment: `${body.data.track_info.latest_status.sub_status}${detail}`, + }, + }); + return NextResponse.json({ + message: "处理成功", + }); + } catch (error) { + console.error("Webhook处理错误:", error); + return NextResponse.json({ + message: "处理失败", + }, { + status: 500, + }); + } +}; diff --git a/apps/admin/src/app/delivery/[deliveryId]/container.tsx b/apps/admin/src/app/delivery/[deliveryId]/container.tsx new file mode 100644 index 0000000..ee5ae68 --- /dev/null +++ b/apps/admin/src/app/delivery/[deliveryId]/container.tsx @@ -0,0 +1,289 @@ +"use client"; +import React from "react"; +import OrderCheckTable from "@/component/form/table/order"; +import GroupSelector from "@/component/form/filter/group"; +import ItemSelector from "@/component/form/filter/item"; +import BaseModalForm from "@repo/component/base/modal"; +import BaseTable from "@repo/component/base/table"; +import printLabel from "@repo/util/client/printer"; +import trpc from "@/trpc/client"; +import {ActionType, PageContainer, ProDescriptions} from "@ant-design/pro-components"; +import {companyMap, statusMap, DeliverySchema} from "@repo/schema/delivery"; +import {DeleteOutlined, LinkOutlined} from "@ant-design/icons"; +import {App, Button, Form, Popconfirm, Typography} from "antd"; +import {labelSdk} from "@repo/util/printer/niimbot"; +import {ShippingData} from "@repo/schema/shipping"; +import {SettingSchema} from "@repo/schema/setting"; +import {ProColumns} from "@ant-design/pro-table"; +import {TRPCClientError} from "@trpc/client"; +import {UserData} from "@repo/schema/user"; +import {useRouter} from "next/navigation"; + +const Container = (props: { + data: DeliverySchema, + setting: SettingSchema, +}) => { + const [loading, setLoading] = React.useState(false); + const table = React.useRef(null); + const message = App.useApp().message; + const router = useRouter(); + const columns: ProColumns[] = [ + { + title: "ID", + dataIndex: "id", + sorter: true, + }, + { + title: "商品", + dataIndex: "itemId", + sorter: true, + render: (_, record) => ( +
+ {record.item.name} + {record.item.group.name} +
+ ), + renderFormItem: () => , + }, + { + title: "团购", + dataIndex: ["item", "groupId"], + hideInTable: true, + renderFormItem: () => , + }, + { + title: "数量", + dataIndex: "count", + valueType: "digit", + sorter: true, + search: false, + }, + { + title: "创建时间", + dataIndex: "createdAt", + valueType: "dateTime", + sorter: true, + search: false, + }, + { + title: "操作", + valueType: "option", + width: 150, + render: (_, record, _1, action) => [ + , + { + try { + await trpc.deliveryDelete.mutate({ + id: props.data.id, + }); + message.success("移除成功"); + router.replace("/delivery"); + return true; + } catch (e) { + if (e instanceof TRPCClientError) { + message.error(e.message); + } else { + message.error("发生未知错误"); + } + return false; + } + }} + > + + , + ]} + > + [ + 添加} + onFinish={async (values: Record) => { + try { + values.id = props.data.id; + await trpc.deliveryAddOrders.mutate(values as ShippingData); + message.success("添加成功"); + table.current?.reload(); + return true; + } catch (e) { + if (e instanceof TRPCClientError) { + message.error(e.message); + } else { + message.error("发生未知错误"); + } + return false; + } + }} + > + + + + , + ]} + request={async (params, sort) => { + const res = await trpc.orderGetAll.query({ + filter: [ + ...(params.id ? [{field: "id", operator: "eq" as const, value: Number(params.id)}] : []), + ...(params.itemId ? [{field: "itemId", operator: "eq" as const, value: Number(params.itemId)}] : []), + ...(params.item?.groupId ? [{field: "item.groupId", operator: "eq" as const, value: Number(params.item.groupId)}] : []), + {field: "deliveries.some.deliveryId", operator: "eq", value: props.data.id}, + ], + search: params.keyword ?? "", + sort: { + field: Object.keys(sort).length > 0 ? Object.keys(sort)[0] : "id", + order: Object.values(sort)[0] === "descend"? "desc" : "asc", + }, + page: { + size: params.pageSize, + current: params.current, + }, + }); + return { + data: res.items, + success: true, + total: res.total, + }; + }} + /> + + ); +}; + +export default Container; diff --git a/apps/admin/src/app/delivery/[deliveryId]/page.tsx b/apps/admin/src/app/delivery/[deliveryId]/page.tsx new file mode 100644 index 0000000..3e5d53b --- /dev/null +++ b/apps/admin/src/app/delivery/[deliveryId]/page.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import Container from "@/app/delivery/[deliveryId]/container"; +import database from "@repo/util/data/database"; +import {notFound} from "next/navigation"; +import {getSetting} from "@repo/util/data/setting"; + +const Page = async (props: { + params: Promise<{ + deliveryId: number, + }>, +}) => { + const deliveryId = Number((await props.params).deliveryId); + const delivery = await database.delivery.findUnique({ + where: { + id: deliveryId, + }, + include: { + user: true, + }, + }); + if (!delivery) { + return notFound(); + } + const setting = await getSetting(); + + return ( + + ); +}; + +export default Page; diff --git a/apps/admin/src/app/delivery/create/page.tsx b/apps/admin/src/app/delivery/create/page.tsx new file mode 100644 index 0000000..e75c397 --- /dev/null +++ b/apps/admin/src/app/delivery/create/page.tsx @@ -0,0 +1,92 @@ +"use client"; +import React from "react"; +import OrderCheckTable from "@/component/form/table/order"; +import UserTable from "@/component/form/table/user"; +import trpc from "@/trpc/client"; +import {ProFormInstance, ProFormText, ProFormTextArea} from "@ant-design/pro-form"; +import {DeliveryData, companyMap} from "@repo/schema/delivery"; +import {PageContainer} from "@ant-design/pro-layout"; +import {StepsForm} from "@ant-design/pro-components"; +import {CheckCard} from "@ant-design/pro-card"; +import {TRPCClientError} from "@trpc/client"; +import {useRouter} from "next/navigation"; +import {App, Avatar, Form} from "antd"; + +const Page = () => { + const router = useRouter(); + const message = App.useApp().message; + const form = React.useRef(null); + const [user, setUser] = React.useState(null); + const [step, setStep] = React.useState(0); + + return ( + + setStep(current)} + onFinish={async (values) => { + try { + values.userId = values.userId[0]; + await trpc.deliveryCreate.mutate(values as DeliveryData); + message.success("创建成功"); + router.back(); + } catch (e) { + if (e instanceof TRPCClientError) { + message.error(e.message); + } else { + message.error("发生未知错误"); + } + return false; + } + return false; + }} + > + { + setUser(values.userId[0]); + form.current?.setFieldsValue(await trpc.userGetById.query({id: values.userId[0]})); + }} + > + + + + + + + + + + + + + { + Object.keys(companyMap).map((method, index) => ( + + } + /> + )) + } + + + + + + + + + + ); +}; + +export default Page; diff --git a/apps/admin/src/app/delivery/page.tsx b/apps/admin/src/app/delivery/page.tsx new file mode 100644 index 0000000..0b276cb --- /dev/null +++ b/apps/admin/src/app/delivery/page.tsx @@ -0,0 +1,182 @@ +"use client"; +import React from "react"; +import UserSelector from "@/component/form/filter/user"; +import BaseTable from "@repo/component/base/table"; +import trpc from "@/trpc/client"; +import Link from "next/link"; +import {App, Avatar, Button, Popconfirm, Space, Typography} from "antd"; +import {ActionType, ProColumns} from "@ant-design/pro-table"; +import {companyMap, statusMap} from "@repo/schema/delivery"; +import {PageContainer} from "@ant-design/pro-layout"; +import {SettingOutlined} from "@ant-design/icons"; +import {TRPCClientError} from "@trpc/client"; + +const Page = () => { + const message = App.useApp().message; + const table = React.useRef(null); + const columns: ProColumns[] = [ + { + title: "ID", + dataIndex: "id", + sorter: true, + }, + { + title: "收件人", + dataIndex: "userId", + sorter: true, + search: false, + render: (_, record) => ( + + +
+ {record.name} + {record.user.qq} +
+
+ ), + renderFormItem: () => , + }, + { + title: "手机号码", + dataIndex: "phone", + sorter: true, + search: false, + }, + { + title: "地址", + dataIndex: "address", + search: false, + }, + { + title: "快递公司", + dataIndex: "company", + sorter: true, + valueType: "select", + valueEnum: companyMap, + }, + { + title: "快递单号", + dataIndex: "expressNumber", + }, + { + title: "状态", + dataIndex: "status", + sorter: true, + valueType: "select", + valueEnum: statusMap, + }, + { + title: "创建时间", + dataIndex: "createdAt", + valueType: "dateTime", + sorter: true, + search: false, + }, + { + title: "操作", + valueType: "option", + width: 150, + render: (_, record) => [ + + + , + { + try { + await trpc.deliverySendTicket.mutate({ + ids: selectedRowKeys.map((id) => Number(id)), + }); + message.success("发送成功"); + table.current?.reload(); + return true; + } catch (e) { + if (e instanceof TRPCClientError) { + message.error(e.message); + } else { + message.error("发生未知错误"); + } + return false; + } + }} + > + + , + , + ]} + toolBarRender={() => [ + + + , + ]} + request={async (params, sort) => { + const res = await trpc.deliveryGetAll.query({ + filter: [ + ...(params.id ? [{field: "id", operator: "eq" as const, value: Number(params.id)}] : []), + ...(params.company ? [{field: "company", operator: "eq" as const, value: params.company}] : []), + ...(params.status ? [{field: "status", operator: "eq" as const, value: params.status}] : []), + ], + search: params.keyword ?? "", + sort: { + field: Object.keys(sort).length > 0 ? Object.keys(sort)[0] : "id", + order: Object.values(sort)[0] === "descend"? "desc" : "asc", + }, + page: { + size: params.pageSize, + current: params.current, + }, + }); + return { + data: res.items, + success: true, + total: res.total, + }; + }} + /> + + ); +}; + +export default Page; diff --git a/apps/admin/src/app/group/[groupId]/container.tsx b/apps/admin/src/app/group/[groupId]/container.tsx new file mode 100644 index 0000000..d2dce20 --- /dev/null +++ b/apps/admin/src/app/group/[groupId]/container.tsx @@ -0,0 +1,119 @@ +"use client"; +import React from "react"; +import ItemTable from "@/component/data/item"; +import ListTable from "@/component/data/list"; +import trpc from "@/trpc/client"; +import {PageContainer, ProDescriptions} from "@ant-design/pro-components"; +import {GroupData, GroupSchema, statusMap} from "@repo/schema/group"; +import {App, Button, Popconfirm} from "antd"; +import {TRPCClientError} from "@trpc/client"; +import {useRouter} from "next/navigation"; +import Link from "next/link"; + +const Container = (props: { + data: GroupSchema, +}) => { + const [index, setIndex] = React.useState("list"); + const message = App.useApp().message; + const router = useRouter(); + + return ( + { + setIndex(key); + }} + tabList={[ + { + tab: '需求表', + key: 'list', + }, + { + tab: '商品', + key: 'item', + }, + ]} + content={ + { + try { + const res = await trpc.groupGetById.query({id: props.data.id}); + return { + data: res, + success: true, + }; + } catch { + return { + data: {}, + success: false, + }; + } + }} + editable={{ + onSave: async (_key, record) => { + try { + delete record.createdAt; + delete record.ended; + record.deadline = new Date(record.deadline).toISOString(); + await trpc.groupUpdate.mutate(record as GroupData); + return true; + } catch (e) { + if (e instanceof TRPCClientError) { + message.error(e.message); + } else { + message.error("发生未知错误"); + } + return false; + } + }, + }} + > + + + + + + + + } + extra={[ + + + , + { + try { + await trpc.groupDelete.mutate({ + id: props.data.id, + }); + message.success("删除成功"); + router.replace("/group"); + return true; + } catch (e) { + if (e instanceof TRPCClientError) { + message.error(e.message); + } else { + message.error("发生未知错误"); + } + return false; + } + }} + > + + , + ]} + > + { + index === "list" ? ( + + ) : ( + + ) + } + + ); +}; + +export default Container; diff --git a/src/component/container/join.tsx b/apps/admin/src/app/group/[groupId]/list/[listId]/container.tsx similarity index 53% rename from src/component/container/join.tsx rename to apps/admin/src/app/group/[groupId]/list/[listId]/container.tsx index f936884..8e3fcc5 100644 --- a/src/component/container/join.tsx +++ b/apps/admin/src/app/group/[groupId]/list/[listId]/container.tsx @@ -1,58 +1,29 @@ "use client"; import React from "react"; -import ItemSelector from "@/component/field/item"; -import OrderForm from "@/component/form/order"; -import trpc from "@/server/client"; -import {DeleteOutlined, EditOutlined, LinkOutlined, MessageOutlined, TruckOutlined} from "@ant-design/icons"; +import ItemSelector from "@/component/form/filter/item"; +import OrderForm from "@/component/form/modal/order"; +import trpc from "@/trpc/client"; +import {DeleteOutlined, EditOutlined, LinkOutlined, MessageOutlined} from "@ant-design/icons"; import {App, Button, Popconfirm, Popover, Typography, Descriptions} from "antd"; import {ActionType, ProColumns, ProTable} from "@ant-design/pro-table"; -import {PageContainer} from "@ant-design/pro-layout"; -import {OrderSchema, statusMap} from "@/type/order"; +import {PageContainer} from "@ant-design/pro-components"; +import {OrderData, statusMap} from "@repo/schema/order"; +import {ListSchema} from "@repo/schema/list"; import {TRPCClientError} from "@trpc/client"; import {useRouter} from "next/navigation"; -import {JoinData} from "@/type/group"; -import {cStd} from "@/util/string"; +import {cStd} from "@repo/util/data/string"; -interface Props { - data: JoinData -} - -type Data = Omit & { - userId: number, - itemIds: number[] -} - -const JoinContainer = (props: Props) => { +const Container = (props: { + data: ListSchema, +}) => { const router = useRouter(); const message = App.useApp().message; const table = React.useRef(null); - const actions = [ - { - status: "confirmed", - description: "您确定选中的订单没有错误?", - buttonText: "确认", - }, - { - status: "arrived", - description: "您确定选中的订单的商品安然无恙?", - buttonText: "完成验货", - }, - { - status: "finished", - description: "您确定选中的订单以面提方式交付?", - buttonText: "交付", - }, - { - status: "failed", - description: "您确定作废选中的订单?", - buttonText: "作废", - }, - ]; const columns: ProColumns[] = [ { title: "ID", dataIndex: "id", - sorter: true + sorter: true, }, { title: "商品", @@ -64,45 +35,35 @@ const JoinContainer = (props: Props) => { {cStd(record.item.price)} ), - renderFormItem: () => + renderFormItem: () => , }, { title: "数量", dataIndex: "count", sorter: true, valueType: "digit", - search: false + search: false, }, { title: "状态", dataIndex: "status", sorter: true, valueEnum: statusMap, - valueType: "select" + valueType: "select", }, { title: "创建时间", - dataIndex: "createAt", + dataIndex: "createdAt", valueType: "dateTime", sorter: true, - search: false + search: false, }, { title: "操作", valueType: "option", width: 150, render: (_, record, _1, action) => [ - - + + , ]} > { - const list = actions.map(({status, description, buttonText}) => ( - { - try { - await trpc.order.flow.mutate({ - ids: selectedRowKeys.map((id) => Number(id)), - status, - }); - table.current?.clearSelected?.(); - message.success("修改成功"); - table.current?.reload(); - return true; - } catch { - message.error("发生错误,请稍后再试"); - return false; - } - }} - > - - - )) - list.push( - - ) - return list; - }} toolBarRender={() => [ 添加} + target={} onSubmit={async (values: Record) => { try { - values.groupId = props.data.groupId; - values.userId = props.data.userId; - await trpc.order.create.mutate(values as Data); + values.userId = props.data.user.id; + await trpc.orderCreateAll.mutate(values as OrderData); message.success("添加成功"); table.current?.reload(); return true; - } catch { - message.error("发生错误,请稍后再试") + } catch (e) { + if (e instanceof TRPCClientError) { + message.error(e.message); + } else { + message.error("发生未知错误"); + } return false; } }} - /> + />, ]} request={async (params, sort) => { - const res = await trpc.order.get.query({ - params: { - ...params, - userId: props.data.userId, - item: { - groupId: props.data.groupId, - } + const res = await trpc.orderGetAll.query({ + filter: [ + ...(params.id ? [{field: "id", operator: "eq" as const, value: Number(params.id)}] : []), + ...(params.itemId ? [{ + field: "itemId", + operator: "eq" as const, + value: Number(params.itemId), + }] : []), + ...(params.status ? [{ + field: "status", + operator: "eq" as const, + value: params.status, + }] : []), + {field: "userId", operator: "eq", value: props.data.userId}, + {field: "item.groupId", operator: "eq", value: props.data.groupId}, + ], + sort: { + field: Object.keys(sort).length > 0 ? Object.keys(sort)[0] : "id", + order: Object.values(sort)[0] === "descend"? "desc" : "asc", + }, + page: { + size: params.pageSize, + current: params.current, }, - sort }); return { data: res.items, success: true, - total: res.total - } + total: res.total, + }; }} /> ); -} +}; -export default JoinContainer; +export default Container; diff --git a/apps/admin/src/app/group/[groupId]/list/[listId]/page.tsx b/apps/admin/src/app/group/[groupId]/list/[listId]/page.tsx new file mode 100644 index 0000000..3cfc729 --- /dev/null +++ b/apps/admin/src/app/group/[groupId]/list/[listId]/page.tsx @@ -0,0 +1,29 @@ +import Container from "@/app/group/[groupId]/list/[listId]/container"; +import database from "@repo/util/data/database"; +import {notFound} from "next/navigation"; + +const Page = async (props: { + params: Promise<{ + listId: number, + }>, +}) => { + const listId = Number((await props.params).listId); + const list = await database.list.findUnique({ + where: { + id: listId, + }, + include: { + user: true, + group: true, + }, + }); + if (!list) { + return notFound(); + } + + return ( + + ); +}; + +export default Page; diff --git a/apps/admin/src/app/group/[groupId]/page.tsx b/apps/admin/src/app/group/[groupId]/page.tsx new file mode 100644 index 0000000..8e72d2c --- /dev/null +++ b/apps/admin/src/app/group/[groupId]/page.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import Container from "@/app/group/[groupId]/container"; +import database from "@repo/util/data/database"; +import {notFound} from "next/navigation"; + +const Page = async (props: { + params: Promise<{ + groupId: number, + }>, +}) => { + const groupId = Number((await props.params).groupId); + const group = await database.group.findUnique({ + where: { + id: groupId, + }, + }); + + if (!group) { + return notFound(); + } + + return ( + + ); +}; + +export default Page; diff --git a/apps/admin/src/app/group/[groupId]/purchase/container.tsx b/apps/admin/src/app/group/[groupId]/purchase/container.tsx new file mode 100644 index 0000000..f29b42c --- /dev/null +++ b/apps/admin/src/app/group/[groupId]/purchase/container.tsx @@ -0,0 +1,49 @@ +"use client"; +import React from "react"; +import OverviewTable from "@/component/data/summary/overview"; +import ItemTable from "@/component/data/summary/item"; +import UserTable from "@/component/data/summary/user"; +import {PageContainer} from "@ant-design/pro-components"; +import {GroupSchema} from "@repo/schema/group"; + +interface Props { + data: GroupSchema, +} + +const Container = (props: Props) => { + const [index, setIndex] = React.useState("overview"); + + return ( + { + setIndex(key); + }} + > + { + index === "overview" ? ( + + ) : index === "user" ? ( + + ) : ( + + ) + } + + ); +}; + +export default Container; diff --git a/apps/admin/src/app/group/[groupId]/purchase/page.tsx b/apps/admin/src/app/group/[groupId]/purchase/page.tsx new file mode 100644 index 0000000..a6b22f6 --- /dev/null +++ b/apps/admin/src/app/group/[groupId]/purchase/page.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import database from "@repo/util/data/database"; +import SummaryContainer from "@/app/group/[groupId]/purchase/container"; +import {notFound} from "next/navigation"; + +const Page = async (props: { + params: Promise<{ + groupId: number, + }>, +}) => { + const groupId = Number((await props.params).groupId); + const group = await database.group.findUnique({ + where: { + id: groupId, + }, + }); + if (!group) { + return notFound(); + } + return ( + + ); +}; + +export default Page; diff --git a/apps/admin/src/app/group/page.tsx b/apps/admin/src/app/group/page.tsx new file mode 100644 index 0000000..2040cfa --- /dev/null +++ b/apps/admin/src/app/group/page.tsx @@ -0,0 +1,131 @@ +"use client"; +import React from "react"; +import GroupForm from "@/component/form/modal/group"; +import BaseTable from "@repo/component/base/table"; +import trpc from "@/trpc/client"; +import Link from "next/link"; +import {ActionType, ProColumns} from "@ant-design/pro-components"; +import {PageContainer} from "@ant-design/pro-components"; +import {GroupData, statusMap} from "@repo/schema/group"; +import {SettingOutlined} from "@ant-design/icons"; +import {Typography, Button, App} from "antd"; +import {TRPCClientError} from "@trpc/client"; + +const Page = () => { + const message = App.useApp().message; + const table = React.useRef(undefined); + const columns: ProColumns[] = [ + { + title: "ID", + dataIndex: "id", + sorter: true, + }, + { + title: "Q群", + dataIndex: "name", + sorter: true, + search: false, + render: (_, record) => ( +
+ {record.name} + {record.qq} +
+ ), + }, + { + title: "创建时间", + dataIndex: "createdAt", + valueType: "dateTime", + sorter: true, + search: false, + }, + { + title: "截止时间", + dataIndex: "deadline", + valueType: "dateTime", + sorter: true, + search: false, + }, + { + title: "状态", + dataIndex: "ended", + valueType: "select", + sorter: true, + valueEnum: statusMap, + }, + { + title: "操作", + valueType: "option", + width: 150, + render: (_, record) => [ + + } + onSubmit={async (values: Record) => { + try { + await trpc.groupCreate.mutate(values as GroupData); + message.success("添加成功"); + table.current?.reload(); + return true; + } catch (e) { + if (e instanceof TRPCClientError) { + message.error(e.message); + } else { + message.error("发生未知错误"); + } + return false; + } + }} + />, + ]} + request={async (params, sort) => { + const res = await trpc.groupGetAll.query({ + filter: [ + ...(params.id ? [{field: "id", operator: "eq" as const, value: Number(params.id)}] : []), + ...(params.status ? [{field: "status", operator: "eq" as const, value: params.status}] : []), + ], + search: params.keyword ?? "", + sort: { + field: Object.keys(sort).length > 0 ? Object.keys(sort)[0] : "id", + order: Object.values(sort)[0] === "descend"? "desc" : "asc", + }, + page: { + size: params.pageSize, + current: params.current, + }, + }); + return { + data: res.items, + success: true, + total: res.total, + }; + }} + /> + + ); +}; + +export default Page; diff --git a/apps/admin/src/app/layout.tsx b/apps/admin/src/app/layout.tsx new file mode 100644 index 0000000..dd90faa --- /dev/null +++ b/apps/admin/src/app/layout.tsx @@ -0,0 +1,133 @@ +import React from "react"; +import '@ant-design/v5-patch-for-react-19'; +import BaseLayout from "@repo/component/base/layout"; +import {AntdRegistry} from "@ant-design/nextjs-registry"; +import {getSetting} from "@repo/util/data/setting"; +import {Metadata} from "next"; +import {App} from "antd"; +import { + MailOutlined, + PayCircleOutlined, + SettingOutlined, + ShoppingCartOutlined, + TruckOutlined, + UserOutlined, +} from "@ant-design/icons"; + +export const dynamic = "force-dynamic"; + +export const generateMetadata = async () : Promise => { + const setting = await getSetting(); + return { + title: setting.title || "NextMyOrder", + icons: { + icon: setting.logo || "", + }, + }; +}; + +const Layout = (props: React.PropsWithChildren) => { + const router = { + path: "/", + routes: [ + { + path: "/group", + name: "团购", + icon: , + children: [ + { + path: "/group/:groupId", + name: "团购管理", + hideInMenu: true, + }, + { + path: "/group/:groupId/list/:userId", + name: "用户管理", + hideInMenu: true, + }, + { + path: "/group/:groupId/purchase", + name: "采购汇总", + hideInMenu: true, + }, + ], + }, + { + path: "/user", + name: "用户", + icon: , + children: [ + { + path: "/user/:userId", + name: "用户详情", + hideInMenu: true, + }, + ], + }, + { + path: "/shipping", + name: "运输", + icon: , + children: [ + { + path: "/shipping/create", + name: "创建运单", + hideInMenu: true, + }, + { + path: "/shipping/:shippingId", + name: "运单详情", + hideInMenu: true, + }, + { + path: "/shipping/:shippingId/check", + name: "检查汇总", + hideInMenu: true, + }, + ], + }, + { + path: "/delivery", + name: "分发", + icon: , + children: [ + { + path: "/delivery/create", + name: "创建运单", + hideInMenu: true, + }, + { + path: "/delivery/:deliveryId", + name: "运单详情", + hideInMenu: true, + }, + ], + }, + { + path: "/payment", + name: "账单", + icon: , + }, + { + path: "/setting", + name: "设置", + icon: , + }, + ], + }; + return ( + + + + + + {props.children} + + + + + + ); +}; + +export default Layout; diff --git a/apps/admin/src/app/not-found.tsx b/apps/admin/src/app/not-found.tsx new file mode 100644 index 0000000..1ccea17 --- /dev/null +++ b/apps/admin/src/app/not-found.tsx @@ -0,0 +1,18 @@ +import {Result} from "antd"; + +const Page = () => { + return ( +
+ +
+ ); +}; + +export default Page; diff --git a/src/app/(ui)/page.tsx b/apps/admin/src/app/page.tsx similarity index 71% rename from src/app/(ui)/page.tsx rename to apps/admin/src/app/page.tsx index 796e292..b0741a7 100644 --- a/src/app/(ui)/page.tsx +++ b/apps/admin/src/app/page.tsx @@ -1,7 +1,7 @@ import {redirect} from "next/navigation"; const Page = () => { - return redirect("/group") -} + return redirect("/group"); +}; export default Page; diff --git a/apps/admin/src/app/setting/container.tsx b/apps/admin/src/app/setting/container.tsx new file mode 100644 index 0000000..f47aa83 --- /dev/null +++ b/apps/admin/src/app/setting/container.tsx @@ -0,0 +1,64 @@ +"use client"; +import trpc from "@/trpc/client"; +import {PageContainer, ProForm} from "@ant-design/pro-components"; +import {ProFormText, ProFormTextArea} from "@ant-design/pro-form"; +import {SettingSchema} from "@repo/schema/setting"; +import {TRPCClientError} from "@trpc/client"; +import {App, Divider} from "antd"; +import {useRouter} from "next/navigation"; + +const Container = (props: { + data: SettingSchema, +}) => { + const message = App.useApp().message; + const router = useRouter(); + + return ( + + { + try { + await trpc.settingUpdate.mutate(values as SettingSchema); + message.success("更新成功"); + router.refresh(); + return true; + } catch (e) { + if (e instanceof TRPCClientError) { + message.error(e.message); + } else { + message.error("发生未知错误"); + } + return false; + } + }} + > + + + + + + + + + + + + ); +}; + +export default Container; diff --git a/apps/admin/src/app/setting/page.tsx b/apps/admin/src/app/setting/page.tsx new file mode 100644 index 0000000..dc7394a --- /dev/null +++ b/apps/admin/src/app/setting/page.tsx @@ -0,0 +1,12 @@ +import Container from "@/app/setting/container"; +import {getSetting} from "@repo/util/data/setting"; + +const Page = async () => { + const setting = await getSetting(); + + return ( + + ); +}; + +export default Page; diff --git a/apps/admin/src/app/shipping/[shippingId]/check/container.tsx b/apps/admin/src/app/shipping/[shippingId]/check/container.tsx new file mode 100644 index 0000000..4d4e9cb --- /dev/null +++ b/apps/admin/src/app/shipping/[shippingId]/check/container.tsx @@ -0,0 +1,47 @@ +"use client"; +import React from "react"; +import CheckTable from "@/component/data/shipping/check"; +import FeeTable from "@/component/data/shipping/fee"; +import TaxTable from "@/component/data/shipping/tax"; +import {PageContainer} from "@ant-design/pro-layout"; +import {ShippingSchema} from "@repo/schema/shipping"; + +const Container = (props: { + data: ShippingSchema, +}) => { + const [index, setIndex] = React.useState("check"); + + return ( + { + setIndex(key); + }} + > + { + index === "check" ? ( + + ) : index === "tax" ? ( + + ) : ( + + ) + } + + ); +}; + +export default Container; diff --git a/apps/admin/src/app/shipping/[shippingId]/check/page.tsx b/apps/admin/src/app/shipping/[shippingId]/check/page.tsx new file mode 100644 index 0000000..ec25eaa --- /dev/null +++ b/apps/admin/src/app/shipping/[shippingId]/check/page.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import Container from "@/app/shipping/[shippingId]/check/container"; +import database from "@repo/util/data/database"; +import {notFound} from "next/navigation"; + +const Page = async (props: { + params: Promise<{ + shippingId: number, + }>, +}) => { + const shippingId = Number((await props.params).shippingId); + const shipping = await database.shipping.findUnique({ + where: { + id: shippingId, + }, + }); + + if (!shipping) { + return notFound(); + } + + return ( + + ); +}; + +export default Page; diff --git a/apps/admin/src/app/shipping/[shippingId]/container.tsx b/apps/admin/src/app/shipping/[shippingId]/container.tsx new file mode 100644 index 0000000..400b4ce --- /dev/null +++ b/apps/admin/src/app/shipping/[shippingId]/container.tsx @@ -0,0 +1,235 @@ +"use client"; +import React from "react"; +import OrderCheckTable from "@/component/form/table/order"; +import GroupSelector from "@/component/form/filter/group"; +import BaseModalForm from "@repo/component/base/modal"; +import ItemSelector from "@/component/form/filter/item"; +import UserSelector from "@/component/form/filter/user"; +import BaseTable from "@repo/component/base/table"; +import trpc from "@/trpc/client"; +import {ShippingData, ShippingSchema, statusMap} from "@repo/schema/shipping"; +import {App, Avatar, Button, Form, Popconfirm, Space, Typography} from "antd"; +import {DeleteOutlined, LinkOutlined} from "@ant-design/icons"; +import {ActionType, ProColumns} from "@ant-design/pro-table"; +import {ProDescriptions} from "@ant-design/pro-components"; +import {PageContainer} from "@ant-design/pro-components"; +import {TRPCClientError} from "@trpc/client"; +import {useRouter} from "next/navigation"; + +const Container = (props: { + data: ShippingSchema, +}) => { + const message = App.useApp().message; + const router = useRouter(); + const table = React.useRef(null); + const columns: ProColumns[] = [ + { + title: "ID", + dataIndex: "id", + sorter: true, + }, + { + title: "用户", + dataIndex: "userId", + sorter: true, + render: (_, record) => ( + + +
+ {record.user.name} + {record.user.qq} +
+
+ ), + renderFormItem: () => , + }, + { + title: "商品", + dataIndex: "itemId", + sorter: true, + render: (_, record) => ( +
+ {record.item.name} + {record.item.group.name} +
+ ), + renderFormItem: () => , + }, + { + title: "团购", + dataIndex: ["item", "groupId"], + hideInTable: true, + renderFormItem: () => , + }, + { + title: "数量", + dataIndex: "count", + valueType: "digit", + sorter: true, + search: false, + }, + { + title: "创建时间", + dataIndex: "createdAt", + valueType: "dateTime", + sorter: true, + search: false, + }, + { + title: "操作", + valueType: "option", + width: 150, + render: (_, record, _1, action) => [ + , + ]} + content={ + { + try { + const res = await trpc.shippingGetById.query({id: props.data.id}); + return { + data: res, + success: true, + }; + } catch { + return { + data: {}, + success: false, + }; + } + }} + editable={{ + onSave: async (_key, record) => { + try { + delete record.items; + record.expressNumber = record.expressNumber === ""? null: record.expressNumber; + await trpc.shippingUpdate.mutate(record as ShippingData); + return true; + } catch (e) { + if (e instanceof TRPCClientError) { + message.error(e.message); + } else { + message.error("发生未知错误"); + } + return false; + } + }, + }} + > + + + + + + + + + } + > + [ + 添加} + onFinish={async (values: Record) => { + try { + values.id = props.data.id; + await trpc.shippingAddOrders.mutate(values as ShippingData); + message.success("添加成功"); + table.current?.reload(); + return true; + } catch (e) { + if (e instanceof TRPCClientError) { + message.error(e.message); + } else { + message.error("发生未知错误"); + } + return false; + } + }} + > + + + + , + ]} + request={async (params, sort) => { + const res = await trpc.orderGetAll.query({ + filter: [ + ...(params.id ? [{field: "id", operator: "eq" as const, value: Number(params.id)}] : []), + ...(params.userId ? [{field: "userId", operator: "eq" as const, value: Number(params.userId)}] : []), + ...(params.itemId ? [{field: "itemId", operator: "eq" as const, value: Number(params.itemId)}] : []), + ...(params.item?.groupId ? [{field: "item.groupId", operator: "eq" as const, value: Number(params.item.groupId)}] : []), + {field: "shippingId", operator: "eq", value: props.data.id}, + ], + search: params.keyword ?? "", + sort: { + field: Object.keys(sort).length > 0 ? Object.keys(sort)[0] : "id", + order: Object.values(sort)[0] === "descend"? "desc" : "asc", + }, + page: { + size: params.pageSize, + current: params.current, + }, + }); + return { + data: res.items, + success: true, + total: res.total, + }; + }} + /> + + ); +}; + +export default Container; diff --git a/apps/admin/src/app/shipping/[shippingId]/page.tsx b/apps/admin/src/app/shipping/[shippingId]/page.tsx new file mode 100644 index 0000000..a23225b --- /dev/null +++ b/apps/admin/src/app/shipping/[shippingId]/page.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import Container from "@/app/shipping/[shippingId]/container"; +import database from "@repo/util/data/database"; +import {notFound} from "next/navigation"; + +const Page = async (props: { + params: Promise<{ + shippingId: number, + }>, +}) => { + const shippingId = Number((await props.params).shippingId); + const shipping = await database.shipping.findUnique({ + where: { + id: shippingId, + }, + }); + + if (!shipping) { + return notFound(); + } + + return ( + + ); +}; + +export default Page; diff --git a/apps/admin/src/app/shipping/create/page.tsx b/apps/admin/src/app/shipping/create/page.tsx new file mode 100644 index 0000000..a9f6670 --- /dev/null +++ b/apps/admin/src/app/shipping/create/page.tsx @@ -0,0 +1,58 @@ +"use client"; +import React from "react"; +import OrderCheckTable from "@/component/form/table/order"; +import trpc from "@/trpc/client"; +import {ProFormMoney, ProFormText, ProFormTextArea} from "@ant-design/pro-form"; +import {PageContainer} from "@ant-design/pro-layout"; +import {StepsForm} from "@ant-design/pro-components"; +import {ShippingData} from "@repo/schema/shipping"; +import {TRPCClientError} from "@trpc/client"; +import {useRouter} from "next/navigation"; +import {App, Form} from "antd"; + +const Page = () => { + const message = App.useApp().message; + const router = useRouter(); + const [step, setStep] = React.useState(0); + + return ( + + setStep(current)} + onFinish={async (values) => { + try { + values.expressNumber = values.expressNumber === ""? null: values.expressNumber; + await trpc.shippingCreate.mutate(values as ShippingData & { + itemIds: number[], + }); + message.success("创建成功"); + router.back(); + } catch (e) { + if (e instanceof TRPCClientError) { + message.error(e.message); + } else { + message.error("发生未知错误"); + } + return false; + } + return false; + }} + > + + + + + + + + + + + + + + ); +}; + +export default Page; diff --git a/apps/admin/src/app/shipping/page.tsx b/apps/admin/src/app/shipping/page.tsx new file mode 100644 index 0000000..ad3b0fd --- /dev/null +++ b/apps/admin/src/app/shipping/page.tsx @@ -0,0 +1,96 @@ +"use client"; +import React from "react"; +import BaseTable from "@repo/component/base/table"; +import trpc from "@/trpc/client"; +import Link from "next/link"; +import {PageContainer} from "@ant-design/pro-components"; +import {SettingOutlined} from "@ant-design/icons"; +import {ProColumns} from "@ant-design/pro-table"; +import {statusMap} from "@repo/schema/shipping"; +import {Button} from "antd"; + +const Page = () => { + const columns: ProColumns[] = [ + { + title: "ID", + dataIndex: "id", + sorter: true, + }, + { + title: "运单号", + dataIndex: "expressNumber", + sorter: true, + search: false, + }, + { + title: "税费", + dataIndex: "tax", + sorter: true, + search: false, + }, + { + title: "运费", + dataIndex: "fee", + sorter: true, + search: false, + }, + { + title: "状态", + dataIndex: "status", + sorter: true, + valueEnum: statusMap, + valueType: "select", + }, + { + title: "创建时间", + dataIndex: "createdAt", + valueType: "dateTime", + sorter: true, + search: false, + }, + { + title: '操作', + valueType: 'option', + width: 150, + render: (_, record) => [ + + + , + ]} + request={async (params, sort) => { + const res = await trpc.shippingGetAll.query({ + filter: params.id? [{field: "id", operator: "eq", value: Number(params.id)}] : [], + search: params.keyword ?? "", + sort: { + field: Object.keys(sort).length > 0 ? Object.keys(sort)[0] : "id", + order: Object.values(sort)[0] === "descend"? "desc" : "asc", + }, + page: { + size: params.pageSize, + current: params.current, + }, + }); + return { + data: res.items, + success: true, + total: res.total, + }; + }} + /> + + ); +}; + +export default Page; diff --git a/apps/admin/src/app/user/[userId]/container.tsx b/apps/admin/src/app/user/[userId]/container.tsx new file mode 100644 index 0000000..448b9f9 --- /dev/null +++ b/apps/admin/src/app/user/[userId]/container.tsx @@ -0,0 +1,90 @@ +"use client"; +import React from "react"; +import trpc from "@/trpc/client"; +import {PageContainer, ProDescriptions} from "@ant-design/pro-components"; +import {UserData, UserSchema} from "@repo/schema/user"; +import {App, Button, Popconfirm} from "antd"; +import {TRPCClientError} from "@trpc/client"; +import {useRouter} from "next/navigation"; + +const Container = (props: { + data: UserSchema, +}) => { + const message = App.useApp().message; + const router = useRouter(); + return ( + { + try { + const res = await trpc.userGetById.query({id: props.data.id}); + return { + data: res, + success: true, + }; + } catch { + return { + data: {}, + success: false, + }; + } + }} + editable={{ + onSave: async (_key, record) => { + try { + delete record.createdAt; + await trpc.userUpdate.mutate(record as UserData); + return true; + } catch (e) { + if (e instanceof TRPCClientError) { + message.error(e.message); + } else { + message.error("发生未知错误"); + } + return false; + } + }, + }} + > + + + + + + + + + } + extra={[ + { + try { + await trpc.userDelete.mutate({ + id: props.data.id, + }); + message.success("移除成功"); + router.replace("/user"); + return true; + } catch (e) { + if (e instanceof TRPCClientError) { + message.error(e.message); + } else { + message.error("发生未知错误"); + } + return false; + } + }} + > + + , + ]} + > + + ); +}; + +export default Container; diff --git a/apps/admin/src/app/user/[userId]/page.tsx b/apps/admin/src/app/user/[userId]/page.tsx new file mode 100644 index 0000000..5c25a86 --- /dev/null +++ b/apps/admin/src/app/user/[userId]/page.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import Container from "@/app/user/[userId]/container"; +import database from "@repo/util/data/database"; +import {notFound} from "next/navigation"; + +const Page = async (props: { + params: Promise<{ + userId: number, + }>, +}) => { + const userId = Number((await props.params).userId); + const user = await database.user.findUnique({ + where: { + id: userId, + }, + }); + if (!user) { + return notFound(); + } + + return ( + + ); +}; + +export default Page; diff --git a/apps/admin/src/app/user/page.tsx b/apps/admin/src/app/user/page.tsx new file mode 100644 index 0000000..d09281d --- /dev/null +++ b/apps/admin/src/app/user/page.tsx @@ -0,0 +1,153 @@ +"use client"; +import React from "react"; +import UserForm from "@/component/form/modal/user"; +import BaseTable from "@repo/component/base/table"; +import trpc from "@/trpc/client"; +import Link from "next/link"; +import {ModalForm, ProFormTextArea} from "@ant-design/pro-components"; +import {ActionType, ProColumns} from "@ant-design/pro-table"; +import {Space, Avatar, Typography, App, Button} from "antd"; +import {PageContainer} from "@ant-design/pro-components"; +import {SettingOutlined} from "@ant-design/icons"; +import {TRPCClientError} from "@trpc/client"; +import {UserData} from "@repo/schema/user"; + +const Page = () => { + const message = App.useApp().message; + const table = React.useRef(null); + const columns: ProColumns[] = [ + { + title: "ID", + dataIndex: "id", + sorter: true, + }, + { + title: "QQ", + dataIndex: "qq", + sorter: true, + search: false, + render: (_, record) => ( + + +
+ {record.name} + {record.qq} +
+
+ ), + }, + { + title: "邮箱", + dataIndex: "email", + sorter: true, + search: false, + }, + { + title: "注册时间", + dataIndex: "createdAt", + valueType: "dateTime", + sorter: true, + search: false, + }, + { + title: '操作', + valueType: 'option', + width: 150, + render: (_, record) => [ + + } + onSubmit={async (values: Record) => { + try { + await trpc.userCreate.mutate(values as UserData); + message.success("添加成功"); + table.current?.reload(); + return true; + } catch (e) { + if (e instanceof TRPCClientError) { + message.error(e.message); + } else { + message.error("发生未知错误"); + } + return false; + } + }} + />, + 批量添加} + modalProps={{ + destroyOnHidden: true, + }} + onFinish={async (values) => { + try { + const list = values.content.split("\n") + .filter((line: string) => line.trim() !== "") + .map(Number); + await trpc.userCreateAll.mutate({ + qqs: list, + }); + message.success("批量添加成功"); + table.current?.reload(); + return true; + } catch (e) { + if (e instanceof TRPCClientError) { + message.error(e.message); + } else { + message.error("发生未知错误"); + } + return false; + } + }} + > + + , + ]} + request={async (params, sort) => { + const res = await trpc.userGetAll.query({ + filter: params.id? [{field: "id", operator: "eq", value: Number(params.id)}] : [], + search: params.keyword ?? "", + sort: { + field: Object.keys(sort).length > 0 ? Object.keys(sort)[0] : "id", + order: Object.values(sort)[0] === "descend"? "desc" : "asc", + }, + page: { + size: params.pageSize, + current: params.current, + }, + }); + return { + data: res.items, + success: true, + total: res.total, + }; + }} + /> + + ); +}; + +export default Page; diff --git a/apps/admin/src/component/data/item.tsx b/apps/admin/src/component/data/item.tsx new file mode 100644 index 0000000..aac1f72 --- /dev/null +++ b/apps/admin/src/component/data/item.tsx @@ -0,0 +1,296 @@ +"use client"; +import React from "react"; +import UnselectWeight from "@/component/weight/unselect"; +import ItemForm from "@/component/form/modal/item"; +import BaseTable from "@repo/component/base/table"; +import trpc from "@/trpc/client"; +import {CheckOutlined, CloseOutlined, DeleteOutlined, EditOutlined, LinkOutlined} from "@ant-design/icons"; +import {ModalForm, ProFormTextArea} from "@ant-design/pro-components"; +import {ActionType, ProColumns} from "@ant-design/pro-table"; +import {ItemData, statusMap} from "@repo/schema/item"; +import {App, Button, Image, Popconfirm} from "antd"; +import {GroupSchema} from "@repo/schema/group"; +import {TRPCClientError} from "@trpc/client"; +import {cStd, mStd} from "@repo/util/data/string"; + +const ItemTable = (props: { + data: GroupSchema, +}) => { + const message = App.useApp().message; + const table = React.useRef(null); + const columns: ProColumns[] = [ + { + title: "ID", + dataIndex: "id", + sorter: true, + }, + { + title: "图片", + width: 100, + sorter: false, + search: false, + render: (_, record) => ( + record.image ? ( + {record.name} + ) : "-" + ), + }, + { + title: "名称", + dataIndex: "name", + sorter: true, + search: false, + }, + { + title: "单价", + dataIndex: "price", + sorter: true, + search: false, + valueType: "money", + render: (_, record) => cStd(record.price), + }, + { + title: "重量", + dataIndex: "weight", + sorter: true, + search: false, + valueType: "digit", + render: (_, record) => mStd(record.weight), + }, + { + title: "合规性", + dataIndex: "allowed", + sorter: true, + valueType: "select", + valueEnum: statusMap, + render: (_, record) => ( + record.allowed? : + ), + }, + { + title: "操作", + valueType: "option", + width: 150, + render: (_, record, _1, action) => [ + + , + { + try { + await trpc.itemDisallowAll.mutate({ + ids: selectedRowKeys as number[], + }); + message.success("操作成功"); + onCleanSelected(); + table.current?.reload(); + return true; + } catch (e) { + if (e instanceof TRPCClientError) { + message.error(e.message); + } else { + message.error("发生未知错误"); + } + return false; + } + }} + okText="确定" + cancelText="取消" + > + + , + , + ]} + toolBarRender={() => [ + 添加} + onSubmit={async (values: Record) => { + try { + values.groupId = props.data.id; + values.allowed = true; + await trpc.itemCreate.mutate(values as ItemData); + message.success("添加成功"); + table.current?.reload(); + return true; + } catch (e) { + if (e instanceof TRPCClientError) { + message.error(e.message); + } else { + message.error("发生未知错误"); + } + return false; + } + }} + />, + 批量添加} + modalProps={{ + destroyOnHidden: true, + }} + onFinish={async (values) => { + try { + const list = values.content.split("\n") + .filter((line: string) => line.trim() !== ""); + await trpc.itemCreateAll.mutate({ + groupId: props.data.id, + urls: list, + }); + message.success("批量添加成功"); + table.current?.reload(); + return true; + } catch (e) { + if (e instanceof TRPCClientError) { + message.error(e.message); + } else { + message.error("发生未知错误"); + } + return false; + } + }} + > + + , + ]} + request={async (params, sort) => { + const res = await trpc.itemGetAll.query({ + filter: [ + ...(params.id ? [{field: "id", operator: "eq" as const, value: Number(params.id)}] : []), + ...(params.allowed ? [{field: "allowed", operator: "eq" as const, value: params.allowed === "true"}] : []), + {field: "groupId", operator: "eq", value: props.data.id}, + ], + search: params.keyword ?? "", + sort: { + field: Object.keys(sort).length > 0 ? Object.keys(sort)[0] : "id", + order: Object.values(sort)[0] === "descend"? "desc" : "asc", + }, + page: { + size: params.pageSize, + current: params.current, + }, + }); + return { + data: res.items, + success: true, + total: res.total, + }; + }} + /> + ); +}; + +export default ItemTable; diff --git a/apps/admin/src/component/data/list.tsx b/apps/admin/src/component/data/list.tsx new file mode 100644 index 0000000..96366ea --- /dev/null +++ b/apps/admin/src/component/data/list.tsx @@ -0,0 +1,164 @@ +"use client"; +import React from "react"; +import UnselectWeight from "@/component/weight/unselect"; +import UserTable from "@/component/form/table/user"; +import BaseModalForm from "@repo/component/base/modal"; +import BaseTable from "@repo/component/base/table"; +import trpc from "@/trpc/client"; +import Link from "next/link"; +import {CheckOutlined, CloseOutlined, SettingOutlined} from "@ant-design/icons"; +import {App, Avatar, Button, Form, Popconfirm, Space, Typography} from "antd"; +import {ActionType, ProColumns} from "@ant-design/pro-table"; +import {confirmMap, GroupSchema} from "@repo/schema/group"; +import {TRPCClientError} from "@trpc/client"; +import {ListData} from "@repo/schema/list"; + +const ListTable = (props: { + data: GroupSchema, +}) => { + const message = App.useApp().message; + const table = React.useRef(null); + const columns: ProColumns[] = [ + { + title: "ID", + dataIndex: "id", + sorter: true, + }, + { + title: "用户", + dataIndex: "userId", + sorter: true, + search: false, + render: (_, record) => ( + + +
+ {record.user.name} + {record.user.qq} +
+
+ ), + }, + { + title: "创建时间", + dataIndex: "createdAt", + valueType: "dateTime", + sorter: true, + search: false, + }, + { + title: "订单确认", + dataIndex: "confirmed", + sorter: true, + valueType: "select", + valueEnum: confirmMap, + render: (_, record) => ( + record.confirmed? : + ), + }, + { + title: "操作", + valueType: "option", + width: 150, + render: (_, record) => [ + + + , + , + ]} + toolBarRender={() => [ + 添加} + onFinish={async (values: Record) => { + try { + values.groupId = props.data.id; + const res = await trpc.listCreateAll.mutate(values as ListData); + message.success(`${res.total}项添加成功`); + table.current?.reload(); + return true; + } catch (e) { + if (e instanceof TRPCClientError) { + message.error(e.message); + } else { + message.error("发生未知错误"); + } + return false; + } + }} + > + + + + , + ]} + request={async (params, sort) => { + const res = await trpc.listGetAll.query({ + filter: [ + {field: "groupId", operator: "eq", value: props.data.id}, + ...(params.id ? [{field: "id", operator: "eq" as const, value: Number(params.id)}] : []), + ...(params.confirmed ? [{ + field: "confirmed", + operator: "eq" as const, + value: params.confirmed === "true", + }] : []), + ], + search: params.keyword ?? "", + sort: { + field: Object.keys(sort).length > 0 ? Object.keys(sort)[0] : "id", + order: Object.values(sort)[0] === "descend"? "desc" : "asc", + }, + page: { + size: params.pageSize, + current: params.current, + }, + }); + return { + data: res.items, + success: true, + total: res.total, + }; + }} + /> + ); +}; + +export default ListTable; diff --git a/apps/admin/src/component/data/shipping.tsx b/apps/admin/src/component/data/shipping.tsx new file mode 100644 index 0000000..6aebcdb --- /dev/null +++ b/apps/admin/src/component/data/shipping.tsx @@ -0,0 +1,172 @@ +"use client"; +import React from "react"; +import trpc from "@/trpc/client"; +import Link from "next/link"; +import {ModalForm, ProFormMoney, ProFormSelect, ProFormText} from "@ant-design/pro-form"; +import {ActionType, ProColumns, ProTable} from "@ant-design/pro-table"; +import {ShippingData, statusMap} from "@repo/schema/shipping"; +import {SettingOutlined} from "@ant-design/icons"; +import {GroupSchema} from "@repo/schema/group"; +import {App, Button, Typography} from "antd"; +import {TRPCClientError} from "@trpc/client"; +import {cStd} from "@repo/util/data/string"; + +const ShippingTable = (props: { + data: GroupSchema, +}) => { + const message = App.useApp().message; + const table = React.useRef(null); + const columns: ProColumns[] = [ + { + title: "ID", + dataIndex: "id", + sorter: true, + }, + { + title: "运单号", + dataIndex: "expressNumber", + sorter: true, + search: false, + }, + { + title: "运费", + dataIndex: "fee", + valueType: "money", + sorter: true, + search: false, + }, + { + title: "税费", + dataIndex: "tax", + valueType: "money", + sorter: true, + search: false, + }, + { + title: "状态", + dataIndex: "status", + sorter: true, + valueEnum: statusMap, + valueType: "select", + }, + { + title: "创建时间", + dataIndex: "createAt", + valueType: "dateTime", + sorter: true, + search: false, + }, + { + title: "操作", + valueType: "option", + width: 150, + render: (_, record) => [ + + } + modalProps={{ + destroyOnHidden: true, + }} + onFinish={async (values: Record) => { + try { + await trpc.shippingCreate.mutate(values as ShippingData); + message.success("添加成功"); + table.current?.reload(); + return true; + } catch (e) { + if (e instanceof TRPCClientError) { + message.error(e.message); + } else { + message.error("发生未知错误"); + } + return false; + } + }} + > + ( +
+ {option.data.name} + + {cStd(option.data.price)} + +
+ ), + }} + request={async (params) => { + const res = await trpc.itemGetAll.query({ + filter: [ + {field: "groupId", operator: "eq", value: props.data.id}, + ], + search: params.keyWords, + }); + return res.items.map((item) => ({ + ...item, + label: item.name, + value: item.id, + })); + }} + /> + + + + , + ]} + request={async (params, sort) => { + const res = await trpc.shippingGetAll.query({ + filter: [ + ...(params.id ? [{field: "id", operator: "eq" as const, value: Number(params.id)}] : []), + ...(params.status ? [{field: "status", operator: "eq" as const, value: params.status}] : []), + {field: "groupId", operator: "eq", value: props.data.id}, + ], + search: params.keyword ?? "", + sort: { + field: Object.keys(sort).length > 0 ? Object.keys(sort)[0] : "id", + order: Object.values(sort)[0] === "descend"? "desc" : "asc", + }, + page: { + size: params.pageSize, + current: params.current, + }, + }); + return { + data: res.items, + success: true, + total: res.total, + }; + }} + /> + ); +}; + +export default ShippingTable; diff --git a/apps/admin/src/component/data/shipping/check.tsx b/apps/admin/src/component/data/shipping/check.tsx new file mode 100644 index 0000000..1851846 --- /dev/null +++ b/apps/admin/src/component/data/shipping/check.tsx @@ -0,0 +1,164 @@ +"use client"; +import React from "react"; +import CheckPushTable from "@/component/form/table/check"; +import UnselectWeight from "@/component/weight/unselect"; +import BaseModalForm from "@repo/component/base/modal"; +import trpc from "@/trpc/client"; +import {ProColumns, ProTable, ProForm} from "@ant-design/pro-components"; +import {ItemData, ItemSchema} from "@repo/schema/item"; +import {ActionType} from "@ant-design/pro-components"; +import {ShippingSchema} from "@repo/schema/shipping"; +import {App, Button, Image, Typography} from "antd"; +import {LinkOutlined} from "@ant-design/icons"; +import {TRPCClientError} from "@trpc/client"; + +const CheckTable = (props: { + data: ShippingSchema, +}) => { + const [items, setItems] = React.useState([]); + const message = App.useApp().message; + const table = React.useRef(null); + const columns: ProColumns[] = [ + { + title: "ID", + dataIndex: "id", + sorter: true, + }, + { + title: "图片", + width: 100, + sorter: false, + search: false, + render: (_, record) => ( + record.image ? ( + {record.name} + ) : "-" + ), + }, + { + title: "商品", + dataIndex: "name", + sorter: true, + search: false, + render: (_, record) => ( +
+ {record.name} + + {record.groupName} + +
+ ), + }, + { + title: "总数量", + dataIndex: "total", + sorter: true, + search: false, + valueType: "digit", + }, + { + title: "待验货", + dataIndex: "check", + sorter: true, + search: false, + valueType: "digit", + }, + { + title: "操作", + valueType: "option", + width: 150, + render: (_, record) => [ + } + initialValues={{ + items: items, + }} + onFinish={async (record) => { + try { + await trpc.itemPush.mutate({ + items: record.items.map((i: ItemData & { + pending: number, + }) => ({ + id: i.id, + count: i.pending, + })), + }); + message.success("操作成功"); + onCleanSelected(); + table.current?.reload(); + return true; + } catch (e) { + if (e instanceof TRPCClientError) { + message.error(e.message); + } else { + message.error("发生未知错误"); + } + return false; + } + }} + > + + + + , + , + ]} + request={async (params, sort) => { + const res = await trpc.checkSummary.query({ + filter: [ + ...(params.id ? [{field: "id", operator: "eq" as const, value: Number(params.id)}] : []), + {field: "shippingId", operator: "eq", value: props.data.id}, + ], + search: params.keyword ?? "", + sort: { + field: Object.keys(sort).length > 0 ? Object.keys(sort)[0] : "id", + order: Object.values(sort)[0] === "descend"? "desc" : "asc", + }, + page: { + size: params.pageSize, + current: params.current, + }, + }); + return { + data: res.items, + success: true, + total: res.total, + }; + }} + /> + ); +}; + +export default CheckTable; diff --git a/apps/admin/src/component/data/shipping/fee.tsx b/apps/admin/src/component/data/shipping/fee.tsx new file mode 100644 index 0000000..efd41df --- /dev/null +++ b/apps/admin/src/component/data/shipping/fee.tsx @@ -0,0 +1,82 @@ +"use client"; +import React from "react"; +import BaseTable from "@repo/component/base/table"; +import trpc from "@/trpc/client"; +import {ProColumns} from "@ant-design/pro-table"; +import {Avatar, Space, Typography} from "antd"; +import {ShippingSchema} from "@repo/schema/shipping"; +import {mStd, rStd} from "@repo/util/data/string"; + +const FeeTable = (props: { + data: ShippingSchema, +}) => { + const columns: ProColumns[] = [ + {title: "ID", dataIndex: "id", sorter: true}, + { + title: "QQ", + dataIndex: "name", + sorter: true, + search: false, + render: (_, record) => ( + + +
+ {record.name} + {record.qq} +
+
+ ), + }, + { + title: "汇总", + dataIndex: "total", + sorter: true, + search: false, + render: (_, record) => mStd(record.total), + }, + { + title: "占比", + dataIndex: "ratio", + valueType: "percent", + search: false, + render: (_, record) => rStd(record.ratio), + }, + { + title: "运费", + dataIndex: "fee", + valueType: "money", + search: false, + }, + ]; + + return ( + { + const res = await trpc.weightSummary.query({ + filter: [ + ...(params.id ? [{field: "id", operator: "eq" as const, value: Number(params.id)}] : []), + {field: "shippingId", operator: "eq", value: props.data.id}, + ], + search: params.keyword ?? "", + sort: { + field: Object.keys(sort).length > 0 ? Object.keys(sort)[0] : "id", + order: Object.values(sort)[0] === "descend"? "desc" : "asc", + }, + page: { + size: params.pageSize, + current: params.current, + }, + }); + return { + data: res.items, + success: true, + total: res.total, + }; + }} + /> + ); +}; + +export default FeeTable; diff --git a/apps/admin/src/component/data/shipping/tax.tsx b/apps/admin/src/component/data/shipping/tax.tsx new file mode 100644 index 0000000..e7dd9d3 --- /dev/null +++ b/apps/admin/src/component/data/shipping/tax.tsx @@ -0,0 +1,83 @@ +"use client"; +import React from "react"; +import BaseTable from "@repo/component/base/table"; +import trpc from "@/trpc/client"; +import {ShippingSchema} from "@repo/schema/shipping"; +import {ProColumns} from "@ant-design/pro-table"; +import {Avatar, Space, Typography} from "antd"; +import {cStd, rStd} from "@repo/util/data/string"; + +const TaxTable = (props: { + data: ShippingSchema, +}) => { + const columns: ProColumns[] = [ + {title: "ID", dataIndex: "id", sorter: true}, + { + title: "QQ", + dataIndex: "name", + sorter: true, + search: false, + render: (_, record) => ( + + +
+ {record.name} + {record.qq} +
+
+ ), + }, + { + title: "汇总", + dataIndex: "total", + valueType: "money", + sorter: true, + search: false, + render: (_, record) => cStd(record.total), + }, + { + title: "占比", + dataIndex: "ratio", + valueType: "percent", + search: false, + render: (_, record) => rStd(record.ratio), + }, + { + title: "税费", + dataIndex: "tax", + valueType: "money", + search: false, + }, + ]; + + return ( + { + const res = await trpc.taxSummary.query({ + filter: [ + ...(params.id ? [{field: "id", operator: "eq" as const, value: Number(params.id)}] : []), + {field: "shippingId", operator: "eq", value: props.data.id}, + ], + search: params.keyword ?? "", + sort: { + field: Object.keys(sort).length > 0 ? Object.keys(sort)[0] : "id", + order: Object.values(sort)[0] === "descend"? "desc" : "asc", + }, + page: { + size: params.pageSize, + current: params.current, + }, + }); + return { + data: res.items, + success: true, + total: res.total, + }; + }} + /> + ); +}; + +export default TaxTable; diff --git a/apps/admin/src/component/data/summary/item.tsx b/apps/admin/src/component/data/summary/item.tsx new file mode 100644 index 0000000..360d2be --- /dev/null +++ b/apps/admin/src/component/data/summary/item.tsx @@ -0,0 +1,181 @@ +"use client"; +import React from "react"; +import UnselectWeight from "@/component/weight/unselect"; +import BaseModalForm from "@repo/component/base/modal"; +import PushTable from "@/component/form/table/push"; +import trpc from "@/trpc/client"; +import {ProColumns, ProTable, ProForm} from "@ant-design/pro-components"; +import {ItemData, ItemSchema} from "@repo/schema/item"; +import {App, Button, Image, Typography} from "antd"; +import {ActionType} from "@ant-design/pro-table"; +import {LinkOutlined} from "@ant-design/icons"; +import {GroupSchema} from "@repo/schema/group"; +import {TRPCClientError} from "@trpc/client"; +import {cStd} from "@repo/util/data/string"; + +const ItemTable = (props: { + data: GroupSchema, +}) => { + const [items, setItems] = React.useState([]); + const message = App.useApp().message; + const table = React.useRef(null); + const columns: ProColumns[] = [ + { + title: "ID", + dataIndex: "id", + sorter: true, + }, + { + title: "图片", + width: 100, + sorter: false, + search: false, + render: (_, record) => ( + record.image ? ( + {record.name} + ) : "-" + ), + }, + { + title: "商品", + dataIndex: "price", + sorter: true, + search: false, + render: (_, record) => ( +
+ {record.name} + + {cStd(Number(record.price))} + +
+ ), + }, + { + title: "总数量", + dataIndex: "count", + sorter: true, + search: false, + valueType: "digit", + }, + { + title: "已确认", + dataIndex: "confirmed", + sorter: true, + search: false, + valueType: "digit", + }, + { + title: "待下单", + dataIndex: "pending", + sorter: true, + search: false, + valueType: "digit", + }, + { + title: "总金额", + tooltip: "仅统计已确认的商品金额", + dataIndex: "total", + sorter: true, + search: false, + valueType: "money", + render: (_, record) => cStd(Number(record.total)), + }, + { + title: "操作", + valueType: "option", + width: 150, + render: (_, record) => [ + } + initialValues={{ + items: items, + }} + onFinish={async (record) => { + try { + await trpc.itemPush.mutate({ + items: record.items.map((i: ItemData & { + pending: number, + }) => ({ + id: i.id, + count: i.pending, + })), + }); + message.success("操作成功"); + onCleanSelected(); + table.current?.reload(); + return true; + } catch (e) { + if (e instanceof TRPCClientError) { + message.error(e.message); + } else { + message.error("发生未知错误"); + } + return false; + } + }} + > + + + + , + , + ]} + request={async (params, sort) => { + const res = await trpc.itemSummary.query({ + filter: [ + ...(params.id ? [{field: "id", operator: "eq" as const, value: Number(params.id)}] : []), + {field: "groupId", operator: "eq", value: props.data.id}, + ], + search: params.keyword ?? "", + sort: { + field: Object.keys(sort).length > 0 ? Object.keys(sort)[0] : "id", + order: Object.values(sort)[0] === "descend"? "desc" : "asc", + }, + page: { + size: params.pageSize, + current: params.current, + }, + }); + return { + data: res.items, + success: true, + total: res.total, + }; + }} + /> + ); +}; + +export default ItemTable; diff --git a/apps/admin/src/component/data/summary/overview.tsx b/apps/admin/src/component/data/summary/overview.tsx new file mode 100644 index 0000000..6d1ea33 --- /dev/null +++ b/apps/admin/src/component/data/summary/overview.tsx @@ -0,0 +1,116 @@ +"use client"; +import React from "react"; +import ItemSelector from "@/component/form/filter/item"; +import UserSelector from "@/component/form/filter/user"; +import BaseTable from "@repo/component/base/table"; +import trpc from "@/trpc/client"; +import {ProColumns} from "@ant-design/pro-table"; +import {Avatar, Space, Typography} from "antd"; +import {GroupSchema} from "@repo/schema/group"; +import {statusMap} from "@repo/schema/order"; +import {cStd} from "@repo/util/data/string"; + +const OverviewTable = (props: { + data: GroupSchema, +}) => { + const columns: ProColumns[] = [ + { + title: "ID", + dataIndex: "id", + sorter: true, + }, + { + title: "用户", + dataIndex: "userId", + sorter: true, + render: (_, record) => ( + + +
+ {record.user.name} + {record.user.qq} +
+
+ ), + renderFormItem: () => , + }, + { + title: "商品", + dataIndex: "itemId", + sorter: true, + render: (_, record) => ( +
+ {record.item.name} + {cStd(record.item.price)} +
+ ), + renderFormItem: () => , + }, + { + title: "数量", + dataIndex: "count", + sorter: true, + valueType: "digit", + search: false, + }, + { + title: "状态", + dataIndex: "status", + sorter: true, + valueEnum: statusMap, + valueType: "select", + }, + { + title: "创建时间", + dataIndex: "createdAt", + valueType: "dateTime", + sorter: true, + search: false, + }, + ]; + + return ( + { + const res = await trpc.orderGetAll.query({ + filter: [ + ...(params.id ? [{field: "id", operator: "eq" as const, value: Number(params.id)}] : []), + ...(params.userId ? [{ + field: "userId", + operator: "eq" as const, + value: Number(params.userId), + }] : []), + ...(params.itemId ? [{ + field: "itemId", + operator: "eq" as const, + value: Number(params.itemId), + }] : []), + ...(params.status ? [{ + field: "status", + operator: "eq" as const, + value: params.status, + }] : []), + {field: "item.groupId", operator: "eq", value: props.data.id}, + ], + sort: { + field: Object.keys(sort).length > 0 ? Object.keys(sort)[0] : "id", + order: Object.values(sort)[0] === "descend"? "desc" : "asc", + }, + page: { + size: params.pageSize, + current: params.current, + }, + }); + return { + data: res.items, + success: true, + total: res.total, + }; + }} + /> + ); +}; + +export default OverviewTable; diff --git a/apps/admin/src/component/data/summary/user.tsx b/apps/admin/src/component/data/summary/user.tsx new file mode 100644 index 0000000..4ccca7c --- /dev/null +++ b/apps/admin/src/component/data/summary/user.tsx @@ -0,0 +1,78 @@ +"use client"; +import React from "react"; +import BaseTable from "@repo/component/base/table"; +import trpc from "@/trpc/client"; +import {ProColumns} from "@ant-design/pro-table"; +import {Avatar, Space, Typography} from "antd"; +import {GroupSchema} from "@repo/schema/group"; +import {cStd} from "@repo/util/data/string"; + +const UserTable = (props: { + data: GroupSchema, +}) => { + const columns: ProColumns[] = [ + {title: "ID", dataIndex: "id", sorter: true}, + { + title: "QQ", + dataIndex: "name", + sorter: true, + search: false, + render: (_, record) => ( + + +
+ {record.name} + {record.qq} +
+
+ ), + }, + { + title: "汇总", + dataIndex: "total", + valueType: "money", + sorter: true, + search: false, + render: (_, record) => cStd(record.total), + }, + ]; + + return ( + { + const res = await trpc.userSummary.query({ + filter: [ + ...(params.id ? [{field: "id", operator: "eq" as const, value: Number(params.id)}] : []), + {field: "groupId", operator: "eq", value: props.data.id}, + ], + search: params.keyword ?? "", + sort: { + field: Object.keys(sort).length > 0 ? Object.keys(sort)[0] : "id", + order: Object.values(sort)[0] === "descend"? "desc" : "asc", + }, + page: { + size: params.pageSize, + current: params.current, + }, + }); + return { + data: res.items, + success: true, + total: res.total, + }; + }} + /> + ); +}; + +export default UserTable; diff --git a/apps/admin/src/component/form/filter/group.tsx b/apps/admin/src/component/form/filter/group.tsx new file mode 100644 index 0000000..3b4878f --- /dev/null +++ b/apps/admin/src/component/form/filter/group.tsx @@ -0,0 +1,93 @@ +"use client"; +import React from "react"; +import debounce from 'lodash/debounce'; +import trpc from "@/trpc/client"; +import {useParams, usePathname} from "next/navigation"; +import {dc, dd, sc, sd} from "@/component/match"; +import {Select, Spin, Typography} from "antd"; +import {Filter} from "@repo/util/data/query"; +import type {SelectProps} from "antd"; + +const GroupFilter = (props: { + value?: number, + onChange?: (value: number) => void, + isShow?: boolean, + userId?: number | null, // 指定用户ID +}) => { + const params = useParams(); + const pathname = usePathname(); + const [fetching, setFetching] = React.useState(true); + const [options, setOptions] = React.useState([]); + const debouncedFetch = React.useMemo( + () => { + const fetcher = (search: string) => { + setFetching(true); + const filter : Filter = []; + if (sd(pathname)) { + if (props.isShow) { + filter.push({field: "items.some.orders.some.shippingId", operator: "eq", value: null}); + filter.push({field: "ended", operator: "eq", value: false}); + } else { + filter.push({field: "items.some.orders.some.shippingId", operator: "eq", value: Number(params.shippingId)}); + } + } + if (sc(pathname)) { + filter.push({field: "items.some.orders.some.shippingId", operator: "eq", value: null}); + filter.push({field: "ended", operator: "eq", value: false}); + } + if (dc(pathname) || dd(pathname)) { + filter.push({field: "ended", operator: "eq", value: false}); + } + if (props.userId) { + filter.push({field: "items.some.orders.some.userId", operator: "eq", value: props.userId}); + } + trpc.groupGetAll.query({ + filter: filter, + search: search ?? "", + }).then(res => { + setOptions( + res.items.map((item) => ({ + ...item, + label: item.name, + value: item.id, + })), + ); + }).finally(() => setFetching(false)); + }; + return debounce(fetcher, 500); + }, + [params.shippingId, pathname, props.isShow, props.userId], + ); + React.useEffect( + () => { + debouncedFetch(""); + return () => debouncedFetch.cancel(); + }, + [debouncedFetch], + ); + + return ( + : undefined} + optionRender={(option) => { + return ( +
+ {option.data.name} + {ld(pathname) ? cStd(option.data.price) : option.data.group.name} +
+ ); + }} + /> + ); +}; + +export default ItemSelector; diff --git a/apps/admin/src/component/form/filter/user.tsx b/apps/admin/src/component/form/filter/user.tsx new file mode 100644 index 0000000..3cd61f0 --- /dev/null +++ b/apps/admin/src/component/form/filter/user.tsx @@ -0,0 +1,95 @@ +"use client"; +import React from "react"; +import debounce from 'lodash/debounce'; +import trpc from "@/trpc/client"; +import {Avatar, Select, Space, Spin, Typography} from "antd"; +import {dc, dd, ls, sc, sd} from "@/component/match"; +import {useParams, usePathname} from "next/navigation"; +import {Filter} from "@repo/util/data/query"; +import type {SelectProps} from "antd"; + +const UserFilter = (props: { + value?: number, + onChange?: (value: number) => void, + isShow?: boolean, // 是否只显示未分配用户 +}) => { + const pathname = usePathname(); + const params = useParams(); + const [fetching, setFetching] = React.useState(true); + const [options, setOptions] = React.useState([]); + const debouncedFetch = React.useMemo( + () => { + const fetcher = (search: string) => { + setFetching(true); + const filter : Filter = []; + if (ls(pathname)) { + filter.push({field: "lists.some.groupId", operator: "eq", value: Number(params.groupId)}); + } + if (sd(pathname)) { + if (props.isShow) { + filter.push({field: "orders.some.shippingId", operator: "eq", value: null}); + filter.push({field: "lists.some.group.ended", operator: "eq", value: false}); + } else { + filter.push({field: "orders.some.shippingId", operator: "eq", value: Number(params.shippingId)}); + } + } + if (sc(pathname)) { + filter.push({field: "orders.some.shippingId", operator: "eq", value: null}); + filter.push({field: "lists.some.group.ended", operator: "eq", value: false}); + } + if (dc(pathname) || dd(pathname)) { + filter.push({field: "lists.some.group.ended", operator: "eq", value: false}); + } + trpc.userGetAll.query({ + filter: filter, + search: search ?? "", + }).then(data => { + setOptions( + data.items.map((data) => ({ + ...data, + label: data.name, + value: data.id, + })), + ); + }).finally(() => setFetching(false)); + }; + return debounce(fetcher, 500); + }, + [params.groupId, params.shippingId, pathname, props.isShow], + ); + React.useEffect( + () => { + debouncedFetch(""); + return () => debouncedFetch.cancel(); + }, + [debouncedFetch], + ); + + return ( + : undefined} - optionRender={(option) => { - return ( -
- {option.label} - {option.data.qq} -
- ); - }} - /> - ); -}; - -export default GroupSelector; diff --git a/src/component/field/item.tsx b/src/component/field/item.tsx deleted file mode 100644 index 5ee21a9..0000000 --- a/src/component/field/item.tsx +++ /dev/null @@ -1,75 +0,0 @@ -"use client"; -import React from "react"; -import debounce from 'lodash/debounce'; -import trpc from "@/server/client"; -import {Select, Spin, Typography} from "antd"; -import {useParams} from "next/navigation"; -import type {SelectProps} from "antd"; -import {Item} from "@prisma/client"; -import {cStd} from "@/util/string"; - -interface Props { - value?: number; - onChange?: (value: number) => void; -} - -const ItemSelector = (props: Props) => { - const params = useParams(); - const [fetching, setFetching] = React.useState(true); - const [options, setOptions] = React.useState([]); - const debouncedFetch = React.useMemo( - () => { - const fetcher = (search: string) => { - setFetching(true); - trpc.item.get.query({ - params: { - keyword: search, - groupId: Number(params.groupId) - }, - }).then(res => { - setOptions( - res.items.map((item: Item) => ({ - ...item, - label: item.name, - value: item.id - })) - ); - }).finally(() => setFetching(false)); - }; - return debounce(fetcher, 500) - }, - [params.groupId] - ); - React.useEffect( - () => { - debouncedFetch(""); - return () => debouncedFetch.cancel(); - }, - [debouncedFetch] - ); - - return ( - : undefined} - optionRender={(option) => { - const qq = params.groupId ? option.data.user.qq : option.data.qq; - return ( - - -
- {option.label} - {qq} -
-
- ); - }} - /> - ); -}; - -export default UserSelector; diff --git a/src/component/form/group.tsx b/src/component/form/group.tsx deleted file mode 100644 index b1bab1e..0000000 --- a/src/component/form/group.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from "react"; -import {ModalForm, ProFormText} from "@ant-design/pro-form"; -import {GroupSchema} from "@/type/group"; - -interface Props { - title: string, - data?: GroupSchema, - target: React.ReactElement, - onSubmit: (values: Record) => Promise -} - -const GroupForm = (props: Props) => { - return ( - - - - - ); -} - -export default GroupForm; diff --git a/src/component/form/join.tsx b/src/component/form/join.tsx deleted file mode 100644 index 981426e..0000000 --- a/src/component/form/join.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from "react"; -import trpc from "@/server/client"; -import {ModalForm, ProFormSelect} from "@ant-design/pro-form"; -import {App, Avatar, Button, Space, Typography} from "antd"; -import {ActionType} from "@ant-design/pro-table"; -import {GroupData} from "@/type/group"; - -interface Props { - table?: ActionType - data: GroupData -} - -const JoinForm =(props: Props) => { - const message = App.useApp().message; - - return ( - 添加} - modalProps={{ - destroyOnHidden: true - }} - onFinish={async (values: Record) => { - try { - await trpc.group.user.add.mutate({ - groupId: props.data.id, - userId: values.userId - }); - message.success("添加成功"); - props.table?.reload(); - return true - } - catch { - message.error("该用户已存在"); - return false; - } - }} - > - ( - - -
- {option.data.name} - {option.data.qq} -
-
- ) - }} - request={async (props) => { - const res = await trpc.user.get.query({ - params: { - keyword: props.keyWords - } - }); - return res.items.map((user) => ({ - ...user, - label: user.name, - value: user.id - })); - }} - /> -
- ); -} - -export default JoinForm; diff --git a/src/component/layout.tsx b/src/component/layout.tsx deleted file mode 100644 index 7329fd8..0000000 --- a/src/component/layout.tsx +++ /dev/null @@ -1,99 +0,0 @@ -"use client"; -import React from "react"; -import Link from "next/link"; -import {SettingOutlined, ShoppingCartOutlined, TruckOutlined, UserOutlined} from "@ant-design/icons"; -import {ProLayout} from "@ant-design/pro-layout"; -import {usePathname} from "next/navigation"; -import {ConfigProvider, theme} from "antd"; - -interface Props { - children: React.ReactNode; -} - -const Layout = (props: Props) => { - const [isDark, setIsDark] = React.useState(false); - const pathname = usePathname(); - React.useEffect(() => { - const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); - const handler = (e: MediaQueryListEvent) => setIsDark(e.matches); - setIsDark(mediaQuery.matches); - mediaQuery.addEventListener('change', handler); - return () => { - mediaQuery.removeEventListener('change', handler) - }; - }, []); - - return ( - - , - children: [ - { - path: "/group/:groupId", - name: "团购管理", - hideInMenu: true - }, - { - path: "/group/:groupId/user/:userId", - name: "用户管理", - hideInMenu: true - }, - { - path: "/group/:groupId/summary", - name: "报表", - hideInMenu: true - } - ] - }, - { - path: "/user", - name: "用户", - icon: - }, - { - path: "/delivery", - name: "分发", - icon: , - children: [ - {path: "/delivery/create", name: "创建运单", hideInMenu: true}, - ] - }, - /* todo:因为无法过审暂停 - { - path: "/payment", - name: "账单", - icon: - }, - */ - { - path: "/setting", - name: "设置", - icon: - } - ] - }} - breadcrumbRender={(routes = []) => routes} - itemRender={(route) => {route.title}} - menuItemRender={(item, defaultDom) => ( - - {defaultDom} - - )} - > - {props.children} - - - ); -} - -export default Layout; diff --git a/src/component/status/group.tsx b/src/component/status/group.tsx deleted file mode 100644 index 495032b..0000000 --- a/src/component/status/group.tsx +++ /dev/null @@ -1,42 +0,0 @@ -"use client"; -import React from "react"; -import RcResizeObserver from 'rc-resize-observer'; -import {ProCard} from "@ant-design/pro-components"; -import {Divider, Statistic} from "antd"; - -interface Props { - user: number; - item: number; - order: number; - delivery: number; - price: number; - weight: number; -} - -const GroupStatus = (props: Props) => { - const [responsive, setResponsive] = React.useState(false); - - return ( - setResponsive(offset.width < 596)}> - - - - - - - - - - - - - - - - - - - ); -} - -export default GroupStatus; diff --git a/src/component/status/summary.tsx b/src/component/status/summary.tsx deleted file mode 100644 index 8db3d4a..0000000 --- a/src/component/status/summary.tsx +++ /dev/null @@ -1,31 +0,0 @@ -"use client"; - -import React from "react"; -import RcResizeObserver from 'rc-resize-observer'; -import {ProCard} from "@ant-design/pro-components"; -import {Divider, Statistic} from "antd"; - -interface Props { - price: number; - weight: number; -} - -const SummaryStatus = (props: Props) => { - const [responsive, setResponsive] = React.useState(false); - - return ( - setResponsive(offset.width < 596)}> - - - - - - - - - - - ); -} - -export default SummaryStatus; diff --git a/src/component/table/delivery.tsx b/src/component/table/delivery.tsx deleted file mode 100644 index 7c54182..0000000 --- a/src/component/table/delivery.tsx +++ /dev/null @@ -1,118 +0,0 @@ -"use client"; -import React from "react"; -import UserSelector from "@/component/field/user"; -import GroupSelector from "@/component/field/group"; -import trpc from "@/server/client"; -import {useControlModel, WithControlPropsType} from "@ant-design/pro-form"; -import {Avatar, Button, Popover, Space, Typography} from "antd"; -import {ProColumns, ProTable} from "@ant-design/pro-table"; -import {MessageOutlined} from "@ant-design/icons"; -import {UserSchema} from "@/type/user"; - -type Props = WithControlPropsType<{ - callback: React.Dispatch> -}> - -const DeliveryTable = (props: Props) => { - const model = useControlModel(props); - const columns: ProColumns[] = [ - { - title: "ID", - dataIndex: "id", - sorter: true - }, - { - title: "用户", - dataIndex: "userId", - sorter: true, - renderFormItem: () => , - render: (_, record) => ( - - -
- {record.user.name} - {record.user.qq} -
-
- ) - }, - { - title: "商品", - dataIndex: "itemId", - sorter: true, - render: (_, record) => ( -
- {record.item.name} - {record.item.group.name} -
- ) - }, - { - title: "团购", - dataIndex: ["item", "groupId"], - hidden: true, - search: false, - renderFormItem: () => - }, - { - title: "数量", - dataIndex: "count", - valueType: "digit", - sorter: true, - search: false - }, - { - title: "创建时间", - dataIndex: "createAt", - valueType: "dateTime", - sorter: true, - search: false - }, - { - title: "操作", - valueType: "option", - width: 150, - render: (_, record) => [ - - } - onSubmit={async (values: Record) => { - try { - values.groupId = props.data.id; - await trpc.item.create.mutate(values as ItemSchema); - message.success("添加成功"); - table.current?.reload(); - return true; - } - catch { - message.error("该商品已存在") - return false; - } - }} - /> - ]} - request={async (params, sort) => { - const res = await trpc.item.get.query({ - params: { - ...params, - groupId: props.data.id - }, - sort - }); - return { - data: res.items, - success: true, - total: res.total - } - }} - /> - ); -} - -export default ItemTable; diff --git a/src/component/table/join.tsx b/src/component/table/join.tsx deleted file mode 100644 index eca0491..0000000 --- a/src/component/table/join.tsx +++ /dev/null @@ -1,99 +0,0 @@ -"use client"; -import React from "react"; -import JoinForm from "@/component/form/join"; -import trpc from "@/server/client"; -import Link from "next/link"; -import {ActionType, ProColumns, ProTable} from "@ant-design/pro-table"; -import {Avatar, Button, Space, Typography} from "antd"; -import {SettingOutlined} from "@ant-design/icons"; -import {SortOrder} from "antd/es/table/interface"; -import {GroupData, JoinData} from "@/type/group"; - -interface Props { - data: GroupData -} - -const JoinTable = (props: Props) => { - const table = React.useRef(undefined); - const columns: ProColumns[] = [ - { - title: "ID", - dataIndex: "userId", - sorter: true - }, - { - title: "QQ", - dataIndex: [ - "user", - "qq" - ], - sorter: true, - search: false, - render: (_, record) => ( - - -
- {record.user.name} - {record.user.qq} -
-
- ) - }, - { - title: "邮箱", - dataIndex: [ - "user", - "email" - ], - sorter: true, - search: false - }, - { - title: "加入时间", - dataIndex: "createAt", - valueType: "dateTime", - sorter: true, - search: false - }, - { - title: "操作", - valueType: "option", - width: 150, - render: (_, record) => [ - - } - initialValues={{ - tax: result?.tax - }} - modalProps={{ - destroyOnHidden: true - }} - onFinish={async (values) => { - await db.localData.put({ - ...result, - id: props.group.id, - tax: values.tax - }); - return true; - }} - > - - - ]} - /> - ); -} - -export default UserTable; diff --git a/src/component/table/summary/weight.tsx b/src/component/table/summary/weight.tsx deleted file mode 100644 index 1d1465c..0000000 --- a/src/component/table/summary/weight.tsx +++ /dev/null @@ -1,105 +0,0 @@ -"use client"; -import React from "react"; -import {ModalForm, ProFormMoney} from "@ant-design/pro-form"; -import {ProColumns, ProTable} from "@ant-design/pro-table"; -import {Avatar, Button, Space, Typography} from "antd"; -import {useLiveQuery} from "dexie-react-hooks"; -import {mStd, rStd} from "@/util/string"; -import {db} from "@/util/data/indexedDB"; -import {GroupData} from "@/type/group"; -import {Weight} from "@/type/summary"; - -interface Props { - data: Weight[] - group: GroupData -} - -const WeightTable = (props: Props) => { - const result = useLiveQuery( - async () => await db.localData.where("id").equals(props.group.id).first(), - [props.group.id], - {fee: 0} // 默认值 - ) - const columns: ProColumns[] = [ - { - title: "ID", - dataIndex: "id", - sorter: (a, b) => a.id - b.id - }, - { - title: "QQ", - dataIndex: "qq", - sorter: (a, b) => Number(a.qq) - Number(b.qq), - render: (_, record) => ( - - -
- {record.name} - {record.qq} -
-
- ) - }, - { - title: "总重", - dataIndex: "total", - valueType: "digit", - sorter: (a, b) => a.total - b.total, - render: (_, record) => mStd(record.total) - }, - { - title: "占比", - dataIndex: "ratio", - valueType: "percent", - sorter: false, - render: (_, record) => rStd(record.ratio) - }, - { - title: "运费", - dataIndex: "fee", - valueType: "money", - sorter: false, - render: (_, record) => new Intl.NumberFormat("zh-CN", { - style: "currency", - currency: "CNY" - }).format((result?.fee || 0) * record.ratio) - } - ]; - - return ( - [ - 运费计算} - initialValues={{ - fee: result?.fee - }} - modalProps={{ - destroyOnHidden: true - }} - onFinish={async (values) => { - await db.localData.put({ - ...result, - id: props.group.id, - fee: values.fee - }); - return true; - }} - > - - - ]} - /> - ); -} - -export default WeightTable; diff --git a/src/server/context.ts b/src/server/context.ts deleted file mode 100644 index 5b70024..0000000 --- a/src/server/context.ts +++ /dev/null @@ -1,7 +0,0 @@ -import database from "@/util/data/database"; - -export const createContext = async () => { - return {database}; -}; - -export type Context = Awaited>; diff --git a/src/server/index.ts b/src/server/index.ts deleted file mode 100644 index ff58aec..0000000 --- a/src/server/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import userRouter from "@/server/route/user"; -import groupRouter from "@/server/route/group"; -import deliveryRouter from "@/server/route/delivery"; -import itemRouter from "@/server/route/item"; -import orderRouter from "@/server/route/order"; -import settingRouter from "@/server/route/setting"; -import {router} from "@/server/loader"; - -const appRouter = router({ - user: userRouter, - group: groupRouter, - item: itemRouter, - order: orderRouter, - delivery: deliveryRouter, - setting: settingRouter -}) - -export default appRouter; diff --git a/src/server/loader.ts b/src/server/loader.ts deleted file mode 100644 index 09b0a8b..0000000 --- a/src/server/loader.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {initTRPC} from '@trpc/server'; -import {Context} from '@/server/context'; - -const t = initTRPC.context().create(); - -export const router = t.router; -export const publicProcedure = t.procedure; diff --git a/src/server/route/delivery.ts b/src/server/route/delivery.ts deleted file mode 100644 index 21a0454..0000000 --- a/src/server/route/delivery.ts +++ /dev/null @@ -1,186 +0,0 @@ -import $ from "@/util/client/kd100"; -import {publicProcedure, router} from "@/server/loader"; -import {queryParams, queryParser} from "@/util/query"; -import {deliverySchema} from "@/type/delivery"; -import {TRPCError} from "@trpc/server"; -import {number, object} from "zod"; -import {parse} from "@/util/data/setting"; - -const deliveryRouter = router({ - get: publicProcedure.input(queryParams).query(async ({ctx, input}) => { - const query = queryParser(input, ["name", "phone"]) - return { - items: await ctx.database.delivery.findMany({...query, include: {orders: true}}), - total: await ctx.database.delivery.count({where: query.where}) - }; - }), - - create: publicProcedure.input(deliverySchema.omit({ - id: true - })).mutation(async ({ctx, input}) => { - return await ctx.database.delivery.create({ - data: { - ...input, - orders: { - connect: input.orders.map((id: number) => ({id})) - } - } - }); - }), - - update: publicProcedure.input(deliverySchema.omit({ - orders: true, - status: true - })).mutation(async ({ctx, input}) => { - const {id, ...data} = input; - if (!await ctx.database.delivery.findUnique({ - where: { - id, - status: "pending" - } - })) { - throw new TRPCError({ - code: "CONFLICT", - message: "该运单已推送不允许修改" - }); - } - return await ctx.database.delivery.update({ - data: data, - where: {id} - }); - }), - - delete: publicProcedure.input(deliverySchema.pick({ - id: true - })).mutation(async ({ctx, input}) => { - if (!await ctx.database.delivery.findUnique({ - where: { - id: input.id, - status: "pending" - } - })) { - throw new TRPCError({ - code: "CONFLICT", - message: "该订单已推送不允许删除" - }); - } - await ctx.database.delivery.delete({ - where: { - id: input.id, - status: "pending" - } - }); - }), - - flow: publicProcedure.input(deliverySchema.pick({ - id: true - })).mutation(async ({ctx, input}) => { - const item = await ctx.database.delivery.findUnique({ - where: { - id: input.id, - status: { - in: ["pushed", "waiting"] - } - } - }); - if (!item) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "该运单不存在或已取消" - }); - } - const url = "https://order.kuaidi100.com/order/corderapi.do" - const payload = new URLSearchParams(); - payload.set("param", JSON.stringify({ - taskId: item.taskId, - orderId: item.expressId, - cancelMsg: "用户取消" - })); - payload.set("method", "cancel"); - const res = await $.post(url, payload); - if (!res.data.result) { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "取消运单失败,请前往管理后台查看详情" - }); - } - await ctx.database.order.updateMany({ - where: { - deliveryId: input.id - }, - data: { - deliveryId: null - } - }) - return await ctx.database.delivery.update({ - where: { - id: input.id - }, - data: { - status: "failed" - } - }) - }), - - push: publicProcedure.input(object({ - ids: number().array() - })).mutation(async ({ctx, input}) => { - const setting = await parse() - const items = await ctx.database.delivery.findMany({ - where: { - id: { - in: input.ids - }, - status: "pending" - } - }) - if (items.length === 0) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "没有需要推送的运单" - }) - } - const url = "https://order.kuaidi100.com/order/corderapi.do" - let error = false; - for (const item of items) { - const payload = new URLSearchParams(); - payload.set("param", JSON.stringify({ - kuaidicom: item.method, - recManName: item.name, - recManMobile: item.phone, - recManPrintAddr: item.address, - sendManName: setting.name, - sendManMobile: setting.phone, - sendManPrintAddr: setting.address, - callBackUrl: setting.callback, - salt: process.env.APP_KEY, - cargo: setting.cargo - })); - payload.set("method", "cOrder"); - const res = await $.post(url, payload); - if (!res.data.result) { - error = true; - continue; - } - await ctx.database.delivery.update({ - where: { - id: item.id - }, - data: { - status: "pushed", - taskId: res.data.data?.taskId ?? null, - expressId: res.data.data?.orderId ?? null, - expressNumber: res.data.data?.kuaidinum ?? null - } - }) - } - if (error) { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "部分运单推送失败,请前往管理后台查看详情" - }); - } - }) -}) - -export default deliveryRouter; diff --git a/src/server/route/group.ts b/src/server/route/group.ts deleted file mode 100644 index b11fbb2..0000000 --- a/src/server/route/group.ts +++ /dev/null @@ -1,138 +0,0 @@ -import {publicProcedure, router} from "@/server/loader"; -import {queryParams, queryParser} from "@/util/query"; -import {groupSchema, joinSchema} from "@/type/group"; -import {TRPCError} from "@trpc/server"; -import {number, object} from "zod"; - -const user = router({ - get: publicProcedure.input(object({ - groupId: number(), - ...queryParams.shape - })).query(async ({ctx, input}) => { - const query = queryParser(input, ["user.name", "user.qq", "user.email"], { - groupId: input.groupId - }); - return { - items: await ctx.database.join.findMany({ - ...query, - include: { - user: true - } - }), - total: await ctx.database.join.count({ - where: query.where - }) - } - }), - - add: publicProcedure.input(joinSchema).mutation(async ({ctx, input}) => { - return await ctx.database.join.create({ - data: { - userId: input.userId, - groupId: input.groupId - } - }); - }), - - delete: publicProcedure.input(joinSchema.pick({ - groupId: true, - userId: true - })).mutation(async ({ctx, input}) => { - if (await ctx.database.order.count({ - where: { - item: { - groupId: input.groupId - }, - userId: input.userId, - status: { - not: "failed" - } - } - }) != 0) { - throw new TRPCError({ - code: "CONFLICT", - message: "该用户存在订单且在正常状态不得移除" - }) - } - await ctx.database.join.delete({ - where: { - userId_groupId: { - userId: input.userId, - groupId: input.groupId - } - } - }); - }) -}); - -const groupRouter = router({ - get: publicProcedure.input(queryParams).query(async ({ctx, input}) => { - const query = queryParser(input, ["qq", "name"]) - return { - items: await ctx.database.group.findMany(query), - total: await ctx.database.group.count({ - where: query.where - }) - }; - }), - - create: publicProcedure.input(groupSchema.omit({ - id: true, - status: true - })).mutation(async ({ctx, input}) => { - return await ctx.database.group.create({ - data: { - ...input, - status: "activated" - } - }); - }), - - update: publicProcedure.input(groupSchema).mutation(async ({ctx, input}) => { - const {id, ...data} = input; - return await ctx.database.group.update({ - data: data, - where: { - id: id - } - }); - }), - - delete: publicProcedure.input(groupSchema.pick({ - id: true - })).mutation(async ({ctx, input}) => { - if (await ctx.database.join.count({ - where: { - groupId: input.id - } - }) != 0) { - throw new TRPCError({ - code: "CONFLICT", - message: "该群组存在用户不得删除" - }); - } - await ctx.database.group.delete({ - where: { - id: input.id - } - }); - }), - - flow: publicProcedure.input(groupSchema.pick({ - id: true, - })).mutation(async ({ctx, input}) => { - await ctx.database.group.update({ - where: { - id: input.id, - status: "activated" - }, - data: { - status: "stopped", - } - }); - }), - - user -}); - -export default groupRouter; diff --git a/src/server/route/item.ts b/src/server/route/item.ts deleted file mode 100644 index a368b8e..0000000 --- a/src/server/route/item.ts +++ /dev/null @@ -1,75 +0,0 @@ -import parseItem from "@/util/item"; -import {publicProcedure, router} from "@/server/loader"; -import {queryParams, queryParser} from "@/util/query"; -import {itemSchema} from "@/type/item"; -import {TRPCError} from "@trpc/server"; -import {object, string} from "zod"; - -const itemRouter = router({ - getInfo: publicProcedure.input(object({ - url: string().url() - })).query(async ({input}) => { - const result = await parseItem(input.url); - if (!result) { - throw new TRPCError({ - code: "NOT_FOUND" - }); - } - return result; - }), - - get: publicProcedure.input(queryParams).query(async ({ctx, input}) => { - const query = queryParser(input, ["name"]); - return { - items: await ctx.database.item.findMany(query), - total: await ctx.database.item.count({ - where: query.where - }) - }; - }), - - create: publicProcedure.input(itemSchema.omit({ - id: true - })).mutation(async ({ctx, input}) => { - return await ctx.database.item.create({ - data: input - }); - }), - - update: publicProcedure.input(itemSchema.omit({ - groupId: true - })).mutation(async ({ctx, input}) => { - const {id, ...data} = input; - return await ctx.database.item.update({ - data: data, - where: { - id: id - } - }); - }), - - delete: publicProcedure.input(itemSchema.pick({ - id: true - })).mutation(async ({ctx, input}) => { - if (await ctx.database.order.count({ - where: { - itemId: input.id, - status: { - not: "failed" - } - } - }) != 0) { - throw new TRPCError({ - code: "CONFLICT", - message: "该商品有相关联的订单且为正常状态不得删除" - }); - } - await ctx.database.item.delete({ - where: { - id: input.id - } - }); - }) -}) - -export default itemRouter; diff --git a/src/server/route/order.ts b/src/server/route/order.ts deleted file mode 100644 index 50ae0f3..0000000 --- a/src/server/route/order.ts +++ /dev/null @@ -1,151 +0,0 @@ -import {publicProcedure, router} from "@/server/loader"; -import {queryParams, queryParser} from "@/util/query"; -import {orderSchema, statusMap} from "@/type/order"; -import {TRPCError} from "@trpc/server"; -import {number} from "zod"; - -const flow = { - failed: ["pushed", "confirmed"], - finished: ["arrived"], - confirmed: ["pending"], - arrived: ["pushed"], - pending: [], - pushed: [] -}; - -const orderRouter = router({ - get: publicProcedure.input(queryParams).query(async ({ctx, input}) => { - const query = queryParser(input, []); - return { - items: await ctx.database.order.findMany({ - ...query, - include: { - user: true, - delivery: true, - item: { - include: { - group: true, - } - } - } - }), - total: await ctx.database.order.count({where: query.where}) - }; - }), - - create: publicProcedure.input(orderSchema.pick({ - count: true, - comment: true, - userId: true - }).extend({ - itemIds: number().array() - })).mutation(async ({ctx, input}) => { - const {itemIds, ...data} = input; - return await ctx.database.order.createMany({ - data: itemIds.map(itemId => ({ - ...data, - itemId: itemId, - userId: input.userId, - status: "pending" - })) - }); - }), - - update: publicProcedure.input(orderSchema.omit({ - userId: true, - itemId: true, - status: true - })).mutation(async ({ctx, input}) => { - const {id, ...data} = input; - if (!await ctx.database.order.findUnique({ - where: { - id: id, - status: "pending" - } - })) { - throw new TRPCError({ - code: "CONFLICT", - message: "该订单已确认不允许修改" - }); - } - return await ctx.database.order.update({ - data: data, - where: { - id: id - } - }); - }), - - delete: publicProcedure.input(orderSchema.pick({ - id: true - })).mutation(async ({ctx, input}) => { - if (!await ctx.database.order.findUnique({ - where: { - id: input.id, - status: "pending" - } - })) { - throw new TRPCError({ - code: "CONFLICT", - message: "该订单已确认不允许取消" - }); - } - await ctx.database.order.delete({ - where: { - id: input.id - } - }); - }), - - flow: publicProcedure.input(orderSchema.pick({ - status: true - }).extend({ - ids: number().array() - })).mutation(async ({ctx, input}) => { - await ctx.database.order.updateMany({ - where: { - id: { - in: input.ids - }, - status: { - in: flow[input.status as keyof typeof statusMap], - } - }, - data: { - status: input.status - } - }) - }), - - push: publicProcedure.input(orderSchema.pick({ - itemId: true, - count: true - })).mutation(async ({ctx, input}) => { - const items = await ctx.database.order.findMany({ - where: { - status: "confirmed", - item: { - id: input.itemId, - group: { - status: { - not: "finished" - } - } - } - }, - take: input.count - }) - await ctx.database.order.updateMany({ - where: { - id: { - in: items.map(i => i.id) - } - }, - data: { - status: "pushed" - } - }) - }) -}); - -export default orderRouter; diff --git a/src/server/route/setting.ts b/src/server/route/setting.ts deleted file mode 100644 index 19bb266..0000000 --- a/src/server/route/setting.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {publicProcedure, router} from "@/server/loader"; -import {settingSchema} from "@/type/setting"; -import {parse, update} from "@/util/data/setting"; - -const settingRouter = router({ - get: publicProcedure.query(async () => { - return await parse() - }), - - update: publicProcedure.input(settingSchema).mutation(async ({input}) => { - await update(input) - }) -}); - -export default settingRouter; diff --git a/src/server/route/user.ts b/src/server/route/user.ts deleted file mode 100644 index 44feb72..0000000 --- a/src/server/route/user.ts +++ /dev/null @@ -1,66 +0,0 @@ -import common from "@/util/client/common"; -import {publicProcedure, router} from "@/server/loader"; -import {queryParams, queryParser} from "@/util/query"; -import {TRPCError} from "@trpc/server"; -import {userSchema} from "@/type/user"; -import {object, string} from "zod"; - -const userRouter = router({ - get: publicProcedure.input(queryParams).query(async ({ctx, input}) => { - const query = queryParser(input, [ - "qq", - "name", - "email" - ]); - return { - items: await ctx.database.user.findMany(query), - total: await ctx.database.user.count({where: query.where}) - }; - }), - - getName: publicProcedure.input(object({ - qq: string().regex(/^\d+$/) - })).query(async ({input}) => { - const res = await common.get("https://jkapi.com/api/qqinfo", { - params: { - qq: input.qq - } - }) - if (!res.data.nick) { - throw new TRPCError({code: "NOT_FOUND"}) - } - return {name: res.data.nick}; - }), - - add: publicProcedure.input(userSchema.omit({ - id: true - })).mutation(async ({ctx, input}) => { - return await ctx.database.user.create({data: input}); - }), - - update: publicProcedure.input(userSchema).mutation(async ({ctx, input}) => { - const {id, ...data} = input; - return await ctx.database.user.update({data: data, where: {id: id}}); - }), - - delete: publicProcedure.input(userSchema.pick({ - id: true - })).mutation(async ({ctx, input}) => { - if (await ctx.database.order.count({ - where: { - userId: input.id, - status: { - not: "failed" - } - } - }) != 0) { - throw new TRPCError({ - code: "CONFLICT", - message: "该用户存在订单且在正常状态不得删除" - }) - } - await ctx.database.user.delete({where: {id: input.id}}); - }) -}) - -export default userRouter; diff --git a/src/type/delivery.ts b/src/type/delivery.ts deleted file mode 100644 index 27dda8b..0000000 --- a/src/type/delivery.ts +++ /dev/null @@ -1,78 +0,0 @@ -import React from "react"; -import ZtoIcon from "@/component/icon/zto"; -import YtoIcon from "@/component/icon/yto"; -import JdIcon from "@/component/icon/jd"; -import SfIcon from "@/component/icon/sf"; -import {number, object, string, infer as zInfer} from "zod"; -import {GroupSchema} from "@/type/group"; -import {OrderSchema} from "@/type/order"; -import {UserSchema} from "@/type/user"; -import {ItemSchema} from "@/type/item"; - -export const methodMap = { - shunfeng: { - text: "顺丰", - icon: React.createElement(SfIcon, null) - }, - zhongtong: { - text: "中通", - icon: React.createElement(ZtoIcon, null) - }, - jd: { - text: "京东", - icon: React.createElement(JdIcon, null) - }, - yuantong: { - text: "圆通", - icon: React.createElement(YtoIcon, null) - } -}; - -export const statusMap = { - pending: { - text: "待处理" - }, - pushed: { - text: "已推送" - }, - waiting: { - text: "待揽收" - }, - confirmed: { - text: "正在运输" - }, - finished: { - text: "已完成" - }, - warning: { - text: "异常" - }, - failed: { - text: "失败" - } -} - -export const deliverySchema = object({ - id: number(), - name: string(), - phone: string().regex(/^1\d{10}$/), - method: string(), - address: string(), - status: string().default("pending"), - orders: number().array(), - comment: string().nullable().default(null) -}) - -export type DeliveryData = Omit & { - orders: OrderSchema[] -} - -export type OrderData = OrderSchema & { - item: ItemSchema & { - group: GroupSchema - }, - user: UserSchema -} - - -export type DeliverySchema = zInfer; \ No newline at end of file diff --git a/src/type/group.ts b/src/type/group.ts deleted file mode 100644 index c651b74..0000000 --- a/src/type/group.ts +++ /dev/null @@ -1,40 +0,0 @@ -import {number, object, string, infer as zInfer, coerce} from "zod"; -import {UserSchema} from "@/type/user"; - -export const statusMap = { - activated: { - text: "进行中" - }, - stopped: { - text: "已截单" - }, - finished: { - text: "已结束" - } -} - -export const groupSchema = object({ - id: number(), - qq: string().regex(/^\d+$/), - name: string(), - status: string() -}) - -export const joinSchema = object({ - userId: number(), - groupId: number(), - createAt: coerce.date().default(new Date()) -}) - -export type GroupData = GroupSchema & { - status: string -} - -export type JoinData = JoinSchema & { - user: UserSchema, - group: GroupSchema -} - -export type GroupSchema = zInfer - -export type JoinSchema = zInfer diff --git a/src/type/item.ts b/src/type/item.ts deleted file mode 100644 index f09704f..0000000 --- a/src/type/item.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {boolean, number, object, string, infer as zInfer} from "zod"; - -export const statusMap = { - true: { - text: "已通过" - }, - false: { - text: "未通过" - } -} - -export const itemSchema = object({ - id: number(), - groupId: number(), - name: string(), - url: string().url(), - price: number(), - weight: number().nullable().default(null), - allowed: boolean().default(false) -}) - -export type ItemSchema = zInfer diff --git a/src/type/order.ts b/src/type/order.ts deleted file mode 100644 index be4bddc..0000000 --- a/src/type/order.ts +++ /dev/null @@ -1,42 +0,0 @@ -import {number, object, string, infer as zInfer} from "zod"; -import {DeliverySchema} from "@/type/delivery"; -import {UserSchema} from "@/type/user"; -import {ItemSchema} from "@/type/item"; - -export const statusMap = { - pending: { - text: "待处理" - }, - confirmed: { - text: "已确认" - }, - pushed: { - text: "已下单" - }, - arrived: { - text: "待发货" - }, - finished: { - text: "已完成" - }, - failed: { - text: "失败" - } -} - -export const orderSchema = object({ - id: number(), - userId: number(), - itemId: number(), - count: number().min(1).default(1), - status: string(), - comment: string().nullable().default(null) -}) - -export type OrderData = OrderSchema & { - item: ItemSchema, - user: UserSchema, - delivery: Omit | null -} - -export type OrderSchema = zInfer diff --git a/src/type/print.ts b/src/type/print.ts deleted file mode 100644 index 18e4a08..0000000 --- a/src/type/print.ts +++ /dev/null @@ -1,62 +0,0 @@ -export type ApiResponse = Record; - -export type PrinterRequest = Record; - -export interface SendMessage

> { - apiName: string; - parameter?: P; - displayScale?: number; -} - -export interface PrinterResponse { - apiName: string; - resultAck: T; -} - -export interface InitCanvasParams { - width: number; - height: number; - rotate: 0 | 90 | 180 | 270, - path: "", - verticalShift: 0, - HorizontalShift: 0 -} - -export interface DrawTextParams { - x: number; - y: number; - width: number; - height: number; - value: string; - fontFamily: ""; - rotate: 0 | 1 | 2 | 3; - fontSize: number; - textAlignHorizonral: 0 | 1 | 2; - textAlignVertical: 0 | 1 | 2; - letterSpacing: number; - lineSpacing: number; - lineMode: 1 | 2 | 4 | 6; - fontStyle: boolean[]; -} - -export interface DrawQrCodeParams { - x: number; - y: number; - height: number; - width: number; - value: string; - codeType: 31 | 32 | 33 | 34; - rotate: 0 | 90 | 180 | 270, -} - -export interface StartJobParams { - printDensity: number; - printLabelType: number; - printMode: number; - count: number; -} - -export interface InitCanvasParams { - width: number; - height: number; -} diff --git a/src/type/setting.ts b/src/type/setting.ts deleted file mode 100644 index 9cc4416..0000000 --- a/src/type/setting.ts +++ /dev/null @@ -1,12 +0,0 @@ -import {object, string, infer as zInfer} from "zod"; - -export const settingSchema = object({ - address: string().default(""), - name: string().default(""), - phone: string().default(""), - label: string().default(""), - callback: string().default(""), - cargo: string().default("") -}) - -export type SettingSchema = zInfer; diff --git a/src/type/summary.ts b/src/type/summary.ts deleted file mode 100644 index 1b87fe8..0000000 --- a/src/type/summary.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface User { - id: number; - qq: string; - total: number -} - -export interface Weight { - id: number; - qq: string; - total: number -} diff --git a/src/type/user.ts b/src/type/user.ts deleted file mode 100644 index 9c2dd90..0000000 --- a/src/type/user.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {number, object, string, infer as zInfer, coerce} from "zod"; - -export const userSchema = object({ - id: number(), - qq: string().regex(/^\d+$/), - name: string(), - email: string().email().nullable().default(null), - phone: string().regex(/^1\d{10}$/).nullable().default(null), - address: string().nullable().default(null), - createAt: coerce.date().default(new Date()), -}) - -export type UserSchema = zInfer diff --git a/src/util/data/indexedDB.ts b/src/util/data/indexedDB.ts deleted file mode 100644 index 7612af0..0000000 --- a/src/util/data/indexedDB.ts +++ /dev/null @@ -1,15 +0,0 @@ -import Dexie, { type EntityTable } from 'dexie'; - -export type LocalData = { - id: number; - tax?: number; - fee?: number; -} - -export const db = new Dexie('localDataDB') as Dexie & { - localData: EntityTable; -}; - -db.version(1).stores({ - localData: '++id, tax, fee' -}); diff --git a/src/util/data/setting.ts b/src/util/data/setting.ts deleted file mode 100644 index 07ee5db..0000000 --- a/src/util/data/setting.ts +++ /dev/null @@ -1,26 +0,0 @@ -import database from "@/util/data/database"; -import {SettingSchema, settingSchema} from "@/type/setting"; - -export const parse = async () => { - const raw = await database.setting.findMany(); - const result: Record = {}; - raw.forEach(({ key, value }) => { - result[key] = value; - }); - return settingSchema.parse(result); -} - -export const update = async (data: SettingSchema) => { - for (const [key, value] of Object.entries(data)) { - await database.setting.upsert({ - where: {key}, - update: { - value: String(value) - }, - create: { - value: String(value), - key - } - }); - } -} diff --git a/src/util/item/comic-zin.ts b/src/util/item/comic-zin.ts deleted file mode 100644 index d0e392c..0000000 --- a/src/util/item/comic-zin.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as cheerio from "cheerio"; -import {jStd} from "@/util/string"; - -export const match = /https:\/\/shop\.comiczin\.jp\/products\/detail\.php\?product_id=\d+/g; - -export const parse = async (url: URL) => { - const $ = await cheerio.fromURL(url); - const price = Number($("span.fnt_mark_color.fnt_size_12em").text().replace(/[^\d.]/g, "")); - const name = jStd($("h2.fw_main_block_header_type2.vb_space_15px").text()); - return {name, price, url: url.toString()}; -} diff --git a/src/util/item/melonbooks.ts b/src/util/item/melonbooks.ts deleted file mode 100644 index 828ee22..0000000 --- a/src/util/item/melonbooks.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as cheerio from "cheerio"; -import {jStd} from "@/util/string"; - -export const match = /^https:\/\/www\.melonbooks\.co\.jp\/detail\/detail\.php\?product_id=\d+$/; - -export const parse = async (url: URL) => { - const $ = await cheerio.fromURL(url); - const price = Number($("span.yen.__discount").text().replace(/[^\d.]/g, "")); - const name = jStd($("h1.page-header").text()); - return {name, price, url: url.toString()} -} diff --git a/src/util/query.ts b/src/util/query.ts deleted file mode 100644 index a628e88..0000000 --- a/src/util/query.ts +++ /dev/null @@ -1,78 +0,0 @@ -import {number, object, string, enum as zEnum, infer as zInfer} from "zod"; -import {any} from "zod"; -import {record} from "zod"; - -type Input = Record; - -const int = ["id", "userId", "groupId", "itemId", "deliveryId"] -const blocked = ["address", "phone", "keyword"] -const bool = ["allowed"] - -const stringToRecord = (key: string, value: Input) => { - const keys = key?.split("."); - return keys?.reverse().reduce>((acc, key) => ({[key]: acc}), value); -} - -export const queryParams = object({ - params: object({ - pageSize: number().default(20), - current: number().default(1), - keyword: string().optional(), - }).catchall(any()).transform((params) => { - for (const i in params) { - if (params[i] == null) { - } else if (int.includes(i)) { - params[i] = Number(params[i]); - } else if (bool.includes(i)) { - params[i] = (params[i] === "true"); - } - } - return params; - }), - sort: record(zEnum(["ascend", "descend"]).nullable()).transform((sort) => { - const field = Object.keys(sort)[0]; - const order = sort[field]; - if (!field || !order) { - return {id: "asc"}; - } - return {[field.replace(/,/g, '.')]: order === 'descend' ? 'desc' : 'asc'}; - }).default({"id": "ascend"}) -}) - -const searchBuilder = (keyword: string, fields: string[]) => { - const result = [] - const condition = { - contains: keyword, - mode: "insensitive" - } - for (const field of fields) { - if (int.includes(field) || bool.includes(field)) continue; - result.push(stringToRecord(field, condition)); - } - return result -} - -export const queryParser = ( - query: zInfer, - fields: string[], - where: Input = {} -) => { - const {pageSize, current, keyword, ...filter} = query.params; - const builder: Input = {}; - if (keyword) { - builder.OR = searchBuilder(keyword || "", fields); - } - const filters = Object.fromEntries(Object.entries(filter).filter(([key]) => !blocked.includes(key))) - for (const key in filters) { - builder[key] = filters[key]; - } - for (const key in where) { - builder[key] = where[key]; - } - return { - where: builder, - orderBy: stringToRecord(Object.keys(query.sort)[0], Object.values(query.sort)[0]) as Record, - skip: (current - 1) * pageSize, - take: pageSize - } -} diff --git a/src/util/summary.ts b/src/util/summary.ts deleted file mode 100644 index 0dbe62e..0000000 --- a/src/util/summary.ts +++ /dev/null @@ -1,115 +0,0 @@ -import database from "@/util/data/database"; -import {User} from "@/type/summary"; - -export const summaryItem = async (id: number) => { - const sum = await database.order.groupBy({ - by: [ - "itemId" - ], - where: { - item: { - groupId: id - }, - status: { - not: "failed" - } - }, - _sum: { - count: true, - } - }); - const items = new Map((await database.item.findMany({ - where: { - id: { - in: sum.map(i => i.itemId) - } - } - })).map(i => [i.id, i])) - return sum.map(i => ({ - id: i.itemId, - name: items.get(i.itemId)?.name, - url: items.get(i.itemId)?.url, - price: items.get(i.itemId)?.price, - count: i._sum.count, - total: (items.get(i.itemId)?.price ?? 0) * (i._sum.count ?? 0) - })) -} - -export const summaryUser = async (id: number) => { - const orders = await database.order.findMany({ - where: { - item: { - groupId: id - }, - status: { - not: "failed" - } - }, - include: { - item: { - select: { - price: true - } - }, - user: true - } - }); - let total = 0; - const userSpendMap: Record = {}; - for (const order of orders) { - const amount = order.count * order.item.price; - userSpendMap[order.userId] = { - ...order.user, - total: (userSpendMap[order.userId]?.total || 0) + amount - }; - total += amount; - } - return Object.entries(userSpendMap).map(([, result]) => ({ - ...result, - ratio: total === 0 ? 0 : result.total / total - })); -} - -export const summaryWeight = async (id: number) => { - const orders = await database.order.findMany({ - where: { - item: { - groupId: id - } - }, - include: { - user: true, - item: true - } - }); - const hasMissingWeight = orders.some(order => order.item.weight == null); - if (hasMissingWeight) { - return []; - } - const map = new Map(); - let grandTotal = 0; - for (const order of orders) { - const {id, name, qq} = order.user; - const weight = Number(order.item.weight); - const total = weight * order.count; - grandTotal += total; - if (!map.has(id)) { - map.set(id, { - id: id, - total: total, - name, qq - }); - } else { - map.get(id)!.total += total; - } - } - return Array.from(map.values()).map(user => ({ - ...user, - ratio: grandTotal === 0 ? 0 : user.total / grandTotal - })); -} \ No newline at end of file diff --git a/turbo.json b/turbo.json new file mode 100644 index 0000000..a4af495 --- /dev/null +++ b/turbo.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://turborepo.com/schema.json", + "ui": "tui", + "tasks": { + "prisma:generate": { + "outputs": ["node_modules/.prisma/**"] + }, + "build": { + "dependsOn": ["^prisma:generate", "^build"], + "inputs": ["$TURBO_DEFAULT$", ".env*"], + "outputs": [".next/**", "!.next/cache/**"] + }, + "lint": { + "dependsOn": ["^lint"] + }, + "check-types": { + "dependsOn": ["^check-types"] + }, + "dev": { + "cache": false, + "persistent": true + } + } +}