index.vue 49 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665
  1. <template>
  2. <ContentWrap>
  3. <div class="fixed-save-bar">
  4. <el-button type="warning" size="default" :loading="submitting" @click="handleSubmit">
  5. 保存设置
  6. </el-button>
  7. </div>
  8. <el-form
  9. ref="formRef"
  10. :model="formData"
  11. :rules="formRules"
  12. label-width="120px"
  13. class="setting-form"
  14. >
  15. <el-row :gutter="20" class="main-row">
  16. <el-col :span="12" class="col-left">
  17. <div class="card card-top-left">
  18. <div class="card-header">
  19. <el-icon class="card-icon icon-blue"><Setting /></el-icon>
  20. <span class="card-title">基础设置</span>
  21. <div class="master-switch">
  22. <span class="switch-label">预约总开关</span>
  23. <el-switch
  24. v-model="formData.appointmentEnabled"
  25. active-text="开启"
  26. inactive-text="关闭"
  27. />
  28. </div>
  29. </div>
  30. <div class="card-body">
  31. <el-form-item label="放号规则">
  32. <el-radio-group v-model="formData.ruleType" :disabled="isDisabled">
  33. <el-radio :value="1">时间循环预约</el-radio>
  34. </el-radio-group>
  35. </el-form-item>
  36. <el-form-item v-show="false" label="开始预约日" prop="startDay">
  37. <el-select
  38. v-model="formData.startDay"
  39. placeholder="请选择"
  40. style="width: 100%"
  41. :disabled="isDisabled"
  42. >
  43. <el-option label="当天" :value="0" />
  44. <el-option label="未来1天" :value="1" />
  45. <el-option label="未来2天" :value="2" />
  46. <el-option label="未来3天" :value="3" />
  47. <el-option label="未来4天" :value="4" />
  48. <el-option label="未来5天" :value="5" />
  49. <el-option label="未来6天" :value="6" />
  50. <el-option label="未来7天" :value="7" />
  51. <el-option label="未来8天" :value="8" />
  52. <el-option label="未来9天" :value="9" />
  53. <el-option label="未来10天" :value="10" />
  54. <el-option label="未来11天" :value="11" />
  55. <el-option label="未来12天" :value="12" />
  56. <el-option label="未来13天" :value="13" />
  57. <el-option label="未来14天" :value="14" />
  58. <el-option label="未来15天" :value="15" />
  59. </el-select>
  60. </el-form-item>
  61. <el-form-item label="提前预约" prop="advanceDays">
  62. <el-input-number
  63. v-model="formData.advanceDays"
  64. :min="1"
  65. :max="30"
  66. style="width: 100%"
  67. :disabled="isDisabled"
  68. >
  69. <template #prefix>
  70. <span>可提前</span>
  71. </template>
  72. <template #suffix>
  73. <span>天预约</span>
  74. </template>
  75. </el-input-number>
  76. </el-form-item>
  77. </div>
  78. </div>
  79. <div class="card card-bottom-left">
  80. <div class="card-header">
  81. <el-icon class="card-icon icon-green"><Calendar /></el-icon>
  82. <span class="card-title">预约组</span>
  83. <el-button
  84. type="primary"
  85. size="small"
  86. :disabled="isDisabled"
  87. @click="addAppointmentGroup"
  88. >
  89. <el-icon><Plus /></el-icon>
  90. 添加预约组
  91. </el-button>
  92. </div>
  93. <div class="card-body card-body-groups">
  94. <div v-if="appointmentGroups.length === 0" class="empty-groups">
  95. <el-empty description="暂无预约组,点击右上角添加" :image-size="60" />
  96. </div>
  97. <div v-else class="group-list">
  98. <div
  99. v-for="(group, groupIndex) in appointmentGroups"
  100. :key="group.id"
  101. class="group-card"
  102. >
  103. <div class="group-header">
  104. <span class="group-label">预约日期:</span>
  105. <div class="cycle-buttons">
  106. <el-button
  107. :type="group.cycleType === 'daily' ? 'success' : 'default'"
  108. size="small"
  109. :disabled="isDisabled || group.enable == 0"
  110. @click="setCycleType(group, 'daily')"
  111. >
  112. 每天
  113. </el-button>
  114. <el-button
  115. :type="group.cycleType === 'weekly' ? 'success' : 'default'"
  116. size="small"
  117. :disabled="isDisabled || group.enable == 0"
  118. @click="openWeeklyDialog(group)"
  119. >
  120. 每周
  121. </el-button>
  122. <el-button
  123. :type="group.cycleType === 'monthly' ? 'success' : 'default'"
  124. size="small"
  125. :disabled="isDisabled || group.enable == 0"
  126. @click="openMonthlyDialog(group)"
  127. >
  128. 每月
  129. </el-button>
  130. <el-button
  131. :type="group.cycleType === 'custom' ? 'success' : 'default'"
  132. size="small"
  133. :disabled="isDisabled || group.enable == 0"
  134. @click="openCustomDialog(group)"
  135. >
  136. 自定义
  137. </el-button>
  138. </div>
  139. <el-switch
  140. v-model="group.enable"
  141. active-text="启用"
  142. size="small"
  143. :disabled="isDisabled"
  144. :active-value="1"
  145. :inactive-value="0"
  146. style="margin-right: 1vw"
  147. inactive-text="关闭"
  148. />
  149. <el-button
  150. type="danger"
  151. link
  152. size="small"
  153. :disabled="isDisabled || group.enable == 0"
  154. @click="removeGroup(groupIndex)"
  155. >
  156. <el-icon><Delete /></el-icon>
  157. 删除组
  158. </el-button>
  159. </div>
  160. <div class="group-dates">
  161. <template v-if="group.cycleType === 'daily'">
  162. <el-tag type="success" size="small">每天都可预约</el-tag>
  163. </template>
  164. <template v-else-if="group.cycleType === 'weekly'">
  165. <span class="dates-label">每周可预约:</span>
  166. <el-tag v-if="group.weeklyDays.length === 0" type="info" size="small"
  167. >未设置</el-tag
  168. >
  169. <el-tag
  170. v-for="day in group.weeklyDays"
  171. :key="day"
  172. type="success"
  173. size="small"
  174. class="date-tag"
  175. >
  176. {{ getWeekDayLabel(day) }}
  177. </el-tag>
  178. </template>
  179. <template v-else-if="group.cycleType === 'monthly'">
  180. <span class="dates-label">每月可预约:</span>
  181. <el-tag v-if="group.monthlyDays.length === 0" type="info" size="small"
  182. >未设置</el-tag
  183. >
  184. <el-tag
  185. v-for="day in group.monthlyDays"
  186. :key="day"
  187. type="success"
  188. size="small"
  189. class="date-tag"
  190. >
  191. {{ day }}号
  192. </el-tag>
  193. </template>
  194. <template v-else-if="group.cycleType === 'custom'">
  195. <span class="dates-label">自定义日期:</span>
  196. <el-tag v-if="group.customDates.length === 0" type="info" size="small"
  197. >未设置</el-tag
  198. >
  199. <el-tag
  200. v-for="date in group.customDates.slice(0, 5)"
  201. :key="date"
  202. type="success"
  203. size="small"
  204. class="date-tag"
  205. >
  206. {{ date }}
  207. </el-tag>
  208. <el-tooltip
  209. v-if="group.customDates.length > 5"
  210. :content="group.customDates.join('、')"
  211. placement="top"
  212. >
  213. <el-tag type="info" size="small" class="date-tag">
  214. +{{ group.customDates.length - 5 }}
  215. </el-tag>
  216. </el-tooltip>
  217. </template>
  218. </div>
  219. <div class="group-items-section">
  220. <div class="items-header">
  221. <span class="items-title">预约项</span>
  222. <span class="items-sub">(多项时间段不可重叠)</span>
  223. </div>
  224. <div v-if="group.items.length === 0" class="empty-items">
  225. 暂无预约项,点击下方按钮添加
  226. </div>
  227. <div v-else class="items-list">
  228. <div v-for="(item, itemIndex) in group.items" :key="item.id" class="item-row">
  229. <div
  230. class="item-delete-btn"
  231. :class="{ disabled: isDisabled || group.enable == 0 }"
  232. @click="removeItem(group, itemIndex)"
  233. >
  234. <el-icon><Delete /></el-icon>
  235. </div>
  236. <div class="item-fields">
  237. <div class="field-block">
  238. <span class="field-label">时间段</span>
  239. <el-time-picker
  240. v-model="item.startTime"
  241. format="HH:mm"
  242. value-format="HH:mm"
  243. placeholder="开始时间"
  244. size="small"
  245. style="width: 110px"
  246. :disabled="isDisabled || group.enable == 0"
  247. @change="checkTimeOverlap(group, itemIndex)"
  248. />
  249. <span class="time-sep">至</span>
  250. <el-time-picker
  251. v-model="item.endTime"
  252. format="HH:mm"
  253. value-format="HH:mm"
  254. placeholder="结束时间"
  255. size="small"
  256. style="width: 110px"
  257. :disabled="isDisabled || group.enable == 0"
  258. @change="checkTimeOverlap(group, itemIndex)"
  259. />
  260. </div>
  261. <div class="field-block">
  262. <span class="field-label">限制人数</span>
  263. <el-input-number
  264. v-model="item.limitCount"
  265. :min="1"
  266. :max="9999"
  267. size="small"
  268. style="width: 110px"
  269. :disabled="isDisabled || group.enable == 0"
  270. />
  271. </div>
  272. </div>
  273. </div>
  274. </div>
  275. <div class="add-item-row">
  276. <el-button
  277. type="success"
  278. size="small"
  279. :disabled="isDisabled || group.enable == 0"
  280. @click="addItem(group)"
  281. >
  282. <el-icon><Plus /></el-icon>
  283. 添加预约项
  284. </el-button>
  285. </div>
  286. </div>
  287. </div>
  288. </div>
  289. </div>
  290. </div>
  291. </el-col>
  292. <el-col :span="12" class="col-right">
  293. <div class="card card-mid-right">
  294. <div class="card-header">
  295. <el-icon class="card-icon icon-green"><Tickets /></el-icon>
  296. <span class="card-title">放号时间段(每天可开始预约时间)</span>
  297. </div>
  298. <div class="card-body card-body-limit" style="padding: 40px 30px">
  299. <span class="field-label">放号开始时间:</span>
  300. <el-time-picker
  301. v-model="formData.startTime"
  302. format="HH:mm"
  303. value-format="HH:mm"
  304. placeholder="开始时间"
  305. style="width: 150px"
  306. :disabled="isDisabled"
  307. />
  308. <span class="field-label" style="margin-left: 3vw">放号结束时间:</span>
  309. <el-time-picker
  310. v-model="formData.endTime"
  311. format="HH:mm"
  312. value-format="HH:mm"
  313. placeholder="结束时间"
  314. style="width: 150px"
  315. :disabled="isDisabled"
  316. />
  317. </div>
  318. </div>
  319. <div class="card card-bottom-right">
  320. <div class="card-header">
  321. <el-icon class="card-icon icon-purple"><User /></el-icon>
  322. <span class="card-title">单人名额限制(按自然周和月)</span>
  323. <el-button
  324. type="primary"
  325. size="small"
  326. :disabled="isDisabled || personLimits.length >= 3"
  327. @click="openPersonLimitDialog"
  328. >
  329. <el-icon><Plus /></el-icon>
  330. 添加
  331. </el-button>
  332. </div>
  333. <div class="card-body card-body-limit">
  334. <div v-if="personLimits.length === 0" class="empty-limits-inline">
  335. <el-empty description="暂无限制,点击右上角添加" :image-size="40" />
  336. </div>
  337. <div v-else class="limit-list-inline">
  338. <div v-for="(limit, index) in personLimits" :key="index" class="limit-item-inline">
  339. <span class="limit-type-badge">{{ getLimitTypeLabel(limit.type) }}</span>
  340. <span class="limit-count-num">{{ limit.count }} <em>次</em></span>
  341. <el-button
  342. type="danger"
  343. link
  344. size="small"
  345. :disabled="isDisabled"
  346. @click="removePersonLimit(index)"
  347. >
  348. <el-icon><Delete /></el-icon>
  349. </el-button>
  350. </div>
  351. </div>
  352. </div>
  353. </div>
  354. </el-col>
  355. </el-row>
  356. </el-form>
  357. <el-dialog v-model="totalLimitDialogVisible" title="添加总名额限制" width="420px" align-center>
  358. <el-form :model="totalLimitForm" label-width="100px" style="margin-bottom: 20px">
  359. <el-form-item label="限制类型" required>
  360. <el-select v-model="totalLimitForm.type" placeholder="请选择" style="width: 100%">
  361. <el-option
  362. v-for="item in availableTotalTypes"
  363. :key="item.value"
  364. :label="item.label"
  365. :value="item.value"
  366. />
  367. </el-select>
  368. </el-form-item>
  369. <el-form-item label="限制次数" required>
  370. <el-input-number
  371. v-model="totalLimitForm.count"
  372. :min="1"
  373. :max="9999"
  374. style="width: 100%"
  375. />
  376. </el-form-item>
  377. </el-form>
  378. <template #footer>
  379. <el-button @click="totalLimitDialogVisible = false">取消</el-button>
  380. <el-button type="primary" @click="confirmTotalLimit">确定</el-button>
  381. </template>
  382. </el-dialog>
  383. <el-dialog
  384. v-model="personLimitDialogVisible"
  385. title="添加单人名额限制"
  386. width="420px"
  387. align-center
  388. >
  389. <el-form :model="personLimitForm" label-width="100px" style="margin-bottom: 20px">
  390. <el-form-item label="限制类型" required>
  391. <el-select v-model="personLimitForm.type" placeholder="请选择" style="width: 100%">
  392. <el-option
  393. v-for="item in availablePersonTypes"
  394. :key="item.value"
  395. :label="item.label"
  396. :value="item.value"
  397. />
  398. </el-select>
  399. </el-form-item>
  400. <el-form-item label="限制次数" required>
  401. <el-input-number
  402. v-model="personLimitForm.count"
  403. :min="1"
  404. :max="9999"
  405. style="width: 100%"
  406. />
  407. </el-form-item>
  408. </el-form>
  409. <template #footer>
  410. <el-button @click="personLimitDialogVisible = false">取消</el-button>
  411. <el-button type="primary" @click="confirmPersonLimit">确定</el-button>
  412. </template>
  413. </el-dialog>
  414. <el-dialog
  415. v-model="weeklyDialogVisible"
  416. title="选择每周可预约日期"
  417. width="480px"
  418. align-center
  419. class="date-selector-dialog"
  420. :append-to-body="false"
  421. >
  422. <div class="date-selector-body">
  423. <div class="selector-title">请选择每周可预约的日期</div>
  424. <div class="week-days-grid">
  425. <div
  426. v-for="day in weekDays"
  427. :key="day.value"
  428. class="day-cell"
  429. :class="{ active: weeklySelectedDays.includes(day.value) }"
  430. @click="toggleWeekDay(day.value)"
  431. >
  432. {{ day.label }}
  433. </div>
  434. </div>
  435. </div>
  436. <template #footer>
  437. <el-button @click="weeklyDialogVisible = false">取消</el-button>
  438. <el-button type="primary" @click="confirmWeekly">确定</el-button>
  439. </template>
  440. </el-dialog>
  441. <el-dialog
  442. v-model="monthlyDialogVisible"
  443. title="选择每月可预约日期"
  444. width="560px"
  445. align-center
  446. class="date-selector-dialog"
  447. :append-to-body="false"
  448. >
  449. <div class="date-selector-body">
  450. <div class="selector-title">请选择每月可预约的日期</div>
  451. <div class="month-days-grid">
  452. <div
  453. v-for="day in 31"
  454. :key="day"
  455. class="day-cell"
  456. :class="{ active: monthlySelectedDays.includes(day) }"
  457. @click="toggleMonthDay(day)"
  458. >
  459. {{ day }}
  460. </div>
  461. </div>
  462. </div>
  463. <template #footer>
  464. <el-button @click="monthlyDialogVisible = false">取消</el-button>
  465. <el-button type="primary" @click="confirmMonthly">确定</el-button>
  466. </template>
  467. </el-dialog>
  468. <el-dialog
  469. v-model="customDialogVisible"
  470. title="选择自定义日期"
  471. width="420px"
  472. align-center
  473. :append-to-body="false"
  474. >
  475. <div class="date-selector-body">
  476. <div class="selector-title">请选择可预约的日期</div>
  477. <el-date-picker
  478. v-model="customSelectedDates"
  479. type="dates"
  480. placeholder="选择日期"
  481. value-format="YYYY-MM-DD"
  482. style="width: 100%"
  483. />
  484. </div>
  485. <template #footer>
  486. <el-button @click="customDialogVisible = false">取消</el-button>
  487. <el-button type="primary" @click="confirmCustom">确定</el-button>
  488. </template>
  489. </el-dialog>
  490. </ContentWrap>
  491. </template>
  492. <script setup lang="ts">
  493. import { ref, reactive, onMounted, computed } from 'vue'
  494. import { Setting, Calendar, User, Plus, Delete, Tickets, Minus } from '@element-plus/icons-vue'
  495. import type { FormInstance, FormRules } from 'element-plus'
  496. import { ElMessageBox } from 'element-plus'
  497. import { useUserStore } from '@/store/modules/user'
  498. import {
  499. appointmentConfigADD,
  500. appointmentConfigGetByTenant,
  501. appointmentConfigUpdate
  502. } from '@/api/elderly/elder/outwardRegustration'
  503. const userStore = useUserStore()
  504. const message = useMessage()
  505. const formRef = ref<FormInstance>()
  506. const submitting = ref(false)
  507. const tenantIds = ref<number[]>([])
  508. let organizationId = 0
  509. let groupIdCounter = 0
  510. let itemIdCounter = 0
  511. const configId = ref<number | undefined>(undefined)
  512. type LimitItem = { type: number; count: number }
  513. type CycleType = 'daily' | 'weekly' | 'monthly' | 'custom'
  514. type AppointmentItem = {
  515. id: number
  516. // 后端返回的真实 id,新增的 item 没有此字段;提交时回传给后端,删除时进入 deleteIds
  517. serverId?: number
  518. startTime: string
  519. endTime: string
  520. limitCount: number
  521. }
  522. type AppointmentGroup = {
  523. id: number
  524. cycleType: CycleType
  525. enable: number
  526. weeklyDays: number[]
  527. monthlyDays: number[]
  528. customDates: string[]
  529. items: AppointmentItem[]
  530. }
  531. type AppointmentConfigDO = {
  532. id?: number
  533. advanceDays?: number
  534. beginBookDays?: number
  535. enable?: number
  536. personDailyLimit?: number
  537. personMonthlyLimit?: number
  538. personWeeklyLimit?: number
  539. tenantId?: number
  540. totalDailyLimit?: number
  541. totalMonthlyLimit?: number
  542. totalWeeklyLimit?: number
  543. [property: string]: any
  544. }
  545. type AppointmentGroupDO = {
  546. id?: number
  547. beginTime?: string
  548. customDates?: string
  549. dateType?: string
  550. endTime?: string
  551. enable?: number
  552. limitCount?: number
  553. monthDays?: string
  554. tenantId?: number
  555. weekDays?: string
  556. }
  557. const limitTypeOptions = [
  558. { label: '每天', value: 1 },
  559. { label: '每周', value: 2 },
  560. { label: '每月', value: 3 }
  561. ]
  562. const weekDays = [
  563. { label: '周一', value: 1 },
  564. { label: '周二', value: 2 },
  565. { label: '周三', value: 3 },
  566. { label: '周四', value: 4 },
  567. { label: '周五', value: 5 },
  568. { label: '周六', value: 6 },
  569. { label: '周日', value: 7 }
  570. ]
  571. const limitTypeToFieldMap: Record<number, { total: string; person: string }> = {
  572. 1: { total: 'totalDailyLimit', person: 'personDailyLimit' },
  573. 2: { total: 'totalWeeklyLimit', person: 'personWeeklyLimit' },
  574. 3: { total: 'totalMonthlyLimit', person: 'personMonthlyLimit' }
  575. }
  576. const cycleTypeToDateType = (cycleType: CycleType): string => cycleType.toUpperCase()
  577. const dateTypeToCycleType = (dateType: string): CycleType => {
  578. const map: Record<string, CycleType> = {
  579. DAILY: 'daily',
  580. WEEKLY: 'weekly',
  581. MONTHLY: 'monthly',
  582. CUSTOM: 'custom'
  583. }
  584. return map[dateType] || 'daily'
  585. }
  586. const totalLimits = ref<LimitItem[]>([])
  587. const personLimits = ref<LimitItem[]>([])
  588. const appointmentGroups = ref<AppointmentGroup[]>([])
  589. // 被删除的旧 item 后端 id 列表,提交时一并传给后端做物理删除
  590. const deleteIds = ref<number[]>([])
  591. const totalLimitDialogVisible = ref(false)
  592. const personLimitDialogVisible = ref(false)
  593. const weeklyDialogVisible = ref(false)
  594. const monthlyDialogVisible = ref(false)
  595. const customDialogVisible = ref(false)
  596. const totalLimitForm = reactive<LimitItem>({ type: 1, count: 1 })
  597. const personLimitForm = reactive<LimitItem>({ type: 1, count: 1 })
  598. const currentEditGroup = ref<AppointmentGroup | null>(null)
  599. const weeklySelectedDays = ref<number[]>([])
  600. const monthlySelectedDays = ref<number[]>([])
  601. const customSelectedDates = ref<string[]>([])
  602. const availableTotalTypes = computed(() => {
  603. const usedTypes = totalLimits.value.map((item) => item.type)
  604. return limitTypeOptions.filter((item) => !usedTypes.includes(item.value))
  605. })
  606. const availablePersonTypes = computed(() => {
  607. const usedTypes = personLimits.value.map((item) => item.type)
  608. return limitTypeOptions.filter((item) => !usedTypes.includes(item.value))
  609. })
  610. const isDisabled = computed(() => !formData.appointmentEnabled)
  611. const getLimitTypeLabel = (type: number) => {
  612. const found = limitTypeOptions.find((item) => item.value === type)
  613. return found ? found.label : ''
  614. }
  615. const getWeekDayLabel = (day: number) => {
  616. const found = weekDays.find((item) => item.value === day)
  617. return found ? found.label : ''
  618. }
  619. const defaultFormData = () => ({
  620. ruleType: 1,
  621. startDay: 0,
  622. advanceDays: 6,
  623. startTime: '08:00',
  624. endTime: '17:30',
  625. appointmentEnabled: true
  626. })
  627. const formData = reactive(defaultFormData())
  628. const timeToMinutes = (time: string): number => {
  629. if (!time) return -1
  630. const [h, m] = time.split(':').map(Number)
  631. return h * 60 + m
  632. }
  633. const isOverlap = (item1: AppointmentItem, item2: AppointmentItem): boolean => {
  634. if (!item1.startTime || !item1.endTime || !item2.startTime || !item2.endTime) return false
  635. const s1 = timeToMinutes(item1.startTime)
  636. const e1 = timeToMinutes(item1.endTime)
  637. const s2 = timeToMinutes(item2.startTime)
  638. const e2 = timeToMinutes(item2.endTime)
  639. if (s1 < 0 || e1 < 0 || s2 < 0 || e2 < 0) return false
  640. if (s1 >= e1 || s2 >= e2) return false
  641. return s1 < e2 && s2 < e1
  642. }
  643. const getDateDayOfMonth = (dateStr: string): number => {
  644. const parts = dateStr.split('-')
  645. if (parts.length !== 3) return -1
  646. return Number(parts[2])
  647. }
  648. const getDateWeekDay = (dateStr: string): number => {
  649. const parts = dateStr.split('-').map(Number)
  650. if (parts.length !== 3) return -1
  651. const date = new Date(parts[0], parts[1] - 1, parts[2])
  652. const day = date.getDay()
  653. return day === 0 ? 7 : day
  654. }
  655. const checkTimeOverlap = (group: AppointmentGroup, changedIndex: number) => {
  656. const changedItem = group.items[changedIndex]
  657. if (!changedItem.startTime || !changedItem.endTime) return
  658. if (timeToMinutes(changedItem.startTime) >= timeToMinutes(changedItem.endTime)) {
  659. message.warning('结束时间必须晚于开始时间')
  660. return
  661. }
  662. for (let i = 0; i < group.items.length; i++) {
  663. if (i === changedIndex) continue
  664. if (isOverlap(changedItem, group.items[i])) {
  665. message.warning('时间段与其他预约项重叠,请调整时间')
  666. return
  667. }
  668. }
  669. }
  670. const addAppointmentGroup = () => {
  671. groupIdCounter++
  672. itemIdCounter++
  673. appointmentGroups.value.push({
  674. id: groupIdCounter,
  675. cycleType: 'daily',
  676. enable: 1,
  677. weeklyDays: [],
  678. monthlyDays: [],
  679. customDates: [],
  680. items: [
  681. {
  682. id: itemIdCounter,
  683. startTime: '08:00',
  684. endTime: '09:00',
  685. limitCount: 20
  686. }
  687. ]
  688. })
  689. }
  690. const removeGroup = (index: number) => {
  691. const group = appointmentGroups.value[index]
  692. // 该组下所有带 serverId 的 item 都是后端旧数据,删除时记录 id 交给后端物理删除
  693. group.items.forEach((item) => {
  694. if (item.serverId) {
  695. deleteIds.value.push(item.serverId)
  696. }
  697. })
  698. appointmentGroups.value.splice(index, 1)
  699. }
  700. const addItem = (group: AppointmentGroup) => {
  701. let lastEnd = '08:00'
  702. if (group.items.length > 0) {
  703. const lastItem = group.items[group.items.length - 1]
  704. if (lastItem.endTime) {
  705. lastEnd = lastItem.endTime
  706. }
  707. }
  708. const lastEndMin = timeToMinutes(lastEnd)
  709. let newStart = lastEnd
  710. let newEnd = ''
  711. if (lastEndMin >= 0) {
  712. const newStartMin = Math.min(lastEndMin + 60, 23 * 60)
  713. const newEndMin = Math.min(newStartMin + 60, 24 * 60)
  714. const sh = Math.floor(newStartMin / 60)
  715. .toString()
  716. .padStart(2, '0')
  717. const sm = (newStartMin % 60).toString().padStart(2, '0')
  718. const eh = Math.floor(newEndMin / 60)
  719. .toString()
  720. .padStart(2, '0')
  721. const em = (newEndMin % 60).toString().padStart(2, '0')
  722. newStart = `${sh}:${sm}`
  723. newEnd = `${eh}:${em}`
  724. }
  725. itemIdCounter++
  726. group.items.push({
  727. id: itemIdCounter,
  728. startTime: newStart,
  729. endTime: newEnd,
  730. limitCount: 20
  731. })
  732. }
  733. const removeItem = (group: AppointmentGroup, index: number) => {
  734. const item = group.items[index]
  735. // 带 serverId 表示是后端旧数据,删除时记录 id 交给后端物理删除
  736. if (item.serverId) {
  737. deleteIds.value.push(item.serverId)
  738. }
  739. group.items.splice(index, 1)
  740. }
  741. const setCycleType = (group: AppointmentGroup, cycleType: CycleType) => {
  742. if (cycleType === 'daily') {
  743. group.cycleType = 'daily'
  744. }
  745. }
  746. const openWeeklyDialog = (group: AppointmentGroup) => {
  747. currentEditGroup.value = group
  748. weeklySelectedDays.value = [...group.weeklyDays]
  749. weeklyDialogVisible.value = true
  750. }
  751. const toggleWeekDay = (day: number) => {
  752. const idx = weeklySelectedDays.value.indexOf(day)
  753. if (idx > -1) {
  754. weeklySelectedDays.value.splice(idx, 1)
  755. } else {
  756. weeklySelectedDays.value.push(day)
  757. weeklySelectedDays.value.sort((a, b) => a - b)
  758. }
  759. }
  760. const confirmWeekly = () => {
  761. if (currentEditGroup.value) {
  762. currentEditGroup.value.weeklyDays = [...weeklySelectedDays.value]
  763. currentEditGroup.value.cycleType = 'weekly'
  764. }
  765. weeklyDialogVisible.value = false
  766. currentEditGroup.value = null
  767. weeklySelectedDays.value = []
  768. }
  769. const openMonthlyDialog = (group: AppointmentGroup) => {
  770. currentEditGroup.value = group
  771. monthlySelectedDays.value = [...group.monthlyDays]
  772. monthlyDialogVisible.value = true
  773. }
  774. const toggleMonthDay = (day: number) => {
  775. const idx = monthlySelectedDays.value.indexOf(day)
  776. if (idx > -1) {
  777. monthlySelectedDays.value.splice(idx, 1)
  778. } else {
  779. monthlySelectedDays.value.push(day)
  780. monthlySelectedDays.value.sort((a, b) => a - b)
  781. }
  782. }
  783. const confirmMonthly = () => {
  784. if (currentEditGroup.value) {
  785. currentEditGroup.value.monthlyDays = [...monthlySelectedDays.value]
  786. currentEditGroup.value.cycleType = 'monthly'
  787. }
  788. monthlyDialogVisible.value = false
  789. currentEditGroup.value = null
  790. monthlySelectedDays.value = []
  791. }
  792. const openCustomDialog = (group: AppointmentGroup) => {
  793. currentEditGroup.value = group
  794. customSelectedDates.value = [...group.customDates]
  795. customDialogVisible.value = true
  796. }
  797. const confirmCustom = () => {
  798. if (currentEditGroup.value) {
  799. currentEditGroup.value.customDates = [...customSelectedDates.value]
  800. currentEditGroup.value.cycleType = 'custom'
  801. }
  802. customDialogVisible.value = false
  803. currentEditGroup.value = null
  804. customSelectedDates.value = []
  805. }
  806. const openTotalLimitDialog = () => {
  807. if (availableTotalTypes.value.length === 0) return
  808. totalLimitForm.type = availableTotalTypes.value[0].value
  809. totalLimitForm.count = 1
  810. totalLimitDialogVisible.value = true
  811. }
  812. const confirmTotalLimit = () => {
  813. if (!totalLimitForm.type || !totalLimitForm.count) {
  814. message.warning('请填写完整信息')
  815. return
  816. }
  817. totalLimits.value.push({ type: totalLimitForm.type, count: totalLimitForm.count })
  818. totalLimitDialogVisible.value = false
  819. }
  820. const removeTotalLimit = (index: number) => {
  821. totalLimits.value.splice(index, 1)
  822. }
  823. const openPersonLimitDialog = () => {
  824. if (availablePersonTypes.value.length === 0) return
  825. personLimitForm.type = availablePersonTypes.value[0].value
  826. personLimitForm.count = 1
  827. personLimitDialogVisible.value = true
  828. }
  829. const confirmPersonLimit = () => {
  830. if (!personLimitForm.type || !personLimitForm.count) {
  831. message.warning('请填写完整信息')
  832. return
  833. }
  834. personLimits.value.push({ type: personLimitForm.type, count: personLimitForm.count })
  835. personLimitDialogVisible.value = false
  836. }
  837. const removePersonLimit = (index: number) => {
  838. personLimits.value.splice(index, 1)
  839. }
  840. const validatePersonLimits = (): string | null => {
  841. const daily = personLimits.value.find((item) => item.type === 1)
  842. const weekly = personLimits.value.find((item) => item.type === 2)
  843. const monthly = personLimits.value.find((item) => item.type === 3)
  844. if (daily && daily.count <= 0) {
  845. return '单人每日名额限制必须大于0'
  846. }
  847. if (weekly && weekly.count <= 0) {
  848. return '单人每周名额限制必须大于0'
  849. }
  850. if (monthly && monthly.count <= 0) {
  851. return '单人每月名额限制必须大于0'
  852. }
  853. if (monthly && weekly && monthly.count <= weekly.count) {
  854. return '单人每月名额限制必须大于每周名额限制'
  855. }
  856. if (weekly && daily && weekly.count <= daily.count) {
  857. return '单人每周名额限制必须大于每日名额限制'
  858. }
  859. return null
  860. }
  861. const validateAdvanceDays = (_rule: any, value: any, callback: any) => {
  862. if (!value || value < 1) {
  863. callback(new Error('提前预约天数最少为1'))
  864. } else {
  865. callback()
  866. }
  867. }
  868. const formRules: FormRules = reactive({
  869. ruleType: [{ required: true, message: '请选择放号规则', trigger: 'change' }],
  870. startDay: [{ required: true, message: '请选择开始预约日', trigger: 'change' }],
  871. advanceDays: [
  872. { required: true, message: '请输入提前预约天数', trigger: 'change' },
  873. { validator: validateAdvanceDays, trigger: 'change' }
  874. ]
  875. })
  876. const changeTen = (i: any[]) => {
  877. if (i.length > 1) {
  878. message.error('只能选择一个机构!')
  879. if (i[0].length > 1) {
  880. tenantIds.value = [i[0][1]]
  881. } else {
  882. tenantIds.value = []
  883. }
  884. }
  885. if (tenantIds.value.length > 0) {
  886. organizationId = tenantIds.value[0]
  887. getSetting()
  888. }
  889. }
  890. const parseGroups = (groups: AppointmentGroupDO[]): AppointmentGroup[] => {
  891. const map = new Map<string, AppointmentGroup>()
  892. groups.forEach((g) => {
  893. const cycleType = dateTypeToCycleType(g.dateType || 'DAILY')
  894. const weekDays = g.weekDays || ''
  895. const monthDays = g.monthDays || ''
  896. const customDates = g.customDates || ''
  897. const enable = g.enable ?? 1
  898. const key = `${cycleType}_${weekDays}_${monthDays}_${customDates}`
  899. if (!map.has(key)) {
  900. groupIdCounter++
  901. map.set(key, {
  902. id: groupIdCounter,
  903. cycleType,
  904. enable,
  905. weeklyDays: weekDays ? weekDays.split(',').map(Number) : [],
  906. monthlyDays: monthDays ? monthDays.split(',').map(Number) : [],
  907. customDates: customDates ? customDates.split(',') : [],
  908. items: []
  909. })
  910. }
  911. const group = map.get(key)!
  912. itemIdCounter++
  913. group.items.push({
  914. id: itemIdCounter,
  915. // 保留后端真实 id,用于更新时回传和删除时入 deleteIds
  916. serverId: g.id,
  917. startTime: g.beginTime || '',
  918. endTime: g.endTime || '',
  919. limitCount: g.limitCount || 100
  920. })
  921. })
  922. return Array.from(map.values())
  923. }
  924. const getSetting = async () => {
  925. try {
  926. const res = await appointmentConfigGetByTenant({ tenantId: organizationId })
  927. if (res && res.config) {
  928. const config = res.config
  929. configId.value = config.id
  930. let startTime = defaultFormData().startTime
  931. let endTime = defaultFormData().endTime
  932. if (config.openTimeSlot && typeof config.openTimeSlot === 'string') {
  933. const [s, e] = config.openTimeSlot.split('-')
  934. if (s && e) {
  935. startTime = s
  936. endTime = e
  937. }
  938. }
  939. Object.assign(formData, {
  940. ruleType: 1,
  941. startDay: config.beginBookDays ?? 0,
  942. advanceDays: config.advanceDays ?? 0,
  943. appointmentEnabled: config.enabled === 1,
  944. startTime,
  945. endTime
  946. })
  947. totalLimits.value = []
  948. personLimits.value = []
  949. ;[1, 2, 3].forEach((type) => {
  950. const field = limitTypeToFieldMap[type]
  951. const totalVal = config[field.total]
  952. if (totalVal !== undefined && totalVal !== null && totalVal > 0) {
  953. totalLimits.value.push({ type, count: totalVal })
  954. }
  955. const personVal = config[field.person]
  956. if (personVal !== undefined && personVal !== null && personVal > 0) {
  957. personLimits.value.push({ type, count: personVal })
  958. }
  959. })
  960. groupIdCounter = 0
  961. itemIdCounter = 0
  962. // 重新拉取配置后,本地 deleteIds 已无意义,清空避免脏数据
  963. deleteIds.value = []
  964. appointmentGroups.value = parseGroups(res.group || [])
  965. if (appointmentGroups.value.length > 0) {
  966. groupIdCounter = Math.max(...appointmentGroups.value.map((g) => g.id))
  967. let maxItemId = 0
  968. appointmentGroups.value.forEach((g) => {
  969. if (g.items && g.items.length > 0) {
  970. maxItemId = Math.max(maxItemId, ...g.items.map((it) => it.id))
  971. }
  972. })
  973. itemIdCounter = maxItemId
  974. }
  975. } else {
  976. configId.value = undefined
  977. Object.assign(formData, defaultFormData())
  978. totalLimits.value = []
  979. personLimits.value = []
  980. appointmentGroups.value = []
  981. }
  982. } catch (e) {
  983. console.log('获取设置失败', e)
  984. }
  985. }
  986. const validateGroups = (): string | null => {
  987. if (appointmentGroups.value.length === 0) {
  988. return '请至少添加一个预约组'
  989. }
  990. const dailyGroups = appointmentGroups.value.filter((g) => g.cycleType === 'daily')
  991. if (dailyGroups.length > 0 && appointmentGroups.value.length > 1) {
  992. return '选择每天时,只能有一个预约组'
  993. }
  994. const weeklyGroups = appointmentGroups.value.filter((g) => g.cycleType === 'weekly')
  995. const monthlyGroups = appointmentGroups.value.filter((g) => g.cycleType === 'monthly')
  996. if (weeklyGroups.length > 0 && monthlyGroups.length > 0) {
  997. return '每周和每月预约组不能同时存在'
  998. }
  999. if (weeklyGroups.length > 1) {
  1000. const usedDays = new Set<number>()
  1001. for (const group of weeklyGroups) {
  1002. for (const day of group.weeklyDays) {
  1003. if (usedDays.has(day)) {
  1004. return '不同每周预约组存在重复的星期'
  1005. }
  1006. usedDays.add(day)
  1007. }
  1008. }
  1009. }
  1010. if (monthlyGroups.length > 1) {
  1011. const usedDays = new Set<number>()
  1012. for (const group of monthlyGroups) {
  1013. for (const day of group.monthlyDays) {
  1014. if (usedDays.has(day)) {
  1015. return '不同每月预约组存在重复的日期'
  1016. }
  1017. usedDays.add(day)
  1018. }
  1019. }
  1020. }
  1021. const customGroups = appointmentGroups.value.filter((g) => g.cycleType === 'custom')
  1022. if (customGroups.length > 1) {
  1023. const usedDates = new Set<string>()
  1024. for (const group of customGroups) {
  1025. for (const date of group.customDates) {
  1026. if (usedDates.has(date)) {
  1027. return '不同自定义预约组存在重复的日期'
  1028. }
  1029. usedDates.add(date)
  1030. }
  1031. }
  1032. }
  1033. if (monthlyGroups.length > 0 && customGroups.length > 0) {
  1034. const usedDays = new Set<number>()
  1035. for (const group of monthlyGroups) {
  1036. for (const day of group.monthlyDays) {
  1037. usedDays.add(day)
  1038. }
  1039. }
  1040. for (const group of customGroups) {
  1041. for (const date of group.customDates) {
  1042. const day = getDateDayOfMonth(date)
  1043. if (day > 0 && usedDays.has(day)) {
  1044. return `自定义日期 ${date} 与每月预约的 ${day}号冲突`
  1045. }
  1046. }
  1047. }
  1048. }
  1049. if (weeklyGroups.length > 0 && customGroups.length > 0) {
  1050. const usedDays = new Set<number>()
  1051. for (const group of weeklyGroups) {
  1052. for (const day of group.weeklyDays) {
  1053. usedDays.add(day)
  1054. }
  1055. }
  1056. for (const group of customGroups) {
  1057. for (const date of group.customDates) {
  1058. const day = getDateWeekDay(date)
  1059. if (day > 0 && usedDays.has(day)) {
  1060. return `自定义日期 ${date} 与每周预约冲突`
  1061. }
  1062. }
  1063. }
  1064. }
  1065. for (const group of appointmentGroups.value) {
  1066. if (group.cycleType === 'weekly' && group.weeklyDays.length === 0) {
  1067. return '存在未设置每周可预约日期的预约组'
  1068. }
  1069. if (group.cycleType === 'monthly' && group.monthlyDays.length === 0) {
  1070. return '存在未设置每月可预约日期的预约组'
  1071. }
  1072. if (group.cycleType === 'custom' && group.customDates.length === 0) {
  1073. return '存在未设置自定义日期的预约组'
  1074. }
  1075. }
  1076. return null
  1077. }
  1078. const handleSubmit = async () => {
  1079. if (!formRef.value) return
  1080. if (!organizationId) {
  1081. await ElMessageBox.alert('请选择机构!', '警告', { type: 'warning' })
  1082. return
  1083. }
  1084. if (!formData.advanceDays || formData.advanceDays < 1) {
  1085. await ElMessageBox.alert('提前预约天数最少为1', '警告', { type: 'warning' })
  1086. return
  1087. }
  1088. if (
  1089. formData.startTime &&
  1090. formData.endTime &&
  1091. timeToMinutes(formData.startTime) >= timeToMinutes(formData.endTime)
  1092. ) {
  1093. await ElMessageBox.alert('放号开始时间不能晚于或等于结束时间', '警告', { type: 'warning' })
  1094. return
  1095. }
  1096. const groupError = validateGroups()
  1097. if (groupError) {
  1098. await ElMessageBox.alert(groupError, '警告', { type: 'warning' })
  1099. return
  1100. }
  1101. const personLimitError = validatePersonLimits()
  1102. if (personLimitError) {
  1103. await ElMessageBox.alert(personLimitError, '警告', { type: 'warning' })
  1104. return
  1105. }
  1106. for (const group of appointmentGroups.value) {
  1107. for (let i = 0; i < group.items.length; i++) {
  1108. const item = group.items[i]
  1109. if (!item.limitCount || item.limitCount <= 0) {
  1110. await ElMessageBox.alert('限制人数必须大于0', '警告', { type: 'warning' })
  1111. return
  1112. }
  1113. if (
  1114. item.startTime &&
  1115. item.endTime &&
  1116. timeToMinutes(item.startTime) >= timeToMinutes(item.endTime)
  1117. ) {
  1118. await ElMessageBox.alert('存在开始时间晚于或等于结束时间的预约项,请检查', '警告', {
  1119. type: 'warning'
  1120. })
  1121. return
  1122. }
  1123. for (let j = i + 1; j < group.items.length; j++) {
  1124. if (isOverlap(item, group.items[j])) {
  1125. await ElMessageBox.alert('同一预约组内存在时间段重叠的预约项,请检查', '警告', {
  1126. type: 'warning'
  1127. })
  1128. return
  1129. }
  1130. }
  1131. }
  1132. }
  1133. try {
  1134. await formRef.value.validate()
  1135. submitting.value = true
  1136. const config: AppointmentConfigDO = {
  1137. id: configId.value,
  1138. tenantId: organizationId,
  1139. advanceDays: formData.advanceDays,
  1140. beginBookDays: formData.startDay,
  1141. enabled: formData.appointmentEnabled ? 1 : 0,
  1142. openTimeSlot:
  1143. formData.startTime && formData.endTime
  1144. ? `${formData.startTime}-${formData.endTime}`
  1145. : undefined
  1146. }
  1147. totalLimits.value.forEach((limit) => {
  1148. const field = limitTypeToFieldMap[limit.type]
  1149. if (field) {
  1150. config[field.total] = limit.count
  1151. }
  1152. })
  1153. // 单人名额:未设置或删除时传 -1(后端表示无限制)
  1154. ;[1, 2, 3].forEach((type) => {
  1155. const field = limitTypeToFieldMap[type]
  1156. if (field) {
  1157. const limit = personLimits.value.find((item) => item.type === type)
  1158. config[field.person] = limit ? limit.count : -1
  1159. }
  1160. })
  1161. const group: AppointmentGroupDO[] = []
  1162. appointmentGroups.value.forEach((g) => {
  1163. const dateType = cycleTypeToDateType(g.cycleType)
  1164. const weekDays = g.weeklyDays.join(',') || undefined
  1165. const monthDays = g.monthlyDays.join(',') || undefined
  1166. const customDates = g.customDates.join(',') || undefined
  1167. const enable = g.enable || 0
  1168. g.items.forEach((item) => {
  1169. group.push({
  1170. // 旧数据带回后端 id 用于更新;新增数据不带 id,后端按新增处理
  1171. ...(item.serverId ? { id: item.serverId } : {}),
  1172. dateType,
  1173. weekDays,
  1174. monthDays,
  1175. customDates,
  1176. beginTime: item.startTime,
  1177. enable: enable,
  1178. endTime: item.endTime,
  1179. limitCount: item.limitCount,
  1180. tenantId: organizationId
  1181. })
  1182. })
  1183. })
  1184. const params = { config, group, deleteIds: deleteIds.value }
  1185. const res = configId.value
  1186. ? await appointmentConfigUpdate(params)
  1187. : await appointmentConfigADD(params)
  1188. if (res) {
  1189. message.success('设置成功!')
  1190. await getSetting()
  1191. }
  1192. } catch (error: any) {
  1193. if (error?.message) {
  1194. await ElMessageBox.alert(error.message, '警告', { type: 'warning' })
  1195. }
  1196. } finally {
  1197. submitting.value = false
  1198. }
  1199. }
  1200. onMounted(() => {
  1201. if (userStore.orgTenantId.length > 0) {
  1202. tenantIds.value = [userStore.orgTenantId[0]] as number[]
  1203. organizationId = tenantIds.value[0]
  1204. getSetting()
  1205. }
  1206. })
  1207. </script>
  1208. <style scoped lang="scss">
  1209. .setting-form {
  1210. padding-top: 16px;
  1211. .main-row {
  1212. display: flex;
  1213. align-items: stretch;
  1214. }
  1215. .col-left,
  1216. .col-right {
  1217. display: flex;
  1218. flex-direction: column;
  1219. gap: 20px;
  1220. }
  1221. .card {
  1222. background-color: #fff;
  1223. border-radius: 8px;
  1224. box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
  1225. display: flex;
  1226. flex-direction: column;
  1227. overflow: hidden;
  1228. &-top-left,
  1229. &-mid-right {
  1230. flex: 0 0 auto;
  1231. }
  1232. &-bottom-left,
  1233. &-bottom-right {
  1234. flex: 1 1 auto;
  1235. min-height: 0;
  1236. }
  1237. }
  1238. .card-header {
  1239. display: flex;
  1240. align-items: center;
  1241. padding: 16px 20px;
  1242. border-bottom: 1px solid #f0f0f0;
  1243. background: linear-gradient(90deg, #fafbfc 0%, #ffffff 100%);
  1244. .card-icon {
  1245. width: 28px;
  1246. height: 28px;
  1247. display: flex;
  1248. align-items: center;
  1249. justify-content: center;
  1250. border-radius: 6px;
  1251. margin-right: 10px;
  1252. color: #fff;
  1253. font-size: 16px;
  1254. &.icon-blue {
  1255. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  1256. }
  1257. &.icon-green {
  1258. background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
  1259. }
  1260. &.icon-orange {
  1261. background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
  1262. }
  1263. &.icon-purple {
  1264. background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
  1265. }
  1266. }
  1267. .card-title {
  1268. font-size: 16px;
  1269. font-weight: 600;
  1270. color: #303133;
  1271. flex: 1;
  1272. }
  1273. .master-switch {
  1274. display: flex;
  1275. align-items: center;
  1276. gap: 8px;
  1277. .switch-label {
  1278. font-size: 14px;
  1279. color: #606266;
  1280. }
  1281. }
  1282. }
  1283. .card-body {
  1284. padding: 20px;
  1285. flex: 1;
  1286. }
  1287. .empty-groups {
  1288. padding: 30px 0;
  1289. }
  1290. .card-body-groups {
  1291. padding: 16px 20px;
  1292. max-height: 560px;
  1293. overflow-y: auto;
  1294. }
  1295. .group-list {
  1296. display: flex;
  1297. flex-direction: column;
  1298. gap: 16px;
  1299. }
  1300. .group-card {
  1301. background-color: #fff;
  1302. border: 1px solid #e4e7ed;
  1303. border-radius: 8px;
  1304. padding: 16px;
  1305. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
  1306. }
  1307. .group-header {
  1308. display: flex;
  1309. align-items: center;
  1310. gap: 10px;
  1311. margin-bottom: 12px;
  1312. flex-wrap: wrap;
  1313. .group-label {
  1314. font-size: 14px;
  1315. font-weight: 600;
  1316. color: #303133;
  1317. }
  1318. .cycle-buttons {
  1319. display: flex;
  1320. gap: 6px;
  1321. flex: 1;
  1322. flex-wrap: wrap;
  1323. }
  1324. }
  1325. .group-dates {
  1326. display: flex;
  1327. align-items: center;
  1328. flex-wrap: wrap;
  1329. gap: 6px;
  1330. padding: 12px 0;
  1331. border-top: 1px dashed #ebeef5;
  1332. border-bottom: 1px dashed #ebeef5;
  1333. .dates-label {
  1334. font-size: 13px;
  1335. color: #606266;
  1336. margin-right: 4px;
  1337. }
  1338. .date-tag {
  1339. margin: 2px 0;
  1340. }
  1341. }
  1342. .group-items-section {
  1343. margin-top: 14px;
  1344. }
  1345. .items-header {
  1346. display: flex;
  1347. align-items: center;
  1348. margin-bottom: 10px;
  1349. .items-title {
  1350. font-size: 14px;
  1351. font-weight: 600;
  1352. color: #303133;
  1353. }
  1354. .items-sub {
  1355. font-size: 12px;
  1356. color: #909399;
  1357. margin-left: 8px;
  1358. }
  1359. }
  1360. .empty-items {
  1361. text-align: center;
  1362. padding: 20px 0;
  1363. font-size: 13px;
  1364. color: #c0c4cc;
  1365. background-color: #fafafa;
  1366. border-radius: 6px;
  1367. border: 1px dashed #dcdfe6;
  1368. }
  1369. .items-list {
  1370. display: flex;
  1371. flex-direction: column;
  1372. gap: 10px;
  1373. margin-bottom: 12px;
  1374. }
  1375. .item-row {
  1376. display: flex;
  1377. align-items: center;
  1378. gap: 10px;
  1379. background-color: #f8f9fa;
  1380. border: 1px solid #e9ecef;
  1381. border-radius: 6px;
  1382. padding: 10px 12px;
  1383. }
  1384. .item-delete-btn {
  1385. width: 24px;
  1386. height: 24px;
  1387. display: flex;
  1388. align-items: center;
  1389. justify-content: center;
  1390. border-radius: 4px;
  1391. color: #f56c6c;
  1392. cursor: pointer;
  1393. transition: all 0.2s;
  1394. flex-shrink: 0;
  1395. margin-top: 6px;
  1396. &:hover {
  1397. background-color: #fef0f0;
  1398. }
  1399. &.disabled {
  1400. pointer-events: none;
  1401. opacity: 0.4;
  1402. cursor: not-allowed;
  1403. }
  1404. }
  1405. .item-fields {
  1406. flex: 1;
  1407. display: flex;
  1408. flex-wrap: wrap;
  1409. gap: 14px;
  1410. }
  1411. .field-block {
  1412. display: flex;
  1413. align-items: center;
  1414. gap: 6px;
  1415. .field-label {
  1416. font-size: 13px;
  1417. color: #606266;
  1418. white-space: nowrap;
  1419. }
  1420. .time-sep {
  1421. font-size: 13px;
  1422. color: #909399;
  1423. }
  1424. }
  1425. .add-item-row {
  1426. display: flex;
  1427. justify-content: center;
  1428. }
  1429. }
  1430. .fixed-save-bar {
  1431. width: 100%;
  1432. display: flex;
  1433. justify-content: flex-end;
  1434. }
  1435. :deep(.el-form-item) {
  1436. margin-bottom: 18px;
  1437. &:last-child {
  1438. margin-bottom: 0;
  1439. }
  1440. }
  1441. :deep(.el-form-item__content) {
  1442. display: flex;
  1443. align-items: center;
  1444. }
  1445. :deep(.date-selector-dialog) {
  1446. .date-selector-body {
  1447. padding: 6px 0;
  1448. .selector-title {
  1449. font-size: 14px;
  1450. color: #606266;
  1451. margin-bottom: 16px;
  1452. }
  1453. .week-days-grid {
  1454. display: grid;
  1455. grid-template-columns: repeat(7, 1fr);
  1456. gap: 10px;
  1457. }
  1458. .month-days-grid {
  1459. display: grid;
  1460. grid-template-columns: repeat(7, 1fr);
  1461. gap: 8px;
  1462. }
  1463. .day-cell {
  1464. height: 42px;
  1465. display: flex;
  1466. align-items: center;
  1467. justify-content: center;
  1468. border: 2px solid #e4e7ed;
  1469. border-radius: 8px;
  1470. cursor: pointer;
  1471. font-size: 14px;
  1472. color: #606266;
  1473. background-color: #fff;
  1474. transition: all 0.2s ease;
  1475. user-select: none;
  1476. &:hover {
  1477. border-color: #67c23a;
  1478. color: #67c23a;
  1479. background-color: #f0f9eb;
  1480. }
  1481. &.active {
  1482. border-color: #67c23a;
  1483. background-color: #67c23a;
  1484. color: #fff;
  1485. font-weight: 500;
  1486. box-shadow: 0 2px 6px rgba(103, 194, 58, 0.3);
  1487. }
  1488. }
  1489. }
  1490. }
  1491. .limit-list-inline {
  1492. display: flex;
  1493. flex-direction: column;
  1494. gap: 10px;
  1495. }
  1496. .limit-item-inline {
  1497. display: flex;
  1498. align-items: center;
  1499. padding: 10px 14px;
  1500. background-color: #f8f9fa;
  1501. border-radius: 6px;
  1502. border: 1px solid #e9ecef;
  1503. .limit-type-badge {
  1504. font-size: 12px;
  1505. font-weight: 600;
  1506. color: #fff;
  1507. padding: 3px 10px;
  1508. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  1509. border-radius: 4px;
  1510. margin-right: 16px;
  1511. }
  1512. .limit-count-num {
  1513. flex: 1;
  1514. font-size: 16px;
  1515. font-weight: 600;
  1516. color: #f56c6c;
  1517. em {
  1518. font-style: normal;
  1519. font-size: 13px;
  1520. font-weight: 400;
  1521. color: #909399;
  1522. margin-left: 2px;
  1523. }
  1524. }
  1525. }
  1526. </style>