first commit
This commit is contained in:
commit
e8ec46d19f
60 changed files with 13652 additions and 0 deletions
32
src/stores/index.ts
Normal file
32
src/stores/index.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { store } from 'quasar/wrappers';
|
||||
import { createPinia } from 'pinia';
|
||||
import { Router } from 'vue-router';
|
||||
|
||||
/*
|
||||
* When adding new properties to stores, you should also
|
||||
* extend the `PiniaCustomProperties` interface.
|
||||
* @see https://pinia.vuejs.org/core-concepts/plugins.html#typing-new-store-properties
|
||||
*/
|
||||
declare module 'pinia' {
|
||||
export interface PiniaCustomProperties {
|
||||
readonly router: Router;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* If not building with SSR mode, you can
|
||||
* directly export the Store instantiation;
|
||||
*
|
||||
* The function below can be async too; either use
|
||||
* async/await or return a Promise which resolves
|
||||
* with the Store instance.
|
||||
*/
|
||||
|
||||
export default store((/* { ssrContext } */) => {
|
||||
const pinia = createPinia();
|
||||
|
||||
// You can add Pinia plugins here
|
||||
// pinia.use(SomePiniaPlugin)
|
||||
|
||||
return pinia;
|
||||
});
|
||||
21
src/stores/loader/index.ts
Normal file
21
src/stores/loader/index.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const useLoader = defineStore('loader-store', () => {
|
||||
const visible = ref(false);
|
||||
|
||||
return {
|
||||
visible,
|
||||
toggle() {
|
||||
visible.value = !visible.value;
|
||||
},
|
||||
show() {
|
||||
visible.value = true;
|
||||
},
|
||||
hide() {
|
||||
visible.value = false;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default useLoader;
|
||||
558
src/stores/storage/index.ts
Normal file
558
src/stores/storage/index.ts
Normal file
|
|
@ -0,0 +1,558 @@
|
|||
import { computed, reactive, ref } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { io } from 'socket.io-client';
|
||||
import axios from 'axios';
|
||||
|
||||
import { api } from 'src/boot/axios';
|
||||
|
||||
import useLoader from '../loader';
|
||||
import { useUtils } from '../utils';
|
||||
|
||||
type Path = string;
|
||||
|
||||
export interface StorageFolder {
|
||||
pathname: string;
|
||||
name: string;
|
||||
size?: number;
|
||||
createdAt: string;
|
||||
createdBy: string;
|
||||
}
|
||||
|
||||
export interface StorageFile {
|
||||
pathname: string;
|
||||
path: string;
|
||||
fileName: string;
|
||||
fileSize: number;
|
||||
fileType: string;
|
||||
title: string;
|
||||
description: string;
|
||||
author: string;
|
||||
category: string[];
|
||||
keyword: string[];
|
||||
updatedAt: string;
|
||||
updatedBy: string;
|
||||
createdAt: string;
|
||||
createdBy: string;
|
||||
}
|
||||
|
||||
export interface Structure extends StorageFolder {
|
||||
folder: Structure[];
|
||||
file: StorageFile[];
|
||||
}
|
||||
|
||||
type Tree = Structure[];
|
||||
|
||||
export type SearchOptions = {
|
||||
path?: string[];
|
||||
recursive?: boolean;
|
||||
exact?: boolean;
|
||||
within?: string;
|
||||
};
|
||||
|
||||
export type SearchInfo = {
|
||||
field: string;
|
||||
value: string;
|
||||
exact?: boolean;
|
||||
};
|
||||
|
||||
export type SearchOperator = {
|
||||
AND?: (SearchInfo | SearchOperator)[];
|
||||
OR?: (SearchInfo | SearchOperator)[];
|
||||
};
|
||||
|
||||
export type FileMetadata = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
category?: string[];
|
||||
keyword?: string[];
|
||||
author?: string;
|
||||
};
|
||||
|
||||
function normalizePath(path: string | string[]) {
|
||||
return Array.isArray(path)
|
||||
? path.join('/') + '/'
|
||||
: path.split('/').filter(Boolean).join('/') + '/';
|
||||
}
|
||||
|
||||
const useStorage = defineStore('storageStore', () => {
|
||||
const utils = useUtils();
|
||||
const loader = useLoader();
|
||||
const init = ref(false);
|
||||
const folder = ref<Record<Path, StorageFolder[]>>({});
|
||||
const file = ref<Record<Path, StorageFile[]>>({});
|
||||
const tree = computed(() => {
|
||||
let structure: Tree = [];
|
||||
|
||||
// parse list of folder and list of file into tree
|
||||
Object.entries(folder.value).forEach(([key, value]) => {
|
||||
const arr = key.split('/').filter(Boolean);
|
||||
|
||||
// Once run then it is init
|
||||
if (!init.value) init.value = true;
|
||||
|
||||
if (arr.length === 0) {
|
||||
structure = value.map((v) => ({
|
||||
pathname: v.pathname,
|
||||
name: v.name,
|
||||
createdAt: v.createdAt,
|
||||
createdBy: v.createdBy,
|
||||
folder: [],
|
||||
file: [],
|
||||
}));
|
||||
} else {
|
||||
let current: Structure | undefined;
|
||||
|
||||
// traverse into tree
|
||||
arr.forEach((v, i) => {
|
||||
current =
|
||||
i === 0
|
||||
? structure.find((x) => x.name === v)
|
||||
: current?.folder.find((x) => x.name === v);
|
||||
});
|
||||
|
||||
// set data in tree (object is references to the same object)
|
||||
if (current) {
|
||||
current.folder = value.map((v) => ({
|
||||
pathname: v.pathname,
|
||||
name: v.name,
|
||||
createdAt: v.createdAt,
|
||||
createdBy: v.createdBy,
|
||||
folder: [],
|
||||
file: [],
|
||||
}));
|
||||
current.file = file.value[key] ?? [];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return structure;
|
||||
});
|
||||
const currentFile = ref<StorageFile>();
|
||||
const currentInfo = reactive<{
|
||||
path: string;
|
||||
dept: number;
|
||||
}>({
|
||||
path: '',
|
||||
dept: 0,
|
||||
});
|
||||
|
||||
if (!init.value) goto(sessionStorage.getItem('path') || '');
|
||||
|
||||
async function getStorage(path: string = '') {
|
||||
const arr = path.split('/').filter(Boolean);
|
||||
|
||||
const res = await api.post<(typeof folder.value)[string]>('/storage/list', {
|
||||
operation: 'folder',
|
||||
path: arr,
|
||||
});
|
||||
if (res.status === 200 && res.data && Array.isArray(res.data))
|
||||
folder.value[normalizePath(path)] = res.data.sort((a, b) =>
|
||||
a.pathname.localeCompare(b.pathname),
|
||||
);
|
||||
}
|
||||
|
||||
async function getStorageFile(path: string = '') {
|
||||
const arr = path.split('/').filter(Boolean);
|
||||
|
||||
const res = await api.post<(typeof file.value)[string]>('/storage/list', {
|
||||
operation: 'file',
|
||||
path: arr,
|
||||
});
|
||||
if (res.status === 200 && res.data && Array.isArray(res.data))
|
||||
file.value[normalizePath(path)] = res.data.sort((a, b) =>
|
||||
a.pathname.localeCompare(b.pathname),
|
||||
);
|
||||
}
|
||||
|
||||
async function goto(path: string = '', force = false) {
|
||||
loader.show();
|
||||
const arr = path.split('/').filter(Boolean);
|
||||
|
||||
// get all parent to the root structure
|
||||
// this will also triggher init structure as it get root structure
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
const current = normalizePath(arr.slice(0, i - arr.length));
|
||||
if (!folder.value[current] || force) await getStorage(current);
|
||||
}
|
||||
|
||||
path = normalizePath(path);
|
||||
|
||||
// only get this path once, after that will get from socket.io-client instead
|
||||
if (!folder.value[path] || force) await getStorage(path);
|
||||
if (!file.value[path] || force) await getStorageFile(path);
|
||||
|
||||
currentFile.value = undefined;
|
||||
currentInfo.path = path;
|
||||
currentInfo.dept = path.split('/').filter(Boolean).length;
|
||||
sessionStorage.setItem('path', path);
|
||||
loader.hide();
|
||||
}
|
||||
|
||||
async function gotoParent() {
|
||||
const arr = currentInfo.path.split('/').filter(Boolean);
|
||||
await goto(normalizePath(arr.slice(0, -1)));
|
||||
}
|
||||
|
||||
// socket.io zone
|
||||
const socket = io(new URL(import.meta.env.VITE_API_BASE_URL).origin);
|
||||
|
||||
socket.on('connect', () => console.info('Socket.io connected.'));
|
||||
socket.on('disconnect', () => console.info('Socket.io disconnected.'));
|
||||
socket.on(
|
||||
'FolderCreate',
|
||||
(data: {
|
||||
pathname: string;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
createdBy: string;
|
||||
}) => {
|
||||
const arr = data.pathname.split('/').filter(Boolean);
|
||||
const path = normalizePath(arr.slice(0, -1));
|
||||
|
||||
if (folder.value[path]) {
|
||||
const idx = folder.value[path].findIndex(
|
||||
(v) => v.pathname === data.pathname,
|
||||
);
|
||||
|
||||
if (idx === -1) {
|
||||
folder.value[path].push({
|
||||
pathname: data.pathname,
|
||||
name: data.name,
|
||||
createdAt: data.createdAt,
|
||||
createdBy: data.createdBy,
|
||||
});
|
||||
}
|
||||
folder.value[path].sort((a, b) => a.pathname.localeCompare(b.pathname));
|
||||
}
|
||||
},
|
||||
);
|
||||
// NOTE:
|
||||
// API planned to make new endpoint that can move and rename in one go.
|
||||
// Need to change if api handle move and rename file instead of just edit.
|
||||
socket.on('FolderMove', (data: { from: string; to: string }) => {
|
||||
const src = data.from.split('/').filter(Boolean);
|
||||
const dst = data.to.split('/').filter(Boolean);
|
||||
const path = normalizePath(src.slice(0, -1));
|
||||
|
||||
if (folder.value[path]) {
|
||||
const val = folder.value[path].find((v) => v.pathname === data.from);
|
||||
|
||||
if (val) {
|
||||
val.pathname = data.to;
|
||||
val.name = dst[dst.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
const regex = new RegExp(`^${data.from}`);
|
||||
|
||||
for (const key in folder.value) {
|
||||
if (key.startsWith(data.from)) {
|
||||
folder.value[key.replace(regex, data.to)] = folder.value[key].map(
|
||||
(v) => {
|
||||
v.pathname = v.pathname.replace(regex, data.to);
|
||||
return v;
|
||||
},
|
||||
);
|
||||
delete folder.value[key];
|
||||
}
|
||||
}
|
||||
for (const key in file.value) {
|
||||
if (key.startsWith(data.from)) {
|
||||
file.value[key.replace(regex, data.to)] = file.value[key].map((v) => {
|
||||
v.pathname = v.pathname.replace(regex, data.to);
|
||||
return v;
|
||||
});
|
||||
delete file.value[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
socket.on('FolderDelete', (data: { pathname: string }) => {
|
||||
for (const key in folder.value) {
|
||||
if (key.startsWith(data.pathname)) {
|
||||
delete folder.value[key];
|
||||
}
|
||||
}
|
||||
for (const key in file.value) {
|
||||
if (key.startsWith(data.pathname)) {
|
||||
delete file.value[key];
|
||||
}
|
||||
}
|
||||
|
||||
const arr = data.pathname.split('/').filter(Boolean);
|
||||
const path = normalizePath(arr.slice(0, -1));
|
||||
|
||||
if (folder.value[path]) {
|
||||
folder.value[path] = folder.value[path].filter(
|
||||
(v) => v.pathname !== data.pathname,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
currentInfo.path.length >= normalizePath(arr).length &&
|
||||
currentInfo.path.startsWith(normalizePath(arr))
|
||||
) {
|
||||
utils.openDialog({
|
||||
title: 'แจ้งเตือน',
|
||||
message: 'ข้อมูลที่คุณกำลังเข้าถึงอยู่ถูกลบ',
|
||||
});
|
||||
goto();
|
||||
}
|
||||
});
|
||||
socket.on('FileUpload', (data: StorageFile) => {
|
||||
const arr = data.pathname.split('/').filter(Boolean);
|
||||
const path = normalizePath(arr.slice(0, -1));
|
||||
|
||||
if (file.value[path]) {
|
||||
const idx = file.value[path].findIndex(
|
||||
(v) => v.pathname === data.pathname,
|
||||
);
|
||||
if (idx !== -1) file.value[path][idx] = data;
|
||||
else file.value[path].push(data);
|
||||
|
||||
file.value[path].sort((a, b) => a.pathname.localeCompare(b.pathname));
|
||||
}
|
||||
});
|
||||
socket.on('FileDelete', (data: { pathname: string }) => {
|
||||
const arr = data.pathname.split('/').filter(Boolean);
|
||||
const path = normalizePath(arr.slice(0, -1));
|
||||
|
||||
if (file.value[path]) {
|
||||
file.value[path] = file.value[path].filter(
|
||||
(v) => v.pathname !== data.pathname,
|
||||
);
|
||||
}
|
||||
});
|
||||
socket.on('FileMove', (data: { from: StorageFile; to: StorageFile }) => {
|
||||
const arr = data.from.pathname.split('/').filter(Boolean);
|
||||
const path = normalizePath(arr.slice(0, -1));
|
||||
|
||||
if (file.value[path]) {
|
||||
const idx = file.value[path].findIndex(
|
||||
(v) => v.pathname === data.from.pathname,
|
||||
);
|
||||
if (idx !== -1) file.value[path][idx] = data.to;
|
||||
}
|
||||
});
|
||||
socket.on('FileUpdate', (data: StorageFile) => {
|
||||
const arr = data.pathname.split('/').filter(Boolean);
|
||||
const path = normalizePath(arr.slice(0, -1));
|
||||
|
||||
if (file.value[path]) {
|
||||
const idx = file.value[path].findIndex(
|
||||
(v) => v.pathname === data.pathname,
|
||||
);
|
||||
if (idx !== -1) file.value[path][idx] = data;
|
||||
}
|
||||
});
|
||||
socket.on('FileUploadRequest', (data: StorageFile) => {
|
||||
const arr = data.pathname.split('/').filter(Boolean);
|
||||
const path = normalizePath(arr.slice(0, -1));
|
||||
|
||||
if (file.value[path]) {
|
||||
const idx = file.value[path].findIndex(
|
||||
(v) => v.pathname === data.pathname,
|
||||
);
|
||||
|
||||
if (idx !== -1) file.value[path][idx] = data;
|
||||
else file.value[path].push(data);
|
||||
|
||||
file.value[path].sort((a, b) => a.pathname.localeCompare(b.pathname));
|
||||
}
|
||||
});
|
||||
|
||||
async function createFolder(name: string, path: string = currentInfo.path) {
|
||||
loader.show();
|
||||
if (
|
||||
folder.value[normalizePath(path)]?.findIndex((v) => v.name === name) !==
|
||||
-1
|
||||
) {
|
||||
utils.openDialog({
|
||||
title: 'แจ้งเตือน',
|
||||
message: `พบชื่อ \"${name}\" ซ้ำในระบบ`,
|
||||
});
|
||||
} else {
|
||||
const arrayPath: string[] = path.split('/').filter(Boolean);
|
||||
await api.post('/storage/folder', {
|
||||
path: arrayPath,
|
||||
name: name.replace(/^\./, ''),
|
||||
});
|
||||
}
|
||||
loader.hide();
|
||||
}
|
||||
async function editFolder(name: string, path: string) {
|
||||
loader.show();
|
||||
const arrayPath: string[] = path.split('/').filter(Boolean);
|
||||
const beforeName = arrayPath.pop();
|
||||
|
||||
await api.put('/storage/folder', {
|
||||
from: {
|
||||
name: beforeName,
|
||||
path: arrayPath,
|
||||
},
|
||||
to: {
|
||||
name: name,
|
||||
path: arrayPath,
|
||||
},
|
||||
});
|
||||
loader.hide();
|
||||
}
|
||||
async function deleteFolder(path: string) {
|
||||
loader.show();
|
||||
await api.delete<(typeof file.value)[string]>('/storage/folder', {
|
||||
data: { path: path.split('/').filter(Boolean) },
|
||||
});
|
||||
loader.hide();
|
||||
}
|
||||
|
||||
async function createFile(
|
||||
file: File,
|
||||
data: FileMetadata,
|
||||
path: string = currentInfo.path,
|
||||
) {
|
||||
const arr = path.split('/').filter(Boolean);
|
||||
|
||||
loader.show();
|
||||
const res = await api.post('/storage/file', {
|
||||
path: arr,
|
||||
file: file.name,
|
||||
...data,
|
||||
});
|
||||
if (res && res.status === 200 && res.data && res.data.uploadUrl) {
|
||||
await axios
|
||||
.put(res.data.uploadUrl, file, {
|
||||
headers: { 'Content-Type': file.type },
|
||||
onUploadProgress: (e) => console.log(e),
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
loader.hide();
|
||||
}
|
||||
async function updateFile(pathname: string, data: FileMetadata, file?: File) {
|
||||
const arr = pathname.split('/');
|
||||
|
||||
if (arr.length < 1) return;
|
||||
|
||||
loader.show();
|
||||
|
||||
const srcFile = arr.pop();
|
||||
|
||||
const res = await api.put('/storage/file', {
|
||||
...data,
|
||||
from: {
|
||||
file: srcFile,
|
||||
path: arr,
|
||||
},
|
||||
to: file?.name
|
||||
? {
|
||||
file: file.name,
|
||||
path: arr,
|
||||
}
|
||||
: undefined,
|
||||
upload: !!file,
|
||||
});
|
||||
if (res && res.status === 200 && res.data && res.data.uploadUrl) {
|
||||
await axios
|
||||
.put(res.data.uploadUrl, file, {
|
||||
headers: { 'Content-Type': file?.type },
|
||||
onUploadProgress: (e) => console.log(e),
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
loader.hide();
|
||||
}
|
||||
async function deleteFile(pathname: string) {
|
||||
const arr = pathname.split('/');
|
||||
|
||||
if (arr.length < 1) return;
|
||||
|
||||
loader.show();
|
||||
|
||||
await api.delete('/storage/file', {
|
||||
data: { path: arr.slice(0, -1), file: arr[arr.length - 1] },
|
||||
});
|
||||
loader.hide();
|
||||
}
|
||||
|
||||
async function searchFile(params: SearchOperator & SearchOptions) {
|
||||
loader.show();
|
||||
|
||||
return (await api
|
||||
.post(`/search${params.within ? `?within=${params.within}` : ''}`, {
|
||||
...params,
|
||||
within: undefined,
|
||||
})
|
||||
.then((r) => r.data)
|
||||
.finally(loader.hide)) as StorageFile[];
|
||||
}
|
||||
|
||||
async function getFileUrl(pathname: string | undefined) {
|
||||
if (!pathname) return;
|
||||
const arr = pathname.split('/');
|
||||
const file = arr.pop();
|
||||
const res = await api.post<StorageFile & { downloadUrl: string }>(
|
||||
'/storage/file/download',
|
||||
{ path: arr, file },
|
||||
);
|
||||
|
||||
if (res.status === 200 && res.data && res.data.downloadUrl) {
|
||||
return res.data.downloadUrl;
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadFile(pathname: string | undefined) {
|
||||
if (!pathname) return;
|
||||
|
||||
const arr = pathname.split('/');
|
||||
const file = arr.pop();
|
||||
|
||||
const res = await api.post<StorageFile & { downloadUrl: string }>(
|
||||
'/storage/file/download',
|
||||
{ path: arr, file },
|
||||
);
|
||||
|
||||
if (res.status === 200 && res.data && res.data.downloadUrl) {
|
||||
await axios
|
||||
.get(res.data.downloadUrl, {
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: res.data.fileType,
|
||||
},
|
||||
})
|
||||
.then((r) => {
|
||||
const a = document.createElement('a');
|
||||
a.href = window.URL.createObjectURL(r.data);
|
||||
a.download = res.data.fileName;
|
||||
a.click();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// information
|
||||
currentInfo,
|
||||
currentFile,
|
||||
folder,
|
||||
file,
|
||||
tree,
|
||||
// fetch
|
||||
getStorage,
|
||||
getStorageFile,
|
||||
// traverse
|
||||
goto,
|
||||
gotoParent,
|
||||
// operation
|
||||
createFolder,
|
||||
editFolder,
|
||||
deleteFolder,
|
||||
createFile,
|
||||
updateFile,
|
||||
deleteFile,
|
||||
searchFile,
|
||||
getFileUrl,
|
||||
downloadFile,
|
||||
};
|
||||
});
|
||||
|
||||
export default useStorage;
|
||||
10
src/stores/store-flag.d.ts
vendored
Normal file
10
src/stores/store-flag.d.ts
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
/* eslint-disable */
|
||||
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
|
||||
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
|
||||
import 'quasar/dist/types/feature-flag';
|
||||
|
||||
declare module 'quasar/dist/types/feature-flag' {
|
||||
interface QuasarFeatureFlags {
|
||||
store: true;
|
||||
}
|
||||
}
|
||||
19
src/stores/utils/index.ts
Normal file
19
src/stores/utils/index.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { Dialog } from 'quasar';
|
||||
import GlobalDialog from 'components/GlobalDialog.vue';
|
||||
|
||||
export function dialog(opts: {
|
||||
title: string;
|
||||
message: string;
|
||||
color?: string;
|
||||
icon?: string;
|
||||
persistent?: boolean;
|
||||
actionText?: string;
|
||||
cancelText?: string;
|
||||
action?: (...args: unknown[]) => unknown;
|
||||
cancel?: (...args: unknown[]) => unknown;
|
||||
}) {
|
||||
Dialog.create({
|
||||
component: GlobalDialog,
|
||||
componentProps: opts,
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue