Răsfoiți Sursa

统计优化

shish 13 ore în urmă
părinte
comite
4ad700ca43

+ 2 - 0
ghs/src/cool/request/index.js

@@ -36,6 +36,7 @@ import ptItemClass from '@/service/pt-item-class/index';
 import ghsItemClass from '@/service/ghs-item-class/index';
 import statBook from '@/service/stat-book/index';
 import statItem from '@/service/stat-item/index';
+import statKd from '@/service/stat-kd/index';
 import statCg from '@/service/stat-cg/index';
 import stat from '@/service/stat/index';
 import ghs from '@/service/ghs/index';
@@ -121,6 +122,7 @@ export function SET_SERVICE({ store }) {
 		ghsDebtRecord,
 		checkOrder,
 		statCg,
+		statKd,
 		statItem,
 		modify,
 		customBalanceChange,

+ 3 - 1
ghs/src/mock/userInfo.js

@@ -197,7 +197,9 @@ let menu = {
 		{id: '11',parentId: null,name: '邀请',type: 0,icon: 'iconyaoqingyouli',orderNum: 11,router: '/pages/admin/my-invite',viewPath: 'views/my-invite/index.vue',keepAlive: 1},
         {id: '1101',parentId: 11,name: '邀请注册',type: 1,icon: 'iconyaoqingyouli',orderNum: 1,router: '/pages/admin/my-invite',viewPath: 'views/my-invite/index.vue',keepAlive: 1},
 
-		{id: '12',parentId: null,name: '统计',type: 0,icon: 'icondingdanguanli',orderNum: 12,router: '/assets/list',viewPath: 'views/assets/list.vue',keepAlive: 1},
+		{id: '12',parentId: null,name: '统计',type: 0,icon: 'icondingdanguanli',orderNum: 12,router: '/assets/profit',viewPath: 'views/assets/profit.vue',keepAlive: 1},
+		{id: '1199',parentId: '12',name: '利润表',type: 1,icon: 'icondingdanguanli',orderNum: 0,router: '/assets/profit',viewPath: 'views/assets/profit.vue',keepAlive: 1},
+		{id: '1200',parentId: '12',name: '收入统计',type: 1,icon: 'icondingdanguanli',orderNum: 1,router: '/assets/kdIncome',viewPath: 'views/assets/kdIncome.vue',keepAlive: 1},
 		{id: '1201',parentId: '12',name: '余额明细',type: 1,icon: 'icondingdanguanli',orderNum: 12,router: '/assets/list',viewPath: 'views/assets/list.vue',keepAlive: 1},
 		{id: '1202',parentId: '12',name: '支出明细',type: 1,icon: 'icondingdanguanli',orderNum: 12,router: '/assets/expendList',viewPath: 'views/assets/expendList.vue',keepAlive: 1},
 		{id: '1203',parentId: '12',name: '收支分类',type: 1,icon: 'icondingdanguanli',orderNum: 12,router: '/assets/incomeClass',viewPath: 'views/assets/incomeClass.vue',keepAlive: 1},

+ 22 - 0
ghs/src/service/stat-kd/index.js

@@ -0,0 +1,22 @@
+import { BaseService, Service } from '@/cool';
+
+/**
+ * 收入统计:对应 StatKdController::actionProfile / StatKdClass::eachChannelIncome。
+ * timeType=bill 按账单日 payTime(默认);=send 按发货日 sendTime。
+ */
+@Service('stat-kd')
+export class StatKdService extends BaseService {
+
+	/**
+	 * 各渠道收入汇总。
+	 * 入参含 searchTime/startTime/endTime/timeType;返回 list、totalIncome、payCodeIncome、staffAmountList。
+	 */
+	getProfile(data) {
+		return this.request({
+			url: '/profile',
+			params: data
+		});
+	}
+
+}
+export default new StatKdService();

+ 11 - 0
ghs/src/service/stat/index.js

@@ -9,6 +9,17 @@ export class StatService extends BaseService {
 		});
 	}
 
