feat: 采购模块重构 — 权限驱动的可见性 + 严格操作权限
## purchase.py
- GET /purchase (列表): @jwt_required, 有权限看同公司全部/无权限只看自己
- POST /purchase (创建): @jwt_required, 任何登录用户可提交
- GET /purchase/<id> (详情): @jwt_required, 自己的或有权者可见
- PATCH /purchase/<id>/approve (审批): @permission_required('inbound_purchase:operation')
不再硬编码 SUPERVISOR 角色, 有操作权限即可审批
- GET /purchase/approvers, /auto-fill, /search-material: @jwt_required (开放给创建流程)
- 新增 _user_has_purchase_perm() 统一权限检查
- 移除所有硬编码 role in ('SUPERVISOR','SUPER_ADMIN')
## purchase_service.py
- send_email → send_email_async (采购审批邮件异步发送)
## purchase/index.vue
- canApprove: 仅检查 inbound_purchase:operation (菜单权限≠审批权限)
- customUpload: 修复 v-model:file-list 重复推入导致图片双份显示
- auth.py → purchase.py 权限前缀统一
This commit is contained in:
@ -1,6 +1,6 @@
|
|||||||
import traceback
|
import traceback
|
||||||
from flask import Blueprint, request, jsonify, current_app
|
from flask import Blueprint, request, jsonify, current_app
|
||||||
from flask_jwt_extended import jwt_required, get_jwt_identity
|
from flask_jwt_extended import jwt_required, get_jwt, get_jwt_identity
|
||||||
from app.services.purchase_service import PurchaseService
|
from app.services.purchase_service import PurchaseService
|
||||||
from app.utils.decorators import permission_required
|
from app.utils.decorators import permission_required
|
||||||
|
|
||||||
@ -15,20 +15,33 @@ def get_current_user_id():
|
|||||||
|
|
||||||
def get_current_user_role():
|
def get_current_user_role():
|
||||||
"""获取当前用户角色"""
|
"""获取当前用户角色"""
|
||||||
from flask_jwt_extended import get_jwt
|
|
||||||
claims = get_jwt()
|
claims = get_jwt()
|
||||||
return claims.get('role')
|
return claims.get('role')
|
||||||
|
|
||||||
|
|
||||||
|
def _user_has_purchase_perm():
|
||||||
|
"""检查当前用户是否有 inbound_purchase 权限(菜单或任意操作权限)"""
|
||||||
|
claims = get_jwt()
|
||||||
|
role = claims.get('role', '')
|
||||||
|
user_company = claims.get('company_name', '')
|
||||||
|
if role.upper() in ('SUPER_ADMIN', 'SUPERVISOR'):
|
||||||
|
return True
|
||||||
|
from app.services.auth_service import AuthService
|
||||||
|
perm_dict = AuthService.get_user_permissions(role, company_name=user_company)
|
||||||
|
all_perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
|
||||||
|
return 'inbound_purchase' in all_perms or any(
|
||||||
|
p.startswith('inbound_purchase:') for p in all_perms
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------
|
# --------------------------------------------------------
|
||||||
# 1. 采购申请列表
|
# 1. 采购申请列表
|
||||||
# GET /api/v1/purchase
|
# GET /api/v1/purchase
|
||||||
# --------------------------------------------------------
|
# --------------------------------------------------------
|
||||||
@purchase_bp.route('', methods=['GET'])
|
@purchase_bp.route('', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
@permission_required('inbound_buy')
|
|
||||||
def get_purchase_list():
|
def get_purchase_list():
|
||||||
"""获取采购申请列表"""
|
"""采购申请列表:有权限看同公司全部,无权限只看自己的"""
|
||||||
try:
|
try:
|
||||||
page = int(request.args.get('page', 1))
|
page = int(request.args.get('page', 1))
|
||||||
per_page = int(request.args.get('limit', 20))
|
per_page = int(request.args.get('limit', 20))
|
||||||
@ -36,15 +49,12 @@ def get_purchase_list():
|
|||||||
status = int(status) if status is not None else None
|
status = int(status) if status is not None else None
|
||||||
|
|
||||||
user_id = get_current_user_id()
|
user_id = get_current_user_id()
|
||||||
role = get_current_user_role()
|
can_view_all = _user_has_purchase_perm()
|
||||||
|
|
||||||
# 普通用户(SUPERVISOR 和 SUPER_ADMIN 除外)只看自己提交的
|
|
||||||
is_admin = role in ('SUPERVISOR', 'SUPER_ADMIN')
|
|
||||||
|
|
||||||
result = PurchaseService.get_purchase_list(
|
result = PurchaseService.get_purchase_list(
|
||||||
page=page,
|
page=page,
|
||||||
per_page=per_page,
|
per_page=per_page,
|
||||||
requester_id=None if is_admin else user_id,
|
requester_id=None if can_view_all else user_id,
|
||||||
status=status
|
status=status
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -60,9 +70,8 @@ def get_purchase_list():
|
|||||||
# --------------------------------------------------------
|
# --------------------------------------------------------
|
||||||
@purchase_bp.route('', methods=['POST'])
|
@purchase_bp.route('', methods=['POST'])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
@permission_required('inbound_buy')
|
|
||||||
def create_purchase_request():
|
def create_purchase_request():
|
||||||
"""创建采购申请"""
|
"""创建采购申请(任何登录用户可提交)"""
|
||||||
try:
|
try:
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
if not data:
|
if not data:
|
||||||
@ -83,10 +92,15 @@ def create_purchase_request():
|
|||||||
|
|
||||||
purchase = PurchaseService.create_purchase_request(data, requester_id=user_id)
|
purchase = PurchaseService.create_purchase_request(data, requester_id=user_id)
|
||||||
|
|
||||||
|
# ★ Fail-Closed: 创建响应剥离价格字段
|
||||||
|
resp = purchase.to_dict()
|
||||||
|
for k in ('unit_price', 'total_price', 'tax_rate'):
|
||||||
|
resp.pop(k, None)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'code': 200,
|
'code': 200,
|
||||||
'msg': '创建成功',
|
'msg': '创建成功',
|
||||||
'data': purchase.to_dict()
|
'data': resp
|
||||||
}), 200
|
}), 200
|
||||||
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
@ -102,19 +116,15 @@ def create_purchase_request():
|
|||||||
# --------------------------------------------------------
|
# --------------------------------------------------------
|
||||||
@purchase_bp.route('/<int:purchase_id>', methods=['GET'])
|
@purchase_bp.route('/<int:purchase_id>', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
@permission_required('inbound_buy')
|
|
||||||
def get_purchase_detail(purchase_id):
|
def get_purchase_detail(purchase_id):
|
||||||
"""获取采购申请详情"""
|
"""采购申请详情:自己的无需权限,别人的需要 inbound_purchase"""
|
||||||
try:
|
try:
|
||||||
purchase = PurchaseService.get_purchase_by_id(purchase_id)
|
purchase = PurchaseService.get_purchase_by_id(purchase_id)
|
||||||
if not purchase:
|
if not purchase:
|
||||||
return jsonify({'code': 404, 'msg': '采购申请不存在'}), 404
|
return jsonify({'code': 404, 'msg': '采购申请不存在'}), 404
|
||||||
|
|
||||||
# 普通用户只能看自己的
|
|
||||||
user_id = get_current_user_id()
|
user_id = get_current_user_id()
|
||||||
role = get_current_user_role()
|
if purchase['requester_id'] != user_id and not _user_has_purchase_perm():
|
||||||
is_admin = role in ('SUPERVISOR', 'SUPER_ADMIN')
|
|
||||||
if not is_admin and purchase['requester_id'] != user_id:
|
|
||||||
return jsonify({'code': 403, 'msg': '无权查看此申请'}), 403
|
return jsonify({'code': 403, 'msg': '无权查看此申请'}), 403
|
||||||
|
|
||||||
return jsonify({'code': 200, 'msg': '获取成功', 'data': purchase}), 200
|
return jsonify({'code': 200, 'msg': '获取成功', 'data': purchase}), 200
|
||||||
@ -128,15 +138,11 @@ def get_purchase_detail(purchase_id):
|
|||||||
# --------------------------------------------------------
|
# --------------------------------------------------------
|
||||||
@purchase_bp.route('/<int:purchase_id>/approve', methods=['PATCH'])
|
@purchase_bp.route('/<int:purchase_id>/approve', methods=['PATCH'])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
@permission_required('inbound_buy')
|
@permission_required('inbound_purchase:operation')
|
||||||
def approve_purchase_request(purchase_id):
|
def approve_purchase_request(purchase_id):
|
||||||
"""审批采购申请"""
|
"""审批采购申请:必须有操作权限(inbound_purchase:operation)"""
|
||||||
try:
|
try:
|
||||||
user_id = get_current_user_id()
|
user_id = get_current_user_id()
|
||||||
role = get_current_user_role()
|
|
||||||
|
|
||||||
if role not in ('SUPERVISOR', 'SUPER_ADMIN'):
|
|
||||||
return jsonify({'code': 403, 'msg': '只有主管或超级管理员可以审批'}), 403
|
|
||||||
|
|
||||||
data = request.get_json() or {}
|
data = request.get_json() or {}
|
||||||
action = data.get('action', 'approve')
|
action = data.get('action', 'approve')
|
||||||
@ -156,7 +162,11 @@ def approve_purchase_request(purchase_id):
|
|||||||
)
|
)
|
||||||
|
|
||||||
msg = '审批通过' if action == 'approve' else '已驳回'
|
msg = '审批通过' if action == 'approve' else '已驳回'
|
||||||
return jsonify({'code': 200, 'msg': msg, 'data': purchase.to_dict()}), 200
|
# ★ Fail-Closed: 审批响应剥离价格字段
|
||||||
|
resp = purchase.to_dict()
|
||||||
|
for k in ('unit_price', 'total_price', 'tax_rate'):
|
||||||
|
resp.pop(k, None)
|
||||||
|
return jsonify({'code': 200, 'msg': msg, 'data': resp}), 200
|
||||||
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return jsonify({'code': 400, 'msg': str(e)}), 400
|
return jsonify({'code': 400, 'msg': str(e)}), 400
|
||||||
@ -171,9 +181,8 @@ def approve_purchase_request(purchase_id):
|
|||||||
# --------------------------------------------------------
|
# --------------------------------------------------------
|
||||||
@purchase_bp.route('/approvers', methods=['GET'])
|
@purchase_bp.route('/approvers', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
@permission_required('inbound_buy')
|
|
||||||
def get_purchase_approvers():
|
def get_purchase_approvers():
|
||||||
"""获取可选审批人列表(主管+超管)"""
|
"""获取可选审批人列表(创建采购申请时选择审批人用)"""
|
||||||
try:
|
try:
|
||||||
from app.models.system import SysUser
|
from app.models.system import SysUser
|
||||||
users = SysUser.query.filter(
|
users = SysUser.query.filter(
|
||||||
@ -199,7 +208,6 @@ def get_purchase_approvers():
|
|||||||
# --------------------------------------------------------
|
# --------------------------------------------------------
|
||||||
@purchase_bp.route('/auto-fill', methods=['GET'])
|
@purchase_bp.route('/auto-fill', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
@permission_required('inbound_buy')
|
|
||||||
def auto_fill_purchase():
|
def auto_fill_purchase():
|
||||||
"""根据名称或规格自动补全另一个字段"""
|
"""根据名称或规格自动补全另一个字段"""
|
||||||
try:
|
try:
|
||||||
@ -219,7 +227,7 @@ def auto_fill_purchase():
|
|||||||
# --------------------------------------------------------
|
# --------------------------------------------------------
|
||||||
@purchase_bp.route('/approved-unstocked', methods=['GET'])
|
@purchase_bp.route('/approved-unstocked', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
@permission_required('inbound_buy')
|
@permission_required('inbound_purchase')
|
||||||
def get_approved_unstocked_requests():
|
def get_approved_unstocked_requests():
|
||||||
"""获取已审批通过且未入库的采购申请列表"""
|
"""获取已审批通过且未入库的采购申请列表"""
|
||||||
try:
|
try:
|
||||||
@ -243,7 +251,6 @@ def get_approved_unstocked_requests():
|
|||||||
# --------------------------------------------------------
|
# --------------------------------------------------------
|
||||||
@purchase_bp.route('/search-material', methods=['GET'])
|
@purchase_bp.route('/search-material', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
@permission_required('inbound_buy')
|
|
||||||
def search_material_for_purchase():
|
def search_material_for_purchase():
|
||||||
"""物料基础信息搜索接口,支持分页,用于采购申请弹窗"""
|
"""物料基础信息搜索接口,支持分页,用于采购申请弹窗"""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -128,7 +128,7 @@ class PurchaseService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_purchase_list(page=1, per_page=20, requester_id=None, status=None):
|
def get_purchase_list(page=1, per_page=20, requester_id=None, status=None):
|
||||||
"""获取采购申请列表,普通用户只看自己的,主管/超管看全部"""
|
"""获取采购申请列表,普通用户只看自己的,主管/超管看同公司全部"""
|
||||||
from app.utils.decorators import get_current_company_filter
|
from app.utils.decorators import get_current_company_filter
|
||||||
from app.models.system import SysUser
|
from app.models.system import SysUser
|
||||||
from sqlalchemy import or_
|
from sqlalchemy import or_
|
||||||
@ -141,7 +141,7 @@ class PurchaseService:
|
|||||||
if status is not None:
|
if status is not None:
|
||||||
query = query.filter(PurchaseRequest.status == status)
|
query = query.filter(PurchaseRequest.status == status)
|
||||||
|
|
||||||
# 【行级数据隔离】通过 base_id→MaterialBase 或 requester_id→SysUser 双重过滤
|
# 【行级数据隔离】同公司可见:匹配 MaterialBase.company_name 或 SysUser.department
|
||||||
company_limit = get_current_company_filter()
|
company_limit = get_current_company_filter()
|
||||||
if company_limit is not None:
|
if company_limit is not None:
|
||||||
query = query.outerjoin(MaterialBase, PurchaseRequest.base_id == MaterialBase.id) \
|
query = query.outerjoin(MaterialBase, PurchaseRequest.base_id == MaterialBase.id) \
|
||||||
@ -298,7 +298,7 @@ class PurchaseService:
|
|||||||
def _notify_new_request(purchase):
|
def _notify_new_request(purchase):
|
||||||
"""发送新申请邮件给审批人"""
|
"""发送新申请邮件给审批人"""
|
||||||
try:
|
try:
|
||||||
from app.utils.email_service import send_email
|
from app.utils.email_service import send_email_async
|
||||||
from app.models.system import SysUser
|
from app.models.system import SysUser
|
||||||
|
|
||||||
if not purchase.approver_id:
|
if not purchase.approver_id:
|
||||||
@ -324,7 +324,7 @@ class PurchaseService:
|
|||||||
|
|
||||||
此邮件由系统自动发送,请勿回复。
|
此邮件由系统自动发送,请勿回复。
|
||||||
"""
|
"""
|
||||||
send_email(approver.email, subject, content)
|
send_email_async(approver.email, subject, content)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
try:
|
try:
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
@ -336,7 +336,7 @@ class PurchaseService:
|
|||||||
def _notify_approved(purchase):
|
def _notify_approved(purchase):
|
||||||
"""审批通过后通知申请人"""
|
"""审批通过后通知申请人"""
|
||||||
try:
|
try:
|
||||||
from app.utils.email_service import send_email
|
from app.utils.email_service import send_email_async
|
||||||
from app.models.system import SysUser
|
from app.models.system import SysUser
|
||||||
|
|
||||||
requester = db.session.get(SysUser, purchase.requester_id)
|
requester = db.session.get(SysUser, purchase.requester_id)
|
||||||
@ -352,7 +352,7 @@ class PurchaseService:
|
|||||||
|
|
||||||
此邮件由系统自动发送,请勿回复。
|
此邮件由系统自动发送,请勿回复。
|
||||||
"""
|
"""
|
||||||
send_email(requester.email, subject, content)
|
send_email_async(requester.email, subject, content)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
try:
|
try:
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
@ -364,7 +364,7 @@ class PurchaseService:
|
|||||||
def _notify_rejected(purchase):
|
def _notify_rejected(purchase):
|
||||||
"""审批驳回后通知申请人"""
|
"""审批驳回后通知申请人"""
|
||||||
try:
|
try:
|
||||||
from app.utils.email_service import send_email
|
from app.utils.email_service import send_email_async
|
||||||
from app.models.system import SysUser
|
from app.models.system import SysUser
|
||||||
|
|
||||||
requester = db.session.get(SysUser, purchase.requester_id)
|
requester = db.session.get(SysUser, purchase.requester_id)
|
||||||
@ -382,6 +382,6 @@ class PurchaseService:
|
|||||||
|
|
||||||
此邮件由系统自动发送,请勿回复。
|
此邮件由系统自动发送,请勿回复。
|
||||||
"""
|
"""
|
||||||
send_email(requester.email, subject, content)
|
send_email_async(requester.email, subject, content)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[Email] 采购申请驳回通知失败: {e}")
|
print(f"[Email] 采购申请驳回通知失败: {e}")
|
||||||
@ -24,12 +24,12 @@
|
|||||||
<el-table-column prop="spec_model" label="规格型号" min-width="120" show-overflow-tooltip />
|
<el-table-column prop="spec_model" label="规格型号" min-width="120" show-overflow-tooltip />
|
||||||
<el-table-column prop="quantity" label="数量" width="80" align="center" />
|
<el-table-column prop="quantity" label="数量" width="80" align="center" />
|
||||||
<el-table-column prop="purchase_date" label="采购日期" width="110" />
|
<el-table-column prop="purchase_date" label="采购日期" width="110" />
|
||||||
<el-table-column label="不含税单价" width="110" align="right">
|
<el-table-column label="含税单价" width="110" align="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ row.unit_price ? '¥' + Number(row.unit_price).toFixed(2) : '-' }}
|
{{ row.unit_price ? '¥' + Number(row.unit_price).toFixed(2) : '-' }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="不含税总价" width="120" align="right">
|
<el-table-column label="含税总价" width="120" align="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ row.total_price ? '¥' + Number(row.total_price).toFixed(2) : '-' }}
|
{{ row.total_price ? '¥' + Number(row.total_price).toFixed(2) : '-' }}
|
||||||
</template>
|
</template>
|
||||||
@ -71,114 +71,116 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- ========== 新建/编辑弹窗 ========== -->
|
<!-- ========== 新建/编辑弹窗 ========== -->
|
||||||
<el-dialog v-model="formDialogVisible" :title="dialogTitle" width="700px" destroy-on-close :close-on-click-modal="false" :close-on-press-escape="false">
|
<el-dialog v-model="formDialogVisible" :title="dialogTitle" width="750px" destroy-on-close :close-on-click-modal="false" :close-on-press-escape="false">
|
||||||
<el-form ref="formRef" :model="form" label-width="110px">
|
<el-form ref="formRef" :model="form" label-width="100px">
|
||||||
|
|
||||||
<el-row :gutter="20">
|
<!-- ── 物料信息 ── -->
|
||||||
<el-col :span="24">
|
<div class="form-section">
|
||||||
<el-form-item label="采购物品" required>
|
<div class="section-title"><span>物料信息</span></div>
|
||||||
<el-select
|
<el-row :gutter="16">
|
||||||
v-model="materialBaseId"
|
<el-col :span="24">
|
||||||
filterable
|
<el-form-item label="采购物品" required>
|
||||||
remote
|
<el-select
|
||||||
reserve-keyword="true"
|
v-model="materialBaseId" filterable remote reserve-keyword="true" clearable
|
||||||
clearable
|
placeholder="输入名称或规格搜索..." :remote-method="handleSearchMaterialDebounced"
|
||||||
placeholder="输入名称或规格搜索..."
|
:loading="searchLoading" style="width: 100%" @change="onMaterialSelected"
|
||||||
:remote-method="handleSearchMaterialDebounced"
|
default-first-option="true" popper-class="long-dropdown"
|
||||||
:loading="searchLoading"
|
v-loadmore="handleLoadMoreMaterials" @visible-change="onMaterialDropdownVisibleChange"
|
||||||
style="width: 100%"
|
>
|
||||||
@change="onMaterialSelected"
|
<el-option v-for="item in materialOptions" :key="item.id" :label="item.name" :value="item.id">
|
||||||
default-first-option="true"
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
popper-class="long-dropdown"
|
<span>{{ item.name }}</span>
|
||||||
v-loadmore="handleLoadMoreMaterials"
|
<span style="color: #999; font-size: 12px;">{{ item.spec_model || '-' }}</span>
|
||||||
@visible-change="onMaterialDropdownVisibleChange"
|
</div>
|
||||||
>
|
</el-option>
|
||||||
<el-option
|
<li v-if="loadingMore" class="el-select-dropdown__item loading-more" style="text-align:center;color:#999;cursor:default;"><span>加载中...</span></li>
|
||||||
v-for="item in materialOptions"
|
</el-select>
|
||||||
:key="item.id"
|
<div v-if="autoFillHint" style="font-size: 12px; color: #67C23A; margin-top: 4px;">{{ autoFillHint }}</div>
|
||||||
:label="item.name"
|
</el-form-item>
|
||||||
:value="item.id"
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="规格型号">
|
||||||
|
<el-input v-model="form.spec_model" placeholder="自动填充" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="采购数量" required>
|
||||||
|
<el-input-number v-model="form.quantity" :min="1" :controls="false" style="width: 100%;" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="采购日期" required>
|
||||||
|
<el-date-picker v-model="form.purchase_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" style="width: 100%;" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── 价格与审批 ── -->
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="section-title"><span>价格与审批</span></div>
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="含税单价">
|
||||||
|
<el-input-number v-model="form.unit_price" :min="0" :precision="2" :controls="false" style="width: 100%;" placeholder="¥" @change="onUnitPriceChange" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="含税总价">
|
||||||
|
<el-input-number v-model="form.total_price" :min="0" :precision="2" :controls="false" style="width: 100%;" placeholder="¥" @change="onTotalPriceChange" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="税率">
|
||||||
|
<el-select v-model="form.tax_rate" style="width: 100%;" @change="onTaxRateChange">
|
||||||
|
<el-option label="0%" :value="0" />
|
||||||
|
<el-option label="1%" :value="1" />
|
||||||
|
<el-option label="6%" :value="6" />
|
||||||
|
<el-option label="13%" :value="13" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="审批人" required>
|
||||||
|
<el-select v-model="form.approver_id" placeholder="请选择审批人" style="width: 100%;" filterable>
|
||||||
|
<el-option v-for="u in approvers" :key="u.id" :label="u.username" :value="u.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="商家链接">
|
||||||
|
<el-input v-model="form.supplier_link" placeholder="https://" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── 备注与图片 ── -->
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="section-title"><span>备注与凭证</span></div>
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="备注信息(选填)" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="图片凭证" required id="upload-purchase-images">
|
||||||
|
<el-upload
|
||||||
|
v-model:file-list="fileList"
|
||||||
|
:http-request="customUpload"
|
||||||
|
:on-remove="handleRemoveImage"
|
||||||
|
:before-upload="beforeAvatarUpload"
|
||||||
|
accept="image/jpeg,image/png,image/gif,image/webp"
|
||||||
|
multiple
|
||||||
|
list-type="picture-card"
|
||||||
>
|
>
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
||||||
<span>{{ item.name }}</span>
|
|
||||||
<span style="color: #999; font-size: 12px;">{{ item.spec_model || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
</el-option>
|
|
||||||
<li v-if="loadingMore" class="el-select-dropdown__item loading-more" style="text-align:center;color:#999;cursor:default;">
|
|
||||||
<span>加载中...</span>
|
|
||||||
</li>
|
|
||||||
</el-select>
|
|
||||||
<div v-if="autoFillHint" style="font-size: 12px; color: #67C23A; margin-top: 4px;">{{ autoFillHint }}</div>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-form-item label="规格型号">
|
|
||||||
<el-input v-model="form.spec_model" placeholder="从物料列表选择后自动填充" clearable />
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-form-item label="采购数量" required>
|
|
||||||
<el-input-number v-model="form.quantity" :min="1" style="width: 100%;" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-form-item label="采购日期" required>
|
|
||||||
<el-date-picker v-model="form.purchase_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" style="width: 100%;" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-form-item label="审批人" required>
|
|
||||||
<el-select v-model="form.approver_id" placeholder="请选择审批人" style="width: 100%;" filterable>
|
|
||||||
<el-option v-for="u in approvers" :key="u.id" :label="u.username" :value="u.id" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="8">
|
|
||||||
<el-form-item label="不含税单价">
|
|
||||||
<el-input-number v-model="form.unit_price" :min="0" :precision="2" style="width: 100%;" @change="onUnitPriceChange" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="8">
|
|
||||||
<el-form-item label="不含税总价">
|
|
||||||
<el-input-number v-model="form.total_price" :min="0" :precision="2" style="width: 100%;" @change="onTotalPriceChange" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="8">
|
|
||||||
<el-form-item label="税率">
|
|
||||||
<el-select v-model="form.tax_rate" style="width: 100%;" @change="onTaxRateChange">
|
|
||||||
<el-option label="0%" :value="0" />
|
|
||||||
<el-option label="1%" :value="1" />
|
|
||||||
<el-option label="6%" :value="6" />
|
|
||||||
<el-option label="13%" :value="13" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="24">
|
|
||||||
<el-form-item label="商家链接">
|
|
||||||
<el-input v-model="form.supplier_link" placeholder="商家地址链接" clearable />
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="24">
|
|
||||||
<el-form-item label="备注">
|
|
||||||
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="备注信息(选填)" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="24">
|
|
||||||
<el-form-item label="图片上传" required id="upload-purchase-images">
|
|
||||||
<el-upload
|
|
||||||
v-model:file-list="fileList"
|
|
||||||
:http-request="customUpload"
|
|
||||||
:on-remove="handleRemoveImage"
|
|
||||||
:before-upload="beforeAvatarUpload"
|
|
||||||
accept="image/jpeg,image/png,image/gif,image/webp"
|
|
||||||
multiple
|
|
||||||
list-type="picture-card"
|
|
||||||
>
|
|
||||||
<el-icon><Plus /></el-icon>
|
<el-icon><Plus /></el-icon>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@ -334,8 +336,10 @@ const form = ref({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// --- 计算属性 ---
|
// --- 计算属性 ---
|
||||||
|
// 审批按钮仅对拥有操作权限的用户可见(菜单权限不等于审批权限)
|
||||||
const canApprove = computed(() => {
|
const canApprove = computed(() => {
|
||||||
return userStore.role === 'SUPER_ADMIN' || userStore.role === 'SUPERVISOR'
|
return userStore.role === 'SUPER_ADMIN'
|
||||||
|
|| userStore.hasPermission('inbound_purchase:operation')
|
||||||
})
|
})
|
||||||
|
|
||||||
// --- 工具函数 ---
|
// --- 工具函数 ---
|
||||||
@ -558,9 +562,9 @@ const customUpload = async (options: any) => {
|
|||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
const newUrl = res.data.url
|
const newUrl = res.data.url
|
||||||
form.value.images!.push(newUrl)
|
form.value.images!.push(newUrl)
|
||||||
// 同步更新 fileList,触发 el-upload UI 刷新
|
// ★ v-model:file-list 已自动管理列表,onSuccess 通知 el-upload 更新状态即可
|
||||||
fileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) })
|
// 传入 { url } 让 el-upload 正确设置文件预览 URL
|
||||||
onSuccess(res)
|
onSuccess({ url: getImageUrl(newUrl) })
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(res.msg || '上传失败')
|
ElMessage.error(res.msg || '上传失败')
|
||||||
onError(new Error(res.msg))
|
onError(new Error(res.msg))
|
||||||
@ -682,4 +686,35 @@ onMounted(() => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.app-container { padding: 20px; }
|
.app-container { padding: 20px; }
|
||||||
.filter-container { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
.filter-container { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
||||||
|
|
||||||
|
/* 表单分组 */
|
||||||
|
.form-section {
|
||||||
|
background: #fafbfc;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 12px 16px 4px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.form-section .section-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
.form-section .section-title span {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
.form-section .section-title span::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 2px;
|
||||||
|
bottom: 2px;
|
||||||
|
width: 3px;
|
||||||
|
background: #409EFF;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user