@sugarat/easypicker2-client 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/.env +6 -0
  2. package/.env.production +3 -0
  3. package/.env.test +4 -0
  4. package/.eslintignore +0 -0
  5. package/.eslintrc.json +57 -0
  6. package/.github/workflows/main.yml +61 -0
  7. package/.prettierrc.js +9 -0
  8. package/LICENSE +21 -0
  9. package/README.md +86 -0
  10. package/auto-imports.d.ts +6 -0
  11. package/components.d.ts +56 -0
  12. package/docker/ep_backup/easypicker2.sql +214 -0
  13. package/docker/ep_backup/mongodb/easypicker2/action.bson +0 -0
  14. package/docker/ep_backup/mongodb/easypicker2/action.metadata.json +1 -0
  15. package/docker/ep_backup/mongodb/easypicker2/log.bson +0 -0
  16. package/docker/ep_backup/mongodb/easypicker2/log.metadata.json +1 -0
  17. package/docker/ep_backup/user-config.json +176 -0
  18. package/docs/.env +1 -0
  19. package/docs/.env.production +2 -0
  20. package/docs/.vitepress/config.ts +204 -0
  21. package/docs/.vitepress/theme/bg.png +0 -0
  22. package/docs/.vitepress/theme/index.scss +41 -0
  23. package/docs/.vitepress/theme/index.ts +5 -0
  24. package/docs/author.md +24 -0
  25. package/docs/auto-imports.d.ts +6 -0
  26. package/docs/components.d.ts +17 -0
  27. package/docs/deploy/design/api.md +3 -0
  28. package/docs/deploy/design/db.md +3 -0
  29. package/docs/deploy/design/index.md +3 -0
  30. package/docs/deploy/design/shell.md +9 -0
  31. package/docs/deploy/faq.md +86 -0
  32. package/docs/deploy/index.md +9 -0
  33. package/docs/deploy/local.md +275 -0
  34. package/docs/deploy/online-new.md +610 -0
  35. package/docs/deploy/online.md +683 -0
  36. package/docs/deploy/qiniu.md +183 -0
  37. package/docs/index.md +40 -0
  38. package/docs/introduction/about/code.md +26 -0
  39. package/docs/introduction/about/index.md +33 -0
  40. package/docs/introduction/feature/index.md +3 -0
  41. package/docs/plan/log.md +333 -0
  42. package/docs/plan/todo.md +127 -0
  43. package/docs/plan/wish.md +29 -0
  44. package/docs/praise/index.md +45 -0
  45. package/docs/public/favicon.ico +0 -0
  46. package/docs/public/logo.png +0 -0
  47. package/docs/public/robots.txt +2 -0
  48. package/docs/src/apis/ajax.ts +66 -0
  49. package/docs/src/apis/index.ts +1 -0
  50. package/docs/src/apis/modules/wish.ts +20 -0
  51. package/docs/src/components/Avatar.vue +60 -0
  52. package/docs/src/components/Home.vue +85 -0
  53. package/docs/src/components/Picture.vue +13 -0
  54. package/docs/src/components/Praise.vue +52 -0
  55. package/docs/src/components/WishBtn.vue +98 -0
  56. package/docs/src/components/WishPanel.vue +170 -0
  57. package/docs/src/components/callme/index.vue +72 -0
  58. package/docs/vite.config.ts +42 -0
  59. package/index.html +127 -0
  60. package/package.json +52 -0
  61. package/public/favicon.ico +0 -0
  62. package/public/logo.png +0 -0
  63. package/scripts/deploy/docs.mjs +24 -0
  64. package/scripts/deploy/prod.mjs +24 -0
  65. package/scripts/deploy/test.mjs +26 -0
  66. package/src/@types/ajax.d.ts +5 -0
  67. package/src/@types/api.d.ts +305 -0
  68. package/src/@types/lib.d.ts +26 -0
  69. package/src/@types/page.d.ts +18 -0
  70. package/src/App.vue +36 -0
  71. package/src/apis/ajax.ts +70 -0
  72. package/src/apis/index.ts +20 -0
  73. package/src/apis/modules/action.ts +17 -0
  74. package/src/apis/modules/category.ts +20 -0
  75. package/src/apis/modules/config.ts +19 -0
  76. package/src/apis/modules/file.ts +150 -0
  77. package/src/apis/modules/people.ts +81 -0
  78. package/src/apis/modules/public.ts +49 -0
  79. package/src/apis/modules/super/overview.ts +56 -0
  80. package/src/apis/modules/super/user.ts +62 -0
  81. package/src/apis/modules/task.ts +67 -0
  82. package/src/apis/modules/user.ts +56 -0
  83. package/src/apis/modules/wish.ts +31 -0
  84. package/src/assets/i/EasyPicker.png +0 -0
  85. package/src/assets/logo.png +0 -0
  86. package/src/assets/styles/app.css +69 -0
  87. package/src/components/HomeFooter/index.vue +134 -0
  88. package/src/components/HomeHeader/index.vue +156 -0
  89. package/src/components/InfosForm/index.vue +73 -0
  90. package/src/components/MessageList/index.vue +155 -0
  91. package/src/components/MessagePanel/index.vue +42 -0
  92. package/src/components/Praise/index.vue +102 -0
  93. package/src/components/QrCode.vue +44 -0
  94. package/src/components/linkDialog.vue +104 -0
  95. package/src/components/loginPanel.vue +92 -0
  96. package/src/constants/index.ts +83 -0
  97. package/src/env.d.ts +8 -0
  98. package/src/main.ts +19 -0
  99. package/src/pages/404/index.vue +59 -0
  100. package/src/pages/about/index.vue +152 -0
  101. package/src/pages/callme/index.vue +155 -0
  102. package/src/pages/dashboard/config/index.vue +264 -0
  103. package/src/pages/dashboard/files/index.vue +1152 -0
  104. package/src/pages/dashboard/index.vue +335 -0
  105. package/src/pages/dashboard/manage/config/index.vue +97 -0
  106. package/src/pages/dashboard/manage/index.vue +105 -0
  107. package/src/pages/dashboard/manage/overview/index.vue +488 -0
  108. package/src/pages/dashboard/manage/user/index.vue +679 -0
  109. package/src/pages/dashboard/manage/wish/index.vue +257 -0
  110. package/src/pages/dashboard/tasks/components/CategoryPanel.vue +208 -0
  111. package/src/pages/dashboard/tasks/components/CreateTask.vue +93 -0
  112. package/src/pages/dashboard/tasks/components/TaskInfo.vue +129 -0
  113. package/src/pages/dashboard/tasks/components/infoPanel/ddl.vue +96 -0
  114. package/src/pages/dashboard/tasks/components/infoPanel/file.vue +175 -0
  115. package/src/pages/dashboard/tasks/components/infoPanel/info.vue +477 -0
  116. package/src/pages/dashboard/tasks/components/infoPanel/people.vue +567 -0
  117. package/src/pages/dashboard/tasks/components/infoPanel/template.vue +146 -0
  118. package/src/pages/dashboard/tasks/components/infoPanel/tip.vue +55 -0
  119. package/src/pages/dashboard/tasks/components/infoPanel/tipInfo.vue +196 -0
  120. package/src/pages/dashboard/tasks/index.vue +302 -0
  121. package/src/pages/dashboard/tasks/public.ts +32 -0
  122. package/src/pages/disabled/index.vue +47 -0
  123. package/src/pages/feedback/index.vue +5 -0
  124. package/src/pages/home/index.vue +72 -0
  125. package/src/pages/login/index.vue +270 -0
  126. package/src/pages/register/index.vue +211 -0
  127. package/src/pages/reset/index.vue +186 -0
  128. package/src/pages/task/index.vue +897 -0
  129. package/src/pages/wish/index.vue +152 -0
  130. package/src/router/Interceptor/index.ts +112 -0
  131. package/src/router/index.ts +13 -0
  132. package/src/router/routes/index.ts +197 -0
  133. package/src/shims-vue.d.ts +6 -0
  134. package/src/store/index.ts +17 -0
  135. package/src/store/modules/category.ts +44 -0
  136. package/src/store/modules/public.ts +27 -0
  137. package/src/store/modules/task.ts +55 -0
  138. package/src/store/modules/user.ts +57 -0
  139. package/src/utils/elementUI.ts +8 -0
  140. package/src/utils/networkUtil.ts +236 -0
  141. package/src/utils/other.ts +25 -0
  142. package/src/utils/regExp.ts +11 -0
  143. package/src/utils/stringUtil.ts +242 -0
  144. package/tsconfig.json +24 -0
  145. package/vite.config.ts +55 -0
