853 lines
20 KiB
Vue
853 lines
20 KiB
Vue
<template>
|
||
<div class="map-address-selector">
|
||
<!-- 地图容器 -->
|
||
<div id="amap-container" class="map-container">
|
||
<div class="search-tip" v-if="showSearchTip">
|
||
<span>{{ searchTipText }}</span>
|
||
<el-icon class="close-icon" @click="showSearchTip = false"><Close /></el-icon>
|
||
</div>
|
||
|
||
<!-- 地图中心点标记 -->
|
||
<div class="center-marker">
|
||
<el-icon class="location-icon"><Location /></el-icon>
|
||
</div>
|
||
|
||
<!-- 获取我的位置按钮 -->
|
||
<div class="location-btn" @click="getCurrentLocation" title="获取我的位置">
|
||
<el-icon><Aim /></el-icon>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 搜索区域 -->
|
||
<div class="search-section">
|
||
<div class="search-input-wrapper">
|
||
<el-icon class="search-icon"><Search /></el-icon>
|
||
<el-input
|
||
id="search-input"
|
||
v-model="searchKeyword"
|
||
placeholder="搜索地址,更快填写"
|
||
@input="onSearchInput"
|
||
@keyup.enter="searchAddress"
|
||
class="search-input"
|
||
clearable
|
||
/>
|
||
<el-button
|
||
type="text"
|
||
class="smart-paste-btn"
|
||
@click="smartPaste"
|
||
title="智能粘贴"
|
||
>
|
||
<el-icon><MagicStick /></el-icon>
|
||
智能粘贴
|
||
</el-button>
|
||
</div>
|
||
|
||
<!-- 搜索结果列表 -->
|
||
<div v-if="searchResults.length > 0" class="search-results">
|
||
<div
|
||
v-for="(result, index) in searchResults"
|
||
:key="index"
|
||
class="search-result-item"
|
||
@click="selectSearchResult(result)"
|
||
>
|
||
<div class="result-main">
|
||
<div class="result-name">{{ result.name }}</div>
|
||
<div class="result-address">{{ result.district }} {{ result.address }}</div>
|
||
</div>
|
||
<div class="result-distance" v-if="result.distance">
|
||
{{ formatDistance(result.distance) }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 当前选中的地址信息 -->
|
||
<div class="selected-address" v-if="selectedAddress">
|
||
<div class="address-info">
|
||
<div class="address-main">
|
||
<span class="address-name">{{ selectedAddress.name || '选中位置' }}</span>
|
||
<span class="address-detail">{{ selectedAddress.formattedAddress }}</span>
|
||
</div>
|
||
<el-button type="primary" @click="confirmAddress" class="use-btn">
|
||
使用
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 地址详细信息表单 -->
|
||
<div class="address-form-section">
|
||
<!-- 所在地区选择 -->
|
||
<div class="form-item">
|
||
<div class="form-label">
|
||
<el-icon class="required-icon"><Star /></el-icon>
|
||
所在地区
|
||
</div>
|
||
<div class="region-display">
|
||
{{ regionText }}
|
||
<el-switch
|
||
v-model="useDefaultAddress"
|
||
class="default-switch"
|
||
active-text="默认地址"
|
||
inactive-text=""
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 详细地址 -->
|
||
<div class="form-item">
|
||
<div class="form-label">
|
||
<el-icon class="required-icon"><Star /></el-icon>
|
||
详细地址与门牌号
|
||
</div>
|
||
<el-input
|
||
v-model="detailAddress"
|
||
placeholder="如:2号楼1901室"
|
||
class="detail-input"
|
||
/>
|
||
<el-button
|
||
type="text"
|
||
class="import-btn"
|
||
@click="importExistingAddress"
|
||
>
|
||
导入已有地址
|
||
</el-button>
|
||
</div>
|
||
|
||
<!-- 联系人信息 -->
|
||
<div class="form-item">
|
||
<div class="form-label">
|
||
<el-icon class="required-icon"><Star /></el-icon>
|
||
收货人名字
|
||
</div>
|
||
<el-input
|
||
v-model="consigneeName"
|
||
placeholder="收货人姓名"
|
||
class="name-input"
|
||
/>
|
||
</div>
|
||
|
||
<div class="form-item">
|
||
<div class="form-label">
|
||
<el-icon class="required-icon"><Star /></el-icon>
|
||
手机号
|
||
</div>
|
||
<div class="phone-input-wrapper">
|
||
<el-select v-model="phonePrefix" class="phone-prefix">
|
||
<el-option label="+86" value="+86" />
|
||
</el-select>
|
||
<el-input
|
||
v-model="phoneNumber"
|
||
placeholder="手机号"
|
||
class="phone-input"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 地址标签 -->
|
||
<div class="form-item">
|
||
<div class="form-label">地址标签</div>
|
||
<div class="address-tags">
|
||
<div
|
||
v-for="tag in addressTags"
|
||
:key="tag.value"
|
||
class="tag-item"
|
||
:class="{ active: selectedTag === tag.value }"
|
||
@click="selectTag(tag.value)"
|
||
>
|
||
<el-icon>
|
||
<component :is="tag.icon" />
|
||
</el-icon>
|
||
<span>{{ tag.label }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 保存按钮 -->
|
||
<div class="save-section">
|
||
<el-button
|
||
type="primary"
|
||
class="save-btn"
|
||
@click="saveAddress"
|
||
:loading="saving"
|
||
>
|
||
保存地址
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { ref, reactive, onMounted, onUnmounted, computed, watch } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import {
|
||
Close, Location, Aim, Search, MagicStick, Star,
|
||
House, OfficeBuilding, School, UserFilled, User, Setting
|
||
} from '@element-plus/icons-vue'
|
||
|
||
export default {
|
||
name: 'MapAddressSelector',
|
||
components: {
|
||
Close, Location, Aim, Search, MagicStick, Star,
|
||
House, OfficeBuilding, School, UserFilled, User, Setting
|
||
},
|
||
props: {
|
||
modelValue: {
|
||
type: Object,
|
||
default: () => ({})
|
||
}
|
||
},
|
||
emits: ['update:modelValue', 'save', 'cancel'],
|
||
setup(props, { emit }) {
|
||
const map = ref(null);
|
||
const marker = ref(null);
|
||
const geolocation = ref(null);
|
||
const geocoder = ref(null);
|
||
|
||
const searchKeyword = ref('');
|
||
const searchResults = ref([]);
|
||
const selectedAddress = ref(null);
|
||
const showSearchTip = ref(true);
|
||
const saving = ref(false);
|
||
|
||
// 表单数据
|
||
const detailAddress = ref('');
|
||
const consigneeName = ref('');
|
||
const phoneNumber = ref('');
|
||
const phonePrefix = ref('+86');
|
||
const useDefaultAddress = ref(false);
|
||
const selectedTag = ref('家');
|
||
|
||
// 地址标签选项
|
||
const addressTags = ref([
|
||
{ label: '家', value: '家', icon: 'House' },
|
||
{ label: '公司', value: '公司', icon: 'OfficeBuilding' },
|
||
{ label: '学校', value: '学校', icon: 'School' },
|
||
{ label: '父母', value: '父母', icon: 'UserFilled' },
|
||
{ label: '朋友', value: '朋友', icon: 'User' },
|
||
{ label: '自定义', value: '自定义', icon: 'Setting' }
|
||
]);
|
||
|
||
const searchTipText = computed(() => {
|
||
return '找不到地址?试试搜索吧';
|
||
});
|
||
|
||
const regionText = computed(() => {
|
||
if (selectedAddress.value) {
|
||
const addr = selectedAddress.value;
|
||
return `${addr.province || ''} ${addr.city || ''} ${addr.district || ''}`.trim();
|
||
}
|
||
return '请在地图上选择位置';
|
||
});
|
||
|
||
// 初始化地图
|
||
const initMap = () => {
|
||
if (!window.AMap) {
|
||
ElMessage.error('地图API加载失败,请刷新页面重试');
|
||
return;
|
||
}
|
||
|
||
map.value = new AMap.Map('amap-container', {
|
||
zoom: 15,
|
||
center: [116.397428, 39.90923],
|
||
viewMode: '3D',
|
||
showLabel: true,
|
||
mapStyle: 'amap://styles/normal'
|
||
});
|
||
|
||
// 初始化地理编码
|
||
geocoder.value = new AMap.Geocoder({
|
||
extensions: 'all'
|
||
});
|
||
|
||
// 初始化定位
|
||
geolocation.value = new AMap.Geolocation({
|
||
enableHighAccuracy: true,
|
||
timeout: 10000,
|
||
maximumAge: 0,
|
||
convert: true,
|
||
showButton: false,
|
||
buttonPosition: 'LB',
|
||
showMarker: false,
|
||
showCircle: false,
|
||
panToLocation: true,
|
||
zoomToAccuracy: true
|
||
});
|
||
|
||
// 监听地图移动
|
||
map.value.on('moveend', onMapMoveEnd);
|
||
map.value.on('zoomend', onMapMoveEnd);
|
||
|
||
// 自动获取当前位置
|
||
getCurrentLocation();
|
||
};
|
||
|
||
// 地图移动结束事件
|
||
const onMapMoveEnd = () => {
|
||
const center = map.value.getCenter();
|
||
geocodeByLocation(center);
|
||
};
|
||
|
||
// 根据坐标获取地址信息
|
||
const geocodeByLocation = (location) => {
|
||
if (!geocoder.value) return;
|
||
|
||
geocoder.value.getAddress([location.lng, location.lat], (status, result) => {
|
||
if (status === 'complete' && result.regeocode) {
|
||
const regeocode = result.regeocode;
|
||
selectedAddress.value = {
|
||
lng: location.lng,
|
||
lat: location.lat,
|
||
formattedAddress: regeocode.formattedAddress,
|
||
province: regeocode.addressComponent.province,
|
||
city: regeocode.addressComponent.city,
|
||
district: regeocode.addressComponent.district,
|
||
township: regeocode.addressComponent.township,
|
||
name: regeocode.pois.length > 0 ? regeocode.pois[0].name : ''
|
||
};
|
||
}
|
||
});
|
||
};
|
||
|
||
// 获取当前位置
|
||
const getCurrentLocation = () => {
|
||
if (!geolocation.value) return;
|
||
|
||
geolocation.value.getCurrentPosition((status, result) => {
|
||
if (status === 'complete') {
|
||
const position = result.position;
|
||
map.value.setCenter([position.lng, position.lat]);
|
||
map.value.setZoom(16);
|
||
geocodeByLocation(position);
|
||
ElMessage.success('定位成功');
|
||
} else {
|
||
ElMessage.warning('定位失败,请手动选择位置');
|
||
}
|
||
});
|
||
};
|
||
|
||
// 搜索输入事件
|
||
const onSearchInput = () => {
|
||
if (searchKeyword.value.trim()) {
|
||
searchAddress();
|
||
} else {
|
||
searchResults.value = [];
|
||
}
|
||
};
|
||
|
||
// 搜索地址 - 调用后端API
|
||
const searchAddress = () => {
|
||
if (!searchKeyword.value.trim()) {
|
||
searchResults.value = [];
|
||
return;
|
||
}
|
||
|
||
const token = localStorage.getItem('token');
|
||
if (!token) {
|
||
ElMessage.error('请先登录');
|
||
return;
|
||
}
|
||
|
||
// 调用后端API进行搜索
|
||
fetch('/api/address/search-poi', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': 'Bearer ' + token
|
||
},
|
||
body: JSON.stringify({
|
||
keyword: searchKeyword.value,
|
||
city: '全国'
|
||
})
|
||
})
|
||
.then(response => {
|
||
if (response.status === 401) {
|
||
ElMessage.error('登录已过期,请重新登录');
|
||
localStorage.removeItem('token');
|
||
window.location.href = '/login';
|
||
return;
|
||
}
|
||
return response.json();
|
||
})
|
||
.then(result => {
|
||
console.log('🔍 前端收到后端响应:', result);
|
||
if (result && result.code === 200 && result.data) {
|
||
console.log('✅ 响应数据正常:', result.data);
|
||
searchResults.value = result.data.map(poi => ({
|
||
id: poi.id,
|
||
name: poi.name,
|
||
address: poi.address,
|
||
district: poi.district,
|
||
location: { lng: poi.lng, lat: poi.lat },
|
||
distance: 0
|
||
}));
|
||
console.log("🎯 处理后的搜索结果:", searchResults.value);
|
||
} else {
|
||
searchResults.value = [];
|
||
if (result && result.message && result.code !== 200) {
|
||
console.error('搜索失败:', result.message);
|
||
}
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('搜索错误:', error);
|
||
searchResults.value = [];
|
||
ElMessage.error('搜索失败,请稍后重试');
|
||
});
|
||
};
|
||
|
||
// 选择搜索结果
|
||
const selectSearchResult = (result) => {
|
||
const location = result.location;
|
||
map.value.setCenter([location.lng, location.lat]);
|
||
map.value.setZoom(16);
|
||
|
||
selectedAddress.value = {
|
||
lng: location.lng,
|
||
lat: location.lat,
|
||
formattedAddress: result.district + result.address,
|
||
name: result.name
|
||
};
|
||
|
||
searchResults.value = [];
|
||
searchKeyword.value = result.name;
|
||
|
||
// 重新地理编码获取详细信息
|
||
geocodeByLocation(location);
|
||
};
|
||
|
||
// 格式化距离
|
||
const formatDistance = (distance) => {
|
||
if (distance < 1000) {
|
||
return Math.round(distance) + 'm';
|
||
} else {
|
||
return (distance / 1000).toFixed(1) + 'km';
|
||
}
|
||
};
|
||
|
||
// 确认地址
|
||
const confirmAddress = () => {
|
||
if (selectedAddress.value) {
|
||
if (selectedAddress.value.name && !detailAddress.value) {
|
||
detailAddress.value = selectedAddress.value.name;
|
||
}
|
||
}
|
||
};
|
||
|
||
// 智能粘贴
|
||
const smartPaste = async () => {
|
||
try {
|
||
const text = await navigator.clipboard.readText();
|
||
if (text) {
|
||
searchKeyword.value = text;
|
||
searchAddress();
|
||
}
|
||
} catch (error) {
|
||
ElMessage.warning('无法读取剪贴板内容');
|
||
}
|
||
};
|
||
|
||
// 导入已有地址
|
||
const importExistingAddress = () => {
|
||
emit('import-existing');
|
||
};
|
||
|
||
// 选择地址标签
|
||
const selectTag = (tag) => {
|
||
selectedTag.value = tag;
|
||
};
|
||
|
||
// 保存地址
|
||
const saveAddress = () => {
|
||
if (!selectedAddress.value) {
|
||
ElMessage.error('请在地图上选择地址');
|
||
return;
|
||
}
|
||
|
||
if (!consigneeName.value.trim()) {
|
||
ElMessage.error('请输入收货人姓名');
|
||
return;
|
||
}
|
||
|
||
if (!phoneNumber.value.trim()) {
|
||
ElMessage.error('请输入手机号');
|
||
return;
|
||
}
|
||
|
||
if (!/^1[3-9]\d{9}$/.test(phoneNumber.value)) {
|
||
ElMessage.error('请输入正确的手机号');
|
||
return;
|
||
}
|
||
|
||
if (!detailAddress.value.trim()) {
|
||
ElMessage.error('请输入详细地址');
|
||
return;
|
||
}
|
||
|
||
const addressData = {
|
||
consignee: consigneeName.value,
|
||
phone: phoneNumber.value,
|
||
province: selectedAddress.value.province,
|
||
city: selectedAddress.value.city,
|
||
district: selectedAddress.value.district,
|
||
address: detailAddress.value,
|
||
isDefault: useDefaultAddress.value,
|
||
tag: selectedTag.value,
|
||
lng: selectedAddress.value.lng,
|
||
lat: selectedAddress.value.lat,
|
||
formattedAddress: selectedAddress.value.formattedAddress
|
||
};
|
||
|
||
emit('save', addressData);
|
||
};
|
||
|
||
// 初始化表单数据
|
||
const initFormData = () => {
|
||
if (props.modelValue) {
|
||
const data = props.modelValue;
|
||
consigneeName.value = data.consignee || '';
|
||
phoneNumber.value = data.phone || '';
|
||
detailAddress.value = data.address || '';
|
||
useDefaultAddress.value = data.isDefault || false;
|
||
selectedTag.value = data.tag || '家';
|
||
|
||
if (data.lng && data.lat && map.value) {
|
||
map.value.setCenter([data.lng, data.lat]);
|
||
selectedAddress.value = {
|
||
lng: data.lng,
|
||
lat: data.lat,
|
||
formattedAddress: `${data.province} ${data.city} ${data.district} ${data.address}`,
|
||
province: data.province,
|
||
city: data.city,
|
||
district: data.district
|
||
};
|
||
}
|
||
}
|
||
};
|
||
|
||
watch(() => props.modelValue, initFormData, { immediate: true });
|
||
|
||
onMounted(() => {
|
||
setTimeout(initMap, 100);
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
if (map.value) {
|
||
map.value.destroy();
|
||
}
|
||
});
|
||
|
||
return {
|
||
searchKeyword,
|
||
searchResults,
|
||
selectedAddress,
|
||
showSearchTip,
|
||
searchTipText,
|
||
saving,
|
||
detailAddress,
|
||
consigneeName,
|
||
phoneNumber,
|
||
phonePrefix,
|
||
useDefaultAddress,
|
||
selectedTag,
|
||
addressTags,
|
||
regionText,
|
||
getCurrentLocation,
|
||
onSearchInput,
|
||
searchAddress,
|
||
selectSearchResult,
|
||
formatDistance,
|
||
confirmAddress,
|
||
smartPaste,
|
||
importExistingAddress,
|
||
selectTag,
|
||
saveAddress
|
||
};
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.map-address-selector {
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
.map-container {
|
||
height: 200px;
|
||
position: relative;
|
||
background: #e8e8e8;
|
||
}
|
||
|
||
.search-tip {
|
||
position: absolute;
|
||
top: 20px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
background: rgba(0, 0, 0, 0.7);
|
||
color: white;
|
||
padding: 8px 16px;
|
||
border-radius: 20px;
|
||
font-size: 14px;
|
||
z-index: 1000;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.close-icon {
|
||
cursor: pointer;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.center-marker {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -100%);
|
||
z-index: 1000;
|
||
color: #ff4757;
|
||
font-size: 24px;
|
||
filter: drop-shadow(0 0 3px rgba(0, 0, 0, 0.3));
|
||
}
|
||
|
||
.location-btn {
|
||
position: absolute;
|
||
bottom: 20px;
|
||
right: 20px;
|
||
width: 40px;
|
||
height: 40px;
|
||
background: white;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||
cursor: pointer;
|
||
z-index: 1000;
|
||
color: #4CAF50;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.location-btn:hover {
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
.search-section {
|
||
background: white;
|
||
padding: 16px;
|
||
}
|
||
|
||
.search-input-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
background: #f8f9fa;
|
||
border-radius: 8px;
|
||
padding: 8px 12px;
|
||
gap: 8px;
|
||
}
|
||
|
||
.search-icon {
|
||
color: #666;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.search-input {
|
||
flex: 1;
|
||
border: none;
|
||
background: transparent;
|
||
}
|
||
|
||
.search-input :deep(.el-input__wrapper) {
|
||
background: transparent;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.smart-paste-btn {
|
||
color: #4CAF50;
|
||
font-size: 12px;
|
||
padding: 0;
|
||
margin: 0;
|
||
}
|
||
|
||
.search-results {
|
||
margin-top: 12px;
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.search-result-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 12px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.search-result-item:hover {
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
.result-main {
|
||
flex: 1;
|
||
}
|
||
|
||
.result-name {
|
||
font-weight: 500;
|
||
color: #333;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.result-address {
|
||
font-size: 12px;
|
||
color: #666;
|
||
}
|
||
|
||
.result-distance {
|
||
font-size: 12px;
|
||
color: #999;
|
||
}
|
||
|
||
.selected-address {
|
||
background: white;
|
||
border-top: 1px solid #e8e8e8;
|
||
padding: 16px;
|
||
}
|
||
|
||
.address-info {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.address-main {
|
||
flex: 1;
|
||
}
|
||
|
||
.address-name {
|
||
font-weight: 500;
|
||
color: #333;
|
||
display: block;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.address-detail {
|
||
font-size: 12px;
|
||
color: #666;
|
||
}
|
||
|
||
.use-btn {
|
||
background: #ff4757;
|
||
border: none;
|
||
border-radius: 20px;
|
||
padding: 8px 20px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.address-form-section {
|
||
flex: 1;
|
||
background: white;
|
||
padding: 16px;
|
||
overflow-y: auto;
|
||
max-height: calc(85vh - 200px - 200px);
|
||
}
|
||
|
||
.form-item {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.form-label {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 14px;
|
||
color: #333;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.required-icon {
|
||
color: #ff4757;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.region-display {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
color: #666;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.detail-input,
|
||
.name-input {
|
||
width: 100%;
|
||
}
|
||
|
||
.import-btn {
|
||
color: #4CAF50;
|
||
font-size: 14px;
|
||
margin-top: 8px;
|
||
padding: 0;
|
||
}
|
||
|
||
.phone-input-wrapper {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.phone-prefix {
|
||
width: 80px;
|
||
}
|
||
|
||
.phone-input {
|
||
flex: 1;
|
||
}
|
||
|
||
.address-tags {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 12px;
|
||
}
|
||
|
||
.tag-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 12px;
|
||
border: 1px solid #e8e8e8;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.tag-item:hover {
|
||
border-color: #4CAF50;
|
||
}
|
||
|
||
.tag-item.active {
|
||
border-color: #4CAF50;
|
||
background: #f8fff8;
|
||
color: #4CAF50;
|
||
}
|
||
|
||
.save-section {
|
||
background: white;
|
||
padding: 12px 16px;
|
||
border-top: 1px solid #e8e8e8;
|
||
}
|
||
|
||
.save-btn {
|
||
width: 100%;
|
||
height: 40px;
|
||
background: #ff6b35;
|
||
border: none;
|
||
border-radius: 22px;
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
}
|
||
</style>
|