소스 검색

feat: implement updated stylist product experience

chenyu 2 달 전
부모
커밋
a8306e3c79
54개의 변경된 파일과 1971개의 추가작업 그리고 1577개의 파일을 삭제
  1. 18 0
      backend/prisma/migrations/20260802_add_beta_application/migration.sql
  2. 20 0
      backend/prisma/schema.prisma
  3. 2 0
      backend/src/app.module.ts
  4. 13 0
      backend/src/modules/beta-applications/beta-applications.controller.ts
  5. 11 0
      backend/src/modules/beta-applications/beta-applications.module.ts
  6. 32 0
      backend/src/modules/beta-applications/beta-applications.service.ts
  7. 59 0
      backend/src/modules/beta-applications/dto/create-beta-application.dto.ts
  8. 74 19
      backend/src/modules/simulations/simulations.service.ts
  9. 1 1
      frontend/index.html
  10. 399 369
      frontend/public/css/aitori.css
  11. BIN
      frontend/public/images/aitori-hairstyle-three-style-loop-final-web.mp4
  12. BIN
      frontend/public/images/aitori-stylist-hairstyle-loop-poster.jpg
  13. BIN
      frontend/public/images/mvp-simulation-history/after-30-days-20260630.jpg
  14. BIN
      frontend/public/images/mvp-simulation-history/after-60-days-20260630.jpg
  15. BIN
      frontend/public/images/mvp-simulation-history/home-styling-20260630.jpg
  16. BIN
      frontend/public/images/mvp-simulation-history/template-324234-20260703.jpg
  17. BIN
      frontend/public/images/mvp-simulation-result-10/after-30-days.jpg
  18. BIN
      frontend/public/images/mvp-simulation-result-10/after-60-days.jpg
  19. BIN
      frontend/public/images/mvp-simulation-result-10/home-styling.jpg
  20. BIN
      frontend/public/images/mvp-simulation-result-10/source-photo.jpg
  21. BIN
      frontend/public/images/mvp-simulation-templates/2026-autumn.jpg
  22. BIN
      frontend/public/images/mvp-simulation-templates/324234.jpg
  23. BIN
      frontend/public/images/mvp-simulation-templates/long-1-a.jpg
  24. BIN
      frontend/public/images/mvp-simulation-templates/long-1-b.jpg
  25. BIN
      frontend/public/images/mvp-simulation-templates/short.jpg
  26. BIN
      frontend/public/images/mvp-simulation-templates/test.jpg
  27. BIN
      frontend/public/images/stylist-profile/demo-stylist-ai.png
  28. 6 0
      frontend/src/App.tsx
  29. 36 0
      frontend/src/components/common/ImageLightbox.tsx
  30. 3 0
      frontend/src/config/routes.ts
  31. 11 10
      frontend/src/i18n/locales/zh/translation.json
  32. 506 0
      frontend/src/index.css
  33. 8 7
      frontend/src/layouts/DashboardLayout.tsx
  34. 9 6
      frontend/src/layouts/PublicLayout.tsx
  35. 48 223
      frontend/src/pages/customer/CustomerSimulationPage.tsx
  36. 39 0
      frontend/src/pages/customer/CustomerSimulationResultPage.tsx
  37. 38 157
      frontend/src/pages/customer/StylistProfilePage.tsx
  38. 35 18
      frontend/src/pages/dashboard/DashboardPage.tsx
  39. 96 0
      frontend/src/pages/dashboard/StylistProfileManagePage.tsx
  40. 2 2
      frontend/src/pages/dashboard/__tests__/DashboardPage.test.tsx
  41. 40 26
      frontend/src/pages/dashboard/customers/CustomerDetailPage.tsx
  42. 12 10
      frontend/src/pages/dashboard/customers/CustomerFormPage.tsx
  43. 4 11
      frontend/src/pages/dashboard/customers/CustomerListPage.tsx
  44. 4 3
      frontend/src/pages/dashboard/customers/__tests__/CustomerDetailPage.test.tsx
  45. 62 210
      frontend/src/pages/dashboard/simulation/SimulationResultPage.tsx
  46. 44 60
      frontend/src/pages/dashboard/simulation/StylistSimulationPage.tsx
  47. 3 3
      frontend/src/pages/dashboard/simulation/__tests__/StylistSimulationPage.test.tsx
  48. 3 9
      frontend/src/pages/dashboard/templates/TemplateFormPage.tsx
  49. 5 5
      frontend/src/pages/dashboard/templates/TemplateListPage.tsx
  50. 120 0
      frontend/src/pages/public/BetaApplyPage.tsx
  51. 93 428
      frontend/src/pages/public/LandingPage.tsx
  52. 19 0
      frontend/src/services/api/betaApplications.ts
  53. 47 0
      frontend/src/shared/demoContent.ts
  54. 49 0
      frontend/src/shared/stylistProfileStore.ts

+ 18 - 0
backend/prisma/migrations/20260802_add_beta_application/migration.sql

