This commit is contained in:
ylj20011123 2025-10-24 11:50:00 +08:00
parent 3bf7e0a813
commit 188282c4f0
6 changed files with 265 additions and 204 deletions

View File

@ -44,21 +44,8 @@
</view> </view>
<view class="chart-content"> <view class="chart-content">
<view class="pie-chart-container"> <view class="pie-chart-container">
<QiunDataCharts <QiunDataCharts type="pie" :opts="pieChartOpts" :chartData="pieChartData" :canvas2d="true"
type="pie" :inScrollView="true" canvasId="inventoryStructurePieChart" />
:opts="pieChartOpts"
:chartData="pieChartData"
:canvas2d="true"
:inScrollView="true"
canvasId="inventoryStructurePieChart"
/>
<view class="pie-legend">
<view class="legend-item" v-for="(item, index) in categoryData" :key="index">
<view class="legend-color" :style="{ backgroundColor: item.color }"></view>
<text class="legend-name">{{ item.name }}</text>
<text class="legend-value">{{ item.percentage }}%</text>
</view>
</view>
</view> </view>
</view> </view>
</view> </view>
@ -71,14 +58,8 @@
</view> </view>
<view class="chart-content"> <view class="chart-content">
<view class="bar-chart-container"> <view class="bar-chart-container">
<QiunDataCharts <QiunDataCharts type="column" :opts="barChartOpts" :chartData="barChartData" :canvas2d="true"
type="column" :inScrollView="true" canvasId="inventoryStructureBarChart" />
:opts="barChartOpts"
:chartData="barChartData"
:canvas2d="true"
:inScrollView="true"
canvasId="inventoryStructureBarChart"
/>
</view> </view>
<view class="chart-legend"> <view class="chart-legend">
<view class="legend-item"> <view class="legend-item">
@ -87,7 +68,7 @@
</view> </view>
<view class="legend-item"> <view class="legend-item">
<view class="legend-dot value"></view> <view class="legend-dot value"></view>
<text class="legend-text">库存价值(÷1000)</text> <text class="legend-text">库存价值</text>
</view> </view>
</view> </view>
</view> </view>
@ -97,34 +78,30 @@
<view class="chart-card"> <view class="chart-card">
<view class="chart-header"> <view class="chart-header">
<text class="chart-title">库存详细数据</text> <text class="chart-title">库存详细数据</text>
<view class="table-actions">
<text class="action-btn" @click="toggleFilter">筛选</text>
<text class="action-btn" @click="exportData">导出</text>
</view>
</view> </view>
<view class="category-cards"> <view class="category-cards">
<view class="category-card" v-for="(item, index) in categoryData" :key="index"> <view class="category-card" v-for="(item, index) in categoryData" :key="index">
<view class="category-header"> <view class="category-header">
<view class="category-type-tag" :style="{ backgroundColor: item.color }"> <view class="category-type-tag">
{{ item.name }} {{ item.name }}
</view> </view>
<text class="category-percentage">{{ item.percentage }}%</text> <text class="category-percentage">{{ item.value }}%</text>
</view> </view>
<view class="category-body"> <view class="category-body">
<view class="category-metrics"> <view class="category-metrics">
<view class="metric-item"> <view class="metric-item">
<text class="metric-label">库存数量</text> <text class="metric-label">库存数量</text>
<text class="metric-value">{{ formatNumber(item.quantity) }}</text> <text class="metric-value"></text>
</view> </view>
<view class="metric-divider"></view> <view class="metric-divider"></view>
<view class="metric-item"> <view class="metric-item">
<text class="metric-label">库存价值</text> <text class="metric-label">库存价值</text>
<text class="metric-value">¥{{ formatMoney(item.value) }}</text> <text class="metric-value">¥</text>
</view> </view>
<view class="metric-divider"></view> <view class="metric-divider"></view>
<view class="metric-item"> <view class="metric-item">
<text class="metric-label">平均单价</text> <text class="metric-label">平均单价</text>
<text class="metric-value">¥{{ formatMoney(item.avgPrice) }}</text> <text class="metric-value">¥</text>
</view> </view>
</view> </view>
<view class="category-trend"> <view class="category-trend">
@ -142,6 +119,7 @@
<script> <script>
import QiunDataCharts from './qiun-data-charts/components/qiun-data-charts/qiun-data-charts.vue' import QiunDataCharts from './qiun-data-charts/components/qiun-data-charts/qiun-data-charts.vue'
import request from "@/util/index.js";
export default { export default {
components: { components: {
@ -154,82 +132,8 @@ export default {
totalCategories: 8, totalCategories: 8,
totalQuantity: 485672, totalQuantity: 485672,
totalValue: 3847.56, totalValue: 3847.56,
// //
categoryData: [ categoryData: []
{
name: '保健品',
quantity: 156234,
value: 1256.78,
percentage: 32.2,
color: '#576EFF',
avgPrice: 80.45,
trend: 5.2
},
{
name: '茶叶',
quantity: 128456,
value: 987.34,
percentage: 26.5,
color: '#52C41A',
avgPrice: 76.84,
trend: -2.1
},
{
name: '食品',
quantity: 98567,
value: 654.23,
percentage: 20.3,
color: '#FAAD14',
avgPrice: 66.38,
trend: 8.7
},
{
name: '饮品',
quantity: 56789,
value: 456.78,
percentage: 11.7,
color: '#FF7875',
avgPrice: 80.46,
trend: 3.4
},
{
name: '工艺品',
quantity: 23456,
value: 345.67,
percentage: 4.8,
color: '#B37FEB',
avgPrice: 147.43,
trend: -1.5
},
{
name: '纺织品',
quantity: 12345,
value: 98.76,
percentage: 2.5,
color: '#13C2C2',
avgPrice: 80.00,
trend: 12.3
},
{
name: '化妆品',
quantity: 8765,
value: 34.56,
percentage: 1.8,
color: '#FF85C0',
avgPrice: 39.45,
trend: 6.8
},
{
name: '其他',
quantity: 560,
value: 13.44,
percentage: 0.2,
color: '#8C8C8C',
avgPrice: 240.00,
trend: -8.9
}
]
} }
}, },
@ -240,7 +144,7 @@ export default {
series: [{ series: [{
data: this.categoryData.map(item => ({ data: this.categoryData.map(item => ({
name: item.name, name: item.name,
value: item.quantity value: Number(item.value)
})) }))
}] }]
} }
@ -249,11 +153,10 @@ export default {
// //
pieChartOpts() { pieChartOpts() {
return { return {
color: this.categoryData.map(item => item.color),
padding: [5, 5, 5, 5], padding: [5, 5, 5, 5],
dataLabel: true, dataLabel: true,
legend: { legend: {
show: false show: true
}, },
extra: { extra: {
pie: { pie: {
@ -325,8 +228,30 @@ export default {
} }
} }
}, },
onReady() {
//
this.handleGetAllData()
},
methods: { methods: {
//
handleGetAllData() {
//
this.handleGetShopTypeNumberRate()
},
//
async handleGetShopTypeNumberRate() {
let req = {
ProvinceCode: "530000",
type: "encryption"
}
const data = await request.$webPost(
"CommercialApi/BaseInfo/GetBrandStructureAnalysis",
req
);
let list = data.Result_Data.List
console.log('商品类别数量占比', list);
this.categoryData = list
},
formatNumber(num) { formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")
}, },
@ -425,17 +350,41 @@ export default {
&.categories { &.categories {
background: linear-gradient(135deg, #1890ff, #40a9ff); background: linear-gradient(135deg, #1890ff, #40a9ff);
&::after { content: '📊'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 24rpx; }
&::after {
content: '📊';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24rpx;
}
} }
&.quantity { &.quantity {
background: linear-gradient(135deg, #52c41a, #73d13d); background: linear-gradient(135deg, #52c41a, #73d13d);
&::after { content: '📦'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 24rpx; }
&::after {
content: '📦';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24rpx;
}
} }
&.value { &.value {
background: linear-gradient(135deg, #faad14, #ffc53d); background: linear-gradient(135deg, #faad14, #ffc53d);
&::after { content: '💰'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 24rpx; }
&::after {
content: '💰';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24rpx;
}
} }
} }

View File

@ -48,25 +48,16 @@
</view> </view>
<view class="chart-content"> <view class="chart-content">
<view class="growth-chart-container"> <view class="growth-chart-container">
<QiunDataCharts <QiunDataCharts type="line" :opts="growthChartOpts" :chartData="growthChartData" :canvas2d="true"
type="line" :inScrollView="true" canvasId="memberGrowthChart" />
:opts="growthChartOpts"
:chartData="growthChartData"
:canvas2d="true"
:inScrollView="true"
canvasId="memberGrowthChart"
/>
</view> </view>
<view class="chart-controls"> <view class="chart-controls">
<text class="control-btn" <text class="control-btn" :class="{ active: growthPeriod === 'month' }"
:class="{ active: growthPeriod === 'month' }" @click="changeGrowthPeriod('month')">月度</text>
@click="changeGrowthPeriod('month')">月度</text> <text class="control-btn" :class="{ active: growthPeriod === 'quarter' }"
<text class="control-btn" @click="changeGrowthPeriod('quarter')">季度</text>
:class="{ active: growthPeriod === 'quarter' }" <text class="control-btn" :class="{ active: growthPeriod === 'year' }"
@click="changeGrowthPeriod('quarter')">季度</text> @click="changeGrowthPeriod('year')">年度</text>
<text class="control-btn"
:class="{ active: growthPeriod === 'year' }"
@click="changeGrowthPeriod('year')">年度</text>
</view> </view>
</view> </view>
</view> </view>
@ -78,14 +69,16 @@
<text class="chart-subtitle">不同等级会员数量占比</text> <text class="chart-subtitle">不同等级会员数量占比</text>
</view> </view>
<view class="member-levels"> <view class="member-levels">
<view class="level-item" v-for="(level, index) in memberLevels" :key="index"> <view class="level-item" v-for="(item, index) in memberLevels" :key="index">
<view class="level-info"> <view class="level-info">
<view class="level-badge" :class="level.type">{{ level.name }}</view> <view class="level-badge">{{ memberLevelObj && item.label &&
<text class="level-count">{{ formatNumber(level.count) }}</text> memberLevelObj[Number(item.label)]
? memberLevelObj[Number(item.label)] : '' }}</view>
<text class="level-count">{{ formatNumber(item.value) }}</text>
</view> </view>
<view class="level-progress"> <view class="level-progress">
<view class="progress-bar" :style="{ width: level.percentage + '%' }"></view> <view class="progress-bar" :style="{ width: item.key + '%' }"></view>
<text class="progress-text">{{ level.percentage }}%</text> <text class="progress-text">{{ item.key }}%</text>
</view> </view>
</view> </view>
</view> </view>
@ -99,13 +92,12 @@
</view> </view>
<view class="activity-analysis"> <view class="activity-analysis">
<view class="activity-grid"> <view class="activity-grid">
<view class="activity-item" v-for="(activity, index) in activityData" :key="index"> <view class="activity-item" v-for="(item, index) in activityData" :key="index">
<view class="activity-icon" :class="activity.status"> <text class="activity-label">{{ memberLevelObj && item.label &&
<text class="icon-text">{{ activity.icon }}</text> memberLevelObj[Number(item.label)]
</view> ? memberLevelObj[Number(item.label)] : '' }}</text>
<text class="activity-label">{{ activity.label }}</text> <text class="activity-count">{{ formatNumber(item.value) }}</text>
<text class="activity-count">{{ formatNumber(activity.count) }}</text> <text class="activity-percent">{{ item.key }}%</text>
<text class="activity-percent">{{ activity.percent }}%</text>
</view> </view>
</view> </view>
</view> </view>
@ -118,16 +110,16 @@
<text class="chart-subtitle">不同渠道会员获取情况</text> <text class="chart-subtitle">不同渠道会员获取情况</text>
</view> </view>
<view class="source-chart"> <view class="source-chart">
<view class="source-item" v-for="(source, index) in memberSources" :key="index"> <view class="source-item" v-for="(item, index) in memberSources" :key="index">
<view class="source-info"> <view class="source-info">
<text class="source-name">{{ source.name }}</text> <text class="source-name">{{ item.label }}</text>
<text class="source-count">{{ formatNumber(source.count) }}</text> <text class="source-count">{{ formatNumber(item.value) }}</text>
</view> </view>
<view class="source-progress"> <view class="source-progress">
<view class="source-bar"> <view class="source-bar">
<view class="bar-fill" :style="{ width: source.percentage + '%' }"></view> <view class="bar-fill" :style="{ width: item.key + '%' }"></view>
</view> </view>
<text class="source-percent">{{ source.percentage }}%</text> <text class="source-percent">{{ item.key }}%</text>
</view> </view>
</view> </view>
</view> </view>
@ -206,6 +198,8 @@
<script> <script>
import QiunDataCharts from './qiun-data-charts/components/qiun-data-charts/qiun-data-charts.vue' import QiunDataCharts from './qiun-data-charts/components/qiun-data-charts/qiun-data-charts.vue'
import request from "@/util/index.js";
import { wrapTreeNode } from "../../../util/dateTime";
export default { export default {
components: { components: {
@ -246,25 +240,10 @@ export default {
currentTime: '14:32', currentTime: '14:32',
lastActiveTime: '14:28', lastActiveTime: '14:28',
lastPurchaseTime: '14:15', lastPurchaseTime: '14:15',
memberLevels: [ memberLevels: [], //
{ name: '普通会员', type: 'normal', count: 89456, percentage: 57.1 }, memberLevelObj: {},//
{ name: '银卡会员', type: 'silver', count: 45678, percentage: 29.1 }, activityData: [], //
{ name: '金卡会员', type: 'gold', count: 18900, percentage: 12.1 }, memberSources: [],//
{ name: '钻石会员', type: 'diamond', count: 2755, percentage: 1.7 }
],
activityData: [
{ label: '高度活跃', icon: '🔥', count: 23456, percent: 26.2, status: 'high' },
{ label: '中度活跃', icon: '⚡', count: 45678, percent: 51.1, status: 'medium' },
{ label: '低度活跃', icon: '👤', count: 15678, percent: 17.5, status: 'low' },
{ label: '沉默会员', icon: '😴', count: 4677, percent: 5.2, status: 'inactive' }
],
memberSources: [
{ name: '小程序注册', count: 67890, percentage: 43.3 },
{ name: '线下扫码', count: 45678, percentage: 29.1 },
{ name: '分享邀请', count: 23456, percentage: 15.0 },
{ name: '活动推广', count: 18965, percentage: 12.1 },
{ name: '其他渠道', count: 800, percentage: 0.5 }
],
retentionData: [ retentionData: [
{ period: '次日留存', rate: 85.6 }, { period: '次日留存', rate: 85.6 },
{ period: '7日留存', rate: 78.9 }, { period: '7日留存', rate: 78.9 },
@ -325,8 +304,126 @@ export default {
} }
} }
}, },
onReady() {
//
this.handleGetAllData()
},
methods: { methods: {
//
handleGetAllData() {
//
this.handleGetMemberLevelData()
//
this.handleGetMemberActiveData()
//
this.handleGetMemberComeData()
},
sortData(data) {
return data.sort((a, b) => b.value - a.value); // 'value'
},
//
async handleGetMemberComeData() {
const req = {
CalcType: "5",
OwnerUnitId: 911,
ExcludeTest: true,
StartDate: '',
EndDate: "",
MembershipType: "",
MembershipLevel: "",
MembershipTarget: "",
type: "encryption"
}
const data = await request.$posPost(
"MemberApi/Member/GetMembershipCount",
req
);
let list = data.Result_Data.List
this.memberSources = this.sortData(list)
},
//
async handleGetMemberActiveData() {
const req = {
CalcType: "4",
OwnerUnitId: 911,
ExcludeTest: true,
StartDate: '',
EndDate: "",
MembershipType: "",
MembershipLevel: "",
MembershipTarget: "",
type: "encryption"
}
const data = await request.$posPost(
"MemberApi/Member/GetMembershipCount",
req
);
let list = data.Result_Data.List
console.log('会员活跃度分析', list);
let res = []
if (list && list.length > 0) {
list.forEach((item) => {
if (item.value > 0) {
res.push(item)
}
})
}
this.activityData = this.sortData(res)
},
//
async handleGetMemberLevelData() {
//
let objReq = {
FIELDEXPLAIN_FIELD: "MEMBERSHIP_LEVEL_YN",
FIELDEXPLAIN_ID: "",
FIELDENUM_PID: "",
FIELDENUM_STATUS: "",
SearchKey: "",
type: "encryption"
}
let levelObj = await request.$posPost(
"MemberApi/Dictionary/GetNestingFIELDENUMList",
objReq
);
let objList = wrapTreeNode(levelObj.Result_Data.List)
let levelResObj = {}
if (objList && objList.length > 0) {
objList.forEach((item) => {
levelResObj[item.FIELDENUM_VALUE] = item.FIELDENUM_NAME
})
}
this.memberLevelObj = levelResObj
console.log('this.memberLevelObjthis.memberLevelObj', this.memberLevelObj);
const req = {
CalcType: "2",
OwnerUnitId: 911,
ExcludeTest: true,
StartDate: '',
EndDate: "",
MembershipType: "",
MembershipLevel: "",
MembershipTarget: "",
type: "encryption"
}
const data = await request.$posPost(
"MemberApi/Member/GetMembershipCount",
req
);
let list = data.Result_Data.List
console.log('会员等级分布', list);
let res = []
if (list && list.length > 0) {
list.forEach((item) => {
if (item.value > 0) {
res.push(item)
}
})
}
this.memberLevels = this.sortData(res)
},
formatNumber(num) { formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}, },
@ -408,7 +505,9 @@ export default {
background: linear-gradient(135deg, @primary-color, @secondary-color); background: linear-gradient(135deg, @primary-color, @secondary-color);
color: white; color: white;
.metric-title, .metric-unit, .metric-trend { .metric-title,
.metric-unit,
.metric-trend {
color: rgba(255, 255, 255, 0.9); color: rgba(255, 255, 255, 0.9);
} }
@ -436,17 +535,41 @@ export default {
&.total { &.total {
background: linear-gradient(135deg, @primary-color, @secondary-color); background: linear-gradient(135deg, @primary-color, @secondary-color);
&::after { content: '👥'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 24rpx; }
&::after {
content: '👥';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24rpx;
}
} }
&.active { &.active {
background: linear-gradient(135deg, @success-color, #73d13d); background: linear-gradient(135deg, @success-color, #73d13d);
&::after { content: '✨'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 24rpx; }
&::after {
content: '✨';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24rpx;
}
} }
&.new { &.new {
background: linear-gradient(135deg, @warning-color, #ffc53d); background: linear-gradient(135deg, @warning-color, #ffc53d);
&::after { content: '🆕'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 24rpx; }
&::after {
content: '🆕';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24rpx;
}
} }
} }
@ -525,7 +648,7 @@ export default {
animation: pulse 2s infinite; animation: pulse 2s infinite;
} }
} }
.growth-chart-container { .growth-chart-container {
margin-bottom: 24rpx; margin-bottom: 24rpx;
@ -566,27 +689,11 @@ export default {
border-radius: 12rpx; border-radius: 12rpx;
font-size: 22rpx; font-size: 22rpx;
font-weight: 500; font-weight: 500;
color: white; color: #010c13;
background: linear-gradient(135deg, #b9f2ff, #69c0ff);
&.normal {
background: @text-secondary;
}
&.silver {
background: @silver;
color: #333;
}
&.gold {
background: @gold;
color: #333;
}
&.diamond {
background: linear-gradient(135deg, #b9f2ff, #69c0ff);
}
} }
.level-count { .level-count {
font-size: 26rpx; font-size: 26rpx;
font-weight: 600; font-weight: 600;
@ -666,7 +773,7 @@ export default {
} }
.activity-count { .activity-count {
font-size: 28rpx; font-size: 24rpx;
font-weight: 600; font-weight: 600;
color: @text-primary; color: @text-primary;
margin-bottom: 4rpx; margin-bottom: 4rpx;
@ -674,7 +781,7 @@ export default {
} }
.activity-percent { .activity-percent {
font-size: 20rpx; font-size: 24rpx;
color: @text-secondary; color: @text-secondary;
} }
} }
@ -853,9 +960,11 @@ export default {
0% { 0% {
opacity: 1; opacity: 1;
} }
50% { 50% {
opacity: 0.7; opacity: 0.7;
} }
100% { 100% {
opacity: 1; opacity: 1;
} }

View File

@ -260,11 +260,7 @@ export default {
}, },
xAxis: { xAxis: {
disableGrid: true, disableGrid: true,
type: 'category', type: 'category'
data: Array.from({ length: 30 }, (_, index) => index + 1),
format: function (val) {
return (val % 5 === 0) ? val : '';
}
}, },
yAxis: { yAxis: {
gridType: 'dash', gridType: 'dash',
@ -312,14 +308,17 @@ export default {
let orderData = [] // let orderData = [] //
let amountData = [] // let amountData = [] //
if (list && list.length > 0) { if (list && list.length > 0) {
list.forEach((item) => { list.forEach((item, index) => {
categories.push(item.StatisticsDate.split('/')[2]) const day = parseInt(item.StatisticsDate.split('/')[2])
if (day % 5 === 0 || day === 1) {
categories.push(day + '日')
}
orderData.push(item.TicketCount) orderData.push(item.TicketCount)
amountData.push(item.SellAmount) amountData.push(item.SellAmount)
}) })
} }
this.trendData = { this.trendData = {
categories: orderData, categories: categories,
orderData: orderData, orderData: orderData,
amountData: amountData amountData: amountData
} }

View File

@ -182,7 +182,7 @@ export default {
}, },
data() { data() {
return { return {
activeTab: 0, activeTab: 1,
tabList: [ tabList: [
{ name: '经营数据分析', key: 'business' }, { name: '经营数据分析', key: 'business' },
{ name: '供应链数据分析', key: 'supply' }, { name: '供应链数据分析', key: 'supply' },

View File

@ -6,6 +6,7 @@ export default {
mpUrl: 'https://eshangtech.com:18998/Coop.Merchant/Handler/handler_ajax.ashx', // 接口 mpUrl: 'https://eshangtech.com:18998/Coop.Merchant/Handler/handler_ajax.ashx', // 接口
apiurl: 'https://eshangtech.com:18900/', // web api正式接口地址 apiurl: 'https://eshangtech.com:18900/', // web api正式接口地址
posApiurl: 'https://pos.eshangtech.com/', // web api正式接口地址
// EshangUrl: 'https://eshangtech.com/', // EshangUrl: 'https://eshangtech.com/',
// apiurl: 'https://erysfeipeng.oicp.net/', // web api // apiurl: 'https://erysfeipeng.oicp.net/', // web api
testApiurl: 'http://dev.eshangtech.com:8001/', // web api测试接口地址 testApiurl: 'http://dev.eshangtech.com:8001/', // web api测试接口地址

View File

@ -31,6 +31,9 @@ export default {
data.action_type = controller data.action_type = controller
return this.post(data) return this.post(data)
}, },
$posPost: function (controller, data) {
return Api.request('POST', ApiPath.posApiurl + controller, data || {}, true)
},
$webGet: function (controller, data) { // webapi $webGet: function (controller, data) { // webapi
return Api.request('GET', ApiPath.apiurl + controller, data || {}, true) return Api.request('GET', ApiPath.apiurl + controller, data || {}, true)
}, },