index.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744
  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.files = []
  332. this.count = 0
  333. this.filesReady = false
  334. this.$emit('countChange', 0)
  335. let files
  336. try {
  337. files = await listTeamBackgroundFiles(teamId)
  338. } catch (error) {
  339. if (!this.isCurrentContext(teamId, generation)
  340. || requestId !== this.refreshRequestId) return this.files
  341. throw error
  342. }
  343. if (!this.isCurrentContext(teamId, generation)
  344. || requestId !== this.refreshRequestId) return this.files
  345. this.files = Array.isArray(files) ? files : []
  346. this.count = this.files.length
  347. this.filesReady = true
  348. this.hasLoadedFiles = true
  349. this.$emit('countChange', this.count)
  350. return this.files
  351. },
  352. isCurrentFile(file, teamId, generation) {
  353. if (!this.filesReady || !file || !this.isCurrentContext(teamId, generation)) return false
  354. return this.files.some(current => String(current.id) === String(file.id))
  355. },
  356. async previewFile(file) {
  357. const teamId = this.activeTeamId()
  358. const generation = this.contextGeneration
  359. if (!teamId || file.id === undefined || file.id === null
  360. || !this.isCurrentFile(file, teamId, generation)) return
  361. const requestId = ++this.previewRequestId
  362. this.previewingId = file.id
  363. try {
  364. const result = await downloadTeamBackgroundFile(teamId, file.id)
  365. if (requestId !== this.previewRequestId
  366. || !this.isCurrentFile(file, teamId, generation)) return
  367. if (result && result.mode === 'text') {
  368. this.textPreview = createTextPreviewState(result, file.fileName || file.name)
  369. } else if (result && result.mode === 'document') {
  370. await this.openDocumentPreview(result)
  371. }
  372. } catch (error) {
  373. if (requestId === this.previewRequestId) this.showError(error, '文件打开失败')
  374. } finally {
  375. if (requestId === this.previewRequestId) this.previewingId = ''
  376. }
  377. },
  378. openDocumentPreview(result) {
  379. return new Promise((resolve, reject) => {
  380. if (typeof wx === 'undefined' || typeof wx.openDocument !== 'function') {
  381. reject(new Error('当前环境不支持文档预览'))
  382. return
  383. }
  384. wx.openDocument({
  385. filePath: result.filePath,
  386. showMenu: true,
  387. success: resolve,
  388. fail: reject
  389. })
  390. })
  391. },
  392. closeTextPreview() {
  393. this.previewRequestId += 1
  394. this.previewingId = ''
  395. this.textPreview = createEmptyTextPreviewState()
  396. },
  397. removeFile(file) {
  398. const teamId = this.activeTeamId()
  399. const generation = this.contextGeneration
  400. if (!teamId || file.id === undefined || file.id === null
  401. || !this.isCurrentFile(file, teamId, generation)) return
  402. uni.showModal({
  403. title: '停用资料',
  404. content: `确定停用“${file.fileName || file.name || '该文件'}”吗?`,
  405. confirmColor: '#199C9C',
  406. success: async result => {
  407. if (!result.confirm || !this.isCurrentFile(file, teamId, generation)) return
  408. try {
  409. await disableTeamBackgroundFile(teamId, file.id)
  410. if (!this.isCurrentContext(teamId, generation)) return
  411. await this.refreshFiles(teamId)
  412. if (!this.componentDestroyed) this.$showToast('已停用')
  413. } catch (error) {
  414. this.showError(error, '停用失败,请重试')
  415. }
  416. }
  417. })
  418. },
  419. usePcUpload() {
  420. const teamId = this.activeTeamId()
  421. if (!teamId) {
  422. this.pcUploadDeferred = true
  423. this.$showToast('将先保存团队,再生成电脑上传方式')
  424. return
  425. }
  426. if (this.sessionCreating) return
  427. this.openPcSession(teamId).catch(error => this.showError(error, '上传会话创建失败'))
  428. },
  429. sessionTeamKey(teamId) {
  430. return String(teamId || '')
  431. },
  432. updateSessionCreating() {
  433. if (this.componentDestroyed) {
  434. this.sessionCreating = false
  435. return
  436. }
  437. const teamId = this.activeTeamId()
  438. this.sessionCreating = Boolean(teamId
  439. && this.sessionRequestsByTeam[this.sessionTeamKey(teamId)])
  440. },
  441. getSessionRequest(teamId) {
  442. const key = this.sessionTeamKey(teamId)
  443. const existing = this.sessionRequestsByTeam[key]
  444. if (existing) return existing
  445. const request = createTeamBackgroundSession(teamId)
  446. this.sessionRequestsByTeam[key] = request
  447. this.updateSessionCreating()
  448. const clearRequest = () => {
  449. if (this.sessionRequestsByTeam[key] === request) {
  450. delete this.sessionRequestsByTeam[key]
  451. this.updateSessionCreating()
  452. }
  453. }
  454. request.then(clearRequest, clearRequest)
  455. return request
  456. },
  457. async openPcSession(teamId) {
  458. if (!teamId || !this.sameTeamId(teamId, this.activeTeamId())) return null
  459. const generation = this.contextGeneration
  460. const requestId = this.sessionRequestId
  461. let session
  462. try {
  463. session = await this.getSessionRequest(teamId)
  464. } catch (error) {
  465. if (!this.isCurrentContext(teamId, generation)
  466. || requestId !== this.sessionRequestId) return null
  467. throw error
  468. }
  469. if (!this.isCurrentContext(teamId, generation)
  470. || requestId !== this.sessionRequestId) return null
  471. this.clearSessionTimer()
  472. this.boundTeamId = teamId
  473. this.session = session
  474. this.pcDialogVisible = true
  475. this.startSessionTimer()
  476. return session
  477. },
  478. startSessionTimer() {
  479. this.clearSessionTimer()
  480. this.updateRemainingSeconds()
  481. if (this.remainingSeconds === 0) return
  482. this.sessionTimer = setInterval(() => this.updateRemainingSeconds(), 1000)
  483. },
  484. updateRemainingSeconds() {
  485. this.remainingSeconds = getSessionRemainingSeconds(this.session && this.session.expiresAt)
  486. if (this.remainingSeconds === 0) this.clearSessionTimer()
  487. },
  488. clearSessionTimer() {
  489. if (this.sessionTimer) clearInterval(this.sessionTimer)
  490. this.sessionTimer = null
  491. },
  492. copyUploadUrl() {
  493. if (this.remainingSeconds === 0 || !this.session || !this.session.uploadUrl) return
  494. uni.setClipboardData({
  495. data: this.session.uploadUrl,
  496. success: () => this.$showToast('链接已复制'),
  497. fail: error => this.showError(error, '复制失败')
  498. })
  499. },
  500. refreshPcSession() {
  501. const teamId = this.activeTeamId()
  502. if (!teamId || this.sessionCreating) return
  503. this.openPcSession(teamId).catch(error => this.showError(error, '上传会话创建失败'))
  504. },
  505. hasDeferredPcUpload() {
  506. return this.pcUploadDeferred
  507. },
  508. waitForDeferredPcUpload(teamId) {
  509. if (this.deferredPcWaitPromise) return this.deferredPcWaitPromise
  510. const waiter = new Promise((resolve, reject) => {
  511. this.deferredPcResolve = resolve
  512. this.deferredPcReject = reject
  513. })
  514. this.deferredPcWaitPromise = waiter
  515. this.openPcSession(teamId).then(session => {
  516. if (this.deferredPcWaitPromise !== waiter) return
  517. if (!session || this.componentDestroyed) {
  518. this.settleDeferredPcUpload(new Error('上传会话已失效'))
  519. return
  520. }
  521. this.pcUploadDeferred = false
  522. }, error => {
  523. if (this.deferredPcWaitPromise === waiter) this.settleDeferredPcUpload(error)
  524. })
  525. return waiter
  526. },
  527. settleDeferredPcUpload(error) {
  528. const resolve = this.deferredPcResolve
  529. const reject = this.deferredPcReject
  530. this.deferredPcResolve = null
  531. this.deferredPcReject = null
  532. this.deferredPcWaitPromise = null
  533. if (error && reject) reject(error)
  534. else if (resolve) resolve()
  535. },
  536. resolveDeferredPcUpload() {
  537. this.settleDeferredPcUpload()
  538. this.pcUploadDeferred = false
  539. },
  540. closePcDialog() {
  541. this.sessionRequestId += 1
  542. this.clearSessionTimer()
  543. this.pcDialogVisible = false
  544. this.session = null
  545. this.remainingSeconds = 0
  546. },
  547. async finishDeferredPcUpload() {
  548. const teamId = this.activeTeamId()
  549. const hasDeferredWaiter = Boolean(this.deferredPcWaitPromise)
  550. this.closePcDialog()
  551. this.pcUploadDeferred = false
  552. try {
  553. if (teamId) await this.refreshFiles(teamId)
  554. this.settleDeferredPcUpload()
  555. } catch(error) {
  556. if (hasDeferredWaiter) this.settleDeferredPcUpload(error)
  557. else this.showError(error, '资料列表刷新失败')
  558. }
  559. },
  560. skipDeferredPcUpload() {
  561. this.closePcDialog()
  562. this.settleDeferredPcUpload()
  563. this.pcUploadDeferred = false
  564. }
  565. }
  566. }
  567. </script>
  568. <style scoped lang="scss">
  569. .background-files {
  570. background: #ffffff;
  571. border-radius: 24rpx;
  572. margin-top: 20rpx;
  573. padding: 28rpx 24rpx;
  574. box-sizing: border-box;
  575. }
  576. .background-files-title, .queue-title, .dialog-title {
  577. font-size: 30rpx;
  578. color: #002846;
  579. line-height: 42rpx;
  580. }
  581. .background-files-count, .file-meta, .dialog-desc {
  582. font-size: 24rpx;
  583. color: #95a5b1;
  584. line-height: 34rpx;
  585. margin-top: 8rpx;
  586. }
  587. .background-files-add {
  588. padding: 18rpx 22rpx;
  589. border-radius: 36rpx;
  590. background: linear-gradient(90deg, #33a7a7 0%, #64bbbb 100%);
  591. font-size: 24rpx;
  592. color: #ffffff;
  593. }
  594. .background-files-pc {
  595. font-size: 26rpx;
  596. color: #199c9c;
  597. line-height: 38rpx;
  598. text-align: right;
  599. margin-top: 20rpx;
  600. }
  601. .background-files-pc.disabled { color: #b3bfc8; }
  602. .file-list, .queue-list {
  603. margin-top: 24rpx;
  604. border-top: 1rpx solid #efefef;
  605. }
  606. .file-row {
  607. padding: 22rpx 0;
  608. border-bottom: 1rpx solid #efefef;
  609. }
  610. .file-main { width: calc(100% - 190rpx); }
  611. .file-name, .queue-name {
  612. font-size: 26rpx;
  613. color: #667e90;
  614. line-height: 36rpx;
  615. word-break: break-all;
  616. }
  617. .file-action {
  618. font-size: 24rpx;
  619. color: #199c9c;
  620. margin-left: 20rpx;
  621. }
  622. .file-action.danger { color: #fd4f66; }
  623. .queue-title { margin-top: 22rpx; }
  624. .queue-row {
  625. padding: 20rpx 0;
  626. border-bottom: 1rpx solid #efefef;
  627. position: relative;
  628. }
  629. .queue-name { width: calc(100% - 130rpx); }
  630. .queue-status { font-size: 24rpx; color: #95a5b1; }
  631. .queue-status.success { color: #199c9c; }
  632. .queue-status.failed, .queue-error { color: #fd4f66; }
  633. .queue-progress {
  634. height: 8rpx;
  635. background: #f0f2f8;
  636. border-radius: 4rpx;
  637. margin-top: 12rpx;
  638. overflow: hidden;
  639. }
  640. .queue-progress-current { height: 100%; background: #4db2b2; }
  641. .queue-error { font-size: 22rpx; line-height: 32rpx; margin-top: 8rpx; }
  642. .queue-retry { font-size: 24rpx; color: #199c9c; margin-top: 10rpx; }
  643. .overlay {
  644. position: fixed;
  645. left: 0;
  646. right: 0;
  647. top: 0;
  648. bottom: 0;
  649. z-index: 1200;
  650. background: rgba(0, 0, 0, .55);
  651. padding: 40rpx;
  652. box-sizing: border-box;
  653. }
  654. .pc-dialog, .text-dialog {
  655. width: 100%;
  656. background: #ffffff;
  657. border-radius: 28rpx;
  658. padding: 40rpx 30rpx;
  659. box-sizing: border-box;
  660. }
  661. .dialog-title { font-weight: bold; text-align: center; }
  662. .dialog-desc, .dialog-expired { text-align: center; margin-top: 24rpx; }
  663. .dialog-expired { font-size: 24rpx; color: #fd4f66; }
  664. .session-expiry { font-size: 22rpx; color: #95a5b1; text-align: center; margin-top: 12rpx; }
  665. .session-code {
  666. font-size: 56rpx;
  667. font-weight: bold;
  668. letter-spacing: 12rpx;
  669. color: #002846;
  670. text-align: center;
  671. margin-top: 30rpx;
  672. }
  673. .session-link {
  674. font-size: 22rpx;
  675. color: #667e90;
  676. line-height: 32rpx;
  677. word-break: break-all;
  678. background: #f7f8fa;
  679. padding: 20rpx;
  680. border-radius: 16rpx;
  681. margin-top: 26rpx;
  682. }
  683. .session-actions { margin-top: 26rpx; }
  684. .session-button {
  685. width: calc(50% - 12rpx);
  686. height: 72rpx;
  687. line-height: 72rpx;
  688. border-radius: 36rpx;
  689. font-size: 26rpx;
  690. text-align: center;
  691. box-sizing: border-box;
  692. }
  693. .session-button.primary { color: #ffffff; background: #33a7a7; }
  694. .session-button.secondary { color: #199c9c; border: 1rpx solid #199c9c; }
  695. .session-button.disabled { color: #b3bfc8; border-color: #b3bfc8; }
  696. .text-dialog { height: 80vh; }
  697. .text-title {
  698. width: 100%;
  699. white-space: nowrap;
  700. overflow: hidden;
  701. text-overflow: ellipsis;
  702. }
  703. .truncated-tip {
  704. font-size: 22rpx;
  705. color: #b06a00;
  706. line-height: 32rpx;
  707. text-align: center;
  708. margin-top: 16rpx;
  709. }
  710. .text-content {
  711. width: 100%;
  712. flex: 1;
  713. background: #f7f8fa;
  714. border-radius: 16rpx;
  715. box-sizing: border-box;
  716. padding: 24rpx;
  717. margin-top: 22rpx;
  718. font-size: 26rpx;
  719. color: #334b5c;
  720. line-height: 40rpx;
  721. white-space: pre-wrap;
  722. word-break: break-all;
  723. }
  724. .text-close {
  725. width: 100%;
  726. height: 76rpx;
  727. line-height: 76rpx;
  728. border-radius: 38rpx;
  729. background: #33a7a7;
  730. color: #ffffff;
  731. font-size: 28rpx;
  732. text-align: center;
  733. margin-top: 24rpx;
  734. }
  735. </style>