潘的第一次 commit

This commit is contained in:
panchengyong
2026-02-27 23:50:25 +08:00
commit 10b6d0099a
117 changed files with 32547 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import ElementPlus from 'element-plus'
import Customer from '../index.vue'
import * as customerApi from '@/api/customer'
vi.mock('@/api/customer', () => ({
getCustomerList: vi.fn(),
getCustomerDetail: vi.fn(),
createCustomer: vi.fn(),
updateCustomer: vi.fn(),
deleteCustomer: vi.fn(),
updateCustomerStatus: vi.fn()
}))
describe('Customer 客户档案', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(customerApi.getCustomerList).mockResolvedValue({ list: [], total: 0 })
})
it('renders and loads list', async () => {
vi.mocked(customerApi.getCustomerList).mockResolvedValue({
list: [
{
clientId: 1,
clientCode: 'C001',
clientName: '客户A',
clientNick: 'A',
contact1: '张三',
contact1Tel: '13800001111',
address: '深圳',
enableFlag: 'Y',
createTime: '2026-01-25 10:00:00'
}
],
total: 1
})
const wrapper = mount(Customer, {
global: {
plugins: [ElementPlus],
stubs: {
ElPopconfirm: { template: '<div @click="$attrs.onConfirm?.()"><slot /></div>' }
}
}
})
await vi.waitFor(() => {
expect(customerApi.getCustomerList).toHaveBeenCalled()
})
expect(wrapper.find('.search-card').exists()).toBe(true)
expect(wrapper.find('.table-card').exists()).toBe(true)
expect(wrapper.find('table').exists()).toBe(true)
expect(wrapper.find('.pagination-wrapper').exists()).toBe(true)
})
it('has query inputs and toolbar buttons', async () => {
const wrapper = mount(Customer, {
global: {
plugins: [ElementPlus],
stubs: {
ElPopconfirm: { template: '<div><slot /></div>' }
}
}
})
await wrapper.vm.$nextTick()
expect(wrapper.find('.search-card').exists()).toBe(true)
expect(wrapper.text()).toMatch(/新增|刷新|导出|批量删除/)
})
})

View File

