| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465 |
- <script setup>
- import { onLoad } from '@dcloudio/uni-app'
- import { reactive, ref } from 'vue'
- import { useUserStore } from '@/stores/user'
- import { request, parseResponseJson } from '@/utils/request'
- import { fetchChinaRegionTree } from '@/utils/chinaRegion'
- const regionTree = ref([])
- const userStore = useUserStore()
- const addressId = ref('')
- const loading = ref(false)
- const submitting = ref(false)
- const form = reactive({
- contactPerson: '',
- contactPhone: '',
- province: '',
- city: '',
- district: '',
- detailAddress: '',
- isDefault: false
- })
- /** H5 上 mode=region 无数据,使用多列选择器 + 接口 bas_china_region 树 */
- const multiIndex = ref([0, 0, 0])
- const multiRange = ref([[], [], []])
- function updateMultiRange() {
- const pi = Math.min(multiIndex.value[0], Math.max(0, regionTree.value.length - 1))
- const p = regionTree.value[pi] ?? regionTree.value[0]
- const cities = p?.children ?? []
- const ci = Math.min(multiIndex.value[1], Math.max(0, cities.length - 1))
- const c = cities[ci]
- const districts = c?.children ?? []
- const row0 = regionTree.value.map((x) => x.label)
- const row1 = cities.map((x) => x.label)
- let row2 = districts.map((x) => x.label)
- if (row2.length === 0 && c) {
- row2 = [c.label]
- }
- if (row1.length === 0) row1.push('')
- if (row2.length === 0) row2.push('')
- multiRange.value = [row0, row1, row2]
- }
- function applySelectionFromIndex() {
- const pi = Math.min(multiIndex.value[0], regionTree.value.length - 1)
- const p = regionTree.value[pi]
- const cities = p?.children ?? []
- const ci = Math.min(multiIndex.value[1], Math.max(0, cities.length - 1))
- const c = cities[ci]
- const districts = c?.children ?? []
- let di = multiIndex.value[2]
- if (districts.length > 0) {
- di = Math.min(di, districts.length - 1)
- form.province = p?.label ?? ''
- form.city = c?.label ?? ''
- form.district = districts[di]?.label ?? ''
- } else if (c) {
- form.province = p?.label ?? ''
- form.city = c.label
- form.district = c.label
- }
- }
- function onRegionColumnChange(e) {
- const col = e.detail.column
- const val = e.detail.value
- const idx = [...multiIndex.value]
- if (col === 0) {
- idx[0] = val
- idx[1] = 0
- idx[2] = 0
- } else if (col === 1) {
- idx[1] = val
- idx[2] = 0
- } else {
- idx[2] = val
- }
- idx[0] = Math.min(Math.max(0, idx[0]), regionTree.value.length - 1)
- const cities = regionTree.value[idx[0]]?.children ?? []
- idx[1] = Math.min(Math.max(0, idx[1]), Math.max(0, cities.length - 1))
- const dists = cities[idx[1]]?.children ?? []
- idx[2] = dists.length > 0 ? Math.min(Math.max(0, idx[2]), dists.length - 1) : 0
- multiIndex.value = idx
- updateMultiRange()
- }
- function onRegionMultiConfirm(e) {
- const v = e.detail.value
- if (v && v.length >= 3) {
- multiIndex.value = [...v]
- updateMultiRange()
- applySelectionFromIndex()
- }
- }
- function syncMultiFromForm() {
- const { province: p, city: c, district: d } = form
- if (!p || !c || !d) {
- multiIndex.value = [0, 0, 0]
- updateMultiRange()
- return
- }
- const pi = regionTree.value.findIndex((x) => x.label === p)
- if (pi < 0) {
- multiIndex.value = [0, 0, 0]
- updateMultiRange()
- return
- }
- const cities = regionTree.value[pi].children ?? []
- const ci = cities.findIndex((x) => x.label === c)
- if (ci < 0) {
- multiIndex.value = [pi, 0, 0]
- updateMultiRange()
- return
- }
- const districts = cities[ci].children ?? []
- let di = districts.findIndex((x) => x.label === d)
- if (di < 0) di = 0
- multiIndex.value = [pi, ci, di]
- updateMultiRange()
- }
- function regionLabel() {
- if (form.province && form.city && form.district) return `${form.province} ${form.city} ${form.district}`
- return '请选择省市区'
- }
- function toggleDefaultDelivery() {
- form.isDefault = !form.isDefault
- }
- async function loadDetail(id, opts) {
- if (!opts?.skipLoading) loading.value = true
- try {
- const res = await request({ url: `/mini/client/addresses/${id}`, method: 'GET' })
- const body = parseResponseJson(res)
- if (body.code !== 200 || !body.data) {
- uni.showToast({ title: body.message || '地址不存在', icon: 'none' })
- setTimeout(() => uni.navigateBack(), 800)
- return
- }
- const d = body.data
- form.contactPerson = d.contactPerson || ''
- form.contactPhone = d.contactPhone || ''
- form.province = d.province || ''
- form.city = d.city || ''
- form.district = d.district || ''
- form.detailAddress = d.detailAddress || ''
- form.isDefault = !!d.isDefault
- syncMultiFromForm()
- } finally {
- if (!opts?.skipLoading) loading.value = false
- }
- }
- function validate() {
- if (!form.contactPerson.trim()) {
- uni.showToast({ title: '请填写联系人', icon: 'none' })
- return false
- }
- if (!form.contactPhone.trim()) {
- uni.showToast({ title: '请填写手机号', icon: 'none' })
- return false
- }
- if (!/^1\d{10}$/.test(form.contactPhone.trim())) {
- uni.showToast({ title: '请输入正确手机号', icon: 'none' })
- return false
- }
- if (!form.province || !form.city || !form.district) {
- uni.showToast({ title: '请选择省市区', icon: 'none' })
- return false
- }
- if (!form.detailAddress.trim()) {
- uni.showToast({ title: '请填写详细地址', icon: 'none' })
- return false
- }
- return true
- }
- async function handleSubmit() {
- if (!validate() || submitting.value) return
- submitting.value = true
- try {
- const payload = {
- addressType: 'delivery',
- contactPerson: form.contactPerson.trim(),
- contactPhone: form.contactPhone.trim(),
- province: form.province || '',
- city: form.city || '',
- district: form.district || '',
- detailAddress: form.detailAddress.trim(),
- isDefault: form.isDefault
- }
- const res = await request({
- url: addressId.value ? `/mini/client/addresses/${addressId.value}` : '/mini/client/addresses',
- method: addressId.value ? 'PUT' : 'POST',
- data: payload
- })
- const body = parseResponseJson(res)
- if (body.code === 200) {
- uni.showToast({ title: addressId.value ? '保存成功' : '添加成功', icon: 'success' })
- setTimeout(() => uni.navigateBack(), 500)
- } else {
- uni.showToast({ title: body.message || '保存失败', icon: 'none' })
- }
- } catch {
- uni.showToast({ title: '网络异常', icon: 'none' })
- } finally {
- submitting.value = false
- }
- }
- onLoad(async (options) => {
- userStore.checkLoginStatus()
- if (!userStore.isLoggedIn) {
- uni.reLaunch({ url: '/pages/login/login' })
- return
- }
- if (!userStore.isClient) {
- uni.showToast({ title: '仅客户可访问', icon: 'none' })
- setTimeout(() => userStore.navigateToHome(), 1200)
- return
- }
- loading.value = true
- try {
- regionTree.value = await fetchChinaRegionTree()
- if (regionTree.value.length === 0) {
- uni.showToast({ title: '省市区数据未配置', icon: 'none' })
- }
- updateMultiRange()
- const id = (options && options.id) || ''
- if (id) {
- addressId.value = id
- uni.setNavigationBarTitle({ title: '编辑收货地址' })
- await loadDetail(id, { skipLoading: true })
- } else {
- uni.setNavigationBarTitle({ title: '新增收货地址' })
- }
- } catch {
- uni.showToast({ title: '省市区加载失败', icon: 'none' })
- } finally {
- loading.value = false
- }
- })
- </script>
- <template>
- <view class="page client-theme client-subpage">
- <view class="client-subpage-hero">
- <text class="client-subpage-hero__title">{{ addressId ? '编辑收货地址' : '新增收货地址' }}</text>
- <text class="client-subpage-hero__sub">请填写真实信息,便于配送联系</text>
- </view>
- <view v-if="loading" class="state">加载中...</view>
- <view v-else class="form client-subpage-body">
- <view class="field">
- <text class="label">联系人</text>
- <input
- v-model="form.contactPerson"
- class="input"
- type="text"
- placeholder="请填写联系人姓名"
- maxlength="50"
- />
- </view>
- <view class="field">
- <text class="label">手机号</text>
- <input
- v-model="form.contactPhone"
- class="input"
- type="number"
- placeholder="请填写11位手机号"
- maxlength="11"
- />
- </view>
- <view class="field">
- <text class="label">省市区</text>
- <picker
- mode="multiSelector"
- :value="multiIndex"
- :range="multiRange"
- @columnchange="onRegionColumnChange"
- @change="onRegionMultiConfirm"
- >
- <view class="picker-row">
- <text :class="['picker-text', !form.province && 'placeholder']">{{ regionLabel() }}</text>
- </view>
- </picker>
- </view>
- <view class="field column">
- <text class="label">详细地址</text>
- <textarea
- v-model="form.detailAddress"
- class="textarea"
- placeholder="街道、门牌号等"
- maxlength="500"
- :auto-height="false"
- />
- </view>
- <view class="field switch-row">
- <text class="label switch-label">设为默认收货地址</text>
- <view
- class="custom-switch"
- :class="{ 'is-on': form.isDefault }"
- @click="toggleDefaultDelivery"
- >
- <view class="custom-switch-thumb" />
- </view>
- </view>
- <button
- class="submit-btn"
- type="default"
- :loading="submitting"
- :disabled="submitting"
- @click="handleSubmit"
- >
- 保存
- </button>
- </view>
- </view>
- </template>
- <style lang="scss" scoped>
- .page {
- min-height: 100vh;
- background: var(--c-bg);
- box-sizing: border-box;
- }
- .state {
- text-align: center;
- color: var(--c-muted);
- padding: 48rpx;
- font-size: 28rpx;
- }
- .form {
- flex: 1;
- min-height: 0;
- background: var(--c-card);
- border-radius: var(--c-radius-card);
- border: 1rpx solid var(--c-border);
- padding: 8rpx 24rpx 40rpx;
- margin: 0;
- box-sizing: border-box;
- }
- .field {
- display: flex;
- align-items: center;
- min-height: 96rpx;
- border-bottom: 1rpx solid rgba(220, 235, 228, 0.65);
- &.column {
- flex-direction: column;
- align-items: stretch;
- padding: 20rpx 0;
- min-height: auto;
- }
- &.switch-row {
- justify-content: space-between;
- border-bottom: none;
- padding-top: 8rpx;
- gap: 24rpx;
- }
- }
- .label {
- width: 160rpx;
- font-size: 28rpx;
- color: var(--c-text);
- }
- .input {
- flex: 1;
- font-size: 28rpx;
- color: var(--c-text);
- padding: 18rpx 0;
- }
- .picker-row {
- flex: 1;
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 20rpx 0;
- }
- .picker-text {
- font-size: 28rpx;
- color: var(--c-text);
- }
- .placeholder {
- color: #c0c4cc;
- }
- .textarea {
- width: 100%;
- font-size: 28rpx;
- line-height: 1.5;
- color: #303133;
- padding: 16rpx;
- background: transparent;
- border-radius: 20rpx;
- box-sizing: border-box;
- background: #f8fcfa;
- border: 1rpx solid rgba(220, 235, 228, 0.65);
- min-height: calc(1.5em * 3 + 32rpx);
- }
- .switch-label {
- width: auto;
- flex: 1;
- }
- .custom-switch {
- width: 96rpx;
- height: 52rpx;
- border-radius: 26rpx;
- background: #dcdfe6;
- position: relative;
- transition: all 0.2s ease;
- &.is-on {
- background: var(--c-brand-green);
- }
- }
- .custom-switch-thumb {
- width: 44rpx;
- height: 44rpx;
- border-radius: 50%;
- background: #fff;
- position: absolute;
- top: 4rpx;
- left: 4rpx;
- transition: all 0.2s ease;
- }
- .custom-switch.is-on .custom-switch-thumb {
- left: 48rpx;
- }
- .submit-btn {
- width: 100%;
- height: 88rpx;
- line-height: 88rpx;
- margin-top: 40rpx;
- background: linear-gradient(145deg, var(--c-barley-yellow), #e4b458);
- color: #ffffff;
- border-radius: 44rpx;
- font-size: 30rpx;
- border: none;
- font-weight: 700;
- }
- .submit-btn[disabled] {
- color: #ffffff !important;
- opacity: 0.92;
- }
- </style>
|