feat(admin): full integration of order, product, and finance modules with real RPC data streams
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
<view class="search-row">
|
||||
<view class="search-item">
|
||||
<text class="label">商品搜索:</text>
|
||||
<input class="mock-input" placeholder="请输入商品名称/关键字/ID" />
|
||||
<input class="mock-input" placeholder="请输入商品名称/关键字/ID" v-model="searchName" @confirm="handleSearch" />
|
||||
</view>
|
||||
<view class="search-item">
|
||||
<text class="label">商品类型:</text>
|
||||
@@ -16,14 +16,19 @@
|
||||
</view>
|
||||
<view class="search-item">
|
||||
<text class="label">商品分类:</text>
|
||||
<view class="mock-select">
|
||||
<text>请选择</text>
|
||||
<text class="arrow">▼</text>
|
||||
</view>
|
||||
<picker :value="categoryIndex" :range="categoryOptions" range-key="label" @change="e => {
|
||||
categoryIndex = e.detail.value;
|
||||
selectedCategoryId = categoryOptions[categoryIndex].value;
|
||||
}">
|
||||
<view class="mock-select">
|
||||
<text>{{ categoryOptions[categoryIndex].label }}</text>
|
||||
<text class="arrow">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="search-btns">
|
||||
<button class="btn-primary">查询</button>
|
||||
<button class="btn-reset">重置</button>
|
||||
<button class="btn-primary" @click="handleSearch">查询</button>
|
||||
<button class="btn-reset" @click="handleReset">重置</button>
|
||||
<view class="expand-control">
|
||||
<text class="expand-txt">展开</text>
|
||||
<text class="expand-arrow">▼</text>
|
||||
@@ -49,7 +54,7 @@
|
||||
:key="index"
|
||||
class="tab-item"
|
||||
:class="{ active: activeStatus === tab.key }"
|
||||
@click="activeStatus = tab.key"
|
||||
@click="changeStatus(tab.key)"
|
||||
>
|
||||
<text>{{ tab.label }}({{ tab.count }})</text>
|
||||
</view>
|
||||
@@ -115,7 +120,7 @@
|
||||
<view class="td col-stock"><text>{{ item.stock }}</text></view>
|
||||
<view class="td col-sort"><text>{{ item.sort }}</text></view>
|
||||
<view class="td col-status">
|
||||
<view class="mock-switch" :class="{ on: item.status === 1 }">
|
||||
<view class="mock-switch" :class="{ on: item.status === 1 }" @click="toggleStatus(item)">
|
||||
<text class="switch-txt">{{ item.status === 1 ? '上架' : '下架' }}</text>
|
||||
<view class="switch-dot"></view>
|
||||
</view>
|
||||
@@ -137,7 +142,7 @@
|
||||
<text class="menu-item" @click.stop="goReviews(item.id)">查看评论</text>
|
||||
<text class="menu-item" @click.stop="goMemberPrice(item.id)">会员价管理</text>
|
||||
<text class="menu-item" @click.stop="uni.showToast({title:'佣金管理开发中', icon:'none'})">佣金管理</text>
|
||||
<text class="menu-item danger-item" @click.stop="moveToRecycle(item.id)">{{ activeStatus === 'recycle' ? '恢复商品' : '移到回收站' }}</text>
|
||||
<text class="menu-item danger-item" @click.stop="moveToRecycle(item)">{{ activeStatus === 4 ? '恢复商品' : '移到回收站' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -149,10 +154,9 @@
|
||||
<view class="pagination-row">
|
||||
<text class="total">共 {{ total }} 条</text>
|
||||
<view class="page-ctrl">
|
||||
<text class="page-btn disabled">{"<"}</text>
|
||||
<text class="page-num active">1</text>
|
||||
<text class="page-num">2</text>
|
||||
<text class="page-btn">{">"}</text>
|
||||
<text class="page-btn" :class="{ disabled: page <= 1 }" @click="page > 1 && (page--, loadData())">{"<"}</text>
|
||||
<text class="page-num active">{{ page }}</text>
|
||||
<text class="page-btn" :class="{ disabled: productList.length < pageSize }" @click="productList.length == pageSize && (page++, loadData())">{">"}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -162,15 +166,21 @@
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { openRoute } from '@/layouts/admin/store/adminNavStore.uts'
|
||||
import { fetchAdminProductPage, updateAdminProductStatus, type AdminProduct } from '@/services/admin/productService.uts'
|
||||
import { fetchAdminProductPage, updateAdminProductStatus, fetchAdminProductCountStats, type AdminProduct } from '@/services/admin/productService.uts'
|
||||
import { fetchAdminCategoryList, type AdminCategory } from '@/services/admin/productCategoryService.uts'
|
||||
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const activeStatus = ref<number | null>(1) // 1:出售中
|
||||
const activeStatus = ref<number>(1) // 1:出售中
|
||||
const activeDropdownId = ref<string | null>(null)
|
||||
const productList = ref<Array<AdminProduct>>([])
|
||||
const searchName = ref('')
|
||||
const selectedCategoryId = ref<string | null>(null)
|
||||
const categoryOptions = ref<Array<{label: string, value: string | null}>>([
|
||||
{ label: '全部', value: null }
|
||||
])
|
||||
const categoryIndex = ref(0)
|
||||
|
||||
const statusTabs = ref([
|
||||
{ key: 1, label: '出售中的商品', count: 0 },
|
||||
@@ -180,13 +190,40 @@ const statusTabs = ref([
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
loadCounts()
|
||||
loadCategories()
|
||||
loadData()
|
||||
})
|
||||
|
||||
async function loadCategories() {
|
||||
try {
|
||||
const categories = await fetchAdminCategoryList({ isActive: true })
|
||||
categories.forEach(item => {
|
||||
categoryOptions.value.push({
|
||||
label: item.name,
|
||||
value: item.id
|
||||
})
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('加载分类失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCounts() {
|
||||
const stats = await fetchAdminProductCountStats()
|
||||
if (stats != null) {
|
||||
statusTabs.value[0].count = parseInt(String(stats['selling'] ?? '0'))
|
||||
statusTabs.value[1].count = parseInt(String(stats['warehouse'] ?? '0'))
|
||||
statusTabs.value[2].count = parseInt(String(stats['draft'] ?? '0'))
|
||||
statusTabs.value[3].count = parseInt(String(stats['recycle'] ?? '0'))
|
||||
}
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
const res = await fetchAdminProductPage(page.value, pageSize.value, {
|
||||
name: searchName.value,
|
||||
status: activeStatus.value ?? undefined
|
||||
status: activeStatus.value,
|
||||
categoryId: selectedCategoryId.value ?? undefined
|
||||
})
|
||||
productList.value = res.items
|
||||
total.value = res.total
|
||||
@@ -195,43 +232,15 @@ async function loadData() {
|
||||
function handleSearch() {
|
||||
page.value = 1
|
||||
loadData()
|
||||
loadCounts()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchName.value = ''
|
||||
selectedCategoryId.value = null
|
||||
page.value = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
async function toggleStatus(item: AdminProduct) {
|
||||
const newStatus = item.status === 1 ? 2 : 1
|
||||
const ok = await updateAdminProductStatus(item.id, newStatus)
|
||||
if (ok) {
|
||||
uni.showToast({ title: '操作成功', icon: 'success' })
|
||||
loadData()
|
||||
} else {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
async function moveToRecycle(item: AdminProduct) {
|
||||
const isDelete = activeStatus.value !== 4
|
||||
const targetStatus = isDelete ? 4 : 2 // 移到回收站或恢复到下架
|
||||
|
||||
const action = isDelete ? '移到回收站' : '恢复'
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: `确认要将该商品${action}吗?`,
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
const ok = await updateAdminProductStatus(item.id, targetStatus)
|
||||
if (ok) {
|
||||
uni.showToast({ title: '操作成功', icon: 'success' })
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
loadCounts()
|
||||
}
|
||||
|
||||
function changeStatus(key: number) {
|
||||
|
||||
@@ -89,7 +89,13 @@
|
||||
<text class="th col-num">收藏数</text>
|
||||
<text class="th col-num wide">访客-支付转化率(%)</text>
|
||||
</view>
|
||||
<view v-for="(item, index) in rankingList" :key="index" class="table-row">
|
||||
<view v-if="loading" class="table-loading" style="padding: 40px; text-align: center;">
|
||||
<text>加载排行中...</text>
|
||||
</view>
|
||||
<view v-else-if="rankingList.length === 0" class="table-empty" style="padding: 40px; text-align: center;">
|
||||
<text>暂无排行数据</text>
|
||||
</view>
|
||||
<view v-else v-for="(item, index) in rankingList" :key="index" class="table-row">
|
||||
<text class="td col-id">{{ item.id }}</text>
|
||||
<view class="td col-img">
|
||||
<image class="product-img" :src="item.image" mode="aspectFill" />
|
||||
@@ -99,12 +105,12 @@
|
||||
</view>
|
||||
<text class="td col-num">{{ item.views }}</text>
|
||||
<text class="td col-num">{{ item.visitors }}</text>
|
||||
<text class="td col-num">{{ item.cartCount }}</text>
|
||||
<text class="td col-num">{{ item.orderCount }}</text>
|
||||
<text class="td col-num">{{ item.payCount }}</text>
|
||||
<text class="td col-num">{{ item.payAmount }}</text>
|
||||
<text class="td col-num">{{ item.favCount }}</text>
|
||||
<text class="td col-num wide">{{ item.conversion }}%</text>
|
||||
<text class="td col-num">{{ item.cart_count }}</text>
|
||||
<text class="td col-num">{{ item.order_count }}</text>
|
||||
<text class="td col-num">{{ item.pay_count }}</text>
|
||||
<text class="td col-num">{{ item.pay_amount }}</text>
|
||||
<text class="td col-num">{{ item.fav_count }}</text>
|
||||
<text class="td col-num wide">{{ item.visitors > 0 ? (item.pay_count / item.visitors * 100).toFixed(2) : '0.00' }}%</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -114,79 +120,54 @@
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
|
||||
import { fetchAdminProductStats, fetchAdminProductTrend, fetchAdminProductRanking } from '@/services/admin/productService.uts'
|
||||
|
||||
const statItems = ref([
|
||||
{ label: '商品浏览量', value: '7576', compare: '0.93%', trend: 'up', trendClass: 'up-red', bgColor: '#e6f7ff', emoji: '👁️' },
|
||||
{ label: '商品访客量', value: '765', compare: '0.79%', trend: 'up', trendClass: 'up-red', bgColor: '#f6ffed', emoji: '👤' },
|
||||
{ label: '支付件数', value: '322', compare: '-49.52%', trend: 'down', trendClass: 'down-green', bgColor: '#fff7e6', emoji: '🛍️' },
|
||||
{ label: '支付金额', value: '443254.62', compare: '-63.62%', trend: 'down', trendClass: 'down-green', bgColor: '#f9f0ff', emoji: '💰' },
|
||||
{ label: '退款件数', value: '0', compare: '0.00%', trend: 'none', trendClass: 'none-gray', bgColor: '#e6f7ff', emoji: '🔄' },
|
||||
{ label: '退款金额', value: '0', compare: '0.00%', trend: 'none', trendClass: 'none-gray', bgColor: '#f6ffed', emoji: '💴' }
|
||||
])
|
||||
|
||||
const rankingList = ref([
|
||||
{
|
||||
id: 963,
|
||||
image: 'https://img1.baidu.com/it/u=254065646,3100346083&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
|
||||
name: 'UR2024夏季新款女装复古纯欲氛围感一字肩短款T恤衫UWG440060',
|
||||
views: 1200,
|
||||
visitors: 246,
|
||||
cartCount: 74,
|
||||
orderCount: 214,
|
||||
payCount: 180,
|
||||
payAmount: '11877.49',
|
||||
favCount: 13,
|
||||
conversion: 18
|
||||
},
|
||||
{
|
||||
id: 116,
|
||||
image: 'https://img2.baidu.com/it/u=3775079632,546700868&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
|
||||
name: '爱奇艺智能 奇遇LT01 投影仪 家用卧室超高清手机便携投影机 (4K超清 支持侧投 手机同屏 华为一碰即投)',
|
||||
views: 959,
|
||||
visitors: 376,
|
||||
cartCount: 1,
|
||||
orderCount: 60,
|
||||
payCount: 29,
|
||||
payAmount: '26.00',
|
||||
favCount: 6,
|
||||
conversion: 7
|
||||
},
|
||||
{
|
||||
id: 48,
|
||||
image: 'https://img0.baidu.com/it/u=1762118431,3101886131&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
|
||||
name: '阿迪达斯官网 adidas BBALL CAP COT 男女训练运动帽子FQ5270 传奇墨水蓝/传奇墨水蓝/白 XL',
|
||||
views: 758,
|
||||
visitors: 207,
|
||||
cartCount: 63,
|
||||
orderCount: 67,
|
||||
payCount: 17,
|
||||
payAmount: '1409.30',
|
||||
favCount: 4,
|
||||
conversion: 7
|
||||
},
|
||||
{
|
||||
id: 108,
|
||||
image: 'https://img2.baidu.com/it/u=3033501986,2204481084&fm=253&fmt=auto&app=138&f=JPEG?w=569&h=500',
|
||||
name: 'FOMIX 蛋壳椅 进口头层牛皮橙色单人沙发椅Egg chair设计师师单椅单沙头层牛皮/单椅',
|
||||
views: 730,
|
||||
visitors: 216,
|
||||
cartCount: 26999,
|
||||
orderCount: 327,
|
||||
payCount: 14,
|
||||
payAmount: '66197.00',
|
||||
favCount: 4,
|
||||
conversion: 6
|
||||
}
|
||||
{ label: '商品浏览量', value: '0', compare: '0%', trend: 'none', trendClass: 'none-gray', bgColor: '#e6f7ff', emoji: '👁️', key: 'views' },
|
||||
{ label: '商品访客量', value: '0', compare: '0%', trend: 'none', trendClass: 'none-gray', bgColor: '#f6ffed', emoji: '👤', key: 'visitors' },
|
||||
{ label: '支付件数', value: '0', compare: '0%', trend: 'none', trendClass: 'none-gray', bgColor: '#fff7e6', emoji: '🛍️', key: 'pay_count' },
|
||||
{ label: '支付金额', value: '0.00', compare: '0%', trend: 'none', trendClass: 'none-gray', bgColor: '#f9f0ff', emoji: '💰', key: 'pay_amount' },
|
||||
{ label: '退款件数', value: '0', compare: '0%', trend: 'none', trendClass: 'none-gray', bgColor: '#e6f7ff', emoji: '🔄', key: 'refund_count' },
|
||||
{ label: '退款金额', value: '0.00', compare: '0%', trend: 'none', trendClass: 'none-gray', bgColor: '#f6ffed', emoji: '💴', key: 'refund_amount' }
|
||||
])
|
||||
|
||||
const rankingList = ref<Array<any>>([])
|
||||
const chartOption = ref<any>({})
|
||||
const loading = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
initChart()
|
||||
}, 300)
|
||||
loadAllData()
|
||||
})
|
||||
|
||||
async function loadAllData() {
|
||||
loading.value = true
|
||||
const endTime = new Date().toISOString()
|
||||
const startTime = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString()
|
||||
|
||||
try {
|
||||
// 1. 加载核心指标
|
||||
const stats = await fetchAdminProductStats(startTime, endTime)
|
||||
if (stats != null) {
|
||||
statItems.value.forEach(item => {
|
||||
const val = stats[item.key as string]
|
||||
item.value = typeof val === 'number' ? (item.key.includes('amount') ? val.toFixed(2) : String(val)) : String(val ?? '0')
|
||||
})
|
||||
}
|
||||
|
||||
// 2. 加载趋势图
|
||||
const trendData = await fetchAdminProductTrend(startTime, endTime)
|
||||
initChart(trendData)
|
||||
|
||||
// 3. 加载排行
|
||||
const rankingData = await fetchAdminProductRanking(startTime, endTime, 'sales', 10)
|
||||
rankingList.value = rankingData
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载统计失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function toPlainObject(obj: any): any {
|
||||
if (obj == null) return null
|
||||
if (typeof obj !== 'object') return obj
|
||||
@@ -210,29 +191,19 @@ function toPlainObject(obj: any): any {
|
||||
return plain
|
||||
}
|
||||
|
||||
function initChart() {
|
||||
const dates = [
|
||||
'01-04', '01-05', '01-06', '01-07', '01-08', '01-09', '01-10', '01-11', '01-12', '01-13',
|
||||
'01-14', '01-15', '01-16', '01-17', '01-18', '01-19', '01-20', '01-21', '01-22', '01-23',
|
||||
'01-24', '01-25', '01-26', '01-27', '01-28', '01-29', '01-30', '01-31', '02-01', '02-02'
|
||||
]
|
||||
function initChart(data: any[]) {
|
||||
const dates = data.map(item => item.date_group.substring(5))
|
||||
const views = data.map(item => item.views)
|
||||
const visitors = data.map(item => item.visitors)
|
||||
const payAmounts = data.map(item => item.pay_amount)
|
||||
const refundAmounts = data.map(item => item.refund_amount)
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.7)',
|
||||
padding: [10, 15],
|
||||
textStyle: { color: '#fff' },
|
||||
formatter: (params: any[]) : string => {
|
||||
let res = `<div style="font-size:12px; color:#ccc; margin-bottom:5px;">${params[0].name}</div>`
|
||||
params.forEach(p => {
|
||||
res += `<div style="display:flex; align-items:center;">
|
||||
<div style="width:8px; height:8px; border-radius:50%; background:${p.color}; margin-right:8px;"></div>
|
||||
<span>${p.seriesName}: ${p.value}</span>
|
||||
</div>`
|
||||
})
|
||||
return res
|
||||
}
|
||||
textStyle: { color: '#fff' }
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
@@ -252,14 +223,12 @@ function initChart() {
|
||||
{
|
||||
type: 'value',
|
||||
name: '金额',
|
||||
nameTextStyle: { color: '#8c8c8c', padding: [0, 30, 0, 0] },
|
||||
splitLine: { lineStyle: { type: 'dashed', color: '#f0f0f0' } },
|
||||
axisLabel: { color: '#8c8c8c' }
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '数量',
|
||||
nameTextStyle: { color: '#8c8c8c', padding: [0, 0, 0, 30] },
|
||||
splitLine: { show: false },
|
||||
axisLabel: { color: '#8c8c8c' }
|
||||
}
|
||||
@@ -272,8 +241,7 @@ function initChart() {
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
itemStyle: { color: '#b37feb' },
|
||||
lineStyle: { width: 2 },
|
||||
data: [90, 110, 115, 100, 95, 80, 60, 40, 70, 85, 75, 65, 70, 80, 100, 120, 110, 90, 60, 95, 115, 110, 85, 50, 45, 55, 75]
|
||||
data: views
|
||||
},
|
||||
{
|
||||
name: '商品访客量',
|
||||
@@ -282,31 +250,23 @@ function initChart() {
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
itemStyle: { color: '#ffbb96' },
|
||||
lineStyle: { width: 2 },
|
||||
data: [15, 12, 10, 8, 11, 14, 13, 8, 9, 11, 10, 15, 12, 11, 9, 12, 14, 15, 11, 10, 13, 15, 11, 8, 12, 10, 14]
|
||||
data: visitors
|
||||
},
|
||||
{
|
||||
name: '支付金额',
|
||||
type: 'bar',
|
||||
barWidth: '25%',
|
||||
itemStyle: { color: '#1890ff' },
|
||||
data: [10, 5, 8, 0, 145, 15, 5, 0, 0, 0, 0, 5, 30, 0, 15, 20, 100, 20, 25, 5, 1, 3, 70, 5, 10, 5, 15, 10]
|
||||
data: payAmounts
|
||||
},
|
||||
{
|
||||
name: '退款金额',
|
||||
type: 'bar',
|
||||
barWidth: '25%',
|
||||
itemStyle: { color: '#52c41a' },
|
||||
data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
data: refundAmounts
|
||||
}
|
||||
],
|
||||
markLine: {
|
||||
silent: true,
|
||||
symbol: ['none', 'none'],
|
||||
label: { show: false },
|
||||
lineStyle: { color: '#bfbfbf', type: 'dashed' },
|
||||
data: [{ yAxis: 145853.16 }]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chartOption.value = toPlainObject(option)
|
||||
|
||||
Reference in New Issue
Block a user