浏览代码

销售业绩

zhuangyunsheng 1 月之前
父节点
当前提交
e68bf018a7

+ 13 - 0
src/api/model/sales.js

@@ -42,5 +42,18 @@ export default {
         del: async function (data = {}) {
             return await http.post(`${this.url}/remove`, data);
         }
+    },
+
+    performance: {
+        name: "销售业绩",
+        url: "/mes/salePerformance",
+        
+        census: async function () {
+            return await http.post(`${this.url}/getTotalPrice`);
+        },
+
+        echart: async function (data = {}) {
+            return await http.post(`${this.url}/getEcharts`, data);
+        }
     }
 }

+ 12 - 53
src/components/scEcharts/echarts-theme-T.js

@@ -1,5 +1,5 @@
 const T = {
-	"color": [
+	color: [
 		"#409EFF",
 		"#36CE9E",
 		"#edb00d",
@@ -12,59 +12,18 @@ const T = {
 		"#5470c6",
 		"#909399"
 	],
-	"grid": {
-		"left": "5%",
-		"right": "5%",
-		"bottom": "10",
-		"containLabel": true
+	grid: { left: "5%", right: "5%", bottom: 10, containLabel: true },
+	legend: { inactiveColor: "rgba(128, 128, 128, 0.4)" },
+	categoryAxis: {
+		axisLine: { show: true, lineStyle: { width: 1, color: "#5d5d5d" } }, // 坐标轴轴线
+		axisTick: { show: false, lineStyle: { color: "#5d5d5d" } }, // 坐标轴刻度
+		splitLine: { show: false, lineStyle: { color: ["#efefef"] } }, // 坐标轴在 grid 区域中的分隔线
+		splitArea: { show: false, areaStyle: { color: ["rgba(255, 255, 255, .01)", "rgba(0, 0, 0, .01)"] } }
 	},
-	"legend": {
-		"inactiveColor": "rgba(128, 128, 128, 0.4)"
-	},
-	"categoryAxis": {
-		"axisLine": {//坐标轴轴线
-			"show": true,
-			"lineStyle": {
-				"color": "#d3d2d3",
-				"width": 1
-			}
-		},
-		"axisTick": {//坐标轴刻度
-			"show": false,
-			"lineStyle": {
-				"color": "#5d5d5d"
-			}
-		},
-		"splitLine": {//坐标轴在 grid 区域中的分隔线
-			"show": false, // 默认数值轴显示,类目轴不显示。
-			"lineStyle": {
-				"color": ["#efefef"]
-			}
-		},
-		"splitArea": {
-			"show": false,
-			"areaStyle": {
-				"color": [
-					"rgba(255,255,255,0.01)",
-					"rgba(0,0,0,0.01)"
-				]
-			}
-		}
-	},
-	"valueAxis": {
-		"axisLine": {
-			"show": false,
-			"lineStyle": {
-				"color": "#5d5d5d"
-			}
-		},
-		"splitLine": {
-			"show": true,
-			"lineStyle": {
-				"color": "rgba(128,128,128,0.2)"
-			}
-		},
+	valueAxis: {
+		axisLine: { show: false, lineStyle: { color: "#5d5d5d" } },
+		splitLine: { show: true, lineStyle: { color: "rgba(128, 128, 128, .2)" } }
 	}
 }
 
-export default T
+export default T

+ 62 - 55
src/components/scEcharts/index.vue

@@ -3,59 +3,66 @@
 </template>
 
 <script>
-	import * as echarts from "echarts";
-	import T from "./echarts-theme-T.js";
-	echarts.registerTheme("T", T);
-	const unwarp = (obj) => obj && (obj.__v_raw || obj.valueOf() || obj);
-
-	export default {
-		...echarts,
-		name: "scEcharts",
-		props: {
-			height: { type: String, default: "100%" },
-			width: { type: String, default: "100%" },
-			nodata: { type: Boolean, default: false },
-			option: { type: Object, default: () => ({}) },
-			clearCache: { type: Boolean, default: false }
-		},
-		data() {
-			return {
-				isActivat: false,
-				myChart: null
-			}
-		},
-		watch: {
-			option: {
-				deep: true,
-				handler(v) {
-					if (this.clearCache) unwarp(this.myChart).clear();
-					unwarp(this.myChart).setOption(v);
-				}
-			}
-		},
-		computed: {
-			myOptions: function() {
-				return this.option || {};
-			}
-		},
-		activated() {
-			if (!this.isActivat) nextTick(() => this.myChart.resize());
-		},
-		deactivated() {
-			this.isActivat = false;
-		},
-		mounted(){
-			this.isActivat = true;
-			nextTick(() => this.draw());
-		},
-		methods: {
-			draw() {
-				const myChart = echarts.init(this.$refs.scEcharts, "T");
-				myChart.setOption(this.myOptions);
-				myChart.on("click", "series.bar", params => this.$emit("chartClick", params));
-				this.myChart = myChart;
-				window.addEventListener("resize", () => myChart.resize());
-			}
-		}
-	}
+import * as echarts from "echarts";
+import T from "./echarts-theme-T.js";
+echarts.registerTheme("T", T);
+const unwarp = obj => obj && (obj.__v_raw || obj.valueOf() || obj);
+
+export default {
+    ...echarts,
+    name: "scEcharts",
+    props: {
+        height: { type: String, default: "100%" },
+        width: { type: String, default: "100%" },
+        nodata: { type: Boolean, default: false },
+        option: { type: Object, default: () => ({}) },
+        clearCache: { type: Boolean, default: false }
+    },
+    
+    data() {
+        return {
+            isActivat: false,
+            myChart: null
+        }
+    },
+
+    watch: {
+        option: {
+            deep: true,
+            handler(v) {
+                if (this.clearCache) unwarp(this.myChart).clear();
+                unwarp(this.myChart).setOption(v);
+            }
+        }
+    },
+
+    computed: {
+        myOptions: function() {
+            return this.option || {};
+        }
+    },
+
+    activated() {
+        if (!this.isActivat) nextTick(() => this.myChart.resize());
+    },
+
+    deactivated() {
+        this.isActivat = false;
+    },
+
+    mounted() {
+        this.isActivat = true;
+        nextTick(() => this.draw());
+    },
+
+    methods: {
+        draw() {
+            const myChart = echarts.init(this.$refs.scEcharts, "T");
+            myChart.setOption(this.myOptions);
+            myChart.on("click", "series.bar", params => this.$emit("chartClick", params));
+            this.myChart = myChart;
+            window.addEventListener("resize", () => myChart.resize());
+        }
+    }
+}
 </script>

+ 19 - 2
src/components/scFormTable/index.vue

@@ -100,8 +100,25 @@ const gridOptions = reactive({
 
     showFooter: computed(() => props.footerField.length > 0 && props.modelValue.length > 0),
     footerRowClassName: "vxe-table-footer-cell-required",
-    mergeFooterItems: props.mergeFooterItems,
-    footerMethod() {
+    footerSpanMethod: ({ rowIndex, itemIndex, column }) => { // ?评估阶段
+        let rowspan = 0, colspan = 0;
+        const mergeItems = XEUtils.find(props.mergeFooterItems, item => item.row == rowIndex && item.col == itemIndex);
+        
+        if (mergeItems) {
+            rowspan = mergeItems.rowspan;
+            colspan = mergeItems.colspan;
+        } else {
+            // 超过合并的列
+            const overColIndex = XEUtils.sum(props.mergeFooterItems, item => item.colspan);
+
+            if (XEUtils.includes(XEUtils.get(props.footerField, rowIndex), column.field) || itemIndex >= overColIndex) {
+                rowspan = 1;
+                colspan = 1;
+            }
+        }
+        return { rowspan, colspan }
+    },
+    footerMethod() { // ?评估阶段
         return XEUtils.map(props.footerField, (fields, fieldsIndex) => {
             return XEUtils.filter(XEUtils.toTreeArray(props.columns), column => !column.children).map((column, index) => {
                 if (index === 0) return props.footerTitle[fieldsIndex] || "合计:";

+ 5 - 0
src/utils/tool.js

@@ -173,6 +173,11 @@ tool.capitalizeWords = function (str) {
 	});
 }
 
+/* 金额格式化 */
+tool.amountFormat = function (num) {
+	return num >= 10000 ? `${XEUtils.divide(num, 10000)}万元` : num > 0 ? `${num}元` : num;
+}
+
 /* 常用加解密 */
 tool.crypto = {
 	//MD5加密

+ 2 - 2
src/views/sales/order/index.vue

@@ -70,7 +70,7 @@ const formConfig = reactive({
 });
 
 const paramsColums = reactive([
-    { column: "orderBy", defaultValue: "code_asc" },
+    { column: "orderBy", defaultValue: "orderDate_asc" },
     { column: "codeLike" },
     { column: "status" },
     { column: "customerId" },
@@ -88,7 +88,7 @@ const columns = reactive([
     { type: "html", field: "contractNo", title: "合同编号", minWidth: 150, sortable: true },
     { type: "html", field: "managerName", title: "业务员", minWidth: 150, sortable: true },
     { visible: false, type: "html", field: "freePrice", title: "整单折扣额", minWidth: 120, sortable: true },
-    { type: "html", field: "actualPrice", title: "成交金额", minWidth: 120, sortable: true },
+    { type: "html", field: "actualPrice", title: "成交金额", minWidth: 120, sortable: true, formatter: ({ cellValue }) => TOOL.amountFormat(cellValue) || cellValue },
     { type: "html", field: "planReceiveDate", title: "预计交期", minWidth: 120, sortable: true },
     { visible: false, type: "html", field: "deliveryDate", title: "实际交期", minWidth: 120, sortable: true },
     { visible: false, type: "html", field: "actualReceiveDate", title: "收货日期", minWidth: 120, sortable: true },

+ 44 - 0
src/views/sales/performance/components/bar.vue

@@ -0,0 +1,44 @@
+<template>
+    <scEcharts :option="option"></scEcharts>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+import API from "@/api";
+
+const option = reactive({
+    title: { text: "近7日实际销售额", textStyle: { fontSize: 16 } },
+    grid: { top: 75 },
+    tooltip: {
+        trigger: "axis",
+        confine: true,
+        axisPointer: { type: "cross", crossStyle: { color: "#999" } }
+    },
+    xAxis: {
+        type: "category",
+        data: Array.from({ length: 7 }, (_, i) => moment().subtract(i, "day").format("MM-DD")).reverse(),
+        axisPointer: { label: { backgroundColor: "#d3d2d3" } }
+    },
+    yAxis: {
+        type: "value",
+        name: "单位:元",
+        min: 0,
+        max: computed(() => XEUtils.max(option.series[0].data) || 100),
+        nameTextStyle: { color: "#5d5d5d" },
+        axisLabel: { color: "#5d5d5d" },
+        axisTick: { show: false },
+        splitLine: { lineStyle: { type: "dashed", color: ["#efefef"] } } // grid区域中的分隔线 数值轴显示,类目轴不显示
+    },
+    series: [{
+        type: "bar",
+        name: "销售额",
+        data: new Array(7).fill(0),
+        animationDuration: 2500,
+        animationDurationUpdate: 2500
+    }]
+});
+
+const getEcharts = () => API.sales.performance.echart({ type: "day", beginDate: moment().startOf("month").format("YYYY-MM-DD"), endDate: moment().format("YYYY-MM-DD") }).then(res => option.series[0].data = Array.from({ length: 7 }, (_, i) => XEUtils.get(XEUtils.find(res.actualList, item => item.date == moment().subtract(i, "day").format("YYYY-MM-DD")), "price", 0)).reverse()).catch(() => option.series[0].data = new Array(7).fill(0));
+getEcharts();
+</script>

+ 12 - 0
src/views/sales/performance/components/index.js

@@ -0,0 +1,12 @@
+import {markRaw} from 'vue';
+const resultComps = {}
+let requireComponent = require.context(
+	'./', // 在当前目录下查找
+	false, // 不遍历子文件夹
+	/\.vue$/ // 正则匹配 以 .vue结尾的文件
+)
+requireComponent.keys().forEach(fileName => {
+	let comp = requireComponent(fileName)
+	resultComps[fileName.replace(/^\.\/(.*)\.\w+$/, '$1')] = comp.default
+})
+export default markRaw(resultComps)

+ 108 - 0
src/views/sales/performance/components/line.vue

@@ -0,0 +1,108 @@
+<template>
+    <el-header>
+        <el-radio-group v-model="query.type" @change="radioChange">
+            <el-radio-button v-for="(item, key) in radioDic" :key="key" :label="key">{{ item.label }}</el-radio-button>
+        </el-radio-group>
+        
+        <el-date-picker v-if="query.type == 'year'" v-model="query.year" type="yearrange" :clearable="false" range-separator="至" @change="getEcharts" />
+        <el-date-picker v-if="query.type == 'month'" v-model="query.month" type="monthrange" :clearable="false" range-separator="至" @change="getEcharts" />
+        <el-date-picker v-if="query.type == 'day'" v-model="query.day" type="daterange" :clearable="false" range-separator="至" @change="getEcharts" />
+    </el-header>
+
+    <div class="echart-panel">
+        <scEcharts clearCache :option="option"></scEcharts>
+    </div>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import { radioDic, lineSeriesDefault } from "../main";
+
+const query = reactive({
+    type: "year",
+    year: [moment().subtract(5, "year"), moment()],
+    month: [moment().startOf("year").format("YYYY-MM-DD"), moment().endOf("year").format("YYYY-MM-DD")],
+    day: [moment().startOf("month").format("YYYY-MM-DD"), moment().endOf("month").format("YYYY-MM-DD")],
+});
+
+const radioChange = (e) => {
+    option.series = []
+    getEcharts()
+}
+
+const option = reactive({
+    color: computed(() => query.type == "day" ? ["#36CE9E"] : []),
+    title: { text: "计划/实际销售额趋势", textStyle: { fontSize: 16 }, top: 15 },
+    legend: { left: "center", top: 55 },
+    grid: { top: 120 },
+    tooltip: {
+        trigger: "axis",
+        confine: true,
+        axisPointer: { type: "cross", crossStyle: { color: "#999" } }
+    },
+    xAxis: {
+        type: "category",
+        data: [],
+        axisLabel: { formatter: value => query.type == "day" ? moment(value).format("MM-DD") : value },
+        axisPointer: { label: { backgroundColor: "#d3d2d3" } }
+    },
+    yAxis: {
+        type: "value",
+        name: "单位:元",
+        min: 0,
+        max: computed(() => XEUtils.max(XEUtils.flatten(XEUtils.map(option.series, item => item.data))) || 100),
+        axisTick: { show: false },
+        splitLine: { lineStyle: { type: "dashed", color: ["#efefef"] } } // grid区域中的分隔线 数值轴显示,类目轴不显示
+    },
+    series: []
+});
+
+const getEcharts = () => {
+    const [beginDate, endDate] = radioDic[query.type].valueFormat(query[query.type]);
+    option.xAxis.data = radioDic[query.type].generateXAxis(beginDate, endDate);
+
+    API.sales.performance.echart({ type: query.type, beginDate, endDate }).then(res => {
+        if (query.type == "day") {
+            option.series = [{
+                ...lineSeriesDefault[1],
+                data: XEUtils.map(option.xAxis.data, date => XEUtils.get(XEUtils.find(res.actualList, item => item.date == date), "price", 0)),
+            }];
+        } else {
+            option.series = [{
+                ...lineSeriesDefault[0],
+                data: XEUtils.map(option.xAxis.data, date => XEUtils.get(XEUtils.find(res.planList, item => item.date2 == date), "price", 0)),
+            }, {
+                ...lineSeriesDefault[1],
+                data: XEUtils.map(option.xAxis.data, date => XEUtils.get(XEUtils.find(res.actualList, item => item.date2 == date), "price", 0)),
+            }];
+        }
+    }).catch(() => {
+        if (query.type == "day") {
+            option.series = [{
+                ...lineSeriesDefault[1],
+                data: new Array(option.xAxis.data.length).fill(0)
+            }];
+        } else {
+            option.series = [{
+                ...lineSeriesDefault[0],
+                data: new Array(option.xAxis.data.length).fill(0),
+            }, {
+                ...lineSeriesDefault[1],
+                data: new Array(option.xAxis.data.length).fill(0),
+            }];
+        }
+    });
+}
+
+getEcharts();
+</script>
+
+<style scoped>
+.el-header {height: fit-content;padding: 0;border: none;}
+.el-header :deep(.el-radio-group) {flex-wrap: nowrap;}
+.el-header :deep(.el-date-editor.el-range-editor) {flex-grow: 0;width: 260px;}
+.echart-panel {flex: 1;}
+</style>

+ 38 - 0
src/views/sales/performance/components/pie.vue

@@ -0,0 +1,38 @@
+<template>
+    <scEcharts :option="option"></scEcharts>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+import API from "@/api";
+
+const option = reactive({
+    color: ["#409EFF", "#36CE9E", "#edb00d","#fc8452"],
+    title: { text: "本年各季度计划/实际销售额占比", textStyle: { fontSize: 16 } },
+    tooltip: { confine: true },
+    series: [{
+        type: "pie",
+        center: ["50%", "56%"],
+        tooltip: { formatter: ({ name, marker, value, data: { plan } }) => `${marker + name}<br />计划完成率<span style="padding-left: 20px;">${XEUtils.commafy(XEUtils.divide(value, plan), { digits: 2 })}%</span>` },
+        data: Array.from({ length: 4 }, (_, i) => ({ name: `Q${i + 1}`, value: 0, plan: 0 })),
+        animationDuration: 2500,
+        animationDurationUpdate: 2500
+    }]
+});
+
+const getEcharts = () => {
+    API.sales.performance.echart({ type: "quarter", beginDate: moment().startOf("year").format("YYYY-MM-DD"), endDate: moment().endOf("year").format("YYYY-MM-DD") }).then(res => {
+        option.series[0].data = Array.from({ length: 4 }, (_, i) => {
+            const planData = XEUtils.find(res.planList, item => item.date2 == i + 1);
+            const actualData = XEUtils.find(res.actualList, item => item.date2 == i + 1);
+            return {
+                name: `Q${i + 1}`,
+                value: XEUtils.get(actualData, "price", 0),
+                plan: XEUtils.get(planData, "price", 0)
+            }
+        });
+    }).catch(() => option.series[0].data = Array.from({ length: 4 }, (_, i) => ({ name: `Q${i + 1}`, value: 0, plan: 0 })));
+}
+getEcharts();
+</script>

+ 24 - 0
src/views/sales/performance/components/statistic.vue

@@ -0,0 +1,24 @@
+<template>
+    <el-statistic :style="{ '--el-statistic-content-color': color }" :title="title" :value="formatValue">
+        <template #suffix>{{ formatSuffix }}</template>
+    </el-statistic>
+</template>
+
+<script setup>
+import XEUtils from "xe-utils";
+
+const props = defineProps({
+    title: { type: String, default: "" },
+    value: { type: Number, default: 0 },
+    color: { type: String, default: "var(--el-color-primary)" },
+});
+
+const formatValue = computed(() => props.value >= 10000 ? XEUtils.round(XEUtils.divide(props.value, 10000)) : XEUtils.round(props.value));
+const formatSuffix = computed(() => props.value >= 10000 ? "万元" : "元");
+</script>
+
+<style scoped>
+.el-statistic :deep(.el-statistic__head) {margin-bottom: 10px;font-size: 15px;font-weight: 500;color: #555;}
+.el-statistic :deep(.el-statistic__number) {font-size: 22px;}
+.el-statistic :deep(.el-statistic__suffix) {font-size: 12px;color: #999;}
+</style>

+ 77 - 0
src/views/sales/performance/index.vue

@@ -0,0 +1,77 @@
+<template>
+	<el-container class="is-vertical">
+        <sc-page-header></sc-page-header>
+        
+        <el-container class="performance-container">
+            <el-main class="nopadding">
+                <el-row :gutter="15">
+                    <el-col :md="12" :xs="24">
+                        <el-card class="statistics-panel">
+                            <component :is="allComps['statistic']" title="本年计划金额" :value="census.planSalePriceByYear"></component>
+                            <component :is="allComps['statistic']" title="本年实际金额" :value="census.actualSalePriceByYear"></component>
+                        </el-card>
+                    </el-col>
+                    <el-col :md="12" :xs="24">
+                        <el-card class="statistics-panel">
+                            <component :is="allComps['statistic']" title="本月计划金额" color="#fc8452" :value="census.planSalePriceByMonth"></component>
+                            <component :is="allComps['statistic']" title="本月实际金额" color="#fc8452" :value="census.actualSalePriceByMonth"></component>
+                        </el-card>
+                    </el-col>
+                </el-row>
+
+                <el-card class="echart-card">
+                    <component :is="allComps['line']"></component>
+                </el-card>
+            </el-main>
+
+            <el-aside width="36%">
+                <el-card>
+                    <component :is="allComps['pie']"></component>
+                </el-card>
+
+                <el-card>
+                    <component :is="allComps['bar']"></component>
+                </el-card>
+            </el-aside>
+	    </el-container>
+	</el-container>
+</template>
+
+<script setup>
+import XEUtils from "xe-utils";
+import API from "@/api";
+import allComps from "./components";
+
+const census = reactive({
+    planSalePriceByYear: 0,
+    planSalePriceByMonth: 0,
+    actualSalePriceByYear: 0,
+    actualSalePriceByMonth: 0
+});
+
+const getCensus = () => API.sales.performance.census().then(res => XEUtils.objectEach(census, (_, key) => XEUtils.set(census, key, XEUtils.get(res, key) || 0))).catch(() => XEUtils.objectEach(census, (_, key) => XEUtils.set(census, key, 0)));
+getCensus();
+</script>
+
+<style lang="scss" scoped>
+.performance-container {padding: 0 15px 15px;background: #fff;}
+.performance-container .el-main.nopadding {display: flex;flex-direction: column;overflow: hidden;}
+.performance-container .el-card :deep(.el-card__body) {height: 100%;}
+
+.performance-container .statistics-panel :deep(.el-card__body) {display: flex;justify-content: space-evenly;align-items: center;padding: 20px 0;}
+.performance-container .echart-card {flex: 1;}
+.performance-container .echart-card :deep(.el-card__body) {display: flex;flex-direction: column;}
+
+.performance-container .el-aside {display: flex;flex-direction: column;justify-content: space-between;margin-left: 15px;border-right: none;}
+.performance-container .el-aside .el-card {height: calc((100% - 15px) / 2);}
+
+@media (max-width: 992px) {
+    .aminui-main > .el-container > .el-container.performance-container {display: block;margin-top: 0;border: none;}
+    .performance-container .el-aside {width: 100%;margin-top: 15px;margin-left: 0;}
+    .performance-container .echart-card {height: 500px;flex: unset;}
+    .performance-container .el-aside .el-card {height: 300px;}
+    .performance-container .el-aside .el-card + .el-card {margin-top: 15px;}
+    // .user-container .el-aside .el-main {padding-bottom: 15px;}
+    // .user-container .el-aside + .el-main {margin-top: 15px;}
+}
+</style>

+ 76 - 0
src/views/sales/performance/main.js

@@ -0,0 +1,76 @@
+import moment from "moment";
+
+export const radioDic = {
+    year: {
+        label: "年度",
+        valueFormat: value => [moment(value[0]).format("YYYY-01-01"), moment(value[1]).format("YYYY-12-31")],
+        generateXAxis: (begin, end) => {
+            const list = [];
+            for (let year = moment(begin).year(); year <= moment(end).year(); year++) {
+                list.push(year);
+            }
+            return list;
+        }
+    },
+    // quarter: {
+    //     label: "季度",
+    // },
+    month: {
+        label: "月度",
+        valueFormat: value => [moment(value[0]).format("YYYY-MM-01"), moment(value[1]).endOf("month").format("YYYY-MM-DD")],
+        generateXAxis: (begin, end) => {
+            const list = [];
+            let current = moment(begin);
+            while (current.isSameOrBefore(end)) {
+                list.push(current.format("YYYY-MM"));
+                current.add(1, "month");
+            }
+            return list;
+        }
+    },
+    day: {
+        label: "天数",
+        valueFormat: value => [moment(value[0]).format("YYYY-MM-DD"), moment(value[1]).format("YYYY-MM-DD")],
+        generateXAxis: (begin, end) => {
+            const list = [];
+            let current = moment(begin);
+            while (current.isSameOrBefore(end)) {
+                list.push(current.format("YYYY-MM-DD"));
+                current.add(1, "day");
+            }
+            return list;
+        }
+    }
+}
+
+export const lineSeriesDefault = [{
+    type: "line",
+    name: "计划销售额",
+    areaStyle: {
+        origin: "start",
+        color: {
+            type: "linear", x: 0, y: 0, x2: 0, y2: 1,
+            colorStops: [
+                { offset: 0, color: "rgba(64, 158, 255, 1)" },
+                { offset: 1, color: "rgba(64, 158, 255, 0)" }
+            ]
+        }
+    },
+    animationDuration: 2500,
+    animationDurationUpdate: 2500
+}, {
+    type: "line",
+    name: "实际销售额",
+    areaStyle: {
+        origin: "start",
+        color: {
+            type: "linear", x: 0, y: 0, x2: 0, y2: 1,
+            colorStops: [
+                { offset: 0, color: "rgba(54, 206, 158, 1)" },
+                { offset: 1, color: "rgba(54, 206, 158, 0)" }
+            ]
+        }
+    },
+    animationDuration: 2500,
+    animationDurationUpdate: 2500
+}]

+ 3 - 3
src/views/sales/plan/detail.vue

@@ -29,7 +29,7 @@
                         <el-col v-if="form.type == 'quarter'" :md="8" :xs="24">
                             <el-form-item label="季度" prop="quarter">
                                 <el-select v-model="form.quarter" :clearable="false" placeholder="请选择计划季度">
-                                    <el-option v-for="item in 4" :key="item" :label="`第${item}季度`" :value="item" :disabled="quarterDisabled(item)"></el-option>
+                                    <el-option v-for="item in 4" :key="item" :label="`Q${item}`" :value="item" :disabled="quarterDisabled(item)"></el-option>
                                 </el-select>
                             </el-form-item>
                         </el-col>
@@ -43,7 +43,7 @@
                         <el-col :md="8" :xs="24">
                             <el-form-item label="销售金额" prop="saleAmount">
                                 <el-input-number v-model="form.saleAmount" :min="1" :precision="2" :controls="false" placeholder="请输入计划销售金额">
-                                    <template #suffix></template>
+                                    <template #suffix></template>
                                 </el-input-number>
                             </el-form-item>
                         </el-col>
@@ -175,7 +175,7 @@ const submit = () => {
                 ElMessage.success("操作成功");
                 isSaving.value = false;
                 visible.value = false;
-                $emit("success", mode.value);
+                $emit("success");
             }).catch(() => isSaving.value = false);
         } else {
             return false;

+ 4 - 3
src/views/sales/plan/index.vue

@@ -67,7 +67,7 @@ const formConfig = reactive({
 
 const options = reactive({
     paramsColums: [
-        { column: "orderBy", defaultValue: "code_asc" },
+        { column: "orderBy", defaultValue: "beginDate_asc" },
         { column: "nameLike" },
         { column: "codeLike" },
         { column: "type" },
@@ -84,7 +84,8 @@ const options = reactive({
         { field: "status", title: "计划状态", minWidth: 120, editRender: { name: "$cell-tag", options: salesDic.planStatus, formatter: row => formatStatus(row) } },
         { type: "html", field: "beginDate", title: "计划开始日期", minWidth: 150, sortable: true },
         { type: "html", field: "endDate", title: "计划结束日期", minWidth: 150, sortable: true },
-        { type: "html", field: "saleAmount", title: "计划销售金额(万)", minWidth: 150, sortable: true },
+        { type: "html", field: "saleAmount", title: "计划销售金额", minWidth: 150, sortable: true, formatter: ({ cellValue }) => TOOL.amountFormat(cellValue) || cellValue },
+        { type: "html", field: "totalActualPrice", title: "实际销售金额", minWidth: 150, sortable: true, formatter: ({ cellValue }) => TOOL.amountFormat(cellValue) || cellValue },
         { type: "html", field: "createTime", title: "创建日期", minWidth: 120, sortable: true, formatter: ({ cellValue }) => TOOL.dateFormat(cellValue, "YYYY-MM-DD") || cellValue },
         { visible: false, type: "html", field: "remark", title: "概要", minWidth: 300, sortable: true },
         { title: "操作", fixed: "right", width: 220, slots: { default: "action" } }
@@ -93,7 +94,7 @@ const options = reactive({
 
 // 显示隐藏 筛选表单
 const xGridTable = ref();
-const refreshTable = (mode = "add") => xGridTable.value.searchData(mode);
+const refreshTable = () => xGridTable.value.searchData();
 
 const planRef = ref();
 const dialog = ref(false);