Merge remote-tracking branch 'origin/huangzhenbao-admin'

This commit is contained in:
not-like-juvenile
2026-03-18 17:14:05 +08:00
676 changed files with 25158 additions and 46646 deletions

View File

@@ -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>

View 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>

View 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>

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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 {

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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>

View 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>

View 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>

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>