add desktop frontend framework

This commit is contained in:
MaysWind
2023-06-22 18:44:24 +08:00
parent a9e36b9a59
commit 4b49c1f30f
29 changed files with 3396 additions and 35 deletions
+820 -26
View File
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -18,6 +18,7 @@
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
},
"dependencies": {
"@mdi/js": "^7.2.96",
"@vuepic/vue-datepicker": "^5.1.2",
"axios": "^1.4.0",
"cbor-js": "^0.1.0",
@@ -38,7 +39,10 @@
"swiper": "^9.3.2",
"ua-parser-js": "^1.0.35",
"vue": "^3.3.4",
"vue-i18n": "^9.2.2"
"vue-i18n": "^9.2.2",
"vue-router": "^4.2.2",
"vue3-perfect-scrollbar": "^1.6.1",
"vuetify": "^3.3.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.0",
+67 -6
View File
@@ -1,15 +1,76 @@
<template>
<div></div>
<img style="display: none;" :src="devCookiePath" v-if="!isProduction" />
<v-app>
<router-view />
</v-app>
</template>
<script>
import { useTheme } from 'vuetify';
import { mapStores } from 'pinia';
import { useSettingsStore } from '@/stores/setting.js';
import { useUserStore } from '@/stores/user.js';
import { useTokensStore } from '@/stores/token.js';
import { useExchangeRatesStore } from '@/stores/exchangeRates.js';
import { loadMapAssets } from '@/lib/map/index.js';
export default {
created() {
if (process.env.NODE_ENV === 'production') {
window.location.replace('../mobile/');
} else {
window.location.replace('../mobile.html');
data() {
const self = this;
return {
isProduction: self.$settings.isProduction(),
devCookiePath: self.$settings.isProduction() ? '' : '/dev/cookies'
}
},
computed: {
...mapStores(useSettingsStore, useUserStore, useTokensStore, useExchangeRatesStore),
},
created() {
const self = this;
const theme = useTheme();
if (self.$settings.getTheme() === 'light') {
theme.global.name.value = 'light';
} else if (self.$settings.getTheme() === 'dark') {
theme.global.name.value = 'dark';
}
let localeDefaultSettings = self.$locale.initLocale(self.userStore.currentUserLanguage);
self.settingsStore.updateLocalizedDefaultSettings(localeDefaultSettings);
if (self.$user.isUserLogined()) {
if (!self.$settings.isEnableApplicationLock()) {
// refresh token if user is logined
self.tokensStore.refreshTokenAndRevokeOldToken().then(response => {
if (response.user && response.user.language) {
localeDefaultSettings = self.$locale.setLanguage(response.user.language);
self.settingsStore.updateLocalizedDefaultSettings(localeDefaultSettings);
}
});
// auto refresh exchange rates data
if (self.$settings.isAutoUpdateExchangeRatesData()) {
self.exchangeRatesStore.getLatestExchangeRates({ silent: true, force: false });
}
}
}
},
mounted() {
document.addEventListener('DOMContentLoaded', () => {
const languageInfo = this.$locale.getCurrentLanguageInfo();
loadMapAssets(languageInfo ? languageInfo.code : null);
});
}
}
</script>
<style>
/** Global style **/
* {
padding: 0;
margin: 0
}
</style>
+311 -1
View File
@@ -1,6 +1,316 @@
import { createApp } from 'vue'
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import { createI18n } from 'vue-i18n';
import { createVuetify } from 'vuetify';
import { VApp } from 'vuetify/components/VApp';
import { VAvatar } from 'vuetify/components/VAvatar';
import { VBtn } from 'vuetify/components/VBtn';
import { VCard, VCardActions, VCardItem, VCardSubtitle, VCardText, VCardTitle } from 'vuetify/components/VCard';
import { VChip } from 'vuetify/components/VChip';
import { VDialog } from 'vuetify/components/VDialog';
import { VDivider } from 'vuetify/components/VDivider';
import { VForm } from 'vuetify/components/VForm';
import { VContainer, VCol, VRow, VSpacer } from 'vuetify/components/VGrid';
import { VIcon } from 'vuetify/components/VIcon';
import { VImg } from 'vuetify/components/VImg';
import { VInput } from 'vuetify/components/VInput';
import { VList, VListGroup, VListImg, VListItem, VListItemAction, VListItemMedia, VListItemSubtitle, VListItemTitle, VListSubheader } from 'vuetify/components/VList';
import { VMenu } from 'vuetify/components/VMenu';
import { VOverlay } from 'vuetify/components/VOverlay';
import { VPagination } from 'vuetify/components/VPagination';
import { VProgressCircular } from 'vuetify/components/VProgressCircular';
import { VProgressLinear } from 'vuetify/components/VProgressLinear';
import { VSelect } from 'vuetify/components/VSelect';
import { VSheet } from 'vuetify/components/VSheet';
import { VSnackbar } from 'vuetify/components/VSnackbar';
import { VTabs } from 'vuetify/components/VTabs';
import { VTable } from 'vuetify/components/VTable';
import { VTextField } from 'vuetify/components/VTextField';
import { aliases, mdi } from 'vuetify/iconsets/mdi-svg';
import 'vuetify/styles';
import 'line-awesome/dist/line-awesome/css/line-awesome.css';
import { PerfectScrollbar } from 'vue3-perfect-scrollbar';
import 'vue3-perfect-scrollbar/dist/vue3-perfect-scrollbar.min.css';
import VueDatePicker from '@vuepic/vue-datepicker';
import '@vuepic/vue-datepicker/dist/main.css';
import router from '@/router/desktop.js';
import version from '@/lib/version.js';
import settings from '@/lib/settings.js';
import userstate from '@/lib/userstate.js';
import {
getI18nOptions,
translateIf,
translateError,
i18nFunctions
} from '@/lib/i18n.js';
import '@/styles/desktop/base.css';
import '@/styles/desktop/layout.css';
import '@/styles/desktop/font-size.css';
import '@/styles/desktop/gap-size.css';
import '@/styles/desktop/vuetify.css';
import '@/styles/desktop/classess.css';
import App from './DesktopApp.vue';
const app = createApp(App);
const pinia = createPinia();
const i18n = createI18n(getI18nOptions());
const vuetify = createVuetify({
components: {
VApp,
VAvatar,
VBtn,
VCard,
VCardActions,
VCardItem,
VCardSubtitle,
VCardText,
VCardTitle,
VChip,
VDialog,
VDivider,
VForm,
VContainer,
VCol,
VRow,
VSpacer,
VIcon,
VImg,
VInput,
VList,
VListGroup,
VListImg,
VListItem,
VListItemAction,
VListItemMedia,
VListItemSubtitle,
VListItemTitle,
VListSubheader,
VMenu,
VOverlay,
VPagination,
VProgressCircular,
VProgressLinear,
VSelect,
VSheet,
VSnackbar,
VTabs,
VTable,
VTextField
},
icons: {
defaultSet: 'mdi',
aliases,
sets: {
mdi
}
},
defaults: {
VAlert: {
VBtn: {
color: undefined
}
},
VAutocomplete: {
variant: 'outlined',
color: 'primary',
hideDetails: 'auto'
},
VAvatar: {
variant: 'flat',
VIcon: {
size: 24,
},
},
VBadge: {
color: 'primary'
},
VBtn: {
color: 'primary'
},
VCheckbox: {
color: 'primary',
hideDetails: 'auto'
},
VChip: {
elevation: 0
},
VList: {
color: 'primary'
},
VPagination: {
activeColor: 'primary'
},
VRadio: {
color: 'primary',
hideDetails: 'auto'
},
VSelect: {
variant: 'outlined',
color: 'primary',
hideDetails: 'auto'
},
VSlider: {
color: 'primary',
hideDetails: 'auto'
},
VSwitch: {
color: 'primary',
hideDetails: 'auto'
},
VProgressCircular: {
size: 40
},
VTabs: {
color: 'primary',
VSlideGroup: {
showArrows: true
}
},
VTextarea: {
variant: 'outlined',
color: 'primary',
hideDetails: 'auto'
},
VTextField: {
variant: 'outlined',
color: 'primary',
hideDetails: 'auto'
},
VTooltip: {
location: 'top'
}
},
theme: {
defaultTheme: 'light',
themes: {
light: {
dark: false,
colors: {
'primary': '#c67e48',
'secondary': '#8a8d93',
'on-secondary': '#fff',
'success': '#4cd964',
'info': '#2196f3',
'warning': '#ff9500',
'error': '#ff3b30',
'on-primary': '#ffffff',
'on-success': '#ffffff',
'on-warning': '#ffffff',
'background': '#faf8f4',
'on-background': '#413935',
'on-surface': '#413935',
'grey-50': '#fafafa',
'grey-100': '#f0f2f8',
'grey-200': '#eeeeee',
'grey-300': '#e0e0e0',
'grey-400': '#bdbdbd',
'grey-500': '#9e9e9e',
'grey-600': '#757575',
'grey-700': '#616161',
'grey-800': '#424242',
'grey-900': '#212121',
'perfect-scrollbar-thumb': '#dbdade',
'skin-bordered-background': '#fff',
'skin-bordered-surface': '#fff'
},
variables: {
'btn-height': '38px',
'code-color': '#ff8000',
'overlay-scrim-background': '#3a3541',
'overlay-scrim-opacity': 0.5,
'hover-opacity': 0.04,
'focus-opacity': 0.1,
'selected-opacity': 0.12,
'activated-opacity': 0.1,
'pressed-opacity': 0.14,
'dragged-opacity': 0.1,
'border-color': '#3a3541',
'table-header-background': '#f9fafc',
'custom-background': '#f9f8f9',
'shadow-key-umbra-opacity': 'rgba(var(--v-theme-on-surface), 0.08)',
'shadow-key-penumbra-opacity': 'rgba(var(--v-theme-on-surface), 0.12)',
'shadow-key-ambient-opacity': 'rgba(var(--v-theme-on-surface), 0.04)'
}
},
dark: {
dark: true,
colors: {
'primary': '#c67e48',
'secondary': '#8a8d93',
'on-secondary': '#fff',
'success': '#4cd964',
'info': '#2196f3',
'warning': '#ff9500',
'error': '#ff3b30',
'on-primary': '#ffffff',
'on-success': '#ffffff',
'on-warning': '#ffffff',
'background': '#28243d',
'on-background': '#fcf0e3',
'surface': '#312d4b',
'on-surface': '#fcf0e3',
'grey-50': '#2a2e42',
'grey-100': '#474360',
'grey-200': '#4a5072',
'grey-300': '#5e6692',
'grey-400': '#7983bb',
'grey-500': '#8692d0',
'grey-600': '#aab3de',
'grey-700': '#b6bee3',
'grey-800': '#cfd3ec',
'grey-900': '#e7e9f6',
'perfect-scrollbar-thumb': '#4a5072',
'skin-bordered-background': '#312d4b',
'skin-bordered-surface': '#312d4b'
},
variables: {
'btn-height': '38px',
'code-color': '#ff8000',
'overlay-scrim-background': '#2C2942',
'overlay-scrim-opacity': 0.6,
'hover-opacity': 0.04,
'focus-opacity': 0.1,
'selected-opacity': 0.12,
'activated-opacity': 0.1,
'pressed-opacity': 0.14,
'dragged-opacity': 0.1,
'border-color': '#E7E3FC',
'table-header-background': '#3D3759',
'custom-background': '#373452',
'shadow-key-umbra-opacity': 'rgba(20, 18, 33, 0.08)',
'shadow-key-penumbra-opacity': 'rgba(20, 18, 33, 0.12)',
'shadow-key-ambient-opacity': 'rgba(20, 18, 33, 0.04)'
}
}
}
}
});
app.use(pinia);
app.use(i18n);
app.use(vuetify);
app.use(router);
app.component('PerfectScrollbar', PerfectScrollbar);
app.component('VueDatePicker', VueDatePicker);
app.config.globalProperties.$version = version.getVersion();
app.config.globalProperties.$buildTime = version.getBuildTime();
app.config.globalProperties.$settings = settings;
app.config.globalProperties.$locale = i18nFunctions(i18n.global);
app.config.globalProperties.$tIf = (text, isTranslate) => translateIf(text, isTranslate, i18n.global.t);
app.config.globalProperties.$tError = (message) => translateError(message, i18n.global.t);
app.config.globalProperties.$user = userstate;
app.mount('#app');
+5
View File
@@ -702,6 +702,7 @@ export default {
'Not Specified': 'Not Specified',
'No results': 'No results',
'Unknown': 'Unknown',
'Other': 'Other',
'Default': 'Default',
'Done': 'Done',
'Continue': 'Continue',
@@ -746,6 +747,7 @@ export default {
'Accounts': 'Accounts',
'Statistics': 'Statistics',
'Settings': 'Settings',
'Application Settings': 'Application Settings',
'Select All': 'Select All',
'Select None': 'Select None',
'Invert Selection': 'Invert Selection',
@@ -806,6 +808,7 @@ export default {
'PIN code is invalid': 'PIN code is invalid',
'PIN code is wrong': 'PIN code is wrong',
'Sign Up': 'Sign Up',
'Overview': 'Overview',
'Transaction List': 'Transaction List',
'Account List': 'Account List',
'This Week': 'This Week',
@@ -915,6 +918,8 @@ export default {
'No transaction data': 'No transaction data',
'Are you sure you want to delete this transaction?': 'Are you sure you want to delete this transaction?',
'Unable to delete this transaction': 'Unable to delete this transaction',
'Transaction Data': 'Transaction Data',
'Statistics Data': 'Statistics Data',
'Unable to get transaction statistics': 'Unable to get transaction statistics',
'Total Amount': 'Total Amount',
'Total Assets': 'Total Assets',
+5
View File
@@ -702,6 +702,7 @@ export default {
'Not Specified': '未指定',
'No results': '无结果',
'Unknown': '未知',
'Other': '其他',
'Default': '默认',
'Done': '完成',
'Continue': '继续',
@@ -746,6 +747,7 @@ export default {
'Accounts': '账户',
'Statistics': '统计',
'Settings': '设置',
'Application Settings': '应用设置',
'Select All': '全部选择',
'Select None': '全部不选',
'Invert Selection': '反向选择',
@@ -806,6 +808,7 @@ export default {
'PIN code is invalid': 'PIN码无效',
'PIN code is wrong': 'PIN码错误',
'Sign Up': '注册',
'Overview': '总览',
'Transaction List': '交易列表',
'Account List': '账户列表',
'This Week': '本周',
@@ -915,6 +918,8 @@ export default {
'No transaction data': '没有交易数据',
'Are you sure you want to delete this transaction?': '您确定要删除该交易?',
'Unable to delete this transaction': '无法删除该交易',
'Transaction Data': '交易数据',
'Statistics Data': '统计数据',
'Unable to get transaction statistics': '无法获取交易统计数据',
'Total Amount': '总金额',
'Total Assets': '总资产',
+137
View File
@@ -0,0 +1,137 @@
import { createRouter, createWebHashHistory } from 'vue-router';
import userState from '@/lib/userstate.js';
import MainLayout from '@/views/desktop/MainLayout.vue';
import LoginPage from '@/views/desktop/LoginPage.vue';
import SignUpPage from '@/views/desktop/SignupPage.vue';
import UnlockPage from '@/views/desktop/UnlockPage.vue';
import HomePage from '@/views/desktop/HomePage.vue';
import TransactionsPage from '@/views/desktop/TransactionsPage.vue';
import StatisticsTransactionPage from '@/views/desktop/statistics/TransactionPage.vue';
import AccountsPage from '@/views/desktop/AccountsPage.vue';
import TransactionCategoriesPage from '@/views/desktop/TransactionCategoriesPage.vue';
import TransactionTagsPage from '@/views/desktop/TransactionTagsPage.vue';
import ExchangeRatesPage from '@/views/desktop/ExchangeRatesPage.vue';
import UserSettingsPage from '@/views/desktop/user/UserSettingsPage.vue';
import AppSettingsPage from '@/views/desktop/app/AppSettingsPage.vue';
import AboutPage from '@/views/desktop/AboutPage.vue';
function checkLogin() {
if (!userState.isUserLogined()) {
return {
path: '/login',
replace: true
};
}
if (!userState.isUserUnlocked()) {
return {
path: '/unlock',
replace: true
};
}
}
function checkLocked() {
if (!userState.isUserLogined()) {
return {
path: '/login',
replace: true
};
}
if (userState.isUserUnlocked()) {
return {
path: '/',
replace: true
};
}
}
function checkNotLogin() {
if (userState.isUserLogined() && !userState.isUserUnlocked()) {
return {
path: '/unlock',
replace: true
};
}
if (userState.isUserLogined()) {
return {
path: '/',
replace: true
};
}
}
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: '/',
component: MainLayout,
beforeEnter: checkLogin,
children: [
{
path: '',
component: HomePage
},
{
path: '/transactions',
component: TransactionsPage
},
{
path: '/statistics/transaction',
component: StatisticsTransactionPage
},
{
path: '/accounts',
component: AccountsPage
},
{
path: '/categories',
component: TransactionCategoriesPage
},
{
path: '/tags',
component: TransactionTagsPage
},
{
path: '/exchange_rates',
component: ExchangeRatesPage
},
{
path: '/user/settings',
component: UserSettingsPage
},
{
path: '/app/settings',
component: AppSettingsPage
},
{
path: '/about',
component: AboutPage
}
]
},
{
path: '/login',
component: LoginPage,
beforeEnter: checkNotLogin
},
{
path: '/signup',
component: LoginPage,
beforeEnter: SignUpPage
},
{
path: '/unlock',
component: UnlockPage,
beforeEnter: checkLocked
}
],
})
export default router;
+42
View File
@@ -0,0 +1,42 @@
/** Base class **/
/** reference: https://github.com/themeselection/materio-vuetify-vuejs-admin-template-free **/
*,
::before,
::after {
box-sizing: inherit;
background-repeat: no-repeat;
}
:root {
--v-theme-overlay-multiplier: 1;
--v-scrollbar-offset: 0px;
}
html {
box-sizing: border-box;
}
html {
font-family: inter,sans-serif,-apple-system,blinkmacsystemfont,Segoe UI,roboto,Helvetica Neue,arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol;
line-height: 1.5;
font-size: 1rem;
overflow-x: hidden;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-tap-highlight-color: rgba(0,0,0,0);
}
html.overflow-y-hidden {
overflow-y: hidden!important;
}
a {
color: rgb(var(--v-theme-primary));
text-decoration: none;
}
p {
margin-block-end: 1rem;
}
+12
View File
@@ -0,0 +1,12 @@
.disabled {
opacity: 0.55 !important;
pointer-events: none !important;
}
.readonly {
pointer-events: none !important;
}
.cursor-pointer {
cursor: pointer;
}
+67
View File
@@ -0,0 +1,67 @@
/** Text size class **/
/** reference: https://github.com/themeselection/materio-vuetify-vuejs-admin-template-free **/
.text-xs {
font-size: .75rem;
line-height: 1rem;
}
.text-sm {
font-size: .875rem;
line-height: 1.25rem;
}
.text-base {
font-size: 1rem;
line-height: 1.5rem;
}
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
.text-xl {
font-size: 1.25rem;
line-height: 1.75rem;
}
.text-2xl {
font-size: 1.5rem;
line-height: 2rem;
}
.text-3xl {
font-size: 1.875rem;
line-height: 2.25rem;
}
.text-4xl {
font-size: 2.25rem;
line-height: 2.5rem;
}
.text-5xl {
font-size: 3rem;
line-height: 1;
}
.text-6xl {
font-size: 3.75rem;
line-height: 1;
}
.text-7xl {
font-size: 4.5rem;
line-height: 1;
}
.text-8xl {
font-size: 6rem;
line-height: 1;
}
.text-9xl {
font-size: 8rem;
line-height: 1;
}
+362
View File
@@ -0,0 +1,362 @@
/** Gap size class **/
/** reference: https://github.com/themeselection/materio-vuetify-vuejs-admin-template-free **/
.gap-0 {
gap: 0;
}
.gap-x-0 {
column-gap: 0;
}
.gap-y-0 {
row-gap: 0;
}
.gap-1 {
gap: .25rem;
}
.gap-x-1 {
column-gap: .25rem;
}
.gap-y-1 {
row-gap: .25rem;
}
.gap-2 {
gap: .5rem;
}
.gap-x-2 {
column-gap: .5rem;
}
.gap-y-2 {
row-gap: .5rem;
}
.gap-3 {
gap: .75rem;
}
.gap-x-3 {
column-gap: .75rem;
}
.gap-y-3 {
row-gap: .75rem;
}
.gap-4 {
gap: 1rem;
}
.gap-x-4 {
column-gap: 1rem;
}
.gap-y-4 {
row-gap: 1rem;
}
.gap-5 {
gap: 1.25rem;
}
.gap-x-5 {
column-gap: 1.25rem;
}
.gap-y-5 {
row-gap: 1.25rem;
}
.gap-6 {
gap: 1.5rem;
}
.gap-x-6 {
column-gap: 1.5rem;
}
.gap-y-6 {
row-gap: 1.5rem;
}
.gap-7 {
gap: 1.75rem;
}
.gap-x-7 {
column-gap: 1.75rem;
}
.gap-y-7 {
row-gap: 1.75rem;
}
.gap-8 {
gap: 2rem;
}
.gap-x-8 {
column-gap: 2rem;
}
.gap-y-8 {
row-gap: 2rem;
}
.gap-9 {
gap: 2.25rem;
}
.gap-x-9 {
column-gap: 2.25rem;
}
.gap-y-9 {
row-gap: 2.25rem;
}
.gap-10 {
gap: 2.5rem;
}
.gap-x-10 {
column-gap: 2.5rem;
}
.gap-y-10 {
row-gap: 2.5rem;
}
.gap-11 {
gap: 2.75rem;
}
.gap-x-11 {
column-gap: 2.75rem;
}
.gap-y-11 {
row-gap: 2.75rem;
}
.gap-12 {
gap: 3rem;
}
.gap-x-12 {
column-gap: 3rem;
}
.gap-y-12 {
row-gap: 3rem;
}
.gap-14 {
gap: 3.5rem;
}
.gap-x-14 {
column-gap: 3.5rem;
}
.gap-y-14 {
row-gap: 3.5rem;
}
.gap-16 {
gap: 4rem;
}
.gap-x-16 {
column-gap: 4rem;
}
.gap-y-16 {
row-gap: 4rem;
}
.gap-20 {
gap: 5rem;
}
.gap-x-20 {
column-gap: 5rem;
}
.gap-y-20 {
row-gap: 5rem;
}
.gap-24 {
gap: 6rem;
}
.gap-x-24 {
column-gap: 6rem;
}
.gap-y-24 {
row-gap: 6rem;
}
.gap-28 {
gap: 7rem;
}
.gap-x-28 {
column-gap: 7rem;
}
.gap-y-28 {
row-gap: 7rem;
}
.gap-32 {
gap: 8rem;
}
.gap-x-32 {
column-gap: 8rem;
}
.gap-y-32 {
row-gap: 8rem;
}
.gap-36 {
gap: 9rem;
}
.gap-x-36 {
column-gap: 9rem;
}
.gap-y-36 {
row-gap: 9rem;
}
.gap-40 {
gap: 10rem;
}
.gap-x-40 {
column-gap: 10rem;
}
.gap-y-40 {
row-gap: 10rem;
}
.gap-44 {
gap: 11rem;
}
.gap-x-44 {
column-gap: 11rem;
}
.gap-y-44 {
row-gap: 11rem;
}
.gap-48 {
gap: 12rem;
}
.gap-x-48 {
column-gap: 12rem;
}
.gap-y-48 {
row-gap: 12rem;
}
.gap-52 {
gap: 13rem;
}
.gap-x-52 {
column-gap: 13rem;
}
.gap-y-52 {
row-gap: 13rem;
}
.gap-56 {
gap: 14rem;
}
.gap-x-56 {
column-gap: 14rem;
}
.gap-y-56 {
row-gap: 14rem;
}
.gap-60 {
gap: 15rem;
}
.gap-x-60 {
column-gap: 15rem;
}
.gap-y-60 {
row-gap: 15rem;
}
.gap-64 {
gap: 16rem;
}
.gap-x-64 {
column-gap: 16rem;
}
.gap-y-64 {
row-gap: 16rem;
}
.gap-72 {
gap: 18rem;
}
.gap-x-72 {
column-gap: 18rem;
}
.gap-y-72 {
row-gap: 18rem;
}
.gap-80 {
gap: 20rem;
}
.gap-x-80 {
column-gap: 20rem;
}
.gap-y-80 {
row-gap: 20rem;
}
.gap-96 {
gap: 24rem;
}
.gap-x-96 {
column-gap: 24rem;
}
.gap-y-96 {
row-gap: 24rem;
}
+544
View File
@@ -0,0 +1,544 @@
/** Layout class **/
/** reference: https://github.com/themeselection/materio-vuetify-vuejs-admin-template-free **/
html,
body {
min-block-size: 100%;
}
.layout-nav-type-vertical .layout-vertical-nav .nav-section-title,
.layout-nav-type-vertical .layout-vertical-nav .nav-link > :first-child,
.layout-nav-type-vertical .layout-vertical-nav .nav-group > :first-child {
margin-block: 0;
margin-inline: 0 1.125rem;
padding-block: 0;
padding-inline: 1.375rem 1rem;
white-space: nowrap;
}
.layout-nav-type-vertical .layout-vertical-nav .nav-link > :first-child,
.layout-nav-type-vertical .layout-vertical-nav .nav-group > :first-child {
border-radius: .4rem;
block-size: 2.75rem;
margin-block-end: .375rem;
}
.layout-nav-type-vertical .layout-vertical-nav .nav-link .nav-item-icon,
.layout-nav-type-vertical .layout-vertical-nav .nav-group .nav-item-icon {
flex-shrink: 0;
font-size: 1.5rem;
margin-inline-end: .625rem;
}
.layout-nav-type-vertical .layout-vertical-nav .nav-group .nav-group .nav-item-icon,
.layout-nav-type-vertical .layout-vertical-nav .nav-group .nav-link .nav-item-icon {
font-size: .9rem;
margin-inline-end: .925rem;
margin-inline-start: .3rem;
}
.layout-nav-type-vertical .layout-vertical-nav .nav-group .nav-group .nav-link .nav-item-icon,
.layout-nav-type-vertical .layout-vertical-nav .nav-group .nav-group .nav-group .nav-item-icon {
visibility: hidden;
}
.layout-nav-type-vertical .layout-vertical-nav .nav-group.active > :first-child:before,
.layout-nav-type-vertical .layout-vertical-nav .nav-group.open > :first-child:before {
opacity: calc(var(--v-selected-opacity) * var(--v-theme-overlay-multiplier));
}
.layout-nav-type-vertical .layout-vertical-nav .nav-group.active > :hover:first-child .nav-group.active > :first-child:before,
.layout-nav-type-vertical .layout-vertical-nav .nav-group.open > :hover:first-child .nav-group.active > :first-child:before,
.layout-nav-type-vertical .layout-vertical-nav .nav-group.active > :hover:first-child .nav-group.open > :first-child:before,
.layout-nav-type-vertical .layout-vertical-nav .nav-group.open > :hover:first-child .nav-group.open > :first-child:before {
opacity: calc(var(--v-selected-opacity) + var(--v-hover-opacity) * var(--v-theme-overlay-multiplier));
}
.layout-nav-type-vertical .layout-vertical-nav .nav-group.active > :focus-visible:first-child .nav-group.active > :first-child:before,
.layout-nav-type-vertical .layout-vertical-nav .nav-group.open > :focus-visible:first-child .nav-group.active > :first-child:before,
.layout-nav-type-vertical .layout-vertical-nav .nav-group.active > :focus-visible:first-child .nav-group.open > :first-child:before,
.layout-nav-type-vertical .layout-vertical-nav .nav-group.open > :focus-visible:first-child .nav-group.open > :first-child:before {
opacity: calc(var(--v-selected-opacity) + var(--v-focus-opacity) * var(--v-theme-overlay-multiplier));
}
@supports not selector(:focus-visible) {
.layout-nav-type-vertical .layout-vertical-nav .nav-group.active > :focus:first-child:before,
.layout-nav-type-vertical .layout-vertical-nav .nav-group.open > :focus:first-child:before {
opacity: calc(var(--v-selected-opacity) + var(--v-focus-opacity) * var(--v-theme-overlay-multiplier));
}
}
.layout-nav-type-vertical .layout-vertical-nav .nav-section-title {
block-size: 1.5rem;
color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity));
font-size: .75rem;
text-transform: uppercase;
}
.layout-nav-type-vertical .layout-vertical-nav .nav-item-badge {
display: inline-block;
border-radius: 1.5rem;
font-size: .8em;
font-weight: 500;
line-height: 1;
padding-block: .25em;
padding-inline: .55em;
text-align: center;
vertical-align: baseline;
white-space: nowrap;
}
.layout-nav-type-vertical .layout-vertical-nav {
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
}
.layout-nav-type-vertical .layout-vertical-nav .nav-item-title {
letter-spacing: .15px;
}
.layout-nav-type-vertical .layout-vertical-nav .nav-section-title {
letter-spacing: .4px;
}
.layout-nav-type-vertical .layout-vertical-nav .nav-link > .router-link-exact-active {
background-color: rgb(var(--v-theme-primary));
color: rgb(var(--v-theme-on-primary));
box-shadow: 0 4px 14px -4px var(--v-shadow-key-umbra-opacity), 0 4px 8px -4px var(--v-shadow-key-penumbra-opacity), 0 4px 8px -4px var(--v-shadow-key-ambient-opacity);
}
.layout-nav-type-vertical .layout-vertical-nav .nav-link a {
color: inherit;
}
.layout-navbar {
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
}
html.v-overlay-scroll-blocked:not([style*="--v-body-scroll-y: 0px;"]) .layout-navbar-sticky .navbar-blur.layout-navbar .navbar-content-container,
.layout-wrapper.layout-nav-type-vertical.window-scrolled.layout-navbar-sticky .navbar-blur.layout-navbar .navbar-content-container {
-webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px);
background-color: rgb(var(--v-theme-surface), .9);
}
html.v-overlay-scroll-blocked:not([style*="--v-body-scroll-y: 0px;"]) .layout-navbar-sticky .layout-navbar .navbar-content-container,
.layout-wrapper.layout-nav-type-vertical.window-scrolled.layout-navbar-sticky .layout-navbar .navbar-content-container {
background-color: rgb(var(--v-theme-surface));
}
html.v-overlay-scroll-blocked:not([style*="--v-body-scroll-y: 0px;"]) .layout-navbar-sticky .layout-navbar .navbar-content-container,
.layout-wrapper.layout-nav-type-vertical.window-scrolled.layout-navbar-sticky .layout-navbar .navbar-content-container {
box-shadow: 0 4px 14px -4px var(--v-shadow-key-umbra-opacity), 0 4px 8px -4px var(--v-shadow-key-penumbra-opacity), 0 4px 8px -4px var(--v-shadow-key-ambient-opacity);
padding-inline: 1.2rem;
}
.layout-nav-type-vertical .layout-vertical-nav .nav-link > .router-link-exact-active {
background: linear-gradient(-72.47deg, rgb(var(--v-theme-primary)) 22.16%, rgba(var(--v-theme-primary), .7) 76.47%) !important;
}
.layout-nav-type-vertical .layout-vertical-nav .nav-section-title .title-text {
display: flex;
flex-wrap: nowrap;
align-items: center;
justify-content: flex-start;
column-gap: .625rem;
}
.layout-nav-type-vertical .layout-vertical-nav .nav-section-title .title-text:before,
.layout-nav-type-vertical .layout-vertical-nav .nav-section-title .title-text:after {
border-block-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
content: "";
}
.layout-nav-type-vertical .layout-vertical-nav .nav-section-title .title-text:after {
flex: 1 1 auto;
}
.layout-nav-type-vertical .layout-vertical-nav .nav-section-title .title-text:before {
flex: 0 1 .75rem;
margin-inline-start: -1.375rem;
}
.layout-nav-type-vertical.layout-vertical-nav-collapsed .layout-vertical-nav:not(.hovered) .nav-section-title {
margin-inline: 4px 0;
}
.layout-nav-type-vertical .layout-vertical-nav .nav-link > :first-child,
.layout-nav-type-vertical .layout-vertical-nav .nav-group > :first-child {
block-size: 2.625rem !important;
border-end-end-radius: 3.125rem !important;
border-end-start-radius: 0 !important;
border-start-end-radius: 3.125rem !important;
border-start-start-radius: 0 !important;
}
.layout-nav-type-vertical .layout-vertical-nav .nav-link > :first-child,
.layout-nav-type-vertical .layout-vertical-nav .nav-group > :first-child {
transition: margin-inline .15s ease-in-out;
will-change: margin-inline;
}
.layout-nav-type-vertical.layout-vertical-nav-collapsed .layout-vertical-nav:not(.hovered) .nav-link > :first-child,
.layout-nav-type-vertical.layout-vertical-nav-collapsed .layout-vertical-nav:not(.hovered) .nav-group > :first-child {
margin-inline: 0 5px;
}
.layout-nav-type-vertical .layout-vertical-nav {
background-color: rgb(var(--v-theme-background));
}
.layout-vertical-nav-collapsed.layout-nav-type-vertical .layout-vertical-nav.hovered {
box-shadow: 0 4px 5px -2px var(--v-shadow-key-umbra-opacity), 0 2px 10px 1px var(--v-shadow-key-penumbra-opacity), 0 2px 16px 1px var(--v-shadow-key-ambient-opacity);
}
.layout-nav-type-vertical .layout-vertical-nav .nav-header {
overflow: hidden;
padding: 1rem .25rem 1rem 1.375rem;
margin-inline: 0 1.125rem;
min-block-size: 64px;
}
.layout-nav-type-vertical .layout-vertical-nav .nav-header .app-logo {
flex-shrink: 0;
transition: transform .25s ease-in-out;
}
.layout-vertical-nav-collapsed.layout-nav-type-vertical .layout-vertical-nav:not(.hovered) .nav-header .app-logo {
transform: translate(-4px);
}
.layout-nav-type-vertical .layout-vertical-nav .nav-header .app-title {
margin-inline-start: .9rem;
}
.layout-nav-type-vertical .layout-vertical-nav .vertical-nav-items-shadow {
position: absolute;
z-index: 1;
background: linear-gradient(rgb(var(--v-theme-background)) 5%, rgba(var(--v-theme-background), 75%) 45%, rgba(var(--v-theme-background), 20%) 80%, transparent);
block-size: 55px;
inline-size: 100%;
inset-block-start: 62px;
opacity: 0;
pointer-events: none;
transition: opacity .15s ease-in-out;
will-change: opacity;
}
.layout-nav-type-vertical .layout-vertical-nav.scrolled .vertical-nav-items-shadow {
opacity: 1;
}
.layout-nav-type-vertical .layout-vertical-nav .ps__rail-y {
z-index: 1;
}
.layout-nav-type-vertical .layout-vertical-nav .nav-section-title {
margin-block-end: .5rem;
}
.layout-nav-type-vertical .layout-vertical-nav .nav-section-title:not(:first-child) {
margin-block-start: 1.5rem;
}
.layout-nav-type-vertical .layout-vertical-nav .nav-section-title .placeholder-icon {
margin-inline: auto;
}
.layout-nav-type-vertical .layout-vertical-nav .nav-link,
.layout-nav-type-vertical .layout-vertical-nav .nav-group {
overflow: hidden;
}
.layout-nav-type-vertical .layout-vertical-nav .nav-link.disabled,
.layout-nav-type-vertical .layout-vertical-nav .nav-group.disabled {
opacity: var(--v-disabled-opacity);
pointer-events: none;
}
.layout-nav-type-vertical .layout-vertical-nav .nav-link > a {
position: relative;
}
.layout-nav-type-vertical .layout-vertical-nav .nav-link > a:before {
position: absolute;
border-radius: inherit;
background: currentcolor;
block-size: 100%;
content: "";
inline-size: 100%;
inset: 0;
opacity: 0;
pointer-events: none;
}
.layout-nav-type-vertical .layout-vertical-nav .nav-link > a:hover:before {
opacity: calc(var(--v-hover-opacity) * var(--v-theme-overlay-multiplier));
}
.layout-nav-type-vertical .layout-vertical-nav .nav-link > a:focus-visible:before {
opacity: calc(var(--v-focus-opacity) * var(--v-theme-overlay-multiplier));
}
@supports not selector(:focus-visible) {
.layout-nav-type-vertical .layout-vertical-nav .nav-link > a:focus:before {
opacity: calc(var(--v-focus-opacity) * var(--v-theme-overlay-multiplier));
}
}
.layout-nav-type-vertical .layout-vertical-nav .nav-group .nav-group-arrow {
flex-shrink: 0;
transform-origin: center;
transition: transform .15s ease-in-out;
will-change: transform;
}
.layout-nav-type-vertical .layout-vertical-nav .nav-group.open > .nav-group-label .nav-group-arrow {
transform: rotate(90deg);
}
.layout-nav-type-vertical .layout-vertical-nav .nav-group > :first-child {
position: relative;
}
.layout-nav-type-vertical .layout-vertical-nav .nav-group > :first-child:before {
position: absolute;
border-radius: inherit;
background: currentcolor;
block-size: 100%;
content: "";
inline-size: 100%;
inset: 0;
opacity: 0;
pointer-events: none;
}
.layout-nav-type-vertical .layout-vertical-nav .nav-group > :first-child:hover:before {
opacity: calc(var(--v-hover-opacity) * var(--v-theme-overlay-multiplier));
}
.layout-nav-type-vertical .layout-vertical-nav .nav-group > :first-child:focus-visible:before {
opacity: calc(var(--v-focus-opacity) * var(--v-theme-overlay-multiplier));
}
@supports not selector(:focus-visible) {
.layout-nav-type-vertical .layout-vertical-nav .nav-group > :first-child:focus:before {
opacity: calc(var(--v-focus-opacity) * var(--v-theme-overlay-multiplier));
}
}
.vertical-nav-section-title-enter-active,
.vertical-nav-section-title-leave-active {
transition: opacity .1s ease-in-out, transform .1s ease-in-out;
}
.vertical-nav-section-title-enter-from,
.vertical-nav-section-title-leave-to {
opacity: 0;
transform: translate(15px);
}
.transition-slide-x-enter-active,
.transition-slide-x-leave-active {
transition: opacity .1s ease-in-out, transform .12s ease-in-out;
}
.transition-slide-x-enter-from,
.transition-slide-x-leave-to {
opacity: 0;
transform: translate(-15px);
}
.vertical-nav-app-title-enter-active,
.vertical-nav-app-title-leave-active {
transition: opacity .1s ease-in-out, transform .12s ease-in-out;
}
.vertical-nav-app-title-enter-from,
.vertical-nav-app-title-leave-to {
opacity: 0;
transform: translate(-15px);
}
.layout-vertical-nav ol, .layout-vertical-nav ul,
.layout-horizontal-nav ol, .layout-horizontal-nav ul {
list-style: none;
}
.scrollable-content.v-navigation-drawer .v-navigation-drawer__content {
display: flex;
overflow: hidden;
flex-direction: column;
}
.layout-wrapper.layout-nav-type-vertical .layout-navbar .navbar-content-container {
transition: padding .2s ease, background-color .18s ease;
}
.layout-wrapper.layout-nav-type-vertical .layout-navbar .navbar-content-container {
border-radius: 0 0 10px 10px;
}
.layout-wrapper.layout-nav-type-vertical .layout-footer .footer-content-container {
border-radius: 10px 10px 0 0;
}
.layout-footer-sticky.layout-wrapper.layout-nav-type-vertical .layout-footer .footer-content-container {
background-color: rgb(var(--v-theme-surface));
padding-block: 0;
padding-inline: 1.2rem;
box-shadow: 0 4px 14px -4px var(--v-shadow-key-umbra-opacity), 0 4px 8px -4px var(--v-shadow-key-penumbra-opacity), 0 4px 8px -4px var(--v-shadow-key-ambient-opacity);
}
.layout-wrapper.layout-nav-type-vertical.layout-content-height-fixed .page-content-container > .v-layout:first-child {
overflow: hidden;
min-block-size: 100%;
}
.layout-wrapper.layout-nav-type-vertical.layout-content-height-fixed .page-content-container > .v-layout:first-child > .v-main .v-main__wrap > :first-child {
block-size: 100%;
overflow-y: auto;
}
.layout-wrapper.layout-nav-type-horizontal.layout-content-height-fixed > .layout-page-content {
display: flex;
}
.layout-vertical-nav {
position: fixed;
z-index: 1004;
display: flex;
flex-direction: column;
block-size: 100%;
inline-size: 260px;
inset-block-start: 0;
inset-inline-start: 0;
transition: transform .25s ease-in-out, inline-size .25s ease-in-out, box-shadow .25s ease-in-out;
will-change: transform, inline-size;
}
.layout-vertical-nav .nav-header {
display: flex;
align-items: center;
}
.layout-vertical-nav .app-title-wrapper {
margin-inline-end: auto;
}
.layout-vertical-nav .nav-items {
block-size: 100%;
}
.layout-vertical-nav .nav-item-title {
overflow: hidden;
margin-inline-end: auto;
text-overflow: ellipsis;
white-space: nowrap;
}
.layout-vertical-nav-collapsed .layout-vertical-nav:not(.hovered) {
inline-size: 68px
}
.layout-vertical-nav.overlay-nav:not(.visible) {
transform: translate(-260px);
}
.layout-content-width-boxed.layout-wrapper.layout-nav-type-vertical .layout-navbar {
inline-size: 100%;
margin-inline: auto;
max-inline-size: 1440px;
}
.layout-wrapper.layout-nav-type-vertical .layout-navbar {
padding-inline: 1.5rem;
}
.layout-wrapper.layout-nav-type-vertical.layout-navbar-hidden .layout-navbar {
display: none;
}
.layout-wrapper.layout-nav-type-vertical.layout-navbar-sticky .layout-navbar {
position: sticky;
inset-block-start: 0;
}
.layout-wrapper.layout-nav-type-vertical {
block-size: 100%;
}
.layout-wrapper.layout-nav-type-vertical .layout-content-wrapper {
display: flex;
flex-direction: column;
flex-grow: 1;
min-block-size: calc(var(--vh, 1vh) * 100);
transition: padding-inline-start .2s ease-in-out;
will-change: padding-inline-start;
}
.layout-wrapper.layout-nav-type-vertical .layout-navbar {
z-index: 11;
}
.layout-wrapper.layout-nav-type-vertical .layout-navbar .navbar-content-container {
block-size: 64px;
}
.layout-wrapper.layout-nav-type-vertical .layout-overlay {
position: fixed;
z-index: 1003;
background-color: #0009;
cursor: pointer;
inset: 0;
opacity: 0;
pointer-events: none;
transition: opacity .25s ease-in-out;
will-change: transform;
}
.layout-wrapper.layout-nav-type-vertical .layout-overlay.visible {
opacity: 1;
pointer-events: auto;
}
.layout-wrapper.layout-nav-type-vertical:not(.layout-overlay-nav) .layout-content-wrapper {
padding-inline-start: 260px;
}
.layout-wrapper.layout-nav-type-vertical.layout-vertical-nav-collapsed .layout-content-wrapper {
padding-inline-start: 68px;
}
.layout-wrapper.layout-nav-type-vertical.layout-content-height-fixed .layout-content-wrapper {
max-block-size: calc(var(--vh) * 100);
}
.layout-wrapper.layout-nav-type-vertical.layout-content-height-fixed .layout-page-content {
display: flex;
overflow: hidden;
}
.layout-wrapper.layout-nav-type-vertical.layout-content-height-fixed .layout-page-content .page-content-container {
inline-size: 100%;
}
.layout-wrapper.layout-nav-type-vertical.layout-content-height-fixed .layout-page-content .page-content-container > :first-child {
max-block-size: 100%;
overflow-y: auto;
}
.layout-vertical-nav .nav-link a {
display: flex;
align-items: center;
cursor: pointer;
}
.layout-page-content {
flex-grow: 1;
padding-block: 1.5rem;
}
.layout-page-content {
padding-inline: 1.5rem;
}
+211
View File
@@ -0,0 +1,211 @@
/** Vuetify class overrides **/
/** reference: https://github.com/themeselection/materio-vuetify-vuejs-admin-template-free **/
.v-application__wrap {
min-height: calc(var(--vh, 1vh) * 100);
}
h1,
h2,
h3,
h4,
h5,
h6,
.text-h1,
.text-h2,
.text-h3,
.text-h4,
.text-h5,
.text-h6,
.text-button,
.text-overline,
.v-card-title {
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
}
.v-application,
.text-body-1,
.text-body-2,
.text-subtitle-1,
.text-subtitle-2 {
color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity));
}
.v-row .v-col .v-input__details,
.v-row [class^="v-col-*"] .v-input__details {
margin-block-end: 0;
}
.v-btn--density-compact.v-btn--size-default .v-btn__content > svg {
block-size: 22px;
font-size: 22px;
inline-size: 22px;
}
.v-card-text + .v-card-text {
padding-block-start: 0 !important;
}
.v-checkbox.v-input,
.v-switch.v-input {
--v-input-control-height: auto;
flex: unset;
}
.v-selection-control--density-comfortable.v-checkbox-btn .v-selection-control__wrapper,
.v-selection-control--density-comfortable.v-radio .v-selection-control__wrapper,
.v-selection-control--density-comfortable.v-radio-btn .v-selection-control__wrapper {
margin-inline-start: -.5625rem;
}
.v-selection-control--density-compact.v-radio .v-selection-control__wrapper,
.v-selection-control--density-compact.v-radio-btn .v-selection-control__wrapper,
.v-selection-control--density-compact.v-checkbox-btn .v-selection-control__wrapper {
margin-inline-start: -.3125rem;
}
.v-selection-control--density-default.v-checkbox-btn .v-selection-control__wrapper,
.v-selection-control--density-default.v-radio .v-selection-control__wrapper,
.v-selection-control--density-default.v-radio-btn .v-selection-control__wrapper {
margin-inline-start: -.6875rem;
}
.v-radio-group .v-selection-control-group .v-radio:not(:last-child) {
margin-inline-end: .9rem;
}
.disable-tab-transition {
overflow: unset !important;
}
.disable-tab-transition .v-window__container {
block-size: auto !important;
}
.disable-tab-transition .v-window-item:not(.v-window-item--active) {
display: none !important;
}
.disable-tab-transition .v-window__container .v-window-item {
transform: none !important;
}
.v-list .v-list-item__prepend > .v-icon,
.v-list .v-list-item__append > .v-icon {
opacity: var(--v-high-emphasis-opacity);
}
.card-list {
--v-card-list-gap: 20px;
}
.card-list.v-list {
padding-block: 0;
}
.card-list .v-list-item {
min-block-size: unset;
min-block-size: auto !important;
padding-block: 0 !important;
padding-inline: 0 !important;
}
.card-list .v-list-item > .v-ripple__container {
opacity: 0;
}
.card-list .v-list-item:not(:last-child) {
padding-block-end: var(--v-card-list-gap) !important;
}
.card-list .v-list-item: hover > .v-list-item__overlay,
.card-list .v-list-item:focus > .v-list-item__overlay,
.card-list .v-list-item:active > .v-list-item__overlay,
.card-list .v-list-item.active > .v-list-item__overlay {
opacity: 0 !important;
}
.v-divider {
color: rgb(var(--v-border-color));
}
.v-data-table .v-checkbox-btn .v-selection-control__wrapper {
margin-inline-start: 0 !important;
}
.v-data-table .v-selection-control {
display: flex !important;
}
.v-data-table .v-pagination {
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
}
.v-data-table-footer {
margin-block-start: 1rem;
}
.v-field:hover .v-field__outline {
--v-field-border-opacity: var(--v-medium-emphasis-opacity);
}
.v-label {
opacity: 1 !important;
}
.v-label:not(.v-field-label--floating) {
color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity));
}
.v-messages {
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
opacity: 1;
}
.v-alert__close .v-btn--icon .v-icon {
--v-icon-size-multiplier: 1.5;
}
.v-badge__badge {
display: flex;
align-items: center;
}
.v-btn:focus-visible:after {
opacity: 0 !important;
}
.v-input:not(.v-select--chips) .v-select__selection .v-chip {
margin-block: 2px var(--select-chips-margin-bottom);
}
.v-card-subtitle,
.v-list-item-subtitle {
color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity));
}
.v-field__input input::placeholder,
input.v-field__input::placeholder,
textarea.v-field__input::placeholder {
color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity)) !important;
opacity: 1 !important;
}
.v-card-item {
padding: 1.25rem;
}
.v-card-text {
letter-spacing: .0094rem;
padding: 1.25rem;
}
.v-card--variant-elevated {
box-shadow: 0 4px 5px -2px var(--v-shadow-key-umbra-opacity), 0 2px 10px 1px var(--v-shadow-key-penumbra-opacity), 0 2px 16px 1px var(--v-shadow-key-ambient-opacity);
}
.v-card--variant-elevated,
.v-card--variant-flat {
background: rgb(var(--v-theme-surface));
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
}
+13
View File
@@ -0,0 +1,13 @@
<template>
<v-row class="match-height">
about
</v-row>
</template>
<script>
export default {
created() {
}
}
</script>
+13
View File
@@ -0,0 +1,13 @@
<template>
<v-row class="match-height">
accounts
</v-row>
</template>
<script>
export default {
created() {
}
}
</script>
+13
View File
@@ -0,0 +1,13 @@
<template>
<v-row class="match-height">
exchange rates
</v-row>
</template>
<script>
export default {
created() {
}
}
</script>
+13
View File
@@ -0,0 +1,13 @@
<template>
<v-row class="match-height">
overview
</v-row>
</template>
<script>
export default {
created() {
}
}
</script>
+344
View File
@@ -0,0 +1,344 @@
<template>
<div class="auth-wrapper d-flex align-center justify-center pa-4">
<v-card class="auth-card pa-4 pt-7" max-width="448">
<v-card-item class="justify-center">
<v-card-title class="d-grid font-weight-semibold text-2xl">
<v-img alt="logo" class="login-page-logo" src="/img/ezbookkeeping-192.png" :width="96" />
<p class="mt-4 font-weight-bold">{{ $t('global.app.title') }}</p>
</v-card-title>
</v-card-item>
<v-card-text>
<v-form>
<v-row>
<v-col cols="12">
<v-text-field
type="text"
autocomplete="username"
clearable
:disabled="show2faInput"
:label="$t('Username')"
:placeholder="$t('Your username or email')"
v-model="username"
@input="tempToken = ''"
@keyup.enter="$refs.passwordInput.focus()"
/>
</v-col>
<v-col cols="12">
<v-text-field
autocomplete="current-password"
clearable
ref="passwordInput"
:type="isPasswordVisible ? 'text' : 'password'"
:disabled="show2faInput"
:label="$t('Password')"
:placeholder="$t('Your password')"
:append-inner-icon="isPasswordVisible ? icons.eyeSlash : icons.eye"
v-model="password"
@input="tempToken = ''"
@click:append-inner="isPasswordVisible = !isPasswordVisible"
@keyup.enter="login"
/>
</v-col>
<v-col cols="12" v-show="show2faInput">
<v-text-field
type="number"
autocomplete="one-time-code"
clearable
ref="passcodeInput"
:label="$t('Passcode')"
:placeholder="$t('Passcode')"
:append-inner-icon="icons.backupCode"
v-model="passcode"
@click:append-inner="twoFAVerifyType = 'backupcode'"
@keyup.enter="verify"
v-if="twoFAVerifyType === 'passcode'"
/>
<v-text-field
type="text"
clearable
:label="$t('Backup Code')"
:placeholder="$t('Backup Code')"
:append-inner-icon="icons.passcode"
v-model="backupCode"
@click:append-inner="twoFAVerifyType = 'passcode'"
@keyup.enter="verify"
v-if="twoFAVerifyType === 'backupcode'"
/>
</v-col>
<v-col cols="12">
<v-btn block :class="{ 'disabled': inputIsEmpty || logining }"
@click="login" v-if="!show2faInput">
{{ $t('Log In') }}
</v-btn>
<v-btn block :class="{ 'disabled': twoFAInputIsEmpty || verifying }"
@click="verify" v-else-if="show2faInput">
{{ $t('Continue') }}
</v-btn>
</v-col>
<v-col cols="12" class="text-center text-base">
<span>{{ $t('Don\'t have an account?') }}</span>&nbsp;
<router-link class="text-primary ms-2" to="/signup">
{{ $t('Create an account') }}
</router-link>
</v-col>
<v-col cols="12" class="text-center">
<v-menu location="bottom">
<template #activator="{ props }">
<v-btn variant="text" v-bind="props">{{ currentLanguageName }}</v-btn>
</template>
<v-list>
<v-list-item v-for="(lang, locale) in allLanguages" :key="locale">
<v-list-item-title
class="cursor-pointer"
@click="changeLanguage(locale)">
{{ lang.displayName }}
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-col>
<v-col cols="12" class="d-flex align-center">
<VDivider />
</v-col>
<v-col cols="12" class="text-center text-sm">
<span>Powered by </span>
<a href="https://github.com/mayswind/ezbookkeeping" target="_blank">ezBookkeeping</a>&nbsp;<span>{{ version }}</span>
</v-col>
</v-row>
</v-form>
</v-card-text>
</v-card>
<v-snackbar v-model="showSnackbar">
{{ snackbarMessage }}
<template #actions>
<v-btn color="primary" variant="text" @click="showSnackbar = false">{{ $t('Close') }}</v-btn>
</template>
</v-snackbar>
<v-overlay class="justify-center align-center" :persistent="true" v-model="logining">
<v-progress-circular indeterminate></v-progress-circular>
</v-overlay>
<v-overlay class="justify-center align-center" :persistent="true" v-model="verifying">
<v-progress-circular indeterminate></v-progress-circular>
</v-overlay>
</div>
</template>
<script>
import { mapStores } from 'pinia';
import { useRootStore } from '@/stores/index.js';
import { useSettingsStore } from '@/stores/setting.js';
import { useExchangeRatesStore } from '@/stores/exchangeRates.js';
import {
mdiEyeOutline,
mdiEyeOffOutline,
mdiOnepassword,
mdiHelpCircleOutline
} from '@mdi/js';
export default {
data() {
return {
username: '',
password: '',
passcode: '',
backupCode: '',
tempToken: '',
isPasswordVisible: false,
logining: false,
verifying: false,
show2faInput: false,
twoFAVerifyType: 'passcode',
showSnackbar: false,
snackbarMessage: '',
icons: {
eye: mdiEyeOutline,
eyeSlash: mdiEyeOffOutline,
passcode: mdiOnepassword,
backupCode: mdiHelpCircleOutline
}
};
},
computed: {
...mapStores(useRootStore, useSettingsStore, useExchangeRatesStore),
version() {
return 'v' + this.$version;
},
allLanguages() {
return this.$locale.getAllLanguageInfos();
},
isUserRegistrationEnabled() {
return this.$settings.isUserRegistrationEnabled();
},
inputIsEmpty() {
return !this.username || !this.password;
},
twoFAInputIsEmpty() {
if (this.twoFAVerifyType === 'backupcode') {
return !this.backupCode;
} else {
return !this.passcode;
}
},
currentLanguageName() {
const currentLocale = this.$i18n.locale;
let lang = this.$locale.getLanguageInfo(currentLocale);
if (!lang) {
lang = this.$locale.getLanguageInfo(this.$locale.getDefaultLanguage());
}
return lang.displayName;
}
},
methods: {
login() {
const self = this;
if (!self.username) {
self.showSnackbarMessage(self.$t('Username cannot be empty'));
return;
}
if (!self.password) {
self.showSnackbarMessage(self.$t('Password cannot be empty'));
return;
}
if (self.tempToken) {
self.show2faInput = true;
return;
}
if (self.logining) {
return;
}
self.isPasswordVisible = false;
self.logining = true;
self.rootStore.authorize({
loginName: self.username,
password: self.password
}).then(authResponse => {
self.logining = false;
if (authResponse.need2FA) {
self.tempToken = authResponse.token;
self.show2faInput = true;
this.$nextTick(() => {
if (self.$refs.passcodeInput) {
self.$refs.passcodeInput.focus();
self.$refs.passcodeInput.select();
}
});
return;
}
if (authResponse.user && authResponse.user.language) {
const localeDefaultSettings = self.$locale.setLanguage(authResponse.user.language);
self.settingsStore.updateLocalizedDefaultSettings(localeDefaultSettings);
}
if (self.$settings.isAutoUpdateExchangeRatesData()) {
self.exchangeRatesStore.getLatestExchangeRates({silent: true, force: false});
}
this.$router.replace('/');
}).catch(error => {
self.logining = false;
if (!error.processed) {
self.showSnackbarMessage(self.$tError(error.message || error));
}
});
},
verify() {
const self = this;
if (self.twoFAInputIsEmpty || self.verifying) {
return;
}
if (this.twoFAVerifyType === 'passcode' && !this.passcode) {
self.showSnackbarMessage(self.$t('Passcode cannot be empty'));
return;
} else if (this.twoFAVerifyType === 'backupcode' && !this.backupCode) {
self.showSnackbarMessage(self.$t('Backup code cannot be empty'));
return;
}
self.verifying = true;
self.rootStore.authorize2FA({
token: self.tempToken,
passcode: self.twoFAVerifyType === 'passcode' ? self.passcode : null,
recoveryCode: self.twoFAVerifyType === 'backupcode' ? self.backupCode : null
}).then(authResponse => {
self.verifying = false;
if (authResponse.user && authResponse.user.language) {
const localeDefaultSettings = self.$locale.setLanguage(authResponse.user.language);
self.settingsStore.updateLocalizedDefaultSettings(localeDefaultSettings);
}
if (self.$settings.isAutoUpdateExchangeRatesData()) {
self.exchangeRatesStore.getLatestExchangeRates({ silent: true, force: false });
}
this.$router.replace('/');
}).catch(error => {
self.verifying = false;
if (!error.processed) {
self.showSnackbarMessage(self.$tError(error.message || error));
}
});
},
switch2FAVerifyType() {
if (this.twoFAVerifyType === 'passcode') {
this.twoFAVerifyType = 'backupcode';
} else {
this.twoFAVerifyType = 'passcode';
}
},
changeLanguage(locale) {
const localeDefaultSettings = this.$locale.setLanguage(locale);
this.settingsStore.updateLocalizedDefaultSettings(localeDefaultSettings);
},
showSnackbarMessage(message) {
this.showSnackbar = true;
this.snackbarMessage = message;
}
}
}
</script>
<style>
.auth-wrapper {
min-block-size: calc(var(--vh, 1vh) * 100);
}
.auth-card {
z-index: 1 !important
}
.login-page-logo {
margin: auto;
}
</style>
+268
View File
@@ -0,0 +1,268 @@
<template>
<div class="layout-wrapper layout-nav-type-vertical layout-navbar-static layout-footer-static layout-content-width-fluid"
:class="{ 'layout-overlay-nav': mdAndDown }">
<div class="layout-vertical-nav" :class="{'visible': showVerticalOverlayMenu, 'scrolled': isVerticalNavScrolled, 'overlay-nav': mdAndDown}">
<div class="nav-header">
<router-link to="/" class="app-logo d-flex align-center gap-x-3 app-title-wrapper">
<div class="d-flex">
<v-img alt="logo" class="main-logo" src="/img/ezbookkeeping-192.png" />
</div>
<h1 class="font-weight-medium text-xl">{{ $t('global.app.title') }}</h1>
</router-link>
</div>
<perfect-scrollbar
tag="ul" class="nav-items"
:options="{ wheelPropagation: false }"
@ps-scroll-y="handleNavScroll"
>
<li class="nav-link">
<RouterLink to="/">
<v-icon class="nav-item-icon" :icon="icons.overview"/>
<span class="nav-item-title">{{ $t('Overview') }}</span>
</RouterLink>
</li>
<li class="nav-section-title">
<div class="title-wrapper">
<span class="title-text">{{ $t('Transaction Data') }}</span>
</div>
</li>
<li class="nav-link">
<RouterLink to="/transactions">
<v-icon class="nav-item-icon" :icon="icons.transactions"/>
<span class="nav-item-title">{{ $t('Transaction List') }}</span>
</RouterLink>
</li>
<li class="nav-link">
<RouterLink to="/statistics/transaction">
<v-icon class="nav-item-icon" :icon="icons.statistics"/>
<span class="nav-item-title">{{ $t('Statistics Data') }}</span>
</RouterLink>
</li>
<li class="nav-section-title">
<div class="title-wrapper">
<span class="title-text">{{ $t('Data Management') }}</span>
</div>
</li>
<li class="nav-link">
<RouterLink to="/accounts">
<v-icon class="nav-item-icon" :icon="icons.accounts"/>
<span class="nav-item-title">{{ $t('Account List') }}</span>
</RouterLink>
</li>
<li class="nav-link">
<RouterLink to="/categories">
<v-icon class="nav-item-icon" :icon="icons.categories"/>
<span class="nav-item-title">{{ $t('Transaction Categories') }}</span>
</RouterLink>
</li>
<li class="nav-link">
<RouterLink to="/tags">
<v-icon class="nav-item-icon" :icon="icons.tags"/>
<span class="nav-item-title">{{ $t('Transaction Tags') }}</span>
</RouterLink>
</li>
<li class="nav-section-title">
<div class="title-wrapper">
<span class="title-text">{{ $t('Other') }}</span>
</div>
</li>
<li class="nav-link">
<RouterLink to="/exchange_rates">
<v-icon class="nav-item-icon" :icon="icons.exchangeRates"/>
<span class="nav-item-title">{{ $t('Exchange Rates Data') }}</span>
</RouterLink>
</li>
<li class="nav-link">
<RouterLink to="/about">
<v-icon class="nav-item-icon" :icon="icons.about"/>
<span class="nav-item-title">{{ $t('About') }}</span>
</RouterLink>
</li>
</perfect-scrollbar>
</div>
<div class="layout-content-wrapper">
<div class="layout-navbar navbar-blur">
<div class="navbar-content-container">
<div class="d-flex h-100 align-center">
<v-btn class="ms-n3 d-lg-none" color="default" variant="text"
:icon="true" @click="showVerticalOverlayMenu = true">
<v-icon :icon="icons.menu" size="24" />
</v-btn>
<div class="app-logo d-flex align-center gap-x-3 app-title-wrapper" v-if="mdAndDown">
<div class="d-flex">
<v-img alt="logo" class="main-logo" src="/img/ezbookkeeping-192.png" />
</div>
<h1 class="font-weight-medium text-xl">{{ $t('global.app.title') }}</h1>
</div>
<v-spacer />
<v-avatar class="cursor-pointer" color="primary" variant="tonal">
<v-icon :icon="icons.user"/>
<v-menu activator="parent" width="230" location="bottom end" offset="14px">
<v-list>
<v-list-item>
<template #prepend>
<v-list-item-action start>
<v-avatar color="primary" variant="tonal">
<v-icon :icon="icons.user"/>
</v-avatar>
</v-list-item-action>
</template>
<v-list-item-title class="font-weight-semibold">
{{ currentNickName }}
</v-list-item-title>
</v-list-item>
<v-divider class="my-2"/>
<v-list-item to="/user/settings">
<template #prepend>
<v-icon class="me-2" :icon="icons.profile" size="22"/>
</template>
<v-list-item-title>{{ $t('User Profile') }}</v-list-item-title>
</v-list-item>
<v-list-item to="/app/settings">
<template #prepend>
<v-icon class="me-2" :icon="icons.settings" size="22"/>
</template>
<v-list-item-title>{{ $t('Application Settings') }}</v-list-item-title>
</v-list-item>
<v-divider class="my-2"/>
<v-list-item :class="{ 'disabled': logouting }" @click="logout">
<template #prepend>
<v-icon class="me-2" :icon="icons.logout" size="22"/>
</template>
<v-list-item-title>{{ $t('Log Out') }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-avatar>
</div>
</div>
</div>
<div class="layout-page-content">
<div class="page-content-container">
<router-view/>
</div>
</div>
</div>
<div class="layout-overlay" :class="{ 'visible': showVerticalOverlayMenu }" @click="showVerticalOverlayMenu = false"></div>
<v-overlay class="justify-center align-center" :persistent="true" v-model="showLoading">
<v-progress-circular indeterminate></v-progress-circular>
</v-overlay>
<v-snackbar :timeout="2000" v-model="showSnackbar">
{{ snackbarMessage }}
<template #actions>
<v-btn color="red" variant="text" @click="showSnackbar = false">{{ $t('Close') }}</v-btn>
</template>
</v-snackbar>
</div>
</template>
<script>
import { useDisplay } from 'vuetify';
import { mapStores } from 'pinia';
import { useRootStore } from '@/stores/index.js';
import { useSettingsStore } from '@/stores/setting.js';
import { useUserStore } from '@/stores/user.js';
import {
mdiMenu,
mdiHomeOutline,
mdiListBoxOutline,
mdiCreditCardOutline,
mdiViewDashboardOutline,
mdiTagOutline,
mdiChartPieOutline,
mdiSwapHorizontal,
mdiCogOutline,
mdiInformationOutline,
mdiAccount,
mdiAccountOutline,
mdiLogout
} from '@mdi/js';
export default {
data() {
return {
logouting: false,
isVerticalNavScrolled: false,
showVerticalOverlayMenu: false,
showLoading: false,
showSnackbar: false,
snackbarMessage: '',
icons: {
menu: mdiMenu,
overview: mdiHomeOutline,
transactions: mdiListBoxOutline,
accounts: mdiCreditCardOutline,
categories: mdiViewDashboardOutline,
tags: mdiTagOutline,
statistics: mdiChartPieOutline,
exchangeRates: mdiSwapHorizontal,
settings: mdiCogOutline,
about: mdiInformationOutline,
user: mdiAccount,
profile: mdiAccountOutline,
logout: mdiLogout
}
}
},
computed: {
...mapStores(useRootStore, useSettingsStore, useUserStore),
mdAndDown() {
const { mdAndDown } = useDisplay();
return mdAndDown.value;
},
currentNickName() {
return this.userStore.currentUserNickname || this.$t('User');
}
},
created() {
},
methods: {
handleNavScroll(e) {
this.isVerticalNavScrolled = e.target.scrollTop > 0;
},
logout() {
const self = this;
self.logouting = true;
self.showLoading = true;
self.rootStore.logout().then(() => {
self.logouting = false;
self.showLoading = false;
self.$settings.clearSettings();
const localeDefaultSettings = self.$locale.initLocale(self.userStore.currentUserLanguage);
self.settingsStore.updateLocalizedDefaultSettings(localeDefaultSettings);
this.$router.replace('/login');
}).catch(error => {
self.logouting = false;
self.showLoading = false;
if (!error.processed) {
self.showSnackbarMessage(self.$tError(error.message || error));
}
});
},
showSnackbarMessage(message) {
this.showSnackbar = true;
this.snackbarMessage = message;
}
}
}
</script>
<style>
.main-logo {
width: 1.8em;
height: 1.8em;
}
</style>
+13
View File
@@ -0,0 +1,13 @@
<template>
<div class="auth-wrapper d-flex align-center justify-center pa-4">
</div>
</template>
<script>
export default {
created() {
}
}
</script>
@@ -0,0 +1,13 @@
<template>
<v-row class="match-height">
categories
</v-row>
</template>
<script>
export default {
created() {
}
}
</script>
+13
View File
@@ -0,0 +1,13 @@
<template>
<v-row class="match-height">
tags
</v-row>
</template>
<script>
export default {
created() {
}
}
</script>
+13
View File
@@ -0,0 +1,13 @@
<template>
<v-row class="match-height">
transactions
</v-row>
</template>
<script>
export default {
created() {
}
}
</script>
+13
View File
@@ -0,0 +1,13 @@
<template>
<div class="auth-wrapper d-flex align-center justify-center pa-4">
</div>
</template>
<script>
export default {
created() {
}
}
</script>
+13
View File
@@ -0,0 +1,13 @@
<template>
<v-row class="match-height">
app settings
</v-row>
</template>
<script>
export default {
created() {
}
}
</script>
@@ -0,0 +1,13 @@
<template>
<v-row class="match-height">
statistics
</v-row>
</template>
<script>
export default {
created() {
}
}
</script>
@@ -0,0 +1,13 @@
<template>
<v-row class="match-height">
user settings
</v-row>
</template>
<script>
export default {
created() {
}
}
</script>
+29
View File
@@ -93,12 +93,24 @@
"url": "https://github.com/vuejs/pinia",
"licenseUrl": "https://github.com/vuejs/pinia/blob/pinia%402.1.4/LICENSE"
},
{
"name": "vue-router",
"copyright": "Copyright (c) 2019-present Eduardo San Martin Morote",
"url": "https://github.com/vuejs/router",
"licenseUrl": "https://github.com/vuejs/router/blob/v4.2.2/LICENSE"
},
{
"name": "vue-i18n",
"copyright": "Copyright (c) 2016 kazuya kawaguchi",
"url": "https://github.com/intlify/vue-i18n-next",
"licenseUrl": "https://github.com/intlify/vue-i18n-next/blob/v9.2.2/LICENSE"
},
{
"name": "vuetify",
"copyright": "Copyright (c) 2016-2023 John Jeremy Leider",
"url": "https://vuetifyjs.com",
"licenseUrl": "https://github.com/vuetifyjs/vuetify/blob/v3.3.5/LICENSE.md"
},
{
"name": "register-service-worker",
"copyright": "Copyright (c) 2013-present, Yuxi (Evan) You",
@@ -141,6 +153,12 @@
"url": "https://github.com/nolimits4web/skeleton-elements",
"licenseUrl": "https://github.com/nolimits4web/skeleton-elements/blob/v4.0.1/LICENSE"
},
{
"name": "vue3-perfect-scrollbar",
"copyright": "Copyright (c) 2018 Adam",
"url": "https://github.com/mercs600/vue3-perfect-scrollbar",
"licenseUrl": "https://github.com/mercs600/vue3-perfect-scrollbar/blob/1.6.1/LICENSE"
},
{
"name": "@vuepic/vue-datepicker",
"copyright": "Copyright (c) 2021-present Vuepic",
@@ -201,9 +219,20 @@
"url": "https://github.com/faisalman/ua-parser-js",
"licenseUrl": "https://github.com/faisalman/ua-parser-js/blob/1.0.35/license.md"
},
{
"name": "Materio - Vuetify VueJS 3 Free Admin Template",
"copyright": "Copyright (c) 2022 ThemeSelection",
"url": "https://github.com/themeselection/materio-vuetify-vuejs-admin-template-free",
"licenseUrl": "https://github.com/themeselection/materio-vuetify-vuejs-admin-template-free/blob/v2.1.0/LICENSE"
},
{
"name": "Icons8 Line Awesome",
"url": "https://icons8.com/line-awesome",
"licenseUrl": "https://github.com/icons8/line-awesome/blob/master/LICENSE.md"
},
{
"name": "Material Design Icons for JS/TypeScript",
"url": "https://materialdesignicons.com",
"licenseUrl": "https://github.com/Templarian/MaterialDesign-JS/blob/v7.2.96/LICENSE"
}
]
+11 -1
View File
@@ -110,10 +110,20 @@ export default defineConfig(async () => {
manualChunks: function (id) {
if (/[\\/]node_modules[\\/]leaflet[\\/]/i.test(id)) {
return 'leaflet';
} else if (/[\\/]node_modules[\\/](moment|moment-timezone)[\\/]/i.test(id)) {
return 'moment';
} else if (/[\\/]node_modules[\\/](dom7|framework7.*|skeleton-elements|swiper)[\\/]/i.test(id)) {
return 'vendor-mobile';
} else if (/[\\/]node_modules[\\/](vuetify|vue-router|vue3-perfect-scrollbar|@mdi.*)[\\/]/i.test(id)) {
return 'vendor-desktop';
} else if (/[\\/]node_modules[\\/]/i.test(id)) {
return 'vendor';
return 'vendor-common';
} else if (/[\\/]src[\\/]locales[\\/]/i.test(id)) {
return 'locales';
} else if (/[\\/]src[\\/](consts|stores)[\\/]/i.test(id)) {
return 'common';
} else if (/[\\/]src[\\/]lib[\\/](map[\\/]|[a-zA-Z0-9-_]+\.js)/i.test(id)) {
return 'common';
}
}
},