Browse Source

feat: 包月车管理前端+菜单+多语言——办理/续费/退卡页面

lq 1 tháng trước cách đây
mục cha
commit
454e8c2c7f

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

@@ -0,0 +1,6 @@
+import service from '@/utils/request'
+
+export const createMonthlyCard = (data) => service({ url: '/monthly-card/create', method: 'post', data })
+export const renewMonthlyCard = (data) => service({ url: '/monthly-card/renew', method: 'post', data })
+export const refundMonthlyCard = (id) => service({ url: '/monthly-card/refund?id=' + id, method: 'delete' })
+export const getMonthlyCardList = (params) => service({ url: '/monthly-card/list', method: 'get', params })

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

@@ -223,4 +223,5 @@ export default {
   confirmPayAndExit: 'Confirm & Release',
   insufficientAmount: 'Insufficient amount',
   paymentRecord: 'Payment Record',
+  monthlyCard: 'Monthly Card',
 }

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

@@ -223,4 +223,5 @@ export default {
   confirmPayAndExit: '确认收款并放行',
   insufficientAmount: '实收金额不足',
   paymentRecord: '收费记录',
+  monthlyCard: '包月车管理',
 }

+ 1 - 0
frontend/src/lang/zh-HK.js

@@ -215,4 +215,5 @@ export default {
   rfidInput: '請輸入RFID標籤',
   exitRecordDetail: '進場: {entryTime} | 停留: {duration} | 費用: ¥{fee}',
   paymentRecord: '收費記錄',
+  monthlyCard: '包月車管理',
 }

+ 1 - 0
frontend/src/lang/zh-TW.js

@@ -215,4 +215,5 @@ export default {
   rfidInput: '請輸入RFID標籤',
   exitRecordDetail: '進場: {entryTime} | 停留: {duration} | 費用: ¥{fee}',
   paymentRecord: '收費記錄',
+  monthlyCard: '包月車管理',
 }

+ 147 - 0
frontend/src/view/parking/monthlyCard.vue

