initial commit (WIP)
This commit is contained in:
commit
9d8f424e1b
4
.browserslistrc
Normal file
4
.browserslistrc
Normal file
@ -0,0 +1,4 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
||||
not ie 11
|
||||
5
.editorconfig
Normal file
5
.editorconfig
Normal file
@ -0,0 +1,5 @@
|
||||
[*.{js,jsx,ts,tsx,vue}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
69
.eslintrc-auto-import.json
Normal file
69
.eslintrc-auto-import.json
Normal file
@ -0,0 +1,69 @@
|
||||
{
|
||||
"globals": {
|
||||
"Component": true,
|
||||
"ComponentPublicInstance": true,
|
||||
"ComputedRef": true,
|
||||
"EffectScope": true,
|
||||
"ExtractDefaultPropTypes": true,
|
||||
"ExtractPropTypes": true,
|
||||
"ExtractPublicPropTypes": true,
|
||||
"InjectionKey": true,
|
||||
"PropType": true,
|
||||
"Ref": true,
|
||||
"VNode": true,
|
||||
"WritableComputedRef": true,
|
||||
"computed": true,
|
||||
"createApp": true,
|
||||
"customRef": true,
|
||||
"defineAsyncComponent": true,
|
||||
"defineComponent": true,
|
||||
"effectScope": true,
|
||||
"getCurrentInstance": true,
|
||||
"getCurrentScope": true,
|
||||
"h": true,
|
||||
"inject": true,
|
||||
"isProxy": true,
|
||||
"isReactive": true,
|
||||
"isReadonly": true,
|
||||
"isRef": true,
|
||||
"markRaw": true,
|
||||
"nextTick": true,
|
||||
"onActivated": true,
|
||||
"onBeforeMount": true,
|
||||
"onBeforeUnmount": true,
|
||||
"onBeforeUpdate": true,
|
||||
"onDeactivated": true,
|
||||
"onErrorCaptured": true,
|
||||
"onMounted": true,
|
||||
"onRenderTracked": true,
|
||||
"onRenderTriggered": true,
|
||||
"onScopeDispose": true,
|
||||
"onServerPrefetch": true,
|
||||
"onUnmounted": true,
|
||||
"onUpdated": true,
|
||||
"provide": true,
|
||||
"reactive": true,
|
||||
"readonly": true,
|
||||
"ref": true,
|
||||
"resolveComponent": true,
|
||||
"shallowReactive": true,
|
||||
"shallowReadonly": true,
|
||||
"shallowRef": true,
|
||||
"toRaw": true,
|
||||
"toRef": true,
|
||||
"toRefs": true,
|
||||
"toValue": true,
|
||||
"triggerRef": true,
|
||||
"unref": true,
|
||||
"useAttrs": true,
|
||||
"useCssModule": true,
|
||||
"useCssVars": true,
|
||||
"useRoute": true,
|
||||
"useRouter": true,
|
||||
"useSlots": true,
|
||||
"watch": true,
|
||||
"watchEffect": true,
|
||||
"watchPostEffect": true,
|
||||
"watchSyncEffect": true
|
||||
}
|
||||
}
|
||||
29
.eslintrc.js
Normal file
29
.eslintrc.js
Normal file
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* .eslint.js
|
||||
*
|
||||
* ESLint configuration file.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
'plugin:vue/vue3-essential',
|
||||
'eslint:recommended',
|
||||
'vue3-recommended',
|
||||
'plugin:prettier/recommend',
|
||||
'@vue/eslint-config-typescript',
|
||||
],
|
||||
rules: {
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'vue/valid-v-slot': 'off',
|
||||
'prettier/prettier': [
|
||||
'error',
|
||||
{
|
||||
endOfLine: 'auto',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
10
.prettierrc
Normal file
10
.prettierrc
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": true,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"printWidth": 80,
|
||||
"trailingComma": "all",
|
||||
"arrowParens": "always",
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
81
README.md
Normal file
81
README.md
Normal file
@ -0,0 +1,81 @@
|
||||
# Vuetify (Default)
|
||||
|
||||
This is the official scaffolding tool for Vuetify, designed to give you a head start in building your new Vuetify application. It sets up a base template with all the necessary configurations and standard directory structure, enabling you to begin development without the hassle of setting up the project from scratch.
|
||||
|
||||
## ❗️ Important Links
|
||||
|
||||
- 📄 [Docs](https://vuetifyjs.com/)
|
||||
- 🚨 [Issues](https://issues.vuetifyjs.com/)
|
||||
- 🏬 [Store](https://store.vuetifyjs.com/)
|
||||
- 🎮 [Playground](https://play.vuetifyjs.com/)
|
||||
- 💬 [Discord](https://community.vuetifyjs.com)
|
||||
|
||||
## 💿 Install
|
||||
|
||||
Set up your project using your preferred package manager. Use the corresponding command to install the dependencies:
|
||||
|
||||
| Package Manager | Command |
|
||||
|---------------------------------------------------------------|----------------|
|
||||
| [yarn](https://yarnpkg.com/getting-started) | `yarn install` |
|
||||
| [npm](https://docs.npmjs.com/cli/v7/commands/npm-install) | `npm install` |
|
||||
| [pnpm](https://pnpm.io/installation) | `pnpm install` |
|
||||
| [bun](https://bun.sh/#getting-started) | `bun install` |
|
||||
|
||||
After completing the installation, your environment is ready for Vuetify development.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 🖼️ **Optimized Front-End Stack**: Leverage the latest Vue 3 and Vuetify 3 for a modern, reactive UI development experience. [Vue 3](https://v3.vuejs.org/) | [Vuetify 3](https://vuetifyjs.com/en/)
|
||||
- 🗃️ **State Management**: Integrated with [Pinia](https://pinia.vuejs.org/), the intuitive, modular state management solution for Vue.
|
||||
- 🚦 **Routing and Layouts**: Utilizes Vue Router for SPA navigation and vite-plugin-vue-layouts for organizing Vue file layouts. [Vue Router](https://router.vuejs.org/) | [vite-plugin-vue-layouts](https://github.com/JohnCampionJr/vite-plugin-vue-layouts)
|
||||
- 💻 **Enhanced Development Experience**: Benefit from TypeScript's static type checking and the ESLint plugin suite for Vue, ensuring code quality and consistency. [TypeScript](https://www.typescriptlang.org/) | [ESLint Plugin Vue](https://eslint.vuejs.org/)
|
||||
- ⚡ **Next-Gen Tooling**: Powered by Vite, experience fast cold starts and instant HMR (Hot Module Replacement). [Vite](https://vitejs.dev/)
|
||||
- 🧩 **Automated Component Importing**: Streamline your workflow with unplugin-vue-components, automatically importing components as you use them. [unplugin-vue-components](https://github.com/antfu/unplugin-vue-components)
|
||||
- 🛠️ **Strongly-Typed Vue**: Use vue-tsc for type-checking your Vue components, and enjoy a robust development experience. [vue-tsc](https://github.com/johnsoncodehk/volar/tree/master/packages/vue-tsc)
|
||||
|
||||
These features are curated to provide a seamless development experience from setup to deployment, ensuring that your Vuetify application is both powerful and maintainable.
|
||||
|
||||
## 💡 Usage
|
||||
|
||||
This section covers how to start the development server and build your project for production.
|
||||
|
||||
### Starting the Development Server
|
||||
|
||||
To start the development server with hot-reload, run the following command. The server will be accessible at [http://localhost:3000](http://localhost:3000):
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
(Repeat for npm, pnpm, and bun with respective commands.)
|
||||
|
||||
> Add NODE_OPTIONS='--no-warnings' to suppress the JSON import warnings that happen as part of the Vuetify import mapping. If you are on Node [v21.3.0](https://nodejs.org/en/blog/release/v21.3.0) or higher, you can change this to NODE_OPTIONS='--disable-warning=5401'. If you don't mind the warning, you can remove this from your package.json dev script.
|
||||
|
||||
### Building for Production
|
||||
|
||||
To build your project for production, use:
|
||||
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
|
||||
(Repeat for npm, pnpm, and bun with respective commands.)
|
||||
|
||||
Once the build process is completed, your application will be ready for deployment in a production environment.
|
||||
|
||||
## 💪 Support Vuetify Development
|
||||
|
||||
This project is built with [Vuetify](https://vuetifyjs.com/en/), a UI Library with a comprehensive collection of Vue components. Vuetify is an MIT licensed Open Source project that has been made possible due to the generous contributions by our [sponsors and backers](https://vuetifyjs.com/introduction/sponsors-and-backers/). If you are interested in supporting this project, please consider:
|
||||
|
||||
- [Requesting Enterprise Support](https://support.vuetifyjs.com/)
|
||||
- [Sponsoring John on Github](https://github.com/users/johnleider/sponsorship)
|
||||
- [Sponsoring Kael on Github](https://github.com/users/kaelwd/sponsorship)
|
||||
- [Supporting the team on Open Collective](https://opencollective.com/vuetify)
|
||||
- [Becoming a sponsor on Patreon](https://www.patreon.com/vuetify)
|
||||
- [Becoming a subscriber on Tidelift](https://tidelift.com/subscription/npm/vuetify)
|
||||
- [Making a one-time donation with Paypal](https://paypal.me/vuetify)
|
||||
|
||||
## 📑 License
|
||||
[MIT](http://opensource.org/licenses/MIT)
|
||||
|
||||
Copyright (c) 2016-present Vuetify, LLC
|
||||
16
index.html
Normal file
16
index.html
Normal file
@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Peresvet - System Trace</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
6645
package-lock.json
generated
Normal file
6645
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
51
package.json
Normal file
51
package.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "interface",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --fix --ignore-path .gitignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "6.2.95",
|
||||
"@vueuse/core": "^10.10.0",
|
||||
"axios": "^1.7.2",
|
||||
"core-js": "^3.34.0",
|
||||
"moment": "^2.30.1",
|
||||
"roboto-fontface": "*",
|
||||
"vue": "^3.4.21",
|
||||
"vuetify": "^3.5.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/types": "^7.24.0",
|
||||
"@types/node": "^20.11.25",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vue/eslint-config-typescript": "^13.0.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-n": "^16.6.2",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-vue": "^9.22.0",
|
||||
"pinia": "^2.1.7",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "3.3.2",
|
||||
"sass": "^1.71.1",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "^5.4.2",
|
||||
"unplugin-auto-import": "^0.17.5",
|
||||
"unplugin-fonts": "^1.1.1",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"unplugin-vue-router": "^0.8.4",
|
||||
"vite": "^5.1.5",
|
||||
"vite-plugin-pages": "^0.32.2",
|
||||
"vite-plugin-vue-layouts": "^0.11.0",
|
||||
"vite-plugin-vuetify": "^2.0.3",
|
||||
"vue-router": "^4.3.0",
|
||||
"vue-tsc": "^2.0.6"
|
||||
}
|
||||
}
|
||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
25
public/images/flag_en.svg
Normal file
25
public/images/flag_en.svg
Normal file
@ -0,0 +1,25 @@
|
||||
<svg width="32" height="24" viewBox="0 0 32 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_270_67366)">
|
||||
<rect width="32" height="24" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0V24H32V0H0Z" fill="#2E42A5"/>
|
||||
<mask id="mask0_270_67366" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="32" height="24">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0V24H32V0H0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_270_67366)">
|
||||
<path d="M-3.56311 22.2854L3.47858 25.2635L32.1598 3.23787L35.8741 -1.18761L28.3441 -2.18297L16.6457 7.3085L7.22968 13.7035L-3.56311 22.2854Z" fill="white"/>
|
||||
<path d="M-2.59912 24.3719L0.988295 26.1001L34.5403 -1.59881H29.5032L-2.59912 24.3719Z" fill="#F50100"/>
|
||||
<path d="M35.5631 22.2854L28.5214 25.2635L-0.159817 3.23787L-3.87415 -1.18761L3.65593 -2.18297L15.3543 7.3085L24.7703 13.7035L35.5631 22.2854Z" fill="white"/>
|
||||
<path d="M35.3229 23.7829L31.7355 25.5111L17.4487 13.6518L13.2129 12.3267L-4.23151 -1.17246H0.805637L18.2403 12.0063L22.8713 13.5952L35.3229 23.7829Z" fill="#F50100"/>
|
||||
<mask id="path-7-inside-1_270_67366" fill="white">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.7778 -2H12.2222V8H-1.97247V16H12.2222V26H19.7778V16H34.0275V8H19.7778V-2Z"/>
|
||||
</mask>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.7778 -2H12.2222V8H-1.97247V16H12.2222V26H19.7778V16H34.0275V8H19.7778V-2Z" fill="#F50100"/>
|
||||
<path d="M12.2222 -2V-4H10.2222V-2H12.2222ZM19.7778 -2H21.7778V-4H19.7778V-2ZM12.2222 8V10H14.2222V8H12.2222ZM-1.97247 8V6H-3.97247V8H-1.97247ZM-1.97247 16H-3.97247V18H-1.97247V16ZM12.2222 16H14.2222V14H12.2222V16ZM12.2222 26H10.2222V28H12.2222V26ZM19.7778 26V28H21.7778V26H19.7778ZM19.7778 16V14H17.7778V16H19.7778ZM34.0275 16V18H36.0275V16H34.0275ZM34.0275 8H36.0275V6H34.0275V8ZM19.7778 8H17.7778V10H19.7778V8ZM12.2222 0H19.7778V-4H12.2222V0ZM14.2222 8V-2H10.2222V8H14.2222ZM-1.97247 10H12.2222V6H-1.97247V10ZM0.0275269 16V8H-3.97247V16H0.0275269ZM12.2222 14H-1.97247V18H12.2222V14ZM14.2222 26V16H10.2222V26H14.2222ZM19.7778 24H12.2222V28H19.7778V24ZM17.7778 16V26H21.7778V16H17.7778ZM34.0275 14H19.7778V18H34.0275V14ZM32.0275 8V16H36.0275V8H32.0275ZM19.7778 10H34.0275V6H19.7778V10ZM17.7778 -2V8H21.7778V-2H17.7778Z" fill="white" mask="url(#path-7-inside-1_270_67366)"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_270_67366">
|
||||
<rect width="32" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
18
public/images/flag_ru.svg
Normal file
18
public/images/flag_ru.svg
Normal file
@ -0,0 +1,18 @@
|
||||
<svg width="32" height="24" viewBox="0 0 32 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_270_67492)">
|
||||
<rect width="32" height="24" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0V24H32V0H0Z" fill="#3D58DB"/>
|
||||
<mask id="mask0_270_67492" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="32" height="24">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0V24H32V0H0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_270_67492)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0V8H32V0H0Z" fill="#F7FCFF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 16V24H32V16H0Z" fill="#C51918"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_270_67492">
|
||||
<rect width="32" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 788 B |
11
src/App.vue
Normal file
11
src/App.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<v-main>
|
||||
<router-view />
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
//
|
||||
</script>
|
||||
16
src/api/auth.ts
Normal file
16
src/api/auth.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { $axios } from '@/plugins/axios'
|
||||
import { AxiosResponse } from 'axios'
|
||||
|
||||
export default {
|
||||
login: async (email: string, password: string): Promise<[AxiosResponse?, string?]> => {
|
||||
try {
|
||||
const data = await $axios.post('v1/auth/login', {
|
||||
email,
|
||||
password,
|
||||
})
|
||||
return [data, undefined]
|
||||
} catch (e: any) {
|
||||
return [undefined, e?.response?.data?.error ?? e.message]
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/api/groups.ts
Normal file
24
src/api/groups.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { $axios } from '@/plugins/axios';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { OrderBy } from '../types/order-by';
|
||||
import { camelToSnake } from '../mixins/case-converter';
|
||||
|
||||
export default {
|
||||
getAll: async (
|
||||
count: number = 10,
|
||||
page: number = 1,
|
||||
orderBy: OrderBy,
|
||||
): Promise<[AxiosResponse?, string?]> => {
|
||||
try {
|
||||
const order: string = camelToSnake(
|
||||
new URLSearchParams([...Object.entries(orderBy)]).toString(),
|
||||
);
|
||||
const data = await $axios.get(
|
||||
`v1/groups?count=${count}&page=${page}&${order}`,
|
||||
);
|
||||
return [data, undefined];
|
||||
} catch (e: any) {
|
||||
return [undefined, e?.response?.data?.error ?? e.message];
|
||||
}
|
||||
},
|
||||
};
|
||||
9
src/api/index.ts
Normal file
9
src/api/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import auth from './auth';
|
||||
import users from './users';
|
||||
import groups from './groups';
|
||||
|
||||
export default {
|
||||
auth,
|
||||
users,
|
||||
groups,
|
||||
};
|
||||
24
src/api/users.ts
Normal file
24
src/api/users.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { $axios } from '@/plugins/axios';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { OrderBy } from '../types/order-by';
|
||||
import { camelToSnake } from '../mixins/case-converter';
|
||||
|
||||
export default {
|
||||
getAll: async (
|
||||
count: number = 10,
|
||||
page: number = 1,
|
||||
orderBy: OrderBy,
|
||||
): Promise<[AxiosResponse?, string?]> => {
|
||||
try {
|
||||
const order: string = camelToSnake(
|
||||
new URLSearchParams([...Object.entries(orderBy)]).toString(),
|
||||
);
|
||||
const data = await $axios.get(
|
||||
`v1/users?count=${count}&page=${page}&${order}`,
|
||||
);
|
||||
return [data, undefined];
|
||||
} catch (e: any) {
|
||||
return [undefined, e?.response?.data?.error ?? e.message];
|
||||
}
|
||||
},
|
||||
};
|
||||
BIN
src/assets/fonts/MDI/MaterialSymbolsOutlined-Light.ttf
Normal file
BIN
src/assets/fonts/MDI/MaterialSymbolsOutlined-Light.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/MDI/MaterialSymbolsOutlined-Regular.ttf
Normal file
BIN
src/assets/fonts/MDI/MaterialSymbolsOutlined-Regular.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/Roboto/Roboto-Black.ttf
Normal file
BIN
src/assets/fonts/Roboto/Roboto-Black.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/Roboto/Roboto-Bold.ttf
Normal file
BIN
src/assets/fonts/Roboto/Roboto-Bold.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/Roboto/Roboto-Light.ttf
Normal file
BIN
src/assets/fonts/Roboto/Roboto-Light.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/Roboto/Roboto-Medium.ttf
Normal file
BIN
src/assets/fonts/Roboto/Roboto-Medium.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/Roboto/Roboto-Regular.ttf
Normal file
BIN
src/assets/fonts/Roboto/Roboto-Regular.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/Roboto/Roboto-Thin.ttf
Normal file
BIN
src/assets/fonts/Roboto/Roboto-Thin.ttf
Normal file
Binary file not shown.
BIN
src/assets/logo.png
Normal file
BIN
src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
8
src/assets/logo.svg
Normal file
8
src/assets/logo.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="81" height="24" viewBox="0 0 81 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="48" y="16.0547" width="32.1081" height="7.94595" fill="black"/>
|
||||
<rect width="32.1081" height="7.94595" fill="black"/>
|
||||
<rect x="40.0547" y="7.94531" width="7.94595" height="8.10811" fill="black"/>
|
||||
<rect x="32.1094" y="7.94531" width="7.94595" height="16.0541" fill="black"/>
|
||||
<rect y="7.94531" width="7.94595" height="16.0541" fill="black"/>
|
||||
<rect x="40.0547" width="40.0541" height="7.94595" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 518 B |
3
src/assets/styles/README.md
Normal file
3
src/assets/styles/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Styles
|
||||
|
||||
This directory is for configuring the styles of the application.
|
||||
66
src/assets/styles/core.scss
Normal file
66
src/assets/styles/core.scss
Normal file
@ -0,0 +1,66 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@import './fonts.css';
|
||||
@import './table.css';
|
||||
|
||||
.accent-modal {
|
||||
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)) !important;
|
||||
background: rgb(var(--v-theme-code)) !important;
|
||||
}
|
||||
|
||||
.border-b {
|
||||
border-bottom-width: 1px !important;
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity)) !important;
|
||||
}
|
||||
|
||||
.border-t {
|
||||
border-top-width: 1px !important;
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity)) !important;
|
||||
}
|
||||
|
||||
.border-r {
|
||||
border-right-width: 1px !important;
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity)) !important;
|
||||
}
|
||||
|
||||
.border-l {
|
||||
border-left-width: 1px !important;
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity)) !important;
|
||||
}
|
||||
|
||||
.border-a {
|
||||
border-width: 1px !important;
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity)) !important;
|
||||
}
|
||||
|
||||
.text-h6 {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.cm-mini {
|
||||
min-height: 30px !important;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 0.875rem;
|
||||
/* 14px */
|
||||
line-height: 1.25rem;
|
||||
/* 20px */
|
||||
overscroll-behavior: none;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
.v-theme--light {
|
||||
--v-theme-primary: 11, 87, 208 !important;
|
||||
}
|
||||
|
||||
.v-theme--dark {
|
||||
--v-theme-primary: 51, 129, 255 !important;
|
||||
|
||||
.v-overlay__scrim {
|
||||
--v-theme-on-surface: 0, 0, 0;
|
||||
--v-overlay-opacity: 0.5;
|
||||
}
|
||||
}
|
||||
61
src/assets/styles/fonts.css
Normal file
61
src/assets/styles/fonts.css
Normal file
@ -0,0 +1,61 @@
|
||||
html {
|
||||
font-family: 'Roboto';
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('../fonts/Roboto/Roboto-Thin.ttf') format('truetype');
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('../fonts/Roboto/Roboto-Light.ttf') format('truetype');
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('../fonts/Roboto/Roboto-Regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('../fonts/Roboto/Roboto-Medium.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('../fonts/Roboto/Roboto-Bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('../fonts/Roboto/Roboto-Black.ttf') format('truetype');
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'MDI';
|
||||
src: url('../fonts/MDI/MaterialSymbolsOutlined-Light.ttf') format('truetype');
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'MDI';
|
||||
src: url('../fonts/MDI/MaterialSymbolsOutlined-Regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.mso {
|
||||
font-family: 'MDI';
|
||||
font-size: 24px;
|
||||
font-variation-settings:
|
||||
'FILL' 0,
|
||||
'wght' 300,
|
||||
'GRAD' 0,
|
||||
'opsz' 24
|
||||
}
|
||||
83
src/assets/styles/settings.scss
Normal file
83
src/assets/styles/settings.scss
Normal file
@ -0,0 +1,83 @@
|
||||
/**
|
||||
* src/styles/settings.scss
|
||||
*
|
||||
* Configures SASS variables and Vuetify overwrites
|
||||
*/
|
||||
|
||||
// https://vuetifyjs.com/features/sass-variables/`
|
||||
@use 'vuetify/settings' with ( // $color-pack: false
|
||||
$card-item-padding: .625rem 0,
|
||||
$selection-control-size: 25px,
|
||||
$utilities: ("align-content": false,
|
||||
"align-items": false,
|
||||
"align-self": false,
|
||||
"border-bottom": false,
|
||||
"border-end": false,
|
||||
"border-opacity": false,
|
||||
"border-start": false,
|
||||
"border-style": false,
|
||||
"border-top": false,
|
||||
"border": false,
|
||||
"display": false,
|
||||
"flex-direction": false,
|
||||
"flex-grow": false,
|
||||
"flex-shrink": false,
|
||||
"flex-wrap": false,
|
||||
"flex": false,
|
||||
"float-ltr": false,
|
||||
"float-rtl": false,
|
||||
"float": false,
|
||||
"font-italic": false,
|
||||
"font-weight": false,
|
||||
"justify-content": false,
|
||||
"margin-bottom": false,
|
||||
"margin-end": false,
|
||||
"margin-left": false,
|
||||
"margin-right": false,
|
||||
"margin-start": false,
|
||||
"margin-top": false,
|
||||
"margin-x": false,
|
||||
"margin-y": false,
|
||||
"margin": false,
|
||||
"negative-margin-bottom": false,
|
||||
"negative-margin-end": false,
|
||||
"negative-margin-left": false,
|
||||
"negative-margin-right": false,
|
||||
"negative-margin-start": false,
|
||||
"negative-margin-top": false,
|
||||
"negative-margin-x": false,
|
||||
"negative-margin-y": false,
|
||||
"negative-margin": false,
|
||||
"order": false,
|
||||
"overflow-wrap": false,
|
||||
"overflow-x": false,
|
||||
"overflow-y": false,
|
||||
"overflow": false,
|
||||
"padding-bottom": false,
|
||||
"padding-end": false,
|
||||
"padding-left": false,
|
||||
"padding-right": false,
|
||||
"padding-start": false,
|
||||
"padding-top": false,
|
||||
"padding-x": false,
|
||||
"padding-y": false,
|
||||
"padding": false,
|
||||
// "rounded-bottom-end": false,
|
||||
// "rounded-bottom-start": false,
|
||||
// "rounded-bottom": false,
|
||||
// "rounded-end": false,
|
||||
// "rounded-start": false,
|
||||
// "rounded-top-end": false,
|
||||
// "rounded-top-start": false,
|
||||
// "rounded-top": false,
|
||||
// "rounded": false,
|
||||
"text-align": false,
|
||||
"text-decoration": false,
|
||||
"text-mono": false,
|
||||
"text-opacity": false,
|
||||
"text-overflow": false,
|
||||
"text-transform": false,
|
||||
// "typography": false,
|
||||
"white-space": false,
|
||||
),
|
||||
);
|
||||
29
src/assets/styles/table.css
Normal file
29
src/assets/styles/table.css
Normal file
@ -0,0 +1,29 @@
|
||||
.v-table .v-table__wrapper>table>tbody>tr:not(:last-child)>td,
|
||||
.v-table .v-table__wrapper>table>tbody>tr:not(:last-child)>th {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.v-table .v-table__wrapper>table>tbody>tr>td,
|
||||
.v-table .v-table__wrapper>table>tbody>tr>th {
|
||||
box-shadow: inset 0 -1px 0 rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
|
||||
table>tbody>tr>td:last-child,
|
||||
table>thead>tr>th:last-child {
|
||||
position: sticky !important;
|
||||
position: -webkit-sticky !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
right: 0;
|
||||
border-right: none;
|
||||
border-bottom: none !important;
|
||||
z-index: 9998;
|
||||
background: rgb(var(--v-theme-surface));
|
||||
border-left-width: 1px !important;
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity)) !important;
|
||||
}
|
||||
|
||||
table>thead>tr>th:last-child {
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
191
src/auto-imports.d.ts
vendored
Normal file
191
src/auto-imports.d.ts
vendored
Normal file
@ -0,0 +1,191 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const h: typeof import('vue')['h']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onMounted: typeof import('vue')['onMounted']
|
||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const toValue: typeof import('vue')['toValue']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useLink: typeof import('vue-router')['useLink']
|
||||
const useRoute: typeof import('vue-router/auto')['useRoute']
|
||||
const useRouter: typeof import('vue-router/auto')['useRouter']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
||||
// for vue template auto import
|
||||
import { UnwrapRef } from 'vue'
|
||||
declare module 'vue' {
|
||||
interface GlobalComponents {}
|
||||
interface ComponentCustomProperties {
|
||||
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
||||
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
|
||||
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
|
||||
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
|
||||
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
|
||||
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
|
||||
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
||||
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
|
||||
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
||||
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
||||
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
||||
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
|
||||
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
|
||||
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
|
||||
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
|
||||
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
|
||||
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
|
||||
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
|
||||
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
|
||||
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
|
||||
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
|
||||
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
|
||||
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
|
||||
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
|
||||
readonly provide: UnwrapRef<typeof import('vue')['provide']>
|
||||
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
|
||||
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
|
||||
readonly ref: UnwrapRef<typeof import('vue')['ref']>
|
||||
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
||||
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
|
||||
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
|
||||
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
||||
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
|
||||
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
|
||||
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
|
||||
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
|
||||
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
|
||||
readonly unref: UnwrapRef<typeof import('vue')['unref']>
|
||||
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
|
||||
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
|
||||
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
|
||||
readonly useRoute: UnwrapRef<typeof import('vue-router/auto')['useRoute']>
|
||||
readonly useRouter: UnwrapRef<typeof import('vue-router/auto')['useRouter']>
|
||||
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
||||
readonly watch: UnwrapRef<typeof import('vue')['watch']>
|
||||
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
|
||||
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
|
||||
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
|
||||
}
|
||||
}
|
||||
declare module '@vue/runtime-core' {
|
||||
interface GlobalComponents {}
|
||||
interface ComponentCustomProperties {
|
||||
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
||||
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
|
||||
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
|
||||
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
|
||||
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
|
||||
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
|
||||
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
||||
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
|
||||
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
||||
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
||||
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
||||
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
|
||||
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
|
||||
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
|
||||
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
|
||||
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
|
||||
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
|
||||
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
|
||||
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
|
||||
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
|
||||
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
|
||||
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
|
||||
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
|
||||
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
|
||||
readonly provide: UnwrapRef<typeof import('vue')['provide']>
|
||||
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
|
||||
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
|
||||
readonly ref: UnwrapRef<typeof import('vue')['ref']>
|
||||
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
||||
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
|
||||
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
|
||||
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
||||
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
|
||||
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
|
||||
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
|
||||
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
|
||||
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
|
||||
readonly unref: UnwrapRef<typeof import('vue')['unref']>
|
||||
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
|
||||
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
|
||||
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
|
||||
readonly useRoute: UnwrapRef<typeof import('vue-router/auto')['useRoute']>
|
||||
readonly useRouter: UnwrapRef<typeof import('vue-router/auto')['useRouter']>
|
||||
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
||||
readonly watch: UnwrapRef<typeof import('vue')['watch']>
|
||||
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
|
||||
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
|
||||
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
|
||||
}
|
||||
}
|
||||
20
src/components.d.ts
vendored
Normal file
20
src/components.d.ts
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
export {}
|
||||
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AppFooter: typeof import('./components/AppFooter.vue')['default']
|
||||
ErrorPlate: typeof import('./components/ErrorPlate.vue')['default']
|
||||
HelloWorld: typeof import('./components/HelloWorld.vue')['default']
|
||||
LanguageSwitcher: typeof import('./components/LanguageSwitcher.vue')['default']
|
||||
LogoComponent: typeof import('./components/Icons/LogoComponent.vue')['default']
|
||||
NavigationDrawer: typeof import('./components/NavigationDrawer.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
ThemeSwitcher: typeof import('./components/ThemeSwitcher.vue')['default']
|
||||
}
|
||||
}
|
||||
79
src/components/AppFooter.vue
Normal file
79
src/components/AppFooter.vue
Normal file
@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<v-footer height="40" app>
|
||||
<a
|
||||
v-for="item in items"
|
||||
:key="item.title"
|
||||
:href="item.href"
|
||||
:title="item.title"
|
||||
class="d-inline-block mx-2 social-link"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<v-icon
|
||||
:icon="item.icon"
|
||||
:size="item.icon === '$vuetify' ? 24 : 16"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<div
|
||||
class="text-caption text-disabled"
|
||||
style="position: absolute; right: 16px;"
|
||||
>
|
||||
© 2016-{{ (new Date()).getFullYear() }} <span class="d-none d-sm-inline-block">Vuetify, LLC</span>
|
||||
—
|
||||
<a
|
||||
class="text-decoration-none on-surface"
|
||||
href="https://vuetifyjs.com/about/licensing/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
MIT License
|
||||
</a>
|
||||
</div>
|
||||
</v-footer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const items = [
|
||||
{
|
||||
title: 'Vuetify Documentation',
|
||||
icon: `$vuetify`,
|
||||
href: 'https://vuetifyjs.com/',
|
||||
},
|
||||
{
|
||||
title: 'Vuetify Support',
|
||||
icon: 'mdi-shield-star-outline',
|
||||
href: 'https://support.vuetifyjs.com/',
|
||||
},
|
||||
{
|
||||
title: 'Vuetify X',
|
||||
icon: `svg:M2.04875 3.00002L9.77052 13.3248L1.99998 21.7192H3.74882L10.5519 14.3697L16.0486 21.7192H22L13.8437 10.8137L21.0765 3.00002H19.3277L13.0624 9.76874L8.0001 3.00002H2.04875ZM4.62054 4.28821H7.35461L19.4278 20.4308H16.6937L4.62054 4.28821Z`,
|
||||
href: 'https://x.com/vuetifyjs',
|
||||
},
|
||||
{
|
||||
title: 'Vuetify GitHub',
|
||||
icon: `mdi-github`,
|
||||
href: 'https://github.com/vuetifyjs/vuetify',
|
||||
},
|
||||
{
|
||||
title: 'Vuetify Discord',
|
||||
icon: `mdi-discord`,
|
||||
href: 'https://community.vuetifyjs.com/',
|
||||
},
|
||||
{
|
||||
title: 'Vuetify Reddit',
|
||||
icon: `mdi-reddit`,
|
||||
href: 'https://reddit.com/r/vuetifyjs',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped lang="sass">
|
||||
.social-link :deep(.v-icon)
|
||||
color: rgba(var(--v-theme-on-background), var(--v-disabled-opacity))
|
||||
text-decoration: none
|
||||
transition: .2s ease-in-out
|
||||
|
||||
&:hover
|
||||
color: rgba(25, 118, 210, 1)
|
||||
</style>
|
||||
67
src/components/ErrorPlate.vue
Normal file
67
src/components/ErrorPlate.vue
Normal file
@ -0,0 +1,67 @@
|
||||
<script setup lang="ts">
|
||||
import { useLocale } from 'vuetify'
|
||||
|
||||
defineProps({
|
||||
error: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
block: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
noBorder: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
})
|
||||
|
||||
const { t } = useLocale()
|
||||
|
||||
function parseError(e: string): string {
|
||||
if (e.includes('$vuetify', 0)) {
|
||||
return e
|
||||
}
|
||||
|
||||
if (e.includes('validator')) {
|
||||
const errs = e.split(' || ')
|
||||
for (const err of errs) {
|
||||
if (err.includes('validator', 0)) {
|
||||
const splitted = err.split(':')
|
||||
if (splitted?.length) {
|
||||
const right = splitted.pop()
|
||||
if (right && right.includes('/')) {
|
||||
const [field, key] = right.split('/')
|
||||
return t(`$vuetify.validator.field`,
|
||||
field.toLowerCase(),
|
||||
t(`$vuetify.validator.${key.replace(/'/gm, '')}`),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
v-if="error?.length"
|
||||
class="text-sm text-error flex items-center"
|
||||
:class="{
|
||||
'error-block': block,
|
||||
'my-2': !block,
|
||||
'border-b': !noBorder,
|
||||
}"
|
||||
><span class="mso mr-2 text-2xl">error</span> {{ parseError(error) }}</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.error-block {
|
||||
background-color: rgba(var(--v-theme-error), var(--v-border-opacity));
|
||||
border-color: rgba(var(--v-theme-error), var(--v-border-opacity));
|
||||
padding: 0 0.8rem;
|
||||
}
|
||||
</style>
|
||||
157
src/components/HelloWorld.vue
Normal file
157
src/components/HelloWorld.vue
Normal file
@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<v-container class="fill-height">
|
||||
<v-responsive
|
||||
class="align-centerfill-height mx-auto"
|
||||
max-width="900"
|
||||
>
|
||||
<v-img
|
||||
class="mb-4"
|
||||
height="150"
|
||||
src="@/assets/logo.png"
|
||||
/>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="text-body-2 font-weight-light mb-n1">Welcome to</div>
|
||||
|
||||
<h1 class="text-h2 font-weight-bold">Vuetify</h1>
|
||||
</div>
|
||||
|
||||
<div class="py-4" />
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-card
|
||||
class="py-4"
|
||||
color="surface-variant"
|
||||
image="https://cdn.vuetifyjs.com/docs/images/one/create/feature.png"
|
||||
prepend-icon="mdi-rocket-launch-outline"
|
||||
rounded="lg"
|
||||
variant="outlined"
|
||||
>
|
||||
<template #image>
|
||||
<v-img position="top right" />
|
||||
</template>
|
||||
|
||||
<template #title>
|
||||
<h2 class="text-h5 font-weight-bold">Get started</h2>
|
||||
</template>
|
||||
|
||||
<template #subtitle>
|
||||
<div class="text-subtitle-1">
|
||||
Replace this page by removing <v-kbd>{{ `<HelloWorld />` }}</v-kbd> in <v-kbd>pages/index.vue</v-kbd>.
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<v-overlay
|
||||
opacity=".12"
|
||||
scrim="primary"
|
||||
contained
|
||||
model-value
|
||||
persistent
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="6">
|
||||
<v-card
|
||||
append-icon="mdi-open-in-new"
|
||||
class="py-4"
|
||||
color="surface-variant"
|
||||
href="https://vuetifyjs.com/"
|
||||
prepend-icon="mdi-text-box-outline"
|
||||
rel="noopener noreferrer"
|
||||
rounded="lg"
|
||||
subtitle="Learn about all things Vuetify in our documentation."
|
||||
target="_blank"
|
||||
title="Documentation"
|
||||
variant="text"
|
||||
>
|
||||
<v-overlay
|
||||
opacity=".06"
|
||||
scrim="primary"
|
||||
contained
|
||||
model-value
|
||||
persistent
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="6">
|
||||
<v-card
|
||||
append-icon="mdi-open-in-new"
|
||||
class="py-4"
|
||||
color="surface-variant"
|
||||
href="https://vuetifyjs.com/introduction/why-vuetify/#feature-guides"
|
||||
prepend-icon="mdi-star-circle-outline"
|
||||
rel="noopener noreferrer"
|
||||
rounded="lg"
|
||||
subtitle="Explore available framework Features."
|
||||
target="_blank"
|
||||
title="Features"
|
||||
variant="text"
|
||||
>
|
||||
<v-overlay
|
||||
opacity=".06"
|
||||
scrim="primary"
|
||||
contained
|
||||
model-value
|
||||
persistent
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="6">
|
||||
<v-card
|
||||
append-icon="mdi-open-in-new"
|
||||
class="py-4"
|
||||
color="surface-variant"
|
||||
href="https://vuetifyjs.com/components/all"
|
||||
prepend-icon="mdi-widgets-outline"
|
||||
rel="noopener noreferrer"
|
||||
rounded="lg"
|
||||
subtitle="Discover components in the API Explorer."
|
||||
target="_blank"
|
||||
title="Components"
|
||||
variant="text"
|
||||
>
|
||||
<v-overlay
|
||||
opacity=".06"
|
||||
scrim="primary"
|
||||
contained
|
||||
model-value
|
||||
persistent
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="6">
|
||||
<v-card
|
||||
append-icon="mdi-open-in-new"
|
||||
class="py-4"
|
||||
color="surface-variant"
|
||||
href="https://discord.vuetifyjs.com"
|
||||
prepend-icon="mdi-account-group-outline"
|
||||
rel="noopener noreferrer"
|
||||
rounded="lg"
|
||||
subtitle="Connect with Vuetify developers."
|
||||
target="_blank"
|
||||
title="Community"
|
||||
variant="text"
|
||||
>
|
||||
<v-overlay
|
||||
opacity=".06"
|
||||
scrim="primary"
|
||||
contained
|
||||
model-value
|
||||
persistent
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-responsive>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
//
|
||||
</script>
|
||||
23
src/components/Icons/LogoComponent.vue
Normal file
23
src/components/Icons/LogoComponent.vue
Normal file
@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, type ComputedRef } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
theme: {
|
||||
type: String,
|
||||
default: 'dark'
|
||||
}
|
||||
})
|
||||
|
||||
const fill: ComputedRef<string> = computed(() => props.theme === 'dark' ? '#FFF' : '#000')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg width="81" height="24" viewBox="0 0 81 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="48" y="16.0547" width="32.1081" height="7.94595" :fill="fill"/>
|
||||
<rect width="32.1081" height="7.94595" :fill="fill"/>
|
||||
<rect x="40.0547" y="7.94531" width="7.94595" height="8.10811" :fill="fill"/>
|
||||
<rect x="32.1094" y="7.94531" width="7.94595" height="16.0541" :fill="fill"/>
|
||||
<rect y="7.94531" width="7.94595" height="16.0541" :fill="fill"/>
|
||||
<rect x="40.0547" width="40.0541" height="7.94595" :fill="fill"/>
|
||||
</svg>
|
||||
</template>
|
||||
75
src/components/LanguageSwitcher.vue
Normal file
75
src/components/LanguageSwitcher.vue
Normal file
@ -0,0 +1,75 @@
|
||||
<script lang="ts" setup>
|
||||
import { useLocale } from 'vuetify';
|
||||
import { $cookie } from '@/plugins/cookie'
|
||||
import { onBeforeMount } from 'vue';
|
||||
import { COOKIE_LOCALE } from '../constants/static';
|
||||
import Moment from '@/plugins/moment'
|
||||
|
||||
const { current } = useLocale()
|
||||
const languages = [
|
||||
{
|
||||
title: 'Русский',
|
||||
locale: 'ru'
|
||||
},
|
||||
{
|
||||
title: 'English',
|
||||
locale: 'en'
|
||||
}
|
||||
]
|
||||
const key = COOKIE_LOCALE
|
||||
|
||||
const get = (): string => {
|
||||
const v = $cookie.get(key);
|
||||
Moment.$moment.locale(v);
|
||||
return v
|
||||
}
|
||||
const set = (locale: string): void => {
|
||||
current.value = locale;
|
||||
Moment.$moment.locale(locale);
|
||||
$cookie.set(key, locale);
|
||||
}
|
||||
const setLocaleFromExistCookie = (): void => {
|
||||
const v = get()
|
||||
if (v?.length && typeof v === 'string') {
|
||||
set(v)
|
||||
}
|
||||
}
|
||||
onBeforeMount(setLocaleFromExistCookie)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-menu
|
||||
transition="slide-y-transition"
|
||||
close-delay="0"
|
||||
open-delay="0"
|
||||
>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn icon density="comfortable">
|
||||
<span class="mso" v-bind="props">language</span>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list density="compact" class="mt-3 accent-modal" elevation="0">
|
||||
<v-list-item
|
||||
v-for="(item, i) in languages"
|
||||
:key="i"
|
||||
class="cm-mini"
|
||||
@click="set(item.locale)"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<img
|
||||
class="flag mr-2"
|
||||
:src="`/images/flag_${item.locale}.svg`"
|
||||
/>
|
||||
</template>
|
||||
<v-list-item-title>{{ item.title }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.flag {
|
||||
height: 14px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
45
src/components/NavigationDrawer.vue
Normal file
45
src/components/NavigationDrawer.vue
Normal file
@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { useLocale } from 'vuetify';
|
||||
import { ROUTES } from '@/constants/routes';
|
||||
|
||||
const props = defineProps({
|
||||
drawer: Boolean
|
||||
})
|
||||
const emits = defineEmits(['change'])
|
||||
|
||||
const { t } = useLocale()
|
||||
|
||||
const modelValue = ref<boolean>(props.drawer)
|
||||
|
||||
watch(props, (v: any, _) => {
|
||||
modelValue.value = v.drawer
|
||||
})
|
||||
|
||||
function change(e: boolean) {
|
||||
emits('change', e)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-navigation-drawer
|
||||
v-model="modelValue"
|
||||
@update:modelValue="change"
|
||||
class="pt-3 elevation-0 border-r"
|
||||
:scrim="false"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="(item, i) in ROUTES"
|
||||
:key="i"
|
||||
:value="item"
|
||||
color="primary"
|
||||
:to="item.value"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<span class="mso mr-3">{{ item.icon }}</span>
|
||||
</template>
|
||||
|
||||
<v-list-item-title>{{ t(item.title) }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-navigation-drawer>
|
||||
</template>
|
||||
35
src/components/README.md
Normal file
35
src/components/README.md
Normal file
@ -0,0 +1,35 @@
|
||||
# Components
|
||||
|
||||
Vue template files in this folder are automatically imported.
|
||||
|
||||
## 🚀 Usage
|
||||
|
||||
Importing is handled by [unplugin-vue-components](https://github.com/unplugin/unplugin-vue-components). This plugin automatically imports `.vue` files created in the `src/components` directory, and registers them as global components. This means that you can use any component in your application without having to manually import it.
|
||||
|
||||
The following example assumes a component located at `src/components/MyComponent.vue`:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<MyComponent />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
//
|
||||
</script>
|
||||
```
|
||||
|
||||
When your template is rendered, the component's import will automatically be inlined, which renders to this:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<MyComponent />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import MyComponent from '@/components/MyComponent.vue'
|
||||
</script>
|
||||
```
|
||||
35
src/components/ThemeSwitcher.vue
Normal file
35
src/components/ThemeSwitcher.vue
Normal file
@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { useTheme } from 'vuetify'
|
||||
import { $cookie } from '@/plugins/cookie'
|
||||
import { onBeforeMount } from 'vue';
|
||||
import { COOKIE_THEME } from '../constants/static';
|
||||
|
||||
const theme = useTheme()
|
||||
const key = COOKIE_THEME
|
||||
|
||||
const get = (): string => {
|
||||
return $cookie.get(key);
|
||||
}
|
||||
const set = (value: string): void => {
|
||||
theme.global.name.value = value;
|
||||
$cookie.set(key, value);
|
||||
}
|
||||
const toggle = (): void => {
|
||||
const t = theme.global.current?.value?.dark ? 'light' : 'dark'
|
||||
theme.global.name.value = t
|
||||
$cookie.set(key, t);
|
||||
}
|
||||
const setThemeFromExistCookie = (): void => {
|
||||
const v = get()
|
||||
if (v?.length && typeof v === 'string') {
|
||||
set(v)
|
||||
}
|
||||
}
|
||||
onBeforeMount(setThemeFromExistCookie)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-btn icon density="comfortable" @click="toggle">
|
||||
<span class="mso">routine</span>
|
||||
</v-btn>
|
||||
</template>
|
||||
11
src/constants/defaults.ts
Normal file
11
src/constants/defaults.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export const defaultCursor = JSON.stringify({
|
||||
count: 10,
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
totalRows: 10,
|
||||
});
|
||||
|
||||
export const defaultSort = JSON.stringify({
|
||||
key: 'id',
|
||||
order: 'asc',
|
||||
});
|
||||
32
src/constants/routes.ts
Normal file
32
src/constants/routes.ts
Normal file
@ -0,0 +1,32 @@
|
||||
export const ROUTES = [
|
||||
{
|
||||
title: '$vuetify.navigation.dashboards',
|
||||
value: '/',
|
||||
icon: 'dashboard'
|
||||
},
|
||||
{
|
||||
title: '$vuetify.navigation.tasks',
|
||||
value: '/tasks',
|
||||
icon: 'web_traffic'
|
||||
},
|
||||
{
|
||||
title: '$vuetify.navigation.servers',
|
||||
value: '/servers',
|
||||
icon: 'dns'
|
||||
},
|
||||
{
|
||||
title: '$vuetify.navigation.users',
|
||||
value: '/users',
|
||||
icon: 'group'
|
||||
},
|
||||
{
|
||||
title: '$vuetify.navigation.patterns',
|
||||
value: '/patterns',
|
||||
icon: 'layers'
|
||||
},
|
||||
{
|
||||
title: '$vuetify.navigation.logs',
|
||||
value: '/logs',
|
||||
icon: 'menu'
|
||||
},
|
||||
]
|
||||
4
src/constants/static.ts
Normal file
4
src/constants/static.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const ITEMS_PER_PAGE = [10, 25, 50, 100]
|
||||
|
||||
export const COOKIE_LOCALE = 'user_locale'
|
||||
export const COOKIE_THEME = 'user_theme'
|
||||
7
src/enums/user-actions.enum.ts
Normal file
7
src/enums/user-actions.enum.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export enum UserActions {
|
||||
EDIT,
|
||||
BLOCK,
|
||||
UNBLOCK,
|
||||
RESET_PASSWORD,
|
||||
DELETE,
|
||||
}
|
||||
4
src/enums/user-tab.enum.ts
Normal file
4
src/enums/user-tab.enum.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export enum UsersPageTabs {
|
||||
USERS,
|
||||
GROUPS,
|
||||
}
|
||||
5
src/layouts/README.md
Normal file
5
src/layouts/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Layouts
|
||||
|
||||
Layouts are reusable components that wrap around pages. They are used to provide a consistent look and feel across multiple pages.
|
||||
|
||||
Full documentation for this feature can be found in the Official [vite-plugin-vue-layouts](https://github.com/JohnCampionJr/vite-plugin-vue-layouts) repository.
|
||||
13
src/layouts/centered.vue
Normal file
13
src/layouts/centered.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
//
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-app>
|
||||
<v-main>
|
||||
<div class="flex flex-col items-center justify-center h-full w-full">
|
||||
<router-view />
|
||||
</div>
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
39
src/layouts/default.vue
Normal file
39
src/layouts/default.vue
Normal file
@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useTheme } from 'vuetify'
|
||||
import NavigationDrawer from '../components/NavigationDrawer.vue'
|
||||
import LanguageSwitcher from '../components/LanguageSwitcher.vue'
|
||||
import ThemeSwitcher from '../components/ThemeSwitcher.vue'
|
||||
|
||||
const drawer = ref(true)
|
||||
const theme = useTheme()
|
||||
const currentTheme = computed(() => theme.global.name.value)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-app :style="`background-color: ${currentTheme === 'light' ? '#f0f4f9' : '#1c1c1c'}`">
|
||||
<v-app-bar
|
||||
prominent
|
||||
class="elevation-0 border-b px-2"
|
||||
>
|
||||
<v-app-bar-nav-icon density="comfortable" @click="drawer = !drawer"></v-app-bar-nav-icon>
|
||||
<v-app-bar-title>System Trace</v-app-bar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<!-- Theme switcher -->
|
||||
<ThemeSwitcher class="mr-1" />
|
||||
<!-- Language switcher -->
|
||||
<LanguageSwitcher />
|
||||
</v-app-bar>
|
||||
|
||||
<NavigationDrawer
|
||||
:drawer="drawer"
|
||||
@change="(e) => drawer = e"
|
||||
/>
|
||||
|
||||
<v-main>
|
||||
<router-view></router-view>
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
13
src/locales/en/actions.json
Normal file
13
src/locales/en/actions.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"actions": {
|
||||
"refresh": "Refresh",
|
||||
"create-group": "Create group",
|
||||
"create-user": "Create user",
|
||||
"reset-password": "Reset password",
|
||||
"edit": "Edit",
|
||||
"block": "Block",
|
||||
"unblock": "Unblock",
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
}
|
||||
11
src/locales/en/auth.json
Normal file
11
src/locales/en/auth.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"auth": {
|
||||
"copyright": "Product of Peresvet LLC",
|
||||
"title": "Authentication",
|
||||
"email": "E-Mail address",
|
||||
"password": "Password",
|
||||
"login": "Log in",
|
||||
"help": "Any problems? Contact ",
|
||||
"help-link": "administrator"
|
||||
}
|
||||
}
|
||||
12
src/locales/en/errors.json
Normal file
12
src/locales/en/errors.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"User with specified E-Mail and password not found": "User with specified email and password was not found",
|
||||
"validator": {
|
||||
"field": "Field {0} is not compliant: {1}",
|
||||
"required": "required",
|
||||
"min": "min. value",
|
||||
"max": "max. value",
|
||||
"password": "password rules",
|
||||
"email": "E-Mail rules",
|
||||
"endswith": "invalid value"
|
||||
}
|
||||
}
|
||||
15
src/locales/en/index.ts
Normal file
15
src/locales/en/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import errors from './errors.json'
|
||||
import navigation from './navigation.json'
|
||||
import users from './users.json'
|
||||
import actions from './actions.json'
|
||||
import auth from './auth.json'
|
||||
import misc from './misc.json'
|
||||
|
||||
export default {
|
||||
...errors,
|
||||
...navigation,
|
||||
...users,
|
||||
...actions,
|
||||
...auth,
|
||||
...misc,
|
||||
}
|
||||
5
src/locales/en/misc.json
Normal file
5
src/locales/en/misc.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"misc": {
|
||||
"actions": "Actions"
|
||||
}
|
||||
}
|
||||
10
src/locales/en/navigation.json
Normal file
10
src/locales/en/navigation.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"navigation": {
|
||||
"dashboards": "Dashboards",
|
||||
"tasks": "Tasks",
|
||||
"servers": "Servers",
|
||||
"users": "Users",
|
||||
"patterns": "Patterns",
|
||||
"logs": "Logs"
|
||||
}
|
||||
}
|
||||
31
src/locales/en/users.json
Normal file
31
src/locales/en/users.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"users": {
|
||||
"title": "Users",
|
||||
"search": "Search",
|
||||
"tabs": {
|
||||
"users": "Users",
|
||||
"groups": "Groups"
|
||||
},
|
||||
"all": "Users in group",
|
||||
"heads": {
|
||||
"id": "ID",
|
||||
"email": "E-Mail",
|
||||
"group": "Group",
|
||||
"real-name": "Real name",
|
||||
"password": "Password",
|
||||
"status": "Status",
|
||||
"last-visit": "Last visit",
|
||||
"created-at": "Created at",
|
||||
"updated-at": "Last update",
|
||||
"name": "Name",
|
||||
"permissions": "Permissions",
|
||||
"issuer": "Issuer"
|
||||
},
|
||||
"statuses": {
|
||||
"active": "Active",
|
||||
"blocked": "Blocked",
|
||||
"set": "Set",
|
||||
"unset": "Not set"
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/locales/ru/actions.json
Normal file
13
src/locales/ru/actions.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"actions": {
|
||||
"refresh": "Обновить",
|
||||
"create-group": "Создать группу",
|
||||
"create-user": "Создать пользователя",
|
||||
"reset-password": "Сбросить пароль",
|
||||
"edit": "Редактировать",
|
||||
"block": "Заблокировать",
|
||||
"unblock": "Разблокировать",
|
||||
"confirm": "Применить",
|
||||
"cancel": "Отменить"
|
||||
}
|
||||
}
|
||||
11
src/locales/ru/auth.json
Normal file
11
src/locales/ru/auth.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"auth": {
|
||||
"copyright": "Продукт компании Пересвет",
|
||||
"title": "Авторизация",
|
||||
"email": "Адрес эл. почты",
|
||||
"password": "Пароль",
|
||||
"login": "Войти",
|
||||
"help": "Проблемы? Сообщите ",
|
||||
"help-link": "администратору"
|
||||
}
|
||||
}
|
||||
12
src/locales/ru/errors.json
Normal file
12
src/locales/ru/errors.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"User with specified E-Mail and password not found": "Пользователь с указанными E-Mail и паролем не найден",
|
||||
"validator": {
|
||||
"field": "Поле {0} не соответствует требованиям: {1}",
|
||||
"required": "обязательное поле",
|
||||
"min": "мин. значение",
|
||||
"max": "макс. значение",
|
||||
"password": "требования к паролю",
|
||||
"email": "требования к E-Mail",
|
||||
"endswith": "недействительное значение"
|
||||
}
|
||||
}
|
||||
15
src/locales/ru/index.ts
Normal file
15
src/locales/ru/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import errors from './errors.json'
|
||||
import navigation from './navigation.json'
|
||||
import users from './users.json'
|
||||
import actions from './actions.json'
|
||||
import auth from './auth.json'
|
||||
import misc from './misc.json'
|
||||
|
||||
export default {
|
||||
...errors,
|
||||
...navigation,
|
||||
...users,
|
||||
...actions,
|
||||
...auth,
|
||||
...misc,
|
||||
}
|
||||
5
src/locales/ru/misc.json
Normal file
5
src/locales/ru/misc.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"misc": {
|
||||
"actions": "Действия"
|
||||
}
|
||||
}
|
||||
10
src/locales/ru/navigation.json
Normal file
10
src/locales/ru/navigation.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"navigation": {
|
||||
"dashboards": "Дашборды",
|
||||
"tasks": "Задачи",
|
||||
"servers": "Серверы",
|
||||
"users": "Пользователи",
|
||||
"patterns": "Паттерны",
|
||||
"logs": "Логи"
|
||||
}
|
||||
}
|
||||
34
src/locales/ru/users.json
Normal file
34
src/locales/ru/users.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"users": {
|
||||
"title": "Пользователи",
|
||||
"search": "Поиск",
|
||||
"tabs": {
|
||||
"users": "Пользователи",
|
||||
"groups": "Группы"
|
||||
},
|
||||
"all": "Пользователи в группе",
|
||||
"heads": {
|
||||
"id": "ID",
|
||||
"email": "E-Mail",
|
||||
"group": "Группа",
|
||||
"real-name": "Наст. имя",
|
||||
"password": "Пароль",
|
||||
"status": "Статус",
|
||||
"last-visit": "Посл. вход",
|
||||
"created-at": "Создан",
|
||||
"updated-at": "Посл. изменения",
|
||||
"name": "Название",
|
||||
"permissions": "Права",
|
||||
"issuer": "Издатель"
|
||||
},
|
||||
"statuses": {
|
||||
"active": "Активный",
|
||||
"blocked": "Заблокирован",
|
||||
"set": "Установлен",
|
||||
"unset": "Не установлен"
|
||||
},
|
||||
"creation": {
|
||||
"create": "Создание пользователя"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/main.ts
Normal file
23
src/main.ts
Normal file
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* main.ts
|
||||
*
|
||||
* Bootstraps Vuetify and other plugins then mounts the App`
|
||||
*/
|
||||
|
||||
// Plugins
|
||||
import { registerPlugins } from '@/plugins'
|
||||
|
||||
// Components
|
||||
import App from './App.vue'
|
||||
|
||||
// Styles
|
||||
import './assets/styles/core.scss'
|
||||
|
||||
// Composables
|
||||
import { createApp } from 'vue'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
registerPlugins(app)
|
||||
|
||||
app.mount('#app')
|
||||
3
src/mixins/case-converter.ts
Normal file
3
src/mixins/case-converter.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function camelToSnake(str: string): string {
|
||||
return str.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase();
|
||||
}
|
||||
12
src/mixins/format.ts
Normal file
12
src/mixins/format.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export function secondsToHms(seconds: any): string {
|
||||
const d = Math.floor(seconds / (3600 * 24));
|
||||
const h = Math.floor((seconds % (3600 * 24)) / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
const s = Math.floor(seconds % 60);
|
||||
|
||||
const dDisplay = d > 0 ? d + 'd ' : '';
|
||||
const hDisplay = h > 0 ? h + 'h ' : '';
|
||||
const mDisplay = m > 0 ? m + 'm ' : '';
|
||||
const sDisplay = s > 0 ? s + 's ' : '';
|
||||
return dDisplay + hDisplay + mDisplay + sDisplay;
|
||||
}
|
||||
5
src/pages/README.md
Normal file
5
src/pages/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Pages
|
||||
|
||||
Vue components created in this folder will automatically be converted to navigatable routes.
|
||||
|
||||
Full documentation for this feature can be found in the Official [unplugin-vue-router](https://github.com/posva/unplugin-vue-router) repository.
|
||||
100
src/pages/auth.vue
Normal file
100
src/pages/auth.vue
Normal file
@ -0,0 +1,100 @@
|
||||
<script lang="ts" setup>
|
||||
import { type Ref, ref, computed } from 'vue'
|
||||
import ErrorPlate from '../components/ErrorPlate.vue'
|
||||
import LogoComponent from '../components/Icons/LogoComponent.vue'
|
||||
import api from '../api/index'
|
||||
import { useLocale, useTheme } from 'vuetify';
|
||||
|
||||
type AuthForm = {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
const { t } = useLocale()
|
||||
const theme = useTheme()
|
||||
const currentTheme = computed(() => theme.global.name.value)
|
||||
|
||||
const err: Ref<string> = ref('')
|
||||
const form: Ref<AuthForm> = ref({
|
||||
email: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
const doAuth = async () => {
|
||||
const [_, e] = await api.auth.login(form.value.email, form.value.password)
|
||||
if (e && typeof e === 'string') {
|
||||
err.value = e
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex relative flex-col items-center justify-center h-full">
|
||||
<!-- Card -->
|
||||
<v-card
|
||||
width="800px"
|
||||
class="p-12 rounded-3xl"
|
||||
elevation="0"
|
||||
>
|
||||
<!-- Logo -->
|
||||
<LogoComponent :theme="currentTheme" />
|
||||
<div class="grid grid-cols-5 mt-8">
|
||||
<!-- Left side -->
|
||||
<div class="items-start flex flex-col col-span-2">
|
||||
<span class="text-3xl font-medium">{{ t('$vuetify.auth.title') }}</span>
|
||||
<!-- Copyright -->
|
||||
<span class="text-sm mt-2 opacity-75">{{ t('$vuetify.auth.copyright') }}</span>
|
||||
</div>
|
||||
<!-- Right side -->
|
||||
<div class="flex flex-col col-span-3">
|
||||
<!-- Login -->
|
||||
<v-text-field
|
||||
v-model="form.email"
|
||||
:label="t('$vuetify.auth.email')"
|
||||
:variant="currentTheme === 'dark' ? 'solo-filled' : 'outlined'"
|
||||
flat
|
||||
hide-details
|
||||
>
|
||||
<!-- <template v-slot:prepend-inner>
|
||||
<span class="mso mr-1 text-2xl">email</span>
|
||||
</template> -->
|
||||
</v-text-field>
|
||||
<!-- Password -->
|
||||
<v-text-field
|
||||
v-model="form.password"
|
||||
:label="t('$vuetify.auth.password')"
|
||||
:variant="currentTheme === 'dark' ? 'solo-filled' : 'outlined'"
|
||||
hide-details
|
||||
flat
|
||||
class="mt-2"
|
||||
>
|
||||
<!-- <template v-slot:prepend-inner>
|
||||
<span class="mso mr-1 text-2xl">password</span>
|
||||
</template> -->
|
||||
</v-text-field>
|
||||
<!-- Error -->
|
||||
<ErrorPlate :error="err" no-border />
|
||||
<!-- Help -->
|
||||
<span class="mt-5">{{ t('$vuetify.auth.help') }}<a href="#" class="text-primary font-bold">{{ t('$vuetify.auth.help-link') }}</a></span>
|
||||
<!-- Buttons -->
|
||||
<v-card-actions class="flex justify-end px-0 mt-8">
|
||||
<v-btn
|
||||
:variant="currentTheme === 'dark' ? 'flat' : 'flat'"
|
||||
color="primary"
|
||||
class="px-8 rounded-3xl"
|
||||
@click="doAuth"
|
||||
>
|
||||
<i class="mi-log-in"/>
|
||||
<span>{{ t('$vuetify.auth.login') }}</span>
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- <route lang="yaml">
|
||||
meta:
|
||||
layout: centered
|
||||
</route> -->
|
||||
7
src/pages/index.vue
Normal file
7
src/pages/index.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<HelloWorld />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
//
|
||||
</script>
|
||||
44
src/pages/users/components/AddUser.vue
Normal file
44
src/pages/users/components/AddUser.vue
Normal file
@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, type Ref } from 'vue';
|
||||
import { useLocale, useTheme } from 'vuetify';
|
||||
|
||||
defineProps<{
|
||||
modelValue: boolean;
|
||||
}>()
|
||||
defineEmits(['close'])
|
||||
|
||||
const { t } = useLocale()
|
||||
const theme = useTheme()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog persistent :model-value="modelValue" width="450">
|
||||
<v-card
|
||||
text="When using the activator slot, you must bind the slot props to the activator element."
|
||||
title="Slot Activator"
|
||||
>
|
||||
<template v-slot:title>
|
||||
<v-card-title class="p-0 w-full flex items-center">
|
||||
<span class="mso mr-1 text-2xl">person</span>
|
||||
<span>{{ t('$vuetify.users.creation.create') }}</span>
|
||||
</v-card-title>
|
||||
</template>
|
||||
<v-card-actions class="flex justify-end p-5 gap-1">
|
||||
<!-- Cancel -->
|
||||
<v-btn
|
||||
variant="text"
|
||||
class="px-4"
|
||||
:text="t('$vuetify.actions.cancel')"
|
||||
@click="$emit('close')"
|
||||
></v-btn>
|
||||
<!-- Confirm -->
|
||||
<v-btn
|
||||
variant="flat"
|
||||
color="primary"
|
||||
class="px-4"
|
||||
:text="t('$vuetify.actions.confirm')"
|
||||
></v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
198
src/pages/users/components/GroupsTab.vue
Normal file
198
src/pages/users/components/GroupsTab.vue
Normal file
@ -0,0 +1,198 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
computed,
|
||||
ref,
|
||||
watch,
|
||||
onBeforeMount,
|
||||
type Ref,
|
||||
type ComputedRef,
|
||||
} from 'vue';
|
||||
import { useLocale } from 'vuetify';
|
||||
import { ITEMS_PER_PAGE } from '@/constants/static';
|
||||
import { useGroupsStore } from '@/stores/groups';
|
||||
import { UserActions } from '@/enums/user-actions.enum';
|
||||
import Moment from '@/plugins/moment';
|
||||
|
||||
defineProps<{
|
||||
tableHeight: number;
|
||||
}>();
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
const $groups = useGroupsStore();
|
||||
|
||||
const headers: ComputedRef<any[]> = computed(() => [
|
||||
{ title: t('$vuetify.users.heads.id'), key: 'ID' },
|
||||
{ title: t('$vuetify.users.heads.email'), key: 'email' },
|
||||
{ title: t('$vuetify.users.heads.group'), key: 'group' },
|
||||
{ title: t('$vuetify.users.heads.real-name'), key: 'realName' },
|
||||
{ title: t('$vuetify.users.heads.password'), key: 'isRequiredToSetPassword' },
|
||||
{ title: t('$vuetify.users.heads.status'), key: 'isActive' },
|
||||
{ title: t('$vuetify.users.heads.last-visit'), key: 'lastLogin' },
|
||||
{ title: t('$vuetify.users.heads.created-at'), key: 'createdAt' },
|
||||
{
|
||||
title: t('$vuetify.misc.actions'),
|
||||
key: 'actions',
|
||||
sortable: false,
|
||||
align: 'center',
|
||||
},
|
||||
]);
|
||||
const actions = computed(() => [
|
||||
{
|
||||
title: t('$vuetify.actions.edit'),
|
||||
value: UserActions.EDIT,
|
||||
blocked: false,
|
||||
},
|
||||
{
|
||||
title: t('$vuetify.actions.block'),
|
||||
value: UserActions.BLOCK,
|
||||
blocked: false,
|
||||
},
|
||||
{
|
||||
title: t('$vuetify.actions.reset-password'),
|
||||
value: UserActions.RESET_PASSWORD,
|
||||
blocked: false,
|
||||
},
|
||||
{
|
||||
title: t('$vuetify.actions.unblock'),
|
||||
value: UserActions.UNBLOCK,
|
||||
blocked: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const selectedGroups: Ref<number[]> = ref([]);
|
||||
|
||||
onBeforeMount($groups.getAll);
|
||||
watch(() => [$groups.page, $groups.perPage, $groups.orderBy], $groups.getAll);
|
||||
watch(selectedGroups, (n, o) => console.log(n, o));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-data-table-server
|
||||
v-model="selectedGroups"
|
||||
:search="$groups.search"
|
||||
:header-props="{ nowrap: true }"
|
||||
:headers="headers"
|
||||
:items="$groups.all"
|
||||
:items-per-page-options="ITEMS_PER_PAGE"
|
||||
:items-length="$groups.cursor?.totalRows"
|
||||
:loading="$groups.loading"
|
||||
:height="tableHeight - 62"
|
||||
fixed-footer
|
||||
fixed-header
|
||||
show-select
|
||||
density="compact"
|
||||
style="--fixed: 10"
|
||||
@update:sort-by="$groups.setSort"
|
||||
@update:page="$groups.setPage"
|
||||
@update:itemsPerPage="$groups.setPerPage"
|
||||
>
|
||||
<!-- Fields -->
|
||||
<!-- Group -->
|
||||
<template v-slot:item.group="{ item }">
|
||||
<template v-if="item.group">
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-nowrap">{{ item.group.name }}</span>
|
||||
<!-- Group users -->
|
||||
<v-tooltip
|
||||
:text="t('$vuetify.users.all')"
|
||||
:close-delay="0"
|
||||
:open-delay="0"
|
||||
transition="no"
|
||||
>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
icon
|
||||
size="small"
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
>
|
||||
<span class="mso text-base">group</span>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else class="text-nowrap">—</span>
|
||||
</template>
|
||||
<!-- Real name -->
|
||||
<template v-slot:item.realName="{ item }">
|
||||
<span class="text-nowrap">
|
||||
{{ item.realName?.length ? item.realName : '—' }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- Required to set password -->
|
||||
<template v-slot:item.isRequiredToSetPassword="{ item }">
|
||||
<span
|
||||
:class="!item.isRequiredToSetPassword ? '' : 'text-error'"
|
||||
class="text-nowrap"
|
||||
>
|
||||
{{
|
||||
item.isRequiredToSetPassword
|
||||
? t('$vuetify.users.statuses.unset')
|
||||
: t('$vuetify.users.statuses.set')
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
<!-- User status -->
|
||||
<template v-slot:item.isActive="{ item }">
|
||||
<span :class="item.isActive ? '' : 'text-error'">
|
||||
{{
|
||||
item.isActive
|
||||
? t('$vuetify.users.statuses.active')
|
||||
: t('$vuetify.users.statuses.blocked')
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
<!-- Last visit -->
|
||||
<template v-slot:item.lastLogin="{ item }">
|
||||
<span class="text-nowrap">
|
||||
{{ Moment.$moment(item.lastLogin).format('DD MMM YYYY HH:mm') }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- Created at -->
|
||||
<template v-slot:item.createdAt="{ item }">
|
||||
<span class="text-nowrap">
|
||||
{{ Moment.$moment(item.createdAt).format('DD MMM YYYY HH:mm') }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- Actions -->
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-menu transition="slide-y-transition">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
icon
|
||||
size="small"
|
||||
variant="text"
|
||||
density="comfortable"
|
||||
class="mr-1"
|
||||
>
|
||||
<span class="mso text-xl">more_vert</span>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list class="mt-2 accent-modal" elevation="0" density="compact">
|
||||
<template v-for="(act, index) of actions">
|
||||
<v-list-item
|
||||
v-if="
|
||||
(act?.blocked && !item.isActive) ||
|
||||
(!act?.blocked && item.isActive)
|
||||
"
|
||||
:key="index"
|
||||
:value="act.value"
|
||||
lines="one"
|
||||
class="cm-mini"
|
||||
>
|
||||
<v-list-item-title>{{ act.title }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<!-- Delete -->
|
||||
<v-btn icon size="small" variant="text" density="comfortable">
|
||||
<span class="mso text-xl">close</span>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-data-table-server>
|
||||
</template>
|
||||
198
src/pages/users/components/UsersTab.vue
Normal file
198
src/pages/users/components/UsersTab.vue
Normal file
@ -0,0 +1,198 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
computed,
|
||||
ref,
|
||||
watch,
|
||||
onBeforeMount,
|
||||
type Ref,
|
||||
type ComputedRef,
|
||||
} from 'vue';
|
||||
import { useLocale } from 'vuetify';
|
||||
import { ITEMS_PER_PAGE } from '@/constants/static';
|
||||
import { useUsersStore } from '@/stores/users';
|
||||
import { UserActions } from '@/enums/user-actions.enum';
|
||||
import Moment from '@/plugins/moment';
|
||||
|
||||
defineProps<{
|
||||
tableHeight: number;
|
||||
}>();
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
const $users = useUsersStore();
|
||||
|
||||
const headers: ComputedRef<any[]> = computed(() => [
|
||||
{ title: t('$vuetify.users.heads.id'), key: 'ID' },
|
||||
{ title: t('$vuetify.users.heads.email'), key: 'email' },
|
||||
{ title: t('$vuetify.users.heads.group'), key: 'group' },
|
||||
{ title: t('$vuetify.users.heads.real-name'), key: 'realName' },
|
||||
{ title: t('$vuetify.users.heads.password'), key: 'isRequiredToSetPassword' },
|
||||
{ title: t('$vuetify.users.heads.status'), key: 'isActive' },
|
||||
{ title: t('$vuetify.users.heads.last-visit'), key: 'lastLogin' },
|
||||
{ title: t('$vuetify.users.heads.created-at'), key: 'createdAt' },
|
||||
{
|
||||
title: t('$vuetify.misc.actions'),
|
||||
key: 'actions',
|
||||
sortable: false,
|
||||
align: 'center',
|
||||
},
|
||||
]);
|
||||
const actions = computed(() => [
|
||||
{
|
||||
title: t('$vuetify.actions.edit'),
|
||||
value: UserActions.EDIT,
|
||||
blocked: false,
|
||||
},
|
||||
{
|
||||
title: t('$vuetify.actions.block'),
|
||||
value: UserActions.BLOCK,
|
||||
blocked: false,
|
||||
},
|
||||
{
|
||||
title: t('$vuetify.actions.reset-password'),
|
||||
value: UserActions.RESET_PASSWORD,
|
||||
blocked: false,
|
||||
},
|
||||
{
|
||||
title: t('$vuetify.actions.unblock'),
|
||||
value: UserActions.UNBLOCK,
|
||||
blocked: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const selectedUsers: Ref<number[]> = ref([]);
|
||||
|
||||
onBeforeMount($users.getAll);
|
||||
watch(() => [$users.page, $users.perPage, $users.orderBy], $users.getAll);
|
||||
watch(selectedUsers, (n, o) => console.log(n, o));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-data-table-server
|
||||
v-model="selectedUsers"
|
||||
:search="$users.search"
|
||||
:header-props="{ nowrap: true }"
|
||||
:headers="headers"
|
||||
:items="$users.all"
|
||||
:items-per-page-options="ITEMS_PER_PAGE"
|
||||
:items-length="$users.cursor?.totalRows"
|
||||
:loading="$users.loading"
|
||||
:height="tableHeight - 62"
|
||||
fixed-footer
|
||||
fixed-header
|
||||
show-select
|
||||
density="compact"
|
||||
style="--fixed: 10"
|
||||
@update:sort-by="$users.setSort"
|
||||
@update:page="$users.setPage"
|
||||
@update:itemsPerPage="$users.setPerPage"
|
||||
>
|
||||
<!-- Fields -->
|
||||
<!-- Group -->
|
||||
<template v-slot:item.group="{ item }">
|
||||
<template v-if="item.group">
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-nowrap">{{ item.group.name }}</span>
|
||||
<!-- Group users -->
|
||||
<v-tooltip
|
||||
:text="t('$vuetify.users.all')"
|
||||
:close-delay="0"
|
||||
:open-delay="0"
|
||||
transition="no"
|
||||
>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
icon
|
||||
size="small"
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
>
|
||||
<span class="mso text-base">group</span>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else class="text-nowrap">—</span>
|
||||
</template>
|
||||
<!-- Real name -->
|
||||
<template v-slot:item.realName="{ item }">
|
||||
<span class="text-nowrap">
|
||||
{{ item.realName?.length ? item.realName : '—' }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- Required to set password -->
|
||||
<template v-slot:item.isRequiredToSetPassword="{ item }">
|
||||
<span
|
||||
:class="!item.isRequiredToSetPassword ? '' : 'text-error'"
|
||||
class="text-nowrap"
|
||||
>
|
||||
{{
|
||||
item.isRequiredToSetPassword
|
||||
? t('$vuetify.users.statuses.unset')
|
||||
: t('$vuetify.users.statuses.set')
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
<!-- User status -->
|
||||
<template v-slot:item.isActive="{ item }">
|
||||
<span :class="item.isActive ? '' : 'text-error'">
|
||||
{{
|
||||
item.isActive
|
||||
? t('$vuetify.users.statuses.active')
|
||||
: t('$vuetify.users.statuses.blocked')
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
<!-- Last visit -->
|
||||
<template v-slot:item.lastLogin="{ item }">
|
||||
<span class="text-nowrap">
|
||||
{{ Moment.$moment(item.lastLogin).format('DD MMM YYYY HH:mm') }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- Created at -->
|
||||
<template v-slot:item.createdAt="{ item }">
|
||||
<span class="text-nowrap">
|
||||
{{ Moment.$moment(item.createdAt).format('DD MMM YYYY HH:mm') }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- Actions -->
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-menu transition="slide-y-transition">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
icon
|
||||
size="small"
|
||||
variant="text"
|
||||
density="comfortable"
|
||||
class="mr-1"
|
||||
>
|
||||
<span class="mso text-xl">more_vert</span>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list class="mt-2 accent-modal" elevation="0" density="compact">
|
||||
<template v-for="(act, index) of actions">
|
||||
<v-list-item
|
||||
v-if="
|
||||
(act?.blocked && !item.isActive) ||
|
||||
(!act?.blocked && item.isActive)
|
||||
"
|
||||
:key="index"
|
||||
:value="act.value"
|
||||
lines="one"
|
||||
class="cm-mini"
|
||||
>
|
||||
<v-list-item-title>{{ act.title }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<!-- Delete -->
|
||||
<v-btn icon size="small" variant="text" density="comfortable">
|
||||
<span class="mso text-xl">close</span>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-data-table-server>
|
||||
</template>
|
||||
117
src/pages/users/index.vue
Normal file
117
src/pages/users/index.vue
Normal file
@ -0,0 +1,117 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, Ref } from 'vue';
|
||||
import { useLocale } from 'vuetify';
|
||||
import { UsersPageTabs } from '@/enums/user-tab.enum';
|
||||
import { useUsersStore } from '@/stores/users';
|
||||
import { useGroupsStore } from '@/stores/groups';
|
||||
import ErrorPlate from '@/components/ErrorPlate.vue';
|
||||
import AddUser from './components/AddUser.vue';
|
||||
import UsersTab from './components/UsersTab.vue';
|
||||
import GroupsTab from './components/GroupsTab.vue';
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
const $users = useUsersStore();
|
||||
const $groups = useGroupsStore();
|
||||
|
||||
const tableHeight: Ref<number> = ref(500);
|
||||
setInterval(() => {
|
||||
const wY = window.innerHeight;
|
||||
const el = document.getElementById('table');
|
||||
if (el) {
|
||||
const bb = el.getBoundingClientRect();
|
||||
tableHeight.value = wY - bb.top;
|
||||
}
|
||||
}, 100);
|
||||
|
||||
const tab: Ref<UsersPageTabs> = ref(UsersPageTabs.USERS);
|
||||
|
||||
const userModal: Ref<boolean> = ref(false);
|
||||
|
||||
const getAll = () => {
|
||||
tab.value === UsersPageTabs.USERS ? $users.getAll() : $groups.getAll();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Header -->
|
||||
<v-app-bar prominent class="elevation-0 border-b">
|
||||
<!-- Title -->
|
||||
<span class="text-h6 ml-5 mr-10">{{ t('$vuetify.users.title') }}</span>
|
||||
|
||||
<!-- Create user -->
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="text"
|
||||
class="mr-3"
|
||||
@click="userModal = true"
|
||||
>
|
||||
<span class="mso mr-1 text-xl">person_add</span>
|
||||
<span>{{ t('$vuetify.actions.create-user') }}</span>
|
||||
</v-btn>
|
||||
|
||||
<AddUser v-model="userModal" @close="userModal = false" />
|
||||
|
||||
<!-- Refresh users -->
|
||||
<v-btn color="primary" variant="text" @click="getAll()">
|
||||
<span class="mso mr-1 text-xl">refresh</span>
|
||||
<span>{{ t('$vuetify.actions.refresh') }}</span>
|
||||
</v-btn>
|
||||
</v-app-bar>
|
||||
|
||||
<!-- Tabs -->
|
||||
<v-tabs
|
||||
v-model="tab"
|
||||
color="primary"
|
||||
class="border-b"
|
||||
density="comfortable"
|
||||
bg-color="surface"
|
||||
>
|
||||
<v-tab :value="UsersPageTabs.USERS">{{
|
||||
t('$vuetify.users.tabs.users')
|
||||
}}</v-tab>
|
||||
<v-tab :value="UsersPageTabs.GROUPS">{{
|
||||
t('$vuetify.users.tabs.groups')
|
||||
}}</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-card-title class="p-0 w-full">
|
||||
<v-text-field
|
||||
v-model="$users.search"
|
||||
density="compact"
|
||||
:label="t('$vuetify.users.search')"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
variant="solo-filled"
|
||||
class="border-b"
|
||||
:rounded="false"
|
||||
flat
|
||||
hide-details
|
||||
single-line
|
||||
></v-text-field>
|
||||
</v-card-title>
|
||||
|
||||
<!-- Error -->
|
||||
<ErrorPlate :error="$users.error" block />
|
||||
|
||||
<div class="h-0" id="table"></div>
|
||||
|
||||
<v-tabs-window v-model="tab">
|
||||
<!-- USERS -->
|
||||
<v-tabs-window-item :value="UsersPageTabs.USERS">
|
||||
<!-- Data -->
|
||||
<v-card flat rounded="0" :height="`${tableHeight}px`">
|
||||
<users-tab :tableHeight="tableHeight" />
|
||||
</v-card>
|
||||
</v-tabs-window-item>
|
||||
|
||||
<!-- GROUPS -->
|
||||
<v-tabs-window-item :value="UsersPageTabs.GROUPS">
|
||||
<!-- Data -->
|
||||
<v-card flat rounded="0" :height="`${tableHeight}px`">
|
||||
<groups-tab :tableHeight="tableHeight" />
|
||||
</v-card>
|
||||
</v-tabs-window-item>
|
||||
</v-tabs-window>
|
||||
</div>
|
||||
</template>
|
||||
3
src/plugins/README.md
Normal file
3
src/plugins/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Plugins
|
||||
|
||||
Plugins are a way to extend the functionality of your Vue application. Use this folder for registering plugins that you want to use globally.
|
||||
12
src/plugins/axios.ts
Normal file
12
src/plugins/axios.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import axios, { Axios } from 'axios';
|
||||
import type { App } from 'vue';
|
||||
|
||||
export const $axios: Axios = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_URL,
|
||||
});
|
||||
|
||||
export default {
|
||||
install: (app: App) => {
|
||||
app.config.globalProperties.$axios = $axios;
|
||||
},
|
||||
};
|
||||
43
src/plugins/cookie.ts
Normal file
43
src/plugins/cookie.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { ref, type Ref, type App } from 'vue';
|
||||
|
||||
class Cookie {
|
||||
private items: Ref<Map<any, any>> = ref(new Map());
|
||||
|
||||
constructor() {
|
||||
const stringified: string = document.cookie;
|
||||
if (stringified?.length) {
|
||||
const splitted = stringified.split('; ')
|
||||
for (const cookie of splitted) {
|
||||
const [key, value] = cookie.split('=')
|
||||
this.items.value.set(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public has(key: string): boolean {
|
||||
return this.items.value.has(key);
|
||||
}
|
||||
|
||||
public get(key: string): any {
|
||||
return this.items.value.get(key);
|
||||
}
|
||||
|
||||
public set(key: string, value: any): void {
|
||||
this.items.value.set(key, value);
|
||||
document.cookie = `${key}=${value}; Secure`;
|
||||
}
|
||||
|
||||
public remove(key: string): void {
|
||||
this.items.value.delete(key);
|
||||
document.cookie =
|
||||
`${key}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure`;
|
||||
}
|
||||
}
|
||||
|
||||
export const $cookie: Cookie = new Cookie()
|
||||
|
||||
export default {
|
||||
install: (app: App) => {
|
||||
app.config.globalProperties.$cookie = $cookie;
|
||||
},
|
||||
};
|
||||
20
src/plugins/index.ts
Normal file
20
src/plugins/index.ts
Normal file
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* plugins/index.ts
|
||||
*
|
||||
* Automatically included in `./src/main.ts`
|
||||
*/
|
||||
|
||||
// Plugins
|
||||
import vuetify from './vuetify'
|
||||
import pinia from '../stores'
|
||||
import router from '../router'
|
||||
|
||||
// Types
|
||||
import type { App } from 'vue'
|
||||
|
||||
export function registerPlugins(app: App) {
|
||||
app
|
||||
.use(vuetify)
|
||||
.use(router)
|
||||
.use(pinia)
|
||||
}
|
||||
9
src/plugins/moment.ts
Normal file
9
src/plugins/moment.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import moment from 'moment/min/moment-with-locales';
|
||||
|
||||
class Moment {
|
||||
public $moment = moment
|
||||
public locale(locale: string) {
|
||||
this.$moment.locale(locale)
|
||||
}
|
||||
}
|
||||
export default new Moment()
|
||||
38
src/plugins/vuetify.ts
Normal file
38
src/plugins/vuetify.ts
Normal file
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* plugins/vuetify.ts
|
||||
*
|
||||
* Framework documentation: https://vuetifyjs.com`
|
||||
*/
|
||||
|
||||
// Styles
|
||||
import '@mdi/font/css/materialdesignicons.css'
|
||||
import 'vuetify/styles'
|
||||
|
||||
// Composables
|
||||
import { createVuetify } from 'vuetify'
|
||||
|
||||
// Locales
|
||||
import { ru, en } from 'vuetify/locale'
|
||||
import ruLocal from '../locales/ru'
|
||||
import enLocal from '../locales/en'
|
||||
|
||||
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
|
||||
export default createVuetify({
|
||||
theme: {
|
||||
defaultTheme: 'dark',
|
||||
},
|
||||
locale: {
|
||||
locale: 'ru',
|
||||
fallback: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
...en,
|
||||
...enLocal,
|
||||
},
|
||||
ru: {
|
||||
...ru,
|
||||
...ruLocal,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
16
src/router/index.ts
Normal file
16
src/router/index.ts
Normal file
@ -0,0 +1,16 @@
|
||||
/**
|
||||
* router/index.ts
|
||||
*
|
||||
* Automatic routes for `./src/pages/*.vue`
|
||||
*/
|
||||
|
||||
// Composables
|
||||
import { createRouter, createWebHistory } from 'vue-router/auto'
|
||||
import { setupLayouts } from 'virtual:generated-layouts'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
extendRoutes: setupLayouts,
|
||||
})
|
||||
|
||||
export default router
|
||||
5
src/stores/README.md
Normal file
5
src/stores/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Store
|
||||
|
||||
Pinia stores are used to store reactive state and expose actions to mutate it.
|
||||
|
||||
Full documentation for this feature can be found in the Official [Pinia](https://pinia.esm.dev/) repository.
|
||||
8
src/stores/app.ts
Normal file
8
src/stores/app.ts
Normal file
@ -0,0 +1,8 @@
|
||||
// Utilities
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useAppStore = defineStore('app', {
|
||||
state: () => ({
|
||||
//
|
||||
}),
|
||||
})
|
||||
72
src/stores/groups.ts
Normal file
72
src/stores/groups.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, type Ref } from 'vue';
|
||||
import { User } from '@/types/user';
|
||||
import { Cursor } from '@/types/cursor';
|
||||
import { OrderBy } from '@/types/order-by';
|
||||
import { ITEMS_PER_PAGE } from '@/constants/static';
|
||||
import { defaultCursor, defaultSort } from '@/constants/defaults';
|
||||
import api from '@/api';
|
||||
|
||||
export const useGroupsStore = defineStore('groups', () => {
|
||||
const all: Ref<User[]> = ref([]);
|
||||
const cursor: Ref<Cursor> = ref(JSON.parse(defaultCursor));
|
||||
|
||||
const search: Ref<string> = ref('');
|
||||
const page: Ref<number> = ref(1);
|
||||
const perPage: Ref<number> = ref(ITEMS_PER_PAGE[0]);
|
||||
const orderBy: Ref<OrderBy> = ref(JSON.parse(defaultSort));
|
||||
|
||||
const loading: Ref<boolean> = ref(false);
|
||||
const error: Ref<string> = ref('');
|
||||
|
||||
const getAll = async () => {
|
||||
if (loading.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
const [res, e] = await api.groups.getAll(
|
||||
perPage.value,
|
||||
page.value,
|
||||
orderBy.value,
|
||||
);
|
||||
if (e && typeof e === 'string') {
|
||||
error.value = e;
|
||||
} else {
|
||||
error.value = '';
|
||||
all.value = res?.data?.data;
|
||||
cursor.value = res?.data?.cursor;
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
const setPage = (v: number) => (page.value = v);
|
||||
const setPerPage = (v: number) => (perPage.value = v);
|
||||
const setSort = (v: OrderBy[]) => {
|
||||
if (!v || !v.length) {
|
||||
orderBy.value = JSON.parse(defaultSort);
|
||||
} else {
|
||||
orderBy.value = v[0];
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
all,
|
||||
cursor,
|
||||
|
||||
search,
|
||||
page,
|
||||
perPage,
|
||||
orderBy,
|
||||
|
||||
loading,
|
||||
error,
|
||||
|
||||
getAll,
|
||||
setPage,
|
||||
setPerPage,
|
||||
setSort,
|
||||
};
|
||||
});
|
||||
4
src/stores/index.ts
Normal file
4
src/stores/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
// Utilities
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
export default createPinia()
|
||||
72
src/stores/users.ts
Normal file
72
src/stores/users.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, type Ref } from 'vue';
|
||||
import { User } from '@/types/user';
|
||||
import { Cursor } from '@/types/cursor';
|
||||
import { OrderBy } from '@/types/order-by';
|
||||
import { ITEMS_PER_PAGE } from '@/constants/static';
|
||||
import { defaultCursor, defaultSort } from '@/constants/defaults';
|
||||
import api from '@/api';
|
||||
|
||||
export const useUsersStore = defineStore('users', () => {
|
||||
const all: Ref<User[]> = ref([]);
|
||||
const cursor: Ref<Cursor> = ref(JSON.parse(defaultCursor));
|
||||
|
||||
const search: Ref<string> = ref('');
|
||||
const page: Ref<number> = ref(1);
|
||||
const perPage: Ref<number> = ref(ITEMS_PER_PAGE[0]);
|
||||
const orderBy: Ref<OrderBy> = ref(JSON.parse(defaultSort));
|
||||
|
||||
const loading: Ref<boolean> = ref(false);
|
||||
const error: Ref<string> = ref('');
|
||||
|
||||
const getAll = async () => {
|
||||
if (loading.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
const [res, e] = await api.users.getAll(
|
||||
perPage.value,
|
||||
page.value,
|
||||
orderBy.value,
|
||||
);
|
||||
if (e && typeof e === 'string') {
|
||||
error.value = e;
|
||||
} else {
|
||||
error.value = '';
|
||||
all.value = res?.data?.data;
|
||||
cursor.value = res?.data?.cursor;
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
const setPage = (v: number) => (page.value = v);
|
||||
const setPerPage = (v: number) => (perPage.value = v);
|
||||
const setSort = (v: OrderBy[]) => {
|
||||
if (!v || !v.length) {
|
||||
orderBy.value = JSON.parse(defaultSort);
|
||||
} else {
|
||||
orderBy.value = v[0];
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
all,
|
||||
cursor,
|
||||
|
||||
search,
|
||||
page,
|
||||
perPage,
|
||||
orderBy,
|
||||
|
||||
loading,
|
||||
error,
|
||||
|
||||
getAll,
|
||||
setPage,
|
||||
setPerPage,
|
||||
setSort,
|
||||
};
|
||||
});
|
||||
28
src/typed-router.d.ts
vendored
Normal file
28
src/typed-router.d.ts
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-router. ‼️ DO NOT MODIFY THIS FILE ‼️
|
||||
// It's recommended to commit this file.
|
||||
// Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry.
|
||||
|
||||
declare module 'vue-router/auto-routes' {
|
||||
import type {
|
||||
RouteRecordInfo,
|
||||
ParamValue,
|
||||
ParamValueOneOrMore,
|
||||
ParamValueZeroOrMore,
|
||||
ParamValueZeroOrOne,
|
||||
} from 'unplugin-vue-router/types'
|
||||
|
||||
/**
|
||||
* Route name map generated by unplugin-vue-router
|
||||
*/
|
||||
export interface RouteNamedMap {
|
||||
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
|
||||
'/auth': RouteRecordInfo<'/auth', '/auth', Record<never, never>, Record<never, never>>,
|
||||
'/users/': RouteRecordInfo<'/users/', '/users', Record<never, never>, Record<never, never>>,
|
||||
'/users/components/AddUser': RouteRecordInfo<'/users/components/AddUser', '/users/components/AddUser', Record<never, never>, Record<never, never>>,
|
||||
'/users/components/GroupsTab': RouteRecordInfo<'/users/components/GroupsTab', '/users/components/GroupsTab', Record<never, never>, Record<never, never>>,
|
||||
'/users/components/UsersTab': RouteRecordInfo<'/users/components/UsersTab', '/users/components/UsersTab', Record<never, never>, Record<never, never>>,
|
||||
}
|
||||
}
|
||||
6
src/types/cursor.d.ts
vendored
Normal file
6
src/types/cursor.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
export interface Cursor {
|
||||
count: number;
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
totalRows: number;
|
||||
}
|
||||
4
src/types/group-permission.d.ts
vendored
Normal file
4
src/types/group-permission.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
export interface GroupPermission {
|
||||
groupID: number;
|
||||
value: number
|
||||
}
|
||||
14
src/types/group.d.ts
vendored
Normal file
14
src/types/group.d.ts
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
import { GroupPermission } from "./group-permission";
|
||||
import { User } from "./user";
|
||||
|
||||
export interface Group {
|
||||
ID: number;
|
||||
issuerID: number;
|
||||
issuer: User;
|
||||
name: string;
|
||||
users: User[];
|
||||
permissions: GroupPermission[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
deletedAt: Date;
|
||||
}
|
||||
4
src/types/order-by.d.ts
vendored
Normal file
4
src/types/order-by.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
export interface OrderBy {
|
||||
key: string;
|
||||
order?: boolean | 'asc' | 'desc';
|
||||
}
|
||||
17
src/types/user.d.ts
vendored
Normal file
17
src/types/user.d.ts
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
import { Group } from "./group";
|
||||
|
||||
export interface User {
|
||||
ID: number;
|
||||
email: string;
|
||||
passwordHash: string;
|
||||
passwordLength: number;
|
||||
realName: string;
|
||||
groupID: number;
|
||||
group: Group;
|
||||
isRequiredToSetPassword: boolean;
|
||||
isActive: boolean;
|
||||
lastLogin: Date;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
deletedAt: Date;
|
||||
}
|
||||
8
src/vite-env.d.ts
vendored
Normal file
8
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
/* eslint-disable */
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
6
src/vue.d.ts
vendored
Normal file
6
src/vue.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const component: DefineComponent<object, object, any>
|
||||
export default component
|
||||
}
|
||||
13
tailwind.config.js
Normal file
13
tailwind.config.js
Normal file
@ -0,0 +1,13 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx,vue}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
important: true,
|
||||
}
|
||||
|
||||
48
tsconfig.json
Normal file
48
tsconfig.json
Normal file
@ -0,0 +1,48 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"jsx": "preserve",
|
||||
"lib": [
|
||||
"DOM",
|
||||
"ESNext"
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
"resolveJsonModule": true,
|
||||
"types": [
|
||||
"vite/client",
|
||||
"vite-plugin-vue-layouts/client",
|
||||
"unplugin-vue-router/client"
|
||||
],
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"noUnusedLocals": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*.d.ts",
|
||||
"./src/**/*",
|
||||
"./src/**/*.{vue,ts,js}",
|
||||
"./src/**/**/*.json"
|
||||
],
|
||||
"exclude": [
|
||||
"dist",
|
||||
"node_modules",
|
||||
"cypress"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
],
|
||||
}
|
||||
11
tsconfig.node.json
Normal file
11
tsconfig.node.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": [
|
||||
"vite.config.mts"
|
||||
]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user