mirror of https://github.com/4xmen/xshop.git
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
425 lines
13 KiB
Vue
425 lines
13 KiB
Vue
6 months ago
|
<template>
|
||
|
<div id="address-input">
|
||
|
|
||
|
<table class="w-100">
|
||
|
<tr v-for="ad in addresses">
|
||
|
<td>
|
||
|
{{ ad.address }}
|
||
|
</td>
|
||
|
<td class="py-1">
|
||
|
<div class="btn btn-outline-danger btn-sm float-end mx-2" @click="removing(ad.id)">
|
||
|
<i class="ri-close-line"></i>
|
||
|
</div>
|
||
|
<div class="btn btn-outline-primary btn-sm float-end" @click="editing(ad)">
|
||
|
<i class="ri-edit-2-line"></i>
|
||
|
</div>
|
||
|
</td>
|
||
|
</tr>
|
||
|
</table>
|
||
|
<button type="button" class="btn btn-primary" @click="adding">
|
||
|
<i class="ri-add-line"></i>
|
||
|
</button>
|
||
|
|
||
|
<div id="address-modal" v-if="modal" @click.self="modal = false">
|
||
|
<div class="card">
|
||
|
<div class="card-header">
|
||
|
Address editor <!-- WIP Translate -->
|
||
|
</div>
|
||
|
<div class="card-body">
|
||
|
<div class="row">
|
||
|
<div class="col-md-6">
|
||
|
<label for="st">
|
||
|
State: <!-- WIP Translate -->
|
||
|
</label>
|
||
|
<select @change="updateState" class="form-control" v-model="state_id" id="st">
|
||
|
<option :data-lat="s.lat" :data-lng="s.lng" :value="s.id" v-for="s in states">
|
||
|
{{ s.name }}
|
||
|
</option>
|
||
|
</select>
|
||
|
</div>
|
||
|
<div class="col-md-6">
|
||
|
<label for="st">
|
||
|
City: <!-- WIP Translate -->
|
||
|
</label>
|
||
|
<select @change="updateCity" class="form-control" v-model="city_id" id="st">
|
||
|
<option :value="c.id" v-for="c in cities"> {{ c.name }}</option>
|
||
|
</select>
|
||
|
</div>
|
||
|
<div class="col-12 my-3">
|
||
|
<div ref="mapContainer" :style="'height: 300px;'+mapStyle"></div>
|
||
|
</div>
|
||
|
<div class="col-12">
|
||
|
<textarea rows="2" class="form-control" placeholder="Address" v-model="address"></textarea>
|
||
|
<!-- WIP Translate -->
|
||
|
</div>
|
||
|
<div class="col-12">
|
||
|
<label for="zip">
|
||
|
Postcode: <!-- WIP Translate -->
|
||
|
</label>
|
||
|
<input type="text" class="form-control" v-model="zip">
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
<div class="card-footer">
|
||
|
<button class="btn btn-primary w-100" type="button" @click="save">
|
||
|
<i class="ri-save-2-line"></i>
|
||
|
</button>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
</template>
|
||
|
|
||
|
<script>
|
||
|
|
||
|
import L from 'leaflet';
|
||
|
import 'leaflet/dist/leaflet.css';
|
||
|
import {useToast} from 'vue-toast-notification';
|
||
|
|
||
|
const $toast = useToast();
|
||
|
|
||
|
export default {
|
||
|
name: "address-input",
|
||
|
components: {},
|
||
|
data: () => {
|
||
|
return {
|
||
|
id: null,
|
||
|
action: 'add',
|
||
|
modal: false,
|
||
|
addresses: [],
|
||
|
states: [],
|
||
|
cities: [],
|
||
|
state_id: null,
|
||
|
city_id: null,
|
||
|
map: null,
|
||
|
marker: null,
|
||
|
zoom: 10,
|
||
|
address: '',
|
||
|
zip: '',
|
||
|
lat: null,
|
||
|
lng: null,
|
||
|
}
|
||
|
},
|
||
|
props: {
|
||
|
listLink: {
|
||
|
type: String,
|
||
|
required: true,
|
||
|
},
|
||
|
addLink: {
|
||
|
type: String,
|
||
|
required: true,
|
||
|
},
|
||
|
updateLink: {
|
||
|
type: String,
|
||
|
required: true,
|
||
|
},
|
||
|
remLink: {
|
||
|
type: String,
|
||
|
required: true,
|
||
|
},
|
||
|
stateLink: {
|
||
|
type: String,
|
||
|
required: true,
|
||
|
},
|
||
|
citiesLink: {
|
||
|
type: String,
|
||
|
required: true,
|
||
|
},
|
||
|
darkMode: {
|
||
|
type: Boolean,
|
||
|
default: false,
|
||
|
}
|
||
|
},
|
||
|
async mounted() {
|
||
|
try {
|
||
|
let res = await axios.get(this.stateLink);
|
||
|
this.states = res.data.data;
|
||
|
// console.log(res.data);
|
||
|
} catch (e) {
|
||
|
$toast.error(e.message);
|
||
|
}
|
||
|
await this.updateList();
|
||
|
// await this.initMap();
|
||
|
// if (this.states[0].lat != null && this.states[0].lng != null){
|
||
|
// this.changeMapCenter(this.states[0].lat,this.states[0].lng)
|
||
|
// }
|
||
|
},
|
||
|
computed: {
|
||
|
mapStyle() {
|
||
|
if (this.darkMode) {
|
||
|
return 'filter: invert(100%) hue-rotate(120deg) brightness(95%) contrast(90%);';
|
||
|
}
|
||
|
return '';
|
||
|
}
|
||
|
},
|
||
|
methods: {
|
||
|
async save() {
|
||
|
let canSave = true;
|
||
|
if (this.state_id == null) {
|
||
|
$toast.error("State is required"); // WIP translate
|
||
|
canSave = false;
|
||
|
}
|
||
|
if (this.city_id == null) {
|
||
|
$toast.error("City is required"); // WIP translate
|
||
|
canSave = false;
|
||
|
}
|
||
|
if (this.address.length < 10) {
|
||
|
$toast.error("Address is required"); // WIP translate
|
||
|
canSave = false;
|
||
|
}
|
||
|
if (this.zip.length < 5) {
|
||
|
$toast.error("Post code is required"); // WIP translate
|
||
|
canSave = false;
|
||
|
}
|
||
|
if (!canSave) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if (this.action == 'add') {
|
||
|
|
||
|
let data = {
|
||
|
address: this.address,
|
||
|
state_id: this.state_id,
|
||
|
city_id: this.city_id,
|
||
|
zip: this.zip,
|
||
|
lat: this.lat,
|
||
|
lng: this.lng
|
||
|
};
|
||
|
try {
|
||
|
let r = await axios.post(this.addLink, data);
|
||
|
if (r.data.OK) {
|
||
|
this.addresses = r.data.list;
|
||
|
$toast.success(r.data.message);
|
||
|
this.modal = false;
|
||
|
}
|
||
|
} catch (e) {
|
||
|
$toast.error('err!' + e.message);
|
||
|
}
|
||
|
|
||
|
|
||
|
} else {
|
||
|
let data = {
|
||
|
address: this.address,
|
||
|
state_id: this.state_id,
|
||
|
city_id: this.city_id,
|
||
|
zip: this.zip,
|
||
|
lat: this.lat,
|
||
|
lng: this.lng
|
||
|
};
|
||
|
try {
|
||
|
const url = this.updateLink + '/' + this.id;
|
||
|
let r = await axios.post(url, data);
|
||
|
if (r.data.OK) {
|
||
|
$toast.success(r.data.message);
|
||
|
await this.updateList();
|
||
|
this.modal = false;
|
||
|
}
|
||
|
} catch (e) {
|
||
|
$toast.error('err!' + e.message);
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
showModal() {
|
||
|
|
||
|
this.modal = true;
|
||
|
setTimeout(() => {
|
||
|
this.initMap();
|
||
|
}, 50);
|
||
|
},
|
||
|
async removing(id) {
|
||
|
if (!confirm('Sure?')) { //WIP: translate
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const url = this.remLink + '/' + id;
|
||
|
try {
|
||
|
let r = await axios.get(url);
|
||
|
if (r.data.OK) {
|
||
|
$toast.success(r.data.message);
|
||
|
this.updateList();
|
||
|
}
|
||
|
} catch(e) {
|
||
|
$toast.error('err!' + e.message);
|
||
|
}
|
||
|
},
|
||
|
async editing(dt) {
|
||
|
this.showModal();
|
||
|
this.action = 'edit';
|
||
|
this.id = dt.id;
|
||
|
this.lat = dt.lat;
|
||
|
this.lng = dt.lng;
|
||
|
this.zip = dt.zip;
|
||
|
this.address = dt.address;
|
||
|
this.state_id = dt.state_id;
|
||
|
await this.updateState();
|
||
|
this.city_id = dt.city_id;
|
||
|
if (this.lng != null && this.lat != null) {
|
||
|
this.zoom = 16;
|
||
|
setTimeout(() => {
|
||
|
this.changeMapCenter(this.lat, this.lng);
|
||
|
this.marker = L.marker({lat:this.lat, lng: this.lng}).addTo(this.map);
|
||
|
}, 100);
|
||
|
}
|
||
|
|
||
|
},
|
||
|
adding() {
|
||
|
this.action = 'add';
|
||
|
this.address = '';
|
||
|
this.zip = '';
|
||
|
this.state_id = null;
|
||
|
this.showModal();
|
||
|
|
||
|
},
|
||
|
initMap() {
|
||
|
this.map = L.map(this.$refs.mapContainer).setView([35.83266000, 50.99155000], 10);
|
||
|
|
||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||
|
attribution: '© openstreetmap',
|
||
|
attributionControl: false,
|
||
|
}).addTo(this.map);
|
||
|
|
||
|
this.map.on('click', this.onMapClick);
|
||
|
this.map.attributionControl.setPrefix('xShop');
|
||
|
|
||
|
},
|
||
|
onMapClick(e) {
|
||
|
if (this.marker) {
|
||
|
this.map.removeLayer(this.marker);
|
||
|
}
|
||
|
|
||
|
this.marker = L.marker(e.latlng).addTo(this.map);
|
||
|
// You can emit the selected location or perform any other desired action here
|
||
|
// console.log('Selected location:', e.latlng);
|
||
|
this.getAddress(e.latlng);
|
||
|
this.lat = e.latlng.lat;
|
||
|
this.lng = e.latlng.lng;
|
||
|
},
|
||
|
changeMapCenter(lat, lng) {
|
||
|
try {
|
||
|
|
||
|
this.map.setView([lat, lng], this.zoom);
|
||
|
} catch (e) {
|
||
|
// console.log(e.message);
|
||
|
setTimeout(() => {
|
||
|
console.log('repeat');
|
||
|
this.changeMapCenter(lat, lng);
|
||
|
}, 10);
|
||
|
}
|
||
|
|
||
|
// Change the map center to [40.7128, -74.0059] (New York City) with zoom level 12
|
||
|
|
||
|
},
|
||
|
async updateList() {
|
||
|
try {
|
||
|
let res = await axios.get(this.listLink);
|
||
|
this.addresses = res.data;
|
||
|
} catch (e) {
|
||
|
$toast.error('err!' + e.message);
|
||
|
}
|
||
|
},
|
||
|
async updateState() {
|
||
|
for (const st of this.states) {
|
||
|
if (st.id == this.state_id) {
|
||
|
// console.log(st);
|
||
|
if (st.lat != null && st.lng != null) {
|
||
|
this.zoom = 10;
|
||
|
this.changeMapCenter(st.lat, st.lng)
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
let res = await axios.get(this.citiesLink + '/' + this.state_id);
|
||
|
this.cities = res.data.data;
|
||
|
} catch (e) {
|
||
|
$toast.error('err!' + e.message);
|
||
|
}
|
||
|
},
|
||
|
async updateCity() {
|
||
|
for (const c of this.cities) {
|
||
|
if (c.id == this.city_id) {
|
||
|
if (c.lat != null && c.lng != null) {
|
||
|
this.zoom = 12;
|
||
|
this.changeMapCenter(c.lat, c.lng)
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
getAddress(latlng) {
|
||
|
const url = `https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${latlng.lat}&lon=${latlng.lng}&addressdetails=1&accept-language=en`;
|
||
|
|
||
|
fetch(url)
|
||
|
.then(response => response.json())
|
||
|
.then(data => {
|
||
|
const address = this.formatAddress(data.address);
|
||
|
this.address = address;
|
||
|
})
|
||
|
.catch(error => {
|
||
|
$toast.error('err!' + error.message);
|
||
|
});
|
||
|
|
||
|
},
|
||
|
formatAddress(addressData) {
|
||
|
|
||
|
let formattedAddress = '';
|
||
|
|
||
|
if (addressData.road) {
|
||
|
formattedAddress += addressData.road;
|
||
|
}
|
||
|
if (addressData.neighbourhood) {
|
||
|
formattedAddress += addressData.neighbourhood;
|
||
|
}
|
||
|
//
|
||
|
// if (addressData.house_number) {
|
||
|
// formattedAddress += ` ${addressData.house_number}`;
|
||
|
// }
|
||
|
//
|
||
|
if (addressData.postcode) {
|
||
|
// formattedAddress += `, ${addressData.postcode}`;
|
||
|
let x = addressData.postcode.split('-');
|
||
|
this.zip = x.join('');
|
||
|
}
|
||
|
|
||
|
if (addressData.city) {
|
||
|
formattedAddress += `, ${addressData.city}`;
|
||
|
}
|
||
|
//
|
||
|
// if (addressData.country) {
|
||
|
// formattedAddress += `, ${addressData.country}`;
|
||
|
// }
|
||
|
|
||
|
return formattedAddress;
|
||
|
},
|
||
|
}
|
||
|
}
|
||
|
</script>
|
||
|
|
||
|
<style scoped>
|
||
|
#address-input {
|
||
|
padding: 0 .75rem 1rem;
|
||
|
}
|
||
|
|
||
|
#address-modal {
|
||
|
position: fixed;
|
||
|
left: 0;
|
||
|
right: 0;
|
||
|
bottom: 0;
|
||
|
top: 0;
|
||
|
backdrop-filter: blur(4px);
|
||
|
background: #ffffff33;
|
||
|
z-index: 9;
|
||
|
display: flex;
|
||
|
align-items: center;
|
||
|
justify-content: center;
|
||
|
}
|
||
|
|
||
|
#address-modal .card {
|
||
|
max-width: 900px;
|
||
|
width: 100%;
|
||
|
}
|
||
|
|
||
|
|
||
|
</style>
|