Compare commits

25 Commits

Author SHA1 Message Date
481f4cdd84 李红攀:隐藏库存记录里面得操作按钮 2026-03-20 10:45:33 +08:00
f023251e30 AI客服的浮现 2026-03-14 14:22:07 +08:00
30570713bf 采购订单、采购入库、销售订单、销售出库添加打印功能 2026-03-14 14:13:35 +08:00
44e2e40e24 销售后的合并 2026-03-14 13:51:02 +08:00
4546481f48 销售换货模块 2026-03-14 10:57:04 +08:00
5a1923d9ca PC端和手机端端双端适配 2026-03-14 10:22:48 +08:00
cb13784141 甜菊糖库存适配手机端 2026-03-13 15:05:31 +08:00
dbc35688e7 车辆管理、站内信消息适配手机端 2026-03-13 14:31:59 +08:00
20990a419c 同步售后管理前端页面 2026-03-12 17:43:20 +08:00
41d7bd6c86 CRM里面的客户、线索管理适配手机端 2026-03-12 17:31:23 +08:00
0432e2430a 生产领料、BOM管理、生产工单适配手机端 2026-03-12 13:58:58 +08:00
52f1a1cda2 生产适配手机端 2026-03-12 11:27:31 +08:00
1a4f106ed7 首页样式的修改 2026-03-11 18:54:59 +08:00
a6e263dd70 功能页面的跳转 2026-03-11 18:24:00 +08:00
87430999e9 产品信息的三张表适配手机端 2026-03-11 18:00:43 +08:00
21abd429e5 组装拆卸单适配手机端 2026-03-10 19:50:34 +08:00
bf281ea13e 修改P4数通宝+;修改默认登录信息 2026-03-10 14:13:40 +08:00
41befdf4da 其它出库适配手机端 2026-03-10 09:37:06 +08:00
1db391a2f0 其它出库适配手机端 2026-03-09 17:42:12 +08:00
78fb7b8eca 其它入库适配手机端 2026-03-09 17:01:53 +08:00
d89670e94a 仓库、总库存、库存明细适配手机端 2026-03-09 14:39:40 +08:00
2080f3921c 添加快捷分类查询 2026-03-06 17:21:01 +08:00
6d706e8961 打包问题的修复,以及质量模块的合并 2026-03-06 16:45:32 +08:00
956834103e 采购管理适配手机端问题修改 2026-03-06 15:39:21 +08:00
1b3863bd7e 销售管理适配手机端 2026-03-06 14:46:40 +08:00
172 changed files with 33825 additions and 12190 deletions

8
.env
View File

@@ -1,5 +1,5 @@
# 标题 # 标题
VITE_APP_TITLE=P4数通宝 VITE_APP_TITLE=P4数通宝+
# 项目本地运行端口号 # 项目本地运行端口号
VITE_PORT=6188 VITE_PORT=6188
@@ -20,6 +20,6 @@ VITE_APP_DOCALERT_ENABLE=false
VITE_APP_BAIDU_CODE = a1ff8825baa73c3a78eb96aa40325abc VITE_APP_BAIDU_CODE = a1ff8825baa73c3a78eb96aa40325abc
# 默认账户密码 # 默认账户密码
VITE_APP_DEFAULT_LOGIN_TENANT = 武汉亚为电子科技有限公司 VITE_APP_DEFAULT_LOGIN_TENANT = YAVII
VITE_APP_DEFAULT_LOGIN_USERNAME = admin VITE_APP_DEFAULT_LOGIN_USERNAME = YAVII
VITE_APP_DEFAULT_LOGIN_PASSWORD = 123456 VITE_APP_DEFAULT_LOGIN_PASSWORD = yavii123

View File

@@ -0,0 +1,58 @@
import request from '@/config/axios'
export interface AnalysisQueryParams {
registerCreateTime?: string[]
processFinishTime?: string[]
visitCreateTime?: string[]
pageSize?: number
}
export interface DistItem {
name: string
value: number
}
export interface RegisterSummary {
total: number
pending: number
approved: number
rejected: number
}
export interface ProcessSummary {
total: number
processing: number
completed: number
failed: number
}
export interface VisitSummary {
total: number
avgRating: number
fiveStar: number
highRepurchase: number
}
export interface RegisterAnalysisResponse {
registerSummary?: RegisterSummary
registerTypeDist?: DistItem[]
registerStatusDist?: DistItem[]
}
export interface ProcessAnalysisResponse {
processSummary?: ProcessSummary
processTypeDist?: DistItem[]
processStatusDist?: DistItem[]
}
export interface VisitAnalysisResponse {
visitSummary?: VisitSummary
visitRatingDist?: DistItem[]
visitRepurchaseDist?: DistItem[]
}
export const AfterSaleAnalysisApi = {
getAnalysis: async (params: AnalysisQueryParams) => {
return await request.post({ url: '/erp/after-sale/analysis', data: params })
}
}

View File

@@ -0,0 +1,57 @@
import request from '@/config/axios'
import type { Dayjs } from 'dayjs';
/** ERP 售后处理信息 */
export interface AfterSaleProcess {
id: number; // 处理记录编号
afterSaleId?: number; // 关联售后登记编号
processType?: number; // 处理类型1-退货入库2-换货出库3-维修处理4-退款处理5-取消处理6-其他
processCount?: number; // 处理数量(与售后类型对应:退货数量/换货数量/维修数量等)
processResult?: string; // 处理结果描述
processEvidence: string; // 处理凭证(图片/文件URL多个用逗号分隔
relatedOrderId: number; // 关联业务订单编号(如换货对应的新销售订单号、退款对应的财务订单号)
processStatus?: number; // 处理状态1-处理中2-处理完成3-处理失败
processFailReason: string; // 处理失败原因
processUser?: string; // 处理人
processTime?: string | Dayjs; // 处理时间
finishTime: string | Dayjs; // 处理完成时间
userRating?: number; // 用户评价评分0-10
}
// ERP 售后处理 API
export const AfterSaleProcessApi = {
// 查询ERP 售后处理分页
getAfterSaleProcessPage: async (params: any) => {
return await request.get({ url: `/erp/after-sale-process/page`, params })
},
// 查询ERP 售后处理详情
getAfterSaleProcess: async (id: number) => {
return await request.get({ url: `/erp/after-sale-process/get?id=` + id })
},
// 新增ERP 售后处理
createAfterSaleProcess: async (data: AfterSaleProcess) => {
return await request.post({ url: `/erp/after-sale-process/create`, data })
},
// 修改ERP 售后处理
updateAfterSaleProcess: async (data: AfterSaleProcess) => {
return await request.put({ url: `/erp/after-sale-process/update`, data })
},
// 删除ERP 售后处理
deleteAfterSaleProcess: async (id: number) => {
return await request.delete({ url: `/erp/after-sale-process/delete?id=` + id })
},
/** 批量删除ERP 售后处理 */
deleteAfterSaleProcessList: async (ids: number[]) => {
return await request.delete({ url: `/erp/after-sale-process/delete-list?ids=${ids.join(',')}` })
},
// 导出ERP 售后处理 Excel
exportAfterSaleProcess: async (params) => {
return await request.download({ url: `/erp/after-sale-process/export-excel`, params })
}
}

View File

@@ -0,0 +1,56 @@
import request from '@/config/axios'
import type { Dayjs } from 'dayjs';
/** ERP 售后登记信息 */
export interface AfterSaleRegister {
id: number; // 售后登记编号
orderId?: number; // 关联销售订单编号
orderItemId?: number; // 关联销售订单项编号
afterSaleType?: number; // 售后类型1-退货2-换货3-维修4-退款
applyReason?: string; // 申请原因
contactName?: string; // 联系人
contactPhone?: string; // 联系电话
applyStatus?: number; // 申请状态1-待审核2-审核通过3-审核驳回4-已取消
rejectReason: string; // 驳回原因(审核驳回时填写)
applicant?: string; // 申请人
auditUser: string; // 审核人
auditTime: string | Dayjs; // 审核时间
}
// ERP 售后登记 API
export const AfterSaleRegisterApi = {
// 查询ERP 售后登记分页
getAfterSaleRegisterPage: async (params: any) => {
return await request.get({ url: `/erp/after-sale-register/page`, params })
},
// 查询ERP 售后登记详情
getAfterSaleRegister: async (id: number) => {
return await request.get({ url: `/erp/after-sale-register/get?id=` + id })
},
// 新增ERP 售后登记
createAfterSaleRegister: async (data: AfterSaleRegister) => {
return await request.post({ url: `/erp/after-sale-register/create`, data })
},
// 修改ERP 售后登记
updateAfterSaleRegister: async (data: AfterSaleRegister) => {
return await request.put({ url: `/erp/after-sale-register/update`, data })
},
// 删除ERP 售后登记
deleteAfterSaleRegister: async (id: number) => {
return await request.delete({ url: `/erp/after-sale-register/delete?id=` + id })
},
/** 批量删除ERP 售后登记 */
deleteAfterSaleRegisterList: async (ids: number[]) => {
return await request.delete({ url: `/erp/after-sale-register/delete-list?ids=${ids.join(',')}` })
},
// 导出ERP 售后登记 Excel
exportAfterSaleRegister: async (params) => {
return await request.download({ url: `/erp/after-sale-register/export-excel`, params })
}
}

View File

@@ -0,0 +1,53 @@
import request from '@/config/axios'
import type { Dayjs } from 'dayjs';
/** 售后回访信息 */
export interface AfterSalesVisit {
id: number; // 主键ID
customerName?: string; // 客户名称
contactInfo?: string; // 联系方式
customerUsage: string; // 客户用途
productName?: string; // 产品名称
productEvaluation: string; // 产品评价
customerType?: string; // 客户类型:大型食品厂/中小型加工厂/经销商/餐饮连锁/机关单位/其他
repurchaseIntention?: string; // 重复采购意愿:一定会/应该会/不确定/可能不会/肯定不会
serviceRating?: number; // 服务评分0~5星
}
// 售后回访 API
export const AfterSalesVisitApi = {
// 查询售后回访分页
getAfterSalesVisitPage: async (params: any) => {
return await request.get({ url: `/erp/after-sales-visit/page`, params })
},
// 查询售后回访详情
getAfterSalesVisit: async (id: number) => {
return await request.get({ url: `/erp/after-sales-visit/get?id=` + id })
},
// 新增售后回访
createAfterSalesVisit: async (data: AfterSalesVisit) => {
return await request.post({ url: `/erp/after-sales-visit/create`, data })
},
// 修改售后回访
updateAfterSalesVisit: async (data: AfterSalesVisit) => {
return await request.put({ url: `/erp/after-sales-visit/update`, data })
},
// 删除售后回访
deleteAfterSalesVisit: async (id: number) => {
return await request.delete({ url: `/erp/after-sales-visit/delete?id=` + id })
},
/** 批量删除售后回访 */
deleteAfterSalesVisitList: async (ids: number[]) => {
return await request.delete({ url: `/erp/after-sales-visit/delete-list?ids=${ids.join(',')}` })
},
// 导出售后回访 Excel
exportAfterSalesVisit: async (params) => {
return await request.download({ url: `/erp/after-sales-visit/export-excel`, params })
}
}

View File

@@ -0,0 +1,102 @@
import request from '@/config/axios'
// ERP 销售换货 VO
export interface SaleExchangeVO {
id: number // 换货编号
no: string // 换货单号
exchangeTime: Date // 换货时间
outId: number // 关联出库单编号
outNo: string // 关联出库单号
customerId: number // 客户编号
customerName: string // 客户名称
saleUserId: number // 销售人员编号
saleUserName: string // 销售人员名称
accountId: number // 结算账户编号
totalOutCount: number // 换出商品总数
totalInCount: number // 换入商品总数
totalPrice: number // 差价金额,单位:元
otherPrice: number // 其它费用,单位:元
status: number // 状态
remark: string // 备注
fileUrl: string // 附件地址
outItems: SaleExchangeOutItemVO[] // 换出商品明细
inItems: SaleExchangeInItemVO[] // 换入商品明细
}
// ERP 销售换货换出商品明细 VO
export interface SaleExchangeOutItemVO {
id: number // 明细编号
exchangeId: number // 换货编号
productId: number // 产品编号
productName: string // 产品名称
productBarCode: string // 产品条码
productUnitId: number // 产品单位编号
productUnitName: string // 产品单位名称
warehouseId: number // 仓库编号
warehouseName: string // 仓库名称
count: number // 换出数量
productPrice: number // 产品单价,单位:元
totalPrice: number // 合计金额,单位:元
}
// ERP 销售换货换入商品明细 VO
export interface SaleExchangeInItemVO {
id: number // 明细编号
exchangeId: number // 换货编号
productId: number // 产品编号
productName: string // 产品名称
productBarCode: string // 产品条码
productUnitId: number // 产品单位编号
productUnitName: string // 产品单位名称
warehouseId: number // 仓库编号
warehouseName: string // 仓库名称
count: number // 换入数量
productPrice: number // 产品单价,单位:元
totalPrice: number // 合计金额,单位:元
}
// 查询销售换货分页
export const getSaleExchangePage = async (params: any) => {
return await request.get({ url: `/erp/sale-exchange/page`, params })
}
// 查询销售换货详情
export const getSaleExchange = async (id: number) => {
return await request.get({ url: `/erp/sale-exchange/get?id=` + id })
}
// 新增销售换货
export const createSaleExchange = async (data: SaleExchangeVO) => {
return await request.post({ url: `/erp/sale-exchange/create`, data })
}
// 修改销售换货
export const updateSaleExchange = async (data: SaleExchangeVO) => {
return await request.put({ url: `/erp/sale-exchange/update`, data })
}
// 更新销售换货的状态
export const updateSaleExchangeStatus = async (id: number, status: number) => {
return await request.put({
url: `/erp/sale-exchange/update-status`,
params: {
id,
status
}
})
}
// 删除销售换货
export const deleteSaleExchange = async (ids: number[]) => {
return await request.delete({
url: `/erp/sale-exchange/delete`,
params: {
ids: ids.join(',')
}
})
}
// 导出销售换货 Excel
export const exportSaleExchange = async (params: any) => {
return await request.download({ url: `/erp/sale-exchange/export-excel`, params })
}

View File

@@ -1,5 +1,7 @@
<template> <template>
<!-- 移动端布局 -->
<el-drawer <el-drawer
v-if="isMobile"
v-model="dialogVisible" v-model="dialogVisible"
title="审批流程" title="审批流程"
direction="rtl" direction="rtl"
@@ -65,23 +67,48 @@
<el-button @click="dialogVisible = false">关闭</el-button> <el-button @click="dialogVisible = false">关闭</el-button>
</template> </template>
</el-drawer> </el-drawer>
<!-- PC端布局 -->
<el-dialog v-else v-model="dialogVisible" title="审批流程" width="700px" :close-on-press-escape="true" :destroy-on-close="true">
<el-table v-loading="loading" :data="approvalRecords" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="审批层级" align="center" width="100"><template #default="scope">{{ getLevelText(scope.row.approvalLevel) }}</template></el-table-column>
<el-table-column label="申请人" align="center" prop="applicantName" width="100"><template #default="scope">{{ scope.row.applicantName || scope.row.applicant }}</template></el-table-column>
<el-table-column label="审批人" align="center" prop="approverName" width="100"><template #default="scope">{{ scope.row.approverName || scope.row.approver }}</template></el-table-column>
<el-table-column label="审批结果" align="center" width="90"><template #default="scope"><el-tag :type="getStatusTagType(scope.row.approvalResult)" size="small">{{ getApprovalResultText(scope.row.approvalResult) }}</el-tag></template></el-table-column>
<el-table-column label="流程状态" align="center" width="90"><template #default="scope"><el-tag :type="getProcessStatusTagType(scope.row.processStatus)" size="small">{{ getProcessStatusText(scope.row.processStatus) }}</el-tag></template></el-table-column>
<el-table-column label="审批时间" align="center" prop="approvalTime" width="160"><template #default="scope">{{ scope.row.approvalTime ? formatDate(scope.row.approvalTime) : '-' }}</template></el-table-column>
<el-table-column label="审批意见" align="center" prop="comment" min-width="150" />
</el-table>
<el-empty v-if="approvalRecords.length === 0" description="暂无审批记录" />
<template #footer>
<el-button @click="dialogVisible = false">关闭</el-button>
</template>
</el-dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, computed } from 'vue'
import { ApprovalRecordApi } from '@/api/erp/approval' import { ApprovalRecordApi } from '@/api/erp/approval'
import { formatDate } from '@/utils/formatTime' import { formatDate } from '@/utils/formatTime'
import { useWindowSize } from '@vueuse/core'
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const dialogVisible = ref(false) const dialogVisible = ref(false)
const loading = ref(false)
const approvalRecords = ref<any[]>([]) const approvalRecords = ref<any[]>([])
const open = async (bizId: string, bizTableName: string) => { const open = async (bizId: string, bizTableName: string) => {
dialogVisible.value = true dialogVisible.value = true
loading.value = true
try { try {
const data = await ApprovalRecordApi.getApprovalRecordListByBiz(bizId, bizTableName) const data = await ApprovalRecordApi.getApprovalRecordListByBiz(bizId, bizTableName)
approvalRecords.value = data approvalRecords.value = data
} catch (error) { } catch (error) {
console.error('获取审批记录失败', error) console.error('获取审批记录失败', error)
} finally {
loading.value = false
} }
} }

View File

@@ -1,5 +1,7 @@
<template> <template>
<!-- 移动端布局 -->
<el-drawer <el-drawer
v-if="isMobile"
v-model="dialogVisible" v-model="dialogVisible"
title="处理审批" title="处理审批"
direction="rtl" direction="rtl"
@@ -113,6 +115,52 @@
<el-button type="primary" @click="submitForm" :loading="loading">确定</el-button> <el-button type="primary" @click="submitForm" :loading="loading">确定</el-button>
</template> </template>
</el-drawer> </el-drawer>
<!-- PC端布局 -->
<el-dialog v-else v-model="dialogVisible" title="处理审批" width="600px" :close-on-press-escape="true" :destroy-on-close="true">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
<el-form-item label="审批记录" prop="approvalRecordId">
<el-select v-model="formData.approvalRecordId" placeholder="请选择待处理的审批记录" filterable clearable style="width: 100%" @change="handleRecordChange">
<el-option v-for="record in pendingRecords" :key="record.id" :label="`第${record.approvalLevel}级审批 - ${record.applicantName || record.applicant}`" :value="record.id">
<div style="display: flex; justify-content: space-between; align-items: center"><span>{{ record.approvalLevel }}级审批</span><el-tag size="small" type="warning">待处理</el-tag></div>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="审批结果" prop="approvalResult">
<el-radio-group v-model="formData.approvalResult">
<el-radio :label="1">通过</el-radio>
<el-radio :label="2">驳回</el-radio>
<el-radio :label="3">转审</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="下一级审批人" prop="nextApprover" v-if="formData.approvalResult === 1 || formData.approvalResult === 3">
<el-select v-model="formData.nextApprover" placeholder="请选择下一级审批人" filterable clearable style="width: 100%">
<el-option v-for="user in userList" :key="user.id" :label="user.nickname" :value="String(user.id)" />
</el-select>
<div style="color: #909399; font-size: 12px; margin-top: 4px">{{ formData.approvalResult === 1 ? '通过时可指定下一级审批人,不指定则流程结束' : '转审时必须指定审批人' }}</div>
</el-form-item>
<el-form-item label="指定原因" prop="assignReason" v-if="formData.approvalResult === 3">
<el-input v-model="formData.assignReason" type="textarea" :rows="2" placeholder="请输入转审原因" />
</el-form-item>
<el-form-item label="审批意见" prop="comment">
<el-input v-model="formData.comment" type="textarea" :rows="3" placeholder="请输入审批意见" />
</el-form-item>
</el-form>
<!-- 审批信息卡片 -->
<div class="pc-process-info" v-if="selectedRecord">
<div class="pc-process-info__title">审批信息</div>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="申请人">{{ selectedRecord.applicantName || selectedRecord.applicant }}</el-descriptions-item>
<el-descriptions-item label="当前审批人">{{ selectedRecord.approverName || selectedRecord.approver }}</el-descriptions-item>
<el-descriptions-item label="审批层级">{{ selectedRecord.approvalLevel }}</el-descriptions-item>
<el-descriptions-item label="提交时间">{{ formatDate(selectedRecord.createTime) }}</el-descriptions-item>
</el-descriptions>
</div>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm" :loading="loading">确定</el-button>
</template>
</el-dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -122,6 +170,10 @@ import * as UserApi from '@/api/system/user'
import type { UserVO } from '@/api/system/user' import type { UserVO } from '@/api/system/user'
import { formatDate } from '@/utils/formatTime' import { formatDate } from '@/utils/formatTime'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
import { useWindowSize } from '@vueuse/core'
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const message = useMessage() const message = useMessage()
const userStore = useUserStore() const userStore = useUserStore()
@@ -299,4 +351,17 @@ defineExpose({ open })
height: auto; height: auto;
padding: 8px 20px; padding: 8px 20px;
} }
.pc-process-info {
background: #f0f9eb;
border-radius: 8px;
padding: 14px;
border: 1px solid #e1f3d8;
margin-top: 16px;
&__title {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 10px;
}
}
</style> </style>

View File

@@ -1,5 +1,7 @@
<template> <template>
<!-- 移动端布局 -->
<el-drawer <el-drawer
v-if="isMobile"
v-model="dialogVisible" v-model="dialogVisible"
title="提交审批" title="提交审批"
direction="rtl" direction="rtl"
@@ -40,13 +42,35 @@
<el-button type="primary" @click="submitForm" :loading="loading">确定</el-button> <el-button type="primary" @click="submitForm" :loading="loading">确定</el-button>
</template> </template>
</el-drawer> </el-drawer>
<!-- PC端布局 -->
<el-dialog v-else v-model="dialogVisible" title="提交审批" width="500px" :close-on-press-escape="true" :destroy-on-close="true">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
<el-form-item label="下一级审批人" prop="nextApprover">
<el-select v-model="formData.nextApprover" placeholder="请选择下一级审批人" filterable clearable style="width: 100%">
<el-option v-for="user in userList" :key="user.id" :label="user.nickname" :value="String(user.id)" />
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" type="textarea" :rows="4" placeholder="请输入备注信息" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm" :loading="loading">确定</el-button>
</template>
</el-dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive } from 'vue' import { ref, reactive, computed } from 'vue'
import { ApprovalRecordApi } from '@/api/erp/approval' import { ApprovalRecordApi } from '@/api/erp/approval'
import * as UserApi from '@/api/system/user' import * as UserApi from '@/api/system/user'
import type { UserVO } from '@/api/system/user' import type { UserVO } from '@/api/system/user'
import { useWindowSize } from '@vueuse/core'
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const message = useMessage() const message = useMessage()
const dialogVisible = ref(false) const dialogVisible = ref(false)

View File

@@ -5,7 +5,7 @@
<el-avatar :size="60"> <el-avatar :size="60">
<Icon icon="ep:avatar" :size="60" /> <Icon icon="ep:avatar" :size="60" />
</el-avatar> </el-avatar>
<span class="text-18px font-bold">P4数通宝</span> <span class="text-18px font-bold">P4数通宝+</span>
</div> </div>
<Icon icon="tdesign:qrcode" :size="20" /> <Icon icon="tdesign:qrcode" :size="20" />
</div> </div>

View File

@@ -0,0 +1,258 @@
# DynamicForm 动态表单组件
一个通用的动态表单组件,支持多种输入类型和灵活的配置。
## 功能特性
- 🎯 **多种输入类型**:支持数字、文本、选择器、日期、开关、单选框、复选框、滑块等
- 🔧 **灵活配置**:支持自定义验证规则、占位符、禁用状态等
- 📱 **响应式布局**:支持自定义列数和间距
- 🎨 **自定义插槽**:支持自定义输入组件
- 📊 **数据绑定**:支持 v-model 双向绑定
- 🔍 **类型安全**:完整的 TypeScript 类型定义
## 基础用法
```vue
<template>
<DynamicForm
v-model="formData"
:items="formItems"
title="用户信息"
@form-change="handleFormChange"
/>
</template>
<script setup>
import DynamicForm, {
createNumberItem,
createTextItem,
createSelectItem
} from '@/components/DynamicForm'
const formData = ref({})
const formItems = [
createTextItem('name', '姓名', { required: true }),
createNumberItem('age', '年龄', { min: 0, max: 120 }),
createSelectItem('gender', '性别', [
{ label: '男', value: 'male' },
{ label: '女', value: 'female' }
])
]
const handleFormChange = (value) => {
console.log('表单数据变化:', value)
}
</script>
```
## 支持的输入类型
### 1. 数字输入 (number)
```javascript
createNumberItem('price', '价格', {
min: 0,
max: 10000,
precision: 2,
step: 0.01,
placeholder: '请输入价格'
})
```
### 2. 文本输入 (text/textarea)
```javascript
// 单行文本
createTextItem('name', '姓名', {
placeholder: '请输入姓名',
required: true
})
// 多行文本
createTextItem('description', '描述', {
rows: 4,
placeholder: '请输入描述'
})
```
### 3. 选择器 (select)
```javascript
createSelectItem('status', '状态', [
{ label: '启用', value: 'enabled' },
{ label: '禁用', value: 'disabled' }
], {
placeholder: '请选择状态',
filterable: true
})
```
### 4. 日期选择 (date)
```javascript
createDateItem('birthday', '生日', {
dateType: 'date',
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD'
})
```
### 5. 开关 (switch)
```javascript
createSwitchItem('enabled', '启用状态', {
activeText: '启用',
inactiveText: '禁用'
})
```
### 6. 单选框组 (radio)
```javascript
createRadioItem('level', '等级', [
{ label: '初级', value: 'beginner' },
{ label: '中级', value: 'intermediate' },
{ label: '高级', value: 'advanced' }
])
```
### 7. 复选框组 (checkbox)
```javascript
createCheckboxItem('hobbies', '爱好', [
{ label: '阅读', value: 'reading' },
{ label: '运动', value: 'sports' },
{ label: '音乐', value: 'music' }
])
```
### 8. 滑块 (slider)
```javascript
createSliderItem('volume', '音量', {
min: 0,
max: 100,
step: 1,
showInput: true
})
```
## Props
| 参数 | 说明 | 类型 | 默认值 |
|------|------|------|--------|
| modelValue | 表单数据 | `Record<string, any>` | `{}` |
| items | 表单项配置 | `DynamicFormItem[]` | `[]` |
| title | 表单标题 | `string` | `''` |
| gutter | 列间距 | `number` | `16` |
| columnSpan | 列跨度 | `number` | `12` |
| formProp | 表单属性前缀 | `string` | `''` |
## Events
| 事件名 | 说明 | 参数 |
|--------|------|------|
| update:modelValue | 表单数据更新 | `(value: Record<string, any>)` |
| fieldChange | 单个字段变化 | `(key: string, value: any, item: DynamicFormItem)` |
| formChange | 整个表单变化 | `(value: Record<string, any>)` |
## Methods
| 方法名 | 说明 | 参数 | 返回值 |
|--------|------|------|--------|
| getFormData | 获取表单数据 | - | `Record<string, any>` |
| setFormData | 设置表单数据 | `data: Record<string, any>` | - |
| resetForm | 重置表单 | - | - |
| validateForm | 验证表单 | - | `boolean` |
## 自定义插槽
```vue
<template>
<DynamicForm v-model="formData" :items="formItems">
<template #customField="{ item, value, onChange }">
<el-input
v-model="value"
@input="onChange"
:placeholder="item.placeholder"
/>
</template>
</DynamicForm>
</template>
<script setup>
const formItems = [
{
key: 'customField',
label: '自定义字段',
type: 'custom',
slotName: 'customField',
placeholder: '请输入自定义内容'
}
]
</script>
```
## 高级用法
### 条件显示
```javascript
const formItems = computed(() => {
const items = [
createSelectItem('type', '类型', [
{ label: '类型A', value: 'typeA' },
{ label: '类型B', value: 'typeB' }
])
]
// 根据类型动态添加字段
if (formData.value.type === 'typeA') {
items.push(createTextItem('fieldA', '字段A'))
} else if (formData.value.type === 'typeB') {
items.push(createNumberItem('fieldB', '字段B'))
}
return items
})
```
### 表单验证
```javascript
const formItems = [
createTextItem('email', '邮箱', {
required: true,
rules: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
]
})
]
```
## 类型定义
```typescript
interface DynamicFormItem {
key: string
label: string
type: 'number' | 'text' | 'textarea' | 'select' | 'date' | 'time' | 'switch' | 'radio' | 'checkbox' | 'slider' | 'custom'
placeholder?: string
required?: boolean
disabled?: boolean
clearable?: boolean
rules?: any[]
// ... 更多配置项
}
```
## 注意事项
1. 每个表单项的 `key` 必须唯一
2. 选择器类型的表单项必须提供 `options` 配置
3. 自定义插槽类型需要指定 `slotName`
4. 表单数据会自动同步到 `v-model` 绑定的变量
5. 建议使用工具函数创建表单项,确保类型安全

View File

@@ -0,0 +1,48 @@
import DynamicForm from './index.vue'
import type {
DynamicFormItem,
DynamicFormConfig,
DynamicFormEvents,
DynamicFormMethods
} from './types'
import {
createNumberItem,
createTextItem,
createSelectItem,
createDateItem,
createSwitchItem,
createRadioItem,
createCheckboxItem,
createSliderItem,
createCustomItem,
createDynamicFormConfig,
validateFormItem,
validateFormConfig
} from './utils'
// 导出组件
export default DynamicForm
// 导出类型
export type {
DynamicFormItem,
DynamicFormConfig,
DynamicFormEvents,
DynamicFormMethods
}
// 导出工具函数
export {
createNumberItem,
createTextItem,
createSelectItem,
createDateItem,
createSwitchItem,
createRadioItem,
createCheckboxItem,
createSliderItem,
createCustomItem,
createDynamicFormConfig,
validateFormItem,
validateFormConfig
}

View File

@@ -0,0 +1,322 @@
<template>
<div class="dynamic-form">
<!-- 表单标题 -->
<el-divider v-if="title" content-position="left">{{ title }}</el-divider>
<!-- 动态表单项目 -->
<el-row :gutter="gutter">
<el-col v-for="item in formItems" :key="item.key" :span="getColumnSpan(item)">
<el-form-item :label="item.label" :required="item.required">
<div class="form-field-container">
<!-- 输入控件区域 -->
<div class="form-field-input" :class="{ 'with-standard': item.showStandard && item.standard }">
<!-- 数字输入 -->
<el-input-number v-if="item.type === 'number'" v-model="formData[item.key]" :min="item.min"
:max="item.max" :precision="item.precision || 0" :step="item.step || 1" :placeholder="item.placeholder"
:disabled="item.disabled" :clearable="item.clearable !== false" style="width: 100%"
@change="handleFieldChange(item.key, $event)" />
<!-- 文本输入 -->
<el-input v-else-if="item.type === 'text'" v-model="formData[item.key]" :type="item.inputType || 'text'"
:placeholder="item.placeholder" :disabled="item.disabled" :clearable="item.clearable !== false"
:rows="item.rows || 1" :maxlength="item.maxlength" :show-word-limit="!!item.maxlength"
@input="handleFieldChange(item.key, $event)" />
<!-- 多行文本 -->
<el-input v-else-if="item.type === 'textarea'" v-model="formData[item.key]" type="textarea"
:placeholder="item.placeholder" :disabled="item.disabled" :clearable="item.clearable !== false"
:rows="item.rows || 3" @input="handleFieldChange(item.key, $event)" />
<!-- 选择器 -->
<el-select v-else-if="item.type === 'select'" v-model="formData[item.key]" :placeholder="item.placeholder"
:disabled="item.disabled" :clearable="item.clearable !== false" :filterable="item.filterable"
:multiple="item.multiple" style="width: 100%" @change="handleFieldChange(item.key, $event)">
<el-option v-for="option in item.options" :key="option.value" :label="option.label"
:value="option.value" :disabled="option.disabled" />
</el-select>
<!-- 日期选择 -->
<el-date-picker v-else-if="item.type === 'date'" v-model="formData[item.key]"
:type="item.dateType || 'date'" :placeholder="item.placeholder" :disabled="item.disabled"
:clearable="item.clearable !== false" :format="item.format" :value-format="item.valueFormat"
style="width: 100%" @change="handleFieldChange(item.key, $event)" />
<!-- 时间选择 -->
<el-time-picker v-else-if="item.type === 'time'" v-model="formData[item.key]"
:placeholder="item.placeholder" :disabled="item.disabled" :clearable="item.clearable !== false"
:format="item.format || 'HH:mm:ss'" :value-format="item.valueFormat || 'HH:mm:ss'" style="width: 100%"
@change="handleFieldChange(item.key, $event)" />
<!-- 开关 -->
<el-switch v-else-if="item.type === 'switch'" v-model="formData[item.key]" :disabled="item.disabled"
:active-text="item.activeText" :inactive-text="item.inactiveText"
@change="handleFieldChange(item.key, $event)" />
<!-- 单选框组 -->
<el-radio-group v-else-if="item.type === 'radio'" v-model="formData[item.key]" :disabled="item.disabled"
@change="handleFieldChange(item.key, $event)">
<el-radio v-for="option in item.options" :key="option.value" :value="option.value"
:disabled="option.disabled">
{{ option.label }}
</el-radio>
</el-radio-group>
<!-- 复选框组 -->
<el-checkbox-group v-else-if="item.type === 'checkbox'" v-model="formData[item.key]"
:disabled="item.disabled" @change="handleFieldChange(item.key, $event)">
<el-checkbox v-for="option in item.options" :key="option.value" :value="option.value"
:disabled="option.disabled">
{{ option.label }}
</el-checkbox>
</el-checkbox-group>
<!-- 滑块 -->
<el-slider v-else-if="item.type === 'slider'" v-model="formData[item.key]" :min="item.min || 0"
:max="item.max || 100" :step="item.step || 1" :disabled="item.disabled" :show-input="item.showInput"
:show-stops="item.showStops" :show-tooltip="item.showTooltip"
@change="handleFieldChange(item.key, $event)" />
<!-- 自定义插槽 -->
<slot v-else-if="item.type === 'custom'" :name="item.slotName || item.key" :item="item"
:value="formData[item.key]" :onChange="(value) => handleFieldChange(item.key, value)"></slot>
<!-- 未知类型提示 -->
<div v-else class="unknown-type">
<el-alert :title="`未知的表单类型: ${item.type}`" type="warning" :closable="false" />
</div>
</div>
<!-- 质检标准显示区域 -->
<div
v-if="item.showStandard && (item.standard || item.standardValue || item.standardRange || item.standardOptions?.length)"
class="form-field-standard">
<el-tooltip :content="getStandardTooltip(item)" placement="top">
<div class="standard-display">
<Icon icon="ep:info-filled" class="standard-icon" />
<span class="standard-text">{{ getStandardDisplayText(item) }}</span>
</div>
</el-tooltip>
</div>
</div>
</el-form-item>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import type { DynamicFormItem } from './types'
// 定义Props
interface Props {
modelValue?: Record<string, any>
items?: DynamicFormItem[]
title?: string
gutter?: number
columnSpan?: number
formProp?: string
}
// 定义Emits
interface Emits {
(e: 'update:modelValue', value: Record<string, any>): void
(e: 'fieldChange', key: string, value: any, item: DynamicFormItem): void
(e: 'formChange', value: Record<string, any>): void
}
const props = withDefaults(defineProps<Props>(), {
modelValue: () => ({}),
items: () => [],
title: '',
gutter: 16,
columnSpan: 12,
formProp: ''
})
const emit = defineEmits<Emits>()
// 表单数据
const formData = ref<Record<string, any>>({ ...props.modelValue })
// 计算属性
const formItems = computed(() => props.items)
// 获取列跨度
const getColumnSpan = (item: DynamicFormItem) => {
if (item.fullWidth) return 24
return item.span || props.columnSpan
}
// 处理字段变化
const handleFieldChange = (key: string, value: any) => {
formData.value[key] = value
// 查找对应的表单项配置
const item = formItems.value.find(item => item.key === key)
// 触发事件
emit('fieldChange', key, value, item!)
emit('update:modelValue', { ...formData.value })
emit('formChange', { ...formData.value })
}
// 获取标准显示文本(只显示标准值)
const getStandardDisplayText = (item: DynamicFormItem): string => {
// 如果没有标准信息,不显示
if (!item.standardValue && !item.standardRange && !item.standardOptions?.length) {
return ''
}
// 根据标准类型显示标准值
switch (item.standardType) {
case 'text':
return item.standardValue || ''
case 'number':
return item.standardValue !== undefined ? String(item.standardValue) : ''
case 'range':
if (item.standardRange) {
const { min, max } = item.standardRange
// 如果只有最小值max为null、undefined或0
if (min !== undefined && min !== null && (max === undefined || max === null || max === 0)) {
return `${min}`
}
// 如果只有最大值min为null、undefined或0
if ((min === undefined || min === null || min === 0) && max !== undefined && max !== null) {
return `${max}`
}
// 如果有范围两个值都有且不为null/undefined/0
if (min !== undefined && min !== null && max !== undefined && max !== null && min !== 0 && max !== 0) {
return `${min}~${max}`
}
}
return ''
case 'select':
return item.standardOptions?.length ? item.standardOptions.map(opt => opt.label).join(', ') : ''
default:
return ''
}
}
// 获取标准提示内容(只显示标准描述)
const getStandardTooltip = (item: DynamicFormItem): string => {
// 只显示标准描述
return item.standard || ''
}
// 监听外部数据变化
watch(() => props.modelValue, (newValue) => {
formData.value = { ...newValue }
}, { deep: true })
// 暴露方法
const getFormData = () => {
return { ...formData.value }
}
const setFormData = (data: Record<string, any>) => {
formData.value = { ...data }
emit('update:modelValue', { ...formData.value })
}
const resetForm = () => {
formData.value = {}
emit('update:modelValue', {})
}
const validateForm = () => {
// 这里可以添加表单验证逻辑
return true
}
defineExpose({
getFormData,
setFormData,
resetForm,
validateForm
})
</script>
<style scoped>
.dynamic-form {
width: 100%;
}
.unknown-type {
margin: 8px 0;
}
.form-field-container {
display: flex;
align-items: flex-start;
gap: 12px;
}
.form-field-input {
flex: 1;
}
.form-field-input.with-standard {
flex: 0 0 calc(100% - 200px);
}
.form-field-standard {
flex: 0 0 180px;
margin-top: 4px;
}
.standard-display {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
}
.standard-display:hover {
background: #e0f2fe;
border-color: #7dd3fc;
}
.standard-icon {
color: #0ea5e9;
font-size: 14px;
}
.standard-text {
color: #0369a1;
font-size: 12px;
font-weight: 500;
line-height: 1.4;
word-break: break-all;
}
:deep(.el-form-item__label) {
font-weight: 500;
}
:deep(.el-form-item) {
margin-bottom: 18px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.form-field-container {
flex-direction: column;
gap: 8px;
}
.form-field-input.with-standard {
flex: 1;
}
.form-field-standard {
flex: 1;
}
}
</style>

View File

@@ -0,0 +1,83 @@
// 动态表单项目接口
export interface DynamicFormItem {
key: string
label: string
type: 'number' | 'text' | 'textarea' | 'select' | 'date' | 'time' | 'switch' | 'radio' | 'checkbox' | 'slider' | 'custom'
placeholder?: string
required?: boolean
disabled?: boolean
clearable?: boolean
rules?: any[]
defaultValue?: any
// 质检标准相关
standard?: string
standardType?: 'text' | 'number' | 'range' | 'select'
standardValue?: any
standardRange?: { min: number; max: number }
standardOptions?: Array<{ label: string; value: any; disabled?: boolean }>
showStandard?: boolean
// 数字输入相关
min?: number
max?: number
precision?: number
step?: number
// 文本输入相关
inputType?: string
rows?: number
// 选择器相关
options?: Array<{
label: string
value: any
disabled?: boolean
}>
filterable?: boolean
multiple?: boolean
// 日期时间相关
dateType?: 'date' | 'datetime' | 'daterange' | 'datetimerange'
format?: string
valueFormat?: string
// 开关相关
activeText?: string
inactiveText?: string
// 滑块相关
showInput?: boolean
showStops?: boolean
showTooltip?: boolean
// 自定义插槽
slotName?: string
// 布局相关
span?: number
fullWidth?: boolean
}
// 动态表单配置接口
export interface DynamicFormConfig {
title?: string
gutter?: number
columnSpan?: number
formProp?: string
items: DynamicFormItem[]
}
// 动态表单事件接口
export interface DynamicFormEvents {
fieldChange: (key: string, value: any, item: DynamicFormItem) => void
formChange: (value: Record<string, any>) => void
}
// 动态表单方法接口
export interface DynamicFormMethods {
getFormData: () => Record<string, any>
setFormData: (data: Record<string, any>) => void
resetForm: () => void
validateForm: () => boolean
}

View File

@@ -0,0 +1,322 @@
import type { DynamicFormItem, DynamicFormConfig } from './types'
/**
* 创建数字输入表单项
*/
export const createNumberItem = (
key: string,
label: string,
options: {
min?: number
max?: number
precision?: number
step?: number
placeholder?: string
required?: boolean
disabled?: boolean
span?: number
fullWidth?: boolean
} = {}
): DynamicFormItem => {
return {
key,
label,
type: 'number',
min: options.min,
max: options.max,
precision: options.precision || 0,
step: options.step || 1,
placeholder: options.placeholder || `请输入${label}`,
required: options.required,
disabled: options.disabled,
span: options.span,
fullWidth: options.fullWidth
}
}
/**
* 创建文本输入表单项
*/
export const createTextItem = (
key: string,
label: string,
options: {
inputType?: string
rows?: number
placeholder?: string
required?: boolean
disabled?: boolean
span?: number
fullWidth?: boolean
} = {}
): DynamicFormItem => {
return {
key,
label,
type: options.rows && options.rows > 1 ? 'textarea' : 'text',
inputType: options.inputType || 'text',
rows: options.rows,
placeholder: options.placeholder || `请输入${label}`,
required: options.required,
disabled: options.disabled,
span: options.span,
fullWidth: options.fullWidth
}
}
/**
* 创建选择器表单项
*/
export const createSelectItem = (
key: string,
label: string,
options: Array<{ label: string; value: any; disabled?: boolean }>,
config: {
placeholder?: string
required?: boolean
disabled?: boolean
filterable?: boolean
multiple?: boolean
span?: number
fullWidth?: boolean
} = {}
): DynamicFormItem => {
return {
key,
label,
type: 'select',
options,
placeholder: config.placeholder || `请选择${label}`,
required: config.required,
disabled: config.disabled,
filterable: config.filterable,
multiple: config.multiple,
span: config.span,
fullWidth: config.fullWidth
}
}
/**
* 创建日期选择表单项
*/
export const createDateItem = (
key: string,
label: string,
options: {
dateType?: 'date' | 'datetime' | 'daterange' | 'datetimerange'
format?: string
valueFormat?: string
placeholder?: string
required?: boolean
disabled?: boolean
span?: number
fullWidth?: boolean
} = {}
): DynamicFormItem => {
return {
key,
label,
type: 'date',
dateType: options.dateType || 'date',
format: options.format,
valueFormat: options.valueFormat,
placeholder: options.placeholder || `请选择${label}`,
required: options.required,
disabled: options.disabled,
span: options.span,
fullWidth: options.fullWidth
}
}
/**
* 创建开关表单项
*/
export const createSwitchItem = (
key: string,
label: string,
options: {
activeText?: string
inactiveText?: string
required?: boolean
disabled?: boolean
span?: number
fullWidth?: boolean
} = {}
): DynamicFormItem => {
return {
key,
label,
type: 'switch',
activeText: options.activeText,
inactiveText: options.inactiveText,
required: options.required,
disabled: options.disabled,
span: options.span,
fullWidth: options.fullWidth
}
}
/**
* 创建单选框组表单项
*/
export const createRadioItem = (
key: string,
label: string,
options: Array<{ label: string; value: any; disabled?: boolean }>,
config: {
required?: boolean
disabled?: boolean
span?: number
fullWidth?: boolean
} = {}
): DynamicFormItem => {
return {
key,
label,
type: 'radio',
options,
required: config.required,
disabled: config.disabled,
span: config.span,
fullWidth: config.fullWidth
}
}
/**
* 创建复选框组表单项
*/
export const createCheckboxItem = (
key: string,
label: string,
options: Array<{ label: string; value: any; disabled?: boolean }>,
config: {
required?: boolean
disabled?: boolean
span?: number
fullWidth?: boolean
} = {}
): DynamicFormItem => {
return {
key,
label,
type: 'checkbox',
options,
required: config.required,
disabled: config.disabled,
span: config.span,
fullWidth: config.fullWidth
}
}
/**
* 创建滑块表单项
*/
export const createSliderItem = (
key: string,
label: string,
options: {
min?: number
max?: number
step?: number
showInput?: boolean
showStops?: boolean
showTooltip?: boolean
required?: boolean
disabled?: boolean
span?: number
fullWidth?: boolean
} = {}
): DynamicFormItem => {
return {
key,
label,
type: 'slider',
min: options.min || 0,
max: options.max || 100,
step: options.step || 1,
showInput: options.showInput,
showStops: options.showStops,
showTooltip: options.showTooltip,
required: options.required,
disabled: options.disabled,
span: options.span,
fullWidth: options.fullWidth
}
}
/**
* 创建自定义插槽表单项
*/
export const createCustomItem = (
key: string,
label: string,
slotName: string,
options: {
required?: boolean
disabled?: boolean
span?: number
fullWidth?: boolean
} = {}
): DynamicFormItem => {
return {
key,
label,
type: 'custom',
slotName,
required: options.required,
disabled: options.disabled,
span: options.span,
fullWidth: options.fullWidth
}
}
/**
* 创建动态表单配置
*/
export const createDynamicFormConfig = (
items: DynamicFormItem[],
config: {
title?: string
gutter?: number
columnSpan?: number
formProp?: string
} = {}
): DynamicFormConfig => {
return {
title: config.title,
gutter: config.gutter || 16,
columnSpan: config.columnSpan || 12,
formProp: config.formProp,
items
}
}
/**
* 验证表单项配置
*/
export const validateFormItem = (item: DynamicFormItem): boolean => {
if (!item.key || !item.label || !item.type) {
console.error('表单项配置不完整:', item)
return false
}
// 验证选择器类型必须有选项
if (['select', 'radio', 'checkbox'].includes(item.type) && (!item.options || item.options.length === 0)) {
console.error('选择器类型表单项必须有选项:', item)
return false
}
return true
}
/**
* 验证表单配置
*/
export const validateFormConfig = (config: DynamicFormConfig): boolean => {
if (!config.items || config.items.length === 0) {
console.error('表单配置必须有表单项')
return false
}
return config.items.every(validateFormItem)
}

View File

@@ -0,0 +1,45 @@
import FormBuilder from './index.vue'
import type {
FormBuilderItem,
FormBuilderConfig,
FormBuilderEvents,
FormBuilderMethods
} from './types'
import {
createFormBuilderItem,
createFormBuilderConfig,
validateFormBuilderItem,
validateFormBuilderConfig,
generateDefaultItems,
importFromJson,
exportToJson,
duplicateItem,
createBatchItems
} from './utils'
// 导出组件
export default FormBuilder
// 导出类型
export type {
FormBuilderItem,
FormBuilderConfig,
FormBuilderEvents,
FormBuilderMethods
}
// 导出工具函数
export {
createFormBuilderItem,
createFormBuilderConfig,
validateFormBuilderItem,
validateFormBuilderConfig,
generateDefaultItems,
importFromJson,
exportToJson,
duplicateItem,
createBatchItems
}

View File

@@ -0,0 +1,706 @@
<template>
<div class="form-builder">
<!-- 表单构建器标题 -->
<div class="form-builder-header">
<h4>{{ title }}</h4>
<div class="header-actions">
<el-select v-model="selectedFieldType" placeholder="选择字段类型" style="width: 120px; margin-right: 8px;">
<el-option v-for="type in availableFieldTypes" :key="type" :label="getFieldTypeLabel(type)" :value="type" />
</el-select>
<el-button type="primary" @click="() => addItem(selectedFieldType)">
<Icon icon="ep:plus" />
添加项目
</el-button>
</div>
</div>
<!-- 表单项列表 -->
<div class="form-items-list">
<div v-for="(item, index) in formItems" :key="item.id" class="form-item-card">
<div class="form-item-header">
<span class="item-index"> {{ index + 1 }} 个质检项目</span>
<div class="item-actions">
<el-button type="primary" size="small" @click="moveUp(index)" :disabled="index === 0">
<Icon icon="ep:arrow-up" />
</el-button>
<el-button type="primary" size="small" @click="moveDown(index)" :disabled="index === formItems.length - 1">
<Icon icon="ep:arrow-down" />
</el-button>
<el-button type="danger" size="small" @click="removeItem(index)">
<Icon icon="ep:delete" />
</el-button>
</div>
</div>
<div class="form-item-content">
<el-row :gutter="16">
<el-col :span="6">
<el-form-item label="项目名称" :prop="`items.${index}.label`">
<el-input v-model="item.label" placeholder="请输入项目标题" @input="handleItemChange" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="项目key建议保持默认" :prop="`items.${index}.key`">
<el-input v-model="item.key" placeholder="请输入项目键值" @input="handleItemChange" />
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="输入类型" :prop="`items.${index}.type`">
<el-select v-model="item.type" placeholder="选择类型" @change="handleTypeChange(index, $event)">
<el-option v-for="type in availableFieldTypes" :key="type" :label="getFieldTypeLabel(type)"
:value="type" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="默认值" :prop="`items.${index}.defaultValue`">
<!-- 根据字段类型显示不同的输入控件 -->
<el-input v-if="item.type === 'text' || item.type === 'textarea'" v-model="item.defaultValue"
:type="item.type === 'textarea' ? 'textarea' : 'text'" :rows="item.type === 'textarea' ? 2 : 1"
placeholder="请输入默认值" @input="handleItemChange" />
<el-input-number v-else-if="item.type === 'number'" v-model="item.defaultValue" placeholder="请输入数字"
@change="handleItemChange" style="width: 100%" />
<el-select v-else-if="item.type === 'select'" v-model="item.defaultValue" placeholder="请选择默认值"
@change="handleItemChange" style="width: 100%">
<el-option label="选项1" value="option1" />
<el-option label="选项2" value="option2" />
</el-select>
<el-switch v-else-if="item.type === 'switch'" v-model="item.defaultValue" @change="handleItemChange" />
<el-date-picker v-else-if="item.type === 'date'" v-model="item.defaultValue" type="date"
placeholder="选择日期" @change="handleItemChange" style="width: 100%" />
<el-input v-else v-model="item.defaultValue" placeholder="请输入默认值" @input="handleItemChange" />
</el-form-item>
</el-col>
</el-row>
<!-- text类型特殊配置 -->
<el-row :gutter="16" v-if="item.type === 'text'" style="margin-top: 16px; padding: 16px; background: #f0f9ff; border-radius: 8px; border: 1px solid #e0f2fe;">
<el-col :span="24">
<div style="display: flex; align-items: center; margin-bottom: 12px;">
<Icon icon="ep:document-copy" style="color: #0ea5e9; margin-right: 8px;" />
<span style="font-weight: 600; color: #0f172a;">文本字段配置</span>
<el-tooltip content="为文本字段设置显示行数和最大长度限制" placement="top">
<Icon icon="ep:question-filled" style="color: #64748b; margin-left: 8px; cursor: help;" />
</el-tooltip>
</div>
</el-col>
<el-col :span="12">
<el-form-item label="显示行数">
<el-input-number v-model="item.rows" :min="1" :max="10" placeholder="行数" @change="handleItemChange" style="width: 100%" />
<div style="font-size: 12px; color: #64748b; margin-top: 4px;">
设置文本框的显示行数1-10
</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="最大长度">
<el-input-number v-model="item.maxlength" :min="1" :max="10000" placeholder="字符数" @change="handleItemChange" style="width: 100%" />
<div style="font-size: 12px; color: #64748b; margin-top: 4px;">
设置允许输入的最大字符数
</div>
</el-form-item>
</el-col>
</el-row>
<!-- 质检标准配置 -->
<el-row :gutter="16"
style="margin-top: 16px; padding: 16px; background: #f8f9fa; border-radius: 8px; border: 1px solid #e9ecef;">
<el-col :span="24">
<div style="display: flex; align-items: center; margin-bottom: 12px;">
<Icon icon="ep:info-filled" style="color: #409eff; margin-right: 8px;" />
<span style="font-weight: 600; color: #303133;">质检标准配置</span>
<el-tooltip content="为这个字段设置质检标准,帮助质检员了解检验要求" placement="top">
<Icon icon="ep:question-filled" style="color: #909399; margin-left: 8px; cursor: help;" />
</el-tooltip>
</div>
</el-col>
<el-col :span="8">
<el-form-item label="标准描述" :required="true">
<el-input v-model="item.standard" placeholder="例如:重量必须符合标准要求" @input="handleItemChange" />
<div style="font-size: 12px; color: #909399; margin-top: 4px;">
描述这个字段的质检标准要求
</div>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="标准类型">
<div style="padding: 8px 12px; background: #f5f7fa; border-radius: 4px; color: #606266; font-size: 14px;">
{{ getStandardTypeLabel(item.standardType) }}
</div>
<div style="font-size: 12px; color: #909399; margin-top: 4px;">
根据字段类型自动设置
</div>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item :label="getStandardValueLabel(item.standardType)">
<el-input v-if="item.standardType === 'text'" v-model="item.standardValue" placeholder="例如:合格"
@input="handleItemChange" />
<div v-else-if="item.standardType === 'range'" style="display: flex; gap: 8px;">
<el-input-number v-model="item.standardRange!.min" placeholder="最小值" @change="handleItemChange"
style="flex: 1" />
<span style="line-height: 32px;">~</span>
<el-input-number v-model="item.standardRange!.max" placeholder="最大值" @change="handleItemChange"
style="flex: 1" />
</div>
<el-button v-else-if="item.standardType === 'select'" type="primary" size="small" @click="editStandardOptions(index)">
<Icon icon="ep:setting" />
编辑选项
</el-button>
<div style="font-size: 12px; color: #909399; margin-top: 4px;">
{{ getStandardValueHint(item.standardType) }}
</div>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="显示标准">
<el-switch v-model="item.showStandard" @change="handleItemChange" />
<div style="font-size: 12px; color: #909399; margin-top: 4px;">
在表单中显示标准
</div>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="必填">
<el-switch v-model="item.required" @change="handleItemChange" />
<div style="font-size: 12px; color: #909399; margin-top: 4px;">
此字段是否必填
</div>
</el-form-item>
</el-col>
</el-row>
</div>
</div>
<!-- 空状态 -->
<div v-if="formItems.length === 0" class="empty-state">
<el-empty description="暂无表单项,点击上方按钮添加">
<el-button type="primary" @click="() => addItem()">
<Icon icon="ep:plus" />
添加第一个项目
</el-button>
</el-empty>
</div>
</div>
<!-- 预览区域 -->
<div v-if="formItems.length > 0" class="preview-section">
<el-divider content-position="left">预览效果</el-divider>
<div class="preview-content">
<DynamicForm v-model="previewData" :items="dynamicFormItems" :gutter="16" :column-span="12"
@form-change="handlePreviewChange" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import DynamicForm, { type DynamicFormItem } from '@/components/DynamicForm'
import type { FormBuilderItem, FormFieldType } from './types'
// 定义Props
interface Props {
modelValue?: FormBuilderItem[]
title?: string
mode?: 'simple' | 'advanced' | 'custom'
showPreview?: boolean
allowDrag?: boolean
customFieldTypes?: FormFieldType[]
}
// 定义Emits
interface Emits {
(e: 'update:modelValue', value: FormBuilderItem[]): void
(e: 'change', value: FormBuilderItem[]): void
(e: 'preview-change', value: Record<string, any>): void
(e: 'itemAdd', item: FormBuilderItem): void
(e: 'itemRemove', item: FormBuilderItem): void
(e: 'itemUpdate', item: FormBuilderItem, index: number): void
}
const props = withDefaults(defineProps<Props>(), {
modelValue: () => [],
title: '表单构建器',
mode: 'simple',
showPreview: true,
allowDrag: true,
customFieldTypes: () => ['text', 'number', 'select']
})
const emit = defineEmits<Emits>()
// 表单数据
const formItems = ref<FormBuilderItem[]>([...props.modelValue])
const previewData = ref<Record<string, any>>({})
const selectedFieldType = ref<FormFieldType>('text')
// 初始化时确保标准字段有默认值
formItems.value.forEach(item => {
if (!item.standard) item.standard = ''
if (!item.standardType) item.standardType = 'text'
if (!item.standardValue) item.standardValue = ''
if (!item.standardRange) item.standardRange = { min: 0, max: 100 }
if (!item.standardOptions) item.standardOptions = []
if (item.showStandard === undefined) item.showStandard = true
})
// 可用的字段类型
const availableFieldTypes = computed(() => {
return props.customFieldTypes || ['text', 'number', 'select']
})
// 获取字段类型标签
const getFieldTypeLabel = (type: FormFieldType): string => {
const labels: Record<FormFieldType, string> = {
text: '文本',
number: '数字',
select: '选择器',
textarea: '多行文本',
date: '日期',
switch: '开关',
radio: '单选框',
checkbox: '复选框',
custom: '自定义',
time: '时间',
slider: '滑块'
}
return labels[type] || type
}
// 生成唯一ID
const generateId = () => {
return `item_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
}
// 转换为DynamicFormItem格式
const dynamicFormItems = computed<DynamicFormItem[]>(() => {
return formItems.value.map(item => ({
key: item.key,
label: item.label,
type: item.type,
placeholder: item.placeholder || `请输入${item.label}`,
defaultValue: item.defaultValue,
required: item.required,
disabled: item.disabled,
// 传递质检标准相关属性
standard: item.standard,
standardType: item.standardType,
standardValue: item.standardValue,
standardRange: item.standardRange,
standardOptions: item.standardOptions,
showStandard: item.showStandard,
// 传递其他属性
...(item.type === 'number' && {
min: item.min,
max: item.max,
step: item.step,
precision: item.precision
}),
...(item.type === 'textarea' && {
rows: item.rows,
maxlength: item.maxlength
}),
...(item.type === 'select' && {
options: item.options || []
}),
...(item.type === 'date' && {
dateType: item.dateType || 'date',
format: item.format,
valueFormat: item.valueFormat
}),
...(item.type === 'switch' && {
activeText: item.activeText,
inactiveText: item.inactiveText
}),
rules: item.rules
}))
})
// 添加表单项
const addItem = (type: FormFieldType = 'text') => {
// 根据字段类型自动设置标准类型
const standardType = getDefaultStandardType(type)
const newItem: FormBuilderItem = {
id: generateId(),
label: `项目${formItems.value.length + 1}`,
key: `item_${formItems.value.length + 1}`,
type,
defaultValue: getDefaultValueByType(type),
placeholder: `请输入${type === 'text' ? '文本' : type === 'number' ? '数字' : '内容'}`,
// text类型特有属性
...(type === 'text' && {
rows: 1,
maxlength: 100
}),
// 初始化标准字段(根据类型自动设置)
standard: '',
standardType,
standardValue: '',
standardRange: { min: 0, max: 100 },
standardOptions: [],
showStandard: true,
required: false
}
formItems.value.push(newItem)
handleItemChange()
emit('itemAdd', newItem)
}
// 根据字段类型获取默认标准类型
const getDefaultStandardType = (type: FormFieldType): string => {
switch (type) {
case 'text':
return 'text'
case 'number':
return 'range'
case 'select':
return 'select'
default:
return 'text'
}
}
// 获取标准类型显示标签
const getStandardTypeLabel = (standardType: string): string => {
const labels: Record<string, string> = {
text: '📄 文本标准',
number: '🔢 数值标准',
range: '📊 范围标准',
select: '✅ 选择标准'
}
return labels[standardType] || standardType
}
// 获取标准值标签
const getStandardValueLabel = (standardType: string): string => {
switch (standardType) {
case 'text':
return '期望文本'
case 'range':
return '数值范围'
case 'select':
return '标准选项'
default:
return '标准值'
}
}
// 获取标准值提示
const getStandardValueHint = (standardType: string): string => {
switch (standardType) {
case 'text':
return '输入期望的文本值'
case 'range':
return '设置允许的数值范围'
case 'select':
return '设置可选的标准选项'
default:
return '设置标准值'
}
}
// 根据类型获取默认值
const getDefaultValueByType = (type: FormFieldType): any => {
switch (type) {
case 'number':
return 0
case 'switch':
return false
case 'select':
case 'radio':
case 'checkbox':
return []
case 'date':
return null
default:
return ''
}
}
// 删除表单项
const removeItem = (index: number) => {
formItems.value.splice(index, 1)
handleItemChange()
}
// 上移表单项
const moveUp = (index: number) => {
if (index > 0) {
const item = formItems.value.splice(index, 1)[0]
formItems.value.splice(index - 1, 0, item)
handleItemChange()
}
}
// 下移表单项
const moveDown = (index: number) => {
if (index < formItems.value.length - 1) {
const item = formItems.value.splice(index, 1)[0]
formItems.value.splice(index + 1, 0, item)
handleItemChange()
}
}
// 处理字段类型变化
const handleTypeChange = (index: number, newType: FormFieldType) => {
const item = formItems.value[index]
if (item) {
// 根据新的字段类型自动更新标准类型
item.standardType = getDefaultStandardType(newType)
// 重置相关字段的值
item.standardValue = ''
item.standardRange = { min: 0, max: 100 }
item.standardOptions = []
// 为text类型添加默认属性
if (newType === 'text') {
item.rows = item.rows || 1
item.maxlength = item.maxlength || 100
}
}
handleItemChange()
}
// 处理表单项变化
const handleItemChange = () => {
// 确保所有标准字段都有值,并根据字段类型自动设置标准类型
formItems.value.forEach(item => {
if (!item.standard) item.standard = ''
if (!item.standardType) {
// 如果没有标准类型,根据字段类型设置默认值
item.standardType = getDefaultStandardType(item.type)
}
if (!item.standardValue) item.standardValue = ''
if (!item.standardRange) item.standardRange = { min: 0, max: 100 }
if (!item.standardOptions) item.standardOptions = []
if (item.showStandard === undefined) item.showStandard = true
})
emit('update:modelValue', [...formItems.value])
emit('change', [...formItems.value])
// 更新预览数据
updatePreviewData()
}
// 更新预览数据
const updatePreviewData = () => {
const newPreviewData: Record<string, any> = {}
formItems.value.forEach(item => {
if (item.defaultValue !== null && item.defaultValue !== undefined && item.defaultValue !== '') {
newPreviewData[item.key] = item.defaultValue
}
})
previewData.value = newPreviewData
emit('preview-change', newPreviewData)
}
// 处理预览数据变化
const handlePreviewChange = (value: Record<string, any>) => {
console.log('FormBuilder预览数据变化:', value)
previewData.value = value
emit('preview-change', value)
}
// 监听外部数据变化
watch(() => props.modelValue, (newValue) => {
formItems.value = [...newValue]
// 确保标准字段有默认值
formItems.value.forEach(item => {
if (!item.standard) item.standard = ''
if (!item.standardType) item.standardType = 'text'
if (!item.standardValue) item.standardValue = ''
if (!item.standardRange) item.standardRange = { min: 0, max: 100 }
if (!item.standardOptions) item.standardOptions = []
if (item.showStandard === undefined) item.showStandard = true
})
updatePreviewData()
}, { deep: true })
// 暴露方法
const getFormItems = () => {
return [...formItems.value]
}
const setFormItems = (items: FormBuilderItem[]) => {
formItems.value = [...items]
handleItemChange()
}
const clearItems = () => {
formItems.value = []
handleItemChange()
}
const addItemWithData = (label: string, key: string, type: FormFieldType = 'text', defaultValue: any = '') => {
const standardType = getDefaultStandardType(type)
const newItem: FormBuilderItem = {
id: generateId(),
label,
key,
type,
defaultValue,
// 初始化标准字段
standard: '',
standardType,
standardValue: '',
standardRange: { min: 0, max: 100 },
standardOptions: [],
showStandard: true,
required: false
}
formItems.value.push(newItem)
handleItemChange()
}
// 编辑标准选项
const editStandardOptions = (index: number) => {
const item = formItems.value[index]
if (!item.standardOptions) {
item.standardOptions = []
}
// 构建当前选项的文本
const currentOptions = item.standardOptions.map(opt => opt.label).join('')
// 使用更友好的提示
const newOptions = prompt(
`📋 编辑标准选项\n\n当前选项${currentOptions || '无'}\n\n请输入新的标准选项用逗号分隔\n例如合格,不合格,待定`,
currentOptions
)
if (newOptions !== null) {
if (newOptions.trim()) {
// 解析新选项
const options = newOptions.split(',').map((label, idx) => ({
label: label.trim(),
value: `option_${idx + 1}`,
disabled: false
})).filter(opt => opt.label) // 过滤空选项
item.standardOptions = options
handleItemChange()
// 显示成功提示
console.log(`已设置 ${options.length} 个标准选项:${options.map(opt => opt.label).join('')}`)
} else {
// 清空选项
item.standardOptions = []
handleItemChange()
}
}
}
defineExpose({
getFormItems,
setFormItems,
clearItems,
addItemWithData
})
</script>
<style scoped>
.form-builder {
width: 100%;
}
.form-builder-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid #e4e7ed;
}
.header-actions {
display: flex;
align-items: center;
}
.form-builder-header h4 {
margin: 0;
color: #303133;
font-size: 16px;
font-weight: 600;
}
.form-items-list {
margin-bottom: 24px;
}
.form-item-card {
border: 1px solid #e4e7ed;
border-radius: 8px;
margin-bottom: 16px;
background: #fff;
transition: all 0.3s ease;
}
.form-item-card:hover {
border-color: #409eff;
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.1);
}
.form-item-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #f5f7fa;
border-bottom: 1px solid #e4e7ed;
border-radius: 8px 8px 0 0;
}
.item-index {
font-weight: 500;
color: #606266;
}
.item-actions {
display: flex;
gap: 8px;
}
.form-item-content {
padding: 16px;
}
.empty-state {
text-align: center;
padding: 40px 20px;
background: #fafafa;
border: 2px dashed #d9d9d9;
border-radius: 8px;
}
.preview-section {
margin-top: 24px;
padding-top: 24px;
border-top: 1px solid #e4e7ed;
}
.preview-content {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
border: 1px solid #e4e7ed;
}
:deep(.el-form-item) {
margin-bottom: 16px;
}
:deep(.el-form-item__label) {
font-weight: 500;
color: #606266;
}
:deep(.el-button + .el-button) {
margin-left: 8px;
}
</style>

View File

@@ -0,0 +1,82 @@
// 表单字段类型
export type FormFieldType = 'text' | 'number' | 'select' | 'textarea' | 'date' | 'switch' | 'radio' | 'checkbox' | 'custom' | 'time' | 'slider'
// 表单字段选项
export interface FormFieldOption {
label: string
value: any
disabled?: boolean
}
// 表单构建器项目接口
export interface FormBuilderItem {
id: string
label: string
key: string
type: FormFieldType
defaultValue: any
placeholder?: string
required?: boolean
disabled?: boolean
// 质检标准相关
standard?: string // 质检标准描述
standardType?: 'text' | 'number' | 'range' | 'select' // 标准类型
standardValue?: any // 标准值
standardRange?: { min: number; max: number } // 标准范围
standardOptions?: FormFieldOption[] // 标准选项
showStandard?: boolean // 是否显示标准
// 选择器相关
options?: FormFieldOption[]
// 数字输入相关
min?: number
max?: number
step?: number
precision?: number
// 文本输入相关
rows?: number
maxlength?: number
// 日期相关
dateType?: 'date' | 'datetime' | 'daterange' | 'datetimerange'
format?: string
valueFormat?: string
// 开关相关
activeText?: string
inactiveText?: string
// 自定义验证规则
rules?: any[]
}
// 表单构建器配置接口
export interface FormBuilderConfig {
title?: string
items: FormBuilderItem[]
// 使用场景配置
mode?: 'simple' | 'advanced' | 'custom'
// 是否显示预览
showPreview?: boolean
// 是否允许拖拽排序
allowDrag?: boolean
// 自定义字段类型
customFieldTypes?: FormFieldType[]
}
// 表单构建器事件接口
export interface FormBuilderEvents {
change: (items: FormBuilderItem[]) => void
itemAdd: (item: FormBuilderItem) => void
itemRemove: (item: FormBuilderItem) => void
itemUpdate: (item: FormBuilderItem, index: number) => void
}
// 表单构建器方法接口
export interface FormBuilderMethods {
getFormItems: () => FormBuilderItem[]
setFormItems: (items: FormBuilderItem[]) => void
clearItems: () => void
addItemWithData: (label: string, key: string, type?: FormFieldType, defaultValue?: any) => void
exportConfig: () => FormBuilderConfig
importConfig: (config: FormBuilderConfig) => void
}

View File

@@ -0,0 +1,147 @@
import type { FormBuilderItem, FormBuilderConfig } from './types'
/**
* 创建表单构建器项目
*/
export const createFormBuilderItem = (
label: string,
key: string,
defaultValue: string = '',
id?: string
): FormBuilderItem => {
return {
id: id || `item_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
label,
key,
defaultValue
}
}
/**
* 创建表单构建器配置
*/
export const createFormBuilderConfig = (
items: FormBuilderItem[],
title: string = '表单构建器'
): FormBuilderConfig => {
return {
title,
items
}
}
/**
* 验证表单构建器项目
*/
export const validateFormBuilderItem = (item: FormBuilderItem): boolean => {
if (!item.id || !item.label || !item.key) {
console.error('表单构建器项目配置不完整:', item)
return false
}
// 验证键值格式
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(item.key)) {
console.error('键值格式不正确,只能包含字母、数字和下划线,且不能以数字开头:', item.key)
return false
}
return true
}
/**
* 验证表单构建器配置
*/
export const validateFormBuilderConfig = (config: FormBuilderConfig): boolean => {
if (!config.items || config.items.length === 0) {
console.error('表单构建器配置必须有表单项')
return false
}
// 检查键值是否重复
const keys = config.items.map(item => item.key)
const uniqueKeys = new Set(keys)
if (keys.length !== uniqueKeys.size) {
console.error('表单项的键值不能重复')
return false
}
return config.items.every(validateFormBuilderItem)
}
/**
* 生成默认的表单构建器项目
*/
export const generateDefaultItems = (count: number = 3): FormBuilderItem[] => {
const items: FormBuilderItem[] = []
for (let i = 1; i <= count; i++) {
items.push(createFormBuilderItem(
`项目${i}`,
`item_${i}`,
''
))
}
return items
}
/**
* 从JSON数据导入表单构建器项目
*/
export const importFromJson = (jsonData: any[]): FormBuilderItem[] => {
const items: FormBuilderItem[] = []
jsonData.forEach((item, index) => {
if (typeof item === 'object' && item !== null) {
items.push(createFormBuilderItem(
item.label || `项目${index + 1}`,
item.key || `item_${index + 1}`,
item.defaultValue || '',
item.id
))
}
})
return items
}
/**
* 导出表单构建器项目为JSON
*/
export const exportToJson = (items: FormBuilderItem[]): string => {
return JSON.stringify(items, null, 2)
}
/**
* 复制表单构建器项目
*/
export const duplicateItem = (item: FormBuilderItem): FormBuilderItem => {
return createFormBuilderItem(
`${item.label}_副本`,
`${item.key}_copy`,
item.defaultValue
)
}
/**
* 批量创建表单构建器项目
*/
export const createBatchItems = (
labels: string[],
keys?: string[],
defaultValues?: string[]
): FormBuilderItem[] => {
const items: FormBuilderItem[] = []
labels.forEach((label, index) => {
const key = keys?.[index] || `item_${index + 1}`
const defaultValue = defaultValues?.[index] || ''
items.push(createFormBuilderItem(label, key, defaultValue))
})
return items
}

View File

@@ -6,6 +6,7 @@ import { Setting } from '@/layout/components/Setting'
import { useRenderLayout } from './components/useRenderLayout' import { useRenderLayout } from './components/useRenderLayout'
import { useDesign } from '@/hooks/web/useDesign' import { useDesign } from '@/hooks/web/useDesign'
import MobileLayout from './components/MobileLayout.vue' import MobileLayout from './components/MobileLayout.vue'
import AiChatFloat from '@/layout/components/AiChatFloat/index.vue'
const { getPrefixCls } = useDesign() const { getPrefixCls } = useDesign()
@@ -64,7 +65,7 @@ export default defineComponent({
{renderLayout()} {renderLayout()}
<Backtop></Backtop> <Backtop></Backtop>
<AiChatFloat></AiChatFloat>
<Setting></Setting> <Setting></Setting>
</section> </section>
)} )}

View File

@@ -24,13 +24,14 @@ const pageTitle = computed(() => {
const tabs = [ const tabs = [
{ name: 'Index', path: '/index', label: '首页', icon: 'home' }, { name: 'Index', path: '/index', label: '首页', icon: 'home' },
{ name: 'Workbench', path: '/index', label: '工作台', icon: 'workbench' }, { name: 'Workbench', path: '/index', label: '工作台', icon: 'workbench' },
{ name: 'Function', path: '/index', label: '功能', icon: 'function' }, { name: 'Function', path: '/function', label: '功能', icon: 'function' },
{ name: 'Mine', path: '/user/center', label: '我的', icon: 'mine' } { name: 'Mine', path: '/user/center', label: '我的', icon: 'mine' }
] ]
const activeTab = computed(() => { const activeTab = computed(() => {
const p = route.path const p = route.path
if (p === '/index' || p === '/') return 'Index' if (p === '/index' || p === '/') return 'Index'
if (p === '/function' || p.startsWith('/function/')) return 'Function'
if (p.includes('/user/center')) return 'Mine' if (p.includes('/user/center')) return 'Mine'
return 'Index' return 'Index'
}) })

View File

@@ -128,6 +128,28 @@ const remainingRouter: AppRouteRecordRaw[] = [
} }
] ]
}, },
{
path: '/function',
component: Layout,
name: 'FunctionCenter',
meta: {
hidden: true
},
children: [
{
path: '',
component: () => import('@/views/function/index.vue'),
name: 'FunctionIndex',
meta: {
canTo: true,
hidden: true,
noTagsView: true,
icon: 'ep:grid',
title: '功能'
}
}
]
},
{ {
path: '/dict', path: '/dict',
component: Layout, component: Layout,

View File

@@ -316,6 +316,19 @@ export function getLast1Year(): [dayjs.ConfigType, dayjs.ConfigType] {
return getDateRange(lastYearDay, yesterday) return getDateRange(lastYearDay, yesterday)
} }
export const formatTime = (timestamp?: number) => {
if (!timestamp) return '-'
const d = new Date(timestamp)
const pad = (n: number) => n.toString().padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ` +
`${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
}
export const formatDate2 = (date: any) => {
if (!date) return '-'
return formatDate(new Date(date), 'YYYY-MM-DD')
}
/** /**
* 获取指定日期的开始时间、截止时间 * 获取指定日期的开始时间、截止时间
* @param beginDate 开始日期 * @param beginDate 开始日期

View File

@@ -5,21 +5,28 @@
<div class="mobile-home__banner-bg"> <div class="mobile-home__banner-bg">
<div class="mobile-home__banner-content"> <div class="mobile-home__banner-content">
<div class="mobile-home__brand"> <div class="mobile-home__brand">
<div class="mobile-home__brand-logo">亚为MOM</div> <div class="mobile-home__brand-logo">
<div class="mobile-home__brand-text"> <img src="@/assets/imgs/logo.png" alt="Logo" class="mobile-home__logo-icon" />
<div class="mobile-home__brand-title">智能一体化管理系统</div> <div class="mobile-home__logo-text">
<div class="mobile-home__brand-sub">私有化部署可定制</div> <div class="mobile-home__logo-title">亚为MOM</div>
<div class="mobile-home__logo-subtitle">Manufacturing Operations Management</div>
</div>
</div>
<div class="mobile-home__brand-desc">
<div class="mobile-home__brand-title">智能制造一体化管理平台</div>
<div class="mobile-home__brand-tags">
<span class="tag">私有化部署</span>
<span class="tag">可定制</span>
<span class="tag">安全可靠</span>
</div>
</div> </div>
</div> </div>
<div class="mobile-home__banner-deco"> </div>
<svg width="80" height="80" viewBox="0 0 80 80" fill="none"> <!-- 装饰元素 -->
<rect x="10" y="20" width="25" height="35" rx="3" fill="rgba(255,255,255,0.3)"/> <div class="mobile-home__banner-circles">
<rect x="40" y="10" width="30" height="45" rx="3" fill="rgba(255,255,255,0.2)"/> <div class="circle circle-1"></div>
<circle cx="55" cy="60" r="15" fill="rgba(255,255,255,0.15)"/> <div class="circle circle-2"></div>
<rect x="15" y="58" width="20" height="3" rx="1" fill="rgba(255,255,255,0.4)"/> <div class="circle circle-3"></div>
<rect x="15" y="64" width="14" height="3" rx="1" fill="rgba(255,255,255,0.3)"/>
</svg>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -27,12 +34,8 @@
<!-- 常用功能区 --> <!-- 常用功能区 -->
<div class="mobile-home__section"> <div class="mobile-home__section">
<div class="mobile-home__section-header"> <div class="mobile-home__section-header">
<div class="mobile-home__section-icon">
<svg viewBox="0 0 24 24" width="18" height="18" fill="#409eff">
<path d="M3.5 18.49l6-6.01 4 4L22 6.92l-1.41-1.41-7.09 7.97-4-4L2 16.99z"/>
</svg>
</div>
<span class="mobile-home__section-title">常用功能</span> <span class="mobile-home__section-title">常用功能</span>
<span class="mobile-home__section-more">全部 ></span>
</div> </div>
<div class="mobile-home__grid" v-loading="loading"> <div class="mobile-home__grid" v-loading="loading">
@@ -42,7 +45,7 @@
class="mobile-home__grid-item" class="mobile-home__grid-item"
@click="handleShortcutClick(item.url)" @click="handleShortcutClick(item.url)"
> >
<div class="mobile-home__grid-icon" :style="{ color: item.color, background: item.color + '18' }"> <div class="mobile-home__grid-icon" :style="{ background: item.color }">
<Icon :icon="item.icon" /> <Icon :icon="item.icon" />
</div> </div>
<span class="mobile-home__grid-label">{{ item.name }}</span> <span class="mobile-home__grid-label">{{ item.name }}</span>
@@ -131,129 +134,251 @@ getAllApi()
<style lang="scss" scoped> <style lang="scss" scoped>
.mobile-home { .mobile-home {
background: #f5f5f5; background: linear-gradient(180deg, #f0f4f8 0%, #f5f7fa 100%);
min-height: 100%; min-height: 100vh;
padding-bottom: 16px; padding-bottom: 20px;
} }
/* Banner */ /* Banner 区域 */
.mobile-home__banner { .mobile-home__banner {
padding: 0 12px; margin-bottom: 16px;
margin-bottom: 12px;
} }
.mobile-home__banner-bg { .mobile-home__banner-bg {
background: linear-gradient(135deg, #43cea2 0%, #2a9d8f 50%, #264653 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 0 0 16px 16px; border-radius: 0 0 24px 24px;
padding: 20px 16px 24px; padding: 24px 16px 32px;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.3);
} }
.mobile-home__banner-content { .mobile-home__banner-content {
display: flex; position: relative;
justify-content: space-between; z-index: 2;
align-items: center;
} }
.mobile-home__brand { .mobile-home__brand {
display: flex; color: #fff;
flex-direction: column;
gap: 8px;
} }
.mobile-home__brand-logo { .mobile-home__brand-logo {
font-size: 28px; display: flex;
font-weight: 800; align-items: center;
color: #fff; gap: 12px;
letter-spacing: 2px; margin-bottom: 16px;
text-shadow: 0 2px 8px rgba(0,0,0,0.2);
} }
.mobile-home__brand-text {
.mobile-home__logo-icon {
width: 48px;
height: 48px;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: 700;
color: #fff; color: #fff;
border: 2px solid rgba(255, 255, 255, 0.3);
object-fit: contain;
padding: 6px;
} }
.mobile-home__brand-title {
font-size: 16px; .mobile-home__logo-text {
font-weight: 600; flex: 1;
}
.mobile-home__logo-title {
font-size: 22px;
font-weight: 700;
margin-bottom: 2px; margin-bottom: 2px;
letter-spacing: 0.5px;
} }
.mobile-home__brand-sub {
font-size: 12px; .mobile-home__logo-subtitle {
opacity: 0.8; font-size: 10px;
opacity: 0.85;
letter-spacing: 0.3px;
text-transform: uppercase;
} }
.mobile-home__banner-deco {
opacity: 0.7; .mobile-home__brand-desc {
flex-shrink: 0; margin-top: 4px;
}
.mobile-home__brand-title {
font-size: 15px;
font-weight: 500;
margin-bottom: 10px;
opacity: 0.95;
}
.mobile-home__brand-tags {
display: flex;
gap: 8px;
flex-wrap: wrap;
.tag {
padding: 4px 12px;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
border-radius: 12px;
font-size: 11px;
font-weight: 500;
border: 1px solid rgba(255, 255, 255, 0.3);
}
}
/* Banner 装饰圆圈 */
.mobile-home__banner-circles {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
z-index: 1;
.circle {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
&.circle-1 {
width: 120px;
height: 120px;
top: -40px;
right: -20px;
}
&.circle-2 {
width: 80px;
height: 80px;
bottom: -20px;
right: 60px;
background: rgba(255, 255, 255, 0.08);
}
&.circle-3 {
width: 60px;
height: 60px;
top: 50%;
right: -10px;
background: rgba(255, 255, 255, 0.06);
}
}
} }
/* 功能区 */ /* 功能区 */
.mobile-home__section { .mobile-home__section {
margin: 0 12px; margin: 0 12px;
background: #fff; background: #fff;
border-radius: 12px; border-radius: 16px;
padding: 14px; padding: 16px;
box-shadow: 0 1px 4px rgba(0,0,0,0.06); box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
} }
.mobile-home__section-header { .mobile-home__section-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; justify-content: space-between;
margin-bottom: 16px; margin-bottom: 16px;
padding-bottom: 10px; padding-bottom: 12px;
border-bottom: 2px solid #409eff; border-bottom: 1px solid #f0f0f0;
}
.mobile-home__section-icon {
width: 28px;
height: 28px;
background: rgba(64,158,255,0.1);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
} }
.mobile-home__section-title { .mobile-home__section-title {
font-size: 16px; font-size: 17px;
font-weight: 600; font-weight: 600;
color: #303133; color: #1a1a1a;
}
.mobile-home__section-more {
font-size: 13px;
color: #909399;
cursor: pointer;
&:active {
opacity: 0.7;
}
} }
/* 功能网格 */ /* 功能网格 */
.mobile-home__grid { .mobile-home__grid {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
gap: 16px 8px; gap: 16px 12px;
min-height: 100px; min-height: 100px;
} }
.mobile-home__grid-item { .mobile-home__grid-item {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 6px; gap: 8px;
cursor: pointer; cursor: pointer;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
&:active { &:active .mobile-home__grid-icon {
opacity: 0.7; transform: scale(0.9);
} }
} }
.mobile-home__grid-icon { .mobile-home__grid-icon {
width: 44px; width: 56px;
height: 44px; height: 56px;
border-radius: 12px; border-radius: 16px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 22px; font-size: 28px;
transition: transform 0.2s; color: #fff;
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
&:active { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
transform: scale(0.92); position: relative;
&::before {
content: '';
position: absolute;
inset: 0;
border-radius: 16px;
background: linear-gradient(135deg, rgba(255,255,255,0.3) 0%, rgba(255,255,255,0) 100%);
pointer-events: none;
} }
} }
.mobile-home__grid-label { .mobile-home__grid-label {
font-size: 11px; font-size: 12px;
color: #303133; color: #303133;
text-align: center; text-align: center;
line-height: 1.3; line-height: 1.3;
max-width: 64px; max-width: 70px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
white-space: normal;
word-break: break-all;
}
/* 响应式调整 */
@media (max-width: 375px) {
.mobile-home__grid {
grid-template-columns: repeat(4, 1fr);
gap: 12px 8px;
}
.mobile-home__grid-icon {
width: 52px;
height: 52px;
font-size: 26px;
}
.mobile-home__grid-label {
font-size: 11px;
}
} }
</style> </style>

View File

@@ -199,9 +199,9 @@ const loginData = reactive({
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE, captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE, tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
loginForm: { loginForm: {
tenantName: '武汉亚为电子科技有限公司', tenantName: 'YAVII',
username: 'admin', username: 'YAVII',
password: '123456', password: 'yavii123',
captchaVerification: '', captchaVerification: '',
rememberMe: false rememberMe: false
} }

View File

@@ -158,9 +158,9 @@ const loginData = reactive({
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE, captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE, tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
loginForm: { loginForm: {
tenantName: '武汉亚为电子科技有限公司', tenantName: 'YAVII',
username: 'admin', username: 'YAVII',
password: '123456', password: 'yavii123',
captcha: '', // 新增验证码字段 captcha: '', // 新增验证码字段
captchaVerification: '', captchaVerification: '',
rememberMe: true // 默认记录我。如果不需要,可手动修改 rememberMe: true // 默认记录我。如果不需要,可手动修改

View File

@@ -101,7 +101,7 @@
<el-dialog v-model="versionDialogVisible" title="版本信息" width="350px" center> <el-dialog v-model="versionDialogVisible" title="版本信息" width="350px" center>
<div class="version-info"> <div class="version-info">
<p>应用名称MOM管理系统</p> <p>应用名称MOM管理系统</p>
<p>当前版本v1.0.0</p> <p>当前版本v1.0.1</p>
</div> </div>
<template #footer> <template #footer>
<el-button type="primary" @click="versionDialogVisible = false">确定</el-button> <el-button type="primary" @click="versionDialogVisible = false">确定</el-button>

View File

@@ -0,0 +1,265 @@
<template>
<ContentWrap>
<el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="110px">
<!-- <el-form-item label="登记创建时间" prop="registerCreateTime">
<el-date-picker
v-model="queryParams.registerCreateTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-220px"
/>
</el-form-item>
<el-form-item label="处理完成时间" prop="processFinishTime">
<el-date-picker
v-model="queryParams.processFinishTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-220px"
/>
</el-form-item>
<el-form-item label="回访创建时间" prop="visitCreateTime">
<el-date-picker
v-model="queryParams.visitCreateTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-220px"
/>
</el-form-item>
<el-form-item label="统计上限" prop="pageSize">
<el-input-number v-model="queryParams.pageSize" :min="100" :max="2000" :step="100" />
</el-form-item> -->
<el-form-item>
<el-button type="primary" @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> 统计
</el-button>
<!-- <el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" /> 重置
</el-button> -->
</el-form-item>
</el-form>
<!-- <el-alert
type="info"
:closable="false"
class="mt-2"
:title="`统计基于当前筛选条件的前 ${queryParams.pageSize} 条记录`"
/> -->
</ContentWrap>
<div v-loading="loading">
<ContentWrap>
<div class="mb-4 text-lg font-medium">售后登记统计</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
<SummaryCard title="登记总数" :value="registerSummary.total" icon="ep:document" icon-color="text-blue-600" icon-bg-color="bg-blue-100" />
<SummaryCard title="待审核" :value="registerSummary.pending" icon="ep:clock" icon-color="text-orange-600" icon-bg-color="bg-orange-100" />
<SummaryCard title="审核通过" :value="registerSummary.approved" icon="ep:circle-check" icon-color="text-green-600" icon-bg-color="bg-green-100" />
<SummaryCard title="审核驳回" :value="registerSummary.rejected" icon="ep:circle-close" icon-color="text-red-600" icon-bg-color="bg-red-100" />
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<el-card shadow="never">
<div class="mb-2 font-medium">售后类型分布</div>
<Echart :height="320" :options="registerTypeOption" />
</el-card>
<el-card shadow="never">
<div class="mb-2 font-medium">申请状态分布</div>
<Echart :height="320" :options="registerStatusOption" />
</el-card>
</div>
</ContentWrap>
<ContentWrap>
<div class="mb-4 text-lg font-medium">售后处理统计</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
<SummaryCard title="处理总数" :value="processSummary.total" icon="ep:setting" icon-color="text-blue-600" icon-bg-color="bg-blue-100" />
<SummaryCard title="处理中" :value="processSummary.processing" icon="ep:loading" icon-color="text-orange-600" icon-bg-color="bg-orange-100" />
<SummaryCard title="处理完成" :value="processSummary.completed" icon="ep:check" icon-color="text-green-600" icon-bg-color="bg-green-100" />
<SummaryCard title="处理失败" :value="processSummary.failed" icon="ep:close" icon-color="text-red-600" icon-bg-color="bg-red-100" />
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<el-card shadow="never">
<div class="mb-2 font-medium">处理类型分布</div>
<Echart :height="320" :options="processTypeOption" />
</el-card>
<el-card shadow="never">
<div class="mb-2 font-medium">处理状态分布</div>
<Echart :height="320" :options="processStatusOption" />
</el-card>
</div>
</ContentWrap>
<ContentWrap>
<div class="mb-4 text-lg font-medium">售后回访统计</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
<SummaryCard title="回访总数" :value="visitSummary.total" icon="ep:user" icon-color="text-blue-600" icon-bg-color="bg-blue-100" />
<SummaryCard title="平均评分" :value="visitSummary.avgRating" :decimals="1" icon="ep:star" icon-color="text-yellow-600" icon-bg-color="bg-yellow-100" />
<SummaryCard title="五星评分" :value="visitSummary.fiveStar" icon="ep:star" icon-color="text-yellow-600" icon-bg-color="bg-yellow-100" />
<SummaryCard title="强复购意愿" :value="visitSummary.highRepurchase" icon="ep:promotion" icon-color="text-green-600" icon-bg-color="bg-green-100" />
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<el-card shadow="never">
<div class="mb-2 font-medium">评分分布</div>
<Echart :height="320" :options="visitRatingOption" />
</el-card>
<el-card shadow="never">
<div class="mb-2 font-medium">复购意愿分布</div>
<Echart :height="320" :options="visitRepurchaseOption" />
</el-card>
</div>
</ContentWrap>
</div>
</template>
<script setup lang="ts">
import { EChartsOption } from 'echarts'
import SummaryCard from '@/components/SummaryCard/index.vue'
import { Echart } from '@/components/Echart'
import {
AfterSaleAnalysisApi,
DistItem,
ProcessSummary,
RegisterSummary,
VisitSummary
} from '@/api/erp/aftersale/aftersaleanalysis'
defineOptions({ name: 'AfterSaleAnalysis' })
const loading = ref(false)
const queryFormRef = ref()
const queryParams = reactive({
registerCreateTime: [],
processFinishTime: [],
visitCreateTime: [],
pageSize: 500
})
const registerSummary = reactive<RegisterSummary>({
total: 0,
pending: 0,
approved: 0,
rejected: 0
})
const processSummary = reactive<ProcessSummary>({
total: 0,
processing: 0,
completed: 0,
failed: 0
})
const visitSummary = reactive<VisitSummary>({
total: 0,
avgRating: 0,
fiveStar: 0,
highRepurchase: 0
})
const registerTypeDist = ref<DistItem[]>([])
const registerStatusDist = ref<DistItem[]>([])
const processTypeDist = ref<DistItem[]>([])
const processStatusDist = ref<DistItem[]>([])
const visitRatingDist = ref<DistItem[]>([])
const visitRepurchaseDist = ref<DistItem[]>([])
const buildPieOption = (data: { name: string; value: number }[]): EChartsOption => ({
tooltip: { trigger: 'item' },
legend: { bottom: 0 },
series: [
{
type: 'pie',
radius: ['35%', '60%'],
data,
label: { formatter: '{b}: {c}' }
}
]
})
const buildBarOption = (labels: string[], data: number[]): EChartsOption => ({
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
grid: { left: 20, right: 20, bottom: 20, containLabel: true },
xAxis: { type: 'category', data: labels },
yAxis: { type: 'value', minInterval: 1 },
series: [{ type: 'bar', data, barWidth: '45%' }]
})
const registerTypeOption = computed(() => buildPieOption(registerTypeDist.value))
const registerStatusOption = computed(() =>
buildBarOption(
registerStatusDist.value.map((item) => item.name),
registerStatusDist.value.map((item) => item.value)
)
)
const processTypeOption = computed(() =>
buildBarOption(
processTypeDist.value.map((item) => item.name),
processTypeDist.value.map((item) => item.value)
)
)
const processStatusOption = computed(() => buildPieOption(processStatusDist.value))
const visitRatingOption = computed(() =>
buildBarOption(
visitRatingDist.value.map((item) => item.name),
visitRatingDist.value.map((item) => item.value)
)
)
const visitRepurchaseOption = computed(() => buildPieOption(visitRepurchaseDist.value))
const resetSummary = () => {
Object.assign(registerSummary, { total: 0, pending: 0, approved: 0, rejected: 0 })
Object.assign(processSummary, { total: 0, processing: 0, completed: 0, failed: 0 })
Object.assign(visitSummary, { total: 0, avgRating: 0, fiveStar: 0, highRepurchase: 0 })
registerTypeDist.value = []
registerStatusDist.value = []
processTypeDist.value = []
processStatusDist.value = []
visitRatingDist.value = []
visitRepurchaseDist.value = []
}
const loadData = async () => {
loading.value = true
try {
resetSummary()
const res = await AfterSaleAnalysisApi.getAnalysis({
registerCreateTime: queryParams.registerCreateTime,
processFinishTime: queryParams.processFinishTime,
visitCreateTime: queryParams.visitCreateTime,
pageSize: queryParams.pageSize
})
const data = (res as any).data ?? res
Object.assign(registerSummary, data.registerSummary || {})
Object.assign(processSummary, data.processSummary || {})
Object.assign(visitSummary, data.visitSummary || {})
registerTypeDist.value = data.registerTypeDist || []
registerStatusDist.value = data.registerStatusDist || []
processTypeDist.value = data.processTypeDist || []
processStatusDist.value = data.processStatusDist || []
visitRatingDist.value = data.visitRatingDist || []
visitRepurchaseDist.value = data.visitRepurchaseDist || []
} finally {
loading.value = false
}
}
const handleQuery = () => {
loadData()
}
const resetQuery = () => {
queryFormRef.value?.resetFields()
queryParams.pageSize = 500
loadData()
}
onMounted(() => {
loadData()
})
</script>

View File

@@ -0,0 +1,407 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="售后登记" prop="afterSaleId">
<el-input
v-model="selectedRegisterLabel"
placeholder="请选择已审核通过的售后登记"
readonly
@click="openRegisterSelector"
style="cursor: pointer"
>
<template #append>
<el-button :icon="Search" @click="openRegisterSelector" />
</template>
</el-input>
</el-form-item>
<el-form-item label="处理类型" prop="processType">
<el-select v-model="formData.processType" placeholder="请选择处理类型" filterable>
<el-option
v-for="item in processTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="处理数量" prop="processCount">
<el-input-number v-model="formData.processCount" :min="0" class="!w-full" />
</el-form-item>
<el-form-item label="处理结果" prop="processResult">
<el-input v-model="formData.processResult" placeholder="请输入处理结果" />
</el-form-item>
<el-form-item label="处理凭证" prop="processEvidence">
<UploadImgs v-model="processEvidenceList" :limit="5" />
</el-form-item>
<el-form-item label="业务订单" prop="relatedOrderId">
<el-input v-model="formData.relatedOrderId" placeholder="请输入业务订单" />
</el-form-item>
<el-form-item label="处理人" prop="processUser">
<el-input v-model="formData.processUser" placeholder="请输入处理人" />
</el-form-item>
<el-form-item label="时间" prop="processTime">
<el-date-picker
v-model="formData.processTime as any"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择时间"
/>
</el-form-item>
<el-form-item v-if="formData.processStatus === 2" label="处理完成时间" prop="finishTime">
<el-date-picker
v-model="formData.finishTime as any"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择处理完成时间"
/>
</el-form-item>
<el-form-item label="评价" prop="userRating">
<el-rate
v-model="formData.userRating"
:max="10"
class="!w-full"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
<!-- 售后登记选择弹窗 -->
<Dialog v-model="registerSelectorVisible" title="选择售后登记" width="70%" :appendToBody="true">
<ContentWrap>
<el-form
ref="registerQueryFormRef"
:inline="true"
:model="registerQueryParams"
class="-mb-15px"
label-width="100px"
>
<el-form-item label="售后类型" prop="afterSaleType">
<el-select
v-model="registerQueryParams.afterSaleType"
placeholder="请选择售后类型"
clearable
class="!w-200px"
>
<el-option
v-for="item in afterSaleTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="销售订单ID" prop="orderId">
<el-input
v-model="registerQueryParams.orderId"
class="!w-200px"
clearable
placeholder="请输入销售订单ID"
@keyup.enter="handleRegisterQuery"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleRegisterQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetRegisterQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
</el-form-item>
</el-form>
<el-table v-loading="registerLoading" :data="registerList" show-overflow-tooltip>
<el-table-column label="#" width="55">
<template #default="{ row }">
<el-radio :value="row.id" v-model="selectedRegisterId" @change="handleRegisterSelected(row)">
&nbsp;
</el-radio>
</template>
</el-table-column>
<el-table-column label="登记ID" align="center" prop="id" width="100" />
<el-table-column label="销售订单ID" align="center" prop="orderId" width="120" />
<el-table-column label="售后类型" align="center" prop="afterSaleType" width="120">
<template #default="{ row }">
{{ formatAfterSaleType(row.afterSaleType) }}
</template>
</el-table-column>
<el-table-column label="申请原因" align="center" prop="applyReason" min-width="150" />
<el-table-column label="申请人" align="center" prop="applicant" width="100" />
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="160px"
/>
</el-table>
<Pagination
v-model:limit="registerQueryParams.pageSize"
v-model:page="registerQueryParams.pageNo"
:total="registerTotal"
@pagination="getRegisterList"
/>
</ContentWrap>
</Dialog>
</template>
<script setup lang="ts">
import { Search } from '@element-plus/icons-vue'
import dayjs from 'dayjs'
import { dateFormatter } from '@/utils/formatTime'
import { AfterSaleProcessApi, AfterSaleProcess } from '@/api/erp/aftersale/aftersaleprocess'
import { AfterSaleRegisterApi, AfterSaleRegister } from '@/api/erp/aftersale/aftersaleregister'
import { useUserStoreWithOut } from '@/store/modules/user'
/** ERP 售后处理 表单 */
defineOptions({ name: 'AfterSaleProcessForm' })
const processTypeOptions = [
{ label: '退货入库', value: 1 },
{ label: '换货出库', value: 2 },
{ label: '维修处理', value: 3 },
{ label: '退款处理', value: 4 },
{ label: '取消处理', value: 5 },
{ label: '其他', value: 6 }
]
const processStatusOptions = [
{ label: '退换货', value: 1 },
{ label: '维修申请', value: 2 },
{ label: '售后评价', value: 3 }
]
const processEvidenceList = ref<string[]>([])
// 售后登记选择器相关
const registerSelectorVisible = ref(false)
const selectedRegisterId = ref<number>()
const selectedRegisterLabel = ref<string>('')
const registerLoading = ref(false)
const registerList = ref<AfterSaleRegister[]>([])
const registerTotal = ref(0)
const registerQueryParams = ref({
pageNo: 1,
pageSize: 10,
applyStatus: 2, // 仅已审核通过
afterSaleType: undefined,
orderId: undefined
})
const registerQueryFormRef = ref()
const afterSaleTypeOptions = [
{ label: '退货', value: 1 },
{ label: '换货', value: 2 },
{ label: '维修', value: 3 },
{ label: '退款', value: 4 }
]
/** 打开售后登记选择器 */
const openRegisterSelector = () => {
registerSelectorVisible.value = true
registerQueryParams.value = {
pageNo: 1,
pageSize: 10,
applyStatus: 2,
afterSaleType: undefined,
orderId: undefined
}
getRegisterList()
}
/** 查询售后登记列表 */
const getRegisterList = async () => {
registerLoading.value = true
try {
const data = await AfterSaleRegisterApi.getAfterSaleRegisterPage(registerQueryParams.value)
registerList.value = data.list
registerTotal.value = data.total
} finally {
registerLoading.value = false
}
}
/** 搜索售后登记 */
const handleRegisterQuery = () => {
registerQueryParams.value.pageNo = 1
getRegisterList()
}
/** 重置售后登记查询 */
const resetRegisterQuery = () => {
registerQueryParams.value = {
pageNo: 1,
pageSize: 10,
applyStatus: 2,
afterSaleType: undefined,
orderId: undefined
}
getRegisterList()
}
/** 选中售后登记 */
const handleRegisterSelected = (row: AfterSaleRegister) => {
formData.value.afterSaleId = row.id
selectedRegisterLabel.value = `登记 #${row.id}(订单 ${row.orderId || '-'}`
selectedRegisterId.value = row.id
registerSelectorVisible.value = false
}
/** 格式化售后类型 */
const formatAfterSaleType = (value?: number) => {
if (!value) return '-'
const match = afterSaleTypeOptions.find(item => item.value === value)
return match?.label || String(value)
}
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const userStore = useUserStoreWithOut()
const currentUserNickname = computed(() => userStore.getUser?.nickname || '')
const nowString = () => dayjs().format('YYYY-MM-DD HH:mm:ss')
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref<Partial<AfterSaleProcess>>({
id: undefined,
afterSaleId: undefined,
processType: undefined,
processCount: undefined,
processResult: undefined,
processEvidence: '',
relatedOrderId: undefined,
processStatus: undefined,
processFailReason: undefined,
processUser: undefined,
processTime: undefined,
finishTime: undefined,
userRating: undefined
})
const formRules = reactive({
afterSaleId: [{ required: true, message: '售后登记不能为空', trigger: 'change' }],
processType: [{ required: true, message: '处理类型不能为空', trigger: 'change' }],
processResult: [{ required: true, message: '处理结果不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
// 同步处理凭证列表和表单数据
watch(
() => formData.value.processEvidence,
(newVal: string) => {
processEvidenceList.value = newVal ? newVal.split(',').filter(url => url.trim()) : []
},
{ immediate: true }
)
watch(
processEvidenceList,
(newList: string[]) => {
formData.value.processEvidence = newList?.length ? newList.join(',') : ''
},
{ deep: true }
)
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await AfterSaleProcessApi.getAfterSaleProcess(id)
// 如果有售后登记ID获取登记信息显示
if (formData.value.afterSaleId) {
const register = await AfterSaleRegisterApi.getAfterSaleRegister(formData.value.afterSaleId)
selectedRegisterLabel.value = `登记 #${register.id}(订单 ${register.orderId || '-'}`
selectedRegisterId.value = register.id
}
ensureDefaultsForExisting()
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
await formRef.value.validate()
// 提交请求
formLoading.value = true
try {
let data = formData.value as unknown as AfterSaleProcess
// 如果上传了处理凭证,自动设置为处理完成状态
if (data.processEvidence && data.processEvidence.trim() !== '') {
data = {
...data,
processStatus: 2, // 处理完成
finishTime: nowString()
}
}
if (formType.value === 'create') {
await AfterSaleProcessApi.createAfterSaleProcess(data)
message.success(t('common.createSuccess'))
} else {
await AfterSaleProcessApi.updateAfterSaleProcess(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
afterSaleId: undefined,
processType: undefined,
processCount: undefined,
processResult: undefined,
processEvidence: '',
relatedOrderId: undefined,
processStatus: 1, // 新增时默认为处理中状态
processFailReason: undefined,
processUser: currentUserNickname.value || undefined,
processTime: nowString(),
finishTime: undefined,
userRating: undefined
}
selectedRegisterLabel.value = ''
selectedRegisterId.value = undefined
formRef.value?.resetFields()
}
const ensureDefaultsForExisting = () => {
if (!formData.value.processUser) {
formData.value.processUser = currentUserNickname.value || undefined
}
if (!formData.value.processTime) {
formData.value.processTime = nowString()
}
if (formData.value.processStatus === 2 && !formData.value.finishTime) {
formData.value.finishTime = nowString()
}
}
</script>

View File

@@ -0,0 +1,405 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="120px"
>
<el-form-item label="售后登记号" prop="afterSaleId">
<el-input
v-model="queryParams.afterSaleId"
placeholder="请输入售后登记号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="处理类型" prop="processType">
<el-select
v-model="queryParams.processType"
placeholder="请选择处理类型"
clearable
filterable
class="!w-240px"
>
<el-option
v-for="item in processTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="处理结果" prop="processResult">
<el-input
v-model="queryParams.processResult"
placeholder="请输入处理结果"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="处理状态" prop="processStatus">
<el-select
v-model="queryParams.processStatus"
placeholder="请选择处理状态"
clearable
filterable
class="!w-240px"
>
<el-option
v-for="item in processStatusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="处理人" prop="processUser">
<el-input
v-model="queryParams.processUser"
placeholder="请输入处理人"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="处理完成时间" prop="finishTime">
<el-date-picker
v-model="queryParams.finishTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-220px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['erp:after-sale-process:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['erp:after-sale-process:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button
type="danger"
plain
:disabled="isEmpty(checkedIds)"
@click="handleDeleteBatch"
v-hasPermi="['erp:after-sale-process:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
row-key="id"
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleRowCheckboxChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="序号" align="center" type="index" width="80" />
<el-table-column
label="处理类型"
align="center"
prop="processType"
:formatter="formatProcessType"
/>
<el-table-column
label="处理状态"
align="center"
prop="processStatus"
:formatter="formatProcessStatus"
/>
<el-table-column label="处理人" align="center" prop="processUser" />
<el-table-column label="用户评价" align="center" prop="userRating">
<template #default="scope">
<el-tooltip :content="`${scope.row.userRating || 0} 分`" placement="top">
<el-icon size="20" :style="{ color: getRatingColor(scope.row.userRating) }">
<Star />
</el-icon>
</el-tooltip>
</template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center" min-width="200px">
<template #default="scope">
<el-button link type="primary" @click="openDetail(scope.row)">详情</el-button>
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['erp:after-sale-process:update']"
>
编辑
</el-button>
<el-button
link
type="success"
@click="handleProcessComplete(scope.row)"
v-if="scope.row.processStatus === 1"
v-hasPermi="['erp:after-sale-process:update']"
>
处理完成
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['erp:after-sale-process:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<AfterSaleProcessForm ref="formRef" @success="getList" />
<!-- 详情弹窗 -->
<el-dialog v-model="detailVisible" title="售后处理详情" width="560px">
<el-descriptions v-if="detailRow" :column="2" border>
<el-descriptions-item label="售后登记号">{{ detailRow.afterSaleId }}</el-descriptions-item>
<el-descriptions-item label="处理类型">{{ formatProcessType(detailRow, null as any, detailRow.processType as any) }}</el-descriptions-item>
<el-descriptions-item label="处理数量">{{ detailRow.processCount ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="业务订单号">{{ detailRow.relatedOrderId ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="处理结果">{{ detailRow.processResult ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="失败原因">{{ detailRow.processFailReason ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="处理状态">{{ formatProcessStatus(detailRow, null as any, detailRow.processStatus as any) }}</el-descriptions-item>
<el-descriptions-item label="处理人">{{ detailRow.processUser ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="处理时间">{{ dateFormatter(detailRow, null as any, detailRow.processTime as any) }}</el-descriptions-item>
<el-descriptions-item label="处理完成时间">{{ dateFormatter(detailRow, null as any, detailRow.finishTime as any) }}</el-descriptions-item>
<el-descriptions-item label="用户评价">
<el-tooltip :content="`${detailRow.userRating || 0} 分`" placement="top">
<el-icon size="20" :style="{ color: getRatingColor(detailRow.userRating) }">
<Star />
</el-icon>
</el-tooltip>
</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ dateFormatter(detailRow, null as any, (detailRow as any).createTime) }}</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button @click="detailVisible = false">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import dayjs from 'dayjs'
import { isEmpty } from '@/utils/is'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { AfterSaleProcessApi, AfterSaleProcess } from '@/api/erp/aftersale/aftersaleprocess'
import { useUserStoreWithOut } from '@/store/modules/user'
import AfterSaleProcessForm from './AfterSaleProcessForm.vue'
import { Star } from '@element-plus/icons-vue'
const processTypeOptions = [
{ label: '退货入库', value: 1 },
{ label: '换货出库', value: 2 },
{ label: '维修处理', value: 3 },
{ label: '退款处理', value: 4 },
{ label: '取消处理', value: 5 },
{ label: '其他', value: 6 }
]
const processStatusOptions = [
{ label: '处理中', value: 1 },
{ label: '处理完成', value: 2 },
{ label: '处理失败', value: 3 }
]
const optionLabel = (options: { label: string; value: number }[], value?: number | string) => {
if (value === undefined || value === null || value === '') return '-'
const match = options.find((item) => item.value === value || item.value === Number(value))
return match?.label ?? String(value)
}
const formatProcessType = (_row: AfterSaleProcess, _column: any, value: number) =>
optionLabel(processTypeOptions, value)
const formatProcessStatus = (_row: AfterSaleProcess, _column: any, value: number) =>
optionLabel(processStatusOptions, value)
/** ERP 售后处理 列表 */
defineOptions({ name: 'AfterSaleProcess' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const userStore = useUserStoreWithOut()
const loading = ref(true) // 列表的加载中
const list = ref<AfterSaleProcess[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
afterSaleId: undefined,
processType: undefined,
processCount: undefined,
processResult: undefined,
processEvidence: undefined,
relatedOrderId: undefined,
processStatus: undefined,
processFailReason: undefined,
processUser: undefined,
processTime: [],
finishTime: [],
createTime: [],
userRating: undefined
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await AfterSaleProcessApi.getAfterSaleProcessPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await AfterSaleProcessApi.deleteAfterSaleProcess(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 处理完成操作 */
const handleProcessComplete = async (row: AfterSaleProcess) => {
try {
// 处理完成的二次确认
await message.confirm('确认处理完成吗?')
// 更新状态为处理完成
const updateData = {
...row,
processStatus: 2,
processUser: userStore.getUser?.nickname || row.processUser,
finishTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
}
await AfterSaleProcessApi.updateAfterSaleProcess(updateData)
message.success('处理完成')
// 刷新列表
await getList()
} catch {}
}
/** 批量删除ERP 售后处理 */
const handleDeleteBatch = async () => {
try {
// 删除的二次确认
await message.delConfirm()
await AfterSaleProcessApi.deleteAfterSaleProcessList(checkedIds.value);
message.success(t('common.delSuccess'))
await getList();
} catch {}
}
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (records: AfterSaleProcess[]) => {
checkedIds.value = records.map((item) => item.id);
}
const detailVisible = ref(false)
const detailRow = ref<AfterSaleProcess | null>(null)
const openDetail = (row: AfterSaleProcess) => {
detailRow.value = row
detailVisible.value = true
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
const data = await AfterSaleProcessApi.exportAfterSaleProcess(queryParams)
download.excel(data, 'ERP 售后处理.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 根据评分获取颜色 */
const getRatingColor = (rating?: number) => {
if (!rating || rating === 0) return '#cccccc' // 灰色表示无评价
if (rating < 4) return '#ff4d4f' // 红色表示差评
if (rating < 7) return '#ffa940' // 橙色表示一般
if (rating < 9) return '#52c41a' // 绿色表示好评
return '#1890ff' // 蓝色表示优秀
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

View File

@@ -0,0 +1,307 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="销售订单" prop="orderId">
<el-input
v-model="selectedOrderNo"
placeholder="请选择销售订单"
readonly
@click="openOrderSelector"
style="cursor: pointer"
>
<template #append>
<el-button :icon="Search" @click="openOrderSelector" />
</template>
</el-input>
</el-form-item>
<el-form-item label="售后类型" prop="afterSaleType">
<el-select v-model="formData.afterSaleType" placeholder="请选择售后类型" filterable>
<el-option
v-for="item in afterSaleTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="申请原因" prop="applyReason">
<el-input v-model="formData.applyReason" placeholder="请输入申请原因" />
</el-form-item>
<el-form-item label="联系人" prop="contactName">
<el-input v-model="formData.contactName" placeholder="请输入联系人" />
</el-form-item>
<el-form-item label="联系电话" prop="contactPhone">
<el-input v-model="formData.contactPhone" placeholder="请输入联系电话" maxlength="20" />
</el-form-item>
<el-form-item label="申请人" prop="applicant">
<el-input v-model="formData.applicant" placeholder="请输入申请人" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
<!-- 销售订单选择弹窗 -->
<Dialog v-model="orderSelectorVisible" title="选择销售订单" width="70%" :appendToBody="true">
<ContentWrap>
<el-form
ref="orderQueryFormRef"
:inline="true"
:model="orderQueryParams"
class="-mb-15px"
label-width="100px"
>
<el-form-item label="订单单号" prop="no">
<el-input
v-model="orderQueryParams.no"
class="!w-200px"
clearable
placeholder="请输入订单单号"
@keyup.enter="handleOrderQuery"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleOrderQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetOrderQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
</el-form-item>
</el-form>
<el-table v-loading="orderLoading" :data="orderList" show-overflow-tooltip>
<el-table-column label="#" width="55">
<template #default="{ row }">
<el-radio :value="row.id" v-model="selectedOrderId" @change="handleOrderSelected(row)">
&nbsp;
</el-radio>
</template>
</el-table-column>
<el-table-column label="订单单号" align="center" prop="no" min-width="140" />
<el-table-column label="客户" align="center" prop="customerName" min-width="120" />
<el-table-column
label="订单时间"
align="center"
prop="orderTime"
:formatter="dateFormatter"
width="160px"
/>
</el-table>
<Pagination
v-model:limit="orderQueryParams.pageSize"
v-model:page="orderQueryParams.pageNo"
:total="orderTotal"
@pagination="getOrderList"
/>
</ContentWrap>
</Dialog>
</template>
<script setup lang="ts">
import { Search } from '@element-plus/icons-vue'
import dayjs from 'dayjs'
import { dateFormatter } from '@/utils/formatTime'
import { AfterSaleRegisterApi, AfterSaleRegister } from '@/api/erp/aftersale/aftersaleregister'
import { SaleOrderApi, SaleOrderVO } from '@/api/erp/sale/order'
import { useUserStoreWithOut } from '@/store/modules/user'
/** ERP 售后登记 表单 */
defineOptions({ name: 'AfterSaleRegisterForm' })
const afterSaleTypeOptions = [
{ label: '退货', value: 1 },
{ label: '换货', value: 2 },
{ label: '维修', value: 3 },
{ label: '退款', value: 4 }
]
const applyStatusOptions = [
{ label: '待审核', value: 1 },
{ label: '审核通过', value: 2 },
{ label: '审核驳回', value: 3 },
{ label: '已取消', value: 4 }
]
// 销售订单选择器相关
const orderSelectorVisible = ref(false)
const selectedOrderId = ref<number>()
const selectedOrderNo = ref<string>('')
const orderLoading = ref(false)
const orderList = ref<SaleOrderVO[]>([])
const orderTotal = ref(0)
const orderQueryParams = ref({
pageNo: 1,
pageSize: 10,
no: ''
})
const orderQueryFormRef = ref()
/** 打开订单选择器 */
const openOrderSelector = () => {
orderSelectorVisible.value = true
orderQueryParams.value = {
pageNo: 1,
pageSize: 10,
no: ''
}
getOrderList()
}
/** 查询订单列表 */
const getOrderList = async () => {
orderLoading.value = true
try {
const data = await SaleOrderApi.getSaleOrderPage(orderQueryParams.value)
orderList.value = data.list
orderTotal.value = data.total
} finally {
orderLoading.value = false
}
}
/** 搜索订单 */
const handleOrderQuery = () => {
orderQueryParams.value.pageNo = 1
getOrderList()
}
/** 重置订单查询 */
const resetOrderQuery = () => {
orderQueryParams.value = {
pageNo: 1,
pageSize: 10,
no: ''
}
getOrderList()
}
/** 选中订单 */
const handleOrderSelected = (row: SaleOrderVO) => {
formData.value.orderId = row.id
selectedOrderNo.value = row.no || ''
selectedOrderId.value = row.id
orderSelectorVisible.value = false
}
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const userStore = useUserStoreWithOut()
const currentUserNickname = computed(() => userStore.getUser?.nickname || '')
const nowString = () => dayjs().format('YYYY-MM-DD HH:mm:ss')
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref<Partial<AfterSaleRegister>>({
id: undefined,
orderId: undefined,
orderItemId: undefined,
afterSaleType: undefined,
applyReason: undefined,
contactName: undefined,
contactPhone: undefined,
applyStatus: undefined,
rejectReason: undefined,
applicant: undefined,
auditUser: undefined,
auditTime: undefined
})
const formRules = reactive({
afterSaleType: [{ required: true, message: '售后类型不能为空', trigger: 'change' }],
applyReason: [{ required: true, message: '申请原因不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await AfterSaleRegisterApi.getAfterSaleRegister(id)
// 如果有订单ID获取订单号显示
if (formData.value.orderId) {
const order = await SaleOrderApi.getSaleOrder(formData.value.orderId)
selectedOrderNo.value = order.no || ''
selectedOrderId.value = order.id
}
ensureDefaultsForExisting()
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单(当前无必填规则,直接通过)
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as AfterSaleRegister
if (formType.value === 'create') {
await AfterSaleRegisterApi.createAfterSaleRegister(data)
message.success(t('common.createSuccess'))
} else {
await AfterSaleRegisterApi.updateAfterSaleRegister(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
orderId: undefined,
orderItemId: undefined,
afterSaleType: undefined,
applyReason: undefined,
contactName: undefined,
contactPhone: undefined,
applyStatus: 1, // 新增时默认为待审核状态
rejectReason: undefined,
applicant: currentUserNickname.value || undefined,
auditUser: undefined,
auditTime: undefined
}
selectedOrderNo.value = ''
selectedOrderId.value = undefined
formRef.value?.resetFields()
}
const ensureDefaultsForExisting = () => {
if (!formData.value.applicant) {
formData.value.applicant = currentUserNickname.value || undefined
}
if (formData.value.applyStatus === 2) {
if (!formData.value.auditUser) {
formData.value.auditUser = currentUserNickname.value || undefined
}
if (!formData.value.auditTime) {
formData.value.auditTime = nowString()
}
}
}
</script>

View File

@@ -0,0 +1,391 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="120px"
>
<el-form-item label="销售订单" prop="orderId">
<el-select
v-model="queryParams.orderId"
placeholder="请选择销售订单"
clearable
filterable
class="!w-240px"
>
<el-option
v-for="item in saleOrderOptions"
:key="item.id"
:label="item.no"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="售后类型" prop="afterSaleType">
<el-select
v-model="queryParams.afterSaleType"
placeholder="请选择售后类型"
clearable
filterable
class="!w-240px"
>
<el-option
v-for="item in afterSaleTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="申请原因" prop="applyReason">
<el-input
v-model="queryParams.applyReason"
placeholder="请输入申请原因"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="申请状态" prop="applyStatus">
<el-select
v-model="queryParams.applyStatus"
placeholder="请选择申请状态"
clearable
filterable
class="!w-240px"
>
<el-option
v-for="item in applyStatusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="申请人" prop="applicant">
<el-input
v-model="queryParams.applicant"
placeholder="请输入申请人"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="审核人" prop="auditUser">
<el-input
v-model="queryParams.auditUser"
placeholder="请输入审核人"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['erp:after-sale-register:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['erp:after-sale-register:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button
type="danger"
plain
:disabled="isEmpty(checkedIds)"
@click="handleDeleteBatch"
v-hasPermi="['erp:after-sale-register:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
row-key="id"
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleRowCheckboxChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="序号" align="center" type="index" width="80" />
<el-table-column
label="售后类型"
align="center"
prop="afterSaleType"
:formatter="formatAfterSaleType"
/>
<el-table-column
label="申请状态"
align="center"
prop="applyStatus"
:formatter="formatApplyStatus"
/>
<el-table-column label="申请人" align="center" prop="applicant" />
<el-table-column label="审核人" align="center" prop="auditUser" />
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center" min-width="200px">
<template #default="scope">
<el-button link type="primary" @click="openDetail(scope.row)">详情</el-button>
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['erp:after-sale-register:update']"
>
编辑
</el-button>
<el-button
link
type="success"
@click="handleAudit(scope.row)"
v-if="scope.row.applyStatus === 1"
v-hasPermi="['erp:after-sale-register:update']"
>
审核通过
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['erp:after-sale-register:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<AfterSaleRegisterForm ref="formRef" @success="getList" />
<!-- 详情弹窗 -->
<el-dialog v-model="detailVisible" title="售后登记详情" width="560px">
<el-descriptions v-if="detailRow" :column="2" border>
<el-descriptions-item label="销售订单">{{ detailRow.orderId }}</el-descriptions-item>
<el-descriptions-item label="售后类型">{{ formatAfterSaleType(detailRow, null as any, detailRow.afterSaleType as any) }}</el-descriptions-item>
<el-descriptions-item label="申请原因">{{ detailRow.applyReason ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="联系人">{{ detailRow.contactName ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ detailRow.contactPhone ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="申请状态">{{ formatApplyStatus(detailRow, null as any, detailRow.applyStatus as any) }}</el-descriptions-item>
<el-descriptions-item label="驳回原因">{{ detailRow.rejectReason ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="申请人">{{ detailRow.applicant ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="审核人">{{ detailRow.auditUser ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="审核时间">{{ dateFormatter(detailRow, null as any, detailRow.auditTime as any) }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ dateFormatter(detailRow, null as any, (detailRow as any).createTime) }}</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button @click="detailVisible = false">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import dayjs from 'dayjs'
import { isEmpty } from '@/utils/is'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { AfterSaleRegisterApi, AfterSaleRegister } from '@/api/erp/aftersale/aftersaleregister'
import { SaleOrderApi, SaleOrderVO } from '@/api/erp/sale/order'
import { useUserStoreWithOut } from '@/store/modules/user'
import AfterSaleRegisterForm from './AfterSaleRegisterForm.vue'
const afterSaleTypeOptions = [
{ label: '退货', value: 1 },
{ label: '换货', value: 2 },
{ label: '维修', value: 3 },
{ label: '退款', value: 4 }
]
const applyStatusOptions = [
{ label: '待审核', value: 1 },
{ label: '审核通过', value: 2 },
{ label: '审核驳回', value: 3 },
{ label: '已取消', value: 4 }
]
const optionLabel = (options: { label: string; value: number }[], value?: number | string) => {
if (value === undefined || value === null || value === '') return '-'
const match = options.find((item) => item.value === value || item.value === Number(value))
return match?.label ?? String(value)
}
const formatAfterSaleType = (_row: AfterSaleRegister, _column: any, value: number) =>
optionLabel(afterSaleTypeOptions, value)
const formatApplyStatus = (_row: AfterSaleRegister, _column: any, value: number) =>
optionLabel(applyStatusOptions, value)
const saleOrderOptions = ref<SaleOrderVO[]>([])
const fetchSaleOrderOptions = async () => {
const data = await SaleOrderApi.getSaleOrderPage({ pageNo: 1, pageSize: 100 })
saleOrderOptions.value = data.list || []
}
/** ERP 售后登记 列表 */
defineOptions({ name: 'AfterSaleRegister' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const userStore = useUserStoreWithOut()
const loading = ref(true) // 列表的加载中
const list = ref<AfterSaleRegister[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
orderId: undefined,
orderItemId: undefined,
afterSaleType: undefined,
applyReason: undefined,
contactName: undefined,
contactPhone: undefined,
applyStatus: undefined,
rejectReason: undefined,
applicant: undefined,
auditUser: undefined,
auditTime: [],
createTime: []
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await AfterSaleRegisterApi.getAfterSaleRegisterPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await AfterSaleRegisterApi.deleteAfterSaleRegister(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 审核通过操作 */
const handleAudit = async (row: AfterSaleRegister) => {
try {
// 审核确认
await message.confirm('确认审核通过该售后登记吗?')
// 更新状态为审核通过
const updateData = {
...row,
applyStatus: 2,
auditUser: userStore.getUser?.nickname || '',
auditTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
}
await AfterSaleRegisterApi.updateAfterSaleRegister(updateData)
message.success('审核通过成功')
// 刷新列表
await getList()
} catch {}
}
/** 批量删除ERP 售后登记 */
const handleDeleteBatch = async () => {
try {
// 删除的二次确认
await message.delConfirm()
await AfterSaleRegisterApi.deleteAfterSaleRegisterList(checkedIds.value);
message.success(t('common.delSuccess'))
await getList();
} catch {}
}
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (records: AfterSaleRegister[]) => {
checkedIds.value = records.map((item) => item.id);
}
const detailVisible = ref(false)
const detailRow = ref<AfterSaleRegister | null>(null)
const openDetail = (row: AfterSaleRegister) => {
detailRow.value = row
detailVisible.value = true
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
const data = await AfterSaleRegisterApi.exportAfterSaleRegister(queryParams)
download.excel(data, 'ERP 售后登记.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
fetchSaleOrderOptions()
})
</script>

View File

@@ -0,0 +1,267 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="600px">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="90px"
v-loading="formLoading"
>
<!-- 客户基本信息 -->
<el-divider content-position="left">客户信息</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="客户名称" prop="customerName">
<el-select
v-model="formData.customerName"
placeholder="请选择客户"
clearable
filterable
@change="handleCustomerChange"
>
<el-option
v-for="customer in customerList"
:key="customer.id"
:label="customer.name"
:value="customer.name"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系方式" prop="contactInfo">
<el-input v-model="formData.contactInfo" placeholder="请输入联系方式" clearable />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="客户类型" prop="customerType">
<el-select v-model="formData.customerType" placeholder="请选择客户类型" clearable>
<el-option
v-for="item in customerTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户用途" prop="customerUsage">
<el-input v-model="formData.customerUsage" placeholder="请输入客户用途" clearable />
</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="产品名称" prop="productName">
<el-input v-model="formData.productName" placeholder="请输入产品名称" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="产品评价" prop="productEvaluation">
<el-input
v-model="formData.productEvaluation"
placeholder="请输入产品评价"
type="textarea"
:rows="2"
clearable
/>
</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="采购意愿" prop="repurchaseIntention">
<el-select v-model="formData.repurchaseIntention" placeholder="请选择采购意愿" clearable>
<el-option
v-for="item in repurchaseOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="服务评分" prop="serviceRating">
<el-rate
v-model="formData.serviceRating"
:max="5"
:allow-half="false"
:show-text="false"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm" :loading="formLoading">
确定
</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { AfterSalesVisitApi, AfterSalesVisit } from '@/api/erp/aftersale/aftersalesvisit'
import { CustomerApi, CustomerVO } from '@/api/erp/sale/customer'
/** 售后回访 表单 */
defineOptions({ name: 'AfterSalesVisitForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref({
id: undefined,
customerName: undefined,
contactInfo: undefined,
customerUsage: undefined,
productName: undefined,
productEvaluation: undefined,
customerType: undefined,
repurchaseIntention: undefined,
serviceRating: undefined
})
// 客户类型选项
const customerTypeOptions = [
{ value: 'large_food_factory', label: '大型食品厂' },
{ value: 'small_food_factory', label: '中小型加工厂' },
{ value: 'distributor', label: '经销商' },
{ value: 'catering_chain', label: '餐饮连锁' },
{ value: 'government_unit', label: '机关单位' },
{ value: 'other', label: '其他' }
]
// 重复采购意愿选项
const repurchaseOptions = [
{ value: 'definitely', label: '一定会' },
{ value: 'probably', label: '应该会' },
{ value: 'uncertain', label: '不确定' },
{ value: 'unlikely', label: '可能不会' },
{ value: 'never', label: '肯定不会' }
]
const formRules = reactive({
customerName: [
{ required: true, message: '客户名称不能为空', trigger: 'blur' },
{ min: 2, max: 50, message: '客户名称长度应在2-50字符之间', trigger: 'blur' }
],
contactInfo: [
{ required: true, message: '联系方式不能为空', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
],
productName: [
{ required: true, message: '产品名称不能为空', trigger: 'blur' },
{ min: 1, max: 100, message: '产品名称长度应在1-100字符之间', trigger: 'blur' }
],
customerType: [{ required: true, message: '请选择客户类型', trigger: 'change' }],
repurchaseIntention: [{ required: true, message: '请选择采购意愿', trigger: 'change' }],
serviceRating: [
{ required: true, message: '请给出服务评分', trigger: 'change' },
{ type: 'number', min: 0, max: 5, message: '评分应在0-5之间', trigger: 'change' }
]
})
const formRef = ref() // 表单 Ref
const customerList = ref<CustomerVO[]>([]) // 客户列表
/** 加载客户列表 */
const loadCustomerList = async () => {
try {
const data = await CustomerApi.getCustomerSimpleList()
customerList.value = data
} catch (error) {
console.error('加载客户列表失败:', error)
}
}
/** 处理客户选择 */
const handleCustomerChange = (customerName: string) => {
if (!customerName) {
formData.value.contactInfo = ''
return
}
const selectedCustomer = customerList.value.find(customer => customer.name === customerName)
if (selectedCustomer) {
// 自动填充联系方式,优先使用手机号,其次电话
formData.value.contactInfo = selectedCustomer.mobile || selectedCustomer.telephone || ''
}
}
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await AfterSalesVisitApi.getAfterSalesVisit(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
await formRef.value.validate()
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as AfterSalesVisit
if (formType.value === 'create') {
await AfterSalesVisitApi.createAfterSalesVisit(data)
message.success(t('common.createSuccess'))
} else {
await AfterSalesVisitApi.updateAfterSalesVisit(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
customerName: undefined,
contactInfo: undefined,
customerUsage: undefined,
productName: undefined,
productEvaluation: undefined,
customerType: undefined,
repurchaseIntention: undefined,
serviceRating: undefined
}
formRef.value?.resetFields()
}
/** 初始化 */
onMounted(() => {
loadCustomerList()
})
</script>

View File

@@ -0,0 +1,306 @@
<template>
<!-- 统计卡片 -->
<ContentWrap>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
<SummaryCard title="总回访数" :value="total" icon="ep:user" icon-color="text-blue-600" icon-bg-color="bg-blue-100" />
<SummaryCard title="平均评分" :value="avgRating" :decimals="1" icon="ep:star" icon-color="text-yellow-600"
icon-bg-color="bg-yellow-100" />
<SummaryCard title="今日新增" :value="todayCount" icon="ep:calendar" icon-color="text-green-600"
icon-bg-color="bg-green-100" />
<SummaryCard title="五星评分" :value="fiveStarCount" icon="ep:star" icon-color="text-yellow-600"
icon-bg-color="bg-yellow-100" />
</div>
<!-- 搜索工作栏 -->
<el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="80px">
<el-form-item label="客户名称" prop="customerName">
<el-input v-model="queryParams.customerName" placeholder="请输入客户名称" clearable @keyup.enter="handleQuery"
class="!w-200px" />
</el-form-item>
<el-form-item label="联系方式" prop="contactInfo">
<el-input v-model="queryParams.contactInfo" placeholder="请输入联系方式" clearable @keyup.enter="handleQuery"
class="!w-200px" />
</el-form-item>
<el-form-item label="产品名称" prop="productName">
<el-input v-model="queryParams.productName" placeholder="请输入产品名称" clearable @keyup.enter="handleQuery"
class="!w-200px" />
</el-form-item>
<el-form-item label="客户类型" prop="customerType">
<el-select v-model="queryParams.customerType" placeholder="请选择客户类型" clearable class="!w-200px">
<el-option v-for="item in customerTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="采购意愿" prop="repurchaseIntention">
<el-select v-model="queryParams.repurchaseIntention" placeholder="请选择采购意愿" clearable class="!w-200px">
<el-option v-for="item in repurchaseOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker v-model="queryParams.createTime" value-format="YYYY-MM-DD HH:mm:ss" type="daterange"
start-placeholder="开始日期" end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" class="!w-220px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> 搜索
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" /> 重置
</el-button>
</el-form-item>
</el-form>
<!-- 操作按钮 -->
<div class="flex flex-wrap gap-2 mb-4">
<el-button type="primary" plain @click="openForm('create')" v-hasPermi="['erp:after-sales-visit:create']">
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button type="success" plain @click="handleExport" :loading="exportLoading"
v-hasPermi="['erp:after-sales-visit:export']">
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button type="danger" plain :disabled="isEmpty(checkedIds)" @click="handleDeleteBatch"
v-hasPermi="['erp:after-sales-visit:delete']">
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
</div>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table row-key="id" v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"
@selection-change="handleRowCheckboxChange" class="mb-4">
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="客户名称" min-width="120" prop="customerName" show-overflow-tooltip />
<el-table-column label="联系方式" min-width="120" prop="contactInfo" show-overflow-tooltip />
<el-table-column label="产品名称" min-width="120" prop="productName" show-overflow-tooltip />
<el-table-column label="客户类型" min-width="140" align="center">
<template #default="scope">
<el-tag :type="getCustomerTypeColor(scope.row.customerType)">
{{ getCustomerTypeLabel(scope.row.customerType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="采购意愿" min-width="100" align="center">
<template #default="scope">
<span :class="getRepurchaseColor(scope.row.repurchaseIntention)">
{{ getRepurchaseLabel(scope.row.repurchaseIntention) }}
</span>
</template>
</el-table-column>
<el-table-column label="服务评分" width="120" align="center">
<template #default="scope">
<el-rate :model-value="scope.row.serviceRating" disabled :max="5" show-score text-color="#ff9900"
score-template="{value}" />
</template>
</el-table-column>
<el-table-column label="创建时间" width="160" align="center" prop="createTime" :formatter="dateFormatter" />
<el-table-column label="操作" width="140" align="center" fixed="right">
<template #default="scope">
<el-button link type="primary" size="small" @click="openForm('update', scope.row.id)"
v-hasPermi="['erp:after-sales-visit:update']">
<Icon icon="ep:edit" class="mr-1" />编辑
</el-button>
<el-button link type="danger" size="small" @click="handleDelete(scope.row.id)"
v-hasPermi="['erp:after-sales-visit:delete']">
<Icon icon="ep:delete" class="mr-1" />删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="getList" />
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<AfterSalesVisitForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { isEmpty } from '@/utils/is'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import SummaryCard from '@/components/SummaryCard/index.vue'
import { AfterSalesVisitApi, AfterSalesVisit } from '@/api/erp/aftersale/aftersalesvisit'
import AfterSalesVisitForm from './AfterSalesVisitForm.vue'
/** 售后回访 列表 */
defineOptions({ name: 'AfterSalesVisit' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中
const list = ref<AfterSalesVisit[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
const activeNames = ref(['search']) // 折叠面板默认展开
// 客户类型选项
const customerTypeOptions = [
{ value: 'large_food_factory', label: '大型食品厂' },
{ value: 'small_food_factory', label: '中小型加工厂' },
{ value: 'distributor', label: '经销商' },
{ value: 'catering_chain', label: '餐饮连锁' },
{ value: 'government_unit', label: '机关单位' },
{ value: 'other', label: '其他' }
]
// 重复采购意愿选项
const repurchaseOptions = [
{ value: 'definitely', label: '一定会' },
{ value: 'probably', label: '应该会' },
{ value: 'uncertain', label: '不确定' },
{ value: 'unlikely', label: '可能不会' },
{ value: 'never', label: '肯定不会' }
]
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
customerName: undefined,
contactInfo: undefined,
customerUsage: undefined,
productName: undefined,
productEvaluation: undefined,
customerType: undefined,
repurchaseIntention: undefined,
serviceRating: undefined,
createTime: []
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await AfterSalesVisitApi.getAfterSalesVisitPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
// 重置评分查询条件
queryParams.serviceRating = undefined
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await AfterSalesVisitApi.deleteAfterSalesVisit(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch { }
}
/** 批量删除售后回访 */
const handleDeleteBatch = async () => {
try {
// 删除的二次确认
await message.delConfirm()
await AfterSalesVisitApi.deleteAfterSalesVisitList(checkedIds.value);
message.success(t('common.delSuccess'))
await getList();
} catch { }
}
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (records: AfterSalesVisit[]) => {
checkedIds.value = records.map((item) => item.id);
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
const data = await AfterSalesVisitApi.exportAfterSalesVisit(queryParams)
download.excel(data, '售后回访.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 统计数据计算 */
const avgRating = computed(() => {
if (list.value.length === 0) return 0
const sum = list.value.reduce((acc, item) => acc + (item.serviceRating || 0), 0)
return sum / list.value.length
})
const todayCount = computed(() => {
const today = new Date().toISOString().split('T')[0]
return list.value.filter(item => {
const createDate = new Date(item.createTime).toISOString().split('T')[0]
return createDate === today
}).length
})
const fiveStarCount = computed(() => {
return list.value.filter(item => (item.serviceRating || 0) === 5).length
})
/** 格式化函数 */
const getCustomerTypeLabel = (value: string) => {
const option = customerTypeOptions.find(item => item.value === value)
return option ? option.label : value
}
const getCustomerTypeColor = (value: string) => {
const colorMap = {
'large_food_factory': 'success',
'small_food_factory': 'info',
'distributor': 'warning',
'catering_chain': 'danger',
'government_unit': 'primary',
'other': ''
}
return colorMap[value as keyof typeof colorMap] || ''
}
const getRepurchaseLabel = (value: string) => {
const option = repurchaseOptions.find(item => item.value === value)
return option ? option.label : value
}
const getRepurchaseColor = (value: string) => {
const colorMap = {
'definitely': 'text-green-600 font-medium',
'probably': 'text-blue-600',
'uncertain': 'text-yellow-600',
'unlikely': 'text-orange-600',
'never': 'text-red-600'
}
return colorMap[value as keyof typeof colorMap] || ''
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

View File

@@ -1,21 +1,28 @@
<template> <template>
<Dialog v-model="dialogVisible" :title="dialogTitle"> <el-drawer
<el-form v-model="dialogVisible"
ref="formRef" :title="dialogTitle"
v-loading="formLoading" direction="rtl"
:model="formData" size="100%"
:rules="formRules" :close-on-press-escape="true"
label-width="100px" :destroy-on-close="true"
> class="mobile-form-drawer"
<el-row> >
<el-col :span="12"> <div class="mobile-form" v-loading="formLoading">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
>
<!-- 基本信息 -->
<div class="mobile-form__section">
<div class="mobile-form__section-title">基本信息</div>
<el-form-item label="线索名称" prop="name"> <el-form-item label="线索名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入线索名称" /> <el-input v-model="formData.name" placeholder="请输入线索名称" />
</el-form-item> </el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户来源" prop="source"> <el-form-item label="客户来源" prop="source">
<el-select v-model="formData.source" placeholder="请选择客户来源" class="w-1/1"> <el-select v-model="formData.source" placeholder="请选择客户来源" style="width: 100%">
<el-option <el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE)" v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE)"
:key="dict.value" :key="dict.value"
@@ -24,20 +31,11 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="手机" prop="mobile">
<el-input v-model="formData.mobile" placeholder="请输入手机" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="负责人" prop="ownerUserId"> <el-form-item label="负责人" prop="ownerUserId">
<el-select <el-select
v-model="formData.ownerUserId" v-model="formData.ownerUserId"
:disabled="formType !== 'create'" :disabled="formType !== 'create'"
class="w-1/1" style="width: 100%"
> >
<el-option <el-option
v-for="item in userOptions" v-for="item in userOptions"
@@ -47,36 +45,33 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </div>
</el-row>
<el-row> <!-- 联系方式 -->
<el-col :span="12"> <div class="mobile-form__section">
<div class="mobile-form__section-title">联系方式</div>
<el-form-item label="手机" prop="mobile">
<el-input v-model="formData.mobile" placeholder="请输入手机" />
</el-form-item>
<el-form-item label="电话" prop="telephone"> <el-form-item label="电话" prop="telephone">
<el-input v-model="formData.telephone" placeholder="请输入电话" /> <el-input v-model="formData.telephone" placeholder="请输入电话" />
</el-form-item> </el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="邮箱" prop="email"> <el-form-item label="邮箱" prop="email">
<el-input v-model="formData.email" placeholder="请输入邮箱" /> <el-input v-model="formData.email" placeholder="请输入邮箱" />
</el-form-item> </el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="微信" prop="wechat"> <el-form-item label="微信" prop="wechat">
<el-input v-model="formData.wechat" placeholder="请输入微信" /> <el-input v-model="formData.wechat" placeholder="请输入微信" />
</el-form-item> </el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="QQ" prop="qq"> <el-form-item label="QQ" prop="qq">
<el-input v-model="formData.qq" placeholder="请输入 QQ" /> <el-input v-model="formData.qq" placeholder="请输入 QQ" />
</el-form-item> </el-form-item>
</el-col> </div>
</el-row>
<el-row> <!-- 客户信息 -->
<el-col :span="12"> <div class="mobile-form__section">
<div class="mobile-form__section-title">客户信息</div>
<el-form-item label="客户行业" prop="industryId"> <el-form-item label="客户行业" prop="industryId">
<el-select v-model="formData.industryId" placeholder="请选择客户行业" class="w-1/1"> <el-select v-model="formData.industryId" placeholder="请选择客户行业" style="width: 100%">
<el-option <el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY)" v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY)"
:key="dict.value" :key="dict.value"
@@ -85,10 +80,8 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户级别" prop="level"> <el-form-item label="客户级别" prop="level">
<el-select v-model="formData.level" placeholder="请选择客户级别" class="w-1/1"> <el-select v-model="formData.level" placeholder="请选择客户级别" style="width: 100%">
<el-option <el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL)" v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL)"
:key="dict.value" :key="dict.value"
@@ -97,52 +90,52 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </div>
</el-row>
<el-row> <!-- 地址信息 -->
<el-col :span="12"> <div class="mobile-form__section">
<div class="mobile-form__section-title">地址信息</div>
<el-form-item label="地址" prop="areaId"> <el-form-item label="地址" prop="areaId">
<el-cascader <el-cascader
v-model="formData.areaId" v-model="formData.areaId"
:options="areaList" :options="areaList"
:props="defaultProps" :props="defaultProps"
class="w-1/1" style="width: 100%"
clearable clearable
filterable filterable
placeholder="请选择城市" placeholder="请选择城市"
/> />
</el-form-item> </el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="详细地址" prop="detailAddress"> <el-form-item label="详细地址" prop="detailAddress">
<el-input v-model="formData.detailAddress" placeholder="请输入详细地址" /> <el-input v-model="formData.detailAddress" placeholder="请输入详细地址" />
</el-form-item> </el-form-item>
</el-col> </div>
</el-row>
<el-row> <!-- 其他信息 -->
<el-col :span="12"> <div class="mobile-form__section">
<div class="mobile-form__section-title">其他信息</div>
<el-form-item label="下次联系时间" prop="contactNextTime"> <el-form-item label="下次联系时间" prop="contactNextTime">
<el-date-picker <el-date-picker
v-model="formData.contactNextTime" v-model="formData.contactNextTime"
placeholder="选择下次联系时间" placeholder="选择下次联系时间"
type="datetime" type="datetime"
value-format="x" value-format="x"
class="!w-1/1" style="width: 100%"
/> />
</el-form-item> </el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="备注" prop="remark"> <el-form-item label="备注" prop="remark">
<el-input type="textarea" v-model="formData.remark" placeholder="请输入备注" /> <el-input type="textarea" v-model="formData.remark" placeholder="请输入备注" :rows="3" />
</el-form-item> </el-form-item>
</el-col> </div>
</el-row> </el-form>
</el-form>
<template #footer> <!-- 底部操作按钮 -->
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button> <div class="mobile-form__footer">
<el-button @click="dialogVisible = false"> </el-button> <el-button @click="dialogVisible = false"> </el-button>
</template> <el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
</Dialog> </div>
</div>
</el-drawer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
@@ -258,3 +251,44 @@ const resetForm = () => {
formRef.value?.resetFields() formRef.value?.resetFields()
} }
</script> </script>
<style lang="scss" scoped>
.mobile-form {
padding: 0 4px;
}
.mobile-form__section {
background: #fff;
border-radius: 10px;
padding: 14px;
margin-bottom: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.mobile-form__section-title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.mobile-form__footer {
position: sticky;
bottom: 0;
background: #fff;
padding: 12px 16px;
padding-bottom: calc(12px + constant(safe-area-inset-bottom));
padding-bottom: calc(12px + env(safe-area-inset-bottom));
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 12px;
z-index: 10;
margin: 0 -4px;
.el-button {
flex: 1;
height: 40px;
font-size: 15px;
}
}
</style>

View File

@@ -1,34 +1,36 @@
<template> <template>
<div v-loading="loading"> <div class="mobile-detail-header" v-loading="loading">
<div class="flex items-start justify-between"> <!-- 标题和操作按钮 -->
<div> <div class="mobile-detail-header__top">
<!-- 左上线索基本信息 --> <div class="mobile-detail-header__title">{{ clue.name }}</div>
<el-col> <div class="mobile-detail-header__actions">
<el-row>
<span class="text-xl font-bold">{{ clue.name }}</span>
</el-row>
</el-col>
</div>
<div>
<!-- 右上按钮 -->
<slot></slot> <slot></slot>
</div> </div>
</div> </div>
<!-- 关键信息卡片 -->
<div class="mobile-detail-header__card">
<div class="mobile-info-list">
<div class="mobile-info-row">
<span class="mobile-info-row__label">线索来源</span>
<span class="mobile-info-row__value">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="clue.source" />
</span>
</div>
<div class="mobile-info-row">
<span class="mobile-info-row__label">手机</span>
<span class="mobile-info-row__value">{{ clue.mobile || '-' }}</span>
</div>
<div class="mobile-info-row">
<span class="mobile-info-row__label">负责人</span>
<span class="mobile-info-row__value">{{ clue.ownerUserName || '-' }}</span>
</div>
<div class="mobile-info-row">
<span class="mobile-info-row__label">创建时间</span>
<span class="mobile-info-row__value">{{ formatDate(clue.createTime) || '-' }}</span>
</div>
</div>
</div>
</div> </div>
<ContentWrap class="mt-10px">
<el-descriptions :column="5" direction="vertical">
<el-descriptions-item label="线索来源">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="clue.source" />
</el-descriptions-item>
<el-descriptions-item label="手机"> {{ clue.mobile }} </el-descriptions-item>
<el-descriptions-item label="负责人">
{{ clue.ownerUserName }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ formatDate(clue.createTime) }}
</el-descriptions-item>
</el-descriptions>
</ContentWrap>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { DICT_TYPE } from '@/utils/dict' import { DICT_TYPE } from '@/utils/dict'
@@ -41,3 +43,57 @@ defineProps<{
loading: boolean // 加载中 loading: boolean // 加载中
}>() }>()
</script> </script>
<style lang="scss" scoped>
.mobile-detail-header {
padding: 12px;
background: #f5f7fa;
}
.mobile-detail-header__top {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
gap: 12px;
}
.mobile-detail-header__title {
font-size: 18px;
font-weight: 600;
color: #303133;
flex: 1;
word-break: break-all;
}
.mobile-detail-header__actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
flex-shrink: 0;
}
.mobile-detail-header__card {
background: #fff;
border-radius: 10px;
padding: 14px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.mobile-info-list {
font-size: 13px;
}
.mobile-info-row {
display: flex;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px solid #f5f5f5;
&:last-child {
border-bottom: none;
}
&__label {
color: #909399;
flex-shrink: 0;
margin-right: 12px;
}
&__value {
color: #303133;
text-align: right;
}
}
</style>

View File

@@ -1,61 +1,97 @@
<template> <template>
<ContentWrap> <div class="mobile-detail-info">
<el-collapse v-model="activeNames" class=""> <!-- 基本信息 -->
<el-collapse-item name="basicInfo"> <div class="mobile-form__section">
<template #title> <div class="mobile-form__section-title">基本信息</div>
<span class="text-base font-bold">基本信息</span> <div class="mobile-info-list">
</template> <div class="mobile-info-row">
<el-descriptions :column="4"> <span class="mobile-info-row__label">线索名称</span>
<el-descriptions-item label="线索名称"> <span class="mobile-info-row__value">{{ clue.name || '-' }}</span>
{{ clue.name }} </div>
</el-descriptions-item> <div class="mobile-info-row">
<el-descriptions-item label="客户来源"> <span class="mobile-info-row__label">客户来源</span>
<span class="mobile-info-row__value">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="clue.source" /> <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="clue.source" />
</el-descriptions-item> </span>
<el-descriptions-item label="手机">{{ clue.mobile }}</el-descriptions-item> </div>
<el-descriptions-item label="电话">{{ clue.telephone }}</el-descriptions-item> <div class="mobile-info-row">
<el-descriptions-item label="邮箱">{{ clue.email }}</el-descriptions-item> <span class="mobile-info-row__label">手机</span>
<el-descriptions-item label="地址"> <span class="mobile-info-row__value">{{ clue.mobile || '-' }}</span>
{{ clue.areaName }} {{ clue.detailAddress }} </div>
</el-descriptions-item> <div class="mobile-info-row">
<el-descriptions-item label="QQ">{{ clue.qq }}</el-descriptions-item> <span class="mobile-info-row__label">电话</span>
<el-descriptions-item label="微信">{{ clue.wechat }}</el-descriptions-item> <span class="mobile-info-row__value">{{ clue.telephone || '-' }}</span>
<el-descriptions-item label="客户行业"> </div>
<div class="mobile-info-row">
<span class="mobile-info-row__label">邮箱</span>
<span class="mobile-info-row__value">{{ clue.email || '-' }}</span>
</div>
<div class="mobile-info-row">
<span class="mobile-info-row__label">地址</span>
<span class="mobile-info-row__value">{{ clue.areaName || '' }} {{ clue.detailAddress || '-' }}</span>
</div>
<div class="mobile-info-row">
<span class="mobile-info-row__label">QQ</span>
<span class="mobile-info-row__value">{{ clue.qq || '-' }}</span>
</div>
<div class="mobile-info-row">
<span class="mobile-info-row__label">微信</span>
<span class="mobile-info-row__value">{{ clue.wechat || '-' }}</span>
</div>
<div class="mobile-info-row">
<span class="mobile-info-row__label">客户行业</span>
<span class="mobile-info-row__value">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="clue.industryId" /> <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="clue.industryId" />
</el-descriptions-item> </span>
<el-descriptions-item label="客户级别"> </div>
<div class="mobile-info-row">
<span class="mobile-info-row__label">客户级别</span>
<span class="mobile-info-row__value">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="clue.level" /> <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="clue.level" />
</el-descriptions-item> </span>
<el-descriptions-item label="下次联系时间"> </div>
{{ formatDate(clue.contactNextTime) }} <div class="mobile-info-row">
</el-descriptions-item> <span class="mobile-info-row__label">下次联系时间</span>
<el-descriptions-item label="备注">{{ clue.remark }}</el-descriptions-item> <span class="mobile-info-row__value">{{ formatDate(clue.contactNextTime) || '-' }}</span>
</el-descriptions> </div>
</el-collapse-item> <div class="mobile-info-row" v-if="clue.remark">
<el-collapse-item name="systemInfo"> <span class="mobile-info-row__label">备注</span>
<template #title> <span class="mobile-info-row__value">{{ clue.remark }}</span>
<span class="text-base font-bold">系统信息</span> </div>
</template> </div>
<el-descriptions :column="4"> </div>
<el-descriptions-item label="负责人">{{ clue.ownerUserName }}</el-descriptions-item>
<el-descriptions-item label="最后跟进记录"> <!-- 系统信息 -->
{{ clue.contactLastContent }} <div class="mobile-form__section">
</el-descriptions-item> <div class="mobile-form__section-title">系统信息</div>
<el-descriptions-item label="最后跟进时间"> <div class="mobile-info-list">
{{ formatDate(clue.contactLastTime) }} <div class="mobile-info-row">
</el-descriptions-item> <span class="mobile-info-row__label">负责人</span>
<el-descriptions-item label="">&nbsp;</el-descriptions-item> <span class="mobile-info-row__value">{{ clue.ownerUserName || '-' }}</span>
<el-descriptions-item label="创建人">{{ clue.creatorName }}</el-descriptions-item> </div>
<el-descriptions-item label="创建时间"> <div class="mobile-info-row" v-if="clue.contactLastContent">
{{ formatDate(clue.createTime) }} <span class="mobile-info-row__label">最后跟进记录</span>
</el-descriptions-item> <span class="mobile-info-row__value">{{ clue.contactLastContent }}</span>
<el-descriptions-item label="更新时间"> </div>
{{ formatDate(clue.updateTime) }} <div class="mobile-info-row">
</el-descriptions-item> <span class="mobile-info-row__label">最后跟进时间</span>
</el-descriptions> <span class="mobile-info-row__value">{{ formatDate(clue.contactLastTime) || '-' }}</span>
</el-collapse-item> </div>
</el-collapse> <div class="mobile-info-row">
</ContentWrap> <span class="mobile-info-row__label">创建人</span>
<span class="mobile-info-row__value">{{ clue.creatorName || '-' }}</span>
</div>
<div class="mobile-info-row">
<span class="mobile-info-row__label">创建时间</span>
<span class="mobile-info-row__value">{{ formatDate(clue.createTime) || '-' }}</span>
</div>
<div class="mobile-info-row">
<span class="mobile-info-row__label">更新时间</span>
<span class="mobile-info-row__value">{{ formatDate(clue.updateTime) || '-' }}</span>
</div>
</div>
</div>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import * as ClueApi from '@/api/crm/clue' import * as ClueApi from '@/api/crm/clue'
@@ -66,7 +102,48 @@ defineOptions({ name: 'CrmClueDetailsInfo' })
const { clue } = defineProps<{ const { clue } = defineProps<{
clue: ClueApi.ClueVO // 线索明细 clue: ClueApi.ClueVO // 线索明细
}>() }>()
const activeNames = ref(['basicInfo', 'systemInfo']) // 展示的折叠面板
</script> </script>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.mobile-detail-info {
padding: 12px;
background: #f5f7fa;
}
.mobile-form__section {
background: #fff;
border-radius: 10px;
padding: 14px;
margin-bottom: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.mobile-form__section-title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.mobile-info-list {
font-size: 13px;
}
.mobile-info-row {
display: flex;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px solid #f5f5f5;
&:last-child {
border-bottom: none;
}
&__label {
color: #909399;
flex-shrink: 0;
margin-right: 12px;
}
&__value {
color: #303133;
text-align: right;
word-break: break-all;
}
}
</style>

View File

@@ -1,52 +1,18 @@
<template> <template>
<doc-alert title="【线索】线索管理" url="https://doc.iocoder.cn/crm/clue/" /> <div class="mobile-page">
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" /> <!-- 搜索头部 -->
<div class="mobile-page__header">
<ContentWrap> <div class="mobile-page__search">
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="线索名称" prop="name">
<el-input <el-input
v-model="queryParams.name" v-model="queryParams.name"
placeholder="请输入线索名称" placeholder="搜索线索名称"
clearable clearable
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
class="!w-240px" :prefix-icon="Search"
/> />
</el-form-item> <el-button type="primary" :icon="Filter" @click="filterDrawerVisible = true" />
<el-form-item label="转化状态" prop="transformStatus"> </div>
<el-select v-model="queryParams.transformStatus" class="!w-240px"> <div class="mobile-page__actions">
<el-option :value="false" label="未转化" />
<el-option :value="true" label="已转化" />
</el-select>
</el-form-item>
<el-form-item label="手机号" prop="mobile">
<el-input
v-model="queryParams.mobile"
placeholder="请输入手机号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="电话" prop="telephone">
<el-input
v-model="queryParams.telephone"
placeholder="请输入电话"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button type="primary" @click="openForm('create')" v-hasPermi="['crm:clue:create']"> <el-button type="primary" @click="openForm('create')" v-hasPermi="['crm:clue:create']">
<Icon icon="ep:plus" class="mr-5px" /> 新增 <Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button> </el-button>
@@ -57,108 +23,129 @@
:loading="exportLoading" :loading="exportLoading"
v-hasPermi="['crm:clue:export']" v-hasPermi="['crm:clue:export']"
> >
<Icon icon="ep:download" class="mr-5px" /> 导出 导出
</el-button> </el-button>
</el-form-item> </div>
</el-form> </div>
</ContentWrap>
<!-- 列表 --> <!-- Tab 切换 -->
<ContentWrap> <div class="mobile-page__tabs">
<el-tabs v-model="activeName" @tab-click="handleTabClick"> <el-tabs v-model="activeName" @tab-click="handleTabClick">
<el-tab-pane label="我负责的" name="1" /> <el-tab-pane label="我负责的" name="1" />
<el-tab-pane label="我参与的" name="2" /> <el-tab-pane label="我参与的" name="2" />
<el-tab-pane label="下属负责的" name="3" /> <el-tab-pane label="下属负责的" name="3" />
</el-tabs> </el-tabs>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"> </div>
<el-table-column label="线索名称" align="center" prop="name" fixed="left" width="160">
<template #default="scope"> <!-- 线索列表 -->
<el-link :underline="false" type="primary" @click="openDetail(scope.row.id)"> <div class="mobile-page__content" v-loading="loading">
{{ scope.row.name }} <div class="mobile-item-list">
</el-link> <div
</template> v-for="item in list"
</el-table-column> :key="item.id"
<el-table-column label="线索来源" align="center" prop="source" width="100"> class="mobile-item-card mobile-item-card--clickable"
<template #default="scope"> @click="openDetail(item.id)"
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="scope.row.source" /> >
</template> <div class="mobile-item-card__header">
</el-table-column> <span class="mobile-item-card__name">{{ item.name }}</span>
<el-table-column label="手机" align="center" prop="mobile" width="120" /> <el-tag v-if="item.transformStatus" type="success" size="small">已转化</el-tag>
<el-table-column label="电话" align="center" prop="telephone" width="130" /> <el-tag v-else type="info" size="small">未转化</el-tag>
<el-table-column label="邮箱" align="center" prop="email" width="180" /> </div>
<el-table-column label="地址" align="center" prop="detailAddress" width="180" /> <div class="mobile-item-card__body">
<el-table-column align="center" label="客户行业" prop="industryId" width="100"> <div class="mobile-item-card__info-row">
<template #default="scope"> <span class="mobile-item-card__info-label">线索来源</span>
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="scope.row.industryId" /> <span class="mobile-item-card__info-value">
</template> <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="item.source" />
</el-table-column> </span>
<el-table-column align="center" label="客户级别" prop="level" width="135"> </div>
<template #default="scope"> <div class="mobile-item-card__info-row">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="scope.row.level" /> <span class="mobile-item-card__info-label">手机</span>
</template> <span class="mobile-item-card__info-value">{{ item.mobile || '-' }}</span>
</el-table-column> </div>
<el-table-column <div class="mobile-item-card__info-row">
:formatter="dateFormatter" <span class="mobile-item-card__info-label">负责人</span>
align="center" <span class="mobile-item-card__info-value">{{ item.ownerUserName || '-' }}</span>
label="下次联系时间" </div>
prop="contactNextTime" <div class="mobile-item-card__info-row">
width="180px" <span class="mobile-item-card__info-label">客户级别</span>
/> <span class="mobile-item-card__info-value">
<el-table-column align="center" label="备注" prop="remark" width="200" /> <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="item.level" />
<el-table-column </span>
label="最后跟进时间" </div>
align="center" <div class="mobile-item-card__info-row">
prop="contactLastTime" <span class="mobile-item-card__info-label">最后跟进</span>
:formatter="dateFormatter" <span class="mobile-item-card__info-value">{{ item.contactLastTime ? dateFormatter(null, null, item.contactLastTime) : '-' }}</span>
width="180px" </div>
/> </div>
<el-table-column align="center" label="最后跟进记录" prop="contactLastContent" width="200" /> <div class="mobile-item-card__footer" @click.stop>
<el-table-column align="center" label="负责人" prop="ownerUserName" width="100px" /> <el-button
<el-table-column align="center" label="所属部门" prop="ownerUserDeptName" width="100" /> size="small"
<el-table-column type="primary"
label="更新时间" @click="openForm('update', item.id)"
align="center" v-hasPermi="['crm:clue:update']"
prop="updateTime" >编辑</el-button>
:formatter="dateFormatter" <el-button
width="180px" size="small"
/> type="danger"
<el-table-column @click="handleDelete(item.id)"
label="创建时间" v-hasPermi="['crm:clue:delete']"
align="center" >删除</el-button>
prop="createTime" </div>
:formatter="dateFormatter" </div>
width="180px" <div v-if="list.length === 0 && !loading" class="mobile-empty-tip">暂无线索数据</div>
/> </div>
<el-table-column align="center" label="创建人" prop="creatorName" width="100px" /> <!-- 分页 -->
<el-table-column label="操作" align="center" min-width="110" fixed="right"> <div class="mobile-pagination" v-if="total > 0">
<template #default="scope"> <el-pagination
<el-button v-model:current-page="queryParams.pageNo"
link v-model:page-size="queryParams.pageSize"
type="primary" :total="total"
@click="openForm('update', scope.row.id)" :page-sizes="[10, 20]"
v-hasPermi="['crm:clue:update']" layout="total, prev, pager, next"
> :pager-count="5"
编辑 @size-change="getList"
</el-button> @current-change="getList"
<el-button />
link </div>
type="danger" </div>
@click="handleDelete(scope.row.id)" </div>
v-hasPermi="['crm:clue:delete']"
> <!-- 筛选抽屉 -->
删除 <el-drawer
</el-button> v-model="filterDrawerVisible"
</template> title="筛选条件"
</el-table-column> direction="rtl"
</el-table> size="100%"
<!-- 分页 --> :append-to-body="true"
<Pagination class="mobile-form-drawer"
:total="total" >
v-model:page="queryParams.pageNo" <div class="mobile-form">
v-model:limit="queryParams.pageSize" <div class="mobile-form__section">
@pagination="getList" <div class="mobile-form__section-title">筛选条件</div>
/> <el-form :model="queryParams" ref="queryFormRef" label-position="top">
</ContentWrap> <el-form-item label="线索名称" prop="name">
<el-input v-model="queryParams.name" placeholder="请输入线索名称" clearable />
</el-form-item>
<el-form-item label="转化状态" prop="transformStatus">
<el-select v-model="queryParams.transformStatus" style="width: 100%">
<el-option :value="false" label="未转化" />
<el-option :value="true" label="已转化" />
</el-select>
</el-form-item>
<el-form-item label="手机号" prop="mobile">
<el-input v-model="queryParams.mobile" placeholder="请输入手机号" clearable />
</el-form-item>
<el-form-item label="电话" prop="telephone">
<el-input v-model="queryParams.telephone" placeholder="请输入电话" clearable />
</el-form-item>
</el-form>
</div>
<div class="mobile-form__footer">
<el-button @click="resetQuery" style="flex: 1">重置</el-button>
<el-button type="primary" @click="handleFilterConfirm" style="flex: 1">确认</el-button>
</div>
</div>
</el-drawer>
<!-- 表单弹窗添加/修改 --> <!-- 表单弹窗添加/修改 -->
<ClueForm ref="formRef" @success="getList" /> <ClueForm ref="formRef" @success="getList" />
@@ -171,6 +158,7 @@ import download from '@/utils/download'
import * as ClueApi from '@/api/crm/clue' import * as ClueApi from '@/api/crm/clue'
import ClueForm from './ClueForm.vue' import ClueForm from './ClueForm.vue'
import { TabsPaneContext } from 'element-plus' import { TabsPaneContext } from 'element-plus'
import { Search, Filter } from '@element-plus/icons-vue'
defineOptions({ name: 'CrmClue' }) defineOptions({ name: 'CrmClue' })
@@ -180,6 +168,7 @@ const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中 const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数 const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据 const list = ref([]) // 列表的数据
const filterDrawerVisible = ref(false) // 筛选抽屉
const queryParams = reactive({ const queryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
@@ -263,8 +252,149 @@ const handleExport = async () => {
} }
} }
/** 筛选确认 */
const handleFilterConfirm = () => {
filterDrawerVisible.value = false
handleQuery()
}
/** 初始化 **/ /** 初始化 **/
onMounted(() => { onMounted(() => {
getList() getList()
}) })
</script> </script>
<style lang="scss" scoped>
.mobile-page {
padding: 12px;
background: #f5f7fa;
min-height: 100vh;
}
.mobile-page__header {
margin-bottom: 12px;
}
.mobile-page__search {
display: flex;
gap: 8px;
margin-bottom: 10px;
}
.mobile-page__actions {
display: flex;
gap: 8px;
}
.mobile-page__tabs {
background: #fff;
border-radius: 10px;
padding: 0 12px;
margin-bottom: 12px;
}
.mobile-page__content {
min-height: 200px;
}
.mobile-item-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.mobile-item-card {
background: #fff;
border-radius: 10px;
padding: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
&--clickable {
cursor: pointer;
transition: all 0.2s;
&:active {
background: #f5f5f5;
}
}
&__header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
&__name {
flex: 1;
font-size: 15px;
font-weight: 600;
color: #303133;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__body {
font-size: 13px;
}
&__info-row {
display: flex;
justify-content: space-between;
padding: 4px 0;
}
&__info-label {
color: #909399;
flex-shrink: 0;
}
&__info-value {
color: #606266;
text-align: right;
}
&__footer {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #f0f0f0;
}
}
.mobile-empty-tip {
text-align: center;
color: #909399;
padding: 40px 0;
font-size: 14px;
}
.mobile-pagination {
margin-top: 12px;
display: flex;
justify-content: center;
:deep(.el-pagination) {
flex-wrap: wrap;
justify-content: center;
}
}
.mobile-form {
padding: 0 4px;
}
.mobile-form__section {
background: #fff;
border-radius: 10px;
padding: 14px;
margin-bottom: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.mobile-form__section-title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.mobile-form__footer {
position: sticky;
bottom: 0;
background: #fff;
padding: 12px 16px;
padding-bottom: calc(12px + constant(safe-area-inset-bottom));
padding-bottom: calc(12px + env(safe-area-inset-bottom));
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 12px;
z-index: 10;
margin: 0 -4px;
}
</style>

View File

@@ -1,21 +1,28 @@
<template> <template>
<Dialog v-model="dialogVisible" :title="dialogTitle"> <el-drawer
<el-form v-model="dialogVisible"
ref="formRef" :title="dialogTitle"
v-loading="formLoading" direction="rtl"
:model="formData" size="100%"
:rules="formRules" :close-on-press-escape="true"
label-width="100px" :destroy-on-close="true"
> class="mobile-form-drawer"
<el-row> >
<el-col :span="12"> <div class="mobile-form" v-loading="formLoading">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
>
<!-- 基本信息 -->
<div class="mobile-form__section">
<div class="mobile-form__section-title">基本信息</div>
<el-form-item label="客户名称" prop="name"> <el-form-item label="客户名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入客户名称" /> <el-input v-model="formData.name" placeholder="请输入客户名称" />
</el-form-item> </el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户来源" prop="source"> <el-form-item label="客户来源" prop="source">
<el-select v-model="formData.source" placeholder="请选择客户来源" class="w-1/1"> <el-select v-model="formData.source" placeholder="请选择客户来源" style="width: 100%">
<el-option <el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE)" v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE)"
:key="dict.value" :key="dict.value"
@@ -24,20 +31,11 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="手机" prop="mobile">
<el-input v-model="formData.mobile" placeholder="请输入手机" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="负责人" prop="ownerUserId"> <el-form-item label="负责人" prop="ownerUserId">
<el-select <el-select
v-model="formData.ownerUserId" v-model="formData.ownerUserId"
:disabled="formType !== 'create'" :disabled="formType !== 'create'"
class="w-1/1" style="width: 100%"
> >
<el-option <el-option
v-for="item in userOptions" v-for="item in userOptions"
@@ -47,36 +45,33 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </div>
</el-row>
<el-row> <!-- 联系方式 -->
<el-col :span="12"> <div class="mobile-form__section">
<div class="mobile-form__section-title">联系方式</div>
<el-form-item label="手机" prop="mobile">
<el-input v-model="formData.mobile" placeholder="请输入手机" />
</el-form-item>
<el-form-item label="电话" prop="telephone"> <el-form-item label="电话" prop="telephone">
<el-input v-model="formData.telephone" placeholder="请输入电话" /> <el-input v-model="formData.telephone" placeholder="请输入电话" />
</el-form-item> </el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="邮箱" prop="email"> <el-form-item label="邮箱" prop="email">
<el-input v-model="formData.email" placeholder="请输入邮箱" /> <el-input v-model="formData.email" placeholder="请输入邮箱" />
</el-form-item> </el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="微信" prop="wechat"> <el-form-item label="微信" prop="wechat">
<el-input v-model="formData.wechat" placeholder="请输入微信" /> <el-input v-model="formData.wechat" placeholder="请输入微信" />
</el-form-item> </el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="QQ" prop="qq"> <el-form-item label="QQ" prop="qq">
<el-input v-model="formData.qq" placeholder="请输入 QQ" /> <el-input v-model="formData.qq" placeholder="请输入 QQ" />
</el-form-item> </el-form-item>
</el-col> </div>
</el-row>
<el-row> <!-- 客户信息 -->
<el-col :span="12"> <div class="mobile-form__section">
<div class="mobile-form__section-title">客户信息</div>
<el-form-item label="客户行业" prop="industryId"> <el-form-item label="客户行业" prop="industryId">
<el-select v-model="formData.industryId" placeholder="请选择客户行业" class="w-1/1"> <el-select v-model="formData.industryId" placeholder="请选择客户行业" style="width: 100%">
<el-option <el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY)" v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY)"
:key="dict.value" :key="dict.value"
@@ -85,10 +80,8 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户级别" prop="level"> <el-form-item label="客户级别" prop="level">
<el-select v-model="formData.level" placeholder="请选择客户级别" class="w-1/1"> <el-select v-model="formData.level" placeholder="请选择客户级别" style="width: 100%">
<el-option <el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL)" v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL)"
:key="dict.value" :key="dict.value"
@@ -97,52 +90,52 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </div>
</el-row>
<el-row> <!-- 地址信息 -->
<el-col :span="12"> <div class="mobile-form__section">
<div class="mobile-form__section-title">地址信息</div>
<el-form-item label="地址" prop="areaId"> <el-form-item label="地址" prop="areaId">
<el-cascader <el-cascader
v-model="formData.areaId" v-model="formData.areaId"
:options="areaList" :options="areaList"
:props="defaultProps" :props="defaultProps"
class="w-1/1" style="width: 100%"
clearable clearable
filterable filterable
placeholder="请选择城市" placeholder="请选择城市"
/> />
</el-form-item> </el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="详细地址" prop="detailAddress"> <el-form-item label="详细地址" prop="detailAddress">
<el-input v-model="formData.detailAddress" placeholder="请输入详细地址" /> <el-input v-model="formData.detailAddress" placeholder="请输入详细地址" />
</el-form-item> </el-form-item>
</el-col> </div>
</el-row>
<el-row> <!-- 其他信息 -->
<el-col :span="12"> <div class="mobile-form__section">
<div class="mobile-form__section-title">其他信息</div>
<el-form-item label="下次联系时间" prop="contactNextTime"> <el-form-item label="下次联系时间" prop="contactNextTime">
<el-date-picker <el-date-picker
v-model="formData.contactNextTime" v-model="formData.contactNextTime"
placeholder="选择下次联系时间" placeholder="选择下次联系时间"
type="datetime" type="datetime"
value-format="x" value-format="x"
class="!w-1/1" style="width: 100%"
/> />
</el-form-item> </el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="备注" prop="remark"> <el-form-item label="备注" prop="remark">
<el-input type="textarea" v-model="formData.remark" placeholder="请输入备注" /> <el-input type="textarea" v-model="formData.remark" placeholder="请输入备注" :rows="3" />
</el-form-item> </el-form-item>
</el-col> </div>
</el-row> </el-form>
</el-form>
<template #footer> <!-- 底部操作按钮 -->
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button> <div class="mobile-form__footer">
<el-button @click="dialogVisible = false"> </el-button> <el-button @click="dialogVisible = false"> </el-button>
</template> <el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
</Dialog> </div>
</div>
</el-drawer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
@@ -258,3 +251,44 @@ const resetForm = () => {
formRef.value?.resetFields() formRef.value?.resetFields()
} }
</script> </script>
<style lang="scss" scoped>
.mobile-form {
padding: 0 4px;
}
.mobile-form__section {
background: #fff;
border-radius: 10px;
padding: 14px;
margin-bottom: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.mobile-form__section-title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.mobile-form__footer {
position: sticky;
bottom: 0;
background: #fff;
padding: 12px 16px;
padding-bottom: calc(12px + constant(safe-area-inset-bottom));
padding-bottom: calc(12px + env(safe-area-inset-bottom));
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 12px;
z-index: 10;
margin: 0 -4px;
.el-button {
flex: 1;
height: 40px;
font-size: 15px;
}
}
</style>

View File

@@ -1,53 +1,73 @@
<!-- 客户导入窗口 --> <!-- 客户导入窗口 -->
<template> <template>
<Dialog v-model="dialogVisible" title="客户导入" width="400"> <el-drawer
<div class="flex items-center my-10px"> v-model="dialogVisible"
<span class="mr-10px">负责人</span> title="客户导入"
<el-select v-model="ownerUserId" class="!w-240px" clearable> direction="rtl"
<el-option size="100%"
v-for="item in userOptions" :close-on-press-escape="true"
:key="item.id" :destroy-on-close="true"
:label="item.nickname" class="mobile-form-drawer"
:value="item.id" >
/> <div class="mobile-form" v-loading="formLoading">
</el-select> <div class="mobile-form__section">
<div class="mobile-form__section-title">导入设置</div>
<el-form label-position="top">
<el-form-item label="负责人">
<el-select v-model="ownerUserId" style="width: 100%" clearable>
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-form>
</div>
<div class="mobile-form__section">
<div class="mobile-form__section-title">上传文件</div>
<el-upload
ref="uploadRef"
v-model:file-list="fileList"
:auto-upload="false"
:disabled="formLoading"
:limit="1"
:on-exceed="handleExceed"
accept=".xlsx, .xls"
action="none"
drag
>
<Icon icon="ep:upload" />
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip text-center">
<div class="el-upload__tip">
<el-checkbox v-model="updateSupport" />
是否更新已经存在的客户数据客户名称重复
</div>
<span>仅允许导入 xlsxlsx 格式文件</span>
<el-link
:underline="false"
style="font-size: 12px; vertical-align: baseline"
type="primary"
@click="importTemplate"
>
下载模板
</el-link>
</div>
</template>
</el-upload>
</div>
<!-- 底部操作按钮 -->
<div class="mobile-form__footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
</div>
</div> </div>
<el-upload </el-drawer>
ref="uploadRef"
v-model:file-list="fileList"
:auto-upload="false"
:disabled="formLoading"
:limit="1"
:on-exceed="handleExceed"
accept=".xlsx, .xls"
action="none"
drag
>
<Icon icon="ep:upload" />
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip text-center">
<div class="el-upload__tip">
<el-checkbox v-model="updateSupport" />
是否更新已经存在的客户数据客户名称重复
</div>
<span>仅允许导入 xlsxlsx 格式文件</span>
<el-link
:underline="false"
style="font-size: 12px; vertical-align: baseline"
type="primary"
@click="importTemplate"
>
下载模板
</el-link>
</div>
</template>
</el-upload>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import * as CustomerApi from '@/api/crm/customer' import * as CustomerApi from '@/api/crm/customer'
@@ -156,3 +176,44 @@ const importTemplate = async () => {
download.excel(res, '客户导入模版.xls') download.excel(res, '客户导入模版.xls')
} }
</script> </script>
<style lang="scss" scoped>
.mobile-form {
padding: 0 4px;
}
.mobile-form__section {
background: #fff;
border-radius: 10px;
padding: 14px;
margin-bottom: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.mobile-form__section-title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.mobile-form__footer {
position: sticky;
bottom: 0;
background: #fff;
padding: 12px 16px;
padding-bottom: calc(12px + constant(safe-area-inset-bottom));
padding-bottom: calc(12px + env(safe-area-inset-bottom));
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 12px;
z-index: 10;
margin: 0 -4px;
.el-button {
flex: 1;
height: 40px;
font-size: 15px;
}
}
</style>

View File

@@ -1,219 +1,175 @@
<template> <template>
<doc-alert title="【客户】客户管理、公海客户" url="https://doc.iocoder.cn/crm/customer/" /> <div class="mobile-page">
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" /> <!-- 搜索头部 -->
<div class="mobile-page__header">
<ContentWrap> <div class="mobile-page__search">
<!-- 搜索工作栏 -->
<el-form
ref="queryFormRef"
:inline="true"
:model="queryParams"
class="-mb-15px"
label-width="68px"
>
<el-form-item label="客户名称" prop="name">
<el-input <el-input
v-model="queryParams.name" v-model="queryParams.name"
class="!w-240px" placeholder="搜索客户名称"
clearable clearable
placeholder="请输入客户名称"
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
:prefix-icon="Search"
/> />
</el-form-item> <el-button type="primary" :icon="Filter" @click="filterDrawerVisible = true" />
<el-form-item label="手机" prop="mobile"> </div>
<el-input <div class="mobile-page__actions">
v-model="queryParams.mobile" <el-button type="primary" @click="openForm('create')" v-hasPermi="['crm:customer:create']">
class="!w-240px" <Icon icon="ep:plus" class="mr-5px" /> 新增
clearable
placeholder="请输入手机"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="所属行业" prop="industryId">
<el-select
v-model="queryParams.industryId"
class="!w-240px"
clearable
placeholder="请选择所属行业"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="客户级别" prop="level">
<el-select
v-model="queryParams.level"
class="!w-240px"
clearable
placeholder="请选择客户级别"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="客户来源" prop="source">
<el-select
v-model="queryParams.source"
class="!w-240px"
clearable
placeholder="请选择客户来源"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button> </el-button>
<el-button @click="resetQuery"> <el-button type="warning" plain @click="handleImport" v-hasPermi="['crm:customer:import']">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
<el-button v-hasPermi="['crm:customer:create']" type="primary" @click="openForm('create')">
<Icon class="mr-5px" icon="ep:plus" />
新增
</el-button>
<el-button v-hasPermi="['crm:customer:import']" plain type="warning" @click="handleImport">
<Icon icon="ep:upload" />
导入 导入
</el-button> </el-button>
<el-button <el-button
v-hasPermi="['crm:customer:export']"
:loading="exportLoading"
plain
type="success" type="success"
plain
@click="handleExport" @click="handleExport"
:loading="exportLoading"
v-hasPermi="['crm:customer:export']"
> >
<Icon class="mr-5px" icon="ep:download" />
导出 导出
</el-button> </el-button>
</el-form-item> </div>
</el-form> </div>
</ContentWrap>
<!-- 列表 --> <!-- Tab 切换 -->
<ContentWrap> <div class="mobile-page__tabs">
<el-tabs v-model="activeName" @tab-click="handleTabClick"> <el-tabs v-model="activeName" @tab-click="handleTabClick">
<el-tab-pane label="我负责的" name="1" /> <el-tab-pane label="我负责的" name="1" />
<el-tab-pane label="我参与的" name="2" /> <el-tab-pane label="我参与的" name="2" />
<el-tab-pane label="下属负责的" name="3" /> <el-tab-pane label="下属负责的" name="3" />
</el-tabs> </el-tabs>
<el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true"> </div>
<el-table-column align="center" fixed="left" label="客户名称" prop="name" width="160">
<template #default="scope"> <!-- 客户列表 -->
<el-link :underline="false" type="primary" @click="openDetail(scope.row.id)"> <div class="mobile-page__content" v-loading="loading">
{{ scope.row.name }} <div class="mobile-item-list">
</el-link> <div
</template> v-for="item in list"
</el-table-column> :key="item.id"
<el-table-column align="center" label="客户来源" prop="source" width="100"> class="mobile-item-card mobile-item-card--clickable"
<template #default="scope"> @click="openDetail(item.id)"
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="scope.row.source" /> >
</template> <div class="mobile-item-card__header">
</el-table-column> <span class="mobile-item-card__name">{{ item.name }}</span>
<el-table-column align="center" label="手机" prop="mobile" width="120" /> <el-tag v-if="item.dealStatus" type="success" size="small">已成交</el-tag>
<el-table-column align="center" label="电话" prop="telephone" width="130" /> <el-tag v-else type="info" size="small">未成交</el-tag>
<el-table-column align="center" label="邮箱" prop="email" width="180" /> </div>
<el-table-column align="center" label="客户级别" prop="level" width="135"> <div class="mobile-item-card__body">
<template #default="scope"> <div class="mobile-item-card__info-row">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="scope.row.level" /> <span class="mobile-item-card__info-label">客户来源</span>
</template> <span class="mobile-item-card__info-value">
</el-table-column> <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="item.source" />
<el-table-column align="center" label="客户行业" prop="industryId" width="100"> </span>
<template #default="scope"> </div>
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="scope.row.industryId" /> <div class="mobile-item-card__info-row">
</template> <span class="mobile-item-card__info-label">手机</span>
</el-table-column> <span class="mobile-item-card__info-value">{{ item.mobile || '-' }}</span>
<el-table-column </div>
:formatter="dateFormatter" <div class="mobile-item-card__info-row">
align="center" <span class="mobile-item-card__info-label">负责人</span>
label="下次联系时间" <span class="mobile-item-card__info-value">{{ item.ownerUserName || '-' }}</span>
prop="contactNextTime" </div>
width="180px" <div class="mobile-item-card__info-row">
/> <span class="mobile-item-card__info-label">客户级别</span>
<el-table-column align="center" label="备注" prop="remark" width="200" /> <span class="mobile-item-card__info-value">
<el-table-column align="center" label="锁定状态" prop="lockStatus"> <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="item.level" />
<template #default="scope"> </span>
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.lockStatus" /> </div>
</template> <div class="mobile-item-card__info-row">
</el-table-column> <span class="mobile-item-card__info-label">最后跟进</span>
<el-table-column align="center" label="成交状态" prop="dealStatus"> <span class="mobile-item-card__info-value">{{ item.contactLastTime ? dateFormatter(null, null, item.contactLastTime) : '-' }}</span>
<template #default="scope"> </div>
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.dealStatus" /> </div>
</template> <div class="mobile-item-card__footer" @click.stop>
</el-table-column> <el-button
<el-table-column size="small"
:formatter="dateFormatter" type="primary"
align="center" @click="openForm('update', item.id)"
label="最后跟进时间" v-hasPermi="['crm:customer:update']"
prop="contactLastTime" >编辑</el-button>
width="180px" <el-button
/> size="small"
<el-table-column align="center" label="最后跟进记录" prop="contactLastContent" width="200" /> type="danger"
<el-table-column align="center" label="地址" prop="detailAddress" width="180" /> @click="handleDelete(item.id)"
<el-table-column align="center" label="距离进入公海天数" prop="poolDay" width="140"> v-hasPermi="['crm:customer:delete']"
<template #default="scope"> {{ scope.row.poolDay }} </template> >删除</el-button>
</el-table-column> </div>
<el-table-column align="center" label="负责人" prop="ownerUserName" width="100px" /> </div>
<el-table-column align="center" label="所属部门" prop="ownerUserDeptName" width="100px" /> <div v-if="list.length === 0 && !loading" class="mobile-empty-tip">暂无客户数据</div>
<el-table-column </div>
:formatter="dateFormatter" <!-- 分页 -->
align="center" <div class="mobile-pagination" v-if="total > 0">
label="更新时间" <el-pagination
prop="updateTime" v-model:current-page="queryParams.pageNo"
width="180px" v-model:page-size="queryParams.pageSize"
/> :total="total"
<el-table-column :page-sizes="[10, 20]"
:formatter="dateFormatter" layout="total, prev, pager, next"
align="center" :pager-count="5"
label="创建时间" @size-change="getList"
prop="createTime" @current-change="getList"
width="180px" />
/> </div>
<el-table-column align="center" label="创建人" prop="creatorName" width="100px" /> </div>
<el-table-column align="center" fixed="right" label="操作" min-width="150"> </div>
<template #default="scope">
<el-button <!-- 筛选抽屉 -->
v-hasPermi="['crm:customer:update']" <el-drawer
link v-model="filterDrawerVisible"
type="primary" title="筛选条件"
@click="openForm('update', scope.row.id)" direction="rtl"
> size="100%"
编辑 :append-to-body="true"
</el-button> class="mobile-form-drawer"
<el-button >
v-hasPermi="['crm:customer:delete']" <div class="mobile-form">
link <div class="mobile-form__section">
type="danger" <div class="mobile-form__section-title">筛选条件</div>
@click="handleDelete(scope.row.id)" <el-form :model="queryParams" ref="queryFormRef" label-position="top">
> <el-form-item label="客户名称" prop="name">
删除 <el-input v-model="queryParams.name" placeholder="请输入客户名称" clearable />
</el-button> </el-form-item>
</template> <el-form-item label="手机" prop="mobile">
</el-table-column> <el-input v-model="queryParams.mobile" placeholder="请输入手机" clearable />
</el-table> </el-form-item>
<!-- 分页 --> <el-form-item label="所属行业" prop="industryId">
<Pagination <el-select v-model="queryParams.industryId" placeholder="请选择所属行业" clearable style="width: 100%">
v-model:limit="queryParams.pageSize" <el-option
v-model:page="queryParams.pageNo" v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY)"
:total="total" :key="dict.value"
@pagination="getList" :label="dict.label"
/> :value="dict.value"
</ContentWrap> />
</el-select>
</el-form-item>
<el-form-item label="客户级别" prop="level">
<el-select v-model="queryParams.level" placeholder="请选择客户级别" clearable style="width: 100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="客户来源" prop="source">
<el-select v-model="queryParams.source" placeholder="请选择客户来源" clearable style="width: 100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-form>
</div>
<div class="mobile-form__footer">
<el-button @click="resetQuery" style="flex: 1">重置</el-button>
<el-button type="primary" @click="handleFilterConfirm" style="flex: 1">确认</el-button>
</div>
</div>
</el-drawer>
<!-- 表单弹窗添加/修改 --> <!-- 表单弹窗添加/修改 -->
<CustomerForm ref="formRef" @success="getList" /> <CustomerForm ref="formRef" @success="getList" />
@@ -228,6 +184,7 @@ import * as CustomerApi from '@/api/crm/customer'
import CustomerForm from './CustomerForm.vue' import CustomerForm from './CustomerForm.vue'
import CustomerImportForm from './CustomerImportForm.vue' import CustomerImportForm from './CustomerImportForm.vue'
import { TabsPaneContext } from 'element-plus' import { TabsPaneContext } from 'element-plus'
import { Search, Filter } from '@element-plus/icons-vue'
defineOptions({ name: 'CrmCustomer' }) defineOptions({ name: 'CrmCustomer' })
@@ -237,6 +194,7 @@ const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中 const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数 const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据 const list = ref([]) // 列表的数据
const filterDrawerVisible = ref(false) // 筛选抽屉
const queryParams = reactive({ const queryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
@@ -328,6 +286,12 @@ const handleExport = async () => {
} }
} }
/** 筛选确认 */
const handleFilterConfirm = () => {
filterDrawerVisible.value = false
handleQuery()
}
/** 监听路由变化更新列表 */ /** 监听路由变化更新列表 */
watch( watch(
() => currentRoute.value, () => currentRoute.value,
@@ -341,3 +305,138 @@ onMounted(() => {
getList() getList()
}) })
</script> </script>
<style lang="scss" scoped>
.mobile-page {
padding: 12px;
background: #f5f7fa;
min-height: 100vh;
}
.mobile-page__header {
margin-bottom: 12px;
}
.mobile-page__search {
display: flex;
gap: 8px;
margin-bottom: 10px;
}
.mobile-page__actions {
display: flex;
gap: 8px;
}
.mobile-page__tabs {
background: #fff;
border-radius: 10px;
padding: 0 12px;
margin-bottom: 12px;
}
.mobile-page__content {
min-height: 200px;
}
.mobile-item-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.mobile-item-card {
background: #fff;
border-radius: 10px;
padding: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
&--clickable {
cursor: pointer;
transition: all 0.2s;
&:active {
background: #f5f5f5;
}
}
&__header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
&__name {
flex: 1;
font-size: 15px;
font-weight: 600;
color: #303133;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__body {
font-size: 13px;
}
&__info-row {
display: flex;
justify-content: space-between;
padding: 4px 0;
}
&__info-label {
color: #909399;
flex-shrink: 0;
}
&__info-value {
color: #606266;
text-align: right;
}
&__footer {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #f0f0f0;
}
}
.mobile-empty-tip {
text-align: center;
color: #909399;
padding: 40px 0;
font-size: 14px;
}
.mobile-pagination {
margin-top: 12px;
display: flex;
justify-content: center;
:deep(.el-pagination) {
flex-wrap: wrap;
justify-content: center;
}
}
.mobile-form {
padding: 0 4px;
}
.mobile-form__section {
background: #fff;
border-radius: 10px;
padding: 14px;
margin-bottom: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.mobile-form__section-title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.mobile-form__footer {
position: sticky;
bottom: 0;
background: #fff;
padding: 12px 16px;
padding-bottom: calc(12px + constant(safe-area-inset-bottom));
padding-bottom: calc(12px + env(safe-area-inset-bottom));
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 12px;
z-index: 10;
margin: 0 -4px;
}
</style>

View File

@@ -1,57 +1,72 @@
<template> <template>
<Dialog :title="dialogTitle" v-model="dialogVisible"> <el-drawer
<el-form v-model="dialogVisible"
ref="formRef" :title="dialogTitle"
:model="formData" direction="rtl"
:rules="formRules" size="100%"
label-width="200px" :close-on-press-escape="true"
v-loading="formLoading" :destroy-on-close="true"
> class="mobile-form-drawer"
<el-form-item label="规则适用人群" prop="userIds"> >
<el-select multiple filterable v-model="formData.userIds"> <div class="mobile-form" v-loading="formLoading">
<el-option <el-form
v-for="item in userOptions" ref="formRef"
:key="item.id" :model="formData"
:label="item.nickname" :rules="formRules"
:value="item.id" label-position="top"
/>
</el-select>
</el-form-item>
<el-form-item label="规则适用部门" prop="deptIds">
<el-tree-select
v-model="formData.deptIds"
:data="deptTree"
:props="defaultProps"
multiple
filterable
check-strictly
node-key="id"
placeholder="请选择规则适用部门"
/>
</el-form-item>
<el-form-item
:label="
formData.type === LimitConfType.CUSTOMER_QUANTITY_LIMIT
? '拥有客户数上限'
: '锁定客户数上限'
"
prop="maxCount"
> >
<el-input-number v-model="formData.maxCount" placeholder="请输入数量上限" /> <div class="mobile-form__section">
</el-form-item> <div class="mobile-form__section-title">规则设置</div>
<el-form-item <el-form-item label="规则适用人群" prop="userIds">
label="成交客户是否占用拥有客户数" <el-select multiple filterable v-model="formData.userIds" style="width: 100%">
v-if="formData.type === LimitConfType.CUSTOMER_QUANTITY_LIMIT" <el-option
prop="dealCountEnabled" v-for="item in userOptions"
> :key="item.id"
<el-switch v-model="formData.dealCountEnabled" /> :label="item.nickname"
</el-form-item> :value="item.id"
</el-form> />
<template #footer> </el-select>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button> </el-form-item>
<el-button @click="dialogVisible = false"> </el-button> <el-form-item label="规则适用部门" prop="deptIds">
</template> <el-tree-select
</Dialog> v-model="formData.deptIds"
:data="deptTree"
:props="defaultProps"
multiple
filterable
check-strictly
node-key="id"
placeholder="请选择规则适用部门"
style="width: 100%"
/>
</el-form-item>
<el-form-item
:label="
formData.type === LimitConfType.CUSTOMER_QUANTITY_LIMIT
? '拥有客户数上限'
: '锁定客户数上限'
"
prop="maxCount"
>
<el-input-number v-model="formData.maxCount" placeholder="请输入数量上限" style="width: 100%" />
</el-form-item>
<el-form-item
label="成交客户是否占用拥有客户数"
v-if="formData.type === LimitConfType.CUSTOMER_QUANTITY_LIMIT"
prop="dealCountEnabled"
>
<el-switch v-model="formData.dealCountEnabled" />
</el-form-item>
</div>
</el-form>
<!-- 底部操作按钮 -->
<div class="mobile-form__footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
</div>
</div>
</el-drawer>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import * as CustomerLimitConfigApi from '@/api/crm/customer/limitConfig' import * as CustomerLimitConfigApi from '@/api/crm/customer/limitConfig'
@@ -148,3 +163,44 @@ const resetForm = () => {
formRef.value?.resetFields() formRef.value?.resetFields()
} }
</script> </script>
<style lang="scss" scoped>
.mobile-form {
padding: 0 4px;
}
.mobile-form__section {
background: #fff;
border-radius: 10px;
padding: 14px;
margin-bottom: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.mobile-form__section-title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.mobile-form__footer {
position: sticky;
bottom: 0;
background: #fff;
padding: 12px 16px;
padding-bottom: calc(12px + constant(safe-area-inset-bottom));
padding-bottom: calc(12px + env(safe-area-inset-bottom));
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 12px;
z-index: 10;
margin: 0 -4px;
.el-button {
flex: 1;
height: 40px;
font-size: 15px;
}
}
</style>

View File

@@ -1,84 +1,83 @@
<template> <template>
<el-button plain @click="handleQuery"> <Icon icon="ep:refresh" class="mr-5px" /> 刷新 </el-button> <div class="mobile-list-page">
<el-button <!-- 操作按钮 -->
type="primary" <div class="mobile-list-page__actions">
plain <el-button plain size="small" @click="handleQuery">
@click="openForm('create')" <Icon icon="ep:refresh" class="mr-5px" /> 刷新
v-hasPermi="['crm:customer-limit-config:create']" </el-button>
> <el-button
<Icon icon="ep:plus" class="mr-5px" /> 新增 type="primary"
</el-button> size="small"
<el-table @click="openForm('create')"
v-loading="loading" v-hasPermi="['crm:customer-limit-config:create']"
:data="list" >
:stripe="true" <Icon icon="ep:plus" class="mr-5px" /> 新增
:show-overflow-tooltip="true" </el-button>
class="mt-4" </div>
>
<el-table-column label="编号" align="center" prop="id" /> <!-- 列表 -->
<el-table-column <div class="mobile-item-list" v-loading="loading">
label="规则适用人群" <div
align="center" v-for="item in list"
:formatter="(row) => row.users?.map((user: any) => user.nickname).join('')" :key="item.id"
/> class="mobile-item-card"
<el-table-column >
label="规则适用部门" <div class="mobile-item-card__header">
align="center" <span class="mobile-item-card__name">规则 #{{ item.id }}</span>
:formatter="(row) => row.depts?.map((dept: any) => dept.name).join('')" <el-tag type="primary" size="small">上限 {{ item.maxCount }}</el-tag>
/> </div>
<el-table-column <div class="mobile-item-card__body">
:label=" <div class="mobile-item-card__info-row">
confType === LimitConfType.CUSTOMER_QUANTITY_LIMIT ? '拥有客户数上限' : '锁定客户数上限' <span class="mobile-item-card__info-label">适用人群</span>
" <span class="mobile-item-card__info-value">{{ item.users?.map((user: any) => user.nickname).join('') || '-' }}</span>
align="center" </div>
prop="maxCount" <div class="mobile-item-card__info-row">
/> <span class="mobile-item-card__info-label">适用部门</span>
<el-table-column <span class="mobile-item-card__info-value">{{ item.depts?.map((dept: any) => dept.name).join('') || '-' }}</span>
v-if="confType === LimitConfType.CUSTOMER_QUANTITY_LIMIT" </div>
label="成交客户是否占用拥有客户数" <div class="mobile-item-card__info-row" v-if="confType === LimitConfType.CUSTOMER_QUANTITY_LIMIT">
align="center" <span class="mobile-item-card__info-label">成交客户占用</span>
prop="dealCountEnabled" <span class="mobile-item-card__info-value">
min-width="100" <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="item.dealCountEnabled" />
> </span>
<template #default="scope"> </div>
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.dealCountEnabled" /> <div class="mobile-item-card__info-row">
</template> <span class="mobile-item-card__info-label">创建时间</span>
</el-table-column> <span class="mobile-item-card__info-value">{{ dateFormatter(null, null, item.createTime) }}</span>
<el-table-column </div>
label="创建时间" </div>
align="center" <div class="mobile-item-card__footer">
prop="createTime" <el-button
:formatter="dateFormatter" size="small"
width="180px" type="primary"
/> @click="openForm('update', item.id)"
<el-table-column label="操作" align="center" min-width="110" fixed="right"> v-hasPermi="['crm:customer-limit-config:update']"
<template #default="scope"> >编辑</el-button>
<el-button <el-button
link size="small"
type="primary" type="danger"
@click="openForm('update', scope.row.id)" @click="handleDelete(item.id)"
v-hasPermi="['crm:customer-limit-config:update']" v-hasPermi="['crm:customer-limit-config:delete']"
> >删除</el-button>
编辑 </div>
</el-button> </div>
<el-button <div v-if="list.length === 0 && !loading" class="mobile-empty-tip">暂无配置数据</div>
link </div>
type="danger"
@click="handleDelete(scope.row.id)" <!-- 分页 -->
v-hasPermi="['crm:customer-limit-config:delete']" <div class="mobile-pagination" v-if="total > 0">
> <el-pagination
删除 v-model:current-page="queryParams.pageNo"
</el-button> v-model:page-size="queryParams.pageSize"
</template> :total="total"
</el-table-column> :page-sizes="[10, 20]"
</el-table> layout="total, prev, pager, next"
<!-- 分页 --> :pager-count="5"
<Pagination @size-change="getList"
:total="total" @current-change="getList"
v-model:page="queryParams.pageNo" />
v-model:limit="queryParams.pageSize" </div>
@pagination="getList" </div>
/>
<!-- 表单弹窗添加/修改 --> <!-- 表单弹窗添加/修改 -->
<CustomerLimitConfigForm ref="formRef" @success="getList" /> <CustomerLimitConfigForm ref="formRef" @success="getList" />
@@ -148,3 +147,79 @@ onMounted(() => {
getList() getList()
}) })
</script> </script>
<style lang="scss" scoped>
.mobile-list-page {
padding: 12px 0;
}
.mobile-list-page__actions {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.mobile-item-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.mobile-item-card {
background: #fff;
border-radius: 10px;
padding: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
&__header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
&__name {
flex: 1;
font-size: 15px;
font-weight: 600;
color: #303133;
}
&__body {
font-size: 13px;
}
&__info-row {
display: flex;
justify-content: space-between;
padding: 4px 0;
}
&__info-label {
color: #909399;
flex-shrink: 0;
}
&__info-value {
color: #606266;
text-align: right;
word-break: break-all;
}
&__footer {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #f0f0f0;
}
}
.mobile-empty-tip {
text-align: center;
color: #909399;
padding: 40px 0;
font-size: 14px;
}
.mobile-pagination {
margin-top: 12px;
display: flex;
justify-content: center;
:deep(.el-pagination) {
flex-wrap: wrap;
justify-content: center;
}
}
</style>

View File

@@ -1,22 +1,36 @@
<template> <template>
<doc-alert title="【客户】客户管理、公海客户" url="https://doc.iocoder.cn/crm/customer/" /> <div class="mobile-page">
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" /> <!-- Tab 切换 -->
<div class="mobile-page__tabs">
<!-- 列表 --> <el-tabs v-model="activeTab">
<ContentWrap> <el-tab-pane label="拥有客户数限制" name="quantity">
<el-tabs> <CustomerLimitConfigList :confType="LimitConfType.CUSTOMER_QUANTITY_LIMIT" />
<el-tab-pane label="拥有客户数限制"> </el-tab-pane>
<CustomerLimitConfigList :confType="LimitConfType.CUSTOMER_QUANTITY_LIMIT" /> <el-tab-pane label="锁定客户数限制" name="lock">
</el-tab-pane> <CustomerLimitConfigList :confType="LimitConfType.CUSTOMER_LOCK_LIMIT" />
<el-tab-pane label="锁定客户数限制"> </el-tab-pane>
<CustomerLimitConfigList :confType="LimitConfType.CUSTOMER_LOCK_LIMIT" /> </el-tabs>
</el-tab-pane> </div>
</el-tabs> </div>
</ContentWrap>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import CustomerLimitConfigList from './CustomerLimitConfigList.vue' import CustomerLimitConfigList from './CustomerLimitConfigList.vue'
import { LimitConfType } from '@/api/crm/customer/limitConfig' import { LimitConfType } from '@/api/crm/customer/limitConfig'
defineOptions({ name: 'CrmCustomerLimitConfig' }) defineOptions({ name: 'CrmCustomerLimitConfig' })
const activeTab = ref('quantity')
</script> </script>
<style lang="scss" scoped>
.mobile-page {
padding: 12px;
background: #f5f7fa;
min-height: 100vh;
}
.mobile-page__tabs {
background: #fff;
border-radius: 10px;
padding: 0 12px;
}
</style>

View File

@@ -1,28 +1,42 @@
<template> <template>
<Dialog v-model="dialogVisible" title="分配客户"> <el-drawer
<el-form v-model="dialogVisible"
ref="formRef" title="分配客户"
v-loading="formLoading" direction="rtl"
:model="formData" size="100%"
:rules="formRules" :close-on-press-escape="true"
label-width="100px" :destroy-on-close="true"
> class="mobile-form-drawer"
<el-form-item label="负责人" prop="ownerUserId"> >
<el-select v-model="formData.ownerUserId" class="w-1/1"> <div class="mobile-form" v-loading="formLoading">
<el-option <el-form
v-for="item in userOptions" ref="formRef"
:key="item.id" :model="formData"
:label="item.nickname" :rules="formRules"
:value="item.id" label-position="top"
/> >
</el-select> <div class="mobile-form__section">
</el-form-item> <div class="mobile-form__section-title">分配设置</div>
</el-form> <el-form-item label="负责人" prop="ownerUserId">
<template #footer> <el-select v-model="formData.ownerUserId" style="width: 100%">
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button> <el-option
<el-button @click="dialogVisible = false"> </el-button> v-for="item in userOptions"
</template> :key="item.id"
</Dialog> :label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
</div>
</el-form>
<!-- 底部操作按钮 -->
<div class="mobile-form__footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
</div>
</div>
</el-drawer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import * as CustomerApi from '@/api/crm/customer' import * as CustomerApi from '@/api/crm/customer'
@@ -83,3 +97,44 @@ const resetForm = () => {
formRef.value?.resetFields() formRef.value?.resetFields()
} }
</script> </script>
<style lang="scss" scoped>
.mobile-form {
padding: 0 4px;
}
.mobile-form__section {
background: #fff;
border-radius: 10px;
padding: 14px;
margin-bottom: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.mobile-form__section-title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.mobile-form__footer {
position: sticky;
bottom: 0;
background: #fff;
padding: 12px 16px;
padding-bottom: calc(12px + constant(safe-area-inset-bottom));
padding-bottom: calc(12px + env(safe-area-inset-bottom));
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 12px;
z-index: 10;
margin: 0 -4px;
.el-button {
flex: 1;
height: 40px;
font-size: 15px;
}
}
</style>

View File

@@ -1,175 +1,142 @@
<template> <template>
<doc-alert title="【客户】客户管理、公海客户" url="https://doc.iocoder.cn/crm/customer/" /> <div class="mobile-page">
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" /> <!-- 搜索头部 -->
<div class="mobile-page__header">
<ContentWrap> <div class="mobile-page__search">
<!-- 搜索工作栏 -->
<el-form
ref="queryFormRef"
:inline="true"
:model="queryParams"
class="-mb-15px"
label-width="68px"
>
<el-form-item label="客户名称" prop="name">
<el-input <el-input
v-model="queryParams.name" v-model="queryParams.name"
class="!w-240px" placeholder="搜索客户名称"
clearable clearable
placeholder="请输入客户名称"
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
:prefix-icon="Search"
/> />
</el-form-item> <el-button type="primary" :icon="Filter" @click="filterDrawerVisible = true" />
<el-form-item label="手机" prop="mobile"> </div>
<el-input <div class="mobile-page__actions">
v-model="queryParams.mobile"
class="!w-240px"
clearable
placeholder="请输入手机"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="所属行业" prop="industryId">
<el-select
v-model="queryParams.industryId"
class="!w-240px"
clearable
placeholder="请选择所属行业"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="客户级别" prop="level">
<el-select
v-model="queryParams.level"
class="!w-240px"
clearable
placeholder="请选择客户级别"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="客户来源" prop="source">
<el-select
v-model="queryParams.source"
class="!w-240px"
clearable
placeholder="请选择客户来源"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery(undefined)">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
<el-button <el-button
v-hasPermi="['crm:customer:export']"
:loading="exportLoading"
plain
type="success" type="success"
plain
@click="handleExport" @click="handleExport"
:loading="exportLoading"
v-hasPermi="['crm:customer:export']"
> >
<Icon class="mr-5px" icon="ep:download" />
导出 导出
</el-button> </el-button>
</el-form-item> </div>
</el-form> </div>
</ContentWrap>
<!-- 列表 --> <!-- 公海客户列表 -->
<ContentWrap> <div class="mobile-page__content" v-loading="loading">
<el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true"> <div class="mobile-item-list">
<el-table-column align="center" label="客户名称" fixed="left" prop="name" width="160"> <div
<template #default="scope"> v-for="item in list"
<el-link :underline="false" type="primary" @click="openDetail(scope.row.id)"> :key="item.id"
{{ scope.row.name }} class="mobile-item-card mobile-item-card--clickable"
</el-link> @click="openDetail(item.id)"
</template> >
</el-table-column> <div class="mobile-item-card__header">
<el-table-column align="center" label="客户来源" prop="source" width="100"> <span class="mobile-item-card__name">{{ item.name }}</span>
<template #default="scope"> <el-tag v-if="item.dealStatus" type="success" size="small">已成交</el-tag>
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="scope.row.source" /> <el-tag v-else type="info" size="small">未成交</el-tag>
</template> </div>
</el-table-column> <div class="mobile-item-card__body">
<el-table-column label="手机" align="center" prop="mobile" width="120" /> <div class="mobile-item-card__info-row">
<el-table-column label="电话" align="center" prop="telephone" width="130" /> <span class="mobile-item-card__info-label">客户来源</span>
<el-table-column label="邮箱" align="center" prop="email" width="180" /> <span class="mobile-item-card__info-value">
<el-table-column align="center" label="客户级别" prop="level" width="135"> <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="item.source" />
<template #default="scope"> </span>
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="scope.row.level" /> </div>
</template> <div class="mobile-item-card__info-row">
</el-table-column> <span class="mobile-item-card__info-label">手机</span>
<el-table-column align="center" label="客户行业" prop="industryId" width="100"> <span class="mobile-item-card__info-value">{{ item.mobile || '-' }}</span>
<template #default="scope"> </div>
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="scope.row.industryId" /> <div class="mobile-item-card__info-row">
</template> <span class="mobile-item-card__info-label">客户级别</span>
</el-table-column> <span class="mobile-item-card__info-value">
<el-table-column <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="item.level" />
:formatter="dateFormatter" </span>
align="center" </div>
label="下次联系时间" <div class="mobile-item-card__info-row">
prop="contactNextTime" <span class="mobile-item-card__info-label">最后跟进</span>
width="180px" <span class="mobile-item-card__info-value">{{ item.contactLastTime ? dateFormatter(null, null, item.contactLastTime) : '-' }}</span>
/> </div>
<el-table-column align="center" label="备注" prop="remark" width="200" /> </div>
<el-table-column align="center" label="成交状态" prop="dealStatus"> </div>
<template #default="scope"> <div v-if="list.length === 0 && !loading" class="mobile-empty-tip">暂无公海客户数据</div>
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.dealStatus" /> </div>
</template> <!-- 分页 -->
</el-table-column> <div class="mobile-pagination" v-if="total > 0">
<el-table-column <el-pagination
:formatter="dateFormatter" v-model:current-page="queryParams.pageNo"
align="center" v-model:page-size="queryParams.pageSize"
label="最后跟进时间" :total="total"
prop="contactLastTime" :page-sizes="[10, 20]"
width="180px" layout="total, prev, pager, next"
/> :pager-count="5"
<el-table-column align="center" label="最后跟进记录" prop="contactLastContent" width="200" /> @size-change="getList"
<el-table-column @current-change="getList"
:formatter="dateFormatter" />
align="center" </div>
label="更新时间" </div>
prop="updateTime" </div>
width="180px"
/> <!-- 筛选抽屉 -->
<el-table-column <el-drawer
:formatter="dateFormatter" v-model="filterDrawerVisible"
align="center" title="筛选条件"
label="创建时间" direction="rtl"
prop="createTime" size="100%"
width="180px" :append-to-body="true"
/> class="mobile-form-drawer"
<el-table-column align="center" label="创建人" prop="creatorName" width="100px" /> >
</el-table> <div class="mobile-form">
<!-- 分页 --> <div class="mobile-form__section">
<Pagination <div class="mobile-form__section-title">筛选条件</div>
v-model:limit="queryParams.pageSize" <el-form :model="queryParams" ref="queryFormRef" label-position="top">
v-model:page="queryParams.pageNo" <el-form-item label="客户名称" prop="name">
:total="total" <el-input v-model="queryParams.name" placeholder="请输入客户名称" clearable />
@pagination="getList" </el-form-item>
/> <el-form-item label="手机" prop="mobile">
</ContentWrap> <el-input v-model="queryParams.mobile" placeholder="请输入手机" clearable />
</el-form-item>
<el-form-item label="所属行业" prop="industryId">
<el-select v-model="queryParams.industryId" placeholder="请选择所属行业" clearable style="width: 100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="客户级别" prop="level">
<el-select v-model="queryParams.level" placeholder="请选择客户级别" clearable style="width: 100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="客户来源" prop="source">
<el-select v-model="queryParams.source" placeholder="请选择客户来源" clearable style="width: 100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-form>
</div>
<div class="mobile-form__footer">
<el-button @click="resetQuery" style="flex: 1">重置</el-button>
<el-button type="primary" @click="handleFilterConfirm" style="flex: 1">确认</el-button>
</div>
</div>
</el-drawer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -177,6 +144,7 @@ import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime' import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download' import download from '@/utils/download'
import * as CustomerApi from '@/api/crm/customer' import * as CustomerApi from '@/api/crm/customer'
import { Search, Filter } from '@element-plus/icons-vue'
defineOptions({ name: 'CrmCustomerPool' }) defineOptions({ name: 'CrmCustomerPool' })
@@ -185,6 +153,7 @@ const message = useMessage() // 消息弹窗
const loading = ref(true) // 列表的加载中 const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数 const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据 const list = ref([]) // 列表的数据
const filterDrawerVisible = ref(false) // 筛选抽屉
const queryParams = ref({ const queryParams = ref({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
@@ -255,6 +224,12 @@ const handleExport = async () => {
} }
} }
/** 筛选确认 */
const handleFilterConfirm = () => {
filterDrawerVisible.value = false
handleQuery()
}
/** 监听路由变化更新列表 */ /** 监听路由变化更新列表 */
watch( watch(
() => currentRoute.value, () => currentRoute.value,
@@ -268,3 +243,124 @@ onMounted(() => {
getList() getList()
}) })
</script> </script>
<style lang="scss" scoped>
.mobile-page {
padding: 12px;
background: #f5f7fa;
min-height: 100vh;
}
.mobile-page__header {
margin-bottom: 12px;
}
.mobile-page__search {
display: flex;
gap: 8px;
margin-bottom: 10px;
}
.mobile-page__actions {
display: flex;
gap: 8px;
}
.mobile-page__content {
min-height: 200px;
}
.mobile-item-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.mobile-item-card {
background: #fff;
border-radius: 10px;
padding: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
&--clickable {
cursor: pointer;
transition: all 0.2s;
&:active {
background: #f5f5f5;
}
}
&__header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
&__name {
flex: 1;
font-size: 15px;
font-weight: 600;
color: #303133;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__body {
font-size: 13px;
}
&__info-row {
display: flex;
justify-content: space-between;
padding: 4px 0;
}
&__info-label {
color: #909399;
flex-shrink: 0;
}
&__info-value {
color: #606266;
text-align: right;
}
}
.mobile-empty-tip {
text-align: center;
color: #909399;
padding: 40px 0;
font-size: 14px;
}
.mobile-pagination {
margin-top: 12px;
display: flex;
justify-content: center;
:deep(.el-pagination) {
flex-wrap: wrap;
justify-content: center;
}
}
.mobile-form {
padding: 0 4px;
}
.mobile-form__section {
background: #fff;
border-radius: 10px;
padding: 14px;
margin-bottom: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.mobile-form__section-title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.mobile-form__footer {
position: sticky;
bottom: 0;
background: #fff;
padding: 12px 16px;
padding-bottom: calc(12px + constant(safe-area-inset-bottom));
padding-bottom: calc(12px + env(safe-area-inset-bottom));
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 12px;
z-index: 10;
margin: 0 -4px;
}
</style>

View File

@@ -1,62 +1,55 @@
<template> <template>
<doc-alert title="【客户】客户管理、公海客户" url="https://doc.iocoder.cn/crm/customer/" /> <div class="mobile-page" v-loading="formLoading">
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
<ContentWrap>
<el-form <el-form
ref="formRef" ref="formRef"
:model="formData" :model="formData"
:rules="formRules" :rules="formRules"
label-width="160px" label-position="top"
v-loading="formLoading"
> >
<el-card shadow="never"> <!-- 公海规则设置 -->
<!-- 操作 --> <div class="mobile-form__section">
<template #header> <div class="mobile-form__section-header">
<div class="flex items-center justify-between"> <span class="mobile-form__section-title">客户公海规则设置</span>
<CardTitle title="客户公海规则设置" /> <el-button
<el-button type="primary"
type="primary" size="small"
@click="onSubmit" @click="onSubmit"
v-hasPermi="['crm:customer-pool-config:update']" v-hasPermi="['crm:customer-pool-config:update']"
> >
保存 保存
</el-button> </el-button>
</div> </div>
</template> <el-form-item label="客户公海规则" prop="enabled">
<!-- 表单 --> <el-radio-group v-model="formData.enabled" @change="changeEnable">
<el-form-item label="客户公海规则设置" prop="enabled"> <el-radio :value="false">不启用</el-radio>
<el-radio-group v-model="formData.enabled" @change="changeEnable" class="ml-4"> <el-radio :value="true">启用</el-radio>
<el-radio :value="false" size="large">不启用</el-radio>
<el-radio :value="true" size="large">启用</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<div v-if="formData.enabled"> </div>
<el-form-item>
<el-input-number class="mr-2" v-model="formData.contactExpireDays" /> <!-- 规则详情 -->
天不跟进或 <div class="mobile-form__section" v-if="formData.enabled">
<el-input-number class="mx-2" v-model="formData.dealExpireDays" /> <div class="mobile-form__section-title">规则详情</div>
天未成交 <div class="mobile-form__inline-group">
</el-form-item> <el-input-number v-model="formData.contactExpireDays" :min="1" style="width: 80px" />
<el-form-item label="提前提醒设置" prop="notifyEnabled"> <span class="mobile-form__inline-text">天不跟进或</span>
<el-radio-group <el-input-number v-model="formData.dealExpireDays" :min="1" style="width: 80px" />
v-model="formData.notifyEnabled" <span class="mobile-form__inline-text">天未成交</span>
@change="changeNotifyEnable"
class="ml-4"
>
<el-radio :value="false" size="large">不提醒</el-radio>
<el-radio :value="true" size="large">提醒</el-radio>
</el-radio-group>
</el-form-item>
<div v-if="formData.notifyEnabled">
<el-form-item>
提前 <el-input-number class="mx-2" v-model="formData.notifyDays" /> 天提醒
</el-form-item>
</div>
</div> </div>
</el-card> <el-form-item label="提前提醒设置" prop="notifyEnabled" style="margin-top: 16px">
<el-radio-group v-model="formData.notifyEnabled" @change="changeNotifyEnable">
<el-radio :value="false">不提醒</el-radio>
<el-radio :value="true">提醒</el-radio>
</el-radio-group>
</el-form-item>
<div class="mobile-form__inline-group" v-if="formData.notifyEnabled">
<span class="mobile-form__inline-text">提前</span>
<el-input-number v-model="formData.notifyDays" :min="1" style="width: 80px" />
<span class="mobile-form__inline-text">天提醒</span>
</div>
</div>
</el-form> </el-form>
</ContentWrap> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import * as CustomerPoolConfigApi from '@/api/crm/customer/poolConfig' import * as CustomerPoolConfigApi from '@/api/crm/customer/poolConfig'
@@ -134,3 +127,42 @@ onMounted(() => {
getConfig() getConfig()
}) })
</script> </script>
<style lang="scss" scoped>
.mobile-page {
padding: 12px;
background: #f5f7fa;
min-height: 100vh;
}
.mobile-form__section {
background: #fff;
border-radius: 10px;
padding: 14px;
margin-bottom: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.mobile-form__section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.mobile-form__section-title {
font-size: 15px;
font-weight: 600;
color: #303133;
}
.mobile-form__inline-group {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin-top: 8px;
}
.mobile-form__inline-text {
color: #606266;
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,265 @@
<template>
<ContentWrap>
<el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="110px">
<!-- <el-form-item label="登记创建时间" prop="registerCreateTime">
<el-date-picker
v-model="queryParams.registerCreateTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-220px"
/>
</el-form-item>
<el-form-item label="处理完成时间" prop="processFinishTime">
<el-date-picker
v-model="queryParams.processFinishTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-220px"
/>
</el-form-item>
<el-form-item label="回访创建时间" prop="visitCreateTime">
<el-date-picker
v-model="queryParams.visitCreateTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-220px"
/>
</el-form-item>
<el-form-item label="统计上限" prop="pageSize">
<el-input-number v-model="queryParams.pageSize" :min="100" :max="2000" :step="100" />
</el-form-item> -->
<el-form-item>
<el-button type="primary" @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> 统计
</el-button>
<!-- <el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" /> 重置
</el-button> -->
</el-form-item>
</el-form>
<!-- <el-alert
type="info"
:closable="false"
class="mt-2"
:title="`统计基于当前筛选条件的前 ${queryParams.pageSize} 条记录`"
/> -->
</ContentWrap>
<div v-loading="loading">
<ContentWrap>
<div class="mb-4 text-lg font-medium">售后登记统计</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
<SummaryCard title="登记总数" :value="registerSummary.total" icon="ep:document" icon-color="text-blue-600" icon-bg-color="bg-blue-100" />
<SummaryCard title="待审核" :value="registerSummary.pending" icon="ep:clock" icon-color="text-orange-600" icon-bg-color="bg-orange-100" />
<SummaryCard title="审核通过" :value="registerSummary.approved" icon="ep:circle-check" icon-color="text-green-600" icon-bg-color="bg-green-100" />
<SummaryCard title="审核驳回" :value="registerSummary.rejected" icon="ep:circle-close" icon-color="text-red-600" icon-bg-color="bg-red-100" />
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<el-card shadow="never">
<div class="mb-2 font-medium">售后类型分布</div>
<Echart :height="320" :options="registerTypeOption" />
</el-card>
<el-card shadow="never">
<div class="mb-2 font-medium">申请状态分布</div>
<Echart :height="320" :options="registerStatusOption" />
</el-card>
</div>
</ContentWrap>
<ContentWrap>
<div class="mb-4 text-lg font-medium">售后处理统计</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
<SummaryCard title="处理总数" :value="processSummary.total" icon="ep:setting" icon-color="text-blue-600" icon-bg-color="bg-blue-100" />
<SummaryCard title="处理中" :value="processSummary.processing" icon="ep:loading" icon-color="text-orange-600" icon-bg-color="bg-orange-100" />
<SummaryCard title="处理完成" :value="processSummary.completed" icon="ep:check" icon-color="text-green-600" icon-bg-color="bg-green-100" />
<SummaryCard title="处理失败" :value="processSummary.failed" icon="ep:close" icon-color="text-red-600" icon-bg-color="bg-red-100" />
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<el-card shadow="never">
<div class="mb-2 font-medium">处理类型分布</div>
<Echart :height="320" :options="processTypeOption" />
</el-card>
<el-card shadow="never">
<div class="mb-2 font-medium">处理状态分布</div>
<Echart :height="320" :options="processStatusOption" />
</el-card>
</div>
</ContentWrap>
<ContentWrap>
<div class="mb-4 text-lg font-medium">售后回访统计</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
<SummaryCard title="回访总数" :value="visitSummary.total" icon="ep:user" icon-color="text-blue-600" icon-bg-color="bg-blue-100" />
<SummaryCard title="平均评分" :value="visitSummary.avgRating" :decimals="1" icon="ep:star" icon-color="text-yellow-600" icon-bg-color="bg-yellow-100" />
<SummaryCard title="五星评分" :value="visitSummary.fiveStar" icon="ep:star" icon-color="text-yellow-600" icon-bg-color="bg-yellow-100" />
<SummaryCard title="强复购意愿" :value="visitSummary.highRepurchase" icon="ep:promotion" icon-color="text-green-600" icon-bg-color="bg-green-100" />
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<el-card shadow="never">
<div class="mb-2 font-medium">评分分布</div>
<Echart :height="320" :options="visitRatingOption" />
</el-card>
<el-card shadow="never">
<div class="mb-2 font-medium">复购意愿分布</div>
<Echart :height="320" :options="visitRepurchaseOption" />
</el-card>
</div>
</ContentWrap>
</div>
</template>
<script setup lang="ts">
import { EChartsOption } from 'echarts'
import SummaryCard from '@/components/SummaryCard/index.vue'
import { Echart } from '@/components/Echart'
import {
AfterSaleAnalysisApi,
DistItem,
ProcessSummary,
RegisterSummary,
VisitSummary
} from '@/api/erp/aftersale/aftersaleanalysis'
defineOptions({ name: 'AfterSaleAnalysis' })
const loading = ref(false)
const queryFormRef = ref()
const queryParams = reactive({
registerCreateTime: [],
processFinishTime: [],
visitCreateTime: [],
pageSize: 500
})
const registerSummary = reactive<RegisterSummary>({
total: 0,
pending: 0,
approved: 0,
rejected: 0
})
const processSummary = reactive<ProcessSummary>({
total: 0,
processing: 0,
completed: 0,
failed: 0
})
const visitSummary = reactive<VisitSummary>({
total: 0,
avgRating: 0,
fiveStar: 0,
highRepurchase: 0
})
const registerTypeDist = ref<DistItem[]>([])
const registerStatusDist = ref<DistItem[]>([])
const processTypeDist = ref<DistItem[]>([])
const processStatusDist = ref<DistItem[]>([])
const visitRatingDist = ref<DistItem[]>([])
const visitRepurchaseDist = ref<DistItem[]>([])
const buildPieOption = (data: { name: string; value: number }[]): EChartsOption => ({
tooltip: { trigger: 'item' },
legend: { bottom: 0 },
series: [
{
type: 'pie',
radius: ['35%', '60%'],
data,
label: { formatter: '{b}: {c}' }
}
]
})
const buildBarOption = (labels: string[], data: number[]): EChartsOption => ({
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
grid: { left: 20, right: 20, bottom: 20, containLabel: true },
xAxis: { type: 'category', data: labels },
yAxis: { type: 'value', minInterval: 1 },
series: [{ type: 'bar', data, barWidth: '45%' }]
})
const registerTypeOption = computed(() => buildPieOption(registerTypeDist.value))
const registerStatusOption = computed(() =>
buildBarOption(
registerStatusDist.value.map((item) => item.name),
registerStatusDist.value.map((item) => item.value)
)
)
const processTypeOption = computed(() =>
buildBarOption(
processTypeDist.value.map((item) => item.name),
processTypeDist.value.map((item) => item.value)
)
)
const processStatusOption = computed(() => buildPieOption(processStatusDist.value))
const visitRatingOption = computed(() =>
buildBarOption(
visitRatingDist.value.map((item) => item.name),
visitRatingDist.value.map((item) => item.value)
)
)
const visitRepurchaseOption = computed(() => buildPieOption(visitRepurchaseDist.value))
const resetSummary = () => {
Object.assign(registerSummary, { total: 0, pending: 0, approved: 0, rejected: 0 })
Object.assign(processSummary, { total: 0, processing: 0, completed: 0, failed: 0 })
Object.assign(visitSummary, { total: 0, avgRating: 0, fiveStar: 0, highRepurchase: 0 })
registerTypeDist.value = []
registerStatusDist.value = []
processTypeDist.value = []
processStatusDist.value = []
visitRatingDist.value = []
visitRepurchaseDist.value = []
}
const loadData = async () => {
loading.value = true
try {
resetSummary()
const res = await AfterSaleAnalysisApi.getAnalysis({
registerCreateTime: queryParams.registerCreateTime,
processFinishTime: queryParams.processFinishTime,
visitCreateTime: queryParams.visitCreateTime,
pageSize: queryParams.pageSize
})
const data = (res as any).data ?? res
Object.assign(registerSummary, data.registerSummary || {})
Object.assign(processSummary, data.processSummary || {})
Object.assign(visitSummary, data.visitSummary || {})
registerTypeDist.value = data.registerTypeDist || []
registerStatusDist.value = data.registerStatusDist || []
processTypeDist.value = data.processTypeDist || []
processStatusDist.value = data.processStatusDist || []
visitRatingDist.value = data.visitRatingDist || []
visitRepurchaseDist.value = data.visitRepurchaseDist || []
} finally {
loading.value = false
}
}
const handleQuery = () => {
loadData()
}
const resetQuery = () => {
queryFormRef.value?.resetFields()
queryParams.pageSize = 500
loadData()
}
onMounted(() => {
loadData()
})
</script>

View File

@@ -0,0 +1,407 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="售后登记" prop="afterSaleId">
<el-input
v-model="selectedRegisterLabel"
placeholder="请选择已审核通过的售后登记"
readonly
@click="openRegisterSelector"
style="cursor: pointer"
>
<template #append>
<el-button :icon="Search" @click="openRegisterSelector" />
</template>
</el-input>
</el-form-item>
<el-form-item label="处理类型" prop="processType">
<el-select v-model="formData.processType" placeholder="请选择处理类型" filterable>
<el-option
v-for="item in processTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="处理数量" prop="processCount">
<el-input-number v-model="formData.processCount" :min="0" class="!w-full" />
</el-form-item>
<el-form-item label="处理结果" prop="processResult">
<el-input v-model="formData.processResult" placeholder="请输入处理结果" />
</el-form-item>
<el-form-item label="处理凭证" prop="processEvidence">
<UploadImgs v-model="processEvidenceList" :limit="5" />
</el-form-item>
<el-form-item label="业务订单" prop="relatedOrderId">
<el-input v-model="formData.relatedOrderId" placeholder="请输入业务订单" />
</el-form-item>
<el-form-item label="处理人" prop="processUser">
<el-input v-model="formData.processUser" placeholder="请输入处理人" />
</el-form-item>
<el-form-item label="时间" prop="processTime">
<el-date-picker
v-model="formData.processTime as any"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择时间"
/>
</el-form-item>
<el-form-item v-if="formData.processStatus === 2" label="处理完成时间" prop="finishTime">
<el-date-picker
v-model="formData.finishTime as any"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择处理完成时间"
/>
</el-form-item>
<el-form-item label="评价" prop="userRating">
<el-rate
v-model="formData.userRating"
:max="10"
class="!w-full"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
<!-- 售后登记选择弹窗 -->
<Dialog v-model="registerSelectorVisible" title="选择售后登记" width="70%" :appendToBody="true">
<ContentWrap>
<el-form
ref="registerQueryFormRef"
:inline="true"
:model="registerQueryParams"
class="-mb-15px"
label-width="100px"
>
<el-form-item label="售后类型" prop="afterSaleType">
<el-select
v-model="registerQueryParams.afterSaleType"
placeholder="请选择售后类型"
clearable
class="!w-200px"
>
<el-option
v-for="item in afterSaleTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="销售订单ID" prop="orderId">
<el-input
v-model="registerQueryParams.orderId"
class="!w-200px"
clearable
placeholder="请输入销售订单ID"
@keyup.enter="handleRegisterQuery"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleRegisterQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetRegisterQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
</el-form-item>
</el-form>
<el-table v-loading="registerLoading" :data="registerList" show-overflow-tooltip>
<el-table-column label="#" width="55">
<template #default="{ row }">
<el-radio :value="row.id" v-model="selectedRegisterId" @change="handleRegisterSelected(row)">
&nbsp;
</el-radio>
</template>
</el-table-column>
<el-table-column label="登记ID" align="center" prop="id" width="100" />
<el-table-column label="销售订单ID" align="center" prop="orderId" width="120" />
<el-table-column label="售后类型" align="center" prop="afterSaleType" width="120">
<template #default="{ row }">
{{ formatAfterSaleType(row.afterSaleType) }}
</template>
</el-table-column>
<el-table-column label="申请原因" align="center" prop="applyReason" min-width="150" />
<el-table-column label="申请人" align="center" prop="applicant" width="100" />
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="160px"
/>
</el-table>
<Pagination
v-model:limit="registerQueryParams.pageSize"
v-model:page="registerQueryParams.pageNo"
:total="registerTotal"
@pagination="getRegisterList"
/>
</ContentWrap>
</Dialog>
</template>
<script setup lang="ts">
import { Search } from '@element-plus/icons-vue'
import dayjs from 'dayjs'
import { dateFormatter } from '@/utils/formatTime'
import { AfterSaleProcessApi, AfterSaleProcess } from '@/api/erp/aftersale/aftersaleprocess'
import { AfterSaleRegisterApi, AfterSaleRegister } from '@/api/erp/aftersale/aftersaleregister'
import { useUserStoreWithOut } from '@/store/modules/user'
/** ERP 售后处理 表单 */
defineOptions({ name: 'AfterSaleProcessForm' })
const processTypeOptions = [
{ label: '退货入库', value: 1 },
{ label: '换货出库', value: 2 },
{ label: '维修处理', value: 3 },
{ label: '退款处理', value: 4 },
{ label: '取消处理', value: 5 },
{ label: '其他', value: 6 }
]
const processStatusOptions = [
{ label: '退换货', value: 1 },
{ label: '维修申请', value: 2 },
{ label: '售后评价', value: 3 }
]
const processEvidenceList = ref<string[]>([])
// 售后登记选择器相关
const registerSelectorVisible = ref(false)
const selectedRegisterId = ref<number>()
const selectedRegisterLabel = ref<string>('')
const registerLoading = ref(false)
const registerList = ref<AfterSaleRegister[]>([])
const registerTotal = ref(0)
const registerQueryParams = ref({
pageNo: 1,
pageSize: 10,
applyStatus: 2, // 仅已审核通过
afterSaleType: undefined,
orderId: undefined
})
const registerQueryFormRef = ref()
const afterSaleTypeOptions = [
{ label: '退货', value: 1 },
{ label: '换货', value: 2 },
{ label: '维修', value: 3 },
{ label: '退款', value: 4 }
]
/** 打开售后登记选择器 */
const openRegisterSelector = () => {
registerSelectorVisible.value = true
registerQueryParams.value = {
pageNo: 1,
pageSize: 10,
applyStatus: 2,
afterSaleType: undefined,
orderId: undefined
}
getRegisterList()
}
/** 查询售后登记列表 */
const getRegisterList = async () => {
registerLoading.value = true
try {
const data = await AfterSaleRegisterApi.getAfterSaleRegisterPage(registerQueryParams.value)
registerList.value = data.list
registerTotal.value = data.total
} finally {
registerLoading.value = false
}
}
/** 搜索售后登记 */
const handleRegisterQuery = () => {
registerQueryParams.value.pageNo = 1
getRegisterList()
}
/** 重置售后登记查询 */
const resetRegisterQuery = () => {
registerQueryParams.value = {
pageNo: 1,
pageSize: 10,
applyStatus: 2,
afterSaleType: undefined,
orderId: undefined
}
getRegisterList()
}
/** 选中售后登记 */
const handleRegisterSelected = (row: AfterSaleRegister) => {
formData.value.afterSaleId = row.id
selectedRegisterLabel.value = `登记 #${row.id}(订单 ${row.orderId || '-'}`
selectedRegisterId.value = row.id
registerSelectorVisible.value = false
}
/** 格式化售后类型 */
const formatAfterSaleType = (value?: number) => {
if (!value) return '-'
const match = afterSaleTypeOptions.find(item => item.value === value)
return match?.label || String(value)
}
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const userStore = useUserStoreWithOut()
const currentUserNickname = computed(() => userStore.getUser?.nickname || '')
const nowString = () => dayjs().format('YYYY-MM-DD HH:mm:ss')
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref<Partial<AfterSaleProcess>>({
id: undefined,
afterSaleId: undefined,
processType: undefined,
processCount: undefined,
processResult: undefined,
processEvidence: '',
relatedOrderId: undefined,
processStatus: undefined,
processFailReason: undefined,
processUser: undefined,
processTime: undefined,
finishTime: undefined,
userRating: undefined
})
const formRules = reactive({
afterSaleId: [{ required: true, message: '售后登记不能为空', trigger: 'change' }],
processType: [{ required: true, message: '处理类型不能为空', trigger: 'change' }],
processResult: [{ required: true, message: '处理结果不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
// 同步处理凭证列表和表单数据
watch(
() => formData.value.processEvidence,
(newVal: string) => {
processEvidenceList.value = newVal ? newVal.split(',').filter(url => url.trim()) : []
},
{ immediate: true }
)
watch(
processEvidenceList,
(newList: string[]) => {
formData.value.processEvidence = newList?.length ? newList.join(',') : ''
},
{ deep: true }
)
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await AfterSaleProcessApi.getAfterSaleProcess(id)
// 如果有售后登记ID获取登记信息显示
if (formData.value.afterSaleId) {
const register = await AfterSaleRegisterApi.getAfterSaleRegister(formData.value.afterSaleId)
selectedRegisterLabel.value = `登记 #${register.id}(订单 ${register.orderId || '-'}`
selectedRegisterId.value = register.id
}
ensureDefaultsForExisting()
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
await formRef.value.validate()
// 提交请求
formLoading.value = true
try {
let data = formData.value as unknown as AfterSaleProcess
// 如果上传了处理凭证,自动设置为处理完成状态
if (data.processEvidence && data.processEvidence.trim() !== '') {
data = {
...data,
processStatus: 2, // 处理完成
finishTime: nowString()
}
}
if (formType.value === 'create') {
await AfterSaleProcessApi.createAfterSaleProcess(data)
message.success(t('common.createSuccess'))
} else {
await AfterSaleProcessApi.updateAfterSaleProcess(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
afterSaleId: undefined,
processType: undefined,
processCount: undefined,
processResult: undefined,
processEvidence: '',
relatedOrderId: undefined,
processStatus: 1, // 新增时默认为处理中状态
processFailReason: undefined,
processUser: currentUserNickname.value || undefined,
processTime: nowString(),
finishTime: undefined,
userRating: undefined
}
selectedRegisterLabel.value = ''
selectedRegisterId.value = undefined
formRef.value?.resetFields()
}
const ensureDefaultsForExisting = () => {
if (!formData.value.processUser) {
formData.value.processUser = currentUserNickname.value || undefined
}
if (!formData.value.processTime) {
formData.value.processTime = nowString()
}
if (formData.value.processStatus === 2 && !formData.value.finishTime) {
formData.value.finishTime = nowString()
}
}
</script>

View File

@@ -0,0 +1,405 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="120px"
>
<el-form-item label="售后登记号" prop="afterSaleId">
<el-input
v-model="queryParams.afterSaleId"
placeholder="请输入售后登记号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="处理类型" prop="processType">
<el-select
v-model="queryParams.processType"
placeholder="请选择处理类型"
clearable
filterable
class="!w-240px"
>
<el-option
v-for="item in processTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="处理结果" prop="processResult">
<el-input
v-model="queryParams.processResult"
placeholder="请输入处理结果"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="处理状态" prop="processStatus">
<el-select
v-model="queryParams.processStatus"
placeholder="请选择处理状态"
clearable
filterable
class="!w-240px"
>
<el-option
v-for="item in processStatusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="处理人" prop="processUser">
<el-input
v-model="queryParams.processUser"
placeholder="请输入处理人"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="处理完成时间" prop="finishTime">
<el-date-picker
v-model="queryParams.finishTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-220px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['erp:after-sale-process:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['erp:after-sale-process:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button
type="danger"
plain
:disabled="isEmpty(checkedIds)"
@click="handleDeleteBatch"
v-hasPermi="['erp:after-sale-process:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
row-key="id"
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleRowCheckboxChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="序号" align="center" type="index" width="80" />
<el-table-column
label="处理类型"
align="center"
prop="processType"
:formatter="formatProcessType"
/>
<el-table-column
label="处理状态"
align="center"
prop="processStatus"
:formatter="formatProcessStatus"
/>
<el-table-column label="处理人" align="center" prop="processUser" />
<el-table-column label="用户评价" align="center" prop="userRating">
<template #default="scope">
<el-tooltip :content="`${scope.row.userRating || 0} 分`" placement="top">
<el-icon size="20" :style="{ color: getRatingColor(scope.row.userRating) }">
<Star />
</el-icon>
</el-tooltip>
</template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center" min-width="200px">
<template #default="scope">
<el-button link type="primary" @click="openDetail(scope.row)">详情</el-button>
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['erp:after-sale-process:update']"
>
编辑
</el-button>
<el-button
link
type="success"
@click="handleProcessComplete(scope.row)"
v-if="scope.row.processStatus === 1"
v-hasPermi="['erp:after-sale-process:update']"
>
处理完成
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['erp:after-sale-process:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<AfterSaleProcessForm ref="formRef" @success="getList" />
<!-- 详情弹窗 -->
<el-dialog v-model="detailVisible" title="售后处理详情" width="560px">
<el-descriptions v-if="detailRow" :column="2" border>
<el-descriptions-item label="售后登记号">{{ detailRow.afterSaleId }}</el-descriptions-item>
<el-descriptions-item label="处理类型">{{ formatProcessType(detailRow, null as any, detailRow.processType as any) }}</el-descriptions-item>
<el-descriptions-item label="处理数量">{{ detailRow.processCount ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="业务订单号">{{ detailRow.relatedOrderId ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="处理结果">{{ detailRow.processResult ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="失败原因">{{ detailRow.processFailReason ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="处理状态">{{ formatProcessStatus(detailRow, null as any, detailRow.processStatus as any) }}</el-descriptions-item>
<el-descriptions-item label="处理人">{{ detailRow.processUser ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="处理时间">{{ dateFormatter(detailRow, null as any, detailRow.processTime as any) }}</el-descriptions-item>
<el-descriptions-item label="处理完成时间">{{ dateFormatter(detailRow, null as any, detailRow.finishTime as any) }}</el-descriptions-item>
<el-descriptions-item label="用户评价">
<el-tooltip :content="`${detailRow.userRating || 0} 分`" placement="top">
<el-icon size="20" :style="{ color: getRatingColor(detailRow.userRating) }">
<Star />
</el-icon>
</el-tooltip>
</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ dateFormatter(detailRow, null as any, (detailRow as any).createTime) }}</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button @click="detailVisible = false">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import dayjs from 'dayjs'
import { isEmpty } from '@/utils/is'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { AfterSaleProcessApi, AfterSaleProcess } from '@/api/erp/aftersale/aftersaleprocess'
import { useUserStoreWithOut } from '@/store/modules/user'
import AfterSaleProcessForm from './AfterSaleProcessForm.vue'
import { Star } from '@element-plus/icons-vue'
const processTypeOptions = [
{ label: '退货入库', value: 1 },
{ label: '换货出库', value: 2 },
{ label: '维修处理', value: 3 },
{ label: '退款处理', value: 4 },
{ label: '取消处理', value: 5 },
{ label: '其他', value: 6 }
]
const processStatusOptions = [
{ label: '处理中', value: 1 },
{ label: '处理完成', value: 2 },
{ label: '处理失败', value: 3 }
]
const optionLabel = (options: { label: string; value: number }[], value?: number | string) => {
if (value === undefined || value === null || value === '') return '-'
const match = options.find((item) => item.value === value || item.value === Number(value))
return match?.label ?? String(value)
}
const formatProcessType = (_row: AfterSaleProcess, _column: any, value: number) =>
optionLabel(processTypeOptions, value)
const formatProcessStatus = (_row: AfterSaleProcess, _column: any, value: number) =>
optionLabel(processStatusOptions, value)
/** ERP 售后处理 列表 */
defineOptions({ name: 'AfterSaleProcess' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const userStore = useUserStoreWithOut()
const loading = ref(true) // 列表的加载中
const list = ref<AfterSaleProcess[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
afterSaleId: undefined,
processType: undefined,
processCount: undefined,
processResult: undefined,
processEvidence: undefined,
relatedOrderId: undefined,
processStatus: undefined,
processFailReason: undefined,
processUser: undefined,
processTime: [],
finishTime: [],
createTime: [],
userRating: undefined
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await AfterSaleProcessApi.getAfterSaleProcessPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await AfterSaleProcessApi.deleteAfterSaleProcess(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 处理完成操作 */
const handleProcessComplete = async (row: AfterSaleProcess) => {
try {
// 处理完成的二次确认
await message.confirm('确认处理完成吗?')
// 更新状态为处理完成
const updateData = {
...row,
processStatus: 2,
processUser: userStore.getUser?.nickname || row.processUser,
finishTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
}
await AfterSaleProcessApi.updateAfterSaleProcess(updateData)
message.success('处理完成')
// 刷新列表
await getList()
} catch {}
}
/** 批量删除ERP 售后处理 */
const handleDeleteBatch = async () => {
try {
// 删除的二次确认
await message.delConfirm()
await AfterSaleProcessApi.deleteAfterSaleProcessList(checkedIds.value);
message.success(t('common.delSuccess'))
await getList();
} catch {}
}
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (records: AfterSaleProcess[]) => {
checkedIds.value = records.map((item) => item.id);
}
const detailVisible = ref(false)
const detailRow = ref<AfterSaleProcess | null>(null)
const openDetail = (row: AfterSaleProcess) => {
detailRow.value = row
detailVisible.value = true
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
const data = await AfterSaleProcessApi.exportAfterSaleProcess(queryParams)
download.excel(data, 'ERP 售后处理.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 根据评分获取颜色 */
const getRatingColor = (rating?: number) => {
if (!rating || rating === 0) return '#cccccc' // 灰色表示无评价
if (rating < 4) return '#ff4d4f' // 红色表示差评
if (rating < 7) return '#ffa940' // 橙色表示一般
if (rating < 9) return '#52c41a' // 绿色表示好评
return '#1890ff' // 蓝色表示优秀
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

View File

@@ -0,0 +1,307 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="销售订单" prop="orderId">
<el-input
v-model="selectedOrderNo"
placeholder="请选择销售订单"
readonly
@click="openOrderSelector"
style="cursor: pointer"
>
<template #append>
<el-button :icon="Search" @click="openOrderSelector" />
</template>
</el-input>
</el-form-item>
<el-form-item label="售后类型" prop="afterSaleType">
<el-select v-model="formData.afterSaleType" placeholder="请选择售后类型" filterable>
<el-option
v-for="item in afterSaleTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="申请原因" prop="applyReason">
<el-input v-model="formData.applyReason" placeholder="请输入申请原因" />
</el-form-item>
<el-form-item label="联系人" prop="contactName">
<el-input v-model="formData.contactName" placeholder="请输入联系人" />
</el-form-item>
<el-form-item label="联系电话" prop="contactPhone">
<el-input v-model="formData.contactPhone" placeholder="请输入联系电话" maxlength="20" />
</el-form-item>
<el-form-item label="申请人" prop="applicant">
<el-input v-model="formData.applicant" placeholder="请输入申请人" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
<!-- 销售订单选择弹窗 -->
<Dialog v-model="orderSelectorVisible" title="选择销售订单" width="70%" :appendToBody="true">
<ContentWrap>
<el-form
ref="orderQueryFormRef"
:inline="true"
:model="orderQueryParams"
class="-mb-15px"
label-width="100px"
>
<el-form-item label="订单单号" prop="no">
<el-input
v-model="orderQueryParams.no"
class="!w-200px"
clearable
placeholder="请输入订单单号"
@keyup.enter="handleOrderQuery"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleOrderQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetOrderQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
</el-form-item>
</el-form>
<el-table v-loading="orderLoading" :data="orderList" show-overflow-tooltip>
<el-table-column label="#" width="55">
<template #default="{ row }">
<el-radio :value="row.id" v-model="selectedOrderId" @change="handleOrderSelected(row)">
&nbsp;
</el-radio>
</template>
</el-table-column>
<el-table-column label="订单单号" align="center" prop="no" min-width="140" />
<el-table-column label="客户" align="center" prop="customerName" min-width="120" />
<el-table-column
label="订单时间"
align="center"
prop="orderTime"
:formatter="dateFormatter"
width="160px"
/>
</el-table>
<Pagination
v-model:limit="orderQueryParams.pageSize"
v-model:page="orderQueryParams.pageNo"
:total="orderTotal"
@pagination="getOrderList"
/>
</ContentWrap>
</Dialog>
</template>
<script setup lang="ts">
import { Search } from '@element-plus/icons-vue'
import dayjs from 'dayjs'
import { dateFormatter } from '@/utils/formatTime'
import { AfterSaleRegisterApi, AfterSaleRegister } from '@/api/erp/aftersale/aftersaleregister'
import { SaleOrderApi, SaleOrderVO } from '@/api/erp/sale/order'
import { useUserStoreWithOut } from '@/store/modules/user'
/** ERP 售后登记 表单 */
defineOptions({ name: 'AfterSaleRegisterForm' })
const afterSaleTypeOptions = [
{ label: '退货', value: 1 },
{ label: '换货', value: 2 },
{ label: '维修', value: 3 },
{ label: '退款', value: 4 }
]
const applyStatusOptions = [
{ label: '待审核', value: 1 },
{ label: '审核通过', value: 2 },
{ label: '审核驳回', value: 3 },
{ label: '已取消', value: 4 }
]
// 销售订单选择器相关
const orderSelectorVisible = ref(false)
const selectedOrderId = ref<number>()
const selectedOrderNo = ref<string>('')
const orderLoading = ref(false)
const orderList = ref<SaleOrderVO[]>([])
const orderTotal = ref(0)
const orderQueryParams = ref({
pageNo: 1,
pageSize: 10,
no: ''
})
const orderQueryFormRef = ref()
/** 打开订单选择器 */
const openOrderSelector = () => {
orderSelectorVisible.value = true
orderQueryParams.value = {
pageNo: 1,
pageSize: 10,
no: ''
}
getOrderList()
}
/** 查询订单列表 */
const getOrderList = async () => {
orderLoading.value = true
try {
const data = await SaleOrderApi.getSaleOrderPage(orderQueryParams.value)
orderList.value = data.list
orderTotal.value = data.total
} finally {
orderLoading.value = false
}
}
/** 搜索订单 */
const handleOrderQuery = () => {
orderQueryParams.value.pageNo = 1
getOrderList()
}
/** 重置订单查询 */
const resetOrderQuery = () => {
orderQueryParams.value = {
pageNo: 1,
pageSize: 10,
no: ''
}
getOrderList()
}
/** 选中订单 */
const handleOrderSelected = (row: SaleOrderVO) => {
formData.value.orderId = row.id
selectedOrderNo.value = row.no || ''
selectedOrderId.value = row.id
orderSelectorVisible.value = false
}
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const userStore = useUserStoreWithOut()
const currentUserNickname = computed(() => userStore.getUser?.nickname || '')
const nowString = () => dayjs().format('YYYY-MM-DD HH:mm:ss')
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref<Partial<AfterSaleRegister>>({
id: undefined,
orderId: undefined,
orderItemId: undefined,
afterSaleType: undefined,
applyReason: undefined,
contactName: undefined,
contactPhone: undefined,
applyStatus: undefined,
rejectReason: undefined,
applicant: undefined,
auditUser: undefined,
auditTime: undefined
})
const formRules = reactive({
afterSaleType: [{ required: true, message: '售后类型不能为空', trigger: 'change' }],
applyReason: [{ required: true, message: '申请原因不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await AfterSaleRegisterApi.getAfterSaleRegister(id)
// 如果有订单ID获取订单号显示
if (formData.value.orderId) {
const order = await SaleOrderApi.getSaleOrder(formData.value.orderId)
selectedOrderNo.value = order.no || ''
selectedOrderId.value = order.id
}
ensureDefaultsForExisting()
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单(当前无必填规则,直接通过)
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as AfterSaleRegister
if (formType.value === 'create') {
await AfterSaleRegisterApi.createAfterSaleRegister(data)
message.success(t('common.createSuccess'))
} else {
await AfterSaleRegisterApi.updateAfterSaleRegister(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
orderId: undefined,
orderItemId: undefined,
afterSaleType: undefined,
applyReason: undefined,
contactName: undefined,
contactPhone: undefined,
applyStatus: 1, // 新增时默认为待审核状态
rejectReason: undefined,
applicant: currentUserNickname.value || undefined,
auditUser: undefined,
auditTime: undefined
}
selectedOrderNo.value = ''
selectedOrderId.value = undefined
formRef.value?.resetFields()
}
const ensureDefaultsForExisting = () => {
if (!formData.value.applicant) {
formData.value.applicant = currentUserNickname.value || undefined
}
if (formData.value.applyStatus === 2) {
if (!formData.value.auditUser) {
formData.value.auditUser = currentUserNickname.value || undefined
}
if (!formData.value.auditTime) {
formData.value.auditTime = nowString()
}
}
}
</script>

View File

@@ -0,0 +1,391 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="120px"
>
<el-form-item label="销售订单" prop="orderId">
<el-select
v-model="queryParams.orderId"
placeholder="请选择销售订单"
clearable
filterable
class="!w-240px"
>
<el-option
v-for="item in saleOrderOptions"
:key="item.id"
:label="item.no"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="售后类型" prop="afterSaleType">
<el-select
v-model="queryParams.afterSaleType"
placeholder="请选择售后类型"
clearable
filterable
class="!w-240px"
>
<el-option
v-for="item in afterSaleTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="申请原因" prop="applyReason">
<el-input
v-model="queryParams.applyReason"
placeholder="请输入申请原因"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="申请状态" prop="applyStatus">
<el-select
v-model="queryParams.applyStatus"
placeholder="请选择申请状态"
clearable
filterable
class="!w-240px"
>
<el-option
v-for="item in applyStatusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="申请人" prop="applicant">
<el-input
v-model="queryParams.applicant"
placeholder="请输入申请人"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="审核人" prop="auditUser">
<el-input
v-model="queryParams.auditUser"
placeholder="请输入审核人"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['erp:after-sale-register:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['erp:after-sale-register:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button
type="danger"
plain
:disabled="isEmpty(checkedIds)"
@click="handleDeleteBatch"
v-hasPermi="['erp:after-sale-register:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
row-key="id"
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleRowCheckboxChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="序号" align="center" type="index" width="80" />
<el-table-column
label="售后类型"
align="center"
prop="afterSaleType"
:formatter="formatAfterSaleType"
/>
<el-table-column
label="申请状态"
align="center"
prop="applyStatus"
:formatter="formatApplyStatus"
/>
<el-table-column label="申请人" align="center" prop="applicant" />
<el-table-column label="审核人" align="center" prop="auditUser" />
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center" min-width="200px">
<template #default="scope">
<el-button link type="primary" @click="openDetail(scope.row)">详情</el-button>
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['erp:after-sale-register:update']"
>
编辑
</el-button>
<el-button
link
type="success"
@click="handleAudit(scope.row)"
v-if="scope.row.applyStatus === 1"
v-hasPermi="['erp:after-sale-register:update']"
>
审核通过
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['erp:after-sale-register:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<AfterSaleRegisterForm ref="formRef" @success="getList" />
<!-- 详情弹窗 -->
<el-dialog v-model="detailVisible" title="售后登记详情" width="560px">
<el-descriptions v-if="detailRow" :column="2" border>
<el-descriptions-item label="销售订单">{{ detailRow.orderId }}</el-descriptions-item>
<el-descriptions-item label="售后类型">{{ formatAfterSaleType(detailRow, null as any, detailRow.afterSaleType as any) }}</el-descriptions-item>
<el-descriptions-item label="申请原因">{{ detailRow.applyReason ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="联系人">{{ detailRow.contactName ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ detailRow.contactPhone ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="申请状态">{{ formatApplyStatus(detailRow, null as any, detailRow.applyStatus as any) }}</el-descriptions-item>
<el-descriptions-item label="驳回原因">{{ detailRow.rejectReason ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="申请人">{{ detailRow.applicant ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="审核人">{{ detailRow.auditUser ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="审核时间">{{ dateFormatter(detailRow, null as any, detailRow.auditTime as any) }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ dateFormatter(detailRow, null as any, (detailRow as any).createTime) }}</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button @click="detailVisible = false">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import dayjs from 'dayjs'
import { isEmpty } from '@/utils/is'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { AfterSaleRegisterApi, AfterSaleRegister } from '@/api/erp/aftersale/aftersaleregister'
import { SaleOrderApi, SaleOrderVO } from '@/api/erp/sale/order'
import { useUserStoreWithOut } from '@/store/modules/user'
import AfterSaleRegisterForm from './AfterSaleRegisterForm.vue'
const afterSaleTypeOptions = [
{ label: '退货', value: 1 },
{ label: '换货', value: 2 },
{ label: '维修', value: 3 },
{ label: '退款', value: 4 }
]
const applyStatusOptions = [
{ label: '待审核', value: 1 },
{ label: '审核通过', value: 2 },
{ label: '审核驳回', value: 3 },
{ label: '已取消', value: 4 }
]
const optionLabel = (options: { label: string; value: number }[], value?: number | string) => {
if (value === undefined || value === null || value === '') return '-'
const match = options.find((item) => item.value === value || item.value === Number(value))
return match?.label ?? String(value)
}
const formatAfterSaleType = (_row: AfterSaleRegister, _column: any, value: number) =>
optionLabel(afterSaleTypeOptions, value)
const formatApplyStatus = (_row: AfterSaleRegister, _column: any, value: number) =>
optionLabel(applyStatusOptions, value)
const saleOrderOptions = ref<SaleOrderVO[]>([])
const fetchSaleOrderOptions = async () => {
const data = await SaleOrderApi.getSaleOrderPage({ pageNo: 1, pageSize: 100 })
saleOrderOptions.value = data.list || []
}
/** ERP 售后登记 列表 */
defineOptions({ name: 'AfterSaleRegister' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const userStore = useUserStoreWithOut()
const loading = ref(true) // 列表的加载中
const list = ref<AfterSaleRegister[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
orderId: undefined,
orderItemId: undefined,
afterSaleType: undefined,
applyReason: undefined,
contactName: undefined,
contactPhone: undefined,
applyStatus: undefined,
rejectReason: undefined,
applicant: undefined,
auditUser: undefined,
auditTime: [],
createTime: []
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await AfterSaleRegisterApi.getAfterSaleRegisterPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await AfterSaleRegisterApi.deleteAfterSaleRegister(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 审核通过操作 */
const handleAudit = async (row: AfterSaleRegister) => {
try {
// 审核确认
await message.confirm('确认审核通过该售后登记吗?')
// 更新状态为审核通过
const updateData = {
...row,
applyStatus: 2,
auditUser: userStore.getUser?.nickname || '',
auditTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
}
await AfterSaleRegisterApi.updateAfterSaleRegister(updateData)
message.success('审核通过成功')
// 刷新列表
await getList()
} catch {}
}
/** 批量删除ERP 售后登记 */
const handleDeleteBatch = async () => {
try {
// 删除的二次确认
await message.delConfirm()
await AfterSaleRegisterApi.deleteAfterSaleRegisterList(checkedIds.value);
message.success(t('common.delSuccess'))
await getList();
} catch {}
}
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (records: AfterSaleRegister[]) => {
checkedIds.value = records.map((item) => item.id);
}
const detailVisible = ref(false)
const detailRow = ref<AfterSaleRegister | null>(null)
const openDetail = (row: AfterSaleRegister) => {
detailRow.value = row
detailVisible.value = true
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
const data = await AfterSaleRegisterApi.exportAfterSaleRegister(queryParams)
download.excel(data, 'ERP 售后登记.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
fetchSaleOrderOptions()
})
</script>

View File

@@ -0,0 +1,267 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="600px">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="90px"
v-loading="formLoading"
>
<!-- 客户基本信息 -->
<el-divider content-position="left">客户信息</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="客户名称" prop="customerName">
<el-select
v-model="formData.customerName"
placeholder="请选择客户"
clearable
filterable
@change="handleCustomerChange"
>
<el-option
v-for="customer in customerList"
:key="customer.id"
:label="customer.name"
:value="customer.name"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系方式" prop="contactInfo">
<el-input v-model="formData.contactInfo" placeholder="请输入联系方式" clearable />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="客户类型" prop="customerType">
<el-select v-model="formData.customerType" placeholder="请选择客户类型" clearable>
<el-option
v-for="item in customerTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户用途" prop="customerUsage">
<el-input v-model="formData.customerUsage" placeholder="请输入客户用途" clearable />
</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="产品名称" prop="productName">
<el-input v-model="formData.productName" placeholder="请输入产品名称" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="产品评价" prop="productEvaluation">
<el-input
v-model="formData.productEvaluation"
placeholder="请输入产品评价"
type="textarea"
:rows="2"
clearable
/>
</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="采购意愿" prop="repurchaseIntention">
<el-select v-model="formData.repurchaseIntention" placeholder="请选择采购意愿" clearable>
<el-option
v-for="item in repurchaseOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="服务评分" prop="serviceRating">
<el-rate
v-model="formData.serviceRating"
:max="5"
:allow-half="false"
:show-text="false"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm" :loading="formLoading">
确定
</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { AfterSalesVisitApi, AfterSalesVisit } from '@/api/erp/aftersale/aftersalesvisit'
import { CustomerApi, CustomerVO } from '@/api/erp/sale/customer'
/** 售后回访 表单 */
defineOptions({ name: 'AfterSalesVisitForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref({
id: undefined,
customerName: undefined,
contactInfo: undefined,
customerUsage: undefined,
productName: undefined,
productEvaluation: undefined,
customerType: undefined,
repurchaseIntention: undefined,
serviceRating: undefined
})
// 客户类型选项
const customerTypeOptions = [
{ value: 'large_food_factory', label: '大型食品厂' },
{ value: 'small_food_factory', label: '中小型加工厂' },
{ value: 'distributor', label: '经销商' },
{ value: 'catering_chain', label: '餐饮连锁' },
{ value: 'government_unit', label: '机关单位' },
{ value: 'other', label: '其他' }
]
// 重复采购意愿选项
const repurchaseOptions = [
{ value: 'definitely', label: '一定会' },
{ value: 'probably', label: '应该会' },
{ value: 'uncertain', label: '不确定' },
{ value: 'unlikely', label: '可能不会' },
{ value: 'never', label: '肯定不会' }
]
const formRules = reactive({
customerName: [
{ required: true, message: '客户名称不能为空', trigger: 'blur' },
{ min: 2, max: 50, message: '客户名称长度应在2-50字符之间', trigger: 'blur' }
],
contactInfo: [
{ required: true, message: '联系方式不能为空', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
],
productName: [
{ required: true, message: '产品名称不能为空', trigger: 'blur' },
{ min: 1, max: 100, message: '产品名称长度应在1-100字符之间', trigger: 'blur' }
],
customerType: [{ required: true, message: '请选择客户类型', trigger: 'change' }],
repurchaseIntention: [{ required: true, message: '请选择采购意愿', trigger: 'change' }],
serviceRating: [
{ required: true, message: '请给出服务评分', trigger: 'change' },
{ type: 'number', min: 0, max: 5, message: '评分应在0-5之间', trigger: 'change' }
]
})
const formRef = ref() // 表单 Ref
const customerList = ref<CustomerVO[]>([]) // 客户列表
/** 加载客户列表 */
const loadCustomerList = async () => {
try {
const data = await CustomerApi.getCustomerSimpleList()
customerList.value = data
} catch (error) {
console.error('加载客户列表失败:', error)
}
}
/** 处理客户选择 */
const handleCustomerChange = (customerName: string) => {
if (!customerName) {
formData.value.contactInfo = ''
return
}
const selectedCustomer = customerList.value.find(customer => customer.name === customerName)
if (selectedCustomer) {
// 自动填充联系方式,优先使用手机号,其次电话
formData.value.contactInfo = selectedCustomer.mobile || selectedCustomer.telephone || ''
}
}
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await AfterSalesVisitApi.getAfterSalesVisit(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
await formRef.value.validate()
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as AfterSalesVisit
if (formType.value === 'create') {
await AfterSalesVisitApi.createAfterSalesVisit(data)
message.success(t('common.createSuccess'))
} else {
await AfterSalesVisitApi.updateAfterSalesVisit(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
customerName: undefined,
contactInfo: undefined,
customerUsage: undefined,
productName: undefined,
productEvaluation: undefined,
customerType: undefined,
repurchaseIntention: undefined,
serviceRating: undefined
}
formRef.value?.resetFields()
}
/** 初始化 */
onMounted(() => {
loadCustomerList()
})
</script>

View File

@@ -0,0 +1,306 @@
<template>
<!-- 统计卡片 -->
<ContentWrap>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
<SummaryCard title="总回访数" :value="total" icon="ep:user" icon-color="text-blue-600" icon-bg-color="bg-blue-100" />
<SummaryCard title="平均评分" :value="avgRating" :decimals="1" icon="ep:star" icon-color="text-yellow-600"
icon-bg-color="bg-yellow-100" />
<SummaryCard title="今日新增" :value="todayCount" icon="ep:calendar" icon-color="text-green-600"
icon-bg-color="bg-green-100" />
<SummaryCard title="五星评分" :value="fiveStarCount" icon="ep:star" icon-color="text-yellow-600"
icon-bg-color="bg-yellow-100" />
</div>
<!-- 搜索工作栏 -->
<el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="80px">
<el-form-item label="客户名称" prop="customerName">
<el-input v-model="queryParams.customerName" placeholder="请输入客户名称" clearable @keyup.enter="handleQuery"
class="!w-200px" />
</el-form-item>
<el-form-item label="联系方式" prop="contactInfo">
<el-input v-model="queryParams.contactInfo" placeholder="请输入联系方式" clearable @keyup.enter="handleQuery"
class="!w-200px" />
</el-form-item>
<el-form-item label="产品名称" prop="productName">
<el-input v-model="queryParams.productName" placeholder="请输入产品名称" clearable @keyup.enter="handleQuery"
class="!w-200px" />
</el-form-item>
<el-form-item label="客户类型" prop="customerType">
<el-select v-model="queryParams.customerType" placeholder="请选择客户类型" clearable class="!w-200px">
<el-option v-for="item in customerTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="采购意愿" prop="repurchaseIntention">
<el-select v-model="queryParams.repurchaseIntention" placeholder="请选择采购意愿" clearable class="!w-200px">
<el-option v-for="item in repurchaseOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker v-model="queryParams.createTime" value-format="YYYY-MM-DD HH:mm:ss" type="daterange"
start-placeholder="开始日期" end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" class="!w-220px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> 搜索
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" /> 重置
</el-button>
</el-form-item>
</el-form>
<!-- 操作按钮 -->
<div class="flex flex-wrap gap-2 mb-4">
<el-button type="primary" plain @click="openForm('create')" v-hasPermi="['erp:after-sales-visit:create']">
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button type="success" plain @click="handleExport" :loading="exportLoading"
v-hasPermi="['erp:after-sales-visit:export']">
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button type="danger" plain :disabled="isEmpty(checkedIds)" @click="handleDeleteBatch"
v-hasPermi="['erp:after-sales-visit:delete']">
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
</div>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table row-key="id" v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"
@selection-change="handleRowCheckboxChange" class="mb-4">
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="客户名称" min-width="120" prop="customerName" show-overflow-tooltip />
<el-table-column label="联系方式" min-width="120" prop="contactInfo" show-overflow-tooltip />
<el-table-column label="产品名称" min-width="120" prop="productName" show-overflow-tooltip />
<el-table-column label="客户类型" min-width="140" align="center">
<template #default="scope">
<el-tag :type="getCustomerTypeColor(scope.row.customerType)">
{{ getCustomerTypeLabel(scope.row.customerType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="采购意愿" min-width="100" align="center">
<template #default="scope">
<span :class="getRepurchaseColor(scope.row.repurchaseIntention)">
{{ getRepurchaseLabel(scope.row.repurchaseIntention) }}
</span>
</template>
</el-table-column>
<el-table-column label="服务评分" width="120" align="center">
<template #default="scope">
<el-rate :model-value="scope.row.serviceRating" disabled :max="5" show-score text-color="#ff9900"
score-template="{value}" />
</template>
</el-table-column>
<el-table-column label="创建时间" width="160" align="center" prop="createTime" :formatter="dateFormatter" />
<el-table-column label="操作" width="140" align="center" fixed="right">
<template #default="scope">
<el-button link type="primary" size="small" @click="openForm('update', scope.row.id)"
v-hasPermi="['erp:after-sales-visit:update']">
<Icon icon="ep:edit" class="mr-1" />编辑
</el-button>
<el-button link type="danger" size="small" @click="handleDelete(scope.row.id)"
v-hasPermi="['erp:after-sales-visit:delete']">
<Icon icon="ep:delete" class="mr-1" />删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="getList" />
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<AfterSalesVisitForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { isEmpty } from '@/utils/is'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import SummaryCard from '@/components/SummaryCard/index.vue'
import { AfterSalesVisitApi, AfterSalesVisit } from '@/api/erp/aftersale/aftersalesvisit'
import AfterSalesVisitForm from './AfterSalesVisitForm.vue'
/** 售后回访 列表 */
defineOptions({ name: 'AfterSalesVisit' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中
const list = ref<AfterSalesVisit[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
const activeNames = ref(['search']) // 折叠面板默认展开
// 客户类型选项
const customerTypeOptions = [
{ value: 'large_food_factory', label: '大型食品厂' },
{ value: 'small_food_factory', label: '中小型加工厂' },
{ value: 'distributor', label: '经销商' },
{ value: 'catering_chain', label: '餐饮连锁' },
{ value: 'government_unit', label: '机关单位' },
{ value: 'other', label: '其他' }
]
// 重复采购意愿选项
const repurchaseOptions = [
{ value: 'definitely', label: '一定会' },
{ value: 'probably', label: '应该会' },
{ value: 'uncertain', label: '不确定' },
{ value: 'unlikely', label: '可能不会' },
{ value: 'never', label: '肯定不会' }
]
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
customerName: undefined,
contactInfo: undefined,
customerUsage: undefined,
productName: undefined,
productEvaluation: undefined,
customerType: undefined,
repurchaseIntention: undefined,
serviceRating: undefined,
createTime: []
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await AfterSalesVisitApi.getAfterSalesVisitPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
// 重置评分查询条件
queryParams.serviceRating = undefined
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await AfterSalesVisitApi.deleteAfterSalesVisit(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch { }
}
/** 批量删除售后回访 */
const handleDeleteBatch = async () => {
try {
// 删除的二次确认
await message.delConfirm()
await AfterSalesVisitApi.deleteAfterSalesVisitList(checkedIds.value);
message.success(t('common.delSuccess'))
await getList();
} catch { }
}
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (records: AfterSalesVisit[]) => {
checkedIds.value = records.map((item) => item.id);
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
const data = await AfterSalesVisitApi.exportAfterSalesVisit(queryParams)
download.excel(data, '售后回访.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 统计数据计算 */
const avgRating = computed(() => {
if (list.value.length === 0) return 0
const sum = list.value.reduce((acc, item) => acc + (item.serviceRating || 0), 0)
return sum / list.value.length
})
const todayCount = computed(() => {
const today = new Date().toISOString().split('T')[0]
return list.value.filter(item => {
const createDate = new Date(item.createTime).toISOString().split('T')[0]
return createDate === today
}).length
})
const fiveStarCount = computed(() => {
return list.value.filter(item => (item.serviceRating || 0) === 5).length
})
/** 格式化函数 */
const getCustomerTypeLabel = (value: string) => {
const option = customerTypeOptions.find(item => item.value === value)
return option ? option.label : value
}
const getCustomerTypeColor = (value: string) => {
const colorMap = {
'large_food_factory': 'success',
'small_food_factory': 'info',
'distributor': 'warning',
'catering_chain': 'danger',
'government_unit': 'primary',
'other': ''
}
return colorMap[value as keyof typeof colorMap] || ''
}
const getRepurchaseLabel = (value: string) => {
const option = repurchaseOptions.find(item => item.value === value)
return option ? option.label : value
}
const getRepurchaseColor = (value: string) => {
const colorMap = {
'definitely': 'text-green-600 font-medium',
'probably': 'text-blue-600',
'uncertain': 'text-yellow-600',
'unlikely': 'text-orange-600',
'never': 'text-red-600'
}
return colorMap[value as keyof typeof colorMap] || ''
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

View File

@@ -1,134 +1,78 @@
<template> <template>
<div class="mobile-approved"> <!-- 移动端布局 -->
<!-- 顶部操作栏 --> <div v-if="isMobile" class="mobile-approved">
<div class="mobile-header"> <div class="mobile-header">
<div class="mobile-header__search"> <div class="mobile-header__search">
<el-select <el-select v-model="queryParams.bizTableName" placeholder="业务类型" clearable style="width: 100%" @change="handleQuery"><el-option v-for="item in bizTableOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select>
v-model="queryParams.bizTableName"
placeholder="业务类型"
clearable
style="width: 100%"
@change="handleQuery"
>
<el-option
v-for="item in bizTableOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
<div class="mobile-header__actions">
<el-button :icon="Filter" circle @click="filterVisible = true" />
</div> </div>
<div class="mobile-header__actions"><el-button :icon="Filter" circle @click="filterVisible = true" /></div>
</div> </div>
<!-- 卡片列表 -->
<div class="mobile-list" v-loading="loading"> <div class="mobile-list" v-loading="loading">
<div v-if="list.length === 0 && !loading" class="mobile-empty"> <div v-if="list.length === 0 && !loading" class="mobile-empty"><el-empty description="暂无已审批记录" /></div>
<el-empty description="暂无已审批记录" /> <div v-for="item in list" :key="item.id" class="mobile-card" @click="handleViewRecords(item)">
</div> <div class="mobile-card__header"><span class="mobile-card__no">{{ item.bizId }}</span><el-tag :type="getResultTagType(item.approvalResult)" size="small">{{ getApprovalResultText(item.approvalResult) }}</el-tag></div>
<div
v-for="item in list"
:key="item.id"
class="mobile-card"
@click="handleViewRecords(item)"
>
<div class="mobile-card__header">
<span class="mobile-card__no">{{ item.bizId }}</span>
<el-tag :type="getResultTagType(item.approvalResult)" size="small">
{{ getApprovalResultText(item.approvalResult) }}
</el-tag>
</div>
<div class="mobile-card__body"> <div class="mobile-card__body">
<div class="mobile-card__row"> <div class="mobile-card__row"><span class="mobile-card__label">业务类型</span><span class="mobile-card__value">{{ getBizTableLabel(item.bizTableName) }}</span></div>
<span class="mobile-card__label">业务类型</span> <div class="mobile-card__row"><span class="mobile-card__label">申请人</span><span class="mobile-card__value">{{ item.applicantName || '-' }}</span></div>
<span class="mobile-card__value">{{ getBizTableLabel(item.bizTableName) }}</span> <div class="mobile-card__row"><span class="mobile-card__label">审批层级</span><span class="mobile-card__value">{{ item.approvalLevel }}</span></div>
</div> <div class="mobile-card__row"><span class="mobile-card__label">审批意见</span><span class="mobile-card__value mobile-card__value--ellipsis">{{ item.comment || '-' }}</span></div>
<div class="mobile-card__row"> <div class="mobile-card__row"><span class="mobile-card__label">审批时间</span><span class="mobile-card__value">{{ formatDate2(item.approvalTime) }}</span></div>
<span class="mobile-card__label">申请人</span>
<span class="mobile-card__value">{{ item.applicantName || '-' }}</span>
</div>
<div class="mobile-card__row">
<span class="mobile-card__label">审批层级</span>
<span class="mobile-card__value">{{ item.approvalLevel }}</span>
</div>
<div class="mobile-card__row">
<span class="mobile-card__label">审批意见</span>
<span class="mobile-card__value mobile-card__value--ellipsis">{{ item.comment || '-' }}</span>
</div>
<div class="mobile-card__row">
<span class="mobile-card__label">审批时间</span>
<span class="mobile-card__value">{{ formatDate2(item.approvalTime) }}</span>
</div>
</div>
<div class="mobile-card__footer">
<el-button size="small" type="info" @click.stop="handleViewRecords(item)">审批记录</el-button>
</div> </div>
<div class="mobile-card__footer"><el-button size="small" type="info" @click.stop="handleViewRecords(item)">审批记录</el-button></div>
</div> </div>
</div> </div>
<div class="mobile-pagination" v-if="total > 0"><Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" :page-sizes="[10, 20]" layout="total, prev, pager, next" :pager-count="5" @pagination="getList" /></div>
<!-- 分页 -->
<div class="mobile-pagination" v-if="total > 0">
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
:page-sizes="[10, 20]"
layout="total, prev, pager, next"
:pager-count="5"
@pagination="getList"
/>
</div>
<!-- 筛选抽屉 -->
<el-drawer v-model="filterVisible" title="筛选条件" direction="btt" size="50%"> <el-drawer v-model="filterVisible" title="筛选条件" direction="btt" size="50%">
<el-form :model="queryParams" ref="queryFormRef" label-position="top"> <el-form :model="queryParams" ref="queryFormRef" label-position="top">
<el-form-item label="审批结果" prop="approvalResult"> <el-form-item label="审批结果" prop="approvalResult"><el-select v-model="queryParams.approvalResult" placeholder="请选择审批结果" clearable style="width: 100%"><el-option label="已通过" :value="1" /><el-option label="已驳回" :value="2" /><el-option label="已转审" :value="3" /></el-select></el-form-item>
<el-select <el-form-item label="审批时间" prop="createTime"><el-date-picker v-model="queryParams.createTime" value-format="YYYY-MM-DD HH:mm:ss" type="daterange" start-placeholder="开始日期" end-placeholder="结束日期" :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" style="width: 100%" /></el-form-item>
v-model="queryParams.approvalResult"
placeholder="请选择审批结果"
clearable
style="width: 100%"
>
<el-option label="已通过" :value="1" />
<el-option label="已驳回" :value="2" />
<el-option label="已转审" :value="3" />
</el-select>
</el-form-item>
<el-form-item label="审批时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
style="width: 100%"
/>
</el-form-item>
</el-form> </el-form>
<template #footer> <template #footer><el-button @click="resetQuery">重置</el-button><el-button type="primary" @click="handleFilterConfirm">确认筛选</el-button></template>
<el-button @click="resetQuery">重置</el-button>
<el-button type="primary" @click="handleFilterConfirm">确认筛选</el-button>
</template>
</el-drawer> </el-drawer>
<!-- 审批记录对话框 -->
<ApprovalRecordsDialog ref="approvalDialogRef" /> <ApprovalRecordsDialog ref="approvalDialogRef" />
</div> </div>
<!-- PC端布局 -->
<template v-else>
<ContentWrap>
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
<el-form-item label="业务类型" prop="bizTableName"><el-select v-model="queryParams.bizTableName" placeholder="请选择业务类型" clearable class="!w-240px"><el-option v-for="item in bizTableOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item>
<el-form-item label="审批结果" prop="approvalResult"><el-select v-model="queryParams.approvalResult" placeholder="请选择审批结果" clearable class="!w-240px"><el-option label="已通过" :value="1" /><el-option label="已驳回" :value="2" /><el-option label="已转审" :value="3" /></el-select></el-form-item>
<el-form-item label="审批时间" prop="createTime"><el-date-picker v-model="queryParams.createTime" value-format="YYYY-MM-DD HH:mm:ss" type="daterange" start-placeholder="开始日期" end-placeholder="结束日期" :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" class="!w-240px" /></el-form-item>
<el-form-item><el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button><el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button></el-form-item>
</el-form>
</ContentWrap>
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="业务单号" align="center" prop="bizId" min-width="120" />
<el-table-column label="业务类型" align="center" prop="bizTableName" min-width="150"><template #default="scope">{{ getBizTableLabel(scope.row.bizTableName) }}</template></el-table-column>
<el-table-column label="申请人" align="center" prop="applicantName" min-width="100" />
<el-table-column label="审批层级" align="center" prop="approvalLevel" min-width="100"><template #default="scope">{{ scope.row.approvalLevel }}</template></el-table-column>
<el-table-column label="审批结果" align="center" prop="approvalResult" min-width="100"><template #default="scope"><el-tag :type="getResultTagType(scope.row.approvalResult)">{{ getApprovalResultText(scope.row.approvalResult) }}</el-tag></template></el-table-column>
<el-table-column label="审批意见" align="center" prop="comment" min-width="150" />
<el-table-column label="审批时间" align="center" prop="approvalTime" :formatter="dateFormatter" width="180px" sortable />
<el-table-column label="操作" align="center" fixed="right" width="120"><template #default="scope"><el-button link type="info" @click="handleViewRecords(scope.row)">审批记录</el-button></template></el-table-column>
</el-table>
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" />
</ContentWrap>
<ApprovalRecordsDialog ref="approvalDialogRef" />
</template>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted, computed } from 'vue'
import { Filter } from '@element-plus/icons-vue' import { Filter } from '@element-plus/icons-vue'
import { formatDate } from '@/utils/formatTime' import { formatDate, dateFormatter } from '@/utils/formatTime'
import { ApprovalRecordApi, ApprovalRecordVO } from '@/api/erp/approval' import { ApprovalRecordApi, ApprovalRecordVO } from '@/api/erp/approval'
import { ApprovalRecordsDialog } from '@/components/Approval' import { ApprovalRecordsDialog } from '@/components/Approval'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
import { useWindowSize } from '@vueuse/core'
defineOptions({ name: 'ErpApprovalApproved' }) defineOptions({ name: 'ErpApprovalApproved' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const userStore = useUserStore() const userStore = useUserStore()
const loading = ref(true) const loading = ref(true)
const list = ref<ApprovalRecordVO[]>([]) const list = ref<ApprovalRecordVO[]>([])

View File

@@ -1,68 +1,23 @@
<template> <template>
<div class="mobile-pending"> <!-- 移动端布局 -->
<!-- 顶部操作栏 --> <div v-if="isMobile" class="mobile-pending">
<div class="mobile-header"> <div class="mobile-header">
<div class="mobile-header__search"> <div class="mobile-header__search">
<el-select <el-select v-model="queryParams.bizTableName" placeholder="业务类型" clearable style="width: 100%" @change="handleQuery"><el-option v-for="item in bizTableOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select>
v-model="queryParams.bizTableName"
placeholder="业务类型"
clearable
style="width: 100%"
@change="handleQuery"
>
<el-option
v-for="item in bizTableOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
<div class="mobile-header__actions">
<el-button :icon="Refresh" circle @click="resetQuery" />
</div> </div>
<div class="mobile-header__actions"><el-button :icon="Refresh" circle @click="resetQuery" /></div>
</div> </div>
<!-- 卡片列表 -->
<div class="mobile-list" v-loading="loading"> <div class="mobile-list" v-loading="loading">
<div v-if="filteredList.length === 0 && !loading" class="mobile-empty"> <div v-if="filteredList.length === 0 && !loading" class="mobile-empty"><el-empty description="暂无待审批记录" /></div>
<el-empty description="暂无待审批记录" /> <div v-for="item in filteredList" :key="item.id" class="mobile-card" @click="handleProcess(item)">
</div> <div class="mobile-card__header"><span class="mobile-card__no">{{ item.bizId }}</span><el-tag type="warning" size="small">待审批</el-tag></div>
<div
v-for="item in filteredList"
:key="item.id"
class="mobile-card"
@click="handleProcess(item)"
>
<div class="mobile-card__header">
<span class="mobile-card__no">{{ item.bizId }}</span>
<el-tag type="warning" size="small">待审批</el-tag>
</div>
<div class="mobile-card__body"> <div class="mobile-card__body">
<div class="mobile-card__row"> <div class="mobile-card__row"><span class="mobile-card__label">业务类型</span><span class="mobile-card__value">{{ getBizTableLabel(item.bizTableName) }}</span></div>
<span class="mobile-card__label">业务类型</span> <div class="mobile-card__row"><span class="mobile-card__label">申请人</span><span class="mobile-card__value">{{ item.applicantName || '-' }}</span></div>
<span class="mobile-card__value">{{ getBizTableLabel(item.bizTableName) }}</span> <div class="mobile-card__row"><span class="mobile-card__label">审批层级</span><span class="mobile-card__value">{{ item.approvalLevel }}</span></div>
</div> <div class="mobile-card__row" v-if="item.assignerName"><span class="mobile-card__label">指定者</span><span class="mobile-card__value">{{ item.assignerName }}</span></div>
<div class="mobile-card__row"> <div class="mobile-card__row" v-if="item.assignReason"><span class="mobile-card__label">指定原因</span><span class="mobile-card__value mobile-card__value--ellipsis">{{ item.assignReason }}</span></div>
<span class="mobile-card__label">申请人</span> <div class="mobile-card__row"><span class="mobile-card__label">提交时间</span><span class="mobile-card__value">{{ formatDate2(item.createTime) }}</span></div>
<span class="mobile-card__value">{{ item.applicantName || '-' }}</span>
</div>
<div class="mobile-card__row">
<span class="mobile-card__label">审批层级</span>
<span class="mobile-card__value">{{ item.approvalLevel }}</span>
</div>
<div class="mobile-card__row" v-if="item.assignerName">
<span class="mobile-card__label">指定者</span>
<span class="mobile-card__value">{{ item.assignerName }}</span>
</div>
<div class="mobile-card__row" v-if="item.assignReason">
<span class="mobile-card__label">指定原因</span>
<span class="mobile-card__value mobile-card__value--ellipsis">{{ item.assignReason }}</span>
</div>
<div class="mobile-card__row">
<span class="mobile-card__label">提交时间</span>
<span class="mobile-card__value">{{ formatDate2(item.createTime) }}</span>
</div>
</div> </div>
<div class="mobile-card__footer"> <div class="mobile-card__footer">
<el-button size="small" type="primary" @click.stop="handleProcess(item)">处理审批</el-button> <el-button size="small" type="primary" @click.stop="handleProcess(item)">处理审批</el-button>
@@ -70,25 +25,49 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 处理审批对话框 -->
<ProcessApprovalDialog ref="processApprovalDialogRef" @success="getList" /> <ProcessApprovalDialog ref="processApprovalDialogRef" @success="getList" />
<!-- 审批记录对话框 -->
<ApprovalRecordsDialog ref="approvalDialogRef" /> <ApprovalRecordsDialog ref="approvalDialogRef" />
</div> </div>
<!-- PC端布局 -->
<template v-else>
<ContentWrap>
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
<el-form-item label="业务类型" prop="bizTableName"><el-select v-model="queryParams.bizTableName" placeholder="请选择业务类型" clearable class="!w-240px" @change="handleQuery"><el-option v-for="item in bizTableOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item>
<el-form-item><el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button><el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button></el-form-item>
</el-form>
</ContentWrap>
<ContentWrap>
<el-table v-loading="loading" :data="filteredList" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="业务单号" align="center" prop="bizId" min-width="120" />
<el-table-column label="业务类型" align="center" prop="bizTableName" min-width="150"><template #default="scope">{{ getBizTableLabel(scope.row.bizTableName) }}</template></el-table-column>
<el-table-column label="申请人" align="center" prop="applicantName" min-width="100" />
<el-table-column label="审批层级" align="center" prop="approvalLevel" min-width="100"><template #default="scope">{{ scope.row.approvalLevel }}</template></el-table-column>
<el-table-column label="指定者" align="center" prop="assignerName" min-width="100" />
<el-table-column label="指定原因" align="center" prop="assignReason" min-width="150" />
<el-table-column label="提交时间" align="center" prop="createTime" :formatter="dateFormatter" width="180px" sortable />
<el-table-column label="操作" align="center" fixed="right" width="200"><template #default="scope"><el-button link type="primary" @click="handleProcess(scope.row)">处理审批</el-button><el-button link type="info" @click="handleViewRecords(scope.row)">审批记录</el-button></template></el-table-column>
</el-table>
</ContentWrap>
<ProcessApprovalDialog ref="processApprovalDialogRef" @success="getList" />
<ApprovalRecordsDialog ref="approvalDialogRef" />
</template>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { Refresh } from '@element-plus/icons-vue' import { Refresh } from '@element-plus/icons-vue'
import { formatDate } from '@/utils/formatTime' import { formatDate, dateFormatter } from '@/utils/formatTime'
import { ApprovalRecordApi, ApprovalRecordVO } from '@/api/erp/approval' import { ApprovalRecordApi, ApprovalRecordVO } from '@/api/erp/approval'
import { ProcessApprovalDialog, ApprovalRecordsDialog } from '@/components/Approval' import { ProcessApprovalDialog, ApprovalRecordsDialog } from '@/components/Approval'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
import { useWindowSize } from '@vueuse/core'
defineOptions({ name: 'ErpApprovalPending' }) defineOptions({ name: 'ErpApprovalPending' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const userStore = useUserStore() const userStore = useUserStore()
const loading = ref(true) const loading = ref(true)
const list = ref<ApprovalRecordVO[]>([]) const list = ref<ApprovalRecordVO[]>([])

View File

@@ -1,35 +1,31 @@
<template> <template>
<Dialog :title="dialogTitle" v-model="dialogVisible"> <!-- 移动端布局 -->
<el-form <el-drawer v-if="isMobile" v-model="dialogVisible" :title="dialogTitle" direction="rtl" size="100%" :close-on-press-escape="true" :destroy-on-close="true" :append-to-body="true">
ref="formRef" <div class="mobile-form" v-loading="formLoading">
:model="formData" <el-form ref="formRef" :model="formData" :rules="formRules" label-position="top">
:rules="formRules" <div class="mobile-form-section">
label-width="100px" <el-form-item label="名称" prop="name"><el-input v-model="formData.name" placeholder="请输入名称" /></el-form-item>
v-loading="formLoading" <el-form-item label="编码" prop="no"><el-input v-model="formData.no" placeholder="请输入编码" /></el-form-item>
> <el-form-item label="备注" prop="remark"><el-input v-model="formData.remark" placeholder="请输入备注" /></el-form-item>
<el-form-item label="名称" prop="name"> <el-form-item label="状态" prop="status"><el-radio-group v-model="formData.status"><el-radio v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio></el-radio-group></el-form-item>
<el-input v-model="formData.name" placeholder="请输入名称" /> <el-form-item label="排序" prop="sort"><el-input v-model="formData.sort" placeholder="请输入排序" /></el-form-item>
</el-form-item> </div>
<el-form-item label="编码" prop="no"> </el-form>
<el-input v-model="formData.no" placeholder="请输入编码" /> <div class="mobile-form__footer">
</el-form-item> <el-button @click="dialogVisible = false"> </el-button>
<el-form-item label="备注" prop="remark"> <el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-input v-model="formData.remark" placeholder="请输入备注" /> </div>
</el-form-item> </div>
<el-form-item label="状态" prop="status"> </el-drawer>
<el-radio-group v-model="formData.status">
<el-radio <!-- PC端布局 -->
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" <Dialog v-else :title="dialogTitle" v-model="dialogVisible">
:key="dict.value" <el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" v-loading="formLoading">
:value="dict.value" <el-form-item label="名称" prop="name"><el-input v-model="formData.name" placeholder="请输入名称" /></el-form-item>
> <el-form-item label="编码" prop="no"><el-input v-model="formData.no" placeholder="请输入编码" /></el-form-item>
{{ dict.label }} <el-form-item label="备注" prop="remark"><el-input v-model="formData.remark" placeholder="请输入备注" /></el-form-item>
</el-radio> <el-form-item label="状态" prop="status"><el-radio-group v-model="formData.status"><el-radio v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio></el-radio-group></el-form-item>
</el-radio-group> <el-form-item label="排序" prop="sort"><el-input v-model="formData.sort" placeholder="请输入排序" /></el-form-item>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input v-model="formData.sort" placeholder="请输入排序" />
</el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button> <el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
@@ -38,12 +34,17 @@
</Dialog> </Dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { AccountApi, AccountVO } from '@/api/erp/finance/account' import { AccountApi, AccountVO } from '@/api/erp/finance/account'
import { useWindowSize } from '@vueuse/core'
/** ERP 结算 表单 */ /** ERP 结算 表单 */
defineOptions({ name: 'AccountForm' }) defineOptions({ name: 'AccountForm' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
@@ -122,3 +123,9 @@ const resetForm = () => {
formRef.value?.resetFields() formRef.value?.resetFields()
} }
</script> </script>
<style lang="scss" scoped>
.mobile-form { padding: 12px; }
.mobile-form-section { background: #fff; border-radius: 10px; padding: 14px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
.mobile-form__footer { position: sticky; bottom: 0; background: #fff; padding: 12px 16px; padding-bottom: calc(12px + env(safe-area-inset-bottom)); border-top: 1px solid #eee; display: flex; justify-content: flex-end; gap: 12px; margin: 12px -12px -12px; .el-button { flex: 1; height: 40px; font-size: 15px; } }
</style>

View File

@@ -1,71 +1,63 @@
<template> <template>
<doc-alert <!-- 移动端布局 -->
title="【财务】采购付款、销售收款" <div v-if="isMobile" class="mobile-account">
url="https://doc.iocoder.cn/sale/finance-payment-receipt/" <div class="mobile-header">
/> <div class="mobile-header__search"><el-input v-model="queryParams.name" placeholder="搜索账户名称" clearable @keyup.enter="handleQuery" :prefix-icon="Search" /></div>
<div class="mobile-header__actions">
<el-button :icon="Filter" circle @click="filterVisible = true" />
<el-button type="primary" :icon="Plus" circle @click="openForm('create')" v-hasPermi="['erp:account:create']" />
</div>
</div>
<div class="mobile-quick-actions"><el-button size="small" type="success" plain @click="handleExport" :loading="exportLoading" v-hasPermi="['erp:account:export']">导出</el-button></div>
<div class="mobile-list" v-loading="loading">
<div v-if="list.length === 0 && !loading" class="mobile-empty"><el-empty description="暂无账户" /></div>
<div v-for="item in list" :key="item.id" class="mobile-card" @click="openForm('update', item.id)">
<div class="mobile-card__header">
<span class="mobile-card__name">{{ item.name }}</span>
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
</div>
<div class="mobile-card__body">
<div class="mobile-card__row"><span class="mobile-card__label">编码</span><span class="mobile-card__value">{{ item.no || '-' }}</span></div>
<div class="mobile-card__row"><span class="mobile-card__label">备注</span><span class="mobile-card__value">{{ item.remark || '-' }}</span></div>
<div class="mobile-card__row"><span class="mobile-card__label">排序</span><span class="mobile-card__value">{{ item.sort }}</span></div>
<div class="mobile-card__row"><span class="mobile-card__label">默认</span><el-switch v-model="item.defaultStatus" :active-value="true" :inactive-value="false" @click.stop @change="handleDefaultStatusChange(item)" /></div>
</div>
<div class="mobile-card__footer">
<el-button size="small" type="primary" @click.stop="openForm('update', item.id)" v-hasPermi="['erp:account:update']">编辑</el-button>
<el-button size="small" type="danger" @click.stop="handleDelete(item.id)" v-hasPermi="['erp:account:delete']">删除</el-button>
</div>
</div>
</div>
<div class="mobile-pagination" v-if="total > 0"><Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" :page-sizes="[10, 20]" layout="total, prev, pager, next" :pager-count="5" @pagination="getList" /></div>
<el-drawer v-model="filterVisible" title="筛选条件" direction="btt" size="50%">
<el-form :model="queryParams" ref="queryFormRef" label-position="top">
<el-form-item label="名称" prop="name"><el-input v-model="queryParams.name" placeholder="请输入名称" clearable style="width:100%" /></el-form-item>
<el-form-item label="编码" prop="no"><el-input v-model="queryParams.no" placeholder="请输入编码" clearable style="width:100%" /></el-form-item>
<el-form-item label="备注" prop="remark"><el-input v-model="queryParams.remark" placeholder="请输入备注" clearable style="width:100%" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="resetQuery">重置</el-button>
<el-button type="primary" @click="handleFilterConfirm">确认筛选</el-button>
</template>
</el-drawer>
</div>
<ContentWrap> <!-- PC端布局 -->
<!-- 搜索工作栏 --> <template v-else>
<el-form <ContentWrap>
class="-mb-15px" <el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
:model="queryParams" <el-form-item label="名称" prop="name"><el-input v-model="queryParams.name" placeholder="请输入名称" clearable @keyup.enter="handleQuery" class="!w-240px" /></el-form-item>
ref="queryFormRef" <el-form-item label="编码" prop="no"><el-input v-model="queryParams.no" placeholder="请输入编码" clearable @keyup.enter="handleQuery" class="!w-240px" /></el-form-item>
:inline="true" <el-form-item label="备注" prop="remark"><el-input v-model="queryParams.remark" placeholder="请输入备注" clearable @keyup.enter="handleQuery" class="!w-240px" /></el-form-item>
label-width="68px" <el-form-item>
> <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-form-item label="名称" prop="name"> <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-input <el-button type="primary" plain @click="openForm('create')" v-hasPermi="['erp:account:create']"><Icon icon="ep:plus" class="mr-5px" /> 新增</el-button>
v-model="queryParams.name" <el-button type="success" plain @click="handleExport" :loading="exportLoading" v-hasPermi="['erp:account:export']"><Icon icon="ep:download" class="mr-5px" /> 导出</el-button>
placeholder="请输入名称" </el-form-item>
clearable </el-form>
@keyup.enter="handleQuery" </ContentWrap>
class="!w-240px" <ContentWrap>
/>
</el-form-item>
<el-form-item label="编码" prop="no">
<el-input
v-model="queryParams.no"
placeholder="请输入编码"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="queryParams.remark"
placeholder="请输入备注"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['erp:account:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['erp:account:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" @row-click="handleRowClick"> <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" @row-click="handleRowClick">
<el-table-column label="名称" align="center" prop="name" /> <el-table-column label="名称" align="center" prop="name" />
<el-table-column label="编码" align="center" prop="no" /> <el-table-column label="编码" align="center" prop="no" />
@@ -115,29 +107,30 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<!-- 分页 --> <Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" />
<Pagination </ContentWrap>
:total="total" </template>
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 --> <!-- 表单弹窗添加/修改 -->
<AccountForm ref="formRef" @success="getList" /> <AccountForm ref="formRef" @success="getList" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime' import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download' import download from '@/utils/download'
import { AccountApi, AccountVO } from '@/api/erp/finance/account' import { AccountApi, AccountVO } from '@/api/erp/finance/account'
import AccountForm from './AccountForm.vue' import AccountForm from './AccountForm.vue'
import { useWindowSize } from '@vueuse/core'
import { Search, Plus, Filter } from '@element-plus/icons-vue'
/** ERP 结算账户 列表 */ /** ERP 结算账户 列表 */
defineOptions({ name: 'ErpAccount' }) defineOptions({ name: 'ErpAccount' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
@@ -154,6 +147,7 @@ const queryParams = reactive({
}) })
const queryFormRef = ref() // 搜索的表单 const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中 const exportLoading = ref(false) // 导出的加载中
const filterVisible = ref(false) // 筛选抽屉
/** 查询列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {
@@ -175,7 +169,13 @@ const handleQuery = () => {
/** 重置按钮操作 */ /** 重置按钮操作 */
const resetQuery = () => { const resetQuery = () => {
queryFormRef.value.resetFields() queryFormRef.value?.resetFields()
handleQuery()
}
/** 筛选确认 */
const handleFilterConfirm = () => {
filterVisible.value = false
handleQuery() handleQuery()
} }
@@ -254,3 +254,22 @@ onMounted(() => {
getList() getList()
}) })
</script> </script>
<style lang="scss" scoped>
.mobile-account { padding: 12px; background: #f5f5f5; min-height: 100vh; }
.mobile-header { display: flex; gap: 8px; align-items: center; margin-bottom: 12px; }
.mobile-header__search { flex: 1; }
.mobile-header__actions { display: flex; gap: 4px; }
.mobile-quick-actions { display: flex; gap: 8px; margin-bottom: 12px; }
.mobile-list { display: flex; flex-direction: column; gap: 10px; }
.mobile-empty { padding: 40px 0; }
.mobile-card { background: #fff; border-radius: 10px; padding: 14px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
.mobile-card__header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.mobile-card__name { font-weight: 600; font-size: 15px; color: #303133; }
.mobile-card__body { font-size: 13px; }
.mobile-card__row { display: flex; justify-content: space-between; align-items: center; padding: 3px 0; }
.mobile-card__label { color: #909399; flex-shrink: 0; margin-right: 12px; }
.mobile-card__value { color: #606266; text-align: right; }
.mobile-card__footer { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; padding-top: 10px; border-top: 1px solid #f0f0f0; }
.mobile-pagination { margin-top: 12px; display: flex; justify-content: center; :deep(.el-pagination) { flex-wrap: wrap; justify-content: center; } }
</style>

View File

@@ -1,12 +1,37 @@
<template> <template>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="700px"> <!-- 移动端布局 -->
<el-form <el-drawer v-if="isMobile" v-model="dialogVisible" :title="dialogTitle" direction="rtl" size="100%" :close-on-press-escape="true" :destroy-on-close="true" :append-to-body="true" class="mobile-form-drawer">
ref="formRef" <div class="mobile-form" v-loading="formLoading">
:model="formData" <el-form ref="formRef" :model="formData" :rules="formRules" label-position="top">
:rules="formRules" <div class="mobile-form__section">
label-width="120px" <div class="mobile-form__section-title">银行流水信息</div>
v-loading="formLoading" <el-form-item label="交易流水号" prop="transactionNo"><el-input v-model="formData.transactionNo" placeholder="请输入交易流水号" /></el-form-item>
> <el-form-item label="结算账户" prop="accountId"><el-select v-model="formData.accountId" placeholder="请选择结算账户" style="width:100%" @change="handleAccountChange"><el-option v-for="item in accountList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item>
<el-form-item label="账户号" prop="accountNo"><el-input v-model="formData.accountNo" placeholder="请输入账户号" /></el-form-item>
<el-form-item label="交易机构" prop="transactionInstitution"><el-input v-model="formData.transactionInstitution" placeholder="请输入交易机构" /></el-form-item>
<el-form-item label="交易金额" prop="transactionAmount"><el-input-number v-model="formData.transactionAmount" :precision="2" :min="0" placeholder="请输入交易金额" style="width:100%" /></el-form-item>
<el-form-item label="交易类型" prop="transactionType"><el-radio-group v-model="formData.transactionType"><el-radio :value="1">收入</el-radio><el-radio :value="2">支出</el-radio></el-radio-group></el-form-item>
<el-form-item label="交易对手账户" prop="counterpartyAccount"><el-input v-model="formData.counterpartyAccount" placeholder="请输入交易对手账户" /></el-form-item>
<el-form-item label="交易对手户名" prop="counterpartyName"><el-input v-model="formData.counterpartyName" placeholder="请输入交易对手户名" /></el-form-item>
<el-form-item label="交易对手银行" prop="counterpartyBank"><el-input v-model="formData.counterpartyBank" placeholder="请输入交易对手银行" /></el-form-item>
<el-form-item label="操作柜员" prop="operatorTeller"><el-input v-model="formData.operatorTeller" placeholder="请输入操作柜员" /></el-form-item>
<el-form-item label="交易时间" prop="transactionTime"><el-date-picker v-model="formData.transactionTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择交易时间" style="width:100%" /></el-form-item>
<el-form-item label="摘要" prop="summary"><el-input v-model="formData.summary" placeholder="请输入摘要" /></el-form-item>
<el-form-item label="备注信息" prop="remark"><el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注信息" /></el-form-item>
</div>
</el-form>
</div>
<template #footer>
<div class="mobile-form__footer">
<el-button @click="dialogVisible = false" style="flex:1"> </el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading" style="flex:2"> </el-button>
</div>
</template>
</el-drawer>
<!-- PC端布局 -->
<Dialog v-else :title="dialogTitle" v-model="dialogVisible" width="700px">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px" v-loading="formLoading">
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="交易流水号" prop="transactionNo"> <el-form-item label="交易流水号" prop="transactionNo">
@@ -113,12 +138,17 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { BankTransactionApi, BankTransactionVO } from '@/api/erp/finance/bank' import { BankTransactionApi, BankTransactionVO } from '@/api/erp/finance/bank'
import { AccountApi } from '@/api/erp/finance/account' import { AccountApi } from '@/api/erp/finance/account'
import { useWindowSize } from '@vueuse/core'
/** ERP 银行流水 表单 */ /** ERP 银行流水 表单 */
defineOptions({ name: 'BankTransactionForm' }) defineOptions({ name: 'BankTransactionForm' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗

View File

@@ -1,13 +1,52 @@
<template> <template>
<Dialog v-model="dialogVisible" :title="dialogTitle" width="900px"> <!-- 移动端布局 -->
<el-form <el-drawer v-if="isMobile" v-model="dialogVisible" :title="dialogTitle" direction="rtl" size="100%" :close-on-press-escape="true" :destroy-on-close="true" :append-to-body="true" class="mobile-form-drawer">
ref="formRef" <div class="mobile-form" v-loading="formLoading">
v-loading="formLoading" <el-form ref="formRef" :model="formData" :rules="formRules" :disabled="isDetail" label-position="top">
:model="formData" <div class="mobile-form__section">
:rules="formRules" <div class="mobile-form__section-title">基本信息</div>
:disabled="isDetail" <el-form-item label="凭证编号" prop="voucherNo"><el-input v-model="formData.voucherNo" disabled placeholder="系统自动生成" /></el-form-item>
label-width="100px" <el-form-item v-if="!isDetail" label="制单日期" prop="voucherDate"><el-date-picker v-model="formData.voucherDate" type="date" placeholder="选择制单日期" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item>
> <el-form-item label="系统名" prop="systemName"><el-input v-model="formData.systemName" placeholder="请输入系统名" /></el-form-item>
<el-form-item label="摘要" prop="summary"><el-input v-model="formData.summary" placeholder="请输入摘要" @input="syncSummaryToItems" /></el-form-item>
<el-form-item v-if="formType !== 'create'" label="审核状态" prop="status"><dict-tag :type="DICT_TYPE.ERP_AUDIT_STATUS" :value="formData.status" /></el-form-item>
<el-form-item label="备注" prop="remark"><el-input v-model="formData.remark" type="textarea" :rows="2" placeholder="请输入备注" /></el-form-item>
</div>
<div class="mobile-form__section">
<div class="mobile-form__section-title">凭证明细</div>
<div v-for="(item, index) in formData.items" :key="index" class="mobile-item-card">
<div class="mobile-item-card__header">
<span class="mobile-item-card__index">#{{ index + 1 }}</span>
<span class="mobile-item-card__name">{{ item.subjectName || '未选择科目' }}</span>
<el-button v-if="!isDetail" :disabled="isDetail" @click="removeItem(index)" link type="danger" size="small">删除</el-button>
</div>
<div class="mobile-item-card__body">
<el-form-item label="摘要"><el-input v-model="item.summary" placeholder="请输入摘要" :disabled="isDetail" /></el-form-item>
<el-form-item label="科目名称"><el-tree-select v-model="item.subjectId" :data="subjectTree" :props="{ label: 'name', value: 'id', children: 'children' }" placeholder="请选择科目名称" check-strictly filterable :disabled="isDetail" style="width:100%" @change="(val) => handleSubjectChange(item, val)" /></el-form-item>
<el-form-item label="借方金额"><el-input-number v-model="item.debitAmount" :precision="2" :min="0" :controls="false" placeholder="借方金额" :disabled="isDetail || (item.creditAmount > 0)" style="width:100%" @change="(val) => handleDebitChange(item, val)" /></el-form-item>
<el-form-item label="贷方金额"><el-input-number v-model="item.creditAmount" :precision="2" :min="0" :controls="false" placeholder="贷方金额" :disabled="isDetail || (item.debitAmount > 0)" style="width:100%" @change="(val) => handleCreditChange(item, val)" /></el-form-item>
</div>
</div>
<div class="mobile-item-add" v-if="!isDetail"><el-button @click="addItem" round>+ 添加明细</el-button></div>
<div class="mobile-item-summary">
<div class="mobile-item-summary__row"><span>借方合计</span><span class="text-red-500">{{ formatAmount(debitTotal) }}</span></div>
<div class="mobile-item-summary__row"><span>贷方合计</span><span class="text-blue-500">{{ formatAmount(creditTotal) }}</span></div>
</div>
<el-alert v-if="!isBalanced && formData.items.length > 0" title="借贷不平衡,请检查金额" type="warning" :closable="false" class="mt-10px" />
</div>
</el-form>
</div>
<template #footer>
<div class="mobile-form__footer">
<el-button @click="dialogVisible = false" style="flex:1"> </el-button>
<el-button v-if="!isDetail" type="primary" :disabled="formLoading" @click="submitForm" style="flex:2"> </el-button>
</div>
</template>
</el-drawer>
<!-- PC端布局 -->
<Dialog v-else v-model="dialogVisible" :title="dialogTitle" width="900px">
<el-form ref="formRef" v-loading="formLoading" :model="formData" :rules="formRules" :disabled="isDetail" label-width="100px">
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="8"> <el-col :span="8">
<el-form-item label="凭证编号" prop="voucherNo"> <el-form-item label="凭证编号" prop="voucherNo">
@@ -146,13 +185,18 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import { DICT_TYPE } from '@/utils/dict' import { DICT_TYPE } from '@/utils/dict'
import { formatDate } from '@/utils/formatTime' import { formatDate } from '@/utils/formatTime'
import * as BookkeepingVoucherApi from '@/api/erp/finance/bookkeeping' import * as BookkeepingVoucherApi from '@/api/erp/finance/bookkeeping'
import { SubjectApi } from '@/api/erp/finance/subject' import { SubjectApi } from '@/api/erp/finance/subject'
import { useWindowSize } from '@vueuse/core'
defineOptions({ name: 'BookkeepingVoucherForm' }) defineOptions({ name: 'BookkeepingVoucherForm' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗

View File

@@ -1,12 +1,32 @@
<template> <template>
<Dialog :title="dialogTitle" v-model="dialogVisible"> <!-- 移动端布局 -->
<el-form <el-drawer v-if="isMobile" v-model="dialogVisible" :title="dialogTitle" direction="rtl" size="100%" :close-on-press-escape="true" :destroy-on-close="true" :append-to-body="true" class="mobile-form-drawer">
ref="formRef" <div class="mobile-form" v-loading="formLoading">
:model="formData" <el-form ref="formRef" :model="formData" :rules="formRules" label-position="top">
:rules="formRules" <div class="mobile-form__section">
label-width="120px" <div class="mobile-form__section-title">会计期间信息</div>
v-loading="formLoading" <el-form-item label="期间编码" prop="periodCode"><el-input v-model="formData.periodCode" placeholder="请输入期间编码yyyyMM格式" /></el-form-item>
> <el-form-item label="期间名称" prop="periodName"><el-input v-model="formData.periodName" placeholder="请输入期间名称" /></el-form-item>
<el-form-item label="会计年度" prop="fiscalYear"><el-input v-model="formData.fiscalYear" placeholder="请输入会计年度" /></el-form-item>
<el-form-item label="开始日期" prop="startDate"><el-date-picker v-model="formData.startDate" type="date" placeholder="请选择开始日期" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item>
<el-form-item label="结束日期" prop="endDate"><el-date-picker v-model="formData.endDate" type="date" placeholder="请选择结束日期" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item>
<el-form-item label="状态" prop="status"><el-radio-group v-model="formData.status"><el-radio v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio></el-radio-group></el-form-item>
<el-form-item label="是否当前期间" prop="isCurrent"><el-switch v-model="formData.isCurrent" active-text="是" inactive-text="否" /></el-form-item>
<el-form-item label="描述" prop="description"><el-input v-model="formData.description" type="textarea" placeholder="请输入描述" /></el-form-item>
</div>
</el-form>
</div>
<template #footer>
<div class="mobile-form__footer">
<el-button @click="dialogVisible = false" style="flex:1"> </el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading" style="flex:2"> </el-button>
</div>
</template>
</el-drawer>
<!-- PC端布局 -->
<Dialog v-else :title="dialogTitle" v-model="dialogVisible">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px" v-loading="formLoading">
<el-form-item label="期间编码" prop="periodCode"> <el-form-item label="期间编码" prop="periodCode">
<el-input v-model="formData.periodCode" placeholder="请输入期间编码yyyyMM格式" /> <el-input v-model="formData.periodCode" placeholder="请输入期间编码yyyyMM格式" />
</el-form-item> </el-form-item>
@@ -63,12 +83,17 @@
</Dialog> </Dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { FiscalPeriodApi, FiscalPeriodVO } from '@/api/erp/finance/fiscal-period' import { FiscalPeriodApi, FiscalPeriodVO } from '@/api/erp/finance/fiscal-period'
import { useWindowSize } from '@vueuse/core'
/** ERP 会计期间 表单 */ /** ERP 会计期间 表单 */
defineOptions({ name: 'FiscalPeriodForm' }) defineOptions({ name: 'FiscalPeriodForm' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗

View File

@@ -1,12 +1,35 @@
<template> <template>
<Dialog :title="dialogTitle" v-model="dialogVisible"> <!-- 移动端布局 -->
<el-form <el-drawer v-if="isMobile" v-model="dialogVisible" :title="dialogTitle" direction="rtl" size="100%" :close-on-press-escape="true" :destroy-on-close="true" :append-to-body="true" class="mobile-form-drawer">
ref="formRef" <div class="mobile-form" v-loading="formLoading">
:model="formData" <el-form ref="formRef" :model="formData" :rules="formRules" label-position="top">
:rules="formRules" <div class="mobile-form__section">
label-width="110px" <div class="mobile-form__section-title">发票信息</div>
v-loading="formLoading" <el-form-item label="发票日期" prop="invoiceDate"><el-date-picker v-model="formData.invoiceDate" type="date" value-format="x" placeholder="选择发票日期" style="width:100%" /></el-form-item>
> <el-form-item label="发票类别" prop="invoiceCategory"><el-select v-model="formData.invoiceCategory" placeholder="请选择发票类别" style="width:100%"><el-option v-for="dict in getStrDictOptions(DICT_TYPE.ERP_FINANCE_INVOICE_INOUT_TYPE)" :key="dict.value" :label="dict.label" :value="dict.value" /></el-select></el-form-item>
<el-form-item label="发票类型" prop="invoiceType"><el-select v-model="formData.invoiceType" placeholder="请选择发票类型" style="width:100%"><el-option v-for="dict in getStrDictOptions(DICT_TYPE.ERP_FINANCE_INVOICE_TYPE)" :key="dict.value" :label="dict.label" :value="dict.value" /></el-select></el-form-item>
<el-form-item label="会计科目" prop="accountSubjects" required><el-select v-model="formData.accountSubjects" placeholder="请选择会计科目" style="width:100%"><el-option v-for="dict in getStrDictOptions(DICT_TYPE.ERP_FINANCE_SUBJECT_TYPE)" :key="dict.value" :label="dict.label" :value="dict.value" /></el-select></el-form-item>
<el-form-item label="发票号码" prop="invoiceNo"><el-input v-model="formData.invoiceNo" placeholder="发票号码" /></el-form-item>
<el-form-item label="发票方" prop="counterpartyName"><el-input v-model="formData.counterpartyName" placeholder="客户/供应商名称" /></el-form-item>
<el-form-item label="金额(不含税)" prop="amountWithoutTax"><el-input v-model="formData.amountWithoutTax" placeholder="不含税金额" /></el-form-item>
<el-form-item label="税率(%)" prop="taxRate"><el-input v-model="formData.taxRate" placeholder="税率 %" /></el-form-item>
<el-form-item label="发票附件" prop="attachment"><UploadFile v-model="formData.attachment" :limit="1" :fileType="['pdf', 'png', 'jpg', 'jpeg']" :fileSize="10" /></el-form-item>
<el-form-item label="发票状态" prop="invoiceStatus"><el-radio-group v-model="formData.invoiceStatus"><el-radio label="待认证">待认证</el-radio><el-radio label="已认证">已认证</el-radio></el-radio-group></el-form-item>
<el-form-item label="备注" prop="remark"><el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" /></el-form-item>
</div>
</el-form>
</div>
<template #footer>
<div class="mobile-form__footer">
<el-button @click="dialogVisible = false" style="flex:1"> </el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading" style="flex:2"> </el-button>
</div>
</template>
</el-drawer>
<!-- PC端布局 -->
<Dialog v-else :title="dialogTitle" v-model="dialogVisible">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="110px" v-loading="formLoading">
<el-row :gutter="16"> <el-row :gutter="16">
<el-col :span="12"> <el-col :span="12">
<!-- <el-form-item label="发票日期" prop="invoiceDate"> <!-- <el-form-item label="发票日期" prop="invoiceDate">
@@ -172,15 +195,20 @@
</Dialog> </Dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { InvoiceApi, Invoice } from '@/api/erp/finance/invoice' import { InvoiceApi, Invoice } from '@/api/erp/finance/invoice'
import UploadFile from '@/components/UploadFile/src/UploadFile.vue' import UploadFile from '@/components/UploadFile/src/UploadFile.vue'
import { getStrDictOptions, DICT_TYPE } from '@/utils/dict' import { getStrDictOptions, DICT_TYPE } from '@/utils/dict'
import { formatDate } from '@/utils/formatTime' import { formatDate } from '@/utils/formatTime'
import { SubjectApi } from '@/api/erp/finance/subject' import { SubjectApi } from '@/api/erp/finance/subject'
import { useWindowSize } from '@vueuse/core'
/** 财务发票主 表单 */ /** 财务发票主 表单 */
defineOptions({ name: 'InvoiceForm' }) defineOptions({ name: 'InvoiceForm' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗

View File

@@ -1,12 +1,31 @@
<template> <template>
<Dialog :title="dialogTitle" v-model="dialogVisible"> <!-- 移动端布局 -->
<el-form <el-drawer v-if="isMobile" v-model="dialogVisible" :title="dialogTitle" direction="rtl" size="100%" :close-on-press-escape="true" :destroy-on-close="true" :append-to-body="true" class="mobile-form-drawer">
ref="formRef" <div class="mobile-form" v-loading="formLoading">
:model="formData" <el-form ref="formRef" :model="formData" :rules="formRules" label-position="top">
:rules="formRules" <div class="mobile-form__section">
label-width="100px" <div class="mobile-form__section-title">应付单信息</div>
v-loading="formLoading" <el-form-item label="采购入库订单" prop="purchaseInNo"><el-input v-model="formData.purchaseInNo" readonly placeholder="请选择采购入库订单"><template #append><el-button @click="openPurchaseInSelect"><Icon icon="ep:search" /></el-button></template></el-input></el-form-item>
> <el-form-item label="应付单号" prop="apoCode"><el-input v-model="formData.apoCode" placeholder="请输入应付单号" /></el-form-item>
<el-form-item label="供应商" prop="supplier"><el-input v-model="formData.supplier" placeholder="请输入供应商" /></el-form-item>
<el-form-item label="单据日期" prop="billDate"><el-date-picker v-model="formData.billDate" type="date" value-format="x" placeholder="选择单据日期" style="width:100%" /></el-form-item>
<el-form-item label="已付金额" prop="paymentPrice"><el-input v-model="formData.paymentPrice" placeholder="请输入已付金额" /></el-form-item>
<el-form-item label="总金额" prop="totalAmount"><el-input v-model="formData.totalAmount" placeholder="请输入总金额" /></el-form-item>
<el-form-item label="备注" prop="remark"><el-input v-model="formData.remark" placeholder="请输入备注" /></el-form-item>
</div>
</el-form>
</div>
<template #footer>
<div class="mobile-form__footer">
<el-button @click="dialogVisible = false" style="flex:1"> </el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading" style="flex:2"> </el-button>
</div>
</template>
</el-drawer>
<!-- PC端布局 -->
<Dialog v-else :title="dialogTitle" v-model="dialogVisible">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" v-loading="formLoading">
<el-form-item label="采购入库订单" prop="purchaseInNo"> <el-form-item label="采购入库订单" prop="purchaseInNo">
<el-input v-model="formData.purchaseInNo" readonly placeholder="请选择采购入库订单"> <el-input v-model="formData.purchaseInNo" readonly placeholder="请选择采购入库订单">
<template #append> <template #append>
@@ -94,15 +113,20 @@
<PurchaseInPaymentEnableList ref="purchaseInSelectRef" @success="onPurchaseInSelected" /> <PurchaseInPaymentEnableList ref="purchaseInSelectRef" @success="onPurchaseInSelected" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { PayableOrderApi, PayableOrder } from '@/api/erp/finance/payableorder' import { PayableOrderApi, PayableOrder } from '@/api/erp/finance/payableorder'
import PurchaseInPaymentEnableList from '@/views/erp/purchase/in/components/PurchaseInPaymentEnableList.vue' import PurchaseInPaymentEnableList from '@/views/erp/purchase/in/components/PurchaseInPaymentEnableList.vue'
import { PurchaseInVO } from '@/api/erp/purchase/in' import { PurchaseInVO } from '@/api/erp/purchase/in'
import { formatToDate } from '@/utils/dateUtil' import { formatToDate } from '@/utils/dateUtil'
import { SubjectApi, SubjectVO } from '@/api/erp/finance/subject' import { SubjectApi, SubjectVO } from '@/api/erp/finance/subject'
import { useWindowSize } from '@vueuse/core'
/** 应付单 表单 */ /** 应付单 表单 */
defineOptions({ name: 'PayableOrderForm' }) defineOptions({ name: 'PayableOrderForm' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗

View File

@@ -1,156 +1,63 @@
<template> <template>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="1080"> <!-- 移动端布局 -->
<el-form <el-drawer v-if="isMobile" v-model="dialogVisible" :title="dialogTitle" direction="rtl" size="100%" :close-on-press-escape="true" :destroy-on-close="true" :append-to-body="true">
ref="formRef" <div class="mobile-form" v-loading="formLoading">
:model="formData" <el-form ref="formRef" :model="formData" :rules="formRules" label-position="top" :disabled="disabled">
:rules="formRules" <div class="mobile-form__section">
label-width="100px" <div class="mobile-form__section-title">基本信息</div>
v-loading="formLoading" <el-form-item label="付款单号" prop="no"><el-input disabled v-model="formData.no" placeholder="保存时自动生成" /></el-form-item>
:disabled="disabled" <el-form-item label="付款时间" prop="paymentTime"><el-date-picker v-model="formData.paymentTime" type="date" value-format="x" placeholder="选择付款时间" style="width:100%" /></el-form-item>
> <el-form-item label="供应商" prop="supplierId"><el-select v-model="formData.supplierId" clearable filterable placeholder="请选择供应商" style="width:100%"><el-option v-for="item in supplierList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item>
<el-form-item label="发票" prop="invoiceNo"><el-input v-model="invoiceDisplay" readonly placeholder="请选择发票"><template #append><el-button @click="openInvoiceSelect"><Icon icon="ep:search" /></el-button></template></el-input></el-form-item>
<el-form-item label="财务人员" prop="financeUserId"><el-select v-model="formData.financeUserId" clearable filterable placeholder="请选择财务人员" style="width:100%"><el-option v-for="item in userList" :key="item.id" :label="item.nickname" :value="item.id" /></el-select></el-form-item>
<el-form-item label="备注" prop="remark"><el-input type="textarea" v-model="formData.remark" :rows="2" placeholder="请输入备注" /></el-form-item>
<el-form-item label="附件" prop="fileUrl"><UploadFile :is-show-tip="false" v-model="formData.fileUrl" :limit="1" /></el-form-item>
</div>
<div class="mobile-form__section">
<div class="mobile-form__section-title">采购入库退货单</div>
<FinancePaymentItemForm ref="itemFormRef" :supplier-id="formData.supplierId" :items="formData.items" :disabled="disabled" />
</div>
<div class="mobile-form__section">
<div class="mobile-form__section-title">结算信息</div>
<el-form-item label="付款账户" prop="accountId"><el-select v-model="formData.accountId" clearable filterable placeholder="请选择结算账户" style="width:100%"><el-option v-for="item in accountList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item>
<el-form-item label="合计付款" prop="totalPrice"><el-input disabled v-model="formData.totalPrice" :formatter="erpPriceInputFormatter" /></el-form-item>
<el-form-item label="优惠金额" prop="discountPrice"><el-input-number v-model="formData.discountPrice" controls-position="right" :precision="4" placeholder="请输入优惠金额" style="width:100%" /></el-form-item>
<el-form-item label="实际付款"><el-input disabled v-model="formData.paymentPrice" :formatter="erpPriceInputFormatter" /></el-form-item>
</div>
</el-form>
<div class="mobile-form__footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled"> </el-button>
</div>
</div>
</el-drawer>
<!-- PC端布局 -->
<Dialog v-else :title="dialogTitle" v-model="dialogVisible" width="1080">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" v-loading="formLoading" :disabled="disabled">
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="8"> <el-col :span="8"><el-form-item label="付款单号" prop="no"><el-input disabled v-model="formData.no" placeholder="保存时自动生成" /></el-form-item></el-col>
<el-form-item label="付款单号" prop="no"> <el-col :span="8"><el-form-item label="付款时间" prop="paymentTime"><el-date-picker v-model="formData.paymentTime" type="date" value-format="x" placeholder="选择付款时间" class="!w-1/1" /></el-form-item></el-col>
<el-input disabled v-model="formData.no" placeholder="保存时自动生成" /> <el-col :span="8"><el-form-item label="供应商" prop="supplierId"><el-select v-model="formData.supplierId" clearable filterable placeholder="请选择供应商" class="!w-1/1"><el-option v-for="item in supplierList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></el-col>
</el-form-item> <el-col :span="8"><el-form-item label="发票" prop="invoiceNo"><el-input v-model="invoiceDisplay" readonly placeholder="请选择发票"><template #append><el-button @click="openInvoiceSelect"><Icon icon="ep:search" /> 选择</el-button></template></el-input></el-form-item></el-col>
</el-col> <el-col :span="8"><el-form-item label="财务人员" prop="financeUserId"><el-select v-model="formData.financeUserId" clearable filterable placeholder="请选择财务人员" class="!w-1/1"><el-option v-for="item in userList" :key="item.id" :label="item.nickname" :value="item.id" /></el-select></el-form-item></el-col>
<el-col :span="8"> <el-col :span="16"><el-form-item label="备注" prop="remark"><el-input type="textarea" v-model="formData.remark" :rows="1" placeholder="请输入备注" /></el-form-item></el-col>
<el-form-item label="付款时间" prop="paymentTime"> <el-col :span="8"><el-form-item label="附件" prop="fileUrl"><UploadFile :is-show-tip="false" v-model="formData.fileUrl" :limit="1" /></el-form-item></el-col>
<el-date-picker
v-model="formData.paymentTime"
type="date"
value-format="x"
placeholder="选择付款时间"
class="!w-1/1"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="供应商" prop="supplierId">
<el-select
v-model="formData.supplierId"
clearable
filterable
placeholder="请选择供应商"
class="!w-1/1"
>
<el-option
v-for="item in supplierList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="发票" prop="invoiceNo">
<el-input v-model="invoiceDisplay" readonly placeholder="请选择发票">
<template #append>
<el-button @click="openInvoiceSelect">
<Icon icon="ep:search" /> 选择
</el-button>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="财务人员" prop="financeUserId">
<el-select
v-model="formData.financeUserId"
clearable
filterable
placeholder="请选择财务人员"
class="!w-1/1"
>
<el-option
v-for="item in userList"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="16">
<el-form-item label="备注" prop="remark">
<el-input
type="textarea"
v-model="formData.remark"
:rows="1"
placeholder="请输入备注"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="附件" prop="fileUrl">
<UploadFile :is-show-tip="false" v-model="formData.fileUrl" :limit="1" />
</el-form-item>
</el-col>
</el-row> </el-row>
<!-- 子表的表单 -->
<ContentWrap> <ContentWrap>
<el-tabs v-model="subTabsName" class="-mt-15px -mb-10px"> <el-tabs v-model="subTabsName" class="-mt-15px -mb-10px">
<el-tab-pane label="采购入库、退货单" name="item"> <el-tab-pane label="采购入库、退货单" name="item"><FinancePaymentItemForm ref="itemFormRef" :supplier-id="formData.supplierId" :items="formData.items" :disabled="disabled" /></el-tab-pane>
<FinancePaymentItemForm
ref="itemFormRef"
:supplier-id="formData.supplierId"
:items="formData.items"
:disabled="disabled"
/>
</el-tab-pane>
</el-tabs> </el-tabs>
</ContentWrap> </ContentWrap>
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="8"> <el-col :span="8"><el-form-item label="付款账户" prop="accountId"><el-select v-model="formData.accountId" clearable filterable placeholder="请选择结算账户" class="!w-1/1"><el-option v-for="item in accountList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></el-col>
<el-form-item label="付款账户" prop="accountId"> <el-col :span="8"><el-form-item label="合计付款" prop="totalPrice"><el-input disabled v-model="formData.totalPrice" :formatter="erpPriceInputFormatter" /></el-form-item></el-col>
<el-select <el-col :span="8"><el-form-item label="优惠金额" prop="discountPrice"><el-input-number v-model="formData.discountPrice" controls-position="right" :precision="4" placeholder="请输入优惠金额" class="!w-1/1" /></el-form-item></el-col>
v-model="formData.accountId" <el-col :span="8"><el-form-item label="实际付款"><el-input disabled v-model="formData.paymentPrice" :formatter="erpPriceInputFormatter" /></el-form-item></el-col>
clearable
filterable
placeholder="请选择结算账户"
class="!w-1/1"
>
<el-option
v-for="item in accountList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="合计付款" prop="totalPrice">
<el-input disabled v-model="formData.totalPrice" :formatter="erpPriceInputFormatter" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="优惠金额" prop="discountPrice">
<el-input-number
v-model="formData.discountPrice"
controls-position="right"
:precision="4"
placeholder="请输入优惠金额"
class="!w-1/1"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="实际付款">
<el-input
disabled
v-model="formData.paymentPrice"
:formatter="erpPriceInputFormatter"
/>
</el-form-item>
</el-col>
</el-row> </el-row>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled"> <el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled"> </el-button>
</el-button>
<el-button @click="dialogVisible = false"> </el-button> <el-button @click="dialogVisible = false"> </el-button>
</template> </template>
</Dialog> </Dialog>
@@ -164,6 +71,7 @@
/> />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import { FinancePaymentApi, FinancePaymentVO } from '@/api/erp/finance/payment' import { FinancePaymentApi, FinancePaymentVO } from '@/api/erp/finance/payment'
import FinancePaymentItemForm from './components/FinancePaymentItemForm.vue' import FinancePaymentItemForm from './components/FinancePaymentItemForm.vue'
import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier' import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier'
@@ -172,10 +80,14 @@ import * as UserApi from '@/api/system/user'
import { AccountApi, AccountVO } from '@/api/erp/finance/account' import { AccountApi, AccountVO } from '@/api/erp/finance/account'
import InvoiceSelectList from '@/views/erp/finance/invoice/components/InvoiceSelectList.vue' import InvoiceSelectList from '@/views/erp/finance/invoice/components/InvoiceSelectList.vue'
import { Invoice } from '@/api/erp/finance/invoice' import { Invoice } from '@/api/erp/finance/invoice'
import { useWindowSize } from '@vueuse/core'
/** ERP 付款单表单 */ /** ERP 付款单表单 */
defineOptions({ name: 'FinancePaymentForm' }) defineOptions({ name: 'FinancePaymentForm' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const props = defineProps<{ const props = defineProps<{
fetchInvoicePage: (params: any) => Promise<any> fetchInvoicePage: (params: any) => Promise<any>
}>() }>()
@@ -330,3 +242,10 @@ const resetForm = () => {
formRef.value?.resetFields() formRef.value?.resetFields()
} }
</script> </script>
<style lang="scss" scoped>
.mobile-form { padding: 0 4px; }
.mobile-form__section { background: #fff; border-radius: 10px; padding: 14px; margin-bottom: 12px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
.mobile-form__section-title { font-size: 15px; font-weight: 600; color: #303133; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid #f0f0f0; }
.mobile-form__footer { position: sticky; bottom: 0; background: #fff; padding: 12px 16px; padding-bottom: calc(12px + env(safe-area-inset-bottom)); border-top: 1px solid #eee; display: flex; justify-content: flex-end; gap: 12px; z-index: 10; margin: 0 -4px; .el-button { flex: 1; height: 40px; font-size: 15px; } }
</style>

View File

@@ -1,13 +1,34 @@
<template> <template>
<el-form <!-- 移动端布局 -->
ref="formRef" <el-form v-if="isMobile" ref="formRef" :model="formData" :rules="formRules" v-loading="formLoading" label-position="top" :inline-message="true" :disabled="disabled">
:model="formData" <div class="mobile-item-list">
:rules="formRules" <div v-for="(row, $index) in formData" :key="$index" class="mobile-item-card">
v-loading="formLoading" <div class="mobile-item-card__header">
label-width="0px" <span class="mobile-item-card__index">#{{ $index + 1 }}</span>
:inline-message="true" <span class="mobile-item-card__name">{{ row.bizNo || '未选择单据' }}</span>
:disabled="disabled" <el-button :disabled="disabled" @click="handleDelete($index)" link type="danger" size="small">删除</el-button>
> </div>
<div class="mobile-item-card__body">
<div class="mobile-item-card__info-row"><span class="mobile-item-card__info-label">应付金额</span><span class="mobile-item-card__info-value">{{ erpPriceInputFormatter(row.totalPrice) }}</span></div>
<div class="mobile-item-card__info-row"><span class="mobile-item-card__info-label">已付金额</span><span class="mobile-item-card__info-value">{{ erpPriceInputFormatter(row.paidPrice) }}</span></div>
<el-form-item label="本次付款" :prop="`${$index}.paymentPrice`"><el-input-number v-model="row.paymentPrice" controls-position="right" :precision="4" style="width:100%" /></el-form-item>
<el-form-item label="备注" :prop="`${$index}.remark`"><el-input v-model="row.remark" placeholder="请输入备注" /></el-form-item>
</div>
</div>
<div class="mobile-item-summary" v-if="formData.length > 0">
<div class="mobile-item-summary__row"><span>应付合计</span><span>{{ erpPriceInputFormatter(summaryData.totalPrice) }}</span></div>
<div class="mobile-item-summary__row"><span>已付合计</span><span>{{ erpPriceInputFormatter(summaryData.paidPrice) }}</span></div>
<div class="mobile-item-summary__row mobile-item-summary__row--total"><span>本次付款合计</span><span>{{ erpPriceInputFormatter(summaryData.paymentPrice) }}</span></div>
</div>
<div class="mobile-item-add" v-if="!disabled">
<el-button @click="handleOpenPurchaseIn" round>+ 添加采购入库单</el-button>
<el-button @click="handleOpenPurchaseReturn" round>+ 添加采购退货单</el-button>
</div>
</div>
</el-form>
<!-- PC端布局 -->
<el-form v-if="!isMobile" ref="formRef" :model="formData" :rules="formRules" v-loading="formLoading" label-width="0px" :inline-message="true" :disabled="disabled">
<el-table :data="formData" show-summary :summary-method="getSummaries" class="-mt-10px"> <el-table :data="formData" show-summary :summary-method="getSummaries" class="-mt-10px">
<el-table-column label="序号" type="index" align="center" width="60" /> <el-table-column label="序号" type="index" align="center" width="60" />
<el-table-column label="采购单据编号" min-width="200"> <el-table-column label="采购单据编号" min-width="200">
@@ -69,7 +90,7 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
</el-form> </el-form>
<el-row justify="center" class="mt-3" v-if="!disabled"> <el-row v-if="!isMobile && !disabled" justify="center" class="mt-3">
<el-button @click="handleOpenPurchaseIn" round>+ 添加采购入库单</el-button> <el-button @click="handleOpenPurchaseIn" round>+ 添加采购入库单</el-button>
<el-button @click="handleOpenPurchaseReturn" round>+ 添加采购退货单</el-button> <el-button @click="handleOpenPurchaseReturn" round>+ 添加采购退货单</el-button>
</el-row> </el-row>
@@ -91,6 +112,7 @@
<PurchaseReturnForm ref="purchaseReturnFormRef" /> <PurchaseReturnForm ref="purchaseReturnFormRef" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue'
import { ProductVO } from '@/api/erp/product/product' import { ProductVO } from '@/api/erp/product/product'
import { erpPriceInputFormatter, getSumValue } from '@/utils' import { erpPriceInputFormatter, getSumValue } from '@/utils'
import PurchaseInPaymentEnableList from '@/views/erp/purchase/in/components/PurchaseInPaymentEnableList.vue' import PurchaseInPaymentEnableList from '@/views/erp/purchase/in/components/PurchaseInPaymentEnableList.vue'
@@ -100,6 +122,10 @@ import PurchaseReturnForm from '@/views/erp/purchase/return/PurchaseReturnForm.v
import { PurchaseInVO } from '@/api/erp/purchase/in' import { PurchaseInVO } from '@/api/erp/purchase/in'
import { ErpBizType } from '@/utils/constants' import { ErpBizType } from '@/utils/constants'
import { PurchaseReturnVO } from '@/api/erp/purchase/return' import { PurchaseReturnVO } from '@/api/erp/purchase/return'
import { useWindowSize } from '@vueuse/core'
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const props = defineProps<{ const props = defineProps<{
items: undefined items: undefined
@@ -125,21 +151,25 @@ watch(
{ immediate: true } { immediate: true }
) )
/** 合计 */ /** 合计 - 移动端 */
const summaryData = computed(() => {
return {
totalPrice: getSumValue(formData.value.map((item) => Number(item.totalPrice))),
paidPrice: getSumValue(formData.value.map((item) => Number(item.paidPrice))),
paymentPrice: getSumValue(formData.value.map((item) => Number(item.paymentPrice)))
}
})
/** 合计 - PC端 */
const getSummaries = (param: SummaryMethodProps) => { const getSummaries = (param: SummaryMethodProps) => {
const { columns, data } = param const { columns, data } = param
const sums: string[] = [] const sums: string[] = []
columns.forEach((column, index: number) => { columns.forEach((column, index: number) => {
if (index === 0) { if (index === 0) { sums[index] = '合计'; return }
sums[index] = '合计'
return
}
if (['totalPrice', 'paidPrice', 'paymentPrice'].includes(column.property)) { if (['totalPrice', 'paidPrice', 'paymentPrice'].includes(column.property)) {
const sum = getSumValue(data.map((item) => Number(item[column.property]))) const sum = getSumValue(data.map((item) => Number(item[column.property])))
sums[index] = erpPriceInputFormatter(sum) sums[index] = erpPriceInputFormatter(sum)
} else { } else { sums[index] = '' }
sums[index] = ''
}
}) })
return sums return sums
} }

View File

@@ -1,27 +1,63 @@
<template> <template>
<doc-alert <!-- 移动端布局 -->
title="【财务】采购付款、销售收款" <div v-if="isMobile" class="mobile-payment">
url="https://doc.iocoder.cn/sale/finance-payment-receipt/" <div class="mobile-header">
/> <div class="mobile-header__search"><el-input v-model="queryParams.no" placeholder="搜索付款单号" clearable @keyup.enter="handleQuery" :prefix-icon="Search" /></div>
<div class="mobile-header__actions">
<el-button :icon="Filter" circle @click="filterVisible = true" />
<el-button type="primary" :icon="Plus" circle @click="openForm('create')" v-hasPermi="['erp:finance-payment:create']" />
</div>
</div>
<div class="mobile-header__quick-filter">
<div class="quick-filter-item" :class="{ active: queryParams.status === undefined }" @click="handleQuickFilter(undefined)">全部</div>
<div class="quick-filter-item" :class="{ active: queryParams.status === 10 }" @click="handleQuickFilter(10)">待审核</div>
<div class="quick-filter-item" :class="{ active: queryParams.status === 20 }" @click="handleQuickFilter(20)">已审核</div>
</div>
<div class="mobile-list" v-loading="loading">
<div v-if="list.length === 0 && !loading" class="mobile-empty"><el-empty description="暂无付款记录" /></div>
<div v-for="item in list" :key="item.id" class="mobile-card" @click="handleRowClickMobile(item)">
<div class="mobile-card__header">
<span class="mobile-card__no">{{ item.no }}</span>
<dict-tag :type="DICT_TYPE.ERP_AUDIT_STATUS" :value="item.status" />
</div>
<div class="mobile-card__body">
<div class="mobile-card__row"><span class="mobile-card__label">供应商</span><span class="mobile-card__value">{{ item.supplierName || '-' }}</span></div>
<div class="mobile-card__row"><span class="mobile-card__label">付款时间</span><span class="mobile-card__value">{{ formatDate2(item.paymentTime) }}</span></div>
<div class="mobile-card__row"><span class="mobile-card__label">财务人员</span><span class="mobile-card__value">{{ item.financeUserName || '-' }}</span></div>
<div class="mobile-card__nums">
<div class="mobile-card__num-item"><div class="mobile-card__num-val">{{ erpPriceTableColumnFormatter(null,null,item.totalPrice,null) }}</div><div class="mobile-card__num-label">合计付款</div></div>
<div class="mobile-card__num-item"><div class="mobile-card__num-val mobile-card__num-val--price">{{ erpPriceTableColumnFormatter(null,null,item.paymentPrice,null) }}</div><div class="mobile-card__num-label">实际付款</div></div>
</div>
</div>
<div class="mobile-card__footer">
<el-button size="small" @click.stop="openForm('detail', item.id)" v-hasPermi="['erp:finance-payment:query']">详情</el-button>
<el-button size="small" type="primary" @click.stop="openForm('update', item.id)" v-hasPermi="['erp:finance-payment:update']" :disabled="item.status === 20">编辑</el-button>
<el-button size="small" type="primary" @click.stop="handleUpdateStatus(item.id, 20)" v-hasPermi="['erp:finance-payment:update-status']" v-if="item.status === 10">审批</el-button>
<el-button size="small" type="danger" @click.stop="handleUpdateStatus(item.id, 10)" v-hasPermi="['erp:finance-payment:update-status']" v-else>反审批</el-button>
<el-button size="small" type="danger" @click.stop="handleDelete([item.id])" v-hasPermi="['erp:finance-payment:delete']">删除</el-button>
</div>
</div>
</div>
<div class="mobile-pagination" v-if="total > 0"><Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" :page-sizes="[10, 20]" layout="total, prev, pager, next" :pager-count="5" @pagination="getList" /></div>
<el-drawer v-model="filterVisible" title="筛选条件" direction="btt" size="70%">
<el-form :model="queryParams" ref="queryFormRef" label-position="top">
<el-form-item label="供应商" prop="supplierId"><el-select v-model="queryParams.supplierId" clearable filterable placeholder="请选择供应商" style="width:100%"><el-option v-for="item in supplierList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item>
<el-form-item label="付款时间" prop="paymentTime"><el-date-picker v-model="queryParams.paymentTime" value-format="YYYY-MM-DD HH:mm:ss" type="daterange" start-placeholder="开始" end-placeholder="结束" :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" style="width:100%" /></el-form-item>
<el-form-item label="付款账户" prop="accountId"><el-select v-model="queryParams.accountId" clearable filterable placeholder="请选择付款账户" style="width:100%"><el-option v-for="item in accountList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item>
<el-form-item label="状态" prop="status"><el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width:100%"><el-option v-for="dict in getIntDictOptions(DICT_TYPE.ERP_AUDIT_STATUS)" :key="dict.value" :label="dict.label" :value="dict.value" /></el-select></el-form-item>
</el-form>
<template #footer>
<el-button @click="resetQuery">重置</el-button>
<el-button type="primary" @click="handleFilterConfirm">确认筛选</el-button>
</template>
</el-drawer>
</div>
<ContentWrap> <!-- PC端布局 -->
<!-- 搜索工作栏 --> <template v-else>
<el-form <ContentWrap>
class="-mb-15px" <el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
:model="queryParams" <el-form-item label="付款单号" prop="no"><el-input v-model="queryParams.no" placeholder="请输入付款单号" clearable @keyup.enter="handleQuery" class="!w-240px" /></el-form-item>
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="付款单号" prop="no">
<el-input
v-model="queryParams.no"
placeholder="请输入付款单号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="付款时间" prop="paymentTime"> <el-form-item label="付款时间" prop="paymentTime">
<el-date-picker <el-date-picker
v-model="queryParams.paymentTime" v-model="queryParams.paymentTime"
@@ -252,22 +288,18 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<!-- 分页 --> <Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" />
<Pagination </ContentWrap>
:total="total" </template>
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 --> <!-- 表单弹窗添加/修改 -->
<FinancePaymentForm ref="formRef" :fetch-invoice-page="fetchInvoicePage" @success="getList" /> <FinancePaymentForm ref="formRef" :fetch-invoice-page="fetchInvoicePage" @success="getList" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { dateFormatter2 } from '@/utils/formatTime' import { dateFormatter2, formatDate2 } from '@/utils/formatTime'
import download from '@/utils/download' import download from '@/utils/download'
import { FinancePaymentApi, FinancePaymentVO } from '@/api/erp/finance/payment' import { FinancePaymentApi, FinancePaymentVO } from '@/api/erp/finance/payment'
import FinancePaymentForm from './FinancePaymentForm.vue' import FinancePaymentForm from './FinancePaymentForm.vue'
@@ -277,9 +309,14 @@ import * as UserApi from '@/api/system/user'
import { erpPriceTableColumnFormatter } from '@/utils' import { erpPriceTableColumnFormatter } from '@/utils'
import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier' import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier'
import { AccountApi, AccountVO } from '@/api/erp/finance/account' import { AccountApi, AccountVO } from '@/api/erp/finance/account'
import { useWindowSize } from '@vueuse/core'
import { Search, Plus, Filter } from '@element-plus/icons-vue'
/** ERP 付款单列表 */ /** ERP 付款单列表 */
defineOptions({ name: 'ErpPurchaseOrder' }) defineOptions({ name: 'ErpFinancePayment' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
@@ -302,6 +339,7 @@ const queryParams = reactive({
}) })
const queryFormRef = ref() // 搜索的表单 const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中 const exportLoading = ref(false) // 导出的加载中
const filterVisible = ref(false) // 筛选抽屉
const supplierList = ref<SupplierVO[]>([]) // 供应商列表 const supplierList = ref<SupplierVO[]>([]) // 供应商列表
const userList = ref<UserVO[]>([]) // 用户列表 const userList = ref<UserVO[]>([]) // 用户列表
const accountList = ref<AccountVO[]>([]) // 账户列表 const accountList = ref<AccountVO[]>([]) // 账户列表
@@ -329,10 +367,29 @@ const handleQuery = () => {
/** 重置按钮操作 */ /** 重置按钮操作 */
const resetQuery = () => { const resetQuery = () => {
queryFormRef.value.resetFields() queryFormRef.value?.resetFields()
handleQuery() handleQuery()
} }
/** 筛选确认 */
const handleFilterConfirm = () => {
filterVisible.value = false
handleQuery()
}
/** 快捷筛选 */
const handleQuickFilter = (status: number | undefined) => {
queryParams.status = status
queryParams.pageNo = 1
getList()
}
/** 移动端行点击 */
const handleRowClickMobile = (row: FinancePaymentVO) => {
if (row.status === 20) openForm('detail', row.id)
else if (row.status === 10) openForm('update', row.id)
}
/** 添加/修改操作 */ /** 添加/修改操作 */
const formRef = ref() const formRef = ref()
const openForm = (type: string, id?: number) => { const openForm = (type: string, id?: number) => {
@@ -420,6 +477,30 @@ onMounted(async () => {
userList.value = await UserApi.getSimpleUserList() userList.value = await UserApi.getSimpleUserList()
accountList.value = await AccountApi.getAccountSimpleList() accountList.value = await AccountApi.getAccountSimpleList()
}) })
// TODO 芋艿:可优化功能:列表界面,支持导入
// TODO 芋艿:可优化功能:详情界面,支持打印
</script> </script>
<style lang="scss" scoped>
.mobile-payment { padding: 12px; background: #f5f5f5; min-height: 100vh; }
.mobile-header { display: flex; gap: 8px; align-items: center; margin-bottom: 12px; }
.mobile-header__search { flex: 1; }
.mobile-header__actions { display: flex; gap: 4px; }
.mobile-header__quick-filter { display: flex; gap: 12px; margin: 8px 0; }
.quick-filter-item { padding: 4px 12px; font-size: 14px; border-radius: 20px; cursor: pointer; color: #909399; background: transparent; transition: all 0.2s; }
.quick-filter-item.active { color: #fff; background: #409eff; }
.mobile-list { display: flex; flex-direction: column; gap: 10px; }
.mobile-empty { padding: 40px 0; }
.mobile-card { background: #fff; border-radius: 10px; padding: 14px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
.mobile-card__header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.mobile-card__no { font-weight: 600; font-size: 15px; color: #303133; }
.mobile-card__body { font-size: 13px; }
.mobile-card__row { display: flex; justify-content: space-between; padding: 3px 0; }
.mobile-card__label { color: #909399; flex-shrink: 0; margin-right: 12px; }
.mobile-card__value { color: #606266; text-align: right; }
.mobile-card__nums { display: flex; justify-content: space-around; margin-top: 10px; padding: 10px 0; border-top: 1px solid #f0f0f0; border-bottom: 1px solid #f0f0f0; }
.mobile-card__num-item { text-align: center; }
.mobile-card__num-val { font-size: 15px; font-weight: 600; color: #303133; }
.mobile-card__num-val--price { color: #e6a23c; }
.mobile-card__num-label { font-size: 11px; color: #909399; margin-top: 2px; }
.mobile-card__footer { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.mobile-pagination { margin-top: 12px; display: flex; justify-content: center; :deep(.el-pagination) { flex-wrap: wrap; justify-content: center; } }
</style>

View File

@@ -1,167 +1,72 @@
<template> <template>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="1080"> <!-- 移动端布局 -->
<el-form <el-drawer v-if="isMobile" v-model="dialogVisible" :title="dialogTitle" direction="rtl" size="100%" :close-on-press-escape="true" :destroy-on-close="true" :append-to-body="true">
ref="formRef" <div class="mobile-form" v-loading="formLoading">
:model="formData" <el-form ref="formRef" :model="formData" :rules="formRules" label-position="top" :disabled="disabled">
:rules="formRules" <div class="mobile-form__section">
label-width="100px" <div class="mobile-form__section-title">基本信息</div>
v-loading="formLoading" <el-form-item label="收款单号" prop="no"><el-input disabled v-model="formData.no" placeholder="保存时自动生成" /></el-form-item>
:disabled="disabled" <el-form-item label="收款时间" prop="receiptTime"><el-date-picker v-model="formData.receiptTime" type="date" value-format="x" placeholder="选择收款时间" style="width:100%" /></el-form-item>
> <el-form-item label="客户" prop="customerId"><el-select v-model="formData.customerId" clearable filterable placeholder="请选择客户" style="width:100%"><el-option v-for="item in customerList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item>
<el-form-item label="发票" prop="invoiceNo"><el-input v-model="invoiceDisplay" readonly placeholder="请选择发票"><template #append><el-button @click="openInvoiceSelect"><Icon icon="ep:search" /></el-button></template></el-input></el-form-item>
<el-form-item label="财务人员" prop="financeUserId"><el-select v-model="formData.financeUserId" clearable filterable placeholder="请选择财务人员" style="width:100%"><el-option v-for="item in userList" :key="item.id" :label="item.nickname" :value="item.id" /></el-select></el-form-item>
<el-form-item label="备注" prop="remark"><el-input type="textarea" v-model="formData.remark" :rows="2" placeholder="请输入备注" /></el-form-item>
<el-form-item label="附件" prop="fileUrl"><UploadFile :is-show-tip="false" v-model="formData.fileUrl" :limit="1" /></el-form-item>
</div>
<div class="mobile-form__section">
<div class="mobile-form__section-title">销售出库退货单</div>
<FinanceReceiptItemForm ref="itemFormRef" :customer-id="formData.customerId" :items="formData.items" :disabled="disabled" />
</div>
<div class="mobile-form__section">
<div class="mobile-form__section-title">结算信息</div>
<el-form-item label="收款账户" prop="accountId"><el-select v-model="formData.accountId" clearable filterable placeholder="请选择结算账户" style="width:100%"><el-option v-for="item in accountList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item>
<el-form-item label="合计收款" prop="totalPrice"><el-input disabled v-model="formData.totalPrice" :formatter="erpPriceInputFormatter" /></el-form-item>
<el-form-item label="优惠金额" prop="discountPrice"><el-input-number v-model="formData.discountPrice" controls-position="right" :precision="4" placeholder="请输入优惠金额" style="width:100%" /></el-form-item>
<el-form-item label="实际收款"><el-input disabled v-model="formData.receiptPrice" :formatter="erpPriceInputFormatter" /></el-form-item>
</div>
</el-form>
<div class="mobile-form__footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled"> </el-button>
</div>
</div>
</el-drawer>
<!-- PC端布局 -->
<Dialog v-else :title="dialogTitle" v-model="dialogVisible" width="1080">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" v-loading="formLoading" :disabled="disabled">
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="8"> <el-col :span="8"><el-form-item label="收款单号" prop="no"><el-input disabled v-model="formData.no" placeholder="保存时自动生成" /></el-form-item></el-col>
<el-form-item label="收款单号" prop="no"> <el-col :span="8"><el-form-item label="收款时间" prop="receiptTime"><el-date-picker v-model="formData.receiptTime" type="date" value-format="x" placeholder="选择收款时间" class="!w-1/1" /></el-form-item></el-col>
<el-input disabled v-model="formData.no" placeholder="保存时自动生成" /> <el-col :span="8"><el-form-item label="客户" prop="customerId"><el-select v-model="formData.customerId" clearable filterable placeholder="请选择客户" class="!w-1/1"><el-option v-for="item in customerList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></el-col>
</el-form-item> <el-col :span="8"><el-form-item label="发票" prop="invoiceNo"><el-input v-model="invoiceDisplay" readonly placeholder="请选择发票"><template #append><el-button @click="openInvoiceSelect"><Icon icon="ep:search" /> 选择</el-button></template></el-input></el-form-item></el-col>
</el-col> <el-col :span="8"><el-form-item label="财务人员" prop="financeUserId"><el-select v-model="formData.financeUserId" clearable filterable placeholder="请选择财务人员" class="!w-1/1"><el-option v-for="item in userList" :key="item.id" :label="item.nickname" :value="item.id" /></el-select></el-form-item></el-col>
<el-col :span="8"> <el-col :span="16"><el-form-item label="备注" prop="remark"><el-input type="textarea" v-model="formData.remark" :rows="1" placeholder="请输入备注" /></el-form-item></el-col>
<el-form-item label="收款时间" prop="receiptTime"> <el-col :span="8"><el-form-item label="附件" prop="fileUrl"><UploadFile :is-show-tip="false" v-model="formData.fileUrl" :limit="1" /></el-form-item></el-col>
<el-date-picker
v-model="formData.receiptTime"
type="date"
value-format="x"
placeholder="选择收款时间"
class="!w-1/1"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="客户" prop="customerId">
<el-select
v-model="formData.customerId"
clearable
filterable
placeholder="请选择客户"
class="!w-1/1"
>
<el-option
v-for="item in customerList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="发票" prop="invoiceNo">
<el-input v-model="invoiceDisplay" readonly placeholder="请选择发票">
<template #append>
<el-button @click="openInvoiceSelect"> <Icon icon="ep:search" /> 选择 </el-button>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="财务人员" prop="financeUserId">
<el-select
v-model="formData.financeUserId"
clearable
filterable
placeholder="请选择财务人员"
class="!w-1/1"
>
<el-option
v-for="item in userList"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="16">
<el-form-item label="备注" prop="remark">
<el-input
type="textarea"
v-model="formData.remark"
:rows="1"
placeholder="请输入备注"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="附件" prop="fileUrl">
<UploadFile :is-show-tip="false" v-model="formData.fileUrl" :limit="1" />
</el-form-item>
</el-col>
</el-row> </el-row>
<!-- 子表的表单 -->
<ContentWrap> <ContentWrap>
<el-tabs v-model="subTabsName" class="-mt-15px -mb-10px"> <el-tabs v-model="subTabsName" class="-mt-15px -mb-10px">
<el-tab-pane label="采购入库、退货单" name="item"> <el-tab-pane label="销售出库、退货单" name="item"><FinanceReceiptItemForm ref="itemFormRef" :customer-id="formData.customerId" :items="formData.items" :disabled="disabled" /></el-tab-pane>
<FinanceReceiptItemForm
ref="itemFormRef"
:customer-id="formData.customerId"
:items="formData.items"
:disabled="disabled"
/>
</el-tab-pane>
</el-tabs> </el-tabs>
</ContentWrap> </ContentWrap>
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="8"> <el-col :span="8"><el-form-item label="收款账户" prop="accountId"><el-select v-model="formData.accountId" clearable filterable placeholder="请选择结算账户" class="!w-1/1"><el-option v-for="item in accountList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></el-col>
<el-form-item label="收款账户" prop="accountId"> <el-col :span="8"><el-form-item label="合计收款" prop="totalPrice"><el-input disabled v-model="formData.totalPrice" :formatter="erpPriceInputFormatter" /></el-form-item></el-col>
<el-select <el-col :span="8"><el-form-item label="优惠金额" prop="discountPrice"><el-input-number v-model="formData.discountPrice" controls-position="right" :precision="4" placeholder="请输入优惠金额" class="!w-1/1" /></el-form-item></el-col>
v-model="formData.accountId" <el-col :span="8"><el-form-item label="实际收款"><el-input disabled v-model="formData.receiptPrice" :formatter="erpPriceInputFormatter" /></el-form-item></el-col>
clearable
filterable
placeholder="请选择结算账户"
class="!w-1/1"
>
<el-option
v-for="item in accountList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="合计收款" prop="totalPrice">
<el-input disabled v-model="formData.totalPrice" :formatter="erpPriceInputFormatter" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="优惠金额" prop="discountPrice">
<el-input-number
v-model="formData.discountPrice"
controls-position="right"
:precision="4"
placeholder="请输入优惠金额"
class="!w-1/1"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="实际收款">
<el-input
disabled
v-model="formData.receiptPrice"
:formatter="erpPriceInputFormatter"
/>
</el-form-item>
</el-col>
</el-row> </el-row>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled"> <el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled"> </el-button>
</el-button>
<el-button @click="dialogVisible = false"> </el-button> <el-button @click="dialogVisible = false"> </el-button>
</template> </template>
</Dialog> </Dialog>
<!-- 发票选择弹窗 --> <!-- 发票选择弹窗 -->
<InvoiceSelectList <InvoiceSelectList ref="invoiceSelectRef" :invoice-category="2" :fetch-invoice-page="props.fetchInvoicePage" @success="handleInvoiceSelect" />
ref="invoiceSelectRef"
:invoice-category="2"
:fetch-invoice-page="props.fetchInvoicePage"
@success="handleInvoiceSelect"
/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import { FinanceReceiptApi, FinanceReceiptVO } from '@/api/erp/finance/receipt' import { FinanceReceiptApi, FinanceReceiptVO } from '@/api/erp/finance/receipt'
import FinanceReceiptItemForm from './components/FinanceReceiptItemForm.vue' import FinanceReceiptItemForm from './components/FinanceReceiptItemForm.vue'
import { erpPriceInputFormatter } from '@/utils' import { erpPriceInputFormatter } from '@/utils'
@@ -170,10 +75,14 @@ import { AccountApi, AccountVO } from '@/api/erp/finance/account'
import { CustomerApi, CustomerVO } from '@/api/erp/sale/customer' import { CustomerApi, CustomerVO } from '@/api/erp/sale/customer'
import InvoiceSelectList from '@/views/erp/finance/invoice/components/InvoiceSelectList.vue' import InvoiceSelectList from '@/views/erp/finance/invoice/components/InvoiceSelectList.vue'
import { Invoice } from '@/api/erp/finance/invoice' import { Invoice } from '@/api/erp/finance/invoice'
import { useWindowSize } from '@vueuse/core'
/** ERP 收款单表单 */ /** ERP 收款单表单 */
defineOptions({ name: 'FinanceReceiptForm' }) defineOptions({ name: 'FinanceReceiptForm' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const props = defineProps<{ const props = defineProps<{
fetchInvoicePage: (params: any) => Promise<any> fetchInvoicePage: (params: any) => Promise<any>
}>() }>()
@@ -324,3 +233,10 @@ const resetForm = () => {
formRef.value?.resetFields() formRef.value?.resetFields()
} }
</script> </script>
<style lang="scss" scoped>
.mobile-form { padding: 0 4px; }
.mobile-form__section { background: #fff; border-radius: 10px; padding: 14px; margin-bottom: 12px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
.mobile-form__section-title { font-size: 15px; font-weight: 600; color: #303133; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid #f0f0f0; }
.mobile-form__footer { position: sticky; bottom: 0; background: #fff; padding: 12px 16px; padding-bottom: calc(12px + env(safe-area-inset-bottom)); border-top: 1px solid #eee; display: flex; justify-content: flex-end; gap: 12px; z-index: 10; margin: 0 -4px; .el-button { flex: 1; height: 40px; font-size: 15px; } }
</style>

View File

@@ -1,13 +1,34 @@
<template> <template>
<el-form <!-- 移动端布局 -->
ref="formRef" <el-form v-if="isMobile" ref="formRef" :model="formData" :rules="formRules" v-loading="formLoading" label-position="top" :inline-message="true" :disabled="disabled">
:model="formData" <div class="mobile-item-list">
:rules="formRules" <div v-for="(row, $index) in formData" :key="$index" class="mobile-item-card">
v-loading="formLoading" <div class="mobile-item-card__header">
label-width="0px" <span class="mobile-item-card__index">#{{ $index + 1 }}</span>
:inline-message="true" <span class="mobile-item-card__name">{{ row.bizNo || '未选择单据' }}</span>
:disabled="disabled" <el-button :disabled="disabled" @click="handleDelete($index)" link type="danger" size="small">删除</el-button>
> </div>
<div class="mobile-item-card__body">
<div class="mobile-item-card__info-row"><span class="mobile-item-card__info-label">应收金额</span><span class="mobile-item-card__info-value">{{ erpPriceInputFormatter(row.totalPrice) }}</span></div>
<div class="mobile-item-card__info-row"><span class="mobile-item-card__info-label">已收金额</span><span class="mobile-item-card__info-value">{{ erpPriceInputFormatter(row.receiptedPrice) }}</span></div>
<el-form-item label="本次收款" :prop="`${$index}.receiptPrice`"><el-input-number v-model="row.receiptPrice" controls-position="right" :precision="4" style="width:100%" /></el-form-item>
<el-form-item label="备注" :prop="`${$index}.remark`"><el-input v-model="row.remark" placeholder="请输入备注" /></el-form-item>
</div>
</div>
<div class="mobile-item-summary" v-if="formData.length > 0">
<div class="mobile-item-summary__row"><span>应收合计</span><span>{{ erpPriceInputFormatter(summaryData.totalPrice) }}</span></div>
<div class="mobile-item-summary__row"><span>已收合计</span><span>{{ erpPriceInputFormatter(summaryData.receiptedPrice) }}</span></div>
<div class="mobile-item-summary__row mobile-item-summary__row--total"><span>本次收款合计</span><span>{{ erpPriceInputFormatter(summaryData.receiptPrice) }}</span></div>
</div>
<div class="mobile-item-add" v-if="!disabled">
<el-button @click="handleOpenSaleOut" round>+ 添加销售出库单</el-button>
<el-button @click="handleOpenSaleReturn" round>+ 添加销售退货单</el-button>
</div>
</div>
</el-form>
<!-- PC端布局 -->
<el-form v-if="!isMobile" ref="formRef" :model="formData" :rules="formRules" v-loading="formLoading" label-width="0px" :inline-message="true" :disabled="disabled">
<el-table :data="formData" show-summary :summary-method="getSummaries" class="-mt-10px"> <el-table :data="formData" show-summary :summary-method="getSummaries" class="-mt-10px">
<el-table-column label="序号" type="index" align="center" width="60" /> <el-table-column label="序号" type="index" align="center" width="60" />
<el-table-column label="销售单据编号" min-width="200"> <el-table-column label="销售单据编号" min-width="200">
@@ -69,7 +90,7 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
</el-form> </el-form>
<el-row justify="center" class="mt-3" v-if="!disabled"> <el-row v-if="!isMobile && !disabled" justify="center" class="mt-3">
<el-button @click="handleOpenSaleOut" round>+ 添加销售出库单</el-button> <el-button @click="handleOpenSaleOut" round>+ 添加销售出库单</el-button>
<el-button @click="handleOpenSaleReturn" round>+ 添加销售退货单</el-button> <el-button @click="handleOpenSaleReturn" round>+ 添加销售退货单</el-button>
</el-row> </el-row>
@@ -85,6 +106,7 @@
<SaleReturnForm ref="saleReturnFormRef" /> <SaleReturnForm ref="saleReturnFormRef" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue'
import { ProductVO } from '@/api/erp/product/product' import { ProductVO } from '@/api/erp/product/product'
import { erpPriceInputFormatter, getSumValue } from '@/utils' import { erpPriceInputFormatter, getSumValue } from '@/utils'
import SaleOutReceiptEnableList from '@/views/erp/sale/out/components/SaleOutReceiptEnableList.vue' import SaleOutReceiptEnableList from '@/views/erp/sale/out/components/SaleOutReceiptEnableList.vue'
@@ -94,6 +116,10 @@ import SaleReturnForm from '@/views/erp/sale/return/SaleReturnForm.vue'
import { SaleOutVO } from '@/api/erp/sale/out' import { SaleOutVO } from '@/api/erp/sale/out'
import { ErpBizType } from '@/utils/constants' import { ErpBizType } from '@/utils/constants'
import { SaleReturnVO } from '@/api/erp/sale/return' import { SaleReturnVO } from '@/api/erp/sale/return'
import { useWindowSize } from '@vueuse/core'
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const props = defineProps<{ const props = defineProps<{
items: undefined items: undefined
@@ -119,21 +145,25 @@ watch(
{ immediate: true } { immediate: true }
) )
/** 合计 */ /** 合计 - 移动端 */
const summaryData = computed(() => {
return {
totalPrice: getSumValue(formData.value.map((item) => Number(item.totalPrice))),
receiptedPrice: getSumValue(formData.value.map((item) => Number(item.receiptedPrice))),
receiptPrice: getSumValue(formData.value.map((item) => Number(item.receiptPrice)))
}
})
/** 合计 - PC端 */
const getSummaries = (param: SummaryMethodProps) => { const getSummaries = (param: SummaryMethodProps) => {
const { columns, data } = param const { columns, data } = param
const sums: string[] = [] const sums: string[] = []
columns.forEach((column, index: number) => { columns.forEach((column, index: number) => {
if (index === 0) { if (index === 0) { sums[index] = '合计'; return }
sums[index] = '合计'
return
}
if (['totalPrice', 'receiptedPrice', 'receiptPrice'].includes(column.property)) { if (['totalPrice', 'receiptedPrice', 'receiptPrice'].includes(column.property)) {
const sum = getSumValue(data.map((item) => Number(item[column.property]))) const sum = getSumValue(data.map((item) => Number(item[column.property])))
sums[index] = erpPriceInputFormatter(sum) sums[index] = erpPriceInputFormatter(sum)
} else { } else { sums[index] = '' }
sums[index] = ''
}
}) })
return sums return sums
} }

View File

@@ -1,27 +1,63 @@
<template> <template>
<doc-alert <!-- 移动端布局 -->
title="【财务】采购付款、销售收款" <div v-if="isMobile" class="mobile-receipt">
url="https://doc.iocoder.cn/sale/finance-payment-receipt/" <div class="mobile-header">
/> <div class="mobile-header__search"><el-input v-model="queryParams.no" placeholder="搜索收款单号" clearable @keyup.enter="handleQuery" :prefix-icon="Search" /></div>
<div class="mobile-header__actions">
<el-button :icon="Filter" circle @click="filterVisible = true" />
<el-button type="primary" :icon="Plus" circle @click="openForm('create')" v-hasPermi="['erp:finance-receipt:create']" />
</div>
</div>
<div class="mobile-header__quick-filter">
<div class="quick-filter-item" :class="{ active: queryParams.status === undefined }" @click="handleQuickFilter(undefined)">全部</div>
<div class="quick-filter-item" :class="{ active: queryParams.status === 10 }" @click="handleQuickFilter(10)">待审核</div>
<div class="quick-filter-item" :class="{ active: queryParams.status === 20 }" @click="handleQuickFilter(20)">已审核</div>
</div>
<div class="mobile-list" v-loading="loading">
<div v-if="list.length === 0 && !loading" class="mobile-empty"><el-empty description="暂无收款记录" /></div>
<div v-for="item in list" :key="item.id" class="mobile-card" @click="handleRowClickMobile(item)">
<div class="mobile-card__header">
<span class="mobile-card__no">{{ item.no }}</span>
<dict-tag :type="DICT_TYPE.ERP_AUDIT_STATUS" :value="item.status" />
</div>
<div class="mobile-card__body">
<div class="mobile-card__row"><span class="mobile-card__label">客户</span><span class="mobile-card__value">{{ item.customerName || '-' }}</span></div>
<div class="mobile-card__row"><span class="mobile-card__label">收款时间</span><span class="mobile-card__value">{{ formatDate2(item.receiptTime) }}</span></div>
<div class="mobile-card__row"><span class="mobile-card__label">财务人员</span><span class="mobile-card__value">{{ item.financeUserName || '-' }}</span></div>
<div class="mobile-card__nums">
<div class="mobile-card__num-item"><div class="mobile-card__num-val">{{ erpPriceTableColumnFormatter(null,null,item.totalPrice,null) }}</div><div class="mobile-card__num-label">合计收款</div></div>
<div class="mobile-card__num-item"><div class="mobile-card__num-val mobile-card__num-val--price">{{ erpPriceTableColumnFormatter(null,null,item.receiptPrice,null) }}</div><div class="mobile-card__num-label">实际收款</div></div>
</div>
</div>
<div class="mobile-card__footer">
<el-button size="small" @click.stop="openForm('detail', item.id)" v-hasPermi="['erp:finance-receipt:query']">详情</el-button>
<el-button size="small" type="primary" @click.stop="openForm('update', item.id)" v-hasPermi="['erp:finance-receipt:update']" :disabled="item.status === 20">编辑</el-button>
<el-button size="small" type="primary" @click.stop="handleUpdateStatus(item.id, 20)" v-hasPermi="['erp:finance-receipt:update-status']" v-if="item.status === 10">审批</el-button>
<el-button size="small" type="danger" @click.stop="handleUpdateStatus(item.id, 10)" v-hasPermi="['erp:finance-receipt:update-status']" v-else>反审批</el-button>
<el-button size="small" type="danger" @click.stop="handleDelete([item.id])" v-hasPermi="['erp:finance-receipt:delete']">删除</el-button>
</div>
</div>
</div>
<div class="mobile-pagination" v-if="total > 0"><Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" :page-sizes="[10, 20]" layout="total, prev, pager, next" :pager-count="5" @pagination="getList" /></div>
<el-drawer v-model="filterVisible" title="筛选条件" direction="btt" size="70%">
<el-form :model="queryParams" ref="queryFormRef" label-position="top">
<el-form-item label="客户" prop="customerId"><el-select v-model="queryParams.customerId" clearable filterable placeholder="请选择客户" style="width:100%"><el-option v-for="item in customerList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item>
<el-form-item label="收款时间" prop="receiptTime"><el-date-picker v-model="queryParams.receiptTime" value-format="YYYY-MM-DD HH:mm:ss" type="daterange" start-placeholder="开始" end-placeholder="结束" :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" style="width:100%" /></el-form-item>
<el-form-item label="收款账户" prop="accountId"><el-select v-model="queryParams.accountId" clearable filterable placeholder="请选择收款账户" style="width:100%"><el-option v-for="item in accountList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item>
<el-form-item label="状态" prop="status"><el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width:100%"><el-option v-for="dict in getIntDictOptions(DICT_TYPE.ERP_AUDIT_STATUS)" :key="dict.value" :label="dict.label" :value="dict.value" /></el-select></el-form-item>
</el-form>
<template #footer>
<el-button @click="resetQuery">重置</el-button>
<el-button type="primary" @click="handleFilterConfirm">确认筛选</el-button>
</template>
</el-drawer>
</div>
<ContentWrap> <!-- PC端布局 -->
<!-- 搜索工作栏 --> <template v-else>
<el-form <ContentWrap>
class="-mb-15px" <el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
:model="queryParams" <el-form-item label="收款单号" prop="no"><el-input v-model="queryParams.no" placeholder="请输入收款单号" clearable @keyup.enter="handleQuery" class="!w-240px" /></el-form-item>
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="收款单号" prop="no">
<el-input
v-model="queryParams.no"
placeholder="请输入收款单号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="收款时间" prop="receiptTime"> <el-form-item label="收款时间" prop="receiptTime">
<el-date-picker <el-date-picker
v-model="queryParams.receiptTime" v-model="queryParams.receiptTime"
@@ -252,22 +288,18 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<!-- 分页 --> <Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" />
<Pagination </ContentWrap>
:total="total" </template>
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 --> <!-- 表单弹窗添加/修改 -->
<FinanceReceiptForm ref="formRef" :fetch-invoice-page="fetchInvoicePage" @success="getList" /> <FinanceReceiptForm ref="formRef" :fetch-invoice-page="fetchInvoicePage" @success="getList" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { dateFormatter2 } from '@/utils/formatTime' import { dateFormatter2, formatDate2 } from '@/utils/formatTime'
import download from '@/utils/download' import download from '@/utils/download'
import { FinanceReceiptApi, FinanceReceiptVO } from '@/api/erp/finance/receipt' import { FinanceReceiptApi, FinanceReceiptVO } from '@/api/erp/finance/receipt'
import FinanceReceiptForm from './FinanceReceiptForm.vue' import FinanceReceiptForm from './FinanceReceiptForm.vue'
@@ -277,9 +309,14 @@ import * as UserApi from '@/api/system/user'
import { erpPriceTableColumnFormatter } from '@/utils' import { erpPriceTableColumnFormatter } from '@/utils'
import { CustomerApi, CustomerVO } from '@/api/erp/sale/customer' import { CustomerApi, CustomerVO } from '@/api/erp/sale/customer'
import { AccountApi, AccountVO } from '@/api/erp/finance/account' import { AccountApi, AccountVO } from '@/api/erp/finance/account'
import { useWindowSize } from '@vueuse/core'
import { Search, Plus, Filter } from '@element-plus/icons-vue'
/** ERP 收款单列表 */ /** ERP 收款单列表 */
defineOptions({ name: 'ErpPurchaseOrder' }) defineOptions({ name: 'ErpFinanceReceipt' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
@@ -302,6 +339,7 @@ const queryParams = reactive({
}) })
const queryFormRef = ref() // 搜索的表单 const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中 const exportLoading = ref(false) // 导出的加载中
const filterVisible = ref(false) // 筛选抽屉
const customerList = ref<CustomerVO[]>([]) // 客户列表 const customerList = ref<CustomerVO[]>([]) // 客户列表
const userList = ref<UserVO[]>([]) // 用户列表 const userList = ref<UserVO[]>([]) // 用户列表
const accountList = ref<AccountVO[]>([]) // 账户列表 const accountList = ref<AccountVO[]>([]) // 账户列表
@@ -329,10 +367,29 @@ const handleQuery = () => {
/** 重置按钮操作 */ /** 重置按钮操作 */
const resetQuery = () => { const resetQuery = () => {
queryFormRef.value.resetFields() queryFormRef.value?.resetFields()
handleQuery() handleQuery()
} }
/** 筛选确认 */
const handleFilterConfirm = () => {
filterVisible.value = false
handleQuery()
}
/** 快捷筛选 */
const handleQuickFilter = (status: number | undefined) => {
queryParams.status = status
queryParams.pageNo = 1
getList()
}
/** 移动端行点击 */
const handleRowClickMobile = (row: FinanceReceiptVO) => {
if (row.status === 20) openForm('detail', row.id)
else if (row.status === 10) openForm('update', row.id)
}
/** 添加/修改操作 */ /** 添加/修改操作 */
const formRef = ref() const formRef = ref()
const openForm = (type: string, id?: number) => { const openForm = (type: string, id?: number) => {
@@ -420,6 +477,30 @@ onMounted(async () => {
userList.value = await UserApi.getSimpleUserList() userList.value = await UserApi.getSimpleUserList()
accountList.value = await AccountApi.getAccountSimpleList() accountList.value = await AccountApi.getAccountSimpleList()
}) })
// TODO 芋艿:可优化功能:列表界面,支持导入
// TODO 芋艿:可优化功能:详情界面,支持打印
</script> </script>
<style lang="scss" scoped>
.mobile-receipt { padding: 12px; background: #f5f5f5; min-height: 100vh; }
.mobile-header { display: flex; gap: 8px; align-items: center; margin-bottom: 12px; }
.mobile-header__search { flex: 1; }
.mobile-header__actions { display: flex; gap: 4px; }
.mobile-header__quick-filter { display: flex; gap: 12px; margin: 8px 0; }
.quick-filter-item { padding: 4px 12px; font-size: 14px; border-radius: 20px; cursor: pointer; color: #909399; background: transparent; transition: all 0.2s; }
.quick-filter-item.active { color: #fff; background: #409eff; }
.mobile-list { display: flex; flex-direction: column; gap: 10px; }
.mobile-empty { padding: 40px 0; }
.mobile-card { background: #fff; border-radius: 10px; padding: 14px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
.mobile-card__header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.mobile-card__no { font-weight: 600; font-size: 15px; color: #303133; }
.mobile-card__body { font-size: 13px; }
.mobile-card__row { display: flex; justify-content: space-between; padding: 3px 0; }
.mobile-card__label { color: #909399; flex-shrink: 0; margin-right: 12px; }
.mobile-card__value { color: #606266; text-align: right; }
.mobile-card__nums { display: flex; justify-content: space-around; margin-top: 10px; padding: 10px 0; border-top: 1px solid #f0f0f0; border-bottom: 1px solid #f0f0f0; }
.mobile-card__num-item { text-align: center; }
.mobile-card__num-val { font-size: 15px; font-weight: 600; color: #303133; }
.mobile-card__num-val--price { color: #67c23a; }
.mobile-card__num-label { font-size: 11px; color: #909399; margin-top: 2px; }
.mobile-card__footer { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.mobile-pagination { margin-top: 12px; display: flex; justify-content: center; :deep(.el-pagination) { flex-wrap: wrap; justify-content: center; } }
</style>

View File

@@ -1,12 +1,31 @@
<template> <template>
<Dialog :title="dialogTitle" v-model="dialogVisible"> <!-- 移动端布局 -->
<el-form <el-drawer v-if="isMobile" v-model="dialogVisible" :title="dialogTitle" direction="rtl" size="100%" :close-on-press-escape="true" :destroy-on-close="true" :append-to-body="true" class="mobile-form-drawer">
ref="formRef" <div class="mobile-form" v-loading="formLoading">
:model="formData" <el-form ref="formRef" :model="formData" :rules="formRules" label-position="top">
:rules="formRules" <div class="mobile-form__section">
label-width="110px" <div class="mobile-form__section-title">应收单信息</div>
v-loading="formLoading" <el-form-item label="销售出库订单" prop="saleOrderNo"><el-input v-model="formData.saleOrderNo" readonly placeholder="请选择销售出库订单"><template #append><el-button @click="openSaleOutSelect"><Icon icon="ep:search" /></el-button></template></el-input></el-form-item>
> <el-form-item label="应收单号" prop="aroCode"><el-input v-model="formData.aroCode" placeholder="请输入应收单号" /></el-form-item>
<el-form-item label="客户" prop="customer"><el-input v-model="formData.customer" placeholder="请选择客户" /></el-form-item>
<el-form-item label="单据日期" prop="billDate"><el-date-picker v-model="formData.billDate" type="date" value-format="x" placeholder="选择单据日期" style="width:100%" /></el-form-item>
<el-form-item label="已收金额" prop="receiptPrice"><el-input v-model="formData.receiptPrice" placeholder="请输入已收金额" /></el-form-item>
<el-form-item label="总金额" prop="totalAmount"><el-input v-model="formData.totalAmount" placeholder="请输入总金额" /></el-form-item>
<el-form-item label="备注" prop="remark"><el-input v-model="formData.remark" placeholder="请输入备注" /></el-form-item>
</div>
</el-form>
</div>
<template #footer>
<div class="mobile-form__footer">
<el-button @click="dialogVisible = false" style="flex:1"> </el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading" style="flex:2"> </el-button>
</div>
</template>
</el-drawer>
<!-- PC端布局 -->
<Dialog v-else :title="dialogTitle" v-model="dialogVisible">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="110px" v-loading="formLoading">
<el-form-item label="销售出库订单" prop="saleOrderNo"> <el-form-item label="销售出库订单" prop="saleOrderNo">
<el-input v-model="formData.saleOrderNo" readonly placeholder="请选择销售出库订单"> <el-input v-model="formData.saleOrderNo" readonly placeholder="请选择销售出库订单">
<template #append> <template #append>
@@ -103,15 +122,20 @@
/> />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { ReceivableOrderApi, ReceivableOrder } from '@/api/erp/finance/receivableorder' import { ReceivableOrderApi, ReceivableOrder } from '@/api/erp/finance/receivableorder'
import SaleOutReceiptEnableList from '@/views/erp/sale/out/components/SaleOutReceiptEnableList.vue' import SaleOutReceiptEnableList from '@/views/erp/sale/out/components/SaleOutReceiptEnableList.vue'
import { SaleOutVO } from '@/api/erp/sale/out' import { SaleOutVO } from '@/api/erp/sale/out'
import { formatToDate } from '@/utils/dateUtil' import { formatToDate } from '@/utils/dateUtil'
import { SubjectApi, SubjectVO } from '@/api/erp/finance/subject' import { SubjectApi, SubjectVO } from '@/api/erp/finance/subject'
import { useWindowSize } from '@vueuse/core'
/** 应收单 表单 */ /** 应收单 表单 */
defineOptions({ name: 'ReceivableOrderForm' }) defineOptions({ name: 'ReceivableOrderForm' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗

View File

@@ -1,5 +1,31 @@
<template> <template>
<Dialog :title="dialogTitle" v-model="dialogVisible"> <!-- 移动端布局 -->
<el-drawer v-if="isMobile" v-model="dialogVisible" :title="dialogTitle" direction="rtl" size="100%" :close-on-press-escape="true" :destroy-on-close="true" :append-to-body="true" class="mobile-form-drawer">
<div class="mobile-form" v-loading="formLoading">
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="top">
<div class="mobile-form__section">
<div class="mobile-form__section-title">会计科目信息</div>
<el-form-item label="上级科目" prop="parentId"><el-tree-select v-model="formData.parentId" :data="subjectTree" placeholder="请选择上级科目" :props="{ label: 'name', value: 'id', children: 'children' }" check-strictly clearable style="width:100%" /></el-form-item>
<el-form-item label="科目编码" prop="code"><el-input v-model="formData.code" placeholder="请输入科目编码" /></el-form-item>
<el-form-item label="科目名称" prop="name"><el-input v-model="formData.name" placeholder="请输入科目名称" /></el-form-item>
<el-form-item label="科目类型" prop="type"><el-select v-model="formData.type" placeholder="请选择科目类型" style="width:100%"><el-option v-for="dict in getIntDictOptions(DICT_TYPE.ERP_FINANCE_SUBJECT_TYPE)" :key="dict.value" :label="dict.label" :value="dict.value" /></el-select></el-form-item>
<el-form-item label="余额方向" prop="direction"><el-select v-model="formData.direction" placeholder="请选择余额方向" style="width:100%"><el-option v-for="dict in getIntDictOptions(DICT_TYPE.ERP_FINANCE_SUBJECT_DIRECTION)" :key="dict.value" :label="dict.label" :value="dict.value" /></el-select></el-form-item>
<el-form-item label="状态" prop="status"><el-radio-group v-model="formData.status"><el-radio v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio></el-radio-group></el-form-item>
<el-form-item label="排序" prop="sort"><el-input-number v-model="formData.sort" :min="0" style="width:100%" /></el-form-item>
<el-form-item label="备注" prop="remark"><el-input v-model="formData.remark" type="textarea" placeholder="请输入备注" /></el-form-item>
</div>
</el-form>
</div>
<template #footer>
<div class="mobile-form__footer">
<el-button @click="dialogVisible = false" style="flex:1"> </el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading" style="flex:2"> </el-button>
</div>
</template>
</el-drawer>
<!-- PC端布局 -->
<Dialog v-else :title="dialogTitle" v-model="dialogVisible">
<el-form <el-form
ref="formRef" ref="formRef"
:model="formData" :model="formData"
@@ -68,12 +94,17 @@
</Dialog> </Dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { SubjectApi, SubjectVO } from '@/api/erp/finance/subject' import { SubjectApi, SubjectVO } from '@/api/erp/finance/subject'
import { useWindowSize } from '@vueuse/core'
/** ERP 会计科目 表单 */ /** ERP 会计科目 表单 */
defineOptions({ name: 'SubjectForm' }) defineOptions({ name: 'SubjectForm' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗

View File

@@ -1,21 +1,54 @@
<template> <template>
<Dialog :title="dialogTitle" v-model="dialogVisible"> <!-- 移动端使用抽屉 -->
<el-form <el-drawer
ref="formRef" v-if="isMobile"
:model="formData" v-model="dialogVisible"
:rules="formRules" :title="dialogTitle"
label-width="100px" direction="rtl"
v-loading="formLoading" size="100%"
> :close-on-press-escape="true"
:destroy-on-close="true"
:append-to-body="true"
class="mobile-form-drawer"
>
<div class="mobile-form" v-loading="formLoading">
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="top">
<div class="mobile-form-section">
<div class="mobile-form-section__title">基础信息</div>
<el-form-item label="上级分类" prop="parentId">
<el-tree-select v-model="formData.parentId" :data="productCategoryTree" :props="defaultProps" check-strictly default-expand-all placeholder="请选择上级分类" />
</el-form-item>
<el-form-item label="分类名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入分类名称" clearable />
</el-form-item>
<el-form-item label="分类编码" prop="code">
<el-input v-model="formData.code" placeholder="请输入分类编码" clearable />
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="formData.sort" placeholder="请输入排序" :precision="0" controls-position="right" />
</el-form-item>
</div>
<div class="mobile-form-section">
<div class="mobile-form-section__title">状态信息</div>
<el-form-item label="开启状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
</div>
</el-form>
<div class="mobile-form__footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
</div>
</div>
</el-drawer>
<!-- PC端使用对话框 -->
<Dialog v-else :title="dialogTitle" v-model="dialogVisible">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" v-loading="formLoading">
<el-form-item label="上级编号" prop="parentId"> <el-form-item label="上级编号" prop="parentId">
<el-tree-select <el-tree-select v-model="formData.parentId" :data="productCategoryTree" :props="defaultProps" check-strictly default-expand-all placeholder="请选择上级编号" />
v-model="formData.parentId"
:data="productCategoryTree"
:props="defaultProps"
check-strictly
default-expand-all
placeholder="请选择上级编号"
/>
</el-form-item> </el-form-item>
<el-form-item label="名称" prop="name"> <el-form-item label="名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入名称" /> <el-input v-model="formData.name" placeholder="请输入名称" />
@@ -28,13 +61,7 @@
</el-form-item> </el-form-item>
<el-form-item label="状态" prop="status"> <el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status"> <el-radio-group v-model="formData.status">
<el-radio <el-radio v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
</el-form> </el-form>
@@ -45,14 +72,19 @@
</Dialog> </Dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { ProductCategoryApi, ProductCategoryVO } from '@/api/erp/product/category' import { ProductCategoryApi, ProductCategoryVO } from '@/api/erp/product/category'
import { defaultProps, handleTree } from '@/utils/tree' import { defaultProps, handleTree } from '@/utils/tree'
import { CommonStatusEnum } from '@/utils/constants' import { CommonStatusEnum } from '@/utils/constants'
import { useWindowSize } from '@vueuse/core'
/** ERP 产品分类 表单 */ /** ERP 产品分类 表单 */
defineOptions({ name: 'ProductCategoryForm' }) defineOptions({ name: 'ProductCategoryForm' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
@@ -143,3 +175,54 @@ const getProductCategoryTree = async () => {
productCategoryTree.value.push(root) productCategoryTree.value.push(root)
} }
</script> </script>
<style lang="scss" scoped>
.mobile-form {
padding: 12px;
}
.mobile-form-section {
background: #fff;
border-radius: 10px;
padding: 14px;
margin-bottom: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.mobile-form-section__title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.mobile-form__footer {
position: sticky;
bottom: 0;
background: #fff;
padding: 12px 16px;
padding-bottom: calc(12px + constant(safe-area-inset-bottom));
padding-bottom: calc(12px + env(safe-area-inset-bottom));
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 12px;
z-index: 10;
margin: 0 -12px -12px;
.el-button {
flex: 1;
height: 40px;
font-size: 15px;
}
}
:deep(.el-form-item) {
margin-bottom: 16px;
}
:deep(.el-input),
:deep(.el-select),
:deep(.el-input-number),
:deep(.el-tree-select) {
width: 100% !important;
}
</style>

View File

@@ -1,144 +1,157 @@
<template> <template>
<doc-alert title="【产品】产品信息、分类、单位" url="https://doc.iocoder.cn/erp/product/" /> <!-- 移动端布局 -->
<div v-if="isMobile" class="mobile-product-category">
<!-- 搜索操作栏 -->
<div class="mobile-header">
<div class="mobile-header__search">
<el-input v-model="queryParams.name" placeholder="搜索分类名称" clearable @keyup.enter="handleQuery" :prefix-icon="Search" />
</div>
<div class="mobile-header__actions">
<el-button :icon="Filter" circle @click="filterVisible = true" />
<el-button type="primary" :icon="Plus" circle @click="openForm('create')" v-hasPermi="['erp:product-category:create']" />
</div>
</div>
<ContentWrap> <!-- 快捷操作 -->
<!-- 搜索工作栏 --> <div class="mobile-quick-actions">
<el-form <el-button size="small" type="success" plain @click="handleExport" :loading="exportLoading" v-hasPermi="['erp:product-category:export']">导出</el-button>
class="-mb-15px" <el-button size="small" type="danger" plain @click="toggleExpandAll">{{ isExpandAll ? '折叠' : '展开' }}</el-button>
:model="queryParams" </div>
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="分类名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入分类名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="开启状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择开启状态"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['erp:product-category:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['erp:product-category:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button type="danger" plain @click="toggleExpandAll">
<Icon icon="ep:sort" class="mr-5px" /> 展开/折叠
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 --> <!-- 卡片列表 -->
<ContentWrap> <div class="mobile-list" v-loading="loading">
<el-table <div v-if="list.length === 0 && !loading" class="mobile-empty">
v-loading="loading" <el-empty description="暂无产品分类" />
:data="list" </div>
:stripe="true" <div v-for="item in flatList" :key="item.id" class="mobile-card" @click="handleCardClick(item)" :style="{ marginLeft: (item.level || 0) * 12 + 'px' }">
:show-overflow-tooltip="true" <div class="mobile-card__header">
row-key="id" <span class="mobile-card__name">
:default-expand-all="isExpandAll" <span v-if="item.level > 0" class="mobile-card__indent"></span>
v-if="refreshTable" {{ item.name || '-' }}
@row-click="handleRowClick" </span>
> <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
<el-table-column label="编码" align="center" prop="code" /> </div>
<el-table-column label="名称" align="center" prop="name" /> <div class="mobile-card__body">
<el-table-column label="排序" align="center" prop="sort" /> <div class="mobile-card__row">
<el-table-column label="状态" align="center" prop="status"> <span class="mobile-card__label">编码</span>
<template #default="scope"> <span class="mobile-card__value">{{ item.code || '-' }}</span>
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" /> </div>
</template> <div class="mobile-card__row">
</el-table-column> <span class="mobile-card__label">排序</span>
<el-table-column <span class="mobile-card__value">{{ item.sort || '-' }}</span>
label="创建时间" </div>
align="center" <div class="mobile-card__row">
prop="createTime" <span class="mobile-card__label">创建时间</span>
:formatter="dateFormatter" <span class="mobile-card__value">{{ formatDate(item.createTime) }}</span>
width="180px" </div>
sortable </div>
/> <div class="mobile-card__footer">
<el-table-column label="操作" align="center"> <el-button size="small" type="primary" @click.stop="openForm('update', item.id)" v-hasPermi="['erp:product-category:update']">编辑</el-button>
<template #default="scope"> <el-button size="small" type="danger" @click.stop="handleDelete(item.id)" v-hasPermi="['erp:product-category:delete']">删除</el-button>
<el-button </div>
link </div>
type="primary" </div>
@click="openForm('update', scope.row.id)"
v-hasPermi="['erp:product-category:update']" <!-- 筛选抽屉 -->
> <el-drawer v-model="filterVisible" title="筛选条件" direction="btt" size="50%">
编辑 <el-form :model="queryParams" ref="queryFormRef" label-position="top">
<el-form-item label="开启状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择开启状态" clearable style="width:100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="resetQuery">重置</el-button>
<el-button type="primary" @click="handleFilterConfirm">确认筛选</el-button>
</template>
</el-drawer>
</div>
<!-- PC端布局 -->
<template v-else>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
<el-form-item label="分类名称" prop="name">
<el-input v-model="queryParams.name" placeholder="请输入分类名称" clearable @keyup.enter="handleQuery" class="!w-240px" />
</el-form-item>
<el-form-item label="开启状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择开启状态" clearable class="!w-240px">
<el-option v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button type="primary" plain @click="openForm('create')" v-hasPermi="['erp:product-category:create']">
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button> </el-button>
<el-button <el-button type="success" plain @click="handleExport" :loading="exportLoading" v-hasPermi="['erp:product-category:export']">
link <Icon icon="ep:download" class="mr-5px" /> 导出
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['erp:product-category:delete']"
>
删除
</el-button> </el-button>
</template> <el-button type="danger" plain @click="toggleExpandAll">
</el-table-column> <Icon icon="ep:sort" class="mr-5px" /> 展开/折叠
</el-table> </el-button>
<!-- 分页 --> </el-form-item>
<Pagination </el-form>
:total="total" </ContentWrap>
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize" <!-- 列表 -->
@pagination="getList" <ContentWrap>
/> <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" row-key="id" :default-expand-all="isExpandAll" v-if="refreshTable" @row-click="handleRowClick">
</ContentWrap> <el-table-column label="编码" align="center" prop="code" />
<el-table-column label="名称" align="center" prop="name" />
<el-table-column label="排序" align="center" prop="sort" />
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" :formatter="dateFormatter" width="180px" sortable />
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button link type="primary" @click="openForm('update', scope.row.id)" v-hasPermi="['erp:product-category:update']">编辑</el-button>
<el-button link type="danger" @click="handleDelete(scope.row.id)" v-hasPermi="['erp:product-category:delete']">删除</el-button>
</template>
</el-table-column>
</el-table>
</ContentWrap>
</template>
<!-- 表单弹窗添加/修改 --> <!-- 表单弹窗添加/修改 -->
<ProductCategoryForm ref="formRef" @success="getList" /> <ProductCategoryForm ref="formRef" @success="getList" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import { Search, Filter, Plus } from '@element-plus/icons-vue'
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime' import { dateFormatter } from '@/utils/formatTime'
import { handleTree } from '@/utils/tree' import { handleTree } from '@/utils/tree'
import download from '@/utils/download' import download from '@/utils/download'
import { ProductCategoryApi, ProductCategoryVO } from '@/api/erp/product/category' import { ProductCategoryApi, ProductCategoryVO } from '@/api/erp/product/category'
import ProductCategoryForm from './ProductCategoryForm.vue' import ProductCategoryForm from './ProductCategoryForm.vue'
import { useWindowSize } from '@vueuse/core'
/** ERP 产品分类 列表 */ /** ERP 产品分类 列表 */
defineOptions({ name: 'ErpProductCategory' }) defineOptions({ name: 'ErpProductCategory' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中 const loading = ref(true) // 列表的加载中
const list = ref<ProductCategoryVO[]>([]) // 列表的数据 const list = ref<ProductCategoryVO[]>([]) // 列表的数据
const flatList = ref<ProductCategoryVO[]>([]) // 扁平化列表
const filterVisible = ref(false) // 筛选抽屉
const queryParams = reactive({ const queryParams = reactive({
name: undefined, name: undefined,
status: undefined status: undefined
@@ -152,11 +165,27 @@ const getList = async () => {
try { try {
const data = await ProductCategoryApi.getProductCategoryList(queryParams) const data = await ProductCategoryApi.getProductCategoryList(queryParams)
list.value = handleTree(data, 'id', 'parentId') list.value = handleTree(data, 'id', 'parentId')
flattenTree()
} finally { } finally {
loading.value = false loading.value = false
} }
} }
/** 扁平化树形结构 */
const flattenTree = () => {
const result: any[] = []
const flatten = (nodes: any[], level = 0) => {
nodes.forEach(node => {
result.push({ ...node, level })
if (node.children && node.children.length > 0 && isExpandAll.value) {
flatten(node.children, level + 1)
}
})
}
flatten(list.value)
flatList.value = result
}
/** 搜索按钮操作 */ /** 搜索按钮操作 */
const handleQuery = () => { const handleQuery = () => {
queryParams.pageNo = 1 queryParams.pageNo = 1
@@ -165,7 +194,13 @@ const handleQuery = () => {
/** 重置按钮操作 */ /** 重置按钮操作 */
const resetQuery = () => { const resetQuery = () => {
queryFormRef.value.resetFields() queryFormRef.value?.resetFields()
handleQuery()
}
/** 筛选确认 */
const handleFilterConfirm = () => {
filterVisible.value = false
handleQuery() handleQuery()
} }
@@ -175,10 +210,16 @@ const openForm = (type: string, id?: number) => {
formRef.value.open(type, id) formRef.value.open(type, id)
} }
/** 行点击操作 */ /** 卡片点击 */
const handleRowClick = (row: ProductCategoryVO) => { const handleCardClick = (row: ProductCategoryVO) => {
openForm('update', row.id) openForm('update', row.id)
} }
/** 格式化日期 */
const formatDate = (date: any) => {
if (!date) return '-'
return dateFormatter({ cellValue: date })
}
/** 删除按钮操作 */ /** 删除按钮操作 */
const handleDelete = async (id: number) => { const handleDelete = async (id: number) => {
try { try {
@@ -209,16 +250,53 @@ const handleExport = async () => {
/** 展开/折叠操作 */ /** 展开/折叠操作 */
const isExpandAll = ref(true) // 是否展开,默认全部展开 const isExpandAll = ref(true) // 是否展开,默认全部展开
const refreshTable = ref(true) // 重新渲染表格状态 const refreshTable = ref(true) // PC端重新渲染表格状态
const toggleExpandAll = async () => { const toggleExpandAll = async () => {
refreshTable.value = false
isExpandAll.value = !isExpandAll.value isExpandAll.value = !isExpandAll.value
// 移动端
flattenTree()
// PC端
refreshTable.value = false
await nextTick() await nextTick()
refreshTable.value = true refreshTable.value = true
} }
/** PC端行点击操作 */
const handleRowClick = (row: ProductCategoryVO) => {
openForm('update', row.id)
}
/** 初始化 **/ /** 初始化 **/
onMounted(() => { onMounted(() => {
getList() getList()
}) })
</script> </script>
<style lang="scss" scoped>
.mobile-product-category {
padding: 12px;
background: #f5f5f5;
min-height: 100vh;
}
.mobile-header {
display: flex; gap: 8px; align-items: center; margin-bottom: 12px;
&__search { flex: 1; }
&__actions { display: flex; gap: 4px; flex-shrink: 0; }
}
.mobile-quick-actions {
display: flex; gap: 8px; margin-bottom: 12px;
}
.mobile-list { display: flex; flex-direction: column; gap: 10px; }
.mobile-empty { padding: 40px 0; }
.mobile-card {
background: #fff; border-radius: 10px; padding: 14px; box-shadow: 0 1px 4px rgba(0,0,0,0.06);
&__header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
&__name { font-weight: 600; font-size: 15px; color: #303133; }
&__indent { color: #909399; margin-right: 4px; }
&__body { font-size: 13px; }
&__row { display: flex; justify-content: space-between; padding: 3px 0; }
&__label { color: #909399; flex-shrink: 0; margin-right: 12px; }
&__value { color: #606266; text-align: right; }
&__footer { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; padding-top: 10px; border-top: 1px solid #f0f0f0; }
}
</style>

View File

@@ -1,149 +1,66 @@
<!-- ERP 产品的新增/修改 -->
<template> <template>
<Dialog :title="dialogTitle" v-model="dialogVisible"> <!-- 移动端使用抽屉 -->
<el-form <el-drawer v-if="isMobile" v-model="dialogVisible" :title="dialogTitle" direction="rtl" size="100%" :close-on-press-escape="true" :destroy-on-close="true" :append-to-body="true" class="mobile-form-drawer">
ref="formRef" <div class="mobile-form" v-loading="formLoading">
:model="formData" <el-form ref="formRef" :model="formData" :rules="formRules" label-position="top">
:rules="formRules" <div class="mobile-form-section">
label-width="100px" <div class="mobile-form-section__title">基础信息</div>
v-loading="formLoading" <el-form-item label="产品名称" prop="name"><el-input v-model="formData.name" placeholder="请输入产品名称" clearable /></el-form-item>
> <el-form-item label="产品条码" prop="barCode"><el-input v-model="formData.barCode" placeholder="请输入产品条码" clearable /></el-form-item>
<el-form-item label="产品分类" prop="categoryId"><el-tree-select v-model="formData.categoryId" :data="categoryList" :props="defaultProps" check-strictly default-expand-all placeholder="请选择产品分类" /></el-form-item>
<el-form-item label="产品单位" prop="unitId"><el-select v-model="formData.unitId" clearable placeholder="请选择产品单位"><el-option v-for="unit in unitList" :key="unit.id" :label="unit.name" :value="unit.id" /></el-select></el-form-item>
<el-form-item label="产品规格" prop="standard"><el-input v-model="formData.standard" placeholder="请输入产品规格" clearable /></el-form-item>
</div>
<div class="mobile-form-section">
<div class="mobile-form-section__title">价格信息</div>
<el-form-item label="采购价格(元)" prop="purchasePrice"><el-input-number v-model="formData.purchasePrice" placeholder="请输入采购价格" :min="0" :precision="4" controls-position="right" /></el-form-item>
<el-form-item label="销售价格(元)" prop="salePrice"><el-input-number v-model="formData.salePrice" placeholder="请输入销售价格" :min="0" :precision="4" controls-position="right" /></el-form-item>
<el-form-item label="货值(元)" prop="minPrice"><el-input-number v-model="formData.minPrice" placeholder="请输入最低价格" :min="0" :precision="4" controls-position="right" /></el-form-item>
</div>
<div class="mobile-form-section">
<div class="mobile-form-section__title">库存信息</div>
<el-form-item label="库存预警数量" prop="stockAlertCount"><el-input-number v-model="formData.stockAlertCount" placeholder="请输入库存预警数量" :min="0" :precision="2" controls-position="right" /></el-form-item>
<el-form-item label="保质期天数" prop="expiryDay"><el-input-number v-model="formData.expiryDay" placeholder="请输入保质期天数" :min="0" :precision="0" controls-position="right" /></el-form-item>
<el-form-item label="重量kg" prop="weight"><el-input-number v-model="formData.weight" placeholder="请输入重量" :min="0" controls-position="right" /></el-form-item>
</div>
<div class="mobile-form-section">
<div class="mobile-form-section__title">供应商信息</div>
<el-form-item label="供应商" prop="supplierId"><el-select v-model="formData.supplierId" clearable filterable placeholder="请选择供应商"><el-option v-for="item in supplierList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item>
</div>
<div class="mobile-form-section">
<div class="mobile-form-section__title">其他信息</div>
<el-form-item label="备注" prop="remark"><el-input type="textarea" v-model="formData.remark" placeholder="请输入备注" :rows="3" /></el-form-item>
</div>
<div class="mobile-form-section">
<div class="mobile-form-section__title">状态信息</div>
<el-form-item label="开启状态" prop="status"><el-radio-group v-model="formData.status"><el-radio v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio></el-radio-group></el-form-item>
</div>
</el-form>
<div class="mobile-form__footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
</div>
</div>
</el-drawer>
<!-- PC端使用对话框 -->
<Dialog v-else :title="dialogTitle" v-model="dialogVisible">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" v-loading="formLoading">
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12"><el-form-item label="名称" prop="name"><el-input v-model="formData.name" placeholder="请输入名称" /></el-form-item></el-col>
<el-form-item label="名称" prop="name"> <el-col :span="12"><el-form-item label="条码" prop="barCode"><el-input v-model="formData.barCode" placeholder="请输入条码" /></el-form-item></el-col>
<el-input v-model="formData.name" placeholder="请输入名称" /> <el-col :span="12"><el-form-item label="分类" prop="categoryId"><el-tree-select v-model="formData.categoryId" :data="categoryList" :props="defaultProps" check-strictly default-expand-all placeholder="请选择分类" class="w-1/1" /></el-form-item></el-col>
</el-form-item> <el-col :span="12"><el-form-item label="单位" prop="unitId"><el-select v-model="formData.unitId" clearable placeholder="请选择单位" class="w-1/1"><el-option v-for="unit in unitList" :key="unit.id" :label="unit.name" :value="unit.id" /></el-select></el-form-item></el-col>
</el-col> <el-col :span="12"><el-form-item label="状态" prop="status"><el-radio-group v-model="formData.status"><el-radio v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio></el-radio-group></el-form-item></el-col>
<el-col :span="12"> <el-col :span="12"><el-form-item label="规格" prop="standard"><el-input v-model="formData.standard" placeholder="请输入规格" /></el-form-item></el-col>
<el-form-item label="条码" prop="barCode"> <el-col :span="12"><el-form-item label="保质期天数" prop="expiryDay"><el-input-number v-model="formData.expiryDay" placeholder="请输入保质期天数" :min="0" :precision="0" class="!w-1/1" /></el-form-item></el-col>
<el-input v-model="formData.barCode" placeholder="请输入条码" /> <el-col :span="12"><el-form-item label="重量kg" prop="weight"><el-input-number v-model="formData.weight" placeholder="请输入重量kg" :min="0" class="!w-1/1" /></el-form-item></el-col>
</el-form-item> <el-col :span="12"><el-form-item label="采购价格" prop="purchasePrice"><el-input-number v-model="formData.purchasePrice" placeholder="请输入采购价格,单位:元" :min="0" :precision="4" class="!w-1/1" /></el-form-item></el-col>
</el-col> <el-col :span="12"><el-form-item label="销售价格" prop="salePrice"><el-input-number v-model="formData.salePrice" placeholder="请输入销售价格,单位:元" :min="0" :precision="4" class="!w-1/1" /></el-form-item></el-col>
<el-col :span="12"> <el-col :span="12"><el-form-item label="货值" prop="minPrice"><el-input-number v-model="formData.minPrice" placeholder="请输入最低价格,单位:元" :min="0" :precision="4" class="!w-1/1" /></el-form-item></el-col>
<el-form-item label="分类" prop="categoryId"> <el-col :span="12"><el-form-item label="库存预警" prop="stockAlertCount"><el-input-number v-model="formData.stockAlertCount" placeholder="请输入库存预警数量" :min="0" :precision="2" class="!w-1/1" /></el-form-item></el-col>
<el-tree-select <el-col :span="12"><el-form-item label="供应商" prop="supplierId"><el-select v-model="formData.supplierId" clearable filterable placeholder="请选择供应商" class="w-1/1"><el-option v-for="item in supplierList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></el-col>
v-model="formData.categoryId" <el-col :span="24"><el-form-item label="备注" prop="remark"><el-input type="textarea" v-model="formData.remark" placeholder="请输入备注" /></el-form-item></el-col>
:data="categoryList"
:props="defaultProps"
check-strictly
default-expand-all
placeholder="请选择分类"
class="w-1/1"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="单位" prop="unitId">
<el-select v-model="formData.unitId" clearable placeholder="请选择单位" class="w-1/1">
<el-option
v-for="unit in unitList"
:key="unit.id"
:label="unit.name"
:value="unit.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="规格" prop="standard">
<el-input v-model="formData.standard" placeholder="请输入规格" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="保质期天数" prop="expiryDay">
<el-input-number
v-model="formData.expiryDay"
placeholder="请输入保质期天数"
:min="0"
:precision="0"
class="!w-1/1"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="重量kg" prop="weight">
<el-input-number
v-model="formData.weight"
placeholder="请输入重量kg"
:min="0"
class="!w-1/1"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="采购价格" prop="purchasePrice">
<el-input-number
v-model="formData.purchasePrice"
placeholder="请输入采购价格,单位:元"
:min="0"
:precision="4"
class="!w-1/1"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="销售价格" prop="salePrice">
<el-input-number
v-model="formData.salePrice"
placeholder="请输入销售价格,单位:元"
:min="0"
:precision="4"
class="!w-1/1"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="货值" prop="minPrice">
<el-input-number
v-model="formData.minPrice"
placeholder="请输入最低价格,单位:元"
:min="0"
:precision="4"
class="!w-1/1"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="库存预警" prop="stockAlertCount">
<el-input-number
v-model="formData.stockAlertCount"
placeholder="请输入库存预警数量"
:min="0"
:precision="2"
class="!w-1/1"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="供应商" prop="supplierId">
<el-select v-model="formData.supplierId" clearable filterable placeholder="请选择供应商" class="w-1/1">
<el-option
v-for="item in supplierList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input type="textarea" v-model="formData.remark" placeholder="请输入备注" />
</el-form-item>
</el-col>
</el-row> </el-row>
</el-form> </el-form>
<template #footer> <template #footer>
@@ -153,6 +70,7 @@
</Dialog> </Dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import { ProductApi, ProductVO } from '@/api/erp/product/product' import { ProductApi, ProductVO } from '@/api/erp/product/product'
import { ProductCategoryApi, ProductCategoryVO } from '@/api/erp/product/category' import { ProductCategoryApi, ProductCategoryVO } from '@/api/erp/product/category'
import { ProductUnitApi, ProductUnitVO } from '@/api/erp/product/unit' import { ProductUnitApi, ProductUnitVO } from '@/api/erp/product/unit'
@@ -160,10 +78,14 @@ import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier'
import { CommonStatusEnum } from '@/utils/constants' import { CommonStatusEnum } from '@/utils/constants'
import { defaultProps, handleTree } from '@/utils/tree' import { defaultProps, handleTree } from '@/utils/tree'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { useWindowSize } from '@vueuse/core'
/** ERP 产品 表单 */ /** ERP 产品 表单 */
defineOptions({ name: 'ProductForm' }) defineOptions({ name: 'ProductForm' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
@@ -269,3 +191,54 @@ const resetForm = () => {
formRef.value?.resetFields() formRef.value?.resetFields()
} }
</script> </script>
<style lang="scss" scoped>
.mobile-form {
padding: 12px;
}
.mobile-form-section {
background: #fff;
border-radius: 10px;
padding: 14px;
margin-bottom: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.mobile-form-section__title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.mobile-form__footer {
position: sticky;
bottom: 0;
background: #fff;
padding: 12px 16px;
padding-bottom: calc(12px + constant(safe-area-inset-bottom));
padding-bottom: calc(12px + env(safe-area-inset-bottom));
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 12px;
z-index: 10;
margin: 0 -12px -12px;
.el-button {
flex: 1;
height: 40px;
font-size: 15px;
}
}
:deep(.el-form-item) {
margin-bottom: 16px;
}
:deep(.el-input),
:deep(.el-select),
:deep(.el-input-number),
:deep(.el-tree-select) {
width: 100% !important;
}
</style>

View File

@@ -1,156 +1,128 @@
<!-- ERP 产品列表 -->
<template> <template>
<doc-alert title="【产品】产品信息、分类、单位" url="https://doc.iocoder.cn/erp/product/" /> <!-- 移动端布局 -->
<div v-if="isMobile" class="mobile-product">
<div class="mobile-header">
<div class="mobile-header__search">
<el-input v-model="queryParams.name" placeholder="搜索产品名称" clearable @keyup.enter="handleQuery" :prefix-icon="Search" />
</div>
<div class="mobile-header__actions">
<el-button :icon="Filter" circle @click="filterVisible = true" />
<el-button type="primary" :icon="Plus" circle @click="openForm('create')" v-hasPermi="['erp:product:create']" />
</div>
</div>
<div class="mobile-quick-actions">
<el-button size="small" type="success" plain @click="handleExport" :loading="exportLoading" v-hasPermi="['erp:product:export']">导出</el-button>
</div>
<div class="mobile-list" v-loading="loading">
<div v-if="list.length === 0 && !loading" class="mobile-empty"><el-empty description="暂无产品" /></div>
<div v-for="item in list" :key="item.id" class="mobile-card" @click="handleCardClick(item)">
<div class="mobile-card__header">
<span class="mobile-card__name">{{ item.name || '-' }}</span>
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
</div>
<div class="mobile-card__body">
<div class="mobile-card__row"><span class="mobile-card__label">条码</span><span class="mobile-card__value">{{ item.barCode || '-' }}</span></div>
<div class="mobile-card__row"><span class="mobile-card__label">规格</span><span class="mobile-card__value">{{ item.standard || '-' }}</span></div>
<div class="mobile-card__row"><span class="mobile-card__label">分类</span><span class="mobile-card__value">{{ item.categoryName || '-' }}</span></div>
<div class="mobile-card__row"><span class="mobile-card__label">单位</span><span class="mobile-card__value">{{ item.unitName || '-' }}</span></div>
<div class="mobile-card__row"><span class="mobile-card__label">供应商</span><span class="mobile-card__value">{{ item.supplierName || '-' }}</span></div>
<div class="mobile-card__row"><span class="mobile-card__label">货值</span><span class="mobile-card__value">{{ formatPrice(item.minPrice) }}</span></div>
</div>
<div class="mobile-card__footer">
<el-button size="small" type="primary" @click.stop="openForm('update', item.id)" v-hasPermi="['erp:product:update']" :disabled="isProtectedProduct(item.name)">编辑</el-button>
<el-button size="small" type="danger" @click.stop="handleDelete(item.id)" v-hasPermi="['erp:product:delete']" :disabled="isProtectedProduct(item.name)">删除</el-button>
</div>
</div>
</div>
<div class="mobile-pagination" v-if="total > 0">
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" :page-sizes="[10, 20]" layout="total, prev, pager, next" :pager-count="5" @pagination="getList" />
</div>
<el-drawer v-model="filterVisible" title="筛选条件" direction="btt" size="50%">
<el-form :model="queryParams" ref="queryFormRef" label-position="top">
<el-form-item label="产品分类" prop="categoryId">
<el-tree-select v-model="queryParams.categoryId" :data="categoryList" :props="defaultProps" check-strictly default-expand-all placeholder="请选择分类" style="width:100%" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="resetQuery">重置</el-button>
<el-button type="primary" @click="handleFilterConfirm">确认筛选</el-button>
</template>
</el-drawer>
</div>
<ContentWrap> <!-- PC端布局 -->
<!-- 搜索工作栏 --> <template v-else>
<el-form <ContentWrap>
class="-mb-15px" <el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
:model="queryParams" <el-form-item label="名称" prop="name">
ref="queryFormRef" <el-input v-model="queryParams.name" placeholder="请输入名称" clearable @keyup.enter="handleQuery" class="!w-240px" />
:inline="true" </el-form-item>
label-width="68px" <el-form-item label="分类" prop="categoryId">
> <el-tree-select v-model="queryParams.categoryId" :data="categoryList" :props="defaultProps" check-strictly default-expand-all placeholder="请输入分类" class="!w-240px" />
<el-form-item label="名称" prop="name"> </el-form-item>
<el-input <el-form-item>
v-model="queryParams.name" <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
placeholder="请输入名称" <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
clearable <el-button type="primary" plain @click="openForm('create')" v-hasPermi="['erp:product:create']"><Icon icon="ep:plus" class="mr-5px" /> 新增</el-button>
@keyup.enter="handleQuery" <el-button type="success" plain @click="handleExport" :loading="exportLoading" v-hasPermi="['erp:product:export']"><Icon icon="ep:download" class="mr-5px" /> 导出</el-button>
class="!w-240px" </el-form-item>
/> </el-form>
</el-form-item> </ContentWrap>
<el-form-item label="分类" prop="categoryId"> <ContentWrap>
<el-tree-select <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" @row-click="handleRowClick">
v-model="queryParams.categoryId" <el-table-column label="条码" align="center" prop="barCode" />
:data="categoryList" <el-table-column label="名称" align="center" prop="name" />
:props="defaultProps" <el-table-column label="规格" align="center" prop="standard" />
check-strictly <el-table-column label="分类" align="center" prop="categoryName" />
default-expand-all <el-table-column label="单位" align="center" prop="unitName" />
placeholder="请输入分类" <el-table-column label="供应商" align="center" prop="supplierName" />
class="!w-240px" <el-table-column label="货值" align="center" prop="minPrice" :formatter="erpPriceTableColumnFormatter" />
/> <el-table-column label="库存预警" align="center" prop="stockAlertCount" />
</el-form-item> <el-table-column label="状态" align="center" prop="status">
<el-form-item> <template #default="scope"><dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" /></template>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> </el-table-column>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> <el-table-column label="创建时间" align="center" prop="createTime" :formatter="dateFormatter" width="180px" sortable />
<el-button <el-table-column label="操作" align="center" width="110">
type="primary" <template #default="scope">
plain <el-button link type="primary" @click="openForm('update', scope.row.id)" v-hasPermi="['erp:product:update']" :disabled="isProtectedProduct(scope.row.name)">编辑</el-button>
@click="openForm('create')" <el-button link type="danger" @click="handleDelete(scope.row.id)" v-hasPermi="['erp:product:delete']" :disabled="isProtectedProduct(scope.row.name)">删除</el-button>
v-hasPermi="['erp:product:create']" </template>
> </el-table-column>
<Icon icon="ep:plus" class="mr-5px" /> 新增 </el-table>
</el-button> <Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" />
<el-button </ContentWrap>
type="success" </template>
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['erp:product:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" @row-click="handleRowClick">
<el-table-column label="条码" align="center" prop="barCode" />
<el-table-column label="名称" align="center" prop="name" />
<el-table-column label="规格" align="center" prop="standard" />
<el-table-column label="分类" align="center" prop="categoryName" />
<el-table-column label="单位" align="center" prop="unitName" />
<el-table-column label="供应商" align="center" prop="supplierName" />
<!-- <el-table-column-->
<!-- label="采购价格"-->
<!-- align="center"-->
<!-- prop="purchasePrice"-->
<!-- :formatter="erpPriceTableColumnFormatter"-->
<!-- />-->
<!-- <el-table-column-->
<!-- label="销售价格"-->
<!-- align="center"-->
<!-- prop="salePrice"-->
<!-- :formatter="erpPriceTableColumnFormatter"-->
<!-- />-->
<el-table-column
label="货值"
align="center"
prop="minPrice"
:formatter="erpPriceTableColumnFormatter"
/>
<el-table-column label="库存预警" align="center" prop="stockAlertCount" />
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
sortable
/>
<el-table-column label="操作" align="center" width="110">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['erp:product:update']"
:disabled="scope.row.name === '番茄' || scope.row.name === '甜菊糖' || scope.row.name === '番茄酱' ||scope.row.name === '甜叶菊'"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['erp:product:delete']"
:disabled="scope.row.name === '番茄' || scope.row.name === '甜菊糖' || scope.row.name === '番茄酱' ||scope.row.name === '甜叶菊'"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 --> <!-- 表单弹窗添加/修改 -->
<ProductForm ref="formRef" @success="getList" /> <ProductForm ref="formRef" @success="getList" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import { Search, Filter, Plus } from '@element-plus/icons-vue'
import { dateFormatter } from '@/utils/formatTime' import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download' import download from '@/utils/download'
import { ProductApi, ProductVO } from '@/api/erp/product/product' import { ProductApi, ProductVO } from '@/api/erp/product/product'
import { ProductCategoryApi, ProductCategoryVO } from '@/api/erp/product/category' import { ProductCategoryApi, ProductCategoryVO } from '@/api/erp/product/category'
import ProductForm from './ProductForm.vue' import ProductForm from './ProductForm.vue'
import { DICT_TYPE } from '@/utils/dict' import { DICT_TYPE } from '@/utils/dict'
import { useWindowSize } from '@vueuse/core'
import { defaultProps, handleTree } from '@/utils/tree' import { defaultProps, handleTree } from '@/utils/tree'
import { erpPriceTableColumnFormatter } from '@/utils' import { erpPriceTableColumnFormatter } from '@/utils'
/** ERP 产品列表 */ /** ERP 产品列表 */
defineOptions({ name: 'ErpProduct' }) defineOptions({ name: 'ErpProduct' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中 const loading = ref(true) // 列表的加载中
const list = ref<ProductVO[]>([]) // 列表的数据 const list = ref<ProductVO[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数 const total = ref(0) // 列表的总页数
const filterVisible = ref(false) // 筛选抽屉
const queryParams = reactive({ const queryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
@@ -181,16 +153,37 @@ const handleQuery = () => {
/** 重置按钮操作 */ /** 重置按钮操作 */
const resetQuery = () => { const resetQuery = () => {
queryFormRef.value.resetFields() queryFormRef.value?.resetFields()
handleQuery() handleQuery()
} }
/** 行点击操作 */
/** 筛选确认 */
const handleFilterConfirm = () => {
filterVisible.value = false
handleQuery()
}
/** 判断是否为受保护产品 */
const isProtectedProduct = (name: string) => {
return name === '番茄' || name === '甜菊糖' || name === '番茄酱' || name === '甜叶菊'
}
/** 卡片点击(移动端) */
const handleCardClick = (row: ProductVO) => {
if (isProtectedProduct(row.name)) return
openForm('update', row.id)
}
/** 行点击(PC端) */
const handleRowClick = (row: ProductVO) => { const handleRowClick = (row: ProductVO) => {
if (row.name === '番茄' || row.name === '甜菊糖' || row.name === '番茄酱' || row.name === '甜叶菊'){ if (isProtectedProduct(row.name)) return
return; openForm('update', row.id)
}else { }
openForm('update', row.id)
} /** 格式化价格 */
const formatPrice = (price: any) => {
if (!price) return '-'
return erpPriceTableColumnFormatter({ cellValue: price })
} }
/** 添加/修改操作 */ /** 添加/修改操作 */
const formRef = ref() const formRef = ref()
@@ -234,3 +227,35 @@ onMounted(async () => {
categoryList.value = handleTree(categoryData, 'id', 'parentId') categoryList.value = handleTree(categoryData, 'id', 'parentId')
}) })
</script> </script>
<style lang="scss" scoped>
.mobile-product {
padding: 12px;
background: #f5f5f5;
min-height: 100vh;
}
.mobile-header {
display: flex; gap: 8px; align-items: center; margin-bottom: 12px;
&__search { flex: 1; }
&__actions { display: flex; gap: 4px; flex-shrink: 0; }
}
.mobile-quick-actions {
display: flex; gap: 8px; margin-bottom: 12px;
}
.mobile-list { display: flex; flex-direction: column; gap: 10px; }
.mobile-empty { padding: 40px 0; }
.mobile-card {
background: #fff; border-radius: 10px; padding: 14px; box-shadow: 0 1px 4px rgba(0,0,0,0.06);
&__header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
&__name { font-weight: 600; font-size: 15px; color: #303133; }
&__body { font-size: 13px; }
&__row { display: flex; justify-content: space-between; padding: 3px 0; }
&__label { color: #909399; flex-shrink: 0; margin-right: 12px; }
&__value { color: #606266; text-align: right; }
&__footer { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; padding-top: 10px; border-top: 1px solid #f0f0f0; }
}
.mobile-pagination {
margin-top: 12px; display: flex; justify-content: center;
:deep(.el-pagination) { flex-wrap: wrap; justify-content: center; }
}
</style>

View File

@@ -1,26 +1,29 @@
<template> <template>
<Dialog :title="dialogTitle" v-model="dialogVisible"> <!-- 移动端使用抽屉 -->
<el-form <el-drawer v-if="isMobile" v-model="dialogVisible" :title="dialogTitle" direction="rtl" size="100%" :close-on-press-escape="true" :destroy-on-close="true" :append-to-body="true" class="mobile-form-drawer">
ref="formRef" <div class="mobile-form" v-loading="formLoading">
:model="formData" <el-form ref="formRef" :model="formData" :rules="formRules" label-position="top">
:rules="formRules" <div class="mobile-form-section">
label-width="100px" <div class="mobile-form-section__title">基础信息</div>
v-loading="formLoading" <el-form-item label="单位名称" prop="name"><el-input v-model="formData.name" placeholder="请输入单位名称" clearable /></el-form-item>
> </div>
<el-form-item label="单位名字" prop="name"> <div class="mobile-form-section">
<el-input v-model="formData.name" placeholder="请输入单位名字" /> <div class="mobile-form-section__title">状态信息</div>
</el-form-item> <el-form-item label="开启状态" prop="status"><el-radio-group v-model="formData.status"><el-radio v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio></el-radio-group></el-form-item>
<el-form-item label="单位状态" prop="status"> </div>
<el-radio-group v-model="formData.status"> </el-form>
<el-radio <div class="mobile-form__footer">
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" <el-button @click="dialogVisible = false"> </el-button>
:key="dict.value" <el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
:value="dict.value" </div>
> </div>
{{ dict.label }} </el-drawer>
</el-radio>
</el-radio-group> <!-- PC端使用对话框 -->
</el-form-item> <Dialog v-else :title="dialogTitle" v-model="dialogVisible">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" v-loading="formLoading">
<el-form-item label="单位名字" prop="name"><el-input v-model="formData.name" placeholder="请输入单位名字" /></el-form-item>
<el-form-item label="单位状态" prop="status"><el-radio-group v-model="formData.status"><el-radio v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio></el-radio-group></el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button> <el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
@@ -29,13 +32,18 @@
</Dialog> </Dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import { ProductUnitApi } from '@/api/erp/product/unit' import { ProductUnitApi } from '@/api/erp/product/unit'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { CommonStatusEnum } from '@/utils/constants' import { CommonStatusEnum } from '@/utils/constants'
import { useWindowSize } from '@vueuse/core'
/** ERP 产品单位表单 */ /** ERP 产品单位表单 */
defineOptions({ name: 'ProductUnitForm' }) defineOptions({ name: 'ProductUnitForm' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
@@ -106,3 +114,52 @@ const resetForm = () => {
formRef.value?.resetFields() formRef.value?.resetFields()
} }
</script> </script>
<style lang="scss" scoped>
.mobile-form {
padding: 12px;
}
.mobile-form-section {
background: #fff;
border-radius: 10px;
padding: 14px;
margin-bottom: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.mobile-form-section__title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.mobile-form__footer {
position: sticky;
bottom: 0;
background: #fff;
padding: 12px 16px;
padding-bottom: calc(12px + constant(safe-area-inset-bottom));
padding-bottom: calc(12px + env(safe-area-inset-bottom));
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 12px;
z-index: 10;
margin: 0 -12px -12px;
.el-button {
flex: 1;
height: 40px;
font-size: 15px;
}
}
:deep(.el-form-item) {
margin-bottom: 16px;
}
:deep(.el-input),
:deep(.el-select) {
width: 100% !important;
}
</style>

View File

@@ -1,130 +1,91 @@
<template> <template>
<doc-alert title="【产品】产品信息、分类、单位" url="https://doc.iocoder.cn/erp/product/" /> <!-- 移动端布局 -->
<div v-if="isMobile" class="mobile-product-unit">
<div class="mobile-header">
<div class="mobile-header__search"><el-input v-model="queryParams.name" placeholder="搜索单位名称" clearable @keyup.enter="handleQuery" :prefix-icon="Search" /></div>
<div class="mobile-header__actions">
<el-button :icon="Filter" circle @click="filterVisible = true" />
<el-button type="primary" :icon="Plus" circle @click="openForm('create')" v-hasPermi="['erp:product-unit:create']" />
</div>
</div>
<div class="mobile-quick-actions"><el-button size="small" type="success" plain @click="handleExport" :loading="exportLoading" v-hasPermi="['erp:product-unit:export']">导出</el-button></div>
<div class="mobile-list" v-loading="loading">
<div v-if="list.length === 0 && !loading" class="mobile-empty"><el-empty description="暂无产品单位" /></div>
<div v-for="item in list" :key="item.id" class="mobile-card" @click="handleCardClick(item)">
<div class="mobile-card__header"><span class="mobile-card__name">{{ item.name || '-' }}</span><dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" /></div>
<div class="mobile-card__body"><div class="mobile-card__row"><span class="mobile-card__label">创建时间</span><span class="mobile-card__value">{{ formatDate(item.createTime) }}</span></div></div>
<div class="mobile-card__footer">
<el-button size="small" type="primary" @click.stop="openForm('update', item.id)" v-hasPermi="['erp:product-unit:update']">编辑</el-button>
<el-button size="small" type="danger" @click.stop="handleDelete(item.id)" v-hasPermi="['erp:product-unit:delete']">删除</el-button>
</div>
</div>
</div>
<div class="mobile-pagination" v-if="total > 0"><Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" :page-sizes="[10, 20]" layout="total, prev, pager, next" :pager-count="5" @pagination="getList" /></div>
<el-drawer v-model="filterVisible" title="筛选条件" direction="btt" size="50%">
<el-form :model="queryParams" ref="queryFormRef" label-position="top">
<el-form-item label="单位状态" prop="status"><el-select v-model="queryParams.status" placeholder="请选择单位状态" clearable style="width:100%"><el-option v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" :key="dict.value" :label="dict.label" :value="dict.value" /></el-select></el-form-item>
</el-form>
<template #footer><el-button @click="resetQuery">重置</el-button><el-button type="primary" @click="handleFilterConfirm">确认筛选</el-button></template>
</el-drawer>
</div>
<ContentWrap> <!-- PC端布局 -->
<!-- 搜索工作栏 --> <template v-else>
<el-form <ContentWrap>
class="-mb-15px" <el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
:model="queryParams" <el-form-item label="单位名字" prop="name"><el-input v-model="queryParams.name" placeholder="请输入单位名字" clearable @keyup.enter="handleQuery" class="!w-240px" /></el-form-item>
ref="queryFormRef" <el-form-item label="单位状态" prop="status"><el-select v-model="queryParams.status" placeholder="请选择单位状态" clearable class="!w-240px"><el-option v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" :key="dict.value" :label="dict.label" :value="dict.value" /></el-select></el-form-item>
:inline="true" <el-form-item>
label-width="68px" <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
> <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-form-item label="单位名字" prop="name"> <el-button type="primary" plain @click="openForm('create')" v-hasPermi="['erp:product-unit:create']"><Icon icon="ep:plus" class="mr-5px" /> 新增</el-button>
<el-input <el-button type="success" plain @click="handleExport" :loading="exportLoading" v-hasPermi="['erp:product-unit:export']"><Icon icon="ep:download" class="mr-5px" /> 导出</el-button>
v-model="queryParams.name" </el-form-item>
placeholder="请输入单位名字" </el-form>
clearable </ContentWrap>
@keyup.enter="handleQuery" <ContentWrap>
class="!w-240px" <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" @row-click="handleRowClick">
/> <el-table-column label="名字" align="center" prop="name" />
</el-form-item> <el-table-column label="状态" align="center" prop="status"><template #default="scope"><dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" /></template></el-table-column>
<el-form-item label="单位状态" prop="status"> <el-table-column label="创建时间" align="center" prop="createTime" :formatter="dateFormatter" width="180px" sortable />
<el-select <el-table-column label="操作" align="center">
v-model="queryParams.status" <template #default="scope">
placeholder="请选择单位状态" <el-button link type="primary" @click="openForm('update', scope.row.id)" v-hasPermi="['erp:product-unit:update']">编辑</el-button>
clearable <el-button link type="danger" @click="handleDelete(scope.row.id)" v-hasPermi="['erp:product-unit:delete']">删除</el-button>
class="!w-240px" </template>
> </el-table-column>
<el-option </el-table>
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" <Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" />
:key="dict.value" </ContentWrap>
:label="dict.label" </template>
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['erp:product-unit:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['erp:product-unit:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" @row-click="handleRowClick">
<el-table-column label="名字" align="center" prop="name" />
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
sortable
/>
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['erp:product-unit:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['erp:product-unit:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 --> <!-- 表单弹窗添加/修改 -->
<ProductUnitForm ref="formRef" @success="getList" /> <ProductUnitForm ref="formRef" @success="getList" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import { Search, Filter, Plus } from '@element-plus/icons-vue'
import { dateFormatter } from '@/utils/formatTime' import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download' import download from '@/utils/download'
import { ProductUnitApi, ProductUnitVO } from '@/api/erp/product/unit' import { ProductUnitApi, ProductUnitVO } from '@/api/erp/product/unit'
import ProductUnitForm from './ProductUnitForm.vue' import ProductUnitForm from './ProductUnitForm.vue'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { useWindowSize } from '@vueuse/core'
/** ERP 产品单位列表 */ /** ERP 产品单位列表 */
defineOptions({ name: 'ErpProductUnit' }) defineOptions({ name: 'ErpProductUnit' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中 const loading = ref(true) // 列表的加载中
const list = ref<ProductUnitVO[]>([]) // 列表的数据 const list = ref<ProductUnitVO[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数 const total = ref(0) // 列表的总页数
const filterVisible = ref(false) // 筛选抽屉
const queryParams = reactive({ const queryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
@@ -154,7 +115,13 @@ const handleQuery = () => {
/** 重置按钮操作 */ /** 重置按钮操作 */
const resetQuery = () => { const resetQuery = () => {
queryFormRef.value.resetFields() queryFormRef.value?.resetFields()
handleQuery()
}
/** 筛选确认 */
const handleFilterConfirm = () => {
filterVisible.value = false
handleQuery() handleQuery()
} }
@@ -177,11 +144,22 @@ const handleDelete = async (id: number) => {
} catch {} } catch {}
} }
/** 行点击操作 */ /** 卡片点击(移动端) */
const handleCardClick = (row: ProductUnitVO) => {
openForm('update', row.id)
}
/** 行点击(PC端) */
const handleRowClick = (row: ProductUnitVO) => { const handleRowClick = (row: ProductUnitVO) => {
openForm('update', row.id) openForm('update', row.id)
} }
/** 格式化日期 */
const formatDate = (date: any) => {
if (!date) return '-'
return dateFormatter({ cellValue: date })
}
/** 导出按钮操作 */ /** 导出按钮操作 */
const handleExport = async () => { const handleExport = async () => {
try { try {
@@ -202,3 +180,35 @@ onMounted(() => {
getList() getList()
}) })
</script> </script>
<style lang="scss" scoped>
.mobile-product-unit {
padding: 12px;
background: #f5f5f5;
min-height: 100vh;
}
.mobile-header {
display: flex; gap: 8px; align-items: center; margin-bottom: 12px;
&__search { flex: 1; }
&__actions { display: flex; gap: 4px; flex-shrink: 0; }
}
.mobile-quick-actions {
display: flex; gap: 8px; margin-bottom: 12px;
}
.mobile-list { display: flex; flex-direction: column; gap: 10px; }
.mobile-empty { padding: 40px 0; }
.mobile-card {
background: #fff; border-radius: 10px; padding: 14px; box-shadow: 0 1px 4px rgba(0,0,0,0.06);
&__header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
&__name { font-weight: 600; font-size: 15px; color: #303133; }
&__body { font-size: 13px; }
&__row { display: flex; justify-content: space-between; padding: 3px 0; }
&__label { color: #909399; flex-shrink: 0; margin-right: 12px; }
&__value { color: #606266; text-align: right; }
&__footer { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; padding-top: 10px; border-top: 1px solid #f0f0f0; }
}
.mobile-pagination {
margin-top: 12px; display: flex; justify-content: center;
:deep(.el-pagination) { flex-wrap: wrap; justify-content: center; }
}
</style>

View File

@@ -1,73 +1,63 @@
<template> <template>
<Dialog v-model="dialogVisible" :title="dialogTitle" width="100%" fullscreen class="mobile-eval-form-dialog"> <!-- 移动端布局 -->
<el-drawer v-if="isMobile" v-model="dialogVisible" :title="dialogTitle" direction="rtl" size="100%" :close-on-press-escape="true" :destroy-on-close="true" :append-to-body="true" class="mobile-form-drawer">
<div class="mobile-form-wrapper" v-loading="formLoading"> <div class="mobile-form-wrapper" v-loading="formLoading">
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="top"> <el-form ref="formRef" :model="formData" :rules="formRules" label-position="top">
<div class="mobile-form-section"> <div class="mobile-form-section">
<div class="mobile-form-section__title">基本信息</div> <div class="mobile-form-section__title">基本信息</div>
<el-form-item label="供应商" prop="supplierName"> <el-form-item label="供应商" prop="supplierName"><el-input v-model="formData.supplierName" disabled /></el-form-item>
<el-input v-model="formData.supplierName" disabled /> <el-form-item label="订单单号" prop="orderNo"><el-input v-model="formData.orderNo" disabled /></el-form-item>
</el-form-item>
<el-form-item label="订单单号" prop="orderNo">
<el-input v-model="formData.orderNo" disabled />
</el-form-item>
</div> </div>
<div class="mobile-form-section"> <div class="mobile-form-section">
<div class="mobile-form-section__title">评分标准满分10分</div> <div class="mobile-form-section__title">评分标准满分10分</div>
<el-form-item label="质量评分" prop="qualityScore"> <el-form-item label="质量评分" prop="qualityScore"><div class="rating-container"><el-rate v-model="formData.qualityScore" :max="10" show-score score-template="{value}分" allow-half /><div class="rating-desc">产品质量规格符合度缺陷率等</div></div></el-form-item>
<div class="rating-container"> <el-form-item label="服务评分" prop="serviceScore"><div class="rating-container"><el-rate v-model="formData.serviceScore" :max="10" show-score score-template="{value}分" allow-half /><div class="rating-desc">售前售后服务响应速度专业程度等</div></div></el-form-item>
<el-rate v-model="formData.qualityScore" :max="10" show-score score-template="{value}分" allow-half /> <el-form-item label="价格评分" prop="priceScore"><div class="rating-container"><el-rate v-model="formData.priceScore" :max="10" show-score score-template="{value}分" allow-half /><div class="rating-desc">价格合理性性价比优惠政策等</div></div></el-form-item>
<div class="rating-desc">产品质量规格符合度缺陷率等</div> <el-form-item label="交付评分" prop="deliveryScore"><div class="rating-container"><el-rate v-model="formData.deliveryScore" :max="10" show-score score-template="{value}分" allow-half /><div class="rating-desc">交付及时性包装质量物流配送等</div></div></el-form-item>
</div>
</el-form-item>
<el-form-item label="服务评分" prop="serviceScore">
<div class="rating-container">
<el-rate v-model="formData.serviceScore" :max="10" show-score score-template="{value}分" allow-half />
<div class="rating-desc">售前售后服务响应速度专业程度等</div>
</div>
</el-form-item>
<el-form-item label="价格评分" prop="priceScore">
<div class="rating-container">
<el-rate v-model="formData.priceScore" :max="10" show-score score-template="{value}分" allow-half />
<div class="rating-desc">价格合理性性价比优惠政策等</div>
</div>
</el-form-item>
<el-form-item label="交付评分" prop="deliveryScore">
<div class="rating-container">
<el-rate v-model="formData.deliveryScore" :max="10" show-score score-template="{value}分" allow-half />
<div class="rating-desc">交付及时性包装质量物流配送等</div>
</div>
</el-form-item>
</div> </div>
<div class="mobile-form-section"> <div class="mobile-form-section">
<div class="mobile-form-section__title">综合评分</div> <div class="mobile-form-section__title">综合评分</div>
<div class="total-score"> <div class="total-score"><span class="score-value">{{ totalScore.toFixed(1) }}</span><span class="score-level">{{ getScoreLevel(totalScore) }}</span></div>
<span class="score-value">{{ totalScore.toFixed(1) }}</span>
<span class="score-level">{{ getScoreLevel(totalScore) }}</span>
</div>
</div> </div>
<div class="mobile-form-section"> <div class="mobile-form-section">
<div class="mobile-form-section__title">评价备注</div> <div class="mobile-form-section__title">评价备注</div>
<el-form-item prop="remark"> <el-form-item prop="remark"><el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入评价备注(可选)" maxlength="500" show-word-limit /></el-form-item>
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入评价备注(可选)" maxlength="500" show-word-limit />
</el-form-item>
</div> </div>
</el-form> </el-form>
</div> <div class="mobile-form__footer">
<el-button @click="dialogVisible = false"> </el-button>
<template #footer> <el-button type="primary" @click="submitForm" :loading="formLoading"> </el-button>
<div class="mobile-form-footer">
<el-button @click="dialogVisible = false" style="flex:1">取消</el-button>
<el-button type="primary" @click="submitForm" :loading="formLoading" style="flex:1">确定</el-button>
</div> </div>
</div>
</el-drawer>
<!-- PC端布局 -->
<Dialog v-else v-model="dialogVisible" :title="dialogTitle" width="600px">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" v-loading="formLoading">
<el-form-item label="供应商" prop="supplierName"><el-input v-model="formData.supplierName" disabled /></el-form-item>
<el-form-item label="订单单号" prop="orderNo"><el-input v-model="formData.orderNo" disabled /></el-form-item>
<el-divider content-position="left">评分标准满分10分</el-divider>
<el-form-item label="质量评分" prop="qualityScore"><div class="rating-container"><el-rate v-model="formData.qualityScore" :max="10" show-score score-template="{value}分" allow-half /><div class="rating-desc">产品质量规格符合度缺陷率等</div></div></el-form-item>
<el-form-item label="服务评分" prop="serviceScore"><div class="rating-container"><el-rate v-model="formData.serviceScore" :max="10" show-score score-template="{value}分" allow-half /><div class="rating-desc">售前售后服务响应速度专业程度等</div></div></el-form-item>
<el-form-item label="价格评分" prop="priceScore"><div class="rating-container"><el-rate v-model="formData.priceScore" :max="10" show-score score-template="{value}分" allow-half /><div class="rating-desc">价格合理性性价比优惠政策等</div></div></el-form-item>
<el-form-item label="交付评分" prop="deliveryScore"><div class="rating-container"><el-rate v-model="formData.deliveryScore" :max="10" show-score score-template="{value}分" allow-half /><div class="rating-desc">交付及时性包装质量物流配送等</div></div></el-form-item>
<el-form-item label="综合评分" prop="totalScore"><div class="total-score"><span class="score-value">{{ totalScore.toFixed(1) }}</span><span class="score-level">{{ getScoreLevel(totalScore) }}</span></div></el-form-item>
<el-form-item label="评价备注" prop="remark"><el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入评价备注(可选)" maxlength="500" show-word-limit /></el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm" :loading="formLoading">确定</el-button>
</template> </template>
</Dialog> </Dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import { SupplierEvaluationApi, SupplierEvaluationVO } from '@/api/erp/purchase/supplierEvaluation' import { SupplierEvaluationApi, SupplierEvaluationVO } from '@/api/erp/purchase/supplierEvaluation'
import { useWindowSize } from '@vueuse/core'
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
interface SupplierEvaluationForm { interface SupplierEvaluationForm {
id?: number id?: number
@@ -222,10 +212,19 @@ defineExpose({ open })
border-bottom: 1px solid #f0f0f0; border-bottom: 1px solid #f0f0f0;
} }
} }
.mobile-form-footer { .mobile-form__footer {
position: sticky;
bottom: 0;
background: #fff;
padding: 12px 16px;
padding-bottom: calc(12px + env(safe-area-inset-bottom));
border-top: 1px solid #eee;
display: flex; display: flex;
justify-content: flex-end;
gap: 12px; gap: 12px;
padding: 0 4px; z-index: 10;
margin: 0 -4px -12px;
.el-button { flex: 1; height: 40px; font-size: 15px; }
} }
.rating-container { .rating-container {
display: flex; display: flex;

View File

@@ -3,7 +3,11 @@
<!-- 筛选条件 --> <!-- 筛选条件 -->
<div class="mobile-dashboard-filter"> <div class="mobile-dashboard-filter">
<el-select v-model="queryParams.supplierId" clearable filterable placeholder="选择供应商" @change="getSupplierStats" size="small" style="flex:1"> <el-select v-model="queryParams.supplierId" clearable filterable placeholder="选择供应商" @change="getSupplierStats" size="small" style="flex:1">
<el-option v-for="item in supplierList" :key="item.id" :label="item.name" :value="item.id" /> <el-option
v-for="item in supplierList"
:key="item.id"
:label="item.name"
:value="item.id" />
</el-select> </el-select>
<el-button size="small" @click="getAllSuppliersStats">刷新</el-button> <el-button size="small" @click="getAllSuppliersStats">刷新</el-button>
</div> </div>
@@ -88,7 +92,7 @@
</div> </div>
<div class="mobile-distribution__item"> <div class="mobile-distribution__item">
<div class="mobile-distribution__header"> <div class="mobile-distribution__header">
<span class="mobile-distribution__label">不及格 (<6)</span> <span class="mobile-distribution__label">不及格 (&lt;6)</span>
<span class="mobile-distribution__count">{{ failCount }} </span> <span class="mobile-distribution__count">{{ failCount }} </span>
</div> </div>
<el-progress :percentage="scoreDistribution.fail" color="#f56c6c" :stroke-width="10" /> <el-progress :percentage="scoreDistribution.fail" color="#f56c6c" :stroke-width="10" />

View File

@@ -1,5 +1,6 @@
<template> <template>
<div class="mobile-evaluation"> <!-- 移动端布局 -->
<div v-if="isMobile" class="mobile-evaluation">
<!-- 顶部操作栏 --> <!-- 顶部操作栏 -->
<div class="mobile-header"> <div class="mobile-header">
<div class="mobile-header__search"> <div class="mobile-header__search">
@@ -135,11 +136,96 @@
<!-- 供应商汇总表弹窗 --> <!-- 供应商汇总表弹窗 -->
<SupplierSummaryTable ref="summaryRef" /> <SupplierSummaryTable ref="summaryRef" />
</div> </div>
<!-- PC端布局 -->
<div v-else>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
<el-form-item label="供应商" prop="supplierId">
<el-select v-model="queryParams.supplierId" clearable filterable placeholder="请选择供应商" class="!w-240px">
<el-option v-for="item in supplierList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="采购订单" prop="purchaseOrderId">
<el-input v-model="queryParams.purchaseOrderNo" placeholder="请输入采购订单号" clearable @keyup.enter="handleQuery" class="!w-240px" />
</el-form-item>
<el-form-item label="评价时间" prop="createTime">
<el-date-picker v-model="queryParams.createTime" value-format="YYYY-MM-DD HH:mm:ss" type="daterange" start-placeholder="开始日期" end-placeholder="结束日期" :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" class="!w-240px" />
</el-form-item>
<el-form-item label="评分范围" prop="scoreRange">
<el-select v-model="queryParams.scoreRange" placeholder="请选择评分范围" clearable class="!w-240px">
<el-option label="优秀 (9.0-10.0)" value="excellent" />
<el-option label="良好 (8.0-8.9)" value="good" />
<el-option label="一般 (7.0-7.9)" value="average" />
<el-option label="及格 (6.0-6.9)" value="pass" />
<el-option label="不及格 (0-5.9)" value="fail" />
</el-select>
</el-form-item>
<el-form-item label="创建人" prop="creator">
<el-select v-model="queryParams.creator" clearable filterable placeholder="请选择创建人" class="!w-240px">
<el-option v-for="item in userList" :key="item.id" :label="item.nickname" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button type="success" plain @click="handleExport" :loading="exportLoading" v-hasPermi="['erp:supplier-evaluation:export']"><Icon icon="ep:download" class="mr-5px" /> 导出</el-button>
<el-button type="info" plain @click="showStatistics = !showStatistics"><Icon icon="ep:data-analysis" class="mr-5px" /> {{ showStatistics ? '隐藏统计' : '显示统计' }}</el-button>
<el-button type="primary" plain @click="openSupplierSummary" v-hasPermi="['erp:supplier-evaluation:query']"><Icon icon="ep:grid" class="mr-5px" /> 供应商汇总表</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 统计信息 -->
<ContentWrap v-if="showStatistics">
<el-row :gutter="20" class="mb-4">
<el-col :span="6"><el-card class="statistics-card"><div class="statistics-content"><div class="statistics-value">{{ statistics.totalEvaluations }}</div><div class="statistics-label">总评价数</div></div><Icon icon="ep:document" class="statistics-icon" /></el-card></el-col>
<el-col :span="6"><el-card class="statistics-card"><div class="statistics-content"><div class="statistics-value">{{ statistics.avgTotalScore }}</div><div class="statistics-label">平均总分</div></div><Icon icon="ep:star" class="statistics-icon" /></el-card></el-col>
<el-col :span="6"><el-card class="statistics-card"><div class="statistics-content"><div class="statistics-value">{{ statistics.excellentCount }}</div><div class="statistics-label">优秀评价</div></div><Icon icon="ep:trophy" class="statistics-icon" /></el-card></el-col>
<el-col :span="6"><el-card class="statistics-card"><div class="statistics-content"><div class="statistics-value">{{ statistics.supplierCount }}</div><div class="statistics-label">评价供应商</div></div><Icon icon="ep:office-building" class="statistics-icon" /></el-card></el-col>
</el-row>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" @selection-change="handleSelectionChange">
<el-table-column width="30" label="选择" type="selection" />
<el-table-column label="供应商" align="center" prop="supplierName" min-width="120" />
<el-table-column label="采购订单号" align="center" prop="orderNo" min-width="140" />
<el-table-column label="质量评分" align="center" prop="qualityScore" width="80"><template #default="scope"><el-tag :type="getScoreTagType(scope.row.qualityScore)">{{ scope.row.qualityScore }}</el-tag></template></el-table-column>
<el-table-column label="服务评分" align="center" prop="serviceScore" width="80"><template #default="scope"><el-tag :type="getScoreTagType(scope.row.serviceScore)">{{ scope.row.serviceScore }}</el-tag></template></el-table-column>
<el-table-column label="价格评分" align="center" prop="priceScore" width="80"><template #default="scope"><el-tag :type="getScoreTagType(scope.row.priceScore)">{{ scope.row.priceScore }}</el-tag></template></el-table-column>
<el-table-column label="交付评分" align="center" prop="deliveryScore" width="80"><template #default="scope"><el-tag :type="getScoreTagType(scope.row.deliveryScore)">{{ scope.row.deliveryScore }}</el-tag></template></el-table-column>
<el-table-column label="综合评分" align="center" prop="totalScore" width="100"><template #default="scope"><div class="total-score-cell"><el-tag :type="getScoreTagType(scope.row.totalScore)" size="large">{{ scope.row.totalScore }}</el-tag><div class="score-level">{{ getScoreLevel(scope.row.totalScore) }}</div></div></template></el-table-column>
<el-table-column label="评价备注" align="center" prop="remark" min-width="150"><template #default="scope"><span v-if="scope.row.remark">{{ scope.row.remark }}</span><span v-else class="text-gray-400">无备注</span></template></el-table-column>
<el-table-column label="评价时间" align="center" prop="createTime" :formatter="dateFormatter2" width="120px" sortable />
<el-table-column label="评价人" align="center" prop="creatorName" width="100" />
<el-table-column label="操作" align="center" fixed="right" width="160">
<template #default="scope">
<el-button link @click="openDetail(scope.row)" v-hasPermi="['erp:supplier-evaluation:query']">详情</el-button>
<el-button link type="primary" @click="openForm('update', scope.row.id)" v-hasPermi="['erp:supplier-evaluation:update']">编辑</el-button>
<el-button link type="danger" @click="handleDelete([scope.row.id])" v-hasPermi="['erp:supplier-evaluation:delete']">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" />
</ContentWrap>
<!-- 表单弹窗编辑 -->
<SupplierEvaluationForm ref="formRef" @success="getList" />
<!-- 详情弹窗 -->
<SupplierEvaluationDetail ref="detailRef" />
<!-- 供应商汇总表弹窗 -->
<SupplierSummaryTable ref="summaryRef" />
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import { Search, Filter, DataAnalysis } from '@element-plus/icons-vue' import { Search, Filter, DataAnalysis } from '@element-plus/icons-vue'
import { formatDate } from '@/utils/formatTime' import { formatDate, dateFormatter2 } from '@/utils/formatTime'
import download from '@/utils/download' import download from '@/utils/download'
import { SupplierEvaluationApi, SupplierEvaluationVO } from '@/api/erp/purchase/supplierEvaluation' import { SupplierEvaluationApi, SupplierEvaluationVO } from '@/api/erp/purchase/supplierEvaluation'
import SupplierEvaluationForm from './SupplierEvaluationForm.vue' import SupplierEvaluationForm from './SupplierEvaluationForm.vue'
@@ -148,10 +234,14 @@ import SupplierSummaryTable from './SupplierSummaryTable.vue'
import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier' import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier'
import { UserVO } from '@/api/system/user' import { UserVO } from '@/api/system/user'
import * as UserApi from '@/api/system/user' import * as UserApi from '@/api/system/user'
import { useWindowSize } from '@vueuse/core'
/** ERP 供应商评价记录列表 */ /** ERP 供应商评价记录列表 */
defineOptions({ name: 'ErpSupplierEvaluationList' }) defineOptions({ name: 'ErpSupplierEvaluationList' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const message = useMessage() const message = useMessage()
const { t } = useI18n() const { t } = useI18n()
@@ -267,6 +357,9 @@ const handleExport = async () => {
} }
const selectionList = ref<SupplierEvaluationVO[]>([]) const selectionList = ref<SupplierEvaluationVO[]>([])
const handleSelectionChange = (rows: SupplierEvaluationVO[]) => {
selectionList.value = rows
}
const getScoreTagType = (score: number) => { const getScoreTagType = (score: number) => {
if (score >= 9) return 'success' if (score >= 9) return 'success'

View File

@@ -1,175 +1,37 @@
<template> <template>
<el-drawer <!-- 移动端布局 -->
v-model="dialogVisible" <el-drawer v-if="isMobile" v-model="dialogVisible" :title="dialogTitle" direction="rtl" size="100%" :close-on-press-escape="true" :destroy-on-close="true" :append-to-body="true" class="mobile-form-drawer">
:title="dialogTitle"
direction="rtl"
size="100%"
:close-on-press-escape="true"
:destroy-on-close="true"
:append-to-body="true"
class="mobile-form-drawer"
>
<div class="mobile-form" v-loading="formLoading"> <div class="mobile-form" v-loading="formLoading">
<el-form <el-form ref="formRef" :model="formData" :rules="formRules" label-position="top" :disabled="disabled">
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
:disabled="disabled"
>
<!-- 基本信息 -->
<div class="mobile-form__section"> <div class="mobile-form__section">
<div class="mobile-form__section-title">基本信息</div> <div class="mobile-form__section-title">基本信息</div>
<el-form-item label="入库单号" prop="no"> <el-form-item label="入库单号" prop="no"><el-input disabled v-model="formData.no" placeholder="保存时自动生成" /></el-form-item>
<el-input disabled v-model="formData.no" placeholder="保存时自动生成" /> <el-form-item label="入库时间" prop="inTime"><el-date-picker v-model="formData.inTime" type="date" value-format="x" placeholder="选择入库时间" style="width:100%" /></el-form-item>
</el-form-item> <el-form-item label="关联订单" prop="orderNo"><el-input readonly><template #prefix><el-link v-if="formData.orderNo && formData.orderId" type="primary" :underline="false" @click.stop="openPurchaseOrderDetail">{{ formData.orderNo }}</el-link></template><template #append><el-button @click="openPurchaseOrderInEnableList"><Icon icon="ep:search" /></el-button></template></el-input></el-form-item>
<el-form-item label="入库时间" prop="inTime"> <el-form-item label="供应商" prop="supplierId"><el-select v-model="formData.supplierId" clearable filterable disabled placeholder="请选择供应商" style="width:100%"><el-option v-for="item in supplierList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item>
<el-date-picker <el-form-item label="备注" prop="remark"><el-input type="textarea" v-model="formData.remark" :rows="2" placeholder="请输入备注" /></el-form-item>
v-model="formData.inTime" <el-form-item label="附件" prop="fileUrl"><UploadFile :is-show-tip="false" v-model="formData.fileUrl" :limit="1" /></el-form-item>
type="date"
value-format="x"
placeholder="选择入库时间"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="关联订单" prop="orderNo">
<el-input readonly>
<template #prefix>
<el-link
v-if="formData.orderNo && formData.orderId"
type="primary"
:underline="false"
@click.stop="openPurchaseOrderDetail"
>
{{ formData.orderNo }}
</el-link>
</template>
<template #append>
<el-button @click="openPurchaseOrderInEnableList">
<Icon icon="ep:search" /> 选择
</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item label="供应商" prop="supplierId">
<el-select
v-model="formData.supplierId"
clearable
filterable
disabled
placeholder="请选择供应商"
style="width: 100%"
>
<el-option
v-for="item in supplierList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
type="textarea"
v-model="formData.remark"
:rows="2"
placeholder="请输入备注"
/>
</el-form-item>
<el-form-item label="附件" prop="fileUrl">
<UploadFile :is-show-tip="false" v-model="formData.fileUrl" :limit="1" />
</el-form-item>
</div> </div>
<!-- 入库产品清单 -->
<div class="mobile-form__section"> <div class="mobile-form__section">
<div class="mobile-form__section-title">入库产品清单</div> <div class="mobile-form__section-title">入库产品清单</div>
<PurchaseInItemForm ref="itemFormRef" :items="formData.items" :disabled="disabled" /> <PurchaseInItemForm ref="itemFormRef" :items="formData.items" :disabled="disabled" />
</div> </div>
<!-- 费用信息 -->
<div class="mobile-form__section"> <div class="mobile-form__section">
<div class="mobile-form__section-title">费用信息</div> <div class="mobile-form__section-title">费用信息</div>
<el-form-item label="优惠率(%" prop="discountPercent"> <el-form-item label="优惠率(%" prop="discountPercent"><el-input-number v-model="formData.discountPercent" controls-position="right" :min="0" :precision="4" placeholder="请输入优惠率" style="width:100%" /></el-form-item>
<el-input-number <el-form-item label="付款优惠" prop="discountPrice"><el-input disabled v-model="formData.discountPrice" :formatter="erpPriceInputFormatter" /></el-form-item>
v-model="formData.discountPercent" <el-form-item label="优惠后金额"><el-input disabled :model-value="formData.totalPrice - formData.otherPrice" :formatter="erpPriceInputFormatter" /></el-form-item>
controls-position="right" <el-form-item label="其它费用" prop="otherPrice"><el-input-number v-model="formData.otherPrice" controls-position="right" :min="0" :precision="4" placeholder="请输入其它费用" style="width:100%" /></el-form-item>
:min="0" <el-form-item label="结算账户" prop="accountId"><el-select v-model="formData.accountId" clearable filterable placeholder="请选择结算账户" style="width:100%"><el-option v-for="item in accountList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item>
:precision="4" <el-form-item label="应付金额"><el-input disabled v-model="formData.totalPrice" :formatter="erpPriceInputFormatter" /></el-form-item>
placeholder="请输入优惠率"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="付款优惠" prop="discountPrice">
<el-input
disabled
v-model="formData.discountPrice"
:formatter="erpPriceInputFormatter"
/>
</el-form-item>
<el-form-item label="优惠后金额">
<el-input
disabled
:model-value="formData.totalPrice - formData.otherPrice"
:formatter="erpPriceInputFormatter"
/>
</el-form-item>
<el-form-item label="其它费用" prop="otherPrice">
<el-input-number
v-model="formData.otherPrice"
controls-position="right"
:min="0"
:precision="4"
placeholder="请输入其它费用"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="结算账户" prop="accountId">
<el-select
v-model="formData.accountId"
clearable
filterable
placeholder="请选择结算账户"
style="width: 100%"
>
<el-option
v-for="item in accountList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="应付金额">
<el-input disabled v-model="formData.totalPrice" :formatter="erpPriceInputFormatter" />
</el-form-item>
</div> </div>
<!-- 审核信息 -->
<div class="mobile-form__section"> <div class="mobile-form__section">
<div class="mobile-form__section-title">审核信息</div> <div class="mobile-form__section-title">审核信息</div>
<el-form-item label="审核状态"> <el-form-item label="审核状态"><el-tag :type="formData.status === 10 ? 'info' : (formData.isQualified ? 'success' : 'danger')">{{ formData.status === 10 ? '未审核' : (formData.isQualified ? '已审核' : '不合格') }}</el-tag></el-form-item>
<el-tag <el-form-item label="返回方式" v-if="formData.status !== 10 && !formData.isQualified"><span>{{ RETURN_TYPE_OPTIONS.find(item => item.value === formData.returnType)?.label || '-' }}</span></el-form-item>
:type="formData.status === 10 ? 'info' : (formData.isQualified ? 'success' : 'danger')" <el-form-item label="返回备注" v-if="formData.status !== 10 && !formData.isQualified"><el-input type="textarea" v-model="formData.returnRemark" :rows="3" disabled placeholder="无" /></el-form-item>
>
{{ formData.status === 10 ? '未审核' : (formData.isQualified ? '已审核' : '不合格') }}
</el-tag>
</el-form-item>
<el-form-item label="返回方式" v-if="formData.status !== 10 && !formData.isQualified">
<span>{{ RETURN_TYPE_OPTIONS.find(item => item.value === formData.returnType)?.label || '-' }}</span>
</el-form-item>
<el-form-item label="返回备注" v-if="formData.status !== 10 && !formData.isQualified">
<el-input
type="textarea"
v-model="formData.returnRemark"
:rows="3"
disabled
placeholder="无"
/>
</el-form-item>
</div> </div>
</el-form> </el-form>
<!-- 底部操作按钮 -->
<div class="mobile-form__footer"> <div class="mobile-form__footer">
<el-button @click="dialogVisible = false"> </el-button> <el-button @click="dialogVisible = false"> </el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled"> </el-button> <el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled"> </el-button>
@@ -177,12 +39,47 @@
</div> </div>
</el-drawer> </el-drawer>
<!-- 可入库的订单列表 --> <!-- PC端布局 -->
<PurchaseOrderInEnableList <Dialog v-else :title="dialogTitle" v-model="dialogVisible" width="1440">
ref="purchaseOrderInEnableListRef" <el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" v-loading="formLoading" :disabled="disabled">
@success="handlePurchaseOrderChange" <el-row :gutter="20">
/> <el-col :span="8"><el-form-item label="入库单号" prop="no"><el-input disabled v-model="formData.no" placeholder="保存时自动生成" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="入库时间" prop="inTime"><el-date-picker v-model="formData.inTime" type="date" value-format="x" placeholder="选择入库时间" class="!w-1/1" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="关联订单" prop="orderNo"><el-input readonly><template #prefix><el-link v-if="formData.orderNo && formData.orderId" type="primary" :underline="false" @click.stop="openPurchaseOrderDetail" class="order-link">{{ formData.orderNo }}</el-link></template><template #append><el-button @click="openPurchaseOrderInEnableList"><Icon icon="ep:search" /> 选择</el-button></template></el-input></el-form-item></el-col>
<el-col :span="8"><el-form-item label="供应商" prop="supplierId"><el-select v-model="formData.supplierId" clearable filterable disabled placeholder="请选择供应商" class="!w-1/1"><el-option v-for="item in supplierList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></el-col>
<el-col :span="16"><el-form-item label="备注" prop="remark"><el-input type="textarea" v-model="formData.remark" :rows="1" placeholder="请输入备注" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="附件" prop="fileUrl"><UploadFile :is-show-tip="false" v-model="formData.fileUrl" :limit="1" /></el-form-item></el-col>
</el-row>
<ContentWrap>
<el-tabs v-model="subTabsName" class="-mt-15px -mb-10px">
<el-tab-pane label="入库产品清单" name="item"><PurchaseInItemForm ref="itemFormRef" :items="formData.items" :disabled="disabled" /></el-tab-pane>
</el-tabs>
</ContentWrap>
<el-row :gutter="20">
<el-col :span="8"><el-form-item label="优惠率(%" prop="discountPercent"><el-input-number v-model="formData.discountPercent" controls-position="right" :min="0" :precision="4" placeholder="请输入优惠率" class="!w-1/1" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="付款优惠" prop="discountPrice"><el-input disabled v-model="formData.discountPrice" :formatter="erpPriceInputFormatter" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="优惠后金额"><el-input disabled :model-value="formData.totalPrice - formData.otherPrice" :formatter="erpPriceInputFormatter" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="其它费用" prop="otherPrice"><el-input-number v-model="formData.otherPrice" controls-position="right" :min="0" :precision="4" placeholder="请输入其它费用" class="!w-1/1" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="结算账户" prop="accountId"><el-select v-model="formData.accountId" clearable filterable placeholder="请选择结算账户" class="!w-1/1"><el-option v-for="item in accountList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></el-col>
<el-col :span="8"><el-form-item label="应付金额"><el-input disabled v-model="formData.totalPrice" :formatter="erpPriceInputFormatter" /></el-form-item></el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8"><el-form-item label="审核状态"><el-tag :type="formData.status === 10 ? 'info' : (formData.isQualified ? 'success' : 'danger')">{{ formData.status === 10 ? '未审核' : (formData.isQualified ? '已审核' : '不合格') }}</el-tag></el-form-item></el-col>
<el-col :span="8" v-if="formData.status !== 10 && !formData.isQualified"><el-form-item label="返回方式"><span>{{ RETURN_TYPE_OPTIONS.find(item => item.value === formData.returnType)?.label || '-' }}</span></el-form-item></el-col>
<el-col :span="16" v-if="formData.status !== 10 && !formData.isQualified"><el-form-item label="返回备注"><el-input type="textarea" v-model="formData.returnRemark" :rows="2" disabled placeholder="无" /></el-form-item></el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled"> </el-button>
<el-button @click="handlePrint" type="warning">
<Icon icon="ep:printer" class="mr-5px" />
</el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
<!-- 可入库的订单列表 -->
<PurchaseOrderInEnableList ref="purchaseOrderInEnableListRef" @success="handlePurchaseOrderChange" />
<!-- 采购订单详情弹窗 --> <!-- 采购订单详情弹窗 -->
<PurchaseOrderForm ref="purchaseOrderFormRef" /> <PurchaseOrderForm ref="purchaseOrderFormRef" />
</template> </template>
@@ -200,10 +97,14 @@ import { PurchaseOrderVO } from '@/api/erp/purchase/order'
import * as UserApi from '@/api/system/user' import * as UserApi from '@/api/system/user'
import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier' import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier'
import { ElTag } from 'element-plus' import { ElTag } from 'element-plus'
import { useWindowSize } from '@vueuse/core'
/** ERP 销售入库表单 */ /** ERP 采购入库表单 */
defineOptions({ name: 'PurchaseInForm' }) defineOptions({ name: 'PurchaseInForm' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
@@ -254,7 +155,7 @@ const formRef = ref() // 表单 Ref
const supplierList = ref<SupplierVO[]>([]) // 供应商列表 const supplierList = ref<SupplierVO[]>([]) // 供应商列表
const accountList = ref<AccountVO[]>([]) // 账户列表 const accountList = ref<AccountVO[]>([]) // 账户列表
const userList = ref<UserApi.UserVO[]>([]) // 用户列表 const userList = ref<UserApi.UserVO[]>([]) // 用户列表
const subTabsName = ref('item')
const itemFormRef = ref() const itemFormRef = ref()
/** 计算 discountPrice、totalPrice 价格 */ /** 计算 discountPrice、totalPrice 价格 */
@@ -394,6 +295,166 @@ const resetForm = () => {
} }
formRef.value?.resetFields() formRef.value?.resetFields()
} }
/** 打印当前页面数据 */
const handlePrint = () => {
// 获取供应商名称
const supplier = supplierList.value.find((item) => item.id === formData.value.supplierId)
const supplierName = supplier ? supplier.name : ''
// 获取结算账户名称
const account = accountList.value.find((item) => item.id === formData.value.accountId)
const accountName = account ? account.name : ''
// 格式化入库时间
const inTime = formData.value.inTime
? new Date(formData.value.inTime).toLocaleDateString('zh-CN')
: ''
// 构建产品清单表格
let itemsTableHtml = ''
if (formData.value.items && formData.value.items.length > 0) {
// 计算合计
const totalCount = formData.value.items.reduce((sum, item) => sum + (item.count || 0), 0)
const totalProductPrice = formData.value.items.reduce((sum, item) => sum + (item.totalProductPrice || 0), 0)
const totalTaxPrice = formData.value.items.reduce((sum, item) => sum + (item.taxPrice || 0), 0)
const totalPriceSum = formData.value.items.reduce((sum, item) => sum + (item.totalPrice || 0), 0)
itemsTableHtml = `
<table border="1" cellpadding="6" cellspacing="0" style="width:100%; border-collapse: collapse; margin-top: 10px; font-size: 12px;">
<thead>
<tr style="background-color: #f5f5f5;">
<th>序号</th>
<th>产品名称</th>
<th>产品分类</th>
<th>库存</th>
<th>条码</th>
<th>单位</th>
<th>数量</th>
<th>产品单价</th>
<th>金额</th>
<th>税率(%)</th>
<th>税额</th>
<th>税额合计</th>
<th>备注</th>
</tr>
</thead>
<tbody>
${formData.value.items
.map(
(item, index) => `
<tr>
<td style="text-align: center;">${index + 1}</td>
<td>${item.productName || ''}</td>
<td>${item.productCategoryName || ''}</td>
<td style="text-align: right;">${item.stockCount || 0}</td>
<td>${item.productBarCode || ''}</td>
<td style="text-align: center;">${item.productUnitName || ''}</td>
<td style="text-align: right;">${item.count || 0}</td>
<td style="text-align: right;">${item.productPrice || 0}</td>
<td style="text-align: right;">${item.totalProductPrice || 0}</td>
<td style="text-align: right;">${item.taxPercent || 0}</td>
<td style="text-align: right;">${item.taxPrice || 0}</td>
<td style="text-align: right;">${item.totalPrice || 0}</td>
<td>${item.remark || ''}</td>
</tr>
`
)
.join('')}
</tbody>
<tfoot>
<tr style="background-color: #f9f9f9; font-weight: bold;">
<td style="text-align: center;">合计</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td style="text-align: right;">${totalCount}</td>
<td></td>
<td style="text-align: right;">${totalProductPrice}</td>
<td></td>
<td style="text-align: right;">${totalTaxPrice}</td>
<td style="text-align: right;">${totalPriceSum}</td>
<td></td>
</tr>
</tfoot>
</table>
`
}
// 构建打印内容
const printContent = `
<html>
<head>
<title>采购入库单打印</title>
<style>
body { font-family: 'Microsoft YaHei', Arial, sans-serif; padding: 20px; }
h1 { text-align: center; margin-bottom: 20px; }
.info-row { display: flex; flex-wrap: wrap; margin-bottom: 10px; }
.info-item { width: 33%; margin-bottom: 10px; }
.info-label { font-weight: bold; color: #666; }
.info-value { margin-left: 10px; }
.section-title { font-size: 16px; font-weight: bold; margin: 20px 0 10px; border-bottom: 1px solid #ddd; padding-bottom: 5px; }
table { font-size: 14px; }
th, td { padding: 8px; text-align: left; }
@media print {
body { padding: 0; }
}
</style>
</head>
<body>
<h1>采购入库单</h1>
<div class="info-row">
<div class="info-item"><span class="info-label">入库单号:</span><span class="info-value">${formData.value.no || '(保存后生成)'}</span></div>
<div class="info-item"><span class="info-label">入库时间:</span><span class="info-value">${inTime}</span></div>
<div class="info-item"><span class="info-label">关联订单:</span><span class="info-value">${formData.value.orderNo || ''}</span></div>
<div class="info-item"><span class="info-label">供应商:</span><span class="info-value">${supplierName}</span></div>
<div class="info-item"><span class="info-label">结算账户:</span><span class="info-value">${accountName}</span></div>
</div>
<div class="info-row">
<div class="info-item" style="width: 100%;"><span class="info-label">备注:</span><span class="info-value">${formData.value.remark || ''}</span></div>
</div>
<div class="section-title">入库产品清单</div>
${itemsTableHtml}
<div class="info-row" style="margin-top: 20px;">
<div class="info-item"><span class="info-label">优惠率(%):</span><span class="info-value">${formData.value.discountPercent || 0}</span></div>
<div class="info-item"><span class="info-label">付款优惠:</span><span class="info-value">${formData.value.discountPrice || 0}</span></div>
<div class="info-item"><span class="info-label">优惠后金额:</span><span class="info-value">${(formData.value.totalPrice || 0) - (formData.value.otherPrice || 0)}</span></div>
<div class="info-item"><span class="info-label">其它费用:</span><span class="info-value">${formData.value.otherPrice || 0}</span></div>
<div class="info-item"><span class="info-label">应付金额:</span><span class="info-value">${formData.value.totalPrice || 0}</span></div>
</div>
</body>
</html>
`
// 使用iframe打印避免被浏览器拦截
const iframe = document.createElement('iframe')
iframe.style.position = 'absolute'
iframe.style.width = '0'
iframe.style.height = '0'
iframe.style.border = 'none'
iframe.style.left = '-9999px'
document.body.appendChild(iframe)
const iframeDoc = iframe.contentWindow?.document
if (iframeDoc) {
iframeDoc.open()
iframeDoc.write(printContent)
iframeDoc.close()
// 等待内容加载完成后打印
iframe.contentWindow?.focus()
setTimeout(() => {
iframe.contentWindow?.print()
// 打印完成后移除iframe
setTimeout(() => {
document.body.removeChild(iframe)
}, 1000)
}, 250)
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -1,13 +1,6 @@
<template> <template>
<el-form <!-- 移动端布局 -->
ref="formRef" <el-form v-if="isMobile" ref="formRef" :model="formData" :rules="formRules" v-loading="formLoading" label-position="top" :inline-message="true" :disabled="disabled">
:model="formData"
:rules="formRules"
v-loading="formLoading"
label-position="top"
:inline-message="true"
:disabled="disabled"
>
<div class="mobile-item-list"> <div class="mobile-item-list">
<div <div
v-for="(row, $index) in formData" v-for="(row, $index) in formData"
@@ -123,25 +116,37 @@
</div> </div>
<!-- 合计 --> <!-- 合计 -->
<div class="mobile-item-summary" v-if="formData.length > 0"> <div class="mobile-item-summary" v-if="formData.length > 0">
<div class="mobile-item-summary__row"> <div class="mobile-item-summary__row"><span>合计数量</span><span>{{ erpCountInputFormatter(summaryData.count) }}</span></div>
<span>合计数量</span> <div class="mobile-item-summary__row"><span>合计金额</span><span>{{ erpPriceInputFormatter(summaryData.totalProductPrice) }}</span></div>
<span>{{ erpCountInputFormatter(summaryData.count) }}</span> <div class="mobile-item-summary__row"><span>合计税额</span><span>{{ erpPriceInputFormatter(summaryData.taxPrice) }}</span></div>
</div> <div class="mobile-item-summary__row mobile-item-summary__row--total"><span>税额合计</span><span>{{ erpPriceInputFormatter(summaryData.totalPrice) }}</span></div>
<div class="mobile-item-summary__row">
<span>合计金额</span>
<span>{{ erpPriceInputFormatter(summaryData.totalProductPrice) }}</span>
</div>
<div class="mobile-item-summary__row">
<span>合计税额</span>
<span>{{ erpPriceInputFormatter(summaryData.taxPrice) }}</span>
</div>
<div class="mobile-item-summary__row mobile-item-summary__row--total">
<span>税额合计</span>
<span>{{ erpPriceInputFormatter(summaryData.totalPrice) }}</span>
</div>
</div> </div>
</div> </div>
</el-form> </el-form>
<!-- PC端布局 -->
<el-form v-else ref="formRef" :model="formData" :rules="formRules" v-loading="formLoading" label-width="0px" :inline-message="true" :disabled="disabled">
<el-table :data="formData" show-summary :summary-method="getSummaries" class="-mt-10px">
<el-table-column label="序号" type="index" align="center" width="60" />
<el-table-column label="仓库名称" min-width="125">
<template #default="{ row, $index }"><el-form-item :prop="`${$index}.warehouseId`" :rules="formRules.warehouseId" class="mb-0px!"><el-select v-model="row.warehouseId" clearable filterable placeholder="请选择仓库" @change="onChangeWarehouse($event, row)"><el-option v-for="item in warehouseList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></template>
</el-table-column>
<el-table-column label="产品名称" min-width="180"><template #default="{ row }"><el-form-item class="mb-0px!"><el-input disabled v-model="row.productName" /></el-form-item></template></el-table-column>
<el-table-column label="库存" min-width="100"><template #default="{ row }"><el-form-item class="mb-0px!"><el-input disabled v-model="row.stockCount" :formatter="erpCountInputFormatter" /></el-form-item></template></el-table-column>
<el-table-column label="条码" min-width="150"><template #default="{ row }"><el-form-item class="mb-0px!"><el-input disabled v-model="row.productBarCode" /></el-form-item></template></el-table-column>
<el-table-column label="单位" min-width="80"><template #default="{ row }"><el-form-item class="mb-0px!"><el-input disabled v-model="row.productUnitName" /></el-form-item></template></el-table-column>
<el-table-column label="原数量" fixed="right" min-width="80" v-if="formData[0]?.totalCount != null"><template #default="{ row }"><el-form-item class="mb-0px!"><el-input disabled v-model="row.totalCount" :formatter="erpCountInputFormatter" /></el-form-item></template></el-table-column>
<el-table-column label="已入库" fixed="right" min-width="80" v-if="formData[0]?.inCount != null"><template #default="{ row }"><el-form-item class="mb-0px!"><el-input disabled v-model="row.inCount" :formatter="erpCountInputFormatter" /></el-form-item></template></el-table-column>
<el-table-column label="数量" prop="count" fixed="right" min-width="140"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.count`" :rules="formRules.count" class="mb-0px!"><el-input-number v-model="row.count" controls-position="right" :min="0.0001" :precision="4" class="!w-100%" /></el-form-item></template></el-table-column>
<el-table-column label="产品单价" fixed="right" min-width="120"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.productPrice`" class="mb-0px!"><el-input-number v-model="row.productPrice" controls-position="right" :min="0" :precision="4" class="!w-100%" /></el-form-item></template></el-table-column>
<el-table-column label="金额" prop="totalProductPrice" fixed="right" min-width="100"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.totalProductPrice`" class="mb-0px!"><el-input disabled v-model="row.totalProductPrice" :formatter="erpPriceInputFormatter" /></el-form-item></template></el-table-column>
<el-table-column label="税率(%" fixed="right" min-width="115"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.taxPercent`" class="mb-0px!"><el-input-number v-model="row.taxPercent" controls-position="right" :min="0" :precision="4" class="!w-100%" /></el-form-item></template></el-table-column>
<el-table-column label="税额" prop="taxPrice" fixed="right" min-width="120"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.taxPrice`" class="mb-0px!"><el-input disabled v-model="row.taxPrice" :formatter="erpPriceInputFormatter" /></el-form-item></template></el-table-column>
<el-table-column label="税额合计" prop="totalPrice" fixed="right" min-width="100"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.totalPrice`" class="mb-0px!"><el-input disabled v-model="row.totalPrice" :formatter="erpPriceInputFormatter" /></el-form-item></template></el-table-column>
<el-table-column label="备注" min-width="150"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.remark`" class="mb-0px!"><el-input v-model="row.remark" placeholder="请输入备注" /></el-form-item></template></el-table-column>
<el-table-column align="center" fixed="right" label="操作" width="60"><template #default="{ $index }"><el-button :disabled="formData.length === 1" @click="handleDelete($index)" link></el-button></template></el-table-column>
</el-table>
</el-form>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
@@ -153,6 +158,10 @@ import {
getSumValue getSumValue
} from '@/utils' } from '@/utils'
import { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse' import { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse'
import { useWindowSize } from '@vueuse/core'
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const props = defineProps<{ const props = defineProps<{
items: undefined items: undefined
@@ -208,7 +217,7 @@ watch(
{ deep: true } { deep: true }
) )
/** 合计 */ /** 合计 - 移动端 */
const summaryData = computed(() => { const summaryData = computed(() => {
const data = formData.value const data = formData.value
return { return {
@@ -219,6 +228,20 @@ const summaryData = computed(() => {
} }
}) })
/** 合计 - PC端 */
const getSummaries = (param: any) => {
const { columns, data } = param
const sums: string[] = []
columns.forEach((column, index: number) => {
if (index === 0) { sums[index] = '合计'; return }
if (['count', 'totalProductPrice', 'taxPrice', 'totalPrice'].includes(column.property)) {
const sum = getSumValue(data.map((item) => Number(item[column.property])))
sums[index] = column.property === 'count' ? erpCountInputFormatter(sum) : erpPriceInputFormatter(sum)
} else { sums[index] = '' }
})
return sums
}
/** 新增按钮操作 */ /** 新增按钮操作 */
const handleAdd = () => { const handleAdd = () => {
const row = { const row = {

View File

@@ -1,21 +1,36 @@
<template> <template>
<div class="mobile-purchase-in"> <!-- 移动端布局 -->
<!-- 顶部操作栏 --> <div v-if="isMobile" class="mobile-purchase-in">
<div class="mobile-header"> <div class="mobile-header">
<div class="mobile-header__search"> <div class="mobile-header__search"><el-input v-model="queryParams.no" placeholder="搜索入库单号" clearable @keyup.enter="handleQuery" :prefix-icon="Search" /></div>
<el-input
v-model="queryParams.no"
placeholder="搜索入库单号"
clearable
@keyup.enter="handleQuery"
:prefix-icon="Search"
/>
</div>
<div class="mobile-header__actions"> <div class="mobile-header__actions">
<el-button :icon="Filter" circle @click="filterVisible = true" /> <el-button :icon="Filter" circle @click="filterVisible = true" />
<el-button type="primary" :icon="Plus" circle @click="openForm('create')" v-hasPermi="['erp:purchase-in:create']" /> <el-button type="primary" :icon="Plus" circle @click="openForm('create')" v-hasPermi="['erp:purchase-in:create']" />
</div> </div>
</div> </div>
<div class="mobile-header__quick-filter">
<div
class="quick-filter-item"
:class="{ active: queryParams.status === undefined }"
@click="handleQuickFilter(undefined)"
>
全部订单
</div>
<div
class="quick-filter-item"
:class="{ active: queryParams.status === 10 }"
@click="handleQuickFilter(10)"
>
待审核
</div>
<div
class="quick-filter-item"
:class="{ active: queryParams.status === 20 }"
@click="handleQuickFilter(20)"
>
已审核
</div>
</div>
<!-- 卡片列表 --> <!-- 卡片列表 -->
<div class="mobile-list" v-loading="loading"> <div class="mobile-list" v-loading="loading">
@@ -136,40 +151,256 @@
<!-- 表单弹窗添加/修改 --> <!-- 表单弹窗添加/修改 -->
<PurchaseInForm ref="formRef" @success="getList" /> <PurchaseInForm ref="formRef" @success="getList" />
<!-- 审核弹窗 --> <!-- 审核弹窗(移动端) -->
<el-dialog v-model="auditVisible" title="采购入库审核" width="90%" append-to-body destroy-on-close> <el-dialog v-model="auditVisible" title="采购入库审核" width="90%" append-to-body destroy-on-close>
<el-form ref="auditFormRef" :model="auditForm" :rules="auditRules" label-position="top"> <el-form ref="auditFormRef" :model="auditForm" :rules="auditRules" label-position="top">
<el-form-item label="是否合格" prop="isQualified"> <el-form-item label="是否合格" prop="isQualified"><el-switch v-model="auditForm.isQualified" :active-value="true" :inactive-value="false" @change="handleQualifiedChange" /></el-form-item>
<el-switch v-model="auditForm.isQualified" :active-value="true" :inactive-value="false" @change="handleQualifiedChange" /> <el-form-item label="返回方式" prop="returnType"><el-select v-model="auditForm.returnType" placeholder="请选择返回方式" clearable style="width:100%" :disabled="auditForm.isQualified"><el-option v-for="option in RETURN_TYPE_OPTIONS" :key="option.value" :label="option.label" :value="option.value" /></el-select></el-form-item>
</el-form-item> <el-form-item label="返回备注" prop="returnRemark"><el-input v-model="auditForm.returnRemark" type="textarea" placeholder="请输入返回方式备注" :rows="3" :disabled="auditForm.isQualified" /></el-form-item>
<el-form-item label="返回方式" prop="returnType">
<el-select v-model="auditForm.returnType" placeholder="请选择返回方式" clearable style="width:100%" :disabled="auditForm.isQualified">
<el-option v-for="option in RETURN_TYPE_OPTIONS" :key="option.value" :label="option.label" :value="option.value" />
</el-select>
</el-form-item>
<el-form-item label="返回备注" prop="returnRemark">
<el-input v-model="auditForm.returnRemark" type="textarea" placeholder="请输入返回方式备注" :rows="3" :disabled="auditForm.isQualified" />
</el-form-item>
</el-form> </el-form>
<template #footer> <template #footer><el-button @click="auditVisible = false">取消</el-button><el-button type="primary" @click="submitAudit" :loading="auditLoading">确定</el-button></template>
<el-button @click="auditVisible = false">取消</el-button>
<el-button type="primary" @click="submitAudit" :loading="auditLoading">确定</el-button>
</template>
</el-dialog> </el-dialog>
</div> </div>
<!-- PC端布局 -->
<template v-else>
<ContentWrap>
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="入库单号" prop="no">
<el-input
v-model="queryParams.no"
placeholder="请输入入库单号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="产品" prop="productId">
<el-select
v-model="queryParams.productId"
clearable
filterable
placeholder="请选择产品"
class="!w-240px"
>
<el-option
v-for="item in productList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="入库时间" prop="inTime">
<el-date-picker
v-model="queryParams.inTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="供应商" prop="supplierId">
<el-select
v-model="queryParams.supplierId"
clearable
filterable
placeholder="请选择供供应商"
class="!w-240px"
>
<el-option
v-for="item in supplierList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="仓库" prop="warehouseId">
<el-select
v-model="queryParams.warehouseId"
clearable
filterable
placeholder="请选择仓库"
class="!w-240px"
>
<el-option
v-for="item in warehouseList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="创建人" prop="creator">
<el-select
v-model="queryParams.creator"
clearable
filterable
placeholder="请选择创建人"
class="!w-240px"
>
<el-option
v-for="item in userList"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="关联订单" prop="orderNo">
<el-input
v-model="queryParams.orderNo"
placeholder="请输入关联订单"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="结算账户" prop="accountId">
<el-select
v-model="queryParams.accountId"
clearable
filterable
placeholder="请选择结算账户"
class="!w-240px"
>
<el-option
v-for="item in accountList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="付款状态" prop="paymentStatus">
<el-select
v-model="queryParams.paymentStatus"
placeholder="请选择有款状态"
clearable
class="!w-240px"
>
<el-option label="未付款" value="0" />
<el-option label="部分付款" value="1" />
<el-option label="全部付款" value="2" />
</el-select>
</el-form-item>
<el-form-item label="审核状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择审核状态"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.ERP_AUDIT_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="queryParams.remark"
placeholder="请输入备注"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['erp:purchase-in:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['erp:purchase-in:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button
type="danger"
plain
@click="handleDelete(selectionList.map((item) => item.id))"
v-hasPermi="['erp:purchase-in:delete']"
:disabled="selectionList.length === 0"
>
<Icon icon="ep:delete" class="mr-5px" /> 删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" @selection-change="handleSelectionChange" @row-click="handleRowClick">
<el-table-column width="30" label="选择" type="selection" />
<el-table-column min-width="180" label="入库单号" align="center" prop="no" />
<el-table-column label="产品信息" align="center" prop="productNames" min-width="200" />
<el-table-column label="供应商" align="center" prop="supplierName" />
<el-table-column label="入库时间" align="center" prop="inTime" :formatter="dateFormatter2" width="120px" sortable />
<el-table-column label="创建人" align="center" prop="creatorName" />
<el-table-column label="总数量" align="center" prop="totalCount" :formatter="erpCountTableColumnFormatter" />
<el-table-column label="应付金额" align="center" prop="totalPrice" :formatter="erpPriceTableColumnFormatter" />
<el-table-column label="已付金额" align="center" prop="paymentPrice" :formatter="erpPriceTableColumnFormatter" />
<el-table-column label="未付金额" align="center"><template #default="scope"><span v-if="scope.row.paymentPrice === scope.row.totalPrice">0</span><el-tag type="danger" v-else>{{ erpPriceInputFormatter(scope.row.totalPrice - scope.row.paymentPrice) }}</el-tag></template></el-table-column>
<el-table-column label="审核状态" align="center" prop="status" width="120"><template #default="scope"><el-tag :type="scope.row.status === 10 ? 'info' : (scope.row.isQualified ? 'success' : 'danger')">{{ scope.row.status === 10 ? '未审核' : (scope.row.isQualified ? '已审核' : '不合格') }}</el-tag></template></el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="220">
<template #default="scope">
<el-button link type="primary" @click="openForm('detail', scope.row.id)" v-hasPermi="['erp:purchase-in:query']">详情</el-button>
<el-button v-if="scope.row.status === 10" link type="primary" @click="openForm('update', scope.row.id)" v-hasPermi="['erp:purchase-in:update']">修改</el-button>
<el-button v-if="scope.row.status === 10" link type="primary" @click="handleUpdateStatus(scope.row.id, scope.row.status)" v-hasPermi="['erp:purchase-in:update-status']">审核</el-button>
<el-button v-if="scope.row.status === 20 || scope.row.status === 30" link type="warning" @click="handleUpdateStatus(scope.row.id, scope.row.status)" v-hasPermi="['erp:purchase-in:update-status']">反审核</el-button>
<el-button v-if="scope.row.status === 10" link type="danger" @click="handleDelete([scope.row.id])" v-hasPermi="['erp:purchase-in:delete']">删除</el-button>
</template>
</el-table-column>
</el-table>
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" />
</ContentWrap>
<!-- 审核弹窗(PC端) -->
<el-dialog v-model="auditVisible" title="采购入库审核" width="500px" append-to-body destroy-on-close>
<el-form ref="auditFormRef" :model="auditForm" :rules="auditRules" label-width="100px">
<el-form-item label="是否合格" prop="isQualified"><el-switch v-model="auditForm.isQualified" :active-value="true" :inactive-value="false" @change="handleQualifiedChange" /></el-form-item>
<el-form-item label="返回方式" prop="returnType"><el-select v-model="auditForm.returnType" placeholder="请选择返回方式" clearable style="width:100%" :disabled="auditForm.isQualified"><el-option v-for="option in RETURN_TYPE_OPTIONS" :key="option.value" :label="option.label" :value="option.value" /></el-select></el-form-item>
<el-form-item label="返回备注" prop="returnRemark"><el-input v-model="auditForm.returnRemark" type="textarea" placeholder="请输入返回方式备注" :rows="3" :disabled="auditForm.isQualified" /></el-form-item>
</el-form>
<template #footer><el-button @click="auditVisible = false"> </el-button><el-button type="primary" @click="submitAudit" :loading="auditLoading"> </el-button></template>
</el-dialog>
</template>
<!-- 表单弹窗添加/修改 -->
<PurchaseInForm ref="formRef" @success="getList" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import { Search, Filter, Plus } from '@element-plus/icons-vue' import { Search, Filter, Plus } from '@element-plus/icons-vue'
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { formatDate } from '@/utils/formatTime' import { formatDate, dateFormatter2 } from '@/utils/formatTime'
import download from '@/utils/download' import download from '@/utils/download'
import { PurchaseInApi, PurchaseInVO, RETURN_TYPE_OPTIONS } from '@/api/erp/purchase/in' import { PurchaseInApi, PurchaseInVO, RETURN_TYPE_OPTIONS } from '@/api/erp/purchase/in'
import PurchaseInForm from './PurchaseInForm.vue' import PurchaseInForm from './PurchaseInForm.vue'
import { ProductApi, ProductVO } from '@/api/erp/product/product' import { ProductApi, ProductVO } from '@/api/erp/product/product'
import { UserVO } from '@/api/system/user' import { UserVO } from '@/api/system/user'
import * as UserApi from '@/api/system/user' import * as UserApi from '@/api/system/user'
import { erpCountInputFormatter, erpPriceInputFormatter } from '@/utils' import { erpCountInputFormatter, erpPriceInputFormatter, erpCountTableColumnFormatter, erpPriceTableColumnFormatter } from '@/utils'
import { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse' import { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse'
import { AccountApi, AccountVO } from '@/api/erp/finance/account' import { AccountApi, AccountVO } from '@/api/erp/finance/account'
import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier' import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier'
@@ -177,6 +408,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
import { useMessage } from '@/hooks/web/useMessage' import { useMessage } from '@/hooks/web/useMessage'
import { useI18n } from '@/hooks/web/useI18n' import { useI18n } from '@/hooks/web/useI18n'
import { ref, reactive, onMounted, onActivated } from 'vue' import { ref, reactive, onMounted, onActivated } from 'vue'
import { useWindowSize } from '@vueuse/core'
interface AuditFormData { interface AuditFormData {
id: number id: number
@@ -189,6 +421,9 @@ interface AuditFormData {
/** ERP 销售入库列表 */ /** ERP 销售入库列表 */
defineOptions({ name: 'ErpPurchaseIn' }) defineOptions({ name: 'ErpPurchaseIn' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const message = useMessage() const message = useMessage()
const { t } = useI18n() const { t } = useI18n()
const loading = ref(true) const loading = ref(true)
@@ -287,9 +522,40 @@ const handleDelete = async (ids: number[]) => {
await PurchaseInApi.deletePurchaseIn(ids) await PurchaseInApi.deletePurchaseIn(ids)
message.success(t('common.delSuccess')) message.success(t('common.delSuccess'))
await getList() await getList()
selectionList.value = selectionList.value.filter((item) => !ids.includes(item.id))
} catch {} } catch {}
} }
/** 选中操作(PC端) */
const selectionList = ref<PurchaseInVO[]>([])
const handleSelectionChange = (rows: PurchaseInVO[]) => {
selectionList.value = rows
}
/** 行点击操作(PC端) */
const handleRowClick = (row: PurchaseInVO, column: any, event: MouseEvent) => {
const target = event.target as HTMLElement
if (target.tagName === 'BUTTON' || target.tagName === 'A' || target.tagName === 'I' || target.tagName === 'svg' || target.closest('button') || target.closest('a') || target.closest('.el-button') || target.closest('.el-checkbox')) return
if (row.status === 20 || row.status === 30) {
openForm('detail', row.id)
} else if (row.status === 10) {
openForm('update', row.id)
}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
await message.exportConfirm()
exportLoading.value = true
const exportParams = selectionList.value.length > 0 ? { ids: selectionList.value.map(item => item.id) } : queryParams
const data = await PurchaseInApi.exportPurchaseIn(exportParams)
download.excel(data, '采购入库.xls')
} catch {} finally {
exportLoading.value = false
}
}
// 审核弹窗相关 // 审核弹窗相关
const auditVisible = ref(false) const auditVisible = ref(false)
const auditLoading = ref(false) const auditLoading = ref(false)
@@ -409,9 +675,40 @@ onActivated(() => {
getList() getList()
} }
}) })
/** 快捷分类筛选 */
const handleQuickFilter = (status: number | undefined) => {
queryParams.status = status
queryParams.pageNo = 1
getList()
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.mobile-header__quick-filter {
display: flex;
gap: 12px;
margin: 8px 0;
justify-content: flex-start;
.quick-filter-item {
padding: 4px 12px;
font-size: 14px;
border-radius: 20px;
cursor: pointer;
color: #909399;
background: transparent;
transition: all 0.2s;
&.active {
color: #fff;
background: #409eff;
}
}
}
.mobile-purchase-in { .mobile-purchase-in {
padding: 12px; padding: 12px;
background: #f5f5f5; background: #f5f5f5;

View File

@@ -1,132 +1,81 @@
<template> <template>
<el-drawer <!-- 移动端布局 -->
v-model="dialogVisible" <el-drawer v-if="isMobile" v-model="dialogVisible" :title="dialogTitle" direction="rtl" size="100%" :close-on-press-escape="true" :destroy-on-close="true" :append-to-body="true" class="mobile-form-drawer">
:title="dialogTitle"
direction="rtl"
size="100%"
:close-on-press-escape="true"
:destroy-on-close="true"
:append-to-body="true"
class="mobile-form-drawer"
>
<div class="mobile-form" v-loading="formLoading"> <div class="mobile-form" v-loading="formLoading">
<el-form <el-form ref="formRef" :model="formData" :rules="formRules" label-position="top" :disabled="disabled">
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
:disabled="disabled"
>
<!-- 基本信息 -->
<div class="mobile-form__section"> <div class="mobile-form__section">
<div class="mobile-form__section-title">基本信息</div> <div class="mobile-form__section-title">基本信息</div>
<el-form-item label="比价单号" prop="no"> <el-form-item label="比价单号" prop="no"><el-input disabled v-model="formData.no" placeholder="保存时自动生成" /></el-form-item>
<el-input disabled v-model="formData.no" placeholder="保存时自动生成" /> <el-form-item label="询价时间" prop="inquiryTime"><el-date-picker v-model="formData.inquiryTime" type="date" value-format="x" placeholder="选择询价时间" style="width: 100%" /></el-form-item>
</el-form-item> <el-form-item label="截止日期" prop="deadline"><el-date-picker v-model="formData.deadline" type="date" value-format="x" placeholder="选择截止日期" style="width: 100%" /></el-form-item>
<el-form-item label="询价时间" prop="inquiryTime"> <el-form-item label="产品" prop="productId"><el-select v-model="formData.productId" clearable filterable placeholder="请选择产品" style="width: 100%" @change="onChangeProduct"><el-option v-for="item in productList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item>
<el-date-picker
v-model="formData.inquiryTime"
type="date"
value-format="x"
placeholder="选择询价时间"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="截止日期" prop="deadline">
<el-date-picker
v-model="formData.deadline"
type="date"
value-format="x"
placeholder="选择截止日期"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="产品" prop="productId">
<el-select
v-model="formData.productId"
clearable
filterable
placeholder="请选择产品"
style="width: 100%"
@change="onChangeProduct"
>
<el-option
v-for="item in productList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<div class="mobile-form__input-group"> <div class="mobile-form__input-group">
<el-form-item label="需求数量" prop="requireCount"> <el-form-item label="需求数量" prop="requireCount"><el-input-number v-model="formData.requireCount" controls-position="right" :min="0.0001" :precision="4" placeholder="需求数量" style="width: 100%" /></el-form-item>
<el-input-number <el-form-item label="预算金额" prop="budgetPrice"><el-input-number v-model="formData.budgetPrice" controls-position="right" :min="0" :precision="2" placeholder="预算金额" style="width: 100%" /></el-form-item>
v-model="formData.requireCount"
controls-position="right"
:min="0.0001"
:precision="4"
placeholder="需求数量"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="预算金额" prop="budgetPrice">
<el-input-number
v-model="formData.budgetPrice"
controls-position="right"
:min="0"
:precision="2"
placeholder="预算金额"
style="width: 100%"
/>
</el-form-item>
</div> </div>
<el-form-item label="备注" prop="remark"> <el-form-item label="备注" prop="remark"><el-input type="textarea" v-model="formData.remark" :rows="2" placeholder="请输入备注" /></el-form-item>
<el-input type="textarea" v-model="formData.remark" :rows="2" placeholder="请输入备注" />
</el-form-item>
</div> </div>
<!-- 供应商报价 -->
<div class="mobile-form__section"> <div class="mobile-form__section">
<div class="mobile-form__section-title">供应商报价</div> <div class="mobile-form__section-title">供应商报价</div>
<InquiryQuoteForm ref="quoteFormRef" :quotes="formData.quotes" :disabled="disabled" :require-count="formData.requireCount" /> <InquiryQuoteForm ref="quoteFormRef" :quotes="formData.quotes" :disabled="disabled" :require-count="formData.requireCount" />
</div> </div>
<!-- 比价汇总 -->
<div class="mobile-form__section" v-if="formData.quotes && formData.quotes.length > 0"> <div class="mobile-form__section" v-if="formData.quotes && formData.quotes.length > 0">
<div class="mobile-form__section-title">比价汇总</div> <div class="mobile-form__section-title">比价汇总</div>
<div class="mobile-form__info-row"> <div class="mobile-form__info-row"><span class="mobile-form__info-label">报价数量</span><span class="mobile-form__info-value">{{ formData.quotes.length }}</span></div>
<span class="mobile-form__info-label">报价数量</span> <div class="mobile-form__info-row"><span class="mobile-form__info-label">最低报价</span><span class="mobile-form__info-value" style="color: #67c23a">{{ erpPriceInputFormatter(minQuotePrice) }}</span></div>
<span class="mobile-form__info-value">{{ formData.quotes.length }}</span> <div class="mobile-form__info-row"><span class="mobile-form__info-label">最高报价</span><span class="mobile-form__info-value" style="color: #e6a23c">{{ erpPriceInputFormatter(maxQuotePrice) }}</span></div>
</div>
<div class="mobile-form__info-row">
<span class="mobile-form__info-label">最低报价</span>
<span class="mobile-form__info-value" style="color: #67c23a">{{ erpPriceInputFormatter(minQuotePrice) }}</span>
</div>
<div class="mobile-form__info-row">
<span class="mobile-form__info-label">最高报价</span>
<span class="mobile-form__info-value" style="color: #e6a23c">{{ erpPriceInputFormatter(maxQuotePrice) }}</span>
</div>
</div> </div>
</el-form> </el-form>
<!-- 底部操作按钮 -->
<div class="mobile-form__footer"> <div class="mobile-form__footer">
<el-button @click="dialogVisible = false"> </el-button> <el-button @click="dialogVisible = false"> </el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled"> </el-button> <el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled"> </el-button>
</div> </div>
</div> </div>
</el-drawer> </el-drawer>
<!-- PC端布局 -->
<Dialog v-else :title="dialogTitle" v-model="dialogVisible" width="1200">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" v-loading="formLoading" :disabled="disabled">
<el-row :gutter="20">
<el-col :span="8"><el-form-item label="比价单号" prop="no"><el-input disabled v-model="formData.no" placeholder="保存时自动生成" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="询价时间" prop="inquiryTime"><el-date-picker v-model="formData.inquiryTime" type="date" value-format="x" placeholder="选择询价时间" class="!w-1/1" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="截止日期" prop="deadline"><el-date-picker v-model="formData.deadline" type="date" value-format="x" placeholder="选择截止日期" class="!w-1/1" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="产品" prop="productId"><el-select v-model="formData.productId" clearable filterable placeholder="请选择产品" class="!w-1/1" @change="onChangeProduct"><el-option v-for="item in productList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></el-col>
<el-col :span="8"><el-form-item label="需求数量" prop="requireCount"><el-input-number v-model="formData.requireCount" controls-position="right" :min="0.0001" :precision="4" placeholder="请输入需求数量" class="!w-1/1" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="预算金额" prop="budgetPrice"><el-input-number v-model="formData.budgetPrice" controls-position="right" :min="0" :precision="2" placeholder="请输入预算金额" class="!w-1/1" /></el-form-item></el-col>
<el-col :span="24"><el-form-item label="备注" prop="remark"><el-input type="textarea" v-model="formData.remark" :rows="2" placeholder="请输入备注" /></el-form-item></el-col>
</el-row>
<ContentWrap>
<el-tabs v-model="subTabsName" class="-mt-15px -mb-10px">
<el-tab-pane label="供应商报价" name="quote"><InquiryQuoteForm ref="quoteFormRef" :quotes="formData.quotes" :disabled="disabled" :require-count="formData.requireCount" /></el-tab-pane>
</el-tabs>
</ContentWrap>
<el-row :gutter="20" v-if="formData.quotes && formData.quotes.length > 0">
<el-col :span="8"><el-form-item label="报价数量"><el-input disabled :value="formData.quotes.length" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="最低报价"><el-input disabled :value="minQuotePrice" :formatter="erpPriceInputFormatter" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="最高报价"><el-input disabled :value="maxQuotePrice" :formatter="erpPriceInputFormatter" /></el-form-item></el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { nextTick } from 'vue' import { nextTick, computed } from 'vue'
import { PurchaseInquiryApi, PurchaseInquiryVO } from '@/api/erp/purchase/inquiry' import { PurchaseInquiryApi, PurchaseInquiryVO } from '@/api/erp/purchase/inquiry'
import InquiryQuoteForm from './components/InquiryQuoteForm.vue' import InquiryQuoteForm from './components/InquiryQuoteForm.vue'
import { ProductApi, ProductVO } from '@/api/erp/product/product' import { ProductApi, ProductVO } from '@/api/erp/product/product'
import { erpPriceInputFormatter } from '@/utils' import { erpPriceInputFormatter } from '@/utils'
import { useWindowSize } from '@vueuse/core'
/** ERP 采购比价表单 */ /** ERP 采购比价表单 */
defineOptions({ name: 'InquiryForm' }) defineOptions({ name: 'InquiryForm' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
@@ -166,7 +115,7 @@ const formRules = reactive({
const disabled = computed(() => formType.value === 'detail') const disabled = computed(() => formType.value === 'detail')
const formRef = ref() // 表单 Ref const formRef = ref() // 表单 Ref
const productList = ref<ProductVO[]>([]) // 产品列表 const productList = ref<ProductVO[]>([]) // 产品列表
const subTabsName = ref('quote') // PC端子表标签
const quoteFormRef = ref() const quoteFormRef = ref()
/** 计算最低报价 */ /** 计算最低报价 */

View File

@@ -1,13 +1,6 @@
<template> <template>
<el-form <!-- 移动端布局 -->
ref="formRef" <el-form v-if="isMobile" ref="formRef" :model="formData" :rules="formRules" v-loading="formLoading" label-position="top" :inline-message="true" :disabled="disabled">
:model="formData"
:rules="formRules"
v-loading="formLoading"
label-position="top"
:inline-message="true"
:disabled="disabled"
>
<div class="mobile-quote-list"> <div class="mobile-quote-list">
<div <div
v-for="(row, $index) in formData" v-for="(row, $index) in formData"
@@ -118,20 +111,50 @@
</div> </div>
<!-- 添加按钮 --> <!-- 添加按钮 -->
<div class="mobile-quote-add" v-if="!disabled"> <div class="mobile-quote-add" v-if="!disabled"><el-button @click="handleAdd" round>+ 添加供应商报价</el-button></div>
<el-button @click="handleAdd" round>+ 添加供应商报价</el-button>
</div>
</div> </div>
</el-form> </el-form>
<!-- PC端布局 -->
<template v-else>
<el-form ref="formRef" :model="formData" :rules="formRules" v-loading="formLoading" label-width="0px" :inline-message="true" :disabled="disabled">
<el-table :data="formData" show-summary :summary-method="getSummaries" class="-mt-10px" :row-class-name="tableRowClassName">
<el-table-column label="序号" type="index" align="center" width="60" />
<el-table-column label="选中" width="70" align="center"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.isSelected`" class="mb-0px!"><el-checkbox v-model="row.isSelected" @change="onSelectChange(row, $index)" /></el-form-item></template></el-table-column>
<el-table-column label="供应商" min-width="200"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.supplierId`" :rules="formRules.supplierId" class="mb-0px!"><el-select v-model="row.supplierId" clearable filterable @change="onChangeSupplier($event, row)" placeholder="请选择供应商" class="!w-full"><template #prefix v-if="!disabled"><el-button type="primary" link size="small" @click.stop="openQuickSupplierForm($index)" title="快速新增供应商" class="!p-0 !m-0"><Icon icon="ep:plus" /></el-button></template><el-option v-for="item in supplierList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></template></el-table-column>
<el-table-column label="联系人" min-width="120"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.contactName`" class="mb-0px!"><el-input v-model="row.contactName" placeholder="联系人" /></el-form-item></template></el-table-column>
<el-table-column label="联系电话" min-width="140"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.contactPhone`" class="mb-0px!"><el-input v-model="row.contactPhone" placeholder="联系电话" /></el-form-item></template></el-table-column>
<el-table-column label="报价日期" min-width="160"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.quoteDate`" class="mb-0px!"><el-date-picker v-model="row.quoteDate" type="date" value-format="x" placeholder="报价日期" class="!w-100%" /></el-form-item></template></el-table-column>
<el-table-column label="单价" prop="unitPrice" min-width="140"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.unitPrice`" :rules="formRules.unitPrice" class="mb-0px!"><el-input-number v-model="row.unitPrice" controls-position="right" :min="0" :precision="4" class="!w-100%" placeholder="单价" /></el-form-item></template></el-table-column>
<el-table-column label="总价" prop="totalPrice" min-width="120"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.totalPrice`" class="mb-0px!"><el-input disabled v-model="row.totalPrice" :formatter="erpPriceInputFormatter" /></el-form-item></template></el-table-column>
<el-table-column label="付款条件" min-width="150"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.paymentTerms`" class="mb-0px!"><el-select v-model="row.paymentTerms" placeholder="付款条件" clearable><el-option label="款到发货" value="款到发货" /><el-option label="货到付款" value="货到付款" /><el-option label="月结30天" value="月结30天" /><el-option label="月结60天" value="月结60天" /><el-option label="月结90天" value="月结90天" /><el-option label="预付30%" value="预付30%" /><el-option label="预付50%" value="预付50%" /><el-option label="其他" value="其他" /></el-select></el-form-item></template></el-table-column>
<el-table-column label="交货周期(天)" min-width="130"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.deliveryCycle`" class="mb-0px!"><el-input-number v-model="row.deliveryCycle" controls-position="right" :min="0" :precision="0" class="!w-100%" placeholder="天数" /></el-form-item></template></el-table-column>
<el-table-column label="资质文件" min-width="150"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.qualificationFile`" class="mb-0px!"><UploadFile :is-show-tip="false" v-model="row.qualificationFile" :limit="1" /></el-form-item></template></el-table-column>
<el-table-column label="备注" min-width="150"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.remark`" class="mb-0px!"><el-input v-model="row.remark" placeholder="请输入备注" /></el-form-item></template></el-table-column>
<el-table-column label="质量评分" width="100"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.qualityScore`" class="mb-0px!"><el-input-number v-model="row.qualityScore" controls-position="right" :min="0" :max="100" :precision="0" class="!w-100%" placeholder="0-100" /></el-form-item></template></el-table-column>
<el-table-column label="服务评分" width="100"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.serviceScore`" class="mb-0px!"><el-input-number v-model="row.serviceScore" controls-position="right" :min="0" :max="100" :precision="0" class="!w-100%" placeholder="0-100" /></el-form-item></template></el-table-column>
<el-table-column label="价格得分" width="90" align="center"><template #default="{ row }"><span :class="row.priceScore >= 100 ? 'text-green-600 font-bold' : ''">{{ row.priceScore?.toFixed(2) ?? '-' }}</span></template></el-table-column>
<el-table-column label="交付得分" width="90" align="center"><template #default="{ row }"><span :class="row.deliveryScore >= 100 ? 'text-green-600 font-bold' : ''">{{ row.deliveryScore?.toFixed(2) ?? '-' }}</span></template></el-table-column>
<el-table-column label="综合得分" width="100" align="center"><template #default="{ row }"><el-tag v-if="row.totalScore != null" :type="row.isSelected ? 'success' : 'info'">{{ row.totalScore?.toFixed(2) }}</el-tag><span v-else class="text-gray-400">-</span></template></el-table-column>
<el-table-column align="center" fixed="right" label="操作" width="60"><template #default="{ $index }"><el-button @click="handleDelete($index)" link></el-button></template></el-table-column>
</el-table>
</el-form>
<el-row justify="center" class="mt-3" v-if="!disabled"><el-button @click="handleAdd" round>+ 添加供应商报价</el-button></el-row>
</template>
<!-- 快速新增供应商弹窗 --> <!-- 快速新增供应商弹窗 -->
<QuickSupplierForm ref="quickSupplierFormRef" @success="onQuickSupplierSuccess" /> <QuickSupplierForm ref="quickSupplierFormRef" @success="onQuickSupplierSuccess" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier' import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier'
import { SupplierEvaluationApi } from '@/api/erp/purchase/supplierEvaluation' import { SupplierEvaluationApi } from '@/api/erp/purchase/supplierEvaluation'
import { erpPriceInputFormatter, erpPriceMultiply, getSumValue } from '@/utils' import { erpPriceInputFormatter, erpPriceMultiply, getSumValue } from '@/utils'
import { PurchaseInquiryQuoteVO } from '@/api/erp/purchase/inquiry' import { PurchaseInquiryQuoteVO } from '@/api/erp/purchase/inquiry'
import QuickSupplierForm from './QuickSupplierForm.vue' import QuickSupplierForm from './QuickSupplierForm.vue'
import { useWindowSize } from '@vueuse/core'
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@@ -241,11 +264,25 @@ watch(
} }
) )
/** 合计总价 */ /** 合计总价 - 移动端 */
const summaryTotalPrice = computed(() => { const summaryTotalPrice = computed(() => {
return getSumValue(formData.value.map((item) => Number(item.totalPrice || 0))) return getSumValue(formData.value.map((item) => Number(item.totalPrice || 0)))
}) })
/** 合计 - PC端 */
const getSummaries = (param: any) => {
const { columns, data } = param
const sums: string[] = []
columns.forEach((column, index: number) => {
if (index === 0) { sums[index] = '合计'; return }
if (['totalPrice'].includes(column.property)) {
const sum = getSumValue(data.map((item) => Number(item[column.property])))
sums[index] = erpPriceInputFormatter(sum)
} else { sums[index] = '' }
})
return sums
}
/** 新增按钮操作 */ /** 新增按钮操作 */
const handleAdd = () => { const handleAdd = () => {
const row: PurchaseInquiryQuoteVO = { const row: PurchaseInquiryQuoteVO = {

View File

@@ -1,34 +1,13 @@
<template> <template>
<el-drawer <!-- 移动端布局 -->
v-model="dialogVisible" <el-drawer v-if="isMobile" v-model="dialogVisible" title="快速新增供应商" direction="rtl" size="100%" :close-on-press-escape="true" :destroy-on-close="true">
title="快速新增供应商"
direction="rtl"
size="100%"
:close-on-press-escape="true"
:destroy-on-close="true"
>
<div class="mobile-quick-supplier" v-loading="formLoading"> <div class="mobile-quick-supplier" v-loading="formLoading">
<el-form <el-form ref="formRef" :model="formData" :rules="formRules" label-position="top">
ref="formRef" <el-form-item label="供应商名称" prop="name"><el-input v-model="formData.name" placeholder="请输入供应商名称" /></el-form-item>
:model="formData" <el-form-item label="联系人" prop="contact"><el-input v-model="formData.contact" placeholder="请输入联系人" /></el-form-item>
:rules="formRules" <el-form-item label="手机号码" prop="mobile"><el-input v-model="formData.mobile" placeholder="请输入手机号码" /></el-form-item>
label-position="top" <el-form-item label="联系电话" prop="telephone"><el-input v-model="formData.telephone" placeholder="请输入联系电话" /></el-form-item>
> <el-form-item label="备注" prop="remark"><el-input type="textarea" v-model="formData.remark" placeholder="请输入备注" :rows="2" /></el-form-item>
<el-form-item label="供应商名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入供应商名称" />
</el-form-item>
<el-form-item label="联系人" prop="contact">
<el-input v-model="formData.contact" placeholder="请输入联系人" />
</el-form-item>
<el-form-item label="手机号码" prop="mobile">
<el-input v-model="formData.mobile" placeholder="请输入手机号码" />
</el-form-item>
<el-form-item label="联系电话" prop="telephone">
<el-input v-model="formData.telephone" placeholder="请输入联系电话" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input type="textarea" v-model="formData.remark" placeholder="请输入备注" :rows="2" />
</el-form-item>
</el-form> </el-form>
</div> </div>
<template #footer> <template #footer>
@@ -36,14 +15,34 @@
<el-button @click="submitForm" type="primary" :loading="formLoading"> </el-button> <el-button @click="submitForm" type="primary" :loading="formLoading"> </el-button>
</template> </template>
</el-drawer> </el-drawer>
<!-- PC端布局 -->
<Dialog v-else title="快速新增供应商" v-model="dialogVisible" width="500px">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" v-loading="formLoading">
<el-form-item label="供应商名称" prop="name"><el-input v-model="formData.name" placeholder="请输入供应商名称" /></el-form-item>
<el-form-item label="联系人" prop="contact"><el-input v-model="formData.contact" placeholder="请输入联系人" /></el-form-item>
<el-form-item label="手机号码" prop="mobile"><el-input v-model="formData.mobile" placeholder="请输入手机号码" /></el-form-item>
<el-form-item label="联系电话" prop="telephone"><el-input v-model="formData.telephone" placeholder="请输入联系电话" /></el-form-item>
<el-form-item label="备注" prop="remark"><el-input type="textarea" v-model="formData.remark" placeholder="请输入备注" :rows="2" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false"> </el-button>
<el-button @click="submitForm" type="primary" :loading="formLoading"> </el-button>
</template>
</Dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier' import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier'
import { CommonStatusEnum } from '@/utils/constants' import { CommonStatusEnum } from '@/utils/constants'
import { useWindowSize } from '@vueuse/core'
defineOptions({ name: 'QuickSupplierForm' }) defineOptions({ name: 'QuickSupplierForm' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const message = useMessage() const message = useMessage()
const dialogVisible = ref(false) const dialogVisible = ref(false)

View File

@@ -1,5 +1,6 @@
<template> <template>
<div class="mobile-purchase-inquiry"> <!-- 移动端布局 -->
<div v-if="isMobile" class="mobile-purchase-inquiry">
<!-- 顶部操作栏 --> <!-- 顶部操作栏 -->
<div class="mobile-header"> <div class="mobile-header">
<div class="mobile-header__search"> <div class="mobile-header__search">
@@ -11,11 +12,16 @@
</div> </div>
</div> </div>
<div class="mobile-header__quick-filter">
<div class="quick-filter-item" :class="{ active: queryParams.status === undefined }" @click="handleQuickFilter(undefined)">全部订单</div>
<div class="quick-filter-item" :class="{ active: queryParams.status === 10 }" @click="handleQuickFilter(10)">待询价</div>
<div class="quick-filter-item" :class="{ active: queryParams.status === 20 }" @click="handleQuickFilter(20)">询价中</div>
<div class="quick-filter-item" :class="{ active: queryParams.status === 30 }" @click="handleQuickFilter(30)">已完成</div>
</div>
<!-- 卡片列表 --> <!-- 卡片列表 -->
<div class="mobile-list" v-loading="loading"> <div class="mobile-list" v-loading="loading">
<div v-if="list.length === 0 && !loading" class="mobile-empty"> <div v-if="list.length === 0 && !loading" class="mobile-empty"><el-empty description="暂无比价记录" /></div>
<el-empty description="暂无比价记录" />
</div>
<div v-for="item in list" :key="item.id" class="mobile-card" @click="handleCardClick(item)"> <div v-for="item in list" :key="item.id" class="mobile-card" @click="handleCardClick(item)">
<div class="mobile-card__header"> <div class="mobile-card__header">
<span class="mobile-card__no">{{ item.no }}</span> <span class="mobile-card__no">{{ item.no }}</span>
@@ -24,39 +30,15 @@
<el-tag v-else-if="item.status === 30" type="success" size="small">已完成</el-tag> <el-tag v-else-if="item.status === 30" type="success" size="small">已完成</el-tag>
</div> </div>
<div class="mobile-card__body"> <div class="mobile-card__body">
<div class="mobile-card__row"> <div class="mobile-card__row"><span class="mobile-card__label">产品</span><span class="mobile-card__value">{{ item.productName || '-' }}</span></div>
<span class="mobile-card__label">产品</span> <div class="mobile-card__row"><span class="mobile-card__label">询价时间</span><span class="mobile-card__value">{{ formatDate2(item.inquiryTime) }}</span></div>
<span class="mobile-card__value">{{ item.productName || '-' }}</span> <div class="mobile-card__row"><span class="mobile-card__label">截止日期</span><span class="mobile-card__value">{{ formatDate2(item.deadline) }}</span></div>
</div> <div class="mobile-card__row"><span class="mobile-card__label">创建人</span><span class="mobile-card__value">{{ item.creatorName || '-' }}</span></div>
<div class="mobile-card__row">
<span class="mobile-card__label">询价时间</span>
<span class="mobile-card__value">{{ formatDate2(item.inquiryTime) }}</span>
</div>
<div class="mobile-card__row">
<span class="mobile-card__label">截止日期</span>
<span class="mobile-card__value">{{ formatDate2(item.deadline) }}</span>
</div>
<div class="mobile-card__row">
<span class="mobile-card__label">创建人</span>
<span class="mobile-card__value">{{ item.creatorName || '-' }}</span>
</div>
<div class="mobile-card__nums"> <div class="mobile-card__nums">
<div class="mobile-card__num-item"> <div class="mobile-card__num-item"><div class="mobile-card__num-val">{{ erpCountInputFormatter(item.requireCount) }}</div><div class="mobile-card__num-label">需求数量</div></div>
<div class="mobile-card__num-val">{{ erpCountInputFormatter(item.requireCount) }}</div> <div class="mobile-card__num-item"><div class="mobile-card__num-val mobile-card__num-val--price">¥{{ erpPriceInputFormatter(item.budgetPrice) }}</div><div class="mobile-card__num-label">预算金额</div></div>
<div class="mobile-card__num-label">需求数量</div> <div class="mobile-card__num-item"><div class="mobile-card__num-val">{{ item.quoteCount || 0 }}</div><div class="mobile-card__num-label">报价数</div></div>
</div> <div class="mobile-card__num-item"><div class="mobile-card__num-val" style="color:#67c23a">¥{{ erpPriceInputFormatter(item.minQuotePrice) }}</div><div class="mobile-card__num-label">最低报价</div></div>
<div class="mobile-card__num-item">
<div class="mobile-card__num-val mobile-card__num-val--price">¥{{ erpPriceInputFormatter(item.budgetPrice) }}</div>
<div class="mobile-card__num-label">预算金额</div>
</div>
<div class="mobile-card__num-item">
<div class="mobile-card__num-val">{{ item.quoteCount || 0 }}</div>
<div class="mobile-card__num-label">报价数</div>
</div>
<div class="mobile-card__num-item">
<div class="mobile-card__num-val" style="color:#67c23a">¥{{ erpPriceInputFormatter(item.minQuotePrice) }}</div>
<div class="mobile-card__num-label">最低报价</div>
</div>
</div> </div>
</div> </div>
<div class="mobile-card__footer"> <div class="mobile-card__footer">
@@ -78,26 +60,10 @@
<!-- 筛选抽屉 --> <!-- 筛选抽屉 -->
<el-drawer v-model="filterVisible" title="筛选条件" direction="btt" size="60%"> <el-drawer v-model="filterVisible" title="筛选条件" direction="btt" size="60%">
<el-form :model="queryParams" ref="queryFormRef" label-position="top"> <el-form :model="queryParams" ref="queryFormRef" label-position="top">
<el-form-item label="产品" prop="productId"> <el-form-item label="产品" prop="productId"><el-select v-model="queryParams.productId" clearable filterable placeholder="请选择产品" style="width:100%"><el-option v-for="item in productList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item>
<el-select v-model="queryParams.productId" clearable filterable placeholder="请选择产品" style="width:100%"> <el-form-item label="询价时间" prop="inquiryTime"><el-date-picker v-model="queryParams.inquiryTime" value-format="YYYY-MM-DD HH:mm:ss" type="daterange" start-placeholder="开始" end-placeholder="结束" :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" style="width:100%" /></el-form-item>
<el-option v-for="item in productList" :key="item.id" :label="item.name" :value="item.id" /> <el-form-item label="状态" prop="status"><el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width:100%"><el-option label="待询价" :value="10" /><el-option label="询价中" :value="20" /><el-option label="已完成" :value="30" /></el-select></el-form-item>
</el-select> <el-form-item label="创建人" prop="creator"><el-select v-model="queryParams.creator" clearable filterable placeholder="请选择创建人" style="width:100%"><el-option v-for="item in userList" :key="item.id" :label="item.nickname" :value="item.id" /></el-select></el-form-item>
</el-form-item>
<el-form-item label="询价时间" prop="inquiryTime">
<el-date-picker v-model="queryParams.inquiryTime" value-format="YYYY-MM-DD HH:mm:ss" type="daterange" start-placeholder="开始" end-placeholder="结束" :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" style="width:100%" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width:100%">
<el-option label="待询价" :value="10" />
<el-option label="询价中" :value="20" />
<el-option label="已完成" :value="30" />
</el-select>
</el-form-item>
<el-form-item label="创建人" prop="creator">
<el-select v-model="queryParams.creator" clearable filterable placeholder="请选择创建人" style="width:100%">
<el-option v-for="item in userList" :key="item.id" :label="item.nickname" :value="item.id" />
</el-select>
</el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="resetQuery">重置</el-button> <el-button @click="resetQuery">重置</el-button>
@@ -110,11 +76,73 @@
<!-- 自动比价弹窗 --> <!-- 自动比价弹窗 -->
<AutoCompareDialog ref="autoCompareRef" @success="getList" /> <AutoCompareDialog ref="autoCompareRef" @success="getList" />
</div> </div>
<!-- PC端布局 -->
<div v-else>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
<el-form-item label="比价单号" prop="no"><el-input v-model="queryParams.no" placeholder="请输入比价单号" clearable @keyup.enter="handleQuery" class="!w-240px" /></el-form-item>
<el-form-item label="产品" prop="productId"><el-select v-model="queryParams.productId" clearable filterable placeholder="请选择产品" class="!w-240px"><el-option v-for="item in productList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item>
<el-form-item label="询价时间" prop="inquiryTime"><el-date-picker v-model="queryParams.inquiryTime" value-format="YYYY-MM-DD HH:mm:ss" type="daterange" start-placeholder="开始日期" end-placeholder="结束日期" :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" class="!w-240px" /></el-form-item>
<el-form-item label="状态" prop="status"><el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px"><el-option label="待询价" :value="10" /><el-option label="询价中" :value="20" /><el-option label="已完成" :value="30" /></el-select></el-form-item>
<el-form-item label="创建人" prop="creator"><el-select v-model="queryParams.creator" clearable filterable placeholder="请选择创建人" class="!w-240px"><el-option v-for="item in userList" :key="item.id" :label="item.nickname" :value="item.id" /></el-select></el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button type="primary" plain @click="openForm('create')" v-hasPermi="['erp:purchase-inquiry:create']"><Icon icon="ep:plus" class="mr-5px" /> 新增</el-button>
<el-button type="success" plain @click="handleExport" :loading="exportLoading" v-hasPermi="['erp:purchase-inquiry:export']"><Icon icon="ep:download" class="mr-5px" /> 导出</el-button>
<el-button type="danger" plain @click="handleDelete(selectionList.map((item) => item.id))" v-hasPermi="['erp:purchase-inquiry:delete']" :disabled="selectionList.length === 0"><Icon icon="ep:delete" class="mr-5px" /> 删除</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" @selection-change="handleSelectionChange" @row-click="handleRowClick">
<el-table-column width="30" label="选择" type="selection" />
<el-table-column min-width="180" label="比价单号" align="center" prop="no" />
<el-table-column label="产品名称" align="center" prop="productName" min-width="150" />
<el-table-column label="需求数量" align="center" prop="requireCount" :formatter="erpCountTableColumnFormatter" width="100" />
<el-table-column label="预算金额" align="center" prop="budgetPrice" :formatter="erpPriceTableColumnFormatter" width="120" />
<el-table-column label="询价时间" align="center" prop="inquiryTime" :formatter="dateFormatter2" width="120" sortable />
<el-table-column label="截止日期" align="center" prop="deadline" :formatter="dateFormatter2" width="120" sortable />
<el-table-column label="报价数量" align="center" prop="quoteCount" width="100" />
<el-table-column label="最低报价" align="center" prop="minQuotePrice" :formatter="erpPriceTableColumnFormatter" width="120" />
<el-table-column label="创建人" align="center" prop="creatorName" width="100" />
<el-table-column label="状态" align="center" fixed="right" width="100" prop="status">
<template #default="scope">
<el-tag v-if="scope.row.status === 10" type="info">待询价</el-tag>
<el-tag v-else-if="scope.row.status === 20" type="warning">询价中</el-tag>
<el-tag v-else-if="scope.row.status === 30" type="success">已完成</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="300">
<template #default="scope">
<el-button link @click="openForm('detail', scope.row.id)" v-hasPermi="['erp:purchase-inquiry:query']">详情</el-button>
<el-button link type="primary" @click="openForm('update', scope.row.id)" v-hasPermi="['erp:purchase-inquiry:update']" :disabled="scope.row.status === 30">编辑</el-button>
<el-button link type="primary" @click="handleUpdateStatus(scope.row.id, 20)" v-hasPermi="['erp:purchase-inquiry:update-status']" v-if="scope.row.status === 10">询价</el-button>
<el-button link type="warning" @click="openAutoCompare(scope.row.id)" v-hasPermi="['erp:purchase-inquiry:update']" v-if="scope.row.status === 20 && scope.row.quoteCount >= 2">比价</el-button>
<el-button link type="success" @click="handleUpdateStatus(scope.row.id, 30)" v-hasPermi="['erp:purchase-inquiry:update-status']" v-if="scope.row.status === 20">完成</el-button>
<el-button link type="danger" @click="handleDelete([scope.row.id])" v-hasPermi="['erp:purchase-inquiry:delete']">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" />
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<InquiryForm ref="formRef" @success="getList" />
<!-- 自动比价弹窗 -->
<AutoCompareDialog ref="autoCompareRef" @success="getList" />
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import { Search, Filter, Plus } from '@element-plus/icons-vue' import { Search, Filter, Plus } from '@element-plus/icons-vue'
import { formatDate } from '@/utils/formatTime' import { formatDate, dateFormatter2 } from '@/utils/formatTime'
import download from '@/utils/download' import download from '@/utils/download'
import { PurchaseInquiryApi, PurchaseInquiryVO } from '@/api/erp/purchase/inquiry' import { PurchaseInquiryApi, PurchaseInquiryVO } from '@/api/erp/purchase/inquiry'
import InquiryForm from './InquiryForm.vue' import InquiryForm from './InquiryForm.vue'
@@ -122,11 +150,15 @@ import AutoCompareDialog from './AutoCompareDialog.vue'
import { ProductApi, ProductVO } from '@/api/erp/product/product' import { ProductApi, ProductVO } from '@/api/erp/product/product'
import { UserVO } from '@/api/system/user' import { UserVO } from '@/api/system/user'
import * as UserApi from '@/api/system/user' import * as UserApi from '@/api/system/user'
import { erpCountInputFormatter, erpPriceInputFormatter } from '@/utils' import { erpCountInputFormatter, erpPriceInputFormatter, erpCountTableColumnFormatter, erpPriceTableColumnFormatter } from '@/utils'
import { useWindowSize } from '@vueuse/core'
/** ERP 采购比价列表 */ /** ERP 采购比价列表 */
defineOptions({ name: 'ErpPurchaseInquiry' }) defineOptions({ name: 'ErpPurchaseInquiry' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const message = useMessage() const message = useMessage()
const { t } = useI18n() const { t } = useI18n()
@@ -220,7 +252,10 @@ const handleExport = async () => {
try { try {
await message.exportConfirm() await message.exportConfirm()
exportLoading.value = true exportLoading.value = true
const data = await PurchaseInquiryApi.exportPurchaseInquiry(queryParams) const exportParams = selectionList.value.length > 0
? { ids: selectionList.value.map(item => item.id) }
: queryParams
const data = await PurchaseInquiryApi.exportPurchaseInquiry(exportParams)
download.excel(data, '采购比价.xls') download.excel(data, '采购比价.xls')
} catch { } catch {
} finally { } finally {
@@ -228,14 +263,65 @@ const handleExport = async () => {
} }
} }
/** 选中操作 */
const selectionList = ref<PurchaseInquiryVO[]>([])
const handleSelectionChange = (rows: PurchaseInquiryVO[]) => {
selectionList.value = rows
}
/** 行点击操作 */
const handleRowClick = (row: PurchaseInquiryVO, column: any, event: MouseEvent) => {
const target = event.target as HTMLElement
if (target.tagName === 'BUTTON' || target.tagName === 'A' || target.tagName === 'I' || target.tagName === 'svg' || target.closest('button') || target.closest('a') || target.closest('.el-button') || target.closest('.el-checkbox')) {
return
}
if (row.status === 30) {
openForm('detail', row.id)
} else {
openForm('update', row.id)
}
}
onMounted(async () => { onMounted(async () => {
await getList() await getList()
productList.value = await ProductApi.getProductSimpleList() productList.value = await ProductApi.getProductSimpleList()
userList.value = await UserApi.getSimpleUserList() userList.value = await UserApi.getSimpleUserList()
}) })
/** 快捷分类筛选 */
const handleQuickFilter = (status: number | undefined) => {
queryParams.status = status
queryParams.pageNo = 1
getList()
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.mobile-header__quick-filter {
display: flex;
gap: 12px;
margin: 8px 0;
justify-content: flex-start;
.quick-filter-item {
padding: 4px 12px;
font-size: 14px;
border-radius: 20px;
cursor: pointer;
color: #909399;
background: transparent;
transition: all 0.2s;
&.active {
color: #fff;
background: #409eff;
}
}
}
.mobile-purchase-inquiry { .mobile-purchase-inquiry {
padding: 12px; padding: 12px;
background: #f5f5f5; background: #f5f5f5;

View File

@@ -1,136 +1,30 @@
<template> <template>
<el-drawer <!-- 移动端布局 -->
v-model="dialogVisible" <el-drawer v-if="isMobile" v-model="dialogVisible" :title="dialogTitle" direction="rtl" size="100%" :close-on-press-escape="true" :destroy-on-close="true" :append-to-body="true" class="mobile-form-drawer">
:title="dialogTitle"
direction="rtl"
size="100%"
:close-on-press-escape="true"
:destroy-on-close="true"
:append-to-body="true"
class="mobile-form-drawer"
>
<div class="mobile-form" v-loading="formLoading"> <div class="mobile-form" v-loading="formLoading">
<el-form <el-form ref="formRef" :model="formData" :rules="formRules" label-position="top" :disabled="disabled">
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
:disabled="disabled"
>
<!-- 基本信息 -->
<div class="mobile-form__section"> <div class="mobile-form__section">
<div class="mobile-form__section-title">基本信息</div> <div class="mobile-form__section-title">基本信息</div>
<el-form-item label="订单单号" prop="no"> <el-form-item label="订单单号" prop="no"><el-input disabled v-model="formData.no" placeholder="保存时自动生成" /></el-form-item>
<el-input disabled v-model="formData.no" placeholder="保存时自动生成" /> <el-form-item label="订单时间" prop="orderTime"><el-date-picker v-model="formData.orderTime" type="date" value-format="x" placeholder="选择订单时间" style="width:100%" /></el-form-item>
</el-form-item> <el-form-item label="关联请购单" prop="purchaseRequisitionNo"><el-input readonly><template #prefix><el-link v-if="formData.purchaseRequisitionId" type="primary" :underline="false" @click.stop="openRequisitionDetail">{{ formData.purchaseRequisitionNo }}</el-link></template><template #append><el-button @click="openRequisitionSelect"><Icon icon="ep:search" /></el-button></template></el-input></el-form-item>
<el-form-item label="订单时间" prop="orderTime"> <el-form-item label="供应商" prop="supplierId"><el-select v-model="formData.supplierId" clearable filterable placeholder="请选择供应商" style="width:100%"><el-option v-for="item in supplierList" :key="item.id" :label="item.name" :value="item.id" /></el-select><div class="mobile-form__tip">*如果没有供应商请忽略此项填写</div></el-form-item>
<el-date-picker <el-form-item label="备注" prop="remark"><el-input type="textarea" v-model="formData.remark" :rows="3" placeholder="请输入备注" /></el-form-item>
v-model="formData.orderTime" <el-form-item label="附件" prop="fileUrl"><UploadFile :is-show-tip="false" v-model="formData.fileUrl" :limit="1" /></el-form-item>
type="date"
value-format="x"
placeholder="选择订单时间"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="关联请购单" prop="purchaseRequisitionNo">
<div class="mobile-form__requisition">
<el-link
v-if="formData.purchaseRequisitionNo && formData.purchaseRequisitionId"
type="primary"
:underline="false"
@click.stop="openRequisitionDetail"
>
{{ formData.purchaseRequisitionNo }}
</el-link>
<span v-else class="mobile-form__requisition-empty">未选择</span>
<el-button size="small" @click="openRequisitionSelect" :disabled="disabled">选择</el-button>
</div>
</el-form-item>
<el-form-item label="供应商" prop="supplierId">
<el-select
v-model="formData.supplierId"
clearable
filterable
placeholder="请选择供应商"
style="width: 100%"
>
<el-option
v-for="item in supplierList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
<div class="mobile-form__tip">*如果没有供应商请忽略此项填写</div>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
type="textarea"
v-model="formData.remark"
:rows="3"
placeholder="请输入备注"
/>
</el-form-item>
<el-form-item label="附件" prop="fileUrl">
<UploadFile :is-show-tip="false" v-model="formData.fileUrl" :limit="1" />
</el-form-item>
</div> </div>
<!-- 产品清单 -->
<div class="mobile-form__section"> <div class="mobile-form__section">
<div class="mobile-form__section-title">订单产品清单</div> <div class="mobile-form__section-title">订单产品清单</div>
<PurchaseOrderItemForm ref="itemFormRef" :items="formData.items" :disabled="disabled" /> <PurchaseOrderItemForm ref="itemFormRef" :items="formData.items" :disabled="disabled" />
</div> </div>
<!-- 费用信息 -->
<div class="mobile-form__section"> <div class="mobile-form__section">
<div class="mobile-form__section-title">费用信息</div> <div class="mobile-form__section-title">费用信息</div>
<el-form-item label="优惠率(%" prop="discountPercent"> <el-form-item label="优惠率(%" prop="discountPercent"><el-input-number v-model="formData.discountPercent" controls-position="right" :min="0" :precision="2" placeholder="请输入优惠率" style="width:100%" /></el-form-item>
<el-input-number <el-form-item label="付款优惠" prop="discountPrice"><el-input disabled v-model="formData.discountPrice" :formatter="erpPriceInputFormatter" /></el-form-item>
v-model="formData.discountPercent" <el-form-item label="优惠后金额"><el-input disabled v-model="formData.totalPrice" :formatter="erpPriceInputFormatter" /></el-form-item>
controls-position="right" <el-form-item label="结算账户" prop="accountId"><el-select v-model="formData.accountId" clearable filterable placeholder="请选择结算账户" style="width:100%"><el-option v-for="item in accountList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item>
:min="0" <el-form-item label="支付订金" prop="depositPrice"><el-input-number v-model="formData.depositPrice" controls-position="right" :min="0" :precision="2" placeholder="请输入支付订金" style="width:100%" /></el-form-item>
:precision="2"
placeholder="请输入优惠率"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="付款优惠" prop="discountPrice">
<el-input disabled v-model="formData.discountPrice" :formatter="erpPriceInputFormatter" />
</el-form-item>
<el-form-item label="优惠后金额">
<el-input disabled v-model="formData.totalPrice" :formatter="erpPriceInputFormatter" />
</el-form-item>
<el-form-item label="结算账户" prop="accountId">
<el-select
v-model="formData.accountId"
clearable
filterable
placeholder="请选择结算账户"
style="width: 100%"
>
<el-option
v-for="item in accountList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="支付订金" prop="depositPrice">
<el-input-number
v-model="formData.depositPrice"
controls-position="right"
:min="0"
:precision="2"
placeholder="请输入支付订金"
style="width: 100%"
/>
</el-form-item>
</div> </div>
</el-form> </el-form>
<!-- 底部操作按钮 -->
<div class="mobile-form__footer"> <div class="mobile-form__footer">
<el-button @click="dialogVisible = false"> </el-button> <el-button @click="dialogVisible = false"> </el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled"> </el-button> <el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled"> </el-button>
@@ -138,13 +32,46 @@
</div> </div>
</el-drawer> </el-drawer>
<!-- PC端布局 -->
<Dialog v-else :title="dialogTitle" v-model="dialogVisible" width="1440">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" v-loading="formLoading" :disabled="disabled">
<el-row :gutter="20">
<el-col :span="8"><el-form-item label="订单单号" prop="no"><el-input disabled v-model="formData.no" placeholder="保存时自动生成" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="订单时间" prop="orderTime"><el-date-picker v-model="formData.orderTime" type="date" value-format="x" placeholder="选择订单时间" class="!w-1/1" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="关联请购单" prop="purchaseRequisitionNo"><el-input readonly><template #prefix><el-link v-if="formData.purchaseRequisitionNo && formData.purchaseRequisitionId" type="primary" :underline="false" @click.stop="openRequisitionDetail" class="requisition-link">{{ formData.purchaseRequisitionNo }}</el-link></template><template #append><el-button @click="openRequisitionSelect" :disabled="disabled"><Icon icon="ep:search" /> 选择</el-button></template></el-input></el-form-item></el-col>
<el-col :span="8"><el-form-item label="供应商" prop="supplierId"><el-select v-model="formData.supplierId" clearable filterable placeholder="请选择供应商" class="!w-1/1"><el-option v-for="item in supplierList" :key="item.id" :label="item.name" :value="item.id" /></el-select><div style="margin-top:4px;font-size:12px;color:var(--el-color-danger);">*如果没有供应商请忽略此项填写</div></el-form-item></el-col>
<el-col :span="16"><el-form-item label="备注" prop="remark"><el-input type="textarea" v-model="formData.remark" :rows="3" placeholder="请输入备注(如果有额外的内容填写,请在此文本框里面填写)" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="附件" prop="fileUrl"><UploadFile :is-show-tip="false" v-model="formData.fileUrl" :limit="1" /></el-form-item></el-col>
</el-row>
<ContentWrap>
<el-tabs v-model="subTabsName" class="-mt-15px -mb-10px">
<el-tab-pane label="订单产品清单" name="item"><PurchaseOrderItemForm ref="itemFormRef" :items="formData.items" :disabled="disabled" /></el-tab-pane>
</el-tabs>
</ContentWrap>
<el-row :gutter="20">
<el-col :span="8"><el-form-item label="优惠率(%" prop="discountPercent"><el-input-number v-model="formData.discountPercent" controls-position="right" :min="0" :precision="2" placeholder="请输入优惠率" class="!w-1/1" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="付款优惠" prop="discountPrice"><el-input disabled v-model="formData.discountPrice" :formatter="erpPriceInputFormatter" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="优惠后金额"><el-input disabled v-model="formData.totalPrice" :formatter="erpPriceInputFormatter" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="结算账户" prop="accountId"><el-select v-model="formData.accountId" clearable filterable placeholder="请选择结算账户" class="!w-1/1"><el-option v-for="item in accountList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></el-col>
<el-col :span="8"><el-form-item label="支付订金" prop="depositPrice"><el-input-number v-model="formData.depositPrice" controls-position="right" :min="0" :precision="2" placeholder="请输入支付订金" class="!w-1/1" /></el-form-item></el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled"> </el-button>
<el-button @click="handlePrint" type="warning">
<Icon icon="ep:printer" class="mr-5px" />
</el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
<!-- 请购单选择弹窗 --> <!-- 请购单选择弹窗 -->
<PurchaseRequisitionTableSelect ref="requisitionSelectRef" @change="onRequisitionSelected" /> <PurchaseRequisitionTableSelect ref="requisitionSelectRef" @change="onRequisitionSelected" />
<!-- 请购单详情弹窗 --> <!-- 请购单详情弹窗 -->
<PurchaseRequisitionForm ref="requisitionFormRef" /> <PurchaseRequisitionForm ref="requisitionFormRef" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { nextTick } from 'vue' import { nextTick, computed } from 'vue'
import { PurchaseOrderApi, PurchaseOrderVO } from '@/api/erp/purchase/order' import { PurchaseOrderApi, PurchaseOrderVO } from '@/api/erp/purchase/order'
import PurchaseOrderItemForm from './components/PurchaseOrderItemForm.vue' import PurchaseOrderItemForm from './components/PurchaseOrderItemForm.vue'
import PurchaseRequisitionTableSelect from './components/PurchaseRequisitionTableSelect.vue' import PurchaseRequisitionTableSelect from './components/PurchaseRequisitionTableSelect.vue'
@@ -154,10 +81,14 @@ import { erpPriceInputFormatter, erpPriceMultiply } from '@/utils'
import * as UserApi from '@/api/system/user' import * as UserApi from '@/api/system/user'
import { AccountApi, AccountVO } from '@/api/erp/finance/account' import { AccountApi, AccountVO } from '@/api/erp/finance/account'
import { PurchaseRequisitionApi, PurchaseRequisition } from '@/api/erp/purchaserequisition' import { PurchaseRequisitionApi, PurchaseRequisition } from '@/api/erp/purchaserequisition'
import { useWindowSize } from '@vueuse/core'
/** ERP 销售订单表单 */ /** ERP 采购订单表单 */
defineOptions({ name: 'PurchaseOrderForm' }) defineOptions({ name: 'PurchaseOrderForm' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
@@ -215,7 +146,7 @@ const accountList = ref<AccountVO[]>([]) // 账户列表
const userList = ref<UserApi.UserVO[]>([]) // 用户列表 const userList = ref<UserApi.UserVO[]>([]) // 用户列表
const requisitionSelectRef = ref() // 请购单选择弹窗Ref const requisitionSelectRef = ref() // 请购单选择弹窗Ref
const requisitionFormRef = ref() // 请购单详情弹窗Ref const requisitionFormRef = ref() // 请购单详情弹窗Ref
const subTabsName = ref('item')
const itemFormRef = ref() const itemFormRef = ref()
/** 计算 discountPrice、totalPrice 价格 */ /** 计算 discountPrice、totalPrice 价格 */
@@ -379,6 +310,161 @@ const resetForm = () => {
formRef.value?.resetFields() formRef.value?.resetFields()
} }
/** 打印当前页面数据 */
const handlePrint = () => {
// 获取供应商名称
const supplier = supplierList.value.find((item) => item.id === formData.value.supplierId)
const supplierName = supplier ? supplier.name : ''
// 获取结算账户名称
const account = accountList.value.find((item) => item.id === formData.value.accountId)
const accountName = account ? account.name : ''
// 格式化订单时间
const orderTime = formData.value.orderTime
? new Date(formData.value.orderTime).toLocaleDateString('zh-CN')
: ''
// 构建产品清单表格
let itemsTableHtml = ''
if (formData.value.items && formData.value.items.length > 0) {
// 计算合计
const totalCount = formData.value.items.reduce((sum, item) => sum + (item.count || 0), 0)
const totalProductPrice = formData.value.items.reduce((sum, item) => sum + (item.totalProductPrice || 0), 0)
const totalTaxPrice = formData.value.items.reduce((sum, item) => sum + (item.taxPrice || 0), 0)
const totalPriceSum = formData.value.items.reduce((sum, item) => sum + (item.totalPrice || 0), 0)
itemsTableHtml = `
<table border="1" cellpadding="6" cellspacing="0" style="width:100%; border-collapse: collapse; margin-top: 10px; font-size: 12px;">
<thead>
<tr style="background-color: #f5f5f5;">
<th>序号</th>
<th>产品名称</th>
<th>库存</th>
<th>条码</th>
<th>单位</th>
<th>数量</th>
<th>产品单价</th>
<th>金额</th>
<th>税率(%)</th>
<th>税额</th>
<th>税额合计</th>
<th>备注</th>
</tr>
</thead>
<tbody>
${formData.value.items
.map(
(item, index) => `
<tr>
<td style="text-align: center;">${index + 1}</td>
<td>${item.productName || ''}</td>
<td style="text-align: right;">${item.stockCount || 0}</td>
<td>${item.productBarCode || ''}</td>
<td style="text-align: center;">${item.productUnitName || ''}</td>
<td style="text-align: right;">${item.count || 0}</td>
<td style="text-align: right;">${item.productPrice || 0}</td>
<td style="text-align: right;">${item.totalProductPrice || 0}</td>
<td style="text-align: right;">${item.taxPercent || 0}</td>
<td style="text-align: right;">${item.taxPrice || 0}</td>
<td style="text-align: right;">${item.totalPrice || 0}</td>
<td>${item.remark || ''}</td>
</tr>
`
)
.join('')}
</tbody>
<tfoot>
<tr style="background-color: #f9f9f9; font-weight: bold;">
<td style="text-align: center;">合计</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td style="text-align: right;">${totalCount}</td>
<td></td>
<td style="text-align: right;">${totalProductPrice}</td>
<td></td>
<td style="text-align: right;">${totalTaxPrice}</td>
<td style="text-align: right;">${totalPriceSum}</td>
<td></td>
</tr>
</tfoot>
</table>
`
}
// 构建打印内容
const printContent = `
<html>
<head>
<title>采购订单打印</title>
<style>
body { font-family: 'Microsoft YaHei', Arial, sans-serif; padding: 20px; }
h1 { text-align: center; margin-bottom: 20px; }
.info-row { display: flex; flex-wrap: wrap; margin-bottom: 10px; }
.info-item { width: 33%; margin-bottom: 10px; }
.info-label { font-weight: bold; color: #666; }
.info-value { margin-left: 10px; }
.section-title { font-size: 16px; font-weight: bold; margin: 20px 0 10px; border-bottom: 1px solid #ddd; padding-bottom: 5px; }
table { font-size: 14px; }
th, td { padding: 8px; text-align: left; }
@media print {
body { padding: 0; }
}
</style>
</head>
<body>
<h1>采购订单</h1>
<div class="info-row">
<div class="info-item"><span class="info-label">订单单号:</span><span class="info-value">${formData.value.no || '(保存后生成)'}</span></div>
<div class="info-item"><span class="info-label">订单时间:</span><span class="info-value">${orderTime}</span></div>
<div class="info-item"><span class="info-label">关联请购单:</span><span class="info-value">${formData.value.purchaseRequisitionNo || ''}</span></div>
<div class="info-item"><span class="info-label">供应商:</span><span class="info-value">${supplierName}</span></div>
<div class="info-item"><span class="info-label">结算账户:</span><span class="info-value">${accountName}</span></div>
<div class="info-item"><span class="info-label">支付订金:</span><span class="info-value">${formData.value.depositPrice || 0}</span></div>
</div>
<div class="info-row">
<div class="info-item" style="width: 100%;"><span class="info-label">备注:</span><span class="info-value">${formData.value.remark || ''}</span></div>
</div>
<div class="section-title">订单产品清单</div>
${itemsTableHtml}
<div class="info-row" style="margin-top: 20px;">
<div class="info-item"><span class="info-label">优惠率(%):</span><span class="info-value">${formData.value.discountPercent || 0}</span></div>
<div class="info-item"><span class="info-label">付款优惠:</span><span class="info-value">${formData.value.discountPrice || 0}</span></div>
<div class="info-item"><span class="info-label">优惠后金额:</span><span class="info-value">${formData.value.totalPrice || 0}</span></div>
</div>
</body>
</html>
`
// 使用iframe打印避免被浏览器拦截
const iframe = document.createElement('iframe')
iframe.style.position = 'absolute'
iframe.style.width = '0'
iframe.style.height = '0'
iframe.style.border = 'none'
iframe.style.left = '-9999px'
document.body.appendChild(iframe)
const iframeDoc = iframe.contentWindow?.document
if (iframeDoc) {
iframeDoc.open()
iframeDoc.write(printContent)
iframeDoc.close()
// 等待内容加载完成后打印
iframe.contentWindow?.focus()
setTimeout(() => {
iframe.contentWindow?.print()
// 打印完成后移除iframe
setTimeout(() => {
document.body.removeChild(iframe)
}, 1000)
}, 250)
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -1,6 +1,8 @@
<!-- 可入库的订单列表 --> <!-- 可入库的订单列表 -->
<template> <template>
<!-- 移动端布局 -->
<el-drawer <el-drawer
v-if="isMobile"
v-model="dialogVisible" v-model="dialogVisible"
title="选择采购订单(仅展示可入库)" title="选择采购订单(仅展示可入库)"
direction="rtl" direction="rtl"
@@ -111,15 +113,50 @@
<el-button @click="dialogVisible = false"> </el-button> <el-button @click="dialogVisible = false"> </el-button>
</template> </template>
</el-drawer> </el-drawer>
<!-- PC端布局 -->
<Dialog v-else title="选择采购订单(仅展示可入库)" v-model="dialogVisible" :appendToBody="true" :scroll="true" width="1080">
<ContentWrap>
<el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px" class="-mb-15px">
<el-form-item label="订单单号" prop="no"><el-input v-model="queryParams.no" placeholder="请输入订单单号" clearable @keyup.enter="handleQuery" class="!w-160px" /></el-form-item>
<el-form-item label="产品" prop="productId"><el-select v-model="queryParams.productId" clearable filterable placeholder="请选择产品" class="!w-160px"><el-option v-for="item in productList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item>
<el-form-item label="订单时间" prop="orderTime"><el-date-picker v-model="queryParams.orderTime" value-format="YYYY-MM-DD HH:mm:ss" type="daterange" start-placeholder="开始日期" end-placeholder="结束日期" :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" class="!w-160px" /></el-form-item>
<el-form-item><el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button><el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button></el-form-item>
</el-form>
</ContentWrap>
<ContentWrap>
<el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
<el-table-column align="center" width="65"><template #default="{ row }"><el-radio :value="row.id" v-model="currentRowValue" @change="handleCurrentChange(row)">&nbsp;</el-radio></template></el-table-column>
<el-table-column label="订单单号" align="center" prop="no" min-width="180" />
<el-table-column label="供应商" align="center" prop="supplierName" />
<el-table-column label="产品信息" align="center" prop="productNames" min-width="200" />
<el-table-column label="订单时间" align="center" prop="orderTime" :formatter="dateFormatter2" width="120px" />
<el-table-column label="总数量" align="center" prop="totalCount" :formatter="erpCountTableColumnFormatter" />
<el-table-column label="入库数量" align="center" prop="inCount" :formatter="erpCountTableColumnFormatter" />
<el-table-column label="金额合计" align="center" prop="totalProductPrice" :formatter="erpPriceTableColumnFormatter" />
<el-table-column label="含税金额" align="center" prop="totalPrice" :formatter="erpPriceTableColumnFormatter" />
</el-table>
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" />
</ContentWrap>
<template #footer>
<el-button :disabled="!currentRow" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'
import { Search, Filter } from '@element-plus/icons-vue' import { Search, Filter } from '@element-plus/icons-vue'
import { PurchaseOrderApi, PurchaseOrderVO } from '@/api/erp/purchase/order' import { PurchaseOrderApi, PurchaseOrderVO } from '@/api/erp/purchase/order'
import { formatDate } from '@/utils/formatTime' import { formatDate, dateFormatter2 } from '@/utils/formatTime'
import { erpCountInputFormatter, erpPriceInputFormatter } from '@/utils' import { erpCountInputFormatter, erpPriceInputFormatter, erpCountTableColumnFormatter, erpPriceTableColumnFormatter } from '@/utils'
import { ProductApi, ProductVO } from '@/api/erp/product/product' import { ProductApi, ProductVO } from '@/api/erp/product/product'
import { useWindowSize } from '@vueuse/core'
defineOptions({ name: 'ErpPurchaseOrderOutEnableList' }) defineOptions({ name: 'ErpPurchaseOrderInEnableList' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const list = ref<PurchaseOrderVO[]>([]) // 列表的数据 const list = ref<PurchaseOrderVO[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数 const total = ref(0) // 列表的总页数

View File

@@ -1,13 +1,6 @@
<template> <template>
<el-form <!-- 移动端布局 -->
ref="formRef" <el-form v-if="isMobile" ref="formRef" :model="formData" :rules="formRules" v-loading="formLoading" label-position="top" :inline-message="true" :disabled="disabled">
:model="formData"
:rules="formRules"
v-loading="formLoading"
label-position="top"
:inline-message="true"
:disabled="disabled"
>
<div class="mobile-item-list"> <div class="mobile-item-list">
<div <div
v-for="(row, $index) in formData" v-for="(row, $index) in formData"
@@ -139,11 +132,32 @@
</div> </div>
<!-- 添加按钮 --> <!-- 添加按钮 -->
<div class="mobile-item-add" v-if="!disabled"> <div class="mobile-item-add" v-if="!disabled"><el-button @click="handleAdd" round>+ 添加采购产品</el-button></div>
<el-button @click="handleAdd" round>+ 添加采购产品</el-button>
</div>
</div> </div>
</el-form> </el-form>
<!-- PC端布局 -->
<template v-else>
<el-form ref="formRef" :model="formData" :rules="formRules" v-loading="formLoading" label-width="0px" :inline-message="true" :disabled="disabled">
<el-table :data="formData" show-summary :summary-method="getSummaries" class="-mt-10px">
<el-table-column label="序号" type="index" align="center" width="60" />
<el-table-column label="产品名称" min-width="180"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.productId`" :rules="formRules.productId" class="mb-0px!"><el-select v-model="row.productId" clearable filterable @change="onChangeProduct($event, row)" placeholder="请选择产品"><el-option v-for="item in productList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></template></el-table-column>
<el-table-column label="库存" min-width="100"><template #default="{ row }"><el-form-item class="mb-0px!"><el-input disabled v-model="row.stockCount" :formatter="erpCountInputFormatter" /></el-form-item></template></el-table-column>
<el-table-column label="条码" min-width="150"><template #default="{ row }"><el-form-item class="mb-0px!"><el-input disabled v-model="row.productBarCode" /></el-form-item></template></el-table-column>
<el-table-column label="单位" min-width="80"><template #default="{ row }"><el-form-item class="mb-0px!"><el-input disabled v-model="row.productUnitName" /></el-form-item></template></el-table-column>
<el-table-column label="供应商" min-width="150"><template #default="{ row }"><el-form-item class="mb-0px!"><el-input disabled v-model="row.supplierName" /></el-form-item></template></el-table-column>
<el-table-column label="数量" prop="count" fixed="right" min-width="140"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.count`" :rules="formRules.count" class="mb-0px!"><el-input-number v-model="row.count" controls-position="right" :min="0.0001" :precision="4" class="!w-100%" /></el-form-item></template></el-table-column>
<el-table-column label="产品单价" fixed="right" min-width="120"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.productPrice`" :rules="formRules.productPrice" class="mb-0px!"><el-input-number v-model="row.productPrice" controls-position="right" :min="0.0001" :precision="4" class="!w-100%" /></el-form-item></template></el-table-column>
<el-table-column label="金额" prop="totalProductPrice" fixed="right" min-width="100"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.totalProductPrice`" class="mb-0px!"><el-input disabled v-model="row.totalProductPrice" :formatter="erpPriceInputFormatter" /></el-form-item></template></el-table-column>
<el-table-column label="税率(%" fixed="right" min-width="115"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.taxPercent`" class="mb-0px!"><el-input-number v-model="row.taxPercent" controls-position="right" :min="0" :precision="4" class="!w-100%" /></el-form-item></template></el-table-column>
<el-table-column label="税额" prop="taxPrice" fixed="right" min-width="120"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.taxPrice`" class="mb-0px!"><el-input disabled v-model="row.taxPrice" :formatter="erpPriceInputFormatter" /></el-form-item></template></el-table-column>
<el-table-column label="税额合计" prop="totalPrice" fixed="right" min-width="120"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.totalPrice`" class="mb-0px!"><el-input-number v-model="row.totalPrice" controls-position="right" :min="0" :precision="4" class="!w-100%" @change="onChangeTotalPrice(row)" /></el-form-item></template></el-table-column>
<el-table-column label="备注" min-width="150"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.remark`" class="mb-0px!"><el-input v-model="row.remark" placeholder="请输入备注" /></el-form-item></template></el-table-column>
<el-table-column align="center" fixed="right" label="操作" width="60"><template #default="{ $index }"><el-button @click="handleDelete($index)" link></el-button></template></el-table-column>
</el-table>
</el-form>
<el-row justify="center" class="mt-3" v-if="!disabled"><el-button @click="handleAdd" round>+ 添加采购产品</el-button></el-row>
</template>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ProductApi, ProductVO } from '@/api/erp/product/product' import { ProductApi, ProductVO } from '@/api/erp/product/product'
@@ -155,6 +169,10 @@ import {
getSumValue getSumValue
} from '@/utils' } from '@/utils'
import { computed } from 'vue' import { computed } from 'vue'
import { useWindowSize } from '@vueuse/core'
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@@ -231,7 +249,7 @@ watch(
{ deep: true } { deep: true }
) )
/** 合计 */ /** 合计 - 移动端 */
const summaryData = computed(() => { const summaryData = computed(() => {
const data = formData.value || [] const data = formData.value || []
return { return {
@@ -242,6 +260,20 @@ const summaryData = computed(() => {
} }
}) })
/** 合计 - PC端 */
const getSummaries = (param: any) => {
const { columns, data } = param
const sums: string[] = []
columns.forEach((column, index: number) => {
if (index === 0) { sums[index] = '合计'; return }
if (['count', 'totalProductPrice', 'taxPrice', 'totalPrice'].includes(column.property)) {
const sum = getSumValue(data.map((item) => Number(item[column.property])))
sums[index] = column.property === 'count' ? erpCountInputFormatter(sum) : erpPriceInputFormatter(sum)
} else { sums[index] = '' }
})
return sums
}
/** 新增按钮操作 */ /** 新增按钮操作 */
const handleAdd = () => { const handleAdd = () => {
const row: any = { const row: any = {

View File

@@ -1,6 +1,8 @@
<!-- 可退货的订单列表 --> <!-- 可退货的订单列表 -->
<template> <template>
<!-- 移动端布局 -->
<el-drawer <el-drawer
v-if="isMobile"
v-model="dialogVisible" v-model="dialogVisible"
title="选择采购订单(仅展示可退货)" title="选择采购订单(仅展示可退货)"
direction="rtl" direction="rtl"
@@ -115,17 +117,53 @@
<el-button @click="dialogVisible = false"> </el-button> <el-button @click="dialogVisible = false"> </el-button>
</template> </template>
</el-drawer> </el-drawer>
<!-- PC端布局 -->
<Dialog v-else title="选择采购订单(仅展示可退货)" v-model="dialogVisible" :appendToBody="true" :scroll="true" width="1100">
<ContentWrap>
<el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px" class="-mb-15px">
<el-form-item label="订单单号" prop="no"><el-input v-model="queryParams.no" placeholder="请输入订单单号" clearable @keyup.enter="handleQuery" class="!w-160px" /></el-form-item>
<el-form-item label="产品" prop="productId"><el-select v-model="queryParams.productId" clearable filterable placeholder="请选择产品" class="!w-160px"><el-option v-for="item in productList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item>
<el-form-item label="订单时间" prop="orderTime"><el-date-picker v-model="queryParams.orderTime" value-format="YYYY-MM-DD HH:mm:ss" type="daterange" start-placeholder="开始日期" end-placeholder="结束日期" :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" class="!w-160px" /></el-form-item>
<el-form-item><el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button><el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button></el-form-item>
</el-form>
</ContentWrap>
<ContentWrap>
<el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
<el-table-column align="center" width="65"><template #default="{ row }"><el-radio :value="row.id" v-model="currentRowValue" @change="handleCurrentChange(row)">&nbsp;</el-radio></template></el-table-column>
<el-table-column label="订单单号" align="center" prop="no" min-width="180" />
<el-table-column label="供应商" align="center" prop="supplierName" />
<el-table-column label="产品信息" align="center" prop="productNames" min-width="200" />
<el-table-column label="订单时间" align="center" prop="orderTime" :formatter="dateFormatter2" width="120px" />
<el-table-column label="总数量" align="center" prop="totalCount" :formatter="erpCountTableColumnFormatter" />
<el-table-column label="入库数量" align="center" prop="inCount" :formatter="erpCountTableColumnFormatter" />
<el-table-column label="退货数量" align="center" prop="returnCount" :formatter="erpCountTableColumnFormatter" />
<el-table-column label="金额合计" align="center" prop="totalProductPrice" :formatter="erpPriceTableColumnFormatter" />
<el-table-column label="含税金额" align="center" prop="totalPrice" :formatter="erpPriceTableColumnFormatter" />
</el-table>
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" />
</ContentWrap>
<template #footer>
<el-button :disabled="!currentRow" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'
import { Search, Filter } from '@element-plus/icons-vue' import { Search, Filter } from '@element-plus/icons-vue'
import { PurchaseOrderApi, PurchaseOrderVO } from '@/api/erp/purchase/order' import { PurchaseOrderApi, PurchaseOrderVO } from '@/api/erp/purchase/order'
import { formatDate } from '@/utils/formatTime' import { formatDate, dateFormatter2 } from '@/utils/formatTime'
import { erpCountInputFormatter, erpPriceInputFormatter } from '@/utils' import { erpCountInputFormatter, erpPriceInputFormatter, erpCountTableColumnFormatter, erpPriceTableColumnFormatter } from '@/utils'
import { ProductApi, ProductVO } from '@/api/erp/product/product' import { ProductApi, ProductVO } from '@/api/erp/product/product'
import { useWindowSize } from '@vueuse/core'
defineOptions({ name: 'PurchaseOrderReturnEnableList' }) defineOptions({ name: 'PurchaseOrderReturnEnableList' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const list = ref<PurchaseOrderVO[]>([]) // 列表的数据 const list = ref<PurchaseOrderVO[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数 const total = ref(0) // 列表的总页数
const loading = ref(false) // 列表的加载中 const loading = ref(false) // 列表的加载中

View File

@@ -1,5 +1,7 @@
<template> <template>
<!-- 移动端布局 -->
<el-drawer <el-drawer
v-if="isMobile"
v-model="dialogVisible" v-model="dialogVisible"
title="选择请购单" title="选择请购单"
direction="rtl" direction="rtl"
@@ -103,16 +105,52 @@
</template> </template>
</el-drawer> </el-drawer>
</el-drawer> </el-drawer>
<!-- PC端布局 -->
<Dialog v-else title="选择请购单" v-model="dialogVisible" :appendToBody="true" :scroll="true" width="1080">
<ContentWrap>
<el-form :model="queryParams" ref="filterFormRef" :inline="true" label-width="68px" class="-mb-15px">
<el-form-item label="请购单号" prop="no"><el-input v-model="queryParams.no" placeholder="请输入请购单号" clearable @keyup.enter="handleQuery" class="!w-160px" /></el-form-item>
<el-form-item label="请购人" prop="requesterName"><el-input v-model="queryParams.requesterName" placeholder="请输入请购人" clearable class="!w-160px" /></el-form-item>
<el-form-item label="请购时间" prop="requestTime"><el-date-picker v-model="queryParams.requestTime" value-format="YYYY-MM-DD HH:mm:ss" type="daterange" start-placeholder="开始日期" end-placeholder="结束日期" :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" class="!w-160px" /></el-form-item>
<el-form-item><el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button><el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button></el-form-item>
</el-form>
</ContentWrap>
<ContentWrap>
<el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
<el-table-column align="center" width="65"><template #default="{ row }"><el-radio :value="row.id" v-model="selectedId" @change="handleSingleSelected(row)">&nbsp;</el-radio></template></el-table-column>
<el-table-column label="请购单号" align="center" prop="no" min-width="180" />
<el-table-column label="类型" align="center" prop="type" width="80"><template #default="scope"><el-tag size="small" :type="getTypeTagType(scope.row.type)">{{ getTypeLabel(scope.row.type) }}</el-tag></template></el-table-column>
<el-table-column label="优先级" align="center" prop="priority" width="80"><template #default="scope"><el-tag size="small" :type="getPriorityTagType(scope.row.priority)">{{ getPriorityLabel(scope.row.priority) }}</el-tag></template></el-table-column>
<el-table-column label="请购人" align="center" prop="requesterNickname" width="100" />
<el-table-column label="请购部门" align="center" prop="requesterDeptName" width="120" />
<el-table-column label="请购时间" align="center" prop="requestTime" :formatter="dateFormatter2" width="120px" />
<el-table-column label="合计数量" align="center" prop="totalCount" :formatter="erpCountTableColumnFormatter" />
<el-table-column label="合计金额" align="center" prop="totalPrice" :formatter="erpPriceTableColumnFormatter" />
</el-table>
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" />
</ContentWrap>
<template #footer>
<el-button :disabled="!currentRow" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'
import { Search, Filter } from '@element-plus/icons-vue' import { Search, Filter } from '@element-plus/icons-vue'
import { formatDate } from '@/utils/formatTime' import { formatDate, dateFormatter2 } from '@/utils/formatTime'
import { erpCountTableColumnFormatter, erpPriceTableColumnFormatter } from '@/utils'
import { PurchaseRequisitionApi, PurchaseRequisition } from '@/api/erp/purchaserequisition' import { PurchaseRequisitionApi, PurchaseRequisition } from '@/api/erp/purchaserequisition'
import { CHANGE_EVENT } from 'element-plus' import { CHANGE_EVENT } from 'element-plus'
import { useWindowSize } from '@vueuse/core'
defineOptions({ name: 'PurchaseRequisitionTableSelect' }) defineOptions({ name: 'PurchaseRequisitionTableSelect' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const total = ref(0) const total = ref(0)
const list = ref<PurchaseRequisition[]>([]) const list = ref<PurchaseRequisition[]>([])
const loading = ref(false) const loading = ref(false)
@@ -128,6 +166,7 @@ const queryParams = ref({
requestTime: [] requestTime: []
}) })
const selectedId = ref() const selectedId = ref()
const currentRow = ref<PurchaseRequisition>()
const formatDate2 = (date: any) => { const formatDate2 = (date: any) => {
if (!date) return '-' if (!date) return '-'
@@ -138,6 +177,7 @@ const formatDate2 = (date: any) => {
const open = (currentNo?: string) => { const open = (currentNo?: string) => {
dialogVisible.value = true dialogVisible.value = true
selectedId.value = undefined selectedId.value = undefined
currentRow.value = undefined
if (currentNo) { if (currentNo) {
queryParams.value.no = currentNo queryParams.value.no = currentNo
} else { } else {
@@ -180,9 +220,20 @@ const resetQuery = () => {
/** 单选中时触发 */ /** 单选中时触发 */
const handleSingleSelected = (row: PurchaseRequisition) => { const handleSingleSelected = (row: PurchaseRequisition) => {
emits(CHANGE_EVENT, row) if (isMobile.value) {
dialogVisible.value = false emits(CHANGE_EVENT, row)
dialogVisible.value = false
}
selectedId.value = row.id selectedId.value = row.id
currentRow.value = row
}
/** 提交选择 */
const submitForm = () => {
if (currentRow.value) {
emits(CHANGE_EVENT, currentRow.value)
}
dialogVisible.value = false
} }
const emits = defineEmits<{ const emits = defineEmits<{

View File

@@ -1,25 +1,34 @@
<template> <template>
<div class="mobile-purchase-order"> <!-- 移动端布局 -->
<!-- 顶部操作栏 --> <div v-if="isMobile" class="mobile-purchase-order">
<div class="mobile-header"> <div class="mobile-header">
<div class="mobile-header__search"> <div class="mobile-header__search"><el-input v-model="queryParams.no" placeholder="搜索订单单号" clearable @keyup.enter="handleQuery" :prefix-icon="Search" /></div>
<el-input
v-model="queryParams.no"
placeholder="搜索订单单号"
clearable
@keyup.enter="handleQuery"
:prefix-icon="Search"
/>
</div>
<div class="mobile-header__actions"> <div class="mobile-header__actions">
<el-button :icon="Filter" circle @click="filterVisible = true" /> <el-button :icon="Filter" circle @click="filterVisible = true" />
<el-button <el-button type="primary" :icon="Plus" circle @click="openForm('create')" v-hasPermi="['erp:purchase-order:create']" />
type="primary" </div>
:icon="Plus" </div>
circle <div class="mobile-header__quick-filter">
@click="openForm('create')" <div
v-hasPermi="['erp:purchase-order:create']" class="quick-filter-item"
/> :class="{ active: queryParams.status === undefined }"
@click="handleQuickFilter(undefined)"
>
全部订单
</div>
<div
class="quick-filter-item"
:class="{ active: queryParams.status === 10 }"
@click="handleQuickFilter(10)"
>
待审核
</div>
<div
class="quick-filter-item"
:class="{ active: queryParams.status === 20 }"
@click="handleQuickFilter(20)"
>
已审核
</div> </div>
</div> </div>
@@ -171,12 +180,334 @@
<!-- 供应商评价对话框 --> <!-- 供应商评价对话框 -->
<SupplierEvaluationDialog ref="supplierEvaluationDialogRef" @success="getList" /> <SupplierEvaluationDialog ref="supplierEvaluationDialogRef" @success="getList" />
</div> </div>
<!-- PC端布局 -->
<template v-else>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="订单单号" prop="no">
<el-input
v-model="queryParams.no"
placeholder="请输入订单单号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="产品" prop="productId">
<el-select
v-model="queryParams.productId"
clearable
filterable
placeholder="请选择产品"
class="!w-240px"
>
<el-option
v-for="item in productList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="订单时间" prop="orderTime">
<el-date-picker
v-model="queryParams.orderTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="供应商" prop="supplierId">
<el-select
v-model="queryParams.supplierId"
clearable
filterable
placeholder="请选择供供应商"
class="!w-240px"
>
<el-option
v-for="item in supplierList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="创建人" prop="creator">
<el-select
v-model="queryParams.creator"
clearable
filterable
placeholder="请选择创建人"
class="!w-240px"
>
<el-option
v-for="item in userList"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.ERP_AUDIT_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="queryParams.remark"
placeholder="请输入备注"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="入库数量" prop="inStatus">
<el-select
v-model="queryParams.inStatus"
placeholder="请选择入库数量"
clearable
class="!w-240px"
>
<el-option label="未入库" value="0" />
<el-option label="部分入库" value="1" />
<el-option label="全部入库" value="2" />
</el-select>
</el-form-item>
<el-form-item label="退货数量" prop="returnStatus">
<el-select
v-model="queryParams.returnStatus"
placeholder="请选择退货数量"
clearable
class="!w-240px"
>
<el-option label="未退货" value="0" />
<el-option label="部分退货" value="1" />
<el-option label="全部退货" value="2" />
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['erp:purchase-order:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['erp:purchase-order:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button
type="danger"
plain
@click="handleDelete(selectionList.map((item) => item.id))"
v-hasPermi="['erp:purchase-order:delete']"
:disabled="selectionList.length === 0"
>
<Icon icon="ep:delete" class="mr-5px" /> 删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleSelectionChange"
@row-click="handleRowClick"
>
<el-table-column width="30" label="选择" type="selection" />
<el-table-column min-width="180" label="订单单号" align="center" prop="no" />
<el-table-column label="产品信息" align="center" prop="productNames" min-width="200" />
<el-table-column label="供应商" align="center" prop="supplierName" />
<el-table-column
label="订单时间"
align="center"
prop="orderTime"
:formatter="dateFormatter2"
width="120px"
sortable
/>
<el-table-column label="创建人" align="center" prop="creatorName" />
<!-- <el-table-column label="审核人" align="center" prop="auditorName" />-->
<el-table-column
label="总数量"
align="center"
prop="totalCount"
:formatter="erpCountTableColumnFormatter"
/>
<el-table-column
label="入库数量"
align="center"
prop="inCount"
:formatter="erpCountTableColumnFormatter"
/>
<el-table-column
label="退货数量"
align="center"
prop="returnCount"
:formatter="erpCountTableColumnFormatter"
/>
<el-table-column
label="金额合计"
align="center"
prop="totalProductPrice"
:formatter="erpPriceTableColumnFormatter"
/>
<el-table-column
label="含税金额"
align="center"
prop="totalPrice"
:formatter="erpPriceTableColumnFormatter"
/>
<el-table-column
label="支付订金"
align="center"
prop="depositPrice"
:formatter="erpPriceTableColumnFormatter"
/>
<el-table-column label="状态" align="center" fixed="right" width="90" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.ERP_AUDIT_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="380">
<template #default="scope">
<el-button
link
@click="openForm('detail', scope.row.id)"
v-hasPermi="['erp:purchase-order:query']"
>
详情
</el-button>
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['erp:purchase-order:update']"
:disabled="scope.row.status === 20 || scope.row.hasApprovalRecords"
>
编辑
</el-button>
<el-button
link
type="success"
@click="handleSubmitApproval(scope.row.id)"
v-hasPermi="['erp:purchase-order:submit-approval']"
v-if="scope.row.status === 10 && !scope.row.hasApprovalRecords"
>
提交审批
</el-button>
<el-button
link
type="info"
@click="handleViewApproval(scope.row.id)"
v-hasPermi="['erp:purchase-order:query-approval']"
v-if="scope.row.hasApprovalRecords"
>
审批记录
</el-button>
<el-button
link
type="primary"
@click="handleProcessApproval(scope.row.id)"
v-hasPermi="['erp:approval-record:process']"
v-if="scope.row.hasApprovalRecords && scope.row.status !== 20"
>
处理审批
</el-button>
<el-button
link
type="primary"
@click="handleUpdateStatus(scope.row.id, 20)"
v-hasPermi="['erp:purchase-order:update-status']"
v-if="scope.row.status === 10"
>
审批
</el-button>
<el-button
link
type="danger"
@click="handleUpdateStatus(scope.row.id, 10)"
v-hasPermi="['erp:purchase-order:update-status']"
v-else
>
反审批
</el-button>
<el-button
link
type="warning"
@click="handleSupplierEvaluation(scope.row)"
v-hasPermi="['erp:supplier-evaluation:create']"
v-if="scope.row.status === 20"
>
供应商评价
</el-button>
<el-button
link
type="danger"
@click="handleDelete([scope.row.id])"
v-hasPermi="['erp:purchase-order:delete']"
:disabled="scope.row.hasApprovalRecords"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
</template>
<!-- 表单弹窗添加/修改 -->
<PurchaseOrderForm ref="formRef" @success="getList" />
<SubmitApprovalDialog ref="submitApprovalDialogRef" @success="getList" />
<ApprovalRecordsDialog ref="approvalDialogRef" />
<ProcessApprovalDialog ref="processApprovalDialogRef" @success="getList" />
<SupplierEvaluationDialog ref="supplierEvaluationDialogRef" @success="getList" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import { Search, Filter, Plus } from '@element-plus/icons-vue' import { Search, Filter, Plus } from '@element-plus/icons-vue'
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { formatDate } from '@/utils/formatTime' import { formatDate, dateFormatter2 } from '@/utils/formatTime'
import download from '@/utils/download' import download from '@/utils/download'
import { PurchaseOrderApi, PurchaseOrderVO } from '@/api/erp/purchase/order' import { PurchaseOrderApi, PurchaseOrderVO } from '@/api/erp/purchase/order'
import PurchaseOrderForm from './PurchaseOrderForm.vue' import PurchaseOrderForm from './PurchaseOrderForm.vue'
@@ -185,13 +516,17 @@ import { SubmitApprovalDialog, ApprovalRecordsDialog, ProcessApprovalDialog } fr
import { ProductApi, ProductVO } from '@/api/erp/product/product' import { ProductApi, ProductVO } from '@/api/erp/product/product'
import { UserVO } from '@/api/system/user' import { UserVO } from '@/api/system/user'
import * as UserApi from '@/api/system/user' import * as UserApi from '@/api/system/user'
import { erpCountInputFormatter, erpPriceInputFormatter } from '@/utils' import { erpCountInputFormatter, erpPriceInputFormatter, erpCountTableColumnFormatter, erpPriceTableColumnFormatter } from '@/utils'
import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier' import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier'
import { ApprovalRecordApi, ApprovalRecordVO } from '@/api/erp/approval/index' import { ApprovalRecordApi, ApprovalRecordVO } from '@/api/erp/approval/index'
import { useWindowSize } from '@vueuse/core'
/** ERP 销售订单列表 */ /** ERP 销售订单列表 */
defineOptions({ name: 'ErpPurchaseOrder' }) defineOptions({ name: 'ErpPurchaseOrder' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
@@ -315,16 +650,31 @@ const handleSupplierEvaluation = (row: PurchaseOrderVO) => {
/** 删除按钮操作 */ /** 删除按钮操作 */
const handleDelete = async (ids: number[]) => { const handleDelete = async (ids: number[]) => {
try { try {
// 删除的二次确认
await message.delConfirm() await message.delConfirm()
// 发起删除
await PurchaseOrderApi.deletePurchaseOrder(ids) await PurchaseOrderApi.deletePurchaseOrder(ids)
message.success(t('common.delSuccess')) message.success(t('common.delSuccess'))
// 刷新列表
await getList() await getList()
selectionList.value = selectionList.value.filter((item) => !ids.includes(item.id))
} catch {} } catch {}
} }
/** 选中操作(PC端) */
const selectionList = ref<PurchaseOrderVO[]>([])
const handleSelectionChange = (rows: PurchaseOrderVO[]) => {
selectionList.value = rows
}
/** 行点击操作(PC端) */
const handleRowClick = (row: PurchaseOrderVO, column: any, event: MouseEvent) => {
const target = event.target as HTMLElement
if (target.tagName === 'BUTTON' || target.tagName === 'A' || target.tagName === 'I' || target.tagName === 'svg' || target.closest('button') || target.closest('a') || target.closest('.el-button') || target.closest('.el-checkbox')) return
if (row.status === 20) {
openForm('detail', row.id)
} else if (row.status === 10) {
openForm('update', row.id)
}
}
/** 审批/反审批操作 */ /** 审批/反审批操作 */
const handleUpdateStatus = async (id: number, status: number) => { const handleUpdateStatus = async (id: number, status: number) => {
try { try {
@@ -361,9 +711,41 @@ onMounted(async () => {
supplierList.value = await SupplierApi.getSupplierSimpleList() supplierList.value = await SupplierApi.getSupplierSimpleList()
userList.value = await UserApi.getSimpleUserList() userList.value = await UserApi.getSimpleUserList()
}) })
/** 快捷分类筛选 */
const handleQuickFilter = (status: number | undefined) => {
queryParams.status = status
queryParams.pageNo = 1
getList()
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.mobile-header__quick-filter {
display: flex;
gap: 12px;
margin: 8px 0;
justify-content: flex-start;
.quick-filter-item {
padding: 4px 12px;
font-size: 14px;
border-radius: 20px;
cursor: pointer;
color: #909399;
background: transparent;
transition: all 0.2s;
&.active {
color: #fff;
background: #409eff;
}
}
}
.mobile-purchase-order { .mobile-purchase-order {
padding: 12px; padding: 12px;
background: #f5f5f5; background: #f5f5f5;

View File

@@ -1,184 +1,75 @@
<template> <template>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="1440"> <!-- 移动端布局 -->
<el-form <el-drawer v-if="isMobile" v-model="dialogVisible" :title="dialogTitle" direction="rtl" size="100%" :close-on-press-escape="true" :destroy-on-close="true" :append-to-body="true" class="mobile-form-drawer">
ref="formRef" <div class="mobile-form" v-loading="formLoading">
:model="formData" <el-form ref="formRef" :model="formData" :rules="formRules" label-position="top" :disabled="disabled">
:rules="formRules" <div class="mobile-form__section">
label-width="100px" <div class="mobile-form__section-title">基本信息</div>
v-loading="formLoading" <el-form-item label="退货单号" prop="no"><el-input disabled v-model="formData.no" placeholder="保存时自动生成" /></el-form-item>
:disabled="disabled" <el-form-item label="退货时间" prop="returnTime"><el-date-picker v-model="formData.returnTime" type="date" value-format="x" placeholder="选择退货时间" style="width:100%" /></el-form-item>
> <el-form-item label="关联订单" prop="orderNo"><el-input readonly><template #prefix><el-link v-if="formData.orderId" type="primary" :underline="false" @click.stop="openPurchaseOrderDetail">{{ formData.orderNo }}</el-link></template><template #append><el-button @click="openPurchaseOrderReturnEnableList"><Icon icon="ep:search" /></el-button></template></el-input></el-form-item>
<el-form-item label="供应商" prop="supplierId"><el-select v-model="formData.supplierId" clearable filterable disabled placeholder="请选择供应商" style="width:100%"><el-option v-for="item in supplierList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item>
<el-form-item label="备注" prop="remark"><el-input type="textarea" v-model="formData.remark" :rows="3" placeholder="请输入备注" /></el-form-item>
<el-form-item label="附件" prop="fileUrl"><UploadFile :is-show-tip="false" v-model="formData.fileUrl" :limit="1" /></el-form-item>
</div>
<div class="mobile-form__section">
<div class="mobile-form__section-title">退货产品清单</div>
<PurchaseReturnItemForm ref="itemFormRef" :items="formData.items" :disabled="disabled" />
</div>
<div class="mobile-form__section">
<div class="mobile-form__section-title">费用信息</div>
<el-form-item label="优惠率(%" prop="discountPercent"><el-input-number v-model="formData.discountPercent" controls-position="right" :min="0" :precision="4" placeholder="请输入优惠率" style="width:100%" /></el-form-item>
<el-form-item label="退款优惠" prop="discountPrice"><el-input disabled v-model="formData.discountPrice" :formatter="erpPriceInputFormatter" /></el-form-item>
<el-form-item label="其它费用" prop="otherPrice"><el-input-number v-model="formData.otherPrice" controls-position="right" :min="0" :precision="4" placeholder="请输入其它费用" style="width:100%" /></el-form-item>
<el-form-item label="优惠后金额"><el-input disabled v-model="formData.totalPrice" :formatter="erpPriceInputFormatter" /></el-form-item>
<el-form-item label="结算账户" prop="accountId"><el-select v-model="formData.accountId" clearable filterable placeholder="请选择结算账户" style="width:100%"><el-option v-for="item in accountList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item>
</div>
</el-form>
<div class="mobile-form__footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled"> </el-button>
</div>
</div>
</el-drawer>
<!-- PC端布局 -->
<Dialog v-else :title="dialogTitle" v-model="dialogVisible" width="1440">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" v-loading="formLoading" :disabled="disabled">
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="8"> <el-col :span="8"><el-form-item label="退货单号" prop="no"><el-input disabled v-model="formData.no" placeholder="保存时自动生成" /></el-form-item></el-col>
<el-form-item label="退货单号" prop="no"> <el-col :span="8"><el-form-item label="退货时间" prop="returnTime"><el-date-picker v-model="formData.returnTime" type="date" value-format="x" placeholder="选择退货时间" class="!w-1/1" /></el-form-item></el-col>
<el-input disabled v-model="formData.no" placeholder="保存时自动生成" /> <el-col :span="8"><el-form-item label="关联订单" prop="orderNo"><el-input readonly><template #prefix><el-link v-if="formData.orderId" type="primary" :underline="false" @click.stop="openPurchaseOrderDetail">{{ formData.orderNo }}</el-link></template><template #append><el-button @click="openPurchaseOrderReturnEnableList"><Icon icon="ep:search" /> 选择</el-button></template></el-input></el-form-item></el-col>
</el-form-item> <el-col :span="8"><el-form-item label="供应商" prop="supplierId"><el-select v-model="formData.supplierId" clearable filterable disabled placeholder="请选择供应商" class="!w-1/1"><el-option v-for="item in supplierList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></el-col>
</el-col> <el-col :span="16"><el-form-item label="备注" prop="remark"><el-input type="textarea" v-model="formData.remark" :rows="1" placeholder="请输入备注" /></el-form-item></el-col>
<el-col :span="8"> <el-col :span="8"><el-form-item label="附件" prop="fileUrl"><UploadFile :is-show-tip="false" v-model="formData.fileUrl" :limit="1" /></el-form-item></el-col>
<el-form-item label="退货时间" prop="returnTime">
<el-date-picker
v-model="formData.returnTime"
type="date"
value-format="x"
placeholder="选择退货时间"
class="!w-1/1"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="关联订单" prop="orderNo">
<el-input readonly>
<template #prefix>
<el-link
v-if="formData.orderId"
type="primary"
@click.stop="openPurchaseOrderDetail"
:underline="false"
>
{{ formData.orderNo }}
</el-link>
</template>
<template #append>
<el-button @click="openPurchaseOrderReturnEnableList">
<Icon icon="ep:search" /> 选择
</el-button>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="供应商" prop="supplierId">
<el-select
v-model="formData.supplierId"
clearable
filterable
disabled
placeholder="请选择供应商"
class="!w-1/1"
>
<el-option
v-for="item in supplierList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="16">
<el-form-item label="备注" prop="remark">
<el-input
type="textarea"
v-model="formData.remark"
:rows="1"
placeholder="请输入备注"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="附件" prop="fileUrl">
<UploadFile :is-show-tip="false" v-model="formData.fileUrl" :limit="1" />
</el-form-item>
</el-col>
</el-row> </el-row>
<!-- 子表的表单 -->
<ContentWrap> <ContentWrap>
<el-tabs v-model="subTabsName" class="-mt-15px -mb-10px"> <el-tabs v-model="subTabsName" class="-mt-15px -mb-10px">
<el-tab-pane label="退货产品清单" name="item"> <el-tab-pane label="退货产品清单" name="item"><PurchaseReturnItemForm ref="itemFormRef" :items="formData.items" :disabled="disabled" /></el-tab-pane>
<PurchaseReturnItemForm
ref="itemFormRef"
:items="formData.items"
:disabled="disabled"
/>
</el-tab-pane>
</el-tabs> </el-tabs>
</ContentWrap> </ContentWrap>
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="8"> <el-col :span="8"><el-form-item label="优惠率(%" prop="discountPercent"><el-input-number v-model="formData.discountPercent" controls-position="right" :min="0" :precision="4" placeholder="请输入优惠率" class="!w-1/1" /></el-form-item></el-col>
<el-form-item label="优惠率(%" prop="discountPercent"> <el-col :span="8"><el-form-item label="退款优惠" prop="discountPrice"><el-input disabled v-model="formData.discountPrice" :formatter="erpPriceInputFormatter" /></el-form-item></el-col>
<el-input-number <el-col :span="8"><el-form-item label="其它费用" prop="otherPrice"><el-input-number v-model="formData.otherPrice" controls-position="right" :min="0" :precision="4" placeholder="请输入其它费用" class="!w-1/1" /></el-form-item></el-col>
v-model="formData.discountPercent" <el-col :span="8"><el-form-item label="优惠后金额"><el-input disabled v-model="formData.totalPrice" :formatter="erpPriceInputFormatter" /></el-form-item></el-col>
controls-position="right" <el-col :span="8"><el-form-item label="结算账户" prop="accountId"><el-select v-model="formData.accountId" clearable filterable placeholder="请选择结算账户" class="!w-1/1"><el-option v-for="item in accountList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></el-col>
:min="0"
:precision="4"
placeholder="请输入优惠率"
class="!w-1/1"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="退款优惠" prop="discountPrice">
<el-input
disabled
v-model="formData.discountPrice"
:formatter="erpPriceInputFormatter"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="优惠后金额">
<el-input
disabled
:model-value="formData.totalPrice - formData.otherPrice"
:formatter="erpPriceInputFormatter"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="其它费用" prop="otherPrice">
<el-input-number
v-model="formData.otherPrice"
controls-position="right"
:min="0"
:precision="4"
placeholder="请输入其它费用"
class="!w-1/1"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="结算账户" prop="accountId">
<el-select
v-model="formData.accountId"
clearable
filterable
placeholder="请选择结算账户"
class="!w-1/1"
>
<el-option
v-for="item in accountList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="应退金额" prop="totalPrice">
<el-input disabled v-model="formData.totalPrice" :formatter="erpPriceInputFormatter" />
</el-form-item>
</el-col>
</el-row> </el-row>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled"> <el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled"> </el-button>
</el-button>
<el-button @click="dialogVisible = false"> </el-button> <el-button @click="dialogVisible = false"> </el-button>
</template> </template>
</Dialog> </Dialog>
<!-- 可退货的订单列表 --> <!-- 可退货的订单列表 -->
<PurchaseOrderReturnEnableList <PurchaseOrderReturnEnableList ref="purchaseOrderReturnEnableListRef" @success="handlePurchaseOrderChange" />
ref="purchaseOrderReturnEnableListRef"
@success="handlePurchaseOrderChange"
/>
<!-- 采购订单详情弹窗 --> <!-- 采购订单详情弹窗 -->
<PurchaseOrderForm ref="purchaseOrderFormRef" /> <PurchaseOrderForm ref="purchaseOrderFormRef" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, watch, nextTick } from 'vue'
import { PurchaseReturnApi, PurchaseReturnVO } from '@/api/erp/purchase/return' import { PurchaseReturnApi, PurchaseReturnVO } from '@/api/erp/purchase/return'
import PurchaseReturnItemForm from './components/PurchaseReturnItemForm.vue' import PurchaseReturnItemForm from './components/PurchaseReturnItemForm.vue'
import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier' import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier'
@@ -188,19 +79,25 @@ import PurchaseOrderReturnEnableList from '@/views/erp/purchase/order/components
import PurchaseOrderForm from '@/views/erp/purchase/order/PurchaseOrderForm.vue' import PurchaseOrderForm from '@/views/erp/purchase/order/PurchaseOrderForm.vue'
import { PurchaseOrderVO } from '@/api/erp/purchase/order' import { PurchaseOrderVO } from '@/api/erp/purchase/order'
import * as UserApi from '@/api/system/user' import * as UserApi from '@/api/system/user'
import { useWindowSize } from '@vueuse/core'
/** ERP 采购退货表单 */
defineOptions({ name: 'PurchaseReturnForm' }) defineOptions({ name: 'PurchaseReturnForm' })
const { t } = useI18n() // 国际化 const { width } = useWindowSize()
const message = useMessage() // 消息弹窗 const isMobile = computed(() => width.value < 768)
const { t } = useI18n()
const message = useMessage()
const dialogVisible = ref(false) // 弹窗的是否展示 /** 弹窗显示 */
const dialogTitle = ref('') // 弹窗的标题 const dialogVisible = ref(false)
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用 const dialogTitle = ref('')
const formType = ref('') // 表单的类型create - 新增update - 修改detail - 详情 const formLoading = ref(false)
const formType = ref('')
/** 表单数据 */
const formData = ref({ const formData = ref({
id: undefined, id: undefined,
orderId: undefined,
supplierId: undefined, supplierId: undefined,
accountId: undefined, accountId: undefined,
returnTime: undefined, returnTime: undefined,
@@ -212,35 +109,35 @@ const formData = ref({
otherPrice: 0, otherPrice: 0,
orderNo: undefined, orderNo: undefined,
items: [], items: [],
no: undefined // 退货单号,后端返回 no: undefined
}) })
/** 表单验证 */
const formRules = reactive({ const formRules = reactive({
supplierId: [{ required: true, message: '供应商不能为空', trigger: 'blur' }], supplierId: [{ required: true, message: '供应商不能为空', trigger: 'blur' }],
returnTime: [{ required: true, message: '退货时间不能为空', trigger: 'blur' }] returnTime: [{ required: true, message: '退货时间不能为空', trigger: 'blur' }]
}) })
/** 计算禁用状态 */
const disabled = computed(() => formType.value === 'detail') const disabled = computed(() => formType.value === 'detail')
const formRef = ref() // 表单 Ref
const supplierList = ref<SupplierVO[]>([]) // 供应商列表
const accountList = ref<AccountVO[]>([]) // 账户列表
const userList = ref<UserApi.UserVO[]>([]) // 用户列表
/** 子表的表单 */ /** ref */
const subTabsName = ref('item') const formRef = ref() // el-form
const itemFormRef = ref() const itemFormRef = ref()
const supplierList = ref<SupplierVO[]>([])
const accountList = ref<AccountVO[]>([])
const userList = ref<UserApi.UserVO[]>([])
const subTabsName = ref('item')
/** 计算 discountPrice、totalPrice 价格 */ /** 计算价格 */
watch( watch(
() => formData.value, () => formData.value,
(val) => { (val) => {
if (!val) { if (!val) return
return const totalItemPrice = (val.items || []).reduce((sum, item) => sum + (item.totalPrice || 0), 0)
} const discountPrice = erpPriceMultiply(totalItemPrice, (val.discountPercent || 0) / 100) || 0
// 计算
const totalPrice = val.items.reduce((prev, curr) => prev + curr.totalPrice, 0)
const discountPrice =
val.discountPercent != null ? erpPriceMultiply(totalPrice, val.discountPercent / 100.0) : 0
formData.value.discountPrice = discountPrice formData.value.discountPrice = discountPrice
formData.value.totalPrice = totalPrice - discountPrice + val.otherPrice formData.value.totalPrice = totalItemPrice - discountPrice + (val.otherPrice || 0)
}, },
{ deep: true } { deep: true }
) )
@@ -251,7 +148,6 @@ const open = async (type: string, id?: number) => {
dialogTitle.value = t('action.' + type) dialogTitle.value = t('action.' + type)
formType.value = type formType.value = type
resetForm() resetForm()
// 修改时,设置数据
if (id) { if (id) {
formLoading.value = true formLoading.value = true
try { try {
@@ -260,35 +156,28 @@ const open = async (type: string, id?: number) => {
formLoading.value = false formLoading.value = false
} }
} }
// 加载供应商列表
supplierList.value = await SupplierApi.getSupplierSimpleList() supplierList.value = await SupplierApi.getSupplierSimpleList()
// 加载用户列表
userList.value = await UserApi.getSimpleUserList()
// 加载账户列表
accountList.value = await AccountApi.getAccountSimpleList() accountList.value = await AccountApi.getAccountSimpleList()
userList.value = await UserApi.getSimpleUserList()
const defaultAccount = accountList.value.find((item) => item.defaultStatus) const defaultAccount = accountList.value.find((item) => item.defaultStatus)
if (defaultAccount) { if (defaultAccount) formData.value.accountId = defaultAccount.id
formData.value.accountId = defaultAccount.id
}
} }
defineExpose({ open }) // 提供 open 方法,用于打开弹窗 defineExpose({ open })
/** 打开可退货订单列表】弹窗 */ /** 打开可退货订单列表 */
const purchaseOrderReturnEnableListRef = ref() // 可退货的订单列表 Ref const purchaseOrderReturnEnableListRef = ref()
const openPurchaseOrderReturnEnableList = () => { const openPurchaseOrderReturnEnableList = () => {
purchaseOrderReturnEnableListRef.value.open() purchaseOrderReturnEnableListRef.value?.open()
} }
/** 打开采购订单详情】弹窗 */ /** 打开采购订单详情 */
const purchaseOrderFormRef = ref() const purchaseOrderFormRef = ref()
const openPurchaseOrderDetail = () => { const openPurchaseOrderDetail = () => {
if (formData.value.orderId) { if (formData.value.orderId) purchaseOrderFormRef.value?.open('detail', formData.value.orderId)
purchaseOrderFormRef.value.open('detail', formData.value.orderId)
}
} }
/** 订单选择回调 */
const handlePurchaseOrderChange = (order: PurchaseOrderVO) => { const handlePurchaseOrderChange = (order: PurchaseOrderVO) => {
// 将订单设置到退货单
formData.value.orderId = order.id formData.value.orderId = order.id
formData.value.orderNo = order.no formData.value.orderNo = order.no
formData.value.supplierId = order.supplierId formData.value.supplierId = order.supplierId
@@ -296,22 +185,21 @@ const handlePurchaseOrderChange = (order: PurchaseOrderVO) => {
formData.value.discountPercent = order.discountPercent formData.value.discountPercent = order.discountPercent
formData.value.remark = order.remark formData.value.remark = order.remark
formData.value.fileUrl = order.fileUrl formData.value.fileUrl = order.fileUrl
// 将订单项设置到退货单项
order.items.forEach((item) => { // 处理订单明细
order.items.forEach(item => {
item.count = item.inCount - item.returnCount item.count = item.inCount - item.returnCount
item.orderItemId = item.id item.orderItemId = item.id
item.id = undefined item.id = undefined
}) })
formData.value.items = order.items.filter((item) => item.count > 0) formData.value.items = order.items.filter(item => item.count > 0)
} }
/** 提交表单 */ /** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 const emit = defineEmits(['success'])
const submitForm = async () => { const submitForm = async () => {
// 校验表单
await formRef.value.validate() await formRef.value.validate()
await itemFormRef.value.validate() await itemFormRef.value.validate()
// 提交请求
formLoading.value = true formLoading.value = true
try { try {
const data = formData.value as unknown as PurchaseReturnVO const data = formData.value as unknown as PurchaseReturnVO
@@ -323,7 +211,6 @@ const submitForm = async () => {
message.success(t('common.updateSuccess')) message.success(t('common.updateSuccess'))
} }
dialogVisible.value = false dialogVisible.value = false
// 发送操作成功的事件
emit('success') emit('success')
} finally { } finally {
formLoading.value = false formLoading.value = false
@@ -334,17 +221,66 @@ const submitForm = async () => {
const resetForm = () => { const resetForm = () => {
formData.value = { formData.value = {
id: undefined, id: undefined,
orderId: undefined,
supplierId: undefined, supplierId: undefined,
accountId: undefined, accountId: undefined,
returnTime: undefined, returnTime: undefined,
remark: undefined, remark: undefined,
fileUrl: undefined, fileUrl: '',
discountPercent: 0, discountPercent: 0,
discountPrice: 0, discountPrice: 0,
totalPrice: 0, totalPrice: 0,
otherPrice: 0, otherPrice: 0,
items: [] orderNo: undefined,
items: [],
no: undefined
} }
formRef.value?.resetFields() formRef.value?.resetFields()
} }
</script> </script>
<style lang="scss" scoped>
.mobile-form {
padding: 12px;
}
.mobile-form__section {
background: #fff;
border-radius: 10px;
padding: 14px;
margin-bottom: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.mobile-form__section-title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.mobile-form__requisition {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.mobile-form__footer {
position: sticky;
bottom: 0;
background: #fff;
padding: 12px 16px;
padding-bottom: calc(12px + env(safe-area-inset-bottom));
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 12px;
z-index: 10;
margin: 0 -12px -12px;
.el-button {
flex: 1;
height: 40px;
font-size: 15px;
}
}
</style>

View File

@@ -1,27 +1,37 @@
<template> <template>
<el-form <!-- 移动端布局 -->
ref="formRef" <el-form v-if="isMobile" ref="formRef" :model="formData" :rules="formRules" v-loading="formLoading" label-position="top" :inline-message="true" :disabled="disabled">
:model="formData" <div class="mobile-item-list">
:rules="formRules" <div
v-loading="formLoading" v-for="(row, $index) in formData"
label-width="0px" :key="$index"
:inline-message="true" class="mobile-item-card"
:disabled="disabled" >
> <!-- 卡片头部 -->
<el-table :data="formData" show-summary :summary-method="getSummaries" class="-mt-10px"> <div class="mobile-item-card__header">
<el-table-column label="序号" type="index" align="center" width="60" /> <span class="mobile-item-card__index">#{{ $index + 1 }}</span>
<el-table-column label="仓库名称" min-width="125"> <span class="mobile-item-card__name">{{ row.productName || '未选择产品' }}</span>
<template #default="{ row, $index }"> <el-button
<el-form-item :disabled="formData.length === 1 || disabled"
:prop="`${$index}.warehouseId`" type="danger"
:rules="formRules.warehouseId" link
class="mb-0px!" size="small"
@click="handleDelete($index)"
> >
删除
</el-button>
</div>
<!-- 卡片内容 -->
<div class="mobile-item-card__body">
<!-- 仓库 -->
<el-form-item :prop="`${$index}.warehouseId`" :rules="formRules.warehouseId" label="仓库">
<el-select <el-select
v-model="row.warehouseId" v-model="row.warehouseId"
clearable clearable
filterable filterable
placeholder="请选择仓库" placeholder="请选择仓库"
style="width: 100%"
@change="onChangeWarehouse($event, row)" @change="onChangeWarehouse($event, row)"
> >
<el-option <el-option
@@ -32,234 +42,222 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
</template>
</el-table-column> <!-- 产品名称 -->
<el-table-column label="产品名称" min-width="180"> <div class="mobile-item-card__info-row">
<template #default="{ row }"> <span class="mobile-item-card__info-label">产品名称</span>
<el-form-item class="mb-0px!"> <span class="mobile-item-card__info-value">{{ row.productName || '-' }}</span>
<el-input disabled v-model="row.productName" /> </div>
</el-form-item>
</template> <!-- 库存 -->
</el-table-column> <div class="mobile-item-card__info-row">
<el-table-column label="库存" min-width="100"> <span class="mobile-item-card__info-label">库存</span>
<template #default="{ row }"> <span class="mobile-item-card__info-value">{{ erpCountInputFormatter(row.stockCount) }}</span>
<el-form-item class="mb-0px!"> </div>
<el-input disabled v-model="row.stockCount" :formatter="erpCountInputFormatter" />
</el-form-item> <!-- 条码 -->
</template> <div class="mobile-item-card__info-row">
</el-table-column> <span class="mobile-item-card__info-label">条码</span>
<el-table-column label="条码" min-width="150"> <span class="mobile-item-card__info-value">{{ row.productBarCode || '-' }}</span>
<template #default="{ row }"> </div>
<el-form-item class="mb-0px!">
<el-input disabled v-model="row.productBarCode" /> <!-- 单位 -->
</el-form-item> <div class="mobile-item-card__info-row">
</template> <span class="mobile-item-card__info-label">单位</span>
</el-table-column> <span class="mobile-item-card__info-value">{{ row.productUnitName || '-' }}</span>
<el-table-column label="单位" min-width="80"> </div>
<template #default="{ row }">
<el-form-item class="mb-0px!"> <!-- 已出库 -->
<el-input disabled v-model="row.productUnitName" /> <div class="mobile-item-card__info-row" v-if="row.inCount != null">
</el-form-item> <span class="mobile-item-card__info-label">已出库</span>
</template> <span class="mobile-item-card__info-value">{{ erpCountInputFormatter(row.inCount) }}</span>
</el-table-column> </div>
<el-table-column
label="已出库" <!-- 已退货 -->
fixed="right" <div class="mobile-item-card__info-row" v-if="row.returnCount != null">
min-width="80" <span class="mobile-item-card__info-label">已退货</span>
v-if="formData[0]?.inCount != null" <span class="mobile-item-card__info-value">{{ erpCountInputFormatter(row.returnCount) }}</span>
> </div>
<template #default="{ row }">
<el-form-item class="mb-0px!"> <!-- 数量 & 单价 -->
<el-input disabled v-model="row.inCount" :formatter="erpCountInputFormatter" /> <div class="mobile-item-card__input-group">
</el-form-item> <el-form-item label="数量" :prop="`${$index}.count`" :rules="formRules.count">
</template> <el-input-number
</el-table-column> v-model="row.count"
<el-table-column controls-position="right"
label="已退货" :min="0.0001"
fixed="right" :precision="4"
min-width="80" style="width: 100%"
v-if="formData[0]?.returnCount != null" />
>
<template #default="{ row }">
<el-form-item class="mb-0px!">
<el-input disabled v-model="row.returnCount" :formatter="erpCountInputFormatter" />
</el-form-item>
</template>
</el-table-column>
<el-table-column label="数量" prop="count" fixed="right" min-width="140">
<template #default="{ row, $index }">
<el-form-item :prop="`${$index}.count`" :rules="formRules.count" class="mb-0px!">
<el-input-number
v-model="row.count"
controls-position="right"
:min="0.0001"
:precision="4"
class="!w-100%"
/>
</el-form-item>
</template>
</el-table-column>
<el-table-column label="产品单价" fixed="right" min-width="120">
<template #default="{ row, $index }">
<el-form-item :prop="`${$index}.productPrice`" class="mb-0px!">
<el-input-number
v-model="row.productPrice"
controls-position="right"
:min="0.0001"
:precision="4"
class="!w-100%"
/>
</el-form-item>
</template>
</el-table-column>
<el-table-column label="金额" prop="totalProductPrice" fixed="right" min-width="100">
<template #default="{ row, $index }">
<el-form-item :prop="`${$index}.totalProductPrice`" class="mb-0px!">
<el-input
disabled
v-model="row.totalProductPrice"
:formatter="erpPriceInputFormatter"
/>
</el-form-item>
</template>
</el-table-column>
<el-table-column label="税率(%" fixed="right" min-width="115">
<template #default="{ row, $index }">
<el-form-item :prop="`${$index}.taxPercent`" class="mb-0px!">
<el-input-number
v-model="row.taxPercent"
controls-position="right"
:min="0"
:precision="4"
class="!w-100%"
/>
</el-form-item>
</template>
</el-table-column>
<el-table-column label="税额" prop="taxPrice" fixed="right" min-width="120">
<template #default="{ row, $index }">
<el-form-item :prop="`${$index}.taxPrice`" class="mb-0px!">
<el-form-item :prop="`${$index}.taxPrice`" class="mb-0px!">
<el-input disabled v-model="row.taxPrice" :formatter="erpPriceInputFormatter" />
</el-form-item> </el-form-item>
</el-form-item> <el-form-item label="单价" :prop="`${$index}.productPrice`">
</template> <el-input-number
</el-table-column> v-model="row.productPrice"
<el-table-column label="税额合计" prop="totalPrice" fixed="right" min-width="100"> controls-position="right"
<template #default="{ row, $index }"> :min="0"
<el-form-item :prop="`${$index}.totalPrice`" class="mb-0px!"> :precision="4"
<el-input disabled v-model="row.totalPrice" :formatter="erpPriceInputFormatter" /> style="width: 100%"
</el-form-item> />
</template> </el-form-item>
</el-table-column> </div>
<el-table-column label="备注" min-width="150">
<template #default="{ row, $index }"> <!-- 金额 -->
<el-form-item :prop="`${$index}.remark`" class="mb-0px!"> <div class="mobile-item-card__info-row">
<span class="mobile-item-card__info-label">金额</span>
<span class="mobile-item-card__info-value">{{ erpPriceInputFormatter(row.totalProductPrice) }}</span>
</div>
<!-- 税率 -->
<div class="mobile-item-card__input-group">
<el-form-item label="税率(%" :prop="`${$index}.taxPercent`">
<el-input-number
v-model="row.taxPercent"
controls-position="right"
:min="0"
:precision="4"
style="width: 100%"
/>
</el-form-item>
</div>
<!-- 税额 -->
<div class="mobile-item-card__info-row">
<span class="mobile-item-card__info-label">税额</span>
<span class="mobile-item-card__info-value">{{ erpPriceInputFormatter(row.taxPrice) }}</span>
</div>
<!-- 税额合计 -->
<div class="mobile-item-card__info-row mobile-item-card__info-row--highlight">
<span class="mobile-item-card__info-label">税额合计</span>
<span class="mobile-item-card__info-value mobile-item-card__info-value--bold">{{ erpPriceInputFormatter(row.totalPrice) }}</span>
</div>
<!-- 备注 -->
<el-form-item label="备注" :prop="`${$index}.remark`">
<el-input v-model="row.remark" placeholder="请输入备注" /> <el-input v-model="row.remark" placeholder="请输入备注" />
</el-form-item> </el-form-item>
</template> </div>
</el-table-column> </div>
<el-table-column align="center" fixed="right" label="操作" width="60">
<template #default="{ $index }"> <!-- 合计 -->
<el-button :disabled="formData.length === 1" @click="handleDelete($index)" link> <div class="mobile-item-summary" v-if="formData.length > 0">
<div class="mobile-item-summary__row"><span>合计数量</span><span>{{ erpCountInputFormatter(summaryData.count) }}</span></div>
</el-button> <div class="mobile-item-summary__row"><span>合计金额</span><span>{{ erpPriceInputFormatter(summaryData.totalProductPrice) }}</span></div>
</template> <div class="mobile-item-summary__row"><span>合计税额</span><span>{{ erpPriceInputFormatter(summaryData.taxPrice) }}</span></div>
</el-table-column> <div class="mobile-item-summary__row mobile-item-summary__row--total"><span>税额合计</span><span>{{ erpPriceInputFormatter(summaryData.totalPrice) }}</span></div>
</div>
</div>
</el-form>
<!-- PC端布局 -->
<el-form v-else ref="formRef" :model="formData" :rules="formRules" v-loading="formLoading" label-width="0px" :inline-message="true" :disabled="disabled">
<el-table :data="formData" show-summary :summary-method="getSummaries" class="-mt-10px">
<el-table-column label="序号" type="index" align="center" width="60" />
<el-table-column label="仓库名称" min-width="125"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.warehouseId`" :rules="formRules.warehouseId" class="mb-0px!"><el-select v-model="row.warehouseId" clearable filterable placeholder="请选择仓库" @change="onChangeWarehouse($event, row)"><el-option v-for="item in warehouseList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></template></el-table-column>
<el-table-column label="产品名称" min-width="180"><template #default="{ row }"><el-form-item class="mb-0px!"><el-input disabled v-model="row.productName" /></el-form-item></template></el-table-column>
<el-table-column label="库存" min-width="100"><template #default="{ row }"><el-form-item class="mb-0px!"><el-input disabled v-model="row.stockCount" :formatter="erpCountInputFormatter" /></el-form-item></template></el-table-column>
<el-table-column label="条码" min-width="150"><template #default="{ row }"><el-form-item class="mb-0px!"><el-input disabled v-model="row.productBarCode" /></el-form-item></template></el-table-column>
<el-table-column label="单位" min-width="80"><template #default="{ row }"><el-form-item class="mb-0px!"><el-input disabled v-model="row.productUnitName" /></el-form-item></template></el-table-column>
<el-table-column label="已出库" fixed="right" min-width="80" v-if="formData[0]?.inCount != null"><template #default="{ row }"><el-form-item class="mb-0px!"><el-input disabled v-model="row.inCount" :formatter="erpCountInputFormatter" /></el-form-item></template></el-table-column>
<el-table-column label="已退货" fixed="right" min-width="80" v-if="formData[0]?.returnCount != null"><template #default="{ row }"><el-form-item class="mb-0px!"><el-input disabled v-model="row.returnCount" :formatter="erpCountInputFormatter" /></el-form-item></template></el-table-column>
<el-table-column label="数量" prop="count" fixed="right" min-width="140"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.count`" :rules="formRules.count" class="mb-0px!"><el-input-number v-model="row.count" controls-position="right" :min="0.0001" :precision="4" class="!w-100%" /></el-form-item></template></el-table-column>
<el-table-column label="产品单价" fixed="right" min-width="120"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.productPrice`" class="mb-0px!"><el-input-number v-model="row.productPrice" controls-position="right" :min="0" :precision="4" class="!w-100%" /></el-form-item></template></el-table-column>
<el-table-column label="金额" prop="totalProductPrice" fixed="right" min-width="100"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.totalProductPrice`" class="mb-0px!"><el-input disabled v-model="row.totalProductPrice" :formatter="erpPriceInputFormatter" /></el-form-item></template></el-table-column>
<el-table-column label="税率(%" fixed="right" min-width="115"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.taxPercent`" class="mb-0px!"><el-input-number v-model="row.taxPercent" controls-position="right" :min="0" :precision="4" class="!w-100%" /></el-form-item></template></el-table-column>
<el-table-column label="税额" prop="taxPrice" fixed="right" min-width="120"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.taxPrice`" class="mb-0px!"><el-input disabled v-model="row.taxPrice" :formatter="erpPriceInputFormatter" /></el-form-item></template></el-table-column>
<el-table-column label="税额合计" prop="totalPrice" fixed="right" min-width="100"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.totalPrice`" class="mb-0px!"><el-input disabled v-model="row.totalPrice" :formatter="erpPriceInputFormatter" /></el-form-item></template></el-table-column>
<el-table-column label="备注" min-width="150"><template #default="{ row, $index }"><el-form-item :prop="`${$index}.remark`" class="mb-0px!"><el-input v-model="row.remark" placeholder="请输入备注" /></el-form-item></template></el-table-column>
<el-table-column align="center" fixed="right" label="操作" width="60"><template #default="{ $index }"><el-button :disabled="formData.length === 1" @click="handleDelete($index)" link></el-button></template></el-table-column>
</el-table> </el-table>
</el-form> </el-form>
</template> </template>
<script setup lang="ts">
import { StockApi } from '@/api/erp/stock/stock'
import {
erpCountInputFormatter,
erpPriceInputFormatter,
erpPriceMultiply,
getSumValue
} from '@/utils'
import { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse'
const props = defineProps<{ <script setup lang="ts">
items: undefined import { ref, reactive, watch, onMounted, computed } from 'vue'
disabled: false import { StockApi } from '@/api/erp/stock/stock'
}>() import { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse'
const formLoading = ref(false) // 表单的加载中 import { erpCountInputFormatter, erpPriceInputFormatter, erpPriceMultiply, getSumValue } from '@/utils'
import { useWindowSize } from '@vueuse/core'
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const props = defineProps<{ items: any; disabled: boolean }>()
const formData = ref([]) const formData = ref([])
const formLoading = ref(false)
const warehouseList = ref<WarehouseVO[]>([])
const defaultWarehouse = ref<WarehouseVO>(undefined)
const formRef = ref<any>(null)
const formRules = reactive({ const formRules = reactive({
warehouseId: [{ required: true, message: '仓库不能为空', trigger: 'blur' }], warehouseId: [{ required: true, message: '仓库不能为空', trigger: 'blur' }],
productId: [{ required: true, message: '产品不能为空', trigger: 'blur' }], productId: [{ required: true, message: '产品不能为空', trigger: 'blur' }],
count: [{ required: true, message: '产品数量不能为空', trigger: 'blur' }] count: [{ required: true, message: '产品数量不能为空', trigger: 'blur' }]
}) })
const formRef = ref([]) // 表单 Ref
const warehouseList = ref<WarehouseVO[]>([]) // 仓库列表
const defaultWarehouse = ref<WarehouseVO>(undefined) // 默认仓库
/** 初始化设置出库项 */ // 初始化
watch( watch(
() => props.items, () => props.items,
async (val) => { async (val) => {
if (!val) return
val.forEach((item) => { val.forEach((item) => {
if (item.warehouseId == null) { if (!item.warehouseId) item.warehouseId = defaultWarehouse.value?.id
item.warehouseId = defaultWarehouse.value?.id if (item.stockCount === null && item.warehouseId != null) setStockCount(item)
}
if (item.stockCount === null && item.warehouseId != null) {
setStockCount(item)
}
}) })
formData.value = val formData.value = val
}, },
{ immediate: true } { immediate: true }
) )
/** 监听合同产品变化,计算合同产品总价 */ // 自动计算金额
watch( watch(
() => formData.value, () => formData.value,
(val) => { (val) => {
if (!val || val.length === 0) { if (!val) return
return
}
// 循环处理
val.forEach((item) => { val.forEach((item) => {
item.totalProductPrice = erpPriceMultiply(item.productPrice, item.count) item.totalProductPrice = erpPriceMultiply(item.productPrice, item.count)
item.taxPrice = erpPriceMultiply(item.totalProductPrice, item.taxPercent / 100.0) item.taxPrice = erpPriceMultiply(item.totalProductPrice, item.taxPercent / 100)
if (item.totalProductPrice != null) { item.totalPrice = item.totalProductPrice + (item.taxPrice || 0)
item.totalPrice = item.totalProductPrice + (item.taxPrice || 0)
} else {
item.totalPrice = undefined
}
}) })
}, },
{ deep: true } { deep: true }
) )
/** 合计 */ // 合计 - 移动端
const getSummaries = (param: SummaryMethodProps) => { const summaryData = computed(() =>
formData.value.reduce(
(acc, item) => {
acc.count += Number(item.count || 0)
acc.totalProductPrice += Number(item.totalProductPrice || 0)
acc.taxPrice += Number(item.taxPrice || 0)
acc.totalPrice += Number(item.totalPrice || 0)
return acc
},
{ count: 0, totalProductPrice: 0, taxPrice: 0, totalPrice: 0 }
)
)
// 合计 - PC端
const getSummaries = (param: any) => {
const { columns, data } = param const { columns, data } = param
const sums: string[] = [] const sums: string[] = []
columns.forEach((column, index: number) => { columns.forEach((column, index: number) => {
if (index === 0) { if (index === 0) { sums[index] = '合计'; return }
sums[index] = '合计'
return
}
if (['count', 'totalProductPrice', 'taxPrice', 'totalPrice'].includes(column.property)) { if (['count', 'totalProductPrice', 'taxPrice', 'totalPrice'].includes(column.property)) {
const sum = getSumValue(data.map((item) => Number(item[column.property]))) const sum = getSumValue(data.map((item) => Number(item[column.property])))
sums[index] = sums[index] = column.property === 'count' ? erpCountInputFormatter(sum) : erpPriceInputFormatter(sum)
column.property === 'count' ? erpCountInputFormatter(sum) : erpPriceInputFormatter(sum) } else { sums[index] = '' }
} else {
sums[index] = ''
}
}) })
return sums return sums
} }
/** 新增按钮操作 */ // 新增/删除/仓库切换
const handleAdd = () => { const handleAdd = () => {
const row = { formData.value.push({
id: undefined, id: undefined,
productId: undefined, productId: undefined,
productUnitName: undefined, // 产品单位 productUnitName: undefined,
productBarCode: undefined, // 产品条码 productBarCode: undefined,
productPrice: undefined, productPrice: undefined,
stockCount: undefined, stockCount: undefined,
count: 1, count: 1,
@@ -268,33 +266,45 @@ const handleAdd = () => {
taxPrice: undefined, taxPrice: undefined,
totalPrice: undefined, totalPrice: undefined,
remark: undefined remark: undefined
} })
formData.value.push(row)
} }
const handleDelete = (index: number) => formData.value.splice(index, 1)
/** 删除按钮操作 */ const onChangeWarehouse = (warehouseId, row) => setStockCount(row)
const handleDelete = (index: number) => {
formData.value.splice(index, 1)
}
/** 加载库存 */
const setStockCount = async (row: any) => { const setStockCount = async (row: any) => {
if (!row.productId) { if (!row.productId) return
return
}
const count = await StockApi.getStockCount(row.productId) const count = await StockApi.getStockCount(row.productId)
row.stockCount = count || 0 row.stockCount = count || 0
} }
const validate = () => formRef.value?.validate()
/** 表单校验 */
const validate = () => {
return formRef.value.validate()
}
defineExpose({ validate }) defineExpose({ validate })
/** 初始化 */
onMounted(async () => { onMounted(async () => {
warehouseList.value = await WarehouseApi.getWarehouseSimpleList() warehouseList.value = await WarehouseApi.getWarehouseSimpleList()
defaultWarehouse.value = warehouseList.value.find((item) => item.defaultStatus) defaultWarehouse.value = warehouseList.value.find((item) => item.defaultStatus)
}) })
</script> </script>
<style scoped lang="scss">
.mobile-item-list { display: flex; flex-direction: column; gap: 10px; }
.mobile-item-card { background: #f9f9fb; border-radius: 8px; padding: 12px; border: 1px solid #ebeef5;
&__header { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px solid #f0f0f0; }
&__index { font-size: 12px; color: #909399; font-weight: 600; }
&__name { flex: 1; font-size: 14px; font-weight: 600; color: #303133; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
&__body { font-size: 13px; }
&__info-row { display: flex; justify-content: space-between; padding: 4px 0;
&--highlight { margin-top: 4px; padding-top: 8px; border-top: 1px dashed #e4e7ed; }
}
&__info-label { color: #909399; flex-shrink: 0; }
&__info-value { color: #606266; text-align: right;
&--bold { font-weight: 600; color: #303133; }
}
&__input-group { display: flex; gap: 10px; margin-top: 6px;
:deep(.el-form-item) { flex: 1; margin-bottom: 10px; }
}
}
.mobile-item-summary { background: #fff; border-radius: 8px; padding: 12px; border: 1px solid #e6a23c;
&__row { display: flex; justify-content: space-between; padding: 4px 0; font-size: 13px; color: #606266;
&--total { margin-top: 4px; padding-top: 8px; border-top: 1px solid #f0f0f0; font-weight: 600; font-size: 14px; color: #303133; }
}
}
</style>

View File

@@ -1,56 +1,80 @@
<!-- 可退款的采购退货单列表 --> <!-- 可退款的采购退货单列表 -->
<template> <template>
<Dialog <!-- 移动端布局 -->
title="选择采购退货(仅展示可退款)" <el-drawer v-if="isMobile" v-model="dialogVisible" title="选择采购退货(仅展示可退款)" direction="rtl" size="100%" :append-to-body="true" class="mobile-enable-drawer">
v-model="dialogVisible" <!-- 搜索栏 -->
:appendToBody="true" <div class="mobile-enable__search">
:scroll="true" <el-input v-model="queryParams.no" placeholder="搜索退货单号" clearable @keyup.enter="handleQuery" :prefix-icon="Search" />
width="1080" <el-button :icon="Filter" circle @click="enableFilterVisible = true" />
> </div>
<!-- 卡片列表 -->
<div class="mobile-enable__list" v-loading="loading">
<div v-if="list.length === 0 && !loading" class="mobile-enable__empty"><el-empty description="暂无可退款退货单" /></div>
<div v-for="item in list" :key="item.id" class="mobile-enable__card" :class="{ 'mobile-enable__card--selected': isSelected(item) }" @click="toggleSelect(item)">
<div class="mobile-enable__card-check"><el-checkbox :model-value="isSelected(item)" @click.stop /></div>
<div class="mobile-enable__card-content">
<div class="mobile-enable__card-header"><span class="mobile-enable__card-no">{{ item.no }}</span></div>
<div class="mobile-enable__card-row"><span class="mobile-enable__card-label">供应商</span><span class="mobile-enable__card-value">{{ item.supplierName || '-' }}</span></div>
<div class="mobile-enable__card-row"><span class="mobile-enable__card-label">产品</span><span class="mobile-enable__card-value mobile-enable__card-value--ellipsis">{{ item.productNames || '-' }}</span></div>
<div class="mobile-enable__card-row"><span class="mobile-enable__card-label">退货时间</span><span class="mobile-enable__card-value">{{ formatDate2(item.returnTime) }}</span></div>
<div class="mobile-enable__card-row"><span class="mobile-enable__card-label">创建人</span><span class="mobile-enable__card-value">{{ item.creatorName || '-' }}</span></div>
<div class="mobile-enable__card-nums">
<div class="mobile-enable__card-num"><div class="mobile-enable__card-num-val">¥{{ erpPriceInputFormatter(item.totalPrice) }}</div><div class="mobile-enable__card-num-label">应退</div></div>
<div class="mobile-enable__card-num"><div class="mobile-enable__card-num-val" style="color:#67c23a">¥{{ erpPriceInputFormatter(item.refundPrice) }}</div><div class="mobile-enable__card-num-label">已退</div></div>
<div class="mobile-enable__card-num"><div class="mobile-enable__card-num-val" :style="{ color: item.refundPrice === item.totalPrice ? '#909399' : '#f56c6c' }">¥{{ erpPriceInputFormatter(item.totalPrice - item.refundPrice) }}</div><div class="mobile-enable__card-num-label">未退</div></div>
</div>
</div>
</div>
</div>
<!-- 分页 -->
<div class="mobile-enable__pagination" v-if="total > 0">
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" :page-sizes="[10, 20]" layout="total, prev, pager, next" :pager-count="5" @pagination="getList" />
</div>
<!-- 筛选抽屉 -->
<el-drawer v-model="enableFilterVisible" title="筛选条件" direction="btt" size="50%" :append-to-body="true">
<el-form :model="queryParams" ref="queryFormRef" label-position="top">
<el-form-item label="产品" prop="productId">
<el-select v-model="queryParams.productId" clearable filterable placeholder="请选择产品" style="width:100%"><el-option v-for="item in productList" :key="item.id" :label="item.name" :value="item.id" /></el-select>
</el-form-item>
<el-form-item label="退货时间" prop="returnTime">
<el-date-picker v-model="queryParams.returnTime" value-format="YYYY-MM-DD HH:mm:ss" type="daterange" start-placeholder="开始" end-placeholder="结束" :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" style="width:100%" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="resetQuery">重置</el-button>
<el-button type="primary" @click="enableFilterVisible = false; handleQuery()">确认筛选</el-button>
</template>
</el-drawer>
<template #footer>
<div class="mobile-enable__footer">
<span class="mobile-enable__footer-info">已选 {{ selectionList.length }} </span>
<div>
<el-button @click="dialogVisible = false"> </el-button>
<el-button :disabled="!selectionList.length" type="primary" @click="submitForm"> </el-button>
</div>
</div>
</template>
</el-drawer>
<!-- PC端布局 -->
<Dialog v-else title="选择采购退货(仅展示可退款)" v-model="dialogVisible" :appendToBody="true" :scroll="true" width="1080">
<ContentWrap> <ContentWrap>
<!-- 搜索工作栏 --> <!-- 搜索工作栏 -->
<el-form <el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="退货单号" prop="no"> <el-form-item label="退货单号" prop="no">
<el-input <el-input v-model="queryParams.no" placeholder="请输入退货单号" clearable @keyup.enter="handleQuery" class="!w-160px" />
v-model="queryParams.no"
placeholder="请输入退货单号"
clearable
@keyup.enter="handleQuery"
class="!w-160px"
/>
</el-form-item> </el-form-item>
<el-form-item label="产品" prop="productId"> <el-form-item label="产品" prop="productId">
<el-select <el-select v-model="queryParams.productId" clearable filterable placeholder="请选择产品" class="!w-160px">
v-model="queryParams.productId" <el-option v-for="item in productList" :key="item.id" :label="item.name" :value="item.id" />
clearable
filterable
placeholder="请选择产品"
class="!w-160px"
>
<el-option
v-for="item in productList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="退货时间" prop="orderTime"> <el-form-item label="退货时间" prop="orderTime">
<el-date-picker <el-date-picker v-model="queryParams.returnTime" value-format="YYYY-MM-DD HH:mm:ss" type="daterange" start-placeholder="开始日期" end-placeholder="结束日期" :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" class="!w-160px" />
v-model="queryParams.returnTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-160px"
/>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
@@ -60,71 +84,65 @@
</ContentWrap> </ContentWrap>
<ContentWrap> <ContentWrap>
<el-table <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true" @selection-change="handleSelectionChange">
v-loading="loading"
:data="list"
:show-overflow-tooltip="true"
:stripe="true"
@selection-change="handleSelectionChange"
>
<el-table-column width="30" label="选择" type="selection" /> <el-table-column width="30" label="选择" type="selection" />
<el-table-column min-width="180" label="退货单号" align="center" prop="no" /> <el-table-column min-width="180" label="退货单号" align="center" prop="no" />
<el-table-column label="供应商" align="center" prop="supplierName" /> <el-table-column label="供应商" align="center" prop="supplierName" />
<el-table-column label="产品信息" align="center" prop="productNames" min-width="200" /> <el-table-column label="产品信息" align="center" prop="productNames" min-width="200" />
<el-table-column <el-table-column label="退货时间" align="center" prop="returnTime" :formatter="dateFormatter2" width="120px" />
label="退货时间"
align="center"
prop="returnTime"
:formatter="dateFormatter2"
width="120px"
/>
<el-table-column label="创建人" align="center" prop="creatorName" /> <el-table-column label="创建人" align="center" prop="creatorName" />
<el-table-column <el-table-column label="应退金额" align="center" prop="totalPrice" :formatter="erpPriceTableColumnFormatter" />
label="退金额" <el-table-column label="退金额" align="center" prop="refundPrice" :formatter="erpPriceTableColumnFormatter" />
align="center"
prop="totalPrice"
:formatter="erpPriceTableColumnFormatter"
/>
<el-table-column
label="已退金额"
align="center"
prop="refundPrice"
:formatter="erpPriceTableColumnFormatter"
/>
<el-table-column label="未退金额" align="center"> <el-table-column label="未退金额" align="center">
<template #default="scope"> <template #default="scope">
<span v-if="scope.row.refundPrice === scope.row.totalPrice">0</span> <span v-if="scope.row.refundPrice === scope.row.totalPrice">0</span>
<el-tag type="danger" v-else> <el-tag type="danger" v-else>{{ erpPriceInputFormatter(scope.row.totalPrice - scope.row.refundPrice) }}</el-tag>
{{ erpPriceInputFormatter(scope.row.totalPrice - scope.row.refundPrice) }}
</el-tag>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<!-- 分页 --> <!-- 分页 -->
<Pagination <Pagination v-model:limit="queryParams.pageSize" v-model:page="queryParams.pageNo" :total="total" @pagination="getList" />
v-model:limit="queryParams.pageSize"
v-model:page="queryParams.pageNo"
:total="total"
@pagination="getList"
/>
</ContentWrap> </ContentWrap>
<template #footer> <template #footer>
<el-button :disabled="!selectionList.length" type="primary" @click="submitForm"> <el-button :disabled="!selectionList.length" type="primary" @click="submitForm"> </el-button>
</el-button>
<el-button @click="dialogVisible = false"> </el-button> <el-button @click="dialogVisible = false"> </el-button>
</template> </template>
</Dialog> </Dialog>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import { Search, Filter } from '@element-plus/icons-vue'
import { ElTable } from 'element-plus' import { ElTable } from 'element-plus'
import { dateFormatter2 } from '@/utils/formatTime' import { dateFormatter2, formatDate } from '@/utils/formatTime'
import { erpPriceInputFormatter, erpPriceTableColumnFormatter } from '@/utils' import { erpPriceInputFormatter, erpPriceTableColumnFormatter } from '@/utils'
import { ProductApi, ProductVO } from '@/api/erp/product/product' import { ProductApi, ProductVO } from '@/api/erp/product/product'
import { PurchaseReturnApi, PurchaseReturnVO } from '@/api/erp/purchase/return' import { PurchaseReturnApi, PurchaseReturnVO } from '@/api/erp/purchase/return'
import { SaleReturnVO } from '@/api/erp/sale/return' import { SaleReturnVO } from '@/api/erp/sale/return'
import { useWindowSize } from '@vueuse/core'
defineOptions({ name: 'PurchaseInPaymentEnableList' }) defineOptions({ name: 'PurchaseReturnRefundEnableList' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const enableFilterVisible = ref(false)
const formatDate2 = (date: any) => {
if (!date) return '-'
return formatDate(new Date(date), 'YYYY-MM-DD')
}
/** 移动端选中操作 */
const isSelected = (item: PurchaseReturnVO) => {
return selectionList.value.some((s) => s.id === item.id)
}
const toggleSelect = (item: PurchaseReturnVO) => {
const idx = selectionList.value.findIndex((s) => s.id === item.id)
if (idx >= 0) {
selectionList.value.splice(idx, 1)
} else {
selectionList.value.push(item)
}
}
const list = ref<PurchaseReturnVO[]>([]) // 列表的数据 const list = ref<PurchaseReturnVO[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数 const total = ref(0) // 列表的总页数
@@ -198,3 +216,109 @@ const handleQuery = () => {
getList() getList()
} }
</script> </script>
<style lang="scss" scoped>
.mobile-enable__search {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 12px;
.el-input { flex: 1; }
}
.mobile-enable__list {
display: flex;
flex-direction: column;
gap: 10px;
}
.mobile-enable__empty {
padding: 40px 0;
}
.mobile-enable__card {
display: flex;
gap: 10px;
background: #fff;
border-radius: 10px;
padding: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
border: 2px solid transparent;
transition: border-color 0.2s;
&--selected {
border-color: #409eff;
background: #f0f7ff;
}
}
.mobile-enable__card-check {
display: flex;
align-items: flex-start;
padding-top: 2px;
}
.mobile-enable__card-content {
flex: 1;
min-width: 0;
}
.mobile-enable__card-header {
margin-bottom: 6px;
}
.mobile-enable__card-no {
font-weight: 600;
font-size: 14px;
color: #303133;
}
.mobile-enable__card-row {
display: flex;
justify-content: space-between;
padding: 2px 0;
font-size: 13px;
}
.mobile-enable__card-label {
color: #909399;
flex-shrink: 0;
margin-right: 12px;
}
.mobile-enable__card-value {
color: #606266;
text-align: right;
&--ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 180px;
}
}
.mobile-enable__card-nums {
display: flex;
justify-content: space-around;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid #f0f0f0;
}
.mobile-enable__card-num {
text-align: center;
}
.mobile-enable__card-num-val {
font-size: 14px;
font-weight: 600;
color: #303133;
}
.mobile-enable__card-num-label {
font-size: 11px;
color: #909399;
margin-top: 2px;
}
.mobile-enable__pagination {
margin-top: 12px;
display: flex;
justify-content: center;
:deep(.el-pagination) { flex-wrap: wrap; justify-content: center; }
}
.mobile-enable__footer {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.mobile-enable__footer-info {
font-size: 13px;
color: #909399;
}
</style>

View File

@@ -1,15 +1,36 @@
<template> <template>
<div class="mobile-purchase-return"> <!-- 移动端布局 -->
<!-- 顶部操作栏 --> <div v-if="isMobile" class="mobile-purchase-return">
<div class="mobile-header"> <div class="mobile-header">
<div class="mobile-header__search"> <div class="mobile-header__search"><el-input v-model="queryParams.no" placeholder="搜索退货单号" clearable @keyup.enter="handleQuery" :prefix-icon="Search" /></div>
<el-input v-model="queryParams.no" placeholder="搜索退货单号" clearable @keyup.enter="handleQuery" :prefix-icon="Search" />
</div>
<div class="mobile-header__actions"> <div class="mobile-header__actions">
<el-button :icon="Filter" circle @click="filterVisible = true" /> <el-button :icon="Filter" circle @click="filterVisible = true" />
<el-button type="primary" :icon="Plus" circle @click="openForm('create')" v-hasPermi="['erp:purchase-return:create']" /> <el-button type="primary" :icon="Plus" circle @click="openForm('create')" v-hasPermi="['erp:purchase-return:create']" />
</div> </div>
</div> </div>
<div class="mobile-header__quick-filter">
<div
class="quick-filter-item"
:class="{ active: queryParams.status === undefined }"
@click="handleQuickFilter(undefined)"
>
全部订单
</div>
<div
class="quick-filter-item"
:class="{ active: queryParams.status === 10 }"
@click="handleQuickFilter(10)"
>
待审核
</div>
<div
class="quick-filter-item"
:class="{ active: queryParams.status === 20 }"
@click="handleQuickFilter(20)"
>
已审核
</div>
</div>
<!-- 卡片列表 --> <!-- 卡片列表 -->
<div class="mobile-list" v-loading="loading"> <div class="mobile-list" v-loading="loading">
@@ -114,29 +135,251 @@
</template> </template>
</el-drawer> </el-drawer>
<!-- 表单弹窗添加/修改 -->
<PurchaseReturnForm ref="formRef" @success="getList" />
</div> </div>
<!-- PC端布局 -->
<template v-else>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="退货单号" prop="no">
<el-input
v-model="queryParams.no"
placeholder="请输入退货单号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="产品" prop="productId">
<el-select
v-model="queryParams.productId"
clearable
filterable
placeholder="请选择产品"
class="!w-240px"
>
<el-option
v-for="item in productList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="退货时间" prop="inTime">
<el-date-picker
v-model="queryParams.inTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="供应商" prop="supplierId">
<el-select
v-model="queryParams.supplierId"
clearable
filterable
placeholder="请选择供供应商"
class="!w-240px"
>
<el-option
v-for="item in supplierList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="仓库" prop="warehouseId">
<el-select
v-model="queryParams.warehouseId"
clearable
filterable
placeholder="请选择仓库"
class="!w-240px"
>
<el-option
v-for="item in warehouseList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="创建人" prop="creator">
<el-select
v-model="queryParams.creator"
clearable
filterable
placeholder="请选择创建人"
class="!w-240px"
>
<el-option
v-for="item in userList"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="关联订单" prop="orderNo">
<el-input
v-model="queryParams.orderNo"
placeholder="请输入关联订单"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="结算账户" prop="accountId">
<el-select
v-model="queryParams.accountId"
clearable
filterable
placeholder="请选择结算账户"
class="!w-240px"
>
<el-option
v-for="item in accountList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="退款状态" prop="refundStatus">
<el-select
v-model="queryParams.refundStatus"
placeholder="请选择退款状态"
clearable
class="!w-240px"
>
<el-option label="未退款" value="0" />
<el-option label="部分退款" value="1" />
<el-option label="全部退款" value="2" />
</el-select>
</el-form-item>
<el-form-item label="审核状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择审核状态"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.ERP_AUDIT_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="queryParams.remark"
placeholder="请输入备注"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['erp:purchase-return:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['erp:purchase-return:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button
type="danger"
plain
@click="handleDelete(selectionList.map((item) => item.id))"
v-hasPermi="['erp:purchase-return:delete']"
:disabled="selectionList.length === 0"
>
<Icon icon="ep:delete" class="mr-5px" /> 删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" @selection-change="handleSelectionChange" @row-click="handleRowClick">
<el-table-column width="30" label="选择" type="selection" />
<el-table-column min-width="180" label="退货单号" align="center" prop="no" />
<el-table-column label="产品信息" align="center" prop="productNames" min-width="200" />
<el-table-column label="供应商" align="center" prop="supplierName" />
<el-table-column label="退货时间" align="center" prop="returnTime" :formatter="dateFormatter2" width="120px" sortable />
<el-table-column label="创建人" align="center" prop="creatorName" />
<el-table-column label="总数量" align="center" prop="totalCount" :formatter="erpCountTableColumnFormatter" />
<el-table-column label="应退金额" align="center" prop="totalPrice" :formatter="erpPriceTableColumnFormatter" />
<el-table-column label="已退金额" align="center" prop="refundPrice" :formatter="erpPriceTableColumnFormatter" />
<el-table-column label="未退金额" align="center"><template #default="scope"><span v-if="scope.row.refundPrice === scope.row.totalPrice">0</span><el-tag type="danger" v-else>{{ erpPriceInputFormatter(scope.row.totalPrice - scope.row.refundPrice) }}</el-tag></template></el-table-column>
<el-table-column label="审核状态" align="center" fixed="right" width="90" prop="status"><template #default="scope"><dict-tag :type="DICT_TYPE.ERP_AUDIT_STATUS" :value="scope.row.status" /></template></el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="220">
<template #default="scope">
<el-button link @click="openForm('detail', scope.row.id)" v-hasPermi="['erp:purchase-return:query']">详情</el-button>
<el-button link type="primary" @click="openForm('update', scope.row.id)" v-hasPermi="['erp:purchase-return:update']" :disabled="scope.row.status === 20">编辑</el-button>
<el-button link type="primary" @click="handleUpdateStatus(scope.row.id, 20)" v-hasPermi="['erp:purchase-return:update-status']" v-if="scope.row.status === 10">审批</el-button>
<el-button link type="danger" @click="handleUpdateStatus(scope.row.id, 10)" v-hasPermi="['erp:purchase-return:update-status']" v-if="scope.row.status === 20">反审批</el-button>
<el-button link type="danger" @click="handleDelete([scope.row.id])" v-hasPermi="['erp:purchase-return:delete']">删除</el-button>
</template>
</el-table-column>
</el-table>
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" />
</ContentWrap>
</template>
<!-- 表单弹窗添加/修改 -->
<PurchaseReturnForm ref="formRef" @success="getList" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import { Search, Filter, Plus } from '@element-plus/icons-vue' import { Search, Filter, Plus } from '@element-plus/icons-vue'
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { formatDate } from '@/utils/formatTime' import { formatDate, dateFormatter2 } from '@/utils/formatTime'
import download from '@/utils/download' import download from '@/utils/download'
import { PurchaseReturnApi, PurchaseReturnVO } from '@/api/erp/purchase/return' import { PurchaseReturnApi, PurchaseReturnVO } from '@/api/erp/purchase/return'
import PurchaseReturnForm from './PurchaseReturnForm.vue' import PurchaseReturnForm from './PurchaseReturnForm.vue'
import { ProductApi, ProductVO } from '@/api/erp/product/product' import { ProductApi, ProductVO } from '@/api/erp/product/product'
import { UserVO } from '@/api/system/user' import { UserVO } from '@/api/system/user'
import * as UserApi from '@/api/system/user' import * as UserApi from '@/api/system/user'
import { erpCountInputFormatter, erpPriceInputFormatter } from '@/utils' import { erpCountInputFormatter, erpPriceInputFormatter, erpCountTableColumnFormatter, erpPriceTableColumnFormatter } from '@/utils'
import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier' import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier'
import { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse' import { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse'
import { AccountApi, AccountVO } from '@/api/erp/finance/account' import { AccountApi, AccountVO } from '@/api/erp/finance/account'
import { useWindowSize } from '@vueuse/core'
/** ERP 采购退货列表 */ /** ERP 采购退货列表 */
defineOptions({ name: 'ErpPurchaseReturn' }) defineOptions({ name: 'ErpPurchaseReturn' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const message = useMessage() const message = useMessage()
const { t } = useI18n() const { t } = useI18n()
@@ -218,9 +461,27 @@ const handleDelete = async (ids: number[]) => {
await PurchaseReturnApi.deletePurchaseReturn(ids) await PurchaseReturnApi.deletePurchaseReturn(ids)
message.success(t('common.delSuccess')) message.success(t('common.delSuccess'))
await getList() await getList()
selectionList.value = selectionList.value.filter((item) => !ids.includes(item.id))
} catch {} } catch {}
} }
/** 选中操作(PC端) */
const selectionList = ref<PurchaseReturnVO[]>([])
const handleSelectionChange = (rows: PurchaseReturnVO[]) => {
selectionList.value = rows
}
/** 行点击操作(PC端) */
const handleRowClick = (row: PurchaseReturnVO, column: any, event: MouseEvent) => {
const target = event.target as HTMLElement
if (target.tagName === 'BUTTON' || target.tagName === 'A' || target.tagName === 'I' || target.tagName === 'svg' || target.closest('button') || target.closest('a') || target.closest('.el-button') || target.closest('.el-checkbox')) return
if (row.status === 20) {
openForm('detail', row.id)
} else if (row.status === 10) {
openForm('update', row.id)
}
}
const handleUpdateStatus = async (id: number, status: number) => { const handleUpdateStatus = async (id: number, status: number) => {
try { try {
await message.confirm(`确定${status === 20 ? '审批' : '反审批'}该退货吗?`) await message.confirm(`确定${status === 20 ? '审批' : '反审批'}该退货吗?`)
@@ -251,9 +512,41 @@ onMounted(async () => {
warehouseList.value = await WarehouseApi.getWarehouseSimpleList() warehouseList.value = await WarehouseApi.getWarehouseSimpleList()
accountList.value = await AccountApi.getAccountSimpleList() accountList.value = await AccountApi.getAccountSimpleList()
}) })
/** 快捷分类筛选 */
const handleQuickFilter = (status: number | undefined) => {
queryParams.status = status
queryParams.pageNo = 1
getList()
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.mobile-header__quick-filter {
display: flex;
gap: 12px;
margin: 8px 0;
justify-content: flex-start;
.quick-filter-item {
padding: 4px 12px;
font-size: 14px;
border-radius: 20px;
cursor: pointer;
color: #909399;
background: transparent;
transition: all 0.2s;
&.active {
color: #fff;
background: #409eff;
}
}
}
.mobile-purchase-return { .mobile-purchase-return {
padding: 12px; padding: 12px;
background: #f5f5f5; background: #f5f5f5;

View File

@@ -1,5 +1,79 @@
<template> <template>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="500"> <!-- 移动端使用抽屉 -->
<el-drawer
v-if="isMobile"
v-model="dialogVisible"
:title="dialogTitle"
direction="rtl"
size="100%"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
v-loading="formLoading"
>
<div class="mobile-form-section">
<div class="mobile-form-section__title">基本信息</div>
<el-form-item label="司机姓名" prop="driverName">
<el-input v-model="formData.driverName" placeholder="请输入司机姓名" />
</el-form-item>
<el-form-item label="车牌号" prop="licensePlate">
<el-input v-model="formData.licensePlate" placeholder="请输入车牌号" />
</el-form-item>
<el-form-item label="联系电话" prop="contactPhone">
<el-input v-model="formData.contactPhone" placeholder="请输入联系电话" />
</el-form-item>
</div>
<div class="mobile-form-section">
<div class="mobile-form-section__title">车辆参数</div>
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="长(米)" prop="carLength">
<el-input-number v-model="formData.carLength" :min="0" :precision="2" :step="0.1" controls-position="right" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="宽(米)" prop="carWidth">
<el-input-number v-model="formData.carWidth" :min="0" :precision="2" :step="0.1" controls-position="right" style="width:100%" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="核定吨位(吨)" prop="ratedTonnage">
<el-input-number v-model="formData.ratedTonnage" :min="0" :precision="2" :step="0.1" controls-position="right" style="width:100%" />
</el-form-item>
</div>
<div class="mobile-form-section">
<div class="mobile-form-section__title">其他信息</div>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" />
</el-form-item>
</div>
</el-form>
<template #footer>
<div class="mobile-drawer-footer">
<el-button @click="dialogVisible = false" style="flex:1">取消</el-button>
<el-button type="primary" @click="submitForm" :disabled="formLoading" style="flex:1">确定</el-button>
</div>
</template>
</el-drawer>
<!-- PC端使用对话框 -->
<Dialog v-else :title="dialogTitle" v-model="dialogVisible" width="500">
<el-form <el-form
ref="formRef" ref="formRef"
:model="formData" :model="formData"
@@ -46,12 +120,19 @@
</template> </template>
</Dialog> </Dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import * as CarApi from '@/api/erp/purchase/car' import * as CarApi from '@/api/erp/purchase/car'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { computed } from 'vue'
import { useWindowSize } from '@vueuse/core'
defineOptions({ name: 'CarForm' }) defineOptions({ name: 'CarForm' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
@@ -136,4 +217,26 @@ const resetForm = () => {
} }
formRef.value?.resetFields() formRef.value?.resetFields()
} }
</script> </script>
<style lang="scss" scoped>
.mobile-form-section {
margin-bottom: 20px;
&__title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
}
.mobile-drawer-footer {
display: flex;
gap: 12px;
padding: 12px 16px;
border-top: 1px solid #f0f0f0;
}
</style>

View File

@@ -1,112 +1,196 @@
<template> <template>
<ContentWrap> <!-- 移动端布局 -->
<!-- 搜索工作栏 --> <div v-if="isMobile" class="mobile-car-list">
<el-form <!-- 顶部操作栏 -->
class="-mb-15px" <div class="mobile-header">
:model="queryParams" <div class="mobile-header__search">
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="司机姓名" prop="driverName">
<el-input
v-model="queryParams.driverName"
placeholder="请输入司机姓名"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="车牌号" prop="licensePlate">
<el-input <el-input
v-model="queryParams.licensePlate" v-model="queryParams.licensePlate"
placeholder="请输入车牌号" placeholder="搜索车牌号/司机"
clearable clearable
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
class="!w-240px" :prefix-icon="Search"
/> />
</el-form-item> </div>
<el-form-item> <div class="mobile-header__actions">
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> <el-button :icon="Filter" circle @click="filterVisible = true" />
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> <el-button type="primary" :icon="Plus" circle @click="openForm('create')" v-hasPermi="['erp:car:create']" />
<el-button </div>
type="primary" </div>
plain
@click="openForm('create')" <!-- 卡片列表 -->
v-hasPermi="['erp:car:create']" <div class="mobile-list" v-loading="loading">
> <div v-if="list.length === 0 && !loading" class="mobile-empty">
<Icon icon="ep:plus" class="mr-5px" /> 新增 <el-empty description="暂无车辆数据" />
</el-button> </div>
<el-button <div
type="success" v-for="row in list"
plain :key="row.id"
@click="openImportForm()" class="mobile-card"
v-hasPermi="['erp:car:import']" @click="openForm('update', row.id)"
> >
<Icon icon="ep:upload" class="mr-5px" /> 导入 <div class="mobile-card__header">
</el-button> <span class="mobile-card__plate">{{ row.licensePlate }}</span>
<el-button <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
type="success" </div>
plain <div class="mobile-card__body">
@click="handleExport" <div class="mobile-card__row">
:loading="exportLoading" <span class="mobile-card__label">司机</span>
v-hasPermi="['erp:car:export']" <span class="mobile-card__value">{{ row.driverName }}</span>
> </div>
<Icon icon="ep:download" class="mr-5px" /> 导出 <div class="mobile-card__row">
</el-button> <span class="mobile-card__label">电话</span>
</el-form-item> <span class="mobile-card__value">{{ row.contactPhone || '-' }}</span>
</el-form> </div>
</ContentWrap> <div class="mobile-card__row">
<span class="mobile-card__label">核定吨位</span>
<span class="mobile-card__value">{{ row.ratedTonnage ? row.ratedTonnage + ' 吨' : '-' }}</span>
</div>
<div class="mobile-card__row">
<span class="mobile-card__label">车辆尺寸</span>
<span class="mobile-card__value">{{ row.carLength && row.carWidth ? `${row.carLength}m × ${row.carWidth}m` : '-' }}</span>
</div>
</div>
<div class="mobile-card__footer" @click.stop>
<el-button size="small" type="primary" @click="openForm('update', row.id)" v-hasPermi="['erp:car:update']">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(row.id)" v-hasPermi="['erp:car:delete']">删除</el-button>
</div>
</div>
</div>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" @row-click="handleRowClick">
<el-table-column label="司机姓名" align="center" prop="driverName" />
<el-table-column label="车牌号" align="center" prop="licensePlate" />
<el-table-column label="联系电话" align="center" prop="contactPhone" />
<el-table-column label="长(米)" align="center" prop="carLength" />
<el-table-column label="宽(米)" align="center" prop="carWidth" />
<el-table-column label="核定吨位(吨)" align="center" prop="ratedTonnage" />
<el-table-column label="创建时间" align="center" prop="createTime" sortable>
<template #default="scope">
<span>{{ formatDate(new Date(scope.row.createTime)) }}</span>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['erp:car:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['erp:car:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 --> <!-- 分页 -->
<Pagination <div class="mobile-pagination" v-if="total > 0">
:total="total" <Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" :page-sizes="[10, 20]" layout="total, prev, pager, next" :pager-count="5" @pagination="getList" />
v-model:page="queryParams.pageNo" </div>
v-model:limit="queryParams.pageSize"
@pagination="getList" <!-- 筛选抽屉 -->
/> <el-drawer v-model="filterVisible" title="筛选条件" direction="btt" size="50%">
</ContentWrap> <el-form :model="queryParams" ref="queryFormRef" label-position="top">
<el-form-item label="司机姓名" prop="driverName">
<el-input v-model="queryParams.driverName" placeholder="请输入司机姓名" clearable style="width:100%" />
</el-form-item>
<el-form-item label="车牌号" prop="licensePlate">
<el-input v-model="queryParams.licensePlate" placeholder="请输入车牌号" clearable style="width:100%" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="resetQuery">重置</el-button>
<el-button type="primary" @click="handleFilterConfirm">确认筛选</el-button>
</template>
</el-drawer>
</div>
<!-- PC端布局 -->
<template v-else>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="司机姓名" prop="driverName">
<el-input
v-model="queryParams.driverName"
placeholder="请输入司机姓名"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="车牌号" prop="licensePlate">
<el-input
v-model="queryParams.licensePlate"
placeholder="请输入车牌号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['erp:car:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="openImportForm()"
v-hasPermi="['erp:car:import']"
>
<Icon icon="ep:upload" class="mr-5px" /> 导入
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['erp:car:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" @row-click="handleRowClick">
<el-table-column label="司机姓名" align="center" prop="driverName" />
<el-table-column label="车牌号" align="center" prop="licensePlate" />
<el-table-column label="联系电话" align="center" prop="contactPhone" />
<el-table-column label="长(米)" align="center" prop="carLength" />
<el-table-column label="宽(米)" align="center" prop="carWidth" />
<el-table-column label="核定吨位(吨)" align="center" prop="ratedTonnage" />
<el-table-column label="创建时间" align="center" prop="createTime" sortable>
<template #default="scope">
<span>{{ formatDate(new Date(scope.row.createTime)) }}</span>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['erp:car:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['erp:car:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
</template>
<!-- 表单弹窗添加/修改 --> <!-- 表单弹窗添加/修改 -->
<CarForm ref="formRef" @success="getList" /> <CarForm ref="formRef" @success="getList" />
@@ -116,6 +200,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import { DICT_TYPE } from '@/utils/dict' import { DICT_TYPE } from '@/utils/dict'
import { formatDate } from '@/utils/formatTime' import { formatDate } from '@/utils/formatTime'
import download from '@/utils/download' import download from '@/utils/download'
@@ -123,16 +208,22 @@ import * as CarApi from '@/api/erp/purchase/car/index'
import * as CarVO from '@/api/erp/purchase/car/index' import * as CarVO from '@/api/erp/purchase/car/index'
import CarForm from './CarForm.vue' import CarForm from './CarForm.vue'
import CarImportForm from './CarImportForm.vue' import CarImportForm from './CarImportForm.vue'
import { useWindowSize } from '@vueuse/core'
import { Search, Filter, Plus } from '@element-plus/icons-vue'
/** 车辆管理列表 */ /** 车辆管理列表 */
defineOptions({ name: 'ErpCar' }) defineOptions({ name: 'ErpCar' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中 const loading = ref(true) // 列表的加载中
const list = ref<any[]>([]) // 列表的数据 const list = ref<any[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数 const total = ref(0) // 列表的总页数
const filterVisible = ref(false) // 筛选抽屉
const queryParams = reactive({ const queryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
@@ -165,7 +256,13 @@ const handleQuery = () => {
/** 重置按钮操作 */ /** 重置按钮操作 */
const resetQuery = () => { const resetQuery = () => {
queryFormRef.value.resetFields() queryFormRef.value?.resetFields()
handleQuery()
}
/** 筛选确认 */
const handleFilterConfirm = () => {
filterVisible.value = false
handleQuery() handleQuery()
} }
@@ -210,7 +307,87 @@ const handleExport = async () => {
} }
/** 初始化 **/ /** 初始化 **/
onMounted(() => { onMounted(() => {
getList() getList()
}) })
</script> </script>
<style lang="scss" scoped>
.mobile-car-list {
padding: 12px;
background: #f5f5f5;
min-height: 100vh;
}
.mobile-header {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 12px;
&__search { flex: 1; }
&__actions { display: flex; gap: 4px; flex-shrink: 0; }
}
.mobile-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.mobile-empty { padding: 40px 0; }
.mobile-card {
background: #fff;
border-radius: 10px;
padding: 14px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
&__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
&__plate {
font-weight: 600;
font-size: 16px;
color: #303133;
}
&__body { font-size: 13px; }
&__row {
display: flex;
justify-content: space-between;
padding: 4px 0;
}
&__label {
color: #909399;
flex-shrink: 0;
margin-right: 12px;
}
&__value {
color: #606266;
text-align: right;
}
&__footer {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #f0f0f0;
}
}
.mobile-pagination {
margin-top: 12px;
display: flex;
justify-content: center;
:deep(.el-pagination) { flex-wrap: wrap; justify-content: center; }
}
</style>

View File

@@ -1,22 +1,8 @@
<template> <template>
<el-drawer <!-- 移动端布局 -->
v-model="dialogVisible" <el-drawer v-if="isMobile" v-model="dialogVisible" :title="dialogTitle" direction="rtl" size="100%" :close-on-press-escape="true" :destroy-on-close="true" :append-to-body="true" class="mobile-form-drawer">
:title="dialogTitle"
direction="rtl"
size="100%"
:close-on-press-escape="true"
:destroy-on-close="true"
:append-to-body="true"
class="mobile-form-drawer"
>
<div class="mobile-form" v-loading="formLoading"> <div class="mobile-form" v-loading="formLoading">
<el-form <el-form ref="formRef" :model="formData" :rules="formRules" label-position="top" :disabled="disabled">
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
:disabled="disabled"
>
<div class="mobile-form-section"> <div class="mobile-form-section">
<div class="mobile-form-section__title">基础信息</div> <div class="mobile-form-section__title">基础信息</div>
<el-form-item label="名称" prop="name"> <el-form-item label="名称" prop="name">
@@ -251,15 +237,62 @@
</div> </div>
</div> </div>
</el-drawer> </el-drawer>
<!-- PC端布局 -->
<Dialog v-else :title="dialogTitle" v-model="dialogVisible" width="800px" class="supplier-form-dialog">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px" class="supplier-form" v-loading="formLoading" :disabled="disabled">
<el-row :gutter="20">
<el-col :span="24"><el-divider class="supplier-section-divider">基础信息</el-divider></el-col>
<el-col :span="12" :xs="24" :sm="12"><el-form-item label="名称" prop="name"><el-input v-model="formData.name" placeholder="请输入名称" /></el-form-item></el-col>
<el-col :span="12" :xs="24" :sm="12"><el-form-item label="企业性质" prop="enterpriseNature"><el-select v-model="formData.enterpriseNature" placeholder="请选择企业性质" class="!w-1/1"><el-option v-for="item in enterpriseNatureOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item></el-col>
<el-col :span="12" :xs="24" :sm="12"><el-form-item label="供应商类型" prop="type"><el-select v-model="formData.type" placeholder="请选择供应商类型" class="!w-1/1"><el-option label="生产型" :value="1" /><el-option label="贸易型" :value="2" /><el-option label="服务型" :value="3" /><el-option label="其他" :value="4" /></el-select></el-form-item></el-col>
<el-col :span="12" :xs="24" :sm="12"><el-form-item label="注册资金" prop="registeredCapital"><el-input-number v-model="formData.registeredCapital" :min="0" :precision="2" placeholder="请输入注册资金" class="!w-1/1" /></el-form-item></el-col>
<el-col :span="12" :xs="24" :sm="12"><el-form-item label="使用/营业期限" prop="validPeriod"><el-input v-model="formData.validPeriod" placeholder="请输入使用/营业期限" /></el-form-item></el-col>
<el-col :span="12" :xs="24" :sm="12"><el-form-item label="邮编" prop="zipCode"><el-input v-model="formData.zipCode" placeholder="请输入邮编" /></el-form-item></el-col>
<el-col :span="12" :xs="24" :sm="12"><el-form-item label="联系人" prop="contact"><el-input v-model="formData.contact" placeholder="请输入联系人" /></el-form-item></el-col>
<el-col :span="12" :xs="24" :sm="12"><el-form-item label="手机号码" prop="mobile"><el-input v-model="formData.mobile" placeholder="请输入手机号码" /></el-form-item></el-col>
<el-col :span="12" :xs="24" :sm="12"><el-form-item label="联系电话" prop="telephone"><el-input v-model="formData.telephone" placeholder="请输入联系电话" /></el-form-item></el-col>
<el-col :span="12" :xs="24" :sm="12"><el-form-item label="电子邮箱" prop="email"><el-input v-model="formData.email" placeholder="请输入电子邮箱" /></el-form-item></el-col>
<el-col :span="24"><el-divider /></el-col>
<el-col :span="12" :xs="24" :sm="12"><el-form-item label="纳税人识别号" prop="taxNo"><el-input v-model="formData.taxNo" placeholder="请输入纳税人识别号" /></el-form-item></el-col>
<el-col :span="12" :xs="24" :sm="12"><el-form-item label="税率(%)" prop="taxPercent"><el-input-number v-model="formData.taxPercent" :min="0" :precision="4" placeholder="请输入税率" class="!w-1/1" /></el-form-item></el-col>
<el-col :span="12" :xs="24" :sm="12"><el-form-item label="开户行" prop="bankName"><el-input v-model="formData.bankName" placeholder="请输入开户行" /></el-form-item></el-col>
<el-col :span="12" :xs="24" :sm="12"><el-form-item label="开户账号" prop="bankAccount"><el-input v-model="formData.bankAccount" placeholder="请输入开户账号" /></el-form-item></el-col>
<el-col :span="12" :xs="24" :sm="12"><el-form-item label="开户地址" prop="bankAddress"><el-input v-model="formData.bankAddress" placeholder="请输入开户地址" /></el-form-item></el-col>
<el-col :span="12" :xs="24" :sm="12"><el-form-item label="银行行号" prop="bankLineNo"><el-input v-model="formData.bankLineNo" placeholder="请输入银行行号" /></el-form-item></el-col>
<el-col :span="12" :xs="24" :sm="12"><el-form-item label="付款条件" prop="paymentTerms"><el-select v-model="formData.paymentTerms" placeholder="请选择付款条件" class="!w-1/1"><el-option v-for="item in paymentTermsOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item></el-col>
<el-col :span="24"><el-divider /></el-col>
<el-col :span="24"><el-form-item label="生产/经营范围" prop="productScope"><el-input type="textarea" v-model="formData.productScope" placeholder="请输入生产产品范围/经营范围" /></el-form-item></el-col>
<el-col :span="24"><el-form-item label="生产厂地址" prop="factoryAddress"><el-input type="textarea" v-model="formData.factoryAddress" placeholder="请输入生产厂地址" /></el-form-item></el-col>
<el-col :span="12" :xs="24" :sm="12"><el-form-item label="年度订单数" prop="annualOrderNums"><el-input :model-value="formatInteger(formData.annualOrderNums)" placeholder="系统自动生成" disabled /></el-form-item></el-col>
<el-col :span="12" :xs="24" :sm="12"><el-form-item label="年度订单额" prop="annualOrderAmounts"><el-input :model-value="formatAmount(formData.annualOrderAmounts)" placeholder="系统自动生成" disabled /></el-form-item></el-col>
<el-col :span="12" :xs="24" :sm="12"><el-form-item label="年度已付款" prop="annualPaidAmounts"><el-input :model-value="formatAmount(formData.annualPaidAmounts)" placeholder="系统自动生成" disabled /></el-form-item></el-col>
<el-col :span="12" :xs="24" :sm="12"><el-form-item label="累计未付款" prop="totalUnpaidAmounts"><el-input :model-value="formatAmount(formData.totalUnpaidAmounts)" placeholder="系统自动生成" disabled /></el-form-item></el-col>
<el-col :span="24"><el-form-item label="备注" prop="remark"><el-input type="textarea" v-model="formData.remark" placeholder="请输入备注" /></el-form-item></el-col>
<el-col :span="24"><el-divider class="supplier-section-divider">状态信息</el-divider></el-col>
<el-col :span="12" :xs="12" :sm="12"><el-form-item label="开启状态" prop="status"><el-radio-group v-model="formData.status"><el-radio v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio></el-radio-group></el-form-item></el-col>
<el-col :span="12" :xs="12" :sm="12"><el-form-item label="排序" prop="sort"><el-input-number v-model="formData.sort" placeholder="请输入排序" class="!w-1/1" :precision="0" /></el-form-item></el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier' import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier'
import { CommonStatusEnum } from '@/utils/constants' import { CommonStatusEnum } from '@/utils/constants'
import { useWindowSize } from '@vueuse/core'
/** ERP 表单 */ /** ERP 表单 */
defineOptions({ name: 'SupplierForm' }) defineOptions({ name: 'SupplierForm' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
@@ -527,4 +560,18 @@ const resetForm = () => {
:deep(.el-date-editor) { :deep(.el-date-editor) {
width: 100% !important; width: 100% !important;
} }
/* PC端样式 */
:deep(.supplier-form .el-form-item__label) {
white-space: nowrap !important;
overflow: hidden;
text-overflow: ellipsis;
}
:deep(.supplier-form .supplier-section-divider) {
margin: 16px 0 12px;
}
:deep(.supplier-form-dialog .el-dialog__body) {
padding-left: 12px;
padding-right: 12px;
}
</style> </style>

Some files were not shown because too many files have changed in this diff Show More