UploadConfig.vue 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. <template>
  2. <div class="settings-container">
  3. <!-- 过期时间设置 -->
  4. <div class="setting-item">
  5. <label>⏰ 过期时间:</label>
  6. <div class="option-buttons">
  7. <button
  8. v-for="hours in expirationOptions"
  9. :key="hours"
  10. :class="['option-btn', { active: expirationHours === hours }]"
  11. @click="$emit('update:expirationHours', hours)"
  12. >
  13. {{ getExpirationLabel(hours) }}
  14. </button>
  15. <input
  16. :value="customHours"
  17. type="number"
  18. min="1"
  19. max="24"
  20. class="custom-input"
  21. placeholder="自定义"
  22. @input="onCustomHoursChange"
  23. >
  24. </div>
  25. </div>
  26. <!-- 下载次数限制 -->
  27. <div class="setting-item">
  28. <label>📥 下载次数:</label>
  29. <select
  30. :value="maxDownloadCount"
  31. class="setting-select"
  32. @change="$emit('update:maxDownloadCount', Number($event.target.value))"
  33. >
  34. <option v-for="option in downloadCountOptions" :key="option.value" :value="option.value">
  35. {{ option.label }}
  36. </option>
  37. </select>
  38. </div>
  39. <!-- 密码设置 -->
  40. <div class="setting-item">
  41. <label>🔒 访问密码:</label>
  42. <input
  43. :value="password"
  44. type="password"
  45. class="password-input"
  46. placeholder="选填,留空则无需密码"
  47. @input="$emit('update:password', $event.target.value)"
  48. >
  49. </div>
  50. <!-- 上传按钮 -->
  51. <button
  52. class="btn-upload"
  53. :disabled="!canUpload || uploading"
  54. @click="$emit('upload')"
  55. >
  56. <span v-if="uploading" class="spinner"></span>
  57. <span v-else>🚀 立即上传</span>
  58. </button>
  59. <!-- 上传进度条 -->
  60. <div v-if="uploading && uploadProgress > 0" class="upload-progress-container">
  61. <div class="progress-info">
  62. <span class="progress-percent">{{ uploadProgress }}%</span>
  63. <span class="progress-speed">{{ formatSpeed(uploadSpeed) }}</span>
  64. <span v-if="remainingTime > 0" class="progress-time">
  65. 剩余 {{ formatTime(remainingTime) }}
  66. </span>
  67. </div>
  68. <div class="progress-bar">
  69. <div class="progress-fill" :style="{ width: uploadProgress + '%' }"></div>
  70. </div>
  71. </div>
  72. </div>
  73. </template>
  74. <script setup>
  75. import { ref, watch } from 'vue'
  76. import config from '@/config'
  77. const props = defineProps({
  78. expirationHours: {
  79. type: Number,
  80. default: 3
  81. },
  82. maxDownloadCount: {
  83. type: Number,
  84. default: -1
  85. },
  86. password: {
  87. type: String,
  88. default: ''
  89. },
  90. canUpload: {
  91. type: Boolean,
  92. default: false
  93. },
  94. uploading: {
  95. type: Boolean,
  96. default: false
  97. },
  98. uploadProgress: {
  99. type: Number,
  100. default: 0
  101. },
  102. uploadSpeed: {
  103. type: Number,
  104. default: 0
  105. },
  106. remainingTime: {
  107. type: Number,
  108. default: 0
  109. }
  110. })
  111. const emit = defineEmits([
  112. 'upload',
  113. 'update:expirationHours',
  114. 'update:maxDownloadCount',
  115. 'update:password'
  116. ])
  117. const expirationOptions = config.expirationOptions
  118. const downloadCountOptions = config.downloadCountOptions
  119. const customHours = ref(null)
  120. function getExpirationLabel(hours) {
  121. return config.expirationLabels[hours] || `${hours}小时`
  122. }
  123. function onCustomHoursChange(e) {
  124. const val = Number(e.target.value)
  125. customHours.value = val
  126. if (val && val >= 1) {
  127. emit('update:expirationHours', val)
  128. }
  129. }
  130. // 格式化上传速度
  131. function formatSpeed(bytesPerSecond) {
  132. if (bytesPerSecond === 0) return '0 B/s'
  133. const k = 1024
  134. const sizes = ['B/s', 'KB/s', 'MB/s', 'GB/s']
  135. const i = Math.floor(Math.log(bytesPerSecond) / Math.log(k))
  136. const speed = bytesPerSecond / Math.pow(k, i)
  137. return Math.round(speed * 100) / 100 + ' ' + sizes[i]
  138. }
  139. // 格式化剩余时间
  140. function formatTime(seconds) {
  141. if (seconds < 60) {
  142. return `${seconds}秒`
  143. } else if (seconds < 3600) {
  144. const minutes = Math.floor(seconds / 60)
  145. const remainingSeconds = seconds % 60
  146. return remainingSeconds > 0 ? `${minutes}分${remainingSeconds}秒` : `${minutes}分钟`
  147. } else {
  148. const hours = Math.floor(seconds / 3600)
  149. const minutes = Math.floor((seconds % 3600) / 60)
  150. return minutes > 0 ? `${hours}小时${minutes}分钟` : `${hours}小时`
  151. }
  152. }
  153. </script>
  154. <style lang="scss" scoped>
  155. @import '@/styles/variables.scss';
  156. .settings-container {
  157. @extend .card !optional;
  158. margin-bottom: $spacing-xl;
  159. }
  160. .setting-item {
  161. display: flex;
  162. align-items: center;
  163. gap: $spacing-md;
  164. margin-bottom: $spacing-lg;
  165. &:last-child {
  166. margin-bottom: 0;
  167. }
  168. label {
  169. min-width: 120px;
  170. color: $text-secondary;
  171. }
  172. }
  173. .option-buttons {
  174. display: flex;
  175. gap: $spacing-sm;
  176. flex-wrap: wrap;
  177. flex: 1;
  178. }
  179. .option-btn {
  180. padding: $spacing-sm $spacing-md;
  181. background: $bg-tertiary;
  182. border: 1px solid $border-color;
  183. border-radius: $radius-md;
  184. color: $text-primary;
  185. cursor: pointer;
  186. transition: all $transition-base ease;
  187. &:hover {
  188. background: $bg-hover;
  189. }
  190. &.active {
  191. background: linear-gradient(135deg, $primary-color, $secondary-color);
  192. border-color: transparent;
  193. }
  194. }
  195. .custom-input {
  196. width: 100px;
  197. padding: $spacing-sm $spacing-md;
  198. background: $bg-tertiary;
  199. border: 1px solid $border-color;
  200. border-radius: $radius-md;
  201. color: $text-primary;
  202. text-align: center;
  203. }
  204. .setting-select,
  205. .password-input {
  206. flex: 1;
  207. }
  208. .btn-upload {
  209. width: 100%;
  210. margin-top: $spacing-lg;
  211. padding: $spacing-lg;
  212. font-size: $font-size-xl;
  213. font-weight: 600;
  214. border-radius: $radius-lg;
  215. }
  216. .spinner {
  217. animation: spin 1s linear infinite;
  218. }
  219. @keyframes spin {
  220. from {
  221. transform: rotate(0deg);
  222. }
  223. to {
  224. transform: rotate(360deg);
  225. }
  226. }
  227. // 上传进度条样式
  228. .upload-progress-container {
  229. margin-top: $spacing-md;
  230. padding: $spacing-md;
  231. background: rgba($bg-secondary, 0.6);
  232. backdrop-filter: blur(10px);
  233. border: 1px solid $border-color;
  234. border-radius: $radius-lg;
  235. animation: progressSlideIn 0.3s ease-out;
  236. }
  237. @keyframes progressSlideIn {
  238. from {
  239. opacity: 0;
  240. transform: translateY(-10px);
  241. }
  242. to {
  243. opacity: 1;
  244. transform: translateY(0);
  245. }
  246. }
  247. .progress-info {
  248. display: flex;
  249. justify-content: space-between;
  250. align-items: center;
  251. margin-bottom: $spacing-sm;
  252. font-size: $font-size-sm;
  253. color: $text-secondary;
  254. .progress-percent {
  255. font-weight: 600;
  256. color: $primary-color;
  257. font-size: $font-size-base;
  258. }
  259. .progress-speed {
  260. color: $text-muted;
  261. }
  262. .progress-time {
  263. color: $warning-color;
  264. font-weight: 500;
  265. }
  266. }
  267. .progress-bar {
  268. width: 100%;
  269. height: 8px;
  270. background: $bg-tertiary;
  271. border-radius: $radius-full;
  272. overflow: hidden;
  273. position: relative;
  274. }
  275. .progress-fill {
  276. height: 100%;
  277. background: linear-gradient(90deg, $primary-color 0%, $secondary-color 100%);
  278. border-radius: $radius-full;
  279. transition: width 0.3s ease;
  280. position: relative;
  281. overflow: hidden;
  282. &::after {
  283. content: '';
  284. position: absolute;
  285. top: 0;
  286. left: 0;
  287. bottom: 0;
  288. right: 0;
  289. background: linear-gradient(
  290. 90deg,
  291. transparent,
  292. rgba(255, 255, 255, 0.3),
  293. transparent
  294. );
  295. animation: progressShimmer 2s infinite;
  296. }
  297. }
  298. @keyframes progressShimmer {
  299. 0% {
  300. transform: translateX(-100%);
  301. }
  302. 100% {
  303. transform: translateX(100%);
  304. }
  305. }
  306. @media (max-width: 768px) {
  307. .option-buttons {
  308. display: grid;
  309. grid-template-columns: repeat(3, 1fr);
  310. gap: $spacing-xs;
  311. }
  312. .option-btn {
  313. padding: $spacing-xs $spacing-sm;
  314. font-size: $font-size-sm;
  315. text-align: center;
  316. }
  317. .custom-input {
  318. width: 100%;
  319. grid-column: 1 / -1;
  320. }
  321. .setting-item {
  322. flex-direction: column;
  323. align-items: stretch;
  324. gap: $spacing-xs;
  325. label {
  326. min-width: auto;
  327. margin-bottom: $spacing-xs;
  328. }
  329. }
  330. .upload-progress-container {
  331. padding: $spacing-sm;
  332. .progress-info {
  333. flex-wrap: wrap;
  334. gap: $spacing-xs;
  335. font-size: $font-size-xs;
  336. .progress-percent {
  337. font-size: $font-size-sm;
  338. }
  339. }
  340. .progress-bar {
  341. height: 6px;
  342. }
  343. }
  344. }
  345. </style>