Files
rui-docs/frontend/plans/cashier-admin-implementation.md
T
vifo a7f3ee3565 refactor: 全局替换 spring-ai -> rui-framework
同步仓库名称变更,涉及 16 个文件 66 处引用:
- ai-skills: 菜单配置
- backend/guides: AI操作手册、环境配置、部署、gitnexus、opencode 工作流
- backend: 模块创建规则、通信规范、协作工作流、实施规范
- frontend: 收银设计、管理后台实施计划
- standards: 数据库设计规范
2026-06-08 12:56:39 +08:00

37 KiB

收银系统后台管理功能完善实施计划

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:

<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:

import StoreFormDialog from './StoreFormDialog.vue'

Add state:

const dialogVisible = ref(false)
const currentRow = ref<any>()

Replace handleAdd:

function handleAdd() {
  currentRow.value = undefined
  dialogVisible.value = true
}

Replace handleEdit:

function handleEdit(row: any) {
  currentRow.value = row
  dialogVisible.value = true
}

Add dialog component in template:

<StoreFormDialog
  v-model:visible="dialogVisible"
  :row="currentRow"
  @success="tableRef?.refresh()"
/>

Step 3: 验证

Run:

cd /Users/zhangsheng/rhkj/rui-framework/admin-ui
pnpm dev:cashier

Expected: 门店管理页面可以正常打开新增/编辑弹窗

Step 4: Commit

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:

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:

<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

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:

<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

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:

<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:

<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:

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

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:

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:

<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

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:

cd /Users/zhangsheng/rhkj/rui-framework/admin-ui
pnpm add echarts vue-echarts

Step 2: 优化报表页面

Add chart components for:

  • 营业趋势图(折线图)
  • 支付方式饼图
  • 包间利用率柱状图

Step 3: Commit

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:

cd /Users/zhangsheng/rhkj/rui-framework/admin-ui
pnpm build:cashier

Expected: 构建成功,无错误

Step 2: 更新状态文档

Update docs/admin-ui-status.md:

### 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

git add docs/admin-ui-status.md
git commit -m "docs: 更新收银系统功能状态"

验证清单

  • 门店管理:新增、编辑、删除、查询、状态切换正常
  • 包间管理:新增、编辑、删除、查询、状态切换正常
  • 商品管理:新增、编辑、删除、查询、状态切换正常
  • 定价策略:新增、编辑、删除、查询、套餐管理正常
  • 订单管理:开台、结账、退款、查询正常
  • 营业报表:数据展示、图表正常
  • 所有页面无编译错误
  • 构建产物正常

风险与依赖

风险 影响 缓解措施
后端 API 字段与前端不一致 开发时对比后端实体字段,确保一致
包间类型数据未对接 先使用 ID 输入,后续优化为下拉选择
图表库引入增加包体积 按需引入 echarts 组件
开台功能需要实时状态更新 开台成功后刷新列表

后续扩展

  1. 包间类型管理 - 添加包间类型 CRUD 页面
  2. 设备管理 - 添加门店设备管理页面
  3. 会员管理 - 完善会员等级、积分管理
  4. 库存管理 - 添加商品库存预警功能
  5. 数据导出 - 为所有列表添加导出功能