Files
CosScene/clients/pages/spot/edit.vue
T
2026-05-09 16:40:29 +08:00

521 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { ref, onMounted, onUnmounted } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { getSpotDetail, updateSpot } from "@/api/spot";
import { getTags } from "@/api/tag";
import { extractList } from "@/utils/request";
import { checkLogin } from "@/utils/auth";
const form = ref({
title: "",
city: "",
description: "",
transport: "",
best_time: "",
difficulty: "",
is_free: true,
price_type: "fixed",
price_min: "",
price_max: "",
longitude: "",
latitude: "",
});
const submitting = ref(false);
const loading = ref(true);
const locationName = ref("");
const allTags = ref([]);
const selectedTagIds = ref([]);
let spotId = null;
const fetchTags = async () => {
try {
const res = await getTags();
allTags.value = extractList(res);
} catch (e) {
console.error(e);
}
};
const toggleTag = (tagId) => {
const idx = selectedTagIds.value.indexOf(tagId);
if (idx >= 0) {
selectedTagIds.value.splice(idx, 1);
} else {
if (selectedTagIds.value.length >= 5) {
uni.showToast({ title: "最多选择5个标签", icon: "none" });
return;
}
selectedTagIds.value.push(tagId);
}
};
const onLocationPicked = (data) => {
form.value.longitude = data.longitude;
form.value.latitude = data.latitude;
form.value.city = data.city || "";
locationName.value = data.name || "已选择位置";
};
onMounted(() => {
uni.$on("locationPicked", onLocationPicked);
});
onUnmounted(() => {
uni.$off("locationPicked", onLocationPicked);
});
const chooseLocation = () => {
uni.navigateTo({ url: "/pages/spot/pick-location" });
};
onLoad(async (query) => {
if (!checkLogin()) return;
if (!query.id) return;
spotId = Number(query.id);
await fetchTags();
try {
const data = await getSpotDetail(spotId);
form.value.title = data.title || "";
form.value.city = data.city || "";
form.value.description = data.description || "";
form.value.transport = data.transport || "";
form.value.best_time = data.best_time || "";
form.value.difficulty = data.difficulty || "";
form.value.is_free = data.is_free !== false;
form.value.longitude = data.longitude || "";
form.value.latitude = data.latitude || "";
if (!form.value.is_free) {
const pMin = data.price_min;
const pMax = data.price_max;
form.value.price_min = pMin != null ? String(pMin) : "";
form.value.price_max = pMax != null ? String(pMax) : "";
form.value.price_type = (pMin != null && pMax != null && pMin !== pMax) ? "range" : "fixed";
}
if (data.longitude && data.latitude) {
locationName.value = data.city || "已选择位置";
}
if (data.tags && data.tags.length) {
selectedTagIds.value = data.tags.map((t) => t.id);
}
} catch (e) {
console.error(e);
uni.showToast({ title: "加载失败", icon: "none" });
} finally {
loading.value = false;
}
});
const validate = () => {
if (!form.value.title.trim()) {
uni.showToast({ title: "请输入地点名称", icon: "none" });
return false;
}
if (!form.value.city) {
uni.showToast({ title: "请先通过定位获取所在城市", icon: "none" });
return false;
}
return true;
};
const handleSubmit = async () => {
if (!validate() || submitting.value) return;
submitting.value = true;
try {
const data = {
title: form.value.title.trim(),
city: form.value.city.trim(),
description: form.value.description.trim() || null,
transport: form.value.transport.trim() || null,
best_time: form.value.best_time.trim() || null,
difficulty: form.value.difficulty.trim() || null,
is_free: form.value.is_free,
price_min: form.value.is_free ? null : (parseFloat(form.value.price_min) || null),
price_max: form.value.is_free
? null
: form.value.price_type === "range"
? (parseFloat(form.value.price_max) || null)
: (parseFloat(form.value.price_min) || null),
};
if (form.value.longitude && form.value.latitude) {
data.longitude = Number(form.value.longitude);
data.latitude = Number(form.value.latitude);
}
data.tag_ids = selectedTagIds.value;
await updateSpot(spotId, data);
uni.showToast({ title: "保存成功", icon: "success" });
setTimeout(() => uni.navigateBack(), 800);
} catch (e) {
console.error(e);
} finally {
submitting.value = false;
}
};
</script>
<template>
<view class="edit-page">
<view v-if="loading" class="loading-state">
<text>加载中...</text>
</view>
<template v-else>
<view class="form-card">
<view class="field">
<text class="label">地点名称 *</text>
<input
v-model="form.title"
class="input"
placeholder="请输入取景地名称"
placeholder-class="placeholder"
/>
</view>
<view class="field">
<text class="label">地点介绍</text>
<textarea
v-model="form.description"
class="textarea"
placeholder="描述这个取景地的特色..."
placeholder-class="placeholder"
maxlength="500"
:auto-height="false"
/>
</view>
<view class="field">
<text class="label">交通方式</text>
<textarea
v-model="form.transport"
class="textarea textarea-sm"
placeholder="如何到达这个地点?"
placeholder-class="placeholder"
maxlength="300"
:auto-height="false"
/>
</view>
<view class="field">
<text class="label">最佳拍摄时间</text>
<input
v-model="form.best_time"
class="input"
placeholder="例如:下午4-6点,黄金时段"
placeholder-class="placeholder"
/>
</view>
<view class="field">
<text class="label">路径难度说明</text>
<textarea
v-model="form.difficulty"
class="textarea textarea-sm"
placeholder="路况如何?是否适合携带大量器材?"
placeholder-class="placeholder"
maxlength="300"
:auto-height="false"
/>
</view>
<view class="field">
<text class="label">是否收费</text>
<view class="free-toggle">
<view
class="toggle-btn"
:class="{ active: form.is_free }"
@tap="form.is_free = true; form.price_min = ''; form.price_max = ''"
>
<text class="toggle-text">免费</text>
</view>
<view
class="toggle-btn"
:class="{ active: !form.is_free }"
@tap="form.is_free = false"
>
<text class="toggle-text">收费</text>
</view>
</view>
</view>
<view class="field" v-if="!form.is_free">
<text class="label">价格类型</text>
<view class="free-toggle">
<view
class="toggle-btn"
:class="{ active: form.price_type === 'fixed' }"
@tap="form.price_type = 'fixed'; form.price_max = ''"
>
<text class="toggle-text">固定价格</text>
</view>
<view
class="toggle-btn"
:class="{ active: form.price_type === 'range' }"
@tap="form.price_type = 'range'"
>
<text class="toggle-text">区间价格</text>
</view>
</view>
</view>
<view class="field" v-if="!form.is_free && form.price_type === 'fixed'">
<text class="label">价格</text>
<input
v-model="form.price_min"
class="input"
type="digit"
placeholder="请输入价格,例如:50"
placeholder-class="placeholder"
/>
</view>
<view class="field" v-if="!form.is_free && form.price_type === 'range'">
<text class="label">价格区间</text>
<view class="price-range">
<input
v-model="form.price_min"
class="input price-input"
type="digit"
placeholder="最低价"
placeholder-class="placeholder"
/>
<text class="price-sep">~</text>
<input
v-model="form.price_max"
class="input price-input"
type="digit"
placeholder="最高价"
placeholder-class="placeholder"
/>
</view>
</view>
<view class="field" v-if="allTags.length > 0">
<text class="label">标签最多5个</text>
<view class="tag-selector">
<view
v-for="tag in allTags"
:key="tag.id"
class="tag-chip"
:class="{ active: selectedTagIds.includes(tag.id) }"
@tap="toggleTag(tag.id)"
>
<text class="tag-chip-text">{{ tag.name }}</text>
</view>
</view>
</view>
<view class="field">
<text class="label">坐标位置</text>
<view class="location-picker" @tap="chooseLocation">
<uni-icons type="location" size="18" color="#6366f1" />
<text v-if="locationName" class="location-text">{{ locationName }}</text>
<text v-else class="location-placeholder">点击选择地图位置</text>
<uni-icons type="right" size="16" color="#cbd5e1" />
</view>
<view v-if="form.longitude && form.latitude" class="coord-display">
<text class="coord-text">经度: {{ form.longitude }} / 纬度: {{ form.latitude }}</text>
</view>
<view class="auto-city-display">
<text class="auto-city-label">所在城市</text>
<text v-if="form.city" class="auto-city-text">{{ form.city }}</text>
<text v-else class="auto-city-placeholder">选择地图位置后自动识别</text>
</view>
</view>
</view>
<view class="submit-area">
<button
class="submit-btn"
:loading="submitting"
:disabled="submitting"
@tap="handleSubmit"
>
{{ submitting ? "保存中..." : "保存修改" }}
</button>
</view>
</template>
</view>
</template>
<style scoped>
.edit-page {
min-height: 100vh;
background: #f5f6fa;
padding: 24rpx 32rpx;
padding-bottom: 48rpx;
}
.loading-state {
display: flex;
align-items: center;
justify-content: center;
min-height: 60vh;
color: #94a3b8;
font-size: 28rpx;
}
.form-card {
background: #ffffff;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
}
.field { margin-bottom: 32rpx; }
.field:last-child { margin-bottom: 0; }
.label {
font-size: 28rpx;
font-weight: 600;
color: #1e293b;
display: block;
margin-bottom: 16rpx;
}
.input {
width: 100%;
height: 84rpx;
background: #f5f6fa;
border-radius: 12rpx;
padding: 0 24rpx;
font-size: 28rpx;
color: #1e293b;
box-sizing: border-box;
}
.textarea {
width: 100%;
height: 200rpx;
background: #f5f6fa;
border-radius: 12rpx;
padding: 20rpx 24rpx;
font-size: 28rpx;
color: #1e293b;
box-sizing: border-box;
line-height: 1.6;
}
.textarea-sm { height: 140rpx; }
.placeholder { color: #94a3b8; }
.location-picker {
display: flex;
align-items: center;
height: 84rpx;
background: #f5f6fa;
border-radius: 12rpx;
padding: 0 24rpx;
gap: 12rpx;
}
.location-text {
flex: 1;
font-size: 28rpx;
color: #1e293b;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.location-placeholder { flex: 1; font-size: 28rpx; color: #94a3b8; }
.coord-display {
margin-top: 12rpx;
padding: 12rpx 24rpx;
background: rgba(99, 102, 241, 0.06);
border-radius: 8rpx;
}
.coord-text { font-size: 24rpx; color: #6366f1; }
.auto-city-display {
margin-top: 12rpx;
padding: 16rpx 24rpx;
background: #f8fafc;
border-radius: 8rpx;
}
.auto-city-label {
display: block;
font-size: 22rpx;
color: #94a3b8;
margin-bottom: 6rpx;
}
.auto-city-text {
display: block;
font-size: 28rpx;
color: #1e293b;
font-weight: 500;
}
.auto-city-placeholder {
display: block;
font-size: 26rpx;
color: #94a3b8;
}
.free-toggle { display: flex; gap: 16rpx; }
.toggle-btn {
flex: 1;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
background: #f5f6fa;
border-radius: 12rpx;
border: 2rpx solid #e2e8f0;
}
.toggle-btn.active {
background: rgba(99, 102, 241, 0.12);
border-color: #6366f1;
}
.toggle-text { font-size: 28rpx; color: #64748b; }
.toggle-btn.active .toggle-text { color: #6366f1; font-weight: 600; }
.price-range { display: flex; align-items: center; gap: 12rpx; }
.price-input { flex: 1; }
.price-sep { font-size: 28rpx; color: #94a3b8; }
.tag-selector { display: flex; flex-wrap: wrap; gap: 16rpx; }
.tag-chip {
padding: 10rpx 24rpx;
background: #f5f6fa;
border-radius: 32rpx;
border: 2rpx solid #e2e8f0;
}
.tag-chip.active {
background: rgba(99, 102, 241, 0.12);
border-color: #6366f1;
}
.tag-chip-text { font-size: 26rpx; color: #64748b; }
.tag-chip.active .tag-chip-text { color: #6366f1; font-weight: 500; }
.submit-area { margin-top: 16rpx; }
.submit-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background: #6366f1;
color: #ffffff;
font-size: 32rpx;
font-weight: 600;
border-radius: 16rpx;
border: none;
}
.submit-btn[disabled] { background: #a5b4fc; }
.submit-btn::after { border: none; }
</style>