newGroup.vue 75 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493
  1. <template>
  2. <!-- 团购包管理 - 新增/编辑页面 -->
  3. <div class="table-box" style="width: 100%; min-height: 100%; background-color: white">
  4. <el-form :model="storeInfoModel" ref="ruleFormRef" :rules="rules" label-width="120px" class="formBox">
  5. <div class="content">
  6. <!-- 左侧内容区域 -->
  7. <div class="contentLeft">
  8. <!-- 基础信息模块 -->
  9. <div class="model">
  10. <h3 style="font-weight: bold">基础信息:</h3>
  11. <!-- 团购图片上传 -->
  12. <el-form-item label="图片" prop="imageList">
  13. <el-upload
  14. v-model:file-list="storeInfoModel.imageList"
  15. :action="uploadUrl"
  16. list-type="picture-card"
  17. :accept="'.jpg,.png'"
  18. :limit="9"
  19. :on-preview="handlePictureCardPreview"
  20. :before-remove="handleBeforeRemove"
  21. :on-remove="handleRemove"
  22. :on-success="handleSuccess"
  23. :show-file-list="true"
  24. >
  25. <el-icon>
  26. <Plus />
  27. </el-icon>
  28. </el-upload>
  29. </el-form-item>
  30. <!-- 团购名称 -->
  31. <el-form-item label="团购名称" prop="groupName">
  32. <el-input maxlength="50" v-model="storeInfoModel.groupName" placeholder="请填写团购名称" clearable />
  33. </el-form-item>
  34. <!-- 开始售卖时间设置方式 -->
  35. <el-form-item label="开始售卖时间" prop="startTimeType">
  36. <el-radio-group v-model="storeInfoModel.startTimeType" class="ml-4">
  37. <el-radio v-for="status in storeStatusList" :value="status.value" :key="status.value">
  38. {{ status.label }}
  39. </el-radio>
  40. </el-radio-group>
  41. </el-form-item>
  42. <!-- 自定义开始售卖时间(当选择"设置售卖时间"时显示) -->
  43. <el-form-item label="" prop="saleTimeStr" v-if="storeInfoModel.startTimeType == 1">
  44. <el-date-picker
  45. v-model="storeInfoModel.saleTimeStr"
  46. format="YYYY/MM/DD"
  47. value-format="YYYY-MM-DD"
  48. placeholder="请选择开始售卖时间"
  49. :disabled-date="disabledStartDate"
  50. />
  51. </el-form-item>
  52. <!-- 结束售卖时间 -->
  53. <el-form-item label="结束售卖时间" prop="endTime">
  54. <el-date-picker
  55. v-model="storeInfoModel.endTime"
  56. format="YYYY/MM/DD"
  57. value-format="YYYY-MM-DD"
  58. placeholder="请选择结束售卖时间"
  59. :disabled-date="disabledEndDate"
  60. />
  61. </el-form-item>
  62. <!-- 库存数量 -->
  63. <el-form-item label="库存数量" prop="inventoryNum">
  64. <el-input v-model="storeInfoModel.inventoryNum" maxlength="15" placeholder="请填写库存数量" clearable />
  65. </el-form-item>
  66. <!-- 每人限购设置 -->
  67. <el-form-item label="每人限购" prop="quotaType">
  68. <el-radio-group v-model="storeInfoModel.quotaType" class="ml-4">
  69. <el-radio v-for="status in perList" :value="status.value" :key="status.value">
  70. {{ status.label }}
  71. </el-radio>
  72. </el-radio-group>
  73. </el-form-item>
  74. <!-- 自定义限购数量(当选择"自定义限购数量"时显示) -->
  75. <el-form-item label="" prop="quotaValue" v-if="storeInfoModel.quotaType == 1">
  76. <el-input v-model="storeInfoModel.quotaValue" maxlength="15" placeholder="请填写自定义限购数量" clearable />
  77. </el-form-item>
  78. <!-- 套餐内容 -->
  79. <el-form-item label="套餐内容" class="package-content-item">
  80. <el-form
  81. ref="packageFormRef"
  82. :model="packageFormModel"
  83. :rules="packageFormRules"
  84. label-width="0"
  85. class="package-content-form"
  86. >
  87. <div class="package-content-wrapper">
  88. <div class="package-content-header">
  89. <el-button type="primary" @click="addGroup" class="add-group-btn"> 添加分组 </el-button>
  90. <el-button
  91. type="primary"
  92. link
  93. @click="toggleAllGroupsCollapse"
  94. :icon="allGroupsCollapsed ? ArrowDown : ArrowUp"
  95. v-show="lifeGroupBuyThalis.length > 1"
  96. >
  97. {{ allGroupsCollapsed ? "展开全部" : "收起全部" }}
  98. </el-button>
  99. </div>
  100. <transition-group name="group-fade" tag="div" class="package-content-container">
  101. <div v-for="item in visibleGroups" :key="item.originalIndex" class="package-group">
  102. <div class="package-group-header">
  103. <span class="group-index">{{ item.group.groupName }}</span>
  104. <div class="header-right">
  105. <el-button
  106. type="primary"
  107. link
  108. @click="toggleGroupCollapse(item.originalIndex)"
  109. :icon="isGroupCollapsed(item.originalIndex) ? ArrowDown : ArrowUp"
  110. v-show="item.group.dishes.length > 1"
  111. >
  112. {{ isGroupCollapsed(item.originalIndex) ? "展开" : "收起" }}
  113. </el-button>
  114. <el-button
  115. type="danger"
  116. link
  117. @click="removeGroup(item.originalIndex)"
  118. :icon="Delete"
  119. v-show="lifeGroupBuyThalis.length > 1"
  120. >
  121. 删除
  122. </el-button>
  123. </div>
  124. </div>
  125. <div class="package-group-content">
  126. <el-form-item
  127. :prop="`groups.${item.originalIndex}.groupName`"
  128. :rules="packageFormRules[`groups.${item.originalIndex}.groupName`]"
  129. class="category-form-item"
  130. >
  131. <div class="category-row">
  132. <span class="label">类别</span>
  133. <el-input v-model="item.group.groupName" placeholder="请输入" clearable />
  134. </div>
  135. </el-form-item>
  136. <!-- 第一行菜品,始终显示 -->
  137. <div v-if="item.group.dishes && item.group.dishes.length > 0" class="dish-row">
  138. <el-form-item
  139. :prop="`groups.${item.originalIndex}.dishes.0.detailId`"
  140. :rules="packageFormRules[`groups.${item.originalIndex}.dishes.0.detailId`]"
  141. class="dish-form-item"
  142. >
  143. <span class="label">菜品</span>
  144. <div class="dish-select-block" @click="openDishDialog(item.originalIndex, 0)">
  145. <span v-if="item.group.dishes[0].dishName" class="dish-selected-name"
  146. >{{ item.group.dishes[0].dishName }},¥{{ item.group.dishes[0].dishPrice }}/{{
  147. item.group.dishes[0].dishesUnit
  148. }}</span
  149. >
  150. <span v-else class="dish-placeholder">请选择</span>
  151. </div>
  152. </el-form-item>
  153. <el-form-item
  154. :prop="`groups.${item.originalIndex}.dishes.0.qty`"
  155. :rules="packageFormRules[`groups.${item.originalIndex}.dishes.0.qty`]"
  156. class="dish-form-item"
  157. >
  158. <span class="label">数量</span>
  159. <el-input v-model="item.group.dishes[0].qty" placeholder="请输入" clearable class="quantity-input" />
  160. <el-button
  161. :icon="Delete"
  162. link
  163. type="danger"
  164. @click="removeDish(item.originalIndex, 0)"
  165. class="delete-dish-btn"
  166. v-show="item.group.dishes.length > 1"
  167. />
  168. </el-form-item>
  169. </div>
  170. <!-- 第二行及以后的菜品,收起时隐藏 -->
  171. <transition name="slide-fade">
  172. <div v-if="!isGroupCollapsed(item.originalIndex)" class="extra-dishes-container">
  173. <div
  174. v-for="(dish, dishIndex) in item.group.dishes"
  175. :key="dishIndex"
  176. class="dish-row"
  177. v-show="dishIndex > 0"
  178. >
  179. <el-form-item
  180. :prop="`groups.${item.originalIndex}.dishes.${dishIndex}.detailId`"
  181. :rules="packageFormRules[`groups.${item.originalIndex}.dishes.${dishIndex}.detailId`]"
  182. class="dish-form-item"
  183. >
  184. <span class="label">菜品</span>
  185. <div class="dish-select-block" @click="openDishDialog(item.originalIndex, dishIndex)">
  186. <span v-if="dish.dishName" class="dish-selected-name"
  187. >{{ dish.dishName }},¥{{ dish.dishPrice }}/{{ dish.dishesUnit }}</span
  188. >
  189. <span v-else class="dish-placeholder">请选择</span>
  190. </div>
  191. </el-form-item>
  192. <el-form-item
  193. :prop="`groups.${item.originalIndex}.dishes.${dishIndex}.qty`"
  194. :rules="packageFormRules[`groups.${item.originalIndex}.dishes.${dishIndex}.qty`]"
  195. class="dish-form-item"
  196. >
  197. <span class="label">数量</span>
  198. <el-input v-model="dish.qty" placeholder="请输入" clearable class="quantity-input" />
  199. <el-button
  200. :icon="Delete"
  201. link
  202. type="danger"
  203. @click="removeDish(item.originalIndex, dishIndex)"
  204. class="delete-dish-btn"
  205. />
  206. </el-form-item>
  207. </div>
  208. <el-button type="primary" @click="addDish(item.originalIndex)" class="add-dish-btn"> 添加 </el-button>
  209. </div>
  210. </transition>
  211. </div>
  212. </div>
  213. </transition-group>
  214. </div>
  215. </el-form>
  216. </el-form-item>
  217. </div>
  218. <!-- 价格信息模块 -->
  219. <div class="model">
  220. <h3 style="font-weight: bold">价格:</h3>
  221. <!-- 原价 -->
  222. <el-form-item label="原价" prop="originalPrice">
  223. <el-input maxlength="50" v-model="storeInfoModel.originalPrice" placeholder="请填写原价" clearable />
  224. </el-form-item>
  225. <!-- 优惠价 -->
  226. <el-form-item label="优惠价" prop="preferentialPrice">
  227. <el-input maxlength="50" v-model="storeInfoModel.preferentialPrice" placeholder="请填写优惠价" clearable />
  228. </el-form-item>
  229. </div>
  230. </div>
  231. <!-- 右侧内容区域 -->
  232. <div class="contentRight">
  233. <!-- 购买须知模块 -->
  234. <div class="model">
  235. <h3 style="font-weight: bold">购买须知:</h3>
  236. <el-form-item label="有效期" prop="effectiveDateType">
  237. <el-radio-group v-model="storeInfoModel.effectiveDateType" class="ml-4">
  238. <el-radio v-for="status in expirationDateList" :value="status.value" :key="status.value">
  239. {{ status.label }}
  240. </el-radio>
  241. </el-radio-group>
  242. </el-form-item>
  243. <el-form-item label="" prop="expirationDate" v-if="storeInfoModel.effectiveDateType == 0">
  244. <div class="expiration-date-container">
  245. <span class="expiration-label">用户购买</span>
  246. <el-input-number v-model="storeInfoModel.expirationDate" placeholder="请输入" :min="0" class="expiration-input" />
  247. <span class="expiration-label">天内有效</span>
  248. </div>
  249. </el-form-item>
  250. <el-form-item label="" prop="effectiveDateValue" v-else>
  251. <el-date-picker
  252. v-model="storeInfoModel.effectiveDateValue"
  253. type="daterange"
  254. value-format="YYYY-MM-DD"
  255. range-separator="-"
  256. start-placeholder="开始时间"
  257. end-placeholder="结束时间"
  258. :disabled-date="disabledEffectiveDate"
  259. />
  260. </el-form-item>
  261. <el-form-item label="不可用日期" prop="disableDateType">
  262. <el-radio-group v-model="storeInfoModel.disableDateType" class="ml-4">
  263. <el-radio v-for="status in unavailableDatesList" :value="status.value" :key="status.value">
  264. {{ status.label }}
  265. </el-radio>
  266. </el-radio-group>
  267. </el-form-item>
  268. <template v-if="storeInfoModel.disableDateType == 1">
  269. <el-form-item label="" prop="unavailableWeekdays">
  270. <div class="unavailable-dates-container">
  271. <!-- 星期选择 -->
  272. <div class="date-select-section">
  273. <div class="section-title">星期</div>
  274. <div class="button-group">
  275. <el-button
  276. v-for="weekday in weekdayList"
  277. :key="weekday.oName"
  278. :type="storeInfoModel.unavailableWeekdays?.includes(weekday.oName) ? 'primary' : ''"
  279. class="date-select-btn"
  280. @click="toggleWeekday(weekday.oName)"
  281. >
  282. {{ weekday.name }}
  283. </el-button>
  284. </div>
  285. </div>
  286. </div>
  287. </el-form-item>
  288. <el-form-item label="" prop="unavailableHolidays">
  289. <div class="unavailable-dates-container">
  290. <!-- 节日选择 -->
  291. <div class="date-select-section">
  292. <div class="section-title">节日</div>
  293. <div class="button-group">
  294. <el-button
  295. v-for="holiday in holidayList"
  296. :key="holiday.id"
  297. :type="storeInfoModel.unavailableHolidays?.includes(holiday.id) ? 'primary' : ''"
  298. class="date-select-btn"
  299. @click="toggleHoliday(holiday.id)"
  300. >
  301. {{ holiday.festivalName }}
  302. </el-button>
  303. </div>
  304. </div>
  305. </div>
  306. </el-form-item>
  307. </template>
  308. <el-form-item label="" prop="customUnavailableDates" v-else-if="storeInfoModel.disableDateType == 2">
  309. <div class="date-picker-container">
  310. <el-button :icon="Plus" class="add-date-btn" type="primary" @click="addDate"> 添加日期 </el-button>
  311. <div v-for="(item, index) in dates" :key="index" class="date-item">
  312. <el-date-picker
  313. v-model="dates[index]"
  314. type="daterange"
  315. value-format="YYYY-MM-DD"
  316. range-separator="-"
  317. start-placeholder="开始时间"
  318. end-placeholder="结束时间"
  319. class="date-picker"
  320. :disabled-date="disabledCustomUnavailableDate"
  321. />
  322. <el-button
  323. :icon="Delete"
  324. type="danger"
  325. circle
  326. size="small"
  327. class="delete-btn"
  328. @click="removeDate(index)"
  329. v-show="dates.length > 1"
  330. />
  331. </div>
  332. </div>
  333. </el-form-item>
  334. <!-- 预约规则 -->
  335. <el-form-item label="预约规则" prop="reservationRules">
  336. <el-input
  337. maxlength="300"
  338. v-model="storeInfoModel.reservationRules"
  339. :rows="4"
  340. type="textarea"
  341. placeholder="请输入预约规则"
  342. />
  343. </el-form-item>
  344. <!-- 使用规则 -->
  345. <el-form-item label="使用规则" prop="useRules">
  346. <el-input
  347. maxlength="300"
  348. v-model="storeInfoModel.useRules"
  349. :rows="4"
  350. type="textarea"
  351. placeholder="请输入使用规则"
  352. />
  353. </el-form-item>
  354. <!-- 适用人数 -->
  355. <el-form-item label="适用人数" prop="applicableNum">
  356. <el-input v-model="storeInfoModel.applicableNum" maxlength="15" placeholder="请填写适用人数" clearable />
  357. </el-form-item>
  358. <!-- 其他规则 -->
  359. <el-form-item label="其他规则" prop="otherRules">
  360. <el-input
  361. maxlength="300"
  362. v-model="storeInfoModel.otherRules"
  363. :rows="4"
  364. type="textarea"
  365. placeholder="请输入其他规则"
  366. />
  367. </el-form-item>
  368. <!-- 发票信息 -->
  369. <el-form-item label="发票信息" prop="invoiceInformation">
  370. <el-checkbox-group v-model="storeInfoModel.invoiceInformation">
  371. <el-checkbox v-for="bt in businessTypes" :key="bt.value" :value="bt.value" :label="bt.label">
  372. {{ bt.label }}
  373. </el-checkbox>
  374. </el-checkbox-group>
  375. </el-form-item>
  376. <!-- 发票说明 -->
  377. <el-form-item label="发票说明" prop="invoiceDescribe">
  378. <el-input v-model="storeInfoModel.invoiceDescribe" maxlength="15" placeholder="请填写发票说明" clearable />
  379. </el-form-item>
  380. </div>
  381. </div>
  382. </div>
  383. </el-form>
  384. <!-- 底部按钮区域 -->
  385. <div class="button-container">
  386. <el-button @click="goBack"> 返回 </el-button>
  387. <el-button @click="handleSubmit('cg')"> 存草稿 </el-button>
  388. <el-button type="primary" @click="handleSubmit"> 确定 </el-button>
  389. </div>
  390. <!-- 图片预览 -->
  391. <el-image-viewer
  392. v-if="imageViewerVisible"
  393. :url-list="imageViewerUrlList"
  394. :initial-index="imageViewerInitialIndex"
  395. @close="imageViewerVisible = false"
  396. />
  397. <!-- 菜品选择对话框 -->
  398. <el-dialog v-model="dishDialogVisible" title="菜品" width="600px" :close-on-click-modal="false">
  399. <div class="dish-dialog-content">
  400. <el-input
  401. v-model="dishSearchKeyword"
  402. placeholder="请输入菜品名称"
  403. clearable
  404. class="dish-search-input"
  405. @input="filterDishList"
  406. />
  407. <el-scrollbar height="400px" class="dish-list-container">
  408. <template v-if="filteredDishList.length > 0">
  409. <div
  410. v-for="dish in filteredDishList"
  411. :key="dish.id"
  412. :class="['dish-item', { 'dish-item-selected': selectedDishId === dish.id }]"
  413. @click="selectDish(dish)"
  414. >
  415. <div class="dish-image">
  416. <el-image v-if="dish.imgUrl" :src="dish.imgUrl" fit="cover" class="dish-img" />
  417. <div v-else class="dish-image-placeholder">
  418. <el-icon><Picture /></el-icon>
  419. </div>
  420. </div>
  421. <div class="dish-info">
  422. <div class="dish-name">
  423. {{ dish.dishName }}
  424. </div>
  425. <div class="dish-price">¥{{ dish.dishPrice }}/{{ dish.dishesUnit }}</div>
  426. </div>
  427. </div>
  428. </template>
  429. <div v-else class="dish-empty-state">
  430. <div class="empty-text">暂无可选择菜品</div>
  431. <div class="empty-hint">请去全部-门店装修-菜品管理中添加</div>
  432. </div>
  433. </el-scrollbar>
  434. </div>
  435. <template #footer>
  436. <div class="dialog-footer">
  437. <el-button type="primary" @click="confirmDishSelection"> 确定 </el-button>
  438. </div>
  439. </template>
  440. </el-dialog>
  441. </div>
  442. </template>
  443. <script setup lang="tsx" name="newGroup">
  444. /**
  445. * 团购包管理 - 新增/编辑页面
  446. * 功能:支持团购包的新增和编辑操作
  447. */
  448. import { ref, reactive, onMounted, watch, nextTick, computed } from "vue";
  449. import { ElMessage, ElMessageBox } from "element-plus";
  450. import { Plus, Delete, ArrowDown, ArrowUp, Picture, Check } from "@element-plus/icons-vue";
  451. import {
  452. saveStoreInfo,
  453. editStoreInfo,
  454. getDetail,
  455. getStoreDetail,
  456. getHolidayList,
  457. getUserByPhone,
  458. getMenuByStoreId
  459. } from "@/api/modules/groupPackageManagement";
  460. import { useRouter, useRoute } from "vue-router";
  461. import type { UploadProps, FormInstance } from "element-plus";
  462. import { localGet, localSet } from "@/utils";
  463. // ==================== 响应式数据定义 ====================
  464. // 图片预览相关
  465. const imageViewerVisible = ref(false);
  466. const imageViewerUrlList = ref<string[]>([]);
  467. const imageViewerInitialIndex = ref(0);
  468. // 路由相关
  469. const router = useRouter();
  470. const route = useRoute();
  471. // 页面状态
  472. const type = ref<string>(""); // 页面类型:add-新增, edit-编辑
  473. const id = ref<string>(""); // 页面ID参数
  474. // 文件上传地址
  475. const uploadUrl = ref(`${import.meta.env.VITE_API_URL_STORE}/file/upload`);
  476. // ==================== 表单验证规则 ====================
  477. const rules = reactive({
  478. imageList: [{ required: true, message: "请上传图片" }],
  479. groupName: [{ required: true, message: "请填写团购名称" }],
  480. startTimeType: [{ required: true, message: "请选择开始售卖时间" }],
  481. saleTimeStr: [
  482. { required: true, message: "请选择开始售卖时间" },
  483. {
  484. validator: (rule: any, value: any, callback: any) => {
  485. if (storeInfoModel.value.startTimeType === 1) {
  486. if (!value) {
  487. callback(new Error("请选择开始售卖时间"));
  488. return;
  489. }
  490. const selectedDate = new Date(value);
  491. const today = new Date();
  492. today.setHours(0, 0, 0, 0);
  493. if (selectedDate < today) {
  494. callback(new Error("开始售卖时间不能早于当前时间"));
  495. return;
  496. }
  497. // 如果结束时间已设置,验证开始时间必须早于结束时间
  498. if (storeInfoModel.value.endTime) {
  499. const endDate = new Date(storeInfoModel.value.endTime);
  500. if (selectedDate >= endDate) {
  501. callback(new Error("开始售卖时间必须早于结束售卖时间"));
  502. return;
  503. }
  504. }
  505. }
  506. callback();
  507. },
  508. trigger: "change"
  509. }
  510. ],
  511. endTime: [
  512. { required: true, message: "请选择结束售卖时间" },
  513. {
  514. validator: (rule: any, value: any, callback: any) => {
  515. if (!value) {
  516. callback(new Error("请选择结束售卖时间"));
  517. return;
  518. }
  519. const selectedDate = new Date(value);
  520. const today = new Date();
  521. today.setHours(0, 0, 0, 0);
  522. if (selectedDate < today) {
  523. callback(new Error("结束售卖时间不能早于当前时间"));
  524. return;
  525. }
  526. // 如果开始时间已设置,验证结束时间必须晚于开始时间
  527. if (storeInfoModel.value.startTimeType === 1 && storeInfoModel.value.saleTimeStr) {
  528. const startDate = new Date(storeInfoModel.value.saleTimeStr);
  529. if (selectedDate <= startDate) {
  530. callback(new Error("结束售卖时间必须晚于开始售卖时间"));
  531. return;
  532. }
  533. }
  534. callback();
  535. },
  536. trigger: "change"
  537. }
  538. ],
  539. inventoryNum: [
  540. { required: true, message: "请填写库存数量" },
  541. {
  542. validator: (rule: any, value: any, callback: any) => {
  543. if (!value || value.toString().trim() === "") {
  544. callback();
  545. return;
  546. }
  547. const num = Number(value);
  548. if (isNaN(num) || num <= 0) {
  549. callback(new Error("库存数量必须为正整数"));
  550. return;
  551. }
  552. if (!Number.isInteger(num)) {
  553. callback(new Error("库存数量必须为正整数"));
  554. return;
  555. }
  556. callback();
  557. },
  558. trigger: "blur"
  559. }
  560. ],
  561. quotaType: [{ required: true, message: "请选择每人限购" }],
  562. quotaValue: [
  563. {
  564. required: true,
  565. validator: (rule: any, value: any, callback: any) => {
  566. if (storeInfoModel.value.quotaType === 1) {
  567. if (!value || value.toString().trim() === "") {
  568. callback(new Error("请输入自定义限购数量"));
  569. return;
  570. }
  571. const num = Number(value);
  572. if (isNaN(num) || num <= 0) {
  573. callback(new Error("自定义限购数量必须为正整数"));
  574. return;
  575. }
  576. if (!Number.isInteger(num)) {
  577. callback(new Error("自定义限购数量必须为正整数"));
  578. return;
  579. }
  580. }
  581. callback();
  582. },
  583. trigger: "blur"
  584. }
  585. ],
  586. lifeGroupBuyThalis: [
  587. {
  588. required: true,
  589. validator: (rule: any, value: any, callback: any) => {
  590. try {
  591. // 只检查是否有至少一个分组,详细验证由独立的套餐内容表单处理
  592. const thalis = lifeGroupBuyThalis.value;
  593. if (!thalis || thalis.length === 0) {
  594. callback(new Error("请至少添加一个套餐分组"));
  595. return;
  596. }
  597. callback();
  598. } catch (error) {
  599. // 如果验证过程中出错,直接通过验证,避免报错
  600. callback();
  601. }
  602. },
  603. trigger: "blur"
  604. }
  605. ],
  606. originalPrice: [
  607. { required: true, message: "请输入原价" },
  608. {
  609. validator: (rule: any, value: any, callback: any) => {
  610. if (!value || value.toString().trim() === "") {
  611. callback();
  612. return;
  613. }
  614. const num = Number(value);
  615. if (isNaN(num) || num <= 0) {
  616. callback(new Error("原价必须为正数"));
  617. return;
  618. }
  619. callback();
  620. },
  621. trigger: "blur"
  622. }
  623. ],
  624. preferentialPrice: [
  625. { required: true, message: "请输入优惠价" },
  626. {
  627. validator: (rule: any, value: any, callback: any) => {
  628. if (!value || value.toString().trim() === "") {
  629. callback();
  630. return;
  631. }
  632. const num = Number(value);
  633. if (isNaN(num) || num <= 0) {
  634. callback(new Error("优惠价必须为正数"));
  635. return;
  636. }
  637. callback();
  638. },
  639. trigger: "blur"
  640. }
  641. ],
  642. effectiveDateType: [{ required: true, message: "请选择有效期" }],
  643. expirationDate: [
  644. {
  645. required: true,
  646. validator: (rule: any, value: any, callback: any) => {
  647. if (storeInfoModel.value.expirationDate === 0) {
  648. if (value === null || value === undefined || value === "") {
  649. callback(new Error("请输入用户购买天数"));
  650. return;
  651. }
  652. if (value <= 0) {
  653. callback(new Error("天数必须大于0"));
  654. return;
  655. }
  656. }
  657. callback();
  658. },
  659. trigger: "blur"
  660. }
  661. ],
  662. effectiveDateValue: [
  663. {
  664. required: true,
  665. validator: (rule: any, value: any, callback: any) => {
  666. if (storeInfoModel.value.effectiveDateType === 1) {
  667. if (!value || value.length !== 2) {
  668. callback(new Error("请选择指定时间段"));
  669. return;
  670. }
  671. // 验证开始时间和结束时间不能早于当前时间
  672. const today = new Date();
  673. today.setHours(0, 0, 0, 0);
  674. const startDate = new Date(value[0]);
  675. const endDate = new Date(value[1]);
  676. if (startDate < today) {
  677. callback(new Error("开始时间不能早于当前时间"));
  678. return;
  679. }
  680. if (endDate < today) {
  681. callback(new Error("结束时间不能早于当前时间"));
  682. return;
  683. }
  684. // 验证开始时间必须早于结束时间
  685. if (startDate >= endDate) {
  686. callback(new Error("开始时间必须早于结束时间"));
  687. return;
  688. }
  689. }
  690. callback();
  691. },
  692. trigger: "change"
  693. }
  694. ],
  695. disableDateType: [{ required: true, message: "请选择不可用日期" }],
  696. unavailableWeekdays: [
  697. {
  698. required: true,
  699. validator: (rule: any, value: any, callback: any) => {
  700. if (storeInfoModel.value.unavailableDates === 1) {
  701. if (!value || value.length === 0) {
  702. callback(new Error("至少需要选择一个星期"));
  703. return;
  704. }
  705. }
  706. callback();
  707. },
  708. trigger: "change"
  709. }
  710. ],
  711. unavailableHolidays: [
  712. {
  713. required: true,
  714. validator: (rule: any, value: any, callback: any) => {
  715. if (storeInfoModel.value.unavailableDates === 1) {
  716. if (!value || value.length === 0) {
  717. callback(new Error("至少需要选择一个节日"));
  718. return;
  719. }
  720. }
  721. callback();
  722. },
  723. trigger: "change"
  724. }
  725. ],
  726. customUnavailableDates: [
  727. {
  728. required: true,
  729. validator: (rule: any, value: any, callback: any) => {
  730. if (storeInfoModel.value.disableDateType === 2) {
  731. if (!dates.value || dates.value.length === 0) {
  732. callback(new Error("至少需要添加一个自定义不可用日期"));
  733. return;
  734. }
  735. const today = new Date();
  736. today.setHours(0, 0, 0, 0);
  737. // 验证每个日期项是否已填写
  738. for (let i = 0; i < dates.value.length; i++) {
  739. if (!dates.value[i] || dates.value[i].length !== 2) {
  740. callback(new Error(`第${i + 1}个日期项未完整填写`));
  741. return;
  742. }
  743. // 验证开始时间和结束时间不能早于当前时间
  744. const startDate = new Date(dates.value[i][0]);
  745. const endDate = new Date(dates.value[i][1]);
  746. if (startDate < today) {
  747. callback(new Error(`第${i + 1}个日期项的开始时间不能早于当前时间`));
  748. return;
  749. }
  750. if (endDate < today) {
  751. callback(new Error(`第${i + 1}个日期项的结束时间不能早于当前时间`));
  752. return;
  753. }
  754. // 验证开始时间必须早于结束时间
  755. if (startDate >= endDate) {
  756. callback(new Error(`第${i + 1}个日期项的开始时间必须早于结束时间`));
  757. return;
  758. }
  759. }
  760. }
  761. callback();
  762. },
  763. trigger: "change"
  764. }
  765. ],
  766. reservationRules: [{ required: true, message: "请输入预约规则" }],
  767. useRules: [{ required: true, message: "请输入使用规则" }],
  768. applicableNum: [
  769. { required: true, message: "请输入适用人数" },
  770. {
  771. validator: (rule: any, value: any, callback: any) => {
  772. if (!value || value.toString().trim() === "") {
  773. callback();
  774. return;
  775. }
  776. const num = Number(value);
  777. if (isNaN(num) || num <= 0) {
  778. callback(new Error("适用人数必须为正整数"));
  779. return;
  780. }
  781. if (!Number.isInteger(num)) {
  782. callback(new Error("适用人数必须为正整数"));
  783. return;
  784. }
  785. callback();
  786. },
  787. trigger: "blur"
  788. }
  789. ],
  790. otherRules: [{ required: true, message: "请输入其他规则" }],
  791. invoiceInformation: [{ required: true, message: "请选择发票信息" }],
  792. invoiceDescribe: [{ required: true, message: "请输入发票说明" }]
  793. });
  794. // ==================== 团购包信息数据模型 ====================
  795. const storeInfoModel = ref<any>({
  796. // 团购图片列表
  797. imageList: [],
  798. // 团购名称
  799. groupName: "",
  800. // 开始售卖时间设置方式:0-审核通过后立即开始,1-设置售卖时间
  801. startTimeType: 0,
  802. // 开始售卖时间(当storeStatus为1时必填)
  803. saleTimeStr: "",
  804. // 结束售卖时间
  805. endTime: "",
  806. // 库存数量
  807. inventoryNum: "",
  808. // 每人限购设置:0-不限量,1-自定义限购数量
  809. quotaType: 0,
  810. // 自定义限购数量(当quotaType为1时必填)
  811. quotaValue: 0,
  812. // 套餐内容(虚拟字段,用于表单验证,实际数据在 lifeGroupBuyThalis 变量中)
  813. lifeGroupBuyThalis: null,
  814. // 原价
  815. originalPrice: "",
  816. // 优惠价
  817. preferentialPrice: "",
  818. // 有效期设置:0-指定天数,1-指定时间段内可用
  819. effectiveDateType: 0,
  820. expirationDate: 0,
  821. effectiveDateValue: [],
  822. // 不可用日期设置:0-全部日期可用,1-限制日期,2-自定义不可用日期
  823. disableDateType: 0,
  824. // 限制日期 - 星期选择(数组,存储选中的星期值)
  825. unavailableWeekdays: [],
  826. // 限制日期 - 节日选择(数组,存储选中的节日值)
  827. unavailableHolidays: [],
  828. // 预约规则
  829. reservationRules: "",
  830. // 使用规则
  831. useRules: "",
  832. // 适用人数
  833. applicableNum: "",
  834. // 其他规则
  835. otherRules: "",
  836. // 发票信息(复选框,可多选)
  837. invoiceInformation: [],
  838. // 发票说明
  839. invoiceDescribe: ""
  840. });
  841. // ==================== 下拉选项数据 ====================
  842. // 发票类型列表
  843. const businessTypes = ref([
  844. { value: 0, label: "提供电子发票" },
  845. { value: 1, label: "提供纸质发票" }
  846. ]);
  847. // 开始售卖时间设置方式列表
  848. const storeStatusList = ref([
  849. { value: 0, label: "审核通过后立即开始" },
  850. { value: 1, label: "设置售卖时间" }
  851. ]);
  852. // 每人限购设置列表
  853. const perList = ref([
  854. { value: 0, label: "不限量" },
  855. { value: 1, label: "自定义限购数量" }
  856. ]);
  857. // 每人限购设置列表
  858. const expirationDateList = ref([
  859. { value: 0, label: "指定天数" },
  860. { value: 1, label: "指定时间段内可用" }
  861. ]);
  862. // 每人限购设置列表
  863. const unavailableDatesList = ref([
  864. { value: 0, label: "全部日期可用" },
  865. { value: 1, label: "限制日期" },
  866. { value: 2, label: "自定义不可用日期" }
  867. ]);
  868. //图片集合
  869. const videoUrlList = ref<string[]>([]);
  870. // 自定义不可用日期列表
  871. const dates = ref([]);
  872. // 星期选项列表
  873. const weekdayList = ref([
  874. { name: "周一", id: "0", oName: "星期一" },
  875. { name: "周二", id: "1", oName: "星期二" },
  876. { name: "周三", id: "2", oName: "星期三" },
  877. { name: "周四", id: "3", oName: "星期四" },
  878. { name: "周五", id: "4", oName: "星期五" },
  879. { name: "周六", id: "5", oName: "星期六" },
  880. { name: "周日", id: "6", oName: "星期日" }
  881. ]);
  882. // 节日选项列表
  883. const holidayList: any = ref([]);
  884. // 菜品选项列表(这里需要根据实际API获取,暂时使用示例数据)
  885. const dishOptions = ref([]);
  886. // 套餐内容(数组,每个元素是一个分组)
  887. const lifeGroupBuyThalis = ref([
  888. {
  889. groupName: "", // 类别
  890. dishes: [
  891. // 菜品列表
  892. {
  893. detailId: "", // 菜品ID
  894. dishName: "", // 菜品名称
  895. dishPrice: "", // 菜品价格
  896. dishImage: "", // 菜品图片
  897. qty: "", // 数量
  898. dishesUnit: ""
  899. }
  900. ]
  901. }
  902. ]);
  903. // 菜品选择对话框相关
  904. const dishDialogVisible = ref(false);
  905. const dishSearchKeyword = ref("");
  906. const filteredDishList: any = ref([]);
  907. const selectedDishId = ref<number | string | null>(null);
  908. const currentDishGroupIndex = ref<number>(-1);
  909. const currentDishIndex = ref<number>(-1);
  910. // 套餐内容整体展开收起状态
  911. const allGroupsCollapsed = ref(false);
  912. // 每个分组的收起状态(数组,索引对应分组索引)
  913. const groupCollapsedStates = ref<boolean[]>([false]); // 默认第一个分组展开
  914. // 计算属性:过滤要显示的分组(收起全部时只显示第一个分组),保留原始索引
  915. const visibleGroups = computed(() => {
  916. if (allGroupsCollapsed.value) {
  917. return lifeGroupBuyThalis.value.map((group, index) => ({ group, originalIndex: index })).filter((_, index) => index === 0);
  918. }
  919. return lifeGroupBuyThalis.value.map((group, index) => ({ group, originalIndex: index }));
  920. });
  921. // 标记是否跳过最后一个分组的验证(用于添加新分组时)
  922. let skipLastGroupValidation = false;
  923. // ==================== 监听器 ====================
  924. /**
  925. * 监听不可用日期选项变化
  926. * 当切换到自定义不可用日期时,确保至少有一个日期项
  927. */
  928. watch(
  929. () => storeInfoModel.value.unavailableDates,
  930. newVal => {
  931. if (newVal === 2) {
  932. // 切换到自定义不可用日期时,如果dates为空,则添加一个默认项
  933. if (!dates.value || dates.value.length === 0) {
  934. dates.value = [null];
  935. }
  936. }
  937. },
  938. { immediate: true }
  939. );
  940. /**
  941. * 监听开始售卖时间变化
  942. * 当开始时间改变时,重新验证结束时间
  943. */
  944. watch(
  945. () => storeInfoModel.value.saleTimeStr,
  946. () => {
  947. if (storeInfoModel.value.endTime) {
  948. nextTick(() => {
  949. ruleFormRef.value?.validateField("endTime");
  950. });
  951. }
  952. }
  953. );
  954. /**
  955. * 监听结束售卖时间变化
  956. * 当结束时间改变时,重新验证开始时间
  957. */
  958. watch(
  959. () => storeInfoModel.value.endTime,
  960. () => {
  961. if (storeInfoModel.value.startTimeType === 1 && storeInfoModel.value.saleTimeStr) {
  962. nextTick(() => {
  963. ruleFormRef.value?.validateField("saleTimeStr");
  964. });
  965. }
  966. }
  967. );
  968. // ==================== 生命周期钩子 ====================
  969. /**
  970. * 组件挂载时初始化
  971. * 从路由参数中获取页面类型和ID
  972. */
  973. onMounted(async () => {
  974. id.value = (route.query.id as string) || "";
  975. type.value = (route.query.type as string) || "";
  976. let param = {
  977. // phone: localGet("iphone")
  978. phone: "18641153170"
  979. };
  980. const resP: any = await getUserByPhone(param);
  981. if (resP.data && resP.data.storeId) {
  982. localSet("createdId", resP.data.storeId);
  983. const resD: any = await getDetail({
  984. id: localGet("createdId")
  985. });
  986. if (resD.data && resD.data.commissionRate) {
  987. localSet("commissionRate", resD.data.commissionRate);
  988. }
  989. if (resD.data && resD.data.businessSection) {
  990. localSet("businessSection", resD.data.businessSection);
  991. }
  992. } else {
  993. ElMessage.warning("请完成商家入驻后再进行新建团购");
  994. }
  995. let params = {
  996. year: new Date().getFullYear(),
  997. page: 1,
  998. size: 500,
  999. openFlag: 1,
  1000. holidayName: ""
  1001. };
  1002. let res = await getHolidayList(params);
  1003. if (res && res.code == 200) {
  1004. holidayList.value = res.data.records;
  1005. }
  1006. if (type.value != "add") {
  1007. let res: any = await getStoreDetail({ id: id.value } as any);
  1008. storeInfoModel.value = res.data as any;
  1009. videoUrlList.value = (res.data as any).businessLicenseAddress || [];
  1010. let imageList: any[] = [];
  1011. handleImageParam((res.data as any).businessLicenseAddress || [], imageList);
  1012. storeInfoModel.value.imageList = imageList;
  1013. // 确保星期和节日字段存在
  1014. if (!storeInfoModel.value.unavailableWeekdays) {
  1015. storeInfoModel.value.unavailableWeekdays = [];
  1016. }
  1017. if (!storeInfoModel.value.unavailableHolidays) {
  1018. storeInfoModel.value.unavailableHolidays = [];
  1019. }
  1020. // 确保套餐内容字段存在
  1021. if (res.data.lifeGroupBuyThalis && res.data.lifeGroupBuyThalis.length > 0) {
  1022. lifeGroupBuyThalis.value = res.data.lifeGroupBuyThalis;
  1023. // 确保每个分组都有必要的字段
  1024. lifeGroupBuyThalis.value.forEach((group: any) => {
  1025. if (!group.dishes || group.dishes.length === 0) {
  1026. group.dishes = [
  1027. {
  1028. detailId: "",
  1029. qty: ""
  1030. }
  1031. ];
  1032. }
  1033. });
  1034. // 初始化所有分组的收起状态为展开
  1035. groupCollapsedStates.value = new Array(lifeGroupBuyThalis.value.length).fill(false);
  1036. } else {
  1037. // 如果没有数据,使用默认值
  1038. lifeGroupBuyThalis.value = [
  1039. {
  1040. groupName: "",
  1041. dishes: [
  1042. {
  1043. detailId: "",
  1044. dishName: "",
  1045. dishPrice: "",
  1046. dishImage: "",
  1047. qty: "",
  1048. dishesUnit: ""
  1049. }
  1050. ]
  1051. }
  1052. ];
  1053. // 初始化默认分组的收起状态为展开
  1054. groupCollapsedStates.value = [false];
  1055. }
  1056. // 确保自定义不可用日期字段存在
  1057. if (storeInfoModel.value.unavailableDates === 2) {
  1058. if (!dates.value || dates.value.length === 0) {
  1059. dates.value = [null];
  1060. }
  1061. }
  1062. } else {
  1063. // 新增模式下,如果默认选择自定义不可用日期,确保dates至少有一个元素
  1064. if (storeInfoModel.value.unavailableDates === 2) {
  1065. if (!dates.value || dates.value.length === 0) {
  1066. dates.value = [null];
  1067. }
  1068. }
  1069. }
  1070. });
  1071. // ==================== 事件处理函数 ====================
  1072. /**
  1073. * 返回上一页
  1074. */
  1075. const goBack = () => {
  1076. router.go(-1);
  1077. };
  1078. /**
  1079. * 图片上传 - 删除前确认
  1080. * @param uploadFile 要删除的文件对象
  1081. * @param uploadFiles 当前文件列表
  1082. * @returns Promise<boolean>,true 允许删除,false 阻止删除
  1083. */
  1084. const handleBeforeRemove = async (uploadFile: any, uploadFiles: any[]): Promise<boolean> => {
  1085. try {
  1086. await ElMessageBox.confirm("确定要删除这张图片吗?", "提示", {
  1087. confirmButtonText: "确定",
  1088. cancelButtonText: "取消",
  1089. type: "warning"
  1090. });
  1091. // 用户确认删除,返回 true 允许删除
  1092. return true;
  1093. } catch {
  1094. // 用户取消删除,返回 false 阻止删除
  1095. return false;
  1096. }
  1097. };
  1098. /**
  1099. * 图片上传 - 移除图片回调(删除成功后调用)
  1100. * @param uploadFile 已删除的文件对象
  1101. * @param uploadFiles 删除后的文件列表
  1102. */
  1103. const handleRemove: UploadProps["onRemove"] = (uploadFile, uploadFiles) => {
  1104. // 删除成功后提示
  1105. ElMessage.success("图片已删除");
  1106. };
  1107. /**
  1108. * 图片上传 - 上传成功回调
  1109. * @param response 上传响应数据
  1110. */
  1111. const handleSuccess = (response: any) => {
  1112. ElMessage.success("图片上传成功");
  1113. // 上传成功后,imageList 会自动更新,response.data 包含图片URL
  1114. };
  1115. /**
  1116. * 图片预览 - 使用 el-image 预览功能
  1117. * @param file 上传文件对象
  1118. */
  1119. const handlePictureCardPreview = (file: any) => {
  1120. // 获取所有图片的 URL 列表
  1121. const urlList = storeInfoModel.value.imageList.map((item: any) => item.url || item.response?.data || item);
  1122. // 找到当前点击的图片索引
  1123. const currentIndex = urlList.findIndex((url: string) => url === file.url);
  1124. imageViewerUrlList.value = urlList;
  1125. imageViewerInitialIndex.value = currentIndex >= 0 ? currentIndex : 0;
  1126. imageViewerVisible.value = true;
  1127. };
  1128. /**
  1129. * 添加自定义不可用日期
  1130. */
  1131. const addDate = () => {
  1132. dates.value.push(null);
  1133. };
  1134. /**
  1135. * 删除自定义不可用日期
  1136. * @param index 要删除的日期索引
  1137. */
  1138. const removeDate = (index: number) => {
  1139. if (dates.value.length <= 1) {
  1140. ElMessage.warning("至少需要保留一个日期项");
  1141. return;
  1142. }
  1143. dates.value.splice(index, 1);
  1144. };
  1145. /**
  1146. * 切换星期选择
  1147. * @param value 星期值
  1148. */
  1149. const toggleWeekday = value => {
  1150. if (!storeInfoModel.value.unavailableWeekdays) {
  1151. storeInfoModel.value.unavailableWeekdays = [];
  1152. }
  1153. const index = storeInfoModel.value.unavailableWeekdays.indexOf(value);
  1154. if (index > -1) {
  1155. storeInfoModel.value.unavailableWeekdays.splice(index, 1);
  1156. } else {
  1157. storeInfoModel.value.unavailableWeekdays.push(value);
  1158. }
  1159. console.log(storeInfoModel.value.unavailableWeekdays);
  1160. // 触发表单验证
  1161. nextTick(() => {
  1162. ruleFormRef.value?.validateField("unavailableWeekdays");
  1163. });
  1164. };
  1165. /**
  1166. * 切换节日选择
  1167. * @param value 节日值
  1168. */
  1169. const toggleHoliday = value => {
  1170. if (!storeInfoModel.value.unavailableHolidays) {
  1171. storeInfoModel.value.unavailableHolidays = [];
  1172. }
  1173. const index = storeInfoModel.value.unavailableHolidays.indexOf(value);
  1174. if (index > -1) {
  1175. storeInfoModel.value.unavailableHolidays.splice(index, 1);
  1176. } else {
  1177. storeInfoModel.value.unavailableHolidays.push(value);
  1178. }
  1179. console.log(storeInfoModel.value.unavailableHolidays);
  1180. // 触发表单验证
  1181. nextTick(() => {
  1182. ruleFormRef.value?.validateField("unavailableHolidays");
  1183. });
  1184. };
  1185. /**
  1186. * 添加套餐分组
  1187. */
  1188. const addGroup = () => {
  1189. if (!lifeGroupBuyThalis.value) {
  1190. lifeGroupBuyThalis.value = [];
  1191. }
  1192. // 检查分组数量限制(最多20个)
  1193. if (lifeGroupBuyThalis.value.length >= 20) {
  1194. ElMessage.warning("最多只能添加20个分组");
  1195. return;
  1196. }
  1197. // 验证所有现有分组是否填写完整
  1198. for (let i = 0; i < lifeGroupBuyThalis.value.length; i++) {
  1199. const group = lifeGroupBuyThalis.value[i];
  1200. // 验证类别
  1201. if (!group.groupName || group.groupName.trim() === "") {
  1202. ElMessage.warning("请先完成现有分组的填写");
  1203. // 触发表单验证,显示验证错误
  1204. nextTick(() => {
  1205. if (packageFormRef.value) {
  1206. packageFormRef.value.validate(() => {});
  1207. }
  1208. });
  1209. return;
  1210. }
  1211. // 验证是否有菜品
  1212. if (!group.dishes || group.dishes.length === 0) {
  1213. ElMessage.warning("请先完成现有分组的填写");
  1214. // 触发表单验证,显示验证错误
  1215. nextTick(() => {
  1216. if (packageFormRef.value) {
  1217. packageFormRef.value.validate(() => {});
  1218. }
  1219. });
  1220. return;
  1221. }
  1222. // 验证当前分组中每个菜品是否填写完整
  1223. for (let j = 0; j < group.dishes.length; j++) {
  1224. const dish = group.dishes[j];
  1225. // 验证菜品是否已选择
  1226. if (!dish.detailId) {
  1227. ElMessage.warning("请先完成现有分组的填写");
  1228. // 触发表单验证,显示验证错误
  1229. nextTick(() => {
  1230. if (packageFormRef.value) {
  1231. packageFormRef.value.validate(() => {});
  1232. }
  1233. });
  1234. return;
  1235. }
  1236. // 验证数量是否已填写
  1237. if (!dish.qty || dish.qty.toString().trim() === "") {
  1238. ElMessage.warning("请先完成现有分组的填写");
  1239. // 触发表单验证,显示验证错误
  1240. nextTick(() => {
  1241. if (packageFormRef.value) {
  1242. packageFormRef.value.validate(() => {});
  1243. }
  1244. });
  1245. return;
  1246. }
  1247. // 验证数量格式
  1248. const quantityNum = Number(dish.qty);
  1249. if (isNaN(quantityNum) || quantityNum <= 0 || !Number.isInteger(quantityNum)) {
  1250. ElMessage.warning("请先完成现有分组的填写");
  1251. // 触发表单验证,显示验证错误
  1252. nextTick(() => {
  1253. if (packageFormRef.value) {
  1254. packageFormRef.value.validate(() => {});
  1255. }
  1256. });
  1257. return;
  1258. }
  1259. }
  1260. }
  1261. // 记录旧分组的数量,用于后续只验证旧分组
  1262. const oldGroupsCount = lifeGroupBuyThalis.value.length;
  1263. // 添加新分组
  1264. lifeGroupBuyThalis.value.push({
  1265. groupName: "",
  1266. dishes: [
  1267. {
  1268. detailId: "",
  1269. dishName: "",
  1270. dishPrice: "",
  1271. dishImage: "",
  1272. qty: "",
  1273. dishesUnit: ""
  1274. }
  1275. ]
  1276. });
  1277. // 初始化新分组的收起状态为展开
  1278. groupCollapsedStates.value.push(false);
  1279. // 清除表单验证状态,避免新分组显示验证错误
  1280. // 使用延迟清除,确保在验证规则更新和 DOM 渲染完成后再清除
  1281. nextTick(() => {
  1282. requestAnimationFrame(() => {
  1283. setTimeout(() => {
  1284. if (packageFormRef.value) {
  1285. // 只清除新添加分组的验证状态
  1286. const newGroupIndex = lifeGroupBuyThalis.value.length - 1;
  1287. const propsToClear = [
  1288. `groups.${newGroupIndex}.groupName`,
  1289. `groups.${newGroupIndex}.dishes.0.detailId`,
  1290. `groups.${newGroupIndex}.dishes.0.qty`
  1291. ];
  1292. // Element Plus 的 clearValidate 可以接受字符串数组
  1293. propsToClear.forEach(prop => {
  1294. packageFormRef.value?.clearValidate(prop);
  1295. });
  1296. }
  1297. skipLastGroupValidation = false;
  1298. }, 150);
  1299. });
  1300. });
  1301. };
  1302. /**
  1303. * 删除套餐分组
  1304. * @param groupIndex 分组索引
  1305. */
  1306. const removeGroup = (groupIndex: number) => {
  1307. if (lifeGroupBuyThalis.value.length <= 1) {
  1308. ElMessage.warning("至少需要保留一个分组");
  1309. return;
  1310. }
  1311. ElMessageBox.confirm("确定要删除这个分组吗?", "提示", {
  1312. confirmButtonText: "确定",
  1313. cancelButtonText: "取消",
  1314. type: "warning"
  1315. })
  1316. .then(() => {
  1317. lifeGroupBuyThalis.value.splice(groupIndex, 1);
  1318. // 同步删除收起状态数组中的对应项
  1319. if (groupCollapsedStates.value.length > groupIndex) {
  1320. groupCollapsedStates.value.splice(groupIndex, 1);
  1321. }
  1322. ElMessage.success("删除成功");
  1323. })
  1324. .catch(() => {});
  1325. };
  1326. /**
  1327. * 判断分组是否收起
  1328. * @param groupIndex 分组索引
  1329. * @returns 是否收起(收起时只显示类别和第一行菜品,隐藏其他菜品行)
  1330. */
  1331. const isGroupCollapsed = (groupIndex: number): boolean => {
  1332. // 如果"收起全部"状态为true,第一个分组应该展开(显示所有菜品),其他分组收起
  1333. if (allGroupsCollapsed.value) {
  1334. return groupIndex !== 0;
  1335. }
  1336. // 否则根据分组的收起状态判断
  1337. return groupCollapsedStates.value[groupIndex] === true;
  1338. };
  1339. /**
  1340. * 切换单个分组的收起/展开状态
  1341. * @param groupIndex 分组索引
  1342. */
  1343. const toggleGroupCollapse = (groupIndex: number) => {
  1344. // 如果当前是"收起全部"状态,先取消该状态
  1345. if (allGroupsCollapsed.value) {
  1346. allGroupsCollapsed.value = false;
  1347. // 设置所有分组状态:第一个分组当前是展开的,其他分组当前是收起的
  1348. // 所以第一个分组应该设置为收起,其他分组应该设置为展开
  1349. groupCollapsedStates.value = new Array(lifeGroupBuyThalis.value.length).fill(true);
  1350. groupCollapsedStates.value[0] = false; // 第一个分组原本是展开的
  1351. // 然后切换当前分组的状态
  1352. groupCollapsedStates.value[groupIndex] = !groupCollapsedStates.value[groupIndex];
  1353. } else {
  1354. // 确保数组长度足够
  1355. while (groupCollapsedStates.value.length <= groupIndex) {
  1356. groupCollapsedStates.value.push(false);
  1357. }
  1358. groupCollapsedStates.value[groupIndex] = !groupCollapsedStates.value[groupIndex];
  1359. }
  1360. };
  1361. /**
  1362. * 切换所有分组的收起/展开状态
  1363. * 收起全部时保留第一个分组展开
  1364. */
  1365. const toggleAllGroupsCollapse = () => {
  1366. if (allGroupsCollapsed.value) {
  1367. // 展开全部:将所有分组设置为展开状态
  1368. allGroupsCollapsed.value = false;
  1369. groupCollapsedStates.value = groupCollapsedStates.value.map(() => false);
  1370. } else {
  1371. // 收起全部:收起除第一个分组外的所有分组
  1372. allGroupsCollapsed.value = true;
  1373. groupCollapsedStates.value = groupCollapsedStates.value.map((_, index) => index !== 0);
  1374. // 确保数组长度足够
  1375. while (groupCollapsedStates.value.length < lifeGroupBuyThalis.value.length) {
  1376. groupCollapsedStates.value.push(true);
  1377. }
  1378. }
  1379. };
  1380. /**
  1381. * 添加菜品
  1382. * @param groupIndex 分组索引
  1383. */
  1384. const addDish = (groupIndex: number) => {
  1385. const group = lifeGroupBuyThalis.value[groupIndex];
  1386. // 检查菜品数量限制(每个分组最多20个菜品)
  1387. if (group.dishes && group.dishes.length >= 20) {
  1388. ElMessage.warning("每个分组最多只能添加20个菜品");
  1389. return;
  1390. }
  1391. // 验证当前分组是否填写完整
  1392. // 验证类别
  1393. if (!group.groupName || group.groupName.trim() === "") {
  1394. ElMessage.warning("请先完成现有菜品的填写");
  1395. // 触发表单验证,显示验证错误
  1396. nextTick(() => {
  1397. if (packageFormRef.value) {
  1398. packageFormRef.value.validate(() => {});
  1399. }
  1400. });
  1401. return;
  1402. }
  1403. // 验证是否有菜品
  1404. if (!group.dishes || group.dishes.length === 0) {
  1405. ElMessage.warning("请先完成现有菜品的填写");
  1406. // 触发表单验证,显示验证错误
  1407. nextTick(() => {
  1408. if (packageFormRef.value) {
  1409. packageFormRef.value.validate(() => {});
  1410. }
  1411. });
  1412. return;
  1413. }
  1414. // 验证当前分组中每个菜品是否填写完整
  1415. for (let j = 0; j < group.dishes.length; j++) {
  1416. const dish = group.dishes[j];
  1417. // 验证菜品是否已选择
  1418. if (!dish.detailId) {
  1419. ElMessage.warning("请先完成现有菜品的填写");
  1420. // 触发表单验证,显示验证错误
  1421. nextTick(() => {
  1422. if (packageFormRef.value) {
  1423. packageFormRef.value.validate(() => {});
  1424. }
  1425. });
  1426. return;
  1427. }
  1428. // 验证数量是否已填写
  1429. if (!dish.qty || dish.qty.toString().trim() === "") {
  1430. ElMessage.warning("请先完成现有菜品的填写");
  1431. // 触发表单验证,显示验证错误
  1432. nextTick(() => {
  1433. if (packageFormRef.value) {
  1434. packageFormRef.value.validate(() => {});
  1435. }
  1436. });
  1437. return;
  1438. }
  1439. // 验证数量格式
  1440. const quantityNum = Number(dish.qty);
  1441. if (isNaN(quantityNum) || quantityNum <= 0 || !Number.isInteger(quantityNum)) {
  1442. ElMessage.warning("请先完成现有菜品的填写");
  1443. // 触发表单验证,显示验证错误
  1444. nextTick(() => {
  1445. if (packageFormRef.value) {
  1446. packageFormRef.value.validate(() => {});
  1447. }
  1448. });
  1449. return;
  1450. }
  1451. }
  1452. // 所有验证通过,添加新菜品
  1453. if (!group.dishes) {
  1454. group.dishes = [];
  1455. }
  1456. group.dishes.push({
  1457. detailId: "",
  1458. dishName: "",
  1459. dishPrice: "",
  1460. dishImage: "",
  1461. qty: "",
  1462. dishesUnit: ""
  1463. });
  1464. // 清除新添加菜品的验证状态,避免立即显示验证错误
  1465. // 使用延迟清除,确保在验证规则更新和 DOM 渲染完成后再清除
  1466. nextTick(() => {
  1467. requestAnimationFrame(() => {
  1468. setTimeout(() => {
  1469. if (packageFormRef.value) {
  1470. // 只清除新添加菜品的验证状态
  1471. const newDishIndex = group.dishes.length - 1;
  1472. const propsToClear = [
  1473. `groups.${groupIndex}.dishes.${newDishIndex}.detailId`,
  1474. `groups.${groupIndex}.dishes.${newDishIndex}.qty`
  1475. ];
  1476. // Element Plus 的 clearValidate 可以接受字符串
  1477. propsToClear.forEach(prop => {
  1478. packageFormRef.value?.clearValidate(prop);
  1479. });
  1480. }
  1481. }, 150);
  1482. });
  1483. });
  1484. };
  1485. /**
  1486. * 删除菜品
  1487. * @param groupIndex 分组索引
  1488. * @param dishIndex 菜品索引
  1489. */
  1490. const removeDish = (groupIndex: number, dishIndex: number) => {
  1491. const dishes = lifeGroupBuyThalis.value[groupIndex].dishes;
  1492. if (dishes.length <= 1) {
  1493. ElMessage.warning("至少需要保留一个菜品");
  1494. return;
  1495. }
  1496. dishes.splice(dishIndex, 1);
  1497. };
  1498. /**
  1499. * 打开菜品选择对话框
  1500. * @param groupIndex 分组索引
  1501. * @param dishIndex 菜品索引
  1502. */
  1503. const openDishDialog = async (groupIndex: number, dishIndex: number) => {
  1504. currentDishGroupIndex.value = groupIndex;
  1505. currentDishIndex.value = dishIndex;
  1506. dishSearchKeyword.value = "";
  1507. const params = {
  1508. storeId: 361,
  1509. phoneId: 18641153170,
  1510. dishType: 0
  1511. };
  1512. const res = await getMenuByStoreId(params);
  1513. if (res && res.code == 200) {
  1514. dishOptions.value = res.data;
  1515. }
  1516. // 如果已有选中的菜品,设置选中状态
  1517. const currentDish = lifeGroupBuyThalis.value[groupIndex].dishes[dishIndex];
  1518. selectedDishId.value = currentDish?.detailId || null;
  1519. filterDishList();
  1520. dishDialogVisible.value = true;
  1521. };
  1522. /**
  1523. * 选择菜品
  1524. * @param dish 菜品对象
  1525. */
  1526. const selectDish = (dish: any) => {
  1527. selectedDishId.value = dish.id;
  1528. };
  1529. /**
  1530. * 确认菜品选择
  1531. */
  1532. const confirmDishSelection = () => {
  1533. if (selectedDishId.value === null) {
  1534. ElMessage.warning("请选择一个菜品");
  1535. return;
  1536. }
  1537. const selectedDish: any = dishOptions.value.find(dish => dish.id === selectedDishId.value);
  1538. if (selectedDish && currentDishGroupIndex.value >= 0 && currentDishIndex.value >= 0) {
  1539. const dish = lifeGroupBuyThalis.value[currentDishGroupIndex.value].dishes[currentDishIndex.value];
  1540. dish.detailId = selectedDish.id;
  1541. dish.dishName = selectedDish.dishName;
  1542. dish.dishPrice = selectedDish.dishPrice;
  1543. dish.dishImage = selectedDish.imgUrl;
  1544. dish.dishesUnit = selectedDish.dishesUnit;
  1545. dishDialogVisible.value = false;
  1546. // 重新验证对应的字段,如果验证通过,错误提示会消失
  1547. nextTick(() => {
  1548. if (packageFormRef.value) {
  1549. const prop = `groups.${currentDishGroupIndex.value}.dishes.${currentDishIndex.value}.detailId`;
  1550. packageFormRef.value.validateField(prop, () => {});
  1551. }
  1552. });
  1553. }
  1554. };
  1555. /**
  1556. * 过滤菜品列表
  1557. * 排除已经在其他分组中被选择的菜品(当前正在编辑的菜品除外)
  1558. */
  1559. const filterDishList = () => {
  1560. // 获取当前正在编辑的菜品ID(如果有)
  1561. const currentDishId = selectedDishId.value;
  1562. // 收集所有其他分组中已选择的菜品ID
  1563. const selectedDishIds = new Set<number | string>();
  1564. lifeGroupBuyThalis.value.forEach((group, groupIndex) => {
  1565. if (group.dishes && group.dishes.length > 0) {
  1566. group.dishes.forEach((dish, dishIndex) => {
  1567. // 排除当前正在编辑的菜品
  1568. if (dish.detailId && !(groupIndex === currentDishGroupIndex.value && dishIndex === currentDishIndex.value)) {
  1569. selectedDishIds.add(dish.detailId);
  1570. }
  1571. });
  1572. }
  1573. });
  1574. // 先根据搜索关键词过滤
  1575. let filtered = dishOptions.value;
  1576. if (dishSearchKeyword.value && dishSearchKeyword.value.trim() !== "") {
  1577. const keyword = dishSearchKeyword.value.trim().toLowerCase();
  1578. filtered = dishOptions.value.filter(dish => dish.dishName.toLowerCase().includes(keyword));
  1579. }
  1580. // 再排除已在其他分组中被选择的菜品(但保留当前正在编辑的菜品)
  1581. filteredDishList.value = filtered.filter(dish => {
  1582. // 如果是当前正在编辑的菜品,保留
  1583. if (currentDishId && dish.id === currentDishId) {
  1584. return true;
  1585. }
  1586. // 如果菜品已在其他分组中被选择,排除
  1587. return !selectedDishIds.has(dish.id);
  1588. });
  1589. };
  1590. // ==================== 表单引用 ====================
  1591. const ruleFormRef = ref<FormInstance>(); // 表单引用
  1592. const packageFormRef = ref<FormInstance>(); // 套餐内容表单引用
  1593. // 套餐内容表单模型(用于验证)
  1594. const packageFormModel = computed(() => {
  1595. return {
  1596. groups: lifeGroupBuyThalis.value
  1597. };
  1598. });
  1599. // 套餐内容验证规则
  1600. const packageFormRules = computed(() => {
  1601. const rules: any = {};
  1602. lifeGroupBuyThalis.value.forEach((group, groupIndex) => {
  1603. // 类别验证规则 - 使用与 prop 相同的路径格式
  1604. rules[`groups.${groupIndex}.groupName`] = [
  1605. {
  1606. required: true,
  1607. message: `第${groupIndex + 1}个分组的类别不能为空`,
  1608. trigger: "blur"
  1609. }
  1610. ];
  1611. // 每个菜品的验证规则
  1612. if (group.dishes) {
  1613. group.dishes.forEach((dish, dishIndex) => {
  1614. // 菜品选择验证
  1615. rules[`groups.${groupIndex}.dishes.${dishIndex}.detailId`] = [
  1616. {
  1617. required: true,
  1618. message: `第${groupIndex + 1}个分组的第${dishIndex + 1}个菜品未选择`,
  1619. trigger: "change"
  1620. }
  1621. ];
  1622. // 菜品数量验证
  1623. rules[`groups.${groupIndex}.dishes.${dishIndex}.qty`] = [
  1624. {
  1625. required: true,
  1626. message: `第${groupIndex + 1}个分组的第${dishIndex + 1}个菜品数量不能为空`,
  1627. trigger: "blur"
  1628. },
  1629. {
  1630. validator: (rule: any, value: any, callback: any) => {
  1631. if (!value || value.toString().trim() === "") {
  1632. callback();
  1633. return;
  1634. }
  1635. const num = Number(value);
  1636. if (isNaN(num) || num <= 0) {
  1637. callback(new Error(`第${groupIndex + 1}个分组的第${dishIndex + 1}个菜品数量必须为正整数`));
  1638. return;
  1639. }
  1640. if (!Number.isInteger(num)) {
  1641. callback(new Error(`第${groupIndex + 1}个分组的第${dishIndex + 1}个菜品数量必须为正整数`));
  1642. return;
  1643. }
  1644. callback();
  1645. },
  1646. trigger: "blur"
  1647. }
  1648. ];
  1649. });
  1650. }
  1651. });
  1652. return rules;
  1653. });
  1654. /**
  1655. * 提交数据(新增/编辑)
  1656. * 验证表单,通过后调用相应的API接口
  1657. */
  1658. const handleSubmit = (type?) => {
  1659. let params: any = { ...storeInfoModel.value };
  1660. if (params.effectiveDateType == 0) {
  1661. params.couponCompDate = `用户购买${params.expirationDate}天内有效`;
  1662. } else {
  1663. params.effectiveDateValue = params.effectiveDateValue.join(",");
  1664. }
  1665. if (params.disableDateType == 1) {
  1666. params.disableDateValue = params.unavailableWeekdays.join(",") + ";" + params.unavailableHolidays.join(",");
  1667. delete params.unavailableWeekdays;
  1668. delete params.unavailableHolidays;
  1669. } else if (params.disableDateType == 2) {
  1670. params.disableDateValue = dates.value.map(subArray => subArray.join(",")).join(";");
  1671. }
  1672. params.invoiceType = params.invoiceInformation.join(",");
  1673. const output = lifeGroupBuyThalis.value.flatMap(group =>
  1674. group.dishes.map(dish => ({
  1675. groupName: group.groupName,
  1676. detailId: dish.detailId,
  1677. qty: dish.qty,
  1678. dishPrice: dish.dishPrice
  1679. }))
  1680. );
  1681. const paramsObj = {
  1682. lifeGroupBuyMain: params,
  1683. lifeGroupBuyThalis: output
  1684. };
  1685. console.log(paramsObj);
  1686. if (type) {
  1687. return;
  1688. }
  1689. // 验证表单
  1690. ruleFormRef.value!.validate(async valid => {
  1691. if (!valid) return;
  1692. // 验证套餐内容表单
  1693. if (packageFormRef.value) {
  1694. let packageValid = false;
  1695. await new Promise<void>(resolve => {
  1696. packageFormRef.value!.validate(valid => {
  1697. packageValid = valid;
  1698. if (!valid) {
  1699. ElMessage.error("请完善套餐内容信息");
  1700. }
  1701. resolve();
  1702. });
  1703. });
  1704. if (!packageValid) {
  1705. return;
  1706. }
  1707. }
  1708. // 组装提交参数
  1709. let params: any = { ...storeInfoModel.value };
  1710. params.lifeGroupBuyThalis = lifeGroupBuyThalis.value;
  1711. });
  1712. };
  1713. // ==================== 工具函数 ====================
  1714. /**
  1715. * 禁用开始售卖时间的日期
  1716. * 不能选择早于当前时间的日期
  1717. * @param time 日期对象
  1718. * @returns 是否禁用该日期
  1719. */
  1720. const disabledStartDate = (time: Date) => {
  1721. const today = new Date();
  1722. today.setHours(0, 0, 0, 0);
  1723. return time.getTime() < today.getTime();
  1724. };
  1725. /**
  1726. * 禁用结束售卖时间的日期
  1727. * 不能选择早于当前时间的日期,也不能选择早于或等于开始售卖时间的日期
  1728. * @param time 日期对象
  1729. * @returns 是否禁用该日期
  1730. */
  1731. const disabledEndDate = (time: Date) => {
  1732. const today = new Date();
  1733. today.setHours(0, 0, 0, 0);
  1734. // 不能早于当前时间
  1735. if (time.getTime() < today.getTime()) {
  1736. return true;
  1737. }
  1738. // 如果开始售卖时间已设置,不能早于或等于开始时间
  1739. if (storeInfoModel.value.startTimeType === 1 && storeInfoModel.value.saleTimeStr) {
  1740. const startDate = new Date(storeInfoModel.value.saleTimeStr);
  1741. startDate.setHours(0, 0, 0, 0);
  1742. return time.getTime() <= startDate.getTime();
  1743. }
  1744. return false;
  1745. };
  1746. /**
  1747. * 禁用指定时间段内可用的日期
  1748. * 不能选择早于当前时间的日期
  1749. * @param time 日期对象
  1750. * @returns 是否禁用该日期
  1751. */
  1752. const disabledEffectiveDate = (time: Date) => {
  1753. const today = new Date();
  1754. today.setHours(0, 0, 0, 0);
  1755. return time.getTime() < today.getTime();
  1756. };
  1757. /**
  1758. * 禁用自定义不可用日期的日期
  1759. * 不能选择早于当前时间的日期
  1760. * @param time 日期对象
  1761. * @returns 是否禁用该日期
  1762. */
  1763. const disabledCustomUnavailableDate = (time: Date) => {
  1764. const today = new Date();
  1765. today.setHours(0, 0, 0, 0);
  1766. return time.getTime() < today.getTime();
  1767. };
  1768. /**
  1769. * 处理图片结果
  1770. * 将上传组件的文件对象或URL字符串转换为URL数组
  1771. * @param list 图片列表(可能是文件对象或URL字符串)
  1772. * @returns URL字符串数组
  1773. */
  1774. const handleImageResult = (list: any[]): string[] => {
  1775. let result: string[] = [];
  1776. (list || []).forEach((item: any) => {
  1777. if (item.uid) {
  1778. // 如果是上传组件的文件对象,取url属性
  1779. result.push(item.url);
  1780. } else {
  1781. // 如果是URL字符串,直接添加
  1782. result.push(item);
  1783. }
  1784. });
  1785. return result;
  1786. };
  1787. //图片list 转换为upload 组件的参数
  1788. const handleImageParam = (list: any[], result: any[]) => {
  1789. (list || []).forEach((item: any) => {
  1790. // 使用split方法以'/'为分隔符将URL拆分成数组
  1791. const parts = item.split("/");
  1792. // 取数组的最后一项,即图片名称
  1793. const imageName = parts[parts.length - 1];
  1794. result.push({
  1795. name: imageName,
  1796. percentage: 100,
  1797. url: item
  1798. });
  1799. });
  1800. };
  1801. </script>
  1802. <style scoped lang="scss">
  1803. /* 页面容器 */
  1804. .table-box {
  1805. display: flex;
  1806. flex-direction: column;
  1807. height: auto !important;
  1808. min-height: 100%;
  1809. }
  1810. /* 内容区域布局 */
  1811. .content {
  1812. display: flex;
  1813. flex: 1;
  1814. column-gap: 20px;
  1815. width: 98%;
  1816. margin: 20px auto;
  1817. /* 左侧内容区域 */
  1818. .contentLeft {
  1819. width: 50%;
  1820. }
  1821. /* 右侧内容区域 */
  1822. .contentRight {
  1823. width: 50%;
  1824. }
  1825. }
  1826. /* 模块容器 */
  1827. .model {
  1828. margin-bottom: 50px;
  1829. }
  1830. /* 表单容器 */
  1831. .formBox {
  1832. display: flex;
  1833. flex-direction: column;
  1834. width: 100%;
  1835. min-height: 100%;
  1836. }
  1837. /* 底部按钮容器 - 居中显示 */
  1838. .button-container {
  1839. display: flex;
  1840. gap: 12px;
  1841. align-items: center;
  1842. justify-content: center;
  1843. padding: 20px 0;
  1844. margin-top: 20px;
  1845. }
  1846. /* 日期选择器容器 */
  1847. .date-picker-container {
  1848. display: flex;
  1849. flex-direction: column;
  1850. gap: 12px;
  1851. width: 100%;
  1852. }
  1853. /* 添加日期按钮 */
  1854. .add-date-btn {
  1855. width: fit-content;
  1856. margin-bottom: 8px;
  1857. }
  1858. /* 日期项容器 */
  1859. .date-item {
  1860. display: flex;
  1861. gap: 12px;
  1862. align-items: center;
  1863. padding: 8px;
  1864. // background-color: #f5f7fa;
  1865. border-radius: 4px;
  1866. transition: background-color 0.1s;
  1867. &:hover {
  1868. background-color: #ecf5ff;
  1869. }
  1870. }
  1871. /* 日期选择器 */
  1872. .date-item .date-picker {
  1873. flex: 1;
  1874. max-width: 500px;
  1875. }
  1876. /* 删除按钮 */
  1877. .date-item .delete-btn {
  1878. flex-shrink: 0;
  1879. }
  1880. /* 有效期天数容器 */
  1881. .expiration-date-container {
  1882. display: flex;
  1883. gap: 12px;
  1884. align-items: center;
  1885. width: fit-content;
  1886. padding: 8px 12px;
  1887. // background-color: #f5f7fa;
  1888. border-radius: 4px;
  1889. }
  1890. /* 有效期标签文字 */
  1891. .expiration-label {
  1892. font-size: 14px;
  1893. color: #606266;
  1894. white-space: nowrap;
  1895. }
  1896. /* 有效期输入框 */
  1897. .expiration-input {
  1898. width: 150px;
  1899. }
  1900. /* 不可用日期容器 */
  1901. .unavailable-dates-container {
  1902. display: flex;
  1903. flex-direction: column;
  1904. gap: 20px;
  1905. width: 100%;
  1906. }
  1907. /* 日期选择区块 */
  1908. .date-select-section {
  1909. display: flex;
  1910. flex-direction: column;
  1911. gap: 12px;
  1912. }
  1913. /* 区块标题 */
  1914. .section-title {
  1915. font-size: 14px;
  1916. font-weight: 500;
  1917. color: #606266;
  1918. }
  1919. /* 按钮组 */
  1920. .button-group {
  1921. display: flex;
  1922. flex-wrap: wrap;
  1923. gap: 10px;
  1924. }
  1925. /* 日期选择按钮 */
  1926. .date-select-btn {
  1927. min-width: 80px;
  1928. height: 36px;
  1929. padding: 8px 16px;
  1930. margin: 0;
  1931. font-size: 14px;
  1932. border-radius: 4px;
  1933. transition: all 0.1s;
  1934. }
  1935. /* 套餐内容相关样式 */
  1936. .package-content-item {
  1937. width: 100%;
  1938. }
  1939. .package-content-form {
  1940. width: 100%;
  1941. }
  1942. /* 表单项基础样式 */
  1943. .package-content-form :deep(.el-form-item) {
  1944. margin-bottom: 0;
  1945. }
  1946. .package-content-form :deep(.el-form-item__label) {
  1947. display: none;
  1948. }
  1949. /* 类别表单项样式 */
  1950. .category-form-item {
  1951. width: 100%;
  1952. margin-bottom: 16px;
  1953. }
  1954. .category-form-item :deep(.el-form-item__content) {
  1955. display: flex;
  1956. flex-wrap: wrap;
  1957. align-items: flex-start;
  1958. width: 100%;
  1959. }
  1960. .category-form-item :deep(.el-form-item__error) {
  1961. position: static;
  1962. width: 100%;
  1963. padding-top: 4px;
  1964. padding-left: 62px;
  1965. margin-top: 0;
  1966. font-size: 12px;
  1967. line-height: 1.5;
  1968. color: #f56c6c;
  1969. }
  1970. .category-row .el-input {
  1971. flex: 1;
  1972. max-width: 400px;
  1973. }
  1974. /* 菜品表单项样式 */
  1975. .dish-form-item {
  1976. position: relative;
  1977. flex: 1;
  1978. margin-bottom: 0;
  1979. }
  1980. .dish-form-item :deep(.el-form-item__content) {
  1981. display: flex;
  1982. flex-wrap: wrap;
  1983. gap: 12px;
  1984. align-items: center;
  1985. }
  1986. .dish-form-item :deep(.el-form-item__error) {
  1987. position: absolute;
  1988. top: 100%;
  1989. left: 0;
  1990. z-index: 1;
  1991. padding-top: 4px;
  1992. padding-right: 4px;
  1993. font-size: 12px;
  1994. line-height: 1.5;
  1995. color: #f56c6c;
  1996. white-space: nowrap;
  1997. background-color: #ffffff;
  1998. }
  1999. .package-content-wrapper {
  2000. display: flex;
  2001. flex-direction: column;
  2002. gap: 12px;
  2003. width: 100%;
  2004. }
  2005. .package-content-header {
  2006. display: flex;
  2007. align-items: center;
  2008. justify-content: space-between;
  2009. width: 100%;
  2010. }
  2011. .add-group-btn {
  2012. width: fit-content;
  2013. }
  2014. .package-content-container {
  2015. display: flex;
  2016. flex-direction: column;
  2017. gap: 16px;
  2018. width: 100%;
  2019. }
  2020. .package-group {
  2021. overflow: hidden;
  2022. background-color: white;
  2023. border: 1px solid #e4e7ed;
  2024. border-radius: 4px;
  2025. }
  2026. .package-group-header {
  2027. display: flex;
  2028. align-items: center;
  2029. justify-content: space-between;
  2030. height: 50px;
  2031. padding: 12px 16px;
  2032. background-color: #f5f7fa;
  2033. border-bottom: 1px solid #e4e7ed;
  2034. }
  2035. .group-index {
  2036. flex: 1;
  2037. overflow: hidden;
  2038. font-size: 14px;
  2039. font-weight: 500;
  2040. color: #303133;
  2041. text-overflow: ellipsis;
  2042. white-space: nowrap;
  2043. }
  2044. .header-right {
  2045. display: flex;
  2046. flex-shrink: 0;
  2047. gap: 12px;
  2048. align-items: center;
  2049. }
  2050. .package-group-content {
  2051. display: flex;
  2052. flex-direction: column;
  2053. gap: 16px;
  2054. min-height: 60px;
  2055. padding: 16px;
  2056. }
  2057. .extra-dishes-container {
  2058. display: flex;
  2059. flex-direction: column;
  2060. gap: 16px;
  2061. margin-top: 0;
  2062. }
  2063. .extra-dishes-container .dish-row {
  2064. padding-bottom: 22px;
  2065. margin-bottom: 4px;
  2066. }
  2067. .extra-dishes-container .dish-row:last-child {
  2068. padding-bottom: 0;
  2069. margin-bottom: 0;
  2070. }
  2071. .package-group-collapsed {
  2072. padding: 16px;
  2073. background-color: #fafafa;
  2074. border-top: 1px solid #e4e7ed;
  2075. }
  2076. .collapsed-dish-row {
  2077. display: flex;
  2078. gap: 12px;
  2079. align-items: center;
  2080. }
  2081. .dish-info-text {
  2082. font-size: 14px;
  2083. color: #606266;
  2084. white-space: nowrap;
  2085. }
  2086. .dish-info-text.empty-text {
  2087. color: #c0c4cc;
  2088. }
  2089. .category-row {
  2090. display: flex;
  2091. gap: 12px;
  2092. align-items: center;
  2093. width: 100%;
  2094. }
  2095. .dish-row {
  2096. position: relative;
  2097. display: flex;
  2098. gap: 12px;
  2099. align-items: flex-start;
  2100. width: 100%;
  2101. margin-bottom: 4px;
  2102. }
  2103. .dish-row:last-child {
  2104. padding-bottom: 0;
  2105. margin-bottom: 0;
  2106. }
  2107. /* 当表单项有错误时,增加底部间距 */
  2108. .dish-row .dish-form-item.is-error {
  2109. margin-bottom: 0;
  2110. }
  2111. .label {
  2112. min-width: 50px;
  2113. font-size: 14px;
  2114. color: #606266;
  2115. white-space: nowrap;
  2116. }
  2117. .dish-select-block {
  2118. position: relative;
  2119. display: flex;
  2120. flex: 1;
  2121. align-items: center;
  2122. max-width: 300px;
  2123. min-height: 32px;
  2124. padding: 0 11px;
  2125. cursor: pointer;
  2126. background-color: #ffffff;
  2127. border: 1px solid #dcdfe6;
  2128. border-radius: 4px;
  2129. transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
  2130. }
  2131. .dish-select-block:hover {
  2132. border-color: #c0c4cc;
  2133. }
  2134. .dish-select-block:focus-within {
  2135. border-color: #409eff;
  2136. }
  2137. /* 当菜品选择有验证错误时,显示错误边框 */
  2138. .dish-form-item.is-error .dish-select-block {
  2139. border-color: #f56c6c;
  2140. }
  2141. .dish-form-item.is-error .dish-select-block:hover {
  2142. border-color: #f56c6c;
  2143. }
  2144. .dish-form-item.is-error .dish-select-block:focus-within {
  2145. border-color: #f56c6c;
  2146. }
  2147. /* 优化表单项的错误状态 */
  2148. .package-content-form :deep(.el-form-item.is-error .el-input__wrapper) {
  2149. box-shadow: 0 0 0 1px #f56c6c inset;
  2150. }
  2151. .package-content-form :deep(.el-form-item.is-error .el-input__wrapper:hover) {
  2152. box-shadow: 0 0 0 1px #f56c6c inset;
  2153. }
  2154. .package-content-form :deep(.el-form-item.is-error .el-input__wrapper.is-focus) {
  2155. box-shadow: 0 0 0 1px #f56c6c inset;
  2156. }
  2157. .dish-selected-name {
  2158. font-size: 14px;
  2159. color: #606266;
  2160. }
  2161. .dish-placeholder {
  2162. font-size: 14px;
  2163. color: #c0c4cc;
  2164. }
  2165. .quantity-input {
  2166. flex-shrink: 0;
  2167. width: 150px;
  2168. }
  2169. .delete-dish-btn {
  2170. flex-shrink: 0;
  2171. margin-left: 8px;
  2172. }
  2173. .add-dish-btn {
  2174. width: fit-content;
  2175. margin-top: 8px;
  2176. }
  2177. /* 优化菜品行的布局,确保在有验证错误时也能正确显示 */
  2178. .dish-row .dish-form-item {
  2179. min-width: 0;
  2180. }
  2181. .dish-row .dish-form-item:first-child {
  2182. flex: 1;
  2183. min-width: 200px;
  2184. max-width: 350px;
  2185. }
  2186. .dish-row .dish-form-item:last-child {
  2187. display: flex;
  2188. flex: 0 0 auto;
  2189. gap: 8px;
  2190. align-items: center;
  2191. }
  2192. /* 确保数量输入框和删除按钮在同一行 */
  2193. .dish-form-item:last-child :deep(.el-form-item__content) {
  2194. display: flex;
  2195. flex-wrap: nowrap;
  2196. gap: 8px;
  2197. align-items: center;
  2198. }
  2199. /* 响应式布局优化 */
  2200. @media (width <= 768px) {
  2201. .dish-row {
  2202. flex-direction: column;
  2203. align-items: stretch;
  2204. }
  2205. .dish-row .dish-form-item {
  2206. width: 100%;
  2207. max-width: 100%;
  2208. }
  2209. .dish-row .dish-form-item:first-child {
  2210. max-width: 100%;
  2211. }
  2212. .category-row {
  2213. flex-direction: column;
  2214. align-items: stretch;
  2215. }
  2216. .category-row .el-input {
  2217. max-width: 100%;
  2218. }
  2219. }
  2220. /* 菜品选择对话框样式 */
  2221. .dish-dialog-content {
  2222. display: flex;
  2223. flex-direction: column;
  2224. gap: 16px;
  2225. }
  2226. .dish-search-input {
  2227. width: 100%;
  2228. }
  2229. .dish-list-container {
  2230. :deep(.el-scrollbar__wrap) {
  2231. overflow-x: hidden;
  2232. }
  2233. :deep(.el-scrollbar__view) {
  2234. display: flex;
  2235. flex-direction: column;
  2236. gap: 12px;
  2237. }
  2238. }
  2239. .dish-empty-state {
  2240. display: flex;
  2241. flex-direction: column;
  2242. align-items: center;
  2243. justify-content: center;
  2244. padding: 60px 20px;
  2245. text-align: center;
  2246. }
  2247. .empty-text {
  2248. margin-bottom: 8px;
  2249. font-size: 16px;
  2250. color: #909399;
  2251. }
  2252. .empty-hint {
  2253. font-size: 14px;
  2254. color: #c0c4cc;
  2255. }
  2256. .dish-item {
  2257. position: relative;
  2258. display: flex;
  2259. gap: 12px;
  2260. align-items: center;
  2261. padding: 12px;
  2262. cursor: pointer;
  2263. border: 1px solid #e4e7ed;
  2264. border-radius: 4px;
  2265. transition: all 0.1s;
  2266. }
  2267. .dish-item:hover {
  2268. background-color: #f5f7fa;
  2269. border-color: #409eff;
  2270. }
  2271. .dish-item-selected {
  2272. background-color: #ecf5ff;
  2273. border-color: #409eff;
  2274. border-width: 2px;
  2275. }
  2276. .dish-item-selected:hover {
  2277. background-color: #d9ecff;
  2278. }
  2279. .dish-image {
  2280. flex-shrink: 0;
  2281. width: 60px;
  2282. height: 60px;
  2283. overflow: hidden;
  2284. background-color: #f5f7fa;
  2285. border-radius: 4px;
  2286. }
  2287. .dish-img {
  2288. width: 100%;
  2289. height: 100%;
  2290. }
  2291. .dish-image-placeholder {
  2292. display: flex;
  2293. align-items: center;
  2294. justify-content: center;
  2295. width: 100%;
  2296. height: 100%;
  2297. font-size: 24px;
  2298. color: #c0c4cc;
  2299. }
  2300. .dish-info {
  2301. display: flex;
  2302. flex: 1;
  2303. flex-direction: column;
  2304. gap: 4px;
  2305. }
  2306. .dish-name {
  2307. font-size: 14px;
  2308. font-weight: 500;
  2309. color: #606266;
  2310. }
  2311. .dish-price {
  2312. font-size: 14px;
  2313. color: #909399;
  2314. }
  2315. .dish-selected-icon {
  2316. display: flex;
  2317. flex-shrink: 0;
  2318. align-items: center;
  2319. justify-content: center;
  2320. width: 24px;
  2321. height: 24px;
  2322. font-size: 16px;
  2323. color: #ffffff;
  2324. background-color: #409eff;
  2325. border-radius: 50%;
  2326. }
  2327. .dialog-footer {
  2328. display: flex;
  2329. justify-content: center;
  2330. }
  2331. /* 展开收起动画 - 高度过渡 */
  2332. .slide-fade-enter-active {
  2333. overflow: hidden;
  2334. transition:
  2335. max-height 0.1s ease-out,
  2336. opacity 0.1s ease-out;
  2337. }
  2338. .slide-fade-leave-active {
  2339. overflow: hidden;
  2340. transition:
  2341. max-height 0.1s ease-in,
  2342. opacity 0.1s ease-in;
  2343. }
  2344. .slide-fade-enter-from {
  2345. max-height: 0;
  2346. opacity: 0;
  2347. }
  2348. .slide-fade-leave-to {
  2349. max-height: 0;
  2350. opacity: 0;
  2351. }
  2352. .slide-fade-enter-to,
  2353. .slide-fade-leave-from {
  2354. max-height: 2000px;
  2355. opacity: 1;
  2356. }
  2357. /* 分组收起全部动画 */
  2358. .group-fade-enter-active {
  2359. transition: all 0.1s ease-out;
  2360. }
  2361. .group-fade-leave-active {
  2362. transition: all 0.1s ease-in;
  2363. }
  2364. .group-fade-enter-from {
  2365. opacity: 0;
  2366. transform: translateY(-10px);
  2367. }
  2368. .group-fade-leave-to {
  2369. opacity: 0;
  2370. transform: translateY(-10px);
  2371. }
  2372. .group-fade-move {
  2373. transition: transform 0.1s linear;
  2374. }
  2375. </style>