feat(pickup): 线下提号支持扩展字段店铺管理CRUD及时间筛选
- 线下提号创建表单扩展:增加「操作人」、「第三方单号」、「下单金额」、「押金金额」字段,重构弹窗布局为紧凑双列/三列金额卡片排布,消除过度垂直滚动并默认预填操作人 - 店铺管理 CRUD:新增 pickup_shops 配置表与配套 API,支持在提号页面直接管理店铺(增删改查、启用/禁用、筛选),下拉选项自动优先加载活跃配置店铺并兼顾历史店铺 - 列表时间范围筛选:增加精确到秒的「日期+时间」时间范围选择器,支持快捷时段与后端时间范围过滤 - 生产环境数据核查:只读提取线上全部 61 项历史提号店铺名称与单量分布,完整整理沉淀至实施文档供后续映射规则参考 - 验证方式:执行并通过全套端到端代码门禁(scripts/check.sh:SQL约束、go vet、go build、MySQL真实测试库单元测试及覆盖率、vue大小检查、prettier、eslint、vue-tsc及vite构建)
This commit is contained in:
@@ -0,0 +1,401 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Delete, Edit, Plus, Refresh, Search } from '@element-plus/icons-vue'
|
||||
import {
|
||||
createPickupShop,
|
||||
deletePickupShop,
|
||||
fetchPickupShops,
|
||||
updatePickupShop,
|
||||
type PickupShop,
|
||||
type PickupShopCreateRequest,
|
||||
type PickupShopUpdateRequest,
|
||||
} from '@/features/admin/api/adminPickup'
|
||||
import { formatDateTime } from '@/shared/utils/time'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'updated'): void
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const shopList = ref<PickupShop[]>([])
|
||||
const filters = reactive({
|
||||
keyword: '',
|
||||
platform: '',
|
||||
status: '',
|
||||
})
|
||||
|
||||
// 新建/编辑店铺弹窗状态
|
||||
const formDialogVisible = ref(false)
|
||||
const formSaving = ref(false)
|
||||
const editingId = ref<number | null>(null)
|
||||
const shopForm = reactive({
|
||||
name: '',
|
||||
platform: '',
|
||||
sort_order: 0,
|
||||
status: 'active' as 'active' | 'disabled',
|
||||
remark: '',
|
||||
})
|
||||
|
||||
const platformOptions = ['淘宝', '拼多多', '闲鱼', '微信', 'QQ', '抖音', '快手']
|
||||
|
||||
function handleClose() {
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
async function loadShops() {
|
||||
loading.value = true
|
||||
try {
|
||||
shopList.value = await fetchPickupShops({
|
||||
keyword: filters.keyword.trim(),
|
||||
platform: filters.platform,
|
||||
status: filters.status,
|
||||
})
|
||||
} catch (err: unknown) {
|
||||
ElMessage.error(getErrorMessage(err) || '加载店铺列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
loadShops()
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
filters.keyword = ''
|
||||
filters.platform = ''
|
||||
filters.status = ''
|
||||
loadShops()
|
||||
}
|
||||
|
||||
function openCreateForm() {
|
||||
editingId.value = null
|
||||
shopForm.name = ''
|
||||
shopForm.platform = ''
|
||||
shopForm.sort_order = 0
|
||||
shopForm.status = 'active'
|
||||
shopForm.remark = ''
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
function openEditForm(row: PickupShop) {
|
||||
editingId.value = row.id
|
||||
shopForm.name = row.name
|
||||
shopForm.platform = row.platform
|
||||
shopForm.sort_order = row.sort_order
|
||||
shopForm.status = row.status
|
||||
shopForm.remark = row.remark
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSaveShop() {
|
||||
if (!shopForm.name.trim()) {
|
||||
ElMessage.warning('请输入店铺名称')
|
||||
return
|
||||
}
|
||||
formSaving.value = true
|
||||
try {
|
||||
if (editingId.value) {
|
||||
const payload: PickupShopUpdateRequest = {
|
||||
name: shopForm.name.trim(),
|
||||
platform: shopForm.platform.trim(),
|
||||
sort_order: Number(shopForm.sort_order) || 0,
|
||||
status: shopForm.status,
|
||||
remark: shopForm.remark.trim(),
|
||||
}
|
||||
await updatePickupShop(editingId.value, payload)
|
||||
ElMessage.success('店铺更新成功')
|
||||
} else {
|
||||
const payload: PickupShopCreateRequest = {
|
||||
name: shopForm.name.trim(),
|
||||
platform: shopForm.platform.trim(),
|
||||
sort_order: Number(shopForm.sort_order) || 0,
|
||||
status: shopForm.status,
|
||||
remark: shopForm.remark.trim(),
|
||||
}
|
||||
await createPickupShop(payload)
|
||||
ElMessage.success('店铺创建成功')
|
||||
}
|
||||
formDialogVisible.value = false
|
||||
await loadShops()
|
||||
emit('updated')
|
||||
} catch (err: unknown) {
|
||||
ElMessage.error(getErrorMessage(err) || '保存店铺失败')
|
||||
} finally {
|
||||
formSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToggleStatus(row: PickupShop) {
|
||||
const nextStatus = row.status === 'active' ? 'disabled' : 'active'
|
||||
const actionText = nextStatus === 'active' ? '启用' : '禁用'
|
||||
try {
|
||||
await updatePickupShop(row.id, {
|
||||
name: row.name,
|
||||
platform: row.platform,
|
||||
sort_order: row.sort_order,
|
||||
status: nextStatus,
|
||||
remark: row.remark,
|
||||
})
|
||||
ElMessage.success(`已${actionText}店铺「${row.name}」`)
|
||||
row.status = nextStatus
|
||||
emit('updated')
|
||||
} catch (err: unknown) {
|
||||
ElMessage.error(getErrorMessage(err) || `${actionText}失败`)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteShop(row: PickupShop) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除店铺「${row.name}」吗?历史提号中的记录不会被删除。`,
|
||||
'确认删除店铺',
|
||||
{
|
||||
confirmButtonText: '确定删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
)
|
||||
loading.value = true
|
||||
try {
|
||||
await deletePickupShop(row.id)
|
||||
ElMessage.success('店铺已删除')
|
||||
await loadShops()
|
||||
emit('updated')
|
||||
} catch (err: unknown) {
|
||||
ElMessage.error(getErrorMessage(err) || '删除店铺失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
} catch {
|
||||
/* 用户放弃 */
|
||||
}
|
||||
}
|
||||
|
||||
function getErrorMessage(e: unknown): string {
|
||||
if (e && typeof e === 'object') {
|
||||
const response = (e as { response?: { data?: { message?: string } } }).response
|
||||
if (response?.data?.message) return response.data.message
|
||||
const message = (e as { message?: string }).message
|
||||
if (message) return message
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadShops()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="props.modelValue"
|
||||
title="店铺管理"
|
||||
width="min(920px, calc(100vw - 32px))"
|
||||
:before-close="handleClose"
|
||||
append-to-body
|
||||
>
|
||||
<div class="shop-mgr-toolbar">
|
||||
<el-form inline @submit.prevent>
|
||||
<el-form-item label="店铺名称">
|
||||
<el-input
|
||||
v-model="filters.keyword"
|
||||
placeholder="搜索名称或备注"
|
||||
clearable
|
||||
style="width: 170px"
|
||||
@keyup.enter="handleSearch"
|
||||
@clear="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="平台">
|
||||
<el-select
|
||||
v-model="filters.platform"
|
||||
placeholder="全部平台"
|
||||
clearable
|
||||
style="width: 120px"
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option v-for="item in platformOptions" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select
|
||||
v-model="filters.status"
|
||||
placeholder="全部状态"
|
||||
clearable
|
||||
style="width: 110px"
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option label="启用" value="active" />
|
||||
<el-option label="禁用" value="disabled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
<el-button :icon="Refresh" @click="loadShops">刷新</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="toolbar-right">
|
||||
<el-button type="success" :icon="Plus" @click="openCreateForm">新增店铺</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="shopList" border stripe max-height="460">
|
||||
<el-table-column prop="name" label="店铺名称" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<strong style="color: var(--el-text-color-primary)">{{ row.name }}</strong>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="platform" label="平台/渠道" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.platform" size="small" effect="plain">{{ row.platform }}</el-tag>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort_order" label="排序权重" width="100" align="center" />
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'active' ? 'success' : 'info'" size="small">
|
||||
{{ row.status === 'active' ? '启用中' : '已禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.remark || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updated_at" label="更新时间" width="160">
|
||||
<template #default="{ row }">{{ formatDateTime(row.updated_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="190" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="shop-actions">
|
||||
<el-button size="small" :icon="Edit" @click="openEditForm(row)">编辑</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
:type="row.status === 'active' ? 'warning' : 'success'"
|
||||
plain
|
||||
@click="handleToggleStatus(row)"
|
||||
>
|
||||
{{ row.status === 'active' ? '禁用' : '启用' }}
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
plain
|
||||
:icon="Delete"
|
||||
@click="handleDeleteShop(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 新建/编辑店铺弹窗 -->
|
||||
<el-dialog
|
||||
v-model="formDialogVisible"
|
||||
:title="editingId ? '编辑店铺' : '新增店铺'"
|
||||
width="460px"
|
||||
append-to-body
|
||||
>
|
||||
<el-form label-width="90px">
|
||||
<el-form-item label="店铺名称" required>
|
||||
<el-input
|
||||
v-model="shopForm.name"
|
||||
placeholder="例如:糖浆、淘宝王大锤专营店"
|
||||
maxlength="100"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="所属平台">
|
||||
<el-select
|
||||
v-model="shopForm.platform"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
clearable
|
||||
placeholder="选择或输入平台(如 淘宝/拼多多)"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option v-for="item in platformOptions" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序权重">
|
||||
<el-input-number
|
||||
v-model="shopForm.sort_order"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div class="form-hint">权重越大的店铺在下拉选择时排在越前面</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="shopForm.status">
|
||||
<el-radio value="active">启用</el-radio>
|
||||
<el-radio value="disabled">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注说明">
|
||||
<el-input
|
||||
v-model="shopForm.remark"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="填写店铺说明或客服负责人(可选)"
|
||||
maxlength="255"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="formDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="formSaving" @click="handleSaveShop">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.shop-mgr-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.shop-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 1.4;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user