add transaction tag list page

This commit is contained in:
MaysWind
2023-07-09 00:37:19 +08:00
parent ac730d6086
commit 89bd041d29
5 changed files with 434 additions and 3 deletions
+2
View File
@@ -27,6 +27,7 @@ import { VProgressCircular } from 'vuetify/components/VProgressCircular';
import { VProgressLinear } from 'vuetify/components/VProgressLinear'; import { VProgressLinear } from 'vuetify/components/VProgressLinear';
import { VSelect } from 'vuetify/components/VSelect'; import { VSelect } from 'vuetify/components/VSelect';
import { VSheet } from 'vuetify/components/VSheet'; import { VSheet } from 'vuetify/components/VSheet';
import { VSkeletonLoader } from 'vuetify/labs/VSkeletonLoader';
import { VSlideGroup, VSlideGroupItem } from 'vuetify/components/VSlideGroup'; import { VSlideGroup, VSlideGroupItem } from 'vuetify/components/VSlideGroup';
import { VSnackbar } from 'vuetify/components/VSnackbar'; import { VSnackbar } from 'vuetify/components/VSnackbar';
import { VSwitch } from 'vuetify/components/VSwitch'; import { VSwitch } from 'vuetify/components/VSwitch';
@@ -124,6 +125,7 @@ const vuetify = createVuetify({
VProgressLinear, VProgressLinear,
VSelect, VSelect,
VSheet, VSheet,
VSkeletonLoader,
VSlideGroup, VSlideGroup,
VSlideGroupItem, VSlideGroupItem,
VSnackbar, VSnackbar,
+3
View File
@@ -720,6 +720,8 @@ export default {
'Disabled': 'Disabled', 'Disabled': 'Disabled',
'Copy': 'Copy', 'Copy': 'Copy',
'Visible': 'Visible', 'Visible': 'Visible',
'Show': 'Show',
'Hide': 'Hide',
'Version': 'Version', 'Version': 'Version',
'Edit': 'Edit', 'Edit': 'Edit',
'Remove': 'Remove', 'Remove': 'Remove',
@@ -728,6 +730,7 @@ export default {
'Sort': 'Sort', 'Sort': 'Sort',
'Date': 'Date', 'Date': 'Date',
'Type': 'Type', 'Type': 'Type',
'More': 'More',
'All': 'All', 'All': 'All',
'Today': 'Today', 'Today': 'Today',
'Yesterday': 'Yesterday', 'Yesterday': 'Yesterday',
+3
View File
@@ -720,6 +720,8 @@ export default {
'Disabled': '禁用', 'Disabled': '禁用',
'Copy': '复制', 'Copy': '复制',
'Visible': '可见', 'Visible': '可见',
'Show': '显示',
'Hide': '隐藏',
'Version': '版本', 'Version': '版本',
'Edit': '编辑', 'Edit': '编辑',
'Remove': '移除', 'Remove': '移除',
@@ -728,6 +730,7 @@ export default {
'Sort': '排序', 'Sort': '排序',
'Date': '日期', 'Date': '日期',
'Type': '类型', 'Type': '类型',
'More': '更多',
'All': '全部', 'All': '全部',
'Today': '今天', 'Today': '今天',
'Yesterday': '昨天', 'Yesterday': '昨天',
+6
View File
@@ -77,3 +77,9 @@ input[type=number] {
background: rgb(var(--v-table-header-background)) !important; background: rgb(var(--v-table-header-background)) !important;
} }
} }
.right-bottom-icon .v-badge__badge {
padding: 0;
min-width: 16px;
height: 1rem;
}
+420 -3
View File
@@ -1,13 +1,430 @@
<template> <template>
<v-row class="match-height"> <v-row class="match-height">
tags <v-col cols="12">
<v-card :class="{ 'disabled': loading }">
<template #title>
<div class="d-flex align-center">
<span>{{ $t('Transaction Tags') }}</span>
<v-btn density="compact" color="default" variant="text"
class="ml-2" :icon="true" :disabled="updating"
v-if="!loading" @click="reload">
<v-icon :icon="icons.refresh" size="24" />
<v-tooltip activator="parent">{{ $t('Refresh') }}</v-tooltip>
</v-btn>
<v-progress-circular indeterminate size="24" class="ml-2" v-if="loading"></v-progress-circular>
<v-btn density="compact" color="default" variant="text"
class="ml-2" :icon="true" :disabled="loading || updating || hasEditingTag"
@click="add">
<v-icon :icon="icons.add" size="24" />
<v-tooltip activator="parent" v-if="!loading && !updating && !hasEditingTag">{{ $t('Add') }}</v-tooltip>
</v-btn>
<v-spacer/>
<v-btn density="comfortable" color="default" variant="text" class="ml-2"
:disabled="loading || updating || hasEditingTag" :icon="true">
<v-icon :icon="icons.more" />
<v-menu activator="parent">
<v-list>
<v-list-item :prepend-icon="icons.show"
:title="$t('Show Hidden Transaction Tag')"
v-if="!showHidden" @click="showHidden = true"></v-list-item>
<v-list-item :prepend-icon="icons.hide"
:title="$t('Hide Hidden Transaction Tag')"
v-if="showHidden" @click="showHidden = false"></v-list-item>
</v-list>
</v-menu>
</v-btn>
</div>
</template>
<v-table class="transaction-tags-table">
<thead>
<tr>
<th class="text-uppercase" style="width: 50%">{{ $t('Tag Title') }}</th>
<th class="text-uppercase text-center">{{ $t('Operation') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="idx in loading ? [1, 2, 3] : []">
<td class="px-0" colspan="2">
<v-skeleton-loader type="text" :loading="true"></v-skeleton-loader>
</td>
</tr>
<tr v-if="!loading && noAvailableTag && !newTag">
<td colspan="2">{{ $t('No available tag') }}</td>
</tr>
<tr :key="tag.id"
v-show="!loading && (showHidden || !tag.hidden)"
v-for="tag in tags">
<td>
<div class="d-flex align-center" v-if="editingTag.id !== tag.id">
<v-badge class="right-bottom-icon" color="secondary"
location="bottom right" offset-x="8" :icon="icons.hide"
v-if="tag.hidden">
<v-icon size="24" start :icon="icons.tag"/>
</v-badge>
<v-icon size="24" start :icon="icons.tag" v-else-if="!tag.hidden"/>
{{ tag.name }}
</div>
<v-text-field
type="text"
clearable
density="compact"
variant="underlined"
:disabled="updating"
:placeholder="$t('Tag Title')"
v-model="editingTag.name"
v-else-if="editingTag.id === tag.id"
@keyup.enter="save(newTag)"
>
<template #prepend>
<v-badge class="right-bottom-icon" color="secondary"
location="bottom right" offset-x="8" :icon="icons.hide"
v-if="tag.hidden">
<v-icon size="24" start :icon="icons.tag"/>
</v-badge>
<v-icon size="24" start :icon="icons.tag" v-else-if="!tag.hidden"/>
</template>
</v-text-field>
</td>
<td class="text-uppercase text-center">
<v-btn color="default"
density="comfortable"
variant="text"
:icon="true"
:loading="tagUpdating[tag.id]"
:disabled="updating"
v-if="editingTag.id !== tag.id"
@click="edit(tag)">
<v-icon size="24" :icon="icons.edit"/>
<v-tooltip activator="parent">{{ $t('Edit') }}</v-tooltip>
</v-btn>
<v-btn color="default"
density="comfortable"
variant="text"
:icon="true"
:disabled="updating"
v-if="editingTag.id !== tag.id">
<v-icon size="24" :icon="icons.more"/>
<v-tooltip activator="parent">{{ $t('More') }}</v-tooltip>
<v-menu activator="parent">
<v-list>
<v-list-item :prepend-icon="tag.hidden ? icons.show : icons.hide"
:title="tag.hidden ? $t('Show') : $t('Hide')"
@click="hide(tag, !tag.hidden)">
</v-list-item>
<v-list-item :prepend-icon="icons.remove"
:title="$t('Delete')" @click="remove(tag)">
</v-list-item>
</v-list>
</v-menu>
</v-btn>
<v-btn density="comfortable"
variant="text"
:icon="true"
:loading="tagUpdating[tag.id]"
:disabled="updating || !isTagModified(tag)"
v-if="editingTag.id === tag.id"
@click="save(editingTag)">
<v-icon size="24" :icon="icons.confirm"/>
<v-tooltip activator="parent">{{ $t('Save') }}</v-tooltip>
</v-btn>
<v-btn class="ml-2" color="default"
density="comfortable"
variant="text"
:icon="true"
:disabled="updating"
v-if="editingTag.id === tag.id"
@click="cancelSave(editingTag)">
<v-icon size="24" :icon="icons.cancel"/>
<v-tooltip activator="parent">{{ $t('Cancel') }}</v-tooltip>
</v-btn>
</td>
</tr>
<tr v-if="newTag">
<td>
<v-text-field
type="text"
color="primary"
clearable
density="compact"
variant="underlined"
:disabled="updating"
:placeholder="$t('Tag Title')"
v-model="newTag.name"
@keyup.enter="save(newTag)"
>
<template #prepend>
<v-icon size="24" start :icon="icons.tag"/>
</template>
</v-text-field>
</td>
<td class="text-uppercase text-center">
<v-btn density="comfortable"
variant="text"
:icon="true"
:loading="tagUpdating[null]"
:disabled="updating || !isTagModified(newTag)"
@click="save(newTag)">
<v-icon size="24" :icon="icons.confirm"/>
<v-tooltip activator="parent">{{ $t('Save') }}</v-tooltip>
</v-btn>
<v-btn class="ml-2" color="default"
density="comfortable"
variant="text"
:icon="true"
:disabled="updating"
@click="cancelSave(newTag)">
<v-icon size="24" :icon="icons.cancel"/>
<v-tooltip activator="parent">{{ $t('Cancel') }}</v-tooltip>
</v-btn>
</td>
</tr>
</tbody>
</v-table>
</v-card>
</v-col>
</v-row> </v-row>
<confirm-dialog ref="confirmDialog"/>
<snack-bar ref="snackbar" />
</template> </template>
<script> <script>
export default { import { mapStores } from 'pinia';
created() { import { useTransactionTagsStore } from '@/stores/transactionTag.js';
import {
mdiRefresh,
mdiPlus,
mdiPencilOutline,
mdiCheck,
mdiClose,
mdiEyeOffOutline,
mdiEyeOutline,
mdiDeleteOutline,
mdiDotsVertical,
mdiPound
} from '@mdi/js';
export default {
data() {
return {
newTag: null,
editingTag: {
id: '',
name: ''
},
loading: true,
updating: false,
tagUpdating: {},
showHidden: false,
icons: {
refresh: mdiRefresh,
add: mdiPlus,
edit: mdiPencilOutline,
confirm: mdiCheck,
cancel: mdiClose,
show: mdiEyeOutline,
hide: mdiEyeOffOutline,
remove: mdiDeleteOutline,
more: mdiDotsVertical,
tag: mdiPound
}
};
},
computed: {
...mapStores(useTransactionTagsStore),
tags() {
return this.transactionTagsStore.allTransactionTags;
},
noAvailableTag() {
for (let i = 0; i < this.tags.length; i++) {
if (this.showHidden || !this.tags[i].hidden) {
return false;
}
}
return true;
},
hasEditingTag() {
return !!(this.newTag || (this.editingTag.id && this.editingTag.id !== ''));
}
},
created() {
const self = this;
self.loading = true;
self.transactionTagsStore.loadAllTags({
force: false
}).then(() => {
self.loading = false;
}).catch(error => {
self.loading = false;
if (!error.processed) {
self.$refs.snackbar.showError(error);
}
});
},
methods: {
reload() {
if (this.hasEditingTag) {
return;
}
const self = this;
self.loading = true;
self.transactionTagsStore.loadAllTags({
force: true
}).then(() => {
self.loading = false;
self.$refs.snackbar.showMessage('Tag list has been updated');
}).catch(error => {
self.loading = false;
if (!error.processed) {
self.$refs.snackbar.showError(error);
}
});
},
saveSortResult() {
const self = this;
self.updating = true;
self.transactionTagsStore.updateTagDisplayOrders().then(() => {
self.updating = false;
}).catch(error => {
self.updating = false;
if (!error.processed) {
self.$refs.snackbar.showError(error);
}
});
},
add() {
this.newTag = {
name: ''
};
},
edit(tag) {
this.editingTag.id = tag.id;
this.editingTag.name = tag.name;
},
save(tag) {
const self = this;
self.updating = true;
self.tagUpdating[tag.id || null] = true;
self.transactionTagsStore.saveTag({
tag: tag
}).then(() => {
self.updating = false;
self.tagUpdating[tag.id || null] = false;
if (tag.id) {
self.editingTag.id = '';
self.editingTag.name = '';
} else {
self.newTag = null;
}
}).catch(error => {
self.updating = false;
self.tagUpdating[tag.id || null] = false;
if (!error.processed) {
self.$refs.snackbar.showError(error);
}
});
},
cancelSave(tag) {
if (tag.id) {
this.editingTag.id = '';
this.editingTag.name = '';
} else {
this.newTag = null;
}
},
isTagModified(tag) {
if (tag.id) {
return this.editingTag.name !== '' && this.editingTag.name !== tag.name;
} else {
return tag.name !== '';
}
},
hide(tag, hidden) {
const self = this;
self.updating = true;
self.tagUpdating[tag.id] = true;
self.transactionTagsStore.hideTag({
tag: tag,
hidden: hidden
}).then(() => {
self.updating = false;
self.tagUpdating[tag.id] = false;
}).catch(error => {
self.updating = false;
self.tagUpdating[tag.id] = false;
if (!error.processed) {
self.$refs.snackbar.showError(error);
}
});
},
remove(tag) {
const self = this;
self.$refs.confirmDialog.open('Are you sure you want to delete this tag?').then(() => {
self.updating = true;
self.tagUpdating[tag.id] = true;
self.transactionTagsStore.deleteTag({
tag: tag
}).then(() => {
self.updating = false;
self.tagUpdating[tag.id] = false;
}).catch(error => {
self.updating = false;
self.tagUpdating[tag.id] = false;
if (!error.processed) {
self.$refs.snackbar.showError(error);
}
});
});
}
} }
} }
</script> </script>
<style>
.transaction-tags-table .v-text-field .v-input__prepend {
margin-right: 0;
color: rgba(var(--v-theme-on-surface));
}
.transaction-tags-table .v-text-field .v-input__prepend .v-badge > .v-badge__wrapper > .v-icon {
opacity: var(--v-medium-emphasis-opacity);
}
.transaction-tags-table .v-text-field.v-text-field--plain-underlined .v-input__prepend {
padding-top: 10px;
}
.transaction-tags-table tr:last-child .v-text-field.v-text-field--plain-underlined .v-input__prepend {
padding-top: 9px;
}
.transaction-tags-table .v-text-field .v-field__input {
padding-top: 2px;
color: rgba(var(--v-theme-on-surface));
}
</style>