diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 1775a29..d25856b 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -12,22 +12,22 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - + - name: Setup Node.js uses: actions/setup-node@v4 with: node-version-file: .nvmrc - + - name: Use Yarn 4 run: corepack enable - + - name: Inject Environment Variables run: | echo "PUBLIC_API_URL=${{ vars.PUBLIC_API_URL }}" >> .env - + - name: Build run: yarn install --immutable && yarn build - + - name: Upload Build Artifacts uses: actions/upload-artifact@v4 with: @@ -35,29 +35,7 @@ jobs: path: dist/ retention-days: 1 - deploy-cloudflare: - runs-on: ubuntu-latest - needs: build - permissions: - contents: read - deployments: write - name: Deploy to Cloudflare Pages - steps: - - name: Download Build Artifacts - uses: actions/download-artifact@v4 - with: - name: dist-files - path: dist/ - - - name: Deploy to Cloudflare Pages - uses: cloudflare/wrangler-action@v3 - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - command: pages deploy dist --project-name=fcc-cms-6b4t - gitHubToken: ${{ secrets.GITHUB_TOKEN }} - - deploy-qcloud: + deploy-eo: runs-on: ubuntu-latest needs: build permissions: @@ -71,7 +49,7 @@ jobs: with: name: dist-files path: dist/ - + - name: Deploy to QCloud Pages id: deploy run: | @@ -81,7 +59,7 @@ jobs: else edgeone pages deploy ./dist -n fcc-dashboard -e preview -t ${{ secrets.QCLOUD_PAGES_TOKEN }} 2>&1 | tee deploy.log fi - + - name: Extract Preview URL id: extract-url run: | @@ -100,7 +78,7 @@ jobs: echo "Deploy log not found" echo "preview_url=" >> $GITHUB_OUTPUT fi - + - name: Comment on PR if: github.event_name == 'pull_request' && steps.extract-url.outputs.preview_url != '' uses: actions/github-script@v7 @@ -108,14 +86,14 @@ jobs: script: | const previewUrl = '${{ steps.extract-url.outputs.preview_url }}'; const comment = `🚀 **预览部署完成!** - + 📱 **预览地址**: ${previewUrl} - + > 此预览链接将在部署完成后可用,请稍等片刻再访问。 - + --- *由 GitHub Actions 自动生成*`; - + github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, diff --git a/.nvmrc b/.nvmrc index 9de2256..941ea48 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -lts/iron +lts/krypton \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..8f02c18 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "tabWidth": 2, + "printWidth": 120 +} diff --git a/README.md b/README.md index e3a9136..01e18ec 100644 --- a/README.md +++ b/README.md @@ -6,16 +6,16 @@ - **前端框架**: React 19 + TypeScript - **构建工具**: Rsbuild -- **UI 组件库**: Mantine + Ant Design + Tailwind CSS +- **UI 组件库**: Ant Design + Tailwind CSS - **状态管理**: Zustand + TanStack Query -- **表单处理**: Mantine Form + Zod +- **表单处理**: React Hook Form + Zod - **路由**: React Router DOM - **样式**: Tailwind CSS + PostCSS ## 开发环境要求 -- Node.js 18+ -- Yarn 4.4.1+ +- Node.js 22+ +- Yarn 4.6.1+ ## 快速开始 @@ -84,4 +84,4 @@ src/ ├── types/ # TypeScript 类型定义 ├── utils/ # 工具函数 └── styles/ # 样式文件 -``` \ No newline at end of file +``` diff --git a/package.json b/package.json index ca6d32e..36919ea 100644 --- a/package.json +++ b/package.json @@ -4,54 +4,43 @@ "version": "1.0.0", "scripts": { "build": "rsbuild build", - "check": "biome check --write", + "check": "prettier --check", "dev": "rsbuild dev --open", - "format": "biome format --write", + "format": "prettier . --write", "preview": "rsbuild preview" }, "dependencies": { - "@ant-design/v5-patch-for-react-19": "^1.0.3", - "@hello-pangea/dnd": "^18.0.1", - "@mantine/core": "^8.1.2", - "@mantine/dates": "^8.1.2", - "@mantine/dropzone": "^8.1.2", - "@mantine/form": "^8.1.2", - "@mantine/hooks": "^8.1.2", - "@mantine/notifications": "^8.1.2", - "@tabler/icons-react": "^3.34.0", - "@tailwindcss/forms": "^0.5.10", - "@tanstack/react-query": "^5.81.5", - "antd": "^5.26.3", - "axios": "^1.10.0", + "@hookform/resolvers": "^5.2.2", + "@tabler/icons-react": "^3.36.1", + "@tanstack/react-query": "^5.90.17", + "antd": "^6.2.0", + "axios": "^1.13.2", "clsx": "^2.1.1", "cos-js-sdk-v5": "^1.10.1", - "dayjs": "^1.11.13", - "es-toolkit": "^1.39.6", - "lodash": "^4.17.21", - "mantine-form-zod-resolver": "^1.3.0", - "nanoid": "^5.1.5", - "nuqs": "^2.4.3", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "react-hook-form": "^7.60.0", - "react-router-dom": "^7.6.3", - "swr": "^2.3.4", - "tailwind-merge": "^2.6.0", - "zod": "^3.25.74", - "zustand": "^5.0.6" + "dayjs": "^1.11.19", + "es-toolkit": "^1.44.0", + "nanoid": "^5.1.6", + "nuqs": "^2.8.6", + "react": "^19.2.3", + "react-dom": "^19.2.3", + "react-hook-form": "^7.71.1", + "react-router-dom": "^7.12.0", + "swr": "^2.3.8", + "tailwind-merge": "^3.4.0", + "zod": "^4.3.5", + "zustand": "^5.0.10" }, "devDependencies": { - "@rsbuild/core": "1.4.3", - "@rsbuild/plugin-react": "1.3.3", - "@rsbuild/plugin-svgr": "^1.2.0", - "@types/lodash": "^4.17.20", - "@types/node": "^22.16.0", - "@types/react": "^19.1.8", - "@types/react-dom": "^19.1.6", - "postcss-preset-mantine": "^1.18.0", - "postcss-simple-vars": "^7.0.1", - "tailwindcss": "^3.4.17", - "typescript": "^5.8.3" + "@rsbuild/core": "1.7.2", + "@rsbuild/plugin-react": "1.4.3", + "@rsbuild/plugin-svgr": "^1.2.4", + "@tailwindcss/postcss": "^4.1.18", + "@types/node": "^22.19.7", + "@types/react": "^19.2.8", + "@types/react-dom": "^19.2.3", + "prettier": "3.8.0", + "tailwindcss": "^4.1.18", + "typescript": "^5.9.3" }, "packageManager": "yarn@4.4.1" } diff --git a/postcss.config.cjs b/postcss.config.cjs index 1578a2c..483f378 100644 --- a/postcss.config.cjs +++ b/postcss.config.cjs @@ -1,15 +1,5 @@ module.exports = { plugins: { - tailwindcss: {}, - "postcss-preset-mantine": {}, - "postcss-simple-vars": { - variables: { - "mantine-breakpoint-xs": "36em", - "mantine-breakpoint-sm": "48em", - "mantine-breakpoint-md": "62em", - "mantine-breakpoint-lg": "75em", - "mantine-breakpoint-xl": "88em", - }, - }, + "@tailwindcss/postcss": {}, }, }; diff --git a/rsbuild.config.ts b/rsbuild.config.ts index daec6ec..c60ee09 100644 --- a/rsbuild.config.ts +++ b/rsbuild.config.ts @@ -13,6 +13,6 @@ export default defineConfig({ define: publicVars, }, html: { - title: "FCC Dashboard", + title: "FCC 工作间", }, }); diff --git a/src/App.tsx b/src/App.tsx index 182fc0c..6f59a0d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,15 +3,11 @@ import { Navigate } from "react-router-dom"; import useAuthStore from "@/stores/auth"; import "@/api/interceptors"; -import FullScreenLoading from "@/components/Loading"; +import FullScreenLoading from "@/components/Layout/FullScreenLoading"; import DashboardLayout from "@/pages/dashboard/layout"; import "@/styles/global.css"; -import "@mantine/core/styles.css"; -import "@mantine/dates/styles.css"; -import "@mantine/notifications/styles.css"; - const App = () => { const { user, _hasHydrated } = useAuthStore(); diff --git a/src/api/auth/index.ts b/src/api/auth/index.ts index 6e8678d..425cd7d 100644 --- a/src/api/auth/index.ts +++ b/src/api/auth/index.ts @@ -1,11 +1,25 @@ import Axios from "@/api"; +import { z } from "zod"; import type { User } from "@/types/User"; +import { InferZodType } from "@/types/common"; -export async function login(data: { email: string; password: string }) { - const res = await Axios.post<{ token: string; user: User }>( - "/auth/login", - data - ); - return res.data; +export const UpdatePasswordApiBody = z.object({ + newPassword: z.string().min(8, "密码长度至少为8位"), +}); +export class AuthAPI { + static async login(data: { email: string; password: string }) { + const res = await Axios.post<{ token: string; user: User }>("/auth/login", data); + return res.data; + } + + static async getCurrentUser() { + const res = await Axios.get("/auth/me"); + return res.data; + } + + static async updatePassword(body: InferZodType) { + const res = await Axios.post("/auth/update-password", body); + return res.data; + } } diff --git a/src/api/dashboard/event.ts b/src/api/dashboard/event.ts index 7b965e0..7991881 100644 --- a/src/api/dashboard/event.ts +++ b/src/api/dashboard/event.ts @@ -1,49 +1,137 @@ import Axios from "@/api"; -import type { EditableEvent, EventItem } from "@/types/event"; +import { + EventLocationType, + EventTicketChannelType, + EventType, + type EditableEvent, + type EventItem, +} from "@/types/event"; import type { List } from "@/types/Request"; +import z from "zod"; -export async function getEventList(params: { - pageSize: number; - current: number; - search: string | null; - orgSearch: string | null; -}) { - const res = await Axios.get>("/internal/cms/event/list", { - params, - }); - - return res.data; -} +export const EditEventApiBody = z.object({ + name: z.string(), + slug: z.string(), + startAt: z.iso.datetime().nullish(), + endAt: z.iso.datetime().nullish(), + status: z.string(), + scale: z.string(), + type: z.enum(EventType).optional(), + locationType: z.enum(EventLocationType).optional(), + organizations: z.array( + z.object({ + id: z.uuid(), + isPrimary: z.boolean(), + }), + ), + regionId: z.uuid().optional(), + address: z.string().nullish(), + addressLat: z.string().nullish(), + addressLon: z.string().nullish(), + sources: z + .array( + z.object({ + url: z.string(), + name: z.string().nullish(), + description: z.string().nullish(), + }), + ) + .nullish(), + ticketChannels: z + .array( + z.object({ + type: z.enum(EventTicketChannelType), + name: z.string(), + url: z.string(), + available: z.boolean(), + }), + ) + .nullish(), -export async function getEventDetail(params: { id: string }) { - const res = await Axios.get(`/internal/cms/event/detail/${params.id}`); + thumbnail: z.string().optional(), + media: z + .object({ + images: z + .array( + z.object({ + url: z.string(), + title: z.string().nullish(), + description: z.string().nullish(), + }), + ) + .optional(), + videos: z + .array( + z.object({ + url: z.string(), + title: z.string().nullish(), + description: z.string().nullish(), + }), + ) + .optional(), + lives: z + .array( + z.object({ + url: z.string(), + title: z.string().nullish(), + description: z.string().nullish(), + }), + ) + .optional(), + }) + .optional(), + detail: z.string().nullish(), + features: z + .object({ + self: z.array(z.string()).optional(), + }) + .nullish(), + featureIds: z.array(z.string()).nullish(), + extra: z + .object({ + overrideOrganizationContact: z + .object({ + qqGroups: z.array(z.object({ label: z.string(), value: z.string() })).optional(), + }) + .optional(), + }) + .optional(), +}); - return res.data; -} +export class EventAPI { + static async getEventList(params: { pageSize: number; current: number; search?: string; orgSearch?: string }) { + const res = await Axios.get>("/internal/cms/event/list", { + params, + }); -export async function createEvent(event: EditableEvent) { - const res = await Axios.post("/internal/cms/event", { - event, - }); + return res.data; + } - return res.data; -} + static async getEventDetail(params: { id: string }) { + const res = await Axios.get(`/internal/cms/event/detail/${params.id}`); -export async function updateEvent(eventId: string, event: EditableEvent) { - const res = await Axios.post(`/internal/cms/event/${eventId}`, { - event, - }); + return res.data; + } - return res.data; -} + static async createEvent(event: EditableEvent) { + const res = await Axios.post("/internal/cms/event", { + event, + }); + + return res.data; + } + + static async updateEvent(eventId: string, event: EditableEvent) { + const res = await Axios.post(`/internal/cms/event/${eventId}`, { + event, + }); + + return res.data; + } -export async function deleteEvent(id: string) { - const res = await Axios.post<{ success: boolean }>( - `/internal/cms/event/${id}`, - { - id, - } - ); + static async deleteEvent(id: string) { + const res = await Axios.delete<{ success: boolean }>(`/internal/cms/event/${id}`); - return res.data; + return res.data; + } } diff --git a/src/api/dashboard/feature.ts b/src/api/dashboard/feature.ts index f2e3e89..6d32c0a 100644 --- a/src/api/dashboard/feature.ts +++ b/src/api/dashboard/feature.ts @@ -1,35 +1,33 @@ import Axios from "@/api"; -import type { - CrateFeatureType, - EditableFeatureType, - FeatureType, -} from "@/types/feature"; +import { InferZodType } from "@/types/common"; +import type { Feature } from "@/types/feature"; import type { List } from "@/types/Request"; +import { z } from "zod"; -export async function getFeatureList(params: { - pageSize: number; - current: number; - name?: string; -}) { - const res = await Axios.get>("/internal/cms/event/feature", { - params, - }); +export const EditFeatureApiBody = z.object({ + name: z.string().min(1), + category: z.string().min(1), + description: z.string().nullish(), +}); - return res.data; -} +export class FeatureAPI { + static async getFeatureList(params: { pageSize: number; current: number; name?: string }) { + const res = await Axios.get>("/internal/cms/event/feature", { + params, + }); -export async function createFeature(feature: CrateFeatureType) { - const res = await Axios.post("/internal/cms/event/feature/create", { - feature, - }); + return res.data; + } - return res.data; -} + static async createFeature(feature: InferZodType) { + const res = await Axios.post("/internal/cms/event/feature", feature); + + return res.data; + } -export async function updateFeature(feature: EditableFeatureType) { - const res = await Axios.post("/internal/cms/event/feature/update", { - feature, - }); + static async updateFeature(id: string, feature: InferZodType) { + const res = await Axios.post(`/internal/cms/event/feature/${id}`, feature); - return res.data; + return res.data; + } } diff --git a/src/api/dashboard/map.ts b/src/api/dashboard/map.ts index c767e88..682bdf1 100644 --- a/src/api/dashboard/map.ts +++ b/src/api/dashboard/map.ts @@ -1,13 +1,9 @@ import Axios from "@/api"; import { TencentLocation } from "@/types/map"; -export async function getTencentLocation(params: { - region: string; - keyword: string; -}) { - const res = await Axios.post<{ count: number; data: TencentLocation[] }>( - `/internal/infra/map/suggestion`, - { ...params } - ); +export async function getTencentLocation(params: { region: string; keyword: string }) { + const res = await Axios.post<{ count: number; data: TencentLocation[] }>(`/internal/infra/map/suggestion`, { + ...params, + }); return res.data; } diff --git a/src/api/dashboard/organization.ts b/src/api/dashboard/organization.ts index 4d6ea1d..3ed3544 100644 --- a/src/api/dashboard/organization.ts +++ b/src/api/dashboard/organization.ts @@ -1,72 +1,74 @@ import Axios from "@/api"; -import { - Organization, - EditableOrganizationType, -} from "@/types/organization"; +import { InferZodType } from "@/types/common"; +import { Organization, OrganizationStatus, OrganizationType } from "@/types/organization"; import { List } from "@/types/Request"; +import { z } from "zod"; -export async function getOrganizationList(params: { - pageSize: number; - current: number; - name?: string; - slug?: string; -}) { - const res = await Axios.get>("/internal/cms/organization/list", { - params, - }); +export const EditOrganizationApiBody = z.object({ + slug: z.string().min(1), + name: z.string().min(1), + description: z.string().nullish(), + status: z.enum(OrganizationStatus), + type: z.enum(OrganizationType), + logoUrl: z.string().nullish(), + richMediaConfig: z.any().nullish(), + contactMail: z.email().nullish(), + website: z.url().nullish(), + twitter: z.url().nullish(), + weibo: z.url().nullish(), + qqGroup: z.string().nullish(), + bilibili: z.url().nullish(), + rednote: z.url().nullish(), + wikifur: z.url().nullish(), + facebook: z.url().nullish(), + plurk: z.url().nullish(), + extraMedia: z + .object({ + qqGroups: z + .array( + z.object({ + label: z.string(), + value: z.string(), + }), + ) + .optional(), + }) + .nullish(), + creationTime: z.iso.datetime().nullish(), +}); - return res.data; -} - -export async function getAllOrganizations(params: { search?: string }) { - const res = await Axios.get("/internal/cms/organization/all", { - params, - }); +export class OrganizationAPI { + static async getOrganizationList(params: { pageSize: number; current: number; name?: string; slug?: string }) { + const res = await Axios.get>("/internal/cms/organization/list", { + params, + }); - return res.data; -} + return res.data; + } -export async function getOrganizationDetail(params: { id: string }) { - const res = await Axios.get( - `/internal/cms/organization/detail/${params.id}` - ); + static async getOrganizationDetail(params: { id: string }) { + const res = await Axios.get(`/internal/cms/organization/${params.id}`); - return res.data; -} + return res.data; + } -export async function createOrganization( - organization: EditableOrganizationType -) { - const res = await Axios.post( - "/internal/cms/organization/create", - { + static async createOrganization(organization: InferZodType) { + const res = await Axios.post("/internal/cms/organization", { organization, - } - ); + }); - return res.data; -} + return res.data; + } -export async function updateOrganization( - organization: EditableOrganizationType -) { - const res = await Axios.post( - "/internal/cms/organization/update", - { - organization, - } - ); + static async updateOrganization(id: string, organization: InferZodType) { + const res = await Axios.post(`/internal/cms/organization/${id}`, organization); - return res.data; -} + return res.data; + } -export async function deleteOrganization(id: string) { - const res = await Axios.post<{ success: boolean }>( - "/internal/cms/organization/delete", - { - id, - } - ); + static async deleteOrganization(id: string) { + const res = await Axios.delete<{ success: boolean }>(`/internal/cms/organization/${id}`); - return res.data; + return res.data; + } } diff --git a/src/api/dashboard/region.ts b/src/api/dashboard/region.ts index aed7659..39dbfd6 100644 --- a/src/api/dashboard/region.ts +++ b/src/api/dashboard/region.ts @@ -1,45 +1,76 @@ import Axios from "@/api"; -import type { EditableRegion, Region } from "@/types/region"; +import { InferZodType } from "@/types/common"; +import { RegionType, type Region } from "@/types/region"; import type { List } from "@/types/Request"; +import { z } from "zod"; -export async function getRegionList(params: { - pageSize: number; - current: number; - code?: string; -}) { - const res = await Axios.get>("/internal/cms/region", { - params, - }); +export const EditRegionApiBody = z.object({ + name: z.string().min(1), + code: z.string().min(1), + type: z.enum(RegionType), + level: z.number(), + parentId: z.uuid().nullish(), + countryCode: z.string().nullish(), + isOverseas: z.boolean(), + addressFormat: z.string().nullish(), + localName: z.string().nullish(), + timezone: z.string().nullish(), + languageCode: z.string().nullish(), + currencyCode: z.string().nullish(), + phoneCode: z.string().nullish(), + isoCode: z.string().nullish(), + latitude: z.number().nullish(), + longitude: z.number().nullish(), + sortOrder: z.number().nullish(), + remark: z.string().nullish(), +}); - return res.data; -} +export class RegionAPI { + static async getRegionList(params: { pageSize: number; current: number; code?: string }) { + const res = await Axios.get>("/internal/cms/region", { + params, + }); -export async function createRegion(region: EditableRegion) { - const res = await Axios.post("/internal/cms/region", { - region, - }); + return res.data; + } - return res.data; -} + static async createRegion(region: InferZodType) { + const res = await Axios.post("/internal/cms/region", { + region, + }); -export async function updateRegion(id: string, region: EditableRegion) { - const res = await Axios.post(`/internal/cms/region/${id}`, { - region, - }); + return res.data; + } - return res.data; -} + static async updateRegion(id: string, region: InferZodType) { + const res = await Axios.post(`/internal/cms/region/${id}`, { + region, + }); -export async function getRegion(id: string) { - const res = await Axios.get(`/internal/cms/region/${id}`); + return res.data; + } - return res.data; -} + static async getRegion(id: string) { + const res = await Axios.get(`/internal/cms/region/${id}`); + + return res.data; + } + + static async deleteRegion(id: string) { + const res = await Axios.delete<{ + success: boolean; + }>(`/internal/cms/region/${id}`); + + return res.data; + } -export async function deleteRegion(id: string) { - const res = await Axios.delete<{ - success: boolean; - }>(`/internal/cms/region/${id}`); + static async recreateRegionOrder(type: RegionType) { + const res = await Axios.post<{ + success: boolean; + }>("/internal/cms/region/recreate-order", { + type, + }); - return res.data; + return res.data; + } } diff --git a/src/api/dashboard/upload.ts b/src/api/dashboard/upload.ts index 1fe9a42..59fa857 100644 --- a/src/api/dashboard/upload.ts +++ b/src/api/dashboard/upload.ts @@ -1,10 +1,7 @@ import Axios from "@/api"; export async function uploadStatic(form: FormData) { - const res = await Axios.post<{ S3UploadRes: { ETag: string } }>( - "/internal/infra/upload/static", - form - ); + const res = await Axios.post<{ S3UploadRes: { ETag: string } }>("/internal/infra/upload/static", form); return res.data; } diff --git a/src/api/index.ts b/src/api/index.ts index a4d8618..75ffd1c 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -17,7 +17,7 @@ Axios.interceptors.request.use( window.location.href = "/auth/logout"; } return Promise.reject(error); - } + }, ); export default Axios; diff --git a/src/api/interceptors.ts b/src/api/interceptors.ts index b3dd630..54e22d1 100644 --- a/src/api/interceptors.ts +++ b/src/api/interceptors.ts @@ -1,5 +1,5 @@ -import Axios from '.'; -import useAuthStore from '@/stores/auth'; +import Axios from "."; +import useAuthStore from "@/stores/auth"; Axios.interceptors.response.use( function (response) { @@ -11,7 +11,7 @@ Axios.interceptors.response.use( // 超出 2xx 范围的状态码都会触发该函数。 // 对响应错误做点什么 if (error?.response?.status === 401) { - console.log('401'); + console.log("401"); useAuthStore.getState().logout(); } return Promise.reject(error); diff --git a/src/components/Container/index.tsx b/src/components/Container/index.tsx deleted file mode 100644 index b3c5ecd..0000000 --- a/src/components/Container/index.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { twMerge } from 'tailwind-merge'; - -export default function DefaultContainer( - props: React.DetailedHTMLProps< - React.HTMLAttributes, - HTMLDivElement - >, -) { - const { children, className, ...reset } = props; - return ( -
- {children} -
- ); -} diff --git a/src/components/Error/index.tsx b/src/components/Error/index.tsx deleted file mode 100644 index 7ba39b2..0000000 --- a/src/components/Error/index.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import DefaultContainer from '@/components/Container'; -import { Container, Title, Text, Group, Button } from '@mantine/core'; -import { useNavigate } from 'react-router-dom'; - -export default function LoadError() { - const navigate = useNavigate(); - - return ( - - - 发生了错误... - - 加载数据时发生了错误,最大的可能是这个数据不存在,如果持续遇到这个问题,请把地址报告给开发者。 - - - - - - - ); -} diff --git a/src/components/Event/LocationSearch/index.tsx b/src/components/Event/LocationSearch.tsx similarity index 85% rename from src/components/Event/LocationSearch/index.tsx rename to src/components/Event/LocationSearch.tsx index 59db538..763bcab 100644 --- a/src/components/Event/LocationSearch/index.tsx +++ b/src/components/Event/LocationSearch.tsx @@ -1,10 +1,9 @@ import { getTencentLocation } from "@/api/dashboard/map"; import { TencentLocation } from "@/types/map"; import { Region } from "@/types/region"; -import { Center, SimpleGrid, Stack } from "@mantine/core"; +import { Flex, Row, Col } from "antd"; import { useMutation } from "@tanstack/react-query"; import { Card, Modal, Spin } from "antd"; -import axios from "axios"; import { useEffect, useState } from "react"; export default function LocationSearch({ @@ -20,8 +19,7 @@ export default function LocationSearch({ region: Region; keyword?: string | null; }) { - const [selectedLocation, setSelectedLocation] = - useState(null); + const [selectedLocation, setSelectedLocation] = useState(null); return ( + - + ) : ( - + {addressSearchResult?.data.map((location) => ( - + + + ))} - + ); } diff --git a/src/components/EventFeature/EventFeatureSelector.tsx b/src/components/EventFeature/EventFeatureSelector.tsx new file mode 100644 index 0000000..15a79c5 --- /dev/null +++ b/src/components/EventFeature/EventFeatureSelector.tsx @@ -0,0 +1,91 @@ +import { FeatureAPI } from "@/api/dashboard/feature"; +import type { Feature } from "@/types/feature"; +import { FeatureCategoryLabel } from "@/types/feature"; +import { Select, Spin, SelectProps } from "antd"; +import { debounce, uniqBy } from "es-toolkit"; +import { useState, useCallback } from "react"; +import useSWR from "swr"; + +interface EventFeatureSelectorProps { + id?: string; + value?: string[]; + onChange?: (value?: string[]) => void; + onSelect?: (value?: Feature[]) => void; + selectedOptions?: Feature[]; + antdSelectProps?: SelectProps; +} + +export default function EventFeatureSelector({ + id, + value, + onChange, + onSelect, + selectedOptions, + antdSelectProps, +}: EventFeatureSelectorProps) { + const [searchValue, setSearchValue] = useState(""); + + const { data, isLoading } = useSWR(["feature/list", searchValue], () => + FeatureAPI.getFeatureList({ pageSize: 50, current: 1, name: searchValue }), + ); + + const features = uniqBy([...(selectedOptions || []), ...(data?.records || [])], (feature) => feature.id); + + const selectOptions = features.reduce( + (acc, feature) => { + const category = feature.category as keyof typeof FeatureCategoryLabel; + const groupLabel = FeatureCategoryLabel[category] || "其他"; + + const existingGroup = acc.find((group) => group.label === groupLabel); + if (existingGroup) { + existingGroup.options.push({ + label: feature.name, + value: feature.id, + feature, + }); + } else { + acc.push({ + label: groupLabel, + options: [ + { + label: feature.name, + value: feature.id, + feature, + }, + ], + }); + } + return acc; + }, + [] as Array<{ label: string; options: Array<{ label: string; value: string; feature: Feature }> }>, + ); + + const handleSearch = useCallback( + debounce((value: string) => { + setSearchValue(value); + }, 300), + [], + ); + + const handleChange = (selectedValue: string[]) => { + onChange?.(selectedValue as string[]); + onSelect?.(features.filter((feature) => selectedValue.includes(feature.id))); + }; + + return ( + : "没找到什么内容"} - style={{ width: "100%" }} - status={error || swrError ? "error" : undefined} - {...props} - /> - - ); -} diff --git a/src/components/EventFeature/FeatureEditor.tsx b/src/components/EventFeature/FeatureEditor.tsx new file mode 100644 index 0000000..9fd27b5 --- /dev/null +++ b/src/components/EventFeature/FeatureEditor.tsx @@ -0,0 +1,87 @@ +import { EditFeatureApiBody, FeatureAPI } from "@/api/dashboard/feature"; +import { Feature, FeatureCategory, FeatureCategoryLabel } from "@/types/feature"; +import { InferZodType } from "@/types/common"; +import { useZodValidateData } from "@/utils/form"; +import { pickBy } from "es-toolkit"; +import { App, Button, Flex, Form, Input, Modal, Select } from "antd"; + +const { TextArea } = Input; + +export default function FeatureEditor({ + opened, + onClose, + editingFeature, +}: { + opened: boolean; + onClose: () => void; + editingFeature: Feature | null; +}) { + const { message, modal } = App.useApp(); + const cleanedFeature = editingFeature ? pickBy(editingFeature, (v) => v !== "" && v != null) : {}; + + const [form] = Form.useForm(); + + const initialValues = { + name: cleanedFeature.name, + category: cleanedFeature.category, + description: cleanedFeature.description, + }; + + const onSubmit = async (value: InferZodType) => { + try { + if (editingFeature?.id) { + await FeatureAPI.updateFeature(editingFeature.id, value); + message.success("更新标签成功"); + return onClose(); + } + await FeatureAPI.createFeature(value); + message.success("创建标签成功"); + return onClose(); + } catch (error) { + message.error(`有错误发生: ${JSON.stringify(error)}`); + } + }; + + const handleFinish = (value: typeof initialValues) => { + const processedValues = useZodValidateData(value, EditFeatureApiBody); + if (processedValues.errors.length > 0) { + return modal.warning({ + title: "接口数据校验失败☹️", + content: processedValues.prettyErrors, + }); + } + if (processedValues.values) { + return onSubmit(processedValues.values); + } + return; + }; + return ( + +
+ + + + + +