index.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742
  1. <template>
  2. <view class="background-files">
  3. <view class="background-files-head adfacjb">
  4. <view>
  5. <view class="background-files-title">团队背景资料</view>
  6. <view class="background-files-count">已上传 {{ count }} 个文档</view>
  7. </view>
  8. <view v-if="editable" class="background-files-add" @click="chooseWechatFiles">从微信会话选择</view>
  9. </view>
  10. <template v-if="editable">
  11. <view class="background-files-pc" :class="{ disabled: sessionCreating }" @click="usePcUpload">{{ sessionCreating ? '生成中' : '使用电脑上传' }}</view>
  12. <view v-if="files.length" class="file-list">
  13. <view v-for="file in files" :key="file.id" class="file-row adfacjb">
  14. <view class="file-main">
  15. <view class="file-name">{{ file.fileName || file.name || '未命名文件' }}</view>
  16. <view class="file-meta">{{ formatFileSize(file.fileSize || file.size) }}</view>
  17. </view>
  18. <view class="file-actions adfac">
  19. <view class="file-action" @click="previewFile(file)">{{ previewingId === file.id ? '打开中' : '查看' }}</view>
  20. <view class="file-action danger" @click="removeFile(file)">停用</view>
  21. </view>
  22. </view>
  23. </view>
  24. <view v-if="queue.length" class="queue-list">
  25. <view class="queue-title">本次选择</view>
  26. <view v-for="item in queue" :key="item.id" class="queue-row">
  27. <view class="queue-line adfacjb">
  28. <view class="queue-name">{{ item.name }}</view>
  29. <view class="queue-status" :class="item.status">{{ queueStatusText(item) }}</view>
  30. </view>
  31. <view v-if="item.status === 'uploading'" class="queue-progress">
  32. <view class="queue-progress-current" :style="{ width: item.progress + '%' }"></view>
  33. </view>
  34. <view v-if="item.error" class="queue-error">{{ item.error }}</view>
  35. <view v-if="item.status === 'failed' && !item.validationError" class="queue-retry" @click="retryItem(item)">重试</view>
  36. </view>
  37. </view>
  38. </template>
  39. <view v-if="pcDialogVisible" class="overlay adffcacjc">
  40. <view class="pc-dialog">
  41. <view class="dialog-title">使用电脑上传</view>
  42. <view class="dialog-desc" v-if="remainingSeconds > 0">在电脑打开链接或输入验证码,有效期剩余 {{ remainingText }}</view>
  43. <view class="dialog-expired" v-else>本次上传会话已过期,请重新生成</view>
  44. <view class="session-expiry">失效时间:{{ session && session.expiresAt || '-' }}</view>
  45. <view class="session-code">{{ session && session.code || '------' }}</view>
  46. <view class="session-link">{{ session && session.uploadUrl || '' }}</view>
  47. <view class="session-actions adfacjb">
  48. <view class="session-button secondary" :class="{ disabled: remainingSeconds === 0 }" @click="copyUploadUrl">复制链接</view>
  49. <view class="session-button secondary" :class="{ disabled: sessionCreating }" @click="refreshPcSession">{{ sessionCreating ? '生成中' : '重新生成' }}</view>
  50. </view>
  51. <view class="session-actions adfacjb">
  52. <view class="session-button primary" @click="finishDeferredPcUpload">完成并刷新</view>
  53. <view class="session-button secondary" @click="skipDeferredPcUpload">暂不上传</view>
  54. </view>
  55. </view>
  56. </view>
  57. <view v-if="textPreview.visible" class="overlay text-overlay adffcacjc">
  58. <view class="text-dialog adffc">
  59. <view class="dialog-title text-title">{{ textPreview.fileName }}</view>
  60. <view v-if="textPreview.truncated" class="truncated-tip">文件较大,仅展示前 256KB 内容</view>
  61. <scroll-view class="text-content" scroll-y>
  62. <text selectable>{{ textPreview.content }}</text>
  63. </scroll-view>
  64. <view class="text-close" @click="closeTextPreview">关闭</view>
  65. </view>
  66. </view>
  67. </view>
  68. </template>
  69. <script>
  70. import {
  71. createTeamBackgroundSession,
  72. disableTeamBackgroundFile,
  73. downloadTeamBackgroundFile,
  74. listTeamBackgroundFiles,
  75. uploadTeamBackgroundFile
  76. } from '@/http/teamBackgroundFile.js'
  77. import teamBackgroundRules from '@/utils/teamBackgroundFile.js'
  78. const {
  79. createBackgroundQueueItem,
  80. createEmptyTextPreviewState,
  81. createTextPreviewState,
  82. getSessionRemainingSeconds,
  83. getUploadableBackgroundFiles,
  84. hasUnresolvedBackgroundFiles
  85. } = teamBackgroundRules
  86. export default {
  87. props: {
  88. teamId: { type: [String, Number], default: '' },
  89. initialCount: { type: Number, default: 0 },
  90. editable: { type: Boolean, default: true }
  91. },
  92. data() {
  93. return {
  94. boundTeamId: '',
  95. contextGeneration: 0,
  96. uploadGeneration: 0,
  97. session: null,
  98. pcDialogVisible: false,
  99. pcUploadDeferred: false,
  100. deferredPcResolve: null,
  101. deferredPcReject: null,
  102. deferredPcWaitPromise: null,
  103. files: [],
  104. count: this.initialCount || 0,
  105. filesReady: false,
  106. queue: [],
  107. remainingSeconds: 0,
  108. sessionTimer: null,
  109. sessionCreating: false,
  110. sessionRequestsByTeam: {},
  111. sessionRequestId: 0,
  112. refreshRequestId: 0,
  113. previewRequestId: 0,
  114. previewingId: '',
  115. uploadPromise: null,
  116. uploadPromiseContext: '',
  117. hasLoadedFiles: false,
  118. componentDestroyed: false,
  119. textPreview: createEmptyTextPreviewState()
  120. }
  121. },
  122. computed: {
  123. remainingText() {
  124. const minutes = Math.floor(this.remainingSeconds / 60)
  125. const seconds = this.remainingSeconds % 60
  126. return `${minutes}分${String(seconds).padStart(2, '0')}秒`
  127. }
  128. },
  129. watch: {
  130. teamId: {
  131. immediate: true,
  132. handler(value) {
  133. const nextTeamId = value || ''
  134. if (String(nextTeamId) === String(this.boundTeamId)) return
  135. this.resetTeamContext(nextTeamId)
  136. if (value && this.editable) {
  137. this.refreshFiles(value).catch(error => this.showError(error, '资料列表加载失败'))
  138. }
  139. }
  140. },
  141. initialCount(value) {
  142. if (!this.hasLoadedFiles) this.count = Number(value) || 0
  143. }
  144. },
  145. beforeDestroy() {
  146. this.componentDestroyed = true
  147. this.sessionRequestId += 1
  148. this.refreshRequestId += 1
  149. this.previewRequestId += 1
  150. this.uploadGeneration += 1
  151. this.clearSessionTimer()
  152. this.sessionCreating = false
  153. this.textPreview = createEmptyTextPreviewState()
  154. this.settleDeferredPcUpload(new Error('背景资料组件已关闭'))
  155. this.uploadPromise = null
  156. this.uploadPromiseContext = ''
  157. },
  158. methods: {
  159. sameTeamId(first, second) {
  160. return String(first || '') === String(second || '')
  161. },
  162. activeTeamId(teamId) {
  163. return teamId || this.teamId || this.boundTeamId
  164. },
  165. isCurrentContext(teamId, generation) {
  166. return !this.componentDestroyed && generation === this.contextGeneration
  167. && this.sameTeamId(teamId, this.activeTeamId())
  168. },
  169. resetTeamContext(teamId) {
  170. this.contextGeneration += 1
  171. this.uploadGeneration += 1
  172. this.refreshRequestId += 1
  173. this.sessionRequestId += 1
  174. this.previewRequestId += 1
  175. this.clearSessionTimer()
  176. this.settleDeferredPcUpload(new Error('团队上下文已变更'))
  177. this.boundTeamId = teamId || ''
  178. this.files = []
  179. this.count = 0
  180. this.filesReady = false
  181. this.hasLoadedFiles = false
  182. this.queue = []
  183. this.session = null
  184. this.pcDialogVisible = false
  185. this.pcUploadDeferred = false
  186. this.remainingSeconds = 0
  187. this.previewingId = ''
  188. this.textPreview = createEmptyTextPreviewState()
  189. this.uploadPromise = null
  190. this.uploadPromiseContext = ''
  191. this.updateSessionCreating()
  192. this.$emit('countChange', 0)
  193. },
  194. bindDeferredTeam(teamId) {
  195. if (!this.boundTeamId && !this.teamId) {
  196. this.boundTeamId = teamId
  197. this.contextGeneration += 1
  198. this.uploadGeneration += 1
  199. return
  200. }
  201. if (!this.sameTeamId(teamId, this.activeTeamId())) this.resetTeamContext(teamId)
  202. },
  203. showError(error, fallback) {
  204. if (this.componentDestroyed) return
  205. this.$showToast(error && (error.message || error.msg) || fallback)
  206. },
  207. queueStatusText(item) {
  208. const labels = {
  209. pending: '待上传',
  210. uploading: `${item.progress || 0}%`,
  211. success: '已上传',
  212. failed: '失败'
  213. }
  214. return labels[item.status] || ''
  215. },
  216. formatFileSize(bytes) {
  217. const size = Number(bytes) || 0
  218. if (size < 1024) return `${size} B`
  219. if (size < 1024 * 1024) return `${Number((size / 1024).toFixed(1))} KB`
  220. return `${Number((size / 1024 / 1024).toFixed(1))} MB`
  221. },
  222. chooseWechatFiles() {
  223. if (typeof wx === 'undefined' || typeof wx.chooseMessageFile !== 'function') {
  224. this.$showToast('请在微信小程序中选择文件')
  225. return
  226. }
  227. wx.chooseMessageFile({
  228. count: 10,
  229. type: 'file',
  230. success: result => {
  231. const selectedAt = Date.now()
  232. ;(result.tempFiles || []).forEach((file, index) => {
  233. this.queue.push(createBackgroundQueueItem(file, `${selectedAt}-${index}-${file.path}`))
  234. })
  235. const teamId = this.activeTeamId()
  236. if (teamId) this.uploadPending(teamId).catch(error => this.showError(error, '上传失败,请重试'))
  237. },
  238. fail: error => {
  239. if (error && !String(error.errMsg || '').includes('cancel')) this.showError(error, '文件选择失败')
  240. }
  241. })
  242. },
  243. uploadPending(teamId) {
  244. return this.flushPendingFiles(teamId)
  245. },
  246. async flushPendingFiles(teamId) {
  247. const targetTeamId = teamId || this.activeTeamId()
  248. if (!targetTeamId) return { success: 0, failed: 0 }
  249. this.bindDeferredTeam(targetTeamId)
  250. const generation = this.contextGeneration
  251. const uploadGeneration = this.uploadGeneration
  252. const contextKey = `${generation}:${targetTeamId}:${uploadGeneration}`
  253. if (this.uploadPromise && this.uploadPromiseContext === contextKey) return this.uploadPromise
  254. const queueSnapshot = getUploadableBackgroundFiles(this.queue).slice()
  255. const upload = this.performPendingUploads(
  256. targetTeamId, queueSnapshot, generation, uploadGeneration
  257. )
  258. this.uploadPromise = upload
  259. this.uploadPromiseContext = contextKey
  260. let result
  261. try {
  262. result = await upload
  263. } finally {
  264. if (this.isCurrentContext(targetTeamId, generation)
  265. && this.uploadPromise === upload && this.uploadPromiseContext === contextKey) {
  266. this.uploadPromise = null
  267. this.uploadPromiseContext = ''
  268. }
  269. }
  270. if (this.isCurrentContext(targetTeamId, generation)
  271. && this.queue.some(item => item.status === 'pending')) {
  272. const laterResult = await this.flushPendingFiles(targetTeamId)
  273. return {
  274. success: result.success + laterResult.success,
  275. failed: result.failed + laterResult.failed
  276. }
  277. }
  278. return result
  279. },
  280. async performPendingUploads(teamId, queueSnapshot, generation, uploadGeneration) {
  281. let success = 0
  282. let failed = 0
  283. for (const item of queueSnapshot) {
  284. if (!this.isCurrentContext(teamId, generation)
  285. || uploadGeneration !== this.uploadGeneration) break
  286. item.status = 'uploading'
  287. item.error = ''
  288. try {
  289. await uploadTeamBackgroundFile(teamId, item.path, progress => {
  290. if (this.isCurrentContext(teamId, generation)
  291. && uploadGeneration === this.uploadGeneration
  292. && this.queue.includes(item)) item.progress = progress
  293. })
  294. if (!this.isCurrentContext(teamId, generation)
  295. || uploadGeneration !== this.uploadGeneration
  296. || !this.queue.includes(item)) break
  297. item.status = 'success'
  298. item.progress = 100
  299. success += 1
  300. } catch (error) {
  301. if (!this.isCurrentContext(teamId, generation)
  302. || uploadGeneration !== this.uploadGeneration
  303. || !this.queue.includes(item)) break
  304. item.status = 'failed'
  305. item.error = error && (error.message || error.msg) || '上传失败,请重试'
  306. failed += 1
  307. }
  308. }
  309. if (this.isCurrentContext(teamId, generation)
  310. && uploadGeneration === this.uploadGeneration) await this.refreshFiles(teamId)
  311. return { success, failed }
  312. },
  313. hasUnresolvedFiles() {
  314. return hasUnresolvedBackgroundFiles(this.queue)
  315. },
  316. isUploadActive() {
  317. return Boolean(this.uploadPromise)
  318. },
  319. retryItem(item) {
  320. if (item.validationError || item.status === 'uploading') return
  321. item.status = 'pending'
  322. item.error = ''
  323. item.progress = 0
  324. const teamId = this.activeTeamId()
  325. if (teamId) this.uploadPending(teamId).catch(error => this.showError(error, '上传失败,请重试'))
  326. },
  327. async refreshFiles(teamId = this.activeTeamId()) {
  328. if (!teamId || !this.editable || !this.sameTeamId(teamId, this.activeTeamId())) return []
  329. const generation = this.contextGeneration
  330. const requestId = ++this.refreshRequestId
  331. this.filesReady = false
  332. let files
  333. try {
  334. files = await listTeamBackgroundFiles(teamId)
  335. } catch (error) {
  336. if (!this.isCurrentContext(teamId, generation)
  337. || requestId !== this.refreshRequestId) return this.files
  338. this.filesReady = this.hasLoadedFiles
  339. throw error
  340. }
  341. if (!this.isCurrentContext(teamId, generation)
  342. || requestId !== this.refreshRequestId) return this.files
  343. this.files = Array.isArray(files) ? files : []
  344. this.count = this.files.length
  345. this.filesReady = true
  346. this.hasLoadedFiles = true
  347. this.$emit('countChange', this.count)
  348. return this.files
  349. },
  350. isCurrentFile(file, teamId, generation) {
  351. if (!this.filesReady || !file || !this.isCurrentContext(teamId, generation)) return false
  352. return this.files.some(current => String(current.id) === String(file.id))
  353. },
  354. async previewFile(file) {
  355. const teamId = this.activeTeamId()
  356. const generation = this.contextGeneration
  357. if (!teamId || file.id === undefined || file.id === null
  358. || !this.isCurrentFile(file, teamId, generation)) return
  359. const requestId = ++this.previewRequestId
  360. this.previewingId = file.id
  361. try {
  362. const result = await downloadTeamBackgroundFile(teamId, file.id)
  363. if (requestId !== this.previewRequestId
  364. || !this.isCurrentFile(file, teamId, generation)) return
  365. if (result && result.mode === 'text') {
  366. this.textPreview = createTextPreviewState(result, file.fileName || file.name)
  367. } else if (result && result.mode === 'document') {
  368. await this.openDocumentPreview(result)
  369. }
  370. } catch (error) {
  371. if (requestId === this.previewRequestId) this.showError(error, '文件打开失败')
  372. } finally {
  373. if (requestId === this.previewRequestId) this.previewingId = ''
  374. }
  375. },
  376. openDocumentPreview(result) {
  377. return new Promise((resolve, reject) => {
  378. if (typeof wx === 'undefined' || typeof wx.openDocument !== 'function') {
  379. reject(new Error('当前环境不支持文档预览'))
  380. return
  381. }
  382. wx.openDocument({
  383. filePath: result.filePath,
  384. showMenu: true,
  385. success: resolve,
  386. fail: reject
  387. })
  388. })
  389. },
  390. closeTextPreview() {
  391. this.previewRequestId += 1
  392. this.previewingId = ''
  393. this.textPreview = createEmptyTextPreviewState()
  394. },
  395. removeFile(file) {
  396. const teamId = this.activeTeamId()
  397. const generation = this.contextGeneration
  398. if (!teamId || file.id === undefined || file.id === null
  399. || !this.isCurrentFile(file, teamId, generation)) return
  400. uni.showModal({
  401. title: '停用资料',
  402. content: `确定停用“${file.fileName || file.name || '该文件'}”吗?`,
  403. confirmColor: '#199C9C',
  404. success: async result => {
  405. if (!result.confirm || !this.isCurrentFile(file, teamId, generation)) return
  406. try {
  407. await disableTeamBackgroundFile(teamId, file.id)
  408. if (!this.isCurrentContext(teamId, generation)) return
  409. await this.refreshFiles(teamId)
  410. if (!this.componentDestroyed) this.$showToast('已停用')
  411. } catch (error) {
  412. this.showError(error, '停用失败,请重试')
  413. }
  414. }
  415. })
  416. },
  417. usePcUpload() {
  418. const teamId = this.activeTeamId()
  419. if (!teamId) {
  420. this.pcUploadDeferred = true
  421. this.$showToast('将先保存团队,再生成电脑上传方式')
  422. return
  423. }
  424. if (this.sessionCreating) return
  425. this.openPcSession(teamId).catch(error => this.showError(error, '上传会话创建失败'))
  426. },
  427. sessionTeamKey(teamId) {
  428. return String(teamId || '')
  429. },
  430. updateSessionCreating() {
  431. if (this.componentDestroyed) {
  432. this.sessionCreating = false
  433. return
  434. }
  435. const teamId = this.activeTeamId()
  436. this.sessionCreating = Boolean(teamId
  437. && this.sessionRequestsByTeam[this.sessionTeamKey(teamId)])
  438. },
  439. getSessionRequest(teamId) {
  440. const key = this.sessionTeamKey(teamId)
  441. const existing = this.sessionRequestsByTeam[key]
  442. if (existing) return existing
  443. const request = createTeamBackgroundSession(teamId)
  444. this.sessionRequestsByTeam[key] = request
  445. this.updateSessionCreating()
  446. const clearRequest = () => {
  447. if (this.sessionRequestsByTeam[key] === request) {
  448. delete this.sessionRequestsByTeam[key]
  449. this.updateSessionCreating()
  450. }
  451. }
  452. request.then(clearRequest, clearRequest)
  453. return request
  454. },
  455. async openPcSession(teamId) {
  456. if (!teamId || !this.sameTeamId(teamId, this.activeTeamId())) return null
  457. const generation = this.contextGeneration
  458. const requestId = this.sessionRequestId
  459. let session
  460. try {
  461. session = await this.getSessionRequest(teamId)
  462. } catch (error) {
  463. if (!this.isCurrentContext(teamId, generation)
  464. || requestId !== this.sessionRequestId) return null
  465. throw error
  466. }
  467. if (!this.isCurrentContext(teamId, generation)
  468. || requestId !== this.sessionRequestId) return null
  469. this.clearSessionTimer()
  470. this.boundTeamId = teamId
  471. this.session = session
  472. this.pcDialogVisible = true
  473. this.startSessionTimer()
  474. return session
  475. },
  476. startSessionTimer() {
  477. this.clearSessionTimer()
  478. this.updateRemainingSeconds()
  479. if (this.remainingSeconds === 0) return
  480. this.sessionTimer = setInterval(() => this.updateRemainingSeconds(), 1000)
  481. },
  482. updateRemainingSeconds() {
  483. this.remainingSeconds = getSessionRemainingSeconds(this.session && this.session.expiresAt)
  484. if (this.remainingSeconds === 0) this.clearSessionTimer()
  485. },
  486. clearSessionTimer() {
  487. if (this.sessionTimer) clearInterval(this.sessionTimer)
  488. this.sessionTimer = null
  489. },
  490. copyUploadUrl() {
  491. if (this.remainingSeconds === 0 || !this.session || !this.session.uploadUrl) return
  492. uni.setClipboardData({
  493. data: this.session.uploadUrl,
  494. success: () => this.$showToast('链接已复制'),
  495. fail: error => this.showError(error, '复制失败')
  496. })
  497. },
  498. refreshPcSession() {
  499. const teamId = this.activeTeamId()
  500. if (!teamId || this.sessionCreating) return
  501. this.openPcSession(teamId).catch(error => this.showError(error, '上传会话创建失败'))
  502. },
  503. hasDeferredPcUpload() {
  504. return this.pcUploadDeferred
  505. },
  506. waitForDeferredPcUpload(teamId) {
  507. if (this.deferredPcWaitPromise) return this.deferredPcWaitPromise
  508. const waiter = new Promise((resolve, reject) => {
  509. this.deferredPcResolve = resolve
  510. this.deferredPcReject = reject
  511. })
  512. this.deferredPcWaitPromise = waiter
  513. this.openPcSession(teamId).then(session => {
  514. if (this.deferredPcWaitPromise !== waiter) return
  515. if (!session || this.componentDestroyed) {
  516. this.settleDeferredPcUpload(new Error('上传会话已失效'))
  517. return
  518. }
  519. this.pcUploadDeferred = false
  520. }, error => {
  521. if (this.deferredPcWaitPromise === waiter) this.settleDeferredPcUpload(error)
  522. })
  523. return waiter
  524. },
  525. settleDeferredPcUpload(error) {
  526. const resolve = this.deferredPcResolve
  527. const reject = this.deferredPcReject
  528. this.deferredPcResolve = null
  529. this.deferredPcReject = null
  530. this.deferredPcWaitPromise = null
  531. if (error && reject) reject(error)
  532. else if (resolve) resolve()
  533. },
  534. resolveDeferredPcUpload() {
  535. this.settleDeferredPcUpload()
  536. this.pcUploadDeferred = false
  537. },
  538. closePcDialog() {
  539. this.sessionRequestId += 1
  540. this.clearSessionTimer()
  541. this.pcDialogVisible = false
  542. this.session = null
  543. this.remainingSeconds = 0
  544. },
  545. async finishDeferredPcUpload() {
  546. const teamId = this.activeTeamId()
  547. const hasDeferredWaiter = Boolean(this.deferredPcWaitPromise)
  548. this.closePcDialog()
  549. this.pcUploadDeferred = false
  550. try {
  551. if (teamId) await this.refreshFiles(teamId)
  552. this.settleDeferredPcUpload()
  553. } catch(error) {
  554. if (hasDeferredWaiter) this.settleDeferredPcUpload(error)
  555. else this.showError(error, '资料列表刷新失败')
  556. }
  557. },
  558. skipDeferredPcUpload() {
  559. this.closePcDialog()
  560. this.settleDeferredPcUpload()
  561. this.pcUploadDeferred = false
  562. }
  563. }
  564. }
  565. </script>
  566. <style scoped lang="scss">
  567. .background-files {
  568. background: #ffffff;
  569. border-radius: 24rpx;
  570. margin-top: 20rpx;
  571. padding: 28rpx 24rpx;
  572. box-sizing: border-box;
  573. }
  574. .background-files-title, .queue-title, .dialog-title {
  575. font-size: 30rpx;
  576. color: #002846;
  577. line-height: 42rpx;
  578. }
  579. .background-files-count, .file-meta, .dialog-desc {
  580. font-size: 24rpx;
  581. color: #95a5b1;
  582. line-height: 34rpx;
  583. margin-top: 8rpx;
  584. }
  585. .background-files-add {
  586. padding: 18rpx 22rpx;
  587. border-radius: 36rpx;
  588. background: linear-gradient(90deg, #33a7a7 0%, #64bbbb 100%);
  589. font-size: 24rpx;
  590. color: #ffffff;
  591. }
  592. .background-files-pc {
  593. font-size: 26rpx;
  594. color: #199c9c;
  595. line-height: 38rpx;
  596. text-align: right;
  597. margin-top: 20rpx;
  598. }
  599. .background-files-pc.disabled { color: #b3bfc8; }
  600. .file-list, .queue-list {
  601. margin-top: 24rpx;
  602. border-top: 1rpx solid #efefef;
  603. }
  604. .file-row {
  605. padding: 22rpx 0;
  606. border-bottom: 1rpx solid #efefef;
  607. }
  608. .file-main { width: calc(100% - 190rpx); }
  609. .file-name, .queue-name {
  610. font-size: 26rpx;
  611. color: #667e90;
  612. line-height: 36rpx;
  613. word-break: break-all;
  614. }
  615. .file-action {
  616. font-size: 24rpx;
  617. color: #199c9c;
  618. margin-left: 20rpx;
  619. }
  620. .file-action.danger { color: #fd4f66; }
  621. .queue-title { margin-top: 22rpx; }
  622. .queue-row {
  623. padding: 20rpx 0;
  624. border-bottom: 1rpx solid #efefef;
  625. position: relative;
  626. }
  627. .queue-name { width: calc(100% - 130rpx); }
  628. .queue-status { font-size: 24rpx; color: #95a5b1; }
  629. .queue-status.success { color: #199c9c; }
  630. .queue-status.failed, .queue-error { color: #fd4f66; }
  631. .queue-progress {
  632. height: 8rpx;
  633. background: #f0f2f8;
  634. border-radius: 4rpx;
  635. margin-top: 12rpx;
  636. overflow: hidden;
  637. }
  638. .queue-progress-current { height: 100%; background: #4db2b2; }
  639. .queue-error { font-size: 22rpx; line-height: 32rpx; margin-top: 8rpx; }
  640. .queue-retry { font-size: 24rpx; color: #199c9c; margin-top: 10rpx; }
  641. .overlay {
  642. position: fixed;
  643. left: 0;
  644. right: 0;
  645. top: 0;
  646. bottom: 0;
  647. z-index: 1200;
  648. background: rgba(0, 0, 0, .55);
  649. padding: 40rpx;
  650. box-sizing: border-box;
  651. }
  652. .pc-dialog, .text-dialog {
  653. width: 100%;
  654. background: #ffffff;
  655. border-radius: 28rpx;
  656. padding: 40rpx 30rpx;
  657. box-sizing: border-box;
  658. }
  659. .dialog-title { font-weight: bold; text-align: center; }
  660. .dialog-desc, .dialog-expired { text-align: center; margin-top: 24rpx; }
  661. .dialog-expired { font-size: 24rpx; color: #fd4f66; }
  662. .session-expiry { font-size: 22rpx; color: #95a5b1; text-align: center; margin-top: 12rpx; }
  663. .session-code {
  664. font-size: 56rpx;
  665. font-weight: bold;
  666. letter-spacing: 12rpx;
  667. color: #002846;
  668. text-align: center;
  669. margin-top: 30rpx;
  670. }
  671. .session-link {
  672. font-size: 22rpx;
  673. color: #667e90;
  674. line-height: 32rpx;
  675. word-break: break-all;
  676. background: #f7f8fa;
  677. padding: 20rpx;
  678. border-radius: 16rpx;
  679. margin-top: 26rpx;
  680. }
  681. .session-actions { margin-top: 26rpx; }
  682. .session-button {
  683. width: calc(50% - 12rpx);
  684. height: 72rpx;
  685. line-height: 72rpx;
  686. border-radius: 36rpx;
  687. font-size: 26rpx;
  688. text-align: center;
  689. box-sizing: border-box;
  690. }
  691. .session-button.primary { color: #ffffff; background: #33a7a7; }
  692. .session-button.secondary { color: #199c9c; border: 1rpx solid #199c9c; }
  693. .session-button.disabled { color: #b3bfc8; border-color: #b3bfc8; }
  694. .text-dialog { height: 80vh; }
  695. .text-title {
  696. width: 100%;
  697. white-space: nowrap;
  698. overflow: hidden;
  699. text-overflow: ellipsis;
  700. }
  701. .truncated-tip {
  702. font-size: 22rpx;
  703. color: #b06a00;
  704. line-height: 32rpx;
  705. text-align: center;
  706. margin-top: 16rpx;
  707. }
  708. .text-content {
  709. width: 100%;
  710. flex: 1;
  711. background: #f7f8fa;
  712. border-radius: 16rpx;
  713. box-sizing: border-box;
  714. padding: 24rpx;
  715. margin-top: 22rpx;
  716. font-size: 26rpx;
  717. color: #334b5c;
  718. line-height: 40rpx;
  719. white-space: pre-wrap;
  720. word-break: break-all;
  721. }
  722. .text-close {
  723. width: 100%;
  724. height: 76rpx;
  725. line-height: 76rpx;
  726. border-radius: 38rpx;
  727. background: #33a7a7;
  728. color: #ffffff;
  729. font-size: 28rpx;
  730. text-align: center;
  731. margin-top: 24rpx;
  732. }
  733. </style>