Compare commits
No commits in common. "9d80345b77c6ef4383f37ef707fda7089762a219" and "b12c79fa213cd1031d9dc9ce8dd715e375b75779" have entirely different histories.
9d80345b77
...
b12c79fa21
10 changed files with 33 additions and 369 deletions
37
src/App.vue
37
src/App.vue
|
|
@ -8,32 +8,29 @@ import Footer from '@/components/Footer/Footer.vue'
|
||||||
<n-message-provider>
|
<n-message-provider>
|
||||||
<n-dialog-provider>
|
<n-dialog-provider>
|
||||||
<n-notification-provider>
|
<n-notification-provider>
|
||||||
<div class="app-layout">
|
<NavBar />
|
||||||
<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" />
|
|
||||||
|
|
||||||
<n-gi span="xs:22 s:22 m:20 l:20 xl:18 xxl:18">
|
<n-grid
|
||||||
<router-view />
|
responsive="screen"
|
||||||
</n-gi>
|
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-gi span="xs:22 s:22 m:20 l:20 xl:18 xxl:18">
|
||||||
</n-grid>
|
<router-view />
|
||||||
</div>
|
</n-gi>
|
||||||
<Footer />
|
|
||||||
</div>
|
<n-gi span="xs:1 s:1 m:2 l:2 xl:3 xxl:3" />
|
||||||
|
</n-grid>
|
||||||
</n-notification-provider>
|
</n-notification-provider>
|
||||||
</n-dialog-provider>
|
</n-dialog-provider>
|
||||||
</n-message-provider>
|
</n-message-provider>
|
||||||
</n-config-provider>
|
</n-config-provider>
|
||||||
|
<Footer />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<svg
|
<svg
|
||||||
|
height="800px"
|
||||||
|
width="800px"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
id="Layer_1"
|
id="Layer_1"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
viewBox="0 0 512 512"
|
viewBox="0 0 512 512"
|
||||||
xml:space="preserve"
|
xml:space="preserve"
|
||||||
style="width: 100%; height: 100%; display: block;"
|
|
||||||
>
|
>
|
||||||
<polygon
|
<polygon
|
||||||
style="fill: #e0b76e"
|
style="fill: #e0b76e"
|
||||||
|
|
|
||||||
|
|
@ -126,17 +126,10 @@ export const apiClient = {
|
||||||
const fullUrl = queryString ? `${url}?${queryString}` : url;
|
const fullUrl = queryString ? `${url}?${queryString}` : url;
|
||||||
return request(fullUrl, { method: 'GET' });
|
return request(fullUrl, { method: 'GET' });
|
||||||
},
|
},
|
||||||
post: (url, data) => {
|
post: (url, data) => request(url, {
|
||||||
const isFormData = data instanceof FormData
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data),
|
||||||
return request(url, {
|
}),
|
||||||
method: 'POST',
|
|
||||||
body: isFormData ? data : JSON.stringify(data),
|
|
||||||
headers: isFormData
|
|
||||||
? {}
|
|
||||||
: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
},
|
|
||||||
put: (url, data) => request(url, {
|
put: (url, data) => request(url, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
export function convertIso(isoString) {
|
export function convertIso(isoString) {
|
||||||
const date = new Date(isoString);
|
const date = new Date(isoString);
|
||||||
|
|
||||||
|
// Извлекаем компоненты времени и даты
|
||||||
const hours = String(date.getUTCHours()).padStart(2, '0');
|
const hours = String(date.getUTCHours()).padStart(2, '0');
|
||||||
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
|
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
|
||||||
const seconds = String(date.getUTCSeconds()).padStart(2, '0');
|
const seconds = String(date.getUTCSeconds()).padStart(2, '0');
|
||||||
const day = String(date.getUTCDate()).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();
|
const year = date.getUTCFullYear();
|
||||||
|
|
||||||
return `${hours}:${minutes}:${seconds} ${day}-${month}-${year}`;
|
return `${hours}:${minutes}:${seconds} ${day}-${month}-${year}`;
|
||||||
|
|
|
||||||
|
|
@ -120,20 +120,3 @@
|
||||||
.link-like-text {
|
.link-like-text {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
|
||||||
body,
|
|
||||||
#app {
|
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-layout {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 100dvh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,12 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import BoxIcon from '@/components/icons/BoxIcon.vue'
|
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: {
|
merch: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -40,13 +16,7 @@ onMounted(async () => {
|
||||||
</template>
|
</template>
|
||||||
<template #cover>
|
<template #cover>
|
||||||
<div class="cover-wrapper">
|
<div class="cover-wrapper">
|
||||||
<img
|
<BoxIcon />
|
||||||
v-if="fileList.length > 0 && fileList[0].url"
|
|
||||||
:src="fileList[0].url"
|
|
||||||
alt="Thumbnail"
|
|
||||||
class="thumbnail"
|
|
||||||
/>
|
|
||||||
<BoxIcon v-else />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
@ -82,13 +52,6 @@ onMounted(async () => {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumbnail {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
max-width: 80%;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cover-wrapper :deep(svg) {
|
.cover-wrapper :deep(svg) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import ChartBlock from '@/components/ChartBlock.vue'
|
||||||
import { useChartsApi } from '@/api/charts.js'
|
import { useChartsApi } from '@/api/charts.js'
|
||||||
import EditLink from '@/views/DetailsView/EditLink.vue'
|
import EditLink from '@/views/DetailsView/EditLink.vue'
|
||||||
import CopyToClipboard from '@/components/CopyToClipboard.vue'
|
import CopyToClipboard from '@/components/CopyToClipboard.vue'
|
||||||
import DetailsViewImages from '@/views/DetailsView/DetailsViewImages.vue'
|
|
||||||
|
|
||||||
const { getMerchDetails, deleteMerch } = useMerchApi()
|
const { getMerchDetails, deleteMerch } = useMerchApi()
|
||||||
const { getDistinctPrices } = useChartsApi()
|
const { getDistinctPrices } = useChartsApi()
|
||||||
|
|
@ -103,13 +102,8 @@ onMounted(() => {
|
||||||
<div v-else-if="error">Error: {{ error }}</div>
|
<div v-else-if="error">Error: {{ error }}</div>
|
||||||
<n-card v-else-if="merchDetails" :title="merchDetails.name">
|
<n-card v-else-if="merchDetails" :title="merchDetails.name">
|
||||||
<n-divider title-placement="left">Main</n-divider>
|
<n-divider title-placement="left">Main</n-divider>
|
||||||
<div class="container-stackable">
|
<p><strong>Uuid:</strong> {{ merchDetails.merch_uuid }}</p>
|
||||||
<div>
|
<p><strong>Name:</strong> {{ merchDetails.name }}</p>
|
||||||
<p><strong>Uuid:</strong> {{ merchDetails.merch_uuid }}</p>
|
|
||||||
<p><strong>Name:</strong> {{ merchDetails.name }}</p>
|
|
||||||
</div>
|
|
||||||
<DetailsViewImages :merch-uuid="merchDetails.merch_uuid"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<n-divider title-placement="left">Prices</n-divider>
|
<n-divider title-placement="left">Prices</n-divider>
|
||||||
<PeriodSelector @days="handleSelectDays" />
|
<PeriodSelector @days="handleSelectDays" />
|
||||||
|
|
@ -137,9 +131,7 @@ onMounted(() => {
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<span class="default-color underline link-like-text" @click="editing.surugaya = true"
|
<span class="default-color underline link-like-text" @click="editing.surugaya = true">Add link</span>
|
||||||
>Add link</span
|
|
||||||
>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -172,9 +164,7 @@ onMounted(() => {
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<span class="default-color underline link-like-text" @click="editing.mandarake = true"
|
<span class="default-color underline link-like-text" @click="editing.mandarake = true">Add link</span>
|
||||||
>Add link</span
|
|
||||||
>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -205,11 +195,4 @@ onMounted(() => {
|
||||||
</n-modal>
|
</n-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
.container-stackable {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 10px;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue