浏览代码

feat(dashboard): Introduce page layout components

Michael Bromley 10 月之前
父节点
当前提交
9d5b14aced

+ 2 - 1
.vscode/settings.json

@@ -19,5 +19,6 @@
         "repo"
     ],
     "conventionalCommits.gitmoji": false,
-    "typescript.tsdk": "node_modules/typescript/lib"
+    "typescript.tsdk": "node_modules/typescript/lib",
+    "angular.enable-strict-mode-prompt": false
 }

文件差异内容过多而无法显示
+ 98 - 114
package-lock.json


+ 0 - 21
packages/dashboard/src/components/shared/page-card.tsx

@@ -1,21 +0,0 @@
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card.js";
-
-export interface PageCardProps {
-    children: React.ReactNode;
-    title?: string;
-    description?: string;
-}
-
-export function PageCard({ children, title, description }: PageCardProps) {
-    return (
-        <Card>
-            {title || description ? (
-                <CardHeader>
-                    {title && <CardTitle>{title}</CardTitle>}
-                    {description && <CardDescription>{description}</CardDescription>}
-                </CardHeader>
-            ) : null}
-            <CardContent className={!title ? 'pt-6' : ''}>{children}</CardContent>
-        </Card>
-    );
-}

+ 16 - 21
packages/dashboard/src/framework/form-engine/use-generated-form.tsx

@@ -10,34 +10,29 @@ import { VariablesOf } from 'gql.tada';
 import { FormEvent } from 'react';
 import { useForm, UseFormReturn } from 'react-hook-form';
 
-type FormField = 'FormField';
-
-type MapToFormField<T> =
-    T extends Array<infer U>
-        ? Array<MapToFormField<U>>
-        : T extends object
-          ? { [K in keyof Required<T>]: MapToFormField<NonNullable<T[K]>> }
-          : FormField;
-
-// Define InputFormField that takes a TypedDocumentNode and the name of the input variable
-type InputFormField<
-    T extends TypedDocumentNode<any, any>,
-    VarName extends keyof VariablesOf<T> = 'input',
-> = MapToFormField<NonNullable<VariablesOf<T>[VarName]>>;
-
-export function useGeneratedForm<
+export interface GeneratedFormOptions<
     T extends TypedDocumentNode<any, any>,
     VarName extends keyof VariablesOf<T> = 'input',
     E extends Record<string, any> = Record<string, any>,
