index.vue 22 KB

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