@brftech/filex-core 0.1.56
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.
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/dist/ArchiveViewer-CJdaw5x3.js +99 -0
- package/dist/ArchiveViewer-CJdaw5x3.js.map +1 -0
- package/dist/CsvViewer-QYx_tePQ.js +132 -0
- package/dist/CsvViewer-QYx_tePQ.js.map +1 -0
- package/dist/DrawioViewer-GWt0TyB7.js +141 -0
- package/dist/DrawioViewer-GWt0TyB7.js.map +1 -0
- package/dist/EpubViewer-Je0ywklQ.js +146 -0
- package/dist/EpubViewer-Je0ywklQ.js.map +1 -0
- package/dist/IpynbViewer-TsB4gqP-.js +175 -0
- package/dist/IpynbViewer-TsB4gqP-.js.map +1 -0
- package/dist/MermaidViewer-CXfeDS-7.js +128 -0
- package/dist/MermaidViewer-CXfeDS-7.js.map +1 -0
- package/dist/PsdViewer-VtPpxKVe.js +113 -0
- package/dist/PsdViewer-VtPpxKVe.js.map +1 -0
- package/dist/TiffViewer-OINn9bbb.js +133 -0
- package/dist/TiffViewer-OINn9bbb.js.map +1 -0
- package/dist/UTIF-Cjsrlm0l.js +3104 -0
- package/dist/UTIF-Cjsrlm0l.js.map +1 -0
- package/dist/Viewer3D-xvvyiWHd.js +61 -0
- package/dist/Viewer3D-xvvyiWHd.js.map +1 -0
- package/dist/_commonjsHelpers-DaMA6jEr.js +9 -0
- package/dist/_commonjsHelpers-DaMA6jEr.js.map +1 -0
- package/dist/filex-core.js +26 -0
- package/dist/filex-core.js.map +1 -0
- package/dist/filex-core.umd.cjs +285 -0
- package/dist/filex-core.umd.cjs.map +1 -0
- package/dist/index-CmSYzt6L.js +5404 -0
- package/dist/index-CmSYzt6L.js.map +1 -0
- package/dist/index-UFULWo35.js +10736 -0
- package/dist/index-UFULWo35.js.map +1 -0
- package/dist/index.d.ts +1089 -0
- package/dist/katex-yuB6V-q6.js +11616 -0
- package/dist/katex-yuB6V-q6.js.map +1 -0
- package/dist/papaparse.min-VB1HBwYX.js +441 -0
- package/dist/papaparse.min-VB1HBwYX.js.map +1 -0
- package/dist/style.css +1 -0
- package/dist/useViewerFetch-czqbd2Lj.js +25 -0
- package/dist/useViewerFetch-czqbd2Lj.js.map +1 -0
- package/package.json +113 -0
- package/src/FileExplorer.vue +1872 -0
- package/src/components/Breadcrumb.vue +282 -0
- package/src/components/ContextMenu.vue +152 -0
- package/src/components/GridView.vue +166 -0
- package/src/components/ListView.vue +212 -0
- package/src/components/PendingOpsTray.vue +91 -0
- package/src/components/RecentlyOpened.vue +155 -0
- package/src/components/StarButton.vue +101 -0
- package/src/components/TagPicker.vue +184 -0
- package/src/components/Toolbar.vue +220 -0
- package/src/components/UploadProgress.vue +78 -0
- package/src/composables/useFileApi.ts +692 -0
- package/src/composables/useKeyboardShortcuts.ts +144 -0
- package/src/composables/useLocale.ts +60 -0
- package/src/composables/useMonacoLoader.ts +98 -0
- package/src/composables/usePendingOps.ts +200 -0
- package/src/composables/useSelection.ts +81 -0
- package/src/composables/useUploadChunked.ts +250 -0
- package/src/composables/useViewerFetch.ts +58 -0
- package/src/index.ts +80 -0
- package/src/locales/en.ts +122 -0
- package/src/locales/index.ts +12 -0
- package/src/locales/tr.ts +122 -0
- package/src/modals/ConvertModal.vue +254 -0
- package/src/modals/DeleteConfirmModal.vue +32 -0
- package/src/modals/Modal.vue +104 -0
- package/src/modals/NewFolderModal.vue +64 -0
- package/src/modals/PermissionsModal.vue +815 -0
- package/src/modals/PreviewModal.vue +1070 -0
- package/src/modals/RenameModal.vue +58 -0
- package/src/modals/ShareModal.vue +135 -0
- package/src/styles/base.css +1224 -0
- package/src/styles/variables.css +90 -0
- package/src/types/ExplorerConfig.ts +282 -0
- package/src/types/FileNode.ts +136 -0
- package/src/types/index.ts +27 -0
- package/src/types/peers.d.ts +45 -0
- package/src/viewers/ArchiveViewer.vue +186 -0
- package/src/viewers/CsvViewer.vue +319 -0
- package/src/viewers/DrawioViewer.vue +243 -0
- package/src/viewers/EpubViewer.vue +307 -0
- package/src/viewers/IpynbViewer.vue +365 -0
- package/src/viewers/MermaidViewer.vue +259 -0
- package/src/viewers/PdfViewer.vue +562 -0
- package/src/viewers/PsdViewer.vue +264 -0
- package/src/viewers/TiffViewer.vue +273 -0
- package/src/viewers/Viewer3D.vue +120 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* ArchiveViewer — minimal zip / archive contents preview.
|
|
4
|
+
*
|
|
5
|
+
* Hits `POST /api/files/archive/list` with the adapter-qualified path
|
|
6
|
+
* and renders the member list as a flat table (name, size, mtime).
|
|
7
|
+
* Read-only — extraction is exposed elsewhere (context menu / actions
|
|
8
|
+
* panel). The viewer's job is just "what's inside?" so the user can
|
|
9
|
+
* decide whether to extract or download.
|
|
10
|
+
*/
|
|
11
|
+
import { computed, onMounted, ref, watch } from 'vue';
|
|
12
|
+
|
|
13
|
+
interface ArchiveEntry {
|
|
14
|
+
name: string;
|
|
15
|
+
size: number;
|
|
16
|
+
mtime?: string;
|
|
17
|
+
is_dir?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const props = defineProps<{
|
|
21
|
+
url: string;
|
|
22
|
+
filePath?: string;
|
|
23
|
+
ext: string;
|
|
24
|
+
t?: (key: string) => string;
|
|
25
|
+
authHeaders?: () => Record<string, string>;
|
|
26
|
+
authCredentials?: RequestCredentials;
|
|
27
|
+
}>();
|
|
28
|
+
|
|
29
|
+
const entries = ref<ArchiveEntry[]>([]);
|
|
30
|
+
const loading = ref(true);
|
|
31
|
+
const error = ref<string | null>(null);
|
|
32
|
+
|
|
33
|
+
function tt(key: string, fallback: string): string {
|
|
34
|
+
return props.t ? props.t(key) : fallback;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function fmtSize(n: number): string {
|
|
38
|
+
if (n < 1024) return `${n} B`;
|
|
39
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
40
|
+
if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
41
|
+
return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function load(): Promise<void> {
|
|
45
|
+
loading.value = true;
|
|
46
|
+
error.value = null;
|
|
47
|
+
entries.value = [];
|
|
48
|
+
// The viewer is mounted with the resolved file path. Hit the API
|
|
49
|
+
// endpoint with the same adapter-qualified path the preview URL
|
|
50
|
+
// points at — the backend resolves storage + relative path itself.
|
|
51
|
+
if (!props.filePath) {
|
|
52
|
+
error.value = tt('viewer.archive.error', 'Could not read archive contents.');
|
|
53
|
+
loading.value = false;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetch('/api/files/archive/list', {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
credentials: props.authCredentials || 'include',
|
|
60
|
+
headers: {
|
|
61
|
+
'Content-Type': 'application/json',
|
|
62
|
+
...(props.authHeaders ? props.authHeaders() : {}),
|
|
63
|
+
},
|
|
64
|
+
body: JSON.stringify({ path: props.filePath }),
|
|
65
|
+
});
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
throw new Error(`${res.status} ${res.statusText}`);
|
|
68
|
+
}
|
|
69
|
+
const body = (await res.json()) as { entries?: ArchiveEntry[] };
|
|
70
|
+
entries.value = body.entries ?? [];
|
|
71
|
+
} catch (err) {
|
|
72
|
+
error.value = err instanceof Error ? err.message : tt('viewer.archive.error', 'Could not read archive contents.');
|
|
73
|
+
} finally {
|
|
74
|
+
loading.value = false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
onMounted(load);
|
|
79
|
+
watch(() => props.filePath, load);
|
|
80
|
+
|
|
81
|
+
const totalSize = computed(() => entries.value.reduce((sum, e) => sum + (e.size || 0), 0));
|
|
82
|
+
const fileCount = computed(() => entries.value.filter((e) => !e.is_dir).length);
|
|
83
|
+
</script>
|
|
84
|
+
|
|
85
|
+
<template>
|
|
86
|
+
<div class="filex-viewer-archive">
|
|
87
|
+
<div v-if="error" class="filex-viewer-fallback">
|
|
88
|
+
<span class="filex-viewer-fallback__icon">🗜️</span>
|
|
89
|
+
<p>{{ error }}</p>
|
|
90
|
+
</div>
|
|
91
|
+
<div v-else-if="loading" class="filex-viewer-fallback">
|
|
92
|
+
<span class="filex-viewer-fallback__icon">⏳</span>
|
|
93
|
+
<p>{{ tt('viewer.loading', 'Loading…') }}</p>
|
|
94
|
+
</div>
|
|
95
|
+
<div v-else-if="entries.length === 0" class="filex-viewer-fallback">
|
|
96
|
+
<span class="filex-viewer-fallback__icon">🗜️</span>
|
|
97
|
+
<p>{{ tt('viewer.archive.empty', 'Archive is empty.') }}</p>
|
|
98
|
+
</div>
|
|
99
|
+
<div v-else class="filex-viewer-archive__pane">
|
|
100
|
+
<div class="filex-viewer-archive__summary">
|
|
101
|
+
{{ fileCount }} {{ tt('viewer.archive.entries', '{n} files').replace('{n}', String(fileCount)) }}
|
|
102
|
+
· {{ fmtSize(totalSize) }}
|
|
103
|
+
</div>
|
|
104
|
+
<table class="filex-viewer-archive__table">
|
|
105
|
+
<thead>
|
|
106
|
+
<tr>
|
|
107
|
+
<th>{{ tt('viewer.name', 'Name') }}</th>
|
|
108
|
+
<th class="filex-viewer-archive__size">{{ tt('viewer.size', 'Size') }}</th>
|
|
109
|
+
</tr>
|
|
110
|
+
</thead>
|
|
111
|
+
<tbody>
|
|
112
|
+
<tr v-for="(e, i) in entries" :key="i" :data-dir="e.is_dir ? '1' : '0'">
|
|
113
|
+
<td>
|
|
114
|
+
<span class="filex-viewer-archive__icon">{{ e.is_dir ? '📁' : '📄' }}</span>
|
|
115
|
+
{{ e.name }}
|
|
116
|
+
</td>
|
|
117
|
+
<td class="filex-viewer-archive__size">{{ e.is_dir ? '' : fmtSize(e.size) }}</td>
|
|
118
|
+
</tr>
|
|
119
|
+
</tbody>
|
|
120
|
+
</table>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</template>
|
|
124
|
+
|
|
125
|
+
<style scoped>
|
|
126
|
+
.filex-viewer-archive {
|
|
127
|
+
display: flex;
|
|
128
|
+
flex-direction: column;
|
|
129
|
+
width: 100%;
|
|
130
|
+
height: 100%;
|
|
131
|
+
min-height: 70vh;
|
|
132
|
+
background: var(--fe-bg, #fff);
|
|
133
|
+
color: var(--fe-text, #1a1e27);
|
|
134
|
+
}
|
|
135
|
+
.filex-viewer-archive__pane {
|
|
136
|
+
flex: 1;
|
|
137
|
+
overflow: auto;
|
|
138
|
+
padding: 16px 20px;
|
|
139
|
+
}
|
|
140
|
+
.filex-viewer-archive__summary {
|
|
141
|
+
font-size: 12px;
|
|
142
|
+
color: var(--fe-text-muted, #5a6475);
|
|
143
|
+
margin-bottom: 12px;
|
|
144
|
+
font-variant-numeric: tabular-nums;
|
|
145
|
+
}
|
|
146
|
+
.filex-viewer-archive__table {
|
|
147
|
+
width: 100%;
|
|
148
|
+
border-collapse: collapse;
|
|
149
|
+
font-size: 13px;
|
|
150
|
+
font-family: var(--fe-font-mono, monospace);
|
|
151
|
+
}
|
|
152
|
+
.filex-viewer-archive__table th,
|
|
153
|
+
.filex-viewer-archive__table td {
|
|
154
|
+
padding: 6px 12px;
|
|
155
|
+
border-bottom: 1px solid var(--fe-border, #e2e6ed);
|
|
156
|
+
text-align: left;
|
|
157
|
+
}
|
|
158
|
+
.filex-viewer-archive__table th {
|
|
159
|
+
background: var(--fe-bg-elev, #f7f8fa);
|
|
160
|
+
font-weight: 600;
|
|
161
|
+
position: sticky;
|
|
162
|
+
top: 0;
|
|
163
|
+
}
|
|
164
|
+
.filex-viewer-archive__size {
|
|
165
|
+
text-align: right;
|
|
166
|
+
font-variant-numeric: tabular-nums;
|
|
167
|
+
white-space: nowrap;
|
|
168
|
+
color: var(--fe-text-muted, #5a6475);
|
|
169
|
+
}
|
|
170
|
+
.filex-viewer-archive__icon {
|
|
171
|
+
margin-right: 6px;
|
|
172
|
+
}
|
|
173
|
+
.filex-viewer-archive__table tr[data-dir="1"] {
|
|
174
|
+
color: var(--fe-text-muted, #5a6475);
|
|
175
|
+
}
|
|
176
|
+
.filex-viewer-fallback {
|
|
177
|
+
text-align: center;
|
|
178
|
+
padding: 32px;
|
|
179
|
+
color: var(--fe-text-muted, #5a6475);
|
|
180
|
+
}
|
|
181
|
+
.filex-viewer-fallback__icon {
|
|
182
|
+
font-size: 48px;
|
|
183
|
+
display: block;
|
|
184
|
+
margin-bottom: 12px;
|
|
185
|
+
}
|
|
186
|
+
</style>
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* CsvViewer — lightweight CSV / TSV table preview.
|
|
4
|
+
*
|
|
5
|
+
* Lazy-imports `papaparse` when available (best-quality parser — handles
|
|
6
|
+
* embedded quotes, multi-line cells, etc). Falls back to a simple
|
|
7
|
+
* `split` parser when the peer is missing — workable for the trivial
|
|
8
|
+
* "first 1000 rows" preview case but loses fidelity on complex files.
|
|
9
|
+
*
|
|
10
|
+
* UX:
|
|
11
|
+
* - first-row-as-header toggle
|
|
12
|
+
* - filter input that case-insensitively matches any cell
|
|
13
|
+
* - 100-rows-per-page pagination
|
|
14
|
+
* - tab vs comma auto-detection (manual override via toolbar)
|
|
15
|
+
*/
|
|
16
|
+
import { computed, onMounted, ref, watch } from 'vue';
|
|
17
|
+
import { fetchViewerText } from '../composables/useViewerFetch';
|
|
18
|
+
|
|
19
|
+
const props = defineProps<{
|
|
20
|
+
url: string;
|
|
21
|
+
ext: string;
|
|
22
|
+
t?: (key: string) => string;
|
|
23
|
+
authHeaders?: () => Record<string, string>;
|
|
24
|
+
authCredentials?: RequestCredentials;
|
|
25
|
+
}>();
|
|
26
|
+
|
|
27
|
+
const PAGE_SIZE = 100;
|
|
28
|
+
const ROW_LIMIT = 1000;
|
|
29
|
+
|
|
30
|
+
const rows = ref<string[][]>([]);
|
|
31
|
+
const error = ref<string | null>(null);
|
|
32
|
+
const loading = ref(true);
|
|
33
|
+
const firstRowHeader = ref(true);
|
|
34
|
+
const filter = ref('');
|
|
35
|
+
const page = ref(1);
|
|
36
|
+
const detectedDelim = ref<',' | '\t' | ';'>(',');
|
|
37
|
+
const userDelim = ref<'auto' | ',' | '\t' | ';'>('auto');
|
|
38
|
+
|
|
39
|
+
let renderToken = 0;
|
|
40
|
+
|
|
41
|
+
function detectDelimiter(sample: string): ',' | '\t' | ';' {
|
|
42
|
+
const sampleLines = sample.split(/\r?\n/).slice(0, 5).join('\n');
|
|
43
|
+
const tab = (sampleLines.match(/\t/g) || []).length;
|
|
44
|
+
const semi = (sampleLines.match(/;/g) || []).length;
|
|
45
|
+
const comma = (sampleLines.match(/,/g) || []).length;
|
|
46
|
+
if (tab > comma && tab > semi) return '\t';
|
|
47
|
+
if (semi > comma && semi > tab) return ';';
|
|
48
|
+
return ',';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function parseWith(text: string, delim: string): Promise<string[][]> {
|
|
52
|
+
try {
|
|
53
|
+
const mod = await import(/* @vite-ignore */ 'papaparse');
|
|
54
|
+
const Papa = (mod as any).default ?? mod;
|
|
55
|
+
const result = Papa.parse(text, {
|
|
56
|
+
delimiter: delim,
|
|
57
|
+
skipEmptyLines: true,
|
|
58
|
+
header: false,
|
|
59
|
+
});
|
|
60
|
+
return (result.data as string[][]).slice(0, ROW_LIMIT);
|
|
61
|
+
} catch {
|
|
62
|
+
// Fallback: naive line/column split. Loses quoted-comma support
|
|
63
|
+
// but renders something reasonable.
|
|
64
|
+
return text
|
|
65
|
+
.split(/\r?\n/)
|
|
66
|
+
.filter((l) => l.length > 0)
|
|
67
|
+
.slice(0, ROW_LIMIT)
|
|
68
|
+
.map((line) => line.split(delim));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function load(): Promise<void> {
|
|
73
|
+
loading.value = true;
|
|
74
|
+
error.value = null;
|
|
75
|
+
rows.value = [];
|
|
76
|
+
page.value = 1;
|
|
77
|
+
const myToken = ++renderToken;
|
|
78
|
+
|
|
79
|
+
let text: string;
|
|
80
|
+
try {
|
|
81
|
+
text = await fetchViewerText({
|
|
82
|
+
url: props.url,
|
|
83
|
+
headers: props.authHeaders?.() ?? {},
|
|
84
|
+
credentials: props.authCredentials,
|
|
85
|
+
});
|
|
86
|
+
} catch (err) {
|
|
87
|
+
error.value = err instanceof Error ? err.message : 'fetch failed';
|
|
88
|
+
loading.value = false;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (myToken !== renderToken) return;
|
|
93
|
+
|
|
94
|
+
if (props.ext === 'tsv') {
|
|
95
|
+
detectedDelim.value = '\t';
|
|
96
|
+
} else {
|
|
97
|
+
detectedDelim.value = detectDelimiter(text.slice(0, 4096));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const delim = userDelim.value === 'auto' ? detectedDelim.value : userDelim.value;
|
|
101
|
+
rows.value = await parseWith(text, delim);
|
|
102
|
+
loading.value = false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
onMounted(load);
|
|
106
|
+
watch(() => props.url, load);
|
|
107
|
+
watch(() => userDelim.value, () => {
|
|
108
|
+
if (rows.value.length === 0) return;
|
|
109
|
+
// Re-parse with the new delimiter using the cached source — but we
|
|
110
|
+
// didn't keep it. Cheapest path is a refetch.
|
|
111
|
+
load();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const headers = computed<string[]>(() => {
|
|
115
|
+
if (!firstRowHeader.value || rows.value.length === 0) {
|
|
116
|
+
if (rows.value.length === 0) return [];
|
|
117
|
+
return rows.value[0].map((_, i) => `Col ${i + 1}`);
|
|
118
|
+
}
|
|
119
|
+
return rows.value[0];
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const dataRows = computed<string[][]>(() => {
|
|
123
|
+
return firstRowHeader.value ? rows.value.slice(1) : rows.value;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const filtered = computed<string[][]>(() => {
|
|
127
|
+
const q = filter.value.trim().toLowerCase();
|
|
128
|
+
if (!q) return dataRows.value;
|
|
129
|
+
return dataRows.value.filter((r) =>
|
|
130
|
+
r.some((c) => (c || '').toLowerCase().includes(q)),
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const totalPages = computed(() =>
|
|
135
|
+
Math.max(1, Math.ceil(filtered.value.length / PAGE_SIZE)),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const visibleRows = computed<string[][]>(() => {
|
|
139
|
+
const start = (page.value - 1) * PAGE_SIZE;
|
|
140
|
+
return filtered.value.slice(start, start + PAGE_SIZE);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
watch(filter, () => {
|
|
144
|
+
page.value = 1;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
function tt(key: string, fallback: string): string {
|
|
148
|
+
return props.t ? props.t(key) : fallback;
|
|
149
|
+
}
|
|
150
|
+
</script>
|
|
151
|
+
|
|
152
|
+
<template>
|
|
153
|
+
<div class="filex-viewer-csv">
|
|
154
|
+
<div class="filex-viewer-csv__pane">
|
|
155
|
+
<div v-if="error" class="filex-viewer-fallback">
|
|
156
|
+
<span class="filex-viewer-fallback__icon">📊</span>
|
|
157
|
+
<p>{{ error }}</p>
|
|
158
|
+
</div>
|
|
159
|
+
<div v-else-if="loading" class="filex-viewer-fallback">
|
|
160
|
+
<span class="filex-viewer-fallback__icon">⏳</span>
|
|
161
|
+
<p>{{ tt('viewer.loading', 'Loading…') }}</p>
|
|
162
|
+
</div>
|
|
163
|
+
<table v-else class="filex-viewer-csv__table">
|
|
164
|
+
<thead>
|
|
165
|
+
<tr>
|
|
166
|
+
<th class="filex-viewer-csv__rownum">#</th>
|
|
167
|
+
<th v-for="(h, i) in headers" :key="i">{{ h }}</th>
|
|
168
|
+
</tr>
|
|
169
|
+
</thead>
|
|
170
|
+
<tbody>
|
|
171
|
+
<tr v-for="(row, ri) in visibleRows" :key="ri">
|
|
172
|
+
<td class="filex-viewer-csv__rownum">{{ (page - 1) * 100 + ri + 1 }}</td>
|
|
173
|
+
<td v-for="(c, ci) in row" :key="ci">{{ c }}</td>
|
|
174
|
+
</tr>
|
|
175
|
+
</tbody>
|
|
176
|
+
</table>
|
|
177
|
+
</div>
|
|
178
|
+
<div v-if="!loading && !error && totalPages > 1" class="filex-viewer-csv__pager">
|
|
179
|
+
<button
|
|
180
|
+
type="button"
|
|
181
|
+
class="filex-viewer-btn"
|
|
182
|
+
:disabled="page <= 1"
|
|
183
|
+
@click="page--"
|
|
184
|
+
>‹</button>
|
|
185
|
+
<span class="filex-viewer-csv__pageno">{{ page }} / {{ totalPages }} ({{ filtered.length }} satır)</span>
|
|
186
|
+
<button
|
|
187
|
+
type="button"
|
|
188
|
+
class="filex-viewer-btn"
|
|
189
|
+
:disabled="page >= totalPages"
|
|
190
|
+
@click="page++"
|
|
191
|
+
>›</button>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
</template>
|
|
195
|
+
|
|
196
|
+
<style scoped>
|
|
197
|
+
.filex-viewer-csv {
|
|
198
|
+
display: flex;
|
|
199
|
+
flex-direction: column;
|
|
200
|
+
width: 100%;
|
|
201
|
+
height: 100%;
|
|
202
|
+
min-height: 70vh;
|
|
203
|
+
background: var(--fe-bg, #fff);
|
|
204
|
+
color: var(--fe-text, #1a1e27);
|
|
205
|
+
}
|
|
206
|
+
.filex-viewer-csv__bar {
|
|
207
|
+
display: flex;
|
|
208
|
+
align-items: center;
|
|
209
|
+
gap: 12px;
|
|
210
|
+
padding: 8px 12px;
|
|
211
|
+
background: var(--fe-bg-elev, #f7f8fa);
|
|
212
|
+
border-bottom: 1px solid var(--fe-border, #e2e6ed);
|
|
213
|
+
font-size: 12px;
|
|
214
|
+
}
|
|
215
|
+
.filex-viewer-csv__check {
|
|
216
|
+
display: flex;
|
|
217
|
+
align-items: center;
|
|
218
|
+
gap: 4px;
|
|
219
|
+
font-size: 12px;
|
|
220
|
+
}
|
|
221
|
+
.filex-viewer-csv__select,
|
|
222
|
+
.filex-viewer-csv__filter {
|
|
223
|
+
border: 1px solid var(--fe-border, #e2e6ed);
|
|
224
|
+
background: var(--fe-bg, #fff);
|
|
225
|
+
color: inherit;
|
|
226
|
+
padding: 4px 8px;
|
|
227
|
+
border-radius: 4px;
|
|
228
|
+
font: inherit;
|
|
229
|
+
font-size: 12px;
|
|
230
|
+
}
|
|
231
|
+
.filex-viewer-csv__filter {
|
|
232
|
+
flex: 0 1 240px;
|
|
233
|
+
}
|
|
234
|
+
.filex-viewer-spacer { flex: 1; }
|
|
235
|
+
.filex-viewer-csv__count {
|
|
236
|
+
font-size: 11px;
|
|
237
|
+
color: var(--fe-text-muted, #5a6475);
|
|
238
|
+
font-variant-numeric: tabular-nums;
|
|
239
|
+
}
|
|
240
|
+
.filex-viewer-csv__pane {
|
|
241
|
+
flex: 1;
|
|
242
|
+
overflow: auto;
|
|
243
|
+
}
|
|
244
|
+
.filex-viewer-csv__table {
|
|
245
|
+
border-collapse: collapse;
|
|
246
|
+
width: max-content;
|
|
247
|
+
min-width: 100%;
|
|
248
|
+
font-size: 12px;
|
|
249
|
+
font-family: var(--fe-font-mono, monospace);
|
|
250
|
+
}
|
|
251
|
+
.filex-viewer-csv__table th,
|
|
252
|
+
.filex-viewer-csv__table td {
|
|
253
|
+
padding: 4px 10px;
|
|
254
|
+
border: 1px solid var(--fe-border, #e2e6ed);
|
|
255
|
+
white-space: nowrap;
|
|
256
|
+
max-width: 320px;
|
|
257
|
+
overflow: hidden;
|
|
258
|
+
text-overflow: ellipsis;
|
|
259
|
+
}
|
|
260
|
+
.filex-viewer-csv__table th {
|
|
261
|
+
background: var(--fe-bg-elev, #f7f8fa);
|
|
262
|
+
position: sticky;
|
|
263
|
+
top: 0;
|
|
264
|
+
font-weight: 600;
|
|
265
|
+
text-align: left;
|
|
266
|
+
}
|
|
267
|
+
.filex-viewer-csv__table tbody tr:nth-child(even) {
|
|
268
|
+
background: var(--fe-bg-hover, rgba(0, 0, 0, 0.02));
|
|
269
|
+
}
|
|
270
|
+
.filex-viewer-csv__rownum {
|
|
271
|
+
color: var(--fe-text-muted, #5a6475);
|
|
272
|
+
text-align: right;
|
|
273
|
+
background: var(--fe-bg-elev, #f7f8fa);
|
|
274
|
+
font-variant-numeric: tabular-nums;
|
|
275
|
+
position: sticky;
|
|
276
|
+
left: 0;
|
|
277
|
+
}
|
|
278
|
+
.filex-viewer-csv__pager {
|
|
279
|
+
display: flex;
|
|
280
|
+
justify-content: center;
|
|
281
|
+
align-items: center;
|
|
282
|
+
gap: 8px;
|
|
283
|
+
padding: 6px;
|
|
284
|
+
background: var(--fe-bg-elev, #f7f8fa);
|
|
285
|
+
border-top: 1px solid var(--fe-border, #e2e6ed);
|
|
286
|
+
font-size: 12px;
|
|
287
|
+
}
|
|
288
|
+
.filex-viewer-csv__pageno {
|
|
289
|
+
font-variant-numeric: tabular-nums;
|
|
290
|
+
font-size: 12px;
|
|
291
|
+
}
|
|
292
|
+
.filex-viewer-fallback {
|
|
293
|
+
text-align: center;
|
|
294
|
+
padding: 32px;
|
|
295
|
+
color: var(--fe-text-muted, #5a6475);
|
|
296
|
+
}
|
|
297
|
+
.filex-viewer-fallback__icon {
|
|
298
|
+
font-size: 48px;
|
|
299
|
+
display: block;
|
|
300
|
+
margin-bottom: 12px;
|
|
301
|
+
}
|
|
302
|
+
.filex-viewer-btn {
|
|
303
|
+
border: 1px solid var(--fe-border, #e2e6ed);
|
|
304
|
+
background: var(--fe-bg, #fff);
|
|
305
|
+
color: var(--fe-text, #1a1e27);
|
|
306
|
+
padding: 4px 10px;
|
|
307
|
+
border-radius: 4px;
|
|
308
|
+
cursor: pointer;
|
|
309
|
+
font: inherit;
|
|
310
|
+
font-size: 12px;
|
|
311
|
+
}
|
|
312
|
+
.filex-viewer-btn:hover:not(:disabled) {
|
|
313
|
+
background: var(--fe-bg-hover, #edf0f5);
|
|
314
|
+
}
|
|
315
|
+
.filex-viewer-btn:disabled {
|
|
316
|
+
opacity: 0.4;
|
|
317
|
+
cursor: not-allowed;
|
|
318
|
+
}
|
|
319
|
+
</style>
|