@@ -0,0 +1,18 @@
+CREATE TABLE `beta_application` (
+  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+  `name` VARCHAR(80) NOT NULL,
+  `email` VARCHAR(150) NOT NULL,
+  `salon_name` VARCHAR(150) NOT NULL,
+  `area` VARCHAR(100) NOT NULL,
+  `role` VARCHAR(50) NOT NULL,
+  `contact` VARCHAR(150) NOT NULL,
+  `portfolio_url` VARCHAR(500) NOT NULL DEFAULT '',
+  `monthly_customers` VARCHAR(30) NOT NULL DEFAULT '',
+  `language` VARCHAR(20) NOT NULL DEFAULT '',
+  `features` TEXT NULL,
+  `pain_point` TEXT NULL,
+  `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
+  INDEX `idx_beta_application_email` (`email`),
+  INDEX `idx_beta_application_create_time` (`create_time`),
+  PRIMARY KEY (`id`)
+) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

+ 20 - 0
backend/prisma/schema.prisma

@@ -242,3 +242,23 @@ model SystemConfig {
   @@index([configGroup, isDeleted], map: "idx_config_group")
   @@map("system_config")
 }
+
+model BetaApplication {
+  id               BigInt   @id @default(autoincrement()) @db.UnsignedBigInt
+  name             String   @db.VarChar(80)
+  email            String   @db.VarChar(150)
+  salonName        String   @map("salon_name") @db.VarChar(150)
+  area             String   @db.VarChar(100)
+  role             String   @db.VarChar(50)
+  contact          String   @db.VarChar(150)
+  portfolioUrl     String   @default("") @map("portfolio_url") @db.VarChar(500)
+  monthlyCustomers String   @default("") @map("monthly_customers") @db.VarChar(30)
+  language         String   @default("") @db.VarChar(20)
+  features         String?  @db.Text
+  painPoint        String?  @map("pain_point") @db.Text
+  createTime       DateTime @default(now()) @map("create_time")
+
+  @@index([email], map: "idx_beta_application_email")
+  @@index([createTime], map: "idx_beta_application_create_time")
+  @@map("beta_application")
+}

+ 2 - 0
backend/src/app.module.ts

@@ -10,6 +10,7 @@ import { StorageModule } from './modules/storage/storage.module';
 import { TemplatesModule } from './modules/templates/templates.module';
 import { PrismaModule } from './prisma/prisma.module';
 import { RedisModule } from './redis/redis.module';
+import { BetaApplicationsModule } from './modules/beta-applications/beta-applications.module';
 
 @Module({
   imports: [
@@ -27,6 +28,7 @@ import { RedisModule } from './redis/redis.module';
     StorageModule,
     TemplatesModule,
     SimulationsModule,
+    BetaApplicationsModule,
   ],
 })
 export class AppModule {}

+ 13 - 0
backend/src/modules/beta-applications/beta-applications.controller.ts

@@ -0,0 +1,13 @@
+import { Body, Controller, Post } from '@nestjs/common';
+import { BetaApplicationsService } from './beta-applications.service';
+import { CreateBetaApplicationDto } from './dto/create-beta-application.dto';
+
+@Controller('beta-applications')
+export class BetaApplicationsController {
+  constructor(private readonly betaApplicationsService: BetaApplicationsService) {}
+
+  @Post()
+  create(@Body() body: CreateBetaApplicationDto) {
+    return this.betaApplicationsService.create(body);
+  }
+}

+ 11 - 0
backend/src/modules/beta-applications/beta-applications.module.ts

@@ -0,0 +1,11 @@
+import { Module } from '@nestjs/common';
+import { PrismaModule } from '../../prisma/prisma.module';
+import { BetaApplicationsController } from './beta-applications.controller';
+import { BetaApplicationsService } from './beta-applications.service';
+
+@Module({
+  imports: [PrismaModule],
+  controllers: [BetaApplicationsController],
+  providers: [BetaApplicationsService],
+})
+export class BetaApplicationsModule {}

+ 32 - 0
backend/src/modules/beta-applications/beta-applications.service.ts

@@ -0,0 +1,32 @@
+import { Injectable } from '@nestjs/common';
+import { PrismaService } from '../../prisma/prisma.service';
+import { CreateBetaApplicationDto } from './dto/create-beta-application.dto';
+
+@Injectable()
+export class BetaApplicationsService {
+  constructor(private readonly prisma: PrismaService) {}
+
+  async create(input: CreateBetaApplicationDto) {
+    const application = await this.prisma.betaApplication.create({
+      data: {
+        name: input.name.trim(),
+        email: input.email.trim().toLowerCase(),
+        salonName: input.salonName.trim(),
+        area: input.area.trim(),
+        role: input.role,
+        contact: input.contact.trim(),
+        portfolioUrl: input.portfolioUrl?.trim() ?? '',
+        monthlyCustomers: input.monthlyCustomers ?? '',
+        language: input.language ?? '',
+        features: input.features?.length ? JSON.stringify(input.features) : null,
+        painPoint: input.painPoint?.trim() || null,
+      },
+      select: { id: true, createTime: true },
+    });
+
+    return {
+      id: application.id.toString(),
+      submittedAt: application.createTime.toISOString(),
+    };
+  }
+}

+ 59 - 0
backend/src/modules/beta-applications/dto/create-beta-application.dto.ts

@@ -0,0 +1,59 @@
+import {
+  ArrayMaxSize,
+  IsArray,
+  IsEmail,
+  IsIn,
+  IsOptional,
+  IsString,
+  IsUrl,
+  MaxLength,
+} from 'class-validator';
+
+export class CreateBetaApplicationDto {
+  @IsString()
+  @MaxLength(80)
+  name!: string;
+
+  @IsEmail()
+  @MaxLength(150)
+  email!: string;
+
+  @IsString()
+  @MaxLength(150)
+  salonName!: string;
+
+  @IsString()
+  @MaxLength(100)
+  area!: string;
+
+  @IsIn(['stylist', 'salon-owner', 'staff', 'other'])
+  role!: string;
+
+  @IsString()
+  @MaxLength(150)
+  contact!: string;
+
+  @IsOptional()
+  @IsUrl({ require_protocol: true })
+  @MaxLength(500)
+  portfolioUrl?: string;
+
+  @IsOptional()
+  @IsIn(['1-30', '31-100', '100+'])
+  monthlyCustomers?: string;
+
+  @IsOptional()
+  @IsIn(['zh', 'ja', 'en'])
+  language?: string;
+
+  @IsOptional()
+  @IsArray()
+  @ArrayMaxSize(4)
+  @IsString({ each: true })
+  features?: string[];
+
+  @IsOptional()
+  @IsString()
+  @MaxLength(3000)
+  painPoint?: string;
+}

+ 74 - 19
backend/src/modules/simulations/simulations.service.ts

@@ -13,6 +13,23 @@ import { ListSimulationTemplateOptionsQueryDto } from './dto/list-simulation-tem
 import { AiHairSimulationDetailDto } from './dto/simulation-detail.dto';
 import { SimulationTemplateOptionDto } from './dto/simulation-template-option.dto';
 
+const SYSTEM_TEMPLATE_PRESETS = {
+  'system-after-30-days': {
+    name: '居家造型30天后造型',
+    image: '/images/mvp-simulation-result-10/after-30-days.jpg',
+  },
+  'system-home-styling': {
+    name: '居家造型',
+    image: '/images/mvp-simulation-result-10/home-styling.jpg',
+  },
+} as const;
+
+type SystemTemplatePresetId = keyof typeof SYSTEM_TEMPLATE_PRESETS;
+
+function isSystemTemplatePresetId(value: string): value is SystemTemplatePresetId {
+  return value in SYSTEM_TEMPLATE_PRESETS;
+}
+
 @Injectable()
 export class SimulationsService {
   private readonly logger = new Logger(SimulationsService.name);
@@ -53,8 +70,7 @@ export class SimulationsService {
       this.prisma.hairTemplate.count({ where }),
     ]);
 
-    return {
-      items: items.map((item) => ({
+    const mappedItems = items.map((item) => ({
         id: item.id.toString(),
         templateName: item.templateName,
         coverUrl: item.coverUrl,
@@ -65,8 +81,28 @@ export class SimulationsService {
         description: item.description,
         templateUrl: item.templateUrl,
         isSystemTemplate: item.isSystemTemplate === 'YES',
-      })),
-      total,
+      }));
+    const systemFallbackItems = Object.entries(SYSTEM_TEMPLATE_PRESETS).map(
+      ([id, preset]) => ({
+        id,
+        templateName: preset.name,
+        coverUrl: preset.image,
+        category: 'LONG' as const,
+        tags: [],
+        description: null,
+        templateUrl: '',
+        isSystemTemplate: true,
+      }),
+    );
+    const systemItems = mappedItems.slice(0, 2);
+    if (systemItems.length < 2) {
+      systemItems.push(...systemFallbackItems.slice(0, 2 - systemItems.length));
+    }
+    const responseItems = query.source === 'SYSTEM' ? systemItems : mappedItems;
+
+    return {
+      items: responseItems,
+      total: query.source === 'SYSTEM' ? responseItems.length : total,
       page: query.page,
       pageSize: query.pageSize,
     };
@@ -91,7 +127,16 @@ export class SimulationsService {
       }
     }
 
-    const templateIds = dto.templateIds.map((value) => BigInt(value));
+    const presetIds = dto.templateIds.filter(isSystemTemplatePresetId);
+    const databaseTemplateIdValues = dto.templateIds.filter(
+      (value) => !isSystemTemplatePresetId(value),
+    );
+    let templateIds: bigint[];
+    try {
+      templateIds = databaseTemplateIdValues.map((value) => BigInt(value));
+    } catch {
+      throw new BadRequestException('部分模板不可用');
+    }
     const templates = await this.prisma.hairTemplate.findMany({
       where: {
         id: { in: templateIds },
@@ -105,16 +150,14 @@ export class SimulationsService {
       },
     });
 
-    if (templates.length !== dto.templateIds.length) {
+    if (templates.length + presetIds.length !== dto.templateIds.length) {
       throw new BadRequestException('部分模板不可用');
     }
 
     const taskSeeds: Array<{
-      template: {
-        id: bigint;
-        templateUrl: string;
-      };
+      templateId: string;
       taskId: string;
+      presetImage?: string;
     }> = [];
     for (const template of templates) {
       const { taskId } = await this.aiSimulationClient.execute(
@@ -122,10 +165,17 @@ export class SimulationsService {
         dto.originImage,
       );
       taskSeeds.push({
-        template,
+        templateId: template.id.toString(),
         taskId,
       });
     }
+    presetIds.forEach((presetId) => {
+      taskSeeds.push({
+        templateId: presetId,
+        taskId: `preset-${presetId}-${Date.now()}`,
+        presetImage: SYSTEM_TEMPLATE_PRESETS[presetId].image,
+      });
+    });
 
     return this.prisma.$transaction(async (tx) => {
       const simulation = await tx.aiHairSimulation.create({
@@ -141,13 +191,13 @@ export class SimulationsService {
       await tx.aiHairSimulationTask.createMany({
         data: taskSeeds.map((seed) => ({
           simulationId: simulation.id,
-          templateId: seed.template.id.toString(),
+          templateId: seed.templateId,
           customerId: dto.customerId ? BigInt(dto.customerId) : null,
           taskId: seed.taskId,
-          taskStatus: 'PROCESSING',
-          resultImages: null,
+          taskStatus: seed.presetImage ? 'SUCCESSES' : 'PROCESSING',
+          resultImages: seed.presetImage ? JSON.stringify([seed.presetImage]) : null,
           taskStartTime: new Date(),
-          taskFinishTime: null,
+          taskFinishTime: seed.presetImage ? new Date() : null,
           errorMsg: null,
         })),
       });
@@ -357,14 +407,17 @@ export class SimulationsService {
       overallStatus: this.getOverallStatus(counts),
       tasks: freshTasks.map((task) => {
         const template = templateMap.get(task.templateId);
+        const preset = isSystemTemplatePresetId(task.templateId)
+          ? SYSTEM_TEMPLATE_PRESETS[task.templateId]
+          : null;
 
         return {
           id: task.id.toString(),
           templateId: task.templateId,
-          templateName: template?.templateName ?? 'Unknown Template',
-          templateCoverUrl: template?.coverUrl ?? '',
+          templateName: preset?.name ?? template?.templateName ?? 'Unknown Template',
+          templateCoverUrl: preset?.image ?? template?.coverUrl ?? '',
           templateCategory: template?.category ?? 'LONG',
-          templateSource: template?.isSystemTemplate === 'YES' ? 'SYSTEM' : 'STYLIST',
+          templateSource: preset || template?.isSystemTemplate === 'YES' ? 'SYSTEM' : 'STYLIST',
           taskId: task.taskId,
           taskStatus: task.taskStatus,
           resultImages: this.parseResultImages(task.resultImages),
@@ -471,7 +524,9 @@ export class SimulationsService {
           simulationId: task.simulationId.toString(),
           taskId: task.taskId,
           imageUrl,
-          templateName: templateMap.get(task.templateId) ?? 'Unknown Template',
+          templateName: isSystemTemplatePresetId(task.templateId)
+            ? SYSTEM_TEMPLATE_PRESETS[task.templateId].name
+            : templateMap.get(task.templateId) ?? 'Unknown Template',
           createdAt: createdAt.toISOString(),
           taskStatus: 'SUCCESSES' as const,
         }));

+ 1 - 1
frontend/index.html

@@ -2,7 +2,7 @@
 <html lang="zh-CN">
   <head>
     <meta charset="UTF-8" />
-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
     <title>Aitori Stylist React Refactor</title>
     <link rel="icon" type="image/x-icon" href="/images/favicon.ico" />
     <link rel="apple-touch-icon" href="/images/logo_color.svg" />

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 399 - 369
frontend/public/css/aitori.css


BIN
frontend/public/images/aitori-hairstyle-three-style-loop-final-web.mp4


BIN
frontend/public/images/aitori-stylist-hairstyle-loop-poster.jpg


BIN
frontend/public/images/mvp-simulation-history/after-30-days-20260630.jpg


BIN
frontend/public/images/mvp-simulation-history/after-60-days-20260630.jpg


BIN
frontend/public/images/mvp-simulation-history/home-styling-20260630.jpg


BIN
frontend/public/images/mvp-simulation-history/template-324234-20260703.jpg


BIN
frontend/public/images/mvp-simulation-result-10/after-30-days.jpg


BIN
frontend/public/images/mvp-simulation-result-10/after-60-days.jpg


BIN
frontend/public/images/mvp-simulation-result-10/home-styling.jpg


BIN
frontend/public/images/mvp-simulation-result-10/source-photo.jpg


BIN
frontend/public/images/mvp-simulation-templates/2026-autumn.jpg


BIN
frontend/public/images/mvp-simulation-templates/324234.jpg


BIN
frontend/public/images/mvp-simulation-templates/long-1-a.jpg


BIN
frontend/public/images/mvp-simulation-templates/long-1-b.jpg


BIN
frontend/public/images/mvp-simulation-templates/short.jpg


BIN
frontend/public/images/mvp-simulation-templates/test.jpg


BIN
frontend/public/images/stylist-profile/demo-stylist-ai.png


+ 6 - 0
frontend/src/App.tsx

@@ -18,6 +18,9 @@ import { SimulationEditPage } from './pages/dashboard/simulation/SimulationEditP
 import { AftercareResultPage } from './pages/dashboard/simulation/AftercareResultPage'
 import { StylistProfilePage } from './pages/customer/StylistProfilePage'
 import { CustomerSimulationPage } from './pages/customer/CustomerSimulationPage'
+import { CustomerSimulationResultPage } from './pages/customer/CustomerSimulationResultPage'
+import { BetaApplyPage } from './pages/public/BetaApplyPage'
+import { StylistProfileManagePage } from './pages/dashboard/StylistProfileManagePage'
 import { RequireAuth } from './shared/RequireAuth'
 
 function App() {
@@ -43,7 +46,10 @@ function App() {
         <Route path={routes.simulationEdit} element={<RequireAuth><SimulationEditPage /></RequireAuth>} />
         <Route path={routes.simulationAftercare} element={<RequireAuth><AftercareResultPage /></RequireAuth>} />
         <Route path={routes.profile} element={<StylistProfilePage />} />
+        <Route path={routes.profileManage} element={<RequireAuth><StylistProfileManagePage /></RequireAuth>} />
         <Route path={routes.customerSimulation} element={<CustomerSimulationPage />} />
+        <Route path={routes.customerSimulationResult} element={<CustomerSimulationResultPage />} />
+        <Route path={routes.betaApply} element={<BetaApplyPage />} />
         <Route path="*" element={<Navigate to={`/${DEFAULT_LOCALE}`} replace />} />
       </Routes>
     </BrowserRouter>

+ 36 - 0
frontend/src/components/common/ImageLightbox.tsx

@@ -0,0 +1,36 @@
+import { useEffect, type ReactNode } from 'react'
+
+type ImageLightboxProps = {
+  image: string | null
+  alt?: string
+  onClose: () => void
+  action?: ReactNode
+}
+
+export function ImageLightbox({ image, alt = '', onClose, action }: ImageLightboxProps) {
+  useEffect(() => {
+    if (!image) return undefined
+    const handleKeyDown = (event: KeyboardEvent) => {
+      if (event.key === 'Escape') onClose()
+    }
+    document.addEventListener('keydown', handleKeyDown)
+    const previousOverflow = document.body.style.overflow
+    document.body.style.overflow = 'hidden'
+    return () => {
+      document.removeEventListener('keydown', handleKeyDown)
+      document.body.style.overflow = previousOverflow
+    }
+  }, [image, onClose])
+
+  if (!image) return null
+
+  return (
+    <div className="aitori-image-lightbox" role="dialog" aria-modal="true" aria-label={alt || '图片预览'} onClick={onClose}>
+      <button type="button" aria-label="关闭图片预览" onClick={onClose}><i className="feather icon-feather-x"></i></button>
+      <div className="aitori-image-lightbox-content" onClick={(event) => event.stopPropagation()}>
+        <img src={image} alt={alt ? `${alt} 放大预览` : '图片放大预览'} />
+        {action ? <div className="aitori-image-lightbox-action">{action}</div> : null}
+      </div>
+    </div>
+  )
+}

+ 3 - 0
frontend/src/config/routes.ts

@@ -19,4 +19,7 @@ export const routes = {
   simulationAftercare: '/:locale/simulation/aftercare',
   profile: '/:locale/profile',
   customerSimulation: '/:locale/customer-simulation',
+  customerSimulationResult: '/:locale/customer-simulation/result',
+  betaApply: '/:locale/beta-apply',
+  profileManage: '/:locale/profile/manage',
 }

+ 11 - 10
frontend/src/i18n/locales/zh/translation.json

@@ -10,9 +10,9 @@
     "login": "登录"
   },
   "dashboardNav": {
-    "home": "工作台",
+    "home": "首页",
     "customers": "顾客",
-    "templates": "模板",
+    "templates": "发型模板",
     "logout": "退出登录"
   },
   "common": {
@@ -169,12 +169,12 @@
   },
   "publicLayout": {
     "nav": {
-      "profile": "个人主页",
-      "templates": "发型模板",
-      "simulation": "AI发型模拟",
+      "profile": "美发师主页",
+      "templates": "发型参考",
+      "simulation": "AI发型试试看",
       "reviews": "评价"
     },
-    "footer": "Aitori Stylist | 美发师个人主页"
+    "footer": "Aitori Stylist | 发型师模板主页"
   },
   "dashboardPage": {
     "title": "美发师工作台",
@@ -188,7 +188,8 @@
     "actions": {
       "newCustomer": { "title": "新建顾客", "body": "登记首次到店顾客" },
       "simulation": { "title": "AI发型模拟", "body": "从照片拍摄到发型提案" },
-      "templates": { "title": "我的发型模板", "body": "查看已登记的发型" }
+      "templates": { "title": "我的发型模板", "body": "查看已登记的发型" },
+      "profile": { "title": "个人主页", "body": "查看和编辑公开主页" }
     },
     "stats": {
       "todayCustomers": "今日顾客",
@@ -685,8 +686,8 @@
       "fallbacks": ["1994/08/18", "4〜6周", "细软 / 轻微发流 / 容易干燥", "8Lv米色、红调抑制、低刺激优先"]
     },
     "history": {
-      "badge": "施术历史",
-      "title": "施术历史",
+      "badge": "模拟历史",
+      "title": "发型模拟历史",
       "action": "添加记录"
     },
     "photos": {
@@ -897,7 +898,7 @@
   },
   "customerSimulationPage": {
     "title": "顾客AI发型模拟",
-    "description": "Aitori Stylist 顾客AI发型模拟页面。注册或登录后上传照片、选择发型模板,即可生成发型完成效果,也可查看居家造型和30天/60天后的变化预测。",
+    "description": "Aitori Stylist 顾客端 AI 试发型页面。输入美发师提供的代码、上传照片并选择发型,即可查看预约前模拟效果。",
     "back": "返回个人主页",
     "submit": "开始生成",
     "badge": "AI发型模拟",

+ 506 - 0
frontend/src/index.css

@@ -39,6 +39,118 @@ textarea {
   min-height: 100vh;
 }
 
+.aitori-image-lightbox {
+  position: fixed;
+  inset: 0;
+  z-index: 10000;
+  display: grid;
+  place-items: center;
+  padding: 4rem 1.5rem 1.5rem;
+  background: rgba(20, 19, 25, 0.88);
+  backdrop-filter: blur(12px);
+}
+
+.aitori-image-lightbox img {
+  max-width: min(1100px, 94vw);
+  max-height: 86vh;
+  border-radius: 8px;
+  box-shadow: 0 24px 70px rgba(0, 0, 0, 0.35);
+}
+
+.aitori-image-lightbox-content {
+  display: grid;
+  justify-items: center;
+  gap: 1rem;
+}
+
+.aitori-image-lightbox-action .btn {
+  color: #fff;
+}
+
+.aitori-image-lightbox button {
+  position: absolute;
+  top: 1.25rem;
+  right: 1.25rem;
+  width: 44px;
+  height: 44px;
+  border: 0;
+  border-radius: 50%;
+  color: #fff;
+  background: rgba(255, 255, 255, 0.14);
+  cursor: pointer;
+}
+
+.aitori-image-lightbox button:focus-visible {
+  outline: 2px solid #fff;
+  outline-offset: 3px;
+}
+
+.aitori-image-button {
+  display: block;
+  width: 100%;
+  padding: 0;
+  border: 0;
+  background: transparent;
+  cursor: zoom-in;
+  text-align: inherit;
+}
+
+.simulation-result-card > .aitori-image-button {
+  overflow: hidden;
+  border-radius: 8px 8px 0 0;
+}
+
+.simulation-result-card > .aitori-image-button:hover .simulation-result-image {
+  transform: scale(1.02);
+}
+
+.customer-row-link {
+  color: inherit;
+  transition: background-color 0.2s ease, transform 0.2s ease;
+}
+
+.customer-row-link:hover {
+  color: inherit;
+  background: rgba(156, 112, 237, 0.055);
+  transform: translateY(-1px);
+}
+
+.aitori-template-gallery-grid {
+  display: grid;
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+  gap: 30px;
+  margin: 0;
+  padding: 0;
+  list-style: none;
+}
+
+.aitori-template-gallery-grid .portfolio-box,
+.aitori-template-gallery-grid button {
+  width: 100%;
+  height: 100%;
+}
+
+.aitori-template-gallery-grid button {
+  padding: 0;
+  border: 0;
+  background: transparent;
+  cursor: zoom-in;
+}
+
+.aitori-template-gallery-grid .portfolio-image img {
+  width: 100%;
+  aspect-ratio: 4 / 5;
+  object-fit: cover;
+}
+
+@media (max-width: 991px) {
+  .aitori-template-gallery-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+}
+
+@media (max-width: 575px) {
+  .aitori-template-gallery-grid { grid-template-columns: 1fr; }
+}
+
 .app-shell {
   min-height: 100vh;
 }
@@ -543,3 +655,397 @@ textarea {
     grid-template-columns: 1fr;
   }
 }
+
+/* Visual polish layer: presentation only, kept separate from business components. */
+:root {
+  --aitori-surface: rgba(255, 255, 255, 0.96);
+  --aitori-line: rgba(44, 46, 60, 0.085);
+  --aitori-line-hover: rgba(245, 126, 72, 0.32);
+  --aitori-focus: rgba(245, 126, 72, 0.16);
+  --aitori-shadow-sm: 0 8px 24px rgba(44, 46, 60, 0.055);
+  --aitori-shadow-md: 0 16px 42px rgba(44, 46, 60, 0.085);
+  --aitori-safe-top: env(safe-area-inset-top, 0px);
+  --aitori-safe-bottom: env(safe-area-inset-bottom, 0px);
+  --aitori-page-bottom-space: clamp(40px, 5vw, 64px);
+}
+
+html,
+body,
+#root {
+  min-height: 100%;
+}
+
+body {
+  -webkit-font-smoothing: antialiased;
+  text-rendering: optimizeLegibility;
+}
+
+::selection {
+  color: #2c2e3c;
+  background: rgba(245, 126, 72, 0.22);
+}
+
+button,
+a,
+label,
+input,
+select,
+textarea {
+  -webkit-tap-highlight-color: transparent;
+}
+
+button,
+[role='button'],
+input[type='button'],
+input[type='submit'] {
+  touch-action: manipulation;
+}
+
+header > .navbar {
+  padding-top: var(--aitori-safe-top);
+}
+
+.stylist-dashboard-nav {
+  border-bottom: 1px solid rgba(44, 46, 60, 0.055);
+  background-color: rgba(255, 255, 255, 0.92);
+  box-shadow: 0 6px 24px rgba(44, 46, 60, 0.035);
+  backdrop-filter: blur(18px) saturate(1.2);
+  -webkit-backdrop-filter: blur(18px) saturate(1.2);
+}
+
+.stylist-workbench {
+  min-height: 100svh;
+  padding-bottom: calc(var(--aitori-page-bottom-space) + var(--aitori-safe-bottom));
+  background:
+    radial-gradient(circle at 86% 7%, rgba(214, 224, 211, 0.24), transparent 25rem),
+    radial-gradient(circle at 9% 3%, rgba(233, 215, 209, 0.28), transparent 22rem),
+    linear-gradient(180deg, #fff8f6 0%, #ffffff 42%, #f7f4ef 100%);
+}
+
+footer.footer-light {
+  padding-bottom: var(--aitori-safe-bottom) !important;
+}
+
+.login-hero {
+  min-height: 100svh;
+  padding-bottom: calc(80px + var(--aitori-safe-bottom));
+}
+
+.aitori-beta-apply-hero {
+  padding-bottom: calc(80px + var(--aitori-safe-bottom));
+}
+
+.aitori-invite-shell {
+  min-height: 100svh;
+  padding-top: calc(28px + var(--aitori-safe-top));
+  padding-bottom: calc(26px + var(--aitori-safe-bottom));
+}
+
+.aitori-invite-body {
+  overflow-x: hidden;
+  overflow-y: auto;
+}
+
+.aitori-invite-page {
+  min-height: 100svh;
+  overflow: clip;
+}
+
+.aitori-invite-nav,
+.aitori-invite-meta,
+.aitori-invite-bottom {
+  flex-shrink: 0;
+}
+
+.aitori-invite-spacer {
+  flex: 1 0 48px;
+  min-height: 48px;
+}
+
+.aitori-invite-mobile-menu {
+  padding-top: calc(28px + var(--aitori-safe-top));
+  padding-bottom: calc(28px + var(--aitori-safe-bottom));
+}
+
+.workbench-panel,
+.workbench-stat {
+  border-color: var(--aitori-line);
+  box-shadow: var(--aitori-shadow-sm);
+}
+
+.workbench-panel {
+  background-color: var(--aitori-surface) !important;
+}
+
+.workbench-stat {
+  background: linear-gradient(145deg, #ffffff 15%, rgba(247, 244, 239, 0.86) 100%) !important;
+}
+
+.workbench-action-card,
+.template-card,
+.customer-photo-card,
+.simulation-result-card,
+.simulation-choice-card,
+.simulation-photo-card,
+.simulation-template-card,
+.simulation-template-tabs label,
+.simulation-service-list label,
+.simulation-preset-grid label,
+.template-option-card,
+.template-choice-list label {
+  will-change: transform;
+  transition:
+    transform 180ms ease,
+    border-color 180ms ease,
+    background-color 180ms ease,
+    box-shadow 180ms ease;
+}
+
+.customer-search-field .form-control,
+.customer-filter-control,
+.customer-edit-control,
+.simulation-date-field input,
+.simulation-slider-field,
+.simulation-edit-upload-button {
+  transition:
+    border-color 180ms ease,
+    background-color 180ms ease,
+    box-shadow 180ms ease;
+}
+
+.customer-search-field .form-control:hover,
+.customer-filter-control:hover,
+.customer-edit-control:hover,
+.simulation-date-field input:hover,
+.simulation-slider-field:hover {
+  border-color: rgba(44, 46, 60, 0.17);
+  background-color: #fff;
+}
+
+.customer-search-field .form-control:focus,
+.customer-filter-control:focus,
+.simulation-date-field input:focus {
+  outline: none;
+  border-color: rgba(245, 126, 72, 0.52);
+  background-color: #fff;
+  box-shadow: 0 0 0 4px var(--aitori-focus);
+}
+
+.simulation-choice-card:has(input:checked),
+.simulation-photo-card:has(input:checked),
+.simulation-template-card:has(input:checked),
+.simulation-template-tabs label:has(input:checked),
+.simulation-service-list label:has(input:checked),
+.simulation-preset-grid label:has(input:checked),
+.template-option-card:has(input:checked) {
+  box-shadow: 0 0 0 4px var(--aitori-focus), var(--aitori-shadow-sm);
+}
+
+.simulation-photo-upload,
+.template-upload-box,
+.template-background-upload,
+.customer-photo-upload {
+  transition:
+    border-color 180ms ease,
+    background-color 180ms ease,
+    box-shadow 180ms ease;
+}
+
+.simulation-photo-upload:hover,
+.template-upload-box:hover,
+.template-background-upload:hover,
+.customer-photo-upload:hover {
+  border-color: rgba(245, 126, 72, 0.52);
+  background-color: #fff;
+  box-shadow: 0 0 0 4px rgba(245, 126, 72, 0.08);
+}
+
+.stylist-workbench .btn,
+.customer-pager-link,
+.template-card-footer button,
+.aitori-image-lightbox button {
+  transition:
+    transform 160ms ease,
+    color 160ms ease,
+    background-color 160ms ease,
+    border-color 160ms ease,
+    box-shadow 160ms ease;
+}
+
+.workbench-action-card:focus-visible,
+.customer-row-link:focus-visible,
+.template-card:focus-visible,
+.customer-photo-card-link:focus-visible,
+.aitori-image-button:focus-visible,
+.customer-pager-link:focus-visible,
+.template-card-footer button:focus-visible,
+.stylist-workbench .btn:focus-visible,
+.simulation-photo-upload:focus-within,
+.template-upload-box:focus-within,
+.template-background-upload:focus-within,
+.customer-photo-upload:focus-within,
+.simulation-choice-card:focus-within,
+.simulation-photo-card:focus-within,
+.simulation-template-card:focus-within,
+.simulation-template-tabs label:focus-within,
+.simulation-service-list label:focus-within,
+.simulation-preset-grid label:focus-within,
+.template-option-card:focus-within {
+  outline: 3px solid rgba(245, 126, 72, 0.32);
+  outline-offset: 3px;
+}
+
+.template-card img,
+.customer-photo-card img,
+.simulation-photo-card img,
+.simulation-template-card img,
+.template-option-card img {
+  transition: transform 260ms ease;
+}
+
+@media (hover: hover) and (pointer: fine) {
+  .workbench-action-card:hover,
+  .template-card:hover,
+  .customer-photo-card:hover,
+  .simulation-result-card:hover {
+    border-color: var(--aitori-line-hover);
+    box-shadow: var(--aitori-shadow-md);
+  }
+
+  .simulation-choice-card:not(.is-static):hover,
+  .simulation-photo-card:hover,
+  .simulation-template-card:not(:has(input:disabled)):hover,
+  .simulation-template-tabs label:hover,
+  .simulation-service-list label:hover,
+  .simulation-preset-grid label:hover,
+  .template-option-card:hover,
+  .template-choice-list label:hover {
+    transform: translateY(-2px);
+    border-color: var(--aitori-line-hover);
+    background-color: #fff;
+    box-shadow: var(--aitori-shadow-sm);
+  }
+
+  .template-card:hover img,
+  .customer-photo-card:hover img,
+  .simulation-photo-card:hover img,
+  .simulation-template-card:not(:has(input:disabled)):hover img,
+  .template-option-card:hover img {
+    transform: scale(1.025);
+  }
+
+  .stylist-workbench .btn:not(:disabled):hover,
+  .customer-pager-link:not(.is-disabled):hover,
+  .template-card-footer button:hover,
+  .aitori-image-lightbox button:hover {
+    transform: translateY(-1px);
+  }
+}
+
+@media (min-width: 992px) and (max-height: 1050px) {
+  .aitori-invite-shell {
+    padding-top: calc(22px + var(--aitori-safe-top));
+    padding-bottom: calc(22px + var(--aitori-safe-bottom));
+  }
+
+  .aitori-invite-meta {
+    margin-top: clamp(20px, 3vh, 30px);
+    gap: 8px clamp(28px, 6vw, 80px);
+  }
+
+  .aitori-invite-meta > div {
+    padding: 12px 14px;
+  }
+
+  .aitori-invite-label,
+  .aitori-invite-kicker {
+    margin-bottom: 8px;
+  }
+
+  .aitori-invite-meta p {
+    margin-top: 8px;
+    line-height: 1.5;
+  }
+
+  .aitori-invite-spacer {
+    flex-basis: 24px;
+    min-height: 24px;
+  }
+
+  .aitori-invite-bottom {
+    gap: clamp(24px, 4vw, 52px);
+  }
+
+  .aitori-invite-heading h1 {
+    font-size: clamp(46px, 5.25vw, 76px);
+  }
+
+  .aitori-invite-side p {
+    margin-bottom: 16px;
+    padding: 14px 16px;
+    font-size: 14px;
+    line-height: 1.6;
+  }
+
+  .aitori-invite-actions {
+    margin-bottom: 12px;
+  }
+
+  .aitori-invite-note {
+    padding: 11px 14px;
+  }
+}
+
+@media (max-width: 575px) {
+  :root {
+    --aitori-page-bottom-space: 36px;
+  }
+
+  .workbench-panel,
+  .workbench-stat {
+    box-shadow: 0 8px 24px rgba(44, 46, 60, 0.045);
+  }
+
+  .customer-pager-link,
+  .customer-pager-dots {
+    min-width: 42px;
+    min-height: 42px;
+  }
+
+  .stylist-workbench .btn {
+    min-height: 44px;
+  }
+
+  .login-hero {
+    padding-bottom: calc(55px + var(--aitori-safe-bottom));
+  }
+
+  .aitori-beta-apply-hero {
+    padding-bottom: calc(40px + var(--aitori-safe-bottom));
+  }
+
+  .aitori-invite-shell {
+    padding-top: calc(18px + var(--aitori-safe-top));
+    padding-bottom: calc(22px + var(--aitori-safe-bottom));
+  }
+
+  .aitori-invite-mobile-menu {
+    padding-top: calc(18px + var(--aitori-safe-top));
+    padding-bottom: calc(18px + var(--aitori-safe-bottom));
+  }
+}
+
+@media (prefers-reduced-motion: reduce) {
+  html {
+    scroll-behavior: auto;
+  }
+
+  *,
+  *::before,
+  *::after {
+    scroll-behavior: auto !important;
+    transition-duration: 0.01ms !important;
+    animation-duration: 0.01ms !important;
+    animation-iteration-count: 1 !important;
+  }
+}

+ 8 - 7
frontend/src/layouts/DashboardLayout.tsx

@@ -3,12 +3,14 @@ import type { ReactNode } from 'react'
 import { Link, NavLink } from 'react-router-dom'
 import { useLegacyThemePage, usePageContext } from '../shared/pageContext'
 import { AppLogo } from '../components/common/AppLogo'
-import { LocaleSwitcher } from '../components/common/LocaleSwitcher'
 import { logout } from '../services/api/auth'
 
 function DashboardHeader() {
   const { locale, navigate, path, copy } = usePageContext()
   const nav = copy.dashboardNav
+  const navCopy = locale === 'zh'
+    ? { home: '首页', customers: '顾客', templates: '发型模板', logout: '退出登录' }
+    : nav
 
   const handleLogout = async (event: React.MouseEvent) => {
     event.preventDefault()
@@ -43,15 +45,14 @@ function DashboardHeader() {
             </button>
             <div className="collapse navbar-collapse" id="navbarNav">
               <ul className="navbar-nav">
-                <li className="nav-item"><NavLink to={path('/dashboard')} className="nav-link">{nav.home}</NavLink></li>
-                <li className="nav-item"><NavLink to={path('/customers')} className="nav-link">{nav.customers}</NavLink></li>
-                <li className="nav-item"><NavLink to={path('/templates')} className="nav-link">{nav.templates}</NavLink></li>
+                <li className="nav-item"><NavLink to={path('/dashboard')} className="nav-link">{navCopy.home}</NavLink></li>
+                <li className="nav-item"><NavLink to={path('/customers')} className="nav-link">{navCopy.customers}</NavLink></li>
+                <li className="nav-item"><NavLink to={path('/templates')} className="nav-link">{navCopy.templates}</NavLink></li>
               </ul>
             </div>
           </div>
-          <div className="col-auto d-none d-lg-flex stylist-dashboard-actions align-items-center gap-3">
-            <a href={path('/login')} onClick={handleLogout} className="login-header-link">{nav.logout}</a>
-            <LocaleSwitcher locale={locale} />
+          <div className="col-auto d-none d-lg-flex stylist-dashboard-actions">
+            <a href={path('/login')} onClick={handleLogout} className="login-header-link">{navCopy.logout}</a>
           </div>
         </div>
       </nav>

+ 9 - 6
frontend/src/layouts/PublicLayout.tsx

@@ -5,12 +5,15 @@ import { useLegacyThemePage, usePageContext } from '../shared/pageContext'
 import { AppLogo } from '../components/common/AppLogo'
 
 function PublicHeader() {
-  const { path, copy } = usePageContext()
+  const { locale, path, copy } = usePageContext()
   const location = useLocation()
   const profilePath = path('/profile')
   const customerSimulationPath = path('/customer-simulation')
-  const isCustomerSimulation = location.pathname === customerSimulationPath
+  const isCustomerSimulation = location.pathname.startsWith(customerSimulationPath)
   const pageCopy = copy.publicLayout
+  const navCopy = locale === 'zh'
+    ? { profile: '美发师主页', templates: '发型参考', simulation: 'AI发型试试看', reviews: '评价' }
+    : pageCopy.nav
 
   return (
     <header>
@@ -37,10 +40,10 @@ function PublicHeader() {
             </button>
             <div className="collapse navbar-collapse" id="publicNavbarNav">
               <ul className="navbar-nav">
-                <li className="nav-item"><a href={`${profilePath}#profile`} className="nav-link">{pageCopy.nav.profile}</a></li>
-                <li className="nav-item"><a href={`${profilePath}#templates`} className="nav-link">{pageCopy.nav.templates}</a></li>
-                <li className="nav-item"><Link to={customerSimulationPath} className={`nav-link${isCustomerSimulation ? ' active' : ''}`}>{pageCopy.nav.simulation}</Link></li>
-                <li className="nav-item"><a href={`${profilePath}#reviews`} className="nav-link">{pageCopy.nav.reviews}</a></li>
+                <li className="nav-item"><a href={`${profilePath}#profile`} className="nav-link">{navCopy.profile}</a></li>
+                <li className="nav-item"><a href={`${profilePath}#templates`} className="nav-link">{navCopy.templates}</a></li>
+                <li className="nav-item"><Link to={customerSimulationPath} className={`nav-link${isCustomerSimulation ? ' active' : ''}`}>{navCopy.simulation}</Link></li>
+                <li className="nav-item"><a href={`${profilePath}#reviews`} className="nav-link">{navCopy.reviews}</a></li>
               </ul>
             </div>
           </div>

+ 48 - 223
frontend/src/pages/customer/CustomerSimulationPage.tsx

@@ -1,244 +1,69 @@
-import React, { useEffect, useState } from 'react'
+import { useState, type FormEvent } from 'react'
 import { Link } from 'react-router-dom'
 import { Helmet } from 'react-helmet-async'
-import { listAftercareOptions, listSimulationTemplateGallery } from '../../services/api/pageData'
-import type { AftercareOptionEntity, SimulationTemplateEntity } from '../../services/api/contracts'
-import { TEMPLATE_LIMIT, usePageContext, usePreviewUrl } from '../../shared/pageContext'
 import { PublicLayout } from '../../layouts/PublicLayout'
+import { TEMPLATE_LIMIT, usePageContext, usePreviewUrl } from '../../shared/pageContext'
+import {
+  CUSTOMER_SIMULATION_PHOTO_KEY,
+  publicHairstyleTemplates,
+  systemHairstyleTemplates,
+} from '../../shared/demoContent'
+import { DEFAULT_USER_AVATAR_URL } from '../../shared/uiAssets'
 
 export function CustomerSimulationPage() {
-  const { locale, navigate, path, copy } = usePageContext()
-  const [authMode, setAuthMode] = useState('register')
-  const [creditMode, setCreditMode] = useState('stylist-credit')
-  const [simulationType, setSimulationType] = useState('hairstyle')
-  const [selectedTemplates, setSelectedTemplates] = useState<string[]>([])
+  const { locale, navigate, path } = usePageContext()
+  const [code, setCode] = useState('')
+  const [codeChecked, setCodeChecked] = useState(false)
   const [file, setFile] = useState<File | null>(null)
-  const [templateGallery, setTemplateGallery] = useState<SimulationTemplateEntity[]>([])
-  const [aftercareOptions, setAftercareOptions] = useState<AftercareOptionEntity[]>([])
-  const previewUrl = usePreviewUrl(file, '/images/aitori-before-photo.png')
-  const pageCopy = copy.customerSimulationPage
-
-  useEffect(() => {
-    listSimulationTemplateGallery(1, 50).then(({ items }) => setTemplateGallery(items))
-    listAftercareOptions().then(setAftercareOptions)
-  }, [])
+  const [source, setSource] = useState<'stylist' | 'system'>('stylist')
+  const [selectedTemplates, setSelectedTemplates] = useState<string[]>([])
+  const [error, setError] = useState('')
+  const previewUrl = usePreviewUrl(file, DEFAULT_USER_AVATAR_URL)
+  const templates = source === 'stylist' ? publicHairstyleTemplates : systemHairstyleTemplates
 
   const toggleTemplate = (templateId: string) => {
-    setSelectedTemplates((current) => {
-      if (current.includes(templateId)) {
-        return current.filter((item) => item !== templateId)
-      }
-      if (current.length >= TEMPLATE_LIMIT) {
-        return current
-      }
-      return [...current, templateId]
-    })
+    setSelectedTemplates((current) => current.includes(templateId)
+      ? current.filter((item) => item !== templateId)
+      : current.length >= TEMPLATE_LIMIT ? current : [...current, templateId])
   }
 
-  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
+  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
     event.preventDefault()
-    navigate(path(simulationType === 'aftercare' ? '/simulation/aftercare' : '/simulation/result'))
+    if (!code.trim()) return setError('请输入美发师给你的模拟代码。')
+    if (!file) return setError('请上传你的照片。')
+    if (!selectedTemplates.length) return setError('请至少选择一个想尝试的发型。')
+    setError('')
+    const reader = new FileReader()
+    reader.onload = () => {
+      try { window.sessionStorage.setItem(CUSTOMER_SIMULATION_PHOTO_KEY, String(reader.result || '')) } catch { /* keep demo fallback */ }
+      navigate(path('/customer-simulation/result'))
+    }
+    reader.readAsDataURL(file)
   }
 
   return (
     <PublicLayout footer={false}>
-      <Helmet>
-        <html lang={locale === 'ja' ? 'ja' : 'zh-CN'} />
-        <title>{`Aitori Stylist | ${pageCopy.title}`}</title>
-        <meta name="description" content={pageCopy.description} />
-      </Helmet>
+      <Helmet><html lang={locale === 'ja' ? 'ja' : 'zh-CN'} /><title>Aitori Stylist | 顾客 AI 发型试试看</title><meta name="description" content="上传自己的照片,尝试 Demo Stylist 设计的发型模板。" /></Helmet>
       <main className="stylist-workbench">
-        <section className="stylist-workbench-top ipad-top-space-margin mb-2">
-          <div className="container">
-            <div className="d-flex align-items-center justify-content-between flex-wrap gap-3 mb-4">
-              <Link to={path('/profile')} className="btn btn-small btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700"><i className="feather icon-feather-arrow-left me-6px"></i>{pageCopy.back}</Link>
-              <button type="submit" form="customer-simulation-form" className="btn btn-large btn-rounded btn-base-color btn-box-shadow fw-700"><i className="feather icon-feather-cpu me-10px"></i>{pageCopy.submit}</button>
-            </div>
-            <div className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px">
-              <span className="bg-base-color d-inline-block mb-15px fw-600 text-white border-radius-30px ps-20px pe-20px fs-12">{pageCopy.badge}</span>
-              <h1 className="fw-700 text-dark-gray ls-minus-1px mb-10px">{pageCopy.title}</h1>
-              <p className="fs-18 lh-30 mb-0">{pageCopy.lead}</p>
-            </div>
+        <section className="stylist-workbench-top ipad-top-space-margin mb-2"><div className="container">
+          <div className="d-flex align-items-center justify-content-between flex-wrap gap-3 mb-4"><Link to={path('/profile')} className="btn btn-small btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700"><i className="feather icon-feather-arrow-left me-6px"></i>返回美发师主页</Link><button type="submit" form="customer-simulation-form" className="btn btn-large btn-rounded btn-base-color btn-box-shadow fw-700"><i className="feather icon-feather-cpu me-10px"></i>查看模拟效果</button></div>
+          <div className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px"><span className="bg-base-color d-inline-block mb-15px fw-600 text-white border-radius-30px ps-20px pe-20px fs-12">预约前试试看</span><h1 className="fw-700 text-dark-gray ls-minus-1px mb-10px">上传照片,先看看适不适合</h1><p className="fs-18 lh-30 mb-0">选择这位美发师公开的发型参考,AI 会把发型套用到你的照片上。预约或到店咨询前,可以先确认想尝试的方向。</p></div>
+        </div></section>
+        <section className="pt-0"><div className="container"><form id="customer-simulation-form" onSubmit={handleSubmit}>
+          <div className="simulation-step workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px mb-4">
+            <div className="simulation-step-heading"><span>1</span><div><span className="fs-14 fw-700 text-base-color">使用代码</span><h2 className="fw-700 text-dark-gray fs-26 mb-0">输入美发师给你的模拟代码</h2></div></div>
+            <div className="simulation-choice-grid mt-25px mb-25px"><div className="simulation-choice-card is-static"><span className="simulation-choice-icon"><i className="feather icon-feather-gift"></i></span><strong>邀请测试代码</strong><small>现在是内测阶段,请使用美发师或 Aitori 提供的代码体验 AI 发型模拟。</small></div></div>
+            <div className="row g-3"><div className="col-md-8"><label className="form-label fw-700 text-dark-gray" htmlFor="stylist-credit-code">模拟代码</label><input id="stylist-credit-code" type="text" className="form-control border-radius-8px" placeholder="输入美发师给你的代码" value={code} onChange={(event) => { setCode(event.target.value); setCodeChecked(false) }} required /></div><div className="col-md-4 d-flex align-items-end"><button type="button" className="btn btn-large btn-rounded btn-base-color btn-box-shadow fw-700 w-100" onClick={() => setCodeChecked(Boolean(code.trim()))}><i className="feather icon-feather-check-circle me-10px"></i>{codeChecked ? '代码已确认' : '使用代码'}</button></div><div className="col-12"><div className="simulation-note-box"><i className="feather icon-feather-info"></i><span>没有代码?请<Link to={path('/beta-apply')}>联系 Aitori</Link>申请内测体验。</span></div></div></div>
           </div>
-        </section>
-        <section className="pt-0">
-          <div className="container">
-            <form id="customer-simulation-form" onSubmit={handleSubmit}>
-              <div className="simulation-step workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px mb-4">
-                <div className="simulation-step-heading">
-                  <span>1</span>
-                  <div>
-                    <span className="fs-14 fw-700 text-base-color">{pageCopy.auth.badge}</span>
-                    <h2 className="fw-700 text-dark-gray fs-26 mb-0">{pageCopy.auth.title}</h2>
-                  </div>
-                </div>
-                <div className="simulation-choice-grid mt-25px mb-30px">
-                  <label className="simulation-choice-card">
-                    <input type="radio" name="auth-mode" value="register" checked={authMode === 'register'} onChange={() => setAuthMode('register')} />
-                    <span className="simulation-choice-icon"><i className="feather icon-feather-user-plus"></i></span>
-                    <strong>{pageCopy.auth.registerTitle}</strong>
-                    <small>{pageCopy.auth.registerLead}</small>
-                  </label>
-                  <label className="simulation-choice-card">
-                    <input type="radio" name="auth-mode" value="login" checked={authMode === 'login'} onChange={() => setAuthMode('login')} />
-                    <span className="simulation-choice-icon"><i className="feather icon-feather-log-in"></i></span>
-                    <strong>{pageCopy.auth.loginTitle}</strong>
-                    <small>{pageCopy.auth.loginLead}</small>
-                  </label>
-                </div>
-                <div data-customer-auth-panel="register" hidden={authMode !== 'register'}>
-                  <div className="row g-3">
-                    <div className="col-md-6"><label className="form-label fw-700 text-dark-gray" htmlFor="customer-register-name">{pageCopy.auth.name}</label><input id="customer-register-name" name="customer-name" type="text" className="form-control border-radius-8px" placeholder={pageCopy.auth.namePlaceholder} required={authMode === 'register'} disabled={authMode !== 'register'} /></div>
-                    <div className="col-md-6"><label className="form-label fw-700 text-dark-gray" htmlFor="customer-register-kana">{pageCopy.auth.kana}</label><input id="customer-register-kana" name="customer-kana" type="text" className="form-control border-radius-8px" placeholder={pageCopy.auth.kanaPlaceholder} disabled={authMode !== 'register'} /></div>
-                    <div className="col-md-6"><label className="form-label fw-700 text-dark-gray" htmlFor="customer-register-email">{pageCopy.auth.email}</label><input id="customer-register-email" name="customer-email" type="email" className="form-control border-radius-8px" placeholder="example@email.com" required={authMode === 'register'} disabled={authMode !== 'register'} /></div>
-                    <div className="col-md-6"><label className="form-label fw-700 text-dark-gray" htmlFor="customer-register-phone">{pageCopy.auth.phone}</label><input id="customer-register-phone" name="customer-phone" type="tel" className="form-control border-radius-8px" placeholder="090-0000-0000" disabled={authMode !== 'register'} /></div>
-                    <div className="col-md-6"><label className="form-label fw-700 text-dark-gray" htmlFor="customer-register-password">{pageCopy.auth.password}</label><input id="customer-register-password" name="customer-password" type="password" className="form-control border-radius-8px" placeholder={pageCopy.auth.passwordPlaceholder} required={authMode === 'register'} disabled={authMode !== 'register'} /></div>
-                    <div className="col-md-6"><label className="form-label fw-700 text-dark-gray" htmlFor="customer-register-hair">{pageCopy.auth.hairLength}</label><select id="customer-register-hair" name="customer-hair-length" className="form-control border-radius-8px" disabled={authMode !== 'register'}>{pageCopy.auth.hairOptions.map((item: string, index: number) => <option key={item} value={['long','medium','bob','short'][index]}>{item}</option>)}</select></div>
-                    <div className="col-12"><label className="form-label fw-700 text-dark-gray" htmlFor="customer-register-note">{pageCopy.auth.note}</label><textarea id="customer-register-note" name="customer-note" className="form-control border-radius-8px" rows={4} placeholder={pageCopy.auth.notePlaceholder} disabled={authMode !== 'register'}></textarea></div>
-                    <div className="col-12"><button type="button" className="btn btn-large btn-rounded btn-base-color btn-box-shadow fw-700" disabled={authMode !== 'register'}><i className="feather icon-feather-user-plus me-10px"></i>{pageCopy.auth.register}</button></div>
-                  </div>
-                </div>
-                <div data-customer-auth-panel="login" hidden={authMode !== 'login'}>
-                  <div className="row g-3">
-                    <div className="col-md-6"><label className="form-label fw-700 text-dark-gray" htmlFor="customer-login-email">{pageCopy.auth.email}</label><input id="customer-login-email" name="login-email" type="email" className="form-control border-radius-8px" placeholder="example@email.com" required={authMode === 'login'} disabled={authMode !== 'login'} /></div>
-                    <div className="col-md-6"><label className="form-label fw-700 text-dark-gray" htmlFor="customer-login-password">{pageCopy.auth.password}</label><input id="customer-login-password" name="login-password" type="password" className="form-control border-radius-8px" placeholder={pageCopy.auth.loginPasswordPlaceholder} required={authMode === 'login'} disabled={authMode !== 'login'} /></div>
-                    <div className="col-12"><button type="button" className="btn btn-large btn-rounded btn-base-color btn-box-shadow fw-700" disabled={authMode !== 'login'}><i className="feather icon-feather-log-in me-10px"></i>{pageCopy.auth.login}</button></div>
-                  </div>
-                </div>
-              </div>
-
-              <div className="simulation-step workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px mb-4">
-                <div className="simulation-step-heading">
-                  <span>2</span>
-                  <div>
-                    <span className="fs-14 fw-700 text-base-color">{pageCopy.credit.badge}</span>
-                    <h2 className="fw-700 text-dark-gray fs-26 mb-0">{pageCopy.credit.title}</h2>
-                  </div>
-                </div>
-                <div className="simulation-choice-grid mt-25px mb-30px">
-                  <label className="simulation-choice-card">
-                    <input type="radio" name="credit-mode" value="stylist-credit" checked={creditMode === 'stylist-credit'} onChange={() => setCreditMode('stylist-credit')} />
-                    <span className="simulation-choice-icon"><i className="feather icon-feather-gift"></i></span>
-                    <strong>{pageCopy.credit.stylistTitle}</strong>
-                    <small>{pageCopy.credit.stylistLead}</small>
-                  </label>
-                  <label className="simulation-choice-card">
-                    <input type="radio" name="credit-mode" value="purchase" checked={creditMode === 'purchase'} onChange={() => setCreditMode('purchase')} />
-                    <span className="simulation-choice-icon"><i className="feather icon-feather-credit-card"></i></span>
-                    <strong>{pageCopy.credit.purchaseTitle}</strong>
-                    <small>{pageCopy.credit.purchaseLead}</small>
-                  </label>
-                </div>
-                <div data-credit-panel="stylist-credit" hidden={creditMode !== 'stylist-credit'}>
-                  <div className="row g-3">
-                    <div className="col-md-8"><label className="form-label fw-700 text-dark-gray" htmlFor="stylist-credit-code">{pageCopy.credit.code}</label><input id="stylist-credit-code" name="stylist-credit-code" type="text" className="form-control border-radius-8px" placeholder={pageCopy.credit.codePlaceholder} disabled={creditMode !== 'stylist-credit'} /></div>
-                    <div className="col-md-4 d-flex align-items-end"><button type="button" className="btn btn-large btn-rounded btn-base-color btn-box-shadow fw-700 w-100" disabled={creditMode !== 'stylist-credit'}><i className="feather icon-feather-check-circle me-10px"></i>{pageCopy.credit.useCredit}</button></div>
-                  </div>
-                </div>
-                <div data-credit-panel="purchase" hidden={creditMode !== 'purchase'}>
-                  <div className="simulation-choice-grid">
-                    <label className="simulation-choice-card">
-                      <input type="radio" name="credit-pack" value="single" defaultChecked disabled={creditMode !== 'purchase'} />
-                      <span className="simulation-choice-icon"><i className="feather icon-feather-zap"></i></span>
-                      <strong>{pageCopy.credit.pack1Title}</strong>
-                      <small>{pageCopy.credit.pack1Lead}</small>
-                    </label>
-                    <label className="simulation-choice-card">
-                      <input type="radio" name="credit-pack" value="five" disabled={creditMode !== 'purchase'} />
-                      <span className="simulation-choice-icon"><i className="feather icon-feather-layers"></i></span>
-                      <strong>{pageCopy.credit.pack5Title}</strong>
-                      <small>{pageCopy.credit.pack5Lead}</small>
-                    </label>
-                  </div>
-                  <button type="button" className="btn btn-large btn-rounded btn-base-color btn-box-shadow fw-700 mt-25px" disabled={creditMode !== 'purchase'}><i className="feather icon-feather-credit-card me-10px"></i>{pageCopy.credit.purchase}</button>
-                </div>
-              </div>
-
-              <div className="simulation-step workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px mb-4">
-                <div className="simulation-step-heading">
-                  <span>3</span>
-                  <div>
-                    <span className="fs-14 fw-700 text-base-color">{pageCopy.photo.badge}</span>
-                    <h2 className="fw-700 text-dark-gray fs-26 mb-0">{pageCopy.photo.title}</h2>
-                  </div>
-                </div>
-                <label htmlFor="customer-simulation-photo-upload" className="simulation-photo-upload mt-25px">
-                  <input id="customer-simulation-photo-upload" type="file" accept="image/*" onChange={(event) => setFile(event.target.files?.[0] || null)} required />
-                  <img id="customer-simulation-photo-preview" src={previewUrl} alt={pageCopy.photo.previewAlt} />
-                  <span><i className="feather icon-feather-upload me-8px"></i>{pageCopy.photo.upload}</span>
-                </label>
-              </div>
-
-              <div className="simulation-step workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px">
-                <div className="simulation-step-heading">
-                  <span>4</span>
-                  <div>
-                    <span className="fs-14 fw-700 text-base-color">{pageCopy.simulation.badge}</span>
-                    <h2 className="fw-700 text-dark-gray fs-26 mb-0">{pageCopy.simulation.title}</h2>
-                  </div>
-                </div>
-                <div className="simulation-choice-grid mt-25px mb-30px">
-                  <label className="simulation-choice-card">
-                    <input type="radio" name="simulation-type" value="hairstyle" checked={simulationType === 'hairstyle'} onChange={() => setSimulationType('hairstyle')} />
-                    <span className="simulation-choice-icon"><i className="feather icon-feather-scissors"></i></span>
-                    <strong>{pageCopy.simulation.hairstyleTitle}</strong>
-                    <small>{pageCopy.simulation.hairstyleLead}</small>
-                  </label>
-                  <label className="simulation-choice-card">
-                    <input type="radio" name="simulation-type" value="aftercare" checked={simulationType === 'aftercare'} onChange={() => setSimulationType('aftercare')} />
-                    <span className="simulation-choice-icon"><i className="feather icon-feather-calendar"></i></span>
-                    <strong>{pageCopy.simulation.aftercareTitle}</strong>
-                    <small>{pageCopy.simulation.aftercareLead}</small>
-                  </label>
-                </div>
-                <div data-customer-simulation-panel="hairstyle" hidden={simulationType !== 'hairstyle'}>
-                  <div className="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-25px">
-                    <p className="mb-0 fs-15 text-medium-gray">{pageCopy.simulation.templateLead}</p>
-                    <span className="simulation-count-badge">{pageCopy.simulation.selectedCount} <strong>{selectedTemplates.length}</strong> / 5</span>
-                  </div>
-                  <div className="simulation-template-grid">
-                    {templateGallery.slice(0, 10).map((template) => {
-                      const active = selectedTemplates.includes(template.id)
-                      return (
-                        <label key={template.id} className="simulation-template-card">
-                          <input type="checkbox" name="templates" value={template.id} checked={active} disabled={!active && selectedTemplates.length >= TEMPLATE_LIMIT} onChange={() => toggleTemplate(template.id)} />
-                          <img src={template.image} alt={template.name} />
-                          <span>{template.name}</span>
-                        </label>
-                      )
-                    })}
-                  </div>
-                </div>
-                <div data-customer-simulation-panel="aftercare" hidden={simulationType !== 'aftercare'}>
-                  <div className="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-25px">
-                    <p className="mb-0 fs-15 text-medium-gray">{pageCopy.simulation.aftercarePanelLead}</p>
-                    <span className="simulation-count-badge">{pageCopy.simulation.aftercareCount}</span>
-                  </div>
-                  <div className="simulation-template-grid simulation-aftercare-grid">
-                    {aftercareOptions.map((item) => (
-                      <label key={item.id} className="simulation-template-card">
-                        <input type="checkbox" name="aftercare-templates" value={item.id} checked readOnly />
-                        <img src={item.image} alt={item.title} />
-                        <span>{pageCopy.simulation.aftercareItems[item.id as keyof typeof pageCopy.simulation.aftercareItems] || item.title}</span>
-                      </label>
-                    ))}
-                  </div>
-                </div>
-              </div>
-
-              <div className="customer-edit-actions workbench-panel border-radius-8px p-25px mt-4">
-                <div>
-                  <strong>{pageCopy.submitPanel.title}</strong>
-                  <span>{pageCopy.submitPanel.lead}</span>
-                </div>
-                <div className="d-flex flex-wrap gap-2">
-                  <Link to={path('/profile')} className="btn btn-large btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700">{copy.common.cancel}</Link>
-                  <button type="submit" className="btn btn-large btn-rounded btn-base-color btn-box-shadow fw-700"><i className="feather icon-feather-cpu me-10px"></i>{pageCopy.submit}</button>
-                </div>
-              </div>
-            </form>
+          <div className="simulation-step workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px mb-4"><div className="simulation-step-heading"><span>2</span><div><h2 className="fw-700 text-dark-gray fs-26 mb-0">上传你的照片</h2></div></div><label htmlFor="customer-simulation-photo-upload" className="simulation-photo-upload mt-25px"><input id="customer-simulation-photo-upload" type="file" accept="image/*" onChange={(event) => setFile(event.target.files?.[0] || null)} required /><img src={previewUrl} alt="上传你的照片" /><span><i className="feather icon-feather-upload me-8px"></i>支持 JPG / PNG</span></label></div>
+          <div className="simulation-step workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px">
+            <div className="simulation-step-heading"><span>3</span><div><h2 className="fw-700 text-dark-gray fs-26 mb-0">选择想尝试的发型</h2></div></div>
+            <div className="simulation-template-tabs mt-25px mb-20px"><label><input type="radio" name="template-source" checked={source === 'stylist'} onChange={() => setSource('stylist')} /><span><i className="feather icon-feather-grid"></i>美发师设计的发型</span></label><label><input type="radio" name="template-source" checked={source === 'system'} onChange={() => setSource('system')} /><span><i className="feather icon-feather-layers"></i>Aitori系统模板</span></label></div>
+            <div className="d-flex align-items-center justify-content-end flex-wrap gap-2 mb-25px"><span className="simulation-count-badge">已选 <strong>{selectedTemplates.length}</strong> 个发型</span></div>
+            <div className={`simulation-template-grid mb-30px${source === 'system' ? ' simulation-system-template-grid' : ''}`}>{templates.map((template) => { const active = selectedTemplates.includes(template.id); return <label key={template.id} className="simulation-template-card"><input type="checkbox" checked={active} disabled={!active && selectedTemplates.length >= TEMPLATE_LIMIT} onChange={() => toggleTemplate(template.id)} /><img src={template.image} alt={template.name} /><span>{template.name}</span></label> })}</div>
           </div>
-        </section>
+          <div className="customer-edit-actions workbench-panel border-radius-8px p-25px mt-4"><div><strong>查看 AI 发型效果</strong><span>根据你的照片和选择的发型,生成预约前可参考的模拟图。</span>{error ? <span className="text-red mt-5px" role="alert">{error}</span> : null}</div><div className="d-flex flex-wrap gap-2"><Link to={path('/profile')} className="btn btn-large btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700">取消</Link><button type="submit" className="btn btn-large btn-rounded btn-base-color btn-box-shadow fw-700"><i className="feather icon-feather-cpu me-10px"></i>查看模拟效果</button></div></div>
+        </form></div></section>
       </main>
     </PublicLayout>
   )

+ 39 - 0
frontend/src/pages/customer/CustomerSimulationResultPage.tsx

@@ -0,0 +1,39 @@
+import { useState } from 'react'
+import { Link } from 'react-router-dom'
+import { Helmet } from 'react-helmet-async'
+import { PublicLayout } from '../../layouts/PublicLayout'
+import { ImageLightbox } from '../../components/common/ImageLightbox'
+import { demoSimulationResults } from '../../shared/demoContent'
+import { usePageContext } from '../../shared/pageContext'
+import { CUSTOMER_SIMULATION_PHOTO_KEY } from '../../shared/demoContent'
+
+export function CustomerSimulationResultPage() {
+  const { locale, path } = usePageContext()
+  const [preview, setPreview] = useState<{ image: string; alt: string } | null>(null)
+  const sourcePhoto = (() => {
+    try { return window.sessionStorage.getItem(CUSTOMER_SIMULATION_PHOTO_KEY) || '/images/mvp-simulation-result-10/source-photo.jpg' } catch { return '/images/mvp-simulation-result-10/source-photo.jpg' }
+  })()
+
+  return (
+    <PublicLayout footer={false}>
+      <Helmet><html lang={locale === 'ja' ? 'ja' : 'zh-CN'} /><title>Aitori Stylist | AI 试发型结果</title></Helmet>
+      <main className="stylist-workbench">
+        <section className="stylist-workbench-top ipad-top-space-margin mb-2"><div className="container"><div className="d-flex align-items-center justify-content-between flex-wrap gap-3 mb-4"><Link to={path('/customer-simulation')} className="btn btn-small btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700"><i className="feather icon-feather-arrow-left me-6px"></i>重新选择</Link><Link to={path('/profile')} className="btn btn-large btn-rounded btn-base-color btn-box-shadow fw-700"><i className="feather icon-feather-user me-10px"></i>返回美发师主页</Link></div></div></section>
+        <section className="pt-0"><div className="container"><div className="simulation-result-layout gallery-box">
+          <aside className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-25px">
+            <h2 className="fw-700 text-dark-gray fs-24 mb-20px">你的照片</h2>
+            <button type="button" className="simulation-original-card aitori-image-button" onClick={() => setPreview({ image: sourcePhoto, alt: '你的照片' })}><img className="simulation-result-image" src={sourcePhoto} alt="上传的照片" /></button>
+            <div className="template-detail-summary mt-20px"><div><span>发型师</span><strong>Demo Stylist</strong></div><div><span>生成时间</span><strong>2026/07/03</strong></div><div><span>生成数量</span><strong>3个模拟结果</strong></div></div>
+            <div className="simulation-note-box mt-20px"><i className="feather icon-feather-info"></i><span>模拟图用于预约前参考。最终发型会根据发质、长度和现场咨询调整。</span></div>
+          </aside>
+          <div className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px">
+            <div className="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-25px"><div><span className="fs-14 fw-700 text-base-color">Demo Result</span><h1 className="fw-700 text-dark-gray fs-26 mb-0">AI 试发型结果</h1></div><span className="simulation-count-badge">3个结果</span></div>
+            <div className="simulation-result-grid">{demoSimulationResults.map((item) => <article key={item.id} className="simulation-result-card"><button type="button" className="aitori-image-button" onClick={() => setPreview({ image: item.image, alt: item.title })}><img className="simulation-result-image" src={item.image} alt={item.title} /></button><div className="simulation-result-body"><h3>{item.title}</h3><span className="template-status-badge">Aitori系统模板</span><p>{item.customerNote}</p></div></article>)}</div>
+            <div className="customer-edit-actions border-radius-8px p-25px mt-4"><div><strong>喜欢这个方向吗?</strong><span>把模拟结果作为参考,到店时可以和 Demo Stylist 更具体地讨论长度、脸周和造型细节。</span></div><div className="d-flex flex-wrap gap-2"><Link to={path('/customer-simulation')} className="btn btn-large btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700">再试一次</Link><Link to={path('/profile')} className="btn btn-large btn-rounded btn-base-color btn-box-shadow fw-700">返回美发师主页</Link></div></div>
+          </div>
+        </div></div></section>
+      </main>
+      <ImageLightbox image={preview?.image || null} alt={preview?.alt} onClose={() => setPreview(null)} />
+    </PublicLayout>
+  )
+}

+ 38 - 157
frontend/src/pages/customer/StylistProfilePage.tsx

@@ -1,172 +1,53 @@
-import React, { useEffect, useState } from 'react'
+import { useState } from 'react'
 import { Link } from 'react-router-dom'
 import { Helmet } from 'react-helmet-async'
-import { listStylistProfileTemplates, listStylistReviewCards } from '../../services/api/pageData'
-import type { MockStylistProfileTemplate, MockStylistReviewCard } from '../../mocks/db'
-import { usePageContext } from '../../shared/pageContext'
 import { PublicLayout } from '../../layouts/PublicLayout'
+import { ImageLightbox } from '../../components/common/ImageLightbox'
+import { publicHairstyleTemplates, type PublicHairstyleTemplate } from '../../shared/demoContent'
+import { getStoredStylistProfile } from '../../shared/stylistProfileStore'
+import { usePageContext } from '../../shared/pageContext'
 
-export function StylistProfilePage() {
-  const { locale, path, copy } = usePageContext()
-  const [profileTemplates, setProfileTemplates] = useState<MockStylistProfileTemplate[]>([])
-  const [reviewCards, setReviewCards] = useState<MockStylistReviewCard[]>([])
-  const pageCopy = copy.stylistProfilePage
+type TemplateFilter = '*' | PublicHairstyleTemplate['category']
 
-  useEffect(() => {
-    listStylistProfileTemplates().then(setProfileTemplates)
-    listStylistReviewCards().then(setReviewCards)
-  }, [])
+export function StylistProfilePage() {
+  const { locale, path } = usePageContext()
+  const [profile] = useState(getStoredStylistProfile)
+  const [filter, setFilter] = useState<TemplateFilter>('*')
+  const [preview, setPreview] = useState<PublicHairstyleTemplate | null>(null)
+  const visibleTemplates = filter === '*' ? publicHairstyleTemplates : publicHairstyleTemplates.filter((item) => item.category === filter)
 
   const structuredData = {
-    '@context': 'https://schema.org',
-    '@type': 'HairSalon',
-    name: 'YASUAKI OGAWA',
-    description: pageCopy.structuredDescription,
-    image: '/images/stylist-profile/ogawa.jpeg',
-    address: {
-      '@type': 'PostalAddress',
-      streetAddress: '神宫前5丁目2-19 表参道山田大楼2楼 共享沙龙pomie表参道',
-      addressLocality: '涩谷区',
-      addressRegion: '东京都',
-      addressCountry: 'JP',
-    },
-    aggregateRating: {
-      '@type': 'AggregateRating',
-      ratingValue: '5.0',
-      reviewCount: '62',
-    },
-    openingHoursSpecification: [
-      { '@type': 'OpeningHoursSpecification', dayOfWeek: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'], opens: '10:00', closes: '21:00' },
-      { '@type': 'OpeningHoursSpecification', dayOfWeek: 'Saturday', opens: '10:00', closes: '20:00' },
-      { '@type': 'OpeningHoursSpecification', dayOfWeek: 'Sunday', opens: '10:00', closes: '19:00' },
-    ],
-    url: 'https://yasuaki-ogawa.stores.jp/reserve/beauty',
+    '@context': 'https://schema.org', '@type': 'Person', name: profile.displayName,
+    description: profile.lead, image: profile.photo, worksFor: { '@type': 'HairSalon', name: profile.salonName },
   }
 
   return (
     <PublicLayout>
-      <Helmet>
-        <html lang={locale === 'ja' ? 'ja' : 'zh-CN'} />
-        <title>{`Aitori Stylist | ${pageCopy.title}`}</title>
-        <meta name="description" content={pageCopy.description} />
-        <script type="application/ld+json">{JSON.stringify(structuredData)}</script>
-      </Helmet>
-      <section id="profile" className="stylist-profile-hero cover-background ipad-top-space-margin" style={{ backgroundImage: 'linear-gradient(135deg, #fff8f6 0%, #fff 56%, #f7f4ef 100%)' }}>
-        <div className="container">
-          <div className="row align-items-center justify-content-center">
-            <div className="col-lg-5 col-md-8 md-mb-40px">
-              <div className="stylist-profile-photo bg-white border-radius-8px box-shadow-quadruple-large p-15px">
-                <img src="/images/stylist-profile/ogawa.jpeg" alt={pageCopy.photoAlt} className="border-radius-6px w-100" />
-              </div>
-            </div>
-            <div className="col-lg-6 offset-lg-1">
-              <span className="bg-base-color d-inline-block mb-20px fw-600 text-white text-uppercase border-radius-30px ps-20px pe-20px fs-12">Aitori Stylist</span>
-              <h1 className="fw-700 text-dark-gray ls-minus-1px mb-20px">{pageCopy.name}</h1>
-              <p className="fs-20 lh-32 text-dark-gray fw-600 mb-20px">{pageCopy.lead}</p>
-              <p className="fs-17 lh-30 mb-30px">{pageCopy.bio}</p>
-              <div className="mb-30px">
-                <a href="https://yasuaki-ogawa.stores.jp/reserve/beauty/1988939" target="_blank" rel="noopener" className="btn btn-extra-large btn-rounded btn-base-color btn-box-shadow fw-700">{pageCopy.book}</a>
-                <Link to={path('/customer-simulation')} className="btn btn-extra-large btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700 ms-10px xs-ms-0 xs-mt-10px">{pageCopy.tryAi}</Link>
-              </div>
-              <div className="row row-cols-3 g-3 text-center">
-                <div className="col"><div className="bg-white border-radius-8px box-shadow-extra-large p-20px h-100"><span className="d-block fs-24 fw-800 text-dark-gray">22年</span><span className="fs-14">{pageCopy.stats.experience}</span></div></div>
-                <div className="col"><div className="bg-white border-radius-8px box-shadow-extra-large p-20px h-100"><span className="d-block fs-24 fw-800 text-dark-gray">5.0</span><span className="fs-14">{pageCopy.stats.rating}</span></div></div>
-                <div className="col"><div className="bg-white border-radius-8px box-shadow-extra-large p-20px h-100"><span className="d-block fs-24 fw-800 text-dark-gray">62</span><span className="fs-14">{pageCopy.stats.reviews}</span></div></div>
-              </div>
-              <ul className="p-0 mt-25px mb-0 list-style-02 text-dark-gray">
-                {pageCopy.achievements.map((item: string, index: number) => (
-                  <li key={item} className={index < pageCopy.achievements.length - 1 ? 'pb-10px' : ''}><i className="feather icon-feather-check text-green me-10px"></i>{item}</li>
-                ))}
-              </ul>
-            </div>
-          </div>
-        </div>
-      </section>
-      <section id="basic-info" className="half-section pt-0">
-        <div className="container">
-          <div className="row justify-content-center mb-5">
-            <div className="col-lg-8 text-center">
-              <span className="bg-base-color d-inline-block mb-15px fw-600 text-white text-uppercase border-radius-30px ps-20px pe-20px fs-12">{pageCopy.basicInfo.badge}</span>
-              <h2 className="fw-700 text-dark-gray ls-minus-1px">{pageCopy.basicInfo.title}</h2>
-              <p className="w-80 lg-w-100 mx-auto">{pageCopy.basicInfo.lead}</p>
-            </div>
-          </div>
-          <div className="row row-cols-1 row-cols-lg-3 row-cols-md-2 justify-content-center g-4">
-            <div className="col"><div className="bg-white border-radius-8px box-shadow-extra-large p-30px h-100"><h6 className="fw-700 text-dark-gray mb-15px">{pageCopy.basicInfo.cards.address}</h6><p className="mb-0">{pageCopy.basicInfo.address.split('\n').map((line: string, index: number) => <React.Fragment key={line}>{index > 0 ? <br /> : null}{line}</React.Fragment>)}</p></div></div>
-            <div className="col"><div className="bg-white border-radius-8px box-shadow-extra-large p-30px h-100"><h6 className="fw-700 text-dark-gray mb-15px">{pageCopy.basicInfo.cards.hours}</h6><p className="mb-10px">{pageCopy.basicInfo.hours[0]}</p><p className="mb-10px">{pageCopy.basicInfo.hours[1]}</p><p className="mb-0">{pageCopy.basicInfo.hours[2]}</p></div></div>
-            <div className="col"><div className="bg-white border-radius-8px box-shadow-extra-large p-30px h-100"><h6 className="fw-700 text-dark-gray mb-15px">{pageCopy.basicInfo.cards.specialty}</h6><p className="mb-0">{pageCopy.basicInfo.specialtyText}</p></div></div>
-          </div>
-          <div className="row justify-content-center mt-4">
-            <div className="col-lg-8 text-center">
-              <p className="fs-14 mb-0">{pageCopy.basicInfo.source} <a href="https://yasuaki-ogawa.stores.jp/reserve/beauty" target="_blank" rel="noopener">YASUAKI OGAWA</a></p>
-            </div>
-          </div>
-        </div>
-      </section>
-      <section id="templates" className="bg-gradient-very-light-gray">
-        <div className="container">
-          <div className="row justify-content-center mb-5">
-            <div className="col-lg-8 text-center">
-              <span className="bg-base-color d-inline-block mb-15px fw-600 text-white text-uppercase border-radius-30px ps-20px pe-20px fs-12">{pageCopy.templates.badge}</span>
-              <h2 className="fw-700 text-dark-gray ls-minus-1px">{pageCopy.templates.title}</h2>
-              <p className="w-80 lg-w-100 mx-auto">{pageCopy.templates.lead}</p>
-            </div>
-          </div>
-          <div className="row">
-            <div className="col-12 text-center">
-              <ul className="portfolio-filter nav nav-tabs justify-content-center border-0 fw-500 alt-font pb-5">
-                <li className="nav active"><a data-filter="*" href="#">{pageCopy.templates.filters[0]}</a></li>
-                <li className="nav"><a data-filter=".long" href="#">{pageCopy.templates.filters[1]}</a></li>
-                <li className="nav"><a data-filter=".medium" href="#">{pageCopy.templates.filters[2]}</a></li>
-                <li className="nav"><a data-filter=".short" href="#">{pageCopy.templates.filters[3]}</a></li>
-              </ul>
-            </div>
-          </div>
-          <div className="row">
-            <div className="col-12 filter-content">
-              <ul className="portfolio-creative stylist-hairstyle-portfolio portfolio-wrapper grid-loading grid grid-4col xxl-grid-4col xl-grid-4col lg-grid-3col md-grid-2col sm-grid-2col xs-grid-1col gutter-large text-center">
-                <li className="grid-sizer"></li>
-                {profileTemplates.map((item, index) => (
-                  <li key={`${item.text}-${index}`} className={`grid-item ${item.cls} transition-inner-all`}>
-                    <div className="portfolio-box">
-                      <a href={item.image} data-group="hairstyle-gallery" title={`${item.category}${pageCopy.templates.templateSuffix} ${String(index + 1).padStart(2, '0')}`}>
-                        <div className="portfolio-image"><img src={item.image} alt={`${item.category}${pageCopy.templates.templateSuffix} ${String(index + 1).padStart(2, '0')}`} /></div>
-                        <div className="portfolio-hover justify-content-center align-items-center d-flex flex-column pt-40px pb-40px">
-                          <div className="text-uppercase portfolio-title mt-auto alt-font fs-26 fw-700 text-white" data-text={item.text}></div>
-                          <span className="mt-auto d-inline-block text-uppercase pt-5px pb-5px fw-500 ps-15px pe-15px bg-white text-dark-gray fs-12 lh-20 border-radius-2px">{item.category}</span>
-                        </div>
-                      </a>
-                    </div>
-                  </li>
-                ))}
-              </ul>
-            </div>
-          </div>
-        </div>
-      </section>
-      <section id="reviews">
-        <div className="container">
-          <div className="row justify-content-center mb-5">
-            <div className="col-lg-8 text-center">
-              <span className="bg-base-color d-inline-block mb-15px fw-600 text-white text-uppercase border-radius-30px ps-20px pe-20px fs-12">{pageCopy.reviewsSection.badge}</span>
-              <h2 className="fw-700 text-dark-gray ls-minus-1px">{pageCopy.reviewsSection.title}</h2>
-              <p className="w-80 lg-w-100 mx-auto">{pageCopy.reviewsSection.lead}</p>
-            </div>
-          </div>
-          <div className="row row-cols-1 row-cols-lg-3 row-cols-md-2 justify-content-center g-4">
-            {reviewCards.map((item) => (
-              <div key={item.title} className="col">
-                <div className="bg-white border-radius-8px box-shadow-extra-large p-30px h-100">
-                  <div className="review-star-icon mb-15px"><i className="bi bi-star-fill"></i><i className="bi bi-star-fill"></i><i className="bi bi-star-fill"></i><i className="bi bi-star-fill"></i><i className="bi bi-star-fill"></i></div>
-                  <h6 className="fw-700 text-dark-gray mb-10px">{item.title}</h6>
-                  <p className="mb-20px">{item.body}</p>
-                  <span className="fw-700 text-dark-gray">{item.role}</span>
-                </div>
-              </div>
-            ))}
-          </div>
-        </div>
+      <Helmet><html lang={locale === 'ja' ? 'ja' : 'zh-CN'} /><title>Aitori Stylist | Demo Stylist 发型模板</title><meta name="description" content="选择 Demo Stylist 设计的发型模板,并用自己的照片进行 AI 试发型。" /><script type="application/ld+json">{JSON.stringify(structuredData)}</script></Helmet>
+      <section id="profile" className="stylist-profile-hero public-profile-hero cover-background ipad-top-space-margin">
+        <div className="container"><div className="row align-items-center justify-content-center">
+          <div className="col-lg-5 col-md-8 md-mb-40px"><div className="stylist-profile-photo public-profile-photo bg-white border-radius-8px box-shadow-quadruple-large p-15px"><img src={profile.photo} alt={`${profile.displayName} 的头像`} className="border-radius-6px w-100" /></div></div>
+          <div className="col-lg-6 offset-lg-1"><span className="bg-base-color d-inline-block mb-20px fw-600 text-white text-uppercase border-radius-30px ps-20px pe-20px fs-12">{profile.displayName} 的发型模板</span><h1 className="fw-700 text-dark-gray ls-minus-1px mb-20px">用你的照片,试试这位发型师设计的发型。</h1><p className="fs-20 lh-32 text-dark-gray fw-600 mb-20px">这里收录了 {profile.displayName} 常用于咨询的发型模板。选择喜欢的发型后,可以上传自己的照片,用 AI 先看看大致效果。</p><p className="fs-17 lh-30 mb-30px">适合在预约前确认长度、脸周层次、刘海和整体氛围。模拟结果可作为到店咨询时的参考。</p><div className="mb-30px"><Link to={path('/customer-simulation')} className="btn btn-extra-large btn-rounded btn-base-color btn-box-shadow fw-700"><i className="feather icon-feather-camera me-10px"></i>用我的照片试发型</Link><a href="#templates" className="btn btn-extra-large btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700 ms-10px xs-ms-0 xs-mt-10px">先看发型模板</a></div><div className="row row-cols-3 g-3 text-center public-profile-stats"><div className="col"><div className="bg-white border-radius-8px p-20px h-100"><span className="d-block fs-24 fw-800 text-dark-gray">{profile.years}</span><span className="fs-14">经验</span></div></div><div className="col"><div className="bg-white border-radius-8px p-20px h-100"><span className="d-block fs-24 fw-800 text-dark-gray">8</span><span className="fs-14">公开模板</span></div></div><div className="col"><div className="bg-white border-radius-8px p-20px h-100"><span className="d-block fs-24 fw-800 text-dark-gray">{profile.rating}</span><span className="fs-14">评分</span></div></div></div></div>
+        </div></div>
       </section>
+
+      <section className="half-section public-profile-actions pt-0"><div className="container"><div className="row row-cols-1 row-cols-lg-3 row-cols-md-3 justify-content-center g-4"><div className="col"><a href="#templates" className="public-profile-action-card public-profile-action-gallery"><i className="feather icon-feather-grid"></i><span><strong>选择发型模板</strong><small>浏览这位发型师公开的推荐发型。</small></span></a></div><div className="col"><Link to={path('/customer-simulation')} className="public-profile-action-card public-profile-action-simulation"><i className="feather icon-feather-camera"></i><span><strong>上传照片试发型</strong><small>用你的照片生成 AI 模拟效果。</small></span></Link></div><div className="col"><a href="#stylist" className="public-profile-action-card public-profile-action-booking"><i className="feather icon-feather-user"></i><span><strong>了解发型师</strong><small>查看擅长风格和咨询特点。</small></span></a></div></div></div></section>
+
+      <section id="templates" className="bg-gradient-very-light-gray"><div className="container">
+        <div className="row justify-content-center mb-5"><div className="col-lg-8 text-center"><span className="bg-base-color d-inline-block mb-15px fw-600 text-white text-uppercase border-radius-30px ps-20px pe-20px fs-12">Style Templates</span><h2 className="fw-700 text-dark-gray ls-minus-1px">选择你想尝试的发型</h2><p className="w-80 lg-w-100 mx-auto">点击图片可以放大查看。决定想试哪一款后,进入 AI 试发型流程,上传自己的照片进行模拟。</p></div></div>
+        <div className="row"><div className="col-12 text-center"><ul className="portfolio-filter nav nav-tabs justify-content-center border-0 fw-500 alt-font pb-5">{([['*', '全部'], ['long', '长发'], ['medium', '中长发'], ['short', '短发']] as const).map(([value, label]) => <li key={value} className={`nav${filter === value ? ' active' : ''}`}><button type="button" onClick={() => setFilter(value)}>{label}</button></li>)}</ul></div></div>
+        <ul className="aitori-template-gallery-grid">{visibleTemplates.map((item) => <li key={item.id} className="transition-inner-all"><div className="portfolio-box"><button type="button" onClick={() => setPreview(item)}><div className="portfolio-image"><img src={item.image} alt={item.name} /></div><div className="portfolio-hover justify-content-center align-items-center d-flex flex-column pt-40px pb-40px"><div className="text-uppercase portfolio-title mt-auto alt-font fs-26 fw-700 text-white" data-text={item.accent}></div><span className="mt-auto d-inline-block text-uppercase pt-5px pb-5px fw-500 ps-15px pe-15px bg-white text-dark-gray fs-12 lh-20 border-radius-2px">{item.name}</span></div></button></div></li>)}</ul>
+      </div></section>
+
+      <section id="stylist" className="half-section"><div className="container"><div className="row align-items-center justify-content-center"><div className="col-lg-5 md-mb-35px"><span className="bg-base-color d-inline-block mb-15px fw-600 text-white text-uppercase border-radius-30px ps-20px pe-20px fs-12">Stylist</span><h2 className="fw-700 text-dark-gray ls-minus-1px mb-20px">{profile.displayName}</h2><p className="fs-18 lh-30 mb-20px">{profile.specialties}</p><p className="mb-0">如果你还不确定适合什么长度或风格,可以先用模板模拟,再把结果带到咨询里一起讨论。</p></div><div className="col-lg-5 offset-lg-1"><div className="row row-cols-1 g-3"><InfoCard label="沙龙" value={profile.address.replace(/\n/g, ' ')} /><InfoCard label="营业时间" value={profile.hours.replace(/\n/g, ' / ')} /><InfoCard label="擅长" value="脸周层次、自然长发、清爽短发、易打理造型" /></div></div></div></div></section>
+
+      <section id="reviews" className="bg-gradient-very-light-gray half-section"><div className="container"><div className="row justify-content-center mb-5"><div className="col-lg-8 text-center"><span className="bg-base-color d-inline-block mb-15px fw-600 text-white text-uppercase border-radius-30px ps-20px pe-20px fs-12">Voice</span><h2 className="fw-700 text-dark-gray ls-minus-1px">顾客反馈</h2></div></div><div className="row row-cols-1 row-cols-lg-3 row-cols-md-2 justify-content-center g-4"><Review title="到店前就比较安心" body="先看过模拟图,再和发型师讨论,比较容易说出自己想要的感觉。" /><Review title="更容易比较长度" body="长一点、短一点的差别可以先看效果,决定发型时不会那么犹豫。" /><Review title="咨询更具体" body="拿着模拟结果沟通,发型师也更容易理解我想改变的地方。" /></div></div></section>
+
+      <section className="half-section"><div className="container"><div className="public-profile-bottom-cta"><div><span className="bg-base-color d-inline-block mb-15px fw-600 text-white text-uppercase border-radius-30px ps-20px pe-20px fs-12">AI Try-on</span><h2 className="fw-700 text-dark-gray fs-32 mb-10px">选好发型后,用你的照片试试看。</h2><p className="mb-0">模拟结果不是最终承诺,而是帮助你和发型师更快对齐方向的参考。</p></div><Link to={path('/customer-simulation')} className="btn btn-large btn-rounded btn-base-color btn-box-shadow fw-700">开始 AI 试发型</Link></div></div></section>
+      <ImageLightbox image={preview?.image || null} alt={preview?.name} onClose={() => setPreview(null)} action={<Link to={path('/customer-simulation')} className="btn btn-large btn-rounded btn-base-color btn-box-shadow fw-700">用我的照片试这款</Link>} />
     </PublicLayout>
   )
 }
+
+function InfoCard({ label, value }: { label: string; value: string }) { return <div className="col"><div className="bg-white border-radius-8px box-shadow-extra-large p-25px"><strong className="text-dark-gray d-block mb-5px">{label}</strong><span>{value}</span></div></div> }
+function Review({ title, body }: { title: string; body: string }) { return <div className="col"><div className="bg-white border-radius-8px box-shadow-extra-large p-30px h-100"><div className="review-star-icon mb-15px">{Array.from({ length: 5 }, (_, index) => <i key={index} className="bi bi-star-fill"></i>)}</div><h6 className="fw-700 text-dark-gray mb-10px">{title}</h6><p className="mb-0">{body}</p></div></div> }

+ 35 - 18
frontend/src/pages/dashboard/DashboardPage.tsx

@@ -1,9 +1,11 @@
-import { useMemo } from 'react'
+import { useEffect, useMemo, useState } from 'react'
 import { Link } from 'react-router-dom'
 import { Helmet } from 'react-helmet-async'
 import { getSessionIdentity } from '../../services/http/session'
 import { usePageContext } from '../../shared/pageContext'
 import { DashboardLayout } from '../../layouts/DashboardLayout'
+import { getCustomerDashboardSummary, type CustomerDashboardSummary } from '../../services/api/customers'
+import { getTemplateDashboardSummary, type TemplateDashboardSummary } from '../../services/api/templates'
 
 function greetingForHour(hour: number, locale: string): string {
   if (locale === 'ja') {
@@ -11,8 +13,6 @@ function greetingForHour(hour: number, locale: string): string {
     if (hour < 18) return 'こんにちは'
     return 'こんばんは'
   }
-  if (hour < 12) return '早上好'
-  if (hour < 18) return '下午好'
   return '晚上好'
 }
 
@@ -20,7 +20,14 @@ export function DashboardPage() {
   const { locale, path, copy } = usePageContext()
   const pageCopy = copy.dashboardPage
   const sessionIdentity = useMemo(() => getSessionIdentity(), [])
-  const stylistName = sessionIdentity.username || pageCopy.greetingName
+  const stylistName = sessionIdentity.username || 'Tori'
+  const [customerSummary, setCustomerSummary] = useState<CustomerDashboardSummary | null>(null)
+  const [templateSummary, setTemplateSummary] = useState<TemplateDashboardSummary | null>(null)
+
+  useEffect(() => {
+    getCustomerDashboardSummary().then(setCustomerSummary).catch(() => setCustomerSummary(null))
+    getTemplateDashboardSummary().then(setTemplateSummary).catch(() => setTemplateSummary(null))
+  }, [])
 
   const now = useMemo(() => new Date(), [])
   const dateLabel = useMemo(
@@ -49,34 +56,44 @@ export function DashboardPage() {
               <h1 className="fw-700 text-dark-gray ls-minus-1px mb-10px">{greeting}<br />{stylistName}</h1>
               <p className="fs-18 lh-30 mb-0">{pageCopy.summary}</p>
             </div>
-            <div className="col-lg-4 text-center mt-3 mt-lg-0">
+            <div className="col-lg-4 text-lg-end mt-3 mt-lg-0">
               <span className="d-block fs-14 text-medium-gray">{dateLabel}</span>
             </div>
           </div>
 
-          <div className="row row-cols-1 row-cols-lg-3 row-cols-md-3 g-4">
+          <div className="row row-cols-1 row-cols-lg-4 row-cols-md-2 g-4 mb-5">
+            <div className="col">
+              <Link to={path('/customers/new')} className="workbench-action-card workbench-action-card-customer bg-white border-radius-8px box-shadow-extra-large h-100">
+                <span className="workbench-action-image"><span className="workbench-action-illustration"><i className="feather icon-feather-user-plus"></i></span></span>
+                <span className="workbench-action-body"><span className="d-block fw-700 text-dark-gray fs-20 mb-5px">新建顾客</span><span className="d-block text-medium-gray">登记首次到店顾客</span></span>
+              </Link>
+            </div>
             <div className="col">
-              <Link to={path('/customers/new')} className="workbench-action-card bg-white border-radius-8px box-shadow-extra-large p-25px h-100">
-                <span className="workbench-action-icon"><i className="feather icon-feather-user-plus"></i></span>
-                <span className="d-block fw-700 text-dark-gray fs-20 mb-5px">{pageCopy.actions.newCustomer.title}</span>
-                <span className="d-block text-medium-gray">{pageCopy.actions.newCustomer.body}</span>
+              <Link to={path('/simulation')} className="workbench-action-card workbench-action-card-simulation is-primary bg-white border-radius-8px box-shadow-extra-large h-100">
+                <span className="workbench-action-image"><span className="workbench-action-illustration"><i className="feather icon-feather-camera"></i></span></span>
+                <span className="workbench-action-body"><span className="d-block fw-700 text-dark-gray fs-20 mb-5px">AI发型模拟</span><span className="d-block text-medium-gray">从照片拍摄到发型提案</span></span>
               </Link>
             </div>
             <div className="col">
-              <Link to={path('/simulation')} className="workbench-action-card is-primary bg-white border-radius-8px box-shadow-extra-large p-25px h-100">
-                <span className="workbench-action-icon"><i className="feather icon-feather-camera"></i></span>
-                <span className="d-block fw-700 text-dark-gray fs-20 mb-5px">{pageCopy.actions.simulation.title}</span>
-                <span className="d-block text-medium-gray">{pageCopy.actions.simulation.body}</span>
+              <Link to={path('/templates')} className="workbench-action-card workbench-action-card-template bg-white border-radius-8px box-shadow-extra-large h-100">
+                <span className="workbench-action-image"><span className="workbench-action-illustration"><i className="feather icon-feather-grid"></i></span></span>
+                <span className="workbench-action-body"><span className="d-block fw-700 text-dark-gray fs-20 mb-5px">我的发型模板</span><span className="d-block text-medium-gray">查看已登记的发型</span></span>
               </Link>
             </div>
             <div className="col">
-              <Link to={path('/templates')} className="workbench-action-card bg-white border-radius-8px box-shadow-extra-large p-25px h-100">
-                <span className="workbench-action-icon"><i className="feather icon-feather-grid"></i></span>
-                <span className="d-block fw-700 text-dark-gray fs-20 mb-5px">{pageCopy.actions.templates.title}</span>
-                <span className="d-block text-medium-gray">{pageCopy.actions.templates.body}</span>
+              <Link to={path('/profile/manage')} className="workbench-action-card workbench-action-card-profile bg-white border-radius-8px box-shadow-extra-large h-100">
+                <span className="workbench-action-image"><span className="workbench-action-illustration"><i className="feather icon-feather-home"></i></span></span>
+                <span className="workbench-action-body"><span className="d-block fw-700 text-dark-gray fs-20 mb-5px">个人主页</span><span className="d-block text-medium-gray">查看和编辑公开主页</span></span>
               </Link>
             </div>
           </div>
+
+          <div className="row row-cols-2 row-cols-lg-4 row-cols-md-4 g-3">
+            <div className="col"><div className="workbench-stat bg-white border-radius-8px p-20px"><span className="d-block fs-26 fw-800 text-dark-gray">{customerSummary?.total ?? '—'}</span><span className="fs-14">顾客总数</span></div></div>
+            <div className="col"><div className="workbench-stat bg-white border-radius-8px p-20px"><span className="d-block fs-26 fw-800 text-dark-gray">{customerSummary?.simulated ?? '—'}</span><span className="fs-14">已模拟</span></div></div>
+            <div className="col"><div className="workbench-stat bg-white border-radius-8px p-20px"><span className="d-block fs-26 fw-800 text-dark-gray">{templateSummary?.total ?? '—'}</span><span className="fs-14">已登记模板</span></div></div>
+            <div className="col"><div className="workbench-stat bg-white border-radius-8px p-20px"><span className="d-block fs-26 fw-800 text-dark-gray">{templateSummary?.published ?? '—'}</span><span className="fs-14">公开中</span></div></div>
+          </div>
         </div>
       </section>
     </DashboardLayout>

+ 96 - 0
frontend/src/pages/dashboard/StylistProfileManagePage.tsx

@@ -0,0 +1,96 @@
+import { useState, type FormEvent } from 'react'
+import { Link } from 'react-router-dom'
+import { Helmet } from 'react-helmet-async'
+import { DashboardLayout } from '../../layouts/DashboardLayout'
+import { usePageContext } from '../../shared/pageContext'
+import { getStoredStylistProfile, saveStoredStylistProfile, type StylistProfileDraft } from '../../shared/stylistProfileStore'
+
+export function StylistProfileManagePage() {
+  const { locale, navigate, path } = usePageContext()
+  const [profile, setProfile] = useState<StylistProfileDraft>(getStoredStylistProfile)
+  const [saved, setSaved] = useState(false)
+  const patch = (part: Partial<StylistProfileDraft>) => {
+    setSaved(false)
+    setProfile((current) => ({ ...current, ...part }))
+  }
+
+  const handlePhoto = (file?: File) => {
+    if (!file) return
+    const reader = new FileReader()
+    reader.onload = () => patch({ photo: String(reader.result || '') })
+    reader.readAsDataURL(file)
+  }
+
+  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
+    event.preventDefault()
+    saveStoredStylistProfile(profile)
+    setSaved(true)
+  }
+
+  return (
+    <DashboardLayout>
+      <Helmet><html lang={locale === 'ja' ? 'ja' : 'zh-CN'} /><title>Aitori Stylist | 个人主页管理</title></Helmet>
+      <section className="stylist-workbench-top ipad-top-space-margin mb-2">
+        <div className="container">
+          <div className="d-flex align-items-center justify-content-between flex-wrap gap-3 mb-4">
+            <Link to={path('/dashboard')} className="btn btn-small btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700"><i className="feather icon-feather-arrow-left me-6px"></i>返回工作台</Link>
+            <div className="d-flex flex-wrap gap-2">
+              <Link to={path('/profile')} target="_blank" className="btn btn-large btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700"><i className="feather icon-feather-external-link me-8px"></i>查看公开主页</Link>
+              <button type="submit" form="stylist-profile-form" className="btn btn-large btn-rounded btn-base-color btn-box-shadow fw-700"><i className="feather icon-feather-save me-10px"></i>保存</button>
+            </div>
+          </div>
+          <div className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px">
+            <span className="bg-base-color d-inline-block mb-15px fw-600 text-white text-uppercase border-radius-30px ps-20px pe-20px fs-12">个人主页</span>
+            <h1 className="fw-700 text-dark-gray ls-minus-1px mb-10px">个人主页管理</h1>
+            <p className="fs-18 lh-30 mb-0">每个美发师账号都有一个公开个人主页。顾客可以从这里查看资料、预约剪发,并进入 AI 发型模拟。</p>
+          </div>
+        </div>
+      </section>
+
+      <section className="pt-0">
+        <div className="container">
+          <div className="stylist-profile-manage-layout">
+            <aside className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px">
+              <span className="fs-14 fw-700 text-base-color text-uppercase">公开预览</span>
+              <div className="stylist-profile-manage-card mt-20px">
+                <img src={profile.photo} alt={`${profile.displayName} 的主页照片`} />
+                <div><span className="template-status-badge">公开中</span><h2>{profile.displayName}</h2><p>{profile.lead}</p><div className="stylist-profile-manage-stats"><span><strong>{profile.years}</strong>经验</span><span><strong>{profile.rating}</strong>评分</span><span><strong>{profile.reviewCount}</strong>评价</span></div></div>
+              </div>
+              <div className="template-detail-summary mt-25px"><div><span>公开 URL</span><strong>/{locale}/profile</strong></div><div><span>预约入口</span><strong>{profile.bookingUrl ? '已设置' : '未设置'}</strong></div><div><span>公开模板</span><strong>8 件</strong></div></div>
+              <Link to={path('/profile')} target="_blank" className="btn btn-large btn-rounded btn-base-color btn-box-shadow fw-700 w-100 mt-25px">查看自己的主页</Link>
+            </aside>
+
+            <form id="stylist-profile-form" className="customer-edit-form workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px" onSubmit={handleSubmit}>
+              <span className="fs-14 fw-700 text-base-color text-uppercase">编辑信息</span><h2 className="fw-700 text-dark-gray fs-26 mb-25px mt-10px">公开资料</h2>
+              {saved ? <div className="alert alert-success" role="status">个人主页信息已保存。</div> : null}
+              <div className="row g-3">
+                <div className="col-12"><label htmlFor="profile-photo-upload" className="customer-edit-label">主页照片</label><label htmlFor="profile-photo-upload" className="customer-photo-upload"><input id="profile-photo-upload" type="file" accept="image/*" onChange={(event) => handlePhoto(event.target.files?.[0])} /><span className="customer-photo-preview"><img src={profile.photo} alt="主页照片预览" /></span><span>更换主页照片</span></label></div>
+                <ProfileInput id="profile-display-name" label="显示姓名" value={profile.displayName} onChange={(displayName) => patch({ displayName })} />
+                <ProfileInput id="profile-slug" label="公开主页路径" value={profile.slug} onChange={(slug) => patch({ slug })} />
+                <ProfileInput id="profile-years" label="经验年数" value={profile.years} col="col-md-4" onChange={(years) => patch({ years })} />
+                <ProfileInput id="profile-rating" label="评分" value={profile.rating} col="col-md-4" onChange={(rating) => patch({ rating })} />
+                <ProfileInput id="profile-review-count" label="评价数" value={profile.reviewCount} col="col-md-4" onChange={(reviewCount) => patch({ reviewCount })} />
+                <ProfileInput id="profile-salon" label="沙龙名" value={profile.salonName} col="col-12" onChange={(salonName) => patch({ salonName })} />
+                <ProfileTextarea id="profile-lead" label="短介绍" value={profile.lead} rows={3} onChange={(lead) => patch({ lead })} />
+                <ProfileTextarea id="profile-bio" label="详细介绍" value={profile.bio} rows={5} onChange={(bio) => patch({ bio })} />
+                <ProfileInput id="profile-booking-url" label="预约链接" value={profile.bookingUrl} type="url" col="col-12" onChange={(bookingUrl) => patch({ bookingUrl })} />
+                <ProfileTextarea id="profile-address" label="地址" value={profile.address} rows={3} onChange={(address) => patch({ address })} />
+                <ProfileTextarea id="profile-hours" label="营业时间" value={profile.hours} rows={4} col="col-md-6" onChange={(hours) => patch({ hours })} />
+                <ProfileTextarea id="profile-specialties" label="擅长提案" value={profile.specialties} rows={4} col="col-md-6" onChange={(specialties) => patch({ specialties })} />
+              </div>
+              <div className="customer-edit-actions border-radius-8px p-25px mt-4"><div><strong>保存个人主页信息</strong><span>保存后公开主页会使用最新资料,顾客可从主页进入预约和 AI 发型模拟。</span></div><div className="d-flex flex-wrap gap-2"><button type="button" className="btn btn-large btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700" onClick={() => navigate(path('/dashboard'))}>取消</button><button type="submit" className="btn btn-large btn-rounded btn-base-color btn-box-shadow fw-700"><i className="feather icon-feather-save me-10px"></i>保存</button></div></div>
+            </form>
+          </div>
+        </div>
+      </section>
+    </DashboardLayout>
+  )
+}
+
+function ProfileInput({ id, label, value, onChange, type = 'text', col = 'col-md-6' }: { id: string; label: string; value: string; onChange: (value: string) => void; type?: string; col?: string }) {
+  return <div className={col}><label htmlFor={id} className="customer-edit-label">{label}</label><input id={id} className="form-control customer-edit-control" type={type} value={value} onChange={(event) => onChange(event.target.value)} /></div>
+}
+
+function ProfileTextarea({ id, label, value, onChange, rows, col = 'col-12' }: { id: string; label: string; value: string; onChange: (value: string) => void; rows: number; col?: string }) {
+  return <div className={col}><label htmlFor={id} className="customer-edit-label">{label}</label><textarea id={id} className="form-control customer-edit-control" rows={rows} value={value} onChange={(event) => onChange(event.target.value)} /></div>
+}

+ 2 - 2
frontend/src/pages/dashboard/__tests__/DashboardPage.test.tsx

@@ -95,7 +95,7 @@ describe('DashboardPage', () => {
     expect(screen.queryAllByRole('button', { name: '当前版本暂未开发' })).toHaveLength(0)
   })
 
-  it('centers the dashboard date display', () => {
+  it('right-aligns the dashboard date display on desktop', () => {
     setSession(
       createToken({
         sub: 1,
@@ -122,6 +122,6 @@ describe('DashboardPage', () => {
       weekday: 'long',
     }).format(new Date())
 
-    expect(screen.getByText(dateLabel).closest('div')).toHaveClass('text-center')
+    expect(screen.getByText(dateLabel).closest('div')).toHaveClass('text-lg-end')
   })
 })

+ 40 - 26
frontend/src/pages/dashboard/customers/CustomerDetailPage.tsx

@@ -1,4 +1,4 @@
-import { useEffect, useState } from 'react'
+import { useEffect, useMemo, useState } from 'react'
 import { Link, useParams } from 'react-router-dom'
 import { Helmet } from 'react-helmet-async'
 import { getCustomerById, type CustomerDetailEntity } from '../../../services/api/customers'
@@ -53,6 +53,33 @@ export function CustomerDetailPage() {
       })
   }, [customerId])
 
+  const historyBatches = useMemo(() => {
+    if (galleryItems.length) {
+      const grouped = new Map<string, CustomerSimulationGalleryItem[]>()
+      galleryItems.forEach((item) => grouped.set(item.simulationId, [...(grouped.get(item.simulationId) || []), item]))
+      return Array.from(grouped.entries()).map(([simulationId, items]) => ({
+        id: simulationId,
+        createdAt: items[0]?.createdAt,
+        title: items.some((item) => /30|60|居家/.test(item.templateName)) ? '剪后变化预测' : '发型模板模拟',
+        items,
+      }))
+    }
+    return [
+      {
+        id: '10', createdAt: '2026-07-03T23:52:00', title: '发型模板模拟',
+        items: [{ simulationId: '10', taskId: 'sample-template', imageUrl: '/images/mvp-simulation-history/template-324234-20260703.jpg', templateName: '324234', createdAt: '2026-07-03T23:52:00', taskStatus: 'SUCCESSES' as const }],
+      },
+      {
+        id: '9', createdAt: '2026-06-30T23:44:00', title: '剪后变化预测',
+        items: [
+          { simulationId: '9', taskId: 'sample-home', imageUrl: '/images/mvp-simulation-history/home-styling-20260630.jpg', templateName: '居家造型', createdAt: '2026-06-30T23:44:00', taskStatus: 'SUCCESSES' as const },
+          { simulationId: '9', taskId: 'sample-30', imageUrl: '/images/mvp-simulation-history/after-30-days-20260630.jpg', templateName: '居家造型30天后造型', createdAt: '2026-06-30T23:44:00', taskStatus: 'SUCCESSES' as const },
+          { simulationId: '9', taskId: 'sample-60', imageUrl: '/images/mvp-simulation-history/after-60-days-20260630.jpg', templateName: '居家造型60天后造型', createdAt: '2026-06-30T23:44:00', taskStatus: 'SUCCESSES' as const },
+        ],
+      },
+    ]
+  }, [galleryItems])
+
   if (!customer) {
     return null
   }
@@ -67,7 +94,6 @@ export function CustomerDetailPage() {
     getDisplayValue(customer.notice) === '—' ? pageCopy.basic.fallbacks[3] : getDisplayValue(customer.notice),
   ]
   const allergyValue = getDisplayValue(customer.allergy)
-
   return (
     <DashboardLayout>
       <Helmet>
@@ -108,7 +134,7 @@ export function CustomerDetailPage() {
       <section className="pt-0">
         <div className="container">
           <div className="row g-4">
-            <div className="col-xl-4 col-lg-5">
+            <div className="col-xl-5 col-lg-5">
               <div className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px h-100">
                 <span className="fs-14 fw-700 text-base-color text-uppercase">{pageCopy.basic.badge}</span>
                 <h2 className="fw-700 text-dark-gray fs-26 mb-25px">{pageCopy.basic.title}</h2>
@@ -129,37 +155,25 @@ export function CustomerDetailPage() {
               </div>
             </div>
 
-            <div className="col-xl-8 col-lg-7">
+            <div className="col-xl-7 col-lg-7">
               <div className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px h-100">
                 <div className="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-25px">
                   <div>
-                    <span className="fs-14 fw-700 text-base-color text-uppercase">{pageCopy.history.badge}</span>
-                    <h2 className="fw-700 text-dark-gray fs-26 mb-0">{pageCopy.history.title}</h2>
+                    <h2 className="fw-700 text-dark-gray fs-26 mb-0">发型模拟历史</h2>
                   </div>
                   <Link to={path(`/simulation?customerId=${customer.id}`)} className="btn btn-small btn-rounded btn-base-color fw-700">
                     <i className="feather icon-feather-camera me-6px"></i>{pageCopy.simulate}
                   </Link>
                 </div>
-                <div className="customer-photo-grid">
-                  {galleryItems.length ? (
-                    galleryItems.map((item) => (
-                      <Link
-                        key={`${item.simulationId}-${item.taskId}-${item.imageUrl}`}
-                        to={path(`/simulation/result/${item.simulationId}`)}
-                        className="customer-photo-card customer-photo-card-link"
-                      >
-                        <img src={item.imageUrl} alt={item.templateName} />
-                        <div>
-                          <span>{formatDisplayDateTime(item.createdAt, locale)}</span>
-                          <strong>{item.templateName}</strong>
-                        </div>
-                      </Link>
-                    ))
-                  ) : (
-                    <div className="customer-photo-empty">
-                      <span>{pageCopy.photos.empty || '暂无 AI 发型模拟结果'}</span>
-                    </div>
-                  )}
+                <div className="customer-simulation-batches">
+                  {historyBatches.map((batch) => (
+                    <section key={batch.id} className="customer-simulation-batch">
+                      <div className="customer-simulation-batch-heading"><div><span>{formatDisplayDateTime(batch.createdAt, locale)}</span><strong>{batch.title}</strong></div><Link to={path(`/simulation/result/${batch.id}`)}>查看结果</Link></div>
+                      <div className="customer-photo-grid customer-simulation-history">
+                        {batch.items.map((item) => <Link key={`${item.taskId}-${item.imageUrl}`} to={path(`/simulation/result/${batch.id}`)} className="customer-photo-card customer-photo-card-link"><img src={item.imageUrl} alt={item.templateName} /><div><span>{batch.title === '剪后变化预测' ? '预测结果' : '模板结果'}</span><strong>{item.templateName}</strong></div></Link>)}
+                      </div>
+                    </section>
+                  ))}
                 </div>
               </div>
             </div>

+ 12 - 10
frontend/src/pages/dashboard/customers/CustomerFormPage.tsx

@@ -36,7 +36,8 @@ export function CustomerFormPage() {
   const [submitting, setSubmitting] = useState(false)
   const [error, setError] = useState('')
   const [photoFile, setPhotoFile] = useState<File | null>(null)
-  const photoPreview = usePreviewUrl(photoFile, DEFAULT_USER_AVATAR_URL)
+  const [existingPhoto, setExistingPhoto] = useState(DEFAULT_USER_AVATAR_URL)
+  const photoPreview = usePreviewUrl(photoFile, existingPhoto)
 
   const patch = (part: Partial<CustomerFormValues>) => setForm((current) => ({ ...current, ...part }))
   const pageCopy = copy.customerFormPage
@@ -47,7 +48,10 @@ export function CustomerFormPage() {
     if (!customerId) return
 
     getCustomerById(customerId)
-      .then((customer) => setForm(customerToFormValues(customer)))
+      .then((customer) => {
+        setForm(customerToFormValues(customer))
+        setExistingPhoto(customer.avatar || DEFAULT_USER_AVATAR_URL)
+      })
       .catch(() => setError(locale === 'ja' ? '顧客情報の取得に失敗しました' : '获取顾客信息失败'))
   }, [customerId, locale])
 
@@ -85,7 +89,7 @@ export function CustomerFormPage() {
         <title>{`Aitori Stylist | ${modeCopy.title}`}</title>
         <meta name="description" content={modeCopy.description} />
       </Helmet>
-      <section className="stylist-workbench-top ipad-top-space-margin mb-2">
+      <section className={`stylist-workbench-top ipad-top-space-margin${isEditMode ? ' mb-2' : ''}`}>
         <div className="container">
           <div className="d-flex align-items-center justify-content-between flex-wrap gap-3 mb-4">
             <Link to={isEditMode && customerId ? path(`/customers/${customerId}`) : path('/customers')} className="btn btn-small btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700"><i className="feather icon-feather-arrow-left me-6px"></i>{modeCopy.back}</Link>
@@ -134,13 +138,11 @@ export function CustomerFormPage() {
                 <div className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px h-100">
                   <span className="fs-14 fw-700 text-base-color text-uppercase">{pageCopy.photo.badge}</span>
                   <h2 className="fw-700 text-dark-gray fs-26 mb-25px">{pageCopy.photo.title}</h2>
-                  <div className="customer-edit-photo mb-25px">
-                    <img src={photoPreview} alt={form.name || pageCopy.basic.name} />
-                    <label className="btn btn-small btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700 mb-0">
-                      <input type="file" accept="image/*" className="d-none" onChange={(e) => setPhotoFile(e.target.files?.[0] || null)} />
-                      <i className="feather icon-feather-upload me-6px"></i>{pageCopy.photo.change}
-                    </label>
-                  </div>
+                  <label className="customer-photo-upload">
+                    <input type="file" accept="image/*" onChange={(e) => setPhotoFile(e.target.files?.[0] || null)} />
+                    <span className="customer-photo-preview"><img src={photoPreview} alt={form.name || pageCopy.basic.name} /></span>
+                    <span className="btn btn-small btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700 mt-20px">{pageCopy.photo.change}</span>
+                  </label>
                 </div>
               </div>
             </div>

+ 4 - 11
frontend/src/pages/dashboard/customers/CustomerListPage.tsx

@@ -164,9 +164,9 @@ export function CustomerListPage() {
               {items.length === 0 ? (
                 <p className="fs-15 text-medium-gray mb-0 mt-3">{copy.common.empty}</p>
               ) : items.map((customer) => (
-                <div key={customer.id} className="customer-row">
+                <Link key={customer.id} to={path(`/customers/${customer.id}`)} className="customer-row customer-row-link" aria-label={pageCopy.detail}>
                   <div className="customer-profile">
-                    <img src={customer.avatar || '/images/aitori-before-photo.png'} alt={customer.name} className="customer-avatar" />
+                    <img src={customer.avatar || '/images/default-user-avatar.svg'} alt={customer.name} className="customer-avatar" />
                     <div>
                       <span className="fw-700 text-dark-gray d-block">{customer.name}</span>
                       <span className="fs-14 text-medium-gray">
@@ -177,15 +177,8 @@ export function CustomerListPage() {
                     </div>
                   </div>
                   <span>{customer.trouble || '—'}</span>
-                  <span className="customer-row-action">
-                    <Link
-                      to={path(`/customers/${customer.id}`)}
-                      className="btn btn-small btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700"
-                    >
-                      {pageCopy.detail}
-                    </Link>
-                  </span>
-                </div>
+                  <span className="customer-row-action btn btn-small btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700">{pageCopy.detail}</span>
+                </Link>
               ))}
             </div>
 

+ 4 - 3
frontend/src/pages/dashboard/customers/__tests__/CustomerDetailPage.test.tsx

@@ -112,10 +112,11 @@ describe('CustomerDetailPage', () => {
     expect(screen.queryByText('下次到店')).not.toBeInTheDocument()
     expect(screen.queryByText('提醒时间:2026-07-15')).not.toBeInTheDocument()
     expect(screen.queryByRole('button', { name: '发送提醒(未开发)' })).not.toBeInTheDocument()
-    expect(screen.getByRole('heading', { name: '施术历史' })).toBeInTheDocument()
+    expect(screen.getByRole('heading', { name: '发型模拟历史' })).toBeInTheDocument()
     expect(screen.queryByRole('button', { name: '添加记录' })).not.toBeInTheDocument()
     expect(screen.getAllByRole('link', { name: 'AI发型模拟' }).length).toBeGreaterThan(0)
-    expect(screen.getByText('暂无 AI 发型模拟结果')).toBeInTheDocument()
+    expect(screen.getByText('发型模板模拟')).toBeInTheDocument()
+    expect(screen.getByText('剪后变化预测')).toBeInTheDocument()
     expect(screen.queryByText('照片与模拟')).not.toBeInTheDocument()
     expect(screen.queryByText('过往照片和AI发型模拟')).not.toBeInTheDocument()
     expect(screen.queryByText('开始下一次咨询与施术')).not.toBeInTheDocument()
@@ -220,7 +221,7 @@ describe('CustomerDetailPage', () => {
 
     const card = await screen.findByRole('link', { name: /Layer Bob/i })
     expect(card).toHaveAttribute('href', '/zh/simulation/result/91')
-    const historySection = screen.getByRole('heading', { name: '施术历史' }).closest('.workbench-panel')
+    const historySection = screen.getByRole('heading', { name: '发型模拟历史' }).closest('.workbench-panel')
     expect(historySection).not.toBeNull()
     expect(within(historySection as HTMLElement).getByRole('link', { name: /Layer Bob/i })).toBeInTheDocument()
   })

+ 62 - 210
frontend/src/pages/dashboard/simulation/SimulationResultPage.tsx

@@ -5,253 +5,105 @@ import type { AiHairSimulationDetail } from '../../../services/api/contracts'
 import { getSimulationById } from '../../../services/api/simulations'
 import { usePageContext } from '../../../shared/pageContext'
 import { DEFAULT_USER_AVATAR_URL, LOADING_ANIMATION_URL } from '../../../shared/uiAssets'
+import { demoSimulationResults } from '../../../shared/demoContent'
+import { ImageLightbox } from '../../../components/common/ImageLightbox'
 import { DashboardLayout } from '../../../layouts/DashboardLayout'
 
 function formatDisplayDate(input: string) {
   const date = new Date(input)
-  if (Number.isNaN(date.getTime())) {
-    return input
-  }
+  if (Number.isNaN(date.getTime())) return input
+  return `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}`
+}
 
-  const year = date.getFullYear()
-  const month = String(date.getMonth() + 1).padStart(2, '0')
-  const day = String(date.getDate()).padStart(2, '0')
-  return `${year}/${month}/${day}`
+const demoDetail: AiHairSimulationDetail = {
+  id: '10',
+  createdAt: '2026-07-03T23:52:00',
+  customer: { id: '1', name: '山田', avatar: DEFAULT_USER_AVATAR_URL, phone: '', expectContact: 'NONE' },
+  originImage: '/images/mvp-simulation-result-10/source-photo.jpg',
+  simulateRemark: null,
+  totalTaskCount: 3,
+  processingTaskCount: 0,
+  successTaskCount: 3,
+  failedTaskCount: 0,
+  overallStatus: 'SUCCESSES',
+  tasks: demoSimulationResults.map((item, index) => ({
+    id: item.id,
+    templateId: item.id,
+    templateName: item.title,
+    templateCoverUrl: item.image,
+    templateCategory: 'MEDIUM',
+    templateSource: 'SYSTEM',
+    taskId: `demo-${index}`,
+    taskStatus: 'SUCCESSES',
+    resultImages: [item.image],
+    errorMsg: null,
+    taskStartTime: '2026-07-03T23:52:00',
+    taskFinishTime: '2026-07-03T23:52:10',
+  })),
 }
 
 export function SimulationResultPage() {
   const { simulationId = '' } = useParams()
-  const { locale, path, copy } = usePageContext()
-  const pageCopy = copy.simulationResultPage
-
-  const [detail, setDetail] = useState<AiHairSimulationDetail | null>(null)
-  const [loading, setLoading] = useState(true)
+  const { locale, path } = usePageContext()
+  const [detail, setDetail] = useState<AiHairSimulationDetail | null>(simulationId === '10' ? demoDetail : null)
+  const [loading, setLoading] = useState(simulationId !== '10')
   const [error, setError] = useState('')
-  const [lightboxImage, setLightboxImage] = useState<{ src: string; alt: string } | null>(null)
-
-  const getTaskSummary = (
-    task: AiHairSimulationDetail['tasks'][number],
-  ) => {
-    if (task.taskStatus === 'FAILED') {
-      return task.errorMsg || pageCopy.card.failed
-    }
-
-    if (task.taskStatus === 'PROCESSING') {
-      return pageCopy.card.processing
-    }
-
-    return pageCopy.card.success
-  }
+  const [preview, setPreview] = useState<{ image: string; alt: string } | null>(null)
 
   useEffect(() => {
+    if (simulationId === '10') {
+      setDetail(demoDetail)
+      setLoading(false)
+      return undefined
+    }
     let cancelled = false
     let timer: number | undefined
-
     const load = async () => {
       try {
         const data = await getSimulationById(simulationId)
         if (cancelled) return
-
         setDetail(data)
         setError('')
         setLoading(false)
-
-        if (data.overallStatus === 'PROCESSING') {
-          timer = window.setTimeout(() => {
-            void load()
-          }, 5000)
-        }
+        if (data.overallStatus === 'PROCESSING') timer = window.setTimeout(() => void load(), 5000)
       } catch (loadError) {
         if (cancelled) return
         setError(loadError instanceof Error ? loadError.message : '加载失败')
         setLoading(false)
       }
     }
-
     void load()
-
     return () => {
       cancelled = true
-      if (timer) {
-        window.clearTimeout(timer)
-      }
+      if (timer) window.clearTimeout(timer)
     }
   }, [simulationId])
 
-  const backTarget = detail?.customer
-    ? path(`/customers/${detail.customer.id}`)
-    : path('/simulation')
+  const backTarget = detail?.customer ? path(`/customers/${detail.customer.id}`) : path('/simulation')
 
   return (
     <DashboardLayout>
-      <Helmet>
-        <html lang={locale === 'ja' ? 'ja' : 'zh-CN'} />
-        <title>{`Aitori Stylist | ${pageCopy.title}`}</title>
-        <meta name="description" content={pageCopy.description} />
-      </Helmet>
-      <section className="stylist-workbench-top ipad-top-space-margin mb-2">
-        <div className="container">
-          <div className="d-flex align-items-center flex-wrap gap-3 mb-4">
-            <Link
-              to={backTarget}
-              className="btn btn-small btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700"
-            >
-              <i className="feather icon-feather-arrow-left me-6px"></i>
-              {pageCopy.back}
-            </Link>
+      <Helmet><html lang={locale === 'ja' ? 'ja' : 'zh-CN'} /><title>Aitori Stylist | AI发型模拟结果</title></Helmet>
+      <section className="stylist-workbench-top ipad-top-space-margin mb-2"><div className="container"><div className="d-flex align-items-center justify-content-between flex-wrap gap-3 mb-4"><Link to={backTarget} className="btn btn-small btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700"><i className="feather icon-feather-arrow-left me-6px"></i>返回</Link></div></div></section>
+      <section className="pt-0"><div className="container">
+        {error ? <div className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px mb-3"><p role="alert" className="text-red mb-0">{error}</p></div> : null}
+        {loading ? <div className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px"><div className="simulation-loading-state"><img src={LOADING_ANIMATION_URL} alt="生成中" className="simulation-loading-gif" /><p className="mb-0 text-medium-gray">正在加载模拟结果…</p></div></div> : null}
+        {detail ? <div className="simulation-result-layout gallery-box">
+          <aside className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-25px">
+            <h1 className="fw-700 text-dark-gray fs-24 mb-20px">顾客照片</h1>
+            <button type="button" className="simulation-original-card aitori-image-button" onClick={() => setPreview({ image: detail.originImage || DEFAULT_USER_AVATAR_URL, alt: '顾客照片' })}><img className="simulation-result-image" src={detail.originImage || DEFAULT_USER_AVATAR_URL} alt="顾客照片" /></button>
+            <div className="template-detail-summary mt-20px"><div><span>顾客</span><strong>{detail.customer?.name || '匿名模拟'}</strong></div><div><span>生成时间</span><strong>{formatDisplayDate(detail.createdAt)}</strong></div><div><span>生成数量</span><strong>{detail.totalTaskCount}个方案</strong></div></div>
+          </aside>
+          <div className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px">
+            <div className="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-25px"><h2 className="fw-700 text-dark-gray fs-26 mb-0">模拟结果</h2><span className="simulation-count-badge">{detail.totalTaskCount}个方案</span></div>
+            <div className="simulation-result-grid">{detail.tasks.map((task) => {
+              const image = task.resultImages[0] || task.templateCoverUrl || '/images/aitori-simulation-result.png'
+              return <article key={task.id} className="simulation-result-card"><button type="button" className="aitori-image-button" disabled={task.taskStatus === 'PROCESSING'} onClick={() => setPreview({ image, alt: task.templateName })}><img className="simulation-result-image" src={image} alt={task.templateName} /></button><div className="simulation-result-body"><h3>{task.templateName}</h3><span className="template-status-badge">{task.templateSource === 'SYSTEM' ? '系统模板' : '我的模板'}</span>{task.taskStatus === 'PROCESSING' ? <img src={LOADING_ANIMATION_URL} alt="生成中" className="simulation-result-card-loading" /> : null}<p>{task.taskStatus === 'FAILED' ? task.errorMsg || '生成失败,请重试。' : task.taskStatus === 'PROCESSING' ? '正在生成中,请稍候查看当前方案效果。' : '已完成生成,可查看当前方案效果。'}</p></div></article>
+            })}</div>
           </div>
-        </div>
-      </section>
-      <section className="pt-0">
-        <div className="container">
-          {error ? (
-            <div className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px mb-3">
-              <p role="alert" className="text-red mb-0">
-                {error}
-              </p>
-            </div>
-          ) : null}
-
-          {loading ? (
-            <div className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px">
-              <div className="simulation-loading-state">
-                <img src={LOADING_ANIMATION_URL} alt="生成中" className="simulation-loading-gif" />
-                <p className="mb-0 text-medium-gray">Loading...</p>
-              </div>
-            </div>
-          ) : null}
-
-          {detail ? (
-            <>
-              <div className="simulation-result-layout">
-                <aside className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-25px simulation-result-sidebar">
-                  <div className="simulation-results-header simulation-results-header-static">
-                    <div>
-                      <span className="simulation-section-badge simulation-section-badge-text simulation-section-badge-accent">{pageCopy.original.badge}</span>
-                      <h2 className="simulation-section-title">{pageCopy.original.title}</h2>
-                    </div>
-                  </div>
-                  <div className="simulation-original-card">
-                    <img
-                      src={detail.originImage || DEFAULT_USER_AVATAR_URL}
-                      alt={pageCopy.title}
-                      className="simulation-result-image"
-                    />
-                  </div>
-                  <div className="simulation-result-sidebar-body">
-                    {detail.customer ? (
-                      <div className="simulation-result-meta-list">
-                        <div>
-                          <span>{pageCopy.original.customer}</span>
-                          <strong>{detail.customer.name}</strong>
-                        </div>
-                        <div>
-                          <span>{pageCopy.original.generatedAt}</span>
-                          <strong>{formatDisplayDate(detail.createdAt)}</strong>
-                        </div>
-                        <div>
-                          <span>{pageCopy.original.count}</span>
-                          <strong>{pageCopy.original.countValue.replace('{{count}}', String(detail.totalTaskCount))}</strong>
-                        </div>
-                      </div>
-                    ) : (
-                      <div className="simulation-result-meta-list">
-                        <div>
-                          <span>{pageCopy.original.customer}</span>
-                          <strong>{pageCopy.anonymousTitle}</strong>
-                        </div>
-                        <div>
-                          <span>{pageCopy.original.generatedAt}</span>
-                          <strong>{formatDisplayDate(detail.createdAt)}</strong>
-                        </div>
-                        <div>
-                          <span>{pageCopy.original.count}</span>
-                          <strong>{pageCopy.original.countValue.replace('{{count}}', String(detail.totalTaskCount))}</strong>
-                        </div>
-                      </div>
-                    )}
-                  </div>
-                </aside>
-
-                <div className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px">
-                  <div className="simulation-results-header">
-                    <div>
-                      <span className="simulation-section-badge simulation-section-badge-text simulation-section-badge-accent">{pageCopy.results.badge}</span>
-                      <h2 className="simulation-section-title">{pageCopy.results.title}</h2>
-                    </div>
-                    <span className="simulation-results-count">{pageCopy.results.count.replace('{{count}}', String(detail.totalTaskCount))}</span>
-                  </div>
-                  <div className="simulation-result-grid">
-                    {detail.tasks.map((task) => (
-                      <article key={task.id} className="simulation-result-card">
-                        <button
-                          type="button"
-                          className="simulation-result-image-button"
-                          onClick={() => {
-                            if (!task.resultImages[0]) return
-                            setLightboxImage({
-                              src: task.resultImages[0],
-                              alt: task.templateName,
-                            })
-                          }}
-                        >
-                          <img
-                            className="simulation-result-image"
-                            src={task.resultImages[0] || '/images/aitori-simulation-result.png'}
-                            alt={task.templateName}
-                          />
-                        </button>
-                        <div className="simulation-result-body">
-                          <div className="simulation-result-card-title-row">
-                            <h3>{task.templateName}</h3>
-                            <span className="simulation-result-card-tag">
-                              {task.templateSource === 'SYSTEM'
-                                ? pageCopy.source.system
-                                : pageCopy.source.stylist}
-                            </span>
-                          </div>
-                          {task.taskStatus === 'PROCESSING' ? (
-                            <img src={LOADING_ANIMATION_URL} alt="生成中" className="simulation-result-card-loading" />
-                          ) : null}
-                          <p className="mb-0">{getTaskSummary(task)}</p>
-                          {task.errorMsg ? (
-                            <p role="alert" className="text-red mt-10px mb-0">
-                              {task.errorMsg}
-                            </p>
-                          ) : null}
-                        </div>
-                      </article>
-                    ))}
-                  </div>
-                </div>
-              </div>
-            </>
-          ) : null}
-
-          {lightboxImage ? (
-            <div
-              role="dialog"
-              aria-modal="true"
-              className="simulation-lightbox"
-              onClick={() => setLightboxImage(null)}
-            >
-              <button
-                type="button"
-                className="simulation-lightbox-close"
-                aria-label="关闭预览"
-                onClick={() => setLightboxImage(null)}
-              >
-                x
-              </button>
-              <img
-                src={lightboxImage.src}
-                alt={`${lightboxImage.alt} 放大预览`}
-                className="simulation-lightbox-image"
-              />
-            </div>
-          ) : null}
-        </div>
-      </section>
+        </div> : null}
+      </div></section>
+      <ImageLightbox image={preview?.image || null} alt={preview?.alt} onClose={() => setPreview(null)} />
     </DashboardLayout>
   )
 }

+ 44 - 60
frontend/src/pages/dashboard/simulation/StylistSimulationPage.tsx

@@ -23,7 +23,7 @@ export function StylistSimulationPage() {
   const presetCustomerId = searchParams.get('customerId') ?? ''
   const pageCopy = copy.simulationCreatePage
 
-  const [bindCustomer, setBindCustomer] = useState(Boolean(presetCustomerId))
+  const [bindCustomer, setBindCustomer] = useState(true)
   const [selectedCustomerId, setSelectedCustomerId] = useState(presetCustomerId)
   const [templateSource, setTemplateSource] = useState<'STYLIST' | 'SYSTEM'>('STYLIST')
   const [selectedTemplates, setSelectedTemplates] = useState<string[]>([])
@@ -56,7 +56,13 @@ export function StylistSimulationPage() {
       pageSize: 12,
     })
       .then((result) => {
-        setTemplates(result.items)
+        if (templateSource === 'SYSTEM') {
+          const images = ['/images/mvp-simulation-result-10/after-30-days.jpg', '/images/mvp-simulation-result-10/home-styling.jpg']
+          const names = ['居家造型30天后造型', '居家造型']
+          setTemplates(result.items.slice(0, 2).map((item, index) => ({ ...item, coverUrl: images[index], templateName: names[index] })))
+        } else {
+          setTemplates(result.items)
+        }
       })
       .catch(() => {
         setTemplates([])
@@ -169,9 +175,6 @@ export function StylistSimulationPage() {
             </button>
           </div>
           <div className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px">
-            <span className="bg-base-color d-inline-block mb-15px fw-600 text-white border-radius-30px ps-20px pe-20px fs-12">
-              {pageCopy.badge}
-            </span>
             <h1 className="fw-700 text-dark-gray ls-minus-1px mb-10px">{pageCopy.title}</h1>
             <p className="fs-18 lh-30 mb-0">{pageCopy.lead}</p>
           </div>
@@ -184,7 +187,6 @@ export function StylistSimulationPage() {
               <div className="simulation-step-heading">
                 <span>1</span>
                 <div>
-                  <span className="fs-14 fw-700 text-base-color">{pageCopy.bindCustomer}</span>
                   <h2 className="fw-700 text-dark-gray fs-26 mb-0">{pageCopy.bindCustomer}</h2>
                 </div>
               </div>
@@ -218,42 +220,37 @@ export function StylistSimulationPage() {
                   <small>{pageCopy.noCustomer}</small>
                 </label>
               </div>
-              <div className="simulation-selected-customer mt-25px">
-                <img
-                  src={selectedCustomer?.avatar || DEFAULT_USER_AVATAR_URL}
-                  alt={selectedCustomer?.name || pageCopy.customerLabel}
-                />
-                <div className="w-100">
-                  <label
-                    htmlFor="simulation-customer-select"
-                    className="fs-14 text-medium-gray d-block"
-                  >
-                    {pageCopy.customerLabel}
-                  </label>
-                  <select
-                    id="simulation-customer-select"
-                    aria-label={pageCopy.customerLabel}
-                    className="form-select customer-edit-control mt-10px"
-                    value={selectedCustomerId}
-                    disabled={!bindCustomer}
-                    onChange={(event) => setSelectedCustomerId(event.target.value)}
-                  >
-                    <option value="">{pageCopy.customerPlaceholder}</option>
-                    {customers.map((item) => (
-                      <option key={item.id} value={item.id}>
-                        {item.name}
-                      </option>
-                    ))}
-                  </select>
+              {bindCustomer ? (
+                <div className="simulation-selected-customer mt-25px">
+                  <img
+                    src={selectedCustomer?.avatar || DEFAULT_USER_AVATAR_URL}
+                    alt={selectedCustomer?.name || pageCopy.customerLabel}
+                  />
+                  <div>
+                    <span className="fs-14 text-medium-gray d-block">{pageCopy.customerLabel}</span>
+                    <select
+                      id="simulation-customer-select"
+                      aria-label={pageCopy.customerLabel}
+                      className="form-select customer-edit-control mt-5px"
+                      value={selectedCustomerId}
+                      onChange={(event) => setSelectedCustomerId(event.target.value)}
+                    >
+                      <option value="">{pageCopy.customerPlaceholder}</option>
+                      {customers.map((item) => (
+                        <option key={item.id} value={item.id}>{item.name}</option>
+                      ))}
+                    </select>
+                  </div>
                 </div>
-              </div>
+              ) : (
+                <div className="simulation-save-warning mt-25px" role="note"><i className="feather icon-feather-alert-circle"></i><span>不绑定顾客时,可以现场生成,但结果不会保存到顾客档案。</span></div>
+              )}
             </div>
 
             <div className="simulation-step workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px mb-4">
               <div className="simulation-step-heading">
                 <span>2</span>
                 <div>
-                  <span className="fs-14 fw-700 text-base-color">{pageCopy.upload}</span>
                   <h2 className="fw-700 text-dark-gray fs-26 mb-0">{pageCopy.upload}</h2>
                 </div>
               </div>
@@ -268,9 +265,8 @@ export function StylistSimulationPage() {
                 <img id="simulation-photo-preview" src={previewUrl} alt={pageCopy.upload} />
                 <span>
                   <i className="feather icon-feather-upload me-8px"></i>
-                  {uploading ? '...' : pageCopy.upload}
+                  {uploading ? '...' : pageCopy.uploadHint}
                 </span>
-                <small className="d-block mt-10px text-medium-gray">{pageCopy.uploadHint}</small>
               </label>
             </div>
 
@@ -278,12 +274,11 @@ export function StylistSimulationPage() {
               <div className="simulation-step-heading">
                 <span>3</span>
                 <div>
-                  <span className="fs-14 fw-700 text-base-color">{pageCopy.myTemplates}</span>
                   <h2 className="fw-700 text-dark-gray fs-26 mb-0">{pageCopy.myTemplates}</h2>
                 </div>
               </div>
-              <div className="simulation-choice-grid mt-25px mb-30px">
-                <label className="simulation-choice-card">
+              <div className="simulation-template-tabs mt-25px mb-20px">
+                <label>
                   <input
                     type="radio"
                     name="template-source"
@@ -291,13 +286,9 @@ export function StylistSimulationPage() {
                     checked={templateSource === 'STYLIST'}
                     onChange={() => setTemplateSource('STYLIST')}
                   />
-                  <span className="simulation-choice-icon">
-                    <i className="feather icon-feather-scissors"></i>
-                  </span>
-                  <strong>{pageCopy.myTemplates}</strong>
-                  <small>{pageCopy.myTemplates}</small>
+                  <span><i className="feather icon-feather-grid"></i>{pageCopy.myTemplates}</span>
                 </label>
-                <label className="simulation-choice-card">
+                <label>
                   <input
                     type="radio"
                     name="template-source"
@@ -305,19 +296,11 @@ export function StylistSimulationPage() {
                     checked={templateSource === 'SYSTEM'}
                     onChange={() => setTemplateSource('SYSTEM')}
                   />
-                  <span className="simulation-choice-icon">
-                    <i className="feather icon-feather-layers"></i>
-                  </span>
-                  <strong>{pageCopy.systemTemplates}</strong>
-                  <small>{pageCopy.systemTemplates}</small>
+                  <span><i className="feather icon-feather-layers"></i>{pageCopy.systemTemplates}</span>
                 </label>
               </div>
-              <div className="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-25px">
-                <p className="mb-0 fs-15 text-medium-gray">
-                  {pageCopy.selectedCount.replace('{{count}}', String(selectedTemplates.length))}
-                </p>
-              </div>
-              <div className="simulation-template-grid">
+              <div className="d-flex align-items-center justify-content-end flex-wrap gap-2 mb-25px"><span className="simulation-count-badge">已选 <strong>{selectedTemplates.length}</strong> 个模板</span></div>
+              <div className={`simulation-template-grid mb-30px${templateSource === 'SYSTEM' ? ' simulation-system-template-grid' : ''}`}>
                 {templates.length === 0 ? (
                   <p className="text-medium-gray mb-0">{pageCopy.emptyTemplates}</p>
                 ) : (
@@ -343,16 +326,17 @@ export function StylistSimulationPage() {
                   })
                 )}
               </div>
-              <div className="mt-25px">
+              <div>
                 <label
                   htmlFor="simulation-remark"
-                  className="fs-14 fw-700 text-dark-gray d-block mb-10px"
+                  className="customer-edit-label"
                 >
                   {pageCopy.remark}
                 </label>
                 <textarea
                   id="simulation-remark"
-                  className="form-control"
+                  className="form-control customer-edit-control"
+                  rows={4}
                   value={remark}
                   placeholder={pageCopy.remarkPlaceholder}
                   onChange={(event) => setRemark(event.target.value)}

+ 3 - 3
frontend/src/pages/dashboard/simulation/__tests__/StylistSimulationPage.test.tsx

@@ -68,7 +68,7 @@ vi.mock('../../../../shared/pageContext', () => ({
 }))
 
 describe('StylistSimulationPage', () => {
-  it('defaults to no customer when opened from dashboard', async () => {
+  it('defaults to binding a customer when opened from dashboard', async () => {
     listCustomers.mockResolvedValue({ items: [], total: 0, page: 1, pageSize: 10 })
     listSimulationTemplates.mockResolvedValue({
       items: [],
@@ -87,8 +87,8 @@ describe('StylistSimulationPage', () => {
       </HelmetProvider>,
     )
 
-    expect(await screen.findByLabelText('不绑定顾客')).toBeChecked()
-    expect(screen.getByLabelText('顾客')).toBeDisabled()
+    expect(await screen.findByLabelText('绑定顾客')).toBeChecked()
+    expect(screen.getByLabelText('顾客')).toBeEnabled()
     expect(screen.getByAltText('顾客')).toHaveAttribute('src', '/images/default-user-avatar.svg')
     expect(screen.getByAltText('上传顾客照片')).toHaveAttribute('src', '/images/default-user-avatar.svg')
   })

+ 3 - 9
frontend/src/pages/dashboard/templates/TemplateFormPage.tsx

@@ -12,6 +12,7 @@ import {
 } from '../../../services/api/templates'
 import { uploadMedia } from '../../../services/api/media'
 import { ApiError } from '../../../services/http/client'
+import { DEFAULT_USER_AVATAR_URL } from '../../../shared/uiAssets'
 
 function getSubmitErrorMessage(err: unknown, fallback: string) {
   if (
@@ -82,7 +83,7 @@ export function TemplateFormPage() {
   const [customModelFile, setCustomModelFile] = useState<File | null>(null)
   const [customOutfitFile, setCustomOutfitFile] = useState<File | null>(null)
   const [customBackgroundFile, setCustomBackgroundFile] = useState<File | null>(null)
-  const referencePreview = usePreviewUrl(referenceFile, '')
+  const referencePreview = usePreviewUrl(referenceFile, DEFAULT_USER_AVATAR_URL)
   const customModelPreview = usePreviewUrl(customModelFile, '')
   const customOutfitPreview = usePreviewUrl(customOutfitFile, '')
   const customBackgroundPreview = usePreviewUrl(customBackgroundFile, '')
@@ -270,14 +271,7 @@ export function TemplateFormPage() {
                   <h2 className="fw-700 text-dark-gray fs-26 mb-25px">{pageCopy.reference.title}</h2>
                   <label htmlFor="template-reference-upload" className="template-upload-box">
                     <input id="template-reference-upload" type="file" accept="image/*" onChange={(event) => setReferenceFile(event.target.files?.[0] || null)} />
-                    {referencePreview ? (
-                      <img src={referencePreview} alt={pageCopy.reference.previewAlt} />
-                    ) : (
-                      <div className="template-upload-placeholder">
-                        <i className="feather icon-feather-image"></i>
-                        <strong>{pageCopy.reference.upload}</strong>
-                      </div>
-                    )}
+                    <img src={referencePreview} alt={pageCopy.reference.previewAlt} />
                     <span><i className="feather icon-feather-upload"></i>{pageCopy.reference.upload}</span>
                   </label>
                   <p className="fs-14 text-medium-gray lh-24 mt-15px mb-0">{pageCopy.reference.help}</p>

+ 5 - 5
frontend/src/pages/dashboard/templates/TemplateListPage.tsx

@@ -188,7 +188,7 @@ export function TemplateListPage() {
 
           <div className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-25px mb-4 template-list-filter-panel">
             <div className="row g-3 align-items-end template-list-filter-grid">
-              <div className="col-lg-5">
+              <div className="col-lg-6">
                 <label htmlFor="template-search" className="fw-700 text-dark-gray mb-10px">{pageCopy.filters.searchLabel}</label>
                 <div className="customer-search-field">
                   <i className="feather icon-feather-search"></i>
@@ -205,7 +205,7 @@ export function TemplateListPage() {
                   />
                 </div>
               </div>
-              <div className="col-lg-3 col-md-6">
+              <div className="col-lg-3 col-md-4">
                 <label htmlFor="category-filter" className="fw-700 text-dark-gray mb-10px">{pageCopy.filters.categoryLabel}</label>
                 <select
                   id="category-filter"
@@ -221,7 +221,7 @@ export function TemplateListPage() {
                   ))}
                 </select>
               </div>
-              <div className="col-lg-3 col-md-6">
+              <div className="col-lg-3 col-md-4">
                 <label htmlFor="status-filter" className="fw-700 text-dark-gray mb-10px">{pageCopy.filters.statusLabel}</label>
                 <select
                   id="status-filter"
@@ -257,7 +257,7 @@ export function TemplateListPage() {
                 <span className="fs-14 fw-700 text-base-color text-uppercase">{pageCopy.resultBadge}</span>
                 <h2 className="fw-700 text-medium-gray fs-26 mb-0">{pageCopy.resultTitle.replace('{{count}}', String(total))}</h2>
               </div>
-              <div className="d-flex align-items-center gap-2 template-list-results-actions">
+              <div className="d-flex flex-wrap gap-2 template-list-results-actions">
                 <button
                   type="button"
                   onClick={handleToggleSelectAllCurrentPage}
@@ -272,7 +272,7 @@ export function TemplateListPage() {
                   type="button"
                   onClick={handleBulkDelete}
                   disabled={selectedIds.length === 0}
-                  className="btn btn-small btn-rounded btn-base-color fw-700"
+                  className="btn btn-small btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700"
                 >
                   {pageCopy.bulkDelete || '批量删除'}
                 </button>

+ 120 - 0
frontend/src/pages/public/BetaApplyPage.tsx

@@ -0,0 +1,120 @@
+import { useState, type FormEvent } from 'react'
+import { Link } from 'react-router-dom'
+import { Helmet } from 'react-helmet-async'
+import { useLegacyThemePage, usePageContext } from '../../shared/pageContext'
+import { submitBetaApplication } from '../../services/api/betaApplications'
+import { AppLogo } from '../../components/common/AppLogo'
+
+export function BetaApplyPage() {
+  const { locale, path } = usePageContext()
+  const [submitting, setSubmitting] = useState(false)
+  const [submitted, setSubmitted] = useState(false)
+  const [error, setError] = useState('')
+  useLegacyThemePage({ bodyClassName: 'aitori-beta-apply-body', mobileNavStyle: 'classic' })
+
+  const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
+    event.preventDefault()
+    if (submitting) return
+    setSubmitting(true)
+    setError('')
+    const data = new FormData(event.currentTarget)
+    try {
+      await submitBetaApplication({
+        name: String(data.get('name') || ''),
+        email: String(data.get('email') || ''),
+        salonName: String(data.get('salonName') || ''),
+        area: String(data.get('area') || ''),
+        role: String(data.get('role') || ''),
+        contact: String(data.get('contact') || ''),
+        portfolioUrl: String(data.get('portfolioUrl') || '') || undefined,
+        monthlyCustomers: String(data.get('monthlyCustomers') || '') || undefined,
+        language: String(data.get('language') || '') || undefined,
+        features: data.getAll('features').map(String),
+        painPoint: String(data.get('painPoint') || '') || undefined,
+      })
+      setSubmitted(true)
+      event.currentTarget.reset()
+    } catch (submitError) {
+      setError(submitError instanceof Error ? submitError.message : '提交失败,请稍后重试')
+    } finally {
+      setSubmitting(false)
+    }
+  }
+
+  return (
+    <>
+      <Helmet>
+        <html lang={locale === 'ja' ? 'ja' : 'zh-CN'} />
+        <title>Aitori Stylist | 申请参与内测</title>
+        <meta name="description" content="申请参与 Aitori Stylist 内测。面向发型设计师与沙龙,测试期间免费,正式上线后内测参与者可获得折扣价格。" />
+      </Helmet>
+      <header>
+        <nav className="navbar navbar-expand-lg header-light bg-white box-shadow-small">
+          <div className="container-fluid">
+            <div className="col-auto me-auto"><Link className="navbar-brand" to={path('')}><AppLogo /></Link></div>
+            <div className="col-auto menu-order position-static">
+              <button className="navbar-toggler float-start" type="button" data-bs-toggle="collapse" data-bs-target="#betaNavbar" aria-controls="betaNavbar" aria-label="打开导航">
+                <span className="navbar-toggler-line"></span><span className="navbar-toggler-line"></span><span className="navbar-toggler-line"></span><span className="navbar-toggler-line"></span>
+              </button>
+              <div className="collapse navbar-collapse" id="betaNavbar">
+                <ul className="navbar-nav">
+                  <li className="nav-item"><Link to={path('')} className="nav-link">内测介绍</Link></li>
+                  <li className="nav-item"><Link to={path('/profile')} className="nav-link">主页示例</Link></li>
+                  <li className="nav-item"><Link to={path('/customer-simulation')} className="nav-link">试发型体验</Link></li>
+                  <li className="nav-item"><Link to={path('/login')} className="nav-link">测试后台</Link></li>
+                  <li className="nav-item"><Link to={path('/beta-apply')} className="nav-link active">申请内测</Link></li>
+                </ul>
+              </div>
+            </div>
+          </div>
+        </nav>
+      </header>
+
+      <main className="aitori-beta-apply-page">
+        <section className="aitori-beta-apply-hero ipad-top-space-margin">
+          <div className="container">
+            <div className="row align-items-center justify-content-between g-5">
+              <div className="col-lg-5">
+                <span className="bg-base-color d-inline-block mb-20px fw-600 text-white border-radius-30px ps-20px pe-20px fs-12">Aitori Stylist Beta</span>
+                <h1 className="fw-700 text-dark-gray ls-minus-1px mb-20px">申请参与 Aitori Stylist 内测</h1>
+                <p className="fs-19 lh-32 text-dark-gray fw-600 mb-20px">我们正在邀请发型设计师与沙龙一起测试预约前 AI 试发型、公开主页、发型模板和顾客咨询体验。</p>
+                <p className="mb-30px">测试期间免费使用。正式上线后,内测参与者也会获得折扣价格。</p>
+                <div className="aitori-beta-apply-points">
+                  <div><i className="feather icon-feather-camera"></i><span>让顾客在预约前先试发型</span></div>
+                  <div><i className="feather icon-feather-grid"></i><span>展示你设计的发型模板</span></div>
+                  <div><i className="feather icon-feather-message-circle"></i><span>让咨询沟通更具体</span></div>
+                </div>
+              </div>
+
+              <div className="col-lg-6">
+                <form className="aitori-beta-apply-form bg-white border-radius-8px box-shadow-extra-large p-35px" onSubmit={handleSubmit}>
+                  <div className="mb-25px"><span className="fs-14 fw-700 text-base-color">Beta Application</span><h2 className="fw-700 text-dark-gray fs-28 mb-0">告诉我们你的场景</h2></div>
+                  {submitted ? <div className="alert alert-success" role="status">申请已提交,感谢参与!我们会根据测试阶段和地区陆续联系。</div> : null}
+                  {error ? <div className="alert alert-danger" role="alert">{error}</div> : null}
+                  <div className="row g-3">
+                    <Field id="apply-name" name="name" label="姓名" placeholder="你的姓名" required />
+                    <Field id="apply-email" name="email" label="邮箱" placeholder="example@email.com" type="email" required />
+                    <Field id="apply-salon" name="salonName" label="沙龙名 / 店铺名" placeholder="例:Aitori Demo Salon" required />
+                    <Field id="apply-area" name="area" label="所在地区" placeholder="例:东京 / 大阪 / 上海" required />
+                    <div className="col-md-6"><label className="form-label fw-700 text-dark-gray" htmlFor="apply-role">你的角色</label><select id="apply-role" name="role" className="form-control border-radius-8px" required defaultValue=""><option value="" disabled>请选择</option><option value="stylist">个人发型设计师</option><option value="salon-owner">沙龙负责人 / 主理人</option><option value="staff">沙龙员工</option><option value="other">其他</option></select></div>
+                    <Field id="apply-contact" name="contact" label="常用联系方式" placeholder="LINE / Instagram / 电话" required />
+                    <div className="col-12"><label className="form-label fw-700 text-dark-gray" htmlFor="apply-url">作品或预约页面 URL</label><input id="apply-url" name="portfolioUrl" type="url" className="form-control border-radius-8px" placeholder="Instagram、Hot Pepper、官网或预约页面" /></div>
+                    <div className="col-md-6"><label className="form-label fw-700 text-dark-gray" htmlFor="apply-volume">每月接待顾客数</label><select id="apply-volume" name="monthlyCustomers" className="form-control border-radius-8px" defaultValue=""><option value="">请选择</option><option value="1-30">1-30</option><option value="31-100">31-100</option><option value="100+">100+</option></select></div>
+                    <div className="col-md-6"><label className="form-label fw-700 text-dark-gray" htmlFor="apply-language">联系语言</label><select id="apply-language" name="language" className="form-control border-radius-8px" defaultValue="zh"><option value="zh">中文</option><option value="ja">日文</option><option value="en">英文</option></select></div>
+                    <div className="col-12"><span className="form-label fw-700 text-dark-gray d-block">最想测试的功能</span><div className="aitori-beta-apply-checks"><label><input type="checkbox" name="features" value="customer-try-on" defaultChecked />顾客预约前 AI 试发型</label><label><input type="checkbox" name="features" value="profile" />美发师公开主页</label><label><input type="checkbox" name="features" value="templates" />自己的发型模板</label><label><input type="checkbox" name="features" value="follow" />顾客管理 / 跟进提醒</label></div></div>
+                    <div className="col-12"><label className="form-label fw-700 text-dark-gray" htmlFor="apply-pain">现在最想解决的问题</label><textarea id="apply-pain" name="painPoint" className="form-control border-radius-8px" rows={4} placeholder="例:顾客预约前不容易确认方向、咨询时间太长、想提高新客预约率"></textarea></div>
+                  </div>
+                  <div className="aitori-beta-apply-submit mt-30px"><button type="submit" className="btn btn-large btn-rounded btn-base-color btn-box-shadow fw-700" disabled={submitting}><i className="feather icon-feather-send me-10px"></i>{submitting ? '提交中…' : '申请内测名额'}</button><span>提交后我们会根据测试阶段和地区陆续联系。</span></div>
+                </form>
+              </div>
+            </div>
+          </div>
+        </section>
+      </main>
+    </>
+  )
+}
+
+function Field({ id, name, label, placeholder, type = 'text', required = false }: { id: string; name: string; label: string; placeholder: string; type?: string; required?: boolean }) {
+  return <div className="col-md-6"><label className="form-label fw-700 text-dark-gray" htmlFor={id}>{label}</label><input id={id} name={name} type={type} className="form-control border-radius-8px" placeholder={placeholder} required={required} /></div>
+}

+ 93 - 428
frontend/src/pages/public/LandingPage.tsx

@@ -1,455 +1,120 @@
-import React from 'react'
+import { useEffect, useRef, useState } from 'react'
+import { Link } from 'react-router-dom'
 import { Helmet } from 'react-helmet-async'
-import { useLegacyThemePage, usePageContext, withLocale } from '../../shared/pageContext'
-import { MarketingLink } from '../../components/common/MarketingLink'
+import { useLegacyThemePage, usePageContext } from '../../shared/pageContext'
+
+const inviteLinks = [
+  { label: '主页示例', path: '/profile' },
+  { label: '试发型体验', path: '/customer-simulation' },
+  { label: '测试后台', path: '/login' },
+  { label: '申请内测', path: '/beta-apply' },
+]
+
+const inviteMeta = [
+  { label: 'Status', title: '少量名额开放中', body: '我们正在邀请愿意共创测试的发型设计师与沙龙。' },
+  { label: 'For', title: '发型设计师 / 沙龙主理人', body: '适合重视咨询体验、提案转化和长期顾客关系的团队。' },
+  { label: 'Core', title: '预约前可视化提案', body: '让顾客在到店前先看到方向,也让咨询从一开始更具体。' },
+  { label: 'Contact', title: '告诉我们你的场景', body: '测试期间免费使用;正式上线后,内测参与者也会获得折扣价格。' },
+]
 
 export function LandingPage() {
-  const { locale, path, copy } = usePageContext()
-  const page = copy.marketing
-  const [navHome, navAbout, navFeatures, navResults, navSample, navFaq, navContact, navLogin, navDevSpec] =
-    page.header.nav
-  const faqJsonLd = {
-    '@context': 'https://schema.org',
-    '@type': 'FAQPage',
-    mainEntity: page.faq.map((item) => ({
-      '@type': 'Question',
-      name: item.q,
-      acceptedAnswer: {
-        '@type': 'Answer',
-        text: item.a,
-      },
-    })),
+  const { locale, path } = usePageContext()
+  const [menuOpen, setMenuOpen] = useState(false)
+  const pageRef = useRef<HTMLElement>(null)
+  const rafRef = useRef<number | null>(null)
+  useLegacyThemePage({ bodyClassName: 'aitori-invite-body', mobileNavStyle: 'classic' })
+
+  useEffect(() => () => {
+    if (rafRef.current) window.cancelAnimationFrame(rafRef.current)
+  }, [])
+
+  const updateVideoShift = (clientX: number, clientY: number) => {
+    const page = pageRef.current
+    if (!page || window.matchMedia('(prefers-reduced-motion: reduce)').matches || !window.matchMedia('(hover: hover) and (pointer: fine)').matches) return
+    const rect = page.getBoundingClientRect()
+    const x = (clientX - rect.left) / rect.width - 0.5
+    const y = (clientY - rect.top) / rect.height - 0.5
+    if (rafRef.current) window.cancelAnimationFrame(rafRef.current)
+    rafRef.current = window.requestAnimationFrame(() => {
+      page.style.setProperty('--video-shift-x', `${(x * -72).toFixed(2)}px`)
+      page.style.setProperty('--video-shift-y', `${(y * -44).toFixed(2)}px`)
+    })
   }
 
-  useLegacyThemePage({ bodyClassName: 'custom-cursor', mobileNavStyle: 'classic' })
+  const resetVideoShift = () => {
+    const page = pageRef.current
+    if (!page) return
+    page.style.setProperty('--video-shift-x', '0px')
+    page.style.setProperty('--video-shift-y', '0px')
+  }
 
   return (
     <>
       <Helmet>
-        <html lang={page.htmlLang} />
-        <title>{page.title}</title>
-        <meta name="description" content={page.description} />
-        <meta name="robots" content="index,follow,max-image-preview:large,max-snippet:-1,max-video-preview:-1" />
-        <link rel="canonical" href={page.canonical} />
-        <script type="application/ld+json">{JSON.stringify(faqJsonLd)}</script>
+        <html lang={locale === 'ja' ? 'ja' : 'zh-CN'} />
+        <title>Aitori Stylist | 发型设计师与沙龙内测招募中</title>
+        <meta name="description" content="Aitori Stylist 正在邀请发型设计师与沙龙参加内测。一起打磨预约前 AI 试发型、公开主页、发型模板和顾客咨询体验。" />
       </Helmet>
-
-      <div className="cursor-page-inner">
-        <div className="circle-cursor circle-cursor-inner"></div>
-        <div className="circle-cursor circle-cursor-outer"></div>
-      </div>
-
-      <header>
-        <nav className="navbar navbar-expand-lg header-light bg-transparent disable-fixed" data-header-hover="light">
-          <div className="container-fluid">
-            <div className="col-auto me-auto">
-              <MarketingLink className="navbar-brand" to={path('')}>
-                <img src="/images/logo_color.svg" alt="" className="default-logo" />
-                <img src="/images/logo_color.svg" alt="" className="alt-logo" />
-                <img src="/images/logo_color.svg" alt="" className="mobile-logo" />
-              </MarketingLink>
-            </div>
-            <div className="col-auto menu-order position-static">
-              <button
-                className="navbar-toggler float-start"
-                type="button"
-                data-bs-toggle="collapse"
-                data-bs-target="#navbarNav"
-                aria-controls="navbarNav"
-                aria-label="Toggle navigation"
-              >
-                <span className="navbar-toggler-line"></span>
-                <span className="navbar-toggler-line"></span>
-                <span className="navbar-toggler-line"></span>
-                <span className="navbar-toggler-line"></span>
-              </button>
-              <div className="collapse navbar-collapse" id="navbarNav">
-                <ul className="navbar-nav">
-                  <li className="nav-item"><a href="#top" className="nav-link">{navHome}</a></li>
-                  <li className="nav-item"><a href="#about" className="nav-link">{navAbout}</a></li>
-                  <li className="nav-item"><a href="#features" className="nav-link">{navFeatures}</a></li>
-                  <li className="nav-item"><a href="#results" className="nav-link">{navResults}</a></li>
-                  <li className="nav-item"><a href="#sample-site" className="nav-link">{navSample}</a></li>
-                  <li className="nav-item"><a href="#faq" className="nav-link">{navFaq}</a></li>
-                  <li className="nav-item"><a href="#contact" className="nav-link">{navContact}</a></li>
-                  <li className="nav-item"><MarketingLink className="nav-link" to={path('/login')}>{navLogin}</MarketingLink></li>
-                  {navDevSpec ? <li className="nav-item"><a href="#" className="nav-link">{navDevSpec}</a></li> : null}
-                  <li className="nav-item dropdown simple-dropdown">
-                    <a href="#" className="nav-link dropdown-toggle" id="languageDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
-                      {page.header.languageLabel}
-                    </a>
-                    <ul className="dropdown-menu" aria-labelledby="languageDropdown">
-                      {page.header.languageItems.map((item) => (
-                        <li key={item.label} className={item.locale === locale ? 'active' : undefined}>
-                          {item.locale ? (
-                            <MarketingLink
-                              className="dropdown-item"
-                              to={withLocale(item.locale)}
-                            >
-                              {item.label}
-                            </MarketingLink>
-                          ) : (
-                            <a href={item.href} className="dropdown-item" lang={item.lang}>{item.label}</a>
-                          )}
-                        </li>
-                      ))}
-                    </ul>
-                  </li>
-                </ul>
-              </div>
-            </div>
-          </div>
-        </nav>
-      </header>
-
-      <section
-        id="top"
-        className="p-0 cover-background full-screen ipad-top-space-margin md-h-auto position-relative md-pb-70px"
-        style={{ backgroundImage: "url('/images/demo-application-home-banner.jpg')" }}
+      <main
+        ref={pageRef}
+        className={`aitori-invite-page${menuOpen ? ' is-menu-open' : ''}`}
+        onPointerMove={(event) => updateVideoShift(event.clientX, event.clientY)}
+        onPointerLeave={resetVideoShift}
       >
-        <div className="container h-100 position-relative z-index-9">
-          <div className="row align-items-center h-100 justify-content-center">
-            <div className="col-xl-6 col-lg-6 col-md-10 text-center text-lg-start md-mb-50px">
-              <span className="bg-base-color d-inline-block mb-20px fw-600 text-white text-uppercase border-radius-30px ps-20px pe-20px fs-12">
-                {page.hero.badge}
-              </span>
-              <h1 className="fw-700 text-dark-gray ls-minus-2px mb-25px">{page.hero.title}</h1>
-              <p className="fs-19 lh-32 mb-20px w-90 lg-w-100">{page.hero.lead}</p>
-              <p className="fs-17 lh-30 mb-35px w-85 lg-w-100">{page.hero.sublead}</p>
-              <div className="d-flex flex-column flex-sm-row justify-content-center justify-content-lg-start gap-3">
-                <a href="#contact" className="btn btn-extra-large btn-rounded btn-dark-gray btn-box-shadow fw-700">{page.hero.primaryCta}</a>
-                <a href="#features" className="btn btn-extra-large btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700">{page.hero.secondaryCta}</a>
-              </div>
-            </div>
-            <div className="col-xl-5 col-lg-6 col-md-10">
-              <div className="hero-visual">
-                <div className="hero-main-card bg-white border-radius-10px box-shadow-quadruple-large p-20px">
-                  <img src="/images/aitori-stylist-hero-ui.png" alt="Aitori Stylist" className="border-radius-8px w-100" />
-                </div>
-                <div className="hero-comparison-strip">
-                  <div className="hero-comparison-card bg-white border-radius-8px box-shadow-extra-large p-10px">
-                    <img src="/images/aitori-before-photo.png" className="border-radius-6px w-100" alt={page.results.items[0].title} />
-                  </div>
-                  <div className="hero-comparison-card bg-white border-radius-8px box-shadow-extra-large p-10px">
-                    <img src="/images/aitori-simulation-result.png" className="border-radius-6px w-100" alt={page.results.items[1].title} />
-                  </div>
-                </div>
-              </div>
-            </div>
-          </div>
-        </div>
-      </section>
-
-      <section className="half-section pb-0">
-        <div className="container">
-          <div className="row justify-content-center mb-4">
-            <div className="col-lg-8 text-center">
-              <span className="fs-19 alt-font d-inline-block text-dark-gray fw-600 ls-minus-05px">{page.highlights.title}</span>
-            </div>
-          </div>
-          <div className="row row-cols-1 row-cols-lg-4 row-cols-md-2 justify-content-center">
-            {page.highlights.items.map((item) => (
-              <div key={item.label} className="col md-mb-30px">
-                <div className="bg-white border-radius-8px box-shadow-quadruple-large p-30px h-100 text-center">
-                  <h4 className="text-dark-gray fw-800 mb-10px">{item.value}</h4>
-                  <p className="mb-0">{item.label}</p>
-                </div>
-              </div>
-            ))}
-          </div>
-        </div>
-      </section>
-
-      <section id="about" className="position-relative overflow-hidden">
-        <div className="container">
-          <div className="row align-items-center justify-content-center">
-            <div className="col-lg-8 md-mb-40px">
-              <span className="bg-base-color d-inline-block mb-20px fw-600 text-white text-uppercase border-radius-30px ps-20px pe-20px fs-12">{page.about.badge}</span>
-              <h2 className="fw-700 text-dark-gray ls-minus-1px mb-25px">{page.about.title}</h2>
-              <p className="mb-20px">{page.about.description}</p>
-              <ul className="p-0 m-0 list-style-02 text-dark-gray">
-                {page.about.bullets.map((item, index) => (
-                  <li key={item} className={index < page.about.bullets.length - 1 ? 'pb-10px' : undefined}>
-                    <i className="feather icon-feather-check text-green me-10px"></i>{item}
-                  </li>
-                ))}
-              </ul>
-            </div>
-            <div className="col-12 mt-5">
-              <div className="bg-white border-radius-10px box-shadow-quadruple-large p-25px">
-                <div className="row row-cols-1 row-cols-md-3 g-4">
-                  {page.about.steps.map((item) => (
-                    <div key={item.title} className="col">
-                      <div className="about-step-card border-radius-8px bg-floral-white p-20px h-100">
-                        <img src={item.image} alt={item.alt} className="about-step-image border-radius-6px w-100 mb-15px" />
-                        <h6 className="text-dark-gray fw-700 mb-10px">{item.title}</h6>
-                        <p className="mb-0">{item.body}</p>
-                      </div>
-                    </div>
-                  ))}
-                </div>
-              </div>
-            </div>
-          </div>
-        </div>
-      </section>
-
-      <section className="bg-gradient-very-light-gray">
-        <div className="container">
-          <div className="row justify-content-center mb-5">
-            <div className="col-lg-8 text-center">
-              <span className="bg-base-color d-inline-block mb-15px fw-600 text-white text-uppercase border-radius-30px ps-20px pe-20px fs-12">{page.challenges.badge}</span>
-              <h2 className="fw-700 text-dark-gray ls-minus-1px">{page.challenges.title}</h2>
-            </div>
-          </div>
-          <div className="row row-cols-1 row-cols-lg-2 justify-content-center">
-            {page.challenges.items.map((item) => (
-              <div key={item.title} className="col md-mb-30px">
-                <div className="bg-white border-radius-8px box-shadow-extra-large p-35px h-100">
-                  <h6 className="text-dark-gray fw-700">{item.title}</h6>
-                  <p className="mb-0">{item.body}</p>
-                </div>
-              </div>
-            ))}
-          </div>
-        </div>
-      </section>
-
-      <section>
-        <div className="container">
-          <div className="row justify-content-center mb-5">
-            <div className="col-lg-8 text-center">
-              <span className="bg-base-color d-inline-block mb-15px fw-600 text-white text-uppercase border-radius-30px ps-20px pe-20px fs-12">{page.process.badge}</span>
-              <h2 className="fw-700 text-dark-gray ls-minus-1px">{page.process.title}</h2>
-            </div>
-          </div>
-          <div className="row row-cols-1 row-cols-lg-3 row-cols-md-2 justify-content-center">
-            {page.process.items.map((item) => (
-              <div key={item.index} className="col md-mb-30px">
-                <div className="feature-box bg-white text-start justify-content-start h-100 border-radius-6px p-16 box-shadow-quadruple-large">
-                  <div className="feature-box-icon mb-25px">
-                    <span className="bg-linen text-dark-gray fw-700 border-radius-100 d-inline-flex align-items-center justify-content-center w-60px h-60px">{item.index}</span>
-                  </div>
-                  <div className="feature-box-content last-paragraph-no-margin">
-                    <span className="d-inline-block text-dark-gray fw-700 fs-18 mb-10px">{item.title}</span>
-                    <p>{item.body}</p>
-                  </div>
-                </div>
-              </div>
-            ))}
-          </div>
-        </div>
-      </section>
-
-      <section id="features" className="bg-gradient-very-light-gray">
-        <div className="container">
-          <div className="row justify-content-center mb-5">
-            <div className="col-lg-7 text-center">
-              <span className="bg-base-color d-inline-block mb-15px fw-600 text-white text-uppercase border-radius-30px ps-20px pe-20px fs-12">{page.features.badge}</span>
-              <h2 className="fw-700 text-dark-gray ls-minus-1px">{page.features.title}</h2>
-            </div>
-          </div>
-          <div className="row row-cols-1 row-cols-lg-3 row-cols-md-2 justify-content-center">
-            {page.features.items.map((item, index) => (
-              <div key={item.title} className={`col ${index < 3 ? 'md-mb-30px' : index < 5 ? 'sm-mb-30px' : ''}`.trim()}>
-                <div className="bg-white border-radius-8px box-shadow-extra-large p-30px h-100">
-                  <h6 className="text-dark-gray fw-700">{item.title}</h6>
-                  <p className="mb-0">{item.body}</p>
-                </div>
+        <video className="aitori-invite-bg" autoPlay muted loop playsInline poster="/images/aitori-stylist-hairstyle-loop-poster.jpg" aria-hidden="true">
+          <source src="/images/aitori-hairstyle-three-style-loop-final-web.mp4" type="video/mp4" />
+        </video>
+        <div className="aitori-invite-shell">
+          <nav className="aitori-invite-nav" aria-label="主导航">
+            <Link className="aitori-invite-logo" to={path('')} aria-label="Aitori Stylist">
+              <img src="/images/logo_color.svg" alt="Aitori Stylist" />
+            </Link>
+            <div className="aitori-invite-links">
+              {inviteLinks.map((item) => <Link key={item.path} to={path(item.path)}>{item.label}</Link>)}
+            </div>
+            <button className="aitori-invite-menu-button" type="button" aria-label="打开菜单" aria-expanded={menuOpen} onClick={() => setMenuOpen(true)}>
+              <i className="feather icon-feather-menu"></i>
+            </button>
+          </nav>
+
+          <div className="aitori-invite-meta">
+            {inviteMeta.map((item) => (
+              <div key={item.label}>
+                <span className="aitori-invite-label">{item.label}</span>
+                <strong>{item.title}</strong>
+                <p>{item.body}</p>
               </div>
             ))}
           </div>
-        </div>
-      </section>
-
-      <section id="results">
-        <div className="container">
-          <div className="row justify-content-center mb-5">
-            <div className="col-lg-8 text-center">
-              <span className="bg-base-color d-inline-block mb-15px fw-600 text-white text-uppercase border-radius-30px ps-20px pe-20px fs-12">{page.results.badge}</span>
-              <h2 className="fw-700 text-dark-gray ls-minus-1px">{page.results.title}</h2>
-              <p className="w-80 lg-w-100 mx-auto">{page.results.body}</p>
-            </div>
-          </div>
-          <div className="gallery-box row row-cols-1 row-cols-lg-4 row-cols-md-2 justify-content-center g-4">
-            {page.results.items.slice(0, 4).map((item) => (
-              <div key={item.title} className="col">
-                <div className="results-gallery-card bg-white border-radius-8px box-shadow-extra-large p-15px text-center h-100">
-                  <a href={item.image} data-group="results-gallery" title={item.title} className="d-block">
-                    <img src={item.image} alt={item.title} className="border-radius-6px w-100 mb-15px results-gallery-image" />
-                  </a>
-                  <span className="fw-700 text-dark-gray d-block">{item.title}</span>
-                </div>
-              </div>
-            ))}
-            <div className="col-lg-8 col-md-10">
-              <div className="results-gallery-card bg-white border-radius-8px box-shadow-extra-large p-15px text-center h-100">
-                <a href={page.results.items[4].image} data-group="results-gallery" title={page.results.items[4].title} className="d-block">
-                  <img src={page.results.items[4].image} alt={page.results.items[4].title} className="border-radius-6px w-100 mb-15px results-gallery-image results-gallery-image-wide" />
-                </a>
-                <span className="fw-700 text-dark-gray d-block">{page.results.items[4].title}</span>
-              </div>
-            </div>
-          </div>
-          <div className="row justify-content-center mt-5">
-            <div className="col-lg-8 text-center">
-              <p className="mb-0">{page.results.caption}</p>
-            </div>
-          </div>
-        </div>
-      </section>
-
-      <section className="bg-floral-white">
-        <div className="container">
-          <div className="row justify-content-center mb-5">
-            <div className="col-lg-7 text-center">
-              <span className="bg-base-color d-inline-block mb-15px fw-600 text-white text-uppercase border-radius-30px ps-20px pe-20px fs-12">{page.values.badge}</span>
-              <h2 className="fw-700 text-dark-gray ls-minus-1px">{page.values.title}</h2>
-            </div>
-          </div>
-          <div className="row row-cols-1 row-cols-lg-5 row-cols-md-2 justify-content-center">
-            {page.values.items.map((item) => (
-              <div key={item.title} className="col md-mb-30px">
-                <div className="bg-white border-radius-8px p-25px h-100 box-shadow-extra-large">
-                  <h6 className="fw-600 text-dark-gray">{item.title}</h6>
-                  <p className="mb-0">{item.body}</p>
-                </div>
-              </div>
-            ))}
-          </div>
-        </div>
-      </section>
-
-      <section>
-        <div className="container">
-          <div className="row justify-content-center mb-5">
-            <div className="col-lg-7 text-center">
-              <span className="bg-base-color d-inline-block mb-15px fw-600 text-white text-uppercase border-radius-30px ps-20px pe-20px fs-12">{page.audiences.badge}</span>
-              <h2 className="fw-700 text-dark-gray ls-minus-1px">{page.audiences.title}</h2>
-            </div>
-          </div>
-          <div className="row row-cols-1 row-cols-lg-3 row-cols-md-2 justify-content-center">
-            {page.audiences.items.map((item, index) => (
-              <div key={item} className={`col ${index < 3 ? 'md-mb-30px' : index < 5 ? 'sm-mb-30px' : ''}`.trim()}>
-                <div className="bg-white border-radius-8px box-shadow-extra-large p-30px h-100">
-                  <p className="mb-0 fw-600 text-dark-gray">{item}</p>
-                </div>
-              </div>
-            ))}
-          </div>
-        </div>
-      </section>
-
-      <section id="sample-site" className="bg-gradient-very-light-gray">
-        <div className="container">
-          <div className="row align-items-center justify-content-center">
-            <div className="col-lg-5 col-md-9 md-mb-40px">
-              <div className="bg-white border-radius-8px box-shadow-quadruple-large p-15px">
-                <img src="/images/stylist-profile/ogawa.jpeg" alt="美发师专用示例主页" className="border-radius-6px w-100" />
-              </div>
-            </div>
-            <div className="col-lg-6 offset-lg-1">
-              <span className="bg-base-color d-inline-block mb-20px fw-600 text-white border-radius-30px ps-20px pe-20px fs-12">{page.sample.badge}</span>
-              <h2 className="fw-700 text-dark-gray ls-minus-1px mb-25px">{page.sample.title}</h2>
-              <p className="fs-18 lh-30 mb-20px">{page.sample.lead}</p>
-              <p className="mb-30px">{page.sample.sublead}</p>
-              <div className="d-flex flex-column flex-sm-row gap-3">
-                <MarketingLink className="btn btn-extra-large btn-rounded btn-base-color btn-box-shadow fw-700" to={path('/profile')}>
-                  {page.sample.cta}
-                </MarketingLink>
-              </div>
-            </div>
-          </div>
-        </div>
-      </section>
 
-      <section>
-        <div className="container">
-          <div className="row justify-content-center">
-            <div className="col-lg-10">
-              <div className="bg-white border-radius-10px p-50px lg-p-35px box-shadow-quadruple-large">
-                <div className="row align-items-center">
-                  <div className="col-md-3 text-center sm-mb-25px">
-                    <img src="/images/stylist-profile/ogawa.jpeg" className="rounded-circle" alt="美发师 小川泰明" />
-                  </div>
-                  <div className="col-md-9 text-center text-md-start">
-                    <span className="bg-base-color d-inline-block mb-15px fw-600 text-white text-uppercase border-radius-30px ps-20px pe-20px fs-12">{page.testimonial.badge}</span>
-                    <p className="fs-20 lh-34 text-dark-gray mb-15px">{page.testimonial.quote}</p>
-                    <div className="fw-700 text-dark-gray">{page.testimonial.author}</div>
-                  </div>
-                </div>
-              </div>
-            </div>
-          </div>
-        </div>
-      </section>
+          <div className="aitori-invite-spacer"></div>
 
-      <section id="faq">
-        <div className="container">
-          <div className="row justify-content-center mb-5">
-            <div className="col-lg-7 text-center">
-              <span className="bg-base-color d-inline-block mb-15px fw-600 text-white text-uppercase border-radius-30px ps-20px pe-20px fs-12">
-                {locale === 'ja' ? 'FAQ' : '常见问题'}
-              </span>
-              <h2 className="fw-700 text-dark-gray ls-minus-1px">{locale === 'ja' ? 'FAQ' : '常见问题'}</h2>
+          <section className="aitori-invite-bottom" aria-labelledby="invite-heading">
+            <div className="aitori-invite-heading">
+              <span className="aitori-invite-kicker">Aitori Stylist Beta</span>
+              <h1 id="invite-heading">你设计的发型,<span>让顾客先用 AI 试试看。</span></h1>
             </div>
-          </div>
-          <div className="row justify-content-center">
-            <div className="col-lg-9">
-              <div className="accordion accordion-style-02" id="accordion-style-02">
-                {page.faq.map((item, index) => {
-                  const panelId = `accordion-style-02-0${index + 1}`
-                  const active = index === 0
-                  return (
-                    <div key={item.q} className={`accordion-item ${active ? 'active-accordion' : ''}`.trim()}>
-                      <div className="accordion-header border-bottom border-color-extra-medium-gray">
-                        <a href="#" data-bs-toggle="collapse" data-bs-target={`#${panelId}`} aria-expanded={active} data-bs-parent="#accordion-style-02">
-                          <div className="accordion-title mb-0 position-relative text-dark-gray">
-                            <i className={`feather ${active ? 'icon-feather-minus' : 'icon-feather-plus'}`}></i>
-                            <span className="fw-600 fs-18">{item.q}</span>
-                          </div>
-                        </a>
-                      </div>
-                      <div id={panelId} className={`accordion-collapse collapse ${active ? 'show' : ''}`.trim()} data-bs-parent="#accordion-style-02">
-                        <div className={`accordion-body last-paragraph-no-margin ${index < page.faq.length - 1 ? 'border-bottom border-color-light-medium-gray' : ''}`.trim()}>
-                          <p>{item.a}</p>
-                        </div>
-                      </div>
-                    </div>
-                  )
-                })}
+            <div className="aitori-invite-side">
+              <p>Aitori Stylist 正在内测。让顾客在预约前用自己的照片尝试你设计的发型,减少不安,提高预约数和客户满意度。测试期间免费,正式上线后内测参与者也会获得折扣价格。</p>
+              <div className="aitori-invite-actions">
+                <Link to={path('/beta-apply')} className="aitori-invite-primary"><i className="feather icon-feather-mail"></i>申请参与测试</Link>
+                <Link to={path('/profile')} className="aitori-invite-secondary">查看设计师主页示例</Link>
               </div>
+              <div className="aitori-invite-note"><span>一起测试的内容</span><strong>预约前试发型 · 设计师主页 · 发型模板 · 顾客咨询记录</strong></div>
             </div>
-          </div>
+          </section>
         </div>
-      </section>
 
-      <section id="contact" className="bg-dark-gray">
-        <div className="container">
-          <div className="row justify-content-center">
-            <div className="col-lg-8 text-center">
-              <span className="bg-white-transparent-very-light d-inline-block mb-15px fw-600 text-white text-uppercase border-radius-30px ps-20px pe-20px fs-12">{page.contact.badge}</span>
-              <h2 className="fw-700 text-white ls-minus-1px mb-20px">{page.contact.title}</h2>
-              <p className="text-white opacity-8 fs-18 mb-35px">{page.contact.body}</p>
-              <div className="d-flex flex-column flex-sm-row justify-content-center gap-3">
-                <a href="#" className="btn btn-extra-large btn-rounded btn-white btn-box-shadow fw-700">{page.contact.primaryCta}</a>
-                <a href="#" className="btn btn-extra-large btn-rounded btn-transparent-white-light border border-1 border-color-white fw-700 text-white">{page.contact.secondaryCta}</a>
-              </div>
-            </div>
+        <div className="aitori-invite-mobile-menu" aria-hidden={!menuOpen}>
+          <div className="aitori-invite-mobile-head">
+            <img src="/images/logo_color.svg" alt="Aitori Stylist" />
+            <button type="button" aria-label="关闭菜单" onClick={() => setMenuOpen(false)}><i className="feather icon-feather-x"></i></button>
           </div>
-        </div>
-      </section>
-
-      <footer className="p-0 footer-light position-relative">
-        <div className="container">
-          <div className="row justify-content-center pt-5 pb-5 sm-pt-40px sm-pb-40px">
-            <div className="col-12 text-center">
-              <img src="/images/logo_color.svg" alt="" className="footer-logo mb-20px" style={{ maxWidth: '160px' }} />
-              <div className="fs-16 text-medium-gray">{page.footer}</div>
-            </div>
+          <div className="aitori-invite-mobile-links">
+            {inviteLinks.map((item) => <Link key={item.path} to={path(item.path)} onClick={() => setMenuOpen(false)}>{item.label}</Link>)}
           </div>
         </div>
-      </footer>
+      </main>
     </>
   )
 }

+ 19 - 0
frontend/src/services/api/betaApplications.ts

@@ -0,0 +1,19 @@
+import { http } from '../http/client'
+
+export type BetaApplicationInput = {
+  name: string
+  email: string
+  salonName: string
+  area: string
+  role: string
+  contact: string
+  portfolioUrl?: string
+  monthlyCustomers?: string
+  language?: string
+  features?: string[]
+  painPoint?: string
+}
+
+export async function submitBetaApplication(input: BetaApplicationInput): Promise<{ id: string; submittedAt: string }> {
+  return http.post('/beta-applications', input)
+}

+ 47 - 0
frontend/src/shared/demoContent.ts

@@ -0,0 +1,47 @@
+export const DEMO_STYLIST_PHOTO = '/images/stylist-profile/demo-stylist-ai.png'
+export const CUSTOMER_SIMULATION_PHOTO_KEY = 'aitori-customer-simulation-photo'
+
+export type PublicHairstyleTemplate = {
+  id: string
+  name: string
+  image: string
+  category: 'long' | 'medium' | 'short'
+  accent: string
+}
+
+export const publicHairstyleTemplates: PublicHairstyleTemplate[] = [
+  { id: 'natural-long', name: '自然长发', image: '/images/stylist-profile/hairstyle/long/User1-1.jpg', category: 'long', accent: 'Long' },
+  { id: 'soft-layer', name: '柔和层次', image: '/images/stylist-profile/hairstyle/long/User3-4.jpg', category: 'long', accent: 'Layer' },
+  { id: 'transparent', name: '透明感', image: '/images/stylist-profile/hairstyle/long/User3-7.jpg', category: 'long', accent: 'Clear' },
+  { id: 'light-medium', name: '轻盈中长发', image: '/images/stylist-profile/hairstyle/medium/User1-8.jpg', category: 'medium', accent: 'Medium' },
+  { id: 'face-layer', name: '脸周层次', image: '/images/stylist-profile/hairstyle/medium/User3-5.jpg', category: 'medium', accent: 'Face' },
+  { id: 'natural-collarbone', name: '自然锁骨发', image: '/images/stylist-profile/hairstyle/medium/User4-10.jpg', category: 'medium', accent: 'Soft' },
+  { id: 'round-short', name: '圆润短发', image: '/images/stylist-profile/hairstyle/short/User1-6.jpg', category: 'short', accent: 'Round' },
+  { id: 'clean-short', name: '清爽短发', image: '/images/stylist-profile/hairstyle/short/User2-3.jpg', category: 'short', accent: 'Short' },
+]
+
+export const systemHairstyleTemplates = [
+  { id: 'after-30-days', name: '居家造型30天后造型', image: '/images/mvp-simulation-result-10/after-30-days.jpg' },
+  { id: 'home-styling', name: '居家造型', image: '/images/mvp-simulation-result-10/home-styling.jpg' },
+]
+
+export const demoSimulationResults = [
+  {
+    id: 'home-styling',
+    title: '居家造型',
+    image: '/images/mvp-simulation-result-10/home-styling.jpg',
+    customerNote: '适合先确认整体氛围和日常打理后的完成感。',
+  },
+  {
+    id: 'after-30-days',
+    title: '居家造型30天后造型',
+    image: '/images/mvp-simulation-result-10/after-30-days.jpg',
+    customerNote: '帮助你提前想象发型长出一段时间后的状态。',
+  },
+  {
+    id: 'after-60-days',
+    title: '居家造型60天后造型',
+    image: '/images/mvp-simulation-result-10/after-60-days.jpg',
+    customerNote: '用于判断下次维护时间,以及是否需要预约调整。',
+  },
+]

+ 49 - 0
frontend/src/shared/stylistProfileStore.ts

@@ -0,0 +1,49 @@
+import { DEMO_STYLIST_PHOTO } from './demoContent'
+
+export type StylistProfileDraft = {
+  photo: string
+  displayName: string
+  slug: string
+  years: string
+  rating: string
+  reviewCount: string
+  salonName: string
+  bookingUrl: string
+  address: string
+  hours: string
+  specialties: string
+  lead: string
+  bio: string
+}
+
+export const defaultStylistProfile: StylistProfileDraft = {
+  photo: DEMO_STYLIST_PHOTO,
+  displayName: 'Demo Stylist',
+  slug: 'demo-stylist',
+  years: '8年',
+  rating: '5.0',
+  reviewCount: '24',
+  salonName: 'Aitori Demo Salon',
+  bookingUrl: 'https://example.com/booking',
+  address: '东京都示例区 1-2-3\nAitori Demo Salon',
+  hours: '平日 10:00 - 21:00\n周末 10:00 - 19:00',
+  specialties: '擅长根据脸型、发质和日常打理习惯,提供自然、容易沟通的发型提案。',
+  lead: 'Aitori Stylist 的 demo 美发师主页。顾客可以查看资料、预约服务,并体验 AI 发型模拟。',
+  bio: '这是用于产品演示的美发师资料。可在后台维护姓名、介绍、预约链接、营业信息和公开模板,用于展示顾客端个人主页体验。',
+}
+
+const STORAGE_KEY = 'aitori-stylist-profile'
+
+export function getStoredStylistProfile(): StylistProfileDraft {
+  try {
+    const stored = window.localStorage.getItem(STORAGE_KEY)
+    return stored ? { ...defaultStylistProfile, ...JSON.parse(stored) } : defaultStylistProfile
+  } catch {
+    return defaultStylistProfile
+  }
+}
+
+export function saveStoredStylistProfile(profile: StylistProfileDraft) {
+  const persistentProfile = profile.photo.startsWith('data:') ? profile : { ...profile, photo: DEMO_STYLIST_PHOTO }
+  window.localStorage.setItem(STORAGE_KEY, JSON.stringify(persistentProfile))
+}

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.