->(options: {
+> {
     document: T;
     entity: E | null | undefined;
     setValues: (entity: NonNullable<E>) => VariablesOf<T>[VarName];
     onSubmit?: (values: VariablesOf<T>[VarName]) => void;
-}): {
-    form: UseFormReturn<VariablesOf<T>[VarName]>;
-    submitHandler: (event: FormEvent) => void;
-} {
+}
+
+/**
+ * @description
+ * This hook is used to create a form from a document and an entity.
+ * It will create a form with the fields defined in the document's input type.
+ * It will also create a submit handler that will submit the form to the server.
+ * 
+ */
+export function useGeneratedForm<
+    T extends TypedDocumentNode<any, any>,
+    VarName extends keyof VariablesOf<T> = 'input',
+    E extends Record<string, any> = Record<string, any>,
+>(options: GeneratedFormOptions<T, VarName, E>) {
     const { document, entity, setValues, onSubmit } = options;
     const availableLanguages = useServerConfig()?.availableLanguages || [];
     const updateFields = getOperationVariablesFields(document);

+ 87 - 0
packages/dashboard/src/framework/layout-engine/page-layout.tsx

@@ -0,0 +1,87 @@
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card.js";
+import { cn } from '@/lib/utils.js';
+import React from 'react';
+
+export type PageBlockProps = {
+    children: React.ReactNode;
+    /** Which column this block should appear in */
+    column: 'main' | 'side';
+    title?: string;
+    description?: string;
+    className?: string;
+};
+
+export type PageLayoutProps = {
+    children: React.ReactNode;
+    className?: string;
+};
+
+function isPageBlock(child: unknown): child is React.ReactElement<PageBlockProps> {
+    return React.isValidElement(child) && 'column' in (child as React.ReactElement<PageBlockProps>).props;
+}
+
+export function PageLayout({ children, className }: PageLayoutProps) {
+    // Separate blocks into categories
+    const childArray = React.Children.toArray(children);
+    const mainBlocks = childArray.filter(child => 
+        isPageBlock(child) && child.props.column === 'main'
+    );
+    const sideBlocks = childArray.filter(child => 
+        isPageBlock(child) && child.props.column === 'side'
+    );
+
+    return (
+        <div className={cn('w-full space-y-4', className)}>
+            {/* Mobile: Natural DOM order */}
+            <div className="md:hidden space-y-4">
+                {children}
+            </div>
+
+            {/* Desktop: Two-column layout */}
+            <div className="hidden md:grid md:grid-cols-5 lg:grid-cols-4 md:gap-4">
+                <div className="md:col-span-3 space-y-4">
+                    {mainBlocks}
+                </div>
+                <div className="md:col-span-2 lg:col-span-1 space-y-4">
+                    {sideBlocks}
+                </div>
+            </div>
+        </div>
+    );
+}
+
+export function Page({ children }: { children: React.ReactNode }) {
+    return (
+        <div className="m-4">
+            {children}
+        </div>
+    );
+}
+
+export function PageTitle({ children }: { children: React.ReactNode }) {
+    return (
+        <h1 className="text-2xl font-bold">{children}</h1>
+    );
+}
+
+export function PageActionBar({ children }: { children: React.ReactNode }) {
+    return (
+        <div className="flex justify-between">
+            {children}
+        </div>
+    );
+}
+
+export function PageBlock({ children, title, description }: PageBlockProps) {
+    return (
+        <Card className="w-full">
+            {title || description ? (
+                <CardHeader>
+                    {title && <CardTitle>{title}</CardTitle>}
+                    {description && <CardDescription>{description}</CardDescription>}
+                </CardHeader>
+            ) : null}
+            <CardContent className={!title ? 'pt-6' : ''}>{children}</CardContent>
+        </Card>
+    );
+} 

+ 0 - 33
packages/dashboard/src/framework/page/detail-page.tsx

@@ -1,33 +0,0 @@
-import { getQueryName } from '@/framework/document-introspection/get-document-structure.js';
-import { PageProps } from '@/framework/page/page-types.js';
-import { api } from '@/graphql/api.js';
-import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
-import { queryOptions } from '@tanstack/react-query';
-import { Variables } from 'graphql-request';
-import { DocumentNode } from 'graphql/index.js';
-import React from 'react';
-
-export function getDetailQueryOptions<T, V extends Variables = Variables>(
-    document: TypedDocumentNode<T, V> | DocumentNode,
-    variables: V,
-) {
-    const queryName = getQueryName(document);
-    return queryOptions({
-        queryKey: ['DetailPage', queryName, variables],
-        queryFn: () => api.query(document, variables),
-    });
-}
-
-export interface DetailPageProps extends PageProps {
-    entity: any;
-    children?: React.ReactNode;
-}
-
-export function DetailPage({ title, entity, children }: DetailPageProps) {
-    return (
-        <div>
-            <h1 className="text-2xl font-bold mb-4">{title}</h1>
-            {children}
-        </div>
-    );
-}

+ 11 - 12
packages/dashboard/src/framework/page/list-page.tsx

@@ -1,17 +1,16 @@
-import {
-    FieldInfo
-} from '@/framework/document-introspection/get-document-structure.js';
 import { PageProps } from '@/framework/page/page-types.js';
 
-import { CustomizeColumnConfig, ListQueryOptionsShape, ListQueryShape, PaginatedListDataTable } from '@/components/shared/paginated-list-data-table.js';
+import {
+    CustomizeColumnConfig,
+    ListQueryOptionsShape,
+    ListQueryShape,
+    PaginatedListDataTable,
+} from '@/components/shared/paginated-list-data-table.js';
 import { TypedDocumentNode } from '@graphql-typed-document-node/core';
 import { AnyRouter, useNavigate } from '@tanstack/react-router';
-import {
-    ColumnFiltersState,
-    SortingState,
-    Table
-} from '@tanstack/react-table';
+import { ColumnFiltersState, SortingState, Table } from '@tanstack/react-table';
 import { ResultOf } from 'gql.tada';
+import { Page, PageTitle } from '../layout-engine/page-layout.js';
 
 type ListQueryFields<T extends TypedDocumentNode<any, any>> = {
     [Key in keyof ResultOf<T>]: ResultOf<T>[Key] extends { items: infer U }
@@ -85,8 +84,8 @@ export function ListPage<
     }
 
     return (
-        <div className="m-4">
-            <h1 className="text-2xl font-bold">{title}</h1>
+        <Page>
+            <PageTitle>{title}</PageTitle>
             <PaginatedListDataTable
                 listQuery={listQuery}
                 customizeColumns={customizeColumns}
@@ -106,6 +105,6 @@ export function ListPage<
                     persistListStateToUrl(table, { filters });
                 }}
             />
-        </div>
+        </Page>
     );
 }

+ 0 - 23
packages/dashboard/src/framework/page/page.tsx

@@ -1,23 +0,0 @@
-export function Page({ children, ...props }: React.ComponentProps<'div'>) {
-    return (
-        <div data-slot="page" {...props}>
-            {children}
-        </div>
-    );
-}
-
-export function PageMain({ children, ...props }: React.ComponentProps<'main'>) {
-    return (
-        <main data-slot="page-main" {...props}>
-            {children}
-        </main>
-    );
-}
-
-export function PageSide({ children, ...props }: React.ComponentProps<'aside'>) {
-    return (
-        <aside data-slot="page-side" {...props}>
-            {children}
-        </aside>
-    );
-}

+ 104 - 0
packages/dashboard/src/framework/page/use-detail-page.ts

@@ -0,0 +1,104 @@
+import { api } from '@/graphql/api.js';
+import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
+import { queryOptions, useMutation, useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
+import { ResultOf, VariablesOf } from 'gql.tada';
+import { DocumentNode } from 'graphql';
+import { Variables } from 'graphql-request';
+
+import { getQueryName } from '../document-introspection/get-document-structure.js';
+import { useGeneratedForm } from '../form-engine/use-generated-form.js';
+
+export interface DetailPageOptions<
+    T extends TypedDocumentNode<any, any>,
+    U extends TypedDocumentNode<any, any>,
+    EntityField extends keyof ResultOf<T> = keyof ResultOf<T>,
+    VarName extends keyof VariablesOf<U> = 'input',
+> {
+    /**
+     * @description
+     * The query document to fetch the entity.
+     */
+    queryDocument: T;
+    /**
+     * @description
+     * The field of the query document that contains the entity.
+     */
+    entityField: EntityField;
+    /**
+     * @description
+     * The parameters used to identify the entity.
+     */
+    params: {
+        id: string;
+    };
+    /**
+     * @description
+     * The document to update the entity.
+     */
+    updateDocument: U;
+    /**
+     * @description
+     * The function to set the values for the update document.
+     */
+    setValuesForUpdate: (entity: NonNullable<ResultOf<T>[EntityField]>) => VariablesOf<U>[VarName];
+    /**
+     * @description
+     * The function to call when the update is successful.
+     */
+    onSuccess?: () => void;
+    /**
+     * @description
+     * The function to call when the update is successful.
+     */
+    onError?: (error: unknown) => void;
+}
+
+export function getDetailQueryOptions<T, V extends Variables = Variables>(
+    document: TypedDocumentNode<T, V> | DocumentNode,
+    variables: V,
+) {
+    const queryName = getQueryName(document);
+    return queryOptions({
+        queryKey: ['DetailPage', queryName, variables],
+        queryFn: () => api.query(document, variables),
+    });
+}
+
+/**
+ * @description
+ * This hook is used to create an entity detail page which can read
+ * and update an entity.
+ */
+export function useDetailPage<
+    T extends TypedDocumentNode<any, any>,
+    U extends TypedDocumentNode<any, any>,
+    EntityField extends keyof ResultOf<T> = keyof ResultOf<T>,
+    VarName extends keyof VariablesOf<U> = 'input',
+>(options: DetailPageOptions<T, U, EntityField, VarName>) {
+    const { queryDocument, updateDocument, setValuesForUpdate, params, entityField, onSuccess, onError } =
+        options;
+    const queryClient = useQueryClient();
+    const detailQueryOptions = getDetailQueryOptions(queryDocument, { id: params.id });
+    const detailQuery = useSuspenseQuery(detailQueryOptions);
+    const entity = detailQuery.data[entityField];
+
+    const updateMutation = useMutation({
+        mutationFn: api.mutate(updateDocument),
+        onSuccess: () => {
+            onSuccess?.();
+            void queryClient.invalidateQueries({ queryKey: detailQueryOptions.queryKey });
+        },
+        onError,
+    });
+
+    const { form, submitHandler } = useGeneratedForm({
+        document: updateDocument,
+        entity,
+        setValues: setValuesForUpdate,
+        onSubmit(values: any) {
+            updateMutation.mutate({ input: values });
+        },
+    });
+
+    return { form, submitHandler, entity, isPending: updateMutation.isPending || detailQuery.isPending };
+}

+ 139 - 156
packages/dashboard/src/routes/_authenticated/_products/products_.$id.tsx

@@ -3,7 +3,6 @@ import { AssignedFacetValues } from '@/components/shared/assigned-facet-values.j
 import { EntityAssets } from '@/components/shared/entity-assets.js';
 import { TranslatableFormField } from '@/components/shared/translatable-form-field.js';
 import { Button } from '@/components/ui/button.js';
-import { Card, CardContent } from '@/components/ui/card.js';
 import {
     Form,
     FormControl,
@@ -16,16 +15,19 @@ import {
 import { Input } from '@/components/ui/input.js';
 import { Switch } from '@/components/ui/switch.js';
 import { Textarea } from '@/components/ui/textarea.js';
-import { useGeneratedForm } from '@/framework/form-engine/use-generated-form.js';
-import { DetailPage, getDetailQueryOptions } from '@/framework/page/detail-page.js';
-import { api } from '@/graphql/api.js';
+import {
+    Page,
+    PageActionBar,
+    PageBlock,
+    PageLayout,
+    PageTitle,
+} from '@/framework/layout-engine/page-layout.js';
+import { useDetailPage, getDetailQueryOptions } from '@/framework/page/use-detail-page.js';
 import { Trans, useLingui } from '@lingui/react/macro';
-import { useMutation, useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
 import { createFileRoute } from '@tanstack/react-router';
 import { toast } from 'sonner';
 import { ProductVariantsTable } from './components/product-variants-table.js';
 import { productDetailDocument, updateProductDocument } from './products.graphql.js';
-import { PageCard } from '@/components/shared/page-card.js';
 
 export const Route = createFileRoute('/_authenticated/_products/products_/$id')({
     component: ProductDetailPage,
@@ -40,28 +42,12 @@ export const Route = createFileRoute('/_authenticated/_products/products_/$id')(
 export function ProductDetailPage() {
     const params = Route.useParams();
     const { i18n } = useLingui();
-    const queryClient = useQueryClient();
-    const detailQueryOptions = getDetailQueryOptions(productDetailDocument, { id: params.id });
-    const detailQuery = useSuspenseQuery(detailQueryOptions);
-    const entity = detailQuery.data.product;
-    const updateMutation = useMutation({
-        mutationFn: api.mutate(updateProductDocument),
-        onSuccess: () => {
-            toast('Updated', {
-                position: 'top-right',
-            });
-            form.reset();
-            queryClient.invalidateQueries({ queryKey: detailQueryOptions.queryKey });
-        },
-        onError: err => {
-            console.error(err);
-        },
-    });
 
-    const { form, submitHandler } = useGeneratedForm({
-        document: updateProductDocument,
-        entity,
-        setValues: entity => ({
+    const { form, submitHandler, entity, isPending } = useDetailPage({
+        queryDocument: productDetailDocument,
+        entityField: 'product',
+        updateDocument: updateProductDocument,
+        setValuesForUpdate: entity => ({
             id: entity.id,
             enabled: entity.enabled,
             featuredAssetId: entity.featuredAsset?.id,
@@ -75,79 +61,87 @@ export function ProductDetailPage() {
                 description: translation.description,
             })),
         }),
-        onSubmit(values) {
-            updateMutation.mutate({ input: values });
+        params: { id: params.id },
+        onSuccess: () => {
+            toast(i18n.t('Successfully updated product'), {
+                position: 'top-right',
+            });
+            form.reset();
+        },
+        onError: err => {
+            toast(i18n.t('Failed to update product'), {
+                position: 'top-right',
+                description: err instanceof Error ? err.message : 'Unknown error',
+            });
         },
     });
 
     return (
-        <DetailPage title={entity?.name ?? ''} route={Route} entity={entity}>
+        <Page>
+            <PageTitle>{entity?.name ?? ''}</PageTitle>
             <Form {...form}>
                 <form onSubmit={submitHandler} className="space-y-8">
-                    <div className="flex justify-between">
+                    <PageActionBar>
                         <ContentLanguageSelector className="mb-4" />
                         <Button
                             type="submit"
-                            disabled={
-                                !form.formState.isDirty || !form.formState.isValid || updateMutation.isPending
-                            }
+                            disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
                         >
                             Submit
                         </Button>
-                    </div>
-
-                    <div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
-                        <div className="lg:col-span-3 flex flex-col gap-4">
-                            <PageCard>
-                                <div className="flex flex-col gap-4">
-                                    <div className="md:flex w-full gap-4">
-                                        <div className="w-1/2">
-                                            <TranslatableFormField
-                                                control={form.control}
-                                                name="name"
-                                                render={({ field }) => (
-                                                    <FormItem>
-                                                        <FormLabel>
-                                                            <Trans>Product name</Trans>
-                                                        </FormLabel>
-                                                        <FormControl>
-                                                            <Input placeholder="" {...field} />
-                                                        </FormControl>
-                                                        <FormDescription></FormDescription>
-                                                        <FormMessage />
-                                                    </FormItem>
-                                                )}
-                                            />
-                                        </div>
-                                        <div className="w-1/2">
-                                            <TranslatableFormField
-                                                control={form.control}
-                                                name="slug"
-                                                render={({ field }) => (
-                                                    <FormItem>
-                                                        <FormLabel>
-                                                            <Trans>Slug</Trans>
-                                                        </FormLabel>
-                                                        <FormControl>
-                                                            <Input placeholder="" {...field} />
-                                                        </FormControl>
-                                                        <FormDescription></FormDescription>
-                                                        <FormMessage />
-                                                    </FormItem>
-                                                )}
-                                            />
-                                        </div>
-                                    </div>
+                    </PageActionBar>
+                    <PageLayout>
+                        <PageBlock column="side">
+                            <FormField
+                                control={form.control}
+                                name="enabled"
+                                render={({ field }) => (
+                                    <FormItem>
+                                        <FormLabel>
+                                            <Trans>Enabled</Trans>
+                                        </FormLabel>
+                                        <FormControl>
+                                            <Switch checked={field.value} onCheckedChange={field.onChange} />
+                                        </FormControl>
+                                        <FormDescription>
+                                            <Trans>When enabled, a product is available in the shop</Trans>
+                                        </FormDescription>
+                                        <FormMessage />
+                                    </FormItem>
+                                )}
+                            />
+                        </PageBlock>
+                        <PageBlock column="main">
+                            <div className="md:flex w-full gap-4">
+                                <div className="w-1/2">
+                                    <TranslatableFormField
+                                        control={form.control}
+                                        name="name"
+                                        render={({ field }) => (
+                                            <FormItem>
+                                                <FormLabel>
+                                                    <Trans>Product name</Trans>
+                                                </FormLabel>
+                                                <FormControl>
+                                                    <Input placeholder="" {...field} />
+                                                </FormControl>
+                                                <FormDescription></FormDescription>
+                                                <FormMessage />
+                                            </FormItem>
+                                        )}
+                                    />
+                                </div>
+                                <div className="w-1/2">
                                     <TranslatableFormField
                                         control={form.control}
-                                        name="description"
+                                        name="slug"
                                         render={({ field }) => (
                                             <FormItem>
                                                 <FormLabel>
-                                                    <Trans>Description</Trans>
+                                                    <Trans>Slug</Trans>
                                                 </FormLabel>
                                                 <FormControl>
-                                                    <Textarea className="resize-none" {...field} />
+                                                    <Input placeholder="" {...field} />
                                                 </FormControl>
                                                 <FormDescription></FormDescription>
                                                 <FormMessage />
@@ -155,82 +149,71 @@ export function ProductDetailPage() {
                                         )}
                                     />
                                 </div>
-                            </PageCard>
-                            <PageCard title={i18n.t('Product variants')}>
-                                <ProductVariantsTable productId={params.id} />
-                            </PageCard>
-                        </div>
-                        <div className="lg:col-span-1 flex flex-col gap-4">
-                            <PageCard>
-                                <FormField
-                                    control={form.control}
-                                    name="enabled"
-                                    render={({ field }) => (
-                                        <FormItem>
-                                            <FormLabel>
-                                                <Trans>Enabled</Trans>
-                                            </FormLabel>
-                                            <FormControl>
-                                                <Switch
-                                                    checked={field.value}
-                                                    onCheckedChange={field.onChange}
-                                                />
-                                            </FormControl>
-                                            <FormDescription>
-                                                <Trans>
-                                                    When enabled, a product is available in the shop
-                                                </Trans>
-                                            </FormDescription>
-                                            <FormMessage />
-                                        </FormItem>
-                                    )}
-                                />
-                            </PageCard>
-                            <PageCard>
-                                <FormField
-                                    control={form.control}
-                                    name="facetValueIds"
-                                    render={({ field }) => (
-                                        <FormItem>
-                                            <FormLabel>
-                                                <Trans>Facet values</Trans>
-                                            </FormLabel>
-                                            <FormControl>
-                                                <AssignedFacetValues
-                                                    facetValues={entity?.facetValues ?? []}
-                                                    {...field}
-                                                />
-                                            </FormControl>
-                                            <FormMessage />
-                                        </FormItem>
-                                    )}
-                                />
-                            </PageCard>
-                            <PageCard>
-                                <FormItem>
-                                    <FormLabel>
-                                        <Trans>Assets</Trans>
-                                    </FormLabel>
-                                    <FormControl>
-                                        <EntityAssets
-                                            assets={entity?.assets}
-                                            featuredAsset={entity?.featuredAsset}
-                                            compact={true}
-                                            value={form.getValues()}
-                                            onChange={value => {
-                                                form.setValue('featuredAssetId', value.featuredAssetId);
-                                                form.setValue('assetIds', value.assetIds);
-                                            }}
-                                        />
-                                    </FormControl>
-                                    <FormDescription></FormDescription>
-                                    <FormMessage />
-                                </FormItem>
-                            </PageCard>
-                        </div>
-                    </div>
+                            </div>
+                            <TranslatableFormField
+                                control={form.control}
+                                name="description"
+                                render={({ field }) => (
+                                    <FormItem>
+                                        <FormLabel>
+                                            <Trans>Description</Trans>
+                                        </FormLabel>
+                                        <FormControl>
+                                            <Textarea className="resize-none" {...field} />
+                                        </FormControl>
+                                        <FormDescription></FormDescription>
+                                        <FormMessage />
+                                    </FormItem>
+                                )}
+                            />
+                        </PageBlock>
+                        <PageBlock column="main">
+                            <ProductVariantsTable productId={params.id} />
+                        </PageBlock>
+                        <PageBlock column="side">
+                            <FormField
+                                control={form.control}
+                                name="facetValueIds"
+                                render={({ field }) => (
+                                    <FormItem>
+                                        <FormLabel>
+                                            <Trans>Facet values</Trans>
+                                        </FormLabel>
+                                        <FormControl>
+                                            <AssignedFacetValues
+                                                facetValues={entity?.facetValues ?? []}
+                                                {...field}
+                                            />
+                                        </FormControl>
+                                        <FormMessage />
+                                    </FormItem>
+                                )}
+                            />
+                        </PageBlock>
+                        <PageBlock column="side">
+                            <FormItem>
+                                <FormLabel>
+                                    <Trans>Assets</Trans>
+                                </FormLabel>
+                                <FormControl>
+                                    <EntityAssets
+                                        assets={entity?.assets}
+                                        featuredAsset={entity?.featuredAsset}
+                                        compact={true}
+                                        value={form.getValues()}
+                                        onChange={value => {
+                                            form.setValue('featuredAssetId', value.featuredAssetId);
+                                            form.setValue('assetIds', value.assetIds);
+                                        }}
+                                    />
+                                </FormControl>
+                                <FormDescription></FormDescription>
+                                <FormMessage />
+                            </FormItem>
+                        </PageBlock>
+                    </PageLayout>
                 </form>
             </Form>
-        </DetailPage>
+        </Page>
     );
 }

+ 8 - 0
packages/dashboard/src/styles.css

@@ -122,3 +122,11 @@
         @apply bg-background text-foreground;
     }
 }
+
+@utility col-main {
+    grid-column: span 3 / span 3;
+}
+@utility col-side {
+    grid-column: span 2 / span 2;
+}
+

部分文件因为文件数量过多而无法显示