Compare commits

..

No commits in common. "9d80345b77c6ef4383f37ef707fda7089762a219" and "b12c79fa213cd1031d9dc9ce8dd715e375b75779" have entirely different histories.

10 changed files with 33 additions and 369 deletions

View file

@ -8,32 +8,29 @@ import Footer from '@/components/Footer/Footer.vue'
<n-message-provider>
<n-dialog-provider>
<n-notification-provider>
<div class="app-layout">
<NavBar />
<div class="main-content">
<n-grid
responsive="screen"
item-responsive
cols="24"
:x-gap="16"
:y-gap="16"
class="shift"
>
<n-gi span="xs:1 s:1 m:2 l:2 xl:3 xxl:3" />
<NavBar />
<n-gi span="xs:22 s:22 m:20 l:20 xl:18 xxl:18">
<router-view />
</n-gi>
<n-grid
responsive="screen"
item-responsive
cols="24"
:x-gap="16"
:y-gap="16"
class="shift"
>
<n-gi span="xs:1 s:1 m:2 l:2 xl:3 xxl:3" />
<n-gi span="xs:1 s:1 m:2 l:2 xl:3 xxl:3" />
</n-grid>
</div>
<Footer />
</div>
<n-gi span="xs:22 s:22 m:20 l:20 xl:18 xxl:18">
<router-view />
</n-gi>
<n-gi span="xs:1 s:1 m:2 l:2 xl:3 xxl:3" />
</n-grid>
</n-notification-provider>
</n-dialog-provider>
</n-message-provider>
</n-config-provider>
<Footer />
</template>
<style scoped>

View file

@ -1,78 +0,0 @@
import { apiClient } from '@/services/apiClient.js'
export const useMerchImagesApi = () => {
const uploadImage = async (uuid, file) => {
const formData = new FormData()
formData.append('file', file)
formData.append('imageType', 'all')
try {
const response = await apiClient.post(`/merch/images/${uuid}`, formData)
if (response.status !== 200) {
throw new Error(`Upload failed: ${response.status}`)
}
return response.data
} catch (error) {
console.error('Upload failed:', error)
throw error
}
}
const cachedImages = new Map() // Map<uuid, { etag, url }>
const getImageUrl = async (uuid, type) => {
try {
const response = await apiClient.get(`/merch/images/${uuid}`, { type })
if (response.status !== 200) {
throw new Error(`Get image failed: ${response.status}`)
}
const { link, ETag } = response.data
if (cachedImages.has(uuid) && cachedImages.get(uuid).etag === ETag) {
return cachedImages.get(uuid)
}
const res = await fetch(link)
if (!res.ok) throw new Error(`Failed to load image: ${res.status}`)
const blob = await res.blob()
const imgUrl = URL.createObjectURL(blob)
cachedImages.set(uuid, { imgUrl, etag: ETag })
return { imgUrl, etag: ETag }
} catch (error) {
console.error('Get image failed:', error)
throw error
}
}
const deleteImage = async (uuid) => {
try {
const response = await apiClient.delete(`/merch/images/${uuid}`)
if (response.status !== 200) {
throw new Error(`Delete failed: ${response.status}`)
}
if (cachedImages.has(uuid)) {
const cached = cachedImages.get(uuid)
if (cached.imgUrl?.startsWith('blob:')) URL.revokeObjectURL(cached.imgUrl)
cachedImages.delete(uuid)
}
return true
} catch (error) {
console.error('Delete image failed:', error)
throw error
}
}
return {
uploadImage,
getImageUrl,
deleteImage,
}
}

View file

@ -1,4 +1,5 @@
<script setup>
</script>
<template>

View file

@ -1,12 +1,13 @@
<template>
<svg
height="800px"
width="800px"
version="1.1"
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 512 512"
xml:space="preserve"
style="width: 100%; height: 100%; display: block;"
>
<polygon
style="fill: #e0b76e"

View file

@ -126,17 +126,10 @@ export const apiClient = {
const fullUrl = queryString ? `${url}?${queryString}` : url;
return request(fullUrl, { method: 'GET' });
},
post: (url, data) => {
const isFormData = data instanceof FormData
return request(url, {
method: 'POST',
body: isFormData ? data : JSON.stringify(data),
headers: isFormData
? {}
: { 'Content-Type': 'application/json' }
})
},
post: (url, data) => request(url, {
method: 'POST',
body: JSON.stringify(data),
}),
put: (url, data) => request(url, {
method: 'PUT',
body: JSON.stringify(data),

View file

@ -1,11 +1,12 @@
export function convertIso(isoString) {
const date = new Date(isoString);
// Извлекаем компоненты времени и даты
const hours = String(date.getUTCHours()).padStart(2, '0');
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
const seconds = String(date.getUTCSeconds()).padStart(2, '0');
const day = String(date.getUTCDate()).padStart(2, '0');
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
const month = String(date.getUTCMonth() + 1).padStart(2, '0'); // Месяцы в JS — от 0 до 11
const year = date.getUTCFullYear();
return `${hours}:${minutes}:${seconds} ${day}-${month}-${year}`;

View file

@ -120,20 +120,3 @@
.link-like-text {
cursor: pointer;
}
html,
body,
#app {
height: 100%;
margin: 0;
}
.app-layout {
display: flex;
flex-direction: column;
min-height: 100dvh;
}
.main-content {
flex: 1;
}

View file

@ -1,36 +1,12 @@
<script setup>
import BoxIcon from '@/components/icons/BoxIcon.vue'
import { useMerchImagesApi } from '@/api/merchImages.js'
import { onMounted, ref } from 'vue'
const { getImageUrl } = useMerchImagesApi()
const props = defineProps({
defineProps({
merch: {
type: Object,
required: true,
}
})
const fileList = ref([])
onMounted(async () => {
try {
const { imgUrl } = await getImageUrl(props.merch.merch_uuid, 'thumbnail')
fileList.value = [
{
name: 'full.jpg',
url: imgUrl,
status: 'finished',
},
]
} catch (error) {
fileList.value = []
if (!error.message?.includes('404')) {
console.error('Error getting image: ', error)
}
}
})
</script>
<template>
@ -40,13 +16,7 @@ onMounted(async () => {
</template>
<template #cover>
<div class="cover-wrapper">
<img
v-if="fileList.length > 0 && fileList[0].url"
:src="fileList[0].url"
alt="Thumbnail"
class="thumbnail"
/>
<BoxIcon v-else />
<BoxIcon />
</div>
</template>
</n-card>
@ -82,13 +52,6 @@ onMounted(async () => {
overflow: hidden;
}
.thumbnail {
width: 100%;
height: auto;
max-width: 80%;
object-fit: contain;
}
.cover-wrapper :deep(svg) {
width: 100%;
height: auto;

View file

@ -7,7 +7,6 @@ import ChartBlock from '@/components/ChartBlock.vue'
import { useChartsApi } from '@/api/charts.js'
import EditLink from '@/views/DetailsView/EditLink.vue'
import CopyToClipboard from '@/components/CopyToClipboard.vue'
import DetailsViewImages from '@/views/DetailsView/DetailsViewImages.vue'
const { getMerchDetails, deleteMerch } = useMerchApi()
const { getDistinctPrices } = useChartsApi()
@ -103,13 +102,8 @@ onMounted(() => {
<div v-else-if="error">Error: {{ error }}</div>
<n-card v-else-if="merchDetails" :title="merchDetails.name">
<n-divider title-placement="left">Main</n-divider>
<div class="container-stackable">
<div>
<p><strong>Uuid:</strong> {{ merchDetails.merch_uuid }}</p>
<p><strong>Name:</strong> {{ merchDetails.name }}</p>
</div>
<DetailsViewImages :merch-uuid="merchDetails.merch_uuid"/>
</div>
<p><strong>Uuid:</strong> {{ merchDetails.merch_uuid }}</p>
<p><strong>Name:</strong> {{ merchDetails.name }}</p>
<n-divider title-placement="left">Prices</n-divider>
<PeriodSelector @days="handleSelectDays" />
@ -137,9 +131,7 @@ onMounted(() => {
</n-button>
</template>
<template v-else>
<span class="default-color underline link-like-text" @click="editing.surugaya = true"
>Add link</span
>
<span class="default-color underline link-like-text" @click="editing.surugaya = true">Add link</span>
</template>
</div>
@ -172,9 +164,7 @@ onMounted(() => {
</n-button>
</template>
<template v-else>
<span class="default-color underline link-like-text" @click="editing.mandarake = true"
>Add link</span
>
<span class="default-color underline link-like-text" @click="editing.mandarake = true">Add link</span>
</template>
</div>
@ -205,11 +195,4 @@ onMounted(() => {
</n-modal>
</template>
<style scoped>
.container-stackable {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: space-between;
}
</style>
<style scoped></style>

View file

@ -1,180 +0,0 @@
<script setup>
import { onMounted, ref } from 'vue'
import { NModal, NUpload, useMessage } from 'naive-ui'
import BoxIcon from '@/components/icons/BoxIcon.vue'
import { useMerchImagesApi } from '@/api/merchImages.js'
const { uploadImage, getImageUrl, deleteImage } = useMerchImagesApi()
const message = useMessage()
const props = defineProps({
merchUuid: { type: String, required: true },
})
const showModal = ref(false)
const previewImageUrl = ref('')
const fileList = ref([])
function handlePreview(file) {
if (file.file) {
previewImageUrl.value = URL.createObjectURL(file.file)
} else if (file.url) {
previewImageUrl.value = file.url
}
showModal.value = true
}
function onModalClose() {
if (previewImageUrl.value && previewImageUrl.value.startsWith('blob:')) {
URL.revokeObjectURL(previewImageUrl.value)
}
previewImageUrl.value = ''
}
function beforeUpload({ fileList: newFileList }) {
return newFileList.length <= 1
}
async function handleUpload({ fileList: newFileList }) {
const file = newFileList[newFileList.length - 1]
try {
await uploadImage(props.merchUuid, file.file)
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 } = await getImageUrl(props.merchUuid, 'full')
fileList.value = [
{
name: 'full.jpg',
url: imgUrl,
status: 'finished',
},
]
} catch (error) {
fileList.value = []
if (!error.message?.includes('404')) {
console.error('Error getting image: ', error)
}
}
})
const showConfirmDelete = ref(false)
const deleteImageHandler = async () => {
showConfirmDelete.value = true
return false //prevents instant "delete image" in n-upload before confirm
}
const confirmDeleteImage = async () => {
try {
await deleteImage(props.merchUuid)
fileList.value = []
message.success('Image deleted successfully.')
} catch (error) {
message.error('Image delete error: ' + (error.message || 'Unknown error.'))
} finally {
showConfirmDelete.value = false
}
}
const cancelDelete = () => {
showConfirmDelete.value = false
}
</script>
<template>
<div>
<n-upload
v-model:file-list="fileList"
:default-upload="false"
list-type="image-card"
:max="1"
:before-upload="beforeUpload"
@before-preview="handlePreview"
@change="handleUpload"
@remove="deleteImageHandler"
>
<div class="upload-trigger" v-if="fileList.length === 0">
<BoxIcon class="upload-icon" />
<span class="upload-text">Click to Upload</span>
</div>
</n-upload>
<n-modal
v-model:show="showModal"
preset="card"
style="width: 600px"
title="Preview"
@after-leave="onModalClose"
>
<img
:src="previewImageUrl"
style="width: 100%; max-height: 600px; object-fit: contain"
alt="Preview"
/>
</n-modal>
</div>
<n-modal v-model:show="showConfirmDelete" preset="dialog" title="Confirmation">
<template #default>
<p>Confirm delete image</p>
</template>
<template #action>
<n-button @click="confirmDeleteImage" type="error">Delete</n-button>
<n-button @click="cancelDelete">Cancel</n-button>
</template>
</n-modal>
</template>
<style scoped>
.upload-trigger {
width: 300px;
max-height: 600px;
min-height: 180px;
border: 1px dashed #ccc;
border-radius: 8px;
background-color: #fafafa;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition:
background-color 0.2s ease,
border-color 0.2s ease;
}
.upload-trigger:hover {
border-color: #18a058;
background-color: #f0fdf4;
}
.upload-icon {
width: 150px;
height: 150px;
opacity: 0.6;
}
.upload-text {
margin-top: 8px;
font-size: 14px;
color: #555;
}
</style>