#51 feat: logo del sitio editable desde el panel de administración
This commit is contained in:
@@ -7,12 +7,7 @@
|
|||||||
<div class="glow-bubble bubble-red"></div>
|
<div class="glow-bubble bubble-red"></div>
|
||||||
|
|
||||||
<div class="login-card">
|
<div class="login-card">
|
||||||
<v-img
|
<SiteLogo max-width="140" class="mx-auto mb-4" />
|
||||||
:src="logo"
|
|
||||||
alt="Don Confiao"
|
|
||||||
max-width="140"
|
|
||||||
class="mx-auto mb-4"
|
|
||||||
/>
|
|
||||||
<h1 class="text-h5 text-sm-h4 font-weight-bold text-center mb-1">
|
<h1 class="text-h5 text-sm-h4 font-weight-bold text-center mb-1">
|
||||||
Iniciar Sesión
|
Iniciar Sesión
|
||||||
</h1>
|
</h1>
|
||||||
@@ -74,7 +69,7 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import AuthService from '@/services/auth'
|
import AuthService from '@/services/auth'
|
||||||
import logo from '@/assets/logo_colorful.png'
|
import SiteLogo from '@/components/SiteLogo.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
|||||||
39
src/components/SiteLogo.vue
Normal file
39
src/components/SiteLogo.vue
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<template>
|
||||||
|
<v-img
|
||||||
|
v-if="logoUrl && !broken"
|
||||||
|
:src="logoUrl"
|
||||||
|
:alt="alt"
|
||||||
|
:max-width="maxWidth"
|
||||||
|
@error="broken = true"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, inject, onMounted, ref } from 'vue';
|
||||||
|
import { useSettingsStore } from '@/stores/settings';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
maxWidth: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 180,
|
||||||
|
},
|
||||||
|
alt: {
|
||||||
|
type: String,
|
||||||
|
default: 'Don Confiao',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
const api = inject('api');
|
||||||
|
const broken = ref(false);
|
||||||
|
|
||||||
|
const logoUrl = computed(() => settingsStore.logo);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
await settingsStore.fetchSettings(api);
|
||||||
|
} catch (e) {
|
||||||
|
broken.value = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -61,10 +61,12 @@
|
|||||||
import { ref, computed, inject, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
import { ref, computed, inject, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||||
import L from 'leaflet';
|
import L from 'leaflet';
|
||||||
import 'leaflet/dist/leaflet.css';
|
import 'leaflet/dist/leaflet.css';
|
||||||
|
import { useSettingsStore } from '@/stores/settings';
|
||||||
|
|
||||||
const api = inject('api');
|
const api = inject('api');
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
|
||||||
const settings = ref(null);
|
const settings = computed(() => settingsStore.settings);
|
||||||
const mapEl = ref(null);
|
const mapEl = ref(null);
|
||||||
|
|
||||||
let map = null;
|
let map = null;
|
||||||
@@ -125,7 +127,7 @@ function handleResize() {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
settings.value = await api.getStoreSettings();
|
await settingsStore.fetchSettings(api);
|
||||||
await nextTick();
|
await nextTick();
|
||||||
initMap();
|
initMap();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -60,6 +60,39 @@
|
|||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
|
<v-divider class="my-4" />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="text-h6 mb-2">Logo del sitio</h3>
|
||||||
|
<v-img
|
||||||
|
v-if="currentLogo"
|
||||||
|
:src="currentLogo"
|
||||||
|
alt="Logo actual"
|
||||||
|
max-width="180"
|
||||||
|
class="mb-3"
|
||||||
|
/>
|
||||||
|
<v-file-input
|
||||||
|
v-model="logoFile"
|
||||||
|
label="Seleccionar nueva imagen del logo"
|
||||||
|
accept="image/*"
|
||||||
|
prepend-icon="mdi-image"
|
||||||
|
variant="outlined"
|
||||||
|
class="mb-3"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
v-if="currentLogo"
|
||||||
|
variant="tonal"
|
||||||
|
color="error"
|
||||||
|
prepend-icon="mdi-image-off"
|
||||||
|
:loading="removingLogo"
|
||||||
|
:disabled="removingLogo || submitting"
|
||||||
|
@click="removeLogo"
|
||||||
|
>
|
||||||
|
Quitar logo actual
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
</v-form>
|
</v-form>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
@@ -82,11 +115,16 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, inject, onMounted } from "vue";
|
import { ref, inject, onMounted } from "vue";
|
||||||
|
import { useSettingsStore } from "@/stores/settings";
|
||||||
|
|
||||||
const api = inject("api");
|
const api = inject("api");
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
|
||||||
const formRef = ref(null);
|
const formRef = ref(null);
|
||||||
const submitting = ref(false);
|
const submitting = ref(false);
|
||||||
|
const removingLogo = ref(false);
|
||||||
|
const logoFile = ref(null);
|
||||||
|
const currentLogo = ref(null);
|
||||||
|
|
||||||
const form = ref({
|
const form = ref({
|
||||||
address: "",
|
address: "",
|
||||||
@@ -99,11 +137,13 @@ const snackbar = ref({ show: false, message: "", color: "success" });
|
|||||||
async function loadSettings() {
|
async function loadSettings() {
|
||||||
try {
|
try {
|
||||||
const data = await api.getStoreSettings();
|
const data = await api.getStoreSettings();
|
||||||
|
settingsStore.setSettings(data);
|
||||||
form.value = {
|
form.value = {
|
||||||
address: data.address || "",
|
address: data.address || "",
|
||||||
latitude: data.latitude ?? null,
|
latitude: data.latitude ?? null,
|
||||||
longitude: data.longitude ?? null,
|
longitude: data.longitude ?? null,
|
||||||
};
|
};
|
||||||
|
currentLogo.value = data.logo || null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error al cargar la información de la tienda:", error);
|
console.error("Error al cargar la información de la tienda:", error);
|
||||||
showSnackbar("Error al cargar la información de la tienda", "error");
|
showSnackbar("Error al cargar la información de la tienda", "error");
|
||||||
@@ -119,11 +159,26 @@ async function submitForm() {
|
|||||||
|
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
try {
|
try {
|
||||||
|
if (logoFile.value) {
|
||||||
|
const payload = new FormData();
|
||||||
|
payload.append("address", form.value.address);
|
||||||
|
if (form.value.latitude !== "" && form.value.latitude != null) {
|
||||||
|
payload.append("latitude", toNumberOrNull(form.value.latitude));
|
||||||
|
}
|
||||||
|
if (form.value.longitude !== "" && form.value.longitude != null) {
|
||||||
|
payload.append("longitude", toNumberOrNull(form.value.longitude));
|
||||||
|
}
|
||||||
|
payload.append("logo", logoFile.value);
|
||||||
|
await api.updateStoreSettings(payload);
|
||||||
|
} else {
|
||||||
await api.updateStoreSettings({
|
await api.updateStoreSettings({
|
||||||
address: form.value.address,
|
address: form.value.address,
|
||||||
latitude: toNumberOrNull(form.value.latitude),
|
latitude: toNumberOrNull(form.value.latitude),
|
||||||
longitude: toNumberOrNull(form.value.longitude),
|
longitude: toNumberOrNull(form.value.longitude),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
logoFile.value = null;
|
||||||
|
await refreshLogo();
|
||||||
showSnackbar("Información de la tienda actualizada", "success");
|
showSnackbar("Información de la tienda actualizada", "success");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error al guardar la información de la tienda:", error);
|
console.error("Error al guardar la información de la tienda:", error);
|
||||||
@@ -133,6 +188,27 @@ async function submitForm() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function removeLogo() {
|
||||||
|
removingLogo.value = true;
|
||||||
|
try {
|
||||||
|
await api.updateStoreSettings({ logo: null });
|
||||||
|
logoFile.value = null;
|
||||||
|
await refreshLogo();
|
||||||
|
showSnackbar("Logo eliminado", "success");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error al quitar el logo:", error);
|
||||||
|
showSnackbar("Error al quitar el logo", "error");
|
||||||
|
} finally {
|
||||||
|
removingLogo.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshLogo() {
|
||||||
|
const data = await api.getStoreSettings();
|
||||||
|
settingsStore.setSettings(data);
|
||||||
|
currentLogo.value = data.logo || null;
|
||||||
|
}
|
||||||
|
|
||||||
function toNumberOrNull(value) {
|
function toNumberOrNull(value) {
|
||||||
if (value === null || value === undefined || value === "") return null;
|
if (value === null || value === undefined || value === "") return null;
|
||||||
const number = Number(value);
|
const number = Number(value);
|
||||||
|
|||||||
@@ -8,12 +8,7 @@
|
|||||||
<div class="glow-bubble bubble-yellow"></div>
|
<div class="glow-bubble bubble-yellow"></div>
|
||||||
<div class="glow-bubble bubble-red"></div>
|
<div class="glow-bubble bubble-red"></div>
|
||||||
<div class="hero-content">
|
<div class="hero-content">
|
||||||
<v-img
|
<SiteLogo max-width="180" class="mx-auto mb-4" />
|
||||||
:src="logo"
|
|
||||||
alt="Don Confiao"
|
|
||||||
max-width="180"
|
|
||||||
class="mx-auto mb-4"
|
|
||||||
/>
|
|
||||||
<h1 class="text-h4 font-weight-bold mb-2">Don Confiao te atiende</h1>
|
<h1 class="text-h4 font-weight-bold mb-2">Don Confiao te atiende</h1>
|
||||||
<p class="text-subtitle-1 font-italic font-weight-bold">
|
<p class="text-subtitle-1 font-italic font-weight-bold">
|
||||||
Economía solidaria, mercado justo, alimentación sana
|
Economía solidaria, mercado justo, alimentación sana
|
||||||
@@ -122,8 +117,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import ResaltedText from "@/components/ResaltedText.vue";
|
import ResaltedText from "@/components/ResaltedText.vue";
|
||||||
import StoreLocation from "@/components/StoreLocation.vue";
|
import StoreLocation from "@/components/StoreLocation.vue";
|
||||||
|
import SiteLogo from "@/components/SiteLogo.vue";
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
import logo from "@/assets/logo_colorful.png";
|
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -162,7 +162,9 @@ class DjangoApi {
|
|||||||
|
|
||||||
updateStoreSettings(data) {
|
updateStoreSettings(data) {
|
||||||
const url = this.base + "/don_confiao/api/store_settings";
|
const url = this.base + "/don_confiao/api/store_settings";
|
||||||
return this.patchRequest(url, data);
|
return http.patch(url, data, {
|
||||||
|
headers: { 'Content-Type': undefined },
|
||||||
|
}).then((r) => r.data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
26
src/stores/settings.js
Normal file
26
src/stores/settings.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useSettingsStore = defineStore('settings', {
|
||||||
|
state: () => ({
|
||||||
|
settings: null,
|
||||||
|
loaded: false
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
logo: (state) => state.settings?.logo || null,
|
||||||
|
address: (state) => state.settings?.address || '',
|
||||||
|
latitude: (state) => state.settings?.latitude,
|
||||||
|
longitude: (state) => state.settings?.longitude
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async fetchSettings(api) {
|
||||||
|
if (this.loaded) return this.settings
|
||||||
|
this.settings = await api.getStoreSettings()
|
||||||
|
this.loaded = true
|
||||||
|
return this.settings
|
||||||
|
},
|
||||||
|
setSettings(settings) {
|
||||||
|
this.settings = settings
|
||||||
|
this.loaded = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user