@smartos-lib/components 1.7.0-beta.0

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 (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>