+	/**
+	 * 经营利润总表:对应 StatController::actionProfit / StatSaleClass::profile
+	 * timeType=bill 记账(销售/支出 payTime、采购 entryTime);=send 均按 sendTime
+	 */
+	getProfit(data) {
+		return this.request({
+			url: '/profit',
+			params: data
+		});
+	}
+
 	/**
 	 * 收支分类:对应 StatSaleClass::classProfile
 	 * timeType=bill 记账日 / send 发货日

+ 256 - 0
ghs/src/views/assets/kdIncome.vue

@@ -0,0 +1,256 @@
+<!--
+  PC 收入统计:与小程序 kdIncome 同源 /stat-kd/profile(StatKdClass::eachChannelIncome)。
+  timeType 切换账单时间(bill=payTime) / 发货时间(send=sendTime);明细弹窗看渠道分类和员工收款。
+-->
+<template>
+	<div class="shop-list">
+		<x-crud @load="onLoad">
+			<template #slot-date-select>
+				<el-button-group style="margin-right:12px;">
+					<el-button size="mini" :type="timeType === 'bill' ? 'primary' : 'default'" @click="changeTimeType('bill')">账单时间</el-button>
+					<el-button size="mini" :type="timeType === 'send' ? 'primary' : 'default'" @click="changeTimeType('send')">发货时间</el-button>
+				</el-button-group>
+				<el-date-picker v-model="currentDateList" type="daterange" align="right" unlink-panels range-separator="至" start-placeholder="开始日期" @change="getDate" value-format="yyyy-MM-dd"
+					end-placeholder="结束日期" :picker-options="pickerOptions" size="mini"> </el-date-picker>
+			</template>
+
+			<template #slot-column-option="{scope}">
+				<el-button size="mini" type="primary" @click="getDetail(scope.row)">明细</el-button>
+			</template>
+
+			<template #table-column-amount="{scope}">
+				<span>{{ formatNum(scope.row.amount) }}</span>
+			</template>
+
+			<template #table-column-num="{scope}">
+				<span>{{ formatNum(scope.row.num) }}</span>
+			</template>
+
+			<template #slot-total-amount>
+				<div class="income-total-wrap">
+					<div class="income-total-nums">
+						<span style="font-size:15px;font-weight:bold;">合计 ¥{{ formatNum(totalIncome) }}</span>
+						<span v-if="Number(payCodeIncome) > 0" style="font-size:15px;font-weight:bold;margin-left:15px;color:#e74c3c;">以上还要减掉重复登记的收款码收入 ¥{{ formatNum(payCodeIncome) }}</span>
+					</div>
+					<div v-if="staffAmountList.length" class="staff-list">
+						<div v-for="(it, idx) in staffAmountList" :key="idx" class="staff-row">
+							<span>{{ it.staffName || '未命名' }}</span>
+							<span>数量 {{ formatNum(it.num) }}</span>
+							<span>¥{{ formatNum(it.amount) }}</span>
+						</div>
+					</div>
+				</div>
+			</template>
+		</x-crud>
+
+		<el-dialog title="明细" :visible.sync="showDetail" width="560px">
+			<el-table v-if="detailClassList.length" :data="detailClassList" border size="mini" style="margin-bottom:16px;">
+				<el-table-column prop="name" label="员工" align="center"></el-table-column>
+				<el-table-column label="笔数" align="center">
+					<template slot-scope="scope">{{ formatNum(scope.row.num) }}笔</template>
+				</el-table-column>
+				<el-table-column label="金额" align="center">
+					<template slot-scope="scope">¥{{ formatNum(scope.row.amount) }}</template>
+				</el-table-column>
+			</el-table>
+			<el-table v-if="detailCategoryList.length" :data="detailCategoryList" border size="mini">
+				<el-table-column prop="name" label="分类" align="center"></el-table-column>
+				<el-table-column label="笔数" align="center">
+					<template slot-scope="scope">{{ formatNum(scope.row.num) }}笔</template>
+				</el-table-column>
+				<el-table-column label="金额" align="center">
+					<template slot-scope="scope">¥{{ formatNum(scope.row.amount) }}</template>
+				</el-table-column>
+			</el-table>
+			<div v-if="!detailClassList.length && !detailCategoryList.length" style="color:#999;text-align:center;">暂无明细</div>
+		</el-dialog>
+	</div>
+</template>
+<script>
+import { mapGetters } from 'vuex';
+export default {
+	data() {
+		return {
+			crud: null,
+			pickerOptions: {
+				shortcuts: [{
+					text: '最近一周',
+					onClick(picker) {
+						const end = new Date();
+						const start = new Date();
+						start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
+						picker.$emit('pick', [start, end]);
+					}
+				}, {
+					text: '最近一个月',
+					onClick(picker) {
+						const end = new Date();
+						const start = new Date();
+						start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
+						picker.$emit('pick', [start, end]);
+					}
+				}, {
+					text: '最近三个月',
+					onClick(picker) {
+						const end = new Date();
+						const start = new Date();
+						start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
+						picker.$emit('pick', [start, end]);
+					}
+				}]
+			},
+			currentDateList: '',
+			startTime: '',
+			endTime: '',
+			searchTime: '',
+			// bill=账单日 payTime(默认) / send=发货 sendTime
+			timeType: 'bill',
+			totalIncome: 0,
+			payCodeIncome: 0,
+			staffAmountList: [],
+			showDetail: false,
+			detailClassList: [],
+			detailCategoryList: []
+		}
+	},
+	computed: {
+		...mapGetters(['shopInfo']),
+		/** 当前筛选口径文案 */
+		timeTypeName() {
+			return this.timeType === 'send' ? '发货时间' : '账单时间'
+		},
+	},
+	methods: {
+		/** 数字展示:不用 truthy,避免 0 被当成空 */
+		formatNum(val) {
+			const n = parseFloat(val);
+			return isNaN(n) ? 0 : n;
+		},
+		/** 对象转数组,接口 list/class/category 可能是关联数组 */
+		toList(raw) {
+			if (!raw) {
+				return []
+			}
+			if (Array.isArray(raw)) {
+				return raw
+			}
+			return Object.keys(raw).map((k) => raw[k])
+		},
+		/** 明细只展示有笔数或金额的行,避免空分类占位 */
+		filterDetailRows(raw) {
+			return this.toList(raw).filter((row) => {
+				return this.formatNum(row.num) > 0 || this.formatNum(row.amount) !== 0
+			})
+		},
+		/** 与小程序 kdIncome 对齐的查询参数 */
+		queryParams(extra) {
+			return {
+				searchTime: this.searchTime,
+				startTime: this.startTime,
+				endTime: this.endTime,
+				timeType: this.timeType,
+				contain: 0,
+				...(extra || {})
+			};
+		},
+		/** 切换账单时间 / 发货时间并刷新 */
+		changeTimeType(type) {
+			if (this.timeType === type) {
+				return
+			}
+			this.timeType = type
+			if (this.app) {
+				this.app.refresh(this.queryParams())
+			}
+		},
+		/** 打开渠道明细:员工收款 + 批发/零售等分类 */
+		getDetail(row) {
+			this.detailClassList = this.filterDetailRows(row.class)
+			this.detailCategoryList = this.filterDetailRows(row.category)
+			this.showDetail = true
+		},
+		onLoad({ ctx, app }) {
+			this.app = app;
+			ctx.service({
+				page: () => {
+					return this.$service.statKd.getProfile(this.queryParams()).then((res) => {
+						this.totalIncome = this.formatNum(res.totalIncome)
+						this.payCodeIncome = this.formatNum(res.payCodeIncome)
+						this.staffAmountList = this.toList(res.staffAmountList)
+						return {
+							list: this.toList(res.list)
+						};
+					});
+				}
+			})
+			.set('table', {
+				columns: [
+					{
+						label: '渠道',
+						prop: 'name',
+						align: 'center',
+					},
+					{
+						label: '笔数',
+						prop: 'num',
+						align: 'center',
+					},
+					{
+						label: '金额',
+						prop: 'amount',
+						align: 'center',
+					},
+				],
+				op: {
+					visible: true,
+					props: {
+						width: 120,
+						align: 'center',
+						fixed: 'right',
+						label: '操作'
+					},
+					layout: ['slot-column-option']
+				}
+			})
+			.set('layout', [
+				['slot-date-select', 'flex1'],
+				['data-table'],
+				['slot-total-amount', 'flex1']
+			])
+			.done();
+			app.refresh();
+		},
+		getDate() {
+			this.searchTime = 'custom'
+			this.startTime = this.currentDateList[0]
+			this.endTime = this.currentDateList[1]
+			this.app.refresh(this.queryParams());
+		}
+	},
+};
+</script>
+
+<style lang="stylus" scoped>
+.shop-list {
+  height: 100%;
+}
+.income-total-wrap {
+  display: block;
+  width: 100%;
+}
+.income-total-nums {
+  display: block;
+  line-height: 1.6;
+}
+.staff-list {
+  margin-top: 12px;
+}
+.staff-row {
+  font-size: 13px;
+  color: #333;
+  margin-top: 6px;
+}
+.staff-row span {
+  margin-right: 16px;
+}
+</style>

