workflowTradeAdd.vue 55 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493
  1. <template>
  2. <div class="wta-page">
  3. <!-- ══════════════════════════════════════
  4. HERO BANNER
  5. ══════════════════════════════════════ -->
  6. <div class="wta-hero">
  7. <!-- 背景装饰层 -->
  8. <div class="wta-hero__grid"></div>
  9. <div class="wta-hero__orb wta-hero__orb--1"></div>
  10. <div class="wta-hero__orb wta-hero__orb--2"></div>
  11. <div class="wta-hero__orb wta-hero__orb--3"></div>
  12. <!-- 面包屑 -->
  13. <div class="container wta-hero__breadcrumb-wrap">
  14. <Breadcrumb />
  15. </div>
  16. <!-- 底部波浪 Canvas(放在内容前,z-index 低于内容)-->
  17. <canvas ref="waveCanvasRef" class="wta-hero__wave-canvas"></canvas>
  18. <!-- 主标题区 -->
  19. <div class="container wta-hero__content">
  20. <div class="wta-hero__badge">
  21. <span class="wta-hero__badge-dot"></span>
  22. <span>{{ $t('workflowTrade.heroBadge') }}</span>
  23. </div>
  24. <h1 class="wta-hero__title">{{ t('workflowTradeAdd.publishDemand') }}</h1>
  25. <p class="wta-hero__subtitle">{{ t('workflowTradeAdd.publishDemandTip') }}</p>
  26. <!-- 统计数字装饰 -->
  27. <div class="wta-hero__stats">
  28. <div class="wta-hero__stat">
  29. <span class="wta-hero__stat-num">12,847</span>
  30. <span class="wta-hero__stat-label">{{ $t('workflowTrade.statPublished') }}</span>
  31. </div>
  32. <div class="wta-hero__stat-divider"></div>
  33. <div class="wta-hero__stat">
  34. <span class="wta-hero__stat-num">9,632</span>
  35. <span class="wta-hero__stat-label">{{ $t('workflowTrade.statSuccess') }}</span>
  36. </div>
  37. <div class="wta-hero__stat-divider"></div>
  38. <div class="wta-hero__stat">
  39. <span class="wta-hero__stat-num">98.6%</span>
  40. <span class="wta-hero__stat-label">{{ $t('workflowTrade.statRate') }}</span>
  41. </div>
  42. </div>
  43. </div>
  44. </div>
  45. <!-- ══════════════════════════════════════
  46. 步骤进度条(sticky sentinel + 步骤条)
  47. ══════════════════════════════════════ -->
  48. <!-- Sentinel:用于检测步骤条是否滚出视口 -->
  49. <div ref="stepsSentinelRef" class="wta-steps-sentinel"></div>
  50. <div class="wta-steps-wrap" :class="{ 'wta-steps-wrap--sticky': isSticky }">
  51. <div class="container">
  52. <div class="wta-steps">
  53. <div v-for="(step, idx) in steps" :key="idx"
  54. class="wta-step"
  55. :class="{ 'wta-step--active': idx === currentStep, 'wta-step--done': idx < currentStep }"
  56. style="cursor: pointer"
  57. @click="scrollToSection(idx)">
  58. <div class="wta-step__circle">
  59. <svg v-if="idx < currentStep" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><polyline points="20 6 9 17 4 12"/></svg>
  60. <span v-else>{{ idx + 1 }}</span>
  61. </div>
  62. <span class="wta-step__label">{{ step }}</span>
  63. <div v-if="idx < steps.length - 1" class="wta-step__line"
  64. :class="{ 'wta-step__line--done': idx < currentStep }"></div>
  65. </div>
  66. </div>
  67. </div>
  68. </div>
  69. <!-- ══════════════════════════════════════
  70. 主体内容
  71. ══════════════════════════════════════ -->
  72. <div class="container wta-body">
  73. <div class="wta-layout">
  74. <!-- 左侧表单 -->
  75. <div class="wta-form-col">
  76. <el-form :model="ruleForm" :rules="rules" ref="ruleFormRef" label-position="top">
  77. <!-- ── 基本信息 ── -->
  78. <div class="wta-card" id="section-basic">
  79. <div class="wta-card__header">
  80. <div class="wta-card__header-icon">
  81. <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
  82. </div>
  83. <div>
  84. <div class="wta-card__title">{{ t('workflowTradeAdd.basicInfo') }}</div>
  85. <div class="wta-card__subtitle">{{ t('workflowTradeAdd.basicInfoSubtitle') }}</div>
  86. </div>
  87. <div class="wta-card__step-badge">Step 1</div>
  88. </div>
  89. <div class="wta-card__body">
  90. <!-- 需求标题 -->
  91. <el-form-item prop="title">
  92. <template #label>
  93. <div class="wta-field-label">
  94. <span class="wta-required">*</span>
  95. <span class="wta-label-text">{{ t('workflowTradeAdd.demandTitle') }}</span>
  96. <el-tooltip content="一个好的标题能吸引更多专业人才关注" placement="top">
  97. <span class="wta-label-info-icon">
  98. <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
  99. </span>
  100. </el-tooltip>
  101. </div>
  102. </template>
  103. <div class="wta-input-wrap">
  104. <span class="wta-input-prefix-icon">
  105. <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
  106. </span>
  107. <el-input
  108. v-model="ruleForm.title"
  109. :placeholder="t('workflowTradeAdd.placeholderDemandTitle')"
  110. maxlength="50"
  111. show-word-limit
  112. class="wta-input wta-input--has-prefix"
  113. @focus="scrollToSection(0)"
  114. />
  115. </div>
  116. <!-- 字数进度条 -->
  117. <div class="wta-char-progress">
  118. <div class="wta-char-progress__bar">
  119. <div class="wta-char-progress__fill" :style="{ width: (ruleForm.title.length / 50 * 100) + '%', background: ruleForm.title.length > 40 ? '#f59e0b' : 'linear-gradient(90deg,#6366f1,#a855f7)' }"></div>
  120. </div>
  121. <span class="wta-char-progress__text" :class="{ 'wta-char-progress__text--warn': ruleForm.title.length > 40 }">{{ ruleForm.title.length }}/50</span>
  122. </div>
  123. </el-form-item>
  124. <!-- 工作流类型 -->
  125. <el-form-item prop="categoryId3">
  126. <template #label>
  127. <div class="wta-field-label">
  128. <span class="wta-required">*</span>
  129. <span class="wta-label-text">{{ t('workflowTradeAdd.workflowType') }}</span>
  130. <span class="wta-label-hint">{{ t('workflowTradeAdd.workflowTypeTip') }}</span>
  131. </div>
  132. </template>
  133. <div class="wta-input-wrap">
  134. <span class="wta-input-prefix-icon">
  135. <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
  136. </span>
  137. <el-cascader
  138. v-model="categoryIdList"
  139. :options="categoryListTree"
  140. :placeholder="t('workflowTradeAdd.placeholderWorkflowType')"
  141. style="width:100%"
  142. class="wta-cascader wta-input--has-prefix"
  143. :props="{ label: 'categoryName', value: 'categoryId', children: 'children' }"
  144. />
  145. </div>
  146. </el-form-item>
  147. </div>
  148. </div>
  149. <!-- ── 详细内容 ── -->
  150. <div class="wta-card mt20" id="section-detail">
  151. <div class="wta-card__header">
  152. <div class="wta-card__header-icon wta-card__header-icon--purple">
  153. <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
  154. </div>
  155. <div>
  156. <div class="wta-card__title">{{ t('workflowTradeAdd.detailInfo') }}</div>
  157. <div class="wta-card__subtitle">{{ t('workflowTradeAdd.detailInfoSubtitle') }}</div>
  158. </div>
  159. <div class="wta-card__step-badge wta-card__step-badge--purple">Step 2</div>
  160. </div>
  161. <div class="wta-card__body">
  162. <!-- 需求背景 -->
  163. <el-form-item :label="$t('workflowTradeAdd.background')" prop="background">
  164. <template #label>
  165. <div class="wta-field-label">
  166. <span class="wta-required">*</span>
  167. <span class="wta-label-text">{{ $t('workflowTradeAdd.background') }}</span>
  168. </div>
  169. </template>
  170. <el-input
  171. v-model="ruleForm.background"
  172. :placeholder="t('workflowTradeAdd.placeholderBackground')"
  173. maxlength="500"
  174. type="textarea"
  175. show-word-limit
  176. :rows="6"
  177. class="wta-textarea"
  178. @focus="scrollToSection(1)"
  179. />
  180. <!-- 字数进度条 -->
  181. <div class="wta-char-progress">
  182. <div class="wta-char-progress__bar">
  183. <div class="wta-char-progress__fill" :style="{ width: (ruleForm.background.length / 500 * 100) + '%' }"></div>
  184. </div>
  185. <span class="wta-char-progress__text">{{ ruleForm.background.length }}/500</span>
  186. </div>
  187. </el-form-item>
  188. <!-- 具体需求 -->
  189. <el-form-item prop="requirements">
  190. <template #label>
  191. <div class="wta-field-label">
  192. <span class="wta-required">*</span>
  193. <span class="wta-label-text">{{ $t('workflowTradeAdd.requirements') }}</span>
  194. <el-tooltip content="建议包含:功能需求、技术要求、交付标准" placement="top">
  195. <span class="wta-label-info-icon">
  196. <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
  197. </span>
  198. </el-tooltip>
  199. </div>
  200. </template>
  201. <!-- 写作提示卡片 -->
  202. <div class="wta-writing-tips">
  203. <div class="wta-writing-tips__item" v-for="tip in writingTips" :key="tip">
  204. <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>
  205. <span>{{ tip }}</span>
  206. </div>
  207. </div>
  208. <el-input
  209. v-model="ruleForm.requirements"
  210. :rows="8"
  211. :placeholder="t('workflowTradeAdd.requirementsTip') + t('workflowTradeAdd.requirementsTip1')"
  212. maxlength="2000"
  213. type="textarea"
  214. show-word-limit
  215. class="wta-textarea"
  216. />
  217. <div class="wta-char-progress">
  218. <div class="wta-char-progress__bar">
  219. <div class="wta-char-progress__fill" :style="{ width: (ruleForm.requirements.length / 2000 * 100) + '%' }"></div>
  220. </div>
  221. <span class="wta-char-progress__text">{{ ruleForm.requirements.length }}/2000</span>
  222. </div>
  223. </el-form-item>
  224. </div>
  225. </div>
  226. <!-- ── 项目信息 ── -->
  227. <div class="wta-card mt20" id="section-project">
  228. <div class="wta-card__header">
  229. <div class="wta-card__header-icon wta-card__header-icon--green">
  230. <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
  231. </div>
  232. <div>
  233. <div class="wta-card__title">{{ $t('workflowTradeAdd.projectInfo') }}</div>
  234. <div class="wta-card__subtitle">{{ t('workflowTradeAdd.projectInfoSubtitle') }}</div>
  235. </div>
  236. <div class="wta-card__step-badge wta-card__step-badge--green">Step 3</div>
  237. </div>
  238. <div class="wta-card__body">
  239. <!-- 预算范围 -->
  240. <div class="wta-budget-row">
  241. <div class="wta-budget-item">
  242. <div class="wta-field-label mb8">
  243. <span class="wta-label-text">{{ $t('workflowTradeAdd.budgetLowerLimit') }}</span>
  244. <span class="wta-currency-badge">¥ RMB</span>
  245. </div>
  246. <el-form-item prop="budgetMin" style="margin-bottom:0">
  247. <div class="wta-input-wrap">
  248. <span class="wta-input-prefix-icon">
  249. <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
  250. </span>
  251. <el-input v-model="ruleForm.budgetMin" :placeholder="$t('workflowTradeAdd.placeholderBudgetLowerLimit')" maxlength="11" type="number" class="wta-input wta-input--has-prefix" @focus="scrollToSection(2)" />
  252. </div>
  253. </el-form-item>
  254. </div>
  255. <div class="wta-budget-range-icon">
  256. <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#a5b4fc" stroke-width="2"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>
  257. </div>
  258. <div class="wta-budget-item">
  259. <div class="wta-field-label mb8">
  260. <span class="wta-label-text">{{ $t('workflowTradeAdd.budgetUpperLimit') }}</span>
  261. <span class="wta-currency-badge">¥ RMB</span>
  262. </div>
  263. <el-form-item prop="budgetMax" style="margin-bottom:0">
  264. <div class="wta-input-wrap">
  265. <span class="wta-input-prefix-icon">
  266. <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
  267. </span>
  268. <el-input v-model="ruleForm.budgetMax" :placeholder="$t('workflowTradeAdd.placeholderBudgetUpperLimit')" maxlength="11" type="number" class="wta-input wta-input--has-prefix" @focus="scrollToSection(2)" />
  269. </div>
  270. </el-form-item>
  271. </div>
  272. </div>
  273. <div class="wta-budget-hint">
  274. <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#6366f1" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
  275. <span>{{ $t('workflowTradeAdd.budgetLowerLimitTip') }}</span>
  276. </div>
  277. <!-- 报名截止日期 -->
  278. <el-form-item :label="$t('common.signUpDeadline')" prop="deadline" class="mt16">
  279. <template #label>
  280. <div class="wta-field-label">
  281. <span class="wta-required">*</span>
  282. <span class="wta-label-text">{{ $t('common.signUpDeadline') }}</span>
  283. </div>
  284. </template>
  285. <div class="wta-input-wrap">
  286. <span class="wta-input-prefix-icon">
  287. <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
  288. </span>
  289. <el-date-picker
  290. v-model="ruleForm.deadline"
  291. type="date"
  292. value-format="YYYY-MM-DD"
  293. :placeholder="$t('workflowTradeAdd.selectSignUpDeadline')"
  294. :disabled-date="time => time.getTime() < Date.now() - 8.64e7"
  295. style="width:100%"
  296. class="wta-datepicker wta-input--has-prefix"
  297. />
  298. </div>
  299. </el-form-item>
  300. </div>
  301. </div>
  302. <!-- ── 联系方式 ── -->
  303. <div class="wta-card mt20" id="section-contact">
  304. <div class="wta-card__header">
  305. <div class="wta-card__header-icon wta-card__header-icon--pink">
  306. <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 12a19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 3.6 1.18h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L7.91 8.73a16 16 0 0 0 6.29 6.29l.91-.91a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z"/></svg>
  307. </div>
  308. <div>
  309. <div class="wta-card__title">{{ $t('workflowTradeAdd.contactInfo') }}</div>
  310. <div class="wta-card__subtitle">{{ t('workflowTradeAdd.contactInfoSubtitle') }}</div>
  311. </div>
  312. <div class="wta-card__step-badge wta-card__step-badge--pink">Step 4</div>
  313. </div>
  314. <div class="wta-card__body">
  315. <div class="wta-contact-grid">
  316. <!-- 手机号 -->
  317. <el-form-item prop="phone">
  318. <template #label>
  319. <div class="wta-field-label">
  320. <span class="wta-required">*</span>
  321. <span class="wta-label-text">{{ $t('workflowTradeAdd.phoneNumber') }}</span>
  322. </div>
  323. </template>
  324. <div class="wta-input-wrap">
  325. <span class="wta-input-prefix-icon">
  326. <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 12a19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 3.6 1.18h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L7.91 8.73a16 16 0 0 0 6.29 6.29l.91-.91a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z"/></svg>
  327. </span>
  328. <el-input v-model="ruleForm.phone" :placeholder="$t('workflowTradeAdd.placeholderPhoneNumber')" maxlength="11" class="wta-input wta-input--has-prefix" @focus="scrollToSection(3)" />
  329. </div>
  330. </el-form-item>
  331. <!-- 微信 -->
  332. <el-form-item prop="wechat">
  333. <template #label>
  334. <div class="wta-field-label">
  335. <span class="wta-label-text">{{ $t('workflowTradeAdd.wechat') }}</span>
  336. </div>
  337. </template>
  338. <div class="wta-input-wrap">
  339. <span class="wta-input-prefix-icon wta-input-prefix-icon--wechat">
  340. <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
  341. </span>
  342. <el-input v-model="ruleForm.wechat" :placeholder="$t('workflowTradeAdd.placeholderWechat')" maxlength="50" class="wta-input wta-input--has-prefix" @focus="scrollToSection(3)" />
  343. </div>
  344. </el-form-item>
  345. <!-- 邮箱 -->
  346. <el-form-item prop="email">
  347. <template #label>
  348. <div class="wta-field-label">
  349. <span class="wta-required">*</span>
  350. <span class="wta-label-text">{{ $t('workflowTradeAdd.email') }}</span>
  351. </div>
  352. </template>
  353. <div class="wta-input-wrap">
  354. <span class="wta-input-prefix-icon wta-input-prefix-icon--email">
  355. <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
  356. </span>
  357. <el-input v-model="ruleForm.email" :placeholder="$t('workflowTradeAdd.placeholderEmail')" maxlength="50" class="wta-input wta-input--has-prefix" @focus="scrollToSection(3)" />
  358. </div>
  359. </el-form-item>
  360. </div>
  361. <!-- 隐私提示 -->
  362. <div class="wta-privacy-tip">
  363. <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#6366f1" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
  364. <span>{{ t('workflowTradeAdd.privacyTip') }}</span>
  365. </div>
  366. </div>
  367. </div>
  368. <!-- ── 操作按钮 ── -->
  369. <div class="wta-actions mt24">
  370. <button
  371. class="wta-btn-primary"
  372. :class="{ 'wta-btn--loading': isSubmiting }"
  373. @click="submitForm"
  374. :disabled="isSubmiting"
  375. >
  376. <span v-if="isSubmiting" class="wta-btn-spinner"></span>
  377. <el-icon v-else><Promotion /></el-icon>
  378. <span>{{ isSubmiting ? t('workflowTradeAdd.publishing') : $t('common.fabuxuqiu') }}</span>
  379. </button>
  380. <button class="wta-btn-cancel" @click.stop.prevent="goBack">
  381. <el-icon><Back /></el-icon>
  382. <span>{{ $t('common.back') }}</span>
  383. </button>
  384. <span class="wta-actions__tip">提交即表示同意 <a href="#" class="wta-link">{{ t('workflowTradeAdd.serviceAgreement') }}</a></span>
  385. </div>
  386. </el-form>
  387. </div>
  388. <!-- ══════════════════════════════════════
  389. 右侧侧边栏
  390. ══════════════════════════════════════ -->
  391. <div class="wta-sidebar">
  392. <div class="wta-sidebar__sticky">
  393. <!-- 发布提示卡片 -->
  394. <div class="wta-sidebar-card wta-sidebar-card--tip">
  395. <div class="wta-sidebar-card__header">
  396. <div class="wta-sidebar-card__icon-wrap wta-sidebar-card__icon-wrap--tip">
  397. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
  398. </div>
  399. <span class="wta-sidebar-card__title">{{ $t('workflowTradeAdd.publishTip') }}</span>
  400. </div>
  401. <div class="ql-container">
  402. <div class="ql-editor wta-sidebar-content">
  403. <div v-html="sanitizedHint"></div>
  404. </div>
  405. </div>
  406. </div>
  407. <!-- 发布规则卡片 -->
  408. <div class="wta-sidebar-card wta-sidebar-card--rules mt16">
  409. <div class="wta-sidebar-card__header">
  410. <div class="wta-sidebar-card__icon-wrap wta-sidebar-card__icon-wrap--rules">
  411. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
  412. </div>
  413. <span class="wta-sidebar-card__title">{{ $t('workflowTradeAdd.publishRules') }}</span>
  414. </div>
  415. <div class="editor ql-container">
  416. <div class="ql-editor wta-sidebar-content">
  417. <div v-html="sanitizedRules"></div>
  418. </div>
  419. </div>
  420. </div>
  421. <!-- 平台保障卡片 -->
  422. <div class="wta-sidebar-card wta-sidebar-card--guarantee mt16">
  423. <div class="wta-sidebar-card__header">
  424. <div class="wta-sidebar-card__icon-wrap wta-sidebar-card__icon-wrap--guarantee">
  425. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
  426. </div>
  427. <span class="wta-sidebar-card__title">{{ t('workflowTradeAdd.platformGuarantee') }}</span>
  428. </div>
  429. <div class="wta-guarantee-list">
  430. <div class="wta-guarantee-item" v-for="g in guarantees" :key="g.text">
  431. <div class="wta-guarantee-item__icon" :style="{ background: g.bg }">
  432. <svg width="12" height="12" viewBox="0 0 24 24" fill="none" :stroke="g.color" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>
  433. </div>
  434. <span>{{ g.text }}</span>
  435. </div>
  436. </div>
  437. </div>
  438. </div>
  439. </div>
  440. </div>
  441. </div>
  442. </div>
  443. </template>
  444. <script setup>
  445. import DGTMessage from '@/utils/message'
  446. import { sanitizeHtml } from '@/utils/sanitize.js'
  447. import { questAdd, getQuestDetail, questEdit } from '@/api/workflowTrade.js'
  448. import { getCategoryListTree } from '@/api/category.js'
  449. import { getAgreementType } from '@/api/common.js'
  450. import { useI18n } from 'vue-i18n'
  451. const { t } = useI18n()
  452. import { useRouter, useRoute } from 'vue-router'
  453. const router = useRouter()
  454. const route = useRoute()
  455. import { ref, computed, reactive, onMounted, onUnmounted, watchEffect, nextTick } from 'vue'
  456. import { useAppStore } from '@/pinia/appStore'
  457. const appStore = useAppStore()
  458. const isSubmiting = ref(false)
  459. const query = route.query
  460. const questId = ref(query.id || '')
  461. const categoryListTree = ref([])
  462. const categoryIdList = ref([])
  463. const ruleFormRef = ref(null)
  464. const currentStep = ref(0)
  465. const isSticky = ref(false)
  466. const waveCanvasRef = ref(null)
  467. const stepsSentinelRef = ref(null)
  468. const steps = [t('workflowTradeAdd.stepBasicInfo'), t('workflowTradeAdd.stepDetailDesc'), t('workflowTradeAdd.stepProjectBudget'), t('workflowTradeAdd.stepContactInfo'), t('workflowTradeAdd.stepConfirmPublish')]
  469. const writingTips = [t('workflowTradeAdd.writingTip1'), t('workflowTradeAdd.writingTip2'), t('workflowTradeAdd.writingTip3'), t('workflowTradeAdd.writingTip4')]
  470. const guarantees = [
  471. { text: t('workflowTradeAdd.guarantee1'), bg: '#ede9fe', color: '#7c3aed' },
  472. { text: t('workflowTradeAdd.guarantee2'), bg: '#dbeafe', color: '#2563eb' },
  473. { text: t('workflowTradeAdd.guarantee3'), bg: '#d1fae5', color: '#059669' },
  474. { text: t('workflowTradeAdd.guarantee4'), bg: '#fef3c7', color: '#d97706' },
  475. ]
  476. const ruleForm = reactive({
  477. questId: '',
  478. title: '',
  479. categoryId1: '',
  480. categoryId2: '',
  481. categoryId3: '',
  482. background: '',
  483. requirements: '',
  484. budgetMin: '',
  485. budgetMax: '',
  486. deadline: '',
  487. phone: '',
  488. wechat: '',
  489. email: '',
  490. })
  491. watchEffect(() => {
  492. ruleForm.categoryId1 = categoryIdList.value[0] || ''
  493. ruleForm.categoryId2 = categoryIdList.value[1] || ''
  494. ruleForm.categoryId3 = categoryIdList.value[2] || ''
  495. })
  496. const rules = reactive({
  497. title: [{ required: true, message: t('workflowTradeAdd.placeholderDemandTitle'), trigger: 'blur' }],
  498. categoryId3: [{ required: true, message: t('workflowTradeAdd.placeholderWorkflowType'), trigger: 'blur' }],
  499. background: [{ required: true, message: t('workflowTradeAdd.placeholderBackground'), trigger: 'blur' }],
  500. requirements: [{ required: true, message: t('workflowTradeAdd.placeholderRequirements'), trigger: 'blur' }],
  501. budgetMin: [{ validator: (rule, value, callback) => /^\d+(\.\d{1,2})?$/.test(value), message: t('workflowTradeAdd.pleaseInputRightBudgetLowerLimit'), trigger: 'blur' }],
  502. budgetMax: [{ validator: (rule, value, callback) => /^\d+(\.\d{1,2})?$/.test(value), message: t('workflowTradeAdd.pleaseInputRightBudgetUpperLimit'), trigger: 'blur' }],
  503. deadline: [{ required: true, message: t('workflowTradeAdd.selectSignUpDeadline'), trigger: 'change' }],
  504. phone: [
  505. { required: true, message: t('workflowTradeAdd.placeholderPhoneNumber'), trigger: 'blur' },
  506. { validator: (rule, value, callback) => /^1[3456789]\d{9}$/.test(value), message: t('common.pleaseInputRightPhoneNumber'), trigger: 'blur' },
  507. ],
  508. email: [
  509. { required: true, message: t('workflowTradeAdd.placeholderEmail'), trigger: 'blur' },
  510. { validator: (rule, value, callback) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value), message: t('common.pleaseInputRightEmail'), trigger: 'blur' },
  511. ],
  512. })
  513. // ── 波浪 Canvas 动画 ──────────────────────────────────────
  514. let waveAnimId = null
  515. function initWaveCanvas() {
  516. const canvas = waveCanvasRef.value
  517. if (!canvas) return
  518. const ctx = canvas.getContext('2d')
  519. let W = 0, H = 0
  520. function resize() {
  521. W = canvas.width = canvas.offsetWidth
  522. H = canvas.height = canvas.offsetHeight
  523. }
  524. resize()
  525. window.addEventListener('resize', resize)
  526. const waves = [
  527. { amp: 40, freq: 0.004, speed: 0.0012, phase: 0, color: 'rgba(99,102,241,0.28)' },
  528. { amp: 28, freq: 0.006, speed: 0.0025, phase: 2.1, color: 'rgba(167,139,250,0.22)' },
  529. { amp: 18, freq: 0.008, speed: 0.004, phase: 4.3, color: 'rgba(255,255,255,1)' },
  530. ]
  531. function draw() {
  532. ctx.clearRect(0, 0, W, H)
  533. waves.forEach(w => {
  534. w.phase += w.speed
  535. ctx.beginPath()
  536. ctx.moveTo(0, H)
  537. for (let x = 0; x <= W; x += 2) {
  538. // Gemini 建议:起始位置改为 60%,让波浪基线更靠下,波浪紧贴步骤条
  539. const y = H * 0.60 + Math.sin(x * w.freq + w.phase) * w.amp
  540. ctx.lineTo(x, y)
  541. }
  542. ctx.lineTo(W, H)
  543. ctx.closePath()
  544. ctx.fillStyle = w.color
  545. ctx.fill()
  546. })
  547. waveAnimId = requestAnimationFrame(draw)
  548. }
  549. draw()
  550. }
  551. // ── 点击步骤跳转 ────────────────────────────────────────
  552. const allSectionIds = ['section-basic', 'section-detail', 'section-project', 'section-contact']
  553. let isScrollingToSection = false
  554. function scrollToSection(idx) {
  555. // 第5步(确认发布)没有对应 section,跳到底部
  556. const targetId = allSectionIds[idx]
  557. if (!targetId) {
  558. window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' })
  559. currentStep.value = idx
  560. return
  561. }
  562. const el = document.getElementById(targetId)
  563. if (!el) return
  564. // 动态获取步骤条实际高度(sticky 激活时为 61px,未激活时为 0)
  565. const stepsWrap = document.querySelector('.wta-steps-wrap')
  566. const stepsH = isSticky.value ? (stepsWrap?.offsetHeight || 61) : 0
  567. // 顶部导航栏高度(固定 60px)
  568. const navH = 60
  569. const offset = navH + stepsH + 16
  570. const top = el.getBoundingClientRect().top + window.scrollY - offset
  571. // 暂停 scroll 联动,避免滚动过程中步骤被重置
  572. isScrollingToSection = true
  573. currentStep.value = idx
  574. window.scrollTo({ top: Math.max(0, top), behavior: 'smooth' })
  575. // 1秒后恢复 scroll 联动
  576. setTimeout(() => { isScrollingToSection = false }, 1000)
  577. }
  578. // ── 步骤条悬浮 + 滚动联动 ──────────────────────────────
  579. const sectionIds = ['section-basic', 'section-detail', 'section-project', 'section-contact']
  580. let scrollHandler = null
  581. function initStickyAndSync() {
  582. // IntersectionObserver 监测 sentinel 是否离开视口
  583. const sentinel = stepsSentinelRef.value
  584. if (sentinel) {
  585. const observer = new IntersectionObserver(
  586. ([entry]) => { isSticky.value = !entry.isIntersecting },
  587. { root: null, threshold: 0 }
  588. )
  589. observer.observe(sentinel)
  590. }
  591. // 滚动联动:找哪个 section 顶部最接近视口 35% 参考线
  592. scrollHandler = () => {
  593. // 点击跳转期间暂停联动,避免步骤被重置
  594. if (isScrollingToSection) return
  595. const ref35 = window.innerHeight * 0.35
  596. let best = 0, bestDist = Infinity
  597. sectionIds.forEach((id, i) => {
  598. const el = document.getElementById(id)
  599. if (!el) return
  600. const dist = Math.abs(el.getBoundingClientRect().top - ref35)
  601. if (dist < bestDist) { bestDist = dist; best = i }
  602. })
  603. currentStep.value = best
  604. }
  605. window.addEventListener('scroll', scrollHandler, { passive: true })
  606. }
  607. onMounted(() => {
  608. getCategoryListTreeFn()
  609. getAgreementTypeFn()
  610. if (questId.value) getDetail()
  611. nextTick(() => {
  612. initWaveCanvas()
  613. initStickyAndSync()
  614. })
  615. })
  616. onUnmounted(() => {
  617. if (waveAnimId) cancelAnimationFrame(waveAnimId)
  618. if (scrollHandler) window.removeEventListener('scroll', scrollHandler)
  619. })
  620. const submitForm = async () => {
  621. await ruleFormRef.value.validate(async (valid, fields) => {
  622. if (!valid) {
  623. DGTMessage.warning(fields[Object.keys(fields)[0]][0].message)
  624. return
  625. }
  626. let req = questId.value ? questEdit : questAdd
  627. isSubmiting.value = true
  628. await req(ruleForm).then(res => {
  629. if (res.code === 200) {
  630. DGTMessage.success(t('workflowTrade.publishSuccess'))
  631. goBack()
  632. }
  633. }).finally(() => {
  634. setTimeout(() => { isSubmiting.value = false }, 1000)
  635. })
  636. })
  637. }
  638. const goBack = () => window.history.back()
  639. const getCategoryListTreeFn = () => {
  640. getCategoryListTree().then(res => { categoryListTree.value = res.rows || [] })
  641. }
  642. const getDetail = () => {
  643. getQuestDetail({ id: questId.value }).then(res => {
  644. const detail = res.data || {}
  645. for (let key in ruleForm) ruleForm[key] = detail[key]
  646. nextTick(() => {
  647. if (ruleForm.categoryId1) categoryIdList.value = [ruleForm.categoryId1, ruleForm.categoryId2, ruleForm.categoryId3]
  648. })
  649. })
  650. }
  651. const release_hint = ref([])
  652. const release_rules = ref([])
  653. const sanitizedHint = computed(() => sanitizeHtml(release_hint.value))
  654. const sanitizedRules = computed(() => sanitizeHtml(release_rules.value))
  655. const getAgreementTypeFn = () => {
  656. getAgreementType({ agreementType: 'release_hint' }).then(res => { release_hint.value = res.data.content || '' })
  657. getAgreementType({ agreementType: 'release_rules' }).then(res => { release_rules.value = res.data.content || '' })
  658. }
  659. </script>
  660. <style scoped lang="scss">
  661. .container {
  662. // max-width: 1200px;
  663. width: 100%;
  664. min-width: auto;
  665. margin: 0 auto;
  666. padding: 0 24px;
  667. position: relative; z-index: 1;
  668. }
  669. /* ══════════════════════════════════════
  670. CSS 变量
  671. ══════════════════════════════════════ */
  672. :root {
  673. --wta-indigo: #4f46e5;
  674. --wta-violet: #7c3aed;
  675. --wta-purple: #a855f7;
  676. --wta-pink: #ec4899;
  677. }
  678. /* ══════════════════════════════════════
  679. 页面整体
  680. ══════════════════════════════════════ */
  681. .wta-page {
  682. min-height: calc(100vh - 60px);
  683. background: #f4f5ff;
  684. margin-bottom: 60px;
  685. }
  686. /* ══════════════════════════════════════
  687. HERO BANNER
  688. ══════════════════════════════════════ */
  689. .wta-hero {
  690. position: relative;
  691. overflow: hidden;
  692. background: linear-gradient(135deg, #312e81 0%, #4f46e5 25%, #7c3aed 55%, #a855f7 80%, #ec4899 100%);
  693. /* Gemini 审查建议:减少 padding-bottom,配合更小的 canvas 高度 */
  694. padding-bottom: 80px;
  695. }
  696. /* 网格背景 */
  697. .wta-hero__grid {
  698. position: absolute;
  699. inset: 0;
  700. background-image:
  701. linear-gradient(rgba(255,255,255,0.06) 1px, transparent 1px),
  702. linear-gradient(90deg, rgba(255,255,255,0.06) 1px, transparent 1px);
  703. background-size: 40px 40px;
  704. pointer-events: none;
  705. }
  706. /* 光晕球 */
  707. .wta-hero__orb {
  708. position: absolute;
  709. border-radius: 50%;
  710. filter: blur(70px);
  711. pointer-events: none;
  712. }
  713. .wta-hero__orb--1 {
  714. width: 500px; height: 500px;
  715. background: radial-gradient(circle, rgba(129,140,248,0.4), transparent 70%);
  716. top: -150px; right: 5%;
  717. }
  718. .wta-hero__orb--2 {
  719. width: 350px; height: 350px;
  720. background: radial-gradient(circle, rgba(240,171,252,0.35), transparent 70%);
  721. bottom: -100px; left: 3%;
  722. }
  723. .wta-hero__orb--3 {
  724. width: 200px; height: 200px;
  725. background: radial-gradient(circle, rgba(251,207,232,0.3), transparent 70%);
  726. top: 30px; left: 40%;
  727. }
  728. .wta-hero__breadcrumb-wrap {
  729. position: relative;
  730. z-index: 2;
  731. padding-top: 18px;
  732. :deep(.el-breadcrumb__inner),
  733. :deep(.el-breadcrumb__separator) { color: rgba(255,255,255,0.65) !important; }
  734. :deep(.el-breadcrumb__inner.is-link:hover) { color: #fff !important; }
  735. :deep(.el-breadcrumb__item:last-child .el-breadcrumb__inner) { color: #fff !important; font-weight: 600; }
  736. }
  737. .wta-hero__content {
  738. position: relative;
  739. z-index: 2;
  740. text-align: center;
  741. padding: 8px 20px 0;
  742. }
  743. .wta-hero__badge {
  744. display: inline-flex;
  745. align-items: center;
  746. gap: 8px;
  747. background: rgba(255,255,255,0.12);
  748. backdrop-filter: blur(10px);
  749. border: 1px solid rgba(255,255,255,0.2);
  750. border-radius: 100px;
  751. padding: 6px 18px;
  752. font-size: 13px;
  753. color: rgba(255,255,255,0.9);
  754. font-weight: 500;
  755. margin-bottom: 18px;
  756. }
  757. .wta-hero__badge-dot {
  758. width: 7px; height: 7px;
  759. border-radius: 50%;
  760. background: #a5f3fc;
  761. box-shadow: 0 0 10px #a5f3fc;
  762. animation: wta-pulse 2s ease-in-out infinite;
  763. }
  764. @keyframes wta-pulse {
  765. 0%, 100% { opacity: 1; transform: scale(1); }
  766. 50% { opacity: 0.5; transform: scale(0.8); }
  767. }
  768. .wta-hero__title {
  769. font-size: 40px;
  770. font-weight: 900;
  771. color: #fff;
  772. letter-spacing: -1px;
  773. margin-bottom: 12px;
  774. line-height: 1.15;
  775. text-shadow: 0 4px 24px rgba(0,0,0,0.2);
  776. }
  777. .wta-hero__subtitle {
  778. font-size: 16px;
  779. color: rgba(255,255,255,0.75);
  780. max-width: 480px;
  781. margin: 0 auto 28px;
  782. line-height: 1.6;
  783. }
  784. .wta-hero__stats {
  785. display: inline-flex;
  786. align-items: center;
  787. gap: 0;
  788. background: rgba(255,255,255,0.1);
  789. backdrop-filter: blur(12px);
  790. border: 1px solid rgba(255,255,255,0.18);
  791. border-radius: 16px;
  792. padding: 14px 28px;
  793. gap: 24px;
  794. }
  795. .wta-hero__stat {
  796. display: flex;
  797. flex-direction: column;
  798. align-items: center;
  799. gap: 2px;
  800. }
  801. .wta-hero__stat-num {
  802. font-size: 22px;
  803. font-weight: 800;
  804. color: #fff;
  805. letter-spacing: -0.5px;
  806. }
  807. .wta-hero__stat-label {
  808. font-size: 11px;
  809. color: rgba(255,255,255,0.65);
  810. font-weight: 500;
  811. }
  812. .wta-hero__stat-divider {
  813. width: 1px;
  814. height: 32px;
  815. background: rgba(255,255,255,0.2);
  816. }
  817. /* ══════════════════════════════════════
  818. 波浪 Canvas
  819. ══════════════════════════════════════ */
  820. .wta-hero__wave-canvas {
  821. position: absolute;
  822. bottom: 0;
  823. left: 0;
  824. width: 100%;
  825. /* Gemini 审查建议:缩小 canvas 高度,让波浪紧贴步骤条 */
  826. height: 100px;
  827. z-index: 1;
  828. pointer-events: none;
  829. display: block;
  830. }
  831. /* ══════════════════════════════════════
  832. 步骤进度条
  833. ══════════════════════════════════════ */
  834. .wta-steps-sentinel { height: 1px; }
  835. .wta-steps-wrap {
  836. background: #fff;
  837. border-bottom: 1px solid #e8eaf6;
  838. box-shadow: 0 2px 12px rgba(79,70,229,0.06);
  839. transition: box-shadow 0.3s;
  840. }
  841. .wta-steps-wrap--sticky {
  842. position: fixed;
  843. top: 60px;
  844. left: 0;
  845. right: 0;
  846. z-index: 100;
  847. box-shadow: 0 4px 24px rgba(79,70,229,0.13);
  848. backdrop-filter: blur(16px);
  849. background: rgba(255,255,255,0.95);
  850. }
  851. .wta-steps {
  852. display: flex;
  853. align-items: center;
  854. justify-content: center;
  855. padding: 16px 0;
  856. overflow-x: auto;
  857. }
  858. .wta-step {
  859. display: flex;
  860. align-items: center;
  861. gap: 8px;
  862. flex-shrink: 0;
  863. }
  864. .wta-step__circle {
  865. width: 28px; height: 28px;
  866. border-radius: 50%;
  867. border: 2px solid #e0e0f0;
  868. display: flex;
  869. align-items: center;
  870. justify-content: center;
  871. font-size: 12px;
  872. font-weight: 700;
  873. color: #9ca3af;
  874. background: #fff;
  875. transition: all 0.3s;
  876. flex-shrink: 0;
  877. }
  878. .wta-step__label {
  879. font-size: 13px;
  880. color: #9ca3af;
  881. font-weight: 500;
  882. white-space: nowrap;
  883. transition: color 0.3s;
  884. }
  885. .wta-step__line {
  886. width: 40px;
  887. height: 2px;
  888. background: #e0e0f0;
  889. margin: 0 4px;
  890. border-radius: 2px;
  891. transition: background 0.3s;
  892. }
  893. .wta-step__line--done { background: linear-gradient(90deg, #6366f1, #a855f7); }
  894. .wta-step--active .wta-step__circle {
  895. border-color: #6366f1;
  896. background: linear-gradient(135deg, #6366f1, #a855f7);
  897. color: #fff;
  898. box-shadow: 0 0 0 4px rgba(99,102,241,0.15);
  899. }
  900. .wta-step--active .wta-step__label { color: #4f46e5; font-weight: 700; }
  901. .wta-step--done .wta-step__circle {
  902. border-color: #6366f1;
  903. background: linear-gradient(135deg, #6366f1, #a855f7);
  904. color: #fff;
  905. }
  906. .wta-step--done .wta-step__label { color: #6366f1; }
  907. /* ══════════════════════════════════════
  908. 主体布局
  909. ══════════════════════════════════════ */
  910. .wta-body {
  911. padding: 28px 0 0;
  912. /* 覆盖全局 container 的 min-width,确保布局不溢出视口,不影响 sticky */
  913. &.container { min-width: unset; }
  914. }
  915. .wta-layout {
  916. display: flex;
  917. gap: 24px;
  918. align-items: flex-start;
  919. /* 确保不被 overflow 截断,保证 sticky 正常工作 */
  920. overflow: visible;
  921. }
  922. .wta-form-col { flex: 1; min-width: 0; }
  923. /* ══════════════════════════════════════
  924. 表单卡片
  925. ══════════════════════════════════════ */
  926. .wta-card {
  927. background: #fff;
  928. border-radius: 20px;
  929. border: 1px solid #eaecf8;
  930. box-shadow: 0 2px 16px rgba(79,70,229,0.06), 0 1px 3px rgba(0,0,0,0.03);
  931. overflow: hidden;
  932. transition: box-shadow 0.25s, transform 0.25s;
  933. &:hover {
  934. box-shadow: 0 8px 32px rgba(79,70,229,0.1), 0 2px 8px rgba(0,0,0,0.05);
  935. transform: translateY(-1px);
  936. }
  937. }
  938. .wta-card__header {
  939. display: flex;
  940. align-items: center;
  941. gap: 14px;
  942. padding: 20px 24px 16px;
  943. border-bottom: 1px solid #f0f0fa;
  944. background: linear-gradient(to right, #fafafe 0%, #fff 100%);
  945. position: relative;
  946. }
  947. .wta-card__header-icon {
  948. width: 40px; height: 40px;
  949. border-radius: 12px;
  950. display: flex;
  951. align-items: center;
  952. justify-content: center;
  953. background: linear-gradient(135deg, #ede9fe, #ddd6fe);
  954. color: #6366f1;
  955. flex-shrink: 0;
  956. }
  957. .wta-card__header-icon--purple { background: linear-gradient(135deg, #f3e8ff, #e9d5ff); color: #9333ea; }
  958. .wta-card__header-icon--green { background: linear-gradient(135deg, #d1fae5, #a7f3d0); color: #059669; }
  959. .wta-card__header-icon--pink { background: linear-gradient(135deg, #fce7f3, #fbcfe8); color: #db2777; }
  960. .wta-card__title {
  961. font-size: 17px;
  962. font-weight: 800;
  963. color: #1e1b4b;
  964. letter-spacing: 0.2px;
  965. line-height: 1.2;
  966. }
  967. .wta-card__subtitle {
  968. font-size: 12px;
  969. color: #9ca3af;
  970. margin-top: 2px;
  971. font-weight: 400;
  972. }
  973. .wta-card__step-badge {
  974. margin-left: auto;
  975. padding: 4px 12px;
  976. border-radius: 100px;
  977. font-size: 11px;
  978. font-weight: 700;
  979. background: linear-gradient(135deg, #ede9fe, #ddd6fe);
  980. color: #6366f1;
  981. letter-spacing: 0.5px;
  982. }
  983. .wta-card__step-badge--purple { background: linear-gradient(135deg, #f3e8ff, #e9d5ff); color: #9333ea; }
  984. .wta-card__step-badge--green { background: linear-gradient(135deg, #d1fae5, #a7f3d0); color: #059669; }
  985. .wta-card__step-badge--pink { background: linear-gradient(135deg, #fce7f3, #fbcfe8); color: #db2777; }
  986. .wta-card__body { padding: 22px 24px; }
  987. /* ══════════════════════════════════════
  988. 字段标签
  989. ══════════════════════════════════════ */
  990. .wta-field-label {
  991. display: flex;
  992. align-items: center;
  993. gap: 5px;
  994. flex-wrap: wrap;
  995. margin-bottom: 8px;
  996. }
  997. .wta-required {
  998. color: #ef4444;
  999. font-size: 16px;
  1000. font-weight: 800;
  1001. line-height: 1;
  1002. }
  1003. .wta-label-text {
  1004. font-size: 14px;
  1005. font-weight: 700;
  1006. color: #1e1b4b;
  1007. }
  1008. .wta-label-hint {
  1009. font-size: 12px;
  1010. color: #9ca3af;
  1011. font-weight: 400;
  1012. }
  1013. .wta-label-info-icon {
  1014. color: #a5b4fc;
  1015. cursor: pointer;
  1016. display: flex;
  1017. align-items: center;
  1018. transition: color 0.2s;
  1019. &:hover { color: #6366f1; }
  1020. }
  1021. /* ══════════════════════════════════════
  1022. 输入框带前缀图标
  1023. ══════════════════════════════════════ */
  1024. .wta-input-wrap {
  1025. position: relative;
  1026. width: 100%;
  1027. }
  1028. .wta-input-prefix-icon {
  1029. position: absolute;
  1030. left: 12px;
  1031. top: 50%;
  1032. transform: translateY(-50%);
  1033. z-index: 10;
  1034. color: #a5b4fc;
  1035. display: flex;
  1036. align-items: center;
  1037. pointer-events: none;
  1038. transition: color 0.2s;
  1039. }
  1040. .wta-input-prefix-icon--wechat { color: #22c55e; }
  1041. .wta-input-prefix-icon--email { color: #f59e0b; }
  1042. .wta-input {
  1043. width: 100%;
  1044. :deep(.el-input__wrapper) {
  1045. border-radius: 12px;
  1046. border: 1.5px solid #e0e0f0;
  1047. box-shadow: none !important;
  1048. background: #fafafe;
  1049. transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
  1050. &:hover { border-color: #a5b4fc; background: #fff; }
  1051. &.is-focus {
  1052. border-color: #6366f1;
  1053. box-shadow: 0 0 0 4px rgba(99,102,241,0.1) !important;
  1054. background: #fff;
  1055. }
  1056. }
  1057. }
  1058. .wta-input--has-prefix {
  1059. :deep(.el-input__wrapper) { padding-left: 36px; }
  1060. }
  1061. .wta-textarea {
  1062. width: 100%;
  1063. :deep(.el-textarea__inner) {
  1064. border-radius: 12px;
  1065. border: 1.5px solid #e0e0f0;
  1066. box-shadow: none !important;
  1067. background: #fafafe;
  1068. transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
  1069. resize: vertical;
  1070. font-size: 14px;
  1071. line-height: 1.7;
  1072. &:hover { border-color: #a5b4fc; background: #fff; }
  1073. &:focus {
  1074. border-color: #6366f1;
  1075. box-shadow: 0 0 0 4px rgba(99,102,241,0.1) !important;
  1076. background: #fff;
  1077. }
  1078. }
  1079. }
  1080. /* 字数进度条 */
  1081. .wta-char-progress {
  1082. display: flex;
  1083. align-items: center;
  1084. gap: 8px;
  1085. margin-top: 6px;
  1086. }
  1087. .wta-char-progress__bar {
  1088. flex: 1;
  1089. height: 3px;
  1090. background: #e8eaf6;
  1091. border-radius: 3px;
  1092. overflow: hidden;
  1093. }
  1094. .wta-char-progress__fill {
  1095. height: 100%;
  1096. border-radius: 3px;
  1097. background: linear-gradient(90deg, #6366f1, #a855f7);
  1098. transition: width 0.3s ease;
  1099. }
  1100. .wta-char-progress__text {
  1101. font-size: 11px;
  1102. color: #9ca3af;
  1103. white-space: nowrap;
  1104. font-weight: 500;
  1105. }
  1106. .wta-char-progress__text--warn { color: #f59e0b; }
  1107. /* 写作提示 */
  1108. .wta-writing-tips {
  1109. display: flex;
  1110. flex-wrap: wrap;
  1111. gap: 6px;
  1112. margin-bottom: 10px;
  1113. }
  1114. .wta-writing-tips__item {
  1115. display: inline-flex;
  1116. align-items: center;
  1117. gap: 4px;
  1118. padding: 3px 10px;
  1119. border-radius: 100px;
  1120. background: linear-gradient(135deg, #ede9fe, #e0e7ff);
  1121. font-size: 11px;
  1122. color: #6366f1;
  1123. font-weight: 600;
  1124. svg { color: #6366f1; flex-shrink: 0; }
  1125. }
  1126. /* ══════════════════════════════════════
  1127. 预算区域
  1128. ══════════════════════════════════════ */
  1129. .wta-budget-row {
  1130. display: flex;
  1131. align-items: flex-end;
  1132. gap: 12px;
  1133. margin-bottom: 8px;
  1134. }
  1135. .wta-budget-item { flex: 1; }
  1136. .wta-budget-range-icon {
  1137. padding-bottom: 10px;
  1138. flex-shrink: 0;
  1139. }
  1140. .wta-currency-badge {
  1141. font-size: 11px;
  1142. font-weight: 700;
  1143. color: #059669;
  1144. background: #d1fae5;
  1145. border-radius: 6px;
  1146. padding: 2px 7px;
  1147. margin-left: 4px;
  1148. }
  1149. .wta-budget-hint {
  1150. display: flex;
  1151. align-items: center;
  1152. gap: 5px;
  1153. font-size: 12px;
  1154. color: #6366f1;
  1155. background: #f0f0ff;
  1156. border-radius: 8px;
  1157. padding: 7px 12px;
  1158. margin-bottom: 4px;
  1159. }
  1160. /* ══════════════════════════════════════
  1161. 联系方式
  1162. ══════════════════════════════════════ */
  1163. .wta-contact-grid {
  1164. display: grid;
  1165. grid-template-columns: 1fr 1fr 1fr;
  1166. gap: 16px;
  1167. }
  1168. .wta-privacy-tip {
  1169. display: flex;
  1170. align-items: center;
  1171. gap: 6px;
  1172. font-size: 12px;
  1173. color: #6366f1;
  1174. background: linear-gradient(135deg, #f0f0ff, #f5f3ff);
  1175. border: 1px solid #e0e0f8;
  1176. border-radius: 10px;
  1177. padding: 8px 14px;
  1178. margin-top: 4px;
  1179. }
  1180. /* ══════════════════════════════════════
  1181. 操作按钮
  1182. ══════════════════════════════════════ */
  1183. .wta-actions {
  1184. display: flex;
  1185. align-items: center;
  1186. gap: 12px;
  1187. flex-wrap: wrap;
  1188. /* Gemini 建议:增加底部留白,避免与 footer 分割线太紧 */
  1189. margin-bottom: 60px;
  1190. }
  1191. .mt24 { margin-top: 24px; }
  1192. .wta-btn-primary {
  1193. display: inline-flex;
  1194. align-items: center;
  1195. gap: 8px;
  1196. padding: 13px 36px;
  1197. border-radius: 14px;
  1198. border: none;
  1199. background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 50%, #a855f7 100%);
  1200. color: #fff;
  1201. font-size: 15px;
  1202. font-weight: 800;
  1203. cursor: pointer;
  1204. box-shadow: 0 4px 20px rgba(99,102,241,0.4), 0 1px 4px rgba(0,0,0,0.1);
  1205. transition: all 0.25s;
  1206. letter-spacing: 0.3px;
  1207. position: relative;
  1208. overflow: hidden;
  1209. &::before {
  1210. content: '';
  1211. position: absolute;
  1212. inset: 0;
  1213. background: linear-gradient(135deg, rgba(255,255,255,0.15), transparent);
  1214. opacity: 0;
  1215. transition: opacity 0.25s;
  1216. }
  1217. &:hover:not(:disabled) {
  1218. transform: translateY(-2px);
  1219. box-shadow: 0 8px 28px rgba(99,102,241,0.5), 0 2px 8px rgba(0,0,0,0.12);
  1220. &::before { opacity: 1; }
  1221. }
  1222. &:active:not(:disabled) {
  1223. transform: translateY(0);
  1224. box-shadow: 0 2px 12px rgba(99,102,241,0.3);
  1225. }
  1226. &:disabled, &.wta-btn--loading {
  1227. opacity: 0.7;
  1228. cursor: not-allowed;
  1229. transform: none;
  1230. }
  1231. }
  1232. .wta-btn-spinner {
  1233. width: 16px; height: 16px;
  1234. border: 2px solid rgba(255,255,255,0.4);
  1235. border-top-color: #fff;
  1236. border-radius: 50%;
  1237. animation: wta-spin 0.7s linear infinite;
  1238. }
  1239. @keyframes wta-spin { to { transform: rotate(360deg); } }
  1240. .wta-btn-cancel {
  1241. display: inline-flex;
  1242. align-items: center;
  1243. gap: 8px;
  1244. padding: 13px 24px;
  1245. border-radius: 14px;
  1246. border: 1.5px solid #e0e0f0;
  1247. background: #fff;
  1248. color: #6b7280;
  1249. font-size: 15px;
  1250. font-weight: 600;
  1251. cursor: pointer;
  1252. transition: all 0.2s;
  1253. &:hover {
  1254. border-color: #a5b4fc;
  1255. color: #4f46e5;
  1256. background: #f5f3ff;
  1257. transform: translateY(-1px);
  1258. }
  1259. }
  1260. .wta-actions__tip {
  1261. font-size: 12px;
  1262. color: #9ca3af;
  1263. margin-left: 4px;
  1264. }
  1265. .wta-link {
  1266. color: #6366f1;
  1267. text-decoration: none;
  1268. font-weight: 600;
  1269. &:hover { text-decoration: underline; }
  1270. }
  1271. /* ══════════════════════════════════════
  1272. 右侧侧边栏
  1273. ══════════════════════════════════════ */
  1274. .wta-sidebar {
  1275. width: 320px;
  1276. flex-shrink: 0;
  1277. /* 让侧边栏拉伸到与左侧表单列等高,为 sticky 提供滚动空间 */
  1278. align-self: stretch;
  1279. }
  1280. .wta-sidebar__sticky {
  1281. position: sticky;
  1282. top: 136px; /* 导航60px + 步骤条61px + 间距15px */
  1283. }
  1284. .wta-sidebar-card {
  1285. background: #fff;
  1286. border-radius: 18px;
  1287. border: 1px solid #eaecf8;
  1288. box-shadow: 0 2px 14px rgba(79,70,229,0.07);
  1289. overflow: hidden;
  1290. }
  1291. .wta-sidebar-card__header {
  1292. display: flex;
  1293. align-items: center;
  1294. gap: 10px;
  1295. padding: 16px 18px 12px;
  1296. border-bottom: 1px solid #f0f0fa;
  1297. }
  1298. .wta-sidebar-card__icon-wrap {
  1299. width: 32px; height: 32px;
  1300. border-radius: 9px;
  1301. display: flex;
  1302. align-items: center;
  1303. justify-content: center;
  1304. flex-shrink: 0;
  1305. }
  1306. .wta-sidebar-card__icon-wrap--tip { background: linear-gradient(135deg, #ede9fe, #ddd6fe); color: #7c3aed; }
  1307. .wta-sidebar-card__icon-wrap--rules { background: linear-gradient(135deg, #dbeafe, #bfdbfe); color: #2563eb; }
  1308. .wta-sidebar-card__icon-wrap--guarantee { background: linear-gradient(135deg, #d1fae5, #a7f3d0); color: #059669; }
  1309. .wta-sidebar-card__title {
  1310. font-size: 14px;
  1311. font-weight: 800;
  1312. color: #1e1b4b;
  1313. }
  1314. .wta-sidebar-content {
  1315. padding: 14px 18px;
  1316. color: #4b5563;
  1317. line-height: 1.75;
  1318. font-size: 13px;
  1319. :deep(ol), :deep(ul) {
  1320. padding-left: 18px;
  1321. li {
  1322. margin-bottom: 6px;
  1323. &::marker { color: #6366f1; }
  1324. }
  1325. }
  1326. }
  1327. /* 平台保障列表 */
  1328. .wta-guarantee-list {
  1329. padding: 14px 18px;
  1330. display: flex;
  1331. flex-direction: column;
  1332. gap: 10px;
  1333. }
  1334. .wta-guarantee-item {
  1335. display: flex;
  1336. align-items: center;
  1337. gap: 10px;
  1338. font-size: 13px;
  1339. color: #374151;
  1340. font-weight: 500;
  1341. }
  1342. .wta-guarantee-item__icon {
  1343. width: 22px; height: 22px;
  1344. border-radius: 6px;
  1345. display: flex;
  1346. align-items: center;
  1347. justify-content: center;
  1348. flex-shrink: 0;
  1349. }
  1350. /* ══════════════════════════════════════
  1351. Element Plus 覆盖
  1352. ══════════════════════════════════════ */
  1353. :deep(.el-form-item__label) {
  1354. font-size: 14px;
  1355. font-weight: 700;
  1356. color: #1e1b4b;
  1357. padding-bottom: 4px;
  1358. /* 隐藏 Element Plus 自动注入的必填星号 */
  1359. &::before { display: none !important; content: '' !important; }
  1360. }
  1361. :deep(.el-form-item) { margin-bottom: 20px; }
  1362. :deep(.el-cascader) {
  1363. width: 100%;
  1364. .el-input__wrapper {
  1365. border-radius: 12px;
  1366. border: 1.5px solid #e0e0f0;
  1367. box-shadow: none !important;
  1368. background: #fafafe;
  1369. padding-left: 36px;
  1370. &:hover { border-color: #a5b4fc; background: #fff; }
  1371. &.is-focus {
  1372. border-color: #6366f1;
  1373. box-shadow: 0 0 0 4px rgba(99,102,241,0.1) !important;
  1374. background: #fff;
  1375. }
  1376. }
  1377. }
  1378. :deep(.el-date-editor) {
  1379. width: 100% !important;
  1380. .el-input__wrapper {
  1381. border-radius: 12px;
  1382. border: 1.5px solid #e0e0f0;
  1383. box-shadow: none !important;
  1384. background: #fafafe;
  1385. padding-left: 36px;
  1386. &:hover { border-color: #a5b4fc; background: #fff; }
  1387. &.is-focus {
  1388. border-color: #6366f1;
  1389. box-shadow: 0 0 0 4px rgba(99,102,241,0.1) !important;
  1390. background: #fff;
  1391. }
  1392. }
  1393. }
  1394. /* 辅助间距 */
  1395. .mt16 { margin-top: 16px; }
  1396. .mt20 { margin-top: 20px; }
  1397. .mb8 { margin-bottom: 8px; }
  1398. </style>