Files
intc-vue-h5/src/pages/statistic/bill/futuresAnalysis/index.vue

974 lines
27 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="container">
<u-sticky offsetTop="0rpx"
customNavHeight="0rpx">
<view class="search-view">
<u-input v-model="queryParams.accountName" border="false" type="select" @click="handleAccount" placeholder="请选择期货账户" suffixIcon="search"
suffixIconStyle="color: #909399" class="search-input">
</u-input>
</view>
<view class="search-view">
<u-input v-model="queryParams.time" border="false" type="select" readonly suffixIcon="calendar"
suffixIconStyle="color: #909399" class="search-input">
</u-input>
<view class="filter-btn" @click="filterPanel = !filterPanel">
<uni-icons type="list" size="18" color="#667eea"></uni-icons>
<text>筛选</text>
</view>
</view>
<div class="app-container">
<div class="header-con" ref="searchHeightRef">
<div class="item">
<view class="item-icon" style="background: linear-gradient(135deg, #5b51d8 0%, #6b21a8 100%);">
<uni-icons type="wallet" size="24" color="#ffffff"></uni-icons>
</view>
<div class="info-sum">
<div class="title">累计收益</div>
<div class="num">{{ futuresStock.accumulateIncome }}<span></span></div>
</div>
</div>
<div class="item">
<view class="item-icon" style="background: linear-gradient(135deg, #c026d3 0%, #dc2626 100%);">
<uni-icons type="bars" size="24" color="#ffffff"></uni-icons>
</view>
<div class="info-sum">
<div class="title">平均收益</div>
<div class="num">{{ futuresStock.averageIncome }}<span></span></div>
</div>
</div>
</div>
<div class="header-con" ref="searchHeightRef">
<div class="item">
<view class="item-icon" style="background: linear-gradient(135deg, #0284c7 0%, #0891b2 100%);">
<uni-icons type="arrow-up" size="24" color="#ffffff"></uni-icons>
</view>
<div class="info-sum">
<div class="title">最大收益</div>
<div class="num">{{ futuresStock.maxRevenue}}<span></span></div>
</div>
</div>
<div class="item">
<view class="item-icon" style="background: linear-gradient(135deg, #be123c 0%, #9f1239 100%);">
<uni-icons type="arrow-down" size="24" color="#ffffff"></uni-icons>
</view>
<div class="info-sum">
<div class="title">最大亏损</div>
<div class="num">{{ futuresStock.maxLoss }}<span></span></div>
</div>
</div>
</div>
</div>
<view class="section-title" v-show="listData.length>0">
<view class="title-decorator"></view>
<text class="title-text">期货统计分析</text>
<view class="view-switch">
<view
:class="['switch-item', { 'active': viewMode === 'list' }]"
@click="viewMode = 'list'"
>
<uni-icons type="list" size="16" :color="viewMode === 'list' ? '#667eea' : '#999'"></uni-icons>
<text>列表</text>
</view>
<view
:class="['switch-item', { 'active': viewMode === 'line' }]"
@click="viewMode = 'line'"
>
<uni-icons type="loop" size="16" :color="viewMode === 'line' ? '#667eea' : '#999'"></uni-icons>
<text>曲线图</text>
</view>
<view
:class="['switch-item', { 'active': viewMode === 'column' }]"
@click="viewMode = 'column'"
>
<uni-icons type="bars" size="16" :color="viewMode === 'column' ? '#667eea' : '#999'"></uni-icons>
<text>柱状图</text>
</view>
</view>
</view>
</u-sticky>
<u-transition :show="filterPanel" mode="fade">
<view class="filter-panel" :style="{ height: `${windowHeight - 42}px` }">
<view class="filter-panel-content">
<view class="filter-title">统计日期</view>
<view class="selcet-content" style="padding: 0 24rpx">
<u-input
:disabled="true"
:disabledColor="'#fff'"
class="dateInput"
border="surround"
v-model="queryParams.startTime"
placeholder="请选择开始时间"
>
<template v-slot:suffix>
<u-icon name="calendar" @click="openOrCloseDate(true)"></u-icon>
</template>
</u-input>
<u-input
:disabled="true"
:disabledColor="'#fff'"
class="dateInput"
border="surround"
v-model="queryParams.endTime"
placeholder="请选择结束时间"
>
<template v-slot:suffix>
<u-icon name="calendar" @click="openOrCloseDate(false)"></u-icon>
</template>
</u-input>
</view>
</view>
<view class="btn-box">
<view class="btn-reset" @click="resetQuery()">
<uni-icons type="reload" size="16" color="#909399"></uni-icons>
<text>重置</text>
</view>
<view class="btn-confirm" @click="searchSubmit()">
<uni-icons type="checkmarkempty" size="16" color="#ffffff"></uni-icons>
<text>确定</text>
</view>
</view>
</view>
</u-transition>
<!-- 日期选择器 -->
<u-datetime-picker
:closeOnClickOverlay="true"
:show="timeShow"
v-model="time"
mode="year-month"
@close="openOrCloseDate"
@cancel="openOrCloseDate"
@confirm="confirm"
></u-datetime-picker>
<!-- 曲线图展示 -->
<view class="chart-container" v-if="listData.length>0 && viewMode === 'line'">
<qiun-data-charts
type="line"
canvasId="futuresAnalysisLineChart"
:chartData="chartData"
:opts="lineChartOpts"
:loadingType="1"
/>
</view>
<!-- 柱状图展示 -->
<view class="chart-container" v-if="listData.length>0 && viewMode === 'column'">
<qiun-data-charts
type="column"
canvasId="futuresAnalysisColumnChart"
:chartData="chartData"
:opts="columnChartOpts"
:loadingType="1"
/>
</view>
<!-- 列表展示 -->
<u-list :spaceHeight="700" lowerThreshold="100" v-show="viewMode === 'list'">
<u-list-item v-for="(item, index) in listData" :key="index">
<view class="list-item">
<view class="item-header">
<view class="header-left">
<view class="time-badge">
<uni-icons type="calendar" size="16" color="#ffffff"></uni-icons>
<text>{{ item.time }}</text>
</view>
</view>
<view class="header-right">
<text class="amount-label">收益</text>
<text class="amount-value">{{ item.value }}</text>
</view>
</view>
<view class="item-body">
<view class="detail-label">
<uni-icons type="list" size="14" color="#667eea"></uni-icons>
<text>收益明细</text>
</view>
<view class="detail-content">
<text>{{ formatMultiLineData(item.detail) || '-' }}</text>
</view>
</view>
</view>
</u-list-item>
<view>
</view>
</u-list>
<u-picker itemHeight="88" :show="settingPickShow" :columns="settingColumns" keyName="settingName"
@confirm="settingConfirm" @cancel="settingCancel"></u-picker>
<u-picker itemHeight="88" :show="showAccount" :columns="accountList" keyName="nameCode" @cancel="handleAccountCancel"
@confirm="handleAccountConfirm"></u-picker>
</view>
<!-- 悬停按钮返回统计分析-->
<statisticBtn></statisticBtn>
</template>
<script setup>
import { getFuturesStocksAnalysis } from '@/api/invest/statisticAnalysis'
import { listFutureStocks } from '@/api/invest/futureStocks'
import { getDicts } from '@/api/system/dict/data.js'
import dayjs from 'dayjs'
import { timeHandler } from '@/utils/common.ts'
import { listBankcardLend } from '@/api/invest/bankcardlend'
import {onLoad,onShow} from "@dcloudio/uni-app";
// 计算属性与监听属性是在vue中而非uniap中 需要注意!!!
import {reactive ,toRefs,ref,computed }from "vue";
const pageNum = ref(1)
const listData = ref([])
const isShow = ref(false)
const accountList = ref([])
const accountTypeList = ref([])
const settingPickShow = ref(false)
const settingColumns = ref([])
const showAccount = ref(false)
const timeShow= ref(false)
const time =ref( Number(new Date()))
const flag= ref(true)
const viewMode = ref('list') // 'list' 或 'line' 或 'column'
const futuresStock = ref({
maxRevenue: '',
maxLoss: '',
averageIncome: '',
accumulateIncome: '',
futuresStocksList: []
})
const data = reactive({
filterPanel: false,
queryFutureStocksListParams: {
pageNum: 1,
type: '1',
pageSize: 1000
},
queryParams: {
type: 2,
time: null,
dataType: '1',
id: null
}
})
const { filterPanel, queryFutureStocksListParams, queryParams} = toRefs(data)
// 曲线图配置
const lineChartOpts = computed(() => {
const dataCount = listData.value ? listData.value.length : 0
const showXAxisLabel = dataCount <= 10
// 计算数据范围以动态设置Y轴
let minValue = 0
let maxValue = 0
if (listData.value && listData.value.length > 0) {
const values = listData.value.map(item => parseFloat(item.value) || 0)
minValue = Math.min(0, ...values)
maxValue = Math.max(0, ...values)
// 给最小值和最大值留出20%的空间
const range = maxValue - minValue
minValue = minValue - range * 0.2
maxValue = maxValue + range * 0.2
}
return {
color: ['#667eea'],
padding: [15, 15, 30, 5],
enableScroll: false,
enableMarkLine: true,
dataLabel: false,
dataPointShape: true,
legend: {
show: false
},
xAxis: {
disableGrid: true,
boundaryGap: 'justify',
axisLine: false,
scrollShow: true,
itemCount: 5,
fontSize: showXAxisLabel ? 10 : 0,
fontColor: showXAxisLabel ? '#999999' : 'transparent',
rotateLabel: true,
rotateAngle: 30,
disabled: !showXAxisLabel
},
yAxis: {
gridType: 'dash',
dashLength: 4,
gridColor: '#EEEEEE',
splitNumber: 5,
min: minValue,
max: maxValue,
fontSize: 10,
fontColor: '#999999',
showTitle: false,
disabled: false,
format: (val) => {
const absVal = Math.abs(val)
if (absVal >= 10000) {
return (val / 10000).toFixed(1) + 'w'
}
return val.toFixed(0)
}
},
extra: {
line: {
type: 'curve',
width: 3,
activeType: 'hollow',
linearType: 'custom',
linearOpacity: 0.2,
onShadow: true,
animation: true
}
}
}
})
// 柱状图配置
const columnChartOpts = computed(() => {
const dataCount = listData.value ? listData.value.length : 0
const showXAxisLabel = dataCount <= 10
// 计算数据范围以动态设置Y轴
let minValue = 0
let maxValue = 0
if (listData.value && listData.value.length > 0) {
const values = listData.value.map(item => parseFloat(item.value) || 0)
const dataMin = Math.min(...values)
const dataMax = Math.max(...values)
// 确保包含0轴
minValue = Math.min(0, dataMin)
maxValue = Math.max(0, dataMax)
// 给数据留出更大的上下空间,特别是负数方向
const range = maxValue - minValue
if (range > 0) {
// 如果有负数,给底部留出更多空间
if (dataMin < 0) {
minValue = minValue - range * 0.3
maxValue = maxValue + range * 0.1
} else {
minValue = minValue - range * 0.1
maxValue = maxValue + range * 0.2
}
}
}
return {
color: ['#667eea'],
padding: [15, 15, 30, 5],
enableScroll: false,
dataLabel: false,
legend: {
show: false
},
xAxis: {
disableGrid: true,
boundaryGap: 'center',
axisLine: false,
scrollShow: true,
itemCount: 5,
fontSize: showXAxisLabel ? 10 : 0,
fontColor: showXAxisLabel ? '#999999' : 'transparent',
rotateLabel: true,
rotateAngle: 30,
disabled: !showXAxisLabel
},
yAxis: {
gridType: 'dash',
dashLength: 4,
gridColor: '#EEEEEE',
splitNumber: 5,
min: minValue,
max: maxValue,
fontSize: 10,
fontColor: '#999999',
showTitle: false,
disabled: false,
format: (val) => {
const absVal = Math.abs(val)
if (absVal >= 10000) {
return (val / 10000).toFixed(1) + 'w'
}
return val.toFixed(0)
}
},
extra: {
column: {
type: 'group',
width: 20,
activeBgColor: '#764ba2',
activeBgOpacity: 0.08,
linearType: 'none',
barBorderCircle: true,
seriesGap: 2
}
}
}
})
// 图表数据
const chartData = computed(() => {
if (!listData.value || listData.value.length === 0) {
return {
categories: [],
series: []
}
}
// 创建副本并反转数组,使时间从前往后排序
const reversedData = [...listData.value].reverse()
// 为每个数据点计算颜色和值
const dataWithColors = reversedData.map(item => {
const value = parseFloat(item.value)
const numValue = isNaN(value) ? 0 : value
return {
value: numValue,
color: numValue < 0 ? '#ef4444' : '#667eea' // 负数红色,正数蓝紫色
}
})
return {
categories: reversedData.map(item => item.time || ''),
series: [
{
name: '收益',
data: dataWithColors.map(item => item.value),
pointColor: dataWithColors.map(item => item.color)
}
]
}
})
const windowHeight = computed(() => {
uni.getSystemInfoSync().windowHeight - 50
})
onLoad(() => {
getDict()
getList()
});
onShow(() => {
if (isShow.value) {
listData.value=[]
getList()
isShow.value = false
}
});
function formatMultiLineData(data) {
if (data != null) {
return data.replace(/<br\/>/g, '\n')
}
}
function openOrCloseDate(data) {
timeShow.value = !timeShow.value
flag.value = data
}
function confirm(e) {
const date = timeHandler(new Date(e.value), '-', ':')
let formatValue = 'YYYY-MM'
dayjs(date).format(formatValue)
if (flag.value) {
queryParams.value.startTime = dayjs(date).format(formatValue)
} else {
queryParams.value.endTime = dayjs(date).format(formatValue)
}
timeShow.value = false
}
function searchSubmit() {
queryParams.value.time = queryParams.value.startTime+'-'+queryParams.value.endTime
pageNum.value = 1
listData.value = []
getList()
filterPanel.value = false
}
function resetQuery() {
queryParams.value.id = ''
queryParams.value.type = 2
queryParams.value.dataType = '1'
let formatValue = 'YYYY-MM'
const today = new Date()
const end = today.getFullYear() + '-' + ('0' + (today.getMonth() + 1)).slice(-2)
const start = dayjs(end).add(-12, 'months')
queryParams.value.startTime = dayjs(start).format(formatValue)
queryParams.value.endTime = dayjs(end).format(formatValue)
queryParams.value.time = dayjs(start).format(formatValue)+'-'+dayjs(end).format(formatValue)
queryParams.value.accountName = ''
}
function getList() {
getFuturesStocksAnalysis({...queryParams.value }).then(res => {
futuresStock.value = { ...res.data }
listData.value = listData.value.concat(res.data.tableFuturesStocksList)
}).catch(() => {
})
}
function getDict() {
let formatValue = 'YYYY-MM'
const today = new Date()
const end = today.getFullYear() + '-' + ('0' + (today.getMonth() + 1)).slice(-2)
const start = dayjs(end).add(-12, 'months')
queryParams.value.startTime = dayjs(start).format(formatValue)
queryParams.value.endTime = dayjs(end).format(formatValue)
queryParams.value.time = dayjs(start).format(formatValue)+'-'+dayjs(end).format(formatValue)
listFutureStocks(queryFutureStocksListParams.value).then((response) => {
accountList.value = [response.rows]
})
}
function settingConfirm(e) {
queryParams.value.settingId = e.value[0].settingId
queryParams.value.settingName = e.value[0].settingName
settingPickShow.value = false
}
function settingCancel() {
settingPickShow.value = false
}
function handleAccount() {
if (accountList.value[0].length === 0) {
proxy.$refs['uToast'].show({
message: '期货账户为空 ', type: 'warning'
})
} else {
showAccount.value = true
}
}
function handleAccountConfirm(e) {
queryParams.value.accountName = e.value[0].nameCode
queryParams.value.id= e.value[0].id
showAccount.value = false
pageNum.value = 1
listData.value = []
getList()
}
function handleAccountCancel() {
queryParams.value.accountName = ''
queryParams.value.id=''
showAccount.value = false
listData.value = []
getList()
filterPanel.value = false
}
</script>
<style lang="scss" scoped>
page {
height: 100%;
overflow: auto;
}
.app-container {
background-color: #f5f7fa;
padding: 8rpx 0;
.header-con {
width: calc(100% - 32rpx);
margin: 0 16rpx 12rpx 16rpx;
background-color: transparent;
display: flex;
gap: 12rpx;
.item {
flex: 1;
display: flex;
align-items: center;
padding: 24rpx 20rpx;
transition: all 0.3s ease;
border: 2rpx solid #e8edf3;
border-radius: 16rpx;
background: linear-gradient(135deg, #ffffff 0%, #f8f9fb 100%);
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
&:active {
transform: scale(0.98);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
}
.item-icon {
width: 72rpx;
height: 72rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.12);
flex-shrink: 0;
}
.info-sum {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
min-width: 0;
.title {
color: #7f8c8d;
margin-bottom: 8rpx;
font-size: 24rpx;
line-height: 1.4;
}
.num {
color: #2c3e50;
font-size: 32rpx;
font-weight: 600;
span {
font-size: 20rpx;
color: #95a5a6;
margin-left: 4rpx;
font-weight: normal;
}
}
}
}
}
}
.btnAdd {
width: 146rpx;
height: 56rpx;
line-height: 56rpx;
border-radius: 8rpx;
display:float;
text-align: center;
}
.section-title {
display: flex;
margin-top: -10rpx;
align-items: center;
padding: 24rpx 32rpx 16rpx;
background-color: #f5f7fa;
.title-decorator {
width: 6rpx;
height: 32rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 3rpx;
margin-right: 12rpx;
}
.title-text {
font-size: 28rpx;
font-weight: 600;
color: #2c3e50;
line-height: 1;
flex: 1;
}
.view-switch {
display: flex;
align-items: center;
gap: 4rpx;
background: #ffffff;
border-radius: 12rpx;
padding: 4rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
.switch-item {
display: flex;
align-items: center;
gap: 4rpx;
padding: 6rpx 12rpx;
border-radius: 8rpx;
transition: all 0.3s ease;
cursor: pointer;
text {
font-size: 22rpx;
color: #999999;
line-height: 1;
}
&.active {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
text {
color: #667eea;
font-weight: 600;
}
}
&:active {
transform: scale(0.95);
}
}
}
}
.chart-container {
margin: 16rpx 24rpx 60rpx;
padding: 16rpx;
background: linear-gradient(135deg, #ffffff 0%, #f8f9fb 100%);
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
width: calc(100% - 48rpx);
height: 800rpx;
overflow: visible;
position: relative;
}
.search-view {
padding: 12rpx 32rpx;
background-color: #ffffff;
display: flex;
align-items: center;
position: relative;
z-index: 100;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
.search-input {
background: rgba(102, 126, 234, 0.08);
color: #333333;
flex: 1;
margin-right: 16rpx;
border-radius: 24rpx;
border: 2rpx solid rgba(102, 126, 234, 0.3);
height: 66rpx !important;
display: flex;
align-items: center;
}
.filter-btn {
display: flex;
align-items: center;
gap: 6rpx;
padding: 12rpx 24rpx;
background: rgba(102, 126, 234, 0.08);
border-radius: 24rpx;
border: 2rpx solid rgba(102, 126, 234, 0.3);
transition: all 0.3s ease;
flex-shrink: 0;
&:active {
transform: scale(0.95);
background: rgba(102, 126, 234, 0.12);
}
text {
color: #667eea;
font-size: 28rpx;
font-weight: 600;
}
uni-icons {
color: #667eea;
}
}
}
.filter-panel {
width: 100%;
position: absolute;
left: 0;
top: 180rpx;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999;
.filter-panel-content {
background-color: #ffffff;
padding: 0 30rpx 30rpx;
border-radius: 16rpx 16rpx 0 0;
.filter-title {
color: #2c3e50;
font-size: 32rpx;
font-weight: 600;
padding: 32rpx 0 24rpx 20rpx;
position: relative;
display: flex;
align-items: center;
&::before {
content: '';
width: 6rpx;
height: 32rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 3rpx;
margin-right: 12rpx;
flex-shrink: 0;
}
}
.state-list {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
gap: 16rpx;
.state-item {
padding: 0 32rpx;
height: 68rpx;
border: 2rpx solid #e8edf3;
border-radius: 34rpx;
text-align: center;
line-height: 68rpx;
font-size: 28rpx;
color: #666666;
transition: all 0.3s ease;
background: #ffffff;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
&:active {
transform: scale(0.95);
}
}
.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: 2rpx solid transparent;
color: #ffffff;
box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.3);
font-weight: 600;
}
}
}
.btn-box {
display: flex;
gap: 20rpx;
padding: 24rpx 30rpx;
background-color: #fff;
box-shadow: 0rpx -10rpx 20rpx #EEEEEE;
.btn-reset,
.btn-confirm {
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
height: 88rpx;
border-radius: 12rpx;
font-size: 30rpx;
font-weight: 600;
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
opacity: 0.9;
}
text {
line-height: 1;
}
}
.btn-reset {
flex: 1;
background: #f5f7fa;
border: 2rpx solid #dcdfe6;
text {
color: #606266;
}
}
.btn-confirm {
flex: 1;
background: #667eea;
box-shadow: 0 2rpx 8rpx rgba(102, 126, 234, 0.2);
border: none;
text {
color: #ffffff;
}
}
}
}
.list-item {
margin: 0 24rpx 16rpx;
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
.item-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 24rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
.header-left {
flex: 1;
.time-badge {
display: inline-flex;
align-items: center;
gap: 6rpx;
padding: 4rpx 12rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 12rpx;
text {
font-size: 28rpx;
color: #ffffff;
font-weight: 600;
line-height: 1;
}
}
}
.header-right {
display: flex;
flex-direction: row;
align-items: center;
gap: 4rpx;
.amount-label {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.85);
line-height: 1;
}
.amount-value {
font-size: 28rpx;
font-weight: 700;
color: #ffffff;
line-height: 1;
}
}
}
.item-body {
padding: 24rpx;
.detail-label {
display: flex;
align-items: center;
gap: 6rpx;
margin-bottom: 12rpx;
text {
font-size: 26rpx;
color: #667eea;
font-weight: 600;
}
}
.detail-content {
padding: 16rpx;
background: #f8f9fb;
border-radius: 12rpx;
border-left: 4rpx solid #667eea;
text {
font-size: 26rpx;
color: #333333;
line-height: 1.8;
white-space: pre-wrap;
word-break: break-all;
}
}
}
}
</style>