Explorar o código

feat: survey with new scss

ryanemax hai 2 días
pai
achega
68da22f6d9

+ 302 - 245
src/modules/project/pages/project-survey/project-survey.component.html

@@ -1,243 +1,292 @@
-<ion-header>
-  <ion-toolbar>
-    <ion-buttons slot="start">
-      <ion-button (click)="goBack()">
-        <ion-icon name="arrow-back"></ion-icon>
-      </ion-button>
-    </ion-buttons>
-    <ion-title>项目需求调查</ion-title>
-  </ion-toolbar>
-</ion-header>
-
-<ion-content class="survey-content">
+<!-- 顶部导航栏 -->
+<div class="survey-header">
+  <div class="header-content">
+    <button class="back-button" (click)="goBack()">
+      <svg class="icon" viewBox="0 0 512 512">
+        <path fill="currentColor" d="M244 400L100 256l144-144M120 256h292"/>
+      </svg>
+    </button>
+    <h1 class="header-title">项目需求调查</h1>
+    <div class="header-spacer"></div>
+  </div>
+</div>
+
+<!-- 主内容区 -->
+<div class="survey-container">
   <!-- 加载状态 -->
   @if (loading) {
-    <div class="loading-container">
-      <ion-spinner name="crescent"></ion-spinner>
-      <p>加载中...</p>
+    <div class="status-view loading-view">
+      <div class="spinner">
+        <div class="spinner-circle"></div>
+      </div>
+      <p class="status-text">加载中...</p>
     </div>
   }
 
   <!-- 错误状态 -->
   @if (error && !loading) {
-    <div class="error-container">
-      <ion-icon name="alert-circle-outline"></ion-icon>
-      <p>{{ error }}</p>
-      <ion-button (click)="goBack()">返回</ion-button>
+    <div class="status-view error-view">
+      <div class="error-icon-wrapper">
+        <svg class="icon error-icon" viewBox="0 0 512 512">
+          @if (isCustomerOnly) {
+            <!-- 仅限客户图标 -->
+            <path fill="currentColor" d="M336 256c-20.56 0-40.44-9.18-56-25.84-15.13-16.25-24.37-37.92-26-61-1.74-24.62 5.77-47.26 21.14-63.76S312 80 336 80c23.83 0 45.38 9.06 60.7 25.52 15.47 16.62 23 39.22 21.26 63.63-1.67 23.11-10.9 44.77-26 61C376.44 246.82 356.57 256 336 256zm66-88c0-51.18-42.82-92-94-92s-94 40.82-94 92 42.82 92 94 92 94-40.82 94-92z" opacity=".3"/>
+            <path fill="currentColor" d="M336 256c-20.56 0-40.44-9.18-56-25.84-15.13-16.25-24.37-37.92-26-61-1.74-24.62 5.77-47.26 21.14-63.76S312 80 336 80c23.83 0 45.38 9.06 60.7 25.52 15.47 16.62 23 39.22 21.26 63.63-1.67 23.11-10.9 44.77-26 61C376.44 246.82 356.57 256 336 256zM467.83 432H204.18a27.71 27.71 0 01-22-10.67 30.22 30.22 0 01-5.26-25.79c8.42-33.81 29.28-61.85 60.32-81.08C264.79 297.4 299.86 288 336 288c36.85 0 71 9.23 98.83 26.73 31.45 19.86 52.3 48 60.38 81.55a30.27 30.27 0 01-5.32 25.78A27.68 27.68 0 01467.83 432z"/>
+          } @else {
+            <!-- 常规错误图标 -->
+            <path fill="currentColor" d="M256 48C141.31 48 48 141.31 48 256s93.31 208 208 208 208-93.31 208-208S370.69 48 256 48zm0 319.91a20 20 0 1120-20 20 20 0 01-20 20zm21.72-201.15l-5.74 122a16 16 0 01-32 0l-5.74-121.94v-.05a21.74 21.74 0 1143.44 0z"/>
+          }
+        </svg>
+      </div>
+      <h2 class="error-title">{{ isCustomerOnly ? '仅限客户填写' : '加载失败' }}</h2>
+      <p class="error-message">{{ error }}</p>
+      @if (isCustomerOnly) {
+        <div class="info-box">
+          <svg class="icon" viewBox="0 0 512 512">
+            <path fill="currentColor" d="M256 56C145.72 56 56 145.72 56 256s89.72 200 200 200 200-89.72 200-200S366.28 56 256 56zm0 82a26 26 0 11-26 26 26 26 0 0126-26zm48 226h-88a16 16 0 010-32h28v-88h-16a16 16 0 010-32h32a16 16 0 0116 16v104h28a16 16 0 010 32z"/>
+          </svg>
+          <div class="info-text">
+            <p class="info-title">客户填写入口</p>
+            <p class="info-desc">请通过企微群聊中收到的问卷链接进入</p>
+          </div>
+        </div>
+      }
+      <button class="btn-primary" (click)="goBack()">返回</button>
     </div>
   }
 
   <!-- 欢迎页 -->
   @if (currentState === 'welcome' && !loading && !error) {
-    <div class="welcome-page">
-      <div class="welcome-header">
-        @if (currentContact) {
-          <div class="user-avatar">
-            <img [src]="currentContact.get('data')?.avatar || 'assets/default-avatar.png'" alt="头像" />
-          </div>
-          <h2>您好,{{ currentContact.get('realname') || currentContact.get('name') }}</h2>
-        }
+    <div class="welcome-view">
+      <!-- 用户信息卡片 -->
+      <div class="user-card">
+        <div class="user-avatar">
+          <img [src]="currentContact?.get('data')?.avatar || 'assets/default-avatar.png'" alt="头像" />
+        </div>
+        <h2 class="user-greeting">您好, {{ currentContact?.get('realname') || currentContact?.get('name') }}</h2>
+        <p class="user-subtitle">欢迎参与需求调查</p>
       </div>
 
-      <div class="welcome-content">
-        <h1>《家装效果图服务初次合作需求调查表》</h1>
+      <!-- 问卷介绍 -->
+      <div class="intro-card">
+        <div class="intro-header">
+          <svg class="icon" viewBox="0 0 512 512">
+            <path fill="currentColor" d="M336 64h32a48 48 0 0148 48v320a48 48 0 01-48 48H144a48 48 0 01-48-48V112a48 48 0 0148-48h32" opacity=".3"/>
+            <path fill="currentColor" d="M336 64h-80a48 48 0 00-96 0h-80a48 48 0 00-48 48v320a48 48 0 0048 48h224a48 48 0 0048-48V112a48 48 0 00-48-48zM256 32a16 16 0 11-16 16 16 16 0 0116-16zm112 400H144V112h224z"/>
+          </svg>
+          <h3>家装效果图服务需求调查</h3>
+        </div>
 
-        <div class="welcome-intro">
-          <p>尊敬的伙伴:</p>
-          <p>为让本次效果图服务更贴合您的工作节奏与核心需求,我们准备了简短选择式问卷,您的偏好将直接帮我们校准服务方向,感谢支持!</p>
+        <div class="intro-body">
+          <p class="intro-text">
+            尊敬的伙伴,为让本次效果图服务更贴合您的工作节奏与核心需求,我们准备了简短选择式问卷。
+          </p>
+          <p class="intro-text">
+            您的偏好将直接帮我们校准服务方向,感谢支持!
+          </p>
         </div>
 
-        <div class="survey-info">
-          <div class="info-item">
-            <ion-icon name="time-outline"></ion-icon>
-            <span>预计用时: 3-5分钟</span>
+        <!-- 问卷信息标签 -->
+        <div class="info-tags">
+          <div class="info-tag">
+            <svg class="icon" viewBox="0 0 512 512">
+              <path fill="currentColor" d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm61.8-104.4l-84.9-61.7c-3.1-2.3-4.9-5.9-4.9-9.7V116c0-6.6 5.4-12 12-12h32c6.6 0 12 5.4 12 12v141.7l66.8 48.6c5.4 3.9 6.5 11.4 2.6 16.8L334.6 349c-3.9 5.3-11.4 6.5-16.8 2.6z"/>
+            </svg>
+            <span>3-5分钟</span>
           </div>
-          <div class="info-item">
-            <ion-icon name="list-outline"></ion-icon>
-            <span>题目数量: {{ effectiveQuestions.length }}题</span>
+          <div class="info-tag">
+            <svg class="icon" viewBox="0 0 512 512">
+              <path fill="currentColor" d="M144 144v296a8 8 0 008 8h56V144zm144 0v304h56a8 8 0 008-8V144zm144 0v272a24 24 0 01-24 24h-40V144zM64 144v328a24 24 0 0024 24h40V144z" opacity=".3"/>
+            </svg>
+            <span>{{ effectiveQuestions.length }}道题</span>
           </div>
-          <div class="info-item">
-            <ion-icon name="checkmark-circle-outline"></ion-icon>
-            <span>题型: 选择题为主</span>
+          <div class="info-tag">
+            <svg class="icon" viewBox="0 0 512 512">
+              <path fill="currentColor" d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"/>
+            </svg>
+            <span>选择为主</span>
           </div>
         </div>
-
-        <ion-button expand="block" size="large" (click)="startSurvey()" class="start-button">
-          开始填写
-        </ion-button>
       </div>
+
+      <!-- 开始按钮 -->
+      <button class="btn-start" (click)="startSurvey()">
+        <span>开始填写</span>
+        <svg class="icon" viewBox="0 0 512 512">
+          <path fill="currentColor" d="M294.1 256L167 129c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.3 34 0L345 239c9.1 9.1 9.3 23.7.7 33.1L201.1 417c-4.7 4.7-10.9 7-17 7s-12.3-2.3-17-7c-9.4-9.4-9.4-24.6 0-33.9l127-127.1z"/>
+        </svg>
+      </button>
     </div>
   }
 
   <!-- 答题页 -->
   @if (currentState === 'questionnaire' && !loading && !error) {
-    <div class="questionnaire-page">
-      <!-- 进度条 -->
-      <div class="progress-bar">
-        <div class="progress-fill" [style.width.%]="getProgress()"></div>
-      </div>
-      <div class="progress-text">
-        {{ currentQuestionIndex + 1 }} / {{ effectiveQuestions.length }}
+    <div class="questionnaire-view">
+      <!-- 进度指示器 -->
+      <div class="progress-section">
+        <div class="progress-bar-wrapper">
+          <div class="progress-bar">
+            <div class="progress-fill" [style.width.%]="getProgress()"></div>
+          </div>
+        </div>
+        <div class="progress-info">
+          <span class="progress-current">{{ currentQuestionIndex + 1 }}</span>
+          <span class="progress-separator">/</span>
+          <span class="progress-total">{{ effectiveQuestions.length }}</span>
+        </div>
       </div>
 
       @if (getCurrentQuestion(); as question) {
-        <div class="question-container">
-          <!-- 章节标题 -->
-          <div class="section-title">{{ question.section }}</div>
-
-          <!-- 题目 -->
-          <div class="question-title">
-            <span class="question-number">{{ currentQuestionIndex + 1 }}.</span>
-            {{ question.title }}
-            @if (question.required) {
-              <span class="required-mark">*</span>
-            }
-          </div>
-
-          <!-- 单选题 -->
-          @if (question.type === 'single') {
-            <div class="options-container">
-              @for (option of question.options; track option) {
-                <div
-                  class="option-item"
-                  [class.selected]="answers[question.id] === option"
-                  (click)="selectSingleOption(option)"
-                >
-                  <div class="option-radio">
-                    @if (answers[question.id] === option) {
-                      <ion-icon name="radio-button-on"></ion-icon>
-                    } @else {
-                      <ion-icon name="radio-button-off"></ion-icon>
-                    }
-                  </div>
-                  <div class="option-text">{{ option }}</div>
-                </div>
+        <!-- 题目卡片 -->
+        <div class="question-card">
+          <!-- 章节标签 -->
+          <div class="section-badge">{{ question.section }}</div>
+
+          <!-- 题目内容 -->
+          <div class="question-content">
+            <h3 class="question-title">
+              <span class="question-number">{{ currentQuestionIndex + 1 }}.</span>
+              <span class="question-text">{{ question.title }}</span>
+              @if (question.required) {
+                <span class="required-star">*</span>
               }
-
-              @if (question.hasOther) {
-                <div
-                  class="option-item"
-                  [class.selected]="answers[question.id]?.startsWith('其他')"
-                  (click)="selectSingleOption('其他')"
-                >
-                  <div class="option-radio">
-                    @if (answers[question.id]?.startsWith('其他')) {
-                      <ion-icon name="radio-button-on"></ion-icon>
-                    } @else {
-                      <ion-icon name="radio-button-off"></ion-icon>
-                    }
+            </h3>
+
+            <!-- 单选题 -->
+            @if (question.type === 'single') {
+              <div class="options-list">
+                @for (option of question.options; track option) {
+                  <div
+                    class="option-item"
+                    [class.selected]="answers[question.id] === option"
+                    (click)="selectSingleOption(option)">
+                    <div class="option-radio">
+                      <div class="radio-outer">
+                        <div class="radio-inner"></div>
+                      </div>
+                    </div>
+                    <span class="option-label">{{ option }}</span>
                   </div>
-                  <div class="option-text">其他</div>
-                </div>
-              }
-            </div>
-
-            @if (showOtherInput) {
-              <div class="other-input-container">
-                <ion-input
-                  [(ngModel)]="otherInput"
-                  placeholder="请输入其他内容..."
-                  class="other-input"
-                ></ion-input>
+                }
+
+                @if (question.hasOther) {
+                  <div
+                    class="option-item"
+                    [class.selected]="answers[question.id]?.startsWith('其他')"
+                    (click)="selectSingleOption('其他')">
+                    <div class="option-radio">
+                      <div class="radio-outer">
+                        <div class="radio-inner"></div>
+                      </div>
+                    </div>
+                    <span class="option-label">其他</span>
+                  </div>
+                }
               </div>
-            }
-          }
 
-          <!-- 多选题 -->
-          @if (question.type === 'multiple') {
-            <div class="options-container">
-              @for (option of question.options; track option) {
-                <div
-                  class="option-item"
-                  [class.selected]="hasMultipleOption(question.id, option)"
-                  (click)="toggleMultipleOption(option)"
-                >
-                  <div class="option-checkbox">
-                    @if (hasMultipleOption(question.id, option)) {
-                      <ion-icon name="checkbox"></ion-icon>
-                    } @else {
-                      <ion-icon name="square-outline"></ion-icon>
-                    }
-                  </div>
-                  <div class="option-text">{{ option }}</div>
+              @if (showOtherInput) {
+                <div class="input-wrapper">
+                  <input
+                    type="text"
+                    class="text-input"
+                    [(ngModel)]="otherInput"
+                    placeholder="请输入其他内容..."
+                    autofocus />
                 </div>
               }
+            }
 
-              @if (question.hasOther) {
-                <div
-                  class="option-item"
-                  [class.selected]="hasMultipleOptionStartsWith(question.id, '其他')"
-                  (click)="toggleMultipleOption('其他')"
-                >
-                  <div class="option-checkbox">
-                    @if (hasMultipleOptionStartsWith(question.id, '其他')) {
-                      <ion-icon name="checkbox"></ion-icon>
-                    } @else {
-                      <ion-icon name="square-outline"></ion-icon>
-                    }
+            <!-- 多选题 -->
+            @if (question.type === 'multiple') {
+              <div class="options-list">
+                @for (option of question.options; track option) {
+                  <div
+                    class="option-item"
+                    [class.selected]="hasMultipleOption(question.id, option)"
+                    (click)="toggleMultipleOption(option)">
+                    <div class="option-checkbox">
+                      <div class="checkbox-box">
+                        <svg class="icon checkmark" viewBox="0 0 512 512">
+                          <path fill="currentColor" d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"/>
+                        </svg>
+                      </div>
+                    </div>
+                    <span class="option-label">{{ option }}</span>
+                  </div>
+                }
+
+                @if (question.hasOther) {
+                  <div
+                    class="option-item"
+                    [class.selected]="hasMultipleOptionStartsWith(question.id, '其他')"
+                    (click)="toggleMultipleOption('其他')">
+                    <div class="option-checkbox">
+                      <div class="checkbox-box">
+                        <svg class="icon checkmark" viewBox="0 0 512 512">
+                          <path fill="currentColor" d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"/>
+                        </svg>
+                      </div>
+                    </div>
+                    <span class="option-label">其他</span>
                   </div>
-                  <div class="option-text">其他</div>
+                }
+              </div>
+
+              @if (showOtherInput) {
+                <div class="input-wrapper">
+                  <input
+                    type="text"
+                    class="text-input"
+                    [(ngModel)]="otherInput"
+                    placeholder="请输入其他内容..." />
                 </div>
               }
-            </div>
+            }
 
-            @if (showOtherInput) {
-              <div class="other-input-container">
-                <ion-input
-                  [(ngModel)]="otherInput"
-                  placeholder="请输入其他内容..."
-                  class="other-input"
-                ></ion-input>
+            <!-- 文本题 -->
+            @if (question.type === 'text') {
+              <div class="input-wrapper">
+                <textarea
+                  class="textarea-input"
+                  [(ngModel)]="answers[question.id]"
+                  [placeholder]="question.placeholder || '请输入...'"
+                  rows="4"></textarea>
               </div>
             }
-          }
 
-          <!-- 文本题 -->
-          @if (question.type === 'text') {
-            <div class="text-input-container">
-              <ion-textarea
-                [(ngModel)]="answers[question.id]"
-                [placeholder]="question.placeholder || '请输入...'"
-                rows="4"
-                class="text-input"
-              ></ion-textarea>
-            </div>
-          }
-
-          <!-- 数字题 -->
-          @if (question.type === 'number') {
-            <div class="number-input-container">
-              <ion-input
-                type="number"
-                [(ngModel)]="answers[question.id]"
-                [placeholder]="question.placeholder || '请输入数字...'"
-                class="number-input"
-              ></ion-input>
-            </div>
-          }
+            <!-- 数字题 -->
+            @if (question.type === 'number') {
+              <div class="input-wrapper">
+                <input
+                  type="number"
+                  class="text-input"
+                  [(ngModel)]="answers[question.id]"
+                  [placeholder]="question.placeholder || '请输入数字...'" />
+              </div>
+            }
+          </div>
         </div>
 
         <!-- 导航按钮 -->
         <div class="nav-buttons">
-          <ion-button
-            fill="outline"
-            (click)="previousQuestion()"
+          <button
+            class="btn-nav btn-prev"
             [disabled]="currentQuestionIndex === 0"
-          >
-            <ion-icon name="chevron-back" slot="start"></ion-icon>
-            上一题
-          </ion-button>
-
-          <ion-button
-            (click)="nextQuestion()"
-          >
-            @if (currentQuestionIndex >= effectiveQuestions.length - 1) {
-              提交
-            } @else {
-              下一题
-            }
-            <ion-icon name="chevron-forward" slot="end"></ion-icon>
-          </ion-button>
+            (click)="previousQuestion()">
+            <svg class="icon" viewBox="0 0 512 512">
+              <path fill="currentColor" d="M217.9 256L345 129c9.4-9.4 9.4-24.6 0-33.9-9.4-9.4-24.6-9.3-34 0L167 239c-9.1 9.1-9.3 23.7-.7 33.1L310.9 417c4.7 4.7 10.9 7 17 7s12.3-2.3 17-7c9.4-9.4 9.4-24.6 0-33.9L217.9 256z"/>
+            </svg>
+            <span>上一题</span>
+          </button>
+
+          <button
+            class="btn-nav btn-next"
+            (click)="nextQuestion()">
+            <span>{{ currentQuestionIndex >= effectiveQuestions.length - 1 ? '提交' : '下一题' }}</span>
+            <svg class="icon" viewBox="0 0 512 512">
+              <path fill="currentColor" d="M294.1 256L167 129c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.3 34 0L345 239c9.1 9.1 9.3 23.7.7 33.1L201.1 417c-4.7 4.7-10.9 7-17 7s-12.3-2.3-17-7c-9.4-9.4-9.4-24.6 0-33.9l127-127.1z"/>
+            </svg>
+          </button>
         </div>
       }
     </div>
@@ -245,81 +294,89 @@
 
   <!-- 结果页 -->
   @if (currentState === 'result' && !loading && !error) {
-    <div class="result-page">
-      <div class="result-header">
-        <ion-icon name="checkmark-circle" color="success"></ion-icon>
-        <h2>问卷提交成功</h2>
-        <p>感谢您的反馈!</p>
-        <p>我们将根据您的选择制定服务方案</p>
+    <div class="result-view">
+      <!-- 成功图标 -->
+      <div class="success-icon-wrapper">
+        <svg class="icon success-icon" viewBox="0 0 512 512">
+          <path fill="currentColor" d="M256 48C141.31 48 48 141.31 48 256s93.31 208 208 208 208-93.31 208-208S370.69 48 256 48zm108.25 138.29l-134.4 160a16 16 0 01-12 5.71h-.27a16 16 0 01-11.89-5.3l-57.6-64a16 16 0 1123.78-21.4l45.29 50.32 122.59-145.91a16 16 0 0124.5 20.58z"/>
+        </svg>
       </div>
 
-      <div class="result-content">
-        <h3>【您的答卷】</h3>
+      <h2 class="result-title">问卷提交成功</h2>
+      <p class="result-subtitle">感谢您的反馈!</p>
+      <p class="result-desc">我们将根据您的选择制定服务方案</p>
 
-        <div class="result-item">
-          <div class="result-label">核心服务:</div>
-          <div class="result-value">{{ getFormattedAnswer('q1') }}</div>
-        </div>
+      <!-- 答卷内容 -->
+      <div class="result-card">
+        <h3 class="result-card-title">您的答卷</h3>
 
-        <div class="result-item">
-          <div class="result-label">空间数量:</div>
-          <div class="result-value">{{ getFormattedAnswer('q2') }}</div>
-        </div>
-
-        <div class="result-item">
-          <div class="result-label">价值侧重:</div>
-          <div class="result-value">{{ getFormattedAnswer('q3') }}</div>
-        </div>
-
-        <div class="result-item">
-          <div class="result-label">技术配合:</div>
-          <div class="result-value">{{ getFormattedAnswer('q4') }}</div>
-        </div>
+        <div class="result-list">
+          <div class="result-item">
+            <div class="result-label">核心服务</div>
+            <div class="result-value">{{ getFormattedAnswer('q1') }}</div>
+          </div>
 
-        <div class="result-item">
-          <div class="result-label">协作方式:</div>
-          <div class="result-value">{{ getFormattedAnswer('q5') }}</div>
-        </div>
+          <div class="result-item">
+            <div class="result-label">空间数量</div>
+            <div class="result-value">{{ getFormattedAnswer('q2') }}</div>
+          </div>
 
-        @if (answers['q6']) {
           <div class="result-item">
-            <div class="result-label">注意事项:</div>
-            <div class="result-value">{{ getFormattedAnswer('q6') }}</div>
+            <div class="result-label">价值侧重</div>
+            <div class="result-value">{{ getFormattedAnswer('q3') }}</div>
           </div>
-        }
 
-        @if (answers['q7']) {
           <div class="result-item">
-            <div class="result-label">特殊要求:</div>
-            <div class="result-value">{{ getFormattedAnswer('q7') }}</div>
+            <div class="result-label">技术配合</div>
+            <div class="result-value">{{ getFormattedAnswer('q4') }}</div>
           </div>
-        }
 
-        @if (answers['q8']) {
           <div class="result-item">
-            <div class="result-label">参考素材:</div>
-            <div class="result-value">{{ getFormattedAnswer('q8') }}</div>
+            <div class="result-label">协作方式</div>
+            <div class="result-value">{{ getFormattedAnswer('q5') }}</div>
           </div>
-        }
 
-        <div class="result-divider"></div>
+          @if (answers['q6']) {
+            <div class="result-item">
+              <div class="result-label">注意事项</div>
+              <div class="result-value">{{ getFormattedAnswer('q6') }}</div>
+            </div>
+          }
+
+          @if (answers['q7']) {
+            <div class="result-item">
+              <div class="result-label">特殊要求</div>
+              <div class="result-value">{{ getFormattedAnswer('q7') }}</div>
+            </div>
+          }
 
-        <div class="result-item">
-          <div class="result-label">对接人:</div>
-          <div class="result-value">{{ answers['contact_name'] || currentContact?.get('realname') || '-' }}</div>
+          @if (answers['q8']) {
+            <div class="result-item">
+              <div class="result-label">参考素材</div>
+              <div class="result-value">{{ getFormattedAnswer('q8') }}</div>
+            </div>
+          }
         </div>
 
-        <div class="result-item">
-          <div class="result-label">电话:</div>
-          <div class="result-value">
-            {{ maskPhone(answers['contact_phone'] || currentContact?.get('mobile') || '') }}
+        <div class="result-divider"></div>
+
+        <div class="result-list">
+          <div class="result-item">
+            <div class="result-label">对接人</div>
+            <div class="result-value">{{ answers['contact_name'] || currentContact?.get('realname') || '-' }}</div>
+          </div>
+
+          <div class="result-item">
+            <div class="result-label">电话</div>
+            <div class="result-value">{{ maskPhone(answers['contact_phone'] || currentContact?.get('mobile') || '') }}</div>
           </div>
         </div>
       </div>
 
-      <ion-button expand="block" size="large" (click)="goBack()" class="back-button">
-        返回项目
-      </ion-button>
+      <!-- 返回按钮 -->
+      <button class="btn-primary" (click)="goBack()">
+        <span>返回项目</span>
+      </button>
     </div>
   }
-</ion-content>
+</div>

+ 749 - 231
src/modules/project/pages/project-survey/project-survey.component.scss

@@ -1,48 +1,222 @@
-.survey-content {
-  --background: #f5f5f5;
+// 项目问卷组件 - 移动端优先设计
+
+// CSS 变量
+:host {
+  --primary-color: #3880ff;
+  --success-color: #2dd36f;
+  --warning-color: #ffc409;
+  --danger-color: #eb445a;
+  --dark-color: #222428;
+  --medium-color: #92949c;
+  --light-color: #f4f5f8;
+  --white: #ffffff;
+  --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.08);
+  --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.12);
+  --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.16);
+  --radius-sm: 8px;
+  --radius-md: 12px;
+  --radius-lg: 16px;
+  --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
 }
 
-// 加载和错误状态
-.loading-container,
-.error-container {
+// 重置样式
+* {
+  box-sizing: border-box;
+  -webkit-tap-highlight-color: transparent;
+}
+
+// 顶部导航栏
+.survey-header {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  z-index: 1000;
+  background: var(--white);
+  box-shadow: var(--shadow-sm);
+
+  .header-content {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    height: 56px;
+    padding: 0 16px;
+    max-width: 640px;
+    margin: 0 auto;
+  }
+
+  .back-button {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 40px;
+    height: 40px;
+    border: none;
+    background: transparent;
+    color: var(--dark-color);
+    cursor: pointer;
+    border-radius: var(--radius-sm);
+    transition: var(--transition);
+
+    &:active {
+      background: var(--light-color);
+      transform: scale(0.95);
+    }
+
+    .icon {
+      width: 24px;
+      height: 24px;
+    }
+  }
+
+  .header-title {
+    flex: 1;
+    margin: 0 16px;
+    font-size: 18px;
+    font-weight: 600;
+    color: var(--dark-color);
+    text-align: center;
+  }
+
+  .header-spacer {
+    width: 40px;
+  }
+}
+
+// 主容器
+.survey-container {
+  min-height: 100vh;
+  padding-top: 56px;
+  background: linear-gradient(180deg, #f8f9fa 0%, #ffffff 100%);
+}
+
+// 状态视图(加载/错误)
+.status-view {
   display: flex;
   flex-direction: column;
   align-items: center;
   justify-content: center;
-  min-height: 60vh;
-  padding: 2rem;
+  min-height: calc(100vh - 56px);
+  padding: 32px 24px;
   text-align: center;
 
-  ion-icon {
-    font-size: 4rem;
-    color: var(--ion-color-medium);
-    margin-bottom: 1rem;
+  .status-text {
+    margin: 16px 0 0;
+    font-size: 16px;
+    color: var(--medium-color);
+  }
+}
+
+// 加载动画
+.spinner {
+  width: 48px;
+  height: 48px;
+  position: relative;
+
+  .spinner-circle {
+    width: 100%;
+    height: 100%;
+    border: 4px solid var(--light-color);
+    border-top-color: var(--primary-color);
+    border-radius: 50%;
+    animation: spin 0.8s linear infinite;
+  }
+}
+
+@keyframes spin {
+  to { transform: rotate(360deg); }
+}
+
+// 错误视图
+.error-view {
+  .error-icon-wrapper {
+    width: 80px;
+    height: 80px;
+    margin-bottom: 24px;
+    border-radius: 50%;
+    background: rgba(235, 68, 90, 0.1);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    .error-icon {
+      width: 48px;
+      height: 48px;
+      color: var(--danger-color);
+    }
   }
 
-  p {
-    font-size: 1rem;
-    color: var(--ion-color-medium);
-    margin: 1rem 0;
+  .error-title {
+    margin: 0 0 12px;
+    font-size: 20px;
+    font-weight: 600;
+    color: var(--dark-color);
+  }
+
+  .error-message {
+    margin: 0 0 24px;
+    font-size: 15px;
+    color: var(--medium-color);
+    line-height: 1.5;
+  }
+
+  .info-box {
+    display: flex;
+    align-items: flex-start;
+    gap: 12px;
+    padding: 16px;
+    margin-bottom: 24px;
+    background: rgba(255, 196, 9, 0.1);
+    border-radius: var(--radius-md);
+    border-left: 4px solid var(--warning-color);
+    text-align: left;
+
+    .icon {
+      width: 24px;
+      height: 24px;
+      color: var(--warning-color);
+      flex-shrink: 0;
+      margin-top: 2px;
+    }
+
+    .info-text {
+      flex: 1;
+
+      .info-title {
+        margin: 0 0 4px;
+        font-size: 14px;
+        font-weight: 600;
+        color: var(--dark-color);
+      }
+
+      .info-desc {
+        margin: 0;
+        font-size: 13px;
+        color: var(--medium-color);
+        line-height: 1.4;
+      }
+    }
   }
 }
 
 // 欢迎页
-.welcome-page {
-  padding: 2rem 1.5rem;
-  max-width: 600px;
+.welcome-view {
+  max-width: 640px;
   margin: 0 auto;
+  padding: 24px 16px 32px;
 
-  .welcome-header {
+  .user-card {
     text-align: center;
-    margin-bottom: 2rem;
+    margin-bottom: 24px;
+    animation: fadeInUp 0.6s ease;
 
     .user-avatar {
       width: 80px;
       height: 80px;
-      margin: 0 auto 1rem;
+      margin: 0 auto 16px;
       border-radius: 50%;
       overflow: hidden;
-      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+      box-shadow: var(--shadow-md);
 
       img {
         width: 100%;
@@ -51,37 +225,58 @@
       }
     }
 
-    h2 {
-      font-size: 1.5rem;
+    .user-greeting {
+      margin: 0 0 8px;
+      font-size: 24px;
       font-weight: 600;
-      color: var(--ion-color-dark);
+      color: var(--dark-color);
+    }
+
+    .user-subtitle {
       margin: 0;
+      font-size: 15px;
+      color: var(--medium-color);
     }
   }
 
-  .welcome-content {
-    background: white;
-    border-radius: 12px;
-    padding: 2rem;
-    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+  .intro-card {
+    background: var(--white);
+    border-radius: var(--radius-lg);
+    padding: 24px;
+    margin-bottom: 24px;
+    box-shadow: var(--shadow-sm);
+    animation: fadeInUp 0.6s ease 0.1s both;
 
-    h1 {
-      font-size: 1.25rem;
-      font-weight: 600;
-      color: var(--ion-color-primary);
-      margin: 0 0 1.5rem;
-      text-align: center;
-      line-height: 1.6;
+    .intro-header {
+      display: flex;
+      align-items: flex-start;
+      gap: 12px;
+      margin-bottom: 20px;
+
+      .icon {
+        width: 28px;
+        height: 28px;
+        color: var(--primary-color);
+        flex-shrink: 0;
+      }
+
+      h3 {
+        margin: 0;
+        font-size: 18px;
+        font-weight: 600;
+        color: var(--dark-color);
+        line-height: 1.4;
+      }
     }
 
-    .welcome-intro {
-      margin-bottom: 2rem;
-      line-height: 1.8;
+    .intro-body {
+      margin-bottom: 20px;
 
-      p {
-        margin: 0 0 1rem;
-        color: var(--ion-color-dark);
-        font-size: 0.95rem;
+      .intro-text {
+        margin: 0 0 12px;
+        font-size: 15px;
+        color: var(--dark-color);
+        line-height: 1.6;
 
         &:last-child {
           margin-bottom: 0;
@@ -89,279 +284,602 @@
       }
     }
 
-    .survey-info {
-      background: #f9f9f9;
-      border-radius: 8px;
-      padding: 1.5rem;
-      margin-bottom: 2rem;
+    .info-tags {
+      display: flex;
+      justify-content: space-around;
+      gap: 12px;
+      padding-top: 20px;
+      border-top: 1px solid var(--light-color);
 
-      .info-item {
+      .info-tag {
         display: flex;
+        flex-direction: column;
         align-items: center;
-        margin-bottom: 0.75rem;
-
-        &:last-child {
-          margin-bottom: 0;
-        }
+        gap: 8px;
+        flex: 1;
 
-        ion-icon {
-          font-size: 1.25rem;
-          color: var(--ion-color-primary);
-          margin-right: 0.75rem;
+        .icon {
+          width: 24px;
+          height: 24px;
+          color: var(--primary-color);
+          opacity: 0.8;
         }
 
         span {
-          font-size: 0.95rem;
-          color: var(--ion-color-dark);
+          font-size: 13px;
+          font-weight: 500;
+          color: var(--medium-color);
         }
       }
     }
+  }
+
+  .btn-start {
+    width: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 8px;
+    height: 56px;
+    background: var(--primary-color);
+    color: var(--white);
+    border: none;
+    border-radius: var(--radius-md);
+    font-size: 16px;
+    font-weight: 600;
+    box-shadow: var(--shadow-md);
+    cursor: pointer;
+    transition: var(--transition);
+    animation: fadeInUp 0.6s ease 0.2s both;
+
+    &:active {
+      transform: translateY(2px);
+      box-shadow: var(--shadow-sm);
+    }
 
-    .start-button {
-      margin-top: 1.5rem;
-      --border-radius: 8px;
-      font-weight: 600;
+    .icon {
+      width: 20px;
+      height: 20px;
     }
   }
 }
 
 // 答题页
-.questionnaire-page {
-  padding: 1.5rem;
-  max-width: 600px;
+.questionnaire-view {
+  max-width: 640px;
   margin: 0 auto;
+  padding: 24px 16px 32px;
 
-  .progress-bar {
-    height: 4px;
-    background: #e0e0e0;
-    border-radius: 2px;
-    overflow: hidden;
-    margin-bottom: 0.5rem;
-
-    .progress-fill {
-      height: 100%;
-      background: var(--ion-color-primary);
-      transition: width 0.3s ease;
-    }
-  }
+  .progress-section {
+    margin-bottom: 24px;
 
-  .progress-text {
-    text-align: center;
-    font-size: 0.875rem;
-    color: var(--ion-color-medium);
-    margin-bottom: 1.5rem;
-  }
+    .progress-bar-wrapper {
+      margin-bottom: 12px;
 
-  .question-container {
-    background: white;
-    border-radius: 12px;
-    padding: 2rem;
-    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
-    margin-bottom: 1.5rem;
+      .progress-bar {
+        height: 6px;
+        background: var(--light-color);
+        border-radius: 3px;
+        overflow: hidden;
 
-    .section-title {
-      font-size: 0.875rem;
-      color: var(--ion-color-primary);
-      font-weight: 600;
-      margin-bottom: 1rem;
-      text-transform: uppercase;
-      letter-spacing: 0.5px;
+        .progress-fill {
+          height: 100%;
+          background: linear-gradient(90deg, var(--primary-color) 0%, #5a9cff 100%);
+          border-radius: 3px;
+          transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+        }
+      }
     }
 
-    .question-title {
-      font-size: 1.125rem;
-      font-weight: 600;
-      color: var(--ion-color-dark);
-      margin-bottom: 1.5rem;
-      line-height: 1.6;
+    .progress-info {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      gap: 4px;
+      font-size: 14px;
 
-      .question-number {
-        color: var(--ion-color-primary);
-        margin-right: 0.5rem;
+      .progress-current {
+        font-size: 18px;
+        font-weight: 600;
+        color: var(--primary-color);
       }
 
-      .required-mark {
-        color: var(--ion-color-danger);
-        margin-left: 0.25rem;
+      .progress-separator {
+        color: var(--medium-color);
+      }
+
+      .progress-total {
+        color: var(--medium-color);
       }
     }
+  }
 
-    .options-container {
-      .option-item {
-        display: flex;
-        align-items: center;
-        padding: 1rem;
-        margin-bottom: 0.75rem;
-        border: 2px solid #e0e0e0;
-        border-radius: 8px;
-        cursor: pointer;
-        transition: all 0.2s ease;
+  .question-card {
+    background: var(--white);
+    border-radius: var(--radius-lg);
+    padding: 24px;
+    margin-bottom: 20px;
+    box-shadow: var(--shadow-sm);
+    animation: slideInRight 0.4s ease;
+
+    .section-badge {
+      display: inline-block;
+      padding: 4px 12px;
+      margin-bottom: 16px;
+      background: rgba(56, 128, 255, 0.1);
+      color: var(--primary-color);
+      border-radius: 12px;
+      font-size: 12px;
+      font-weight: 600;
+      text-transform: uppercase;
+      letter-spacing: 0.5px;
+    }
 
-        &:last-child {
-          margin-bottom: 0;
-        }
+    .question-content {
+      .question-title {
+        margin: 0 0 20px;
+        font-size: 17px;
+        font-weight: 600;
+        color: var(--dark-color);
+        line-height: 1.5;
 
-        &:hover {
-          border-color: var(--ion-color-primary-tint);
-          background: #f9f9f9;
+        .question-number {
+          color: var(--primary-color);
+          margin-right: 8px;
         }
 
-        &.selected {
-          border-color: var(--ion-color-primary);
-          background: var(--ion-color-primary-tint);
+        .question-text {
+          display: inline;
+        }
 
-          .option-text {
-            color: var(--ion-color-primary);
-            font-weight: 600;
-          }
+        .required-star {
+          color: var(--danger-color);
+          margin-left: 4px;
         }
+      }
+
+      .options-list {
+        display: flex;
+        flex-direction: column;
+        gap: 12px;
 
-        .option-radio,
-        .option-checkbox {
-          font-size: 1.5rem;
-          margin-right: 0.75rem;
+        .option-item {
           display: flex;
           align-items: center;
+          gap: 12px;
+          padding: 16px;
+          background: var(--white);
+          border: 2px solid var(--light-color);
+          border-radius: var(--radius-md);
+          cursor: pointer;
+          transition: var(--transition);
+
+          &:active {
+            transform: scale(0.98);
+          }
 
-          ion-icon {
-            color: var(--ion-color-primary);
+          &.selected {
+            border-color: var(--primary-color);
+            background: rgba(56, 128, 255, 0.05);
+
+            .option-radio .radio-outer {
+              border-color: var(--primary-color);
+
+              .radio-inner {
+                transform: scale(1);
+                background: var(--primary-color);
+              }
+            }
+
+            .option-checkbox .checkbox-box {
+              border-color: var(--primary-color);
+              background: var(--primary-color);
+
+              .checkmark {
+                opacity: 1;
+                transform: scale(1);
+              }
+            }
+
+            .option-label {
+              color: var(--primary-color);
+              font-weight: 600;
+            }
           }
-        }
 
-        .option-text {
-          flex: 1;
-          font-size: 1rem;
-          color: var(--ion-color-dark);
+          .option-radio {
+            flex-shrink: 0;
+
+            .radio-outer {
+              width: 24px;
+              height: 24px;
+              border: 2px solid var(--medium-color);
+              border-radius: 50%;
+              display: flex;
+              align-items: center;
+              justify-content: center;
+              transition: var(--transition);
+
+              .radio-inner {
+                width: 12px;
+                height: 12px;
+                border-radius: 50%;
+                transform: scale(0);
+                transition: var(--transition);
+              }
+            }
+          }
+
+          .option-checkbox {
+            flex-shrink: 0;
+
+            .checkbox-box {
+              width: 24px;
+              height: 24px;
+              border: 2px solid var(--medium-color);
+              border-radius: 6px;
+              display: flex;
+              align-items: center;
+              justify-content: center;
+              transition: var(--transition);
+
+              .checkmark {
+                width: 14px;
+                height: 14px;
+                color: var(--white);
+                opacity: 0;
+                transform: scale(0);
+                transition: var(--transition);
+              }
+            }
+          }
+
+          .option-label {
+            flex: 1;
+            font-size: 15px;
+            color: var(--dark-color);
+            line-height: 1.4;
+            transition: var(--transition);
+          }
         }
       }
-    }
 
-    .other-input-container,
-    .text-input-container,
-    .number-input-container {
-      margin-top: 1rem;
-
-      .other-input,
-      .text-input,
-      .number-input {
-        --background: #f9f9f9;
-        --padding-start: 1rem;
-        --padding-end: 1rem;
-        border-radius: 8px;
-        border: 2px solid #e0e0e0;
+      .input-wrapper {
+        margin-top: 12px;
+
+        .text-input,
+        .textarea-input {
+          width: 100%;
+          padding: 14px 16px;
+          border: 2px solid var(--light-color);
+          border-radius: var(--radius-md);
+          font-size: 15px;
+          color: var(--dark-color);
+          background: var(--white);
+          font-family: inherit;
+          transition: var(--transition);
+
+          &:focus {
+            outline: none;
+            border-color: var(--primary-color);
+            box-shadow: 0 0 0 4px rgba(56, 128, 255, 0.1);
+          }
+
+          &::placeholder {
+            color: var(--medium-color);
+          }
+        }
+
+        .textarea-input {
+          resize: vertical;
+          min-height: 100px;
+          line-height: 1.5;
+        }
       }
     }
   }
 
   .nav-buttons {
     display: flex;
-    gap: 1rem;
+    gap: 12px;
 
-    ion-button {
+    .btn-nav {
       flex: 1;
-      --border-radius: 8px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      gap: 8px;
+      height: 50px;
+      border: none;
+      border-radius: var(--radius-md);
+      font-size: 15px;
       font-weight: 600;
+      cursor: pointer;
+      transition: var(--transition);
+
+      .icon {
+        width: 18px;
+        height: 18px;
+      }
+
+      &.btn-prev {
+        background: var(--white);
+        color: var(--dark-color);
+        border: 2px solid var(--light-color);
+
+        &:active:not(:disabled) {
+          transform: translateX(-2px);
+          background: var(--light-color);
+        }
+
+        &:disabled {
+          opacity: 0.4;
+          cursor: not-allowed;
+        }
+      }
+
+      &.btn-next {
+        background: var(--primary-color);
+        color: var(--white);
+        box-shadow: var(--shadow-sm);
+
+        &:active {
+          transform: translateX(2px);
+          box-shadow: none;
+        }
+      }
     }
   }
 }
 
 // 结果页
-.result-page {
-  padding: 2rem 1.5rem;
-  max-width: 600px;
+.result-view {
+  max-width: 640px;
   margin: 0 auto;
+  padding: 32px 16px;
+  text-align: center;
 
-  .result-header {
-    text-align: center;
-    margin-bottom: 2rem;
-
-    ion-icon {
-      font-size: 4rem;
-      margin-bottom: 1rem;
+  .success-icon-wrapper {
+    width: 80px;
+    height: 80px;
+    margin: 0 auto 24px;
+    border-radius: 50%;
+    background: rgba(45, 211, 111, 0.1);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    animation: scaleIn 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+
+    .success-icon {
+      width: 48px;
+      height: 48px;
+      color: var(--success-color);
     }
+  }
 
-    h2 {
-      font-size: 1.5rem;
-      font-weight: 600;
-      color: var(--ion-color-dark);
-      margin: 0 0 0.5rem;
-    }
+  .result-title {
+    margin: 0 0 8px;
+    font-size: 24px;
+    font-weight: 600;
+    color: var(--dark-color);
+    animation: fadeInUp 0.6s ease 0.1s both;
+  }
 
-    p {
-      font-size: 1rem;
-      color: var(--ion-color-medium);
-      margin: 0.25rem 0;
-    }
+  .result-subtitle {
+    margin: 0 0 8px;
+    font-size: 16px;
+    font-weight: 500;
+    color: var(--medium-color);
+    animation: fadeInUp 0.6s ease 0.2s both;
   }
 
-  .result-content {
-    background: white;
-    border-radius: 12px;
-    padding: 2rem;
-    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
-    margin-bottom: 1.5rem;
+  .result-desc {
+    margin: 0 0 32px;
+    font-size: 14px;
+    color: var(--medium-color);
+    animation: fadeInUp 0.6s ease 0.3s both;
+  }
 
-    h3 {
-      font-size: 1.125rem;
+  .result-card {
+    background: var(--white);
+    border-radius: var(--radius-lg);
+    padding: 24px;
+    margin-bottom: 24px;
+    box-shadow: var(--shadow-sm);
+    text-align: left;
+    animation: fadeInUp 0.6s ease 0.4s both;
+
+    .result-card-title {
+      margin: 0 0 20px;
+      font-size: 16px;
       font-weight: 600;
-      color: var(--ion-color-dark);
-      margin: 0 0 1.5rem;
+      color: var(--dark-color);
       text-align: center;
     }
 
-    .result-item {
-      display: flex;
-      margin-bottom: 1rem;
-      padding-bottom: 1rem;
-      border-bottom: 1px solid #f0f0f0;
-
-      &:last-child {
-        margin-bottom: 0;
-        padding-bottom: 0;
-        border-bottom: none;
-      }
+    .result-list {
+      .result-item {
+        display: flex;
+        gap: 12px;
+        padding: 12px 0;
+        border-bottom: 1px solid var(--light-color);
 
-      .result-label {
-        width: 100px;
-        font-weight: 600;
-        color: var(--ion-color-medium);
-        font-size: 0.95rem;
-        flex-shrink: 0;
-      }
+        &:first-child {
+          padding-top: 0;
+        }
 
-      .result-value {
-        flex: 1;
-        color: var(--ion-color-dark);
-        font-size: 0.95rem;
-        line-height: 1.6;
+        &:last-child {
+          padding-bottom: 0;
+          border-bottom: none;
+        }
+
+        .result-label {
+          min-width: 80px;
+          font-size: 14px;
+          font-weight: 600;
+          color: var(--medium-color);
+          flex-shrink: 0;
+        }
+
+        .result-value {
+          flex: 1;
+          font-size: 14px;
+          color: var(--dark-color);
+          line-height: 1.5;
+          word-break: break-word;
+        }
       }
     }
 
     .result-divider {
       height: 1px;
-      background: #e0e0e0;
-      margin: 1.5rem 0;
+      background: var(--light-color);
+      margin: 20px 0;
     }
   }
 
-  .back-button {
-    --border-radius: 8px;
+  .btn-primary {
+    width: 100%;
+    height: 50px;
+    background: var(--primary-color);
+    color: var(--white);
+    border: none;
+    border-radius: var(--radius-md);
+    font-size: 16px;
     font-weight: 600;
+    box-shadow: var(--shadow-sm);
+    cursor: pointer;
+    transition: var(--transition);
+    animation: fadeInUp 0.6s ease 0.5s both;
+
+    &:active {
+      transform: translateY(2px);
+      box-shadow: none;
+    }
+  }
+}
+
+// 动画
+@keyframes fadeInUp {
+  from {
+    opacity: 0;
+    transform: translateY(20px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+@keyframes slideInRight {
+  from {
+    opacity: 0;
+    transform: translateX(30px);
+  }
+  to {
+    opacity: 1;
+    transform: translateX(0);
+  }
+}
+
+@keyframes scaleIn {
+  from {
+    opacity: 0;
+    transform: scale(0);
+  }
+  to {
+    opacity: 1;
+    transform: scale(1);
+  }
+}
+
+// 平板适配 (>= 768px)
+@media (min-width: 768px) {
+  .survey-header .header-content {
+    max-width: 768px;
+  }
+
+  .welcome-view,
+  .questionnaire-view,
+  .result-view {
+    max-width: 768px;
+    padding-left: 32px;
+    padding-right: 32px;
+  }
+
+  .intro-card,
+  .question-card,
+  .result-card {
+    padding: 32px;
+  }
+
+  .btn-start,
+  .btn-primary {
+    height: 56px;
+    font-size: 17px;
+  }
+
+  .nav-buttons .btn-nav {
+    height: 52px;
   }
 }
 
-// 响应式适配
+// 小屏适配 (<= 375px)
 @media (max-width: 375px) {
-  .welcome-page,
-  .questionnaire-page,
-  .result-page {
-    padding-left: 1rem;
-    padding-right: 1rem;
+  .survey-header {
+    .header-title {
+      font-size: 16px;
+    }
   }
 
-  .welcome-page .welcome-content,
-  .questionnaire-page .question-container,
-  .result-page .result-content {
-    padding: 1.5rem;
+  .welcome-view {
+    .user-greeting {
+      font-size: 20px;
+    }
+
+    .intro-card {
+      padding: 20px;
+
+      .intro-header h3 {
+        font-size: 16px;
+      }
+
+      .intro-text {
+        font-size: 14px;
+      }
+    }
+  }
+
+  .questionnaire-view {
+    .question-card {
+      padding: 20px;
+
+      .question-title {
+        font-size: 16px;
+      }
+
+      .option-item {
+        padding: 14px;
+
+        .option-label {
+          font-size: 14px;
+        }
+      }
+    }
+
+    .nav-buttons .btn-nav {
+      height: 48px;
+      font-size: 14px;
+    }
+  }
+
+  .result-view {
+    .result-title {
+      font-size: 20px;
+    }
+
+    .result-card {
+      padding: 20px;
+    }
   }
 }

+ 21 - 7
src/modules/project/pages/project-survey/project-survey.component.ts

@@ -54,6 +54,7 @@ export class ProjectSurveyComponent implements OnInit {
   // 加载状态
   loading: boolean = true;
   error: string | null = null;
+  isCustomerOnly: boolean = false; // 是否仅限客户填写
 
   // 数据对象
   project: FmodeObject | null = null;
@@ -210,15 +211,28 @@ export class ProjectSurveyComponent implements OnInit {
       this.loading = true;
 
       // 1. 获取当前外部联系人
-      if (this.wxAuth) {
-        try {
-          this.currentContact = await this.wxAuth.currentContact();
-          console.log('✅ 当前联系人:', this.currentContact?.get('name'));
-        } catch (error) {
-          console.error('❌ 获取联系人失败:', error);
-          this.error = '无法识别您的身份,请通过企微群聊进入';
+      if (!this.wxAuth) {
+        this.isCustomerOnly = true;
+        this.error = '该问卷仅供客户填写,请通过企微群聊进入';
+        return;
+      }
+
+      try {
+        this.currentContact = await this.wxAuth.currentContact();
+        console.log('✅ 当前联系人:', this.currentContact?.get('name'));
+
+        // 验证是否为外部联系人
+        if (!this.currentContact || !this.currentContact.id) {
+          console.warn('⚠️ 未找到外部联系人信息');
+          this.isCustomerOnly = true;
+          this.error = '该问卷仅供客户填写';
           return;
         }
+      } catch (error) {
+        console.error('❌ 获取联系人失败:', error);
+        this.isCustomerOnly = true;
+        this.error = '该问卷仅供客户填写,请通过企微群聊进入';
+        return;
       }
 
       // 2. 加载项目