@@ -0,0 +1,147 @@
+<template>
+  <div>
+    <el-form inline>
+      <el-form-item :label="$t.value.LicensePlateNo">
+        <el-input v-model="queryForm.plate_number" :placeholder="$t.value.carPlateInput" clearable />
+      </el-form-item>
+      <el-form-item label="卡类型">
+        <el-select v-model="queryForm.card_type" clearable placeholder="全部" style="width:120px">
+          <el-option label="月卡" value="month" />
+          <el-option label="季卡" value="quarter" />
+          <el-option label="年卡" value="year" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="fetchList">{{ $t.value.Search }}</el-button>
+        <el-button @click="resetQuery">{{ $t.value.Reset }}</el-button>
+        <el-button type="success" @click="openCreate">{{ $t.value.new }}</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-table :data="list" height="605" border>
+      <el-table-column :label="$t.value.licensePlate" prop="plate_number" align="center" width="120" />
+      <el-table-column label="卡类型" align="center" width="100">
+        <template #default="{ row }">{{ typeLabel(row.card_type) }}</template>
+      </el-table-column>
+      <el-table-column label="生效日期" prop="start_date" align="center" width="120" />
+      <el-table-column label="到期日期" prop="end_date" align="center" width="120" />
+      <el-table-column label="金额" align="center" width="100">
+        <template #default="{ row }">¥{{ row.fee.toFixed(2) }}</template>
+      </el-table-column>
+      <el-table-column label="状态" align="center" width="80">
+        <template #default="{ row }">
+          <el-tag :type="row.payment_status === 'paid' ? 'success' : 'danger'">{{ row.payment_status === 'paid' ? '有效' : '已退' }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column :label="$t.value.Action" align="center" width="160">
+        <template #default="{ row }">
+          <template v-if="row.payment_status === 'paid'">
+            <el-button text type="primary" @click="openRenew(row)">续费</el-button>
+            <el-button text type="danger" @click="doRefund(row.id)">退卡</el-button>
+          </template>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <el-pagination v-model:current-page="queryForm.page" :page-size="queryForm.page_size" :total="total" layout="total, prev, pager, next" style="margin-top:12px; justify-content:flex-end" @current-change="fetchList" />
+
+    <el-dialog v-model="createVisible" title="办理月卡" width="460px">
+      <el-form :model="createForm" label-width="100px">
+        <el-form-item label="选择车辆" required>
+          <el-select v-model="createForm.vehicle_id" filterable placeholder="搜索车牌号" style="width:100%">
+            <el-option v-for="v in vehicleList" :key="v.ID" :label="v.plate_number" :value="v.ID" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="卡类型" required>
+          <el-select v-model="createForm.card_type" style="width:100%">
+            <el-option label="月卡 (30天)" value="month" />
+            <el-option label="季卡 (90天)" value="quarter" />
+            <el-option label="年卡 (365天)" value="year" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="到期日期">{{ computeEnd(createForm.card_type) }}</el-form-item>
+        <el-form-item label="金额" required>
+          <el-input-number v-model="createForm.fee" :min="0" :precision="2" style="width:100%" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="createVisible = false">{{ $t.value.Cancel }}</el-button>
+        <el-button type="primary" :loading="submitting" @click="doCreate">{{ $t.value.Confirm }}</el-button>
+      </template>
+    </el-dialog>
+
+    <el-dialog v-model="renewVisible" title="续费" width="400px">
+      <el-form :model="renewForm" label-width="100px">
+        <el-form-item label="续费时长" required>
+          <el-select v-model="renewForm.card_type" style="width:100%">
+            <el-option label="月卡 (30天)" value="month" />
+            <el-option label="季卡 (90天)" value="quarter" />
+            <el-option label="年卡 (365天)" value="year" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="金额" required>
+          <el-input-number v-model="renewForm.fee" :min="0" :precision="2" style="width:100%" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="renewVisible = false">{{ $t.value.Cancel }}</el-button>
+        <el-button type="primary" :loading="submitting" @click="doRenew">{{ $t.value.Confirm }}</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { createMonthlyCard, renewMonthlyCard, refundMonthlyCard, getMonthlyCardList } from '@/api/monthlyCard'
+import { queryAllVehicles } from '@/api/vehicle'
+
+const days = { month: 30, quarter: 90, year: 365 }
+const labels = { month: '月卡', quarter: '季卡', year: '年卡' }
+const typeLabel = (t) => labels[t] || t
+
+const queryForm = reactive({ plate_number: '', card_type: '', page: 1, page_size: 10 })
+const list = ref([]), total = ref(0), vehicleList = ref([])
+const createVisible = ref(false), renewVisible = ref(false), submitting = ref(false)
+const createForm = reactive({ vehicle_id: null, card_type: 'month', fee: 0 })
+const renewForm = reactive({ card_id: null, card_type: 'month', fee: 0 })
+
+function computeEnd(t) {
+  const d = new Date(); d.setDate(d.getDate() + (days[t] || 30))
+  return d.toLocaleDateString('zh-CN')
+}
+
+onMounted(() => { fetchList(); queryAllVehicles().then(r => { if (r.code === 0) vehicleList.value = r.data || [] }) })
+
+function fetchList() { getMonthlyCardList(queryForm).then(r => { if (r.code === 0) { list.value = r.data.list || []; total.value = r.data.total || 0 } }) }
+function resetQuery() { queryForm.plate_number = ''; queryForm.card_type = ''; queryForm.page = 1; fetchList() }
+
+function openCreate() { createForm.vehicle_id = null; createForm.card_type = 'month'; createForm.fee = 0; createVisible.value = true }
+function openRenew(row) { renewForm.card_id = row.id; renewForm.card_type = 'month'; renewForm.fee = 0; renewVisible.value = true }
+
+async function doCreate() {
+  if (!createForm.vehicle_id) { ElMessage.warning('请选择车辆'); return }
+  submitting.value = true
+  try {
+    const r = await createMonthlyCard(createForm)
+    if (r.code === 0) { ElMessage.success('办理成功'); createVisible.value = false; fetchList() } else ElMessage.error(r.msg)
+  } catch (e) { ElMessage.error(e.message) } finally { submitting.value = false }
+}
+
+async function doRenew() {
+  submitting.value = true
+  try {
+    const r = await renewMonthlyCard(renewForm)
+    if (r.code === 0) { ElMessage.success('续费成功'); renewVisible.value = false; fetchList() } else ElMessage.error(r.msg)
+  } catch (e) { ElMessage.error(e.message) } finally { submitting.value = false }
+}
+
+async function doRefund(id) {
+  await ElMessageBox.confirm('确定退卡?将移除白名单。', '退卡确认', { type: 'warning' })
+  try {
+    const r = await refundMonthlyCard(id)
+    if (r.code === 0) { ElMessage.success('退卡成功'); fetchList() } else ElMessage.error(r.msg)
+  } catch (e) { ElMessage.error(e.message) }
+}
+</script>

+ 12 - 0
internal/initialize/seed.go

@@ -121,6 +121,7 @@ func SeedSystemData() {
 		{MenuLevel: 0, ParentId: 20, Path: "roster", Name: "roster", Hidden: false, Component: "view/parking/roster.vue", Sort: 3, Meta: dao.Meta{Title: "黑白名单", Icon: "memo"}},
 		{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"}},
 		// 报表查询 (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"}},
@@ -177,6 +178,8 @@ func SeedSystemData() {
 		{MenuId: "29", AuthorityId: "618"}, {MenuId: "29", AuthorityId: "888"}, {MenuId: "29", AuthorityId: "9527"},
 		// 进出场操作 (31) — admin + operator
 		{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"},
 	}
 	db.Create(&authMenus)
 		// --- vehicle_type (默认车辆类型:临时车,标记为系统内置) ---
@@ -415,6 +418,15 @@ func SeedSystemData() {
 		{"p", "618", "/vehicle/exit/preview", "POST", "", "", ""},
 		{"p", "888", "/vehicle/exit/confirm", "POST", "", "", ""},
 		{"p", "618", "/vehicle/exit/confirm", "POST", "", "", ""},
+		// 包月车管理
+		{"p", "888", "/monthly-card/create", "POST", "", "", ""},
+		{"p", "618", "/monthly-card/create", "POST", "", "", ""},
+		{"p", "888", "/monthly-card/renew", "POST", "", "", ""},
+		{"p", "618", "/monthly-card/renew", "POST", "", "", ""},
+		{"p", "888", "/monthly-card/refund", "DELETE", "", "", ""},
+		{"p", "618", "/monthly-card/refund", "DELETE", "", "", ""},
+		{"p", "888", "/monthly-card/list", "GET", "", "", ""},
+		{"p", "618", "/monthly-card/list", "GET", "", "", ""},
 		// 收费记录查询
 		{"p", "888", "/payment/list", "GET", "", "", ""},
 		{"p", "618", "/payment/list", "GET", "", "", ""},