project-survey.component.html 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. <!-- 顶部导航栏 -->
  2. <div class="survey-header">
  3. <div class="header-content">
  4. @if(!currentContact?.id){
  5. <button class="back-button" (click)="goBack()">
  6. <svg class="icon" viewBox="0 0 512 512">
  7. <path fill="currentColor" d="M244 400L100 256l144-144M120 256h292"/>
  8. </svg>
  9. </button>
  10. }
  11. <h1 class="header-title">项目需求调查</h1>
  12. <div class="header-spacer"></div>
  13. </div>
  14. </div>
  15. <!-- 主内容区 -->
  16. <div class="survey-container">
  17. <!-- 加载状态 -->
  18. @if (loading) {
  19. <div class="status-view loading-view">
  20. <div class="spinner">
  21. <div class="spinner-circle"></div>
  22. </div>
  23. <p class="status-text">加载中...</p>
  24. </div>
  25. }
  26. <!-- 错误状态 -->
  27. @if (error && !loading && !surveyLog?.get('isCompleted')) {
  28. <div class="status-view error-view">
  29. <div class="error-icon-wrapper">
  30. <svg class="icon error-icon" viewBox="0 0 512 512">
  31. @if (isCustomerOnly) {
  32. <!-- 仅限客户图标 -->
  33. <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"/>
  34. <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"/>
  35. } @else {
  36. <!-- 常规错误图标 -->
  37. <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"/>
  38. }
  39. </svg>
  40. </div>
  41. <h2 class="error-title">{{ isCustomerOnly ? '仅限客户填写' : '加载失败' }}</h2>
  42. <p class="error-message">{{ error }}</p>
  43. @if (isCustomerOnly) {
  44. <div class="info-box">
  45. <svg class="icon" viewBox="0 0 512 512">
  46. <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"/>
  47. </svg>
  48. <div class="info-text">
  49. <p class="info-title">客户填写入口</p>
  50. <p class="info-desc">请通过企微群聊中收到的问卷链接进入</p>
  51. </div>
  52. </div>
  53. }
  54. <button class="btn-primary" (click)="goBack()">返回</button>
  55. </div>
  56. }
  57. <!-- 欢迎页 -->
  58. @if (currentState === 'welcome' && !loading && !error) {
  59. <div class="welcome-view">
  60. <!-- 用户信息卡片 -->
  61. <div class="user-card">
  62. <div class="user-avatar">
  63. <img [src]="currentContact?.get('data')?.avatar || 'assets/default-avatar.png'" alt="头像" />
  64. </div>
  65. <h2 class="user-greeting">您好, {{ currentContact?.get('realname') || currentContact?.get('name') }}</h2>
  66. <p class="user-subtitle">欢迎参与需求调查</p>
  67. </div>
  68. <!-- 问卷介绍 -->
  69. <div class="intro-card">
  70. <div class="intro-header">
  71. <svg class="icon" viewBox="0 0 512 512">
  72. <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"/>
  73. <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"/>
  74. </svg>
  75. <h3>家装效果图服务需求调查</h3>
  76. </div>
  77. <div class="intro-body">
  78. <p class="intro-text">
  79. 尊敬的伙伴,为让本次效果图服务更贴合您的工作节奏与核心需求,我们准备了简短选择式问卷。
  80. </p>
  81. <p class="intro-text">
  82. 您的偏好将直接帮我们校准服务方向,感谢支持!
  83. </p>
  84. </div>
  85. <!-- 问卷信息标签 -->
  86. <div class="info-tags">
  87. <div class="info-tag">
  88. <svg class="icon" viewBox="0 0 512 512">
  89. <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"/>
  90. </svg>
  91. <span>3-5分钟</span>
  92. </div>
  93. <div class="info-tag">
  94. <svg class="icon" viewBox="0 0 512 512">
  95. <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"/>
  96. </svg>
  97. <span>{{ effectiveQuestions.length }}道题</span>
  98. </div>
  99. <div class="info-tag">
  100. <svg class="icon" viewBox="0 0 512 512">
  101. <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"/>
  102. </svg>
  103. <span>选择为主</span>
  104. </div>
  105. </div>
  106. </div>
  107. <!-- 开始按钮 -->
  108. <button class="btn-start" (click)="startSurvey()">
  109. <span>开始填写</span>
  110. <svg class="icon" viewBox="0 0 512 512">
  111. <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"/>
  112. </svg>
  113. </button>
  114. </div>
  115. }
  116. <!-- 答题页 -->
  117. @if (currentState === 'questionnaire' && !loading && !error) {
  118. <div class="questionnaire-view">
  119. <!-- 进度指示器 -->
  120. <div class="progress-section">
  121. <div class="progress-bar-wrapper">
  122. <div class="progress-bar">
  123. <div class="progress-fill" [style.width.%]="getProgress()"></div>
  124. </div>
  125. </div>
  126. <div class="progress-info">
  127. <span class="progress-current">{{ currentQuestionIndex + 1 }}</span>
  128. <span class="progress-separator">/</span>
  129. <span class="progress-total">{{ effectiveQuestions.length }}</span>
  130. </div>
  131. </div>
  132. @if (getCurrentQuestion(); as question) {
  133. <!-- 题目卡片 -->
  134. <div class="question-card">
  135. <!-- 章节标签 -->
  136. <div class="section-badge">{{ question.section }}</div>
  137. <!-- 题目内容 -->
  138. <div class="question-content">
  139. <h3 class="question-title">
  140. <span class="question-number">{{ currentQuestionIndex + 1 }}.</span>
  141. <span class="question-text">{{ question.title }}</span>
  142. @if (question.required) {
  143. <span class="required-star">*</span>
  144. }
  145. </h3>
  146. <!-- 单选题 -->
  147. @if (question.type === 'single') {
  148. <div class="options-list">
  149. @for (option of question.options; track option) {
  150. <div
  151. class="option-item"
  152. [class.selected]="answers[question.id] === option"
  153. (click)="selectSingleOption(option)">
  154. <div class="option-radio">
  155. <div class="radio-outer">
  156. <div class="radio-inner"></div>
  157. </div>
  158. </div>
  159. <span class="option-label">{{ option }}</span>
  160. </div>
  161. }
  162. @if (question.hasOther) {
  163. <div
  164. class="option-item"
  165. [class.selected]="answers[question.id]?.startsWith('其他')"
  166. (click)="selectSingleOption('其他')">
  167. <div class="option-radio">
  168. <div class="radio-outer">
  169. <div class="radio-inner"></div>
  170. </div>
  171. </div>
  172. <span class="option-label">其他</span>
  173. </div>
  174. }
  175. </div>
  176. @if (showOtherInput) {
  177. <div class="input-wrapper">
  178. <input
  179. type="text"
  180. class="text-input"
  181. [(ngModel)]="otherInput"
  182. placeholder="请输入其他内容..."
  183. autofocus />
  184. </div>
  185. }
  186. }
  187. <!-- 多选题 -->
  188. @if (question.type === 'multiple') {
  189. <div class="options-list">
  190. @for (option of question.options; track option) {
  191. <div
  192. class="option-item"
  193. [class.selected]="hasMultipleOption(question.id, option)"
  194. (click)="toggleMultipleOption(option)">
  195. <div class="option-checkbox">
  196. <div class="checkbox-box">
  197. <svg class="icon checkmark" viewBox="0 0 512 512">
  198. <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"/>
  199. </svg>
  200. </div>
  201. </div>
  202. <span class="option-label">{{ option }}</span>
  203. </div>
  204. }
  205. @if (question.hasOther) {
  206. <div
  207. class="option-item"
  208. [class.selected]="hasMultipleOptionStartsWith(question.id, '其他')"
  209. (click)="toggleMultipleOption('其他')">
  210. <div class="option-checkbox">
  211. <div class="checkbox-box">
  212. <svg class="icon checkmark" viewBox="0 0 512 512">
  213. <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"/>
  214. </svg>
  215. </div>
  216. </div>
  217. <span class="option-label">其他</span>
  218. </div>
  219. }
  220. </div>
  221. @if (showOtherInput) {
  222. <div class="input-wrapper">
  223. <input
  224. type="text"
  225. class="text-input"
  226. [(ngModel)]="otherInput"
  227. placeholder="请输入其他内容..." />
  228. </div>
  229. }
  230. }
  231. <!-- 文本题 -->
  232. @if (question.type === 'text') {
  233. <div class="input-wrapper">
  234. <textarea
  235. class="textarea-input"
  236. [(ngModel)]="answers[question.id]"
  237. [placeholder]="question.placeholder || '请输入...'"
  238. rows="4"></textarea>
  239. </div>
  240. }
  241. <!-- 数字题 -->
  242. @if (question.type === 'number') {
  243. <div class="input-wrapper">
  244. <input
  245. type="number"
  246. class="text-input"
  247. [(ngModel)]="answers[question.id]"
  248. [placeholder]="question.placeholder || '请输入数字...'" />
  249. </div>
  250. }
  251. </div>
  252. </div>
  253. <!-- 导航按钮 -->
  254. <div class="nav-buttons">
  255. <button
  256. class="btn-nav btn-prev"
  257. [disabled]="currentQuestionIndex === 0"
  258. (click)="previousQuestion()">
  259. <svg class="icon" viewBox="0 0 512 512">
  260. <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"/>
  261. </svg>
  262. <span>上一题</span>
  263. </button>
  264. <button
  265. class="btn-nav btn-next"
  266. (click)="nextQuestion()">
  267. <span>{{ currentQuestionIndex >= effectiveQuestions.length - 1 ? '提交' : '下一题' }}</span>
  268. <svg class="icon" viewBox="0 0 512 512">
  269. <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"/>
  270. </svg>
  271. </button>
  272. </div>
  273. }
  274. </div>
  275. }
  276. <!-- 结果页 -->
  277. @if (currentState === 'result' && !loading && (!error || surveyLog?.get('isCompleted'))) {
  278. <div class="result-view">
  279. <!-- 成功图标 -->
  280. <div class="success-icon-wrapper">
  281. <svg class="icon success-icon" viewBox="0 0 512 512">
  282. <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"/>
  283. </svg>
  284. </div>
  285. @if(currentContact?.id){
  286. <h2 class="result-title">问卷提交成功</h2>
  287. <p class="result-subtitle">感谢您的反馈!</p>
  288. <p class="result-desc">我们将根据您的选择制定服务方案</p>
  289. }
  290. <!-- 答卷内容 -->
  291. <div class="result-card">
  292. <h3 class="result-card-title">问卷信息</h3>
  293. <div class="result-list">
  294. <div class="result-item">
  295. <div class="result-label">核心服务</div>
  296. <div class="result-value">{{ getFormattedAnswer('q1') }}</div>
  297. </div>
  298. <div class="result-item">
  299. <div class="result-label">空间数量</div>
  300. <div class="result-value">{{ getFormattedAnswer('q2') }}</div>
  301. </div>
  302. <div class="result-item">
  303. <div class="result-label">价值侧重</div>
  304. <div class="result-value">{{ getFormattedAnswer('q3') }}</div>
  305. </div>
  306. <div class="result-item">
  307. <div class="result-label">技术配合</div>
  308. <div class="result-value">{{ getFormattedAnswer('q4') }}</div>
  309. </div>
  310. <div class="result-item">
  311. <div class="result-label">协作方式</div>
  312. <div class="result-value">{{ getFormattedAnswer('q5') }}</div>
  313. </div>
  314. @if (answers['q6']) {
  315. <div class="result-item">
  316. <div class="result-label">注意事项</div>
  317. <div class="result-value">{{ getFormattedAnswer('q6') }}</div>
  318. </div>
  319. }
  320. @if (answers['q7']) {
  321. <div class="result-item">
  322. <div class="result-label">特殊要求</div>
  323. <div class="result-value">{{ getFormattedAnswer('q7') }}</div>
  324. </div>
  325. }
  326. @if (answers['q8']) {
  327. <div class="result-item">
  328. <div class="result-label">参考素材</div>
  329. <div class="result-value">{{ getFormattedAnswer('q8') }}</div>
  330. </div>
  331. }
  332. </div>
  333. <div class="result-divider"></div>
  334. <div class="result-list">
  335. <div class="result-item">
  336. <div class="result-label">对接人</div>
  337. <div class="result-value">{{ answers['contact_name'] || currentContact?.get('realname') || '-' }}</div>
  338. </div>
  339. <div class="result-item">
  340. <div class="result-label">电话</div>
  341. <div class="result-value">{{ maskPhone(answers['contact_phone'] || currentContact?.get('mobile') || '') }}</div>
  342. </div>
  343. </div>
  344. </div>
  345. <!-- 返回按钮 -->
  346. <button class="btn-primary" (click)="goBack()">
  347. <span>返回项目</span>
  348. </button>
  349. </div>
  350. }
  351. </div>