Compare commits
5 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5639f8031c | ||
|
|
d42e21bae7 | ||
|
|
504f215c5a | ||
|
|
54d814f9b2 | ||
|
|
7aa2ff1d3a |
10 changed files with 227 additions and 88 deletions
|
|
@ -15,13 +15,26 @@ export const useZeroPrices = () => {
|
||||||
await apiClient.delete('/merch/zeroprices', list)
|
await apiClient.delete('/merch/zeroprices', list)
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.log('Delete zero prices error: ', error)
|
console.log('Delete target zero prices error: ', error)
|
||||||
throw 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 {
|
return {
|
||||||
getZeroPrices,
|
getZeroPrices,
|
||||||
deleteZeroPrices,
|
deleteZeroPrices,
|
||||||
|
deleteZeroPricesPeriod,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
} from 'chart.js'
|
} from 'chart.js'
|
||||||
import { Line } from 'vue-chartjs'
|
import { Line } from 'vue-chartjs'
|
||||||
import 'chartjs-adapter-date-fns'
|
import 'chartjs-adapter-date-fns'
|
||||||
|
import { originColors } from '@/services/colors.js'
|
||||||
|
|
||||||
ChartJS.register(
|
ChartJS.register(
|
||||||
Title,
|
Title,
|
||||||
|
|
@ -33,11 +34,6 @@ const props = defineProps({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const originColors = {
|
|
||||||
surugaya: '#2d3081',
|
|
||||||
mandarake: '#924646',
|
|
||||||
}
|
|
||||||
|
|
||||||
const chartData = ref({
|
const chartData = ref({
|
||||||
datasets: [],
|
datasets: [],
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,10 @@ let refreshPromise = null
|
||||||
|
|
||||||
function createConfig(options = {}) {
|
function createConfig(options = {}) {
|
||||||
const token = localStorage.getItem('accessToken');
|
const token = localStorage.getItem('accessToken');
|
||||||
|
const isFormData = options.body instanceof FormData;
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
'Content-Type': 'application/json',
|
...(isFormData ? {} : { 'Content-Type': 'application/json' }),
|
||||||
...options.headers,
|
...options.headers,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
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',
|
||||||
|
};
|
||||||
|
|
@ -54,37 +54,21 @@ function onFileInputChange(event) {
|
||||||
event.target.value = ''
|
event.target.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleUpload({ fileList: newFileList }) {
|
async function fetchImage(bustCache = false) {
|
||||||
const file = newFileList[newFileList.length - 1]
|
|
||||||
try {
|
try {
|
||||||
await uploadImage(props.merchUuid, file.file)
|
let imgUrl = getImageUrl(props.merchUuid, 'full')
|
||||||
|
|
||||||
const { imgUrl } = await getImageUrl(props.merchUuid, 'full')
|
if (bustCache) {
|
||||||
|
const separator = imgUrl.includes('?') ? '&' : '?'
|
||||||
message.success('Image uploaded successfully.')
|
imgUrl += `${separator}_t=${Date.now()}`
|
||||||
|
}
|
||||||
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');
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
const img = new Image();
|
const img = new Image()
|
||||||
img.src = imgUrl;
|
img.src = imgUrl
|
||||||
img.onload = () => resolve(imgUrl);
|
img.onload = () => resolve(imgUrl)
|
||||||
img.onerror = () => reject(new Error('Image not found'));
|
img.onerror = () => reject(new Error('Image not found'))
|
||||||
});
|
})
|
||||||
|
|
||||||
fileList.value = [
|
fileList.value = [
|
||||||
{
|
{
|
||||||
|
|
@ -92,13 +76,28 @@ onMounted(async () => {
|
||||||
url: imgUrl,
|
url: imgUrl,
|
||||||
status: 'finished',
|
status: 'finished',
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
fileList.value = [];
|
fileList.value = []
|
||||||
if (!error.message.includes('404')) {
|
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)
|
const showConfirmDelete = ref(false)
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,20 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, ref } from 'vue'
|
import ScrollToTopButton from '@/components/ScrollToTopButton.vue'
|
||||||
import { useZeroPrices } from '@/api/zeroPrices.js'
|
import TargetZeroesTab from '@/views/ZeroPricesView/TargetZeroesTab.vue'
|
||||||
import ZeroPriceCard from '@/views/ZeroPricesView/ZeroPriceCard.vue'
|
import PeriodSelectTab from '@/views/ZeroPricesView/PeriodSelectTab.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 fetchZeroPrices = async () => {
|
|
||||||
try {
|
|
||||||
const response = await getZeroPrices()
|
|
||||||
zeroPrices.value = response.data
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
fetchZeroPrices()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="zeroPrices.length === 0">
|
<n-tabs type="line" animated>
|
||||||
<n-h2 class="text-center">Zero prices</n-h2>
|
<n-tab-pane name="Target zeroes" tab="Target zeroes">
|
||||||
<n-h3 class="text-center">No data</n-h3>
|
<TargetZeroesTab />
|
||||||
</div>
|
</n-tab-pane>
|
||||||
<div v-else>
|
<n-tab-pane name="Period select" tab="Period select">
|
||||||
<div class="sticky-search-container">
|
<PeriodSelectTab />
|
||||||
<ZeroPricesToolbar :selected="toDelete" />
|
</n-tab-pane>
|
||||||
</div>
|
</n-tabs>
|
||||||
<div v-for="item in zeroPrices" :key="item.created_at">
|
<ScrollToTopButton />
|
||||||
<ZeroPriceCard :zero-price="item" @toggle="handleToggle" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<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>
|
||||||
|
|
@ -1,36 +1,49 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import { originColors } from '@/services/colors.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
zeroPrice: {
|
zeroPrice: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
checked: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const isChecked = ref(false)
|
|
||||||
const emit = defineEmits(['toggle'])
|
const emit = defineEmits(['toggle'])
|
||||||
const handleCheckboxChange = () => {
|
|
||||||
isChecked.value = !isChecked.value
|
const handleCheckboxChange = (newValue) => {
|
||||||
emit('toggle', {
|
emit('toggle', {
|
||||||
id: props.zeroPrice.id,
|
id: props.zeroPrice.id,
|
||||||
merch_uuid: props.zeroPrice.merch_uuid,
|
merch_uuid: props.zeroPrice.merch_uuid,
|
||||||
checked: isChecked.value,
|
checked: newValue,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentOriginColor = computed(() => {
|
||||||
|
return originColors[props.zeroPrice.origin] || '#fff';
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="zeroPriceCard mt-10">
|
<div class="zeroPriceCard mt-10">
|
||||||
<n-grid responsive="screen" item-responsive cols="4" :x-gap="16" :y-gap="16" class="shift">
|
<n-grid responsive="screen" item-responsive cols="4" :x-gap="16" :y-gap="16" class="shift">
|
||||||
<n-gi>
|
<n-gi>
|
||||||
<n-checkbox :checked="isChecked" @update:checked="handleCheckboxChange">
|
<n-checkbox :checked="checked" @update:checked="handleCheckboxChange">
|
||||||
<strong>Delete</strong>
|
<strong>Delete</strong>
|
||||||
</n-checkbox>
|
</n-checkbox>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
<n-gi><strong>Name:</strong> {{ props.zeroPrice.name }}</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>Created:</strong> {{ props.zeroPrice.created_at }}</n-gi>
|
||||||
<n-gi><strong>Origin:</strong> {{ props.zeroPrice.origin }}</n-gi>
|
<n-gi
|
||||||
|
><strong>Origin:</strong>
|
||||||
|
<span class="bordered" :style="{ borderColor: currentOriginColor }">
|
||||||
|
{{ props.zeroPrice.origin }}
|
||||||
|
</span>
|
||||||
|
</n-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -48,4 +61,11 @@ const handleCheckboxChange = () => {
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
border: 1px solid #e0e0e0;
|
border: 1px solid #e0e0e0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bordered {
|
||||||
|
border: 1px solid;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px;
|
||||||
|
margin: 3px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -10,20 +10,26 @@ const props = defineProps({
|
||||||
|
|
||||||
const messages = useMessage()
|
const messages = useMessage()
|
||||||
const { deleteZeroPrices } = useZeroPrices()
|
const { deleteZeroPrices } = useZeroPrices()
|
||||||
|
const emit = defineEmits(['deleted', 'selectAll'])
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
try {
|
try {
|
||||||
await deleteZeroPrices(props.selected)
|
await deleteZeroPrices(props.selected)
|
||||||
messages.success("Selected zero prices deleted")
|
messages.success("Selected zero prices deleted")
|
||||||
|
emit('deleted')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
messages.error("Error deleting selected prices")
|
messages.error("Error deleting selected prices")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSelectAll = async () => {
|
||||||
|
emit('selectAll')
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
{{ props.selected }}
|
|
||||||
<div class="toolbar button-container-evenly padding-lr-30">
|
<div class="toolbar button-container-evenly padding-lr-30">
|
||||||
<div v-if="props.selected.length === 0" class="toolbar-item">
|
<div v-if="props.selected.length === 0" class="toolbar-item">
|
||||||
<span>Select records to delete</span>
|
<span>Select records to delete</span>
|
||||||
|
|
@ -31,6 +37,9 @@ const handleDelete = async () => {
|
||||||
<div v-else class="toolbar-item">
|
<div v-else class="toolbar-item">
|
||||||
{{ props.selected.length }} items selected
|
{{ props.selected.length }} items selected
|
||||||
</div>
|
</div>
|
||||||
|
<div class="toolbar-item">
|
||||||
|
<span @click="handleSelectAll">Click here to select all</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="toolbar-item">
|
<div class="toolbar-item">
|
||||||
<n-button
|
<n-button
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue