Compare commits
8 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5639f8031c | ||
|
|
d42e21bae7 | ||
|
|
504f215c5a | ||
|
|
54d814f9b2 | ||
|
|
7aa2ff1d3a | ||
|
|
cffc6acc2b | ||
|
|
2b08889218 | ||
|
|
0e51451ad2 |
17 changed files with 412 additions and 53 deletions
40
src/api/zeroPrices.js
Normal file
40
src/api/zeroPrices.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { apiClient } from '@/services/apiClient.js'
|
||||
|
||||
export const useZeroPrices = () => {
|
||||
const getZeroPrices = async () => {
|
||||
try {
|
||||
return await apiClient.get('/merch/zeroprices')
|
||||
} catch (error) {
|
||||
console.log('Get zero prices error: ', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const deleteZeroPrices = async (list) => {
|
||||
try {
|
||||
await apiClient.delete('/merch/zeroprices', list)
|
||||
}
|
||||
catch (error) {
|
||||
console.log('Delete target zero prices error: ', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const deleteZeroPricesPeriod = async (start, end) => {
|
||||
const params = new URLSearchParams({ start, end }).toString()
|
||||
const url = `/merch/zeroprices/period${params ? `?${params}` : ''}`
|
||||
const response = await apiClient.delete(url)
|
||||
|
||||
if (response.status === 200) {
|
||||
return response
|
||||
} else {
|
||||
console.log('Delete period select zero prices error: ', response)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
getZeroPrices,
|
||||
deleteZeroPrices,
|
||||
deleteZeroPricesPeriod,
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import {
|
|||
} from 'chart.js'
|
||||
import { Line } from 'vue-chartjs'
|
||||
import 'chartjs-adapter-date-fns'
|
||||
import { originColors } from '@/services/colors.js'
|
||||
|
||||
ChartJS.register(
|
||||
Title,
|
||||
|
|
@ -33,11 +34,6 @@ const props = defineProps({
|
|||
},
|
||||
})
|
||||
|
||||
const originColors = {
|
||||
surugaya: '#2d3081',
|
||||
mandarake: '#924646',
|
||||
}
|
||||
|
||||
const chartData = ref({
|
||||
datasets: [],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ const mainMenu = computed(() => {
|
|||
{ label: 'Collection', key: 'collection' },
|
||||
{ label: 'Charts', key: 'charts' },
|
||||
{ label: 'Parsers', key: 'parsers' },
|
||||
{ label: 'Zero prices', key: 'zeroprices' },
|
||||
]
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import PersonalView from '@/views/PersonalView.vue'
|
|||
import AddMerchView from '@/views/AddMerchView.vue'
|
||||
import DetailsView from '@/views/DetailsView.vue'
|
||||
import LabelsView from '@/views/LabelsView.vue'
|
||||
import ZeroPricesView from '@/views/ZeroPricesView.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
|
|
@ -58,6 +59,11 @@ const router = createRouter({
|
|||
name: 'labels',
|
||||
component: LabelsView,
|
||||
},
|
||||
{
|
||||
path: '/zeroprices',
|
||||
name: 'zeroprices',
|
||||
component: ZeroPricesView,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@ let refreshPromise = null
|
|||
|
||||
function createConfig(options = {}) {
|
||||
const token = localStorage.getItem('accessToken');
|
||||
const isFormData = options.body instanceof FormData;
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...(isFormData ? {} : { 'Content-Type': 'application/json' }),
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
|
|
@ -138,12 +140,14 @@ export const apiClient = {
|
|||
return request(url, {
|
||||
method: 'POST',
|
||||
body: isFormData ? data : JSON.stringify(data),
|
||||
// headers: isFormData ? {} : { 'Content-Type': 'application/json' }
|
||||
})
|
||||
},
|
||||
put: (url, data) => request(url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
delete: (url) => request(url, { method: 'DELETE' }),
|
||||
delete: (url, data) => request(url, {
|
||||
method: 'DELETE',
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
}),
|
||||
}
|
||||
|
|
|
|||
4
src/services/colors.js
Normal file
4
src/services/colors.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export const originColors = {
|
||||
surugaya: '#2d3081',
|
||||
mandarake: '#924646',
|
||||
};
|
||||
|
|
@ -58,6 +58,7 @@ export const useAuthStore = defineStore('auth', () => {
|
|||
router.push({ name: 'collection'})
|
||||
} catch (error) {
|
||||
console.error('Login error:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export const useLabelsStore = defineStore('labels', () => {
|
|||
|
||||
const getLabels = async () => {
|
||||
try {
|
||||
const response = await apiClient.get('/merch/labels')
|
||||
const response = await apiClient.get(`/merch/labels?_=${Date.now()}`)
|
||||
const labelList = Array.isArray(response.data) ? response.data : []
|
||||
labels.value = labelList
|
||||
localStorage.setItem('labels', JSON.stringify(labelList))
|
||||
|
|
@ -46,7 +46,7 @@ export const useLabelsStore = defineStore('labels', () => {
|
|||
|
||||
const updateLabel = async (uuid, updatedData) => {
|
||||
try {
|
||||
await apiClient.put(`/merch/labels/${uuid}`, updatedData)
|
||||
await apiClient.put(`/merch/labels/${uuid}?_=${Date.now()}`, updatedData)
|
||||
await getLabels()
|
||||
} catch (error) {
|
||||
console.error('Failed to update label:', error)
|
||||
|
|
|
|||
|
|
@ -197,3 +197,8 @@ body,
|
|||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.padding-lr-30 {
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import router from '@/router/index.js'
|
||||
import { useLabelsStore } from '@/stores/labelsStore.js'
|
||||
|
||||
|
|
@ -21,14 +21,13 @@ const addMerch = () => {
|
|||
router.push({ name: 'addMerch' })
|
||||
}
|
||||
|
||||
|
||||
const localSearch = computed({
|
||||
get() {
|
||||
return props.modelValue
|
||||
},
|
||||
set(value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const selectedLabelUuids = computed({
|
||||
|
|
@ -37,7 +36,7 @@ const selectedLabelUuids = computed({
|
|||
},
|
||||
set(value) {
|
||||
emit('update:labelUuids', value)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const labelOptions = computed(() => {
|
||||
|
|
@ -69,6 +68,4 @@ const labelOptions = computed(() => {
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -54,37 +54,21 @@ function onFileInputChange(event) {
|
|||
event.target.value = ''
|
||||
}
|
||||
|
||||
async function handleUpload({ fileList: newFileList }) {
|
||||
const file = newFileList[newFileList.length - 1]
|
||||
async function fetchImage(bustCache = false) {
|
||||
try {
|
||||
await uploadImage(props.merchUuid, file.file)
|
||||
let imgUrl = getImageUrl(props.merchUuid, 'full')
|
||||
|
||||
const { imgUrl } = await getImageUrl(props.merchUuid, 'full')
|
||||
|
||||
message.success('Image uploaded successfully.')
|
||||
|
||||
fileList.value = [
|
||||
{
|
||||
name: file.name,
|
||||
url: imgUrl,
|
||||
status: 'finished',
|
||||
},
|
||||
]
|
||||
} catch (error) {
|
||||
message.error('Upload error: ' + (error.message || 'Unknown error.'))
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const imgUrl = getImageUrl(props.merchUuid, 'full');
|
||||
if (bustCache) {
|
||||
const separator = imgUrl.includes('?') ? '&' : '?'
|
||||
imgUrl += `${separator}_t=${Date.now()}`
|
||||
}
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.src = imgUrl;
|
||||
img.onload = () => resolve(imgUrl);
|
||||
img.onerror = () => reject(new Error('Image not found'));
|
||||
});
|
||||
const img = new Image()
|
||||
img.src = imgUrl
|
||||
img.onload = () => resolve(imgUrl)
|
||||
img.onerror = () => reject(new Error('Image not found'))
|
||||
})
|
||||
|
||||
fileList.value = [
|
||||
{
|
||||
|
|
@ -92,13 +76,28 @@ onMounted(async () => {
|
|||
url: imgUrl,
|
||||
status: 'finished',
|
||||
},
|
||||
];
|
||||
]
|
||||
} catch (error) {
|
||||
fileList.value = [];
|
||||
fileList.value = []
|
||||
if (!error.message.includes('404')) {
|
||||
console.error('Error getting image: ', error);
|
||||
console.error('Error getting image:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpload({ fileList: newFileList }) {
|
||||
const file = newFileList[newFileList.length - 1]
|
||||
try {
|
||||
await uploadImage(props.merchUuid, file.file)
|
||||
message.success('Image uploaded successfully.')
|
||||
await fetchImage(true)
|
||||
} catch (error) {
|
||||
message.error('Upload error: ' + (error.message || 'Unknown error.'))
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchImage(false)
|
||||
});
|
||||
|
||||
const showConfirmDelete = ref(false)
|
||||
|
|
|
|||
|
|
@ -2,16 +2,23 @@
|
|||
import { reactive, ref } from 'vue'
|
||||
import { useAuthStore } from '@/stores/authStore.js'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useMessage } from 'naive-ui'
|
||||
|
||||
const store = useAuthStore()
|
||||
const messages = useMessage()
|
||||
|
||||
const { activeTab } = storeToRefs(store)
|
||||
|
||||
const signInEmail = ref('')
|
||||
const signInPassword = ref('')
|
||||
|
||||
const onSignIn = () => {
|
||||
store.login(signInEmail.value, signInPassword.value)
|
||||
const onSignIn = async () => {
|
||||
try{
|
||||
await store.login(signInEmail.value, signInPassword.value)
|
||||
messages.success('Login success')
|
||||
} catch (error) {
|
||||
messages.error("Login error")
|
||||
}
|
||||
}
|
||||
|
||||
const signUp = reactive({
|
||||
|
|
@ -20,8 +27,35 @@ const signUp = reactive({
|
|||
reenterPassword: '',
|
||||
})
|
||||
|
||||
const onSignUp = () => {
|
||||
store.register(signUp.email, signUp.password)
|
||||
const onSignUp = async () => {
|
||||
if (!signUp.email.trim()) {
|
||||
messages.error('Email is required')
|
||||
return
|
||||
}
|
||||
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
if (!emailRegex.test(signUp.email)) {
|
||||
messages.error('Please enter a valid email address')
|
||||
return
|
||||
}
|
||||
|
||||
if (!signUp.password.trim()) {
|
||||
messages.error('Password is required')
|
||||
return
|
||||
}
|
||||
|
||||
if (signUp.password !== signUp.reenterPassword) {
|
||||
messages.error('Passwords do not match')
|
||||
return
|
||||
}
|
||||
|
||||
try{
|
||||
await store.register(signUp.email, signUp.password)
|
||||
messages.success('Register success')
|
||||
activeTab.value = 'signin'
|
||||
} catch (error) {
|
||||
messages.error("Register error")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -43,7 +77,7 @@ const onSignUp = () => {
|
|||
<n-form-item-row label="Password">
|
||||
<n-input v-model:value="signInPassword" type="password" show-password-on="click" />
|
||||
</n-form-item-row>
|
||||
<n-button type="primary" block secondary strong attr-type="submit" @click="onSignIn"> Sign In</n-button>
|
||||
<n-button type="primary" block secondary strong attr-type="submit"> Sign In</n-button>
|
||||
</n-form>
|
||||
</n-tab-pane>
|
||||
|
||||
|
|
@ -58,7 +92,7 @@ const onSignUp = () => {
|
|||
<n-form-item-row label="Reenter Password">
|
||||
<n-input type="password" v-model:value="signUp.reenterPassword" show-password-on="click" />
|
||||
</n-form-item-row>
|
||||
<n-button type="primary" block secondary strong attr-type="submit" @click="onSignUp">Sign up</n-button>
|
||||
<n-button type="primary" block secondary strong attr-type="submit">Sign up</n-button>
|
||||
</n-form>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
|
|
|
|||
20
src/views/ZeroPricesView.vue
Normal file
20
src/views/ZeroPricesView.vue
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<script setup>
|
||||
import ScrollToTopButton from '@/components/ScrollToTopButton.vue'
|
||||
import TargetZeroesTab from '@/views/ZeroPricesView/TargetZeroesTab.vue'
|
||||
import PeriodSelectTab from '@/views/ZeroPricesView/PeriodSelectTab.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-tabs type="line" animated>
|
||||
<n-tab-pane name="Target zeroes" tab="Target zeroes">
|
||||
<TargetZeroesTab />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="Period select" tab="Period select">
|
||||
<PeriodSelectTab />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
<ScrollToTopButton />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
54
src/views/ZeroPricesView/PeriodSelectTab.vue
Normal file
54
src/views/ZeroPricesView/PeriodSelectTab.vue
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useZeroPrices } from '@/api/zeroPrices.js'
|
||||
|
||||
const range = ref(null)
|
||||
|
||||
const setTodayRange = () => {
|
||||
const now = new Date()
|
||||
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||||
const end = new Date(start.getTime() + 24 * 60 * 60 * 1000)
|
||||
|
||||
range.value = [start.getTime(), end.getTime()]
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTodayRange()
|
||||
})
|
||||
|
||||
const toRFCtime = (timestamp) => {
|
||||
return new Date(timestamp).toISOString()
|
||||
}
|
||||
|
||||
const { deleteZeroPricesPeriod } = useZeroPrices()
|
||||
|
||||
const deleteEnabled = computed(() => {
|
||||
return range.value === null
|
||||
})
|
||||
|
||||
const deletePeriod = async () => {
|
||||
if (range.value !== null) {
|
||||
const start = toRFCtime(range.value[0])
|
||||
const end = toRFCtime(range.value[1])
|
||||
await deleteZeroPricesPeriod(start, end)
|
||||
} else {
|
||||
console.log('Delete period select zero prices error')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-date-picker
|
||||
v-model:value="range"
|
||||
type="datetimerange"
|
||||
format="HH:mm:ss dd-MM-yyyy"
|
||||
clearable
|
||||
/>
|
||||
<div class="button-container-center">
|
||||
<n-button class="center-button w360" type="error" :disabled="deleteEnabled" @click="deletePeriod">Delete</n-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
71
src/views/ZeroPricesView/TargetZeroesTab.vue
Normal file
71
src/views/ZeroPricesView/TargetZeroesTab.vue
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useZeroPrices } from '@/api/zeroPrices.js'
|
||||
import ZeroPriceCard from '@/views/ZeroPricesView/ZeroPriceCard.vue'
|
||||
import ZeroPricesToolbar from '@/views/ZeroPricesView/ZeroPricesToolbar.vue'
|
||||
|
||||
const { getZeroPrices } = useZeroPrices()
|
||||
|
||||
const zeroPrices = ref([])
|
||||
const toDelete = ref([])
|
||||
|
||||
const handleToggle = ({ id, merch_uuid, checked }) => {
|
||||
if (checked) {
|
||||
toDelete.value.push({ id, merch_uuid });
|
||||
} else {
|
||||
toDelete.value = toDelete.value.filter(item => item.id !== id);
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectAll = () => {
|
||||
toDelete.value = zeroPrices.value.map(item => ({
|
||||
id: item.id,
|
||||
merch_uuid: item.merch_uuid
|
||||
}))
|
||||
}
|
||||
|
||||
const fetchZeroPrices = async () => {
|
||||
try {
|
||||
const response = await getZeroPrices()
|
||||
zeroPrices.value = Array.isArray(response.data) ? response.data : []
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleted = () => {
|
||||
toDelete.value = []
|
||||
fetchZeroPrices()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchZeroPrices()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="zeroPrices.length === 0">
|
||||
<n-h2 class="text-center">Zero prices</n-h2>
|
||||
<n-h3 class="text-center">No data</n-h3>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="sticky-search-container">
|
||||
<ZeroPricesToolbar
|
||||
:selected="toDelete"
|
||||
@deleted="handleDeleted"
|
||||
@selectAll="handleSelectAll"
|
||||
/>
|
||||
</div>
|
||||
<div v-for="item in zeroPrices" :key="item.created_at">
|
||||
<ZeroPriceCard
|
||||
:zero-price="item"
|
||||
@toggle="handleToggle"
|
||||
:checked="toDelete.some(t => t.id === item.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
71
src/views/ZeroPricesView/ZeroPriceCard.vue
Normal file
71
src/views/ZeroPricesView/ZeroPriceCard.vue
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { originColors } from '@/services/colors.js'
|
||||
|
||||
const props = defineProps({
|
||||
zeroPrice: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
checked: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['toggle'])
|
||||
|
||||
const handleCheckboxChange = (newValue) => {
|
||||
emit('toggle', {
|
||||
id: props.zeroPrice.id,
|
||||
merch_uuid: props.zeroPrice.merch_uuid,
|
||||
checked: newValue,
|
||||
})
|
||||
}
|
||||
|
||||
const currentOriginColor = computed(() => {
|
||||
return originColors[props.zeroPrice.origin] || '#fff';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="zeroPriceCard mt-10">
|
||||
<n-grid responsive="screen" item-responsive cols="4" :x-gap="16" :y-gap="16" class="shift">
|
||||
<n-gi>
|
||||
<n-checkbox :checked="checked" @update:checked="handleCheckboxChange">
|
||||
<strong>Delete</strong>
|
||||
</n-checkbox>
|
||||
</n-gi>
|
||||
<n-gi><strong>Name:</strong> {{ props.zeroPrice.name }}</n-gi>
|
||||
<n-gi><strong>Created:</strong> {{ props.zeroPrice.created_at }}</n-gi>
|
||||
<n-gi
|
||||
><strong>Origin:</strong>
|
||||
<span class="bordered" :style="{ borderColor: currentOriginColor }">
|
||||
{{ props.zeroPrice.origin }}
|
||||
</span>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.zeroPriceCard {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 16px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
gap: 12px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.bordered {
|
||||
border: 1px solid;
|
||||
border-radius: 4px;
|
||||
padding: 2px;
|
||||
margin: 3px;
|
||||
}
|
||||
</style>
|
||||
56
src/views/ZeroPricesView/ZeroPricesToolbar.vue
Normal file
56
src/views/ZeroPricesView/ZeroPricesToolbar.vue
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<script setup>
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { useZeroPrices } from '@/api/zeroPrices.js'
|
||||
const props = defineProps({
|
||||
selected: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const messages = useMessage()
|
||||
const { deleteZeroPrices } = useZeroPrices()
|
||||
const emit = defineEmits(['deleted', 'selectAll'])
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
await deleteZeroPrices(props.selected)
|
||||
messages.success("Selected zero prices deleted")
|
||||
emit('deleted')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
messages.error("Error deleting selected prices")
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectAll = async () => {
|
||||
emit('selectAll')
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="toolbar button-container-evenly padding-lr-30">
|
||||
<div v-if="props.selected.length === 0" class="toolbar-item">
|
||||
<span>Select records to delete</span>
|
||||
</div>
|
||||
<div v-else class="toolbar-item">
|
||||
{{ props.selected.length }} items selected
|
||||
</div>
|
||||
<div class="toolbar-item">
|
||||
<span @click="handleSelectAll">Click here to select all</span>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-item">
|
||||
<n-button
|
||||
type="error"
|
||||
:disabled="props.selected.length === 0"
|
||||
@click="handleDelete"
|
||||
>
|
||||
Delete Selected
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue