فهرست منبع

feat: 交接班前端+收入报表+菜单+多语言——接班/交班/按日汇总

lq 1 ماه پیش
والد
کامیت
9fd5ffd617

+ 3 - 0
frontend/src/api/report.js

@@ -0,0 +1,3 @@
+import service from '@/utils/request'
+
+export const getRevenueReport = (params) => service({ url: '/report/revenue', method: 'get', params })

+ 6 - 0
frontend/src/api/shift.js

@@ -0,0 +1,6 @@
+import service from '@/utils/request'
+
+export const startShift = (data) => service({ url: '/shift/start', method: 'post', data })
+export const endShift = (data) => service({ url: '/shift/end', method: 'post', data })
+export const getCurrentShift = () => service({ url: '/shift/current', method: 'get' })
+export const getShiftList = (params) => service({ url: '/shift/list', method: 'get', params })

+ 2 - 0
frontend/src/lang/en.js

@@ -224,4 +224,6 @@ export default {
   insufficientAmount: 'Insufficient amount',
   paymentRecord: 'Payment Record',
   monthlyCard: 'Monthly Card',
+  shiftRecord: 'Shift Record',
+  revenueReport: 'Revenue Report',
 }

+ 2 - 0
frontend/src/lang/zh-CN.js

@@ -224,4 +224,6 @@ export default {
   insufficientAmount: '实收金额不足',
   paymentRecord: '收费记录',
   monthlyCard: '包月车管理',
+  shiftRecord: '交接班管理',
+  revenueReport: '收入报表',
 }

+ 73 - 0
frontend/src/view/parking/shiftRecord.vue

