- 33 个 ListView.vue 加 :borderless=true + :page-sizes=[10,20,50,100]
覆盖: order/inquiry/finance/operateguide/sys/Dept|Menu|File|Notice|
Params|User|Role|Post|DictType|DictData|Product|Schedule|RoleGroup
user/Enterprise|enterpriselevel|enterprisescore|enterpriserating
supplier/promoter|couponscope|historylog|logistics|ordercomment|Activity
other/goodunit|label|textconfig
api/feedback|help|lunbo
openapi/AuditLog|CompanyAccount
- OrderListView: columns 抽到 script setup (computed) + :borderless +
:page-sizes + sub 动态显示选中数
- router/index.ts: /product/shop → redirect /supplier/good
(删 src/modules/product/shop/ShopListView.vue 旧占位, 后端 404 无意义)
- 沿用 W17 设计规范 v2 (PageHeader+SearchBar+DataTable+ListTabs)
191 lines
7.0 KiB
Vue
191 lines
7.0 KiB
Vue
<script setup lang="ts">
|
|
/**
|
|
* F006 营销活动管理 - W10 A7 标准化
|
|
*
|
|
* W10 A7: 套 PageHeader/SearchBar 标准化模板
|
|
*
|
|
* 后端端点 (ActivityZoneController @ /supplier/activityzone):
|
|
* GET /supplier/activityzone/page 分页列表
|
|
* GET /supplier/activityzone/{id} 详情
|
|
*/
|
|
import { onMounted, ref, reactive } from 'vue'
|
|
import { httpGet } from '@/utils/request'
|
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
|
|
|
interface Activity {
|
|
id: string
|
|
activityName: string
|
|
activityType: number
|
|
status: number
|
|
startTime: string
|
|
endTime: string
|
|
createDate: string
|
|
creatorName: string
|
|
raw: any
|
|
}
|
|
|
|
const ACTIVITY_TYPE_MAP: Record<number, { label: string; type: string }> = {
|
|
1: { label: '秒杀', type: 'danger' },
|
|
2: { label: '拼团', type: 'warning' },
|
|
3: { label: '限时折扣', type: 'success' }
|
|
}
|
|
|
|
const STATUS_MAP: Record<number, { label: string; type: string }> = {
|
|
0: { label: '关闭', type: 'info' },
|
|
1: { label: '开启', type: 'success' }
|
|
}
|
|
|
|
const list = ref<Activity[]>([])
|
|
const total = ref(0)
|
|
const loading = ref(false)
|
|
const page = ref(1)
|
|
const limit = ref(10)
|
|
const loadError = ref<string | null>(null)
|
|
|
|
const filters = reactive({
|
|
activityName: '',
|
|
status: ''
|
|
})
|
|
|
|
const detailVisible = ref(false)
|
|
const detailRow = ref<Activity | null>(null)
|
|
|
|
function fmtDate(ts?: string | number | null) {
|
|
if (!ts) return '--'
|
|
const n = typeof ts === 'string' ? Date.parse(ts) : ts
|
|
if (!n) return String(ts)
|
|
const d = new Date(n)
|
|
if (isNaN(d.getTime())) return String(ts)
|
|
const pad = (x: number) => String(x).padStart(2, '0')
|
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
|
}
|
|
|
|
async function load() {
|
|
loading.value = true
|
|
loadError.value = null
|
|
try {
|
|
const data = await httpGet<{ total: number; list: any[] }>('/supplier/activityzone/page', {
|
|
page: page.value,
|
|
limit: limit.value,
|
|
...(filters.activityName ? { activityName: filters.activityName } : {}),
|
|
...(filters.status !== '' ? { status: Number(filters.status) } : {})
|
|
})
|
|
list.value = (data.list || []).map((it: any) => ({
|
|
id: String(it.id),
|
|
activityName: it.activityName || '--',
|
|
activityType: it.activityType ?? 0,
|
|
status: it.status ?? 0,
|
|
startTime: it.startTime || '--',
|
|
endTime: it.endTime || '--',
|
|
createDate: it.createDate || '--',
|
|
creatorName: it.creatorName || '--',
|
|
raw: it
|
|
}))
|
|
total.value = data.total || 0
|
|
} catch (e: any) {
|
|
loadError.value = e?.msg || e?.message || '加载失败'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function onSearch() { page.value = 1; load() }
|
|
function onReset() { filters.activityName = ''; filters.status = ''; page.value = 1; load() }
|
|
function onPageChange(p: number) { page.value = p; load() }
|
|
function onLimitChange(l: number) { limit.value = l; page.value = 1; load() }
|
|
function showDetail(row: Activity) { detailRow.value = row; detailVisible.value = true }
|
|
|
|
onMounted(() => { load() })
|
|
</script>
|
|
|
|
<template>
|
|
<div class="rr-sys-user">
|
|
<PageHeader title="营销活动" :sub="`共 ${total} 个活动`" />
|
|
|
|
<SearchBar
|
|
v-model:filters="filters"
|
|
:fields="[
|
|
{ key: 'activityName', label: '活动名', type: 'text', placeholder: '活动名搜索' },
|
|
{ key: 'status', label: '状态', type: 'select', options: [
|
|
{ value: 1, label: '开启' },
|
|
{ value: 0, label: '关闭' }
|
|
] }
|
|
]"
|
|
@search="onSearch"
|
|
@reset="onReset"
|
|
/>
|
|
|
|
<div class="rr-list-card">
|
|
<DataTable
|
|
:columns="[
|
|
{ prop: 'activityName', label: '活动名称', width: 220 },
|
|
{ prop: 'activityType', label: '类型', width: 110, slot: 'activityType' },
|
|
{ prop: 'status', label: '状态', width: 90, slot: 'status' },
|
|
{ prop: 'creatorName', label: '创建人', width: 120 },
|
|
{ prop: 'startTime', label: '开始时间', width: 170, slot: 'startTime' },
|
|
{ prop: 'endTime', label: '结束时间', width: 170, slot: 'endTime' },
|
|
{ label: '操作', width: 100, slot: 'action' }
|
|
]"
|
|
:data="list" :borderless="true"
|
|
:loading="loading"
|
|
:total="total"
|
|
:page="page"
|
|
:limit="limit"
|
|
:show-pagination="true" :page-sizes="[10, 20, 50, 100]"
|
|
:error="loadError"
|
|
empty-text="暂无活动"
|
|
row-key="id"
|
|
@page-change="onPageChange"
|
|
@limit-change="onLimitChange"
|
|
>
|
|
<template #activityType="{ row }">
|
|
<el-tag v-if="ACTIVITY_TYPE_MAP[row.activityType]" :type="ACTIVITY_TYPE_MAP[row.activityType].type as any" size="small" effect="light">
|
|
{{ ACTIVITY_TYPE_MAP[row.activityType].label }}
|
|
</el-tag>
|
|
<span v-else class="rr-muted">--</span>
|
|
</template>
|
|
<template #status="{ row }">
|
|
<el-tag v-if="STATUS_MAP[row.status]" :type="STATUS_MAP[row.status].type as any" size="small" effect="light">
|
|
{{ STATUS_MAP[row.status].label }}
|
|
</el-tag>
|
|
<span v-else class="rr-muted">--</span>
|
|
</template>
|
|
<template #startTime="{ row }">
|
|
<span class="rr-time">{{ fmtDate(row.startTime) }}</span>
|
|
</template>
|
|
<template #endTime="{ row }">
|
|
<span class="rr-time">{{ fmtDate(row.endTime) }}</span>
|
|
</template>
|
|
<template #action="{ row }">
|
|
<el-button text type="primary" size="small" @click="showDetail(row)">详情</el-button>
|
|
</template>
|
|
</DataTable>
|
|
</div>
|
|
|
|
<el-dialog v-model="detailVisible" title="活动详情" width="600" :style="{ borderRadius: '12px', overflow: 'hidden' }" align-center destroy-on-close>
|
|
<template v-if="detailRow">
|
|
<el-descriptions :column="2" border size="default">
|
|
<el-descriptions-item label="活动名称" :span="2">{{ detailRow.activityName }}</el-descriptions-item>
|
|
<el-descriptions-item label="类型">{{ ACTIVITY_TYPE_MAP[detailRow.activityType]?.label || '--' }}</el-descriptions-item>
|
|
<el-descriptions-item label="状态">{{ STATUS_MAP[detailRow.status]?.label || '--' }}</el-descriptions-item>
|
|
<el-descriptions-item label="创建人">{{ detailRow.creatorName }}</el-descriptions-item>
|
|
<el-descriptions-item label="开始时间">{{ fmtDate(detailRow.startTime) }}</el-descriptions-item>
|
|
<el-descriptions-item label="结束时间">{{ fmtDate(detailRow.endTime) }}</el-descriptions-item>
|
|
</el-descriptions>
|
|
</template>
|
|
<template #footer>
|
|
<el-button @click="detailVisible = false">关闭</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.rr-sys-user { padding: 16px; }
|
|
.rr-list-card { background: var(--surface); border-radius: 12px; padding: 4px; }
|
|
.rr-time { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 12px; color: var(--ink-2); }
|
|
.rr-muted { color: var(--ink-3); font-size: 12px; }
|
|
</style>
|