Objects.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. <template>
  2. <div>
  3. <template v-if="isInternal">
  4. <a-alert type="warning" class="mb-2" :message="$t('storage.internal_bucket_file')" />
  5. <data-empty />
  6. </template>
  7. <page-list
  8. v-else
  9. :list="list"
  10. :columns="columns"
  11. :group-actions="groupActions"
  12. :single-actions="singleActions"
  13. default-search-key="prefix"
  14. :placeholder="$t('storage.bucket_object_search_placeholder')">
  15. <div slot="table-prepend" class="d-flex align-items-center pt-2 pb-2">
  16. <span><a-icon type="folder-open" theme="filled" style="color: rgb(245,200, 61);font-size:15px" />{{$t('storage.text_150')}}</span>
  17. <a-breadcrumb>
  18. <a-breadcrumb-item>
  19. <a-button style="padding:0" type="link" @click="nextPage('')">{{data.name}}</a-button>
  20. </a-breadcrumb-item>
  21. <a-breadcrumb-item v-for="(value, key) in breadcrumbs" :key="key">
  22. <a-button style="padding:0" type="link" @click="nextPage(key)">{{value}}</a-button>
  23. </a-breadcrumb-item>
  24. </a-breadcrumb>
  25. </div>
  26. </page-list>
  27. </div>
  28. </template>
  29. <script>
  30. import { ACL_TYPE, FINANCE_INTERNAL } from '@Storage/constants/index.js'
  31. import { objectsModel } from '@Storage/views/bucket/utils/controller.js'
  32. import WindowsMixin from '@/mixins/windows'
  33. import ListMixin from '@/mixins/list'
  34. import { sizestrWithUnit } from '@/utils/utils'
  35. import i18n from '@/locales'
  36. const validDirName = (rule, value, callback) => {
  37. if (!value) {
  38. callback(new Error(i18n.t('storage.text_151')))
  39. } else if (value.startsWith('/')) {
  40. callback(new Error(i18n.t('storage.text_152')))
  41. } else if (value.includes('//')) {
  42. callback(new Error(i18n.t('storage.validDirName')))
  43. } else if (value === '..') {
  44. callback(new Error(i18n.t('storage.text_153')))
  45. } else {
  46. callback()
  47. }
  48. }
  49. export default {
  50. name: 'BucketStorageObjectList',
  51. mixins: [WindowsMixin, ListMixin],
  52. props: {
  53. id: String,
  54. data: {
  55. type: Object,
  56. },
  57. },
  58. data () {
  59. return {
  60. paths: [],
  61. prefix: '',
  62. nextFetchListLoading: false,
  63. list: this.$list.createList(this, {
  64. id: 'objectListForBucketStorageSidePage',
  65. resource: 'objects',
  66. getParams: this.getParams,
  67. ctx: [['buckets', this.data.id]],
  68. idKey: 'key',
  69. filterOptions: {
  70. prefix: {
  71. label: this.$t('storage.text_154'),
  72. formatter: val => {
  73. return `${this.prefix}${val}`
  74. },
  75. },
  76. },
  77. }),
  78. columns: [
  79. {
  80. field: 'name',
  81. title: this.$t('storage.text_154'),
  82. minWidth: 120,
  83. showOverflow: 'ellipsis',
  84. slots: {
  85. default: ({ row }) => {
  86. const { key } = row
  87. if (this.nextFetchListLoading && this.prefix === key) {
  88. return [<a-icon type="loading" />]
  89. }
  90. const rkey = key.replace(this.prefix, '')
  91. if (this.isDir(key)) {
  92. return [
  93. <div class="d-flex align-items-center">
  94. <a-icon type="folder" theme="filled" style="color: rgb(245,200, 61)" />
  95. <a class="text-truncate" style="margin-right: 3px" onClick={() => this.nextPage(key)} title={rkey}>{ rkey }</a>
  96. <copy message={rkey} />
  97. </div>,
  98. ]
  99. }
  100. return [
  101. <div class="d-flex align-items-center">
  102. <a-icon theme="filled" type="file" />
  103. <span class="text-truncate" title={rkey}>
  104. {rkey}
  105. </span>
  106. <copy message={rkey} />
  107. </div>,
  108. ]
  109. },
  110. },
  111. },
  112. {
  113. field: 'acl',
  114. title: this.$t('storage.text_93'),
  115. width: 120,
  116. formatter: ({ row }) => {
  117. return ACL_TYPE[row.acl] || row.acl || '-'
  118. },
  119. },
  120. {
  121. field: 'size_bytes',
  122. title: this.$t('storage.text_155'),
  123. minWidth: 100,
  124. formatter: ({ row }) => {
  125. return row.size_bytes ? sizestrWithUnit(row.size_bytes, 'B', 1024) : '-'
  126. },
  127. },
  128. {
  129. field: 'storage_class',
  130. title: this.$t('storage.text_38'),
  131. width: 120,
  132. formatter: ({ row }) => {
  133. return row.storage_class || '-'
  134. },
  135. },
  136. {
  137. field: 'last_modified',
  138. title: this.$t('storage.text_156'),
  139. width: 100,
  140. formatter: ({ row }) => {
  141. return row.last_modified ? this.$moment(row.last_modified).fromNow() : '-'
  142. },
  143. },
  144. ],
  145. groupActions: [
  146. {
  147. label: this.$t('storage.text_157'),
  148. action: () => {
  149. this.createDialog('ObjectsUploadFileDialog', {
  150. title: this.$t('storage.text_157'),
  151. list: this.list,
  152. resId: this.resId,
  153. resItem: this.data,
  154. resName: this.resName,
  155. prefix: this.prefix,
  156. })
  157. },
  158. meta: () => {
  159. const isValidate = !(this.data.brand === 'Azure' && !this.prefix)
  160. return {
  161. buttonType: 'primary',
  162. validate: isValidate,
  163. tooltip: !isValidate && this.$t('storage.text_158'),
  164. }
  165. },
  166. },
  167. {
  168. label: this.$t('storage.text_159'),
  169. action: (row) => {
  170. this.createDialog('SmartFormDialog', {
  171. title: this.$t('storage.text_159'),
  172. data: [row],
  173. list: this.list,
  174. width: 600,
  175. formItemLayout: {
  176. wrapperCol: {
  177. span: 19,
  178. },
  179. labelCol: {
  180. span: 5,
  181. },
  182. },
  183. callback: async (data) => {
  184. const manager = new this.$Manager(`buckets/${this.resId}/makedir`, 'v2')
  185. data.key = `${this.prefix}${data.key}/`
  186. await manager.create({ data })
  187. this.list.fetchData()
  188. },
  189. decorators: {
  190. key: ['key', {
  191. rules: [
  192. { required: true, message: this.$t('storage.text_151') },
  193. { type: 'string', min: 1, max: 254, message: this.$t('storage.text_160'), trigger: 'blur' },
  194. { validator: validDirName, trigger: 'blur' },
  195. ],
  196. },
  197. {
  198. label: this.$t('storage.text_161'),
  199. placeholder: this.$t('storage.text_162'),
  200. extra: () => {
  201. return (
  202. <div>
  203. <div>{ this.$t('storage.text_173') }</div>
  204. <div class='mt-1'>{ this.$t('storage.text_174') }</div>
  205. <div class='mt-1'>{ this.$t('storage.text_175') }</div>
  206. <div class='mt-1'>{ this.$t('storage.text_176') }</div>
  207. </div>
  208. )
  209. },
  210. },
  211. ],
  212. },
  213. })
  214. },
  215. },
  216. {
  217. label: this.$t('storage.text_33'),
  218. actions: (row) => {
  219. return [
  220. {
  221. label: this.$t('storage.text_163'),
  222. action: () => {
  223. this.createDialog('ObjectsUpdateHttpDialog', {
  224. title: this.$t('storage.text_163'),
  225. data: this.list.selectedItems,
  226. resId: this.resId,
  227. resName: this.resName,
  228. columns: this.columns,
  229. list: this.list,
  230. refresh: () => {
  231. this.nextPage(this.prefix)
  232. },
  233. })
  234. },
  235. meta: () => {
  236. return {
  237. validate: this.list.selectedItems.every(row => !this.isDir(row.key)),
  238. }
  239. },
  240. },
  241. {
  242. label: this.$t('storage.text_138'),
  243. action: () => {
  244. this.createDialog('ObjectsUpdateAclDialog', {
  245. title: this.$t('storage.text_138'),
  246. bucket: this.data,
  247. data: this.list.selectedItems,
  248. resName: this.resName,
  249. columns: this.columns,
  250. list: this.list,
  251. refresh: this.refresh,
  252. })
  253. },
  254. meta: () => {
  255. return {
  256. validate: this.list.selectedItems.every(row => !this.isDir(row.key)),
  257. }
  258. },
  259. },
  260. {
  261. label: this.$t('storage.text_36'),
  262. action: () => {
  263. this.createDialog('ObjectsDeleteDialog', {
  264. alert: this.$t('storage.text_164'),
  265. data: this.list.selectedItems,
  266. columns: this.columns,
  267. title: this.$t('storage.text_36'),
  268. resId: this.resId,
  269. list: this.list,
  270. name: this.$t('storage.text_112'),
  271. })
  272. },
  273. },
  274. ]
  275. },
  276. meta: () => {
  277. return {
  278. validate: !!this.list.selectedItems.length,
  279. }
  280. },
  281. },
  282. ],
  283. singleActions: [
  284. {
  285. label: this.$t('storage.text_165'),
  286. action: (row) => {
  287. objectsModel.getUrl(row, this.data.id, this.accessUrl).then((url) => {
  288. // url && window.open(url, '__blank')
  289. const a = document.createElement('a')
  290. document.body.appendChild(a)
  291. a.href = url
  292. a.click()
  293. document.body.removeChild(a)
  294. })
  295. },
  296. meta: row => {
  297. return {
  298. validate: !this.isDir(row.key),
  299. }
  300. },
  301. },
  302. {
  303. label: this.$t('storage.text_65'),
  304. actions: (row) => {
  305. return [
  306. {
  307. label: this.$t('storage.text_166'),
  308. action: () => {
  309. // await curl = controller.getUrl(row, this.data.name)
  310. this.createDialog('ObjectsCreateUrlDialog', {
  311. title: this.$t('storage.text_167'),
  312. data: [row],
  313. resId: this.resId,
  314. resName: this.resName,
  315. accessUrl: this.accessUrl,
  316. columns: this.columns,
  317. list: this.list,
  318. })
  319. },
  320. meta: () => {
  321. return {
  322. validate: !this.isDir(row.key),
  323. }
  324. },
  325. },
  326. {
  327. label: this.$t('storage.text_138'),
  328. action: () => {
  329. this.createDialog('ObjectsUpdateAclDialog', {
  330. title: this.$t('storage.text_138'),
  331. data: [row],
  332. bucket: this.data,
  333. resName: this.resName,
  334. columns: this.columns,
  335. list: this.list,
  336. refresh: this.refresh,
  337. name: this.isDir(row.key) ? this.$t('storage.text_168') : this.$t('storage.text_112'),
  338. })
  339. },
  340. meta: () => {
  341. return {
  342. validate: !this.isDir(row.key),
  343. }
  344. },
  345. },
  346. {
  347. label: this.$t('storage.text_163'),
  348. action: () => {
  349. this.createDialog('ObjectsUpdateHttpDialog', {
  350. title: this.$t('storage.text_163'),
  351. data: [row],
  352. resName: this.resName,
  353. resId: this.resId,
  354. columns: this.columns,
  355. list: this.list,
  356. refresh: () => {
  357. this.nextPage(this.prefix)
  358. },
  359. })
  360. },
  361. meta: () => {
  362. return {
  363. validate: !this.isDir(row.key),
  364. }
  365. },
  366. },
  367. {
  368. label: this.$t('storage.text_36'),
  369. action: () => {
  370. this.createDialog('ObjectsDeleteDialog', {
  371. alert: this.$t('storage.text_164'),
  372. data: [row],
  373. columns: this.columns,
  374. title: this.$t('storage.text_36'),
  375. resName: this.resName,
  376. resId: this.resId,
  377. list: this.list,
  378. name: this.isDir(row.key) ? this.$t('storage.text_168') : this.$t('storage.text_112'),
  379. })
  380. },
  381. },
  382. ]
  383. },
  384. // meta: row => {
  385. // return {
  386. // validate: !this.isDir(row.key),
  387. // }
  388. // },
  389. },
  390. ],
  391. }
  392. },
  393. computed: {
  394. resId () {
  395. return this.data.id
  396. },
  397. resName () {
  398. return this.data.name
  399. },
  400. accessUrl () {
  401. if (this.data.access_urls && this.data.access_urls.length > 0) {
  402. const accessUrls = this.data.access_urls
  403. for (let i = 0; i < accessUrls.length; i++) {
  404. if (accessUrls[i].primary) {
  405. return accessUrls[i].url
  406. }
  407. }
  408. return accessUrls[0].url
  409. }
  410. return ''
  411. },
  412. breadcrumbs () {
  413. const _ = {}
  414. if (this.prefix) {
  415. const paths = this.prefix.split('/')
  416. if (paths && paths.length > 0) {
  417. paths.forEach((path, index) => {
  418. if (path === '') return false
  419. const k = paths.slice(0, index + 1).join('/')
  420. _[k + '/'] = path
  421. })
  422. }
  423. }
  424. return _
  425. },
  426. isInternal () {
  427. return FINANCE_INTERNAL.includes(this.data.location)
  428. },
  429. },
  430. created () {
  431. !this.isInternal && this.list.fetchData()
  432. this.initSidePageTab('detail')
  433. },
  434. methods: {
  435. isDir (key) {
  436. return key.endsWith('/')
  437. },
  438. async nextPage (key, index) {
  439. // 表示根目录,根目录不需要prefix
  440. this.prefix = key
  441. this.nextFetchListLoading = true
  442. await this.list.changeFilter({})
  443. await this.list.fetchData()
  444. this.nextFetchListLoading = false
  445. },
  446. getParams () {
  447. const _ = {}
  448. if (this.prefix) {
  449. _.prefix = this.prefix
  450. }
  451. return _
  452. },
  453. },
  454. }
  455. </script>