@@ -0,0 +1,73 @@
+<template>
+  <div>
+    <el-card v-if="current" class="active-shift">
+      <template #header>当班中</template>
+      <el-descriptions :column="2" border>
+        <el-descriptions-item label="接班时间">{{ current.start_time }}</el-descriptions-item>
+        <el-descriptions-item label="接班现金">¥{{ current.starting_cash.toFixed(2) }}</el-descriptions-item>
+        <el-descriptions-item label="当班收款">¥{{ (current.collected_cash || 0).toFixed(2) }}</el-descriptions-item>
+        <el-descriptions-item label="应交总额"><b>¥{{ ((current.starting_cash || 0) + (current.collected_cash || 0)).toFixed(2) }}</b></el-descriptions-item>
+      </el-descriptions>
+      <div style="margin-top:16px">
+        <el-input-number v-model="endForm.actual_cash" :min="0" :precision="2" placeholder="实交现金" style="width:200px" />
+        <el-button type="danger" style="margin-left:12px" @click="doEnd">交班</el-button>
+      </div>
+    </el-card>
+
+    <el-card v-else>
+      <el-form inline>
+        <el-form-item label="接班现金">
+          <el-input-number v-model="startForm.starting_cash" :min="0" :precision="2" style="width:200px" />
+        </el-form-item>
+        <el-form-item><el-button type="primary" @click="doStart">接班</el-button></el-form-item>
+      </el-form>
+    </el-card>
+
+    <el-table :data="list" border style="margin-top:16px">
+      <el-table-column label="接班时间" prop="start_time" width="170" />
+      <el-table-column label="交班时间" prop="end_time" width="170" />
+      <el-table-column label="接班现金" width="100"><template #default="{row}">¥{{ row.starting_cash.toFixed(2) }}</template></el-table-column>
+      <el-table-column label="当班收款" width="100"><template #default="{row}">¥{{ (row.collected_cash || 0).toFixed(2) }}</template></el-table-column>
+      <el-table-column label="应交" width="100"><template #default="{row}">¥{{ row.expected_total.toFixed(2) }}</template></el-table-column>
+      <el-table-column label="实交" width="100"><template #default="{row}">¥{{ row.actual_total.toFixed(2) }}</template></el-table-column>
+      <el-table-column label="差异" width="100">
+        <template #default="{row}">
+          <span :style="{color: row.difference < 0 ? '#f56c6c' : '#67c23a'}">¥{{ row.difference.toFixed(2) }}</span>
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from 'vue'
+import { ElMessage } from 'element-plus'
+import { startShift, endShift, getCurrentShift, getShiftList } from '@/api/shift'
+
+const current = ref(null), list = ref([])
+const startForm = reactive({ starting_cash: 0 })
+const endForm = reactive({ actual_cash: 0 })
+
+async function fetch() {
+  const r = await getCurrentShift()
+  current.value = r.code === 0 ? r.data : null
+  const l = await getShiftList({ page: 1, page_size: 20 })
+  if (l.code === 0) { list.value = l.data.list || []; list.value.forEach(s => s.collected_cash = s.collected_cash || 0) }
+}
+
+async function doStart() {
+  const r = await startShift(startForm)
+  if (r.code === 0) { ElMessage.success('接班成功'); fetch() } else ElMessage.error(r.msg)
+}
+
+async function doEnd() {
+  const r = await endShift(endForm)
+  if (r.code === 0) { ElMessage.success(`交班完成,差异¥${r.data.difference.toFixed(2)}`); fetch() } else ElMessage.error(r.msg)
+}
+
+onMounted(fetch)
+</script>
+
+<style scoped>
+.active-shift { border-left: 4px solid #67c23a; }
+</style>

+ 57 - 0
frontend/src/view/report/revenue.vue

@@ -0,0 +1,57 @@
+<template>
+  <div>
+    <el-form inline>
+      <el-form-item label="日期">
+        <el-date-picker v-model="dateRange" type="daterange" start-placeholder="开始" end-placeholder="结束" value-format="YYYY-MM-DD" />
+      </el-form-item>
+      <el-form-item label="停车场">
+        <el-select v-model="lotId" clearable placeholder="全部" style="width:180px">
+          <el-option v-for="l in lotList" :key="l.ID" :label="l.lot_name" :value="l.ID" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="fetch">{{ $t.value.Search }}</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-table :data="list" border show-summary :summary-method="summaries">
+      <el-table-column label="日期" prop="date" width="120" />
+      <el-table-column label="停车场" prop="parking_lot" width="140" />
+      <el-table-column label="现金" width="100"><template #default="{row}">¥{{ row.cash.toFixed(2) }}</template></el-table-column>
+      <el-table-column label="免密" width="100"><template #default="{row}">¥{{ row.free.toFixed(2) }}</template></el-table-column>
+      <el-table-column label="总收入" width="120"><template #default="{row}">¥{{ row.total.toFixed(2) }}</template></el-table-column>
+      <el-table-column label="笔数" prop="count" width="80" />
+    </el-table>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { getRevenueReport } from '@/api/report'
+import { obtainPullOverList } from '@/api/pullOver'
+
+const dateRange = ref([]), lotId = ref(null), list = ref([]), lotList = ref([])
+
+function summaries({ columns, data }) {
+  const sums = { 2: 0, 3: 0, 4: 0, 5: 0 }
+  data.forEach(r => { sums[2] += r.cash; sums[3] += r.free; sums[4] += r.total; sums[5] += r.count })
+  return columns.map((col, i) => {
+    if (i < 2) return i === 0 ? '合计' : ''
+    return i === 5 ? sums[i] : '¥' + sums[i].toFixed(2)
+  })
+}
+
+async function fetch() {
+  const [s, e] = dateRange.value && dateRange.value.length === 2 ? dateRange.value : ['', '']
+  const r = await getRevenueReport({ start_date: s, end_date: e, parking_lot_id: lotId.value || '' })
+  if (r.code === 0) list.value = r.data || []
+}
+
+onMounted(async () => {
+  const today = new Date().toISOString().slice(0, 10)
+  dateRange.value = [today, today]
+  const l = await obtainPullOverList({ page: 1, pageSize: 100 })
+  if (l.code === 0) lotList.value = l.data?.list || []
+  fetch()
+})
+</script>

+ 17 - 0
internal/initialize/seed.go

@@ -122,11 +122,13 @@ func SeedSystemData() {
 		{MenuLevel: 0, ParentId: 20, Path: "pricing", Name: "pricing", Hidden: false, Component: "view/parking/pricing.vue", Sort: 4, Meta: dao.Meta{Title: "收费配置", Icon: "office-building"}},
 		{MenuLevel: 0, ParentId: 20, Path: "entryExit", Name: "entryExit", Hidden: false, Component: "view/parking/entryExit.vue", Sort: 5, Meta: dao.Meta{Title: "进出场操作", Icon: "switch"}},
 		{MenuLevel: 0, ParentId: 20, Path: "monthlyCard", Name: "monthlyCard", Hidden: false, Component: "view/parking/monthlyCard.vue", Sort: 6, Meta: dao.Meta{Title: "包月车管理", Icon: "calendar"}},
+		{MenuLevel: 0, ParentId: 20, Path: "shiftRecord", Name: "shiftRecord", Hidden: false, Component: "view/parking/shiftRecord.vue", Sort: 7, Meta: dao.Meta{Title: "交接班管理", Icon: "clock"}},
 		// 报表查询 (ID=26)
 		{MenuLevel: 0, ParentId: 0, Path: "report", Name: "report", Hidden: false, Component: "", Sort: 3, Meta: dao.Meta{Title: "报表查询", Icon: "data-analysis"}},
 		{MenuLevel: 0, ParentId: 25, Path: "enter", Name: "enter", Hidden: false, Component: "view/report/enter.vue", Sort: 1, Meta: dao.Meta{Title: "进出记录", Icon: "sort"}},
 		{MenuLevel: 0, ParentId: 25, Path: "sheet", Name: "sheet", Hidden: false, Component: "view/report/sheet.vue", Sort: 2, Meta: dao.Meta{Title: "异常报表", Icon: "document-copy"}},
 		{MenuLevel: 0, ParentId: 25, Path: "paymentRecord", Name: "paymentRecord", Hidden: false, Component: "view/report/paymentRecord.vue", Sort: 3, Meta: dao.Meta{Title: "收费记录", Icon: "money"}},
+		{MenuLevel: 0, ParentId: 25, Path: "revenue", Name: "revenue", Hidden: false, Component: "view/report/revenue.vue", Sort: 4, Meta: dao.Meta{Title: "收入报表", Icon: "trend-charts"}},
 		// 统计查询 (ID=28)
 		{MenuLevel: 0, ParentId: 0, Path: "statistics", Name: "statistics", Hidden: false, Component: "", Sort: 4, Meta: dao.Meta{Title: "统计查询", Icon: "chat-line-round"}},
 		{MenuLevel: 0, ParentId: 28, Path: "vehicle", Name: "vehicle", Hidden: false, Component: "view/statistics/vehicle.vue", Sort: 1, Meta: dao.Meta{Title: "在线车辆统计", Icon: "van"}},
@@ -180,6 +182,9 @@ func SeedSystemData() {
 		{MenuId: "31", AuthorityId: "618"}, {MenuId: "31", AuthorityId: "888"}, {MenuId: "31", AuthorityId: "9527"},
 		// 包月车管理 (33) — admin + operator
 		{MenuId: "33", AuthorityId: "618"}, {MenuId: "33", AuthorityId: "888"}, {MenuId: "33", AuthorityId: "9527"},
+		// 交接班 (34) / 收入报表 (35) — admin + operator
+		{MenuId: "34", AuthorityId: "618"}, {MenuId: "34", AuthorityId: "888"}, {MenuId: "34", AuthorityId: "9527"},
+		{MenuId: "35", AuthorityId: "618"}, {MenuId: "35", AuthorityId: "888"}, {MenuId: "35", AuthorityId: "9527"},
 	}
 	db.Create(&authMenus)
 		// --- vehicle_type (默认车辆类型:临时车,标记为系统内置) ---
@@ -427,6 +432,18 @@ func SeedSystemData() {
 		{"p", "618", "/monthly-card/refund", "DELETE", "", "", ""},
 		{"p", "888", "/monthly-card/list", "GET", "", "", ""},
 		{"p", "618", "/monthly-card/list", "GET", "", "", ""},
+		// 交接班
+		{"p", "888", "/shift/start", "POST", "", "", ""},
+		{"p", "618", "/shift/start", "POST", "", "", ""},
+		{"p", "888", "/shift/end", "POST", "", "", ""},
+		{"p", "618", "/shift/end", "POST", "", "", ""},
+		{"p", "888", "/shift/current", "GET", "", "", ""},
+		{"p", "618", "/shift/current", "GET", "", "", ""},
+		{"p", "888", "/shift/list", "GET", "", "", ""},
+		{"p", "618", "/shift/list", "GET", "", "", ""},
+		// 收入报表
+		{"p", "888", "/report/revenue", "GET", "", "", ""},
+		{"p", "618", "/report/revenue", "GET", "", "", ""},
 		// 收费记录查询
 		{"p", "888", "/payment/list", "GET", "", "", ""},
 		{"p", "618", "/payment/list", "GET", "", "", ""},