index.vue 47 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148
  1. <template>
  2. <div class="page-container">
  3. <div class="create-layout">
  4. <!-- 左侧:下单填写区 -->
  5. <div class="form-container">
  6. <!-- 1. 服务类型选择 -->
  7. <div class="type-selection">
  8. <div
  9. v-for="item in serviceList"
  10. :key="item.type"
  11. class="type-card"
  12. :class="[item.type, { active: form.type === item.type }]"
  13. @click="handleTypeChange(item.type)"
  14. >
  15. <div class="icon-box"><el-icon><component :is="item.icon" /></el-icon></div>
  16. <div class="text">
  17. <div class="type-name">{{ item.name }}</div>
  18. <div class="type-desc">{{ item.desc }}</div>
  19. </div>
  20. </div>
  21. </div>
  22. <!-- 2. 基础信息:门店与宠主 -->
  23. <el-card shadow="never" class="section-card">
  24. <template #header>
  25. <div class="card-title">
  26. <span class="step-num">02</span> 基础信息
  27. </div>
  28. </template>
  29. <div class="card-body">
  30. <el-form label-position="top" class="base-form">
  31. <el-row :gutter="20">
  32. <el-col :span="12">
  33. <el-form-item>
  34. <template #label>
  35. <div style="display:flex; align-items:center; height: 24px;">
  36. <span>服务门店 (平台代下单)</span>
  37. </div>
  38. </template>
  39. <el-select v-model="form.merchantId" placeholder="请选择商户门店" size="large" style="width: 100%" filterable>
  40. <el-option v-for="m in merchants" :key="m.id" :label="m.name" :value="m.id" />
  41. </el-select>
  42. </el-form-item>
  43. </el-col>
  44. <el-col :span="12">
  45. <el-form-item>
  46. <template #label>
  47. <div style="display:flex; justify-content:space-between; align-items:center; width:100%; height: 24px;">
  48. <span>宠主用户</span>
  49. <el-button type="primary" plain size="small" @click="openAddUser" icon="Plus" style="margin-left: 15px;">添加用户</el-button>
  50. </div>
  51. </template>
  52. <el-select
  53. v-model="form.userId"
  54. placeholder="搜索手机号/姓名"
  55. size="large"
  56. style="width: 100%"
  57. filterable
  58. remote
  59. :remote-method="searchUser"
  60. :loading="userLoading"
  61. @change="handleUserChange"
  62. >
  63. <el-option v-for="u in userOptions" :key="u.id" :label="u.name + ' - ' + u.phone" :value="u.id" />
  64. </el-select>
  65. </el-form-item>
  66. </el-col>
  67. </el-row>
  68. <el-form-item label="选择宠物" v-if="form.userId">
  69. <div class="pet-select-row">
  70. <div
  71. v-for="p in currentPets"
  72. :key="p.id"
  73. class="pet-card"
  74. :class="{ active: form.petId === p.id }"
  75. @click="form.petId = p.id"
  76. >
  77. <el-avatar :size="48" :src="p.avatar" shape="square" style="border-radius: 6px;">{{ p.name.charAt(0) }}</el-avatar>
  78. <div class="pet-info">
  79. <div class="name">{{ p.name }}</div>
  80. <div class="sub">{{ p.breed }}</div>
  81. </div>
  82. <div class="check-mark" v-if="form.petId === p.id"><el-icon><Check /></el-icon></div>
  83. </div>
  84. <!-- Add Button Card (Last Item in Grid) -->
  85. <div class="pet-card add-card" @click="openAddPet">
  86. <el-icon :size="24"><Plus /></el-icon>
  87. <span style="font-size: 15px; font-weight: bold;">新增宠物</span>
  88. </div>
  89. </div>
  90. </el-form-item>
  91. </el-form>
  92. </div>
  93. </el-card>
  94. <!-- 3. 业务详情表单 -->
  95. <el-card shadow="never" class="section-card form-card" v-if="form.type">
  96. <template #header>
  97. <div class="card-title">
  98. <span class="step-num">03</span>
  99. {{ getStepTitle(form.type) }}
  100. </div>
  101. </template>
  102. <div class="card-body">
  103. <!-- 服务套餐信息 -->
  104. <el-form-item label="团购套餐">
  105. <el-input v-model="form.groupBuyPackage" placeholder="请输入团购套餐名称 (选填)" clearable />
  106. </el-form-item>
  107. <div class="divider"></div>
  108. <!-- A. 宠物接送表单 -->
  109. <div v-show="form.type === 'transport'" class="business-form">
  110. <el-form-item label="接送模式">
  111. <el-radio-group v-model="form.transport.subType" size="large" @change="calcPrice('transport')">
  112. <el-radio-button label="round">往返接送</el-radio-button>
  113. <el-radio-button label="pick">单程接 (到店)</el-radio-button>
  114. <el-radio-button label="drop">单程送 (回家)</el-radio-button>
  115. </el-radio-group>
  116. </el-form-item>
  117. <div class="route-box">
  118. <!-- 接宠段 -->
  119. <div class="route-segment" v-if="['round', 'pick'].includes(form.transport.subType)">
  120. <div class="seg-badge start">接</div>
  121. <div class="seg-content">
  122. <el-row :gutter="10">
  123. <el-col :span="8">
  124. <el-cascader v-model="form.transport.pickRegion" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
  125. </el-col>
  126. <el-col :span="16">
  127. <el-input v-model="form.transport.pickDetail" placeholder="详细地址 (街道/门牌号)" prefix-icon="Location" />
  128. </el-col>
  129. </el-row>
  130. <el-row :gutter="10">
  131. <el-col :span="12"><el-input v-model="form.transport.pickContact" placeholder="联系人" /></el-col>
  132. <el-col :span="12"><el-input v-model="form.transport.pickPhone" placeholder="电话" /></el-col>
  133. </el-row>
  134. <el-row :gutter="10">
  135. <el-col :span="24">
  136. <el-date-picker v-model="form.transport.pickTime" type="datetime" placeholder="选择接宠时间" style="width: 100%" />
  137. </el-col>
  138. </el-row>
  139. </div>
  140. </div>
  141. <!-- 门店中转标识 -->
  142. <div class="route-connector">
  143. <div class="line"></div>
  144. <div class="store-node"><el-icon><Shop /></el-icon> 服务门店</div>
  145. <div class="line"></div>
  146. </div>
  147. <!-- 送回段 -->
  148. <div class="route-segment" v-if="['round', 'drop'].includes(form.transport.subType)">
  149. <div class="seg-badge end">送</div>
  150. <div class="seg-content">
  151. <el-row :gutter="10">
  152. <el-col :span="8">
  153. <el-cascader v-model="form.transport.dropRegion" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
  154. </el-col>
  155. <el-col :span="16">
  156. <el-input v-model="form.transport.dropDetail" placeholder="详细地址" prefix-icon="Location" />
  157. </el-col>
  158. </el-row>
  159. <el-row :gutter="10">
  160. <el-col :span="12"><el-input v-model="form.transport.dropContact" placeholder="联系人" /></el-col>
  161. <el-col :span="12"><el-input v-model="form.transport.dropPhone" placeholder="电话" /></el-col>
  162. </el-row>
  163. <el-row :gutter="10">
  164. <el-col :span="24">
  165. <el-date-picker v-model="form.transport.dropTime" type="datetime" placeholder="预计送回时间 (可选)" style="width: 100%" />
  166. </el-col>
  167. </el-row>
  168. </div>
  169. </div>
  170. </div>
  171. </div>
  172. <!-- B. 上门喂遛表单 -->
  173. <div v-show="form.type === 'feeding'" class="business-form">
  174. <div style="margin-bottom: 20px;">
  175. <div class="section-label">上门服务地址</div>
  176. <el-row :gutter="10">
  177. <el-col :span="8">
  178. <el-cascader v-model="form.feeding.region" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
  179. </el-col>
  180. <el-col :span="16">
  181. <el-input v-model="form.feeding.addressDetail" placeholder="详细地址 (街道/门牌号)" prefix-icon="Location" />
  182. </el-col>
  183. </el-row>
  184. </div>
  185. <div style="margin-bottom: 20px;">
  186. <div class="section-label" style="display:flex; align-items:center; margin-bottom:10px;">
  187. 预约服务时间
  188. <el-tag type="info" size="small" style="margin-left:10px;">共 {{ form.feeding.appointments.length }} 次</el-tag>
  189. </div>
  190. <div v-for="(item, index) in form.feeding.appointments" :key="index" style="display:flex; align-items:center; margin-bottom:10px;">
  191. <span style="width:30px; color:#999; font-size:12px; font-weight:bold;">{{ index + 1 }}.</span>
  192. <el-date-picker
  193. v-model="item.startTime"
  194. type="datetime"
  195. placeholder="开始时间"
  196. style="width: 200px; margin-right: 5px;"
  197. format="YYYY-MM-DD HH:mm"
  198. />
  199. <span style="margin:0 5px; color:#999;">~</span>
  200. <el-date-picker
  201. v-model="item.endTime"
  202. type="datetime"
  203. placeholder="结束时间 (可选)"
  204. style="width: 200px; margin-right: 15px;"
  205. format="YYYY-MM-DD HH:mm"
  206. />
  207. <div style="display:flex; gap:8px; margin-left:5px;">
  208. <el-button v-if="index === form.feeding.appointments.length - 1" type="primary" circle size="small" icon="Plus" @click="addAppointment('feeding')" />
  209. <el-button v-if="form.feeding.appointments.length > 1" type="danger" circle size="small" icon="Minus" @click="removeAppointment('feeding', index)" plain />
  210. </div>
  211. </div>
  212. </div>
  213. <div class="remark-section">
  214. <div class="section-label">家庭服务及宠物档案备注</div>
  215. <el-row :gutter="15">
  216. <el-col :span="12"><el-input v-model="form.feeding.area" placeholder="宠物活动区域" /></el-col>
  217. <el-col :span="12"><el-input v-model="form.feeding.itemLoc" placeholder="物品存放位置" /></el-col>
  218. <el-col :span="12" style="margin-top:10px"><el-input v-model="form.feeding.cleanLoc" placeholder="清洗位置" /></el-col>
  219. <el-col :span="12" style="margin-top:10px"><el-input v-model="form.feeding.foodAmount" placeholder="喂食量标准" /></el-col>
  220. <el-col :span="24" style="margin-top:10px"><el-input v-model="form.feeding.other" type="textarea" :rows="2" placeholder="其他注意事项" /></el-col>
  221. </el-row>
  222. </div>
  223. </div>
  224. <!-- C. 上门洗护表单 -->
  225. <div v-show="form.type === 'washing'" class="business-form">
  226. <div style="margin-bottom: 20px;">
  227. <div class="section-label">上门服务地址</div>
  228. <el-row :gutter="10">
  229. <el-col :span="8">
  230. <el-cascader v-model="form.washing.region" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
  231. </el-col>
  232. <el-col :span="16">
  233. <el-input v-model="form.washing.addressDetail" placeholder="详细地址 (街道/门牌号)" prefix-icon="Location" />
  234. </el-col>
  235. </el-row>
  236. </div>
  237. <div style="margin-bottom: 20px;">
  238. <div class="section-label" style="display:flex; align-items:center; margin-bottom:10px;">
  239. 预约服务时间
  240. <el-tag type="info" size="small" style="margin-left:10px;">共 {{ form.washing.appointments.length }} 次</el-tag>
  241. </div>
  242. <div v-for="(item, index) in form.washing.appointments" :key="index" style="display:flex; align-items:center; margin-bottom:10px;">
  243. <span style="width:30px; color:#999; font-size:12px; font-weight:bold;">{{ index + 1 }}.</span>
  244. <el-date-picker
  245. v-model="item.startTime"
  246. type="datetime"
  247. placeholder="开始时间"
  248. style="width: 200px; margin-right: 5px;"
  249. format="YYYY-MM-DD HH:mm"
  250. />
  251. <span style="margin:0 5px; color:#999;">~</span>
  252. <el-date-picker
  253. v-model="item.endTime"
  254. type="datetime"
  255. placeholder="结束时间 (可选)"
  256. style="width: 200px; margin-right: 15px;"
  257. format="YYYY-MM-DD HH:mm"
  258. />
  259. <div style="display:flex; gap:8px; margin-left:5px;">
  260. <el-button v-if="index === form.washing.appointments.length - 1" type="primary" circle size="small" icon="Plus" @click="addAppointment('washing')" />
  261. <el-button v-if="form.washing.appointments.length > 1" type="danger" circle size="small" icon="Minus" @click="removeAppointment('washing', index)" plain />
  262. </div>
  263. </div>
  264. </div>
  265. <div class="remark-section">
  266. <div class="section-label">服务备注及宠物状态</div>
  267. <el-row :gutter="15">
  268. <el-col :span="8">
  269. <el-select v-model="form.washing.petStatus" placeholder="宠物应激状态" style="width:100%">
  270. <el-option label="性格温顺" value="calm" />
  271. <!-- ... options ... -->
  272. <el-option label="胆小怕人" value="shy" />
  273. <el-option label="容易应激" value="stress" />
  274. <el-option label="有攻击性" value="aggressive" />
  275. </el-select>
  276. </el-col>
  277. <el-col :span="8"><el-input v-model="form.washing.cleanLoc" placeholder="清洗位置" /></el-col>
  278. <el-col :span="8"><el-input v-model="form.washing.toolLoc" placeholder="工具/水源位置" /></el-col>
  279. <el-col :span="24" style="margin-top:10px"><el-input v-model="form.washing.other" type="textarea" :rows="2" placeholder="其他注意事项" /></el-col>
  280. </el-row>
  281. </div>
  282. </div>
  283. </div>
  284. </el-card>
  285. </div>
  286. <!-- 右侧:收银台概览 -->
  287. <div class="summary-sidebar">
  288. <div class="summary-panel">
  289. <div class="summary-header">订单概览</div>
  290. <div class="summary-content">
  291. <div class="row" v-if="selectedMerchantName">
  292. <span class="label">服务门店</span>
  293. <span class="value">{{ selectedMerchantName }}</span>
  294. </div>
  295. <div class="row" v-if="selectedUserName">
  296. <span class="label">客户</span>
  297. <span class="value">{{ selectedUserName }}</span>
  298. </div>
  299. <div class="row" v-if="selectedPetName">
  300. <span class="label">服务对象</span>
  301. <span class="value action-text">{{ selectedPetName }} ({{ selectedPetBreed }})</span>
  302. </div>
  303. <div class="divider"></div>
  304. <div class="service-preview" v-if="form.type">
  305. <div class="preview-title">{{ getTypeName(form.type) }}</div>
  306. <!-- 套餐显示 -->
  307. <div class="preview-detail" v-if="selectedPkgName">
  308. <div style="font-weight:bold; color:#409eff">{{ selectedPkgName }}</div>
  309. </div>
  310. <div class="preview-detail" v-else>
  311. <div style="color:#e6a23c">非服务套餐 (单次)</div>
  312. </div>
  313. <!-- 接送预览 -->
  314. <div v-if="form.type === 'transport'" class="preview-detail">
  315. <div>{{ form.transport.subType === 'round' ? '往返接送' : (form.transport.subType === 'pick' ? '单程接' : '单程送') }}</div>
  316. <div class="minor">接: {{ form.transport.pickTime ? formatTime(form.transport.pickTime) : '未选时间' }}</div>
  317. <div class="minor" v-if="form.transport.subType !== 'pick'">送: {{ form.transport.dropTime ? formatTime(form.transport.dropTime) : '未选' }}</div>
  318. </div>
  319. </div>
  320. </div>
  321. <div class="summary-footer">
  322. <el-button type="primary" size="large" class="submit-btn" :disabled="!canSubmit" @click="handleSubmit">
  323. 立即下单
  324. </el-button>
  325. </div>
  326. </div>
  327. </div>
  328. </div>
  329. <!-- Dialogs -->
  330. <!-- Add User Dialog -->
  331. <el-dialog v-model="userDialogVisible" title="新增用户" width="700px" destroy-on-close append-to-body class="add-user-dialog">
  332. <el-form :model="userForm" label-width="90px" class="user-form">
  333. <div style="display: flex; justify-content: center; align-items: center; gap: 20px; margin-bottom: 30px;">
  334. <el-upload action="#" :show-file-list="false" :auto-upload="false" :on-change="handleUserAvatarChange">
  335. <el-avatar :size="80" :src="userForm.avatar || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'" style="cursor: pointer; border: 2px solid #e4e7ed;" />
  336. </el-upload>
  337. <el-button type="primary" link @click="">点击修改头像</el-button>
  338. </div>
  339. <div class="form-section-header">基本资料</div>
  340. <el-row :gutter="30">
  341. <el-col :span="12">
  342. <el-form-item label="录入来源">
  343. <el-select v-model="userForm.source" style="width: 100%" filterable allow-create default-first-option>
  344. <el-option label="平台录入" value="平台录入" />
  345. <el-option label="萌它宠物连锁录入" value="萌它宠物连锁录入" />
  346. </el-select>
  347. </el-form-item>
  348. </el-col>
  349. <el-col :span="12">
  350. <el-form-item label="所属区域">
  351. <el-select v-model="userForm.area" style="width: 100%" filterable allow-create default-first-option placeholder="请选择或输入">
  352. <el-option label="朝阳区" value="朝阳区" />
  353. <el-option label="海淀区" value="海淀区" />
  354. </el-select>
  355. </el-form-item>
  356. </el-col>
  357. <el-col :span="12">
  358. <el-form-item label="姓名" required><el-input v-model="userForm.name" placeholder="请输入姓名" /></el-form-item>
  359. </el-col>
  360. <el-col :span="12">
  361. <el-form-item label="电话" required><el-input v-model="userForm.phone" placeholder="请输入电话" /></el-form-item>
  362. </el-col>
  363. <el-col :span="12">
  364. <el-form-item label="性别">
  365. <el-radio-group v-model="userForm.gender">
  366. <el-radio label="男">男</el-radio>
  367. <el-radio label="女">女</el-radio>
  368. </el-radio-group>
  369. </el-form-item>
  370. </el-col>
  371. </el-row>
  372. <div class="form-section-header">居住信息</div>
  373. <el-row :gutter="30">
  374. <el-col :span="24">
  375. <el-form-item label="所在地区">
  376. <el-cascader v-model="userForm.region" :options="pcaOptions" placeholder="请选择省/市/区" style="width: 100%" />
  377. </el-form-item>
  378. </el-col>
  379. <el-col :span="24">
  380. <el-form-item label="详细住址"><el-input v-model="userForm.detailAddress" placeholder="请输入街道/门牌号" /></el-form-item>
  381. </el-col>
  382. <el-col :span="12">
  383. <el-form-item label="房屋类型">
  384. <el-radio-group v-model="userForm.houseType">
  385. <el-radio label="stairs">楼梯</el-radio>
  386. <el-radio label="elevator">电梯</el-radio>
  387. </el-radio-group>
  388. </el-form-item>
  389. </el-col>
  390. <el-col :span="12">
  391. <el-form-item label="入门方式">
  392. <el-radio-group v-model="userForm.entryMethod">
  393. <el-radio label="password">密码开门</el-radio>
  394. <el-radio label="key">钥匙开门</el-radio>
  395. </el-radio-group>
  396. </el-form-item>
  397. </el-col>
  398. <el-col :span="12" v-if="userForm.entryMethod === 'password'">
  399. <el-form-item label="开门密码">
  400. <el-input v-model="userForm.entryPassword" placeholder="请输入密码" />
  401. </el-form-item>
  402. </el-col>
  403. <el-col :span="12" v-if="userForm.entryMethod === 'key'">
  404. <el-form-item label="钥匙位置">
  405. <el-input v-model="userForm.keyLocation" placeholder="如:地毯下" />
  406. </el-form-item>
  407. </el-col>
  408. </el-row>
  409. <div class="form-section-header">其他</div>
  410. <el-row :gutter="30">
  411. <el-col :span="24">
  412. <el-form-item label="用户标签">
  413. <el-select v-model="userSelectedTagIds" multiple placeholder="选择标签" style="width: 100%">
  414. <el-option v-for="tag in allUserTags" :key="tag.id" :label="tag.name" :value="tag.id">
  415. <el-tag :type="tag.type" effect="light" size="small">{{ tag.name }}</el-tag>
  416. </el-option>
  417. </el-select>
  418. </el-form-item>
  419. </el-col>
  420. <el-col :span="24">
  421. <el-form-item label="备注说明"><el-input type="textarea" v-model="userForm.remark" rows="3" /></el-form-item>
  422. </el-col>
  423. </el-row>
  424. </el-form>
  425. <template #footer>
  426. <div style="text-align: center; margin-top: 20px;">
  427. <el-button @click="userDialogVisible = false" size="large" style="width: 120px;">取消</el-button>
  428. <el-button type="primary" @click="submitUser" size="large" style="width: 120px;">保存</el-button>
  429. </div>
  430. </template>
  431. </el-dialog>
  432. <el-dialog v-model="petDialogVisible" title="宠物档案详情" width="800px" top="10vh" class="pet-profile-dialog">
  433. <el-tabs v-model="activePetTab" class="pet-tabs">
  434. <el-tab-pane label="基本信息" name="basic">
  435. <div class="pet-form-content">
  436. <!-- Avatar Upload -->
  437. <div class="avatar-col">
  438. <el-upload
  439. class="avatar-uploader"
  440. action="#"
  441. :show-file-list="false"
  442. :auto-upload="false"
  443. :on-change="handleAvatarChange"
  444. >
  445. <img v-if="petForm.avatar" :src="petForm.avatar" class="avatar" />
  446. <el-icon v-else class="avatar-uploader-icon" :size="28" color="#8c939d"><Plus /></el-icon>
  447. </el-upload>
  448. <div style="font-size:12px; color:#999; margin-top:8px; text-align:center">点击上传头像</div>
  449. </div>
  450. <!-- Form Fields -->
  451. <el-form :model="petForm" label-width="80px" class="inner-form">
  452. <el-row :gutter="20">
  453. <el-col :span="12">
  454. <el-form-item label="宠物姓名" required>
  455. <el-input v-model="petForm.name" placeholder="请输入" />
  456. </el-form-item>
  457. </el-col>
  458. <el-col :span="12">
  459. <el-form-item label="所属主人" required>
  460. <el-select v-model="form.userId" disabled placeholder="选择主人" style="width:100%">
  461. <el-option v-for="u in userOptions" :key="u.id" :label="u.name" :value="u.id" />
  462. </el-select>
  463. </el-form-item>
  464. </el-col>
  465. </el-row>
  466. <el-row :gutter="20">
  467. <el-col :span="12">
  468. <el-form-item label="性别">
  469. <el-radio-group v-model="petForm.gender">
  470. <el-radio label="MM">公</el-radio>
  471. <el-radio label="GG">母</el-radio>
  472. </el-radio-group>
  473. </el-form-item>
  474. </el-col>
  475. <el-col :span="12">
  476. <el-form-item label="品种">
  477. <el-select v-model="petForm.breed" placeholder="请选择品种" style="width:100%">
  478. <el-option label="金毛" value="金毛" />
  479. <el-option label="布偶" value="布偶" />
  480. <el-option label="边牧" value="边牧" />
  481. </el-select>
  482. </el-form-item>
  483. </el-col>
  484. </el-row>
  485. <el-row :gutter="20">
  486. <el-col :span="12">
  487. <el-form-item label="体型">
  488. <el-select v-model="petForm.bodyType" placeholder="选择体型" style="width:100%">
  489. <el-option label="小型" value="small" />
  490. <el-option label="中型" value="medium" />
  491. <el-option label="大型" value="large" />
  492. </el-select>
  493. </el-form-item>
  494. </el-col>
  495. <el-col :span="12">
  496. <el-form-item label="体重(kg)">
  497. <el-row :gutter="10">
  498. <el-col :span="12"><el-input-number v-model="petForm.weight" :min="0" :step="0.1" controls="false" style="width:100%" /></el-col>
  499. <el-col :span="12"></el-col>
  500. </el-row>
  501. </el-form-item>
  502. </el-col>
  503. </el-row>
  504. <el-row :gutter="20">
  505. <el-col :span="12">
  506. <el-form-item label="年龄(岁)">
  507. <el-input-number v-model="petForm.age" :min="0" style="width:100%" />
  508. </el-form-item>
  509. </el-col>
  510. </el-row>
  511. <el-form-item label="性格关键词">
  512. <el-input v-model="petForm.keywords" placeholder="如:活泼、粘人" />
  513. </el-form-item>
  514. <el-form-item label="萌宠性格">
  515. <el-input v-model="petForm.desc" type="textarea" placeholder="详细描述" :rows="2" />
  516. </el-form-item>
  517. <el-form-item label="宠物标签">
  518. <el-select v-model="petForm.tags" multiple placeholder="选择标签" style="width:100%">
  519. <el-option label="绝育" value="1" />
  520. <el-option label="疫苗齐全" value="2" />
  521. </el-select>
  522. </el-form-item>
  523. </el-form>
  524. </div>
  525. </el-tab-pane>
  526. <el-tab-pane label="家庭信息" name="family">
  527. <el-form :model="petForm" label-width="120px">
  528. <el-form-item label="新来家庭时间">
  529. <el-date-picker v-model="petForm.arrivalTime" type="date" placeholder="选择日期" style="width: 100%" />
  530. </el-form-item>
  531. <el-form-item label="家庭房屋类型">
  532. <el-radio-group v-model="petForm.houseType">
  533. <el-radio label="stairs">楼梯</el-radio>
  534. <el-radio label="elevator">电梯</el-radio>
  535. </el-radio-group>
  536. </el-form-item>
  537. <el-form-item label="入门方式">
  538. <el-radio-group v-model="petForm.entryMethod">
  539. <el-radio label="password">密码开门</el-radio>
  540. <el-radio label="key">钥匙开门</el-radio>
  541. </el-radio-group>
  542. </el-form-item>
  543. <el-form-item label="密码" v-if="petForm.entryMethod === 'password'">
  544. <el-input v-model="petForm.entryPassword" placeholder="请输入门锁密码" />
  545. </el-form-item>
  546. <el-form-item label="钥匙位置" v-if="petForm.entryMethod === 'key'">
  547. <el-input v-model="petForm.keyLocation" placeholder="请输入钥匙存放位置" />
  548. </el-form-item>
  549. </el-form>
  550. </el-tab-pane>
  551. <el-tab-pane label="健康状况" name="health">
  552. <el-form :model="petForm" label-width="120px">
  553. <el-form-item label="健康状态">
  554. <el-radio-group v-model="petForm.healthStatus">
  555. <el-radio label="健康">健康</el-radio>
  556. <el-radio label="亚健康">亚健康</el-radio>
  557. <el-radio label="疾病">疾病</el-radio>
  558. </el-radio-group>
  559. </el-form-item>
  560. <el-form-item label="是否有攻击倾向">
  561. <el-switch v-model="petForm.aggression" active-text="是" inactive-text="否" />
  562. </el-form-item>
  563. <el-form-item label="疫苗情况">
  564. <el-input v-model="petForm.vaccine" type="textarea" placeholder="记录疫苗接种情况" />
  565. </el-form-item>
  566. <el-form-item label="既往病史">
  567. <el-input v-model="petForm.medicalHistory" type="textarea" placeholder="如有病史请记录" />
  568. </el-form-item>
  569. <el-form-item label="过敏史">
  570. <el-input v-model="petForm.allergies" type="textarea" placeholder="如有过敏源请记录" />
  571. </el-form-item>
  572. </el-form>
  573. </el-tab-pane>
  574. </el-tabs>
  575. <template #footer>
  576. <el-button @click="petDialogVisible = false">取消</el-button>
  577. <el-button type="primary" @click="submitPet">保存</el-button>
  578. </template>
  579. </el-dialog>
  580. </div>
  581. </template>
  582. <script setup>
  583. import { ref, reactive, computed, onMounted, watch } from 'vue'
  584. import { ElMessage } from 'element-plus'
  585. // --- Mock Data ---
  586. const merchants = ref([
  587. { id: 1, name: '萌它宠物三里屯店' },
  588. { id: 2, name: '宠爱国际动物医院' }
  589. ])
  590. const userOptions = ref([
  591. { id: 101, name: '张三', phone: '13812345678' },
  592. { id: 102, name: '李四', phone: '13987654321' }
  593. ])
  594. const mockPets = {
  595. 101: [
  596. { id: 1, name: '旺财', breed: '金毛', avatar: '', region: ['北京市', '市辖区', '朝阳区'], address: '三里屯SOHO A座 1001' },
  597. { id: 2, name: '咪咪', breed: '布偶', avatar: '', region: ['北京市', '市辖区', '朝阳区'], address: '三里屯SOHO A座 1001' }
  598. ],
  599. 102: [
  600. { id: 3, name: '奥利奥', breed: '边牧', avatar: '', region: ['上海市', '市辖区', '浦东新区'], address: '陆家嘴一号院 5-502' }
  601. ]
  602. }
  603. const serviceList = [
  604. { type: 'transport', name: '宠物接送', icon: 'Van', desc: '专车接送 · 全程监护', basePrice: 35 },
  605. { type: 'feeding', name: '上门喂遛', icon: 'Food', desc: '喂食添水 · 陪玩遛狗', basePrice: 68 },
  606. { type: 'washing', name: '上门洗护', icon: 'Soap', desc: '专业设备 · 深度清洁', basePrice: 88 }
  607. ]
  608. const allPackages = [
  609. { id: 10, type: 'transport', name: '包月接送套餐', price: 0 },
  610. { id: 11, type: 'feeding', name: '基础喂猫套餐', price: 0 },
  611. { id: 12, type: 'feeding', name: '深度陪玩套餐', price: 0 },
  612. { id: 13, type: 'washing', name: '精致洗护+美容', price: 0 },
  613. { id: 14, type: 'washing', name: '除菌药浴套餐', price: 0 },
  614. ]
  615. // --- State ---
  616. const userLoading = ref(false)
  617. const currentPets = ref([])
  618. const form = reactive({
  619. merchantId: '',
  620. userId: '',
  621. petId: '',
  622. type: 'transport',
  623. groupBuyPackage: '',
  624. // Sub Forms Data
  625. transport: {
  626. pkgId: '',
  627. price: 0,
  628. pickPrice: 35,
  629. dropPrice: 35,
  630. subType: 'round',
  631. pickRegion: [], pickDetail: '', pickContact: '', pickPhone: '', pickTime: '',
  632. dropRegion: [], dropDetail: '', dropContact: '', dropPhone: '', dropTime: ''
  633. },
  634. feeding: {
  635. pkgId: '', price: 68,
  636. appointments: [{ startTime: '', endTime: '' }],
  637. region: [], addressDetail: '',
  638. count: 1, dates: [], area: '', itemLoc: '', cleanLoc: '', foodAmount: '', other: ''
  639. },
  640. washing: {
  641. pkgId: '', price: 88,
  642. appointments: [{ startTime: '', endTime: '' }],
  643. region: [], addressDetail: '',
  644. time: '', petStatus: '', cleanLoc: '', toolLoc: '', other: ''
  645. }
  646. })
  647. // Address Autofill Watcher
  648. watch(() => form.petId, (newId) => {
  649. if (!newId) return
  650. const pet = currentPets.value.find(p => p.id === newId)
  651. if (!pet) return
  652. const user = userOptions.value.find(u => u.id === form.userId)
  653. // Fill Transport
  654. form.transport.pickRegion = pet.region || []
  655. form.transport.pickDetail = pet.address || ''
  656. form.transport.pickContact = user?.name || ''
  657. form.transport.pickPhone = user?.phone || ''
  658. form.transport.dropRegion = pet.region || []
  659. form.transport.dropDetail = pet.address || ''
  660. form.transport.dropContact = user?.name || ''
  661. form.transport.dropPhone = user?.phone || ''
  662. // Fill Feeding
  663. form.feeding.region = pet.region || []
  664. form.feeding.addressDetail = pet.address || ''
  665. // Fill Washing
  666. form.washing.region = pet.region || []
  667. form.washing.addressDetail = pet.address || ''
  668. })
  669. // Current Active Data Helper
  670. const activeData = computed(() => {
  671. return form[form.type]
  672. })
  673. // --- Logic ---
  674. const handleTypeChange = (type) => {
  675. form.type = type
  676. calcPrice(type)
  677. }
  678. const currentPackages = computed(() => {
  679. return allPackages.filter(p => p.type === form.type)
  680. })
  681. const handlePkgSelect = (id) => {
  682. activeData.value.pkgId = id
  683. // Price calculation should remain same (base price), just payable changes
  684. calcPrice(form.type)
  685. }
  686. const calcPrice = (type) => {
  687. const data = form[type]
  688. const base = serviceList.find(s => s.type === type)?.basePrice || 0
  689. // Always use Base Logic for "Order Value", regardless of package
  690. if (type === 'transport') {
  691. if(data.subType === 'round') {
  692. data.pickPrice = base
  693. data.dropPrice = base
  694. } else if (data.subType === 'pick') {
  695. data.pickPrice = base
  696. data.dropPrice = 0
  697. } else if (data.subType === 'drop') {
  698. data.pickPrice = 0
  699. data.dropPrice = base
  700. }
  701. } else if (type === 'feeding') {
  702. data.price = base * data.count
  703. } else if (type === 'washing') {
  704. data.price = base
  705. }
  706. }
  707. // Appointment Logic
  708. const addAppointment = (type) => {
  709. form[type].appointments.push({ startTime: '', endTime: '' })
  710. if(type === 'feeding') {
  711. form.feeding.count = form.feeding.appointments.length
  712. calcPrice('feeding')
  713. }
  714. }
  715. const removeAppointment = (type, index) => {
  716. if (form[type].appointments.length <= 1) return
  717. form[type].appointments.splice(index, 1)
  718. if(type === 'feeding') {
  719. form.feeding.count = form.feeding.appointments.length
  720. calcPrice('feeding')
  721. }
  722. }
  723. // Add User Logic
  724. const userDialogVisible = ref(false)
  725. const userSelectedTagIds = ref([])
  726. const allUserTags = [
  727. { id: 1, name: '优质客户', type: 'success' },
  728. { id: 2, name: '潜在流失', type: 'warning' },
  729. { id: 3, name: '黑名单', type: 'danger' }
  730. ]
  731. const pcaOptions = [
  732. {
  733. value: '北京市', label: '北京市',
  734. children: [
  735. { value: '市辖区', label: '市辖区', children: [ { value: '朝阳区', label: '朝阳区' }, { value: '海淀区', label: '海淀区' } ] }
  736. ]
  737. },
  738. {
  739. value: '上海市', label: '上海市',
  740. children: [
  741. { value: '市辖区', label: '市辖区', children: [ { value: '浦东新区', label: '浦东新区' }, { value: '徐汇区', label: '徐汇区' } ] }
  742. ]
  743. }
  744. ]
  745. const userForm = reactive({
  746. id: null, avatar: '', name: '', phone: '', gender: '男', address: '', detailAddress: '', region: [], remark: '',
  747. houseType: 'elevator', entryMethod: 'password', entryPassword: '', keyLocation: '',
  748. source: '平台录入', area: ''
  749. })
  750. const openAddUser = () => {
  751. userSelectedTagIds.value = []
  752. Object.assign(userForm, {
  753. id: null, avatar: '', name: '', phone: '', gender: '男', address: '', detailAddress: '', region: [], remark: '',
  754. houseType: 'elevator', entryMethod: 'password', entryPassword: '', keyLocation: '',
  755. source: '平台录入', area: ''
  756. })
  757. userDialogVisible.value = true
  758. }
  759. const handleUserAvatarChange = (uploadFile) => {
  760. userForm.avatar = URL.createObjectURL(uploadFile.raw)
  761. }
  762. const submitUser = () => {
  763. if(!userForm.name || !userForm.phone) {
  764. ElMessage.warning('请补全用户必填信息')
  765. return
  766. }
  767. const newUser = {
  768. id: Date.now(),
  769. name: userForm.name,
  770. phone: userForm.phone
  771. }
  772. userOptions.value.push(newUser)
  773. form.userId = newUser.id
  774. // Clear pets for new user
  775. currentPets.value = []
  776. form.petId = ''
  777. userDialogVisible.value = false
  778. ElMessage.success('用户添加成功并已选中')
  779. }
  780. // Add Pet Logic
  781. const petDialogVisible = ref(false)
  782. const activePetTab = ref('basic')
  783. const petForm = reactive({
  784. name: '', breed: '', gender: 'MM', avatar: '',
  785. bodyType: 'small', weight: 0, age: 0, keywords: '', desc: '', tags: [],
  786. // Family
  787. arrivalTime: '', houseType: 'stairs', entryMethod: 'key', entryPassword: '', keyLocation: '',
  788. // Health
  789. healthStatus: '健康', aggression: false, vaccine: '', medicalHistory: '', allergies: ''
  790. })
  791. const openAddPet = () => {
  792. activePetTab.value = 'basic'
  793. Object.assign(petForm, {
  794. name: '', breed: '', gender: 'MM', avatar: '',
  795. bodyType: 'small', weight: 0, age: 0, keywords: '', desc: '', tags: [],
  796. arrivalTime: '', houseType: 'stairs', entryMethod: 'key', entryPassword: '', keyLocation: '',
  797. healthStatus: '健康', aggression: false, vaccine: '', medicalHistory: '', allergies: ''
  798. })
  799. petDialogVisible.value = true
  800. }
  801. const handleAvatarChange = (uploadFile) => {
  802. // Mock upload: create local URL
  803. petForm.avatar = URL.createObjectURL(uploadFile.raw)
  804. }
  805. const submitPet = () => {
  806. if(!petForm.name || !petForm.breed) {
  807. ElMessage.warning('请补全宠物必填信息')
  808. return
  809. }
  810. const newPet = {
  811. id: Date.now(),
  812. name: petForm.name,
  813. breed: petForm.breed,
  814. avatar: petForm.avatar
  815. }
  816. if(!currentPets.value) currentPets.value = []
  817. currentPets.value.push(newPet)
  818. form.petId = newPet.id
  819. petDialogVisible.value = false
  820. ElMessage.success('宠物添加成功')
  821. }
  822. // --- Computed Helpers ---
  823. const selectedMerchantName = computed(() => merchants.value.find(m => m.id === form.merchantId)?.name)
  824. const selectedUserName = computed(() => userOptions.value.find(u => u.id === form.userId)?.name)
  825. const selectedPet = computed(() => currentPets.value.find(p => p.id === form.petId))
  826. const selectedPetName = computed(() => selectedPet.value?.name)
  827. const selectedPetBreed = computed(() => selectedPet.value?.breed)
  828. const selectedPkgName = computed(() => {
  829. const pkgId = activeData.value.pkgId
  830. return allPackages.find(p => p.id === pkgId)?.name || ''
  831. })
  832. const canSubmit = computed(() => {
  833. if(!form.merchantId || !form.userId || !form.petId) return false
  834. return true
  835. })
  836. // --- Methods ---
  837. const searchUser = (query) => { /* Mock */ }
  838. const handleUserChange = (val) => {
  839. currentPets.value = mockPets[val] || []
  840. form.petId = ''
  841. }
  842. const getStepTitle = (type) => {
  843. const map = { transport: '填写接送路线与时间', feeding: '选择套餐与服务的细则', washing: '选择套餐与服务的细则' }
  844. return map[type]
  845. }
  846. const getTypeName = (type) => {
  847. const map = { transport: '宠物接送', feeding: '上门喂遛', washing: '上门洗护' }
  848. return map[type]
  849. }
  850. const formatTime = (time) => {
  851. if(!time) return ''
  852. const d = new Date(time)
  853. return `${d.getMonth()+1}-${d.getDate()} ${d.getHours()}:${d.getMinutes() < 10 ? '0'+d.getMinutes() : d.getMinutes()}`
  854. }
  855. const handleSubmit = () => {
  856. ElMessage.success('下单成功!订单号:ORD20248888')
  857. }
  858. // Initialize
  859. onMounted(() => {
  860. calcPrice('transport')
  861. })
  862. </script>
  863. <style scoped>
  864. .page-container { padding: 20px; background-color: #f0f2f5; min-height: 100vh; }
  865. .create-layout { display: flex; gap: 20px; align-items: flex-start; max-width: 1400px; margin: 0 auto; }
  866. /* Left Content */
  867. .form-container { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 20px; }
  868. .section-card { border-radius: 8px; border: none; }
  869. .card-title { font-size: 16px; font-weight: bold; color: #303133; display: flex; align-items: center; gap: 10px; }
  870. .step-num {
  871. background: #e6f7ff; color: #1890ff; width: 28px; height: 28px; border-radius: 50%;
  872. text-align: center; line-height: 28px; font-family: Impact, sans-serif;
  873. }
  874. .base-form .el-form-item { margin-bottom: 18px; }
  875. /* Pet Selection */
  876. /* Pet Selection */
  877. .pet-select-row {
  878. display: grid;
  879. grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  880. gap: 15px;
  881. width: 100%;
  882. }
  883. .pet-card {
  884. border: 1px solid #8D9095;
  885. border-radius: 8px;
  886. padding: 12px 15px;
  887. cursor: pointer;
  888. display: flex;
  889. align-items: center;
  890. gap: 12px;
  891. position: relative;
  892. transition: all 0.2s ease-in-out;
  893. background: #fff;
  894. min-height: 70px;
  895. }
  896. .pet-card:hover {
  897. border-color: #303133;
  898. transform: translateY(-2px);
  899. box-shadow: 0 4px 12px rgba(0,0,0,0.08);
  900. }
  901. .pet-card.active {
  902. border-color: #409eff;
  903. background-color: #fff;
  904. box-shadow: 0 0 0 1px #409eff inset;
  905. }
  906. .check-mark {
  907. position: absolute;
  908. right: 0;
  909. top: 0;
  910. background: #409eff;
  911. color: white;
  912. width: 28px;
  913. height: 18px;
  914. border-radius: 0 8px 0 12px;
  915. display: flex;
  916. align-items: center;
  917. justify-content: center;
  918. font-size: 12px;
  919. }
  920. .pet-info .name { font-weight: bold; font-size: 15px; color: #303133; margin-bottom: 2px; }
  921. .pet-info .sub { font-size: 12px; color: #606266; line-height: 1.2; }
  922. .pet-card.add-card {
  923. border: 1px solid #8D9095;
  924. justify-content: center;
  925. align-items: center;
  926. color: #303133;
  927. flex-direction: row;
  928. gap: 8px;
  929. background: #fff;
  930. box-shadow: none;
  931. height: auto;
  932. min-height: 70px;
  933. }
  934. .pet-card.add-card:hover {
  935. border-color: #303133;
  936. color: #303133;
  937. background: #f9f9f9;
  938. transform: translateY(-2px);
  939. }
  940. /* Dialog Styles */
  941. .pet-form-content { display: flex; gap: 20px; }
  942. .avatar-col { width: 120px; display: flex; flex-direction: column; align-items: center; padding-top: 10px; }
  943. .avatar-uploader {
  944. display: inline-block;
  945. }
  946. .avatar-uploader .el-upload {
  947. border: 1px dashed #d9d9d9;
  948. border-radius: 6px;
  949. cursor: pointer;
  950. position: relative;
  951. overflow: hidden;
  952. transition: var(--el-transition-duration-fast);
  953. }
  954. .avatar-uploader .el-upload:hover {
  955. border-color: var(--el-color-primary);
  956. }
  957. .avatar-uploader-icon {
  958. font-size: 28px;
  959. color: #8c939d;
  960. width: 100px;
  961. height: 100px;
  962. text-align: center;
  963. border: 1px dashed #d9d9d9;
  964. border-radius: 50%;
  965. display: flex;
  966. align-items: center;
  967. justify-content: center;
  968. }
  969. .avatar {
  970. width: 100px;
  971. height: 100px;
  972. display: block;
  973. border-radius: 50%;
  974. object-fit: cover;
  975. }
  976. .inner-form { flex: 1; }
  977. /* Type Selection */
  978. .type-selection { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; }
  979. .type-card {
  980. background: white; border-radius: 8px; padding: 20px; cursor: pointer; position: relative;
  981. display: flex; align-items: center; gap: 15px; transition: all 0.2s;
  982. border: 2px solid transparent; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
  983. }
  984. .type-card:hover { transform: translateY(-2px); box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1); }
  985. .type-card.active { border-color: #409eff; background-color: #f0f9ff; }
  986. .type-card .icon-box {
  987. width: 48px; height: 48px; border-radius: 12px; background: #f2f3f5;
  988. display: flex; align-items: center; justify-content: center; font-size: 24px; color: #606266;
  989. }
  990. .type-card.active .icon-box { background: #409eff; color: white; }
  991. /* Colors */
  992. .type-card.transport.active .icon-box { background: #409eff; }
  993. .type-card.transport.active { border-color: #409eff; background-color: #f0f9ff; }
  994. .type-card.feeding.active .icon-box { background: #e6a23c; }
  995. .type-card.feeding.active { border-color: #e6a23c; background-color: #fdf6ec; }
  996. .type-card.washing.active .icon-box { background: #67c23a; }
  997. .type-card.washing.active { border-color: #67c23a; background-color: #f0f9eb; }
  998. .type-name { font-weight: bold; font-size: 16px; color: #303133; margin-bottom: 4px; }
  999. .type-desc { font-size: 12px; color: #909399; margin-bottom: 4px; }
  1000. .type-price { font-size: 14px; color: #f56c6c; font-weight: bold; }
  1001. /* Package Selection Grid */
  1002. .form-section-title { font-weight: bold; margin-bottom: 12px; font-size: 14px; }
  1003. .package-selection-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 20px; }
  1004. .pkg-select-card {
  1005. border: 1px solid #dcdfe6; border-radius: 8px; padding: 10px 15px; cursor: pointer; position: relative;
  1006. background: #fff; transition: all 0.2s; min-height: 56px; display: flex; flex-direction: column; justify-content: center;
  1007. }
  1008. .pkg-select-card:hover { border-color: #409eff; }
  1009. .pkg-select-card.active { border-color: #409eff; background-color: #ecf5ff; }
  1010. .pkg-select-card .pkg-name { font-weight: bold; font-size: 14px; color: #303133; }
  1011. .pkg-select-card .pkg-desc { font-size: 12px; color: #909399; margin-top: 2px; }
  1012. /* Business Form */
  1013. .business-form { padding-top: 5px; }
  1014. .route-box { background: #f9f9f9; padding: 20px; border-radius: 8px; border: 1px solid #EBEEF5; }
  1015. .route-segment { display: flex; gap: 15px; }
  1016. .seg-badge {
  1017. width: 32px; height: 32px; background: #409eff; color: white; border-radius: 8px;
  1018. text-align: center; line-height: 32px; font-weight: bold; flex-shrink: 0;
  1019. }
  1020. .seg-badge.end { background: #67c23a; }
  1021. .seg-content { flex: 1; display: flex; flex-direction: column; gap: 10px; }
  1022. .route-connector { display: flex; align-items: center; justify-content: center; margin: 15px 0; gap: 10px; color: #909399; font-size: 12px; }
  1023. .route-connector .line { height: 1px; width: 80px; background: #dcdfe6; }
  1024. .route-connector .store-node { background: white; padding: 4px 12px; border-radius: 20px; border: 1px solid #dcdfe6; display: flex; align-items: center; gap: 5px; }
  1025. .divider { height: 1px; background: #EBEEF5; margin: 15px 0; }
  1026. .remark-section { background: #fdfdfd; border: 1px dashed #dcdfe6; padding: 15px; border-radius: 6px; margin-top: 20px; }
  1027. .section-label { font-size: 13px; font-weight: bold; color: #606266; margin-bottom: 12px; }
  1028. .tip { font-size: 12px; color: #e6a23c; margin-top: 4px; }
  1029. /* Sidebar */
  1030. .summary-sidebar { width: 320px; flex-shrink: 0; }
  1031. .summary-panel { background: white; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.05); position: sticky; top: 20px; }
  1032. .summary-header { background: #304156; color: white; padding: 15px 20px; font-weight: bold; font-size: 16px; border-radius: 8px 8px 0 0; }
  1033. .summary-content { padding: 20px; }
  1034. .row { display: flex; justify-content: space-between; margin-bottom: 12px; font-size: 14px; }
  1035. .row .label { color: #909399; }
  1036. .row .value { color: #303133; font-weight: 500; }
  1037. .preview-title { font-weight: bold; margin-bottom: 8px; color: #333; }
  1038. .preview-detail { background: #f8f9fa; padding: 10px; border-radius: 4px; font-size: 13px; margin-bottom: 8px; }
  1039. .preview-detail .minor { color: #999; font-size: 12px; margin-top: 2px; }
  1040. .placeholder { color: #C0C4CC; text-align: center; padding: 20px 0; font-size: 13px; font-style: italic; }
  1041. .summary-footer { background: #f9f9fc; padding: 15px 20px; border-top: 1px solid #ebeef5; text-align: center; border-radius: 0 0 8px 8px; }
  1042. .submit-btn { width: 100%; font-weight: bold; border-radius: 22px; }
  1043. </style>