Merge remote-tracking branch 'origin/huangzhenbao-admin'
This commit is contained in:
@@ -1,285 +0,0 @@
|
||||
<template>
|
||||
<view class="marketing-bargain-list">
|
||||
<view class="filter-card border-shadow">
|
||||
<view class="filter-row">
|
||||
<view class="filter-item">
|
||||
<text class="label">活动搜索:</text>
|
||||
<input class="input-mock" placeholder="请输入活动名称" v-model="searchQuery" @confirm="handleSearch" />
|
||||
</view>
|
||||
<view class="filter-btns">
|
||||
<button class="btn-query" @click="handleSearch">查询</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="action-bar">
|
||||
<button class="btn-add" @click="handleAdd">添加砍价活动</button>
|
||||
</view>
|
||||
|
||||
<view class="table-card border-shadow">
|
||||
<view class="table-container">
|
||||
<!-- Loading 遮罩 -->
|
||||
<view v-if="isLoading" class="loading-mask">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view class="table-head">
|
||||
<view class="th cell-id">ID</view>
|
||||
<view class="th cell-product">关联商品</view>
|
||||
<view class="th cell-title">活动标题</view>
|
||||
<view class="th cell-price">最低价</view>
|
||||
<view class="th cell-stock">库存</view>
|
||||
<view class="th cell-time">活动时间</view>
|
||||
<view class="th cell-status">状态</view>
|
||||
<view class="th cell-op">操作</view>
|
||||
</view>
|
||||
|
||||
<view class="table-body">
|
||||
<view v-if="bargainList.length === 0 && !isLoading" class="empty-row">
|
||||
<text>暂无砍价活动</text>
|
||||
</view>
|
||||
<view v-for="item in bargainList" :key="item.id" class="table-row">
|
||||
<view class="td cell-id">
|
||||
<text class="td-txt-small">{{ item.id }}</text>
|
||||
</view>
|
||||
<view class="td cell-product">
|
||||
<view class="p-info">
|
||||
<image class="p-img" :src="item.product_image || '/static/logo.png'" mode="aspectFill" />
|
||||
<text class="p-name line-clamp-1">{{ item.product_name || '未知商品' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="td cell-title">
|
||||
<text class="td-txt">{{ item.title }}</text>
|
||||
</view>
|
||||
<view class="td cell-price">
|
||||
<text class="td-txt danger">¥{{ item.min_price }}</text>
|
||||
</view>
|
||||
<view class="td cell-stock">
|
||||
<text class="td-txt">{{ item.stock }}</text>
|
||||
</view>
|
||||
<view class="td cell-time">
|
||||
<text class="td-txt-small">始: {{ formatTime(item.start_time) }}</text>
|
||||
<text class="td-txt-small">终: {{ formatTime(item.stop_time) }}</text>
|
||||
</view>
|
||||
<view class="td cell-status">
|
||||
<view class="switch-mock" :class="{ active: item.status }" @click="toggleStatus(item)">
|
||||
<view class="switch-dot"></view>
|
||||
<text class="switch-txt">{{ item.status ? '开启' : '关闭' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="td cell-op">
|
||||
<view class="op-links">
|
||||
<text class="op-link" @click="handleEdit(item)">编辑</text>
|
||||
<text class="op-split">|</text>
|
||||
<text class="op-link danger" @click="handleDelete(item)">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="pagination-footer">
|
||||
<view class="page-total">
|
||||
<text class="total-txt">共 {{ total }} 条</text>
|
||||
</view>
|
||||
<view class="page-btns">
|
||||
<text class="p-btn" :class="{ disabled: page <= 1 }" @click="onPrevPage">‹</text>
|
||||
<text class="p-btn active">{{ page }}</text>
|
||||
<text class="p-btn" :class="{ disabled: bargainList.length < pageSize }" @click="onNextPage">›</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { fetchBargainActivities, saveBargainActivity, deleteBargainActivity, BargainActivity } from '@/services/admin/marketingService.uts'
|
||||
|
||||
const bargainList = ref<BargainActivity[]>([])
|
||||
const isLoading = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const page = ref(1)
|
||||
const pageSize = 15
|
||||
const total = ref(0)
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
async function loadData() {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const res = await fetchBargainActivities({
|
||||
search: searchQuery.value,
|
||||
page: page.value,
|
||||
pageSize: pageSize
|
||||
})
|
||||
bargainList.value = res.items
|
||||
total.value = res.total
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
page.value = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
async function toggleStatus(item : BargainActivity) {
|
||||
if (item.id == null) return
|
||||
const nextStatus = !item.status
|
||||
const success = await saveBargainActivity({ ...item, status: nextStatus } as BargainActivity)
|
||||
if (success) {
|
||||
item.status = nextStatus
|
||||
uni.showToast({ title: '修改成功', icon: 'success' })
|
||||
}
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
uni.showToast({ title: '添加功能开发中', icon: 'none' })
|
||||
}
|
||||
|
||||
function handleEdit(item : BargainActivity) {
|
||||
uni.showToast({ title: '编辑功能开发中', icon: 'none' })
|
||||
}
|
||||
|
||||
async function handleDelete(item : BargainActivity) {
|
||||
if (item.id == null) return
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该砍价活动吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
const success = await deleteBargainActivity(item.id!)
|
||||
if (success) {
|
||||
uni.showToast({ title: '删除成功' })
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function onPrevPage() {
|
||||
if (page.value > 1) {
|
||||
page.value--
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
|
||||
function onNextPage() {
|
||||
if (bargainList.value.length >= pageSize) {
|
||||
page.value++
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(iso: string): string {
|
||||
if (!iso) return '-'
|
||||
return iso.substring(0, 10)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.marketing-bargain-list {
|
||||
min-height: 100vh;
|
||||
background: #f0f2f5;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.border-shadow {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.filter-card { padding: 24px; margin-bottom: 16px; }
|
||||
.filter-row { display: flex; flex-direction: row; align-items: center; gap: 24px; }
|
||||
.filter-item { display: flex; flex-direction: row; align-items: center; }
|
||||
.label { font-size: 14px; color: #606266; }
|
||||
.input-mock {
|
||||
width: 240px; height: 32px; border: 1px solid #dcdfe6; border-radius: 4px;
|
||||
padding: 0 12px; font-size: 13px;
|
||||
}
|
||||
.btn-query {
|
||||
width: 64px; height: 32px; background-color: #1890ff; color: #fff;
|
||||
font-size: 14px; border: none; border-radius: 4px; cursor: pointer;
|
||||
}
|
||||
|
||||
.action-bar { margin-bottom: 16px; }
|
||||
.btn-add {
|
||||
padding: 0 16px; height: 32px; background-color: #1890ff; color: #fff;
|
||||
font-size: 14px; border: none; border-radius: 4px; cursor: pointer;
|
||||
}
|
||||
|
||||
.table-card { padding: 24px; position: relative; }
|
||||
.table-container { min-height: 400px; position: relative; }
|
||||
.loading-mask {
|
||||
position: absolute; top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(255,255,255,0.7); display: flex; align-items: center; justify-content: center; z-index: 10;
|
||||
}
|
||||
.loading-text { color: #1890ff; font-size: 14px; }
|
||||
|
||||
.table-head {
|
||||
display: flex; flex-direction: row; background-color: #f8f8f9;
|
||||
border-bottom: 1px solid #e8eaec;
|
||||
}
|
||||
.th { padding: 12px 8px; font-size: 13px; color: #515a6e; font-weight: bold; text-align: center; }
|
||||
|
||||
.table-row {
|
||||
display: flex; flex-direction: row; border-bottom: 1px solid #e8eaec; align-items: center;
|
||||
&:hover { background-color: #fafafa; }
|
||||
}
|
||||
.td { padding: 12px 8px; text-align: center; display: flex; align-items: center; justify-content: center; }
|
||||
.td-txt { font-size: 13px; color: #515a6e; }
|
||||
.td-txt-small { font-size: 12px; color: #999; display: block; }
|
||||
.danger { color: #f5222d; }
|
||||
|
||||
.cell-id { width: 60px; }
|
||||
.cell-product { flex: 1.5; justify-content: flex-start; }
|
||||
.cell-title { flex: 1; }
|
||||
.cell-price { width: 100px; }
|
||||
.cell-stock { width: 80px; }
|
||||
.cell-time { width: 160px; }
|
||||
.cell-status { width: 100px; }
|
||||
.cell-op { width: 120px; }
|
||||
|
||||
.p-info { display: flex; flex-direction: row; align-items: center; gap: 8px; }
|
||||
.p-img { width: 32px; height: 32px; border-radius: 4px; background: #f5f5f5; }
|
||||
.p-name { font-size: 12px; color: #1890ff; text-align: left; }
|
||||
.line-clamp-1 { display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
|
||||
.switch-mock {
|
||||
width: 44px; height: 20px; background-color: #bfbfbf; border-radius: 10px;
|
||||
position: relative; transition: all 0.3s;
|
||||
&.active { background-color: #1890ff; }
|
||||
}
|
||||
.switch-dot {
|
||||
width: 16px; height: 16px; background-color: #fff; border-radius: 50%;
|
||||
position: absolute; top: 2px; left: 2px; transition: left 0.3s;
|
||||
}
|
||||
.active .switch-dot { left: 26px; }
|
||||
.switch-txt { font-size: 10px; color: #fff; position: absolute; right: 6px; top: 2px; }
|
||||
.active .switch-txt { left: 6px; right: auto; }
|
||||
|
||||
.op-links { display: flex; flex-direction: row; gap: 8px; }
|
||||
.op-link { font-size: 13px; color: #1890ff; cursor: pointer; &.danger { color: #f5222d; } }
|
||||
.op-split { color: #eee; }
|
||||
|
||||
.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: 28px; height: 28px; border: 1px solid #dcdfe6; border-radius: 4px;
|
||||
display: flex; align-items: center; justify-content: center; font-size: 14px;
|
||||
&.active { background: #1890ff; color: #fff; border-color: #1890ff; }
|
||||
&.disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
}
|
||||
.empty-row { padding: 60px 0; text-align: center; color: #999; }
|
||||
</style>
|
||||
82
pages/mall/admin/marketing/bargain/list/index.uvue
Normal file
82
pages/mall/admin/marketing/bargain/list/index.uvue
Normal file
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<view class="page-header">
|
||||
<text class="page-title">砍价活动</text>
|
||||
<text class="page-subtitle">Component: MarketingBargain</text>
|
||||
</view>
|
||||
|
||||
<view class="page-content">
|
||||
<view class="placeholder-card">
|
||||
<text class="placeholder-title">页面占位</text>
|
||||
<text class="placeholder-desc">该功能模块正在开发中</text>
|
||||
<text class="placeholder-info">当前采用 CRMEB 路由体系 1:1 映射</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
// TODO: 实现 砍价活动 的具体功能
|
||||
const loading = ref<boolean>(false)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-container {
|
||||
/* 使用 Layout 的背景和内边距 */
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.placeholder-card {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.placeholder-title {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.placeholder-desc {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.placeholder-info {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #1890ff;
|
||||
}
|
||||
</style>
|
||||
80
pages/mall/admin/marketing/bargain/products/index.uvue
Normal file
80
pages/mall/admin/marketing/bargain/products/index.uvue
Normal file
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<view class="page-header">
|
||||
<text class="page-title">砍价活动</text>
|
||||
<text class="page-subtitle">Component: MarketingBargain</text>
|
||||
</view>
|
||||
|
||||
<view class="page-content">
|
||||
<view class="placeholder-card">
|
||||
<text class="placeholder-title">页面占位</text>
|
||||
<text class="placeholder-desc">该功能模块正在开发中</text>
|
||||
<text class="placeholder-info">当前采用 CRMEB 路由体系 1:1 映射</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
// TODO: 实现 砍价活动 的具体功能
|
||||
const loading = ref<boolean>(false)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-container {
|
||||
/* 使用 Layout 的背景和内边距 */
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.placeholder-card {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.placeholder-title {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.placeholder-desc {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.placeholder-info {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #1890ff;
|
||||
}
|
||||
</style>
|
||||
@@ -166,9 +166,7 @@ async function handleSave() {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.marketing-checkin-config {
|
||||
padding: 16px;
|
||||
background: #f0f2f5;
|
||||
min-height: 100vh;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.border-shadow {
|
||||
@@ -23,7 +23,7 @@
|
||||
<view class="th cell-type">类型</view>
|
||||
<view class="th cell-days">签到天数</view>
|
||||
<view class="th cell-reward">奖励内容</view>
|
||||
<view class="th cell-status">是否可用</view>
|
||||
<view class="th cell-status">状态</view>
|
||||
<view class="th cell-op">操作</view>
|
||||
</view>
|
||||
|
||||
@@ -36,9 +36,7 @@
|
||||
<text class="td-txt">积分+{{ item.integral }}, 经验+{{ item.exp }}</text>
|
||||
</view>
|
||||
<view class="td cell-status">
|
||||
<view class="switch-mock" :class="{ active: item.is_open }" @click="toggleStatus(item)">
|
||||
<view class="switch-dot"></view>
|
||||
</view>
|
||||
<StatusSwitch v-model="item.is_open" />
|
||||
</view>
|
||||
<view class="td cell-op">
|
||||
<text class="op-link" @click="handleEdit(item)">编辑</text>
|
||||
@@ -81,6 +79,7 @@
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import StatusSwitch from '@/components/StatusSwitch.uvue'
|
||||
|
||||
const currentTab = ref('continuous')
|
||||
const showModal = ref(false)
|
||||
@@ -114,11 +113,6 @@ const openModal = (type: string) => {
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
const toggleStatus = (item: any) => {
|
||||
item.is_open = !item.is_open
|
||||
uni.showToast({ title: '修改成功', icon: 'success' })
|
||||
}
|
||||
|
||||
const handleEdit = (item: any) => {
|
||||
uni.showToast({ title: '编辑功能暂未对接', icon: 'none' })
|
||||
}
|
||||
@@ -168,9 +162,9 @@ const handleSubmit = () => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.marketing-checkin-reward {
|
||||
padding: 16px;
|
||||
background: #f0f2f5;
|
||||
min-height: 100vh;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.border-shadow {
|
||||
@@ -232,24 +226,12 @@ const handleSubmit = () => {
|
||||
.cell-days { width: 120px; }
|
||||
.cell-reward { flex: 1; }
|
||||
.cell-status { width: 100px; text-align: center; }
|
||||
.cell-op { width: 150px; text-align: right; }
|
||||
.cell-op { width: 150px; text-align: right; display: flex; flex-direction: row; }
|
||||
|
||||
.op-link { color: #1890ff; font-size: 13px; cursor: pointer; }
|
||||
.op-link.del { color: #ff4d4f; }
|
||||
.ml-10 { margin-left: 10px; }
|
||||
|
||||
.switch-mock {
|
||||
width: 44px; height: 22px; background-color: #bfbfbf; border-radius: 11px;
|
||||
display: flex; align-items: center; padding: 0 4px; position: relative;
|
||||
transition: background-color 0.3s; cursor: pointer;
|
||||
}
|
||||
.switch-mock.active { background-color: #1890ff; }
|
||||
.switch-dot {
|
||||
width: 14px; height: 14px; background-color: #fff; border-radius: 50%;
|
||||
position: absolute; left: 4px; transition: left 0.3s;
|
||||
}
|
||||
.switch-mock.active .switch-dot { left: 26px; }
|
||||
|
||||
/* Modal */
|
||||
.modal-mask {
|
||||
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||||
@@ -49,7 +49,7 @@
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label required">拼团简介:</text>
|
||||
<textarea class="textarea" v-model="form.info" placeholder="请输入拼团简介" />
|
||||
<textarea class="textarea" v-model="form.info" placeholder="请输入拼团简介"></textarea>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label required">拼团时间:</text>
|
||||
@@ -204,9 +204,9 @@ const prevStep = () => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.marketing-combination-create {
|
||||
background-color: #f5f7fa;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.border-shadow {
|
||||
433
pages/mall/admin/marketing/combination/index.uvue
Normal file
433
pages/mall/admin/marketing/combination/index.uvue
Normal file
@@ -0,0 +1,433 @@
|
||||
<template>
|
||||
<view class="marketing-combination-list">
|
||||
<view class="filter-card border-shadow">
|
||||
<view class="filter-row">
|
||||
<view class="filter-item">
|
||||
<text class="label">时间选择:</text>
|
||||
<view class="date-picker-mock">
|
||||
<text class="calendar-ic">📅</text>
|
||||
<text class="date-placeholder">开始日期 - 结束日期</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="filter-item">
|
||||
<text class="label">拼团状态:</text>
|
||||
<view class="select-mock">
|
||||
<text class="select-val">请选择</text>
|
||||
<text class="arrow">▼</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="stats-board">
|
||||
<view class="stat-card border-shadow">
|
||||
<view class="stat-icon-box bg-blue">
|
||||
<text class="stat-ic">👥</text>
|
||||
</view>
|
||||
<view class="stat-info">
|
||||
<text class="stat-value">349</text>
|
||||
<text class="stat-label">参与人数(人)</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="stat-card border-shadow">
|
||||
<view class="stat-icon-box bg-orange">
|
||||
<text class="stat-ic">📦</text>
|
||||
</view>
|
||||
<view class="stat-info">
|
||||
<text class="stat-value">44</text>
|
||||
<text class="stat-label">成团数量(个)</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="table-card border-shadow">
|
||||
<view class="table-container">
|
||||
<view class="table-head">
|
||||
<view class="th cell-avatar">头像</view>
|
||||
<view class="th cell-leader">开团团长</view>
|
||||
<view class="th cell-time">开团时间</view>
|
||||
<view class="th cell-product">拼团商品</view>
|
||||
<view class="th cell-group">几人团</view>
|
||||
<view class="th cell-num">几人参加</view>
|
||||
<view class="th cell-time">结束时间</view>
|
||||
<view class="th cell-status">状态</view>
|
||||
<view class="th cell-op">操作</view>
|
||||
</view>
|
||||
|
||||
<view class="table-body">
|
||||
<view v-for="item in combos" :key="item.id" class="table-row">
|
||||
<view class="td cell-avatar">
|
||||
<image class="thumb" :src="item.avatar" mode="aspectFill"></image>
|
||||
</view>
|
||||
<view class="td cell-leader">
|
||||
<text class="td-txt">{{ item.nickname }} / {{ item.uid }}</text>
|
||||
</view>
|
||||
<view class="td cell-time">
|
||||
<text class="td-txt-small">{{ item.start_time }}</text>
|
||||
</view>
|
||||
<view class="td cell-product">
|
||||
<text class="product-title line-clamp-2">{{ item.title }} / {{ item.cid }}</text>
|
||||
</view>
|
||||
<view class="td cell-group">
|
||||
<text class="td-txt">{{ item.people }}</text>
|
||||
</view>
|
||||
<view class="td cell-num">
|
||||
<text class="td-txt-bold">{{ item.count_people }}</text>
|
||||
</view>
|
||||
<view class="td cell-time">
|
||||
<text class="td-txt-small">{{ item.stop_time }}</text>
|
||||
</view>
|
||||
<view class="td cell-status">
|
||||
<view :class="['status-tag', item.status]">
|
||||
<text class="tag-txt">{{ statusLabels[item.status] }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="td cell-op">
|
||||
<view class="op-links">
|
||||
<text class="op-link" @click="viewDetails(item)">查看详情</text>
|
||||
<text class="op-split" v-if="item.status === 'ongoing'">|</text>
|
||||
<text class="op-link" v-if="item.status === 'ongoing'" @click="completeGroup(item)">立即成团</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分页 -->
|
||||
<CommonPagination
|
||||
v-if="combos.length > 0"
|
||||
:total="combos.length"
|
||||
:loading="false"
|
||||
:currentPage="currentPage"
|
||||
:pageSize="pageSize"
|
||||
:pageSizeOptionLabels="pageSizeOptionLabels"
|
||||
:pageSizeIndex="pageSizeIndex"
|
||||
:visiblePages="visiblePages"
|
||||
:totalPage="totalPage"
|
||||
:jumpPageInput="jumpPageInput"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
@page-change="handlePageChange"
|
||||
@update:jumpPageInput="(val : string) => { jumpPageInput = val }"
|
||||
@jump-page="handleJumpPage"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import CommonPagination from '@/components/CommonPagination/CommonPagination.uvue'
|
||||
|
||||
const statusLabels = {
|
||||
ongoing: '进行中',
|
||||
pending: '未完成',
|
||||
ended: '已成功',
|
||||
}
|
||||
|
||||
const combos = ref([
|
||||
{
|
||||
id: 101,
|
||||
avatar: 'https://img0.baidu.com/it/u=3033502919,1657850259&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
|
||||
nickname: '1岁上班22岁退休',
|
||||
uid: 82713,
|
||||
start_time: '2026-02-03 10:09',
|
||||
stop_time: '2026-02-04 10:09',
|
||||
title: 'FOMIX 蛋壳椅 进口头层牛皮自然色单人沙发椅 Egg chair设计师蛋椅',
|
||||
cid: 191,
|
||||
people: 2,
|
||||
count_people: 1,
|
||||
status: 'ongoing',
|
||||
},
|
||||
{
|
||||
id: 102,
|
||||
avatar: 'https://img1.baidu.com/it/u=2295552459,2083538461&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
|
||||
nickname: '132****8769',
|
||||
uid: 82683,
|
||||
start_time: '2026-02-01 13:29',
|
||||
stop_time: '2026-02-02 13:29',
|
||||
title: '阿迪达斯官网 adidas BBALL CAP COT 男女训练运动帽子FQ5270',
|
||||
cid: 192,
|
||||
people: 2,
|
||||
count_people: 1,
|
||||
status: 'ongoing',
|
||||
},
|
||||
{
|
||||
id: 103,
|
||||
avatar: 'https://img0.baidu.com/it/u=1550993072,4086699313&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
|
||||
nickname: 'Jk',
|
||||
uid: 82598,
|
||||
start_time: '2026-01-28 16:10',
|
||||
stop_time: '2026-01-29 16:10',
|
||||
title: 'FOMIX 蛋壳椅 进口头层牛皮自然色单人沙发椅 Egg chair设计师蛋椅',
|
||||
cid: 191,
|
||||
people: 2,
|
||||
count_people: 1,
|
||||
status: 'ongoing',
|
||||
},
|
||||
{
|
||||
id: 104,
|
||||
avatar: 'https://img1.baidu.com/it/u=3175865615,2002599723&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
|
||||
nickname: '177****1523',
|
||||
uid: 82565,
|
||||
start_time: '2026-01-27 07:19',
|
||||
stop_time: '2026-01-28 07:19',
|
||||
title: 'FOMIX 蛋壳椅 进口头层牛皮自然色单人沙发椅 Egg chair设计师蛋椅',
|
||||
cid: 191,
|
||||
people: 2,
|
||||
count_people: 1,
|
||||
status: 'ongoing',
|
||||
},
|
||||
{
|
||||
id: 105,
|
||||
avatar: 'https://img2.baidu.com/it/u=2719717192,3826027113&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
|
||||
nickname: '0-1',
|
||||
uid: 79417,
|
||||
start_time: '2026-01-25 23:53',
|
||||
stop_time: '2026-01-26 23:53',
|
||||
title: 'FOMIX 蛋壳椅 进口头层牛皮自然色单人沙发椅 Egg chair设计师蛋椅',
|
||||
cid: 191,
|
||||
people: 2,
|
||||
count_people: 1,
|
||||
status: 'ongoing',
|
||||
},
|
||||
{
|
||||
id: 106,
|
||||
avatar: 'https://img0.baidu.com/it/u=1893322197,2940863863&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
|
||||
nickname: 'abc',
|
||||
uid: 75343,
|
||||
start_time: '2026-01-22 21:29',
|
||||
stop_time: '2026-01-23 21:29',
|
||||
title: 'FOMIX 蛋壳椅 进口头层牛皮自然色单人沙发椅 Egg chair设计师蛋椅',
|
||||
cid: 191,
|
||||
people: 2,
|
||||
count_people: 1,
|
||||
status: 'ongoing',
|
||||
},
|
||||
{
|
||||
id: 107,
|
||||
avatar: 'https://img2.baidu.com/it/u=176219800,2487920112&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
|
||||
nickname: '181****6910',
|
||||
uid: 81141,
|
||||
start_time: '2026-01-19 16:16',
|
||||
stop_time: '2026-01-19 16:45',
|
||||
title: 'UR2024夏季新款女装复古纯欲氛围感一字肩短款T恤UWG440060',
|
||||
cid: 190,
|
||||
people: 2,
|
||||
count_people: 1,
|
||||
status: 'pending',
|
||||
},
|
||||
])
|
||||
|
||||
const viewDetails = (item: any) => {
|
||||
console.log('查看详情', item.id)
|
||||
}
|
||||
|
||||
const completeGroup = (item: any) => {
|
||||
console.log('立即成团', item.id)
|
||||
}
|
||||
|
||||
// 分页适配状态
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(15)
|
||||
let jumpPageInput = ''
|
||||
const pageSizeOptions = [10, 15, 20, 30, 50]
|
||||
const pageSizeOptionLabels = computed(() => pageSizeOptions.map((n: number) => `${n}条/页`))
|
||||
const pageSizeIndex = computed(() => {
|
||||
const idx = pageSizeOptions.indexOf(pageSize.value)
|
||||
return idx >= 0 ? idx : 0
|
||||
})
|
||||
const totalPage = computed(() => Math.max(1, Math.ceil(combos.value.length / pageSize.value)))
|
||||
const visiblePages = computed(() => {
|
||||
const t = totalPage.value
|
||||
const cur = currentPage.value
|
||||
if (t <= 7) return Array.from({ length: t }, (_: any, i: number) => i + 1)
|
||||
if (cur <= 4) return [1, 2, 3, 4, 5, -1, t]
|
||||
if (cur >= t - 3) return [1, -1, t - 4, t - 3, t - 2, t - 1, t]
|
||||
return [1, -1, cur - 1, cur, cur + 1, -1, t]
|
||||
})
|
||||
const handlePageChange = (p: number) => { currentPage.value = p }
|
||||
const handlePageSizeChange = (e: any) => {
|
||||
const idx = Number(e.detail.value)
|
||||
pageSize.value = pageSizeOptions[idx] ?? pageSizeOptions[0]
|
||||
currentPage.value = 1
|
||||
}
|
||||
const handleJumpPage = () => {
|
||||
const p = parseInt(jumpPageInput)
|
||||
if (!isNaN(p) && p >= 1 && p <= totalPage.value) currentPage.value = p
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.marketing-combination-list {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.border-shadow {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 过滤栏 */
|
||||
.filter-card {
|
||||
padding: var(--admin-card-padding);
|
||||
margin-bottom: var(--admin-section-gap);
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.date-picker-mock, .select-mock {
|
||||
width: 280px;
|
||||
height: 32px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
}
|
||||
.select-mock { width: 220px; justify-content: space-between; }
|
||||
.select-mock.mini { width: 100px; height: 28px; }
|
||||
|
||||
.calendar-ic { font-size: 14px; color: #c0c4cc; margin-right: 8px; }
|
||||
.date-placeholder { font-size: 13px; color: #c0c4cc; }
|
||||
.select-val { font-size: 14px; color: #c0c4cc; }
|
||||
.arrow { font-size: 10px; color: #c0c4cc; }
|
||||
|
||||
/* 统计卡片 */
|
||||
.stats-board {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--admin-section-gap);
|
||||
margin-bottom: var(--admin-section-gap);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
flex: 1;
|
||||
height: 120px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0 40px;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.stat-icon-box {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.bg-blue { background-color: #ecf5ff; }
|
||||
.bg-orange { background-color: #fff7eb; }
|
||||
.stat-ic { font-size: 32px; }
|
||||
|
||||
.stat-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.stat-value { font-size: 32px; font-weight: 600; color: #303133; }
|
||||
.stat-label { font-size: 13px; color: #909399; margin-top: 4px; }
|
||||
|
||||
/* 表格区域 */
|
||||
.table-card {
|
||||
padding: var(--admin-card-padding);
|
||||
}
|
||||
|
||||
.table-head {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: #f8f8f9;
|
||||
border-bottom: 1px solid #e8eaec;
|
||||
}
|
||||
|
||||
.th {
|
||||
padding: 12px 8px;
|
||||
font-size: 13px;
|
||||
color: #515a6e;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-bottom: 1px solid #e8eaec;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.td {
|
||||
padding: 16px 8px;
|
||||
}
|
||||
|
||||
.td-txt { font-size: 14px; color: #515a6e; }
|
||||
.td-txt-small { font-size: 13px; color: #808695; }
|
||||
.td-txt-bold { font-size: 14px; color: #515a6e; font-weight: bold; }
|
||||
|
||||
/* 各列宽度 */
|
||||
.cell-avatar { width: 80px; }
|
||||
.cell-leader { width: 160px; }
|
||||
.cell-time { width: 160px; }
|
||||
.cell-product { flex: 1; min-width: 260px; }
|
||||
.cell-group { width: 80px; text-align: center; }
|
||||
.cell-num { width: 80px; text-align: center; }
|
||||
.cell-status { width: 100px; text-align: center; }
|
||||
.cell-op { width: 160px; text-align: right; }
|
||||
|
||||
.thumb {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.product-title {
|
||||
font-size: 13px;
|
||||
color: #515a6e;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.status-tag.ongoing { background-color: #f0f7ff; border: 1px solid #d1e9ff; }
|
||||
.status-tag.ongoing .tag-txt { color: #1890ff; }
|
||||
.status-tag.pending { background-color: #fff7e6; border: 1px solid #ffe7ba; }
|
||||
.status-tag.pending .tag-txt { color: #fa8c16; }
|
||||
|
||||
.op-links {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
.op-link { color: #1890ff; font-size: 13px; cursor: pointer; }
|
||||
.op-split { color: #e8eaec; margin: 0 8px; }
|
||||
|
||||
/* 分页区域已迁至 CommonPagination 组件 */
|
||||
</style>
|
||||
@@ -25,7 +25,7 @@
|
||||
<text class="stat-ic">👥</text>
|
||||
</view>
|
||||
<view class="stat-info">
|
||||
<text class="stat-value">{{ statParticipants }}</text>
|
||||
<text class="stat-value">349</text>
|
||||
<text class="stat-label">参与人数(人)</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -34,7 +34,7 @@
|
||||
<text class="stat-ic">📦</text>
|
||||
</view>
|
||||
<view class="stat-info">
|
||||
<text class="stat-value">{{ statSuccessGroups }}</text>
|
||||
<text class="stat-value">44</text>
|
||||
<text class="stat-label">成团数量(个)</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -42,10 +42,6 @@
|
||||
|
||||
<view class="table-card border-shadow">
|
||||
<view class="table-container">
|
||||
<!-- Loading 遮罩 -->
|
||||
<view v-if="isLoading" class="loading-mask">
|
||||
<text class="loading-text">数据加载中...</text>
|
||||
</view>
|
||||
<view class="table-head">
|
||||
<view class="th cell-avatar">头像</view>
|
||||
<view class="th cell-leader">开团团长</view>
|
||||
@@ -59,18 +55,15 @@
|
||||
</view>
|
||||
|
||||
<view class="table-body">
|
||||
<view v-if="combos.length === 0 && !isLoading" class="empty-row">
|
||||
<text>暂无拼团记录</text>
|
||||
</view>
|
||||
<view v-for="item in combos" :key="item.id" class="table-row">
|
||||
<view class="td cell-avatar">
|
||||
<image class="thumb" :src="item.avatar || '/static/logo.png'" mode="aspectFill"></image>
|
||||
<image class="thumb" :src="item.avatar" mode="aspectFill"></image>
|
||||
</view>
|
||||
<view class="td cell-leader">
|
||||
<text class="td-txt">{{ item.nickname || '未知用户' }} / {{ item.uid }}</text>
|
||||
<text class="td-txt">{{ item.nickname }} / {{ item.uid }}</text>
|
||||
</view>
|
||||
<view class="td cell-time">
|
||||
<text class="td-txt-small">{{ item.start_time.substring(0, 16).replace('T', ' ') }}</text>
|
||||
<text class="td-txt-small">{{ item.start_time }}</text>
|
||||
</view>
|
||||
<view class="td cell-product">
|
||||
<text class="product-title line-clamp-2">{{ item.title }} / {{ item.cid }}</text>
|
||||
@@ -82,7 +75,7 @@
|
||||
<text class="td-txt-bold">{{ item.count_people }}</text>
|
||||
</view>
|
||||
<view class="td cell-time">
|
||||
<text class="td-txt-small">{{ item.stop_time.substring(0, 16).replace('T', ' ') }}</text>
|
||||
<text class="td-txt-small">{{ item.stop_time }}</text>
|
||||
</view>
|
||||
<view class="td cell-status">
|
||||
<view :class="['status-tag', item.status]">
|
||||
@@ -93,129 +86,182 @@
|
||||
<view class="op-links">
|
||||
<text class="op-link" @click="viewDetails(item)">查看详情</text>
|
||||
<text class="op-split" v-if="item.status === 'ongoing'">|</text>
|
||||
<text class="op-link" v-if="item.status === 'ongoing'" @click="handleCompleteGroup(item)">立即成团</text>
|
||||
<text class="op-link" v-if="item.status === 'ongoing'" @click="completeGroup(item)">立即成团</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="pagination-footer">
|
||||
<view class="page-total">
|
||||
<text class="total-txt">共 {{ total }} 条</text>
|
||||
</view>
|
||||
<view class="page-btns">
|
||||
<text class="p-btn" :class="{ disabled: page <= 1 }" @click="onPrevPage">‹</text>
|
||||
<text class="p-btn active">{{ page }}</text>
|
||||
<text class="p-btn" :class="{ disabled: combos.length < pageSize }" @click="onNextPage">›</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 分页 -->
|
||||
<CommonPagination
|
||||
v-if="combos.length > 0"
|
||||
:total="combos.length"
|
||||
:loading="false"
|
||||
:currentPage="currentPage"
|
||||
:pageSize="pageSize"
|
||||
:pageSizeOptionLabels="pageSizeOptionLabels"
|
||||
:pageSizeIndex="pageSizeIndex"
|
||||
:visiblePages="visiblePages"
|
||||
:totalPage="totalPage"
|
||||
:jumpPageInput="jumpPageInput"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
@page-change="handlePageChange"
|
||||
@update:jumpPageInput="(val : string) => { jumpPageInput = val }"
|
||||
@jump-page="handleJumpPage"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { fetchCombinationActivities, getCombinationStats, completeCombinationGroup, CombinationActivity } from '@/services/admin/marketingService.uts'
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import CommonPagination from '@/components/CommonPagination/CommonPagination.uvue'
|
||||
|
||||
const statusLabels : Record<string, string> = {
|
||||
const statusLabels = {
|
||||
ongoing: '进行中',
|
||||
pending: '未完成',
|
||||
ended: '已成功',
|
||||
}
|
||||
|
||||
const combos = ref<CombinationActivity[]>([])
|
||||
const isLoading = ref(false)
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = 15
|
||||
const combos = ref([
|
||||
{
|
||||
id: 101,
|
||||
avatar: 'https://img0.baidu.com/it/u=3033502919,1657850259&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
|
||||
nickname: '1岁上班22岁退休',
|
||||
uid: 82713,
|
||||
start_time: '2026-02-03 10:09',
|
||||
stop_time: '2026-02-04 10:09',
|
||||
title: 'FOMIX 蛋壳椅 进口头层牛皮自然色单人沙发椅 Egg chair设计师蛋椅',
|
||||
cid: 191,
|
||||
people: 2,
|
||||
count_people: 1,
|
||||
status: 'ongoing',
|
||||
},
|
||||
{
|
||||
id: 102,
|
||||
avatar: 'https://img1.baidu.com/it/u=2295552459,2083538461&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
|
||||
nickname: '132****8769',
|
||||
uid: 82683,
|
||||
start_time: '2026-02-01 13:29',
|
||||
stop_time: '2026-02-02 13:29',
|
||||
title: '阿迪达斯官网 adidas BBALL CAP COT 男女训练运动帽子FQ5270',
|
||||
cid: 192,
|
||||
people: 2,
|
||||
count_people: 1,
|
||||
status: 'ongoing',
|
||||
},
|
||||
{
|
||||
id: 103,
|
||||
avatar: 'https://img0.baidu.com/it/u=1550993072,4086699313&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
|
||||
nickname: 'Jk',
|
||||
uid: 82598,
|
||||
start_time: '2026-01-28 16:10',
|
||||
stop_time: '2026-01-29 16:10',
|
||||
title: 'FOMIX 蛋壳椅 进口头层牛皮自然色单人沙发椅 Egg chair设计师蛋椅',
|
||||
cid: 191,
|
||||
people: 2,
|
||||
count_people: 1,
|
||||
status: 'ongoing',
|
||||
},
|
||||
{
|
||||
id: 104,
|
||||
avatar: 'https://img1.baidu.com/it/u=3175865615,2002599723&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
|
||||
nickname: '177****1523',
|
||||
uid: 82565,
|
||||
start_time: '2026-01-27 07:19',
|
||||
stop_time: '2026-01-28 07:19',
|
||||
title: 'FOMIX 蛋壳椅 进口头层牛皮自然色单人沙发椅 Egg chair设计师蛋椅',
|
||||
cid: 191,
|
||||
people: 2,
|
||||
count_people: 1,
|
||||
status: 'ongoing',
|
||||
},
|
||||
{
|
||||
id: 105,
|
||||
avatar: 'https://img2.baidu.com/it/u=2719717192,3826027113&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
|
||||
nickname: '0-1',
|
||||
uid: 79417,
|
||||
start_time: '2026-01-25 23:53',
|
||||
stop_time: '2026-01-26 23:53',
|
||||
title: 'FOMIX 蛋壳椅 进口头层牛皮自然色单人沙发椅 Egg chair设计师蛋椅',
|
||||
cid: 191,
|
||||
people: 2,
|
||||
count_people: 1,
|
||||
status: 'ongoing',
|
||||
},
|
||||
{
|
||||
id: 106,
|
||||
avatar: 'https://img0.baidu.com/it/u=1893322197,2940863863&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
|
||||
nickname: 'abc',
|
||||
uid: 75343,
|
||||
start_time: '2026-01-22 21:29',
|
||||
stop_time: '2026-01-23 21:29',
|
||||
title: 'FOMIX 蛋壳椅 进口头层牛皮自然色单人沙发椅 Egg chair设计师蛋椅',
|
||||
cid: 191,
|
||||
people: 2,
|
||||
count_people: 1,
|
||||
status: 'ongoing',
|
||||
},
|
||||
{
|
||||
id: 107,
|
||||
avatar: 'https://img2.baidu.com/it/u=176219800,2487920112&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
|
||||
nickname: '181****6910',
|
||||
uid: 81141,
|
||||
start_time: '2026-01-19 16:16',
|
||||
stop_time: '2026-01-19 16:45',
|
||||
title: 'UR2024夏季新款女装复古纯欲氛围感一字肩短款T恤UWG440060',
|
||||
cid: 190,
|
||||
people: 2,
|
||||
count_people: 1,
|
||||
status: 'pending',
|
||||
},
|
||||
])
|
||||
|
||||
const statParticipants = ref(0)
|
||||
const statSuccessGroups = ref(0)
|
||||
|
||||
const activeStatus = ref<string | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
loadStats()
|
||||
})
|
||||
|
||||
async function loadData() {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const res = await fetchCombinationActivities({
|
||||
status: activeStatus.value ?? undefined,
|
||||
page: page.value,
|
||||
pageSize: pageSize
|
||||
})
|
||||
combos.value = res.items
|
||||
total.value = res.total
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载拼团列表失败', icon: 'none' })
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
const stats = await getCombinationStats()
|
||||
statParticipants.value = stats.participantCount
|
||||
statSuccessGroups.value = stats.successCount
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const handleStatusChange = (e : any) => {
|
||||
const index = parseInt(String(e.detail.value))
|
||||
const options = [null, 'ongoing', 'pending', 'ended']
|
||||
activeStatus.value = options[index]
|
||||
page.value = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
const viewDetails = (item : CombinationActivity) => {
|
||||
const viewDetails = (item: any) => {
|
||||
console.log('查看详情', item.id)
|
||||
uni.showToast({ title: '详情功能开发中', icon: 'none' })
|
||||
}
|
||||
|
||||
const handleCompleteGroup = (item : CombinationActivity) => {
|
||||
if (item.id == null) return
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要手动将其设为成团状态吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
const success = await completeCombinationGroup(item.id!)
|
||||
if (success) {
|
||||
uni.showToast({ title: '操作成功', icon: 'success' })
|
||||
loadData()
|
||||
loadStats()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
const completeGroup = (item: any) => {
|
||||
console.log('立即成团', item.id)
|
||||
}
|
||||
|
||||
function onPrevPage() {
|
||||
if (page.value > 1) {
|
||||
page.value--
|
||||
loadData()
|
||||
}
|
||||
// 分页适配状态
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(15)
|
||||
let jumpPageInput = ''
|
||||
const pageSizeOptions = [10, 15, 20, 30, 50]
|
||||
const pageSizeOptionLabels = computed(() => pageSizeOptions.map((n: number) => `${n}条/页`))
|
||||
const pageSizeIndex = computed(() => {
|
||||
const idx = pageSizeOptions.indexOf(pageSize.value)
|
||||
return idx >= 0 ? idx : 0
|
||||
})
|
||||
const totalPage = computed(() => Math.max(1, Math.ceil(combos.value.length / pageSize.value)))
|
||||
const visiblePages = computed(() => {
|
||||
const t = totalPage.value
|
||||
const cur = currentPage.value
|
||||
if (t <= 7) return Array.from({ length: t }, (_: any, i: number) => i + 1)
|
||||
if (cur <= 4) return [1, 2, 3, 4, 5, -1, t]
|
||||
if (cur >= t - 3) return [1, -1, t - 4, t - 3, t - 2, t - 1, t]
|
||||
return [1, -1, cur - 1, cur, cur + 1, -1, t]
|
||||
})
|
||||
const handlePageChange = (p: number) => { currentPage.value = p }
|
||||
const handlePageSizeChange = (e: any) => {
|
||||
const idx = Number(e.detail.value)
|
||||
pageSize.value = pageSizeOptions[idx] ?? pageSizeOptions[0]
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
function onNextPage() {
|
||||
if (combos.value.length >= pageSize) {
|
||||
page.value++
|
||||
loadData()
|
||||
}
|
||||
const handleJumpPage = () => {
|
||||
const p = parseInt(jumpPageInput)
|
||||
if (!isNaN(p) && p >= 1 && p <= totalPage.value) currentPage.value = p
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.marketing-combination-list {
|
||||
min-height: 100vh;
|
||||
background: #f0f2f5;
|
||||
padding: 16px;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.border-shadow {
|
||||
@@ -386,32 +432,5 @@ function onNextPage() {
|
||||
.op-link { color: #1890ff; font-size: 13px; cursor: pointer; }
|
||||
.op-split { color: #e8eaec; margin: 0 8px; }
|
||||
|
||||
/* 分页 */
|
||||
.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: #606266; }
|
||||
.page-btns { display: flex; flex-direction: row; gap: 8px; }
|
||||
.p-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
.p-btn.active { background-color: #1890ff; border-color: #1890ff; color: #fff; }
|
||||
|
||||
.page-jump { display: flex; flex-direction: row; align-items: center; gap: 8px; }
|
||||
.jump-txt { font-size: 13px; color: #606266; }
|
||||
.jump-input { width: 40px; height: 28px; border: 1px solid #dcdfe6; border-radius: 4px; text-align: center; }
|
||||
|
||||
/* 分页区域已迁至 CommonPagination 组件 */
|
||||
</style>
|
||||
@@ -107,25 +107,30 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="pagination-footer">
|
||||
<view class="page-total">
|
||||
<text class="total-txt">共 {{ total }} 条</text>
|
||||
</view>
|
||||
<view class="page-select">
|
||||
<text class="page-val">15条/页 ▼</text>
|
||||
</view>
|
||||
<view class="page-btns">
|
||||
<text class="p-btn"><</text>
|
||||
<text class="p-btn active">1</text>
|
||||
<text class="p-btn">></text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 分页 -->
|
||||
<CommonPagination
|
||||
v-if="total > 0"
|
||||
:total="total"
|
||||
:loading="false"
|
||||
:currentPage="currentPage"
|
||||
:pageSize="pageSize"
|
||||
:pageSizeOptionLabels="pageSizeOptionLabels"
|
||||
:pageSizeIndex="pageSizeIndex"
|
||||
:visiblePages="visiblePages"
|
||||
:totalPage="totalPage"
|
||||
:jumpPageInput="jumpPageInput"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
@page-change="handlePageChange"
|
||||
@update:jumpPageInput="(val : string) => { jumpPageInput = val }"
|
||||
@jump-page="handleJumpPage"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, computed } from 'vue'
|
||||
import CommonPagination from '@/components/CommonPagination/CommonPagination.uvue'
|
||||
|
||||
interface CombinationProduct {
|
||||
id: number
|
||||
@@ -219,13 +224,43 @@ const handleStats = (item: CombinationProduct) => { console.log('Stats...', item
|
||||
const toggleShow = (item: CombinationProduct) => {
|
||||
item.is_show = !item.is_show
|
||||
}
|
||||
|
||||
// 分页适配状态
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(15)
|
||||
let jumpPageInput = ''
|
||||
const pageSizeOptions = [10, 15, 20, 30, 50]
|
||||
const pageSizeOptionLabels = computed(() => pageSizeOptions.map((n: number) => `${n}条/页`))
|
||||
const pageSizeIndex = computed(() => {
|
||||
const idx = pageSizeOptions.indexOf(pageSize.value)
|
||||
return idx >= 0 ? idx : 0
|
||||
})
|
||||
const totalPage = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
|
||||
const visiblePages = computed(() => {
|
||||
const t = totalPage.value
|
||||
const cur = currentPage.value
|
||||
if (t <= 7) return Array.from({ length: t }, (_: any, i: number) => i + 1)
|
||||
if (cur <= 4) return [1, 2, 3, 4, 5, -1, t]
|
||||
if (cur >= t - 3) return [1, -1, t - 4, t - 3, t - 2, t - 1, t]
|
||||
return [1, -1, cur - 1, cur, cur + 1, -1, t]
|
||||
})
|
||||
const handlePageChange = (p: number) => { currentPage.value = p }
|
||||
const handlePageSizeChange = (e: any) => {
|
||||
const idx = Number(e.detail.value)
|
||||
pageSize.value = pageSizeOptions[idx] ?? pageSizeOptions[0]
|
||||
currentPage.value = 1
|
||||
}
|
||||
const handleJumpPage = () => {
|
||||
const p = parseInt(jumpPageInput)
|
||||
if (!isNaN(p) && p >= 1 && p <= totalPage.value) currentPage.value = p
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.marketing-combination-product {
|
||||
background-color: #f5f7fa;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.border-shadow {
|
||||
@@ -438,28 +473,5 @@ const toggleShow = (item: CombinationProduct) => {
|
||||
.op-split { color: #e8eaec; margin: 0 5px; }
|
||||
.text-danger { color: #ed4014; }
|
||||
|
||||
/* 分页 */
|
||||
.pagination-footer {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
.total-txt { font-size: 13px; color: #606266; }
|
||||
.page-val { font-size: 13px; color: #606266; border: 1px solid #dcdfe6; padding: 4px 10px; border-radius: 4px; }
|
||||
.page-btns { display: flex; flex-direction: row; gap: 8px; }
|
||||
.p-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid #e8eaec;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
.p-btn.active { background-color: #2d8cf0; border-color: #2d8cf0; color: #fff; }
|
||||
/* 分页区域已迁至 CommonPagination 组件 */
|
||||
</style>
|
||||
@@ -64,30 +64,30 @@
|
||||
</view>
|
||||
|
||||
<!-- 分页 -->
|
||||
<view class="pagination-footer">
|
||||
<text class="total-txt">共 16 条</text>
|
||||
<view class="page-select">
|
||||
<text class="page-val">15条/页 ▼</text>
|
||||
</view>
|
||||
<view class="page-btns">
|
||||
<text class="p-btn disabled"><</text>
|
||||
<text class="p-btn active">1</text>
|
||||
<text class="p-btn">2</text>
|
||||
<text class="p-btn">></text>
|
||||
</view>
|
||||
<view class="page-jump">
|
||||
<text class="jump-txt">前往</text>
|
||||
<input class="jump-input" placeholder="1" />
|
||||
<text class="jump-txt">页</text>
|
||||
</view>
|
||||
</view>
|
||||
<CommonPagination
|
||||
v-if="total > 0"
|
||||
:total="total"
|
||||
:loading="false"
|
||||
:currentPage="currentPage"
|
||||
:pageSize="pageSize"
|
||||
:pageSizeOptionLabels="pageSizeOptionLabels"
|
||||
:pageSizeIndex="pageSizeIndex"
|
||||
:visiblePages="visiblePages"
|
||||
:totalPage="totalPage"
|
||||
:jumpPageInput="jumpPageInput"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
@page-change="handlePageChange"
|
||||
@update:jumpPageInput="(val : string) => { jumpPageInput = val }"
|
||||
@jump-page="handleJumpPage"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import CommonPagination from '@/components/CommonPagination/CommonPagination.uvue'
|
||||
|
||||
interface CouponRecord {
|
||||
id: number
|
||||
@@ -121,13 +121,44 @@ const recordList = ref<CouponRecord[]>([
|
||||
])
|
||||
|
||||
const handleQuery = () => { console.log('Querying redemption records...') }
|
||||
|
||||
// 分页适配状态
|
||||
const total = ref(16)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(15)
|
||||
let jumpPageInput = ''
|
||||
const pageSizeOptions = [10, 15, 20, 30, 50]
|
||||
const pageSizeOptionLabels = computed(() => pageSizeOptions.map((n: number) => `${n}条/页`))
|
||||
const pageSizeIndex = computed(() => {
|
||||
const idx = pageSizeOptions.indexOf(pageSize.value)
|
||||
return idx >= 0 ? idx : 0
|
||||
})
|
||||
const totalPage = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
|
||||
const visiblePages = computed(() => {
|
||||
const t = totalPage.value
|
||||
const cur = currentPage.value
|
||||
if (t <= 7) return Array.from({ length: t }, (_: any, i: number) => i + 1)
|
||||
if (cur <= 4) return [1, 2, 3, 4, 5, -1, t]
|
||||
if (cur >= t - 3) return [1, -1, t - 4, t - 3, t - 2, t - 1, t]
|
||||
return [1, -1, cur - 1, cur, cur + 1, -1, t]
|
||||
})
|
||||
const handlePageChange = (p: number) => { currentPage.value = p }
|
||||
const handlePageSizeChange = (e: any) => {
|
||||
const idx = Number(e.detail.value)
|
||||
pageSize.value = pageSizeOptions[idx] ?? pageSizeOptions[0]
|
||||
currentPage.value = 1
|
||||
}
|
||||
const handleJumpPage = () => {
|
||||
const p = parseInt(jumpPageInput)
|
||||
if (!isNaN(p) && p >= 1 && p <= totalPage.value) currentPage.value = p
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.admin-marketing-coupon-user {
|
||||
background-color: #f0f2f5;
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.border-shadow {
|
||||
@@ -187,8 +218,8 @@ const handleQuery = () => { console.log('Querying redemption records...') }
|
||||
.query-txt { color: #fff; font-size: 14px; }
|
||||
|
||||
/* 表格区域 */
|
||||
.table-card { padding-top: 20px; }
|
||||
.table-container { padding: 0 20px 20px; }
|
||||
.table-card { padding-top: 24px; }
|
||||
.table-container { padding: 0 24px 24px; }
|
||||
.table-header-row { display: flex; flex-direction: row; background-color: #f8f8f9; border-bottom: 1px solid #e8eaec; }
|
||||
.th { padding: 12px 10px; font-size: 14px; color: #515a6e; font-weight: bold; }
|
||||
.table-row { display: flex; flex-direction: row; border-bottom: 1px solid #e8eaec; }
|
||||
@@ -204,27 +235,5 @@ const handleQuery = () => { console.log('Querying redemption records...') }
|
||||
|
||||
.status-used { color: #999; }
|
||||
|
||||
/* 分页 */
|
||||
.pagination-footer {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
.total-txt { font-size: 14px; color: #606266; }
|
||||
.page-val { font-size: 14px; color: #606266; border: 1px solid #dcdfe6; padding: 4px 10px; border-radius: 4px; }
|
||||
.page-btns { display: flex; flex-direction: row; gap: 8px; }
|
||||
.p-btn {
|
||||
width: 32px; height: 32px; border: 1px solid #dcdfe6; border-radius: 4px;
|
||||
display: flex; align-items: center; justify-content: center; font-size: 14px; color: #666;
|
||||
}
|
||||
.p-btn.active { background-color: #2d8cf0; border-color: #2d8cf0; color: #fff; }
|
||||
.p-btn.disabled { color: #c0c4cc; background-color: #f5f7fa; }
|
||||
|
||||
.page-jump { display: flex; flex-direction: row; align-items: center; gap: 8px; }
|
||||
.jump-txt { font-size: 14px; color: #606266; }
|
||||
.jump-input { width: 40px; height: 32px; border: 1px solid #dcdfe6; text-align: center; border-radius: 4px; font-size: 14px; }
|
||||
/* 分页区域已迁至 CommonPagination 组件 */
|
||||
</style>
|
||||
@@ -68,32 +68,26 @@
|
||||
</view>
|
||||
|
||||
<view class="table-body">
|
||||
<view v-if="isLoading" class="table-loading" style="padding: 40px; text-align: center;">
|
||||
<text>数据加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="dataList.length === 0" class="table-empty" style="padding: 40px; text-align: center;">
|
||||
<text>暂无优惠券数据</text>
|
||||
</view>
|
||||
<view v-else v-for="(item, index) in dataList" :key="item.id" class="table-row">
|
||||
<view class="td" style="width: 80px;"><text class="td-txt">{{ item.cid || '-' }}</text></view>
|
||||
<view v-for="(item, index) in dataList" :key="item.id" class="table-row">
|
||||
<view class="td" style="width: 80px;"><text class="td-txt">{{ item.id }}</text></view>
|
||||
<view class="td" style="width: 180px;"><text class="td-txt name-bold">{{ item.name }}</text></view>
|
||||
<view class="td" style="width: 120px;"><text class="td-txt">{{ getTypeName(item.coupon_type) }}</text></view>
|
||||
<view class="td" style="width: 100px;">
|
||||
<text class="td-txt price-txt">{{ item.discount_value }}{{ item.discount_type === 2 ? '%' : '元' }}</text>
|
||||
</view>
|
||||
<view class="td" style="width: 120px;"><text class="td-txt">{{ getReceiveTypeName(1) }}</text></view>
|
||||
<view class="td" style="width: 120px;"><text class="td-txt">{{ item.type }}</text></view>
|
||||
<view class="td" style="width: 100px;"><text class="td-txt price-txt">{{ item.value.toFixed(2) }}</text></view>
|
||||
<view class="td" style="width: 120px;"><text class="td-txt">{{ item.receiveType }}</text></view>
|
||||
<view class="td" style="width: 150px;">
|
||||
<text class="td-txt date-small">{{ formatTime(item.start_time) }} 至 {{ formatTime(item.end_time) }}</text>
|
||||
<text v-if="item.id === 1628" class="td-txt date-small">2023-10-18 00:00 - 2025-11-05 00:00</text>
|
||||
<text v-else class="td-txt">{{ item.receiveDate }}</text>
|
||||
</view>
|
||||
<view class="td" style="width: 120px;"><text class="td-txt">{{ item.usage_limit }} 次/人</text></view>
|
||||
<view class="td" style="width: 120px;"><text class="td-txt">{{ item.useTime }}</text></view>
|
||||
<view class="td" style="width: 120px;">
|
||||
<view v-if="item.total_quantity != null && item.total_quantity > 0" class="pub-info">
|
||||
<text class="pub-txt">总计: {{ item.total_quantity }}</text>
|
||||
<view v-if="item.publishTotal > 0" class="pub-info">
|
||||
<text class="pub-txt">发布: {{ item.publishTotal }}</text>
|
||||
<text class="pub-txt danger">剩余: {{ item.publishRemain }}</text>
|
||||
</view>
|
||||
<text v-else class="td-txt">不限量</text>
|
||||
</view>
|
||||
<view class="td" style="width: 100px;">
|
||||
<view :class="['switch-box', item.status === 1 ? 'active' : '']" @click="toggleStatus(index)">
|
||||
<view :class="['switch-box', item.isOpen ? 'active' : '']" @click="toggleStatus(index)">
|
||||
<view class="switch-dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -103,7 +97,9 @@
|
||||
<text class="op-split">|</text>
|
||||
<text class="op-link">编辑</text>
|
||||
<text class="op-split">|</text>
|
||||
<text class="op-link" @click="handleDelete(item.id)">删除</text>
|
||||
<text class="op-link">复制</text>
|
||||
<text class="op-split">|</text>
|
||||
<text class="op-link text-danger">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -111,167 +107,106 @@
|
||||
</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', page >= totalPages ? 'disabled' : '']" @click="nextPage">></text>
|
||||
</view>
|
||||
</view>
|
||||
<CommonPagination
|
||||
v-if="total > 0"
|
||||
:total="total"
|
||||
:loading="false"
|
||||
:currentPage="currentPage"
|
||||
:pageSize="pageSize"
|
||||
:pageSizeOptionLabels="pageSizeOptionLabels"
|
||||
:pageSizeIndex="pageSizeIndex"
|
||||
:visiblePages="visiblePages"
|
||||
:totalPage="totalPage"
|
||||
:jumpPageInput="jumpPageInput"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
@page-change="handlePageChange"
|
||||
@update:jumpPageInput="(val : string) => { jumpPageInput = val }"
|
||||
@jump-page="handleJumpPage"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import {
|
||||
fetchAdminCoupons,
|
||||
toggleCouponStatus,
|
||||
deleteCouponTemplate,
|
||||
CouponTemplate,
|
||||
CouponQuery
|
||||
} from '@/services/admin/marketingService.uts'
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import CommonPagination from '@/components/CommonPagination/CommonPagination.uvue'
|
||||
|
||||
interface CouponItem {
|
||||
id: number
|
||||
name: string
|
||||
type: string
|
||||
value: number
|
||||
receiveType: string
|
||||
receiveDate: string
|
||||
useTime: string
|
||||
publishTotal: number
|
||||
publishRemain: number
|
||||
isOpen: boolean
|
||||
}
|
||||
|
||||
const filter = reactive({
|
||||
name: '',
|
||||
type: null as number | null,
|
||||
status: null as number | null
|
||||
name: ''
|
||||
})
|
||||
|
||||
const dataList = ref<CouponTemplate[]>([])
|
||||
const total = ref(0)
|
||||
const isLoading = ref(false)
|
||||
const page = ref(1)
|
||||
const pageSize = 15
|
||||
const dataList = ref<CouponItem[]>([
|
||||
{ id: 1643, name: '满100减30', type: '通用券', value: 30.00, receiveType: '用户领取', receiveDate: '不限时', useTime: '10天', publishTotal: 0, publishRemain: 0, isOpen: false },
|
||||
{ id: 1642, name: '满10减7', type: '通用券', value: 7.00, receiveType: '用户领取', receiveDate: '不限时', useTime: '10天', publishTotal: 0, publishRemain: 0, isOpen: true },
|
||||
{ id: 1641, name: '会员优惠券', type: '通用券', value: 200.00, receiveType: '用户领取', receiveDate: '不限时', useTime: '200天', publishTotal: 0, publishRemain: 0, isOpen: true },
|
||||
{ id: 1640, name: '会员优惠券', type: '通用券', value: 29.90, receiveType: '用户领取', receiveDate: '不限时', useTime: '200天', publishTotal: 0, publishRemain: 0, isOpen: true },
|
||||
{ id: 1639, name: '会员优惠券', type: '通用券', value: 1.00, receiveType: '用户领取', receiveDate: '不限时', useTime: '200天', publishTotal: 0, publishRemain: 0, isOpen: true },
|
||||
{ id: 1638, name: '商品券', type: '商品券', value: 1.00, receiveType: '用户领取', receiveDate: '不限时', useTime: '200天', publishTotal: 0, publishRemain: 0, isOpen: true },
|
||||
{ id: 1636, name: '测试多个商品消耗一个券', type: '商品券', value: 500.00, receiveType: '系统赠送', receiveDate: '不限时', useTime: '3天', publishTotal: 0, publishRemain: 0, isOpen: true },
|
||||
{ id: 1635, name: '优惠券', type: '通用券', value: 10.00, receiveType: '系统赠送', receiveDate: '不限时', useTime: '10天', publishTotal: 0, publishRemain: 0, isOpen: true },
|
||||
{ id: 1634, name: '限时优惠', type: '通用券', value: 20.00, receiveType: '用户领取', receiveDate: '不限时', useTime: '5天', publishTotal: 0, publishRemain: 0, isOpen: true },
|
||||
{ id: 1633, name: '店庆券', type: '品类券', value: 100.00, receiveType: '用户领取', receiveDate: '不限时', useTime: '10天', publishTotal: 0, publishRemain: 0, isOpen: true },
|
||||
{ id: 1632, name: '优惠券', type: '品类券', value: 99.00, receiveType: '用户领取', receiveDate: '不限时', useTime: '10天', publishTotal: 8999, publishRemain: 8604, isOpen: true },
|
||||
{ id: 1628, name: '全场通用券', type: '通用券', value: 9.90, receiveType: '用户领取', receiveDate: 'RANGE', useTime: '不限时', publishTotal: 59999, publishRemain: 59331, isOpen: true }
|
||||
])
|
||||
|
||||
const typeOptions = [
|
||||
{ label: '全部', value: null },
|
||||
{ label: '满减券', value: 1 },
|
||||
{ label: '折扣券', value: 2 },
|
||||
{ label: '免运费券', value: 3 }
|
||||
]
|
||||
const typeIndex = ref(0)
|
||||
const handleQuery = () => { console.log('Querying...') }
|
||||
const handleAdd = () => { console.log('Adding coupon...') }
|
||||
const toggleStatus = (index: number) => {
|
||||
dataList.value[index].isOpen = !dataList.value[index].isOpen
|
||||
}
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '全部', value: null },
|
||||
{ label: '开启', value: 1 },
|
||||
{ label: '关闭', value: 2 },
|
||||
{ label: '已结束', value: 3 }
|
||||
]
|
||||
const statusIndex = ref(0)
|
||||
|
||||
const totalPages = computed((): number => {
|
||||
return Math.ceil(total.value / pageSize) || 1
|
||||
// 分页适配状态
|
||||
const total = ref(16)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(15)
|
||||
let jumpPageInput = ''
|
||||
const pageSizeOptions = [10, 15, 20, 30, 50]
|
||||
const pageSizeOptionLabels = computed(() => pageSizeOptions.map((n: number) => `${n}条/页`))
|
||||
const pageSizeIndex = computed(() => {
|
||||
const idx = pageSizeOptions.indexOf(pageSize.value)
|
||||
return idx >= 0 ? idx : 0
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
const totalPage = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
|
||||
const visiblePages = computed(() => {
|
||||
const t = totalPage.value
|
||||
const cur = currentPage.value
|
||||
if (t <= 7) return Array.from({ length: t }, (_: any, i: number) => i + 1)
|
||||
if (cur <= 4) return [1, 2, 3, 4, 5, -1, t]
|
||||
if (cur >= t - 3) return [1, -1, t - 4, t - 3, t - 2, t - 1, t]
|
||||
return [1, -1, cur - 1, cur, cur + 1, -1, t]
|
||||
})
|
||||
|
||||
async function loadData() {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const query: CouponQuery = {
|
||||
name: filter.name || null,
|
||||
type: typeOptions[typeIndex.value].value,
|
||||
status: statusOptions[statusIndex.value].value,
|
||||
page: page.value,
|
||||
pageSize: pageSize
|
||||
}
|
||||
const res = await fetchAdminCoupons(query)
|
||||
dataList.value = res.items
|
||||
total.value = res.total
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
const handlePageChange = (p: number) => { currentPage.value = p }
|
||||
const handlePageSizeChange = (e: any) => {
|
||||
const idx = Number(e.detail.value)
|
||||
pageSize.value = pageSizeOptions[idx] ?? pageSizeOptions[0]
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
function handleQuery() {
|
||||
page.value = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
uni.showToast({ title: '添加功能开发中', icon: 'none' })
|
||||
}
|
||||
|
||||
async function toggleStatus(index: number) {
|
||||
const item = dataList.value[index]
|
||||
if (item.id == null) return
|
||||
|
||||
const currentStatus = item.status === 1
|
||||
const success = await toggleCouponStatus(item.id!, !currentStatus)
|
||||
if (success) {
|
||||
uni.showToast({ title: '操作成功', icon: 'success' })
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id: string | undefined) {
|
||||
if (id == null) return
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定删除该优惠券模板吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
const success = await deleteCouponTemplate(id)
|
||||
if (success) {
|
||||
uni.showToast({ title: '删除成功', icon: 'success' })
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
} as ShowModalOptions)
|
||||
}
|
||||
|
||||
function onTypeChange(e: any) {
|
||||
typeIndex.value = parseInt(String(e.detail.value))
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
function onStatusFilterChange(e: any) {
|
||||
statusIndex.value = parseInt(String(e.detail.value))
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
function prevPage() {
|
||||
if (page.value > 1) {
|
||||
page.value--
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
if (page.value < totalPages.value) {
|
||||
page.value++
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
|
||||
function getTypeName(type: number): string {
|
||||
const found = typeOptions.find(o => o.value === type)
|
||||
return found ? found.label : '未知'
|
||||
}
|
||||
|
||||
function getReceiveTypeName(receiveType: number): string {
|
||||
return receiveType === 1 ? '用户领取' : '系统赠送'
|
||||
}
|
||||
|
||||
function formatTime(iso: string | null): string {
|
||||
if (!iso) return '-'
|
||||
return iso.substring(0, 16).replace('T', ' ')
|
||||
const handleJumpPage = () => {
|
||||
const p = parseInt(jumpPageInput)
|
||||
if (!isNaN(p) && p >= 1 && p <= totalPage.value) currentPage.value = p
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.admin-marketing-coupon {
|
||||
background-color: #f0f2f5;
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.border-shadow {
|
||||
@@ -338,7 +273,7 @@ function formatTime(iso: string | null): string {
|
||||
|
||||
/* 表格区域 */
|
||||
.table-card { background-color: #fff; display: flex; flex-direction: column; }
|
||||
.card-header { padding: 20px; }
|
||||
.card-header { padding: 24px; }
|
||||
|
||||
.btn-primary-blue {
|
||||
background-color: #2d8cf0;
|
||||
@@ -349,7 +284,7 @@ function formatTime(iso: string | null): string {
|
||||
}
|
||||
.btn-txt { color: #fff; font-size: 14px; }
|
||||
|
||||
.table-container { padding: 0 20px 20px; }
|
||||
.table-container { padding: 0 24px 24px; }
|
||||
.table-header-row { display: flex; flex-direction: row; background-color: #f8f8f9; border-bottom: 1px solid #e8eaec; }
|
||||
.th { padding: 12px 10px; font-size: 14px; color: #515a6e; font-weight: bold; }
|
||||
.table-row { display: flex; flex-direction: row; border-bottom: 1px solid #e8eaec; border-left: 1px solid transparent; }
|
||||
@@ -393,28 +328,6 @@ function formatTime(iso: string | null): string {
|
||||
.op-split { color: #e8eaec; margin: 0 5px; }
|
||||
.text-danger { color: #ed4014; }
|
||||
|
||||
/* 分页 */
|
||||
.pagination-footer {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
.total-txt { font-size: 14px; color: #606266; }
|
||||
.page-val { font-size: 14px; color: #606266; border: 1px solid #dcdfe6; padding: 4px 10px; border-radius: 4px; }
|
||||
.page-btns { display: flex; flex-direction: row; gap: 8px; }
|
||||
.p-btn {
|
||||
width: 32px; height: 32px; border: 1px solid #dcdfe6; border-radius: 4px;
|
||||
display: flex; align-items: center; justify-content: center; font-size: 14px; color: #666;
|
||||
}
|
||||
.p-btn.active { background-color: #2d8cf0; border-color: #2d8cf0; color: #fff; }
|
||||
.p-btn.disabled { color: #c0c4cc; background-color: #f5f7fa; }
|
||||
|
||||
.page-jump { display: flex; flex-direction: row; align-items: center; gap: 8px; }
|
||||
.jump-txt { font-size: 14px; color: #606266; }
|
||||
.jump-input { width: 40px; height: 32px; border: 1px solid #dcdfe6; text-align: center; border-radius: 4px; font-size: 14px; }
|
||||
/* 分页区域已迁至 CommonPagination 组件 */
|
||||
</style>
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
<template>
|
||||
<AdminLayout :currentPage="currentPage">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">{{ title }}</text>
|
||||
<text class="sub-title">页面占位 (自动生成)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
const currentPage = ref<string>('groupbuy-goods')
|
||||
const title = ref<string>('goods')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/uni.scss';
|
||||
.page { padding: $space-lg; }
|
||||
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
|
||||
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
|
||||
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
<template>
|
||||
<AdminLayout :currentPage="currentPage">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">{{ title }}</text>
|
||||
<text class="sub-title">页面占位 (自动生成)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
const currentPage = ref<string>('marketing')
|
||||
const title = ref<string>('营销看板')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/uni.scss';
|
||||
.page { padding: $space-lg; }
|
||||
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
|
||||
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
|
||||
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
|
||||
</style>
|
||||
|
||||
766
pages/mall/admin/marketing/live/index.uvue
Normal file
766
pages/mall/admin/marketing/live/index.uvue
Normal file
@@ -0,0 +1,766 @@
|
||||
<template>
|
||||
<view class="marketing-live-room">
|
||||
<view class="filter-card border-shadow">
|
||||
<view class="filter-row">
|
||||
<view class="filter-item">
|
||||
<text class="label">直播状态:</text>
|
||||
<view class="select-mock">
|
||||
<text class="select-val">全部</text>
|
||||
<text class="arrow">▼</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="filter-item ml-24">
|
||||
<text class="label">搜索:</text>
|
||||
<input class="input-mock" placeholder="请输入直播间名称/ID/主播昵称/微信号" />
|
||||
</view>
|
||||
<button class="btn-query ml-16">查询</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="action-bar">
|
||||
<button class="btn-add" @click="showDrawer = true">添加直播间</button>
|
||||
<button class="btn-sync ml-16">同步直播间</button>
|
||||
</view>
|
||||
|
||||
<view class="table-card border-shadow">
|
||||
<view class="table-container">
|
||||
<view class="table-head">
|
||||
<view class="th cell-id">直播间ID</view>
|
||||
<view class="th cell-name">直播间名称</view>
|
||||
<view class="th cell-nick">主播昵称</view>
|
||||
<view class="th cell-wechat">主播微信号</view>
|
||||
<view class="th cell-time">直播开始时间</view>
|
||||
<view class="th cell-time">计划结束时间</view>
|
||||
<view class="th cell-time">创建时间</view>
|
||||
<view class="th cell-status">显示状态</view>
|
||||
<view class="th cell-live-status">直播状态</view>
|
||||
<view class="th cell-sort">排序</view>
|
||||
<view class="th cell-op">操作</view>
|
||||
</view>
|
||||
|
||||
<view class="table-body">
|
||||
<view v-for="item in roomList" :key="item.id" class="table-row">
|
||||
<view class="td cell-id"><text class="td-txt">{{ item.id }}</text></view>
|
||||
<view class="td cell-name"><text class="td-txt">{{ item.name }}</text></view>
|
||||
<view class="td cell-nick"><text class="td-txt">{{ item.anchor_nick }}</text></view>
|
||||
<view class="td cell-wechat"><text class="td-txt">{{ item.anchor_wechat }}</text></view>
|
||||
<view class="td cell-time"><text class="td-txt-small">{{ item.start_time }}</text></view>
|
||||
<view class="td cell-time"><text class="td-txt-small">{{ item.end_time }}</text></view>
|
||||
<view class="td cell-time"><text class="td-txt-small">{{ item.create_time }}</text></view>
|
||||
<view class="td cell-status">
|
||||
<view class="switch-mock" :class="{ active: item.is_show }" @click="toggleStatus(item)">
|
||||
<view class="switch-dot"></view>
|
||||
<text class="switch-txt">{{ item.is_show ? '开启' : '关闭' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="td cell-live-status"><text class="td-txt">{{ item.live_status }}</text></view>
|
||||
<view class="td cell-sort"><text class="td-txt">{{ item.sort }}</text></view>
|
||||
<view class="td cell-op">
|
||||
<view class="op-links">
|
||||
<text class="op-link" @click="handleEdit(item)">详情</text>
|
||||
<text class="op-split">|</text>
|
||||
<text class="op-link" @click="handleDelete(item)">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分页 -->
|
||||
<CommonPagination
|
||||
v-if="roomList.length > 0"
|
||||
:total="roomList.length"
|
||||
:loading="false"
|
||||
:currentPage="currentPage"
|
||||
:pageSize="pageSize"
|
||||
:pageSizeOptionLabels="pageSizeOptionLabels"
|
||||
:pageSizeIndex="pageSizeIndex"
|
||||
:visiblePages="visiblePages"
|
||||
:totalPage="totalPage"
|
||||
:jumpPageInput="jumpPageInput"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
@page-change="handlePageChange"
|
||||
@update:jumpPageInput="(val : string) => { jumpPageInput = val }"
|
||||
@jump-page="handleJumpPage"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- Drawer Overlay -->
|
||||
<view v-if="showDrawer || isAnimating" class="drawer-mask" :class="{ active: showDrawer }" @click="closeDrawer"></view>
|
||||
|
||||
<!-- Drawer Panel -->
|
||||
<view class="drawer-panel" :class="{ active: showDrawer }">
|
||||
<view class="drawer-header">
|
||||
<view class="header-left">
|
||||
<text class="back-btn" @click="closeDrawer">‹ 返回</text>
|
||||
<text class="drawer-title">直播间管理</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="drawer-content">
|
||||
<view class="alert-info">
|
||||
<text class="alert-txt">提示:必须前往微信小程序官方后台开通直播权限,关注【小程序直播】获知直播状态</text>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label required">选择主播:</view>
|
||||
<view class="select-mock full" @click="handleSelectAnchor">
|
||||
<text class="select-val">{{ formData.anchor_nick || '请选择' }}</text>
|
||||
<text class="arrow">▼</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label required">直播间名称:</view>
|
||||
<view class="input-wrap">
|
||||
<input class="form-input" v-model="formData.name" placeholder="请输入直播间名称" />
|
||||
<text class="char-count">{{ formData.name.length }}/80</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label required">背景图:</view>
|
||||
<view class="upload-box" @click="handleUpload('background')">
|
||||
<view class="upload-placeholder" v-if="!formData.background">
|
||||
<text class="up-ic">🖼️</text>
|
||||
</view>
|
||||
<image v-else :src="formData.background" class="upload-preview" mode="aspectFill" />
|
||||
<text class="up-tip blue-bg">尺寸:1080*1920px</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label required">分享图:</view>
|
||||
<view class="upload-box" @click="handleUpload('share')">
|
||||
<view class="upload-placeholder" v-if="!formData.share_img">
|
||||
<text class="up-ic">🖼️</text>
|
||||
</view>
|
||||
<image v-else :src="formData.share_img" class="upload-preview" mode="aspectFill" />
|
||||
<text class="up-tip">尺寸:800*640px</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label">联系电话:</view>
|
||||
<view class="input-wrap">
|
||||
<input class="form-input" v-model="formData.phone" placeholder="请输入主播联系电话" />
|
||||
<text class="char-count">{{ formData.phone.length }}/11</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label required">直播时间:</view>
|
||||
<view class="date-range-mock" @click="handleOpenDatePicker">
|
||||
<text class="calendar-ic">📅</text>
|
||||
<text class="date-val">{{ formData.start_time || '开始日期' }} - {{ formData.end_time || '结束日期' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label">排序:</view>
|
||||
<input class="form-input w-extra-small" type="number" v-model="formData.sort" />
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label">直播间类型:</view>
|
||||
<view class="radio-group">
|
||||
<view class="radio-item" @click="formData.type = 'phone'">
|
||||
<view class="radio-circle" :class="{ active: formData.type === 'phone' }"></view>
|
||||
<text class="radio-txt">手机直播</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item flex-row">
|
||||
<view class="form-label">直播间点赞:</view>
|
||||
<view class="switch-mock" :class="{ active: formData.like_enabled }" @click="formData.like_enabled = !formData.like_enabled">
|
||||
<view class="switch-dot"></view>
|
||||
<text class="switch-txt">{{ formData.like_enabled ? '开启' : '关闭' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item flex-row">
|
||||
<view class="form-label">直播卖货:</view>
|
||||
<view class="switch-mock" :class="{ active: formData.sale_enabled }" @click="formData.sale_enabled = !formData.sale_enabled">
|
||||
<view class="switch-dot"></view>
|
||||
<text class="switch-txt">{{ formData.sale_enabled ? '开启' : '关闭' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item flex-row">
|
||||
<view class="form-label">直播间评论:</view>
|
||||
<view class="switch-mock" :class="{ active: formData.comment_enabled }" @click="formData.comment_enabled = !formData.comment_enabled">
|
||||
<view class="switch-dot"></view>
|
||||
<text class="switch-txt">{{ formData.comment_enabled ? '开启' : '关闭' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-actions-bottom">
|
||||
<button class="btn-submit" @click="handleSubmit">提交</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, computed } from 'vue'
|
||||
import CommonPagination from '@/components/CommonPagination/CommonPagination.uvue'
|
||||
|
||||
const showDrawer = ref(false)
|
||||
const isAnimating = ref(false)
|
||||
|
||||
const formData = ref({
|
||||
anchor_nick: '',
|
||||
name: '',
|
||||
background: '',
|
||||
share_img: '',
|
||||
phone: '',
|
||||
start_time: '',
|
||||
end_time: '',
|
||||
sort: 0,
|
||||
type: 'phone',
|
||||
like_enabled: true,
|
||||
sale_enabled: true,
|
||||
comment_enabled: true
|
||||
})
|
||||
|
||||
const roomList = ref([
|
||||
{
|
||||
id: 88,
|
||||
name: 'CRMEB 年中618活动开始',
|
||||
anchor_nick: '打羽毛球',
|
||||
anchor_wechat: 'evoxwht',
|
||||
start_time: '2025-06-17 00:00:00',
|
||||
end_time: '2025-06-18 00:00:00',
|
||||
create_time: '2025-06-16 14:56:53',
|
||||
is_show: true,
|
||||
live_status: '已结束',
|
||||
sort: 1
|
||||
},
|
||||
{
|
||||
id: 90,
|
||||
name: '123456789',
|
||||
anchor_nick: '万万',
|
||||
anchor_wechat: 'xiao112032014',
|
||||
start_time: '2025-07-07 10:20:00',
|
||||
end_time: '2025-07-07 12:00:00',
|
||||
create_time: '2025-07-07 10:05:43',
|
||||
is_show: true,
|
||||
live_status: '已结束',
|
||||
sort: 0
|
||||
},
|
||||
{
|
||||
id: 89,
|
||||
name: '测试1111111',
|
||||
anchor_nick: '打羽毛球',
|
||||
anchor_wechat: '',
|
||||
start_time: '2025-05-20 14:50:00',
|
||||
end_time: '2025-05-20 15:22:00',
|
||||
create_time: '2025-06-17 10:03:08',
|
||||
is_show: true,
|
||||
live_status: '已结束',
|
||||
sort: 0
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: '开学季,最后一天',
|
||||
anchor_nick: '等风来',
|
||||
anchor_wechat: 'welalnidaobel',
|
||||
start_time: '2021-09-01 19:00:00',
|
||||
end_time: '2021-09-01 20:00:00',
|
||||
create_time: '2021-08-30 11:53:01',
|
||||
is_show: false,
|
||||
live_status: '已结束',
|
||||
sort: 0
|
||||
}
|
||||
])
|
||||
|
||||
const toggleStatus = (item: any) => {
|
||||
item.is_show = !item.is_show
|
||||
uni.showToast({ title: '状态修改成功', icon: 'success' })
|
||||
}
|
||||
|
||||
const handleEdit = (item: any) => {
|
||||
formData.value = { ...item, like_enabled: true, sale_enabled: true, comment_enabled: true }
|
||||
showDrawer.value = true
|
||||
}
|
||||
|
||||
const handleDelete = (item: any) => {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该直播间吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
roomList.value = roomList.value.filter(i => i.id !== item.id)
|
||||
uni.showToast({ title: '删除成功' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleSelectAnchor = () => {
|
||||
uni.showToast({ title: '功能开发中', icon: 'none' })
|
||||
}
|
||||
|
||||
const handleUpload = (type: string) => {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
success: (res) => {
|
||||
if (type === 'background') {
|
||||
formData.value.background = res.tempFilePaths[0]
|
||||
} else {
|
||||
formData.value.share_img = res.tempFilePaths[0]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleOpenDatePicker = () => {
|
||||
uni.showToast({ title: '日期选择功能开发中', icon: 'none' })
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
uni.showToast({ title: '提交成功', icon: 'success' })
|
||||
closeDrawer()
|
||||
}
|
||||
|
||||
const closeDrawer = () => {
|
||||
showDrawer.value = false
|
||||
isAnimating.value = true
|
||||
setTimeout(() => {
|
||||
isAnimating.value = false
|
||||
}, 300)
|
||||
}
|
||||
|
||||
// 分页适配状态
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(15)
|
||||
let jumpPageInput = ''
|
||||
const pageSizeOptions = [10, 15, 20, 30, 50]
|
||||
const pageSizeOptionLabels = computed(() => pageSizeOptions.map((n: number) => `${n}条/页`))
|
||||
const pageSizeIndex = computed(() => {
|
||||
const idx = pageSizeOptions.indexOf(pageSize.value)
|
||||
return idx >= 0 ? idx : 0
|
||||
})
|
||||
const totalPage = computed(() => Math.max(1, Math.ceil(roomList.value.length / pageSize.value)))
|
||||
const visiblePages = computed(() => {
|
||||
const t = totalPage.value
|
||||
const cur = currentPage.value
|
||||
if (t <= 7) return Array.from({ length: t }, (_: any, i: number) => i + 1)
|
||||
if (cur <= 4) return [1, 2, 3, 4, 5, -1, t]
|
||||
if (cur >= t - 3) return [1, -1, t - 4, t - 3, t - 2, t - 1, t]
|
||||
return [1, -1, cur - 1, cur, cur + 1, -1, t]
|
||||
})
|
||||
const handlePageChange = (p: number) => { currentPage.value = p }
|
||||
const handlePageSizeChange = (e: any) => {
|
||||
const idx = Number(e.detail.value)
|
||||
pageSize.value = pageSizeOptions[idx] ?? pageSizeOptions[0]
|
||||
currentPage.value = 1
|
||||
}
|
||||
const handleJumpPage = () => {
|
||||
const p = parseInt(jumpPageInput)
|
||||
if (!isNaN(p) && p >= 1 && p <= totalPage.value) currentPage.value = p
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.marketing-live-room {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.border-shadow {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ml-16 { margin-left: 16px; }
|
||||
.ml-24 { margin-left: 24px; }
|
||||
.mt-16 { margin-top: 16px; }
|
||||
|
||||
/* 过滤栏 */
|
||||
.filter-card {
|
||||
padding: var(--admin-card-padding);
|
||||
margin-bottom: var(--admin-section-gap);
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.input-mock, .select-mock {
|
||||
width: 200px;
|
||||
height: 32px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.input-mock { width: 300px; }
|
||||
.select-mock { width: 160px; justify-content: space-between; }
|
||||
.select-mock.mini { width: 100px; height: 28px; }
|
||||
.select-mock.full { width: 100%; }
|
||||
|
||||
.select-val { font-size: 13px; color: #606266; }
|
||||
.arrow { font-size: 10px; color: #c0c4cc; }
|
||||
|
||||
.btn-query {
|
||||
width: 64px;
|
||||
height: 32px;
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 操作栏 */
|
||||
.action-bar {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.btn-add {
|
||||
width: auto;
|
||||
padding: 0 16px;
|
||||
height: 32px;
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-sync {
|
||||
width: auto;
|
||||
padding: 0 16px;
|
||||
height: 32px;
|
||||
background-color: #fff;
|
||||
color: #1890ff;
|
||||
border: 1px solid #1890ff;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 表格区域 */
|
||||
.table-card {
|
||||
padding: var(--admin-card-padding);
|
||||
}
|
||||
|
||||
.table-head {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: #f8f8f9;
|
||||
border-bottom: 1px solid #e8eaec;
|
||||
}
|
||||
|
||||
.th {
|
||||
padding: 12px 8px;
|
||||
font-size: 13px;
|
||||
color: #515a6e;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-bottom: 1px solid #e8eaec;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.td {
|
||||
padding: 16px 8px;
|
||||
}
|
||||
|
||||
.td-txt { font-size: 13px; color: #515a6e; }
|
||||
.td-txt-small { font-size: 12px; color: #808695; display: block; }
|
||||
|
||||
/* 各列宽度 */
|
||||
.cell-id { width: 70px; }
|
||||
.cell-name { flex: 1; min-width: 150px; }
|
||||
.cell-nick { width: 120px; }
|
||||
.cell-wechat { width: 120px; }
|
||||
.cell-time { width: 150px; }
|
||||
.cell-status { width: 100px; text-align: center; }
|
||||
.cell-live-status { width: 100px; text-align: center; }
|
||||
.cell-sort { width: 60px; text-align: center; }
|
||||
.cell-op { width: 120px; text-align: right; }
|
||||
|
||||
.switch-mock {
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
background-color: #bfbfbf;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0 4px;
|
||||
position: relative;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
.switch-mock.active { background-color: #1890ff; }
|
||||
.switch-dot {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: #fff;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
transition: left 0.3s;
|
||||
}
|
||||
.switch-mock.active .switch-dot { left: 30px; }
|
||||
.switch-txt { font-size: 11px; color: #fff; margin-left: 20px; }
|
||||
.switch-mock.active .switch-txt { margin-left: 4px; }
|
||||
|
||||
.op-links {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
.op-link { color: #1890ff; font-size: 13px; cursor: pointer; }
|
||||
.op-split { color: #e8eaec; margin: 0 8px; }
|
||||
|
||||
/* 分页区域已迁至 CommonPagination 组件 */
|
||||
|
||||
/* Drawer Styles */
|
||||
.drawer-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.45);
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
pointer-events: none;
|
||||
}
|
||||
.drawer-mask.active {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.drawer-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: -50%;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
z-index: 1001;
|
||||
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: right 0.3s ease-out;
|
||||
}
|
||||
.drawer-panel.active {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.drawer-header {
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.header-left {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.back-btn {
|
||||
font-size: 14px;
|
||||
color: #8c8c8c;
|
||||
cursor: pointer;
|
||||
}
|
||||
.drawer-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.drawer-content {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background-color: #fff7e6;
|
||||
border: 1px solid #ffe7ba;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 24px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.alert-txt {
|
||||
font-size: 13px;
|
||||
color: #fa8c16;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #262626;
|
||||
margin-bottom: 8px;
|
||||
width: 120px;
|
||||
}
|
||||
.flex-row .form-label { margin-bottom: 0; }
|
||||
|
||||
.required::before {
|
||||
content: '*';
|
||||
color: #ff4d4f;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.input-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
.form-input {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
padding: 0 40px 0 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.w-extra-small { width: 80px; }
|
||||
.char-count {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 6px;
|
||||
font-size: 12px;
|
||||
color: #bfbfbf;
|
||||
}
|
||||
|
||||
.upload-box {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
.upload-placeholder {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
.up-ic { font-size: 24px; color: #bfbfbf; }
|
||||
.up-tip {
|
||||
font-size: 12px;
|
||||
color: #1890ff;
|
||||
background-color: #e6f7ff;
|
||||
border: 1px solid #91d5ff;
|
||||
padding: 2px 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.up-tip.blue-bg {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.date-range-mock {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
gap: 8px;
|
||||
}
|
||||
.calendar-ic { font-size: 14px; color: #bfbfbf; }
|
||||
.date-val { font-size: 14px; color: #bfbfbf; }
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 24px;
|
||||
}
|
||||
.radio-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.radio-circle {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
}
|
||||
.radio-circle.active { border-color: #1890ff; }
|
||||
.radio-circle.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: #1890ff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.radio-txt { font-size: 14px; color: #262626; }
|
||||
|
||||
.form-actions-bottom {
|
||||
margin-top: 40px;
|
||||
}
|
||||
.btn-submit {
|
||||
width: 64px;
|
||||
height: 32px;
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
@@ -66,20 +66,23 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="pagination-footer">
|
||||
<view class="page-total"><text class="total-txt">共 {{ roomList.length }} 条</text></view>
|
||||
<view class="page-select">
|
||||
<view class="select-mock mini">
|
||||
<text class="select-val">20条/页</text>
|
||||
<text class="arrow">▼</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="page-btns">
|
||||
<text class="p-btn disabled">‹</text>
|
||||
<text class="p-btn active">1</text>
|
||||
<text class="p-btn disabled">›</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 分页 -->
|
||||
<CommonPagination
|
||||
v-if="roomList.length > 0"
|
||||
:total="roomList.length"
|
||||
:loading="false"
|
||||
:currentPage="currentPage"
|
||||
:pageSize="pageSize"
|
||||
:pageSizeOptionLabels="pageSizeOptionLabels"
|
||||
:pageSizeIndex="pageSizeIndex"
|
||||
:visiblePages="visiblePages"
|
||||
:totalPage="totalPage"
|
||||
:jumpPageInput="jumpPageInput"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
@page-change="handlePageChange"
|
||||
@update:jumpPageInput="(val : string) => { jumpPageInput = val }"
|
||||
@jump-page="handleJumpPage"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- Drawer Overlay -->
|
||||
@@ -200,182 +203,125 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import {
|
||||
fetchLiveRooms,
|
||||
saveLiveRoom,
|
||||
deleteLiveRoom,
|
||||
fetchLiveAnchors,
|
||||
LiveRoom,
|
||||
LiveAnchor
|
||||
} from '@/services/admin/marketingService.uts'
|
||||
import { ref, computed } from 'vue'
|
||||
import CommonPagination from '@/components/CommonPagination/CommonPagination.uvue'
|
||||
|
||||
const showDrawer = ref(false)
|
||||
const isAnimating = ref(false)
|
||||
const isLoading = ref(false)
|
||||
const isSaving = ref(false)
|
||||
|
||||
const roomList = ref<LiveRoom[]>([])
|
||||
const anchorList = ref<LiveAnchor[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = 20
|
||||
const searchQuery = ref('')
|
||||
|
||||
const formData = ref<LiveRoom>({
|
||||
name: '',
|
||||
anchor_id: null,
|
||||
const formData = ref({
|
||||
anchor_nick: '',
|
||||
background_url: '',
|
||||
share_img_url: '',
|
||||
name: '',
|
||||
background: '',
|
||||
share_img: '',
|
||||
phone: '',
|
||||
start_time: '',
|
||||
end_time: '',
|
||||
sort: 0,
|
||||
type: 'phone',
|
||||
like_enabled: true,
|
||||
sale_enabled: true,
|
||||
comment_enabled: true,
|
||||
is_show: true,
|
||||
live_status: 1
|
||||
comment_enabled: true
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
loadAnchors()
|
||||
})
|
||||
|
||||
async function loadData() {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const res = await fetchLiveRooms({
|
||||
search: searchQuery.value,
|
||||
page: page.value,
|
||||
pageSize: pageSize
|
||||
})
|
||||
roomList.value = res.items
|
||||
total.value = res.total
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载直播间失败', icon: 'none' })
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAnchors() {
|
||||
anchorList.value = await fetchLiveAnchors()
|
||||
}
|
||||
|
||||
async function toggleStatus(item : LiveRoom) {
|
||||
if (item.id == null) return
|
||||
const nextStatus = !item.is_show
|
||||
const success = await saveLiveRoom({ ...item, is_show: nextStatus } as LiveRoom)
|
||||
if (success) {
|
||||
item.is_show = nextStatus
|
||||
uni.showToast({ title: '状态修改成功', icon: 'success' })
|
||||
}
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
formData.value = {
|
||||
name: '',
|
||||
anchor_id: null,
|
||||
anchor_nick: '',
|
||||
background_url: '',
|
||||
share_img_url: '',
|
||||
start_time: '',
|
||||
end_time: '',
|
||||
sort: 0,
|
||||
type: 'phone',
|
||||
like_enabled: true,
|
||||
sale_enabled: true,
|
||||
comment_enabled: true,
|
||||
const roomList = ref([
|
||||
{
|
||||
id: 88,
|
||||
name: 'CRMEB 年中618活动开始',
|
||||
anchor_nick: '打羽毛球',
|
||||
anchor_wechat: 'evoxwht',
|
||||
start_time: '2025-06-17 00:00:00',
|
||||
end_time: '2025-06-18 00:00:00',
|
||||
create_time: '2025-06-16 14:56:53',
|
||||
is_show: true,
|
||||
live_status: 1
|
||||
} as LiveRoom
|
||||
live_status: '已结束',
|
||||
sort: 1
|
||||
},
|
||||
{
|
||||
id: 90,
|
||||
name: '123456789',
|
||||
anchor_nick: '万万',
|
||||
anchor_wechat: 'xiao112032014',
|
||||
start_time: '2025-07-07 10:20:00',
|
||||
end_time: '2025-07-07 12:00:00',
|
||||
create_time: '2025-07-07 10:05:43',
|
||||
is_show: true,
|
||||
live_status: '已结束',
|
||||
sort: 0
|
||||
},
|
||||
{
|
||||
id: 89,
|
||||
name: '测试1111111',
|
||||
anchor_nick: '打羽毛球',
|
||||
anchor_wechat: '',
|
||||
start_time: '2025-05-20 14:50:00',
|
||||
end_time: '2025-05-20 15:22:00',
|
||||
create_time: '2025-06-17 10:03:08',
|
||||
is_show: true,
|
||||
live_status: '已结束',
|
||||
sort: 0
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: '开学季,最后一天',
|
||||
anchor_nick: '等风来',
|
||||
anchor_wechat: 'welalnidaobel',
|
||||
start_time: '2021-09-01 19:00:00',
|
||||
end_time: '2021-09-01 20:00:00',
|
||||
create_time: '2021-08-30 11:53:01',
|
||||
is_show: false,
|
||||
live_status: '已结束',
|
||||
sort: 0
|
||||
}
|
||||
])
|
||||
|
||||
const toggleStatus = (item: any) => {
|
||||
item.is_show = !item.is_show
|
||||
uni.showToast({ title: '状态修改成功', icon: 'success' })
|
||||
}
|
||||
|
||||
const handleEdit = (item: any) => {
|
||||
formData.value = { ...item, like_enabled: true, sale_enabled: true, comment_enabled: true }
|
||||
showDrawer.value = true
|
||||
}
|
||||
|
||||
function handleEdit(item : LiveRoom) {
|
||||
formData.value = { ...item } as LiveRoom
|
||||
showDrawer.value = true
|
||||
}
|
||||
|
||||
async function handleDelete(item : LiveRoom) {
|
||||
if (item.id == null) return
|
||||
const handleDelete = (item: any) => {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该直播间吗?',
|
||||
success: async (res) => {
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
const success = await deleteLiveRoom(item.id!)
|
||||
if (success) {
|
||||
uni.showToast({ title: '删除成功' })
|
||||
loadData()
|
||||
}
|
||||
roomList.value = roomList.value.filter(i => i.id !== item.id)
|
||||
uni.showToast({ title: '删除成功' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
page.value = 1
|
||||
loadData()
|
||||
const handleSelectAnchor = () => {
|
||||
uni.showToast({ title: '功能开发中', icon: 'none' })
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formData.value.name || !formData.value.start_time) {
|
||||
uni.showToast({ title: '请填写必填项', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
isSaving.value = true
|
||||
try {
|
||||
const success = await saveLiveRoom(formData.value)
|
||||
if (success) {
|
||||
uni.showToast({ title: '提交成功', icon: 'success' })
|
||||
closeDrawer()
|
||||
loadData()
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelectAnchor() {
|
||||
const nicks = anchorList.value.map(a => a.nickname)
|
||||
if (nicks.length === 0) {
|
||||
uni.showToast({ title: '请先添加主播', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.showActionSheet({
|
||||
itemList: nicks,
|
||||
success: (res) => {
|
||||
const anchor = anchorList.value[res.tapIndex]
|
||||
formData.value.anchor_id = anchor.id ?? null
|
||||
formData.value.anchor_nick = anchor.nickname
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleUpload(field: string) {
|
||||
const handleUpload = (type: string) => {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
success: (res) => {
|
||||
if (field === 'background') {
|
||||
formData.value.background_url = res.tempFilePaths[0]
|
||||
if (type === 'background') {
|
||||
formData.value.background = res.tempFilePaths[0]
|
||||
} else {
|
||||
formData.value.share_img_url = res.tempFilePaths[0]
|
||||
formData.value.share_img = res.tempFilePaths[0]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleOpenDatePicker() {
|
||||
// 简化处理,实际推荐使用日期选择组件
|
||||
uni.showToast({ title: '请在输入框直接修改时间', icon: 'none' })
|
||||
const handleOpenDatePicker = () => {
|
||||
uni.showToast({ title: '日期选择功能开发中', icon: 'none' })
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
uni.showToast({ title: '提交成功', icon: 'success' })
|
||||
closeDrawer()
|
||||
}
|
||||
|
||||
const closeDrawer = () => {
|
||||
@@ -386,18 +332,34 @@ const closeDrawer = () => {
|
||||
}, 300)
|
||||
}
|
||||
|
||||
function onPrevPage() {
|
||||
if (page.value > 1) {
|
||||
page.value--
|
||||
loadData()
|
||||
}
|
||||
// 分页适配状态
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(15)
|
||||
let jumpPageInput = ''
|
||||
const pageSizeOptions = [10, 15, 20, 30, 50]
|
||||
const pageSizeOptionLabels = computed(() => pageSizeOptions.map((n: number) => `${n}条/页`))
|
||||
const pageSizeIndex = computed(() => {
|
||||
const idx = pageSizeOptions.indexOf(pageSize.value)
|
||||
return idx >= 0 ? idx : 0
|
||||
})
|
||||
const totalPage = computed(() => Math.max(1, Math.ceil(roomList.value.length / pageSize.value)))
|
||||
const visiblePages = computed(() => {
|
||||
const t = totalPage.value
|
||||
const cur = currentPage.value
|
||||
if (t <= 7) return Array.from({ length: t }, (_: any, i: number) => i + 1)
|
||||
if (cur <= 4) return [1, 2, 3, 4, 5, -1, t]
|
||||
if (cur >= t - 3) return [1, -1, t - 4, t - 3, t - 2, t - 1, t]
|
||||
return [1, -1, cur - 1, cur, cur + 1, -1, t]
|
||||
})
|
||||
const handlePageChange = (p: number) => { currentPage.value = p }
|
||||
const handlePageSizeChange = (e: any) => {
|
||||
const idx = Number(e.detail.value)
|
||||
pageSize.value = pageSizeOptions[idx] ?? pageSizeOptions[0]
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
function onNextPage() {
|
||||
if (roomList.value.length >= pageSize) {
|
||||
page.value++
|
||||
loadData()
|
||||
}
|
||||
const handleJumpPage = () => {
|
||||
const p = parseInt(jumpPageInput)
|
||||
if (!isNaN(p) && p >= 1 && p <= totalPage.value) currentPage.value = p
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -589,16 +551,7 @@ function onNextPage() {
|
||||
.op-link { color: #1890ff; font-size: 13px; cursor: pointer; }
|
||||
.op-split { color: #e8eaec; margin: 0 8px; }
|
||||
|
||||
/* 分页 */
|
||||
.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: #606266; }
|
||||
/* 分页区域已迁至 CommonPagination 组件 */
|
||||
|
||||
/* Drawer Styles */
|
||||
.drawer-mask {
|
||||
@@ -66,20 +66,23 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="pagination-footer">
|
||||
<view class="page-total"><text class="total-txt">共 {{ productList.length }} 条</text></view>
|
||||
<view class="page-select">
|
||||
<view class="select-mock mini">
|
||||
<text class="select-val">20条/页</text>
|
||||
<text class="arrow">▼</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="page-btns">
|
||||
<text class="p-btn disabled">‹</text>
|
||||
<text class="p-btn active">1</text>
|
||||
<text class="p-btn disabled">›</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 分页 -->
|
||||
<CommonPagination
|
||||
v-if="productList.length > 0"
|
||||
:total="productList.length"
|
||||
:loading="false"
|
||||
:currentPage="currentPage"
|
||||
:pageSize="pageSize"
|
||||
:pageSizeOptionLabels="pageSizeOptionLabels"
|
||||
:pageSizeIndex="pageSizeIndex"
|
||||
:visiblePages="visiblePages"
|
||||
:totalPage="totalPage"
|
||||
:jumpPageInput="jumpPageInput"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
@page-change="handlePageChange"
|
||||
@update:jumpPageInput="(val : string) => { jumpPageInput = val }"
|
||||
@jump-page="handleJumpPage"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -112,75 +115,74 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { fetchLiveProducts, saveLiveProduct, deleteLiveProduct, LiveProduct } from '@/services/admin/marketingService.uts'
|
||||
import { ref, computed } from 'vue'
|
||||
import CommonPagination from '@/components/CommonPagination/CommonPagination.uvue'
|
||||
|
||||
const isAdding = ref(false)
|
||||
const isLoading = ref(false)
|
||||
const productList = ref<LiveProduct[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = 20
|
||||
const searchQuery = ref('')
|
||||
const auditStatus = ref(0) // 0:全部
|
||||
|
||||
const selectedList = ref([
|
||||
{ image: 'https://img0.baidu.com/it/u=3033502919,1657850259&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500' }
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
async function loadData() {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const res = await fetchLiveProducts({
|
||||
search: searchQuery.value,
|
||||
audit_status: auditStatus,
|
||||
page: page.value,
|
||||
pageSize: pageSize
|
||||
})
|
||||
productList.value = res.items
|
||||
total.value = res.total
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
const productList = ref([
|
||||
{
|
||||
id: 92,
|
||||
image: 'https://img0.baidu.com/it/u=3023224345,1529124233&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
|
||||
title: '绣球永生花网红干花花束大',
|
||||
live_price: 149.00,
|
||||
price: 149.00,
|
||||
stock: 10617,
|
||||
audit_status: '审核通过',
|
||||
is_show: false
|
||||
},
|
||||
{
|
||||
id: 89,
|
||||
image: 'https://img1.baidu.com/it/u=3175865615,2002599723&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
|
||||
title: '家居梵高系列款软版盒袋',
|
||||
live_price: 350.00,
|
||||
price: 350.00,
|
||||
stock: 8625,
|
||||
audit_status: '审核通过',
|
||||
is_show: false
|
||||
},
|
||||
{
|
||||
id: 93,
|
||||
image: 'https://img2.baidu.com/it/u=2719717192,3826027113&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
|
||||
title: '【LESHUCANGHU',
|
||||
live_price: 300.00,
|
||||
price: 300.00,
|
||||
stock: 164,
|
||||
audit_status: '审核通过',
|
||||
is_show: false
|
||||
},
|
||||
{
|
||||
id: 116,
|
||||
image: 'https://img0.baidu.com/it/u=2257917711,1359654032&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
|
||||
title: '爱奇艺智能 奇遇LT01',
|
||||
live_price: 1199.00,
|
||||
price: 1199.00,
|
||||
stock: 6287,
|
||||
audit_status: '审核通过',
|
||||
is_show: false
|
||||
}
|
||||
])
|
||||
|
||||
const toggleStatus = (item: any) => {
|
||||
item.is_show = !item.is_show
|
||||
uni.showToast({ title: '状态修改成功', icon: 'success' })
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
page.value = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
async function toggleStatus(item : LiveProduct) {
|
||||
if (item.id == null) return
|
||||
const nextShow = !item.is_show
|
||||
const success = await saveLiveProduct({ ...item, is_show: nextShow } as LiveProduct)
|
||||
if (success) {
|
||||
item.is_show = nextShow
|
||||
uni.showToast({ title: '状态修改成功', icon: 'success' })
|
||||
}
|
||||
}
|
||||
|
||||
function handleEdit(item : LiveProduct) {
|
||||
const handleEdit = (item: any) => {
|
||||
uni.showToast({ title: '详情查看中', icon: 'none' })
|
||||
}
|
||||
|
||||
async function handleDelete(item : LiveProduct) {
|
||||
if (item.id == null) return
|
||||
const handleDelete = (item: any) => {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该商品吗?',
|
||||
success: async (res) => {
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
const success = await deleteLiveProduct(item.id!)
|
||||
if (success) {
|
||||
uni.showToast({ title: '删除成功' })
|
||||
loadData()
|
||||
}
|
||||
productList.value = productList.value.filter(i => i.id !== item.id)
|
||||
uni.showToast({ title: '删除成功' })
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -190,7 +192,7 @@ const handleAddProduct = () => {
|
||||
uni.showToast({ title: '选择商品功能开发中', icon: 'none' })
|
||||
}
|
||||
|
||||
const removeSelected = (index : number) => {
|
||||
const removeSelected = (index: number) => {
|
||||
selectedList.value.splice(index, 1)
|
||||
}
|
||||
|
||||
@@ -199,35 +201,42 @@ const handleGenerate = () => {
|
||||
isAdding.value = false
|
||||
}
|
||||
|
||||
function onPrevPage() {
|
||||
if (page.value > 1) {
|
||||
page.value--
|
||||
loadData()
|
||||
}
|
||||
// 分页适配状态
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(15)
|
||||
let jumpPageInput = ''
|
||||
const pageSizeOptions = [10, 15, 20, 30, 50]
|
||||
const pageSizeOptionLabels = computed(() => pageSizeOptions.map((n: number) => `${n}条/页`))
|
||||
const pageSizeIndex = computed(() => {
|
||||
const idx = pageSizeOptions.indexOf(pageSize.value)
|
||||
return idx >= 0 ? idx : 0
|
||||
})
|
||||
const totalPage = computed(() => Math.max(1, Math.ceil(productList.value.length / pageSize.value)))
|
||||
const visiblePages = computed(() => {
|
||||
const t = totalPage.value
|
||||
const cur = currentPage.value
|
||||
if (t <= 7) return Array.from({ length: t }, (_: any, i: number) => i + 1)
|
||||
if (cur <= 4) return [1, 2, 3, 4, 5, -1, t]
|
||||
if (cur >= t - 3) return [1, -1, t - 4, t - 3, t - 2, t - 1, t]
|
||||
return [1, -1, cur - 1, cur, cur + 1, -1, t]
|
||||
})
|
||||
const handlePageChange = (p: number) => { currentPage.value = p }
|
||||
const handlePageSizeChange = (e: any) => {
|
||||
const idx = Number(e.detail.value)
|
||||
pageSize.value = pageSizeOptions[idx] ?? pageSizeOptions[0]
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
function onNextPage() {
|
||||
if (productList.value.length >= pageSize) {
|
||||
page.value++
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
|
||||
function getAuditStatusLabel(status : number) : string {
|
||||
switch (status) {
|
||||
case 1: return '待审核'
|
||||
case 2: return '审核通过'
|
||||
case 3: return '审核驳回'
|
||||
default: return '未知'
|
||||
}
|
||||
const handleJumpPage = () => {
|
||||
const p = parseInt(jumpPageInput)
|
||||
if (!isNaN(p) && p >= 1 && p <= totalPage.value) currentPage.value = p
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.marketing-live-product {
|
||||
min-height: 100vh;
|
||||
background: #f0f2f5;
|
||||
padding: 16px;
|
||||
min-height: auto;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.border-shadow {
|
||||
@@ -497,16 +506,7 @@ function getAuditStatusLabel(status : number) : string {
|
||||
margin-left: 100px;
|
||||
}
|
||||
|
||||
/* 分页 */
|
||||
.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: #606266; }
|
||||
/* 分页区域已迁至 CommonPagination 组件 */
|
||||
|
||||
</style>
|
||||
|
||||
@@ -31,20 +31,23 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="pagination-footer">
|
||||
<view class="page-total"><text class="total-txt">共 {{ anchorList.length }} 条</text></view>
|
||||
<view class="page-select">
|
||||
<view class="select-mock mini">
|
||||
<text class="select-val">15条/页</text>
|
||||
<text class="arrow">▼</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="page-btns">
|
||||
<text class="p-btn disabled">‹</text>
|
||||
<text class="p-btn active">1</text>
|
||||
<text class="p-btn disabled">›</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 分页 -->
|
||||
<CommonPagination
|
||||
v-if="anchorList.length > 0"
|
||||
:total="anchorList.length"
|
||||
:loading="false"
|
||||
:currentPage="currentPage"
|
||||
:pageSize="pageSize"
|
||||
:pageSizeOptionLabels="pageSizeOptionLabels"
|
||||
:pageSizeIndex="pageSizeIndex"
|
||||
:visiblePages="visiblePages"
|
||||
:totalPage="totalPage"
|
||||
:jumpPageInput="jumpPageInput"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
@page-change="handlePageChange"
|
||||
@update:jumpPageInput="(val : string) => { jumpPageInput = val }"
|
||||
@jump-page="handleJumpPage"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- Modal Overlay -->
|
||||
@@ -59,11 +62,11 @@
|
||||
<view class="modal-content">
|
||||
<view class="form-item">
|
||||
<text class="form-label required">主播名称:</text>
|
||||
<input class="form-input" v-model="formData.nickname" placeholder="请输入主播名称" />
|
||||
<input class="form-input" placeholder="请输入主播名称" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label required">主播微信号:</text>
|
||||
<input class="form-input" v-model="formData.wechat" placeholder="请输入主播微信号" />
|
||||
<input class="form-input" placeholder="请输入主播微信号" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label required">主播手机号:</text>
|
||||
@@ -72,7 +75,7 @@
|
||||
<view class="form-item">
|
||||
<text class="form-label">主播图像:</text>
|
||||
<view class="upload-mock" @click="handleUpload">
|
||||
<image v-if="formData.avatar_url" :src="formData.avatar_url" mode="aspectFill" class="avatar-preview" />
|
||||
<image v-if="formData.avatar" :src="formData.avatar" mode="aspectFill" class="avatar-preview" />
|
||||
<text v-else class="upload-ic">🖼️</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -86,65 +89,46 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { fetchLiveAnchors, saveLiveAnchor, deleteLiveAnchor, LiveAnchor } from '@/services/admin/marketingService.uts'
|
||||
import { ref, computed } from 'vue'
|
||||
import CommonPagination from '@/components/CommonPagination/CommonPagination.uvue'
|
||||
|
||||
const showModal = ref(false)
|
||||
const isLoading = ref(false)
|
||||
const anchorList = ref<LiveAnchor[]>([])
|
||||
|
||||
const formData = ref<LiveAnchor>({
|
||||
nickname: '',
|
||||
const formData = ref({
|
||||
id: 0,
|
||||
name: '',
|
||||
wechat: '',
|
||||
phone: '',
|
||||
avatar_url: '',
|
||||
status: true
|
||||
avatar: ''
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
async function loadData() {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const res = await fetchLiveAnchors()
|
||||
anchorList.value = res
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载主播失败', icon: 'none' })
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
const anchorList = ref([
|
||||
{
|
||||
id: 11,
|
||||
name: '万万',
|
||||
phone: '15012341234',
|
||||
wechat: 'xiao112032014'
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: '打羽毛球',
|
||||
phone: '13333333333',
|
||||
wechat: 'evoxwht'
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
function handleAdd() {
|
||||
formData.value = {
|
||||
nickname: '',
|
||||
wechat: '',
|
||||
phone: '',
|
||||
avatar_url: '',
|
||||
status: true
|
||||
} as LiveAnchor
|
||||
const handleEdit = (item: any) => {
|
||||
formData.value = { ...item, avatar: '' }
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
function handleEdit(item : LiveAnchor) {
|
||||
formData.value = { ...item } as LiveAnchor
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
async function handleDelete(item : LiveAnchor) {
|
||||
if (item.id == null) return
|
||||
const handleDelete = (item: any) => {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该主播吗?',
|
||||
success: async (res) => {
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
const success = await deleteLiveAnchor(item.id!)
|
||||
if (success) {
|
||||
uni.showToast({ title: '删除成功' })
|
||||
loadData()
|
||||
}
|
||||
anchorList.value = anchorList.value.filter(i => i.id !== item.id)
|
||||
uni.showToast({ title: '删除成功' })
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -154,35 +138,52 @@ const handleUpload = () => {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
success: (res) => {
|
||||
formData.value.avatar_url = res.tempFilePaths[0]
|
||||
formData.value.avatar = res.tempFilePaths[0]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formData.value.nickname || !formData.value.phone) {
|
||||
uni.showToast({ title: '请填写必填项', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const handleSubmit = () => {
|
||||
uni.showToast({ title: '操作成功', icon: 'success' })
|
||||
showModal.value = false
|
||||
}
|
||||
|
||||
try {
|
||||
const success = await saveLiveAnchor(formData.value)
|
||||
if (success) {
|
||||
uni.showToast({ title: '操作成功', icon: 'success' })
|
||||
showModal.value = false
|
||||
loadData()
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '系统异常', icon: 'none' })
|
||||
}
|
||||
// 分页适配状态
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(15)
|
||||
let jumpPageInput = ''
|
||||
const pageSizeOptions = [10, 15, 20, 30, 50]
|
||||
const pageSizeOptionLabels = computed(() => pageSizeOptions.map((n: number) => `${n}条/页`))
|
||||
const pageSizeIndex = computed(() => {
|
||||
const idx = pageSizeOptions.indexOf(pageSize.value)
|
||||
return idx >= 0 ? idx : 0
|
||||
})
|
||||
const totalPage = computed(() => Math.max(1, Math.ceil(anchorList.value.length / pageSize.value)))
|
||||
const visiblePages = computed(() => {
|
||||
const t = totalPage.value
|
||||
const cur = currentPage.value
|
||||
if (t <= 7) return Array.from({ length: t }, (_: any, i: number) => i + 1)
|
||||
if (cur <= 4) return [1, 2, 3, 4, 5, -1, t]
|
||||
if (cur >= t - 3) return [1, -1, t - 4, t - 3, t - 2, t - 1, t]
|
||||
return [1, -1, cur - 1, cur, cur + 1, -1, t]
|
||||
})
|
||||
const handlePageChange = (p: number) => { currentPage.value = p }
|
||||
const handlePageSizeChange = (e: any) => {
|
||||
const idx = Number(e.detail.value)
|
||||
pageSize.value = pageSizeOptions[idx] ?? pageSizeOptions[0]
|
||||
currentPage.value = 1
|
||||
}
|
||||
const handleJumpPage = () => {
|
||||
const p = parseInt(jumpPageInput)
|
||||
if (!isNaN(p) && p >= 1 && p <= totalPage.value) currentPage.value = p
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.marketing-live-anchor {
|
||||
min-height: 100vh;
|
||||
background: #f0f2f5;
|
||||
padding: 16px;
|
||||
min-height: auto;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.border-shadow {
|
||||
@@ -258,29 +259,7 @@ async function handleSubmit() {
|
||||
.op-link { color: #1890ff; font-size: 13px; cursor: pointer; }
|
||||
.op-split { color: #e8eaec; margin: 0 8px; }
|
||||
|
||||
/* Pagination */
|
||||
.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: #606266; }
|
||||
.select-mock.mini {
|
||||
width: 100px;
|
||||
height: 28px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 12px;
|
||||
}
|
||||
.select-val { font-size: 12px; color: #606266; }
|
||||
.arrow { font-size: 10px; color: #c0c4cc; }
|
||||
/* 分页区域已迁至 CommonPagination 组件 */
|
||||
|
||||
/* Modal Styles */
|
||||
.modal-mask {
|
||||
@@ -66,54 +66,54 @@
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script uts>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
integralLotteryId: 87,
|
||||
payLotteryId: 82,
|
||||
replyLotteryId: 86
|
||||
},
|
||||
lotteryOptions: [
|
||||
{ id: 0, name: '请选择' },
|
||||
{ id: 87, name: '积分抽奖' },
|
||||
{ id: 86, name: '评价抽奖' },
|
||||
{ id: 82, name: '订单抽奖' },
|
||||
{ id: 75, name: '积分' }
|
||||
] as any[]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getLotteryName(id: number): string {
|
||||
const found = this.lotteryOptions.find((item: any): boolean => item.id == id)
|
||||
return found != null ? (found['name'] as string) : '请选择'
|
||||
},
|
||||
onIntegralLotteryChange(e: any) {
|
||||
this.form.integralLotteryId = this.lotteryOptions[e.detail.value].id
|
||||
},
|
||||
onPayLotteryChange(e: any) {
|
||||
this.form.payLotteryId = this.lotteryOptions[e.detail.value].id
|
||||
},
|
||||
onReplyLotteryChange(e: any) {
|
||||
this.form.replyLotteryId = this.lotteryOptions[e.detail.value].id
|
||||
},
|
||||
handleSave() {
|
||||
uni.showLoading({ title: '正在保存...' })
|
||||
setTimeout(() => {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
}, 800)
|
||||
}
|
||||
}
|
||||
<script setup lang="uts">
|
||||
import { reactive } from 'vue'
|
||||
|
||||
const form = reactive({
|
||||
integralLotteryId: 87,
|
||||
payLotteryId: 82,
|
||||
replyLotteryId: 86
|
||||
})
|
||||
|
||||
const lotteryOptions = reactive([
|
||||
{ id: 0, name: '请选择' },
|
||||
{ id: 87, name: '积分抽奖' },
|
||||
{ id: 86, name: '评价抽奖' },
|
||||
{ id: 82, name: '订单抽奖' },
|
||||
{ id: 75, name: '积分' }
|
||||
] as any[])
|
||||
|
||||
const getLotteryName = (id: number): string => {
|
||||
const found = lotteryOptions.find((item: any): boolean => item.id == id)
|
||||
return found != null ? (found['name'] as string) : '请选择'
|
||||
}
|
||||
|
||||
const onIntegralLotteryChange = (e: any) => {
|
||||
form.integralLotteryId = lotteryOptions[e.detail.value].id
|
||||
}
|
||||
|
||||
const onPayLotteryChange = (e: any) => {
|
||||
form.payLotteryId = lotteryOptions[e.detail.value].id
|
||||
}
|
||||
|
||||
const onReplyLotteryChange = (e: any) => {
|
||||
form.replyLotteryId = lotteryOptions[e.detail.value].id
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
uni.showLoading({ title: '正在保存...' })
|
||||
setTimeout(() => {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
}, 800)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-marketing-lottery-config {
|
||||
padding: 16px;
|
||||
background-color: #f5f7f9;
|
||||
min-height: 100vh;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.box-shadow {
|
||||
@@ -1,491 +0,0 @@
|
||||
<template>
|
||||
<view class="admin-marketing-lottery-list">
|
||||
<!-- 筛选区域 -->
|
||||
<view class="filter-card box-shadow">
|
||||
<view class="filter-row">
|
||||
<view class="filter-item">
|
||||
<text class="label">活动时段:</text>
|
||||
<AnalyticsDateRangePicker
|
||||
:initialStartDate="startDate"
|
||||
:initialEndDate="endDate"
|
||||
@apply="onApplyRange"
|
||||
@clear="onClearRange"
|
||||
/>
|
||||
</view>
|
||||
<view class="filter-item">
|
||||
<text class="label">活动状态:</text>
|
||||
<picker :range="statusOptions" range-key="label" @change="statusChange">
|
||||
<view class="picker-value">{{ statusOptions[statusIndex].label }} <text class="iconfont icon-arrow-down">▼</text></view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="filter-item">
|
||||
<text class="label">活动类型:</text>
|
||||
<picker :range="typeOptions" range-key="label" @change="typeChange">
|
||||
<view class="picker-value">{{ typeOptions[typeIndex].label }} <text class="iconfont icon-arrow-down">▼</text></view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
<view class="filter-row second-row">
|
||||
<view class="filter-item">
|
||||
<text class="label">搜索抽奖:</text>
|
||||
<input class="admin-input" placeholder="请输入活动名称" v-model="searchQuery" style="width: 200px;" @confirm="handleSearch" />
|
||||
</view>
|
||||
<view class="filter-item">
|
||||
<button class="admin-btn admin-btn-primary search-btn" @click="handleSearch">搜索</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作工具栏 -->
|
||||
<view class="table-toolbar">
|
||||
<button class="admin-btn admin-btn-primary" @click="handleAdd">创建抽奖活动</button>
|
||||
</view>
|
||||
|
||||
<!-- 表格区域 -->
|
||||
<view class="table-card box-shadow">
|
||||
<view class="table-container">
|
||||
<!-- Loading 遮罩 -->
|
||||
<view v-if="isLoading" class="loading-mask">
|
||||
<text class="loading-text">数据加载中...</text>
|
||||
</view>
|
||||
|
||||
<view class="table-header">
|
||||
<text class="col-60 center">ID</text>
|
||||
<text class="col-120">活动名称</text>
|
||||
<text class="col-100">活动类型</text>
|
||||
<text class="col-80 center">抽奖人数</text>
|
||||
<text class="col-80 center">中奖人数</text>
|
||||
<text class="col-80 center">抽奖次数</text>
|
||||
<text class="col-80 center">中奖次数</text>
|
||||
<text class="col-80 center">活动状态</text>
|
||||
<text class="col-80 center">开启状态</text>
|
||||
<text class="col-180">活动时间</text>
|
||||
<text class="col-120 center">操作</text>
|
||||
</view>
|
||||
|
||||
<view class="table-body">
|
||||
<view v-if="tableData.length === 0 && !isLoading" class="empty-row">
|
||||
<text>暂无抽奖活动数据</text>
|
||||
</view>
|
||||
<view v-for="(item, index) in tableData" :key="index" class="table-row">
|
||||
<view class="col-60 center"><text class="id-text">{{ item.id }}</text></view>
|
||||
<view class="col-120"><text class="cell-text">{{ item.name }}</text></view>
|
||||
<view class="col-100"><text class="cell-text">{{ getTypeName(item.type) }}</text></view>
|
||||
<view class="col-80 center"><text class="cell-text">{{ item.memberCount ?? 0 }}</text></view>
|
||||
<view class="col-80 center"><text class="cell-text">{{ item.winningMemberCount ?? 0 }}</text></view>
|
||||
<view class="col-80 center"><text class="cell-text">{{ item.lotteryTimes ?? 0 }}</text></view>
|
||||
<view class="col-80 center"><text class="cell-text">{{ item.winningTimes ?? 0 }}</text></view>
|
||||
<view class="col-80 center">
|
||||
<text class="status-text">{{ getStatusLabel(item) }}</text>
|
||||
</view>
|
||||
<view class="col-80 center">
|
||||
<view class="switch-box" :class="item.is_open ? 'active' : ''" @click="toggleStatus(item)">
|
||||
<view class="switch-dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="col-180">
|
||||
<view class="time-range">
|
||||
<text class="time-label">始: {{ formatDateTime(item.start_time) }}</text>
|
||||
<text class="time-label">终: {{ formatDateTime(item.end_time) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="col-120 center ops-cell">
|
||||
<text class="op-link" @click="handleEdit(item)">编辑</text>
|
||||
<text class="op-link" @click="handleRecords(item)">记录</text>
|
||||
<text class="op-link danger" @click="handleDelete(item.id)">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分页区域 -->
|
||||
<view class="table-pagination">
|
||||
<text class="total-text">共 {{ total }} 条</text>
|
||||
<view class="page-ops">
|
||||
<button class="page-btn" :disabled="page <= 1" @click="onPrevPage">上一页</button>
|
||||
<text class="current-page">{{ page }}</text>
|
||||
<button class="page-btn" :disabled="tableData.length < pageSize" @click="onNextPage">下一页</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import AnalyticsDateRangePicker from '@/components/analytics/AnalyticsDateRangePicker.uvue'
|
||||
import { fetchLotteryList, saveLotteryActivity, deleteLotteryActivity, LotteryActivity } from '@/services/admin/marketingService.uts'
|
||||
|
||||
const searchQuery = ref('')
|
||||
const isLoading = ref(false)
|
||||
const total = ref(0)
|
||||
const tableData = ref<LotteryActivity[]>([])
|
||||
const page = ref(1)
|
||||
const pageSize = ref(15)
|
||||
|
||||
const startDate = ref('')
|
||||
const endDate = ref('')
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '全部', value: 0 },
|
||||
{ label: '开启', value: 1 },
|
||||
{ label: '关闭', value: 2 }
|
||||
]
|
||||
const statusIndex = ref(0)
|
||||
|
||||
const typeOptions = [
|
||||
{ label: '全部', value: 0 },
|
||||
{ label: '积分抽奖', value: 1 },
|
||||
{ label: '订单评价', value: 2 },
|
||||
{ label: '订单支付', value: 3 }
|
||||
]
|
||||
const typeIndex = ref(0)
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
async function loadData() {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const st = startDate.value ? (startDate.value + ' 00:00:00') : null
|
||||
const et = endDate.value ? (endDate.value + ' 23:59:59') : null
|
||||
|
||||
const res = await fetchLotteryList({
|
||||
search: searchQuery.value,
|
||||
status: statusOptions[statusIndex.value].value,
|
||||
type: typeOptions[typeIndex.value].value,
|
||||
page: page.value,
|
||||
pageSize: pageSize,
|
||||
startTime: st,
|
||||
endTime: et
|
||||
} as any)
|
||||
tableData.value = res.items
|
||||
total.value = res.total
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
page.value = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function onStatusChange(e : any) {
|
||||
statusIndex.value = parseInt(String(e.detail.value))
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function onTypeChange(e : any) {
|
||||
typeIndex.value = parseInt(String(e.detail.value))
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
async function toggleStatus(item : LotteryActivity) {
|
||||
if (item.id == null) return
|
||||
const nextOpen = !item.is_open
|
||||
const success = await saveLotteryActivity({ ...item, is_open: nextOpen } as LotteryActivity)
|
||||
if (success) {
|
||||
item.is_open = nextOpen
|
||||
uni.showToast({ title: '状态已变更', icon: 'success' })
|
||||
}
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
uni.showToast({ title: '添加功能开发中', icon: 'none' })
|
||||
}
|
||||
|
||||
function handleEdit(item : LotteryActivity) {
|
||||
uni.showToast({ title: '编辑功能开发中', icon: 'none' })
|
||||
}
|
||||
|
||||
function handleRecords(item : LotteryActivity) {
|
||||
uni.showToast({ title: '记录功能开发中', icon: 'none' })
|
||||
}
|
||||
|
||||
async function handleDelete(id : string | undefined) {
|
||||
if (id == null) return
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该抽奖活动吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
const success = await deleteLotteryActivity(id!)
|
||||
if (success) {
|
||||
uni.showToast({ title: '删除成功' })
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function onApplyRange(payload : any) {
|
||||
startDate.value = payload?.start ?? ''
|
||||
endDate.value = payload?.end ?? ''
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function onClearRange() {
|
||||
startDate.value = ''
|
||||
endDate.value = ''
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function onPrevPage() {
|
||||
if (page.value > 1) {
|
||||
page.value--
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
|
||||
function onNextPage() {
|
||||
if (tableData.value.length >= pageSize) {
|
||||
page.value++
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
|
||||
function getTypeName(type : number) : string {
|
||||
const found = typeOptions.find(o => o.value === type)
|
||||
return found != null ? found.label : '未知'
|
||||
}
|
||||
|
||||
function getStatusLabel(item : LotteryActivity) : string {
|
||||
const now = Date.now()
|
||||
const start = new Date(item.start_time).getTime()
|
||||
const end = new Date(item.end_time).getTime()
|
||||
|
||||
if (now < start) return '未开始'
|
||||
if (now > end) return '已结束'
|
||||
return '进行中'
|
||||
}
|
||||
|
||||
function formatDateTime(iso : string) : string {
|
||||
return iso.substring(0, 16).replace('T', ' ')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-marketing-lottery-list {
|
||||
padding: 16px;
|
||||
background-color: #f5f7f9;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.box-shadow {
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 筛选区域 */
|
||||
.filter-card {
|
||||
padding: 15px 20px;
|
||||
}
|
||||
.filter-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.second-row {
|
||||
margin-top: 5px;
|
||||
}
|
||||
.filter-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-right: 24px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.picker-value {
|
||||
min-width: 120px;
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.admin-input {
|
||||
height: 32px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.search-btn {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
padding: 0 15px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.admin-btn {
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.admin-btn-primary {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 表格工具栏 */
|
||||
.table-toolbar {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 表格主体 */
|
||||
.table-card {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.table-container {
|
||||
min-height: 400px;
|
||||
position: relative;
|
||||
}
|
||||
.loading-mask {
|
||||
position: absolute; top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(255,255,255,0.7); display: flex; align-items: center; justify-content: center; z-index: 10;
|
||||
}
|
||||
.loading-text { color: #1890ff; font-size: 14px; }
|
||||
|
||||
.table-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: #f8f8f9;
|
||||
border-bottom: 1px solid #e8eaec;
|
||||
padding: 12px 0;
|
||||
}
|
||||
.table-header text {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #515a6e;
|
||||
}
|
||||
.table-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #e8eaec;
|
||||
padding: 12px 0;
|
||||
}
|
||||
.table-row:hover { background-color: #fafafa; }
|
||||
|
||||
/* 列定义 */
|
||||
.col-60 { width: 60px; }
|
||||
.col-80 { width: 80px; }
|
||||
.col-100 { width: 100px; }
|
||||
.col-120 { width: 120px; }
|
||||
.col-180 { width: 180px; }
|
||||
.center { text-align: center; justify-content: center; display: flex; align-items: center; }
|
||||
|
||||
.cell-text { font-size: 13px; color: #515a6e; }
|
||||
.id-text { font-size: 13px; color: #808695; }
|
||||
.status-text { font-size: 13px; color: #1890ff; }
|
||||
|
||||
/* 开关组件 */
|
||||
.switch-box {
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
background-color: #ccc;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
transition: background-color 0.3s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.switch-box.active {
|
||||
background-color: #2d8cf0;
|
||||
}
|
||||
.switch-dot {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: #fff;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
transition: left 0.3s;
|
||||
}
|
||||
.switch-box.active .switch-dot {
|
||||
left: 22px;
|
||||
}
|
||||
|
||||
.time-range {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.time-label {
|
||||
font-size: 12px;
|
||||
color: #808695;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.ops-cell {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
}
|
||||
.op-link {
|
||||
font-size: 13px;
|
||||
color: #2d8cf0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.op-link.danger { color: #f5222d; }
|
||||
|
||||
/* 分页 */
|
||||
.table-pagination {
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.total-text {
|
||||
font-size: 14px;
|
||||
color: #515a6e;
|
||||
margin-right: 15px;
|
||||
}
|
||||
.page-ops {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.page-btn {
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
padding: 0 12px;
|
||||
font-size: 12px;
|
||||
border: 1px solid #dcdfe6;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.page-btn[disabled] { opacity: 0.5; }
|
||||
.current-page {
|
||||
font-size: 14px;
|
||||
color: #2d8cf0;
|
||||
font-weight: bold;
|
||||
}
|
||||
.empty-row { padding: 60px 0; text-align: center; color: #999; }
|
||||
</style>
|
||||
395
pages/mall/admin/marketing/lottery/list/index.uvue
Normal file
395
pages/mall/admin/marketing/lottery/list/index.uvue
Normal file
@@ -0,0 +1,395 @@
|
||||
<template>
|
||||
<view class="admin-marketing-lottery-list">
|
||||
<!-- 筛选区域 -->
|
||||
<view class="filter-card box-shadow">
|
||||
<view class="filter-row">
|
||||
<view class="filter-item">
|
||||
<text class="label">活动时间:</text>
|
||||
<view class="date-range-mock">
|
||||
<text class="date-text">开始日期 - 结束日期</text>
|
||||
<text class="iconfont icon-calendar"></text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="filter-item">
|
||||
<text class="label">活动状态:</text>
|
||||
<picker :range="statusOptions" range-key="label" @change="statusChange">
|
||||
<view class="picker-value">{{ getStatusLabel(currentStatus) }} <text class="iconfont icon-arrow-down"></text></view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="filter-item">
|
||||
<text class="label">活动类型:</text>
|
||||
<picker :range="typeOptions" range-key="label" @change="typeChange">
|
||||
<view class="picker-value">{{ getTypeLabel(currentType) }} <text class="iconfont icon-arrow-down"></text></view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
<view class="filter-row second-row">
|
||||
<view class="filter-item">
|
||||
<text class="label">搜索抽奖:</text>
|
||||
<input class="admin-input" placeholder="请输入活动名称" v-model="searchQuery" style="width: 200px;" />
|
||||
</view>
|
||||
<view class="filter-item">
|
||||
<button class="admin-btn admin-btn-primary search-btn" @click="handleSearch">搜索</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作工具栏 -->
|
||||
<view class="table-toolbar">
|
||||
<button class="admin-btn admin-btn-primary">创建抽奖活动</button>
|
||||
</view>
|
||||
|
||||
<!-- 表格区域 -->
|
||||
<view class="table-card box-shadow">
|
||||
<view class="table-header">
|
||||
<text class="col-60 center">ID</text>
|
||||
<text class="col-120">活动名称</text>
|
||||
<text class="col-100">活动类型</text>
|
||||
<text class="col-80 center">抽奖人数</text>
|
||||
<text class="col-80 center">中奖人数</text>
|
||||
<text class="col-80 center">抽奖次数</text>
|
||||
<text class="col-80 center">中奖次数</text>
|
||||
<text class="col-80 center">活动状态</text>
|
||||
<text class="col-80 center">状态</text>
|
||||
<text class="col-180">活动时间</text>
|
||||
<text class="col-120 center">操作</text>
|
||||
</view>
|
||||
|
||||
<view class="table-body">
|
||||
<view v-for="(item, index) in tableData" :key="index" class="table-row">
|
||||
<view class="col-60 center"><text class="id-text">{{ item.id }}</text></view>
|
||||
<view class="col-120"><text class="cell-text">{{ item.name }}</text></view>
|
||||
<view class="col-100"><text class="cell-text">{{ item.typeName }}</text></view>
|
||||
<view class="col-80 center"><text class="cell-text">{{ item.memberCount }}</text></view>
|
||||
<view class="col-80 center"><text class="cell-text">{{ item.winningMemberCount }}</text></view>
|
||||
<view class="col-80 center"><text class="cell-text">{{ item.lotteryTimes }}</text></view>
|
||||
<view class="col-80 center"><text class="cell-text">{{ item.winningTimes }}</text></view>
|
||||
<view class="col-80 center">
|
||||
<text class="status-text">{{ item.statusText }}</text>
|
||||
</view>
|
||||
<view class="col-80 center">
|
||||
<StatusSwitch v-model="item.isOpen" />
|
||||
</view>
|
||||
<view class="col-180">
|
||||
<view class="time-range">
|
||||
<text class="time-label">开始: {{ item.startTime }}</text>
|
||||
<text class="time-label">结束: {{ item.endTime }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="col-120 center ops-cell">
|
||||
<text class="op-link">编辑</text>
|
||||
<text class="op-link">抽奖记录</text>
|
||||
<text class="op-link">更多 <text class="iconfont icon-arrow-down" style="font-size: 10px;"></text></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分页区域 -->
|
||||
<CommonPagination
|
||||
v-if="total > 0"
|
||||
:total="total"
|
||||
:loading="false"
|
||||
:currentPage="currentPage"
|
||||
:pageSize="currentSize"
|
||||
:pageSizeOptionLabels="pageSizeOptionLabels"
|
||||
:pageSizeIndex="pageSizeIndex"
|
||||
:visiblePages="visiblePages"
|
||||
:totalPage="totalPage"
|
||||
:jumpPageInput="jumpPageInput"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
@page-change="handlePageChange"
|
||||
@update:jumpPageInput="updateJumpPageInput"
|
||||
@jump-page="handleJumpPage"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script uts>
|
||||
import StatusSwitch from '@/components/StatusSwitch.uvue'
|
||||
import CommonPagination from '@/components/CommonPagination/CommonPagination.uvue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
StatusSwitch,
|
||||
CommonPagination
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchQuery: '',
|
||||
currentStatus: 0,
|
||||
currentType: 0,
|
||||
total: 4,
|
||||
currentPage: 1,
|
||||
currentSize: 15,
|
||||
jumpPageInput: '',
|
||||
pageSizes: ['15条/页', '30条/页', '50条/页'],
|
||||
statusOptions: [
|
||||
{ label: '全部', value: 0 },
|
||||
{ label: '未开始', value: 1 },
|
||||
{ label: '进行中', value: 2 },
|
||||
{ label: '已结束', value: 3 }
|
||||
] as any[],
|
||||
typeOptions: [
|
||||
{ label: '全部', value: 0 },
|
||||
{ label: '积分抽奖', value: 1 },
|
||||
{ label: '订单评价', value: 2 },
|
||||
{ label: '订单支付', value: 3 }
|
||||
] as any[],
|
||||
tableData: [
|
||||
{
|
||||
id: 87,
|
||||
name: '积分抽奖',
|
||||
typeName: '积分抽取',
|
||||
memberCount: 166,
|
||||
winningMemberCount: 0,
|
||||
lotteryTimes: 329,
|
||||
winningTimes: 0,
|
||||
statusText: '已结束',
|
||||
isOpen: true,
|
||||
startTime: '2025-12-04 00:00:00',
|
||||
endTime: '2026-01-31 23:59:59'
|
||||
},
|
||||
{
|
||||
id: 86,
|
||||
name: '评价抽奖',
|
||||
typeName: '订单评价',
|
||||
memberCount: 3,
|
||||
winningMemberCount: 3,
|
||||
lotteryTimes: 4,
|
||||
winningTimes: 3,
|
||||
statusText: '已结束',
|
||||
isOpen: true,
|
||||
startTime: '2023-12-12 00:00:00',
|
||||
endTime: '2024-01-16 23:59:59'
|
||||
},
|
||||
{
|
||||
id: 82,
|
||||
name: '订单抽奖',
|
||||
typeName: '订单支付',
|
||||
memberCount: 100,
|
||||
winningMemberCount: 5,
|
||||
lotteryTimes: 124,
|
||||
winningTimes: 6,
|
||||
statusText: '已结束',
|
||||
isOpen: true,
|
||||
startTime: '2023-08-16 00:00:00',
|
||||
endTime: '2024-01-31 23:59:59'
|
||||
},
|
||||
{
|
||||
id: 75,
|
||||
name: '积分',
|
||||
typeName: '积分抽取',
|
||||
memberCount: 1288,
|
||||
winningMemberCount: 1130,
|
||||
lotteryTimes: 3413,
|
||||
winningTimes: 2628,
|
||||
statusText: '已结束',
|
||||
isOpen: true,
|
||||
startTime: '2025-10-01 00:00:00',
|
||||
endTime: '2025-11-30 23:59:59'
|
||||
}
|
||||
] as any[]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pageSizeOptions(): number[] { return [10, 15, 20, 30, 50] },
|
||||
pageSizeOptionLabels(): string[] { return (this.pageSizeOptions as number[]).map((n: number) => `${n}条/页`) },
|
||||
pageSizeIndex(): number {
|
||||
const idx = (this.pageSizeOptions as number[]).indexOf(this.currentSize as number)
|
||||
return idx >= 0 ? idx : 0
|
||||
},
|
||||
totalPage(): number { return Math.max(1, Math.ceil((this.total as number) / (this.currentSize as number))) },
|
||||
visiblePages(): number[] {
|
||||
const t = this.totalPage as number
|
||||
const cur = this.currentPage as number
|
||||
if (t <= 7) return Array.from({ length: t }, (_: any, i: number) => i + 1)
|
||||
if (cur <= 4) return [1, 2, 3, 4, 5, -1, t]
|
||||
if (cur >= t - 3) return [1, -1, t - 4, t - 3, t - 2, t - 1, t]
|
||||
return [1, -1, cur - 1, cur, cur + 1, -1, t]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getStatusLabel(val : number) : string {
|
||||
const found = this.statusOptions.find((item : any) : boolean => item.value == val)
|
||||
return found != null ? (found['label'] as string) : '全部'
|
||||
},
|
||||
getTypeLabel(val : number) : string {
|
||||
const found = this.typeOptions.find((item : any) : boolean => item.value == val)
|
||||
return found != null ? (found['label'] as string) : '全部'
|
||||
},
|
||||
statusChange(e : any) {
|
||||
this.currentStatus = this.statusOptions[e.detail.value].value
|
||||
},
|
||||
typeChange(e : any) {
|
||||
this.currentType = this.typeOptions[e.detail.value].value
|
||||
},
|
||||
pageSizeChange(e : any) {
|
||||
const val = this.pageSizes[e.detail.value]
|
||||
this.currentSize = parseInt(val.replace('条/页', ''))
|
||||
},
|
||||
handleSearch() {
|
||||
uni.showToast({ title: '搜索', icon: 'none' })
|
||||
},
|
||||
handlePageChange(p: number) { this.currentPage = p as any },
|
||||
handlePageSizeChange(e: any) {
|
||||
const idx = Number(e.detail.value)
|
||||
this.currentSize = ((this.pageSizeOptions as number[])[idx] ?? (this.pageSizeOptions as number[])[0]) as any
|
||||
this.currentPage = 1 as any
|
||||
},
|
||||
handleJumpPage() {
|
||||
const p = parseInt(this.jumpPageInput as string)
|
||||
if (!isNaN(p) && p >= 1 && p <= (this.totalPage as number)) this.currentPage = p as any
|
||||
},
|
||||
updateJumpPageInput(val: string) { this.jumpPageInput = val as any }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-marketing-lottery-list {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.box-shadow {
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 筛选区域 */
|
||||
.filter-card {
|
||||
padding: 15px 20px;
|
||||
}
|
||||
.filter-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.second-row {
|
||||
margin-top: 5px;
|
||||
}
|
||||
.filter-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-right: 24px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.date-range-mock {
|
||||
width: 280px;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.date-text { font-size: 13px; color: #c0c4cc; }
|
||||
|
||||
.picker-value {
|
||||
width: 160px;
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.admin-input {
|
||||
height: 32px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.search-btn {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
padding: 0 15px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
/* 表格工具栏 */
|
||||
.table-toolbar {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 表格主体 */
|
||||
.table-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
.table-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: #f8f8f9;
|
||||
border-bottom: 1px solid #e8eaec;
|
||||
padding: 12px 0;
|
||||
}
|
||||
.table-header text {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #515a6e;
|
||||
}
|
||||
.table-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #e8eaec;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
/* 列定义 */
|
||||
.col-60 { width: 60px; }
|
||||
.col-80 { width: 80px; }
|
||||
.col-100 { width: 100px; }
|
||||
.col-120 { width: 120px; }
|
||||
.col-180 { width: 180px; }
|
||||
.center { text-align: center; justify-content: center; }
|
||||
|
||||
.cell-text { font-size: 13px; color: #515a6e; }
|
||||
.id-text { font-size: 13px; color: #808695; }
|
||||
.status-text { font-size: 13px; color: #515a6e; }
|
||||
|
||||
.time-range {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.time-label {
|
||||
font-size: 12px;
|
||||
color: #808695;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.ops-cell {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.op-link {
|
||||
font-size: 13px;
|
||||
color: #2d8cf0;
|
||||
margin: 0 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 分页区域已迁至 CommonPagination 组件 */
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
28
pages/mall/admin/marketing/marketing-statistics/index.uvue
Normal file
28
pages/mall/admin/marketing/marketing-statistics/index.uvue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<view class="admin-page-container">
|
||||
<text class="placeholder-title">Marketing Statistics</text>
|
||||
<view class="chart-box">
|
||||
<EChartsView :option="chartOption" class="chart-view" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import EChartsView from '@/uni_modules/charts/EChartsView.uvue'
|
||||
|
||||
const chartOption = ref({
|
||||
title: { text: 'Marketing Statistics Demo' },
|
||||
tooltip: {},
|
||||
xAxis: { data: ['A', 'B', 'C', 'D'] },
|
||||
yAxis: {},
|
||||
series: [{ type: 'bar', data: [5, 20, 36, 10] }]
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-page-container { padding: 20px; }
|
||||
.placeholder-title { font-size: 24px; font-weight: bold; margin-bottom: 20px; }
|
||||
.chart-box { height: 300px; background: #fff; border: 1px solid #eee; }
|
||||
.chart-view { width: 100%; height: 100%; }
|
||||
</style>
|
||||
@@ -65,9 +65,9 @@ const records = ref([
|
||||
|
||||
<style scoped lang="scss">
|
||||
.marketing-member-record {
|
||||
padding: 16px;
|
||||
background: #f0f2f5;
|
||||
min-height: 100vh;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.border-shadow {
|
||||
@@ -85,9 +85,9 @@ const handleEdit = (item: MemberRight) => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.marketing-member-right {
|
||||
padding: 16px;
|
||||
background: #f0f2f5;
|
||||
min-height: 100vh;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.border-shadow {
|
||||
@@ -99,9 +99,9 @@ const handleEdit = (item: MemberType) => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.marketing-member-type {
|
||||
padding: 16px;
|
||||
background: #f0f2f5;
|
||||
min-height: 100vh;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.border-shadow {
|
||||
@@ -45,13 +45,13 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<button class="btn-select-action" @click="openCouponModal">选择优惠券</button>
|
||||
<button class="btn-select-action" @click="showCouponModal = true">选择优惠券</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 确认按钮 -->
|
||||
<view class="form-submit-bar">
|
||||
<button class="btn-primary-confirm" @click="handleSubmit" :disabled="isLoading">确认</button>
|
||||
<button class="btn-primary-confirm" @click="handleSubmit">确认</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -65,13 +65,8 @@
|
||||
</view>
|
||||
|
||||
<view class="modal-body">
|
||||
<!-- 加载中 -->
|
||||
<view v-if="isLoading" class="modal-loading">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 编辑/设置模式 -->
|
||||
<view v-else-if="isEditing" class="setting-form">
|
||||
<!-- 编辑/设置模式:当用户由于点击“修改设置”时触发 -->
|
||||
<view v-if="isEditing" class="setting-form">
|
||||
<view class="setting-row">
|
||||
<text class="setting-label">显示名称:</text>
|
||||
<input class="setting-input" v-model="editingCoupon.name" placeholder="请输入页面显示的名称" />
|
||||
@@ -80,13 +75,13 @@
|
||||
<text class="setting-label">发放描述:</text>
|
||||
<input class="setting-input" v-model="editingCoupon.desc" placeholder="请输入发放时的描述" />
|
||||
</view>
|
||||
<view class="setting-tip">
|
||||
<text class="tip-text">* 此处的修改仅影响“新人礼”活动中的展示,不影响优惠券自身配置</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 选择模式 -->
|
||||
<view v-else class="selection-list">
|
||||
<view v-if="couponOptions.length === 0" class="empty-tip">
|
||||
<text>暂无可用优惠券</text>
|
||||
</view>
|
||||
<view v-for="(item, index) in couponOptions" :key="index"
|
||||
class="selection-card" :class="{'selected-card': isSelected(item)}"
|
||||
@click="toggleCoupon(item)">
|
||||
@@ -113,73 +108,42 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { fetchNewcomerConfig, saveNewcomerConfig, fetchAdminCoupons, NewcomerConfig, CouponTemplate } from '@/services/admin/marketingService.uts'
|
||||
import { ref, reactive } from 'vue'
|
||||
|
||||
interface NewcomerCoupon {
|
||||
id : string;
|
||||
name : string;
|
||||
desc : string;
|
||||
interface Coupon {
|
||||
id: number;
|
||||
name: string;
|
||||
desc: string;
|
||||
}
|
||||
|
||||
const formData = reactive({
|
||||
balance: '0',
|
||||
integral: '0',
|
||||
coupons: [] as NewcomerCoupon[]
|
||||
balance: '88888',
|
||||
integral: '88888',
|
||||
coupons: [] as Coupon[]
|
||||
})
|
||||
|
||||
const isLoading = ref(false)
|
||||
const showCouponModal = ref(false)
|
||||
const isEditing = ref(false)
|
||||
const editingIndex = ref(-1)
|
||||
const editingCoupon = reactive<NewcomerCoupon>({ id: '', name: '', desc: '' })
|
||||
|
||||
const couponOptions = ref<NewcomerCoupon[]>([])
|
||||
// 使用 reactive 并显式指定初始对象,后续通过类型断言或接口约束保证类型安全
|
||||
const editingCoupon = reactive({
|
||||
id: 0,
|
||||
name: '',
|
||||
desc: ''
|
||||
}) as Coupon
|
||||
|
||||
onMounted(() => {
|
||||
loadConfig()
|
||||
})
|
||||
const couponOptions = reactive([
|
||||
{ id: 1, name: '满100减10元券', desc: '全场通用' },
|
||||
{ id: 2, name: '新人5元无门槛', desc: '仅限新人使用' },
|
||||
{ id: 3, name: '满200减50元券', desc: '限特定商品' }
|
||||
] as Coupon[])
|
||||
|
||||
async function loadConfig() {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const res = await fetchNewcomerConfig()
|
||||
if (res != null) {
|
||||
formData.balance = String(res.balance_reward)
|
||||
formData.integral = String(res.integral_reward)
|
||||
formData.coupons = res.coupons_json.map((c : any) : NewcomerCoupon => ({
|
||||
id: String(c.id),
|
||||
name: String(c.name),
|
||||
desc: String(c.desc)
|
||||
} as NewcomerCoupon))
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载配置失败:', e)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openCouponModal() {
|
||||
showCouponModal.value = true
|
||||
isLoading.value = true
|
||||
try {
|
||||
const res = await fetchAdminCoupons({ page: 1, pageSize: 100, status: 1 })
|
||||
couponOptions.value = res.items.map((item : CouponTemplate) : NewcomerCoupon => ({
|
||||
id: item.id!,
|
||||
name: item.name,
|
||||
desc: item.description ?? ''
|
||||
} as NewcomerCoupon))
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function isSelected(item : NewcomerCoupon) : boolean {
|
||||
function isSelected(item: Coupon): boolean {
|
||||
return formData.coupons.some(c => c.id === item.id)
|
||||
}
|
||||
|
||||
function toggleCoupon(item : NewcomerCoupon) {
|
||||
function toggleCoupon(item: Coupon) {
|
||||
const index = formData.coupons.findIndex(c => c.id === item.id)
|
||||
if (index > -1) {
|
||||
formData.coupons.splice(index, 1)
|
||||
@@ -188,11 +152,11 @@ function toggleCoupon(item : NewcomerCoupon) {
|
||||
}
|
||||
}
|
||||
|
||||
function removeCoupon(index : number) {
|
||||
function removeCoupon(index: number) {
|
||||
formData.coupons.splice(index, 1)
|
||||
}
|
||||
|
||||
function editCoupon(index : number) {
|
||||
function editCoupon(index: number) {
|
||||
editingIndex.value = index
|
||||
const coupon = formData.coupons[index]
|
||||
editingCoupon.id = coupon.id
|
||||
@@ -215,33 +179,23 @@ function confirmModal() {
|
||||
closeModal()
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
function handleSubmit() {
|
||||
uni.showLoading({ title: '保存中...' })
|
||||
try {
|
||||
const payload : NewcomerConfig = {
|
||||
balance_reward: parseFloat(formData.balance),
|
||||
integral_reward: parseInt(formData.integral),
|
||||
coupons_json: formData.coupons as any[]
|
||||
}
|
||||
const success = await saveNewcomerConfig(payload)
|
||||
if (success) {
|
||||
uni.showToast({ title: '设置已生效', icon: 'success' })
|
||||
} else {
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '系统异常', icon: 'none' })
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
uni.hideLoading()
|
||||
}
|
||||
uni.showToast({
|
||||
title: '设置已生效',
|
||||
icon: 'success'
|
||||
})
|
||||
}, 500)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.admin-main {
|
||||
padding: 24px;
|
||||
background-color: #f0f2f5;
|
||||
min-height: 100vh;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.card-container {
|
||||
@@ -568,22 +522,6 @@ async function handleSubmit() {
|
||||
color: #fff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.modal-loading {
|
||||
padding: 40px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #1890ff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
padding: 40px 0;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
<template>
|
||||
<AdminLayout :currentPage="currentPage">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">{{ title }}</text>
|
||||
<text class="sub-title">页面占位 (自动生成)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
const currentPage = ref<string>('points-config')
|
||||
const title = ref<string>('config')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/uni.scss';
|
||||
.page { padding: $space-lg; }
|
||||
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
|
||||
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
|
||||
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
|
||||
</style>
|
||||
|
||||
|
||||
@@ -96,44 +96,34 @@
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script uts>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
activeTab: 0,
|
||||
form: {
|
||||
integral_name: '积分',
|
||||
integral_unit: 10,
|
||||
integral_ratio: 0.1,
|
||||
integral_max: 50,
|
||||
freeze_time: 7,
|
||||
valid_type: 0,
|
||||
valid_year: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
validTypeChange(e: any) {
|
||||
this.form.valid_type = parseInt(e.detail.value as string)
|
||||
},
|
||||
handleSubmit() {
|
||||
uni.showLoading({ title: '保存中...' })
|
||||
setTimeout(() => {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
<script setup lang="uts">
|
||||
import { ref, reactive } from 'vue'
|
||||
|
||||
const activeTab = ref(0)
|
||||
const form = reactive({
|
||||
integral_name: '积分',
|
||||
integral_unit: 10,
|
||||
integral_ratio: 0.1,
|
||||
integral_max: 50,
|
||||
freeze_time: 7,
|
||||
valid_type: 0,
|
||||
valid_year: 1
|
||||
})
|
||||
|
||||
const validTypeChange = (e: any) => {
|
||||
form.valid_type = parseInt(e.detail.value as string)
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
uni.showLoading({ title: '保存中...' })
|
||||
setTimeout(() => {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
}, 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-marketing-integral-config {
|
||||
padding: 16px;
|
||||
background-color: #f5f7f9;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.box-shadow {
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
@@ -144,6 +134,7 @@ export default {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
min-height: 650px;
|
||||
}
|
||||
|
||||
/* 顶部标签 */
|
||||
@@ -175,6 +166,7 @@ export default {
|
||||
|
||||
.config-body {
|
||||
padding: 40px 60px;
|
||||
min-height: 550px;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
@@ -1,25 +0,0 @@
|
||||
<template>
|
||||
<AdminLayout :currentPage="currentPage">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">{{ title }}</text>
|
||||
<text class="sub-title">页面已修复 (UTF-8)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
const currentPage = ref<string>('goods')
|
||||
const title = ref<string>('goods')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/uni.scss';
|
||||
.page { padding: $space-lg; }
|
||||
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
|
||||
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
|
||||
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
|
||||
</style>
|
||||
@@ -1,95 +0,0 @@
|
||||
<template>
|
||||
<AdminLayout :currentPage="currentPage">
|
||||
<view class="Page">
|
||||
<view class="Header">
|
||||
<text class="Title">积分统计</text>
|
||||
<text class="SubTitle">marketing/points/index</text>
|
||||
</view>
|
||||
|
||||
<view class="Card">
|
||||
<text class="Label">页面参数(query)</text>
|
||||
<text class="Mono">{{ params }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
|
||||
const params = ref('')
|
||||
const tab = ref('stats')
|
||||
|
||||
onLoad((options) => {
|
||||
params.value = JSON.stringify(options ?? {})
|
||||
tab.value = options?.tab || 'stats'
|
||||
})
|
||||
|
||||
const currentPage = computed(() => {
|
||||
switch (tab.value) {
|
||||
case 'goods': return 'points-goods'
|
||||
case 'order': return 'points-order'
|
||||
case 'record': return 'points-record'
|
||||
case 'config': return 'points-config'
|
||||
case 'lottery-list': return 'lottery-list'
|
||||
case 'lottery-config': return 'lottery-config'
|
||||
case 'groupbuy-goods': return 'groupbuy-goods'
|
||||
case 'groupbuy-list': return 'groupbuy-list'
|
||||
case 'seckill-goods': return 'seckill-goods'
|
||||
case 'seckill-list': return 'seckill-list'
|
||||
case 'seckill-config': return 'seckill-config'
|
||||
case 'member-type': return 'member-type'
|
||||
case 'member-rights': return 'member-rights'
|
||||
case 'member-card': return 'member-card'
|
||||
case 'member-record': return 'member-record'
|
||||
case 'member-config': return 'member-config'
|
||||
case 'live-room': return 'live-room'
|
||||
case 'live-goods': return 'live-goods'
|
||||
case 'live-anchor': return 'live-anchor'
|
||||
case 'recharge-amount': return 'recharge-amount'
|
||||
case 'recharge-config': return 'recharge-config'
|
||||
case 'recharge-record': return 'recharge-record'
|
||||
case 'newcomer': return 'newcomer'
|
||||
default: return 'points-stats'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.Page {
|
||||
padding: 24rpx;
|
||||
}
|
||||
.Header {
|
||||
padding: 24rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #ffffff;
|
||||
}
|
||||
.Title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.SubTitle {
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.Card {
|
||||
margin-top: 24rpx;
|
||||
padding: 24rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #ffffff;
|
||||
}
|
||||
.Label {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.Mono {
|
||||
font-size: 24rpx;
|
||||
font-family: monospace;
|
||||
line-height: 36rpx;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
@@ -1,25 +0,0 @@
|
||||
<template>
|
||||
<AdminLayout :currentPage="currentPage">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">{{ title }}</text>
|
||||
<text class="sub-title">页面已修复 (UTF-8)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
const currentPage = ref<string>('order')
|
||||
const title = ref<string>('order')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/uni.scss';
|
||||
.page { padding: $space-lg; }
|
||||
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
|
||||
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
|
||||
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
|
||||
</style>
|
||||
@@ -90,26 +90,41 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分页 -->
|
||||
<view class="table-pagination">
|
||||
<text class="total-text">共 {{ total }} 条</text>
|
||||
<view class="page-ops">
|
||||
<button class="page-btn" disabled>上一页</button>
|
||||
<text class="current-page">1</text>
|
||||
<button class="page-btn">下一页</button>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 分页区域 -->
|
||||
<CommonPagination
|
||||
v-if="total > 0"
|
||||
:total="total"
|
||||
:loading="false"
|
||||
:currentPage="currentPage"
|
||||
:pageSize="currentSize"
|
||||
:pageSizeOptionLabels="pageSizeOptionLabels"
|
||||
:pageSizeIndex="pageSizeIndex"
|
||||
:visiblePages="visiblePages"
|
||||
:totalPage="totalPage"
|
||||
:jumpPageInput="jumpPageInput"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
@page-change="handlePageChange"
|
||||
@update:jumpPageInput="updateJumpPageInput"
|
||||
@jump-page="handleJumpPage"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script uts>
|
||||
import CommonPagination from '@/components/CommonPagination/CommonPagination.uvue'
|
||||
export default {
|
||||
components: {
|
||||
CommonPagination
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentStatus: -1,
|
||||
searchQuery: '',
|
||||
total: 8,
|
||||
currentPage: 1,
|
||||
currentSize: 15,
|
||||
jumpPageInput: '',
|
||||
statusOptions: [
|
||||
{ label: '全部', value: -1, count: 8 },
|
||||
{ label: '待发货', value: 0, count: 6 },
|
||||
@@ -181,6 +196,23 @@ export default {
|
||||
] as any[]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pageSizeOptions(): number[] { return [10, 15, 20, 30, 50] },
|
||||
pageSizeOptionLabels(): string[] { return (this.pageSizeOptions as number[]).map((n: number) => `${n}条/页`) },
|
||||
pageSizeIndex(): number {
|
||||
const idx = (this.pageSizeOptions as number[]).indexOf(this.currentSize as number)
|
||||
return idx >= 0 ? idx : 0
|
||||
},
|
||||
totalPage(): number { return Math.max(1, Math.ceil((this.total as number) / (this.currentSize as number))) },
|
||||
visiblePages(): number[] {
|
||||
const t = this.totalPage as number
|
||||
const cur = this.currentPage as number
|
||||
if (t <= 7) return Array.from({ length: t }, (_: any, i: number) => i + 1)
|
||||
if (cur <= 4) return [1, 2, 3, 4, 5, -1, t]
|
||||
if (cur >= t - 3) return [1, -1, t - 4, t - 3, t - 2, t - 1, t]
|
||||
return [1, -1, cur - 1, cur, cur + 1, -1, t]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getStatusLabel(val : number) : string {
|
||||
const item = this.statusOptions.find((opt: any) => opt.value === val)
|
||||
@@ -215,16 +247,27 @@ export default {
|
||||
handleReset() {
|
||||
this.searchQuery = ''
|
||||
this.currentStatus = -1
|
||||
}
|
||||
},
|
||||
handlePageChange(p: number) { this.currentPage = p as any },
|
||||
handlePageSizeChange(e: any) {
|
||||
const idx = Number(e.detail.value)
|
||||
this.currentSize = ((this.pageSizeOptions as number[])[idx] ?? (this.pageSizeOptions as number[])[0]) as any
|
||||
this.currentPage = 1 as any
|
||||
},
|
||||
handleJumpPage() {
|
||||
const p = parseInt(this.jumpPageInput as string)
|
||||
if (!isNaN(p) && p >= 1 && p <= (this.totalPage as number)) this.currentPage = p as any
|
||||
},
|
||||
updateJumpPageInput(val: string) { this.jumpPageInput = val as any }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-marketing-integral-order {
|
||||
padding: 16px;
|
||||
background-color: #f5f7f9;
|
||||
min-height: 100vh;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.box-shadow {
|
||||
@@ -399,14 +442,6 @@ export default {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 分页 */
|
||||
.table-pagination {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.total-text { font-size: 14px; color: #515a6e; margin-right: 15px; }
|
||||
/* 分页区域已迁至 CommonPagination 组件 */
|
||||
</style>
|
||||
|
||||
@@ -86,31 +86,30 @@
|
||||
</view>
|
||||
|
||||
<!-- 分页 -->
|
||||
<view class="pagination-footer">
|
||||
<view class="page-total">
|
||||
<text class="total-txt">共 {{ total }} 条</text>
|
||||
</view>
|
||||
<view class="page-select">
|
||||
<text class="page-val">15条/页 ▼</text>
|
||||
</view>
|
||||
<view class="page-btns">
|
||||
<text class="p-btn"><</text>
|
||||
<text class="p-btn active">1</text>
|
||||
<text class="p-btn">></text>
|
||||
</view>
|
||||
<view class="page-jump">
|
||||
<text class="jump-txt">前往</text>
|
||||
<input class="jump-input" placeholder="1" />
|
||||
<text class="jump-txt">页</text>
|
||||
</view>
|
||||
</view>
|
||||
<CommonPagination
|
||||
v-if="total > 0"
|
||||
:total="total"
|
||||
:loading="false"
|
||||
:currentPage="currentPage"
|
||||
:pageSize="pageSize"
|
||||
:pageSizeOptionLabels="pageSizeOptionLabels"
|
||||
:pageSizeIndex="pageSizeIndex"
|
||||
:visiblePages="visiblePages"
|
||||
:totalPage="totalPage"
|
||||
:jumpPageInput="jumpPageInput"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
@page-change="handlePageChange"
|
||||
@update:jumpPageInput="(val : string) => { jumpPageInput = val }"
|
||||
@jump-page="handleJumpPage"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import CommonPagination from '@/components/CommonPagination/CommonPagination.uvue'
|
||||
|
||||
interface ProductItem {
|
||||
id: number
|
||||
@@ -169,13 +168,43 @@ const handleEdit = (item: ProductItem) => { console.log('Editing...', item.id) }
|
||||
const toggleStatus = (index: number) => {
|
||||
productList.value[index].status = !productList.value[index].status
|
||||
}
|
||||
|
||||
// 分页适配状态
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(15)
|
||||
let jumpPageInput = ''
|
||||
const pageSizeOptions = [10, 15, 20, 30, 50]
|
||||
const pageSizeOptionLabels = computed(() => pageSizeOptions.map((n: number) => `${n}条/页`))
|
||||
const pageSizeIndex = computed(() => {
|
||||
const idx = pageSizeOptions.indexOf(pageSize.value)
|
||||
return idx >= 0 ? idx : 0
|
||||
})
|
||||
const totalPage = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
|
||||
const visiblePages = computed(() => {
|
||||
const t = totalPage.value
|
||||
const cur = currentPage.value
|
||||
if (t <= 7) return Array.from({ length: t }, (_: any, i: number) => i + 1)
|
||||
if (cur <= 4) return [1, 2, 3, 4, 5, -1, t]
|
||||
if (cur >= t - 3) return [1, -1, t - 4, t - 3, t - 2, t - 1, t]
|
||||
return [1, -1, cur - 1, cur, cur + 1, -1, t]
|
||||
})
|
||||
const handlePageChange = (p: number) => { currentPage.value = p }
|
||||
const handlePageSizeChange = (e: any) => {
|
||||
const idx = Number(e.detail.value)
|
||||
pageSize.value = pageSizeOptions[idx] ?? pageSizeOptions[0]
|
||||
currentPage.value = 1
|
||||
}
|
||||
const handleJumpPage = () => {
|
||||
const p = parseInt(jumpPageInput)
|
||||
if (!isNaN(p) && p >= 1 && p <= totalPage.value) currentPage.value = p
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.admin-marketing-integral-product {
|
||||
background-color: #f0f2f5;
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.border-shadow {
|
||||
@@ -365,34 +394,7 @@ const toggleStatus = (index: number) => {
|
||||
.op-split { color: #e8eaec; margin: 0 5px; }
|
||||
.text-danger { color: #ed4014; }
|
||||
|
||||
/* 分页 */
|
||||
.pagination-footer {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
.total-txt { font-size: 13px; color: #606266; }
|
||||
.page-val { font-size: 13px; color: #606266; border: 1px solid #dcdfe6; padding: 4px 10px; border-radius: 4px; }
|
||||
.page-btns { display: flex; flex-direction: row; gap: 8px; }
|
||||
.p-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid #e8eaec;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
.p-btn.active { background-color: #2d8cf0; border-color: #2d8cf0; color: #fff; }
|
||||
|
||||
.page-jump { display: flex; flex-direction: row; align-items: center; gap: 8px; }
|
||||
.jump-txt { font-size: 13px; color: #606266; }
|
||||
.jump-input { width: 40px; height: 32px; border: 1px solid #dcdfe6; text-align: center; border-radius: 4px; font-size: 13px; }
|
||||
/* 分页区域已迁至 CommonPagination 组件 */
|
||||
|
||||
</style>
|
||||
|
||||
@@ -57,21 +57,33 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分页 -->
|
||||
<view class="table-pagination">
|
||||
<text class="total-text">共 {{ total }} 条</text>
|
||||
<view class="page-ops">
|
||||
<button class="page-btn" disabled>上一页</button>
|
||||
<text class="current-page">1</text>
|
||||
<button class="page-btn">下一页</button>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 分页区域 -->
|
||||
<CommonPagination
|
||||
v-if="total > 0"
|
||||
:total="total"
|
||||
:loading="false"
|
||||
:currentPage="currentPage"
|
||||
:pageSize="currentSize"
|
||||
:pageSizeOptionLabels="pageSizeOptionLabels"
|
||||
:pageSizeIndex="pageSizeIndex"
|
||||
:visiblePages="visiblePages"
|
||||
:totalPage="totalPage"
|
||||
:jumpPageInput="jumpPageInput"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
@page-change="handlePageChange"
|
||||
@update:jumpPageInput="updateJumpPageInput"
|
||||
@jump-page="handleJumpPage"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script uts>
|
||||
import CommonPagination from '@/components/CommonPagination/CommonPagination.uvue'
|
||||
export default {
|
||||
components: {
|
||||
CommonPagination
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchQuery: '',
|
||||
@@ -83,6 +95,9 @@ export default {
|
||||
{ label: '后台充值', value: 'system' }
|
||||
] as any[],
|
||||
total: 5,
|
||||
currentPage: 1,
|
||||
currentSize: 15,
|
||||
jumpPageInput: '',
|
||||
tableData: [
|
||||
{
|
||||
id: 1256,
|
||||
@@ -121,6 +136,21 @@ export default {
|
||||
typeLabel(): string {
|
||||
const found = this.typeOptions.find((o: any): boolean => o.value == this.currentType)
|
||||
return found != null ? (found['label'] as string) : '全部'
|
||||
},
|
||||
pageSizeOptions(): number[] { return [10, 15, 20, 30, 50] },
|
||||
pageSizeOptionLabels(): string[] { return (this.pageSizeOptions as number[]).map((n: number) => `${n}条/页`) },
|
||||
pageSizeIndex(): number {
|
||||
const idx = (this.pageSizeOptions as number[]).indexOf(this.currentSize as number)
|
||||
return idx >= 0 ? idx : 0
|
||||
},
|
||||
totalPage(): number { return Math.max(1, Math.ceil((this.total as number) / (this.currentSize as number))) },
|
||||
visiblePages(): number[] {
|
||||
const t = this.totalPage as number
|
||||
const cur = this.currentPage as number
|
||||
if (t <= 7) return Array.from({ length: t }, (_: any, i: number) => i + 1)
|
||||
if (cur <= 4) return [1, 2, 3, 4, 5, -1, t]
|
||||
if (cur >= t - 3) return [1, -1, t - 4, t - 3, t - 2, t - 1, t]
|
||||
return [1, -1, cur - 1, cur, cur + 1, -1, t]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -136,16 +166,27 @@ export default {
|
||||
},
|
||||
handleMark(item: any) {
|
||||
uni.showToast({ title: '修改备注: ' + item.id, icon: 'none' })
|
||||
}
|
||||
},
|
||||
handlePageChange(p: number) { this.currentPage = p as any },
|
||||
handlePageSizeChange(e: any) {
|
||||
const idx = Number(e.detail.value)
|
||||
this.currentSize = ((this.pageSizeOptions as number[])[idx] ?? (this.pageSizeOptions as number[])[0]) as any
|
||||
this.currentPage = 1 as any
|
||||
},
|
||||
handleJumpPage() {
|
||||
const p = parseInt(this.jumpPageInput as string)
|
||||
if (!isNaN(p) && p >= 1 && p <= (this.totalPage as number)) this.currentPage = p as any
|
||||
},
|
||||
updateJumpPageInput(val: string) { this.jumpPageInput = val as any }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-marketing-integral-record {
|
||||
padding: 16px;
|
||||
background-color: #f5f7f9;
|
||||
min-height: 100vh;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.box-shadow {
|
||||
@@ -267,36 +308,7 @@ export default {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 分页 */
|
||||
.table-pagination {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.total-text { font-size: 14px; color: #515a6e; margin-right: 15px; }
|
||||
|
||||
.page-ops {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.page-btn {
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid #d9d9d9;
|
||||
background: #fff;
|
||||
margin: 0;
|
||||
}
|
||||
.current-page {
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
color: #2d8cf0;
|
||||
}
|
||||
/* 分页区域已迁至 CommonPagination 组件 */
|
||||
</style>
|
||||
|
||||
|
||||
@@ -251,9 +251,9 @@ const toggleConsumeStyle = () => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.admin-marketing-integral-statistic {
|
||||
background-color: #f0f2f5;
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.border-shadow {
|
||||
@@ -1,27 +0,0 @@
|
||||
<template>
|
||||
<AdminLayout :currentPage="currentPage">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">{{ title }}</text>
|
||||
<text class="sub-title">积分统计数据,包含积分发放和消费情况?</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
const currentPage = ref<string>('points-stats')
|
||||
const title = ref<string>('积分统计')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/uni.scss';
|
||||
.page { padding: $space-lg; }
|
||||
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
|
||||
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
|
||||
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
|
||||
</style>
|
||||
|
||||
|
||||
@@ -239,9 +239,9 @@ function resetForm() {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.marketing-recharge-quota {
|
||||
padding: 16px;
|
||||
background: #f0f2f5;
|
||||
min-height: 100vh;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.content-layout {
|
||||
@@ -135,9 +135,9 @@ async function handleSave() {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.marketing-recharge-config {
|
||||
padding: 16px;
|
||||
background: #f0f2f5;
|
||||
min-height: 100vh;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.border-shadow {
|
||||
@@ -29,7 +29,7 @@
|
||||
</view>
|
||||
|
||||
<view class="table-body">
|
||||
<view v-for="item in configList" :key="item.id" class="table-row">
|
||||
<view v-for="item in pagedList" :key="item.id" class="table-row">
|
||||
<view class="td cell-id">
|
||||
<text class="td-txt">{{ item.id }}</text>
|
||||
</view>
|
||||
@@ -47,10 +47,7 @@
|
||||
<text class="td-txt">{{ item.sort }}</text>
|
||||
</view>
|
||||
<view class="td cell-status">
|
||||
<view class="switch-mock" :class="{ active: item.status }" @click="toggleStatus(item)">
|
||||
<view class="switch-dot"></view>
|
||||
<text class="switch-txt">{{ item.status ? '开启' : '关闭' }}</text>
|
||||
</view>
|
||||
<StatusSwitch v-model="item.status" />
|
||||
</view>
|
||||
<view class="td cell-op">
|
||||
<view class="op-links">
|
||||
@@ -63,17 +60,22 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="pagination-footer">
|
||||
<view class="page-total">
|
||||
<text class="total-txt">共 {{ configList.length }} 条</text>
|
||||
</view>
|
||||
<view class="page-select">
|
||||
<view class="select-mock mini">
|
||||
<text class="select-val">20条/页</text>
|
||||
<text class="arrow">▼</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<CommonPagination
|
||||
v-if="total > 0"
|
||||
:total="total"
|
||||
:loading="false"
|
||||
:currentPage="currentPage"
|
||||
:pageSize="pageSize"
|
||||
:pageSizeOptionLabels="pageSizeOptionLabels"
|
||||
:pageSizeIndex="pageSizeIndex"
|
||||
:visiblePages="visiblePages"
|
||||
:totalPage="totalPage"
|
||||
:jumpPageInput="jumpPageInput"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
@page-change="handlePageChange"
|
||||
@update:jumpPageInput="(val: string) => { jumpPageInput.value = val }"
|
||||
@jump-page="handleJumpPage"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- Drawer Overlay -->
|
||||
@@ -107,16 +109,7 @@
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">状态:</text>
|
||||
<view class="radio-group">
|
||||
<view class="radio-item" @click="formData.status = true">
|
||||
<view class="radio-circle" :class="{ active: formData.status }"></view>
|
||||
<text class="radio-txt">显示</text>
|
||||
</view>
|
||||
<view class="radio-item" @click="formData.status = false">
|
||||
<view class="radio-circle" :class="{ active: !formData.status }"></view>
|
||||
<text class="radio-txt">隐藏</text>
|
||||
</view>
|
||||
</view>
|
||||
<StatusSwitch v-model="formData.status" activeText="显示" inactiveText="隐藏" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="drawer-footer">
|
||||
@@ -128,7 +121,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import StatusSwitch from '@/components/StatusSwitch.uvue'
|
||||
import CommonPagination from '@/components/CommonPagination/CommonPagination.uvue'
|
||||
|
||||
const showDrawer = ref(false)
|
||||
const isAnimating = ref(false)
|
||||
@@ -142,21 +137,63 @@ const formData = ref({
|
||||
status: true
|
||||
})
|
||||
|
||||
// ========== MOCK DATA START ==========
|
||||
// TODO: 接真实接口时替换此处 configList 为 fetchConfigList() 调用
|
||||
const configList = ref([
|
||||
{
|
||||
id: 2268,
|
||||
start_hour: 6,
|
||||
duration: 18,
|
||||
image: '',
|
||||
sort: 1,
|
||||
status: true
|
||||
}
|
||||
{ id: 2268, start_hour: 0, duration: 2, image: '', sort: 1, status: true },
|
||||
{ id: 2269, start_hour: 2, duration: 2, image: '', sort: 2, status: true },
|
||||
{ id: 2270, start_hour: 4, duration: 2, image: '', sort: 3, status: true },
|
||||
{ id: 2271, start_hour: 6, duration: 3, image: '', sort: 4, status: true },
|
||||
{ id: 2272, start_hour: 9, duration: 3, image: '', sort: 5, status: true },
|
||||
{ id: 2273, start_hour: 12, duration: 3, image: '', sort: 6, status: false },
|
||||
{ id: 2274, start_hour: 15, duration: 3, image: '', sort: 7, status: true },
|
||||
{ id: 2275, start_hour: 18, duration: 2, image: '', sort: 8, status: true },
|
||||
{ id: 2276, start_hour: 20, duration: 2, image: '', sort: 9, status: true },
|
||||
{ id: 2277, start_hour: 22, duration: 2, image: '', sort: 10, status: false },
|
||||
{ id: 2278, start_hour: 8, duration: 4, image: '', sort: 11, status: true },
|
||||
{ id: 2279, start_hour: 10, duration: 2, image: '', sort: 12, status: true },
|
||||
{ id: 2280, start_hour: 13, duration: 2, image: '', sort: 13, status: true },
|
||||
{ id: 2281, start_hour: 16, duration: 3, image: '', sort: 14, status: false },
|
||||
{ id: 2282, start_hour: 19, duration: 1, image: '', sort: 15, status: true },
|
||||
{ id: 2283, start_hour: 21, duration: 2, image: '', sort: 16, status: true },
|
||||
{ id: 2284, start_hour: 7, duration: 3, image: '', sort: 17, status: true },
|
||||
{ id: 2285, start_hour: 11, duration: 2, image: '', sort: 18, status: false },
|
||||
{ id: 2286, start_hour: 14, duration: 4, image: '', sort: 19, status: true },
|
||||
{ id: 2287, start_hour: 17, duration: 2, image: '', sort: 20, status: true },
|
||||
])
|
||||
// ========== MOCK DATA END ==========
|
||||
|
||||
const toggleStatus = (item: any) => {
|
||||
item.status = !item.status
|
||||
uni.showToast({ title: '修改成功', icon: 'success' })
|
||||
// ========== PAGINATION STATE ==========
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(15)
|
||||
const jumpPageInput = ref('')
|
||||
const pageSizeOptions = [10, 15, 20, 30, 50]
|
||||
const pageSizeOptionLabels = computed(() => pageSizeOptions.map((n: number) => `${n}条/页`))
|
||||
const pageSizeIndex = computed(() => { const idx = pageSizeOptions.indexOf(pageSize.value); return idx >= 0 ? idx : 0 })
|
||||
const total = computed(() => configList.value.length)
|
||||
const totalPage = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
|
||||
const pagedList = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
return configList.value.slice(start, start + pageSize.value)
|
||||
})
|
||||
const visiblePages = computed((): number[] => {
|
||||
const t = totalPage.value; const cur = currentPage.value
|
||||
if (t <= 7) return Array.from({ length: t }, (_: any, i: number) => i + 1)
|
||||
if (cur <= 4) return [1, 2, 3, 4, 5, -1, t]
|
||||
if (cur >= t - 3) return [1, -1, t - 4, t - 3, t - 2, t - 1, t]
|
||||
return [1, -1, cur - 1, cur, cur + 1, -1, t]
|
||||
})
|
||||
const handlePageChange = (p: number) => { currentPage.value = p }
|
||||
const handlePageSizeChange = (e: any) => {
|
||||
const idx = Number(e.detail.value)
|
||||
pageSize.value = pageSizeOptions[idx] ?? pageSizeOptions[0]
|
||||
currentPage.value = 1
|
||||
}
|
||||
const handleJumpPage = () => {
|
||||
const p = parseInt(jumpPageInput.value)
|
||||
if (!isNaN(p) && p >= 1 && p <= totalPage.value) currentPage.value = p
|
||||
}
|
||||
// ========== END PAGINATION STATE ==========
|
||||
|
||||
const handleEdit = (item: any) => {
|
||||
formData.value = { ...item }
|
||||
@@ -201,9 +238,9 @@ const closeDrawer = () => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.marketing-seckill-config {
|
||||
min-height: 100vh;
|
||||
background: #f0f2f5;
|
||||
padding: 16px;
|
||||
min-height: auto;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -320,32 +357,6 @@ const closeDrawer = () => {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.switch-mock {
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
background-color: #bfbfbf;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0 4px;
|
||||
position: relative;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
.switch-mock.active { background-color: #1890ff; }
|
||||
.switch-dot {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: #fff;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
transition: left 0.3s;
|
||||
}
|
||||
.switch-mock.active .switch-dot { left: 30px; }
|
||||
.switch-txt { font-size: 11px; color: #fff; margin-left: 20px; }
|
||||
.switch-mock.active .switch-txt { margin-left: 4px; }
|
||||
|
||||
.op-links {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -355,16 +366,7 @@ const closeDrawer = () => {
|
||||
.op-link { color: #1890ff; font-size: 13px; cursor: pointer; }
|
||||
.op-split { color: #e8eaec; margin: 0 8px; }
|
||||
|
||||
/* 分页 */
|
||||
.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: #606266; }
|
||||
/* 分页区域已迁至 CommonPagination 组件 */
|
||||
|
||||
/* Drawer Styles */
|
||||
.drawer-mask {
|
||||
@@ -462,40 +464,6 @@ const closeDrawer = () => {
|
||||
}
|
||||
.upload-ic { font-size: 24px; color: #bfbfbf; }
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 24px;
|
||||
}
|
||||
.radio-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.radio-circle {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
}
|
||||
.radio-circle.active {
|
||||
border-color: #1890ff;
|
||||
}
|
||||
.radio-circle.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: #1890ff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.radio-txt { font-size: 14px; color: #262626; }
|
||||
|
||||
.drawer-footer {
|
||||
padding: 10px 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
@@ -4,12 +4,19 @@
|
||||
<view class="filter-row">
|
||||
<view class="filter-item">
|
||||
<text class="label">活动搜索:</text>
|
||||
<input class="input-mock" placeholder="请输入活动名称, ID" v-model="searchQuery" @confirm="handleSearch" />
|
||||
<input class="input-mock" placeholder="请输入活动名称, ID" />
|
||||
</view>
|
||||
<view class="filter-item">
|
||||
<text class="label">活动状态:</text>
|
||||
<view class="select-mock">
|
||||
<text class="select-val">全部</text>
|
||||
<text class="select-val">请选择</text>
|
||||
<text class="arrow">▼</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="filter-item">
|
||||
<text class="label">活动时段:</text>
|
||||
<view class="select-mock">
|
||||
<text class="select-val">请选择</text>
|
||||
<text class="arrow">▼</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -19,10 +26,10 @@
|
||||
<text class="label">活动时间:</text>
|
||||
<view class="date-picker-mock">
|
||||
<text class="calendar-ic">📅</text>
|
||||
<text class="date-placeholder">暂时使用默认范围</text>
|
||||
<text class="date-placeholder">开始日期 - 结束日期</text>
|
||||
</view>
|
||||
</view>
|
||||
<button class="btn-query" @click="handleSearch">查询</button>
|
||||
<button class="btn-query">查询</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -32,26 +39,19 @@
|
||||
|
||||
<view class="table-card border-shadow">
|
||||
<view class="table-container">
|
||||
<!-- Loading 遮罩 -->
|
||||
<view v-if="isLoading" class="loading-mask">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
<view class="table-head">
|
||||
<view class="th cell-id">ID</view>
|
||||
<view class="th cell-title">活动标题</view>
|
||||
<view class="th cell-limit">单次限购</view>
|
||||
<view class="th cell-total">总限购</view>
|
||||
<view class="th cell-count">商品数</view>
|
||||
<view class="th cell-period">时段</view>
|
||||
<view class="th cell-time">活动日期</view>
|
||||
<view class="th cell-total">总购买数量限制</view>
|
||||
<view class="th cell-count">商品数量</view>
|
||||
<view class="th cell-period">活动时段</view>
|
||||
<view class="th cell-time">活动时间</view>
|
||||
<view class="th cell-status">状态</view>
|
||||
<view class="th cell-op">操作</view>
|
||||
</view>
|
||||
|
||||
<view class="table-body">
|
||||
<view v-if="seckillList.length === 0 && !isLoading" class="empty-row">
|
||||
<text>暂无数据</text>
|
||||
</view>
|
||||
<view v-for="item in seckillList" :key="item.id" class="table-row">
|
||||
<view class="td cell-id">
|
||||
<text class="td-txt">{{ item.id }}</text>
|
||||
@@ -74,14 +74,11 @@
|
||||
</view>
|
||||
</view>
|
||||
<view class="td cell-time">
|
||||
<text class="td-txt-small">{{ item.start_date.substring(0,10) }}</text>
|
||||
<text class="td-txt-small">{{ item.end_date.substring(0,10) }}</text>
|
||||
<text class="td-txt-small">开始: {{ item.start_date }}</text>
|
||||
<text class="td-txt-small">结束: {{ item.end_date }}</text>
|
||||
</view>
|
||||
<view class="td cell-status">
|
||||
<view class="switch-mock" :class="{ active: item.status }" @click="toggleStatus(item)">
|
||||
<view class="switch-dot"></view>
|
||||
<text class="switch-txt">{{ item.status ? '开启' : '关闭' }}</text>
|
||||
</view>
|
||||
<StatusSwitch v-model="item.status" />
|
||||
</view>
|
||||
<view class="td cell-op">
|
||||
<view class="op-links">
|
||||
@@ -94,112 +91,103 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="pagination-footer">
|
||||
<view class="page-total">
|
||||
<text class="total-txt">共 {{ total }} 条</text>
|
||||
</view>
|
||||
<view class="page-btns">
|
||||
<text class="p-btn" :class="{ disabled: page <= 1 }" @click="onPrevPage">‹</text>
|
||||
<text class="p-btn active">{{ page }}</text>
|
||||
<text class="p-btn" :class="{ disabled: seckillList.length < pageSize }" @click="onNextPage">›</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 分页 -->
|
||||
<CommonPagination
|
||||
v-if="seckillList.length > 0"
|
||||
:total="seckillList.length"
|
||||
:loading="false"
|
||||
:currentPage="currentPage"
|
||||
:pageSize="pageSize"
|
||||
:pageSizeOptionLabels="pageSizeOptionLabels"
|
||||
:pageSizeIndex="pageSizeIndex"
|
||||
:visiblePages="visiblePages"
|
||||
:totalPage="totalPage"
|
||||
:jumpPageInput="jumpPageInput"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
@page-change="handlePageChange"
|
||||
@update:jumpPageInput="(val : string) => { jumpPageInput = val }"
|
||||
@jump-page="handleJumpPage"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { fetchSeckillActivities, deleteSeckillActivity, saveSeckillActivity, SeckillActivity } from '@/services/admin/marketingService.uts'
|
||||
import { ref, computed } from 'vue'
|
||||
import StatusSwitch from '@/components/StatusSwitch.uvue'
|
||||
import CommonPagination from '@/components/CommonPagination/CommonPagination.uvue'
|
||||
|
||||
const seckillList = ref<SeckillActivity[]>([])
|
||||
const isLoading = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const page = ref(1)
|
||||
const pageSize = 15
|
||||
const total = ref(0)
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
async function loadData() {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const res = await fetchSeckillActivities({
|
||||
search: searchQuery.value,
|
||||
page: page.value,
|
||||
pageSize: pageSize
|
||||
})
|
||||
seckillList.value = res.items
|
||||
total.value = res.total
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
const seckillList = ref([
|
||||
{
|
||||
id: 91,
|
||||
title: '秒杀活动',
|
||||
single_limit: 1,
|
||||
total_limit: 10,
|
||||
product_count: 5,
|
||||
time_range: '06:00-24:00',
|
||||
start_date: '2025-07-01 00:00:00',
|
||||
end_date: '2028-08-22 23:59:59',
|
||||
status: true
|
||||
}
|
||||
])
|
||||
|
||||
const handleAdd = () => {
|
||||
uni.showToast({ title: '添加活动功能开发中', icon: 'none' })
|
||||
}
|
||||
|
||||
async function toggleStatus(item : SeckillActivity) {
|
||||
if (item.id == null) return
|
||||
const nextStatus = !item.status
|
||||
const success = await saveSeckillActivity({ ...item, status: nextStatus } as SeckillActivity)
|
||||
if (success) {
|
||||
item.status = nextStatus
|
||||
uni.showToast({ title: '修改成功', icon: 'success' })
|
||||
}
|
||||
const handleEdit = (item: any) => {
|
||||
uni.showToast({ title: '编辑活动功能开发中', icon: 'none' })
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
page.value = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
uni.showToast({ title: '添加功能开发中', icon: 'none' })
|
||||
}
|
||||
|
||||
function handleEdit(item : SeckillActivity) {
|
||||
uni.showToast({ title: '编辑功能开发中', icon: 'none' })
|
||||
}
|
||||
|
||||
async function handleDelete(item : SeckillActivity) {
|
||||
if (item.id == null) return
|
||||
const handleDelete = (item: any) => {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该活动吗?',
|
||||
success: async (res) => {
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
const success = await deleteSeckillActivity(item.id!)
|
||||
if (success) {
|
||||
uni.showToast({ title: '删除成功' })
|
||||
loadData()
|
||||
}
|
||||
seckillList.value = seckillList.value.filter(i => i.id !== item.id)
|
||||
uni.showToast({ title: '删除成功' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function onPrevPage() {
|
||||
if (page.value > 1) {
|
||||
page.value--
|
||||
loadData()
|
||||
}
|
||||
// 分页适配状态
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(15)
|
||||
let jumpPageInput = ''
|
||||
const pageSizeOptions = [10, 15, 20, 30, 50]
|
||||
const pageSizeOptionLabels = computed(() => pageSizeOptions.map((n: number) => `${n}条/页`))
|
||||
const pageSizeIndex = computed(() => {
|
||||
const idx = pageSizeOptions.indexOf(pageSize.value)
|
||||
return idx >= 0 ? idx : 0
|
||||
})
|
||||
const totalPage = computed(() => Math.max(1, Math.ceil(seckillList.value.length / pageSize.value)))
|
||||
const visiblePages = computed(() => {
|
||||
const t = totalPage.value
|
||||
const cur = currentPage.value
|
||||
if (t <= 7) return Array.from({ length: t }, (_: any, i: number) => i + 1)
|
||||
if (cur <= 4) return [1, 2, 3, 4, 5, -1, t]
|
||||
if (cur >= t - 3) return [1, -1, t - 4, t - 3, t - 2, t - 1, t]
|
||||
return [1, -1, cur - 1, cur, cur + 1, -1, t]
|
||||
})
|
||||
const handlePageChange = (p: number) => { currentPage.value = p }
|
||||
const handlePageSizeChange = (e: any) => {
|
||||
const idx = Number(e.detail.value)
|
||||
pageSize.value = pageSizeOptions[idx] ?? pageSizeOptions[0]
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
function onNextPage() {
|
||||
if (seckillList.value.length >= pageSize) {
|
||||
page.value++
|
||||
loadData()
|
||||
}
|
||||
const handleJumpPage = () => {
|
||||
const p = parseInt(jumpPageInput)
|
||||
if (!isNaN(p) && p >= 1 && p <= totalPage.value) currentPage.value = p
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.marketing-seckill-list {
|
||||
min-height: 100vh;
|
||||
background: #f0f2f5;
|
||||
padding: 16px;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.border-shadow {
|
||||
@@ -342,32 +330,6 @@ function onNextPage() {
|
||||
}
|
||||
.period-txt { color: #1890ff; font-size: 12px; }
|
||||
|
||||
.switch-mock {
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
background-color: #bfbfbf;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0 4px;
|
||||
position: relative;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
.switch-mock.active { background-color: #1890ff; }
|
||||
.switch-dot {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: #fff;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
transition: left 0.3s;
|
||||
}
|
||||
.switch-mock.active .switch-dot { left: 30px; }
|
||||
.switch-txt { font-size: 11px; color: #fff; margin-left: 20px; }
|
||||
.switch-mock.active .switch-txt { margin-left: 4px; }
|
||||
|
||||
.op-links {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -377,34 +339,7 @@ function onNextPage() {
|
||||
.op-link { color: #1890ff; font-size: 13px; cursor: pointer; }
|
||||
.op-split { color: #e8eaec; margin: 0 8px; }
|
||||
|
||||
/* 分页 */
|
||||
.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: #606266; }
|
||||
.page-btns { display: flex; flex-direction: row; gap: 8px; }
|
||||
.p-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
.p-btn.active { background-color: #1890ff; border-color: #1890ff; color: #fff; }
|
||||
.p-btn.disabled { color: #c0c4cc; cursor: not-allowed; }
|
||||
|
||||
.page-jump { display: flex; flex-direction: row; align-items: center; gap: 8px; }
|
||||
.jump-txt { font-size: 13px; color: #606266; }
|
||||
.jump-input { width: 40px; height: 28px; border: 1px solid #dcdfe6; border-radius: 4px; text-align: center; }
|
||||
/* 分页区域已迁至 CommonPagination 组件 */
|
||||
|
||||
</style>
|
||||
|
||||
@@ -105,33 +105,30 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="pagination-footer">
|
||||
<view class="page-total">
|
||||
<text class="total-txt">共 {{ productList.length }} 条</text>
|
||||
</view>
|
||||
<view class="page-select">
|
||||
<view class="select-mock mini">
|
||||
<text class="select-val">15条/页</text>
|
||||
<text class="arrow">▼</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="page-btns">
|
||||
<text class="p-btn disabled">‹</text>
|
||||
<text class="p-btn active">1</text>
|
||||
<text class="p-btn disabled">›</text>
|
||||
</view>
|
||||
<view class="page-jump">
|
||||
<text class="jump-txt">前往</text>
|
||||
<input class="jump-input" placeholder="1" />
|
||||
<text class="jump-txt">页</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 分页 -->
|
||||
<CommonPagination
|
||||
v-if="productList.length > 0"
|
||||
:total="productList.length"
|
||||
:loading="false"
|
||||
:currentPage="currentPage"
|
||||
:pageSize="pageSize"
|
||||
:pageSizeOptionLabels="pageSizeOptionLabels"
|
||||
:pageSizeIndex="pageSizeIndex"
|
||||
:visiblePages="visiblePages"
|
||||
:totalPage="totalPage"
|
||||
:jumpPageInput="jumpPageInput"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
@page-change="handlePageChange"
|
||||
@update:jumpPageInput="(val : string) => { jumpPageInput = val }"
|
||||
@jump-page="handleJumpPage"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import CommonPagination from '@/components/CommonPagination/CommonPagination.uvue'
|
||||
|
||||
const productList = ref([
|
||||
{
|
||||
@@ -199,13 +196,43 @@ const handleDelete = (item: any) => {
|
||||
const handleStats = (item: any) => {
|
||||
uni.showToast({ title: '统计查看中', icon: 'none' })
|
||||
}
|
||||
|
||||
// 分页适配状态
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(15)
|
||||
let jumpPageInput = ''
|
||||
const pageSizeOptions = [10, 15, 20, 30, 50]
|
||||
const pageSizeOptionLabels = computed(() => pageSizeOptions.map((n: number) => `${n}条/页`))
|
||||
const pageSizeIndex = computed(() => {
|
||||
const idx = pageSizeOptions.indexOf(pageSize.value)
|
||||
return idx >= 0 ? idx : 0
|
||||
})
|
||||
const totalPage = computed(() => Math.max(1, Math.ceil(productList.value.length / pageSize.value)))
|
||||
const visiblePages = computed(() => {
|
||||
const t = totalPage.value
|
||||
const cur = currentPage.value
|
||||
if (t <= 7) return Array.from({ length: t }, (_: any, i: number) => i + 1)
|
||||
if (cur <= 4) return [1, 2, 3, 4, 5, -1, t]
|
||||
if (cur >= t - 3) return [1, -1, t - 4, t - 3, t - 2, t - 1, t]
|
||||
return [1, -1, cur - 1, cur, cur + 1, -1, t]
|
||||
})
|
||||
const handlePageChange = (p: number) => { currentPage.value = p }
|
||||
const handlePageSizeChange = (e: any) => {
|
||||
const idx = Number(e.detail.value)
|
||||
pageSize.value = pageSizeOptions[idx] ?? pageSizeOptions[0]
|
||||
currentPage.value = 1
|
||||
}
|
||||
const handleJumpPage = () => {
|
||||
const p = parseInt(jumpPageInput)
|
||||
if (!isNaN(p) && p >= 1 && p <= totalPage.value) currentPage.value = p
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.marketing-seckill-product {
|
||||
min-height: 100vh;
|
||||
background: #f0f2f5;
|
||||
padding: 16px;
|
||||
min-height: auto;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.border-shadow {
|
||||
@@ -396,34 +423,7 @@ const handleStats = (item: any) => {
|
||||
.op-link { color: #1890ff; font-size: 13px; cursor: pointer; }
|
||||
.op-split { color: #e8eaec; margin: 0 8px; }
|
||||
|
||||
/* 分页 */
|
||||
.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: #606266; }
|
||||
.page-btns { display: flex; flex-direction: row; gap: 8px; }
|
||||
.p-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
.p-btn.active { background-color: #1890ff; border-color: #1890ff; color: #fff; }
|
||||
.p-btn.disabled { color: #c0c4cc; cursor: not-allowed; }
|
||||
|
||||
.page-jump { display: flex; flex-direction: row; align-items: center; gap: 8px; }
|
||||
.jump-txt { font-size: 13px; color: #606266; }
|
||||
.jump-input { width: 40px; height: 28px; border: 1px solid #dcdfe6; border-radius: 4px; text-align: center; }
|
||||
/* 分页区域已迁至 CommonPagination 组件 */
|
||||
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user