feat(wm-revenue): #54 客服工作台+水费查询+语音自助

- Entity: CsWorkItem, VoiceCallRecord
- DTO: CsWorkbenchStats, BillQueryResult, VoiceMenuResponse
- Mapper: CsWorkItemMapper, VoiceCallRecordMapper
- Service: CsWorkbenchService, BillQueryService, VoiceQueryService
- Controller: CsWorkbenchController(9端点), BillQueryController(5端点), VoiceController(7端点)
- DDL: V_cs_workbench.sql (2表+索引)
- Test: CsWorkbenchTest (10个测试用例)
This commit is contained in:
2026-06-15 08:42:08 +08:00
parent ba535c47e2
commit 7cbc17c2c4
23 changed files with 2644 additions and 0 deletions
@@ -0,0 +1,254 @@
<template>
<div class="wx-install-apply">
<el-page-header @back="$router.back()" title="返回" content="报装申请" />
<!-- 申请表单 -->
<el-card shadow="never" style="margin-top: 16px">
<template #header>
<span>新建报装申请</span>
</template>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px" style="max-width: 600px">
<el-form-item label="申请人" prop="name">
<el-input v-model="form.name" placeholder="请输入申请人姓名" />
</el-form-item>
<el-form-item label="联系电话" prop="phone">
<el-input v-model="form.phone" placeholder="请输入手机号" maxlength="11" />
</el-form-item>
<el-form-item label="所在区域" prop="area">
<el-select v-model="form.area" placeholder="请选择区域" style="width: 100%">
<el-option label="城东片区" value="城东片区" />
<el-option label="城西片区" value="城西片区" />
<el-option label="城南片区" value="城南片区" />
<el-option label="城北片区" value="城北片区" />
<el-option label="开发区" value="开发区" />
<el-option label="高新区" value="高新区" />
</el-select>
</el-form-item>
<el-form-item label="详细地址" prop="address">
<el-input v-model="form.address" type="textarea" :rows="2" placeholder="请输入详细地址" />
</el-form-item>
<el-form-item label="用水类型">
<el-select v-model="form.customerType" style="width: 100%">
<el-option label="居民用水" value="resident" />
<el-option label="商业用水" value="commercial" />
<el-option label="工业用水" value="industrial" />
<el-option label="行政事业" value="public" />
</el-select>
</el-form-item>
<el-form-item label="口径">
<el-select v-model="form.caliber" style="width: 100%">
<el-option label="DN15" value="DN15" />
<el-option label="DN20" value="DN20" />
<el-option label="DN25" value="DN25" />
<el-option label="DN32" value="DN32" />
<el-option label="DN40" value="DN40" />
<el-option label="DN50" value="DN50" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSubmit" :loading="submitting">提交申请</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 进度查询 -->
<el-card shadow="never" style="margin-top: 12px">
<template #header>
<span>进度查询</span>
</template>
<el-form :inline="true">
<el-form-item label="申请编号">
<el-input v-model="progressQueryNo" placeholder="输入申请编号查询" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="queryProgress" :disabled="!progressQueryNo">查询</el-button>
</el-form-item>
</el-form>
<el-steps :active="progressStep" finish-status="success" align-center v-if="progressData"
style="margin-top: 20px">
<el-step title="预受理" :description="progressData.status === 'pre_apply' ? '当前阶段' : ''" />
<el-step title="工程审核" :description="progressData.status === 'engineering' ? '当前阶段' : ''" />
<el-step title="派单施工" :description="progressData.status === 'pending_review' ? '当前阶段' : ''" />
<el-step title="已完成" :description="progressData.status === 'completed' ? '当前阶段' : ''" />
</el-steps>
<el-descriptions :column="2" border style="margin-top: 16px" v-if="progressData">
<el-descriptions-item label="申请编号">{{ progressData.application_no }}</el-descriptions-item>
<el-descriptions-item label="申请人">{{ progressData.applicant_name }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ progressData.applicant_phone }}</el-descriptions-item>
<el-descriptions-item label="区域">{{ progressData.area }}</el-descriptions-item>
<el-descriptions-item label="地址" :span="2">{{ progressData.address }}</el-descriptions-item>
<el-descriptions-item label="用水类型">{{ progressData.customer_type }}</el-descriptions-item>
<el-descriptions-item label="口径">{{ progressData.caliber }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="installStatusTag(progressData.status)">{{ installStatusLabel(progressData.status) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ progressData.updated_at }}</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 申请记录 -->
<el-card shadow="never" style="margin-top: 12px">
<template #header>
<div class="card-header">
<span>申请记录</span>
</div>
</template>
<el-form :inline="true" style="margin-bottom: 12px">
<el-form-item label="手机号">
<el-input v-model="recordPhone" placeholder="输入手机号查询" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="fetchRecords">查询</el-button>
</el-form-item>
</el-form>
<el-table :data="records" border stripe v-loading="recordLoading">
<el-table-column prop="application_no" label="申请编号" min-width="180" />
<el-table-column prop="applicant_name" label="申请人" width="100" />
<el-table-column prop="area" label="区域" width="100" />
<el-table-column prop="address" label="地址" min-width="200" show-overflow-tooltip />
<el-table-column prop="customer_type" label="用水类型" width="100" />
<el-table-column prop="caliber" label="口径" width="80" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="installStatusTag(row.status)" size="small">{{ installStatusLabel(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="created_at" label="申请时间" width="170" />
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="viewRecordProgress(row)">进度</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import type { FormInstance } from 'element-plus'
import { submitInstallApply, getInstallProgress, getInstallList } from '@/api/wx-hall'
const formRef = ref<FormInstance>()
const submitting = ref(false)
const progressQueryNo = ref('')
const progressData = ref<any>(null)
const progressStep = ref(0)
const recordPhone = ref('')
const records = ref<any[]>([])
const recordLoading = ref(false)
const form = reactive({
name: '',
phone: '',
area: '',
address: '',
customerType: 'resident',
caliber: 'DN15'
})
const rules = {
name: [{ required: true, message: '请输入申请人姓名', trigger: 'blur' }],
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
],
area: [{ required: true, message: '请选择区域', trigger: 'change' }],
address: [{ required: true, message: '请输入详细地址', trigger: 'blur' }]
}
async function handleSubmit() {
if (!formRef.value) return
await formRef.value.validate()
submitting.value = true
try {
const res = await submitInstallApply(form)
ElMessage.success(`申请提交成功!申请编号: ${res.data?.applicationNo}`)
resetForm()
} catch (e) { /* ignore */ } finally {
submitting.value = false
}
}
function resetForm() {
formRef.value?.resetFields()
Object.assign(form, { name: '', phone: '', area: '', address: '', customerType: 'resident', caliber: 'DN15' })
}
async function queryProgress() {
if (!progressQueryNo.value) return
try {
const res = await getInstallProgress(progressQueryNo.value)
progressData.value = res.data
progressStep.value = calcStep(res.data?.status)
} catch (e) {
progressData.value = null
progressStep.value = 0
}
}
async function fetchRecords() {
if (!recordPhone.value) {
ElMessage.warning('请输入手机号')
return
}
recordLoading.value = true
try {
const res = await getInstallList({ phone: recordPhone.value, limit: 50 })
records.value = res.data || []
} finally {
recordLoading.value = false
}
}
function viewRecordProgress(row: any) {
progressQueryNo.value = row.application_no
queryProgress()
}
function calcStep(status: string): number {
const map: Record<string, number> = {
pre_apply: 0,
engineering: 1,
pending_review: 2,
completed: 3
}
return map[status] ?? 0
}
function installStatusLabel(s: string): string {
const map: Record<string, string> = {
pre_apply: '预受理',
engineering: '工程审核',
pending_review: '派单施工',
completed: '已完成'
}
return map[s] || s
}
function installStatusTag(s: string): any {
const map: Record<string, string> = {
pre_apply: 'info',
engineering: 'warning',
pending_review: '',
completed: 'success'
}
return map[s] || 'info'
}
</script>
<style scoped>
.wx-install-apply {
padding: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>