a7f3ee3565
同步仓库名称变更,涉及 16 个文件 66 处引用: - ai-skills: 菜单配置 - backend/guides: AI操作手册、环境配置、部署、gitnexus、opencode 工作流 - backend: 模块创建规则、通信规范、协作工作流、实施规范 - frontend: 收银设计、管理后台实施计划 - standards: 数据库设计规范
1388 lines
37 KiB
Markdown
1388 lines
37 KiB
Markdown
# 收银系统后台管理功能完善实施计划
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers-subagent-driven-development (recommended) or superpowers-executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** 完善收银系统后台管理的前端页面功能,为所有列表页面添加新增/编辑表单弹窗,完善订单开台功能,优化营业报表展示
|
|
|
|
**Architecture:** 基于现有 admin-ui 框架和 RuiTable 组件,为每个收银模块创建 FormDialog 组件,遵循项目已有的组件模式和编码规范
|
|
|
|
**Tech Stack:** Vue 3 + TypeScript + Element Plus + Vite
|
|
|
|
---
|
|
|
|
## 文件结构
|
|
|
|
### 新增文件
|
|
- `src/views/cashier/store/StoreFormDialog.vue` - 门店表单弹窗
|
|
- `src/views/cashier/room/RoomFormDialog.vue` - 包间表单弹窗
|
|
- `src/views/cashier/product/ProductFormDialog.vue` - 商品表单弹窗
|
|
- `src/views/cashier/pricing/PricingStrategyFormDialog.vue` - 定价策略表单弹窗
|
|
- `src/views/cashier/pricing/PricingPackageDialog.vue` - 套餐管理弹窗
|
|
- `src/views/cashier/order/OpenRoomDialog.vue` - 开台弹窗
|
|
|
|
### 修改文件
|
|
- `src/views/cashier/store/Index.vue` - 集成门店表单弹窗
|
|
- `src/views/cashier/room/Index.vue` - 集成包间表单弹窗
|
|
- `src/views/cashier/product/Index.vue` - 集成商品表单弹窗
|
|
- `src/views/cashier/pricing/Index.vue` - 集成定价策略表单弹窗和套餐管理
|
|
- `src/views/cashier/order/Index.vue` - 集成开台弹窗
|
|
- `src/service/cashier/*Service.ts` - 补充缺失的方法
|
|
|
|
---
|
|
|
|
## 模块优先级
|
|
|
|
按业务依赖关系,实施顺序为:
|
|
1. 门店管理(最基础,其他模块依赖门店)
|
|
2. 商品管理
|
|
3. 包间管理(依赖门店和包间类型)
|
|
4. 定价策略(依赖包间类型)
|
|
5. 订单管理(依赖门店和包间)
|
|
6. 营业报表优化
|
|
|
|
---
|
|
|
|
## Task 1: 完善门店管理
|
|
|
|
**Files:**
|
|
- Create: `src/views/cashier/store/StoreFormDialog.vue`
|
|
- Modify: `src/views/cashier/store/Index.vue`
|
|
|
|
**Context:** 门店实体字段:name, address, phone, contactName, status, remark
|
|
|
|
### Step 1: 创建门店表单弹窗
|
|
|
|
Create `src/views/cashier/store/StoreFormDialog.vue`:
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
import { ref, reactive, watch } from 'vue'
|
|
import { ElMessage } from 'element-plus'
|
|
import { storeService } from '@/service/cashier/storeService'
|
|
|
|
const props = defineProps<{
|
|
visible: boolean
|
|
row?: any
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'update:visible', val: boolean): void
|
|
(e: 'success'): void
|
|
}>()
|
|
|
|
// 表单数据
|
|
const form = reactive({
|
|
id: undefined as number | undefined,
|
|
name: '',
|
|
address: '',
|
|
phone: '',
|
|
contactName: '',
|
|
status: 1,
|
|
remark: '',
|
|
})
|
|
|
|
// 表单引用
|
|
const formRef = ref()
|
|
|
|
// 加载状态
|
|
const loading = ref(false)
|
|
|
|
// 是否编辑模式
|
|
const isEdit = ref(false)
|
|
|
|
// 监听 visible 变化
|
|
watch(() => props.visible, (val) => {
|
|
if (val) {
|
|
if (props.row) {
|
|
isEdit.value = true
|
|
Object.assign(form, props.row)
|
|
} else {
|
|
isEdit.value = false
|
|
Object.assign(form, {
|
|
id: undefined,
|
|
name: '',
|
|
address: '',
|
|
phone: '',
|
|
contactName: '',
|
|
status: 1,
|
|
remark: '',
|
|
})
|
|
}
|
|
}
|
|
})
|
|
|
|
// 表单校验规则
|
|
const rules = {
|
|
name: [
|
|
{ required: true, message: '请输入门店名称', trigger: 'blur' },
|
|
],
|
|
status: [
|
|
{ required: true, message: '请选择状态', trigger: 'change' },
|
|
],
|
|
}
|
|
|
|
// 提交表单
|
|
async function handleSubmit() {
|
|
const valid = await formRef.value?.validate().catch(() => false)
|
|
if (!valid) return
|
|
|
|
loading.value = true
|
|
try {
|
|
if (isEdit.value) {
|
|
await storeService.update(form as any)
|
|
ElMessage.success('修改成功')
|
|
} else {
|
|
await storeService.add(form)
|
|
ElMessage.success('新增成功')
|
|
}
|
|
emit('success')
|
|
emit('update:visible', false)
|
|
} catch {
|
|
// 错误已由请求拦截器统一提示
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
// 关闭弹窗
|
|
function handleClose() {
|
|
emit('update:visible', false)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<el-dialog
|
|
class="rui-dialog"
|
|
:title="isEdit ? '编辑门店' : '新增门店'"
|
|
:model-value="visible"
|
|
width="600px"
|
|
:close-on-click-modal="false"
|
|
@close="handleClose"
|
|
>
|
|
<el-form
|
|
ref="formRef"
|
|
:model="form"
|
|
:rules="rules"
|
|
label-width="100px"
|
|
>
|
|
<el-form-item label="门店名称" prop="name">
|
|
<el-input v-model.trim="form.name" placeholder="请输入门店名称" />
|
|
</el-form-item>
|
|
|
|
<el-form-item label="门店地址" prop="address">
|
|
<el-input v-model.trim="form.address" placeholder="请输入门店地址" />
|
|
</el-form-item>
|
|
|
|
<el-form-item label="联系电话" prop="phone">
|
|
<el-input v-model.trim="form.phone" placeholder="请输入联系电话" />
|
|
</el-form-item>
|
|
|
|
<el-form-item label="联系人" prop="contactName">
|
|
<el-input v-model.trim="form.contactName" placeholder="请输入联系人姓名" />
|
|
</el-form-item>
|
|
|
|
<el-form-item label="状态" prop="status">
|
|
<el-radio-group v-model="form.status">
|
|
<el-radio-button :label="1">启用</el-radio-button>
|
|
<el-radio-button :label="0">禁用</el-radio-button>
|
|
</el-radio-group>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="备注" prop="remark">
|
|
<el-input
|
|
v-model.trim="form.remark"
|
|
type="textarea"
|
|
:rows="3"
|
|
placeholder="请输入备注"
|
|
/>
|
|
</el-form-item>
|
|
</el-form>
|
|
|
|
<template #footer>
|
|
<el-button @click="handleClose">取消</el-button>
|
|
<el-button type="primary" :loading="loading" @click="handleSubmit">
|
|
确定
|
|
</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</template>
|
|
```
|
|
|
|
### Step 2: 修改门店管理页面集成弹窗
|
|
|
|
Modify `src/views/cashier/store/Index.vue`:
|
|
|
|
Add imports:
|
|
```typescript
|
|
import StoreFormDialog from './StoreFormDialog.vue'
|
|
```
|
|
|
|
Add state:
|
|
```typescript
|
|
const dialogVisible = ref(false)
|
|
const currentRow = ref<any>()
|
|
```
|
|
|
|
Replace handleAdd:
|
|
```typescript
|
|
function handleAdd() {
|
|
currentRow.value = undefined
|
|
dialogVisible.value = true
|
|
}
|
|
```
|
|
|
|
Replace handleEdit:
|
|
```typescript
|
|
function handleEdit(row: any) {
|
|
currentRow.value = row
|
|
dialogVisible.value = true
|
|
}
|
|
```
|
|
|
|
Add dialog component in template:
|
|
```vue
|
|
<StoreFormDialog
|
|
v-model:visible="dialogVisible"
|
|
:row="currentRow"
|
|
@success="tableRef?.refresh()"
|
|
/>
|
|
```
|
|
|
|
### Step 3: 验证
|
|
|
|
Run:
|
|
```bash
|
|
cd /Users/zhangsheng/rhkj/rui-framework/admin-ui
|
|
pnpm dev:cashier
|
|
```
|
|
|
|
Expected: 门店管理页面可以正常打开新增/编辑弹窗
|
|
|
|
### Step 4: Commit
|
|
|
|
```bash
|
|
git add src/views/cashier/store/
|
|
git commit -m "feat(cashier): 完善门店管理新增编辑功能"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 2: 完善商品管理
|
|
|
|
**Files:**
|
|
- Create: `src/views/cashier/product/ProductFormDialog.vue`
|
|
- Modify: `src/views/cashier/product/Index.vue`
|
|
- Modify: `src/service/cashier/productService.ts`
|
|
|
|
**Context:** 商品实体字段:name, price, productType(1实物/2服务/3虚拟), category, unit, stock, status, description, storeId
|
|
|
|
### Step 1: 补充商品 Service 方法
|
|
|
|
Verify `src/service/cashier/productService.ts` has:
|
|
```typescript
|
|
import { BaseService } from '../BaseService'
|
|
|
|
class ProductService extends BaseService {
|
|
constructor() {
|
|
super('/cashier/admin/product')
|
|
}
|
|
}
|
|
|
|
export const productService = new ProductService()
|
|
```
|
|
|
|
### Step 2: 创建商品表单弹窗
|
|
|
|
Create `src/views/cashier/product/ProductFormDialog.vue`:
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
import { ref, reactive, watch } from 'vue'
|
|
import { ElMessage } from 'element-plus'
|
|
import { productService } from '@/service/cashier/productService'
|
|
|
|
const props = defineProps<{
|
|
visible: boolean
|
|
row?: any
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'update:visible', val: boolean): void
|
|
(e: 'success'): void
|
|
}>()
|
|
|
|
const form = reactive({
|
|
id: undefined as number | undefined,
|
|
name: '',
|
|
price: 0,
|
|
productType: 1,
|
|
category: '',
|
|
unit: '',
|
|
stock: 0,
|
|
status: 1,
|
|
description: '',
|
|
storeId: undefined as number | undefined,
|
|
})
|
|
|
|
const formRef = ref()
|
|
const loading = ref(false)
|
|
const isEdit = ref(false)
|
|
|
|
watch(() => props.visible, (val) => {
|
|
if (val) {
|
|
if (props.row) {
|
|
isEdit.value = true
|
|
Object.assign(form, props.row)
|
|
} else {
|
|
isEdit.value = false
|
|
Object.assign(form, {
|
|
id: undefined,
|
|
name: '',
|
|
price: 0,
|
|
productType: 1,
|
|
category: '',
|
|
unit: '',
|
|
stock: 0,
|
|
status: 1,
|
|
description: '',
|
|
storeId: undefined,
|
|
})
|
|
}
|
|
}
|
|
})
|
|
|
|
const rules = {
|
|
name: [{ required: true, message: '请输入商品名称', trigger: 'blur' }],
|
|
price: [{ required: true, message: '请输入售价', trigger: 'blur' }],
|
|
productType: [{ required: true, message: '请选择商品类型', trigger: 'change' }],
|
|
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
|
|
}
|
|
|
|
async function handleSubmit() {
|
|
const valid = await formRef.value?.validate().catch(() => false)
|
|
if (!valid) return
|
|
|
|
loading.value = true
|
|
try {
|
|
if (isEdit.value) {
|
|
await productService.update(form as any)
|
|
ElMessage.success('修改成功')
|
|
} else {
|
|
await productService.add(form)
|
|
ElMessage.success('新增成功')
|
|
}
|
|
emit('success')
|
|
emit('update:visible', false)
|
|
} catch {
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function handleClose() {
|
|
emit('update:visible', false)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<el-dialog
|
|
class="rui-dialog"
|
|
:title="isEdit ? '编辑商品' : '新增商品'"
|
|
:model-value="visible"
|
|
width="600px"
|
|
:close-on-click-modal="false"
|
|
@close="handleClose"
|
|
>
|
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
|
<el-form-item label="商品名称" prop="name">
|
|
<el-input v-model.trim="form.name" placeholder="请输入商品名称" />
|
|
</el-form-item>
|
|
|
|
<el-form-item label="商品类型" prop="productType">
|
|
<el-radio-group v-model="form.productType">
|
|
<el-radio-button :label="1">实物商品</el-radio-button>
|
|
<el-radio-button :label="2">服务商品</el-radio-button>
|
|
<el-radio-button :label="3">虚拟商品</el-radio-button>
|
|
</el-radio-group>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="售价" prop="price">
|
|
<el-input-number v-model="form.price" :min="0" :precision="2" style="width: 100%" />
|
|
</el-form-item>
|
|
|
|
<el-form-item label="分类" prop="category">
|
|
<el-input v-model.trim="form.category" placeholder="请输入分类" />
|
|
</el-form-item>
|
|
|
|
<el-form-item label="单位" prop="unit">
|
|
<el-input v-model.trim="form.unit" placeholder="如:个、杯、小时" />
|
|
</el-form-item>
|
|
|
|
<el-form-item label="库存" prop="stock" v-if="form.productType === 1">
|
|
<el-input-number v-model="form.stock" :min="0" style="width: 100%" />
|
|
</el-form-item>
|
|
|
|
<el-form-item label="状态" prop="status">
|
|
<el-radio-group v-model="form.status">
|
|
<el-radio-button :label="1">启用</el-radio-button>
|
|
<el-radio-button :label="0">禁用</el-radio-button>
|
|
</el-radio-group>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="描述" prop="description">
|
|
<el-input v-model.trim="form.description" type="textarea" :rows="3" placeholder="请输入描述" />
|
|
</el-form-item>
|
|
</el-form>
|
|
|
|
<template #footer>
|
|
<el-button @click="handleClose">取消</el-button>
|
|
<el-button type="primary" :loading="loading" @click="handleSubmit">确定</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</template>
|
|
```
|
|
|
|
### Step 3: 修改商品管理页面
|
|
|
|
Similar to Task 1 Step 2, integrate ProductFormDialog into product/Index.vue
|
|
|
|
### Step 4: Commit
|
|
|
|
```bash
|
|
git add src/views/cashier/product/ src/service/cashier/productService.ts
|
|
git commit -m "feat(cashier): 完善商品管理新增编辑功能"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 3: 完善包间管理
|
|
|
|
**Files:**
|
|
- Create: `src/views/cashier/room/RoomFormDialog.vue`
|
|
- Modify: `src/views/cashier/room/Index.vue`
|
|
|
|
**Context:** 包间实体字段:storeId, roomTypeId, name, roomNo, roomStatus, enabled, sort
|
|
需要获取门店列表和包间类型列表
|
|
|
|
### Step 1: 创建包间表单弹窗
|
|
|
|
Create `src/views/cashier/room/RoomFormDialog.vue`:
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
import { ref, reactive, watch, onMounted } from 'vue'
|
|
import { ElMessage } from 'element-plus'
|
|
import { roomService } from '@/service/cashier/roomService'
|
|
import { storeService } from '@/service/cashier/storeService'
|
|
|
|
const props = defineProps<{
|
|
visible: boolean
|
|
row?: any
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'update:visible', val: boolean): void
|
|
(e: 'success'): void
|
|
}>()
|
|
|
|
const form = reactive({
|
|
id: undefined as number | undefined,
|
|
storeId: undefined as number | undefined,
|
|
roomTypeId: undefined as number | undefined,
|
|
name: '',
|
|
roomNo: '',
|
|
enabled: 1,
|
|
sort: 0,
|
|
})
|
|
|
|
const formRef = ref()
|
|
const loading = ref(false)
|
|
const isEdit = ref(false)
|
|
const storeList = ref<any[]>([])
|
|
const roomTypeList = ref<any[]>([])
|
|
|
|
watch(() => props.visible, (val) => {
|
|
if (val) {
|
|
loadStoreList()
|
|
if (props.row) {
|
|
isEdit.value = true
|
|
Object.assign(form, props.row)
|
|
} else {
|
|
isEdit.value = false
|
|
Object.assign(form, {
|
|
id: undefined,
|
|
storeId: undefined,
|
|
roomTypeId: undefined,
|
|
name: '',
|
|
roomNo: '',
|
|
enabled: 1,
|
|
sort: 0,
|
|
})
|
|
}
|
|
}
|
|
})
|
|
|
|
async function loadStoreList() {
|
|
try {
|
|
storeList.value = await storeService.list({ status: 1 })
|
|
} catch {
|
|
storeList.value = []
|
|
}
|
|
}
|
|
|
|
const rules = {
|
|
storeId: [{ required: true, message: '请选择门店', trigger: 'change' }],
|
|
name: [{ required: true, message: '请输入包间名称', trigger: 'blur' }],
|
|
roomNo: [{ required: true, message: '请输入包间编号', trigger: 'blur' }],
|
|
enabled: [{ required: true, message: '请选择状态', trigger: 'change' }],
|
|
}
|
|
|
|
async function handleSubmit() {
|
|
const valid = await formRef.value?.validate().catch(() => false)
|
|
if (!valid) return
|
|
|
|
loading.value = true
|
|
try {
|
|
if (isEdit.value) {
|
|
await roomService.update(form as any)
|
|
ElMessage.success('修改成功')
|
|
} else {
|
|
await roomService.add(form)
|
|
ElMessage.success('新增成功')
|
|
}
|
|
emit('success')
|
|
emit('update:visible', false)
|
|
} catch {
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function handleClose() {
|
|
emit('update:visible', false)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<el-dialog
|
|
class="rui-dialog"
|
|
:title="isEdit ? '编辑包间' : '新增包间'"
|
|
:model-value="visible"
|
|
width="600px"
|
|
:close-on-click-modal="false"
|
|
@close="handleClose"
|
|
>
|
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
|
<el-form-item label="所属门店" prop="storeId">
|
|
<el-select v-model="form.storeId" placeholder="请选择门店" style="width: 100%">
|
|
<el-option
|
|
v-for="item in storeList"
|
|
:key="item.id"
|
|
:label="item.name"
|
|
:value="item.id"
|
|
/>
|
|
</el-select>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="包间名称" prop="name">
|
|
<el-input v-model.trim="form.name" placeholder="请输入包间名称" />
|
|
</el-form-item>
|
|
|
|
<el-form-item label="包间编号" prop="roomNo">
|
|
<el-input v-model.trim="form.roomNo" placeholder="请输入包间编号" />
|
|
</el-form-item>
|
|
|
|
<el-form-item label="排序" prop="sort">
|
|
<el-input-number v-model="form.sort" :min="0" style="width: 100%" />
|
|
</el-form-item>
|
|
|
|
<el-form-item label="状态" prop="enabled">
|
|
<el-radio-group v-model="form.enabled">
|
|
<el-radio-button :label="1">启用</el-radio-button>
|
|
<el-radio-button :label="0">禁用</el-radio-button>
|
|
</el-radio-group>
|
|
</el-form-item>
|
|
</el-form>
|
|
|
|
<template #footer>
|
|
<el-button @click="handleClose">取消</el-button>
|
|
<el-button type="primary" :loading="loading" @click="handleSubmit">确定</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</template>
|
|
```
|
|
|
|
### Step 2: 修改包间管理页面
|
|
|
|
Integrate RoomFormDialog into room/Index.vue (similar pattern)
|
|
|
|
### Step 3: Commit
|
|
|
|
```bash
|
|
git add src/views/cashier/room/
|
|
git commit -m "feat(cashier): 完善包间管理新增编辑功能"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 4: 完善定价策略
|
|
|
|
**Files:**
|
|
- Create: `src/views/cashier/pricing/PricingStrategyFormDialog.vue`
|
|
- Create: `src/views/cashier/pricing/PricingPackageDialog.vue`
|
|
- Modify: `src/views/cashier/pricing/Index.vue`
|
|
|
|
**Context:** 定价策略实体:strategyName, roomTypeId, status
|
|
套餐实体:name, price, duration, durationUnit, description, strategyId, billingType, minDuration, restrictions, isDefault, sort, status
|
|
|
|
### Step 1: 创建定价策略表单弹窗
|
|
|
|
Create `src/views/cashier/pricing/PricingStrategyFormDialog.vue`:
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
import { ref, reactive, watch } from 'vue'
|
|
import { ElMessage } from 'element-plus'
|
|
import { pricingService } from '@/service/cashier/pricingService'
|
|
|
|
const props = defineProps<{
|
|
visible: boolean
|
|
row?: any
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'update:visible', val: boolean): void
|
|
(e: 'success'): void
|
|
}>()
|
|
|
|
const form = reactive({
|
|
id: undefined as number | undefined,
|
|
strategyName: '',
|
|
roomTypeId: undefined as number | undefined,
|
|
status: 1,
|
|
})
|
|
|
|
const formRef = ref()
|
|
const loading = ref(false)
|
|
const isEdit = ref(false)
|
|
|
|
watch(() => props.visible, (val) => {
|
|
if (val) {
|
|
if (props.row) {
|
|
isEdit.value = true
|
|
Object.assign(form, props.row)
|
|
} else {
|
|
isEdit.value = false
|
|
Object.assign(form, {
|
|
id: undefined,
|
|
strategyName: '',
|
|
roomTypeId: undefined,
|
|
status: 1,
|
|
})
|
|
}
|
|
}
|
|
})
|
|
|
|
const rules = {
|
|
strategyName: [{ required: true, message: '请输入策略名称', trigger: 'blur' }],
|
|
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
|
|
}
|
|
|
|
async function handleSubmit() {
|
|
const valid = await formRef.value?.validate().catch(() => false)
|
|
if (!valid) return
|
|
|
|
loading.value = true
|
|
try {
|
|
if (isEdit.value) {
|
|
await pricingService.update(form as any)
|
|
ElMessage.success('修改成功')
|
|
} else {
|
|
await pricingService.add(form)
|
|
ElMessage.success('新增成功')
|
|
}
|
|
emit('success')
|
|
emit('update:visible', false)
|
|
} catch {
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function handleClose() {
|
|
emit('update:visible', false)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<el-dialog
|
|
class="rui-dialog"
|
|
:title="isEdit ? '编辑策略' : '新增策略'"
|
|
:model-value="visible"
|
|
width="600px"
|
|
:close-on-click-modal="false"
|
|
@close="handleClose"
|
|
>
|
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
|
<el-form-item label="策略名称" prop="strategyName">
|
|
<el-input v-model.trim="form.strategyName" placeholder="请输入策略名称" />
|
|
</el-form-item>
|
|
|
|
<el-form-item label="包间类型" prop="roomTypeId">
|
|
<el-input-number v-model="form.roomTypeId" :min="1" style="width: 100%" placeholder="请输入包间类型ID" />
|
|
</el-form-item>
|
|
|
|
<el-form-item label="状态" prop="status">
|
|
<el-radio-group v-model="form.status">
|
|
<el-radio-button :label="1">启用</el-radio-button>
|
|
<el-radio-button :label="0">禁用</el-radio-button>
|
|
</el-radio-group>
|
|
</el-form-item>
|
|
</el-form>
|
|
|
|
<template #footer>
|
|
<el-button @click="handleClose">取消</el-button>
|
|
<el-button type="primary" :loading="loading" @click="handleSubmit">确定</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</template>
|
|
```
|
|
|
|
### Step 2: 创建套餐管理弹窗
|
|
|
|
Create `src/views/cashier/pricing/PricingPackageDialog.vue`:
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
import { ref, reactive, watch } from 'vue'
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
import { pricingService } from '@/service/cashier/pricingService'
|
|
|
|
const props = defineProps<{
|
|
visible: boolean
|
|
strategyId?: number
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'update:visible', val: boolean): void
|
|
}>()
|
|
|
|
const packageList = ref<any[]>([])
|
|
const loading = ref(false)
|
|
const dialogVisible = ref(false)
|
|
const currentPackage = ref<any>()
|
|
const isEditPackage = ref(false)
|
|
|
|
const form = reactive({
|
|
id: undefined as number | undefined,
|
|
name: '',
|
|
price: 0,
|
|
duration: 1,
|
|
durationUnit: 'hour',
|
|
description: '',
|
|
strategyId: undefined as number | undefined,
|
|
billingType: 1,
|
|
minDuration: 0,
|
|
isDefault: 0,
|
|
sort: 0,
|
|
status: 1,
|
|
})
|
|
|
|
const formRef = ref()
|
|
const formLoading = ref(false)
|
|
|
|
watch(() => props.visible, (val) => {
|
|
if (val && props.strategyId) {
|
|
loadPackages()
|
|
}
|
|
})
|
|
|
|
async function loadPackages() {
|
|
loading.value = true
|
|
try {
|
|
const res = await pricingService.getPackages(props.strategyId!)
|
|
packageList.value = res || []
|
|
} catch {
|
|
packageList.value = []
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function handleAddPackage() {
|
|
isEditPackage.value = false
|
|
currentPackage.value = undefined
|
|
Object.assign(form, {
|
|
id: undefined,
|
|
name: '',
|
|
price: 0,
|
|
duration: 1,
|
|
durationUnit: 'hour',
|
|
description: '',
|
|
strategyId: props.strategyId,
|
|
billingType: 1,
|
|
minDuration: 0,
|
|
isDefault: 0,
|
|
sort: 0,
|
|
status: 1,
|
|
})
|
|
dialogVisible.value = true
|
|
}
|
|
|
|
function handleEditPackage(row: any) {
|
|
isEditPackage.value = true
|
|
currentPackage.value = row
|
|
Object.assign(form, row)
|
|
dialogVisible.value = true
|
|
}
|
|
|
|
async function handleDeletePackage(row: any) {
|
|
ElMessageBox.confirm(`确认删除套餐 "${row.name}" 吗?`, '提示', {
|
|
type: 'warning',
|
|
}).then(async () => {
|
|
try {
|
|
await pricingService.deletePackage(row.id)
|
|
ElMessage.success('删除成功')
|
|
loadPackages()
|
|
} catch {}
|
|
}).catch(() => {})
|
|
}
|
|
|
|
async function handleSubmitPackage() {
|
|
const valid = await formRef.value?.validate().catch(() => false)
|
|
if (!valid) return
|
|
|
|
formLoading.value = true
|
|
try {
|
|
if (isEditPackage.value) {
|
|
await pricingService.updatePackage(form as any)
|
|
ElMessage.success('修改成功')
|
|
} else {
|
|
await pricingService.addPackage(form)
|
|
ElMessage.success('新增成功')
|
|
}
|
|
dialogVisible.value = false
|
|
loadPackages()
|
|
} catch {
|
|
} finally {
|
|
formLoading.value = false
|
|
}
|
|
}
|
|
|
|
function handleClose() {
|
|
emit('update:visible', false)
|
|
}
|
|
|
|
function handleCloseForm() {
|
|
dialogVisible.value = false
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<el-dialog
|
|
class="rui-dialog"
|
|
title="套餐管理"
|
|
:model-value="visible"
|
|
width="800px"
|
|
:close-on-click-modal="false"
|
|
@close="handleClose"
|
|
>
|
|
<div class="mb-4">
|
|
<el-button type="primary" @click="handleAddPackage">新增套餐</el-button>
|
|
</div>
|
|
|
|
<el-table :data="packageList" v-loading="loading" border>
|
|
<el-table-column prop="name" label="套餐名称" min-width="150" />
|
|
<el-table-column prop="price" label="价格" width="100" align="right">
|
|
<template #default="{ row }">¥{{ (row.price || 0).toFixed(2) }}</template>
|
|
</el-table-column>
|
|
<el-table-column prop="duration" label="时长" width="100" align="center">
|
|
<template #default="{ row }">{{ row.duration }}{{ row.durationUnit === 'hour' ? '小时' : '天' }}</template>
|
|
</el-table-column>
|
|
<el-table-column prop="billingType" label="计费类型" width="100" align="center">
|
|
<template #default="{ row }">
|
|
<el-tag v-if="row.billingType === 1">按时</el-tag>
|
|
<el-tag v-else-if="row.billingType === 2" type="success">按局</el-tag>
|
|
<el-tag v-else type="warning">包时段</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="status" label="状态" width="80" align="center">
|
|
<template #default="{ row }">
|
|
<el-tag :type="row.status === 1 ? 'success' : 'info'">
|
|
{{ row.status === 1 ? '启用' : '禁用' }}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="150" align="center" fixed="right">
|
|
<template #default="{ row }">
|
|
<el-button link type="primary" size="small" @click="handleEditPackage(row)">编辑</el-button>
|
|
<el-button link type="danger" size="small" @click="handleDeletePackage(row)">删除</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
|
|
<!-- 套餐表单弹窗 -->
|
|
<el-dialog
|
|
v-model="dialogVisible"
|
|
:title="isEditPackage ? '编辑套餐' : '新增套餐'"
|
|
width="500px"
|
|
:close-on-click-modal="false"
|
|
append-to-body
|
|
@close="handleCloseForm"
|
|
>
|
|
<el-form ref="formRef" :model="form" label-width="100px">
|
|
<el-form-item label="套餐名称" prop="name" required>
|
|
<el-input v-model.trim="form.name" placeholder="请输入套餐名称" />
|
|
</el-form-item>
|
|
<el-form-item label="价格" prop="price" required>
|
|
<el-input-number v-model="form.price" :min="0" :precision="2" style="width: 100%" />
|
|
</el-form-item>
|
|
<el-form-item label="时长" prop="duration" required>
|
|
<el-input-number v-model="form.duration" :min="1" style="width: 100%" />
|
|
</el-form-item>
|
|
<el-form-item label="时长单位" prop="durationUnit">
|
|
<el-radio-group v-model="form.durationUnit">
|
|
<el-radio-button label="hour">小时</el-radio-button>
|
|
<el-radio-button label="day">天</el-radio-button>
|
|
</el-radio-group>
|
|
</el-form-item>
|
|
<el-form-item label="计费类型" prop="billingType">
|
|
<el-radio-group v-model="form.billingType">
|
|
<el-radio-button :label="1">按时计费</el-radio-button>
|
|
<el-radio-button :label="2">按局计费</el-radio-button>
|
|
<el-radio-button :label="3">包时段</el-radio-button>
|
|
</el-radio-group>
|
|
</el-form-item>
|
|
<el-form-item label="最小时长" prop="minDuration">
|
|
<el-input-number v-model="form.minDuration" :min="0" style="width: 100%" placeholder="分钟" />
|
|
</el-form-item>
|
|
<el-form-item label="默认套餐" prop="isDefault">
|
|
<el-radio-group v-model="form.isDefault">
|
|
<el-radio-button :label="1">是</el-radio-button>
|
|
<el-radio-button :label="0">否</el-radio-button>
|
|
</el-radio-group>
|
|
</el-form-item>
|
|
<el-form-item label="排序" prop="sort">
|
|
<el-input-number v-model="form.sort" :min="0" style="width: 100%" />
|
|
</el-form-item>
|
|
<el-form-item label="状态" prop="status">
|
|
<el-radio-group v-model="form.status">
|
|
<el-radio-button :label="1">启用</el-radio-button>
|
|
<el-radio-button :label="0">禁用</el-radio-button>
|
|
</el-radio-group>
|
|
</el-form-item>
|
|
<el-form-item label="描述" prop="description">
|
|
<el-input v-model.trim="form.description" type="textarea" :rows="2" placeholder="请输入描述" />
|
|
</el-form-item>
|
|
</el-form>
|
|
<template #footer>
|
|
<el-button @click="handleCloseForm">取消</el-button>
|
|
<el-button type="primary" :loading="formLoading" @click="handleSubmitPackage">确定</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</el-dialog>
|
|
</template>
|
|
```
|
|
|
|
### Step 3: 补充 pricingService 方法
|
|
|
|
Modify `src/service/cashier/pricingService.ts`:
|
|
|
|
```typescript
|
|
import { BaseService } from '../BaseService'
|
|
import { request } from '@/utils/request'
|
|
|
|
class PricingService extends BaseService {
|
|
constructor() {
|
|
super('/cashier/admin/pricing-strategy')
|
|
}
|
|
|
|
async getPackages(strategyId: number) {
|
|
const res: any = await request({
|
|
url: '/cashier/admin/pricing-package/list',
|
|
method: 'get',
|
|
params: { strategyId },
|
|
})
|
|
return res.data || []
|
|
}
|
|
|
|
async addPackage(data: any) {
|
|
const res: any = await request({
|
|
url: '/cashier/admin/pricing-package',
|
|
method: 'post',
|
|
data,
|
|
})
|
|
return res.data
|
|
}
|
|
|
|
async updatePackage(data: any) {
|
|
const res: any = await request({
|
|
url: '/cashier/admin/pricing-package',
|
|
method: 'put',
|
|
data,
|
|
})
|
|
return res.data
|
|
}
|
|
|
|
async deletePackage(id: number) {
|
|
const res: any = await request({
|
|
url: `/cashier/admin/pricing-package/${id}`,
|
|
method: 'delete',
|
|
})
|
|
return res.data
|
|
}
|
|
}
|
|
|
|
export const pricingService = new PricingService()
|
|
```
|
|
|
|
### Step 4: 修改定价策略页面
|
|
|
|
Integrate both dialogs into pricing/Index.vue
|
|
|
|
### Step 5: Commit
|
|
|
|
```bash
|
|
git add src/views/cashier/pricing/ src/service/cashier/pricingService.ts
|
|
git commit -m "feat(cashier): 完善定价策略和套餐管理功能"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 5: 完善订单管理(开台功能)
|
|
|
|
**Files:**
|
|
- Create: `src/views/cashier/order/OpenRoomDialog.vue`
|
|
- Modify: `src/views/cashier/order/Index.vue`
|
|
- Modify: `src/service/cashier/orderService.ts`
|
|
|
|
**Context:** 开台需要:门店ID、包间ID、顾客姓名、顾客电话、订单类型、备注
|
|
|
|
### Step 1: 补充 orderService 方法
|
|
|
|
Modify `src/service/cashier/orderService.ts`:
|
|
|
|
```typescript
|
|
import { BaseService } from '../BaseService'
|
|
import { request } from '@/utils/request'
|
|
|
|
class OrderService extends BaseService {
|
|
constructor() {
|
|
super('/cashier/admin/order')
|
|
}
|
|
|
|
async checkout(id: number) {
|
|
const res: any = await request({
|
|
url: `${this.baseUrl}/${id}/checkout`,
|
|
method: 'post',
|
|
})
|
|
return res.data
|
|
}
|
|
|
|
async refund(id: number, data: { amount: number; reason: string }) {
|
|
const res: any = await request({
|
|
url: `${this.baseUrl}/${id}/refund`,
|
|
method: 'post',
|
|
data,
|
|
})
|
|
return res.data
|
|
}
|
|
|
|
async openRoom(data: {
|
|
storeId: number
|
|
roomId: number
|
|
customerName?: string
|
|
customerPhone?: string
|
|
orderType?: number
|
|
remark?: string
|
|
}) {
|
|
const res: any = await request({
|
|
url: `${this.baseUrl}/open`,
|
|
method: 'post',
|
|
data,
|
|
})
|
|
return res.data
|
|
}
|
|
}
|
|
|
|
export const orderService = new OrderService()
|
|
```
|
|
|
|
### Step 2: 创建开台弹窗
|
|
|
|
Create `src/views/cashier/order/OpenRoomDialog.vue`:
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
import { ref, reactive, watch } from 'vue'
|
|
import { ElMessage } from 'element-plus'
|
|
import { orderService } from '@/service/cashier/orderService'
|
|
import { storeService } from '@/service/cashier/storeService'
|
|
import { roomService } from '@/service/cashier/roomService'
|
|
|
|
const props = defineProps<{
|
|
visible: boolean
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'update:visible', val: boolean): void
|
|
(e: 'success'): void
|
|
}>()
|
|
|
|
const form = reactive({
|
|
storeId: undefined as number | undefined,
|
|
roomId: undefined as number | undefined,
|
|
customerName: '',
|
|
customerPhone: '',
|
|
orderType: 1,
|
|
remark: '',
|
|
})
|
|
|
|
const formRef = ref()
|
|
const loading = ref(false)
|
|
const storeList = ref<any[]>([])
|
|
const roomList = ref<any[]>([])
|
|
|
|
watch(() => props.visible, (val) => {
|
|
if (val) {
|
|
loadStores()
|
|
Object.assign(form, {
|
|
storeId: undefined,
|
|
roomId: undefined,
|
|
customerName: '',
|
|
customerPhone: '',
|
|
orderType: 1,
|
|
remark: '',
|
|
})
|
|
}
|
|
})
|
|
|
|
async function loadStores() {
|
|
try {
|
|
storeList.value = await storeService.list({ status: 1 })
|
|
} catch {
|
|
storeList.value = []
|
|
}
|
|
}
|
|
|
|
async function handleStoreChange(storeId: number) {
|
|
form.roomId = undefined
|
|
if (!storeId) {
|
|
roomList.value = []
|
|
return
|
|
}
|
|
try {
|
|
roomList.value = await roomService.list({ storeId, enabled: 1 })
|
|
} catch {
|
|
roomList.value = []
|
|
}
|
|
}
|
|
|
|
const rules = {
|
|
storeId: [{ required: true, message: '请选择门店', trigger: 'change' }],
|
|
roomId: [{ required: true, message: '请选择包间', trigger: 'change' }],
|
|
}
|
|
|
|
async function handleSubmit() {
|
|
const valid = await formRef.value?.validate().catch(() => false)
|
|
if (!valid) return
|
|
|
|
loading.value = true
|
|
try {
|
|
await orderService.openRoom(form as any)
|
|
ElMessage.success('开台成功')
|
|
emit('success')
|
|
emit('update:visible', false)
|
|
} catch {
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function handleClose() {
|
|
emit('update:visible', false)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<el-dialog
|
|
class="rui-dialog"
|
|
title="开台"
|
|
:model-value="visible"
|
|
width="600px"
|
|
:close-on-click-modal="false"
|
|
@close="handleClose"
|
|
>
|
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
|
<el-form-item label="选择门店" prop="storeId">
|
|
<el-select
|
|
v-model="form.storeId"
|
|
placeholder="请选择门店"
|
|
style="width: 100%"
|
|
@change="handleStoreChange"
|
|
>
|
|
<el-option
|
|
v-for="item in storeList"
|
|
:key="item.id"
|
|
:label="item.name"
|
|
:value="item.id"
|
|
/>
|
|
</el-select>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="选择包间" prop="roomId">
|
|
<el-select v-model="form.roomId" placeholder="请选择包间" style="width: 100%">
|
|
<el-option
|
|
v-for="item in roomList"
|
|
:key="item.id"
|
|
:label="item.name"
|
|
:value="item.id"
|
|
/>
|
|
</el-select>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="顾客姓名" prop="customerName">
|
|
<el-input v-model.trim="form.customerName" placeholder="请输入顾客姓名" />
|
|
</el-form-item>
|
|
|
|
<el-form-item label="顾客电话" prop="customerPhone">
|
|
<el-input v-model.trim="form.customerPhone" placeholder="请输入顾客电话" />
|
|
</el-form-item>
|
|
|
|
<el-form-item label="订单类型" prop="orderType">
|
|
<el-radio-group v-model="form.orderType">
|
|
<el-radio-button :label="1">正常订单</el-radio-button>
|
|
<el-radio-button :label="2">预订订单</el-radio-button>
|
|
</el-radio-group>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="备注" prop="remark">
|
|
<el-input v-model.trim="form.remark" type="textarea" :rows="2" placeholder="请输入备注" />
|
|
</el-form-item>
|
|
</el-form>
|
|
|
|
<template #footer>
|
|
<el-button @click="handleClose">取消</el-button>
|
|
<el-button type="primary" :loading="loading" @click="handleSubmit">确认开台</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</template>
|
|
```
|
|
|
|
### Step 3: 修改订单管理页面
|
|
|
|
Integrate OpenRoomDialog into order/Index.vue
|
|
|
|
### Step 4: Commit
|
|
|
|
```bash
|
|
git add src/views/cashier/order/ src/service/cashier/orderService.ts
|
|
git commit -m "feat(cashier): 完善订单管理开台功能"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 6: 营业报表优化
|
|
|
|
**Files:**
|
|
- Modify: `src/views/cashier/report/Index.vue`
|
|
|
|
**Context:** 当前报表已展示日报数据和包间利用率,可以优化图表展示
|
|
|
|
### Step 1: 添加图表展示
|
|
|
|
Install echarts if not already installed:
|
|
```bash
|
|
cd /Users/zhangsheng/rhkj/rui-framework/admin-ui
|
|
pnpm add echarts vue-echarts
|
|
```
|
|
|
|
### Step 2: 优化报表页面
|
|
|
|
Add chart components for:
|
|
- 营业趋势图(折线图)
|
|
- 支付方式饼图
|
|
- 包间利用率柱状图
|
|
|
|
### Step 3: Commit
|
|
|
|
```bash
|
|
git add src/views/cashier/report/ package.json pnpm-lock.yaml
|
|
git commit -m "feat(cashier): 优化营业报表图表展示"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 7: 验证和文档更新
|
|
|
|
**Files:**
|
|
- Modify: `docs/admin-ui-status.md`
|
|
|
|
### Step 1: 验证所有功能
|
|
|
|
Run:
|
|
```bash
|
|
cd /Users/zhangsheng/rhkj/rui-framework/admin-ui
|
|
pnpm build:cashier
|
|
```
|
|
|
|
Expected: 构建成功,无错误
|
|
|
|
### Step 2: 更新状态文档
|
|
|
|
Update `docs/admin-ui-status.md`:
|
|
|
|
```markdown
|
|
### 8. 收银系统模块
|
|
|
|
| 功能 | 页面 | 状态 | 说明 |
|
|
|------|------|------|------|
|
|
| 门店管理 | cashier/store/Index.vue | ✅ 完成 | 增删改查、状态切换 |
|
|
| 包间管理 | cashier/room/Index.vue | ✅ 完成 | 增删改查、状态切换 |
|
|
| 商品管理 | cashier/product/Index.vue | ✅ 完成 | 增删改查、状态切换 |
|
|
| 定价策略 | cashier/pricing/Index.vue | ✅ 完成 | 增删改查、套餐管理 |
|
|
| 订单管理 | cashier/order/Index.vue | ✅ 完成 | 开台、结账、退款、查询 |
|
|
| 营业报表 | cashier/report/Index.vue | ✅ 完成 | 日报、包间利用率、图表 |
|
|
```
|
|
|
|
### Step 3: Commit
|
|
|
|
```bash
|
|
git add docs/admin-ui-status.md
|
|
git commit -m "docs: 更新收银系统功能状态"
|
|
```
|
|
|
|
---
|
|
|
|
## 验证清单
|
|
|
|
- [ ] 门店管理:新增、编辑、删除、查询、状态切换正常
|
|
- [ ] 包间管理:新增、编辑、删除、查询、状态切换正常
|
|
- [ ] 商品管理:新增、编辑、删除、查询、状态切换正常
|
|
- [ ] 定价策略:新增、编辑、删除、查询、套餐管理正常
|
|
- [ ] 订单管理:开台、结账、退款、查询正常
|
|
- [ ] 营业报表:数据展示、图表正常
|
|
- [ ] 所有页面无编译错误
|
|
- [ ] 构建产物正常
|
|
|
|
---
|
|
|
|
## 风险与依赖
|
|
|
|
| 风险 | 影响 | 缓解措施 |
|
|
|------|------|---------|
|
|
| 后端 API 字段与前端不一致 | 高 | 开发时对比后端实体字段,确保一致 |
|
|
| 包间类型数据未对接 | 中 | 先使用 ID 输入,后续优化为下拉选择 |
|
|
| 图表库引入增加包体积 | 低 | 按需引入 echarts 组件 |
|
|
| 开台功能需要实时状态更新 | 中 | 开台成功后刷新列表 |
|
|
|
|
---
|
|
|
|
## 后续扩展
|
|
|
|
1. **包间类型管理** - 添加包间类型 CRUD 页面
|
|
2. **设备管理** - 添加门店设备管理页面
|
|
3. **会员管理** - 完善会员等级、积分管理
|
|
4. **库存管理** - 添加商品库存预警功能
|
|
5. **数据导出** - 为所有列表添加导出功能
|