ylj20011123 2900c384eb update
2025-10-29 10:02:45 +08:00

842 lines
21 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="product-report">
<!-- 报表标题 -->
<view class="report-header">
<text class="report-title">商品报表及分析</text>
<view class="report-period">
<text class="period-label">分析周期</text>
<text class="period-value">{{ analysisPeriod }}</text>
</view>
</view>
<!-- 关键指标卡片 -->
<view class="metrics-row">
<view class="metric-card">
<view class="metric-icon sales"></view>
<text class="metric-title">总销量</text>
<view class="metric-value-container">
<text class="metric-value">{{ formatNumber(totalSales) }}</text>
<text class="metric-unit"></text>
</view>
</view>
<view class="metric-card">
<view class="metric-icon revenue"></view>
<text class="metric-title">总销售额</text>
<view class="metric-value-container">
<text class="metric-value">¥{{ formatMoney(totalRevenue) }}</text>
<text class="metric-unit"></text>
</view>
</view>
<view class="metric-card">
<view class="metric-icon types"></view>
<text class="metric-title">商品类型</text>
<view class="metric-value-container">
<text class="metric-value">{{ productTypes }}</text>
<text class="metric-unit"></text>
</view>
</view>
</view>
<!-- 商品类型分布图 -->
<view class="chart-card">
<view class="chart-header">
<text class="chart-title">商品类型分布</text>
<text class="chart-subtitle">各类商品销量占比</text>
</view>
<view class="chart-content">
<view class="pie-chart-container">
<QiunDataCharts type="pie" :opts="pieChartOpts" :chartData="pieChartData" :canvas2d="true"
:inScrollView="true" canvasId="productTypePieChart" tooltipFormat="ShopTypeDistribution" />
</view>
</view>
</view>
<!-- 商品销量排行榜 -->
<view class="chart-card">
<view class="chart-header">
<text class="chart-title">商品销量排行榜</text>
<text class="chart-subtitle">TOP 5 热销商品</text>
</view>
<view class="chart-content">
<view class="bar-chart-container">
<QiunDataCharts type="column" :opts="barChartOpts" :chartData="barChartData" :animation="false"
:canvas2d="true" :inScrollView="true" tooltipFormat="SalesRankingOfProducts" />
</view>
</view>
</view>
<!-- 销售趋势图 -->
<view class="chart-card">
<view class="chart-header">
<text class="chart-title">商品销售趋势</text>
<text class="chart-subtitle">最近30天销量与销售额变化</text>
</view>
<view class="chart-content">
<view class="line-chart-container">
<QiunDataCharts type="line" :opts="lineChartOpts" :chartData="lineChartData" :canvas2d="true"
:inScrollView="true" canvasId="productTrendChart" :pageScrollTop="pageScrollTop" />
<view class="chart-legend">
<view class="legend-item">
<view class="legend-dot sales"></view>
<text class="legend-text">销量</text>
</view>
<view class="legend-item">
<view class="legend-dot revenue"></view>
<text class="legend-text">销售额(÷100)</text>
</view>
</view>
</view>
</view>
</view>
<!-- 商品详细数据 -->
<view class="chart-card">
<view class="chart-header">
<text class="chart-title">商品详细数据</text>
<view class="table-actions">
</view>
</view>
<view class="product-cards">
<view class="product-card" v-for="(item, index) in productList" :key="index">
<view class="product-header">
<view class="product-type-tag" :style="{
backgroundColor: index === 0 ? '#576EFF' :
index === 1 ? '#52C41A' :
index === 2 ? '#FAAD14' :
index === 3 ? '#FF7875' :
index === 4 ? '#B37FEB' : '#666666'
}">
{{ index + 1 }}
</view>
<text class="product-code">{{ item.COMMODITY_BARCODE }}</text>
</view>
<view class="product-body">
<text class="product-name">{{ item.COMMODITY_NAME }}</text>
<view class="product-metrics">
<view class="metric-item">
<text class="metric-label">销量</text>
<text class="metric-value">{{ item.SELLCOUNT ? item.SELLCOUNT.toLocaleString() : "" }}</text>
</view>
<view class="metric-divider"></view>
<view class="metric-item">
<text class="metric-label">销售额</text>
<text class="metric-value">¥{{ item.FACTAMOUNT ? item.FACTAMOUNT.toLocaleString() : "" }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import QiunDataCharts from './qiun-data-charts/components/qiun-data-charts/qiun-data-charts.vue'
import request from "@/util/index.js";
export default {
components: {
QiunDataCharts
},
data() {
return {
analysisPeriod: '2024年10月',
totalSales: 15678,
totalRevenue: 2345678,
productTypes: 12,
// 商品类型分布数据
productTypeData: [],
// 销量排行榜数据
salesRankingData: [],
// 销售趋势数据
trendData: {
categories: ['1日', '5日', '10日', '15日', '20日', '25日', '30日'],
salesData: [1200, 1400, 1600, 1800, 2000, 2200, 2400],
revenueData: [100, 200, 300, 400, 500, 600, 700]
},
productList: []
}
},
props: {
pageScrollTop: {
type: Number,
default: 0
}
},
computed: {
// 饼图数据
pieChartData() {
return {
series: [{
data: this.productTypeData.map(item => ({
name: item.name,
value: item.value
}))
}]
}
},
// 饼图配置
pieChartOpts() {
return {
padding: [5, 5, 5, 5],
dataLabel: true,
legend: {
show: true
},
extra: {
pie: {
activeOpacity: 0.5,
activeRadius: 10,
offsetAngle: 0,
labelWidth: 15,
border: false,
borderWidth: 3,
borderColor: '#FFFFFF'
}
}
}
},
// 柱状图数据
barChartData() {
return {
categories: this.salesRankingData.map(item =>
this.formatXAxisLabel(item.name)
),
series: [{
name: '销量',
data: this.salesRankingData.map(item => item.sales)
}]
}
},
// 柱状图配置
barChartOpts() {
// 计算最大值并生成6个刻度每个刻度都是100的倍数
const maxSales = Math.max(...this.salesRankingData.map(item => item.sales));
const roundedMax = Math.ceil(maxSales / 100) * 100; // 向上取整到100的倍数
const yAxisInterval = Math.ceil(roundedMax / 5); // 分成5个间隔总共6个刻度
const finalInterval = Math.ceil(yAxisInterval / 100) * 100; // 确保间隔是100的倍数
const finalMax = finalInterval * 5; // 最终最大值
// 生成Y轴刻度数据
const yAxisData = [];
for (let i = 0; i <= 5; i++) {
yAxisData.push(i * finalInterval);
}
return {
color: ['#576EFF'],
legend: {
show: true,
color: ['#576EFF']
},
padding: [20, 15, 35, 15], // 增加底部padding给X轴标签留空间
dataLabel: false,
enableScroll: false,
xAxis: {
itemCount: 5, // 减少显示的标签数量
scrollAlign: 'right',
scrollColor: '#576EFF',
scrollBackgroundColor: 'rgba(87, 110, 255, 0.1)',
scrollWidth: 4,
scrollHeight: 8,
rotate: 30, // 旋转30度避免重叠
fontSize: 12, // 适当减小字体
margin: 15, // 增加标签与轴线的距离
},
yAxis: {
gridType: 'dash',
dashLength: 2,
data: [{
min: 0,
max: finalMax,
data: yAxisData
}]
},
categoriesReal: this.salesRankingData.map(item =>
item.name
),
extra: {
column: {
type: 'group',
width: 12,
activeBgColor: '#000000',
activeBgOpacity: 0.08,
barBorderCircle: true,
linearType: 'none',
linearOpacity: 0,
}
}
}
},
// 折线图数据
lineChartData() {
return {
categories: this.trendData.categories,
series: [
{
name: '销量',
data: this.trendData.salesData
},
{
name: '销售额(百元)',
data: this.trendData.revenueData.map(v => v / 100)
}
]
}
},
// 折线图配置
lineChartOpts() {
return {
color: ['#576EFF', '#52C41A'],
padding: [15, 15, 15, 15],
dataLabel: false,
legend: {
show: false
},
xAxis: {
disableGrid: true,
itemCount: 7
},
yAxis: {
gridType: 'dash',
dashLength: 2,
data: [{
min: 0
}]
},
extra: {
line: {
type: 'curve',
width: 3,
activeType: 'hollow'
}
}
}
}
},
onReady() {
// 获取整个组件的数据
this.handleGetAllData()
},
methods: {
// 格式化X轴标签文字
formatXAxisLabel(name) {
if (!name) return '';
// 如果名称超过4个字符截取并添加省略号
return name.length > 4 ? name.substring(0, 4) + '...' : name;
},
// 获取整个组件的数据
handleGetAllData() {
// 拿到商品类型分布
this.handleGetShopTypeDistribution()
// 拿到商品销售排行榜
this.handleGetTopSalesRank()
},
// 拿到商品类型分布
async handleGetShopTypeDistribution() {
const req = {
ProvinceCode: '530000',
StatisticsMonth: '202509',
type: 'encryption'
}
const data = await request.$webPost(
"CommercialApi/SupplyChain/GetWelFareSummary",
req
);
console.log('商品类型分布', data);
let list = data.Result_Data.WelFareGoodsTypeList
let res = []
if (list && list.length > 0) {
list.forEach((item) => {
res.push({ name: item.GoodsTypeName, value: item.GoodsCount })
})
}
this.productTypeData = res
},
// 拿到商品销售排行榜
async handleGetTopSalesRank() {
const req = {
action_type: "getCommoditySaleSort",
province_code: 5564,
rowNum: 5
}
const data = await request.$cloudUrlGet(req);
console.log('datadatadatadatadata', data);
let list = data.COMMODITYSALE_DESC
let res = []
if (list && list.length > 0) {
list.forEach((item) => {
res.push({
name: item.COMMODITY_NAME,
sales: item.SELLCOUNT,
})
})
}
this.salesRankingData = res
this.productList = list
},
// 获取屏幕宽度
getScreenWidth() {
try {
return uni.getSystemInfoSync().windowWidth || 375;
} catch (e) {
return 375;
}
},
formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")
},
formatMoney(amount) {
return amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")
},
}
}
</script>
<style scoped lang="less">
@primary-color: #667eea;
@secondary-color: #764ba2;
@gold: #FFD700;
@silver: #C0C0C0;
@bronze: #CD7F32;
@text-primary: #333;
@text-secondary: #666;
@text-light: #999;
@bg-white: #ffffff;
@border-radius: 16rpx;
@shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
.product-report {
.report-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32rpx;
padding: 0 8rpx;
.report-title {
font-size: 30rpx;
font-weight: 600;
color: @text-primary;
}
.report-period {
display: flex;
align-items: center;
.period-label {
font-size: 24rpx;
color: @text-secondary;
margin-right: 8rpx;
}
.period-value {
font-size: 24rpx;
color: @primary-color;
font-weight: 500;
}
}
}
.metrics-row {
display: flex;
gap: 24rpx;
margin-bottom: 32rpx;
.metric-card {
flex: 1;
background: @bg-white;
border-radius: @border-radius;
padding: 32rpx 24rpx;
box-shadow: @shadow;
text-align: center;
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4rpx;
background: linear-gradient(90deg, @primary-color, @secondary-color);
}
.metric-icon {
width: 48rpx;
height: 48rpx;
margin: 0 auto 16rpx;
border-radius: 50%;
position: relative;
&.sales {
background: linear-gradient(135deg, #52c41a, #73d13d);
&::after {
content: '📦';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24rpx;
}
}
&.revenue {
background: linear-gradient(135deg, #faad14, #ffc53d);
&::after {
content: '💰';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24rpx;
}
}
&.types {
background: linear-gradient(135deg, #1890ff, #40a9ff);
&::after {
content: '🏷️';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24rpx;
}
}
}
.metric-title {
font-size: 24rpx;
color: @text-secondary;
margin-bottom: 12rpx;
}
.metric-value-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 4rpx;
}
.metric-value {
font-size: 28rpx;
font-weight: 600;
color: @text-primary;
font-family: 'DINAlternate-Bold', sans-serif;
line-height: 1;
}
.metric-unit {
font-size: 24rpx;
color: @text-light;
line-height: 1;
}
}
}
.chart-card {
background: @bg-white;
border-radius: @border-radius;
padding: 24rpx;
box-shadow: @shadow;
margin-bottom: 24rpx;
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.chart-title {
font-size: 28rpx;
font-weight: 600;
color: @text-primary;
}
.chart-subtitle {
font-size: 24rpx;
color: @text-light;
margin-top: 4rpx;
}
.table-actions {
display: flex;
gap: 16rpx;
.action-btn {
font-size: 24rpx;
color: @primary-color;
padding: 8rpx 16rpx;
border: 1rpx solid @primary-color;
border-radius: 8rpx;
}
}
}
.chart-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200rpx;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
border-radius: 12rpx;
margin-bottom: 16rpx;
.placeholder-text {
font-size: 32rpx;
color: @text-secondary;
margin-bottom: 8rpx;
}
.placeholder-desc {
font-size: 24rpx;
color: @text-light;
}
}
.ranking-list {
.ranking-item {
display: flex;
align-items: center;
padding: 16rpx 0;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.ranking-number {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
font-weight: 600;
color: white;
background: @text-light;
margin-right: 16rpx;
&.rank-gold {
background: @gold;
color: #333;
}
&.rank-silver {
background: @silver;
color: #333;
}
&.rank-bronze {
background: @bronze;
color: white;
}
}
.ranking-info {
flex: 1;
.product-name {
font-size: 28rpx;
color: @text-primary;
margin-bottom: 4rpx;
}
.product-code {
font-size: 24rpx;
color: @text-light;
}
}
.ranking-data {
text-align: right;
.sales-number {
font-size: 24rpx;
color: @text-primary;
font-weight: 500;
display: block;
}
.sales-amount {
font-size: 22rpx;
color: @primary-color;
font-weight: 600;
}
}
}
}
.chart-content {
.pie-chart-container {
display: flex;
flex-direction: column;
align-items: center;
.pie-legend {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 16rpx;
margin-top: 16rpx;
.legend-item {
display: flex;
align-items: center;
gap: 8rpx;
.legend-color {
width: 12rpx;
height: 12rpx;
border-radius: 2rpx;
}
.legend-name {
font-size: 22rpx;
color: @text-secondary;
}
.legend-value {
font-size: 22rpx;
color: @text-primary;
font-weight: 600;
}
}
}
}
.bar-chart-container {
width: 100%;
}
.line-chart-container {
.chart-legend {
display: flex;
justify-content: center;
gap: 32rpx;
margin-top: 16rpx;
.legend-item {
display: flex;
align-items: center;
gap: 8rpx;
.legend-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
&.sales {
background: #576EFF;
}
&.revenue {
background: #52C41A;
}
}
.legend-text {
font-size: 22rpx;
color: @text-secondary;
}
}
}
}
}
.product-cards {
display: flex;
flex-direction: column;
gap: 16rpx;
.product-card {
background: #fafafa;
border-radius: 12rpx;
padding: 20rpx;
border: 1rpx solid #f0f0f0;
.product-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
.product-type-tag {
padding: 4rpx 12rpx;
border-radius: 12rpx;
font-size: 20rpx;
color: white;
font-weight: 500;
}
.product-code {
font-size: 20rpx;
color: @text-light;
font-family: monospace;
}
}
.product-body {
.product-name {
font-size: 24rpx;
color: @text-primary;
font-weight: 600;
margin-bottom: 12rpx;
display: block;
}
.product-metrics {
display: flex;
align-items: center;
.metric-item {
flex: 1;
text-align: center;
.metric-label {
font-size: 24rpx;
color: @text-secondary;
display: block;
margin-bottom: 4rpx;
}
.metric-value {
font-size: 24rpx;
color: @text-primary;
font-weight: 600;
display: block;
}
}
.metric-divider {
width: 1rpx;
height: 40rpx;
background: #e0e0e0;
margin: 0 16rpx;
}
}
}
}
}
}
}
</style>