+ 205 - 0
ghs/src/views/assets/profit.vue

@@ -0,0 +1,205 @@
+<!--
+  PC 利润表:与小程序 profit 同源 /stat/profit(StatSaleClass::profile)。
+  timeType 切换记账时间(bill) / 发货时间(send);收入含采购跨天退货,支出金额前加负号。
+-->
+<template>
+	<div class="shop-list">
+		<x-crud @load="onLoad">
+			<template #slot-date-select>
+				<el-button-group style="margin-right:12px;">
+					<el-button size="mini" :type="timeType === 'bill' ? 'primary' : 'default'" @click="changeTimeType('bill')">记账时间</el-button>
+					<el-button size="mini" :type="timeType === 'send' ? 'primary' : 'default'" @click="changeTimeType('send')">发货时间</el-button>
+				</el-button-group>
+				<el-date-picker v-model="currentDateList" type="daterange" align="right" unlink-panels range-separator="至" start-placeholder="开始日期" @change="getDate" value-format="yyyy-MM-dd"
+					end-placeholder="结束日期" :picker-options="pickerOptions" size="mini"> </el-date-picker>
+			</template>
+
+			<template #table-column-name="{scope}">
+				<span :style="rowNameStyle(scope.row)">{{ scope.row.name }}</span>
+			</template>
+
+			<template #table-column-amount="{scope}">
+				<span :style="rowNameStyle(scope.row)">{{ formatAmount(scope.row) }}</span>
+			</template>
+
+			<template #slot-total-amount>
+				<div class="profit-total-wrap">
+					<div class="profit-total-nums">
+						<span style="font-size:15px;font-weight:bold;">利润 ¥{{ formatNum(balance) }}</span>
+						<span v-if="Number(wastagePrice) > 0" style="font-size:15px;font-weight:bold;margin-left:15px;">花材报损 成本¥{{ formatNum(wastageCost) }} 售价¥{{ formatNum(wastagePrice) }}</span>
+					</div>
+				</div>
+			</template>
+		</x-crud>
+	</div>
+</template>
+<script>
+import { mapGetters } from 'vuex';
+export default {
+	data() {
+		return {
+			crud: null,
+			pickerOptions: {
+				shortcuts: [{
+					text: '最近一周',
+					onClick(picker) {
+						const end = new Date();
+						const start = new Date();
+						start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
+						picker.$emit('pick', [start, end]);
+					}
+				}, {
+					text: '最近一个月',
+					onClick(picker) {
+						const end = new Date();
+						const start = new Date();
+						start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
+						picker.$emit('pick', [start, end]);
+					}
+				}, {
+					text: '最近三个月',
+					onClick(picker) {
+						const end = new Date();
+						const start = new Date();
+						start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
+						picker.$emit('pick', [start, end]);
+					}
+				}]
+			},
+			currentDateList: '',
+			startTime: '',
+			endTime: '',
+			searchTime: '',
+			// bill=记账(销售/支出 payTime、采购 entryTime) / send=发货 sendTime
+			timeType: 'bill',
+			balance: 0,
+			wastagePrice: 0,
+			wastageCost: 0
+		}
+	},
+	computed: {
+		...mapGetters(['shopInfo']),
+	},
+	methods: {
+		/** 数字展示:不用 truthy,避免 0 被当成空 */
+		formatNum(val) {
+			const n = parseFloat(val);
+			return isNaN(n) ? 0 : n;
+		},
+		/** 支出行加负号,与小程序利润表一致 */
+		formatAmount(row) {
+			const n = this.formatNum(row.amount)
+			if (row.kind === 'expend') {
+				return n === 0 ? 0 : ('-' + n)
+			}
+			return n
+		},
+		/** 采购跨天退货标橙,突出通过日冲回 */
+		rowNameStyle(row) {
+			return row.id === 'cgNextDay' ? 'color:#e67e22;font-weight:bold;' : ''
+		},
+		/** 与小程序 profit 对齐的查询参数 */
+		queryParams(extra) {
+			return {
+				searchTime: this.searchTime,
+				startTime: this.startTime,
+				endTime: this.endTime,
+				timeType: this.timeType,
+				...(extra || {})
+			};
+		},
+		/** 切换记账时间 / 发货时间并刷新 */
+		changeTimeType(type) {
+			if (this.timeType === type) {
+				return
+			}
+			this.timeType = type
+			if (this.app) {
+				this.app.refresh(this.queryParams())
+			}
+		},
+		/** 收入非 0 展示(可为负);支出只展示大于 0 的行 */
+		buildRows(res) {
+			const income = Array.isArray(res.income) ? res.income : []
+			const expend = Array.isArray(res.expend) ? res.expend : []
+			const rows = []
+			income.forEach((item) => {
+				if (this.formatNum(item.amount) !== 0) {
+					rows.push({ ...item, kind: 'income' })
+				}
+			})
+			expend.forEach((item) => {
+				if (this.formatNum(item.amount) > 0) {
+					rows.push({ ...item, kind: 'expend' })
+				}
+			})
+			return rows
+		},
+		onLoad({ ctx, app }) {
+			this.app = app;
+			ctx.service({
+				page: () => {
+					return this.$service.stat.getProfit(this.queryParams()).then((res) => {
+						this.balance = this.formatNum(res.balance)
+						this.wastagePrice = this.formatNum(res.wastagePrice)
+						this.wastageCost = this.formatNum(res.wastageCost)
+						return {
+							list: this.buildRows(res)
+						};
+					});
+				}
+			})
+			.set('table', {
+				columns: [
+					{
+						label: '类型',
+						prop: 'name',
+						align: 'center',
+					},
+					{
+						label: '金额',
+						prop: 'amount',
+						align: 'center',
+					},
+				],
+				op: {
+					visible: false,
+					props: {
+						width: 120,
+						align: 'center',
+						label: '操作'
+					},
+					layout: []
+				}
+			})
+			.set('layout', [
+				['slot-date-select', 'flex1'],
+				['data-table'],
+				['slot-total-amount', 'flex1']
+			])
+			.done();
+			app.refresh();
+		},
+		getDate() {
+			this.searchTime = 'custom'
+			this.startTime = this.currentDateList[0]
+			this.endTime = this.currentDateList[1]
+			this.app.refresh(this.queryParams());
+		}
+	},
+};
+</script>
+
+<style lang="stylus" scoped>
+.shop-list {
+  height: 100%;
+}
+.profit-total-wrap {
+  display: block;
+  width: 100%;
+}
+.profit-total-nums {
+  display: block;
+  line-height: 1.6;
+}
+</style>