@smartos-lib/components 1.7.0-beta.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (128) hide show
  1. package/.eslintrc +12 -0
  2. package/.eslintrc-auto-import.json +332 -0
  3. package/Components.code-workspace +143 -0
  4. package/LICENSE +21 -0
  5. package/dist/smart-docx-editor/index.d.ts +2 -0
  6. package/dist/smart-docx-editor/index.js +68 -0
  7. package/dist/smart-file-preview/index.d.ts +18 -0
  8. package/dist/smart-file-preview/index.js +37 -0
  9. package/dist/smart-upload/index.d.ts +2 -0
  10. package/dist/smart-upload/index.js +800 -0
  11. package/index.html +16 -0
  12. package/package.json +23 -0
  13. package/public/favicon.svg +6 -0
  14. package/scripts/components.vite.config.ts +96 -0
  15. package/scripts/shared.ts +9 -0
  16. package/src/App.vue +28 -0
  17. package/src/components/Logo/index.vue +15 -0
  18. package/src/components-private/.gitkeep +0 -0
  19. package/src/composables/useElementStyle.ts +23 -0
  20. package/src/composables/useNaiveStyle.ts +43 -0
  21. package/src/composables/useNaiveTheme.ts +71 -0
  22. package/src/composables/useSmart.ts +36 -0
  23. package/src/layouts/default.vue +3 -0
  24. package/src/main.ts +33 -0
  25. package/src/modules/pinia/index.ts +8 -0
  26. package/src/modules/progress/index.ts +12 -0
  27. package/src/modules/router/install.ts +9 -0
  28. package/src/modules/router/routes.ts +40 -0
  29. package/src/pages/[...all].vue +21 -0
  30. package/src/pages/frame/component/[name].vue +14 -0
  31. package/src/pages/frame/index.vue +81 -0
  32. package/src/pages/index/composables/useTabsManage.ts +46 -0
  33. package/src/pages/index/index.vue +111 -0
  34. package/src/pages/index/type.ts +13 -0
  35. package/src/pages/index/utils/index.ts +41 -0
  36. package/src/settings.ts +9 -0
  37. package/src/shared/components.ts +52 -0
  38. package/src/shared/env.ts +11 -0
  39. package/src/shared/unocss.theme.ts +1600 -0
  40. package/src/stores/theme.ts +29 -0
  41. package/src/styles/element.scss +3 -0
  42. package/src/styles/styles.scss +21 -0
  43. package/src/types.ts +20 -0
  44. package/src/utils/callCustomElementExposed.ts +6 -0
  45. package/src/utils/deepCloneESModule.ts +10 -0
  46. package/src/utils/defineCustomElements.ts +18 -0
  47. package/src/utils/formatComponentsGlob.ts +16 -0
  48. package/src/utils/getFileMD5.ts +31 -0
  49. package/src/utils/getFileNameAndExt.ts +11 -0
  50. package/src/utils/isFileEqual.ts +13 -0
  51. package/src/utils/jsonToFormData.ts +8 -0
  52. package/src/web-components/smart-docx-drive-page/App.vue +37 -0
  53. package/src/web-components/smart-docx-drive-page/apis/doc.ts +85 -0
  54. package/src/web-components/smart-docx-drive-page/apis/file.ts +278 -0
  55. package/src/web-components/smart-docx-drive-page/apis/folder.ts +72 -0
  56. package/src/web-components/smart-docx-drive-page/children/Home.vue +8 -0
  57. package/src/web-components/smart-docx-drive-page/children/Me.vue +47 -0
  58. package/src/web-components/smart-docx-drive-page/components/CustomImage.vue +26 -0
  59. package/src/web-components/smart-docx-drive-page/components/CustomPopover.vue +62 -0
  60. package/src/web-components/smart-docx-drive-page/components/DocxDir.vue +99 -0
  61. package/src/web-components/smart-docx-drive-page/components/DocxDoc.vue +132 -0
  62. package/src/web-components/smart-docx-drive-page/components/DocxDownloadPopoverItem.vue +41 -0
  63. package/src/web-components/smart-docx-drive-page/components/DocxFileList.vue +156 -0
  64. package/src/web-components/smart-docx-drive-page/components/DocxPreview.vue +33 -0
  65. package/src/web-components/smart-docx-drive-page/components/DocxUpload.vue +164 -0
  66. package/src/web-components/smart-docx-drive-page/components/FileIcon.vue +62 -0
  67. package/src/web-components/smart-docx-drive-page/components-private/Header.vue +65 -0
  68. package/src/web-components/smart-docx-drive-page/components-private/Logo.vue +15 -0
  69. package/src/web-components/smart-docx-drive-page/components-private/Menu.vue +34 -0
  70. package/src/web-components/smart-docx-drive-page/components-private/Navbar.vue +36 -0
  71. package/src/web-components/smart-docx-drive-page/composables/useFullscreenElDialog.ts +41 -0
  72. package/src/web-components/smart-docx-drive-page/composables/usePrompt.ts +73 -0
  73. package/src/web-components/smart-docx-drive-page/data.ts +10 -0
  74. package/src/web-components/smart-docx-drive-page/external-style/custom-popover.sass +8 -0
  75. package/src/web-components/smart-docx-drive-page/external-style/index.sass +1 -0
  76. package/src/web-components/smart-docx-drive-page/index.ts +20 -0
  77. package/src/web-components/smart-docx-drive-page/index.vue +39 -0
  78. package/src/web-components/smart-docx-drive-page/info.ts +2 -0
  79. package/src/web-components/smart-docx-drive-page/stores/menu.ts +60 -0
  80. package/src/web-components/smart-docx-drive-page/types.ts +51 -0
  81. package/src/web-components/smart-docx-drive-page/utils/file-actions.ts +63 -0
  82. package/src/web-components/smart-docx-drive-page/utils/file.ts +31 -0
  83. package/src/web-components/smart-docx-editor/App.vue +32 -0
  84. package/src/web-components/smart-docx-editor/MarkdownShortcuts/components/Markdown.vue +202 -0
  85. package/src/web-components/smart-docx-editor/MarkdownShortcuts/components/Menu.vue +100 -0
  86. package/src/web-components/smart-docx-editor/MarkdownShortcuts/components/types.ts +6 -0
  87. package/src/web-components/smart-docx-editor/MarkdownShortcuts/components-react/Markdown.tsx +71 -0
  88. package/src/web-components/smart-docx-editor/MarkdownShortcuts/components-react/MarkdownElement.tsx +81 -0
  89. package/src/web-components/smart-docx-editor/MarkdownShortcuts/components-react/elements/Blockquote/index.sass +6 -0
  90. package/src/web-components/smart-docx-editor/MarkdownShortcuts/components-react/elements/Blockquote/index.tsx +12 -0
  91. package/src/web-components/smart-docx-editor/MarkdownShortcuts/components-react/elements/Heading/index.sass +14 -0
  92. package/src/web-components/smart-docx-editor/MarkdownShortcuts/components-react/elements/Heading/index.tsx +17 -0
  93. package/src/web-components/smart-docx-editor/MarkdownShortcuts/components-react/elements/List/index.scss +16 -0
  94. package/src/web-components/smart-docx-editor/MarkdownShortcuts/components-react/elements/List/index.tsx +39 -0
  95. package/src/web-components/smart-docx-editor/MarkdownShortcuts/components-react/types/custom-types.d.ts +69 -0
  96. package/src/web-components/smart-docx-editor/MarkdownShortcuts/composables/useTextSelection.ts +50 -0
  97. package/src/web-components/smart-docx-editor/MarkdownShortcuts/index.sass +19 -0
  98. package/src/web-components/smart-docx-editor/MarkdownShortcuts/index.vue +21 -0
  99. package/src/web-components/smart-docx-editor/MarkdownShortcuts/shared/const.ts +23 -0
  100. package/src/web-components/smart-docx-editor/MarkdownShortcuts/utils/slateHelpers.ts +23 -0
  101. package/src/web-components/smart-docx-editor/data.ts +38 -0
  102. package/src/web-components/smart-docx-editor/demo.vue +11 -0
  103. package/src/web-components/smart-docx-editor/index.md +3 -0
  104. package/src/web-components/smart-docx-editor/index.ts +5 -0
  105. package/src/web-components/smart-docx-editor/index.vue +12 -0
  106. package/src/web-components/smart-docx-editor/info.ts +2 -0
  107. package/src/web-components/smart-file-preview/category/Code.vue +171 -0
  108. package/src/web-components/smart-file-preview/category/Image.vue +49 -0
  109. package/src/web-components/smart-file-preview/category/Pdf.vue +14 -0
  110. package/src/web-components/smart-file-preview/category/Video.vue +27 -0
  111. package/src/web-components/smart-file-preview/demo.vue +34 -0
  112. package/src/web-components/smart-file-preview/index.md +5 -0
  113. package/src/web-components/smart-file-preview/index.ts +29 -0
  114. package/src/web-components/smart-file-preview/index.vue +56 -0
  115. package/src/web-components/smart-file-preview/info.ts +2 -0
  116. package/src/web-components/smart-file-preview/shared/const.ts +4 -0
  117. package/src/web-components/smart-file-preview/types.ts +38 -0
  118. package/src/web-components/smart-upload/index.ts +5 -0
  119. package/src/web-components/smart-upload/index.vue +101 -0
  120. package/src/web-components/smart-upload/info.ts +2 -0
  121. package/src/web-components/smart-upload/types.ts +28 -0
  122. package/tsconfig.json +15 -0
  123. package/types/auto-imports.d.ts +975 -0
  124. package/types/components.d.ts +14 -0
  125. package/types/env.d.ts +8 -0
  126. package/types/shims.d.ts +6 -0
  127. package/unocss.config.ts +23 -0
  128. package/vite.config.ts +60 -0
@@ -0,0 +1,47 @@
1
+ <template>
2
+ <Header style="margin-bottom: 24px" @upload-success="onUploadSuccess">
3
+ <template #title>
4
+ <b v-if="meActiveMenuInfo?.filePath === '/'" text-xl>我的空间</b>
5
+ <el-scrollbar v-else class="w-full flex-grow">
6
+ <div flex="~ gap-1">
7
+ <div
8
+ v-for="(info, index) in meActiveMenuPathInfo" :key="info.id"
9
+ flex="~ items-center none gap-1" text-xl
10
+ class="[&>*]-flex-none"
11
+ :class="{ 'op-80': index < meActiveMenuPathInfo.length - 1 }"
12
+ >
13
+ <button
14
+ cursor-default rounded bg-transparent px-1
15
+ :class="{ 'cursor-pointer hover:bg-neutral-1': index < meActiveMenuPathInfo.length - 1 }"
16
+ @click="meActiveMenu = info.id"
17
+ >
18
+ {{ info.label === '/' ? '我的空间' : info.label }}
19
+ </button>
20
+ <i-ic-baseline-arrow-forward-ios v-if="index < meActiveMenuPathInfo.length - 1" />
21
+ </div>
22
+ </div>
23
+ </el-scrollbar>
24
+ </template>
25
+ </Header>
26
+ <el-scrollbar class="-mr4" view-style="padding-right: 16px" height="100%">
27
+ <DocxFileList
28
+ ref="fileListRef"
29
+ :file-path="meActiveMenuInfo?.filePath"
30
+ />
31
+ </el-scrollbar>
32
+ </template>
33
+
34
+ <script lang="ts" setup>
35
+ import Header from '../components-private/Header.vue';
36
+ import DocxFileList from '../components/DocxFileList.vue';
37
+ import { menuStore } from '../stores/menu';
38
+
39
+ const fileListRef = ref<typeof DocxFileList>();
40
+
41
+ const { meActiveMenuInfo, dirTree, meActiveMenuPathInfo, meActiveMenu } = menuStore();
42
+
43
+ function onUploadSuccess() {
44
+ dirTree.execute();
45
+ fileListRef.value?.execute();
46
+ }
47
+ </script>
@@ -0,0 +1,26 @@
1
+ <!--
2
+ 基于 el-image 二次封装的图片组件
3
+ - 支持传入 Blob 对象进行显示
4
+ -->
5
+
6
+ <template>
7
+ <el-image v-bind="props" :src="src">
8
+ <template #error>
9
+ <slot name="error" />
10
+ </template>
11
+ </el-image>
12
+ </template>
13
+
14
+ <script lang="ts" setup>
15
+ import type { ImageProps } from 'element-plus';
16
+
17
+ const props = defineProps<Partial<ImageProps>>();
18
+
19
+ const blob = defineModel<Blob>('blob');
20
+ const blobUrl = useObjectUrl(blob);
21
+
22
+ const src = computed(() => {
23
+ if (blob.value) return blobUrl.value;
24
+ return props.src;
25
+ });
26
+ </script>
@@ -0,0 +1,62 @@
1
+ <!--
2
+ 基于 el-popover 二次封装的弹出框
3
+ - 为 reference 插槽传入 visible, 弹出框的显示状态
4
+ - 更好的 web-components 兼容性
5
+ -->
6
+
7
+ <template>
8
+ <ElPopover
9
+ ref="popoverRef"
10
+ v-bind="props"
11
+ @before-enter="visible = true"
12
+ @before-leave="visible = false"
13
+ >
14
+ <template #reference>
15
+ <slot name="reference" :visible="visible" />
16
+ </template>
17
+
18
+ <slot />
19
+ </ElPopover>
20
+ </template>
21
+
22
+ <script lang="ts" setup>
23
+ import { ElPopover } from 'element-plus';
24
+ import type { PopoverProps } from 'element-plus';
25
+
26
+ const props = withDefaults(defineProps<Partial<Omit<PopoverProps, 'visible'>>>(), {
27
+ offset: 4,
28
+ showArrow: false,
29
+ teleported: true,
30
+ popperStyle: 'width: 180px; padding: 8px 0; border-radius: 8px',
31
+ });
32
+
33
+ const visible = ref<boolean>();
34
+
35
+ const popoverRef = ref<typeof ElPopover>();
36
+
37
+ // 当弹窗插入到 body 中时, 将下拉框中的 .cp-item 样式类改为外置样式类
38
+ wheneverEffectScopeImmediate(() => props.teleported && popoverRef.value?.popperRef?.contentRef, (contentRef: HTMLDivElement) => {
39
+ let stop: (() => void) | undefined;
40
+
41
+ function addClass() {
42
+ stop?.();
43
+ contentRef.querySelectorAll('.cp-item').forEach((item) => {
44
+ item.classList.add('smart-docx-drive-page_custom-popover-item');
45
+ });
46
+ startObservation();
47
+ }
48
+
49
+ function startObservation() {
50
+ stop = useMutationObserver(contentRef, addClass, { childList: true, attributes: true, subtree: true }).stop;
51
+ }
52
+
53
+ addClass();
54
+ });
55
+ </script>
56
+
57
+ <style lang="sass">
58
+ @import '../external-style/custom-popover'
59
+
60
+ .cp-item
61
+ @extend .smart-docx-drive-page_custom-popover-item
62
+ </style>
@@ -0,0 +1,99 @@
1
+ <template>
2
+ <div
3
+ class="menu-item group/dir"
4
+ v-bind="attrs"
5
+ :class="{ active: isActive }"
6
+ @click="info && (meActiveMenu = info.id)"
7
+ >
8
+ <div w-4.5 flex="~ justify-center items-center" pos="relative left-1.5">
9
+ <el-button v-if="showOpen" link @click.stop="isOpen = !isOpen">
10
+ <i-akar-icons-triangle-right-fill class="size-4 transition-all" :class="{ 'rotate-90': isOpen }" />
11
+ </el-button>
12
+ </div>
13
+
14
+ <div class="munu-item-icon">
15
+ <slot v-if="slots.icon" name="icon" :isActive="isActive" />
16
+ <i-ph-folder-fill v-else-if="isActive" class="size-5" />
17
+ <i-ph-folder v-else class="size-5" />
18
+ </div>
19
+
20
+ <span flex-grow>{{ name ?? info?.label }}</span>
21
+
22
+ <CustomPopover v-if="!hiddenActions" placement="bottom-start" trigger="click" :offset="2">
23
+ <template #reference="{ visible }">
24
+ <button :class="{ 'bg-neutral-3! flex!': visible }" class="hidden group-hover/dir:flex" size-6 flex="justify-center items-center" bg="transparent hover:neutral-3" transition outline-none rounded @click.stop>
25
+ <i-ph-dots-three-bold class="size-5 c-neutral-5" />
26
+ </button>
27
+ </template>
28
+
29
+ <div class="cp-item" @click="toRename(info!)"><div class="icon-wrap"><i-ph-note-pencil-duotone class="size-4.8" /></div>重命名</div>
30
+ <div class="cp-item" @click="toDelete(info!)"><div class="icon-wrap"><i-material-symbols-delete-outline class="size-5" /></div>删除</div>
31
+ </CustomPopover>
32
+ </div>
33
+
34
+ <template v-if="isOpen">
35
+ <DocxDir v-for="item in info?.children" :key="item.id" :info="item" :level="level + 1" :style="{ paddingLeft: `${level * 16}px` }" />
36
+ </template>
37
+ </template>
38
+
39
+ <script lang="ts" setup>
40
+ import type { DirInfo } from '../apis/folder';
41
+ import { menuStore } from '../stores/menu';
42
+ import { toDeleteFolder, toRenameFolder } from '../utils/file-actions';
43
+ import CustomPopover from './CustomPopover.vue';
44
+
45
+ interface Props {
46
+ name?: string
47
+ info?: DirInfo
48
+ showOpen?: boolean
49
+ level?: number
50
+ hiddenActions?: boolean
51
+ }
52
+
53
+ const props = withDefaults(defineProps<Props>(), {
54
+ showOpen: true,
55
+ level: 1,
56
+ });
57
+ const slots = defineSlots<{
58
+ icon?: (props: { isActive: boolean }) => any
59
+ }>();
60
+
61
+ const attrs = useAttrs();
62
+
63
+ const isOpen = ref(false);
64
+
65
+ const { meActiveMenu, dirTree } = menuStore();
66
+
67
+ const isActive = computed(() => meActiveMenu.value === props.info?.id);
68
+
69
+ function toRename(info: DirInfo) {
70
+ toRenameFolder(info).then(() => {
71
+ dirTree.execute();
72
+ });
73
+ }
74
+
75
+ function toDelete(info: DirInfo) {
76
+ toDeleteFolder(info).then(() => {
77
+ dirTree.execute();
78
+ });
79
+ }
80
+ </script>
81
+
82
+ <script lang="ts">
83
+ export default defineComponent({
84
+ name: 'DocxDir',
85
+ });
86
+ </script>
87
+
88
+ <style lang="sass" scoped>
89
+ .menu-item
90
+ @apply h-10 flex items-center gap-2 relative
91
+ @apply text-15px cursor-pointer select-none rounded pr-2
92
+ @apply hover:bg-neutral-2
93
+
94
+ &.active
95
+ @apply c-blue-6 bg-blue-6/10
96
+
97
+ > .munu-item-icon
98
+ @apply size-5.5 flex justify-center items-center
99
+ </style>
@@ -0,0 +1,132 @@
1
+ <!-- 云文档 -->
2
+
3
+ <template>
4
+ <ElDialog ref="dialogRef" v-model="visible" :z-index="8888" append-to-body>
5
+ <template #header>
6
+ <div flex="~ col items-start gap-1">
7
+ <div c-black>{{ fileName || '未命名文档' }}</div>
8
+ <div text="xs neutral-6/90" bg="neutral-2/60" rounded p="x1 y.5" m--1>{{ (updateDoc.isLoading || updateDocTitle.isLoading) ? '保存中...' : '已经保存到云端' }}</div>
9
+ </div>
10
+ </template>
11
+
12
+ <el-scrollbar height="100%" view-style="display: flex; min-height: 100%">
13
+ <div w-full max-w-221.5 mxa p="x16.5 t5">
14
+ <!-- 标题 -->
15
+ <div
16
+ ref="titleRef"
17
+ class="empty:after-(content-['未命名文档'] c-neutral pointer-events-none)"
18
+ text-3xl font-bold lh-normal break-all c-black b-b="1 solid neutral-2" mb-5 py-5
19
+ contenteditable="plaintext-only"
20
+ @keydown.enter.prevent
21
+ @paste.prevent="removeNewlines"
22
+ />
23
+ <!-- 文档内容 -->
24
+ <smart-docx-editor v-if="visible" ref="docxEditorRef" .initialValue="content" />
25
+ </div>
26
+ </el-scrollbar>
27
+ </ElDialog>
28
+ </template>
29
+
30
+ <script lang="ts" setup>
31
+ import type { Descendant } from 'slate';
32
+ import { ElDialog } from 'element-plus';
33
+ import { useMessage } from 'naive-ui';
34
+ import { useJsonParse } from '@smartos-lib/utils';
35
+ import { debounce } from 'lodash-es';
36
+ import { useFullscreenElDialog } from '../composables/useFullscreenElDialog';
37
+ import { createOnlineDocx, getOnlineDocx, updateOnlineDocx } from '../apis/doc';
38
+ import { type FileInfo, renameFile } from '../apis/file';
39
+
40
+ const emit = defineEmits(['updated']);
41
+
42
+ const dialogRef = ref<typeof ElDialog>();
43
+ const titleRef = ref<HTMLDivElement>();
44
+ const docxEditorRef = ref();
45
+
46
+ const message = useMessage();
47
+
48
+ const visible = ref(false);
49
+ const id = ref<string>();
50
+ const fileName = ref<string>('');
51
+ const content = shallowRef<Descendant[]>();
52
+
53
+ const updateDocTitleParams = () => ({ userFileId: id.value!, fileName: fileName.value || '未命名文档' });
54
+ const updateDocParams = () => ({ userFileId: id.value!, docContext: { value: content.value } });
55
+
56
+ const createDoc = createOnlineDocx();
57
+ const getDoc = getOnlineDocx();
58
+ const updateDocTitle = renameFile(updateDocTitleParams);
59
+ const updateDoc = updateOnlineDocx(updateDocParams);
60
+
61
+ /** 禁止粘贴换行 */
62
+ function removeNewlines(event: ClipboardEvent) {
63
+ const text = event.clipboardData?.getData('text/plain').replace(/(\r\n|\n|\r)/gm, "") ?? '';
64
+ const selection = window.getSelection();
65
+
66
+ if (selection?.getRangeAt && selection.rangeCount) {
67
+ const range = selection.getRangeAt(0);
68
+ range.deleteContents();
69
+
70
+ const textNode = document.createTextNode(text);
71
+ range.insertNode(textNode);
72
+
73
+ range.setStart(textNode, textNode.length);
74
+ range.setEnd(textNode, textNode.length);
75
+
76
+ selection.removeAllRanges();
77
+ selection.addRange(range);
78
+ }
79
+ }
80
+
81
+ useFullscreenElDialog(dialogRef);
82
+
83
+ useEventListener(titleRef, ['input', 'paste'], () => {
84
+ fileName.value = titleRef.value?.textContent?.trim() || '';
85
+ });
86
+
87
+ useEventListener(docxEditorRef, 'change', (event: CustomEvent<[Descendant[]]>) => {
88
+ content.value = event.detail[0] ?? [];
89
+ });
90
+
91
+ whenever(() => (createDoc.isLoading || getDoc.isLoading), (_, __, onCleanup) => {
92
+ const loading = message.loading(`请稍后, 正在${createDoc.isLoading ? '新建' : '加载'}文档...`, { duration: 0 });
93
+ onCleanup(() => loading.destroy());
94
+ });
95
+
96
+ wheneverEffectScope(id, () => {
97
+ watchDeep(updateDocTitleParams, debounce(updateDocTitle.execute, 1000, { maxWait: 1000 }));
98
+ watchDeep(updateDocParams, debounce(updateDoc.execute, 1000, { maxWait: 1000 }));
99
+ });
100
+
101
+ whenever(logicNot(visible), () => {
102
+ updateDocTitle.execute().then(() => emit('updated'));
103
+ updateDoc.execute();
104
+
105
+ id.value = undefined;
106
+ fileName.value = '';
107
+ content.value = [];
108
+ });
109
+
110
+ defineExpose({
111
+ createDoc: async () => {
112
+ if (createDoc.isLoading) return;
113
+
114
+ id.value = (await createDoc.execute({ fileName: '未命名文档' })).data.data;
115
+ visible.value = true;
116
+ },
117
+ updateDoc: async (file: FileInfo) => {
118
+ if (getDoc.isLoading) return;
119
+
120
+ await getDoc.execute({ userFileId: file.userFileId });
121
+
122
+ id.value = file.userFileId;
123
+ fileName.value = file.fileName;
124
+ content.value = useJsonParse(getDoc.data?.data.docContext, { value: [] }).value;
125
+
126
+ visible.value = true;
127
+ nextTick(() => {
128
+ titleRef.value!.textContent = fileName.value;
129
+ });
130
+ },
131
+ });
132
+ </script>
@@ -0,0 +1,41 @@
1
+ <!--
2
+ 弹出框中的下载功能
3
+ -->
4
+
5
+ <template>
6
+ <div
7
+ class="cp-item relative"
8
+ :class="{
9
+ 'c-emerald-5!': download.isLoading,
10
+ }"
11
+ @click="download.isLoading || download.execute()"
12
+ >
13
+ <div class="icon-wrap">
14
+ <i-line-md-downloading-loop v-if="download.isLoading" class="size-5" />
15
+ <i-material-symbols-download-rounded v-else class="size-6" />
16
+ </div>
17
+ 下载<template v-if="download.isLoading">中</template>
18
+
19
+ <!-- 上传进度 / 背景 -->
20
+ <div h-full pos="absolute top-0 left-0" z--1 bg-neutral-1 transition-width v-bind="progressStyle" />
21
+ <!-- 上传进度 / 进度条 -->
22
+ <div h-2px pos="absolute bottom-0 left-0" b-b="2 solid emerald-5" z-1 transition-width :class="{ 'b-red!': download.error }" v-bind="progressStyle" />
23
+ </div>
24
+ </template>
25
+
26
+ <script lang="ts" setup>
27
+ import type { FileInfo } from '../apis/file';
28
+
29
+ const props = defineProps<{ file: FileInfo }>();
30
+
31
+ const download = toRef(props.file, 'download');
32
+ const progress = computed(() => toValue(props.file.downloadProgress));
33
+
34
+ const progressStyle = computed(() => {
35
+ return download.value.error
36
+ ? { class: 'w-full' }
37
+ : download.value.isLoading
38
+ ? { class: 'min-w-2px', style: { width: `${progress.value}%` } }
39
+ : {};
40
+ });
41
+ </script>
@@ -0,0 +1,156 @@
1
+ <template>
2
+ <el-table
3
+ v-loading="fileList.isLoading"
4
+ class="file-list-table text-sm"
5
+ :data="fileList.data?.data.records" row-key="userFileId" size="large"
6
+ @row-click="toPreview"
7
+ >
8
+ <!-- 预览 -->
9
+ <el-table-column v-slot="{ row }" width="36" class-name="preview">
10
+ <i-ri-file-text-fill v-if="row.fileType === 2" class="size-6 c-blue-5" />
11
+ <template v-else-if="ImageExtEnum.includes(row.extendName)">
12
+ <i-svg-spinners-pulse-2 v-if="row.imagePreview.isLoading" />
13
+ <CustomImage v-else class="size-6" :blob="row.imagePreview.data">
14
+ <template #error>
15
+ <FileIcon class="size-6! text-6!" :ext="row.extendName" />
16
+ </template>
17
+ </CustomImage>
18
+ </template>
19
+ <FileIcon v-else class="size-6! text-6!" :ext="row.extendName" />
20
+ </el-table-column>
21
+ <!-- 名称 -->
22
+ <el-table-column v-slot="{ row }" prop="fileName" label="名称" min-width="200">
23
+ <div flex whitespace-nowrap>
24
+ <div truncate>{{ row.fileName }}</div>
25
+ <div v-if="row.extendName">.{{ row.extendName }}</div>
26
+ </div>
27
+ </el-table-column>
28
+ <!-- 所有者 -->
29
+ <el-table-column prop="userId" label="所有者" width="190" />
30
+ <!-- 大小 -->
31
+ <el-table-column v-slot="{ row }" prop="fileSize" label="大小" width="100">
32
+ {{ isNumber(row.fileSize) ? prettyBytes(row.fileSize) : row.fileSize }}
33
+ </el-table-column>
34
+ <!-- 上传时间 -->
35
+ <el-table-column prop="uploadTime" label="上传时间" width="180" />
36
+ <!-- 操作 -->
37
+ <el-table-column v-slot="{ row }" width="48" fixed="right" class-name="actions">
38
+ <CustomPopover placement="bottom-end" trigger="click" :offset="2">
39
+ <template #reference="{ visible }">
40
+ <button :class="{ 'bg-neutral-2!': visible }" size-6 flex="~ justify-center items-center" bg="transparent hover:neutral-2" transition outline-none rounded @click.stop>
41
+ <i-ph-dots-three-bold class="size-5" />
42
+ </button>
43
+ </template>
44
+
45
+ <DocxDownloadPopoverItem :file="row" />
46
+ <div class="cp-item" @click="toRename(row)"><div class="icon-wrap"><i-ph-note-pencil-duotone class="size-4.8" /></div>重命名</div>
47
+ <div class="cp-item" @click="toDelete(row)"><div class="icon-wrap"><i-material-symbols-delete-outline class="size-5" /></div>删除</div>
48
+ </CustomPopover>
49
+ </el-table-column>
50
+ </el-table>
51
+
52
+ <DocxPreview ref="previewRef" />
53
+ <DocxDoc ref="updateDocRef" @updated="fileList.execute()" />
54
+ </template>
55
+
56
+ <script lang="tsx" setup>
57
+ import type { MessageReactive } from 'naive-ui';
58
+ import type { CancelTokenSource } from 'axios';
59
+ import { ImageExtEnum } from '@smartos-lib/types';
60
+ import { isNumber, leastRun } from 'mixte';
61
+ import { NText, useMessage } from 'naive-ui';
62
+ import prettyBytes from 'pretty-bytes';
63
+ import { pick } from 'lodash-es';
64
+ import { getConfigProvider } from '@smartos-lib/core';
65
+ import { downloadFile, getFileList } from '../apis/file';
66
+ import type { FileInfo } from '../apis/file';
67
+ import { toDeleteFile, toRenameFile } from '../utils/file-actions';
68
+ import FileIcon from './FileIcon.vue';
69
+ import CustomImage from './CustomImage.vue';
70
+ import CustomPopover from './CustomPopover.vue';
71
+ import DocxDownloadPopoverItem from './DocxDownloadPopoverItem.vue';
72
+ import DocxPreview from './DocxPreview.vue';
73
+ import DocxDoc from './DocxDoc.vue';
74
+
75
+ const props = defineProps<{ filePath?: string }>();
76
+
77
+ const previewRef = ref<typeof DocxPreview>();
78
+ const updateDocRef = ref<typeof DocxDoc>();
79
+
80
+ const message = useMessage();
81
+
82
+ const fileList = getFileList(() => ({ filePath: props.filePath }));
83
+
84
+ function toRename(file: FileInfo) {
85
+ toRenameFile(file).then(() => {
86
+ fileList.execute();
87
+ });
88
+ }
89
+
90
+ function toDelete(file: FileInfo) {
91
+ toDeleteFile(file).then(() => {
92
+ fileList.execute();
93
+ });
94
+ }
95
+
96
+ // Todo: 移动到 DocxPreview 组件内
97
+ const toPreview = (() => {
98
+ const previewProgress = ref(0);
99
+ let previewMessage: MessageReactive | undefined;
100
+ let previewCancelToken: CancelTokenSource | undefined;
101
+
102
+ function cancelPreview() {
103
+ previewProgress.value = 0;
104
+ previewMessage?.destroy();
105
+ previewCancelToken?.cancel();
106
+ previewCancelToken = getConfigProvider('request')!.CancelToken.source();
107
+ }
108
+
109
+ return async (file: FileInfo) => {
110
+ cancelPreview();
111
+
112
+ if (file.fileType === 2) {
113
+ updateDocRef.value?.updateDoc(file);
114
+ return;
115
+ }
116
+
117
+ previewMessage = message.loading( // @ts-expect-error
118
+ () => <div>正在加载预览文件: <NText type="primary">{ file.fileFullName }</NText> <NText code>{previewProgress.value}%</NText></div>,
119
+ { duration: 0 },
120
+ );
121
+
122
+ try {
123
+ const download = downloadFile(pick(file, ['userFileId']), { progress: previewProgress, cancelToken: previewCancelToken!.token });
124
+ await leastRun(360, () => download.execute());
125
+ previewRef.value?.previewFile(file.fileFullName, download.data!);
126
+ }
127
+ catch (error) {}
128
+ finally {
129
+ cancelPreview();
130
+ }
131
+ };
132
+ })();
133
+
134
+ watchImmediate(() => props.filePath, () => {
135
+ fileList.execute();
136
+ });
137
+
138
+ defineExpose({
139
+ execute: fileList.execute,
140
+ });
141
+ </script>
142
+
143
+ <style lang="sass" scoped>
144
+ // 预览单元格, 操作单元格
145
+ .file-list-table ::v-deep .smart-table__body-wrapper .smart-table__cell:is(.preview, .actions) > .cell
146
+ @apply size-full flex-(~ justify-center items-center)
147
+ @apply pos-(absolute top-0 left-0) px-0
148
+
149
+ // 预览单元格
150
+ .file-list-table ::v-deep .smart-table__body-wrapper .smart-table__cell.preview > .cell
151
+ @apply justify-end
152
+
153
+ // 表格行
154
+ .file-list-table ::v-deep .smart-table__row
155
+ @apply cursor-pointer
156
+ </style>
@@ -0,0 +1,33 @@
1
+ <!-- 文件预览 -->
2
+
3
+ <template>
4
+ <ElDialog ref="dialogRef" v-model="visible" :title="fileName" :z-index="8888" append-to-body>
5
+ <smart-file-preview v-if="file" ref="filePreviewRef" />
6
+ </ElDialog>
7
+ </template>
8
+
9
+ <script lang="ts" setup>
10
+ import { ElDialog } from 'element-plus';
11
+ import { useFullscreenElDialog } from '../composables/useFullscreenElDialog';
12
+ import type { SmartFilePreviewElement } from '@/web-components/smart-file-preview/index';
13
+
14
+ const dialogRef = ref<typeof ElDialog>();
15
+ const filePreviewRef = ref<SmartFilePreviewElement>();
16
+
17
+ const visible = ref(false);
18
+ const fileName = ref('');
19
+ const file = shallowRef<Blob>();
20
+
21
+ useFullscreenElDialog(dialogRef);
22
+
23
+ defineExpose({
24
+ previewFile: (_fileName: string, _file: Blob) => {
25
+ visible.value = true;
26
+ fileName.value = _fileName;
27
+ file.value = _file;
28
+ nextTick(() => {
29
+ filePreviewRef.value?.previewFile(_fileName, _file);
30
+ });
31
+ },
32
+ });
33
+ </script>