@@ -0,0 +1,897 @@
1
+ <template>
2
+ <div class="task-panel">
3
+ <div class="pc-nav">
4
+ <div class="nav">
5
+ <!-- LOGO -->
6
+ <div class="logo">
7
+ <router-link to="/">
8
+ <img
9
+ style="height: 40px; width: 170px"
10
+ src="https://img.cdn.sugarat.top/easypicker/EasyPicker.png"
11
+ alt="logo"
12
+ />
13
+ </router-link>
14
+ </div>
15
+ <nav>
16
+ <div
17
+ class="nav-item"
18
+ v-for="(n, idx) in pcNavs"
19
+ :key="idx"
20
+ @click="handleNav(idx)"
21
+ >
22
+ {{ n.title }}
23
+ </div>
24
+ <!-- TODO:重新加导航内容 -->
25
+ <!-- 底部导航栏 -->
26
+ </nav>
27
+ </div>
28
+ </div>
29
+ <!-- 有效 -->
30
+ <div
31
+ v-loading="isLoadingData"
32
+ element-loading-text="Loading..."
33
+ class="panel tc"
34
+ v-if="k"
35
+ >
36
+ <!-- 任务名 -->
37
+ <h1 class="name">
38
+ {{ taskInfo.name }}
39
+ </h1>
40
+ <!-- 提示信息 -->
41
+ <!-- 时间截止了也不再展示 -->
42
+ <template v-if="tipData.text && (ddlStr ? !isOver : true)">
43
+ <el-divider>⚠️ 注意事项 ⚠️</el-divider>
44
+ <Tip>
45
+ <div class="tip-wrapper">
46
+ <p v-for="(t, i) in tipData.text.split('\n')" :key="i">
47
+ {{ t.replace(/\s/g, '&nbsp;') }}
48
+ </p>
49
+ </div>
50
+ </Tip>
51
+ </template>
52
+ <template v-if="imageList.length && (ddlStr ? !isOver : true)">
53
+ <el-image
54
+ hide-on-click-modal
55
+ v-for="(img, idx) in imageList"
56
+ :key="img.uid"
57
+ style="width: 100px; height: 100px; margin: 10px"
58
+ :src="img.url"
59
+ :zoom-rate="1.2"
60
+ :preview-src-list="imageList.map((v) => v.preview)"
61
+ :initial-index="idx"
62
+ fit="contain"
63
+ />
64
+ </template>
65
+ <!-- 截止时间字符串 -->
66
+ <template v-if="ddlStr">
67
+ <el-divider>截止时间</el-divider>
68
+ <h2 class="ddl">
69
+ {{ timeInfo }}
70
+ </h2>
71
+ <div v-if="isOver">
72
+ <el-empty description="已经结束啦!"> </el-empty>
73
+ </div>
74
+ </template>
75
+ <!-- 未设置ddl 或者 设置了还未结束 -->
76
+ <div v-if="!ddlStr || !isOver">
77
+ <el-divider>必要信息填写</el-divider>
78
+ <div class="infos">
79
+ <div v-show="taskMoreInfo.people">
80
+ <Tip>“姓名”在参与名单里才能正常提交</Tip>
81
+ </div>
82
+ <div v-if="showValidForm">
83
+ <div class="infos">
84
+ <el-form
85
+ ref="validModalRef"
86
+ :rules="validModalRules"
87
+ status-icon
88
+ :model="validModal"
89
+ :disabled="disableForm"
90
+ label-position="top"
91
+ >
92
+ <el-form-item prop="peopleName" label="姓名">
93
+ <el-input
94
+ :maxlength="14"
95
+ clearable
96
+ show-word-limit
97
+ placeholder="参与者填写"
98
+ v-model="validModal.peopleName"
99
+ ></el-input>
100
+ </el-form-item>
101
+ </el-form>
102
+ </div>
103
+ </div>
104
+ <InfosForm :infos="infos" :disabled="disableForm"></InfosForm>
105
+ </div>
106
+ <el-upload
107
+ style="max-width: 400px; margin: 0 auto"
108
+ :drag="!isMobile"
109
+ action=""
110
+ ref="fileUpload"
111
+ :on-change="handleChangeFile"
112
+ :before-remove="handleRemoveFile"
113
+ :on-exceed="handleExceed"
114
+ :auto-upload="false"
115
+ multiple
116
+ :limit="limitUploadCount"
117
+ v-model:file-list="fileList"
118
+ >
119
+ <el-button v-if="isMobile" type="primary">选择文件</el-button>
120
+ <template v-else>
121
+ <el-icon class="el-icon--upload">
122
+ <upload-filled />
123
+ </el-icon>
124
+ <div class="el-upload__text">
125
+ 将文件拖于此处 or <em>直接选择文件</em>
126
+ </div>
127
+ </template>
128
+ <template #tip>
129
+ <div class="p10" v-show="!!calculateMd5Count">
130
+ <tip
131
+ >还有
132
+ {{ calculateMd5Count }}
133
+ 个文件正在生成校验信息,请稍等(1G通常需要20s)</tip
134
+ >
135
+ </div>
136
+ </template>
137
+ </el-upload>
138
+ <div class="p10">
139
+ <el-button
140
+ v-if="isWithdraw"
141
+ size="default"
142
+ @click="startWithdraw"
143
+ type="warning"
144
+ :disabled="!allowWithdraw || !!calculateMd5Count"
145
+ >一键撤回</el-button
146
+ >
147
+ <el-button
148
+ v-else
149
+ size="default"
150
+ @click="submitUpload"
151
+ type="success"
152
+ :disabled="!allowUpload || !!calculateMd5Count"
153
+ >提交文件</el-button
154
+ >
155
+ <el-button @click="checkSubmitStatus" size="default"
156
+ >查询提交情况</el-button
157
+ >
158
+ </div>
159
+ <!-- 提示信息 -->
160
+ <div class="p10 option-tips">
161
+ <tip v-if="formatData.status && formatData.format.length"
162
+ >限制格式为:
163
+ <span style="color: red">{{
164
+ formatData.format.join(', ')
165
+ }}</span></tip
166
+ >
167
+ <tip v-if="formatData.size"
168
+ >限制文件大小不超过:
169
+ <span style="color: red">{{
170
+ formatSize(formatData.size)
171
+ }}</span></tip
172
+ >
173
+ <template v-if="isWithdraw">
174
+ <tip
175
+ >① 须保证选择的文件与提交时的文件一致<br />
176
+ ② 填写表单信息一致 <br />
177
+
178
+ 完全一模一样的文件的提交记录(内容md5+命名),将会一次性全部撤回</tip
179
+ >
180
+ </template>
181
+ <template v-else>
182
+ <tip
183
+ >① 选择完文件,点击 ”提交文件“即可 <br />
184
+ ② <strong>选择大文件后需要等待一会儿才展示处理</strong>
185
+ <template v-if="taskMoreInfo.template"
186
+ ><br />
187
+
188
+ <strong>
189
+ <el-button
190
+ type="primary"
191
+ text
192
+ style="color: #85ce61"
193
+ size="small"
194
+ @click="downloadTemplate"
195
+ >右下角可 “查看提交示例”
196
+ </el-button>
197
+ </strong></template
198
+ >
199
+ </tip>
200
+ </template>
201
+ </div>
202
+ <div class="withdraw">
203
+ <el-button
204
+ type="primary"
205
+ text
206
+ style="color: #85ce61"
207
+ v-if="taskMoreInfo.template"
208
+ size="small"
209
+ @click="downloadTemplate"
210
+ >查看提交示例</el-button
211
+ >
212
+ <el-button
213
+ v-if="isWithdraw"
214
+ @click="isWithdraw = false"
215
+ size="small"
216
+ type="primary"
217
+ text
218
+ >正常提交</el-button
219
+ >
220
+ <el-button
221
+ v-else
222
+ size="small"
223
+ @click="isWithdraw = true"
224
+ type="primary"
225
+ text
226
+ >我要撤回</el-button
227
+ >
228
+ </div>
229
+ </div>
230
+ </div>
231
+ <!-- 无效任务 -->
232
+ <div class="panel tc" v-else>
233
+ <h1 class="name">
234
+ {{ taskInfo.name }}
235
+ </h1>
236
+ </div>
237
+ <LinkDialog
238
+ v-model:value="showLinkModel"
239
+ title="示例文件下载链接"
240
+ :link="templateLink"
241
+ ></LinkDialog>
242
+ <div style="padding-top: 20px">
243
+ <home-footer type="task"></home-footer>
244
+ </div>
245
+ </div>
246
+ </template>
247
+ <script lang="ts" setup>
248
+ import { ElMessage, ElMessageBox } from 'element-plus'
249
+ import type { UploadUserFile, UploadInstance, FormInstance } from 'element-plus'
250
+ import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
251
+ import { useRoute, useRouter } from 'vue-router'
252
+ import HomeFooter from '@components/HomeFooter/index.vue'
253
+ import LinkDialog from '@components/linkDialog.vue'
254
+ import { UploadFilled } from '@element-plus/icons-vue'
255
+ import { useStore } from 'vuex'
256
+ import {
257
+ formatDate,
258
+ formatSize,
259
+ getFileMd5Hash,
260
+ getFileSuffix,
261
+ normalizeFileName,
262
+ parseFileFormat,
263
+ parseInfo
264
+ } from '@/utils/stringUtil'
265
+ import { downLoadByUrl, qiniuUpload } from '@/utils/networkUtil'
266
+ import { FileApi, PeopleApi, PublicApi, TaskApi } from '@/apis'
267
+ import Tip from '../dashboard/tasks/components/infoPanel/tip.vue'
268
+ import InfosForm from '@/components/InfosForm/index.vue'
269
+
270
+ const $store = useStore()
271
+ const isMobile = computed(() => $store.getters['public/isMobile'])
272
+ // 顶部导航
273
+ const $router = useRouter()
274
+ const $route = useRoute()
275
+ const pcNavs = reactive([
276
+ {
277
+ title: '我也要收集',
278
+ path: 'https://docs.ep.sugarat.top/'
279
+ }
280
+ ])
281
+ const handleNav = (idx: number) => {
282
+ if (pcNavs[idx].path.startsWith('http')) {
283
+ window.location.href = pcNavs[idx].path
284
+ return
285
+ }
286
+ $router.push({
287
+ path: pcNavs[idx].path
288
+ })
289
+ }
290
+
291
+ // 任务基本信息展示
292
+ const taskInfo = reactive<TaskApiTypes.TaskInfo>({ name: '', category: '' })
293
+ const taskMoreInfo = reactive<Partial<TaskApiTypes.TaskInfo>>({})
294
+ const formatData = computed(() => parseFileFormat(taskMoreInfo.format))
295
+ const k = ref('')
296
+
297
+ // 用于展示截止日期
298
+ const waitTime = ref(0)
299
+ // 判断是否结束
300
+ const isOver = computed(() => waitTime.value <= 0)
301
+ const waitTimeStr = computed(() => {
302
+ let seconds = ~~(waitTime.value / 1000)
303
+ let hour = ~~(seconds / (60 * 60))
304
+ const day = ~~(hour / 24)
305
+ hour %= 24
306
+ const minute = ~~((seconds % 3600) / 60)
307
+ seconds %= 60
308
+ return `剩余${day}天${hour}时${minute}分${seconds}秒`
309
+ })
310
+ const refreshWaitTime = (loop = true) => {
311
+ if (taskMoreInfo?.ddl) {
312
+ const date = new Date(taskMoreInfo.ddl)
313
+ waitTime.value = date.getTime() - Date.now()
314
+ } else {
315
+ waitTime.value = 0
316
+ }
317
+ if (loop) {
318
+ setTimeout(() => {
319
+ refreshWaitTime()
320
+ }, 1000)
321
+ }
322
+ }
323
+ const ddlStr = computed(() => {
324
+ if (taskMoreInfo?.ddl) {
325
+ const date = new Date(taskMoreInfo.ddl)
326
+ return formatDate(date)
327
+ }
328
+ return ''
329
+ })
330
+
331
+ // 必填信息
332
+ const infos = reactive<InfoItem[]>([])
333
+
334
+ // 文件上传部分
335
+
336
+ // 文件上传
337
+ const fileList = ref<(UploadUserFile & { md5: string; subscription: any })[]>(
338
+ []
339
+ )
340
+ const fileUpload = ref<UploadInstance>()
341
+ const disableForm = computed(
342
+ () => fileList.value.filter((item) => item.status === 'uploading').length > 0
343
+ )
344
+ const handleRemoveFile: any = (file: any) => {
345
+ if (file.status === 'uploading' || file.status === 'success') {
346
+ return ElMessageBox.confirm(
347
+ '不影响已经上传成功的,正在上传的将取消上传',
348
+ '确定从列表移除文件吗?'
349
+ )
350
+ .then(() => {
351
+ if (file.status === 'uploading') {
352
+ ElMessage.info(`取消${file.name}的上传`)
353
+ // 取消上传
354
+ file.subscription.unsubscribe() // 取消上传
355
+ }
356
+ return true
357
+ })
358
+ .catch(() => false)
359
+ }
360
+ return true
361
+ }
362
+
363
+ // 校验表单填写
364
+ const isWriteFinish = computed(() => infos.every((item) => item.value))
365
+ // 提交文件
366
+
367
+ // 身份核验表单
368
+ const isSameFieldName = computed(() => infos.find((v) => v.text === '姓名'))
369
+ const showValidForm = computed(
370
+ () => taskMoreInfo.people && !isSameFieldName.value
371
+ )
372
+ const validModal = reactive({
373
+ peopleName: ''
374
+ })
375
+
376
+ const validatePeopleName = (rule: any, value: any, callback: any) => {
377
+ if (!value) {
378
+ callback(new Error('请输入姓名'))
379
+ ElMessage.error('请输入姓名')
380
+ return
381
+ }
382
+ // 异步校验
383
+ PeopleApi.checkPeopleIsExist(k.value, value).then((res) => {
384
+ if (!res.data.exist) {
385
+ ElMessage.error('你不在此次提交名单中,如有疑问请联系管理员')
386
+ }
387
+ callback(
388
+ res.data.exist
389
+ ? undefined
390
+ : new Error('你不在此次提交名单中,如有疑问请联系管理员')
391
+ )
392
+ })
393
+ }
394
+
395
+ const validModalRef = ref<FormInstance>()
396
+ const validModalRules = reactive({
397
+ peopleName: [{ validator: validatePeopleName, trigger: 'blur' }]
398
+ })
399
+ const confirmPeopleName = () => {
400
+ // 处理表单必填项含有姓名的情况
401
+ if (isSameFieldName.value) {
402
+ const value = infos.find((v) => v.text === '姓名')?.value
403
+ validModal.peopleName = value || ''
404
+ return new Promise((resolve) => {
405
+ validatePeopleName(null, value, resolve)
406
+ }).then((v) => !v)
407
+ }
408
+ return validModalRef.value.validate((isValid: boolean) => isValid)
409
+ }
410
+
411
+ const startUpload = () => {
412
+ const uploadFiles = fileList.value
413
+ for (const file of uploadFiles) {
414
+ if (!file.md5) {
415
+ ElMessage.info(
416
+ `文件(${file.name})的唯一指纹还在计算中,再等待一会儿再点击上传`
417
+ )
418
+ setTimeout(() => {
419
+ ElMessage.info('文件越大计算时间越长(1G通常需要20s)')
420
+ }, 100)
421
+ } else if (file.status === 'ready') {
422
+ // 开始上传
423
+ file.status = 'uploading'
424
+ let { name } = file
425
+ const originName = name
426
+ // 如果开启了自动重命名,这里重命名一下
427
+ if (taskMoreInfo.rewrite) {
428
+ name =
429
+ infos.map((v) => v.value).join(formatData.value.splitChar || '-') +
430
+ getFileSuffix(name)
431
+ }
432
+ // 替换不合法的字符
433
+ name = normalizeFileName(name)
434
+ const key = `easypicker2/${k.value}/${file.md5}/${name}`
435
+
436
+ FileApi.getUploadToken().then((res) => {
437
+ qiniuUpload(res.data.token, file.raw, key, {
438
+ success(data: any) {
439
+ const { fsize } = data
440
+ FileApi.addFile({
441
+ originName,
442
+ name,
443
+ taskKey: k.value,
444
+ taskName: taskInfo.name,
445
+ size: fsize,
446
+ hash: file.md5,
447
+ info: JSON.stringify(infos),
448
+ people: validModal.peopleName
449
+ }).then(() => {
450
+ file.status = 'success'
451
+ ElMessage.success(`文件:${file.name}提交成功`)
452
+ if (taskMoreInfo.people) {
453
+ // 无感知更新一下
454
+ PeopleApi.updatePeopleStatus(
455
+ k.value,
456
+ name,
457
+ validModal.peopleName,
458
+ file.md5
459
+ )
460
+ }
461
+ })
462
+ },
463
+ process(per: number, data: any, subscription: any) {
464
+ file.percentage = Math.floor(per)
465
+ // 挂载取消上传的方法
466
+ file.subscription = subscription
467
+ }
468
+ })
469
+ })
470
+ }
471
+ }
472
+ }
473
+
474
+ const submitUpload = async () => {
475
+ if (!isWriteFinish.value) {
476
+ ElMessage.warning('请先完成必要信息的填写')
477
+ return
478
+ }
479
+
480
+ if (taskMoreInfo.people) {
481
+ const isValid = await confirmPeopleName()
482
+ if (!isValid) {
483
+ return
484
+ }
485
+ }
486
+ startUpload()
487
+ }
488
+
489
+ // 是否允许上传
490
+ const allowUpload = computed(() => {
491
+ for (const file of fileList.value) {
492
+ if (file.status === 'ready') {
493
+ return true
494
+ }
495
+ }
496
+ return false
497
+ })
498
+
499
+ // 是否允许撤回
500
+ const allowWithdraw = computed(() => {
501
+ for (const file of fileList.value) {
502
+ if (['success', 'ready'].includes(file.status)) {
503
+ return true
504
+ }
505
+ }
506
+ return false
507
+ })
508
+
509
+ // 添加文件
510
+ // 正在计算MD5值的文件个数
511
+ const calculateMd5Count = ref(0)
512
+ const handleChangeFile = (file: any) => {
513
+ // 校验文件后缀名
514
+ const { name } = file
515
+ if (formatData.value.format.length && formatData.value.status) {
516
+ const suffix = getFileSuffix(name)
517
+ if (!formatData.value.format.find((v) => suffix.endsWith(v))) {
518
+ ElMessage.error(`${name} 格式不符合要球`)
519
+ fileUpload.value.handleRemove(file)
520
+ return
521
+ }
522
+ }
523
+
524
+ // 校验文件大小
525
+ if (formatData.value.size && formatData.value.size < file.size) {
526
+ ElMessage.error(`${name} 大小${formatSize(file.size)} 不符合要求`)
527
+ fileUpload.value.handleRemove(file)
528
+ return
529
+ }
530
+
531
+ calculateMd5Count.value += 1
532
+ // 计算md5 hash
533
+ getFileMd5Hash(file.raw).then((str) => {
534
+ file.md5 = str
535
+ calculateMd5Count.value -= 1
536
+ })
537
+ }
538
+
539
+ const limitUploadCount = computed(() => formatData.value.limit || 10)
540
+ const handleExceed = () => {
541
+ ElMessage.warning(
542
+ `一次提交最多只能选择${limitUploadCount.value}个文件,请移除已经上传成功的或刷新页面`
543
+ )
544
+ }
545
+ const showLinkModel = ref(false)
546
+ const templateLink = ref('')
547
+ const runWithdraw = () => {
548
+ const uploadFiles = fileList.value
549
+ for (const file of uploadFiles) {
550
+ if (!file.md5) {
551
+ ElMessage.info(
552
+ `文件(${file.name})的唯一指纹还在计算中,再等待一会儿再点击上传`
553
+ )
554
+ setTimeout(() => {
555
+ ElMessage.info('文件越大计算时间越长(1G通常需要20s)')
556
+ }, 100)
557
+ } else if (!['fail', 'uploading'].includes(file.status)) {
558
+ // 准备开始撤回
559
+ let { name } = file
560
+
561
+ // 如果开启了自动重命名,这里重命名一下
562
+ if (taskMoreInfo.rewrite) {
563
+ name =
564
+ infos.map((v) => v.value).join(formatData.value.splitChar || '-') +
565
+ getFileSuffix(name)
566
+ }
567
+
568
+ FileApi.withdrawFile({
569
+ taskKey: k.value,
570
+ taskName: taskInfo.name,
571
+ filename: name,
572
+ hash: file.md5,
573
+ info: JSON.stringify(infos),
574
+ peopleName: validModal.peopleName
575
+ })
576
+ .then(() => {
577
+ ElMessage.success(`文件:${file.name}撤回成功`)
578
+ file.name += ' - (已撤回 ✅ )'
579
+ file.status = 'fail'
580
+ })
581
+ .catch(() => {
582
+ ElMessage.error(`撤回失败: 没有文件:${file.name}对应提交记录`)
583
+ })
584
+ }
585
+ }
586
+ }
587
+ const downloadTemplate = () => {
588
+ FileApi.getTemplateUrl(taskMoreInfo.template, k.value)
589
+ .then((res) => {
590
+ showLinkModel.value = true
591
+ const { link } = res.data
592
+ templateLink.value = link
593
+ downLoadByUrl(link, taskMoreInfo.template)
594
+ })
595
+ .catch(() => {
596
+ ElMessage.warning('文件已从服务器上移除,请联系管理员重新上传')
597
+ })
598
+ }
599
+
600
+ // 撤回相关逻辑
601
+ const isWithdraw = ref(false)
602
+ const startWithdraw = async () => {
603
+ // 校验表单填写
604
+ if (!isWriteFinish.value) {
605
+ ElMessage.warning('请先完成必要信息的填写')
606
+ return
607
+ }
608
+ if (taskMoreInfo.people) {
609
+ const isValid = await confirmPeopleName()
610
+ if (!isValid) {
611
+ return
612
+ }
613
+ }
614
+ runWithdraw()
615
+ }
616
+
617
+ // 查询提交情况
618
+ const checkSubmitStatus = async () => {
619
+ // 校验表单填写
620
+ if (!isWriteFinish.value) {
621
+ ElMessage.warning('请先完成必要信息的填写,需和提交时信息完全一致')
622
+ return
623
+ }
624
+ // 卡控人员限制
625
+ if (taskMoreInfo.people) {
626
+ const isValid = await confirmPeopleName()
627
+ if (!isValid) {
628
+ return
629
+ }
630
+ }
631
+ FileApi.checkSubmitStatus(k.value, infos, validModal.peopleName).then(
632
+ (res) => {
633
+ if (res.data.isSubmit) {
634
+ ElMessage.success('已经提交过啦')
635
+ } else {
636
+ ElMessage.warning('还未提交过哟')
637
+ }
638
+ }
639
+ )
640
+ }
641
+ const isLoadingData = ref(false)
642
+ const readyRefresh = ref(false)
643
+ const isEqualInfos = (a: InfoItem[] = [], b: InfoItem[] = []) => {
644
+ if (a.length !== b.length) {
645
+ return false
646
+ }
647
+ return a.every(
648
+ (v, i) =>
649
+ v.type === b[i].type &&
650
+ v.text === b[i].text &&
651
+ isEqualInfos(v.children, b[i].children)
652
+ )
653
+ }
654
+ const refreshTaskMoreInfo = (hot = false) => {
655
+ TaskApi.getTaskMoreInfo(k.value).then((res) => {
656
+ Object.assign(taskMoreInfo, res.data)
657
+ if (!isEqualInfos(infos, parseInfo(taskMoreInfo.info))) {
658
+ infos.splice(0, infos.length)
659
+ infos.push(...parseInfo(taskMoreInfo.info))
660
+ if (hot) {
661
+ ElMessage.success('表单信息有更新')
662
+ }
663
+ }
664
+ refreshWaitTime(false)
665
+ isLoadingData.value = false
666
+ })
667
+ }
668
+ const handleBlur = () => {
669
+ readyRefresh.value = true
670
+ }
671
+ const handleFocus = () => {
672
+ if (readyRefresh.value && !disableForm.value) {
673
+ readyRefresh.value = false
674
+ refreshTaskMoreInfo(true)
675
+ }
676
+ }
677
+
678
+ // 展示的时间提示文案
679
+ const timeInfo = computed(() => {
680
+ if (!isOver.value) {
681
+ return ddlStr.value + waitTimeStr.value
682
+ }
683
+ return ddlStr.value
684
+ })
685
+
686
+ // tipImage
687
+ const tipData = reactive<{
688
+ text: string
689
+ imgs: {
690
+ uid: number
691
+ name: string
692
+ }[]
693
+ }>({
694
+ text: '',
695
+ imgs: []
696
+ })
697
+ const imageList = ref<
698
+ { name: string; uid: number; preview?: string; url: string }[]
699
+ >([])
700
+
701
+ watch(
702
+ () => taskMoreInfo.tip,
703
+ () => {
704
+ // 初始化
705
+ try {
706
+ const parseData = JSON.parse(taskMoreInfo.tip)
707
+ tipData.imgs = parseData.imgs
708
+ tipData.text = parseData.text || ''
709
+ imageList.value = tipData.imgs.map((v) => {
710
+ return {
711
+ ...v,
712
+ url: 'https://img.cdn.sugarat.top/mdImg/MTY3NzkxMDI1NTU1Nw==20140524124237518.gif'
713
+ }
714
+ })
715
+ if (imageList.value.length) {
716
+ // 异步填充url
717
+ PublicApi.getTipImageUrl(
718
+ k.value,
719
+ imageList.value.map((v) => ({
720
+ uid: v.uid,
721
+ name: v.name
722
+ }))
723
+ ).then((v) => {
724
+ v.data.forEach((url, idx) => {
725
+ imageList.value[idx].url = url.cover
726
+ Object.assign(imageList.value[idx], {
727
+ preview: url.preview
728
+ })
729
+ })
730
+ })
731
+ }
732
+ } catch {
733
+ tipData.text = ''
734
+ tipData.imgs = []
735
+ imageList.value = []
736
+ }
737
+ }
738
+ )
739
+ onMounted(() => {
740
+ k.value = $route.params.key as string
741
+ if (k.value) {
742
+ isLoadingData.value = true
743
+ TaskApi.getTaskInfo(k.value)
744
+ .then((res) => {
745
+ Object.assign(taskInfo, res.data)
746
+ })
747
+ .catch((err) => {
748
+ if (err.code === 4001) {
749
+ ElMessage.error('任务不存在')
750
+ k.value = ''
751
+ taskInfo.name = '任务不存在'
752
+ }
753
+ })
754
+ refreshTaskMoreInfo()
755
+ refreshWaitTime()
756
+ }
757
+ // 页面隐藏
758
+ window.addEventListener('blur', handleBlur)
759
+
760
+ // 页面展示
761
+ window.addEventListener('focus', handleFocus)
762
+ })
763
+
764
+ onUnmounted(() => {
765
+ // 页面隐藏
766
+ window.removeEventListener('blur', handleBlur)
767
+ // 页面展示
768
+ window.removeEventListener('focus', handleFocus)
769
+ })
770
+ </script>
771
+ <style scoped lang="scss">
772
+ .task-panel :deep(ul.el-upload-list) {
773
+ border: 1px dashed #d4d4d4;
774
+ padding: 10px;
775
+
776
+ &::before {
777
+ content: '此处展示选择文件列表';
778
+ font-size: 12px;
779
+ position: relative;
780
+ bottom: 4px;
781
+ }
782
+ }
783
+
784
+ .task-panel :deep(.el-upload-list__item-name) {
785
+ display: block;
786
+ overflow: hidden;
787
+ max-width: 290px;
788
+ text-overflow: ellipsis;
789
+ word-break: keep-all;
790
+ }
791
+
792
+ .task-panel :deep(.is-ready .el-icon--close) {
793
+ display: block;
794
+ color: black;
795
+ }
796
+
797
+ .task-panel {
798
+ background-color: #f3f6f8;
799
+ padding-bottom: 1rem;
800
+ position: relative;
801
+ }
802
+
803
+ .pc-nav {
804
+ background-color: #fff;
805
+ display: flex;
806
+ padding: 10px;
807
+ justify-content: space-between;
808
+ align-items: center;
809
+
810
+ .exit {
811
+ cursor: pointer;
812
+ }
813
+
814
+ .nav {
815
+ display: flex;
816
+
817
+ nav {
818
+ display: flex;
819
+ align-items: center;
820
+
821
+ .nav-item {
822
+ font-size: 1rem;
823
+ color: #595959;
824
+ padding: 10px;
825
+ cursor: pointer;
826
+
827
+ &.active {
828
+ color: #409eff !important;
829
+ font-weight: 600;
830
+ }
831
+ }
832
+ }
833
+
834
+ .exit {
835
+ color: #595959;
836
+ }
837
+ }
838
+
839
+ .logo {
840
+ width: 180px;
841
+ margin: 0 10px;
842
+
843
+ img {
844
+ height: 40px;
845
+ }
846
+ }
847
+ }
848
+
849
+ .panel {
850
+ max-width: 1024px;
851
+ padding: 1em;
852
+ background-color: #fff;
853
+ margin: 10px auto;
854
+ box-sizing: border-box;
855
+ box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
856
+ border-radius: 4px;
857
+
858
+ .name {
859
+ text-align: center;
860
+ }
861
+
862
+ .ddl {
863
+ margin-top: 10px;
864
+ color: #919191;
865
+ font-size: 14px;
866
+ }
867
+
868
+ .infos {
869
+ max-width: 460px;
870
+ margin: auto;
871
+ overflow: hidden;
872
+ :deep(div.el-form-item > label) {
873
+ font-weight: bold;
874
+ &::before {
875
+ content: '* ';
876
+ color: red;
877
+ }
878
+ }
879
+ }
880
+ }
881
+
882
+ .withdraw {
883
+ text-align: right;
884
+ }
885
+
886
+ .tip-wrapper {
887
+ line-height: 20px;
888
+ text-align: left;
889
+ word-break: break-all;
890
+ // max-height: 100px;
891
+ overflow: hidden;
892
+ padding: 0 20px;
893
+ color: #e6a23c;
894
+ max-width: 320px;
895
+ font-size: 14px;
896
+ }
897
+ </style>