feat(销售权限): 实现钢卷销售权限分配功能

新增销售权限管理模块,包含以下功能:
1. 在用户模块添加id字段用于权限控制
2. 重构CoilSelector组件支持销售视角权限过滤
3. 新增销售权限分配页面,支持钢卷分配与移除
4. 优化表格样式和交互体验

组件现在支持根据用户权限动态显示和过滤钢卷数据,管理员可在新页面为销售分配钢卷权限
This commit is contained in:
砂糖
2025-12-18 11:51:14 +08:00
parent b7513bdd2d
commit e900aec86b
9 changed files with 749 additions and 82 deletions

View File

@@ -7,7 +7,8 @@
<el-row :gutter="10">
<el-col :span="18">
<flow-table />
<!-- <flow-table /> -->
<el-empty description="办公模块定制开发中"></el-empty>
</el-col>
<el-col :span="6">
<mini-calendar />

View File

@@ -0,0 +1,586 @@
<template>
<!-- 销售权限 -->
<el-row :gutter="10" class="sales-permission-container">
<!-- 销售列表 -->
<el-col :span="3" class="sales-tree-container">
<div class="tree-title">销售列表</div>
<!-- 用树展示销售用户列表 -->
<el-tree ref="salesTree" :data="userTreeData" :props="treeProps" node-key="id" @node-click="handleTreeNodeClick"
highlight-current :expand-on-click-node="false">
</el-tree>
</el-col>
<el-col :span="21" class="coil-table-container">
<!-- 待分配卷表格标题 -->
<div class="table-title">
<span>待分配钢卷</span>
<!-- 搜索区域 -->
<div style="display: flex; align-items: center; justify-content: flex-end;">
<el-form :inline="true" :model="queryParamsWithUnAssignedId" class="search-form">
<el-form-item label="卷号">
<el-input style="width: 100px;" v-model="queryParamsWithUnAssignedId.currentCoilNo" placeholder="输入卷号"
clearable size="small" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="物料">
<muti-select style="width: 100px;" v-model="queryParamsWithUnAssignedId.itemName"
:options="dict.type.coil_itemname" placeholder="请选择物料" clearable />
</el-form-item>
<el-form-item label="规格">
<memo-input style="width: 100px;" storageKey="coilSpec"
v-model="queryParamsWithUnAssignedId.specification" placeholder="请输入规格" clearable size="small"
@keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="材质">
<muti-select style="width: 100px;" v-model="queryParamsWithUnAssignedId.itemMaterial"
:options="dict.type.coil_material" placeholder="请选择材质" clearable />
</el-form-item>
<el-form-item label="厂家">
<muti-select style="width: 100px;" v-model="queryParamsWithUnAssignedId.itemManufacturer"
:options="dict.type.coil_manufacturer" placeholder="请选择厂家" clearable />
</el-form-item>
</el-form>
<pagination v-show="unAssignedTotal > 0" :total="unAssignedTotal"
:page.sync="queryParamsWithUnAssignedId.pageNum" :limit.sync="queryParamsWithUnAssignedId.pageSize"
@pagination="listUnAssignedCoil" />
<el-button type="primary" size="mini" icon="el-icon-refresh" @click="listUnAssignedCoil">刷新</el-button>
</div>
</div>
<!-- 上方是待分配的卷 -->
<KLPTable height="40vh" v-loading="unAssignedLoading" :data="unAssignedCoilList" :floatLayer="true"
:floatLayerConfig="floatLayerConfig">
<el-table-column label="入场钢卷号" align="center" prop="enterCoilNo">
<template slot-scope="scope">
<coil-no :coil-no="scope.row.enterCoilNo"></coil-no>
</template>
</el-table-column>
<el-table-column label="当前钢卷号" align="center" prop="currentCoilNo">
<template slot-scope="scope">
<coil-no :coil-no="scope.row.currentCoilNo"></coil-no>
</template>
</el-table-column>
<el-table-column label="逻辑库位" align="center" prop="warehouseName" />
<el-table-column label="实际库区" align="center" prop="actualWarehouseName" />
<el-table-column label="产品类型" align="center" width="250">
<template slot-scope="scope">
<ProductInfo v-if="scope.row.itemType == 'product'" :product="scope.row.product" />
<RawMaterialInfo v-else-if="scope.row.itemType === 'raw_material'" :material="scope.row.rawMaterial" />
</template>
</el-table-column>
<el-table-column label="更新时间" align="center" prop="updateTime">
<template slot-scope="scope">
{{ scope.row.updateTime }}
</template>
</el-table-column>
<el-table-column label="更新人" align="center" prop="updateByName" />
<el-table-column label="关联信息" align="center" :show-overflow-tooltip="true">
<template slot-scope="scope">
<span v-if="scope.row.parentCoilNos && scope.row.hasMergeSplit === 1 && scope.row.dataType === 1">
<el-tag type="warning" size="mini">来自母卷{{ scope.row.parentCoilNos }}</el-tag>
</span>
<span v-else-if="scope.row.parentCoilNos && scope.row.dataType === 0">
<el-tag type="info" size="mini">分为子卷{{ scope.row.parentCoilNos }}</el-tag>
</span>
<span v-else-if="scope.row.parentCoilNos && scope.row.hasMergeSplit === 2">
<el-tag type="success" size="mini">合并自{{ scope.row.parentCoilNos }}</el-tag>
</span>
<span v-else></span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button type="primary" size="mini" @click="handleAssign(scope.row)"
:disabled="!currentUser.id">分配</el-button>
</template>
</el-table-column>
</KLPTable>
<!-- 已分配卷区域有选中销售才显示表格否则显示空提示 -->
<div v-if="currentUser.id">
<!-- 已分配卷表格标题 -->
<div class="table-title" style="margin-top: 20px">
<span>已分配给 {{ currentUser.nickName || '未选择销售' }} 的钢卷</span>
<div style="display: flex; align-items: center; justify-content: flex-end;">
<!-- 搜索区域 -->
<el-form :inline="true" :model="queryParamsWithSalesId">
<el-form-item label="卷号">
<el-input style="width: 100px;" v-model="queryParamsWithSalesId.currentCoilNo" placeholder="请输入卷号"
clearable size="small" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="物料">
<muti-select style="width: 100px;" v-model="queryParamsWithSalesId.itemName"
:options="dict.type.coil_itemname" placeholder="请选择物料" clearable />
</el-form-item>
<el-form-item label="规格">
<memo-input style="width: 100px;" storageKey="coilSpec" v-model="queryParamsWithSalesId.specification"
placeholder="请输入规格" clearable size="small" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="材质">
<muti-select style="width: 100px;" v-model="queryParamsWithSalesId.itemMaterial"
:options="dict.type.coil_material" placeholder="请选择材质" clearable />
</el-form-item>
<el-form-item label="厂家">
<muti-select style="width: 100px;" v-model="queryParamsWithSalesId.itemManufacturer"
:options="dict.type.coil_manufacturer" placeholder="请选择厂家" clearable />
</el-form-item>
</el-form>
<pagination v-show="salesTotal > 0" :total="salesTotal" :page.sync="queryParamsWithSalesId.pageNum"
:limit.sync="queryParamsWithSalesId.pageSize" @pagination="listSalesCoil" />
<el-button type="primary" size="mini" icon="el-icon-refresh" @click="listSalesCoil"
:disabled="!currentUser.id">刷新</el-button>
</div>
</div>
<!-- 下方是已分配的卷 -->
<KLPTable height="40vh" v-loading="salesLoading" :data="salesCoilList" :floatLayer="true"
:floatLayerConfig="floatLayerConfig">
<el-table-column label="入场钢卷号" align="center" prop="enterCoilNo">
<template slot-scope="scope">
<coil-no :coil-no="scope.row.enterCoilNo"></coil-no>
</template>
</el-table-column>
<el-table-column label="当前钢卷号" align="center" prop="currentCoilNo">
<template slot-scope="scope">
<coil-no :coil-no="scope.row.currentCoilNo"></coil-no>
</template>
</el-table-column>
<el-table-column label="逻辑库位" align="center" prop="warehouseName" />
<el-table-column label="实际库区" align="center" prop="actualWarehouseName" />
<el-table-column label="产品类型" align="center" width="250">
<template slot-scope="scope">
<ProductInfo v-if="scope.row.itemType == 'product'" :product="scope.row.product" />
<RawMaterialInfo v-else-if="scope.row.itemType === 'raw_material'" :material="scope.row.rawMaterial" />
</template>
</el-table-column>
<el-table-column label="更新时间" align="center" prop="updateTime">
<template slot-scope="scope">
{{ scope.row.updateTime }}
</template>
</el-table-column>
<el-table-column label="更新人" align="center" prop="updateByName" />
<el-table-column label="关联信息" align="center" :show-overflow-tooltip="true">
<template slot-scope="scope">
<span v-if="scope.row.parentCoilNos && scope.row.hasMergeSplit === 1 && scope.row.dataType === 1">
<el-tag type="warning" size="mini">来自母卷{{ scope.row.parentCoilNos }}</el-tag>
</span>
<span v-else-if="scope.row.parentCoilNos && scope.row.dataType === 0">
<el-tag type="info" size="mini">分为子卷{{ scope.row.parentCoilNos }}</el-tag>
</span>
<span v-else-if="scope.row.parentCoilNos && scope.row.hasMergeSplit === 2">
<el-tag type="success" size="mini">合并自{{ scope.row.parentCoilNos }}</el-tag>
</span>
<span v-else></span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button type="danger" size="mini" @click="handleRemove(scope.row)">移除</el-button>
</template>
</el-table-column>
</KLPTable>
</div>
<!-- 未选择销售时的空提示 -->
<div v-else class="empty-container">
<el-empty description="请先从左侧选择销售用户"></el-empty>
</div>
</el-col>
</el-row>
</template>
<script>
import { listUser } from '@/api/system/user'
import { listMaterialCoil, updateMaterialCoilSimple } from '@/api/wms/coil'
import CoilNo from "@/components/KLPService/Renderer/CoilNo.vue";
import ProductInfo from "@/components/KLPService/Renderer/ProductInfo";
import RawMaterialInfo from "@/components/KLPService/Renderer/RawMaterialInfo";
import MutiSelect from "@/components/MutiSelect";
import MemoInput from "@/components/MemoInput";
export default {
name: 'SalesCoilPermission',
components: {
CoilNo,
ProductInfo,
RawMaterialInfo,
MutiSelect,
MemoInput
},
dicts: ['coil_itemname', 'coil_material', 'coil_manufacturer'],
data() {
return {
// 用户列表(原始数据)
userList: [],
// 树形结构用户数据
userTreeData: [],
// 树组件配置
treeProps: {
label: 'nickName',
children: 'children'
},
// 部门ID销售部门
deptId: '2001463253405609986',
// 已分配钢卷查询参数
queryParamsWithSalesId: {
selectType: 'product',
materialType: '成品',
saleId: undefined,
pageNum: 1,
pageSize: 10
},
// 待分配钢卷查询参数
queryParamsWithUnAssignedId: {
selectType: 'product',
materialType: '成品',
saleId: -1,
pageNum: 1,
pageSize: 10
},
// 待分配钢卷列表
unAssignedCoilList: [],
// 待分配钢卷总数
unAssignedTotal: 0,
// 已分配钢卷列表
salesCoilList: [],
// 已分配钢卷总数
salesTotal: 0,
// 当前选中的销售用户
currentUser: {},
// 浮动层配置
floatLayerConfig: {
columns: [
{ label: '入场钢卷号', prop: 'enterCoilNo' },
{ label: '当前钢卷号', prop: 'currentCoilNo' },
{ label: '厂家卷号', prop: 'supplierCoilNo' },
{ label: '逻辑库位', prop: 'warehouseName' },
{ label: '实际库位', prop: 'actualWarehouseName' },
{ label: '物料类型', prop: 'materialType' },
{ label: '班组', prop: 'team' },
{ label: '净重', prop: 'netWeight' },
{ label: '毛重', prop: 'grossWeight' },
{ label: '备注', prop: 'remark' },
{ label: '质量状态', prop: 'qualityStatus' },
{ label: '打包状态', prop: 'packingStatus' },
{ label: '切边要求', prop: 'edgeRequirement' },
{ label: '包装要求', prop: 'packagingRequirement' },
{ label: '厂家', prop: 'itemManufacturer' }
],
title: '详细信息'
},
// 拆分loading状态分别控制两个表格
unAssignedLoading: false, // 待分配表格loading
salesLoading: false, // 已分配表格loading
// 选中的待分配钢卷
selectedUnAssignedCoils: [],
// 选中的已分配钢卷
selectedSalesCoils: []
}
},
created() {
this.listUser()
// 初始化加载待分配钢卷
this.listUnAssignedCoil()
},
methods: {
/**
* 获取销售用户列表并构建树形结构
*/
async listUser() {
try {
const res = await listUser({ deptId: this.deptId, pageSize: 999 })
this.userList = res.rows || []
// 构建树形结构(如果用户有层级关系,这里简单处理为平级)
this.userTreeData = this.userList.map(user => ({
id: user.userId,
nickName: user.nickName,
userName: user.userName,
deptId: user.deptId,
children: [] // 如果有子级可以在这里处理
}))
} catch (error) {
this.$message.error('获取销售列表失败:' + error.message)
}
},
/**
* 树形节点点击事件
* @param {Object} node 选中的节点
*/
handleTreeNodeClick(node) {
this.currentUser = node
console.log(node)
this.queryParamsWithSalesId.saleId = node.id
// 清空之前选中的已分配钢卷
this.selectedSalesCoils = []
// 加载该销售的已分配钢卷
this.listSalesCoil()
},
/**
* 获取待分配钢卷列表
*/
async listUnAssignedCoil() {
this.unAssignedLoading = true
try {
const res = await listMaterialCoil(this.queryParamsWithUnAssignedId)
console.log(res)
this.unAssignedCoilList = res.rows || []
this.unAssignedTotal = res.total || 0
} catch (error) {
this.$message.error('获取待分配钢卷失败:' + error.message)
this.unAssignedCoilList = []
this.unAssignedTotal = 0
} finally {
this.unAssignedLoading = false
}
},
/**
* 获取已分配钢卷列表
*/
async listSalesCoil() {
if (!this.currentUser.id) return
this.salesLoading = true
try {
const res = await listMaterialCoil(this.queryParamsWithSalesId)
this.salesCoilList = res.rows || []
this.salesTotal = res.total || 0
} catch (error) {
this.$message.error('获取已分配钢卷失败:' + error.message)
this.salesCoilList = []
this.salesTotal = 0
} finally {
this.salesLoading = false
}
},
/**
* 单个钢卷分配操作
* @param {Object} row 钢卷数据
*/
async handleAssign(row) {
try {
await this.$confirm('确定要将该钢卷分配给 ' + this.currentUser.nickName + ' 吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
const res = await updateMaterialCoilSimple({
...row,
saleId: this.currentUser.id
})
if (res.code === 200) {
this.$message.success('分配成功!')
// 刷新列表
this.listUnAssignedCoil()
this.listSalesCoil()
} else {
this.$message.error(res.msg || '分配失败')
}
} catch (error) {
if (error !== 'cancel') {
this.$message.error('分配操作失败:' + error.message)
}
}
},
/**
* 单个钢卷移除操作
* @param {Object} row 钢卷数据
*/
async handleRemove(row) {
try {
await this.$confirm('确定要移除该钢卷的分配权限吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
const res = await updateMaterialCoilSimple({
...row,
saleId: null
})
if (res.code === 200) {
this.$message.success('移除成功!')
// 刷新列表
this.listUnAssignedCoil()
this.listSalesCoil()
} else {
this.$message.error(res.msg || '移除失败')
}
} catch (error) {
if (error !== 'cancel') {
this.$message.error('移除操作失败:' + error.message)
}
}
},
/**
* 待分配钢卷选择事件
* @param {Array} val 选中的钢卷列表
*/
handleUnAssignedSelectionChange(val) {
this.selectedUnAssignedCoils = val
},
/**
* 已分配钢卷选择事件
* @param {Array} val 选中的钢卷列表
*/
handleSalesSelectionChange(val) {
this.selectedSalesCoils = val
},
/**
* 批量分配钢卷
*/
async handleBatchAssign() {
if (this.selectedUnAssignedCoils.length === 0) {
return this.$message.warning('请选择要分配的钢卷!')
}
try {
await this.$confirm(
`确定要将选中的 ${this.selectedUnAssignedCoils.length} 个钢卷分配给 ${this.currentUser.nickName} 吗?`,
'批量分配',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
const coilIds = this.selectedUnAssignedCoils.map(item => item.id)
for (const coilId of coilIds) {
await updateMaterialCoilSimple({
coilId: coilId,
saleId: this.currentUser.id
})
}
if (res.code === 200) {
this.$message.success('批量分配成功!')
// 刷新列表
this.listUnAssignedCoil()
this.listSalesCoil()
// 清空选中
this.selectedUnAssignedCoils = []
} else {
this.$message.error(res.msg || '批量分配失败')
}
} catch (error) {
if (error !== 'cancel') {
this.$message.error('批量分配操作失败:' + error.message)
}
}
},
/**
* 批量移除钢卷
*/
async handleBatchRemove() {
if (this.selectedSalesCoils.length === 0) {
return this.$message.warning('请选择要移除的钢卷!')
}
try {
await this.$confirm(
`确定要移除选中的 ${this.selectedSalesCoils.length} 个钢卷的分配权限吗?`,
'批量移除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
const coilIds = this.selectedSalesCoils.map(item => item.id)
for (const coilId of coilIds) {
await updateMaterialCoilSimple({
coilId: coilId,
saleId: null
})
}
if (res.code === 200) {
this.$message.success('批量移除成功!')
// 刷新列表
this.listUnAssignedCoil()
this.listSalesCoil()
// 清空选中
this.selectedSalesCoils = []
} else {
this.$message.error(res.msg || '批量移除失败')
}
} catch (error) {
if (error !== 'cancel') {
this.$message.error('批量移除操作失败:' + error.message)
}
}
}
}
}
</script>
<style scoped>
.sales-permission-container {
padding: 10px;
height: 100%;
}
.sales-tree-container {
background: #f5f7fa;
padding: 10px;
border-radius: 4px;
height: calc(100vh - 40px);
overflow: auto;
}
.tree-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 10px;
color: #303133;
}
.coil-table-container {
height: calc(100vh - 40px);
overflow: auto;
}
.table-title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
font-size: 16px;
font-weight: bold;
color: #303133;
}
.batch-operation {
margin: 10px 0;
text-align: right;
}
/* 空提示容器样式 */
.empty-container {
margin-top: 20px;
padding: 40px 0;
background: #fff;
border-radius: 4px;
text-align: center;
}
/* vue2的样式穿透 */
::v-deep .el-form-item {
margin-bottom: 0 !important;
}
::v-deep .el-pagination {
margin-top: 0 !important;
}
</style>

View File

@@ -76,7 +76,7 @@
<el-descriptions title="配卷">
</el-descriptions>
<coil-selector ref="coilSelector" placeholder="请选择钢卷添加至计划" @change="handleCoilChange"
:filters="coilFilters" :coil-column="coilColumn" dialog-width="1200px"></coil-selector>
:filters="coilFilters" :coil-column="coilColumn" dialog-width="1200px" :sales-restricted="true"></coil-selector>
<div v-if="selectedCoilList.length > 0 && currentPlan.planId">
<el-table :data="selectedCoilList" border highlight-current-row style="width: 100%" max-height="400px">
<!-- <el-table-column type="index" width="50" align="center" label="序号" /> -->