BackupCreator.vue 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. <script setup lang="tsx">
  2. import backup from '@/api/backup'
  3. import { CheckOutlined, CopyOutlined, InfoCircleFilled, WarningOutlined } from '@ant-design/icons-vue'
  4. import { UseClipboard } from '@vueuse/components'
  5. import { message } from 'ant-design-vue'
  6. import { ref } from 'vue'
  7. const isCreatingBackup = ref(false)
  8. const showSecurityModal = ref(false)
  9. const currentSecurityToken = ref('')
  10. const isCopied = ref(false)
  11. async function handleCreateBackup() {
  12. try {
  13. isCreatingBackup.value = true
  14. const response = await backup.createBackup()
  15. // Extract filename from Content-Disposition header if available
  16. const contentDisposition = response.headers['content-disposition']
  17. let filename = 'nginx-ui-backup.zip'
  18. if (contentDisposition) {
  19. const filenameMatch = contentDisposition.match(/filename=(.+)/)
  20. if (filenameMatch && filenameMatch[1]) {
  21. filename = filenameMatch[1].replace(/"/g, '')
  22. }
  23. }
  24. // Extract security token from header
  25. const securityToken = response.headers['x-backup-security']
  26. // Create download link
  27. const url = window.URL.createObjectURL(new Blob([response.data]))
  28. const link = document.createElement('a')
  29. link.href = url
  30. link.setAttribute('download', filename)
  31. document.body.appendChild(link)
  32. link.click()
  33. document.body.removeChild(link)
  34. // Show security token to user
  35. if (securityToken) {
  36. message.success($gettext('Backup has been downloaded successfully'))
  37. // Show the security token modal
  38. currentSecurityToken.value = securityToken
  39. showSecurityModal.value = true
  40. }
  41. }
  42. catch (error) {
  43. console.error('Backup download failed:', error)
  44. }
  45. finally {
  46. isCreatingBackup.value = false
  47. }
  48. }
  49. function handleCloseModal() {
  50. showSecurityModal.value = false
  51. }
  52. function handleCopy(copy) {
  53. copy()
  54. isCopied.value = true
  55. setTimeout(() => {
  56. isCopied.value = false
  57. }, 2000)
  58. }
  59. </script>
  60. <template>
  61. <ACard :title="$gettext('System Backup')" :bordered="false">
  62. <AAlert
  63. show-icon
  64. type="info"
  65. :message="$gettext('Create system backups including Nginx configuration and Nginx UI settings. Backup files will be automatically downloaded to your computer.')"
  66. class="mb-4"
  67. />
  68. <div class="flex justify-between">
  69. <ASpace>
  70. <AButton
  71. type="primary"
  72. :loading="isCreatingBackup"
  73. @click="handleCreateBackup"
  74. >
  75. {{ $gettext('Create Backup') }}
  76. </AButton>
  77. </ASpace>
  78. </div>
  79. <!-- Security Token Modal Component -->
  80. <AModal
  81. v-model:visible="showSecurityModal"
  82. :title="$gettext('Security Token Information')"
  83. :mask-closable="false"
  84. :centered="true"
  85. class="backup-token-modal"
  86. width="550"
  87. @ok="handleCloseModal"
  88. >
  89. <template #icon>
  90. <InfoCircleFilled style="color: #1677ff; font-size: 22px" />
  91. </template>
  92. <div class="security-token-info py-2">
  93. <p class="mb-4">
  94. {{ $gettext('Please save this security token, you will need it for restoration:') }}
  95. </p>
  96. <div class="token-display mb-5">
  97. <div class="token-container p-4 bg-gray-50 border border-gray-200 rounded-md mb-2">
  98. <div class="token-text font-mono select-all break-all leading-relaxed">
  99. {{ currentSecurityToken }}
  100. </div>
  101. </div>
  102. <div class="flex justify-end mt-3">
  103. <UseClipboard v-slot="{ copy }" :source="currentSecurityToken">
  104. <AButton
  105. type="primary"
  106. :style="{ backgroundColor: isCopied ? '#52c41a' : undefined }"
  107. @click="handleCopy(copy)"
  108. >
  109. <template #icon>
  110. <CheckOutlined v-if="isCopied" />
  111. <CopyOutlined v-else />
  112. </template>
  113. {{ isCopied ? $gettext('Copied!') : $gettext('Copy') }}
  114. </AButton>
  115. </UseClipboard>
  116. </div>
  117. </div>
  118. <div class="warning-box flex items-start bg-red-50 border border-red-200 p-4 rounded-md">
  119. <WarningOutlined class="text-red-500 mt-0.5 mr-2 flex-shrink-0" />
  120. <div>
  121. <p class="text-red-600 font-medium mb-1">
  122. {{ $gettext('Warning') }}
  123. </p>
  124. <p class="text-red-600 mb-0 text-sm leading-relaxed">
  125. {{ $gettext('This token will only be shown once and cannot be retrieved later. Please make sure to save it in a secure location.') }}
  126. </p>
  127. </div>
  128. </div>
  129. </div>
  130. <template #footer>
  131. <AButton type="primary" @click="handleCloseModal">
  132. {{ $gettext('OK') }}
  133. </AButton>
  134. </template>
  135. </AModal>
  136. </ACard>
  137. </template>
  138. <style scoped>
  139. .security-token-info {
  140. text-align: left;
  141. }
  142. .token-container {
  143. word-break: break-all;
  144. box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05);
  145. }
  146. .token-text {
  147. line-height: 1.6;
  148. }
  149. /* Dark mode optimization */
  150. :deep(.backup-token-modal) {
  151. /* Modal background */
  152. .ant-modal-content {
  153. background-color: #1f1f1f;
  154. }
  155. /* Modal title */
  156. .ant-modal-header {
  157. background-color: #1f1f1f;
  158. border-bottom: 1px solid #303030;
  159. }
  160. .ant-modal-title {
  161. color: #e6e6e6;
  162. }
  163. /* Modal content */
  164. .ant-modal-body {
  165. color: #e6e6e6;
  166. }
  167. /* Modal footer */
  168. .ant-modal-footer {
  169. border-top: 1px solid #303030;
  170. background-color: #1f1f1f;
  171. }
  172. /* Close button */
  173. .ant-modal-close-x {
  174. color: #e6e6e6;
  175. }
  176. }
  177. /* Token container dark mode styles */
  178. .dark {
  179. .token-container {
  180. background-color: #262626 !important;
  181. border-color: #303030 !important;
  182. box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2);
  183. }
  184. .token-text {
  185. color: #d9d9d9;
  186. }
  187. /* Warning box dark mode */
  188. .warning-box {
  189. background-color: rgba(255, 77, 79, 0.1);
  190. border-color: rgba(255, 77, 79, 0.3);
  191. p {
  192. color: #ff7875;
  193. }
  194. }
  195. }
  196. /* Dark mode support via media query */
  197. @media (prefers-color-scheme: dark) {
  198. .token-container {
  199. background-color: #262626 !important;
  200. border-color: #303030 !important;
  201. }
  202. .token-text {
  203. color: #d9d9d9;
  204. }
  205. .warning-box {
  206. background-color: rgba(255, 77, 79, 0.1);
  207. border-color: rgba(255, 77, 79, 0.3);
  208. p {
  209. color: #ff7875;
  210. }
  211. }
  212. }
  213. </style>