Files
medical-mall/pages/mall/admin/setting/delivery/station.uvue

294 lines
12 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
<template>
<view class="admin-page-container">
<view class="page-card">
<!-- 搜索栏 -->
<view class="search-wrap">
<view class="search-item">
<text class="label">提货点搜索:</text>
<input class="input" placeholder="请输入提货点名称,电话" v-model="searchKey" @confirm="handleQuery" />
</view>
<button class="btn btn-primary" @click="handleQuery">查询</button>
</view>
<view class="action-wrap">
<button class="btn btn-primary" @click="onAdd">添加提货点</button>
</view>
<!-- 表格区域 -->
<view class="table-wrap border-shadow">
<view class="table-header">
<view class="th" style="flex: 1;">序号</view>
<view class="th" style="flex: 2;">提货点图片</view>
<view class="th" style="flex: 2;">提货点名称</view>
<view class="th" style="flex: 2;">提货点电话</view>
<view class="th" style="flex: 3;">地址</view>
<view class="th" style="flex: 1.5;">是否显示</view>
<view class="th" style="flex: 2;">操作</view>
</view>
<view class="table-body">
<view v-if="loading" class="loading-box">
<text>加载中...</text>
</view>
<view v-else-if="stationList.length === 0" class="no-data">
<text class="no-data-text">暂无提货点数据</text>
</view>
<view v-else v-for="(item, index) in stationList" :key="item.id" class="tr">
<view class="td" style="flex: 1;"><text class="td-txt">{{ (page - 1) * pageSize + index + 1 }}</text></view>
<view class="td" style="flex: 2;">
<image v-if="item.image" class="station-img" :src="item.image" mode="aspectFill" />
<view v-else class="img-placeholder"><text>🖼️</text></view>
</view>
<view class="td" style="flex: 2;"><text class="td-txt">{{ item.name }}</text></view>
<view class="td" style="flex: 2;"><text class="td-txt">{{ item.phone }}</text></view>
<view class="td" style="flex: 3;"><text class="td-txt ellipsis-2">{{ item.address }}</text></view>
<view class="td" style="flex: 1.5;">
<switch :checked="item.status === 1" color="#1890ff" scale="0.7" @change="onToggleStatus(item)" />
</view>
<view class="td" style="flex: 2;">
<text class="action-btn" @click="onEdit(item)">编辑</text>
<view class="divider"></view>
<text class="action-btn danger" @click="onDelete(item)">删除</text>
</view>
</view>
</view>
</view>
<!-- 分页栏 -->
<view class="pagination-footer">
<text class="total-txt">共 {{ total }} 条</text>
<view class="page-btns">
<text :class="['p-btn', page <= 1 ? 'disabled' : '']" @click="prevPage"> < </text>
<text class="p-btn active">{{ page }}</text>
<text :class="['p-btn', stationList.length < pageSize ? 'disabled' : '']" @click="nextPage"> > </text>
</view>
</view>
</view>
<!-- 添加/编辑 弹窗 -->
<view v-if="showModal" class="modal-overlay" @click="closeModal">
<view class="modal-card" @click.stop>
<view class="modal-header">
<text class="modal-title">{{ isEdit ? '编辑提货点' : '添加提货点' }}</text>
<text class="close-btn" @click="closeModal">×</text>
</view>
<view class="modal-body">
<scroll-view scroll-y="true" style="max-height: 500px;">
<view class="form-item">
<text class="f-label">提货点名称:</text>
<input class="f-input" v-model="form.name" placeholder="请输入名称" />
</view>
<view class="form-item">
<text class="f-label">联系电话:</text>
<input class="f-input" v-model="form.phone" type="number" placeholder="请输入电话" />
</view>
<view class="form-item">
<text class="f-label">详细地址:</text>
<input class="f-input" v-model="form.address" placeholder="请输入详细地址" />
</view>
<view class="form-item">
<text class="f-label">展示图片:</text>
<view class="upload-placeholder" @click="handleUpload">
<image v-if="form.image" :src="form.image" mode="aspectFill" class="img-preview" />
<text v-else>+</text>
</view>
</view>
</scroll-view>
</view>
<view class="modal-footer">
<button class="btn" @click="closeModal">取消</button>
<button class="btn btn-primary" @click="handleSave">保存</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, reactive, onMounted } from 'vue'
import { fetchDeliveryStationPage, saveDeliveryStation, deleteDeliveryStation, type DeliveryStation } from '@/services/admin/deliveryService.uts'
const stationList = ref<DeliveryStation[]>([])
const loading = ref(false)
const total = ref(0)
const page = ref(1)
const pageSize = 15
const searchKey = ref('')
// Modal state
const showModal = ref(false)
const isEdit = ref(false)
const form = reactive({
id: '' as string | null,
name: '',
phone: '',
address: '',
image: '',
status: 1,
sort_order: 0
})
onMounted(() => {
loadData()
})
async function loadData() {
loading.value = true
try {
const res = await fetchDeliveryStationPage(page.value, pageSize, searchKey.value || null)
stationList.value = res.items
total.value = res.total
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
function handleQuery() {
page.value = 1
loadData()
}
function onAdd() {
isEdit.value = false
form.id = null
form.name = ''
form.phone = ''
form.address = ''
form.image = ''
form.status = 1
form.sort_order = 0
showModal.value = true
}
function onEdit(item : DeliveryStation) {
isEdit.value = true
form.id = item.id
form.name = item.name
form.phone = item.phone
form.address = item.address
form.image = item.image || ''
form.status = item.status
form.sort_order = item.sort_order
showModal.value = true
}
function closeModal() {
showModal.value = false
}
async function handleSave() {
if (!form.name || !form.phone || !form.address) {
uni.showToast({ title: '请完善必要信息', icon: 'none' })
return
}
loading.value = true
try {
const resId = await saveDeliveryStation(form)
if (resId != null) {
uni.showToast({ title: '保存成功' })
closeModal()
loadData()
}
} finally {
loading.value = false
}
}
async function onDelete(item : DeliveryStation) {
uni.showModal({
title: '删除确认',
content: `确定要删除提货点 "${item.name}" 吗?\n\n⚠ 警告:该操作将同时删除该站点下的所有配送员关联!`,
confirmText: '确认删除',
confirmColor: '#ff4d4f',
success: async (res) => {
if (res.confirm) {
try {
const ok = await deleteDeliveryStation(item.id)
if (ok) {
uni.showToast({ title: '删除成功' })
loadData()
}
} catch (e: any) {
const errMsg = e?.message || '删除失败'
uni.showToast({ title: errMsg, icon: 'none', duration: 3000 })
}
}
}
})
}
async function onToggleStatus(item : DeliveryStation) {
const nextStatus = item.status === 1 ? 0 : 1
const ok = await saveDeliveryStation({ ...item, status: nextStatus })
if (ok != null) {
item.status = nextStatus
uni.showToast({ title: '状态已更新' })
}
}
function prevPage() { if (page.value > 1) { page.value--; loadData(); } }
function nextPage() { if (stationList.value.length >= pageSize) { page.value++; loadData(); } }
function handleUpload() {
uni.showToast({ title: '上传功能开发中', icon: 'none' })
}
</script>
<style scoped lang="scss">
.admin-page-container { padding: 24px; background-color: #f5f7f9; min-height: 100vh; }
.page-card { background-color: #fff; border-radius: 4px; padding: 24px; }
.search-wrap { display: flex; flex-direction: row; align-items: center; padding-bottom: 24px; border-bottom: 1px solid #f0f0f0; margin-bottom: 24px; }
.search-item { display: flex; flex-direction: row; align-items: center; margin-right: 24px; }
.label { font-size: 14px; color: #606266; margin-right: 8px; }
.input { width: 250px; height: 32px; border: 1px solid #dcdfe6; border-radius: 4px; padding: 0 12px; font-size: 14px; }
.btn { height: 32px; padding: 0 20px; border-radius: 4px; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 14px; }
.btn-primary { background-color: #1890ff; color: #fff; }
.action-wrap { margin-bottom: 20px; }
.table-wrap { border: 1px solid #f0f0f0; border-radius: 4px; }
.table-header { display: flex; flex-direction: row; background-color: #f8f8f9; }
.th { padding: 12px 10px; font-size: 14px; font-weight: bold; color: #515a6e; border-bottom: 1px solid #f0f0f0; text-align: center; }
.tr { display: flex; flex-direction: row; border-bottom: 1px solid #f0f0f0; min-height: 60px; align-items: center; }
.td { padding: 10px; display: flex; align-items: center; justify-content: center; }
.td-txt { font-size: 13px; color: #606266; text-align: center; }
.ellipsis-2 { overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.station-img { width: 44px; height: 44px; border-radius: 4px; background-color: #f5f5f5; }
.img-placeholder { width: 44px; height: 44px; border-radius: 4px; background-color: #f0f0f0; display: flex; align-items: center; justify-content: center; font-size: 20px; }
.action-btn { color: #1890ff; font-size: 13px; cursor: pointer; }
.danger { color: #ff4d4f; }
.divider { width: 1px; height: 12px; background-color: #e8e8e8; margin: 0 8px; }
.no-data, .loading-box { padding: 60px 0; text-align: center; width: 100%; }
.no-data-text { font-size: 14px; color: #ccc; }
.pagination-footer { margin-top: 24px; display: flex; flex-direction: row; align-items: center; justify-content: flex-end; gap: 12px; }
.total-txt { font-size: 13px; color: #999; }
.page-btns { display: flex; flex-direction: row; gap: 8px; }
.p-btn { width: 30px; height: 30px; border: 1px solid #dcdee2; display: flex; align-items: center; justify-content: center; border-radius: 4px; cursor: pointer; font-size: 13px; }
.p-btn.active { background-color: #1890ff; color: #fff; border-color: #1890ff; }
.p-btn.disabled { opacity: 0.5; cursor: not-allowed; }
/* Modal */
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0,0,0,0.5); z-index: 1000; display: flex; align-items: center; justify-content: center; }
.modal-card { width: 500px; background-color: #fff; border-radius: 8px; overflow: hidden; }
.modal-header { padding: 16px 20px; border-bottom: 1px solid #f0f0f0; display: flex; flex-direction: row; justify-content: space-between; align-items: center; }
.modal-title { font-size: 16px; font-weight: bold; color: #333; }
.close-btn { font-size: 24px; color: #999; cursor: pointer; }
.modal-body { padding: 24px; }
.form-item { margin-bottom: 20px; display: flex; flex-direction: row; align-items: center; }
.f-label { width: 90px; font-size: 14px; color: #666; text-align: right; margin-right: 15px; }
.f-input { flex: 1; border: 1px solid #dcdfe6; height: 36px; padding: 0 12px; border-radius: 4px; font-size: 14px; }
.upload-placeholder { width: 64px; height: 64px; border: 1px dashed #dcdfe6; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 24px; color: #ccc; cursor: pointer; }
.img-preview { width: 100%; height: 100%; border-radius: 4px; }
.modal-footer { padding: 16px 24px; border-top: 1px solid #f0f0f0; display: flex; flex-direction: row; justify-content: flex-end; gap: 12px; }
</style>