index.blade.php 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753
  1. <x-admin::layouts>
  2. <x-slot:title>
  3. @lang('gift::app.admin.title')
  4. </x-slot>
  5. <v-gift-cards>
  6. <!-- Statistics Shimmer -->
  7. <div class="box-shadow rounded bg-white dark:bg-gray-900">
  8. <div class="flex flex-wrap items-center justify-between gap-4 px-4 py-3">
  9. <p class="text-base font-semibold text-gray-800 dark:text-white">
  10. @lang('gift::app.admin.stats.title')
  11. </p>
  12. <div class="shimmer h-[39px] w-[140px] rounded-md"></div>
  13. </div>
  14. </div>
  15. <!-- DataGrid Shimmer -->
  16. <x-admin::shimmer.datagrid />
  17. </v-gift-cards>
  18. @pushOnce('scripts')
  19. <script
  20. type="text/x-template"
  21. id="v-gift-cards-template"
  22. >
  23. <div>
  24. <div class="flex items-center justify-between">
  25. <p class="text-xl font-bold text-gray-800 dark:text-white">
  26. @lang('gift::app.admin.title')
  27. </p>
  28. <div class="flex items-center gap-x-2.5">
  29. @if (bouncer()->hasPermission('gift.create'))
  30. <button
  31. type="button"
  32. class="primary-button"
  33. @click="selectedGift=0; resetForm(); $refs.giftUpdateOrCreateModal.toggle()"
  34. >
  35. @lang('gift::app.admin.index.create-btn')
  36. </button>
  37. @endif
  38. </div>
  39. </div>
  40. <!-- Gift Card Statistics(默认收起,点按钮才异步加载) -->
  41. <v-gift-card-statistics></v-gift-card-statistics>
  42. <x-admin::datagrid
  43. :src="route('admin.gift.index')"
  44. ref="datagrid"
  45. >
  46. <template #body="{
  47. isLoading,
  48. available,
  49. applied,
  50. selectAll,
  51. sort,
  52. performAction
  53. }">
  54. <template v-if="isLoading">
  55. <x-admin::shimmer.datagrid.table.body />
  56. </template>
  57. <template v-else>
  58. <div
  59. v-for="record in available.records"
  60. class="row grid items-center gap-2.5 border-b px-4 py-4 text-gray-600 transition-all hover:bg-gray-50 dark:border-gray-800 dark:text-gray-300 dark:hover:bg-gray-950"
  61. :style="`grid-template-columns: repeat(${gridsCount}, minmax(0, 1fr))`"
  62. >
  63. <!-- Mass Actions Checkbox -->
  64. <p v-if="available.massActions.length">
  65. <label :for="`mass_action_select_record_${record[available.meta.primary_column]}`">
  66. <input
  67. type="checkbox"
  68. :name="`mass_action_select_record_${record[available.meta.primary_column]}`"
  69. :value="record[available.meta.primary_column]"
  70. :id="`mass_action_select_record_${record[available.meta.primary_column]}`"
  71. class="peer hidden"
  72. v-model="applied.massActions.indices"
  73. >
  74. <span class="icon-uncheckbox peer-checked:icon-checked cursor-pointer rounded-md text-2xl peer-checked:text-blue-600"></span>
  75. </label>
  76. </p>
  77. <!-- ID -->
  78. <p>@{{ record.id }}</p>
  79. <!-- Card Amount -->
  80. <p>@{{ record.giftcard_amount }}</p>
  81. <!-- Used Amount -->
  82. <p>@{{ record.used_giftcard_amount }}</p>
  83. <!-- Remaining Amount -->
  84. <p>@{{ record.remaining_giftcard_amount }}</p>
  85. <!-- Customer Email -->
  86. <p v-html="record.customer_email"></p>
  87. <!-- Expiration Date -->
  88. <p>@{{ record.expirationdate }}</p>
  89. <!-- Status -->
  90. <p v-html="record.giftcard_status"></p>
  91. <!-- Notes -->
  92. <p>@{{ record.notes }}</p>
  93. <!-- Created At -->
  94. <p>@{{ record.created_at }}</p>
  95. <!-- Actions -->
  96. <div class="flex justify-end">
  97. @if (bouncer()->hasPermission('gift.edit'))
  98. <a @click="selectedGift=1; editModal(record.actions.find(action => action.index === 'edit')?.url)">
  99. <span
  100. :class="record.actions.find(action => action.index === 'edit')?.icon"
  101. class="cursor-pointer rounded-md p-1.5 text-2xl transition-all hover:bg-gray-200 dark:hover:bg-gray-800 max-sm:place-self-center"
  102. >
  103. </span>
  104. </a>
  105. @endif
  106. @if (bouncer()->hasPermission('gift.delete'))
  107. <a @click="performAction(record.actions.find(action => action.index === 'delete'))">
  108. <span
  109. :class="record.actions.find(action => action.index === 'delete')?.icon"
  110. class="cursor-pointer rounded-md p-1.5 text-2xl transition-all hover:bg-gray-200 dark:hover:bg-gray-800 max-sm:place-self-center"
  111. >
  112. </span>
  113. </a>
  114. @endif
  115. </div>
  116. </div>
  117. </template>
  118. </template>
  119. </x-admin::datagrid>
  120. <!-- Create / Edit Modal -->
  121. <x-admin::form
  122. v-slot="{ meta, errors, handleSubmit }"
  123. as="div"
  124. ref="modalForm"
  125. >
  126. <form
  127. @submit="handleSubmit($event, updateOrCreate)"
  128. ref="giftCreateForm"
  129. >
  130. <x-admin::modal ref="giftUpdateOrCreateModal">
  131. <x-slot:header>
  132. <p class="text-lg font-bold text-gray-800 dark:text-white">
  133. <span v-if="selectedGift">
  134. @lang('gift::app.admin.edit.title')
  135. </span>
  136. <span v-else>
  137. @lang('gift::app.admin.create.title')
  138. </span>
  139. </p>
  140. </x-slot>
  141. <x-slot:content>
  142. <x-admin::form.control-group.control
  143. type="hidden"
  144. name="id"
  145. v-model="selectedGiftData.id"
  146. />
  147. <!-- 编辑模式:显示只读信息 + 可修改的过期时间和状态 -->
  148. <template v-if="selectedGift">
  149. <!-- Gift Card Number (只读) -->
  150. <div class="mb-4 rounded-md bg-gray-50 p-4 dark:bg-gray-800">
  151. <div class="grid grid-cols-2 gap-4 text-sm">
  152. <div>
  153. <span class="text-gray-500">@lang('gift::app.admin.datagrid.giftcard-number')</span>
  154. <p class="font-medium text-gray-800 dark:text-white">@{{ selectedGiftData.giftcard_number }}</p>
  155. </div>
  156. <div>
  157. <span class="text-gray-500">@lang('gift::app.admin.create.giftcard-amount')</span>
  158. <p class="font-medium text-gray-800 dark:text-white">@{{ selectedGiftData.giftcard_amount }}</p>
  159. </div>
  160. <div>
  161. <span class="text-gray-500">@lang('gift::app.admin.datagrid.used-amount')</span>
  162. <p class="font-medium text-gray-800 dark:text-white">@{{ selectedGiftData.used_giftcard_amount }}</p>
  163. </div>
  164. <div>
  165. <span class="text-gray-500">@lang('gift::app.admin.datagrid.remaining-amount')</span>
  166. <p class="font-medium text-gray-800 dark:text-white">@{{ selectedGiftData.remaining_giftcard_amount }}</p>
  167. </div>
  168. <div>
  169. <span class="text-gray-500">用户邮箱</span>
  170. <p class="font-medium text-gray-800 dark:text-white">@{{ selectedGiftData.customer_email || '未绑定' }}</p>
  171. </div>
  172. </div>
  173. </div>
  174. </template>
  175. <!-- 创建模式:显示金额和邮箱 -->
  176. <template v-else>
  177. <!-- Card Amount -->
  178. <x-admin::form.control-group>
  179. <x-admin::form.control-group.label class="required">
  180. @lang('gift::app.admin.create.giftcard-amount')
  181. </x-admin::form.control-group.label>
  182. <x-admin::form.control-group.control
  183. type="text"
  184. name="giftcard_amount"
  185. rules="required|numeric|min:0"
  186. v-model="selectedGiftData.giftcard_amount"
  187. :label="trans('gift::app.admin.create.giftcard-amount')"
  188. placeholder="0.00"
  189. />
  190. <x-admin::form.control-group.error control-name="giftcard_amount" />
  191. </x-admin::form.control-group>
  192. <!-- Customer Email -->
  193. <x-admin::form.control-group>
  194. <x-admin::form.control-group.label>
  195. @lang('gift::app.admin.create.customer-email')
  196. </x-admin::form.control-group.label>
  197. <x-admin::form.control-group.control
  198. type="text"
  199. name="customer_email"
  200. v-model="selectedGiftData.customer_email"
  201. :label="trans('gift::app.admin.create.customer-email')"
  202. placeholder="@lang('gift::app.admin.create.customer-email-placeholder')"
  203. />
  204. <input type="hidden" name="customer_id" v-model="selectedGiftData.customer_id" />
  205. <div v-if="emailValidationMessage" v-html="emailValidationMessage" class="mt-1 text-sm"></div>
  206. <x-admin::form.control-group.error control-name="customer_email" />
  207. <x-admin::form.control-group.error control-name="customer_id" />
  208. </x-admin::form.control-group>
  209. </template>
  210. <!-- Expiration Date (创建和编辑都显示) -->
  211. <x-admin::form.control-group>
  212. <x-admin::form.control-group.label class="required">
  213. @lang('gift::app.admin.create.expirationdate')
  214. </x-admin::form.control-group.label>
  215. <x-admin::form.control-group.control
  216. type="datetime"
  217. name="expirationdate"
  218. rules="required"
  219. v-model="selectedGiftData.expirationdate"
  220. :label="trans('gift::app.admin.create.expirationdate')"
  221. />
  222. <x-admin::form.control-group.error control-name="expirationdate" />
  223. </x-admin::form.control-group>
  224. <!-- Status (创建和编辑都显示) -->
  225. <x-admin::form.control-group>
  226. <x-admin::form.control-group.label>
  227. @lang('gift::app.admin.create.giftcard-status')
  228. </x-admin::form.control-group.label>
  229. <x-admin::form.control-group.control
  230. type="select"
  231. name="giftcard_status"
  232. v-model="selectedGiftData.giftcard_status"
  233. >
  234. <option value="1">@lang('gift::app.admin.create.unused')</option>
  235. <option value="2">@lang('gift::app.admin.create.used')</option>
  236. </x-admin::form.control-group.control>
  237. <x-admin::form.control-group.error control-name="giftcard_status" />
  238. </x-admin::form.control-group>
  239. </x-slot>
  240. <x-slot:footer>
  241. <x-admin::button
  242. button-type="button"
  243. class="secondary-button mr-2"
  244. :title="trans('gift::app.admin.create.back-btn')"
  245. @click="$refs.giftUpdateOrCreateModal.close()"
  246. />
  247. <x-admin::button
  248. button-type="submit"
  249. class="primary-button"
  250. :title="trans('gift::app.admin.create.save-btn')"
  251. ::loading="isLoading"
  252. ::disabled="isLoading"
  253. />
  254. </x-slot>
  255. </x-admin::modal>
  256. </form>
  257. </x-admin::form>
  258. </div>
  259. </script>
  260. <script type="module">
  261. app.component('v-gift-cards', {
  262. template: '#v-gift-cards-template',
  263. data() {
  264. return {
  265. selectedGift: 0,
  266. // giftcard_status 默认未使用(1)
  267. selectedGiftData: {
  268. giftcard_status: '1',
  269. },
  270. isLoading: false,
  271. emailValidationMessage: '',
  272. emailDebounceTimer: null,
  273. }
  274. },
  275. watch: {
  276. 'selectedGiftData.customer_email': {
  277. handler(newEmail) {
  278. // 清除上一次的定时器
  279. if (this.emailDebounceTimer) {
  280. clearTimeout(this.emailDebounceTimer);
  281. }
  282. // 编辑模式不触发验证
  283. if (this.selectedGift) {
  284. return;
  285. }
  286. // 500ms 防抖
  287. this.emailDebounceTimer = setTimeout(() => {
  288. this.validateEmail(newEmail);
  289. }, 500);
  290. },
  291. immediate: false
  292. }
  293. },
  294. computed: {
  295. gridsCount() {
  296. let count = this.$refs.datagrid.available.columns.length;
  297. if (this.$refs.datagrid.available.actions.length) {
  298. ++count;
  299. }
  300. if (this.$refs.datagrid.available.massActions.length) {
  301. ++count;
  302. }
  303. return count;
  304. },
  305. },
  306. methods: {
  307. updateOrCreate(params, { resetForm, setErrors }) {
  308. // 创建模式:检查邮箱是否已填写且验证通过
  309. if (!params.id && this.selectedGiftData.customer_email && !this.selectedGiftData.customer_id) {
  310. this.$emitter.emit('add-flash', { type: 'error', message: '请等待邮箱验证通过后再提交' });
  311. return;
  312. }
  313. this.isLoading = true;
  314. let formData = new FormData(this.$refs.giftCreateForm);
  315. if (params.id) {
  316. formData.append('_method', 'put');
  317. }
  318. this.$axios.post(
  319. params.id
  320. ? "{{ route('admin.gift.update') }}"
  321. : "{{ route('admin.gift.store') }}",
  322. formData
  323. )
  324. .then((response) => {
  325. this.isLoading = false;
  326. this.$emitter.emit('add-flash', { type: 'success', message: response.data.message });
  327. this.$refs.giftUpdateOrCreateModal.close();
  328. this.$refs.datagrid.get();
  329. resetForm();
  330. this.emailValidationMessage = '';
  331. })
  332. .catch(error => {
  333. this.isLoading = false;
  334. if (error.response.status == 422) {
  335. setErrors(error.response.data.errors);
  336. }
  337. });
  338. },
  339. editModal(url) {
  340. this.$axios.get(url)
  341. .then((response) => {
  342. this.selectedGiftData = response.data.data;
  343. // 类型转换:确保 select 能正确匹配
  344. if (this.selectedGiftData.giftcard_status !== undefined) {
  345. this.selectedGiftData.giftcard_status = String(this.selectedGiftData.giftcard_status);
  346. }
  347. // 格式化过期时间为 flat-picker 需要的格式 Y-m-d H:i:s
  348. if (this.selectedGiftData.expirationdate) {
  349. this.selectedGiftData.expirationdate = this.selectedGiftData.expirationdate.replace('T', ' ').replace(/\.\d+Z$/, '');
  350. }
  351. this.emailValidationMessage = '';
  352. this.$refs.giftUpdateOrCreateModal.toggle();
  353. })
  354. .catch(error => this.$emitter.emit('add-flash', {
  355. type: 'error', message: error.response.data.message
  356. }));
  357. },
  358. validateEmail(email) {
  359. this.emailValidationMessage = '';
  360. if (!email) {
  361. this.selectedGiftData.customer_id = null;
  362. return;
  363. }
  364. this.emailValidationMessage = '<span class="text-gray-500">@lang("gift::app.admin.create.validating")</span>';
  365. this.$axios.post(
  366. "{{ route('admin.gift.validate_email') }}",
  367. { email: email }
  368. )
  369. .then((response) => {
  370. if (response.data.valid) {
  371. this.emailValidationMessage = '<span class="text-green-600">' + response.data.message + ' (' + response.data.customer_name + ')</span>';
  372. this.selectedGiftData.customer_id = response.data.customer_id;
  373. } else {
  374. this.emailValidationMessage = '<span class="text-red-600">' + response.data.message + '</span>';
  375. this.selectedGiftData.customer_id = null;
  376. }
  377. })
  378. .catch(error => {
  379. this.emailValidationMessage = '<span class="text-red-600">@lang("gift::app.admin.create.validation-failed")</span>';
  380. this.selectedGiftData.customer_id = null;
  381. });
  382. },
  383. resetForm() {
  384. // 新建时状态默认未使用(1)
  385. this.selectedGiftData = {
  386. giftcard_status: '1',
  387. };
  388. this.emailValidationMessage = '';
  389. }
  390. }
  391. })
  392. </script>
  393. @endPushOnce
  394. @pushOnce('scripts')
  395. <script
  396. type="module"
  397. src="{{ bagisto_asset('js/chart.js') }}"
  398. >
  399. </script>
  400. <script
  401. type="text/x-template"
  402. id="v-gift-card-statistics-template"
  403. >
  404. <div class="mt-3.5 box-shadow rounded bg-white dark:bg-gray-900">
  405. <!-- 标题 + 展开按钮 -->
  406. <div class="flex flex-wrap items-center justify-between gap-4 px-4 py-3">
  407. <div>
  408. <p class="text-base font-semibold text-gray-800 dark:text-white">
  409. @lang('gift::app.admin.stats.title')
  410. </p>
  411. <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
  412. @lang('gift::app.admin.stats.hint')
  413. </p>
  414. </div>
  415. <button
  416. type="button"
  417. class="secondary-button"
  418. @click="toggle()"
  419. >
  420. <span class="flex items-center gap-x-2.5">
  421. <span v-if="isOpen">@lang('gift::app.admin.stats.hide')</span>
  422. <span v-else>@lang('gift::app.admin.stats.show')</span>
  423. <span
  424. class="text-2xl text-gray-400"
  425. :class="isOpen ? 'icon-sort-up' : 'icon-sort-down'"
  426. ></span>
  427. </span>
  428. </button>
  429. </div>
  430. <!-- 统计内容 -->
  431. <div
  432. v-if="isOpen"
  433. class="border-t px-4 py-4 dark:border-gray-800"
  434. >
  435. <!-- 时间区间 -->
  436. <div class="flex flex-wrap items-end gap-x-2.5 gap-y-2">
  437. <button
  438. type="button"
  439. class="secondary-button"
  440. @click="applyQuickRange(7)"
  441. >
  442. @lang('gift::app.admin.stats.range-7')
  443. </button>
  444. <button
  445. type="button"
  446. class="secondary-button"
  447. @click="applyQuickRange(30)"
  448. >
  449. @lang('gift::app.admin.stats.range-30')
  450. </button>
  451. <button
  452. type="button"
  453. class="secondary-button"
  454. @click="applyQuickRange(90)"
  455. >
  456. @lang('gift::app.admin.stats.range-90')
  457. </button>
  458. <button
  459. type="button"
  460. class="secondary-button"
  461. @click="applyQuickRange(365)"
  462. >
  463. @lang('gift::app.admin.stats.range-365')
  464. </button>
  465. <x-admin::flat-picker.date class="!w-[140px]" ::allow-input="false">
  466. <input
  467. class="flex min-h-[39px] w-full rounded-md border px-3 py-2 text-sm text-gray-600 transition-all hover:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400"
  468. v-model="filters.start"
  469. placeholder="@lang('gift::app.admin.stats.start-date')"
  470. />
  471. </x-admin::flat-picker.date>
  472. <x-admin::flat-picker.date class="!w-[140px]" ::allow-input="false">
  473. <input
  474. class="flex min-h-[39px] w-full rounded-md border px-3 py-2 text-sm text-gray-600 transition-all hover:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400"
  475. v-model="filters.end"
  476. placeholder="@lang('gift::app.admin.stats.end-date')"
  477. />
  478. </x-admin::flat-picker.date>
  479. </div>
  480. <!-- 概览 -->
  481. <div class="mt-4 grid grid-cols-2 gap-4">
  482. <div class="rounded-md bg-gray-50 p-4 dark:bg-gray-800">
  483. <p class="text-xs text-gray-500 dark:text-gray-400">
  484. @lang('gift::app.admin.stats.available')
  485. </p>
  486. <p class="mt-1 text-lg font-bold leading-none text-gray-800 dark:text-white">
  487. @{{ summary.available ?? 0 }}
  488. </p>
  489. </div>
  490. <div class="rounded-md bg-gray-50 p-4 dark:bg-gray-800">
  491. <p class="text-xs text-gray-500 dark:text-gray-400">
  492. @lang('gift::app.admin.stats.used')
  493. </p>
  494. <p class="mt-1 text-lg font-bold leading-none text-gray-800 dark:text-white">
  495. @{{ summary.used ?? 0 }}
  496. </p>
  497. </div>
  498. <div class="rounded-md bg-gray-50 p-4 dark:bg-gray-800">
  499. <p class="text-xs text-gray-500 dark:text-gray-400">
  500. @lang('gift::app.admin.stats.new-total')
  501. </p>
  502. <p class="mt-1 text-lg font-bold leading-none text-gray-800 dark:text-white">
  503. @{{ summary.new ?? 0 }}
  504. </p>
  505. </div>
  506. <div class="rounded-md bg-gray-50 p-4 dark:bg-gray-800">
  507. <p class="text-xs text-gray-500 dark:text-gray-400">
  508. @lang('gift::app.admin.stats.expired')
  509. </p>
  510. <p class="mt-1 text-lg font-bold leading-none text-gray-800 dark:text-white">
  511. @{{ summary.expired ?? 0 }}
  512. </p>
  513. </div>
  514. </div>
  515. <!-- 图例 -->
  516. <div class="mt-4 flex flex-wrap justify-center gap-5">
  517. <div class="flex items-center gap-1">
  518. <span class="h-3.5 w-3.5 rounded-md" style="background-color: #598de6"></span>
  519. <p class="text-xs text-gray-600 dark:text-gray-300">
  520. @lang('gift::app.admin.stats.series-new')
  521. </p>
  522. </div>
  523. <div class="flex items-center gap-1">
  524. <span class="h-3.5 w-3.5 rounded-md" style="background-color: #f87171"></span>
  525. <p class="text-xs text-gray-600 dark:text-gray-300">
  526. @lang('gift::app.admin.stats.series-expired')
  527. </p>
  528. </div>
  529. </div>
  530. <!-- 图表 -->
  531. <template v-if="isLoading">
  532. <div class="shimmer mt-4 h-[180px] w-full rounded-md"></div>
  533. </template>
  534. <template v-else>
  535. <x-admin::charts.bar
  536. ::key="chartVersion"
  537. ::labels="labels"
  538. ::datasets="datasets"
  539. ::aspect-ratio="3"
  540. />
  541. <p
  542. v-if="isEmpty"
  543. class="mt-2 text-center text-xs text-gray-500 dark:text-gray-400"
  544. >
  545. @lang('gift::app.admin.stats.empty')
  546. </p>
  547. </template>
  548. </div>
  549. </div>
  550. </script>
  551. <script type="module">
  552. app.component('v-gift-card-statistics', {
  553. template: '#v-gift-card-statistics-template',
  554. data() {
  555. return {
  556. // 默认收起:不请求统计接口,等用户点按钮
  557. isOpen: false,
  558. report: {
  559. labels: [],
  560. new: [],
  561. expired: [],
  562. summary: {},
  563. },
  564. isLoading: true,
  565. // 每次取数后 +1,用来强制图表重建(图表组件只在 mounted 时绘制)
  566. chartVersion: 0,
  567. today: "{{ now()->format('Y-m-d') }}",
  568. filters: {
  569. start: "{{ now()->subDays(29)->format('Y-m-d') }}",
  570. end: "{{ now()->format('Y-m-d') }}",
  571. },
  572. }
  573. },
  574. computed: {
  575. labels() {
  576. return this.report.labels ?? [];
  577. },
  578. summary() {
  579. return this.report.summary ?? {};
  580. },
  581. datasets() {
  582. return [{
  583. label: "{{ trans('gift::app.admin.stats.series-new') }}",
  584. data: this.report.new ?? [],
  585. backgroundColor: '#598de6',
  586. barThickness: 8,
  587. }, {
  588. label: "{{ trans('gift::app.admin.stats.series-expired') }}",
  589. data: this.report.expired ?? [],
  590. backgroundColor: '#f87171',
  591. barThickness: 8,
  592. }];
  593. },
  594. isEmpty() {
  595. if (this.isLoading) {
  596. return false;
  597. }
  598. return ! this.summary.new && ! this.summary.expired;
  599. },
  600. },
  601. watch: {
  602. filters: {
  603. handler() {
  604. this.getStats();
  605. },
  606. deep: true,
  607. },
  608. },
  609. methods: {
  610. toggle() {
  611. this.isOpen = ! this.isOpen;
  612. if (this.isOpen) {
  613. this.getStats();
  614. }
  615. },
  616. getStats() {
  617. this.isLoading = true;
  618. this.$axios.get("{{ route('admin.gift.statistics') }}", {
  619. params: this.filters
  620. })
  621. .then(response => {
  622. this.report = response.data;
  623. this.isLoading = false;
  624. this.chartVersion++;
  625. })
  626. .catch(error => {
  627. this.isLoading = false;
  628. let message = error.response?.data?.message;
  629. if (message) {
  630. this.$emitter.emit('add-flash', { type: 'error', message });
  631. }
  632. });
  633. },
  634. applyQuickRange(days) {
  635. this.filters = {
  636. start: this.shiftDate(this.today, -(days - 1)),
  637. end: this.today,
  638. };
  639. },
  640. shiftDate(dateString, offsetDays) {
  641. let date = new Date(dateString + 'T00:00:00');
  642. date.setDate(date.getDate() + offsetDays);
  643. let month = String(date.getMonth() + 1).padStart(2, '0');
  644. let day = String(date.getDate()).padStart(2, '0');
  645. return `${date.getFullYear()}-${month}-${day}`;
  646. },
  647. },
  648. });
  649. </script>
  650. @endPushOnce
  651. </x-admin::layouts>