更新
This commit is contained in:
1 parent
4ab63e77ed
commit
eb9b7beff5
16 files changed
+1010
-70
No files matched your search
@@ -123,6 +123,89 @@ function mergeFixedCmsMenu(menus) {
|
|||||||
: [fixedCmsMenu, ...menus];
|
: [fixedCmsMenu, ...menus];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ERP_ROOT = "/apps/erp";
|
||||||
|
const ERP_WORKBENCH_DIR = "/apps/erp/workbench";
|
||||||
|
const ERP_DASHBOARD_PAGE = "/apps/erp/dashboard";
|
||||||
|
const ERP_APPROVAL_PAGE = "/apps/erp/workbench/approval";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进销存「工作台」统一为目录,目录内固定挂「工作台 / 审批列表」两个页面。
|
||||||
|
*
|
||||||
|
* 菜单由数据库下发,erp_menus.sql 未重跑时工作台仍是模块根下的一个叶子页面,
|
||||||
|
* 这里在导航模型里就地补齐目录结构,保证前端展示与 SQL 的目标结构一致;
|
||||||
|
* 数据库已是正确的目录结构时只做幂等补充,不会重复插入。
|
||||||
|
*/
|
||||||
|
function normalizeErpWorkbench(menus) {
|
||||||
|
return (menus || []).map((menu) => {
|
||||||
|
if (menu.path !== ERP_ROOT) {
|
||||||
|
return menu.children?.length
|
||||||
|
? { ...menu, children: normalizeErpWorkbench(menu.children) }
|
||||||
|
: menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = [...(menu.children || [])];
|
||||||
|
const dir = children.find((m) => m.path === ERP_WORKBENCH_DIR && m.children?.length);
|
||||||
|
const dashboard = children.find((m) => m.path === ERP_DASHBOARD_PAGE);
|
||||||
|
const pages = dir ? [...(dir.children || [])] : [];
|
||||||
|
|
||||||
|
// 不能用 isWorkbenchMenu 判断:审批列表路径里也带 workbench,会被误判成工作台页
|
||||||
|
const hasDashboard = pages.some(
|
||||||
|
(m) => m.path === ERP_DASHBOARD_PAGE || String(m.title || "") === "工作台"
|
||||||
|
);
|
||||||
|
if (!hasDashboard) {
|
||||||
|
pages.push(
|
||||||
|
dashboard
|
||||||
|
? { ...dashboard, title: "工作台", sort: 1, order: 1 }
|
||||||
|
: {
|
||||||
|
id: -3001,
|
||||||
|
path: ERP_DASHBOARD_PAGE,
|
||||||
|
title: "工作台",
|
||||||
|
icon: "DataBoard",
|
||||||
|
component_path: "/apps/erp/dashboard/index.vue",
|
||||||
|
type: 2,
|
||||||
|
sort: 1,
|
||||||
|
order: 1,
|
||||||
|
children: [],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!pages.some((m) => m.path === ERP_APPROVAL_PAGE)) {
|
||||||
|
pages.push({
|
||||||
|
id: -3002,
|
||||||
|
path: ERP_APPROVAL_PAGE,
|
||||||
|
title: "审批列表",
|
||||||
|
icon: "Tickets",
|
||||||
|
component_path: "/apps/erp/workbench/approvalList.vue",
|
||||||
|
type: 2,
|
||||||
|
sort: 2,
|
||||||
|
order: 2,
|
||||||
|
children: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const workbenchDir = dir
|
||||||
|
? { ...dir, type: 1, component_path: "", children: pages }
|
||||||
|
: {
|
||||||
|
id: -300,
|
||||||
|
pid: menu.id,
|
||||||
|
path: ERP_WORKBENCH_DIR,
|
||||||
|
title: "工作台",
|
||||||
|
icon: "DataBoard",
|
||||||
|
component_path: "",
|
||||||
|
type: 1,
|
||||||
|
sort: 1,
|
||||||
|
order: 1,
|
||||||
|
children: pages,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 原工作台叶子(或空的工作台目录)已被收进新目录,避免同一功能出现两份
|
||||||
|
const rest = children.filter(
|
||||||
|
(m) => m.path !== ERP_WORKBENCH_DIR && m.path !== ERP_DASHBOARD_PAGE
|
||||||
|
);
|
||||||
|
return { ...menu, children: [workbenchDir, ...rest] };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function sortMenusRecursively(menus) {
|
function sortMenusRecursively(menus) {
|
||||||
(menus || []).forEach((menu) => {
|
(menus || []).forEach((menu) => {
|
||||||
if (menu.children && menu.children.length > 0) {
|
if (menu.children && menu.children.length > 0) {
|
||||||
@@ -260,7 +343,9 @@ export function useAdminNavigation() {
|
|||||||
const menuStore = useMenuStore();
|
const menuStore = useMenuStore();
|
||||||
|
|
||||||
const list = computed(() =>
|
const list = computed(() =>
|
||||||
sortMenusRecursively(processMenus(mergeFixedCmsMenu(menuStore.menus || []))),
|
sortMenusRecursively(
|
||||||
|
processMenus(normalizeErpWorkbench(mergeFixedCmsMenu(menuStore.menus || [])))
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const currentModule = computed(() => {
|
const currentModule = computed(() => {
|
||||||
|
|||||||
@@ -22,6 +22,19 @@ const staticMainChildren = [
|
|||||||
component: () => import("@/views/system/approval/index.vue"),
|
component: () => import("@/views/system/approval/index.vue"),
|
||||||
meta: { requiresAuth: true, title: "审批流", modulePath: "/systemManager" }
|
meta: { requiresAuth: true, title: "审批流", modulePath: "/systemManager" }
|
||||||
},
|
},
|
||||||
|
// ERP 工作台目录内的两个页面:静态注册保证 URL 直达,菜单入口由数据库驱动
|
||||||
|
{
|
||||||
|
path: "/apps/erp/dashboard",
|
||||||
|
name: "ErpWorkbenchDashboard",
|
||||||
|
component: () => import("@/views/apps/erp/dashboard/index.vue"),
|
||||||
|
meta: { requiresAuth: true, title: "工作台", modulePath: "/apps/erp" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/apps/erp/workbench/approval",
|
||||||
|
name: "ErpWorkbenchApprovalList",
|
||||||
|
component: () => import("@/views/apps/erp/workbench/approvalList.vue"),
|
||||||
|
meta: { requiresAuth: true, title: "审批列表", modulePath: "/apps/erp" }
|
||||||
|
},
|
||||||
// OA 审批中心:待我审批 / 审批列表 / 审批设置(静态注册保证 URL 直达)
|
// OA 审批中心:待我审批 / 审批列表 / 审批设置(静态注册保证 URL 直达)
|
||||||
{
|
{
|
||||||
path: "/apps/oa/approvalcenter/mine",
|
path: "/apps/oa/approvalcenter/mine",
|
||||||
@@ -263,10 +276,14 @@ export async function loadAndAddDynamicRoutes() {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载动态路由失败:', error);
|
// 菜单接口异常时不要把错误抛给导航守卫,否则应用会卡在白屏;
|
||||||
|
// 保留内置静态路由(工作台、审批列表、审批流配置等)继续可用。
|
||||||
|
// 不重置 dynamicRoutesAdded,避免每次路由切换都重试失败的接口,
|
||||||
|
// 刷新页面或手动刷新菜单即可重新拉取。
|
||||||
|
console.error('加载动态路由失败,已保留内置路由:', error);
|
||||||
dynamicRoutesAdded = true;
|
dynamicRoutesAdded = true;
|
||||||
routesLoadingPromise = null;
|
routesLoadingPromise = null;
|
||||||
throw error;
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
@@ -132,9 +132,11 @@ export const useMenuStore = defineStore('menu', () => {
|
|||||||
// 用户登录,使用 getMenus 接口
|
// 用户登录,使用 getMenus 接口
|
||||||
res = await getMenus(userInfo.id);
|
res = await getMenus(userInfo.id);
|
||||||
|
|
||||||
// 检查响应格式
|
// 检查响应格式:HTTP 200 但 body 为空(后端/网关异常)会走到这里,
|
||||||
|
// 打印原始值便于区分「服务挂了」和「返回了空内容」
|
||||||
if (!res) {
|
if (!res) {
|
||||||
throw new Error('获取菜单失败:服务器无响应');
|
console.error('菜单接口返回空响应:', { res, url: `/backend/menu/${userInfo.id}` });
|
||||||
|
throw new Error('获取菜单失败:接口返回空响应(后端服务或网关异常)');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查后端返回的 code 字段
|
// 检查后端返回的 code 字段
|
||||||
@@ -215,8 +217,9 @@ export const useMenuStore = defineStore('menu', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 刷新菜单(强制从 API 获取)
|
// 刷新菜单(强制从 API 获取)
|
||||||
|
// 不预先清缓存:接口异常时 fetchMenus 还能回退到上一份菜单,避免整个应用没菜单可用;
|
||||||
|
// 请求成功后 saveToCache 会直接覆盖旧缓存。
|
||||||
const refreshMenus = async () => {
|
const refreshMenus = async () => {
|
||||||
clearCache();
|
|
||||||
return await fetchMenus(true);
|
return await fetchMenus(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-loading="loading" class="erp-dashboard">
|
<div v-loading="loading" class="erp-dashboard">
|
||||||
|
<!-- 工作台直达审批列表:菜单尚未刷新时也能进入待办审批 -->
|
||||||
|
<div class="approval-entry">
|
||||||
|
<span>单据审批待办</span>
|
||||||
|
<el-button type="primary" size="small" @click="goApproval">进入审批列表</el-button>
|
||||||
|
</div>
|
||||||
<!-- 第一行统计卡片 -->
|
<!-- 第一行统计卡片 -->
|
||||||
<el-row :gutter="16" class="stat-row">
|
<el-row :gutter="16" class="stat-row">
|
||||||
<el-col :span="4" :xs="12" v-for="(item, index) in firstRowStats" :key="'first-' + index">
|
<el-col :span="4" :xs="12" v-for="(item, index) in firstRowStats" :key="'first-' + index">
|
||||||
@@ -58,6 +63,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, shallowRef, nextTick } from 'vue';
|
import { ref, onMounted, onUnmounted, shallowRef, nextTick } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
import * as echarts from 'echarts';
|
import * as echarts from 'echarts';
|
||||||
import { InfoFilled } from '@element-plus/icons-vue';
|
import { InfoFilled } from '@element-plus/icons-vue';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
@@ -66,6 +72,8 @@ import { getErpDashboard } from '@/api/erpReport';
|
|||||||
type Stat = { label: string; text: string; prefix?: string };
|
type Stat = { label: string; text: string; prefix?: string };
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
const router = useRouter();
|
||||||
|
const goApproval = () => router.push('/apps/erp/workbench/approval');
|
||||||
|
|
||||||
// 统计卡片数据 - 第一行(金额)
|
// 统计卡片数据 - 第一行(金额)
|
||||||
const firstRowStats = ref<Stat[]>([]);
|
const firstRowStats = ref<Stat[]>([]);
|
||||||
@@ -255,6 +263,16 @@ onUnmounted(() => {
|
|||||||
// min-height: calc(100vh - 84px);
|
// min-height: calc(100vh - 84px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.approval-entry {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
.stat-row {
|
.stat-row {
|
||||||
.el-col {
|
.el-col {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|||||||
@@ -29,9 +29,14 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
<el-form-item :label="isTransfer ? '调出仓库' : '仓库'" required>
|
<el-form-item :label="isTransfer ? '调出仓库' : '仓库'" required>
|
||||||
<el-select v-model="form.warehouse_id" placeholder="请选择仓库" style="width: 100%" @change="onWarehouseChange">
|
<div class="field-with-action">
|
||||||
|
<el-select v-model="form.warehouse_id" placeholder="请选择仓库" class="field-select" @change="onWarehouseChange">
|
||||||
<el-option v-for="w in warehouses" :key="w.id" :label="w.warehouse_name" :value="w.id" />
|
<el-option v-for="w in warehouses" :key="w.id" :label="w.warehouse_name" :value="w.id" />
|
||||||
</el-select>
|
</el-select>
|
||||||
|
<el-tooltip content="快捷新增仓库" placement="top">
|
||||||
|
<el-button :icon="Plus" aria-label="快捷新增仓库" @click="openQuickWarehouse" />
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
|
||||||
@@ -50,16 +55,21 @@
|
|||||||
|
|
||||||
<el-col v-if="partnerType" :span="8">
|
<el-col v-if="partnerType" :span="8">
|
||||||
<el-form-item :label="partnerType === 'supplier' ? '供应商' : '客户'" required>
|
<el-form-item :label="partnerType === 'supplier' ? '供应商' : '客户'" required>
|
||||||
|
<div class="field-with-action">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="form.partner_id"
|
v-model="form.partner_id"
|
||||||
filterable
|
filterable
|
||||||
clearable
|
clearable
|
||||||
placeholder="搜索名称选择"
|
placeholder="搜索名称选择"
|
||||||
style="width: 100%"
|
class="field-select"
|
||||||
@change="onPartnerChange"
|
@change="onPartnerChange"
|
||||||
>
|
>
|
||||||
<el-option v-for="p in partners" :key="p.id" :label="partnerLabel(p)" :value="p.id" />
|
<el-option v-for="p in partners" :key="p.id" :label="partnerLabel(p)" :value="p.id" />
|
||||||
</el-select>
|
</el-select>
|
||||||
|
<el-tooltip v-if="partnerType === 'customer'" content="快捷新增客户" placement="top">
|
||||||
|
<el-button :icon="Plus" aria-label="快捷新增客户" @click="openQuickCustomer" />
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
|
||||||
@@ -199,12 +209,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<QuickCreateWarehouse v-model:visible="quickWarehouseVisible" @created="onWarehouseCreated" />
|
||||||
|
<QuickCreateCustomer v-model:visible="quickCustomerVisible" @created="onCustomerCreated" />
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="drawer-footer">
|
<div class="drawer-footer">
|
||||||
<el-button @click="close">取消</el-button>
|
<el-button @click="close">取消</el-button>
|
||||||
<el-button :loading="saving" @click="save('draft')">保存草稿</el-button>
|
<el-button :loading="saving" @click="save('draft')">保存草稿</el-button>
|
||||||
<el-button v-if="flows.length" type="warning" :loading="saving" @click="save('submit')">保存并提交审批</el-button>
|
<el-button v-if="flows.length" type="warning" :loading="saving" @click="save('submit')">保存并提交审批</el-button>
|
||||||
<el-button type="primary" :loading="saving" @click="save('approve')">保存并审核生效</el-button>
|
<!-- 配了审批流的单据只能提交审批,由工作台审批列表处理;未配流程时保留直接生效兜底 -->
|
||||||
|
<el-button v-if="!flows.length" type="primary" :loading="saving" @click="save('approve')">保存并生效</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
@@ -215,7 +229,10 @@ import { computed, reactive, ref } from 'vue'
|
|||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { Plus, Download, DocumentCopy } from '@element-plus/icons-vue'
|
import { Plus, Download, DocumentCopy } from '@element-plus/icons-vue'
|
||||||
import ProductSelect from './ProductSelect.vue'
|
import ProductSelect from './ProductSelect.vue'
|
||||||
|
import QuickCreateWarehouse from './QuickCreateWarehouse.vue'
|
||||||
|
import QuickCreateCustomer from './QuickCreateCustomer.vue'
|
||||||
import {
|
import {
|
||||||
|
DOC_STATUS_VALUE,
|
||||||
dateOnly,
|
dateOnly,
|
||||||
docTypeLabel,
|
docTypeLabel,
|
||||||
docTypePartner,
|
docTypePartner,
|
||||||
@@ -241,10 +258,12 @@ const props = defineProps({
|
|||||||
/** 预填明细(补货建议转采购单用):[{ product_id, product_name, spec, unit, quantity, unit_price }] */
|
/** 预填明细(补货建议转采购单用):[{ product_id, product_name, spec, unit, quantity, unit_price }] */
|
||||||
presetItems: { type: Array, default: null }
|
presetItems: { type: Array, default: null }
|
||||||
})
|
})
|
||||||
const emit = defineEmits(['update:visible', 'success'])
|
const emit = defineEmits(['update:visible', 'success', 'warehouse-created'])
|
||||||
|
|
||||||
let rowSeq = 0
|
let rowSeq = 0
|
||||||
const warehouses = ref([])
|
const warehouses = ref([])
|
||||||
|
const quickWarehouseVisible = ref(false)
|
||||||
|
const quickCustomerVisible = ref(false)
|
||||||
const partners = ref([])
|
const partners = ref([])
|
||||||
const sourceDocs = ref([])
|
const sourceDocs = ref([])
|
||||||
const flows = ref([])
|
const flows = ref([])
|
||||||
@@ -261,7 +280,7 @@ const form = reactive({
|
|||||||
warehouse_id: 0,
|
warehouse_id: 0,
|
||||||
to_warehouse_id: 0,
|
to_warehouse_id: 0,
|
||||||
partner_type: '',
|
partner_type: '',
|
||||||
partner_id: 0,
|
partner_id: '',
|
||||||
partner_name: '',
|
partner_name: '',
|
||||||
source_doc_id: 0,
|
source_doc_id: 0,
|
||||||
discount_amount: 0,
|
discount_amount: 0,
|
||||||
@@ -332,7 +351,8 @@ async function loadPartners() {
|
|||||||
|
|
||||||
async function loadSourceDocs() {
|
async function loadSourceDocs() {
|
||||||
try {
|
try {
|
||||||
const res = await getDocumentList({ doc_types: sourceTypes.value.join(','), status: 2, pageSize: 50 })
|
// 来源单据只列已生效的,与下推入口的状态限制保持一致
|
||||||
|
const res = await getDocumentList({ doc_types: sourceTypes.value.join(','), status: DOC_STATUS_VALUE.APPROVED, pageSize: 50 })
|
||||||
if (res.code === 200) sourceDocs.value = res.data.list || []
|
if (res.code === 200) sourceDocs.value = res.data.list || []
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
sourceDocs.value = []
|
sourceDocs.value = []
|
||||||
@@ -357,7 +377,7 @@ function resetForm() {
|
|||||||
form.warehouse_id = src ? Number(src.warehouse_id) || 0 : form.warehouse_id || 0
|
form.warehouse_id = src ? Number(src.warehouse_id) || 0 : form.warehouse_id || 0
|
||||||
form.to_warehouse_id = src ? Number(src.to_warehouse_id) || 0 : 0
|
form.to_warehouse_id = src ? Number(src.to_warehouse_id) || 0 : 0
|
||||||
form.partner_type = partnerType.value
|
form.partner_type = partnerType.value
|
||||||
form.partner_id = src ? Number(src.partner_id) || 0 : 0
|
form.partner_id = src ? Number(src.partner_id) || '' : ''
|
||||||
form.partner_name = src ? src.partner_name || '' : ''
|
form.partner_name = src ? src.partner_name || '' : ''
|
||||||
form.source_doc_id = src ? Number(src.source_doc_id) || 0 : 0
|
form.source_doc_id = src ? Number(src.source_doc_id) || 0 : 0
|
||||||
form.discount_amount = src ? Number(src.discount_amount) || 0 : 0
|
form.discount_amount = src ? Number(src.discount_amount) || 0 : 0
|
||||||
@@ -433,7 +453,7 @@ async function syncBatchFlags(rows) {
|
|||||||
function applyPushFrom(pf) {
|
function applyPushFrom(pf) {
|
||||||
const src = pf.doc || {}
|
const src = pf.doc || {}
|
||||||
form.partner_type = src.partner_type || partnerType.value
|
form.partner_type = src.partner_type || partnerType.value
|
||||||
form.partner_id = Number(src.partner_id) || 0
|
form.partner_id = Number(src.partner_id) || ''
|
||||||
form.partner_name = src.partner_name || ''
|
form.partner_name = src.partner_name || ''
|
||||||
form.warehouse_id = Number(src.warehouse_id) || form.warehouse_id
|
form.warehouse_id = Number(src.warehouse_id) || form.warehouse_id
|
||||||
form.source_doc_id = Number(src.id) || 0
|
form.source_doc_id = Number(src.id) || 0
|
||||||
@@ -477,6 +497,39 @@ function onWarehouseChange() {
|
|||||||
if (isTransfer.value && form.to_warehouse_id === form.warehouse_id) form.to_warehouse_id = 0
|
if (isTransfer.value && form.to_warehouse_id === form.warehouse_id) form.to_warehouse_id = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openQuickWarehouse() {
|
||||||
|
quickWarehouseVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 快捷新增后刷新仓库下拉并自动选中,避免用户还要手动去仓库档案页建仓 */
|
||||||
|
async function onWarehouseCreated(row) {
|
||||||
|
emit('warehouse-created', row)
|
||||||
|
try {
|
||||||
|
const res = await getWarehouseList({ pageSize: 100 })
|
||||||
|
if (res.code === 200) warehouses.value = res.data.list || []
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e.message || '仓库列表刷新失败')
|
||||||
|
}
|
||||||
|
if (!warehouses.value.some((w) => String(w.id) === String(row.id))) warehouses.value.push(row)
|
||||||
|
form.warehouse_id = Number(row.id) || 0
|
||||||
|
onWarehouseChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
function openQuickCustomer() {
|
||||||
|
quickCustomerVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 快捷新增客户后刷新往来单位下拉并自动选中,保持开单不中断 */
|
||||||
|
async function onCustomerCreated(row) {
|
||||||
|
await loadPartners()
|
||||||
|
if (!partners.value.some((p) => String(p.id) === String(row.id))) partners.value.push(row)
|
||||||
|
form.partner_id = Number(row.id) || ''
|
||||||
|
form.partner_name = row.customer_name || ''
|
||||||
|
// 与手动选择客户保持一致:刷新来源单据,并清掉上一家客户残留的信用提示
|
||||||
|
loadSourceDocs()
|
||||||
|
loadCredit()
|
||||||
|
}
|
||||||
|
|
||||||
function onPartnerChange(id) {
|
function onPartnerChange(id) {
|
||||||
const p = partners.value.find((x) => String(x.id) === String(id))
|
const p = partners.value.find((x) => String(x.id) === String(id))
|
||||||
form.partner_name = p ? partnerLabel(p) : ''
|
form.partner_name = p ? partnerLabel(p) : ''
|
||||||
@@ -608,7 +661,7 @@ function buildPayload() {
|
|||||||
warehouse_id: form.warehouse_id,
|
warehouse_id: form.warehouse_id,
|
||||||
to_warehouse_id: form.to_warehouse_id,
|
to_warehouse_id: form.to_warehouse_id,
|
||||||
partner_type: partnerType.value,
|
partner_type: partnerType.value,
|
||||||
partner_id: form.partner_id,
|
partner_id: Number(form.partner_id) || 0,
|
||||||
source_doc_id: form.source_doc_id,
|
source_doc_id: form.source_doc_id,
|
||||||
// 调拨/盘点等不涉及成交价的单据不允许有折扣,否则会被后端计入金额
|
// 调拨/盘点等不涉及成交价的单据不允许有折扣,否则会被后端计入金额
|
||||||
discount_amount: showPrice.value ? form.discount_amount : 0,
|
discount_amount: showPrice.value ? form.discount_amount : 0,
|
||||||
@@ -663,6 +716,8 @@ async function save(mode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
|
quickWarehouseVisible.value = false
|
||||||
|
quickCustomerVisible.value = false
|
||||||
emit('update:visible', false)
|
emit('update:visible', false)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -676,6 +731,19 @@ function close() {
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 仓库、客户等下拉右侧挂「+」快捷新增按钮时共用
|
||||||
|
.field-with-action {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.field-select {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.credit-tip {
|
.credit-tip {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--el-text-color-secondary);
|
color: var(--el-text-color-secondary);
|
||||||
|
|||||||
@@ -20,6 +20,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<el-tabs v-model="activeStatus" class="status-tabs">
|
||||||
|
<el-tab-pane v-for="t in statusTabs" :key="t.value" :name="t.value" :label="t.label" />
|
||||||
|
</el-tabs>
|
||||||
|
|
||||||
<div class="filter-bar">
|
<div class="filter-bar">
|
||||||
<el-form :inline="true" @submit.prevent>
|
<el-form :inline="true" @submit.prevent>
|
||||||
<el-form-item v-if="typeOptions.length > 1" label="单据类型">
|
<el-form-item v-if="typeOptions.length > 1" label="单据类型">
|
||||||
@@ -37,11 +41,6 @@
|
|||||||
@keyup.enter="handleSearch"
|
@keyup.enter="handleSearch"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="状态">
|
|
||||||
<el-select v-model="filters.status" clearable placeholder="全部" style="width: 110px" @change="handleSearch">
|
|
||||||
<el-option v-for="s in statusOptions" :key="s.value" :label="s.label" :value="s.value" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="仓库">
|
<el-form-item label="仓库">
|
||||||
<el-select v-model="filters.warehouse_id" clearable placeholder="全部" style="width: 140px" @change="handleSearch">
|
<el-select v-model="filters.warehouse_id" clearable placeholder="全部" style="width: 140px" @change="handleSearch">
|
||||||
<el-option v-for="w in warehouses" :key="w.id" :label="w.warehouse_name" :value="w.id" />
|
<el-option v-for="w in warehouses" :key="w.id" :label="w.warehouse_name" :value="w.id" />
|
||||||
@@ -102,22 +101,27 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="create_user_name" label="制单" width="100" show-overflow-tooltip />
|
<el-table-column prop="create_user_name" label="制单" width="100" show-overflow-tooltip />
|
||||||
<el-table-column label="操作" width="230" align="center" fixed="right">
|
<el-table-column label="操作" width="320" align="center" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button link type="primary" size="small" @click="openDetail(row)">详情</el-button>
|
<div class="row-actions">
|
||||||
<el-button v-if="canEdit(row)" link type="primary" size="small" @click="openEdit(row)">编辑</el-button>
|
<el-button v-if="canEdit(row)" link type="primary" size="small" @click="openEdit(row)">编辑</el-button>
|
||||||
<el-button v-if="canEdit(row)" link type="warning" size="small" @click="handleSubmit(row)">提交审批</el-button>
|
<el-button link type="primary" size="small" @click="openDetail(row)">详情</el-button>
|
||||||
<el-button v-if="canApprove(row)" link type="success" size="small" @click="handleApprove(row)">审核</el-button>
|
<el-dropdown v-if="hasMoreActions(row)" trigger="click" @command="(cmd) => handleMoreCommand(cmd, row)">
|
||||||
<el-dropdown v-if="nextTypes(row).length" trigger="click" @command="(t) => openPush(row, t)">
|
<el-button link type="primary" size="small">更多<el-icon class="el-icon--right"><ArrowDown /></el-icon></el-button>
|
||||||
<el-button link type="primary" size="small">下推<el-icon class="el-icon--right"><ArrowDown /></el-icon></el-button>
|
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<el-dropdown-menu>
|
<el-dropdown-menu>
|
||||||
<el-dropdown-item v-for="n in nextTypes(row)" :key="n.type" :command="n.type">{{ n.label }}</el-dropdown-item>
|
<el-dropdown-item
|
||||||
|
v-for="n in nextTypes(row)"
|
||||||
|
:key="n.type"
|
||||||
|
:command="'push:' + n.type"
|
||||||
|
>下推:{{ n.label }}</el-dropdown-item>
|
||||||
|
<el-dropdown-item v-if="canSubmit(row)" command="submit" :divided="needExtraDivider(row, 'submit')">提交审批</el-dropdown-item>
|
||||||
|
<el-dropdown-item v-if="canCancel(row)" command="cancel" :divided="needExtraDivider(row, 'cancel')">作废</el-dropdown-item>
|
||||||
|
<el-dropdown-item v-if="canDelete(row)" command="delete" divided>删除</el-dropdown-item>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
<el-button v-if="row.status !== 2 && row.status !== 4" link type="danger" size="small" @click="handleCancel(row)">作废</el-button>
|
</div>
|
||||||
<el-button v-if="canEdit(row)" link type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -141,13 +145,14 @@
|
|||||||
:edit-data="editData"
|
:edit-data="editData"
|
||||||
:push-from="pushFrom"
|
:push-from="pushFrom"
|
||||||
@success="fetchList"
|
@success="fetchList"
|
||||||
|
@warehouse-created="loadWarehouses"
|
||||||
/>
|
/>
|
||||||
<DocDetailDrawer v-model:visible="detailVisible" :doc-id="detailId" />
|
<DocDetailDrawer v-model:visible="detailVisible" :doc-id="detailId" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Plus, Search, Refresh, ArrowDown } from '@element-plus/icons-vue'
|
import { Plus, Search, Refresh, ArrowDown } from '@element-plus/icons-vue'
|
||||||
import DocEditor from './DocEditor.vue'
|
import DocEditor from './DocEditor.vue'
|
||||||
@@ -155,6 +160,7 @@ import DocDetailDrawer from './DocDetailDrawer.vue'
|
|||||||
import {
|
import {
|
||||||
DOC_NEXT_TYPES,
|
DOC_NEXT_TYPES,
|
||||||
DOC_STATUS,
|
DOC_STATUS,
|
||||||
|
DOC_STATUS_VALUE,
|
||||||
dateOnly,
|
dateOnly,
|
||||||
docStatusTag,
|
docStatusTag,
|
||||||
docStatusText,
|
docStatusText,
|
||||||
@@ -163,7 +169,6 @@ import {
|
|||||||
qty
|
qty
|
||||||
} from './dict'
|
} from './dict'
|
||||||
import {
|
import {
|
||||||
approveDocument,
|
|
||||||
cancelDocument,
|
cancelDocument,
|
||||||
deleteDocument,
|
deleteDocument,
|
||||||
getDocumentDetail,
|
getDocumentDetail,
|
||||||
@@ -183,9 +188,14 @@ const loading = ref(false)
|
|||||||
const rows = ref([])
|
const rows = ref([])
|
||||||
const warehouses = ref([])
|
const warehouses = ref([])
|
||||||
const dateRange = ref([])
|
const dateRange = ref([])
|
||||||
const statusOptions = Object.keys(DOC_STATUS).map((k) => ({ value: k, label: DOC_STATUS[k].label }))
|
/** 状态分类 tab:all 代表「全部」,其余与 DOC_STATUS 一一对应 */
|
||||||
|
const statusTabs = [
|
||||||
|
{ value: 'all', label: '全部' },
|
||||||
|
...Object.keys(DOC_STATUS).map((k) => ({ value: k, label: DOC_STATUS[k].label }))
|
||||||
|
]
|
||||||
|
const activeStatus = ref('all')
|
||||||
|
|
||||||
const filters = reactive({ keyword: '', status: '', doc_type: '', warehouse_id: '' })
|
const filters = reactive({ keyword: '', doc_type: '', warehouse_id: '' })
|
||||||
const pagination = reactive({ page: 1, pageSize: 20, total: 0 })
|
const pagination = reactive({ page: 1, pageSize: 20, total: 0 })
|
||||||
|
|
||||||
const editorVisible = ref(false)
|
const editorVisible = ref(false)
|
||||||
@@ -206,16 +216,71 @@ function amountOf(row) {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列表页的状态与可执行操作对照:审核统一走工作台的审批列表,单据列表不再做审核。
|
||||||
|
* 草稿只能提交审批、编辑、删除;已驳回可编辑、提交审批、作废、删除;
|
||||||
|
* 审批中可作废;已生效只读且仅可下推;已作废完全只读。
|
||||||
|
*/
|
||||||
function canEdit(row) {
|
function canEdit(row) {
|
||||||
return row.status === 0 || row.status === 3
|
return row.status === DOC_STATUS_VALUE.DRAFT || row.status === DOC_STATUS_VALUE.REJECTED
|
||||||
}
|
}
|
||||||
|
|
||||||
function canApprove(row) {
|
function canSubmit(row) {
|
||||||
return row.status === 0 || row.status === 3 || row.status === 1
|
return canEdit(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
function canCancel(row) {
|
||||||
|
return row.status === DOC_STATUS_VALUE.REJECTED || row.status === DOC_STATUS_VALUE.PENDING
|
||||||
|
}
|
||||||
|
|
||||||
|
function canDelete(row) {
|
||||||
|
return canEdit(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 只有已生效单据能作为下推来源,作废/草稿/审批中/驳回都不允许 */
|
||||||
|
function canPush(row) {
|
||||||
|
return row.status === DOC_STATUS_VALUE.APPROVED
|
||||||
}
|
}
|
||||||
|
|
||||||
function nextTypes(row) {
|
function nextTypes(row) {
|
||||||
return DOC_NEXT_TYPES[row.doc_type] || []
|
return canPush(row) ? (DOC_NEXT_TYPES[row.doc_type] || []) : []
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 「更多」里至少有一项时才显示 */
|
||||||
|
function hasMoreActions(row) {
|
||||||
|
return canEdit(row) || canCancel(row) || nextTypes(row).length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 下推项与常规操作之间只加一条分隔线,落在第一个显示的常规操作上 */
|
||||||
|
function firstExtraCommand(row) {
|
||||||
|
if (canSubmit(row)) return 'submit'
|
||||||
|
if (canCancel(row)) return 'cancel'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function needExtraDivider(row, command) {
|
||||||
|
return nextTypes(row).length > 0 && firstExtraCommand(row) === command
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMoreCommand(command, row) {
|
||||||
|
const cmd = String(command)
|
||||||
|
if (cmd.startsWith('push:')) {
|
||||||
|
openPush(row, cmd.slice(5))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 菜单按状态隐藏后再兜一次底,避免旧数据或绕过菜单触发不该执行的操作
|
||||||
|
const allowed = {
|
||||||
|
submit: canSubmit(row),
|
||||||
|
cancel: canCancel(row),
|
||||||
|
delete: canDelete(row)
|
||||||
|
}
|
||||||
|
if (!allowed[cmd]) {
|
||||||
|
ElMessage.warning('当前状态不可执行该操作')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (cmd === 'submit') handleSubmit(row)
|
||||||
|
else if (cmd === 'cancel') handleCancel(row)
|
||||||
|
else if (cmd === 'delete') handleDelete(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchList() {
|
async function fetchList() {
|
||||||
@@ -227,7 +292,7 @@ async function fetchList() {
|
|||||||
doc_types: typeOptions.value.join(',')
|
doc_types: typeOptions.value.join(',')
|
||||||
}
|
}
|
||||||
if (filters.keyword) params.keyword = filters.keyword
|
if (filters.keyword) params.keyword = filters.keyword
|
||||||
if (filters.status !== '') params.status = filters.status
|
if (activeStatus.value !== 'all') params.status = activeStatus.value
|
||||||
if (filters.doc_type) params.doc_type = filters.doc_type
|
if (filters.doc_type) params.doc_type = filters.doc_type
|
||||||
if (filters.warehouse_id) params.warehouse_id = filters.warehouse_id
|
if (filters.warehouse_id) params.warehouse_id = filters.warehouse_id
|
||||||
if (dateRange.value && dateRange.value.length === 2) {
|
if (dateRange.value && dateRange.value.length === 2) {
|
||||||
@@ -253,13 +318,19 @@ function handleSearch() {
|
|||||||
fetchList()
|
fetchList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 切换状态分类即重新查询,页码回到第一页;与关键字、仓库、日期等条件叠加生效
|
||||||
|
watch(activeStatus, handleSearch)
|
||||||
|
|
||||||
function resetFilters() {
|
function resetFilters() {
|
||||||
filters.keyword = ''
|
filters.keyword = ''
|
||||||
filters.status = ''
|
|
||||||
filters.doc_type = ''
|
filters.doc_type = ''
|
||||||
filters.warehouse_id = ''
|
filters.warehouse_id = ''
|
||||||
dateRange.value = []
|
dateRange.value = []
|
||||||
|
if (activeStatus.value !== 'all') {
|
||||||
|
activeStatus.value = 'all' // 触发上面的 watch 刷新
|
||||||
|
} else {
|
||||||
handleSearch()
|
handleSearch()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openCreate(type) {
|
function openCreate(type) {
|
||||||
@@ -283,6 +354,11 @@ async function openEdit(row) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openPush(row, targetType) {
|
function openPush(row, targetType) {
|
||||||
|
// 二次兜底:即使菜单被绕过,也不能用未生效/已作废单据生成下游单据
|
||||||
|
if (!canPush(row)) {
|
||||||
|
ElMessage.warning('仅已生效的单据可以下推')
|
||||||
|
return
|
||||||
|
}
|
||||||
editData.value = null
|
editData.value = null
|
||||||
pushFrom.value = { doc: row, type: targetType }
|
pushFrom.value = { doc: row, type: targetType }
|
||||||
editorType.value = targetType
|
editorType.value = targetType
|
||||||
@@ -305,26 +381,6 @@ async function handleSubmit(row) {
|
|||||||
fetchList()
|
fetchList()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleApprove(row) {
|
|
||||||
try {
|
|
||||||
await ElMessageBox.confirm(
|
|
||||||
`确认审核「${row.doc_no}」?审核通过后将立即产生库存流水与往来账。`,
|
|
||||||
'审核确认',
|
|
||||||
{ type: 'warning' }
|
|
||||||
)
|
|
||||||
} catch (e) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await approveDocument(row.id)
|
|
||||||
ElMessage.success('单据已生效')
|
|
||||||
} catch (e) {
|
|
||||||
ElMessage.error(e.message || '审核失败')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fetchList()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleCancel(row) {
|
async function handleCancel(row) {
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(`确认作废「${row.doc_no}」?作废后不可恢复。`, '作废确认', { type: 'warning' })
|
await ElMessageBox.confirm(`确认作废「${row.doc_no}」?作废后不可恢复。`, '作废确认', { type: 'warning' })
|
||||||
@@ -357,13 +413,18 @@ async function handleDelete(row) {
|
|||||||
fetchList()
|
fetchList()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
/** 编辑器里快捷新增仓库后,列表页的仓库筛选下拉也要能选到新仓 */
|
||||||
|
async function loadWarehouses() {
|
||||||
try {
|
try {
|
||||||
const res = await getWarehouseList({ pageSize: 100 })
|
const res = await getWarehouseList({ pageSize: 100 })
|
||||||
if (res.code === 200) warehouses.value = res.data.list || []
|
if (res.code === 200) warehouses.value = res.data.list || []
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
warehouses.value = []
|
warehouses.value = []
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadWarehouses()
|
||||||
fetchList()
|
fetchList()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -393,6 +454,19 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 状态分类只作为筛选导航,列表和筛选栏在 tabs 外面,隐藏空的内容区
|
||||||
|
.status-tabs {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
:deep(.el-tabs__header) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tabs__content) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.filter-bar {
|
.filter-bar {
|
||||||
background: var(--el-bg-color);
|
background: var(--el-bg-color);
|
||||||
padding: 16px 16px 0;
|
padding: 16px 16px 0;
|
||||||
@@ -408,6 +482,29 @@ onMounted(async () => {
|
|||||||
border: 1px solid var(--el-border-color-lighter);
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 操作列统一用 flex 控制间距:el-dropdown 内的按钮拿不到 .el-button + .el-button 的
|
||||||
|
// 相邻间距,会出现「详情」「下推」贴得太近、箭头却离文字很远、按钮基线不齐的问题
|
||||||
|
.row-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 2px 10px;
|
||||||
|
|
||||||
|
:deep(.el-button + .el-button) {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dropdown) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-icon--right) {
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.doc-no {
|
.doc-no {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:model-value="visible"
|
||||||
|
title="快捷新增客户"
|
||||||
|
width="480px"
|
||||||
|
append-to-body
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@update:model-value="close"
|
||||||
|
@closed="resetForm"
|
||||||
|
>
|
||||||
|
<el-alert
|
||||||
|
title="保存后自动选中新客户,可继续开单"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
style="margin-bottom: 16px"
|
||||||
|
/>
|
||||||
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px" @submit.prevent>
|
||||||
|
<el-form-item label="客户名称" prop="customer_name">
|
||||||
|
<el-input ref="nameInput" v-model="form.customer_name" maxlength="100" placeholder="请输入客户名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="客户类型" prop="customer_type">
|
||||||
|
<el-select v-model="form.customer_type" style="width: 100%">
|
||||||
|
<el-option v-for="t in customerTypes" :key="t.value" :label="t.label" :value="t.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="联系人" prop="contact_person">
|
||||||
|
<el-input v-model="form.contact_person" placeholder="选填" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="联系电话" prop="contact_phone">
|
||||||
|
<el-input v-model="form.contact_phone" maxlength="20" placeholder="正式客户必填" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="联系邮箱" prop="contact_email">
|
||||||
|
<el-input v-model="form.contact_email" placeholder="选填" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="地址" prop="address">
|
||||||
|
<el-input v-model="form.address" placeholder="选填" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注" prop="remark">
|
||||||
|
<el-input v-model="form.remark" type="textarea" :rows="2" maxlength="500" placeholder="选填" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="close">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="saving" @click="submit">创建并选中</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { nextTick, reactive, ref, watch } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { createCustomer } from '@/api/customer'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: { type: Boolean, default: false }
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['update:visible', 'created'])
|
||||||
|
|
||||||
|
const formRef = ref()
|
||||||
|
const nameInput = ref()
|
||||||
|
const saving = ref(false)
|
||||||
|
|
||||||
|
const customerTypes = [
|
||||||
|
{ value: '1', label: '企业' },
|
||||||
|
{ value: '2', label: '政府机构' },
|
||||||
|
{ value: '3', label: '国企' },
|
||||||
|
{ value: '4', label: '教育机构' },
|
||||||
|
{ value: '5', label: '个人' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// status=1 / is_draft=0 才会出现在开单页的客户下拉里(列表按正式启用的客户过滤)
|
||||||
|
const defaultForm = () => ({
|
||||||
|
customer_name: '',
|
||||||
|
customer_type: '1',
|
||||||
|
contact_person: '',
|
||||||
|
contact_phone: '',
|
||||||
|
contact_email: '',
|
||||||
|
address: '',
|
||||||
|
customer_level: '3',
|
||||||
|
status: '1',
|
||||||
|
is_draft: 0,
|
||||||
|
credit_limit: 0,
|
||||||
|
payment_days: 0,
|
||||||
|
remark: ''
|
||||||
|
})
|
||||||
|
const form = reactive(defaultForm())
|
||||||
|
const rules = {
|
||||||
|
customer_name: [{ required: true, whitespace: true, message: '请填写客户名称', trigger: 'blur' }],
|
||||||
|
// 后端正式保存要求联系电话非空,这里提前拦截
|
||||||
|
contact_phone: [{ required: true, whitespace: true, message: '请填写联系电话', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.visible, (val) => {
|
||||||
|
if (!val) return
|
||||||
|
Object.assign(form, defaultForm())
|
||||||
|
nextTick(() => {
|
||||||
|
formRef.value?.clearValidate()
|
||||||
|
nameInput.value?.focus()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
emit('update:visible', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetForm() {
|
||||||
|
Object.assign(form, defaultForm())
|
||||||
|
formRef.value?.clearValidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
if (!formRef.value) return
|
||||||
|
try {
|
||||||
|
await formRef.value.validate()
|
||||||
|
} catch (e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.customer_name.trim() || !form.contact_phone.trim()) {
|
||||||
|
ElMessage.warning('请填写客户名称和联系电话')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
...form,
|
||||||
|
customer_name: form.customer_name.trim(),
|
||||||
|
contact_phone: form.contact_phone.trim(),
|
||||||
|
contact_person: form.contact_person.trim()
|
||||||
|
}
|
||||||
|
const res = await createCustomer(payload)
|
||||||
|
if (!res || res.code !== 200) {
|
||||||
|
ElMessage.error(res?.msg || '新增客户失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const id = Number(res.data?.id) || 0
|
||||||
|
ElMessage.success('客户已新增')
|
||||||
|
emit('created', { ...payload, id })
|
||||||
|
close()
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e.message || '新增客户失败')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:model-value="visible"
|
||||||
|
title="快捷新增仓库"
|
||||||
|
width="480px"
|
||||||
|
append-to-body
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@update:model-value="close"
|
||||||
|
@closed="resetForm"
|
||||||
|
>
|
||||||
|
<el-alert
|
||||||
|
title="保存后自动选中新仓库,可继续开单"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
style="margin-bottom: 16px"
|
||||||
|
/>
|
||||||
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px" @submit.prevent>
|
||||||
|
<el-form-item label="仓库名称" prop="warehouse_name">
|
||||||
|
<el-input ref="nameInput" v-model="form.warehouse_name" maxlength="100" placeholder="请输入仓库名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="仓库编号" prop="warehouse_code">
|
||||||
|
<el-input v-model="form.warehouse_code" placeholder="留空自动生成" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="负责人" prop="manager">
|
||||||
|
<el-input v-model="form.manager" placeholder="选填" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="联系电话" prop="phone">
|
||||||
|
<el-input v-model="form.phone" maxlength="20" placeholder="选填" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="地址" prop="address">
|
||||||
|
<el-input v-model="form.address" placeholder="选填" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="默认仓库">
|
||||||
|
<el-switch v-model="form.is_default" :active-value="1" :inactive-value="0" />
|
||||||
|
<span class="hint">开单时默认带出该仓</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注" prop="remark">
|
||||||
|
<el-input v-model="form.remark" type="textarea" :rows="2" maxlength="500" placeholder="选填" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="close">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="saving" @click="submit">创建并选中</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { nextTick, reactive, ref, watch } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { createWarehouse } from '@/api/erpWarehouse'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: { type: Boolean, default: false }
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['update:visible', 'created'])
|
||||||
|
|
||||||
|
const formRef = ref()
|
||||||
|
const nameInput = ref()
|
||||||
|
const saving = ref(false)
|
||||||
|
|
||||||
|
const defaultForm = () => ({
|
||||||
|
warehouse_name: '',
|
||||||
|
warehouse_code: '',
|
||||||
|
manager: '',
|
||||||
|
phone: '',
|
||||||
|
address: '',
|
||||||
|
is_default: 0,
|
||||||
|
status: 1,
|
||||||
|
remark: ''
|
||||||
|
})
|
||||||
|
const form = reactive(defaultForm())
|
||||||
|
const rules = {
|
||||||
|
warehouse_name: [{ required: true, whitespace: true, message: '请填写仓库名称', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.visible, (val) => {
|
||||||
|
if (!val) return
|
||||||
|
Object.assign(form, defaultForm())
|
||||||
|
nextTick(() => {
|
||||||
|
formRef.value?.clearValidate()
|
||||||
|
nameInput.value?.focus()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
emit('update:visible', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetForm() {
|
||||||
|
Object.assign(form, defaultForm())
|
||||||
|
formRef.value?.clearValidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
if (!formRef.value) return
|
||||||
|
try {
|
||||||
|
await formRef.value.validate()
|
||||||
|
} catch (e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.warehouse_name.trim()) {
|
||||||
|
ElMessage.warning('请填写仓库名称')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
...form,
|
||||||
|
warehouse_name: form.warehouse_name.trim(),
|
||||||
|
warehouse_code: form.warehouse_code.trim()
|
||||||
|
}
|
||||||
|
const res = await createWarehouse(payload)
|
||||||
|
if (!res || res.code !== 200) {
|
||||||
|
ElMessage.error(res?.msg || '新增仓库失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const id = Number(res.data?.id) || 0
|
||||||
|
ElMessage.success('仓库已新增')
|
||||||
|
emit('created', { ...payload, id })
|
||||||
|
close()
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e.message || '新增仓库失败')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.hint {
|
||||||
|
margin-left: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -32,6 +32,15 @@ export const DOC_STATUS = {
|
|||||||
4: { label: '已作废', tag: 'info' }
|
4: { label: '已作废', tag: 'info' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 单据状态值(与后端 models.ErpDocStatus* 一致),供权限判断使用,避免各处硬编码数字 */
|
||||||
|
export const DOC_STATUS_VALUE = {
|
||||||
|
DRAFT: 0,
|
||||||
|
PENDING: 1,
|
||||||
|
APPROVED: 2,
|
||||||
|
REJECTED: 3,
|
||||||
|
CANCELLED: 4
|
||||||
|
}
|
||||||
|
|
||||||
/** 下游单据:从已生效单据「下推」时用的目标类型 */
|
/** 下游单据:从已生效单据「下推」时用的目标类型 */
|
||||||
export const DOC_NEXT_TYPES = {
|
export const DOC_NEXT_TYPES = {
|
||||||
purchase_order: [
|
purchase_order: [
|
||||||
|
|||||||
@@ -0,0 +1,309 @@
|
|||||||
|
<template>
|
||||||
|
<div class="approval-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<div>
|
||||||
|
<h2>审批列表</h2>
|
||||||
|
<p>处理待我审批的进销存单据,审批通过后单据自动生效</p>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-radio-group v-model="filters.handled" @change="handleScopeChange">
|
||||||
|
<el-radio-button :value="0">待我审批</el-radio-button>
|
||||||
|
<el-radio-button :value="1">我已处理</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
<el-button :icon="Refresh" @click="loadList">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="approval-layout">
|
||||||
|
<aside class="type-nav">
|
||||||
|
<div class="type-nav-title">单据类型</div>
|
||||||
|
<div
|
||||||
|
v-for="t in bizTypes"
|
||||||
|
:key="t.value"
|
||||||
|
class="type-nav-item"
|
||||||
|
:class="{ active: filters.biz_type === t.value }"
|
||||||
|
@click="selectType(t.value)"
|
||||||
|
>{{ t.label }}</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<section class="list-content">
|
||||||
|
<div class="table-card">
|
||||||
|
<el-table v-loading="loading" :data="list" stripe border size="small">
|
||||||
|
<el-table-column label="单据标题" min-width="200" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">{{ row.instance?.biz_title || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="单据类型" width="140">
|
||||||
|
<template #default="{ row }">{{ bizTypeLabel(row.instance?.biz_type) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="发起人" width="120">
|
||||||
|
<template #default="{ row }">{{ row.instance?.submit_user_name || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="审批节点" width="150" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">{{ row.node_name || row.node_key || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="单据状态" width="110">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="tagType(row.instance?.status_tag)" size="small">{{ row.instance?.status_label || '-' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="任务状态" width="110">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="tagType(row.status_tag)" size="small">{{ row.status_label || '-' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="到达时间" width="170">
|
||||||
|
<template #default="{ row }">{{ row.create_time || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="primary" size="small" @click="openDoc(row)">单据详情</el-button>
|
||||||
|
<el-button link type="primary" size="small" @click="openDetail(row)">
|
||||||
|
{{ row.status === 0 ? '去处理' : '审批详情' }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<template #empty><el-empty :description="emptyText" /></template>
|
||||||
|
</el-table>
|
||||||
|
<div class="pagination">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.page"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@current-change="loadList"
|
||||||
|
@size-change="loadList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 审批前查看单据明细;审批动作与流程时间线复用 OA 审批中心 -->
|
||||||
|
<DocDetailDrawer v-model:visible="docVisible" :doc-id="docId" />
|
||||||
|
<ApprovalDetailDrawer v-model="detailVisible" :instance-id="detailId" @changed="loadList" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { Refresh } from '@element-plus/icons-vue'
|
||||||
|
import DocDetailDrawer from '../document/DocDetailDrawer.vue'
|
||||||
|
import ApprovalDetailDrawer from '@/views/apps/oa/approvalcenter/components/DetailDrawer.vue'
|
||||||
|
import { getApprovalTodo } from '@/api/approval'
|
||||||
|
import { DOC_TYPE_ORDER, docTypeLabel } from '../document/dict'
|
||||||
|
|
||||||
|
// 单据审批流的 biz_type 固定为 "erp_" + 单据类型;erp 表示全部进销存单据
|
||||||
|
const ERP_BIZ_PREFIX = 'erp_'
|
||||||
|
// tab 按单据类型分类:全部 + 十类进销存单据,value 即运行期 biz_type
|
||||||
|
const bizTypes = [
|
||||||
|
{ value: 'erp', label: '全部' },
|
||||||
|
...DOC_TYPE_ORDER.map((t) => ({ value: ERP_BIZ_PREFIX + t, label: docTypeLabel(t) }))
|
||||||
|
]
|
||||||
|
|
||||||
|
const filters = reactive({ handled: 0, biz_type: 'erp' })
|
||||||
|
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||||
|
const list = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const detailVisible = ref(false)
|
||||||
|
const detailId = ref(0)
|
||||||
|
const docVisible = ref(false)
|
||||||
|
const docId = ref(0)
|
||||||
|
|
||||||
|
const emptyText = computed(() => (filters.handled === 1 ? '暂无已处理的审批' : '暂无待我审批的单据'))
|
||||||
|
const responseData = (res) => res?.data?.data ?? res?.data ?? res ?? {}
|
||||||
|
|
||||||
|
/** 本列表只认 erp_ 前缀的任务:即使后端旧版本忽略 biz_type 参数,也不会混入 OA/报销等审批 */
|
||||||
|
function isErpTask(row) {
|
||||||
|
return String(row?.instance?.biz_type || row?.biz_type || '').startsWith(ERP_BIZ_PREFIX)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 请求范围强制收敛到进销存,异常值一律回落为「全部 ERP 单据」 */
|
||||||
|
function erpScope(value) {
|
||||||
|
return value === 'erp' || String(value || '').startsWith(ERP_BIZ_PREFIX) ? value : 'erp'
|
||||||
|
}
|
||||||
|
|
||||||
|
function bizTypeLabel(value) {
|
||||||
|
const raw = String(value || '')
|
||||||
|
return raw.startsWith(ERP_BIZ_PREFIX) ? docTypeLabel(raw.slice(ERP_BIZ_PREFIX.length)) : (raw || '-')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadList() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const data = responseData(
|
||||||
|
await getApprovalTodo({
|
||||||
|
handled: filters.handled,
|
||||||
|
biz_type: erpScope(filters.biz_type),
|
||||||
|
page: pagination.page,
|
||||||
|
pageSize: pagination.pageSize
|
||||||
|
})
|
||||||
|
)
|
||||||
|
// 后端按 biz_type 过滤,前端再兜一层,保证列表与 OA/报销等审批完全隔离
|
||||||
|
const rows = (data.list || []).filter(isErpTask)
|
||||||
|
list.value = filters.biz_type === 'erp'
|
||||||
|
? rows
|
||||||
|
: rows.filter((row) => String(row.instance?.biz_type || '') === filters.biz_type)
|
||||||
|
pagination.total = Number(data.total || 0)
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e.message || '加载审批列表失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleScopeChange() {
|
||||||
|
pagination.page = 1
|
||||||
|
loadList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 左侧类型导航:切换单据类型后右侧列表回到第一页重新查询 */
|
||||||
|
function selectType(value) {
|
||||||
|
if (filters.biz_type === value) return
|
||||||
|
filters.biz_type = value
|
||||||
|
handleScopeChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDetail(row) {
|
||||||
|
const id = Number(row.instance_id || row.instance?.id || 0)
|
||||||
|
if (!id) {
|
||||||
|
ElMessage.warning('未找到审批实例')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
detailId.value = id
|
||||||
|
detailVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDoc(row) {
|
||||||
|
const id = Number(row.instance?.biz_id || 0)
|
||||||
|
if (!id) {
|
||||||
|
ElMessage.warning('未找到关联单据')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
docId.value = id
|
||||||
|
docVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function tagType(type) {
|
||||||
|
if (type === 'error') return 'danger'
|
||||||
|
return type || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadList)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.approval-page {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 左侧单据类型导航,右侧是对应的审批列表
|
||||||
|
.approval-layout {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-nav {
|
||||||
|
width: 180px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-nav-title {
|
||||||
|
padding: 8px 12px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-nav-item {
|
||||||
|
padding: 9px 12px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s, color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: var(--el-color-primary-light-9);
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.approval-layout {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-nav {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-nav-title {
|
||||||
|
width: 100%;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-nav-item {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-card {
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -108,12 +108,17 @@ import { Refresh, Search } from "@element-plus/icons-vue";
|
|||||||
import DetailDrawer from "./components/DetailDrawer.vue";
|
import DetailDrawer from "./components/DetailDrawer.vue";
|
||||||
import { getApprovalTodo } from "@/api/approval";
|
import { getApprovalTodo } from "@/api/approval";
|
||||||
|
|
||||||
|
// 进销存审批由 ERP 工作台的审批列表单独处理,OA 审批中心只管其它审批
|
||||||
|
const ERP_BIZ_PREFIX = "erp_";
|
||||||
|
const ALL_NON_ERP = "non_erp";
|
||||||
|
|
||||||
const bizTypes = [
|
const bizTypes = [
|
||||||
|
{ value: ALL_NON_ERP, label: "全部类型" },
|
||||||
{ value: "reimburse", label: "报销单" },
|
{ value: "reimburse", label: "报销单" },
|
||||||
{ value: "oa", label: "OA 通用审批" },
|
{ value: "oa", label: "OA 通用审批" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const filters = reactive({ handled: 0, biz_type: "" });
|
const filters = reactive({ handled: 0, biz_type: ALL_NON_ERP });
|
||||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 });
|
const pagination = reactive({ page: 1, pageSize: 10, total: 0 });
|
||||||
const list = ref([]);
|
const list = ref([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
@@ -129,19 +134,23 @@ const responseData = (res) => res?.data?.data ?? res?.data ?? res ?? {};
|
|||||||
const loadList = async () => {
|
const loadList = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
|
// 清空下拉也按「全部非 ERP 审批」处理,保证不会混入进销存审批
|
||||||
|
const scope = filters.biz_type || ALL_NON_ERP;
|
||||||
const data = responseData(
|
const data = responseData(
|
||||||
await getApprovalTodo({
|
await getApprovalTodo({
|
||||||
handled: filters.handled,
|
handled: filters.handled,
|
||||||
biz_type: filters.biz_type || "",
|
biz_type: scope,
|
||||||
page: pagination.page,
|
page: pagination.page,
|
||||||
pageSize: pagination.pageSize,
|
pageSize: pagination.pageSize,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
// 前端二次过滤:类型下拉切换后即时生效
|
// 后端旧版本忽略 biz_type 时再兜一层,隔离始终生效
|
||||||
const rows = data.list || [];
|
const rows = (data.list || []).filter(
|
||||||
list.value = filters.biz_type
|
(row) => !String(row?.instance?.biz_type || "").startsWith(ERP_BIZ_PREFIX)
|
||||||
? rows.filter((row) => row.instance?.biz_type === filters.biz_type)
|
);
|
||||||
: rows;
|
list.value = scope === ALL_NON_ERP
|
||||||
|
? rows
|
||||||
|
: rows.filter((row) => row.instance?.biz_type === scope);
|
||||||
pagination.total = Number(data.total || 0);
|
pagination.total = Number(data.total || 0);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(error?.message || "加载审批列表失败");
|
ElMessage.error(error?.message || "加载审批列表失败");
|
||||||
|
|||||||
@@ -482,6 +482,12 @@ func (c *BackendApprovalController) Options() {
|
|||||||
"code": m.Code, "name": m.Name, "enabled": enabled,
|
"code": m.Code, "name": m.Name, "enabled": enabled,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// 进销存单据运行期按 biz_type = "erp_" + doc_type 找流程。这里额外开放「采购订单审批」
|
||||||
|
// 分类,让用户能配出只作用于采购订单的流程;不配时仍会回落到模块级 erp 流程。
|
||||||
|
// 分类 code 即 biz_type,与 FlowSave 的「分类与单据类型对齐」约定一致。
|
||||||
|
moduleList = append(moduleList, map[string]interface{}{
|
||||||
|
"code": models.ApprovalBizPurchaseOrder, "name": "采购订单审批", "enabled": true,
|
||||||
|
})
|
||||||
|
|
||||||
// 租户用户
|
// 租户用户
|
||||||
var tu []models.SystemTenantUser
|
var tu []models.SystemTenantUser
|
||||||
|
|||||||
@@ -142,6 +142,25 @@ func (c *BackendApprovalRuntimeController) InstanceTodo() {
|
|||||||
} else {
|
} else {
|
||||||
qs = qs.Filter("status", models.ApprovalTaskPending)
|
qs = qs.Filter("status", models.ApprovalTaskPending)
|
||||||
}
|
}
|
||||||
|
// 审批任务自带 biz_type,直接在任务查询上过滤,total 与分页才准确。
|
||||||
|
// erp = 只查进销存单据;non_erp = 只查其它审批,两个审批列表互不混入。
|
||||||
|
if bizType := strings.TrimSpace(c.GetString("biz_type")); bizType != "" {
|
||||||
|
switch bizType {
|
||||||
|
case "erp", "erp_":
|
||||||
|
qs = qs.Filter("biz_type__startswith", "erp_")
|
||||||
|
case "non_erp":
|
||||||
|
// 用已知单据类型精确排除,避免依赖 Exclude 对前缀操作符的支持
|
||||||
|
erpBizTypes := make([]string, 0, len(erpDocTypes))
|
||||||
|
for docType := range erpDocTypes {
|
||||||
|
erpBizTypes = append(erpBizTypes, models.ErpDocApprovalBizType(docType))
|
||||||
|
}
|
||||||
|
if len(erpBizTypes) > 0 {
|
||||||
|
qs = qs.Exclude("biz_type__in", erpBizTypes)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
qs = qs.Filter("biz_type", bizType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
total, _ := qs.Count()
|
total, _ := qs.Count()
|
||||||
var tasks []models.ApprovalTask
|
var tasks []models.ApprovalTask
|
||||||
@@ -163,6 +182,7 @@ func (c *BackendApprovalRuntimeController) InstanceTodo() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
taskView := oaApprovalTaskView(t, true, oaApprovalNodeTypes(inst)[t.NodeKey])
|
taskView := oaApprovalTaskView(t, true, oaApprovalNodeTypes(inst)[t.NodeKey])
|
||||||
|
taskView["instance_id"] = t.InstanceID
|
||||||
taskView["instance"] = oaApprovalInstanceView(inst)
|
taskView["instance"] = oaApprovalInstanceView(inst)
|
||||||
list = append(list, taskView)
|
list = append(list, taskView)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -578,6 +578,11 @@ func erpDocExecutableCore(a erpActor, id uint64) (map[string]interface{}, int, s
|
|||||||
if e != nil {
|
if e != nil {
|
||||||
return nil, 404, "单据未找到"
|
return nil, 404, "单据未找到"
|
||||||
}
|
}
|
||||||
|
// 只有已生效单据可作为下推来源;作废、草稿、审批中、驳回都直接拒绝,
|
||||||
|
// 与保存时 erpValidateDocHead 的来源状态校验保持一致,避免绕过前端菜单
|
||||||
|
if doc.Status != models.ErpDocStatusApproved {
|
||||||
|
return nil, 400, "来源单据未生效,不能下推"
|
||||||
|
}
|
||||||
isReturn := doc.DocType == models.ErpDocPurchaseReturn || doc.DocType == models.ErpDocSalesReturn
|
isReturn := doc.DocType == models.ErpDocPurchaseReturn || doc.DocType == models.ErpDocSalesReturn
|
||||||
items := erpDocItems(models.Orm, doc.ID)
|
items := erpDocItems(models.Orm, doc.ID)
|
||||||
out := make([]map[string]interface{}, 0, len(items))
|
out := make([]map[string]interface{}, 0, len(items))
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- 目标结构(「大类」= 模块根下一层目录,会作为移动端功能页的分组标题;
|
-- 目标结构(「大类」= 模块根下一层目录,会作为移动端功能页的分组标题;
|
||||||
-- 「小类」= 叶子菜单,渲染为功能卡片):
|
-- 「小类」= 叶子菜单,渲染为功能卡片):
|
||||||
-- 工作台 -> 工作台(复用已有仪表盘页面 /apps/erp/dashboard)
|
-- 工作台(目录)-> 工作台(仪表盘 /apps/erp/dashboard)/ 审批列表(/apps/erp/workbench/approval)
|
||||||
-- 基础资料 -> 商品 / 供应商 / 客户 / 仓库
|
-- 基础资料 -> 商品 / 供应商 / 客户 / 仓库
|
||||||
-- 采购管理 -> 采购订单 / 采购入库 / 采购退货
|
-- 采购管理 -> 采购订单 / 采购入库 / 采购退货
|
||||||
-- 销售管理 -> 销售订单 / 销售出库 / 销售退货
|
-- 销售管理 -> 销售订单 / 销售出库 / 销售退货
|
||||||
@@ -36,6 +36,12 @@ FROM DUAL
|
|||||||
WHERE @erp_id IS NOT NULL
|
WHERE @erp_id IS NOT NULL
|
||||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM yz_system_menu WHERE path = '/apps/erp/workbench' AND delete_time IS NULL) x);
|
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM yz_system_menu WHERE path = '/apps/erp/workbench' AND delete_time IS NULL) x);
|
||||||
|
|
||||||
|
-- 工作台必须是目录:历史数据里可能被建成了页面,统一纠正,目录内才放工作台/审批列表两个页面
|
||||||
|
UPDATE yz_system_menu
|
||||||
|
SET type = 1, component_path = NULL, update_time = NOW()
|
||||||
|
WHERE delete_time IS NULL AND path = '/apps/erp/workbench'
|
||||||
|
AND (type <> 1 OR (component_path IS NOT NULL AND component_path <> ''));
|
||||||
|
|
||||||
INSERT INTO yz_system_menu
|
INSERT INTO yz_system_menu
|
||||||
(pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark)
|
(pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark)
|
||||||
SELECT @erp_id, '基础资料', '/apps/erp/basic', NULL, 'fa-solid fa-database', 2, 1, 1, '[2]', 1, 'system', '进销存大类:基础资料'
|
SELECT @erp_id, '基础资料', '/apps/erp/basic', NULL, 'fa-solid fa-database', 2, 1, 1, '[2]', 1, 'system', '进销存大类:基础资料'
|
||||||
@@ -91,9 +97,9 @@ SET @erp_basic := (SELECT id FROM yz_system_menu WHERE path = '/apps/erp/bas
|
|||||||
SET @erp_system := (SELECT id FROM yz_system_menu WHERE path = '/apps/erp/system' AND delete_time IS NULL LIMIT 1);
|
SET @erp_system := (SELECT id FROM yz_system_menu WHERE path = '/apps/erp/system' AND delete_time IS NULL LIMIT 1);
|
||||||
|
|
||||||
-- ---------- 2. 已有菜单迁入新树(避免同一功能出现两份) ----------
|
-- ---------- 2. 已有菜单迁入新树(避免同一功能出现两份) ----------
|
||||||
-- 仪表盘 -> 工作台大类,并更名为「工作台」;路由与页面文件不变
|
-- 仪表盘 -> 工作台目录内的「工作台」页面;路由与页面文件不变
|
||||||
UPDATE yz_system_menu
|
UPDATE yz_system_menu
|
||||||
SET pid = @erp_workbench, title = '工作台', sort = 1, update_time = NOW()
|
SET pid = @erp_workbench, title = '工作台', sort = 1, type = 2, update_time = NOW()
|
||||||
WHERE delete_time IS NULL AND path = '/apps/erp/dashboard' AND @erp_workbench IS NOT NULL;
|
WHERE delete_time IS NULL AND path = '/apps/erp/dashboard' AND @erp_workbench IS NOT NULL;
|
||||||
|
|
||||||
-- 供应商列表 / 客户列表 -> 基础资料大类,去掉「列表」后缀;路由与页面文件不变
|
-- 供应商列表 / 客户列表 -> 基础资料大类,去掉「列表」后缀;路由与页面文件不变
|
||||||
@@ -127,6 +133,8 @@ CREATE TEMPORARY TABLE tmp_erp_leaf (
|
|||||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
||||||
|
|
||||||
INSERT INTO tmp_erp_leaf (parent_path, title, path, icon, sort, remark) VALUES
|
INSERT INTO tmp_erp_leaf (parent_path, title, path, icon, sort, remark) VALUES
|
||||||
|
('/apps/erp/workbench', '工作台', '/apps/erp/dashboard', 'fa-solid fa-gauge', 1, '进销存工作台(仪表盘)'),
|
||||||
|
('/apps/erp/workbench', '审批列表', '/apps/erp/workbench/approval', 'fa-solid fa-clipboard-check', 2, '待我审批 / 我已处理,审批后单据自动生效'),
|
||||||
('/apps/erp/basic', '商品', '/apps/erp/basic/product', 'fa-solid fa-box', 1, '商品档案'),
|
('/apps/erp/basic', '商品', '/apps/erp/basic/product', 'fa-solid fa-box', 1, '商品档案'),
|
||||||
('/apps/erp/basic', '仓库', '/apps/erp/basic/warehouse', 'fa-solid fa-boxes-packing', 4, '仓库档案'),
|
('/apps/erp/basic', '仓库', '/apps/erp/basic/warehouse', 'fa-solid fa-boxes-packing', 4, '仓库档案'),
|
||||||
('/apps/erp/purchase', '采购订单', '/apps/erp/purchase/order', 'fa-solid fa-file-invoice', 1, '采购订单'),
|
('/apps/erp/purchase', '采购订单', '/apps/erp/purchase/order', 'fa-solid fa-file-invoice', 1, '采购订单'),
|
||||||
@@ -168,6 +176,8 @@ CREATE TEMPORARY TABLE tmp_erp_page (
|
|||||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
||||||
|
|
||||||
INSERT INTO tmp_erp_page (path, component_path) VALUES
|
INSERT INTO tmp_erp_page (path, component_path) VALUES
|
||||||
|
('/apps/erp/dashboard', '/apps/erp/dashboard/index.vue'),
|
||||||
|
('/apps/erp/workbench/approval', '/apps/erp/workbench/approvalList.vue'),
|
||||||
('/apps/erp/basic/product', '/apps/erp/basic/product.vue'),
|
('/apps/erp/basic/product', '/apps/erp/basic/product.vue'),
|
||||||
('/apps/erp/basic/warehouse', '/apps/erp/basic/warehouse.vue'),
|
('/apps/erp/basic/warehouse', '/apps/erp/basic/warehouse.vue'),
|
||||||
('/apps/erp/purchase/order', '/apps/erp/purchase/order.vue'),
|
('/apps/erp/purchase/order', '/apps/erp/purchase/order.vue'),
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ const (
|
|||||||
ApprovalBizReimburse = "reimburse" // OA 报销单
|
ApprovalBizReimburse = "reimburse" // OA 报销单
|
||||||
ApprovalBizBid = "bid" // CRM 投标立项
|
ApprovalBizBid = "bid" // CRM 投标立项
|
||||||
ApprovalBizDomain = "domain" // 域名申请(平台侧审批)
|
ApprovalBizDomain = "domain" // 域名申请(平台侧审批)
|
||||||
|
ApprovalBizPurchaseOrder = "erp_purchase_order" // 采购订单审批(与 ErpDocApprovalBizType 一致)
|
||||||
)
|
)
|
||||||
|
|
||||||
// ---------------- 租户侧配置 ----------------
|
// ---------------- 租户侧配置 ----------------
|
||||||
|
|||||||
Reference in new issue
Block a user