@@ -0,0 +1,514 @@
<template>
<div class="page-container">
<!-- 查询区域 -->
<el-card class="search-card" shadow="never">
<el-form :model="queryParams" inline>
<el-form-item label="客户编码">
<el-input v-model="queryParams.clientCode" placeholder="请输入客户编码" clearable style="width: 160px;" />
</el-form-item>
<el-form-item label="客户名称">
<el-input v-model="queryParams.clientName" placeholder="请输入客户名称" clearable style="width: 160px;" />
</el-form-item>
<el-form-item label="客户简称">
<el-input v-model="queryParams.clientNick" placeholder="请输入客户简称" clearable style="width: 160px;" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.enableFlag" placeholder="全部" clearable style="width: 100px;">
<el-option label="启用" value="Y" />
<el-option label="停用" value="N" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">
<el-icon><Search /></el-icon>查询
</el-button>
<el-button @click="handleReset">
<el-icon><Refresh /></el-icon>重置
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 工具栏与表格 -->
<el-card class="table-card" shadow="never">
<div class="toolbar">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增
</el-button>
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
<el-icon><Delete /></el-icon>批量删除
</el-button>
<el-button type="success" @click="handleExport">
<el-icon><Download /></el-icon>导出
</el-button>
<el-button @click="handleRefresh">
<el-icon><Refresh /></el-icon>刷新
</el-button>
</div>
<el-table
v-loading="loading"
:data="tableData"
stripe
border
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="50" />
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="clientCode" label="客户编码" width="120" />
<el-table-column prop="clientName" label="客户名称" min-width="160" show-overflow-tooltip />
<el-table-column prop="clientNick" label="简称" width="100" />
<el-table-column prop="contact1" label="联系人" width="100" />
<el-table-column prop="contact1Tel" label="联系电话" width="120" />
<el-table-column prop="address" label="地址" min-width="180" show-overflow-tooltip />
<el-table-column prop="enableFlag" label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.enableFlag === 'Y' ? 'success' : 'info'">
{{ row.enableFlag === 'Y' ? '启用' : '停用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="160" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleView(row)">查看</el-button>
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-popconfirm title="确定删除该客户吗?" @confirm="handleDelete(row)">
<template #reference>
<el-button type="danger" link>删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="loadData"
@current-change="loadData"
/>
</div>
</el-card>
<!-- 新增/编辑弹窗 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="800px"
:close-on-click-modal="false"
destroy-on-close
>
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="客户编码" prop="clientCode">
<el-input v-model="formData.clientCode" placeholder="请输入客户编码" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户名称" prop="clientName">
<el-input v-model="formData.clientName" placeholder="请输入客户名称" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="客户简称">
<el-input v-model="formData.clientNick" placeholder="请输入简称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户级别">
<el-select v-model="formData.clientLevel" placeholder="请选择" clearable style="width: 100%;">
<el-option label="A级" value="A" />
<el-option label="B级" value="B" />
<el-option label="C级" value="C" />
<el-option label="D级" value="D" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="联系人1">
<el-input v-model="formData.contact1" placeholder="请输入联系人" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系人1电话">
<el-input v-model="formData.contact1Tel" placeholder="请输入电话" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="联系人2">
<el-input v-model="formData.contact2" placeholder="请输入联系人" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系人2电话">
<el-input v-model="formData.contact2Tel" placeholder="请输入电话" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="电话">
<el-input v-model="formData.tel" placeholder="请输入电话" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="邮箱">
<el-input v-model="formData.email" placeholder="请输入邮箱" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="网址">
<el-input v-model="formData.website" placeholder="请输入网址" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="启用状态">
<el-radio-group v-model="formData.enableFlag">
<el-radio label="Y">启用</el-radio>
<el-radio label="N">停用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="地址">
<el-input v-model="formData.address" placeholder="请输入地址" />
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">开票信息</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="税号">
<el-input v-model="formData.taxNo" placeholder="请输入税号" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="开户银行">
<el-input v-model="formData.bankName" placeholder="请输入开户银行" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="银行账号">
<el-input v-model="formData.bankAccount" placeholder="请输入银行账号" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="信用代码">
<el-input v-model="formData.creditCode" placeholder="统一社会信用代码" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="开票地址">
<el-input v-model="formData.invoiceAddress" placeholder="开票地址电话" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="备注">
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
<!-- 查看弹窗 -->
<el-dialog v-model="viewDialogVisible" title="客户详情" width="800px">
<div v-loading="viewLoading">
<el-descriptions :column="2" border>
<el-descriptions-item label="客户编码">{{ viewData.clientCode }}</el-descriptions-item>
<el-descriptions-item label="客户名称">{{ viewData.clientName }}</el-descriptions-item>
<el-descriptions-item label="客户简称">{{ viewData.clientNick }}</el-descriptions-item>
<el-descriptions-item label="客户级别">{{ viewData.clientLevel || '-' }}</el-descriptions-item>
<el-descriptions-item label="联系人1">{{ viewData.contact1 || '-' }}</el-descriptions-item>
<el-descriptions-item label="联系人1电话">{{ viewData.contact1Tel || '-' }}</el-descriptions-item>
<el-descriptions-item label="联系人2">{{ viewData.contact2 || '-' }}</el-descriptions-item>
<el-descriptions-item label="联系人2电话">{{ viewData.contact2Tel || '-' }}</el-descriptions-item>
<el-descriptions-item label="电话">{{ viewData.tel || '-' }}</el-descriptions-item>
<el-descriptions-item label="邮箱">{{ viewData.email || '-' }}</el-descriptions-item>
<el-descriptions-item label="网址">{{ viewData.website || '-' }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="viewData.enableFlag === 'Y' ? 'success' : 'info'">
{{ viewData.enableFlag === 'Y' ? '启用' : '停用' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="地址" :span="2">{{ viewData.address || '-' }}</el-descriptions-item>
<el-descriptions-item label="税号">{{ viewData.taxNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="开户银行">{{ viewData.bankName || '-' }}</el-descriptions-item>
<el-descriptions-item label="银行账号">{{ viewData.bankAccount || '-' }}</el-descriptions-item>
<el-descriptions-item label="信用代码">{{ viewData.creditCode || '-' }}</el-descriptions-item>
<el-descriptions-item label="开票地址" :span="2">{{ viewData.invoiceAddress || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ viewData.createTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ viewData.updateTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ viewData.remark || '-' }}</el-descriptions-item>
</el-descriptions>
</div>
<template #footer>
<el-button @click="viewDialogVisible = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Search, Refresh, Plus, Download, Delete } from '@element-plus/icons-vue'
import {
getCustomerList,
getCustomerDetail,
createCustomer,
updateCustomer,
deleteCustomer,
type Customer,
type CustomerQuery
} from '@/api/customer'
const queryParams = reactive<CustomerQuery & { pageNum: number; pageSize: number }>({
clientCode: '',
clientName: '',
clientNick: '',
enableFlag: undefined,
pageNum: 1,
pageSize: 10
})
const total = ref(0)
const loading = ref(false)
const tableData = ref<Customer[]>([])
const selectedRows = ref<Customer[]>([])
const dialogVisible = ref(false)
const dialogTitle = ref('新增客户')
const submitLoading = ref(false)
const formRef = ref()
const formData = reactive<Partial<Customer>>({
clientCode: '',
clientName: '',
clientNick: '',
clientLevel: 'B',
contact1: '',
contact1Tel: '',
contact2: '',
contact2Tel: '',
tel: '',
email: '',
website: '',
address: '',
taxNo: '',
bankName: '',
bankAccount: '',
creditCode: '',
invoiceAddress: '',
enableFlag: 'Y',
remark: ''
})
const formRules = {
clientCode: [{ required: true, message: '请输入客户编码', trigger: 'blur' }],
clientName: [{ required: true, message: '请输入客户名称', trigger: 'blur' }]
}
const viewDialogVisible = ref(false)
const viewLoading = ref(false)
const viewData = ref<Customer>({} as Customer)
function resetForm() {
Object.assign(formData, {
clientId: undefined,
clientCode: '',
clientName: '',
clientNick: '',
clientLevel: 'B',
contact1: '',
contact1Tel: '',
contact2: '',
contact2Tel: '',
tel: '',
email: '',
website: '',
address: '',
taxNo: '',
bankName: '',
bankAccount: '',
creditCode: '',
invoiceAddress: '',
enableFlag: 'Y',
remark: ''
})
}
async function loadData() {
loading.value = true
try {
const res = await getCustomerList({
clientCode: queryParams.clientCode || undefined,
clientName: queryParams.clientName || undefined,
clientNick: queryParams.clientNick || undefined,
enableFlag: queryParams.enableFlag,
pageNum: queryParams.pageNum,
pageSize: queryParams.pageSize
})
tableData.value = res.list
total.value = res.total
} catch (e) {
console.error(e)
} finally {
loading.value = false
}
}
function handleQuery() {
queryParams.pageNum = 1
loadData()
}
function handleReset() {
queryParams.clientCode = ''
queryParams.clientName = ''
queryParams.clientNick = ''
queryParams.enableFlag = undefined
queryParams.pageNum = 1
handleQuery()
}
function handleRefresh() {
loadData()
}
function handleAdd() {
dialogTitle.value = '新增客户'
resetForm()
dialogVisible.value = true
}
function handleEdit(row: Customer) {
dialogTitle.value = '编辑客户'
Object.assign(formData, { ...row })
dialogVisible.value = true
}
async function handleView(row: Customer) {
viewDialogVisible.value = true
viewData.value = {} as Customer
viewLoading.value = true
try {
if (row.clientId != null) {
const detail = await getCustomerDetail(row.clientId)
viewData.value = detail
} else {
viewData.value = { ...row }
}
} catch (e) {
console.error(e)
viewData.value = { ...row }
} finally {
viewLoading.value = false
}
}
async function handleDelete(row: Customer) {
if (row.clientId == null) return
try {
await deleteCustomer(row.clientId)
ElMessage.success('删除成功')
loadData()
} catch (e) {
console.error(e)
}
}
function handleSelectionChange(rows: Customer[]) {
selectedRows.value = rows
}
async function handleBatchDelete() {
const ids = selectedRows.value.map((r) => r.clientId).filter((id): id is number => id != null)
if (!ids.length) return
try {
await deleteCustomer(ids)
ElMessage.success('删除成功')
loadData()
} catch (e) {
console.error(e)
}
}
function handleExport() {
ElMessage.info('导出功能开发中')
}
async function handleSubmit() {
try {
await formRef.value?.validate()
submitLoading.value = true
if (formData.clientId != null) {
await updateCustomer(formData)
ElMessage.success('更新成功')
} else {
await createCustomer(formData)
ElMessage.success('新增成功')
}
dialogVisible.value = false
loadData()
} catch (e) {
if (e !== false) console.error(e)
} finally {
submitLoading.value = false
}
}
onMounted(() => {
loadData()
})
</script>
<style scoped>
.page-container {
padding: 4px;
}
.search-card {
margin-bottom: 16px;
}
.search-card :deep(.el-card__body) {
padding-bottom: 0;
}
.toolbar {
margin-bottom: 16px;
}
.pagination-wrapper {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
</style>