feat(approve): 新增订单审批功能模块

- 添加审批页面布局组件 ApproveLayout.vue
- 实现订单审批主页面 Approve.vue
- 创建配置信息展示组件 ConfigInfo.vue
- 增加审批相关 API 接口方法
- 更新环境变量配置文件标题
- 修改首页标题显示内容- 调整路由配置结构
dev_1.0.0
chenhao 2025-11-20 16:23:07 +08:00
parent 8a0329fb4b
commit 6633a09861
25 changed files with 1284 additions and 59 deletions

View File

@ -1,5 +1,5 @@
# 页面标题
VUE_APP_TITLE = 若依管理系统
VUE_APP_TITLE = UNISSENSE-OMS
# 开发环境配置
ENV = 'development'

View File

@ -1,5 +1,5 @@
# 页面标题
VUE_APP_TITLE = 若依管理系统
VUE_APP_TITLE = UNISSENSE-OMS
# 生产环境配置
ENV = 'production'

View File

@ -1,5 +1,5 @@
# 页面标题
VUE_APP_TITLE = 若依管理系统
VUE_APP_TITLE = UNISSENSE-OMS
BABEL_ENV = production

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -6,7 +6,8 @@
<meta name="renderer" content="webkit">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= webpackConfig.name %></title>
<!-- <title><%= webpackConfig.name %></title>-->
<title>汇智OMS订单管理系统</title>
<!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
<style>
html,

View File

@ -0,0 +1,20 @@
import request from '@/utils/request'
// 查询订单列表
export function listOrder(query) {
return request({
url: '/project/order/vue/approve/list',
method: 'get',
params: query
})
}
// 提交审批(同意或驳回)
export function approveOrder(data) {
return request({
url: '/project/order/order/approve',
method: 'post',
data: data,
headers: { 'Content-Type': 'multipart/form-data' }
})
}

View File

@ -12,7 +12,7 @@ export function listOrder(query) {
// 查询订单管理详细信息
export function getOrder(id) {
return request({
url: '/project/order/vue/' + id,
url: '/project/order/h5/approve/' + id,
method: 'get'
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 509 KiB

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -2,11 +2,15 @@
<div class="sidebar-logo-container" :class="{'collapse':collapse}" :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
<transition name="sidebarLogoFade">
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo" />
<div v-if="logo" style="width: 30px; position: absolute; overflow: hidden;">
<img :src="logo" class="sidebar-logo"/>
</div>
<h1 v-else class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }} </h1>
</router-link>
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo" />
<div v-if="logo" style="width: 30px; position: absolute; overflow: hidden;">
<img :src="logo" class="sidebar-logo"/>
</div>
<h1 class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }} </h1>
</router-link>
</transition>
@ -66,8 +70,7 @@ export default {
width: 100%;
& .sidebar-logo {
width: 32px;
height: 32px;
vertical-align: middle;
margin-right: 12px;
}

View File

@ -88,37 +88,8 @@ export const constantRoutes = [
}
]
},
{
path: '/manage',
component: Layout,
redirect: 'noRedirect',
name: 'Manage',
meta: {
title: '档案管理',
icon: 'documentation'
},
children: [
{
path: 'order',
component: () => import('@/views/manage/order/index'),
name: 'ManageOrder',
meta: { title: '合同档案', icon: 'documentation' }
}
]
},
{
path: '/project/order',
component: Layout,
hidden: true,
children: [
{
path: '',
component: () => import('@/views/project/order/index'),
name: 'ProjectOrder',
meta: { title: '订单管理', icon: 'order' }
}
]
},
{
path: '/service-query',
component: () => import('@/views/manage/service/index'),

View File

@ -2,8 +2,8 @@ module.exports = {
/**
* 网页标题
*/
title: process.env.VUE_APP_TITLE,
// title: process.env.VUE_APP_TITLE,
title: '汇智OMS订单管理系统',
/**
* 侧边栏主题 深色主题theme-dark浅色主题theme-light
*/
@ -23,7 +23,7 @@ module.exports = {
* 是否显示 tagsView
*/
tagsView: true,
/**
* 显示页签图标
*/
@ -52,5 +52,5 @@ module.exports = {
/**
* 底部版权文本内容
*/
footerContent: 'Copyright © 2018-2025 RuoYi. All Rights Reserved.'
footerContent: 'Copyright © 2018-2025 Unissense. All Rights Reserved.'
}

View File

@ -0,0 +1,81 @@
<template>
<div class="approve-layout">
<header class="approve-layout-header">
<div class="header-center">
<h1>{{ title }}</h1>
</div>
<div class="header-right">
<img src="@/assets/images/companyLogo.png" alt="Company Logo" class="company-logo" />
</div>
</header>
<main class="approve-layout-content">
<slot></slot>
</main>
<footer class="approve-layout-footer">
<slot name="footer"></slot>
</footer>
</div>
</template>
<script>
export default {
name: 'ApproveLayout',
props: {
title: {
type: String,
default: '审批页面'
}
}
}
</script>
<style lang="scss" scoped>
.approve-layout {
min-height: 100vh;
background-color: #F8F5F0;
padding: 20px;
display: flex;
flex-direction: column;
}
.approve-layout-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 15px 20px;
border-bottom: 1px solid ; /* Separator */
.header-center {
flex-grow: 1;
text-align: left; /* Changed to left */
h1 {
margin: 0;
font-size: 24px;
color: #606266; /* Lighter color */
}
}
.header-right {
display: flex;
align-items: center;
}
.company-logo {
height: 40px; /* Adjust as needed */
width: auto;
}
}
.approve-layout-footer {
text-align: left;
padding: 15px 20px;
border-top: 1px solid; /* Separator */
color: #666;
font-size: 14px;
}
</style>

View File

@ -0,0 +1,487 @@
<template>
<div class="approve-container">
<ApproveLayout title="紫光汇智信息技术有限公司">
<template #default>
<div style="display: flex;align-items: center;justify-content: center;">
<span id="projectNameBox" style="margin-left: 10px;color: black;font-size: 30px">
紫光汇智信息技术有限公司进货供货订单
</span>
</div>
<el-form ref="orderForm" :model="order" label-width="120px" class="mb20">
<h3 class="section-title">订单信息</h3>
<order-info-view
:order-data.sync="order"
:is-readonly="true"
/>
</el-form>
<h3 class="section-title">配置信息</h3>
<config-info
:order-data="order"
:can-edit-discount="canEditDiscount"
:hide-price="hidePrice"
@discount-change="handleDiscountChange"
@tax-rate-change="handleTaxRateChange"
class="mb20"
/>
<!-- Tab页附件信息和流转过程 -->
<el-tabs v-model="activeTab" class="approve-tabs">
<el-tab-pane v-if="order.processTemplate !== '1'" label="附件信息" name="attachment">
<el-table :data="attachmentList" border>
<el-table-column label="附件类型" align="center" width="150">
<template slot-scope="scope">
{{ getAttachmentType(scope.$index) }}
</template>
</el-table-column>
<el-table-column label="附件名称" align="center" prop="fileName" />
<el-table-column label="上传人" align="center" prop="uploadUserName" width="120" />
<el-table-column label="上传时间" align="center" width="180">
<template slot-scope="scope">
{{ formatDate(scope.row.uploadTime) }}
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="150">
<template slot-scope="scope">
<el-button
v-if="scope.row.id && scope.row.id !== -1"
size="mini"
type="text"
@click="previewFile(scope.row)"
>预览</el-button>
<el-button
v-if="scope.row.id && scope.row.id !== -1"
size="mini"
type="text"
@click="downloadFile(scope.row)"
>下载</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="流转过程" name="process">
<div class="process-container">
<el-tabs v-model="activeVersionTab" type="card" v-if="uniqueVersions.length > 0">
<el-tab-pane
v-for="version in uniqueVersions"
:key="version"
:label="'版本号Rev.' + version"
:name="version">
<el-timeline>
<el-timeline-item
v-for="log in groupedApproveLogs[version]"
:key="log.id"
:timestamp="log.approveTime"
placement="top">
<el-card>
<h4>{{ log.approveOpinion }}</h4>
<p><b>操作人:</b> {{ log.approveUserName }} ({{ log.roleName }})</p>
<p><b>接收人:</b> {{ getReceiverName(log) }}</p>
<p><b>审批状态:</b> <el-tag :type="getStatusTagType(log.approveStatus)" size="small">{{ getStatusText(log.approveStatus) }}</el-tag></p>
</el-card>
</el-timeline-item>
</el-timeline>
</el-tab-pane>
</el-tabs>
<div v-else></div>
</div>
</el-tab-pane>
</el-tabs>
</template>
<template #footer>
<p>{{ order.projectCode }}-{{ order.orderCode }}-Rev.{{ order.versionCode }}</p>
</template>
</ApproveLayout>
<!-- 审批意见弹窗 -->
<el-dialog
:title="dialogTitle"
:visible.sync="opinionDialogVisible"
width="500px"
append-to-body
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<el-form ref="opinionForm" :model="opinionForm" :rules="opinionRules" label-width="100px">
<el-form-item label="审批意见" prop="approveOpinion">
<el-input
v-model="opinionForm.approveOpinion"
type="textarea"
:rows="4"
placeholder="请输入审批意见"
:disabled="submitLoading"
/>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="opinionDialogVisible = false" :disabled="submitLoading"> </el-button>
<el-button type="primary" @click="submitOpinion" :loading="submitLoading"> </el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { getOrder } from "@/api/project/order";
import { approveOrder } from "@/api/approve/order";
import ConfigInfo from './ConfigInfo.vue';
import ApproveLayout from '@/views/approve/ApproveLayout.vue';
export default {
name: "Approve",
components: {
ApproveLayout,
OrderInfoView: () => import('@/components/order/OrderInfo.vue'),
ConfigInfo
},
props: {
orderId: {
type: Number,
required: true
}
},
data() {
return {
order: {},
todo: {}, //
approveLog: [], //
activeTab: 'attachment', //
activeVersionTab: null, // Tab
selectedDiscount: 1, //
opinionDialogVisible: false,
dialogTitle: '',
approveType: '', // 'approve' 'reject'
submitLoading: false, // loading
opinionForm: {
approveOpinion: '',
},
opinionRules: {
approveOpinion: [
{ required: true, message: '审批意见不能为空', trigger: 'blur' },
],
},
};
},
computed: {
//
attachmentList() {
if (!this.order.contractTableData || !this.order.versionCode) {
return [];
}
return this.order.contractTableData[this.order.versionCode] || [];
},
//
uniqueVersions() {
if (!this.approveLog || this.approveLog.length === 0) return [];
const versions = [...new Set(this.approveLog.map(log => log.extendField1))];
return versions.sort((a, b) => b - a); //
},
//
groupedApproveLogs() {
if (!this.approveLog || this.approveLog.length === 0) return {};
return this.approveLog.reduce((acc, log) => {
const version = log.extendField1;
if (!acc[version]) acc[version] = [];
acc[version].push(log);
return acc;
}, {});
},
//
canEditDiscount() {
if (!this.todo || !this.todo.taskName) return false;
const taskName = this.todo.taskName;
return taskName.startsWith('商务') || taskName.startsWith('财务');
},
//
hidePrice() {
if (!this.todo || !this.todo.taskName) return false;
const taskName = this.todo.taskName;
return taskName.includes('省代');
}
},
watch: {
orderId: {
immediate: true,
handler(id) {
if (id) {
this.getOrderDetails();
}
}
}
},
methods: {
getOrderDetails() {
getOrder(this.orderId).then(response => {
this.order = { ...response.data.projectOrderInfo };
this.approveLog = response.data.approveLog || [];
this.todo = response.data.todo || {};
// tab
if (this.order.processTemplate === '1') {
this.activeTab = 'process';
}
// Tab
if (this.uniqueVersions.length > 0) {
this.activeVersionTab = String(this.uniqueVersions[0]);
}
});
},
//
handleDiscountChange(value) {
this.selectedDiscount = value;
},
//
handleTaxRateChange(product) {
//
if (product.taxRate !== null && product.taxRate !== undefined) {
// 2
product.taxRate = Math.round(product.taxRate * 100) / 100;
if (product.taxRate < 0) {
product.taxRate = 0;
this.$modal.msgWarning('税率不能小于0');
} else if (product.taxRate > 100) {
product.taxRate = 100;
this.$modal.msgWarning('税率不能大于100');
}
}
},
handleApprove() {
this.approveType = 'approve';
this.dialogTitle = '同意审批';
this.opinionForm.approveOpinion = '';
this.opinionDialogVisible = true;
},
handleReject() {
this.approveType = 'reject';
this.dialogTitle = '驳回审批';
this.opinionForm.approveOpinion = '';
this.opinionDialogVisible = true;
},
submitOpinion() {
this.$refs.opinionForm.validate(valid => {
if (valid) {
// loading
this.submitLoading = true;
//
const { taxRateData, ...todoData } = this.todo;
const data = {
...todoData,
approveOpinion: this.opinionForm.approveOpinion,
approveStatus: this.approveType === 'approve' ? 3 : 2,
variables: {
comment: this.opinionForm.approveOpinion,
approveBtn: this.approveType === 'approve' ? 1 : 0,
allPriceCountValue: this.selectedDiscount
}
};
// variables
if (this.order.actualPurchaseAmount) {
data.variables.actualPurchaseAmount = this.order.actualPurchaseAmount;
}
//
if (this.canEditDiscount) {
const taxRateList = this.collectTaxRateData();
if (taxRateList && taxRateList.length > 0) {
for (let i = 0; i < taxRateList.length; i++) {
for (let key in taxRateList[i]) {
data['taxRateData[' + i + '].' + key] = taxRateList[i][key];
//
if (key === 'taxRate' && (taxRateList[i][key] > 100 || taxRateList[i][key] < 0)) {
this.$modal.msgError("税率区间为0-100");
this.submitLoading = false;
return;
}
}
}
}
}
//
approveOrder(data).then(() => {
this.$modal.msgSuccess(this.approveType === 'approve' ? '审批成功' : '驳回成功');
this.submitLoading = false;
this.opinionDialogVisible = false;
this.closeDialog(true);
}).catch(() => {
this.$modal.msgError(this.approveType === 'approve' ? '审批失败' : '驳回失败');
this.submitLoading = false;
});
}
});
},
//
collectTaxRateData() {
const taxRateList = [];
//
if (this.order.softwareProjectProductInfoList) {
this.order.softwareProjectProductInfoList.forEach(product => {
if (product.id) {
taxRateList.push({
productId: product.id,
projectId: this.order.projectId,
taxRate: product.taxRate || 13
});
}
});
}
//
if (this.order.hardwareProjectProductInfoList) {
this.order.hardwareProjectProductInfoList.forEach(product => {
if (product.id) {
taxRateList.push({
productId: product.id,
projectId: this.order.projectId,
taxRate: product.taxRate || 13
});
}
});
}
//
if (this.order.maintenanceProjectProductInfoList) {
this.order.maintenanceProjectProductInfoList.forEach(product => {
if (product.id) {
taxRateList.push({
productId: product.id,
projectId: this.order.projectId,
taxRate: product.taxRate || 13
});
}
});
}
return taxRateList;
},
closeDialog(isSuccess = false) {
this.$emit('close', isSuccess);
},
//
getAttachmentType(index) {
const types = ['商务折扣审批', '合同', '补充附件'];
return types[index] || '其他';
},
//
formatDate(date) {
if (!date) return '';
return this.parseTime(date, '{y}-{m}-{d}');
},
//
parseTime(time, pattern) {
if (!time) return '';
const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}';
let date;
if (typeof time === 'object') {
date = time;
} else {
if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
time = parseInt(time);
}
if ((typeof time === 'number') && (time.toString().length === 10)) {
time = time * 1000;
}
date = new Date(time);
}
const formatObj = {
y: date.getFullYear(),
m: date.getMonth() + 1,
d: date.getDate(),
h: date.getHours(),
i: date.getMinutes(),
s: date.getSeconds(),
a: date.getDay()
};
const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => {
const value = formatObj[key];
if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value]; }
return value.toString().padStart(2, '0');
});
return time_str;
},
//
getStatusTagType(status) {
const typeMap = {
'1': 'info', //
'2': 'danger', //
'3': 'success' //
};
return typeMap[String(status)] || 'info';
},
//
getStatusText(status) {
const textMap = {
'1': '提交审批',
'2': '驳回',
'3': '批准'
};
return textMap[String(status)] || '提交审批';
},
//
getReceiverName(log) {
if (log.taskName === '公司领导') {
return log.applyUserName || '';
}
return log.nextAllApproveUserName || '';
},
//
previewFile(file) {
const filePath = file.filePath;
const fileName = file.fileName;
const ext = fileName.substring(fileName.lastIndexOf('.')).toLowerCase();
if (['.png', '.jpg', '.jpeg', '.pdf'].includes(ext)) {
const url = `${process.env.VUE_APP_BASE_API}/project/order/file/view?filePath=${encodeURIComponent(filePath)}&fileName=${encodeURIComponent(fileName)}`;
window.open(url);
} else {
this.downloadFile(file);
}
},
//
downloadFile(file) {
const url = `${process.env.VUE_APP_BASE_API}/project/order/file/download?filePath=${encodeURIComponent(file.filePath)}&fileName=${encodeURIComponent(file.fileName)}`;
window.location.href = url;
}
},
};
</script>
<style scoped>
.approve-container {
position: relative;
}
.section-title {
font-size: 16px;
font-weight: bold;
color: #303133;
margin-bottom: 15px;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
.mb20 {
margin-bottom: 20px;
}
/* Tab样式 */
.approve-tabs {
margin-top: 20px;
}
/* 流转过程容器 */
.process-container {
padding: 0;
}
/* 审批意见弹窗样式 */
::v-deep .el-dialog__body {
padding-top: 10px;
}
</style>

View File

@ -0,0 +1,434 @@
<template>
<div class="config-info-container">
<div v-if="order.softwareProjectProductInfoList && order.softwareProjectProductInfoList.length > 0">
<h3>软件产品</h3>
<table class="product-table">
<thead>
<tr>
<th class="col-seq">序号</th>
<th class="col-code">产品编码</th>
<th class="col-model">产品型号</th>
<th class="col-desc">描述</th>
<th class="col-qty">数量</th>
<th v-if="!hidePrice" class="col-price">(¥)</th>
<th v-if="!hidePrice" class="col-discount"></th>
<th class="col-price">单价(¥)</th>
<th class="col-discount">现金折扣</th>
<th class="col-price">总价(¥)</th>
<th class="col-price">折后总价(¥)</th>
<th class="col-tax">税率(%)</th>
</tr>
</thead>
<tbody>
<tr v-for="(product, index) in order.softwareProjectProductInfoList" :key="'sw-' + product.id">
<td>{{ index + 1 }}</td>
<td>{{ product.productBomCode }}</td>
<td>{{ product.model }}</td>
<td>{{ product.productDesc }}</td>
<td>{{ product.quantity }}</td>
<td v-if="!hidePrice">{{ formatCurrency(product.cataloguePrice) }}</td>
<td v-if="!hidePrice">{{ product.discount ? (product.discount * 100).toFixed(2) + '%' : '-' }}</td>
<td>{{ formatCurrency(product.price) }}</td>
<td>{{ selectedDiscountLabel }}</td>
<td>{{ formatCurrency(product.allPrice) }}</td>
<td>{{ formatCurrency(getDiscountedAllPrice(product, selectedDiscount)) }}</td>
<td>
<el-input
v-if="canEditDiscount"
v-model.number="product.taxRate"
type="number"
size="small"
:min="0"
:max="100"
:step="0.01"
@input="handleTaxRateChange(product)"
/>
<span v-else>{{ product.taxRate || '-' }}</span>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td :colspan="hidePrice ? 8 : 10" style="text-align: right;"><strong>软件折后小计</strong></td>
<td colspan="2"><strong>{{ formatCurrency(softwareDiscountedTotal) }}</strong></td>
</tr>
</tfoot>
</table>
</div>
<div v-if="order.hardwareProjectProductInfoList && order.hardwareProjectProductInfoList.length > 0">
<h3>硬件产品</h3>
<table class="product-table">
<thead>
<tr>
<th class="col-seq">序号</th>
<th class="col-code">产品编码</th>
<th class="col-model">产品型号</th>
<th class="col-desc">描述</th>
<th class="col-qty">数量</th>
<th v-if="!hidePrice" class="col-price">(¥)</th>
<th v-if="!hidePrice" class="col-discount"></th>
<th class="col-price">单价(¥)</th>
<th class="col-discount">现金折扣</th>
<th class="col-price">总价(¥)</th>
<th class="col-price">折后总价(¥)</th>
<th class="col-tax">税率(%)</th>
</tr>
</thead>
<tbody>
<tr v-for="(product, index) in order.hardwareProjectProductInfoList" :key="'hw-' + product.id">
<td>{{ index + 1 }}</td>
<td>{{ product.productBomCode }}</td>
<td>{{ product.model }}</td>
<td>{{ product.productDesc }}</td>
<td>{{ product.quantity }}</td>
<td v-if="!hidePrice">{{ formatCurrency(product.cataloguePrice) }}</td>
<td v-if="!hidePrice">{{ product.discount ? (product.discount * 100).toFixed(2) + '%' : '-' }}</td>
<td>{{ formatCurrency(product.price) }}</td>
<td>{{ selectedDiscountLabel }}</td>
<td>{{ formatCurrency(product.allPrice) }}</td>
<td>{{ formatCurrency(getDiscountedAllPrice(product, selectedDiscount)) }}</td>
<td>
<el-input
v-if="canEditDiscount"
v-model.number="product.taxRate"
type="number"
size="small"
:min="0"
:max="100"
:step="0.01"
@input="handleTaxRateChange(product)"
/>
<span v-else>{{ product.taxRate || '-' }}</span>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td :colspan="hidePrice ? 8 : 10" style="text-align: right;"><strong>硬件折后小计</strong></td>
<td colspan="2"><strong>{{ formatCurrency(hardwareDiscountedTotal) }}</strong></td>
</tr>
</tfoot>
</table>
</div>
<div v-if="order.maintenanceProjectProductInfoList && order.maintenanceProjectProductInfoList.length > 0">
<h3>维保产品</h3>
<table class="product-table">
<thead>
<tr>
<th class="col-seq">序号</th>
<th class="col-code">产品编码</th>
<th class="col-model">产品型号</th>
<th class="col-desc">描述</th>
<th class="col-qty">数量</th>
<th v-if="!hidePrice" class="col-price">(¥)</th>
<th v-if="!hidePrice" class="col-discount"></th>
<th class="col-price">单价(¥)</th>
<th class="col-discount">现金折扣</th>
<th class="col-price">总价(¥)</th>
<th class="col-price">折后总价(¥)</th>
<th class="col-tax">税率(%)</th>
</tr>
</thead>
<tbody>
<tr v-for="(product, index) in order.maintenanceProjectProductInfoList" :key="'m-' + product.id">
<td>{{ index + 1 }}</td>
<td>{{ product.productBomCode }}</td>
<td>{{ product.model }}</td>
<td>{{ product.productDesc }}</td>
<td>{{ product.quantity }}</td>
<td v-if="!hidePrice">{{ formatCurrency(product.cataloguePrice) }}</td>
<td v-if="!hidePrice">{{ product.discount ? (product.discount * 100).toFixed(2) + '%' : '-' }}</td>
<td>{{ formatCurrency(product.price) }}</td>
<td>{{ selectedDiscountLabel }}</td>
<td>{{ formatCurrency(product.allPrice) }}</td>
<td>{{ formatCurrency(getDiscountedAllPrice(product, selectedDiscount)) }}</td>
<td>
<el-input
v-if="canEditDiscount"
v-model.number="product.taxRate"
type="number"
size="small"
:min="0"
:max="100"
:step="0.01"
@input="handleTaxRateChange(product)"
/>
<span v-else>{{ product.taxRate || '-' }}</span>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td :colspan="hidePrice ? 8 : 10" style="text-align: right;"><strong>服务折后小计</strong></td>
<td colspan="2"><strong>{{ formatCurrency(maintenanceDiscountedTotal) }}</strong></td>
</tr>
</tfoot>
</table>
</div>
<div class="summary-section">
<el-row type="flex" justify="end" align="middle" class="summary-row">
<el-col :span="6" type="flex" justify="end">
<span class="summary-label">总价合计</span> <span class="summary-value-right"> {{
formatCurrency(grandTotal)
}}</span>
</el-col>
</el-row>
<el-row type="flex" justify="end" align="middle" class="summary-row">
<el-col :span="18" style="text-align: center;">
<span style="margin-right: 5px;">现金折扣</span>
<el-select
v-model="selectedDiscount"
placeholder="请选择折扣率"
size="small"
style="width: 120px;"
:disabled="!canEditDiscount"
@change="handleDiscountChange">
<el-option
v-for="item in discountOptions"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-col>
<el-col :span="6">
<span class="summary-label"> 折后总价合计</span> <span
class="summary-value-right"> {{ formatCurrency(finalTotal) }}</span>
</el-col>
</el-row>
</div>
</div>
</template>
<script>
export default {
name: "ConfigInfo",
props: {
orderData: {
type: Object,
required: true,
default: () => ({
softwareProjectProductInfoList: [],
hardwareProjectProductInfoList: [],
maintenanceProjectProductInfoList: []
})
},
canEditDiscount: {
type: Boolean,
default: false
},
hidePrice: {
type: Boolean,
default: false
}
},
data() {
return {
selectedDiscount: 1,
discountOptions: [
{value: 1, label: '100%'},
{value: 0.988, label: '98.8%'},
{value: 0.985, label: '98.5%'}
]
};
},
watch: {
'orderData.discountFold': {
immediate: true,
handler(newVal) {
// order.discountFold1
this.selectedDiscount = newVal || 1;
}
}
},
computed: {
order() {
return this.orderData || {
softwareProjectProductInfoList: [],
hardwareProjectProductInfoList: [],
maintenanceProjectProductInfoList: []
};
},
selectedDiscountLabel() {
const option = this.discountOptions.find(opt => opt.value === this.selectedDiscount);
return option ? option.label : '';
},
softwareTotal() {
return this.calculateTotal(this.order.softwareProjectProductInfoList, 1);
},
hardwareTotal() {
return this.calculateTotal(this.order.hardwareProjectProductInfoList, 1);
},
maintenanceTotal() {
return this.calculateTotal(this.order.maintenanceProjectProductInfoList, 1);
},
softwareDiscountedTotal() {
return this.calculateTotal(this.order.softwareProjectProductInfoList, this.selectedDiscount);
},
hardwareDiscountedTotal() {
return this.calculateTotal(this.order.hardwareProjectProductInfoList, this.selectedDiscount);
},
maintenanceDiscountedTotal() {
return this.calculateTotal(this.order.maintenanceProjectProductInfoList, this.selectedDiscount);
},
grandTotal() {
return this.softwareTotal + this.hardwareTotal + this.maintenanceTotal;
},
finalTotal() {
return this.softwareDiscountedTotal + this.hardwareDiscountedTotal + this.maintenanceDiscountedTotal;
}
},
methods: {
handleDiscountChange(value) {
this.$emit('discount-change', value);
},
handleTaxRateChange(product) {
//
if (product.taxRate !== null && product.taxRate !== undefined) {
// 2
product.taxRate = Math.round(product.taxRate * 100) / 100;
}
//
this.$emit('tax-rate-change', product);
},
getDiscountedAllPrice(product, discount) {
if (discount === 1) {
return product.allPrice;
}
const discountedUnitPrice = product.price * discount;
const roundedDiscountedUnitPrice = Math.round(discountedUnitPrice * 100) / 100;
return roundedDiscountedUnitPrice * product.quantity;
},
calculateTotal(productList, discount) {
if (!productList) return 0;
return productList.reduce((sum, product) => sum + (this.getDiscountedAllPrice(product, discount) || 0), 0);
},
formatCurrency(value) {
if (value == null) return '0.00';
return Number(value).toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2});
}
}
};
</script>
<style scoped>
.config-info-container {
font-family: 'Arial', sans-serif;
padding: 20px;
}
h3 {
color: #333;
border-bottom: 2px solid #eee;
padding-bottom: 10px;
margin-top: 20px;
}
.product-table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
table-layout: fixed;
}
.product-table th, .product-table td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
word-wrap: break-word;
}
/* Column widths for alignment */
.col-seq {
width: 4%;
}
.col-code {
width: 10%;
}
.col-model {
width: 12%;
}
.col-desc {
width: 14%;
}
.col-qty {
width: 5%;
}
.col-price {
width: 9%;
}
.col-discount {
width: 7%;
}
.col-tax {
width: 7%;
min-width: 70px;
}
/* 税率输入框样式 */
.col-tax >>> .el-input {
width: 100%;
}
.col-tax >>> .el-input__inner {
text-align: center;
padding-left: 3px;
padding-right: 3px;
}
.product-table th {
background-color: #f5f5f5;
font-weight: bold;
}
.product-table tbody tr:nth-child(odd) {
background-color: #f9f9f9;
}
.product-table tfoot {
font-weight: bold;
background-color: #f0f0f0;
}
.summary-section {
margin-top: 30px;
padding: 20px;
border-top: 2px solid #ccc;
text-align: center;
}
.summary-row {
margin-bottom: 15px;
font-size: 1.1em;
}
.summary-label {
font-weight: bold;
text-align: right;
padding-right: 15px;
}
.summary-value {
font-weight: bold;
color: #d9534f;
text-align: left;
}
.summary-value-right {
font-weight: bold;
color: #d9534f;
text-align: right; /* Ensure right alignment */
}
</style>

View File

@ -0,0 +1,219 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="合同编号" prop="orderCode">
<el-input
v-model="queryParams.orderCode"
placeholder="请输入合同编号"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="项目名称" prop="projectName">
<el-input
v-model="queryParams.projectName"
placeholder="请输入项目名称"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="项目编号" prop="projectCode">
<el-input
v-model="queryParams.projectCode"
placeholder="请输入项目编号"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="客户名称" prop="customerName">
<el-input
v-model="queryParams.customerName"
placeholder="请输入客户名称"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="汇智负责人" prop="dutyName">
<el-input
v-model="queryParams.dutyName"
placeholder="请输入汇智负责人"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="审批节点" prop="approveNode">
<el-input
v-model="queryParams.approveNode"
placeholder="请输入审批节点"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery"></el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery"></el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="orderList">
<el-table-column label="合同编号" align="center" prop="orderCode" />
<el-table-column label="项目名称" align="center" prop="projectName" />
<el-table-column label="项目编号" align="center" prop="projectCode" />
<el-table-column label="客户名称" align="center" prop="customerName" />
<el-table-column label="订单金额" align="center" prop="actualPurchaseAmount" />
<el-table-column label="汇智负责人" align="center" prop="dutyName" />
<el-table-column label="审批节点" align="center" prop="approveNode" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleApprove(scope.row)"
>审批</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 审批弹窗 -->
<el-dialog
title="订单审批"
:visible.sync="approveDialogVisible"
custom-class="approve-dialog"
width="80%"
top="5vh"
append-to-body
destroy-on-close
>
<approve-dialog
v-if="approveDialogVisible"
ref="approveDialog"
:order-id="currentOrderId"
@close="closeApproveDialog"
/>
<span slot="footer" class="dialog-footer">
<el-button @click="approveDialogVisible = false"> </el-button>
<el-button type="danger" @click="handleReject"> </el-button>
<el-button type="primary" @click="handleApproveConfirm"> </el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { listOrder } from "@/api/approve/order";
import ApproveDialog from './Approve.vue';
export default {
name: "Order",
components: {
ApproveDialog
},
data() {
return {
// ...data...
approveDialogVisible: false,
currentOrderId: null,
//
loading: true,
//
showSearch: true,
//
total: 0,
//
orderList: [],
//
queryParams: {
pageNum: 1,
pageSize: 10,
orderCode: null,
projectName: null,
projectCode: null,
customerName: null,
dutyName: null,
approveNode: null,
},
};
},
created() {
this.getList();
},
methods: {
/** 查询订单列表 */
getList() {
this.loading = true;
listOrder(this.queryParams).then(response => {
this.orderList = response.rows;
this.total = response.total;
this.loading = false;
});
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm");
this.handleQuery();
},
//
resetForm(formName) {
if (this.$refs[formName]) {
this.$refs[formName].resetFields();
}
},
/** 审批按钮操作 */
handleApprove(row) {
this.currentOrderId = row.id;
this.approveDialogVisible = true;
},
/** 关闭审批弹窗 */
closeApproveDialog() {
this.approveDialogVisible = false;
this.currentOrderId = null;
this.getList(); //
},
/** 同意按钮操作 */
handleApproveConfirm() {
if (this.$refs.approveDialog) {
this.$refs.approveDialog.handleApprove();
}
},
/** 驳回按钮操作 */
handleReject() {
if (this.$refs.approveDialog) {
this.$refs.approveDialog.handleReject();
}
}
}
};
</script>
<style scoped>
/* 审批弹窗样式 */
::v-deep .approve-dialog {
display: flex;
flex-direction: column;
max-height: 90vh;
}
::v-deep .approve-dialog .el-dialog__body {
max-height: calc(90vh - 120px);
overflow-y: auto;
overflow-x: hidden;
}
</style>

View File

@ -56,7 +56,7 @@
</el-form>
<!-- 底部 -->
<div class="el-login-footer">
<span>Copyright © 2018-2025 ruoyi.vip All Rights Reserved.</span>
<span>Copyright © 2018-2025 unissense All Rights Reserved.</span>
</div>
</div>
</template>

View File

@ -134,7 +134,7 @@
<div v-else></div>
</div>
</div>
<input id="uploadInput" type="file" accept=".pdf,.jpg,.png" style="display: none" @change="handleUploadFile"/>
<input id="uploadInput0" type="file" accept=".pdf,.jpg,.png" style="display: none" @change="handleUploadFile"/>
<input id="uploadInput1" type="file" accept=".pdf,.jpg,.png" style="display: none" @change="handleUploadFile"/>
<input id="uploadInput2" type="file" accept=".zip,.rar,.jpg,.png" style="display: none" @change="handleUploadFile"/>
<input id="fileSort" type="hidden" v-model="fileSort"/>
@ -443,12 +443,12 @@ export default {
},
saveDraft() {
if (!this.form.projectCode) {
this.msgError("项目编号为必填");
this.$modal.msgError("项目编号为必填");
return;
}
const checkDiscount = (list) => !list || list.every(item => item.discount === null || item.discount === undefined || item.discount <= 1);
if (!checkDiscount(this.form.softwareProjectProductInfoList) || !checkDiscount(this.form.hardwareProjectProductInfoList) || !checkDiscount(this.form.maintenanceProjectProductInfoList)) {
this.msgError("折扣不能大于100%");
this.$modal.msgError("折扣不能大于100%");
return;
}
this.form.orderStatus = '0';
@ -457,12 +457,12 @@ export default {
submitForApproval() {
const hasBusinessApprovalFile = this.currentContractFiles && this.currentContractFiles.length > 0 && this.currentContractFiles[0].id !== -1;
if (!hasBusinessApprovalFile) {
this.msgError("请补充商务审批文件");
this.$modal.msgError("请补充商务审批文件");
return;
}
this.$refs["form"].validate(valid => {
if (valid) this.selectCommitTypeVisible = true;
else this.msgError("请完善表单");
else this.$modal.msgError("请完善表单");
});
},
handleCommitTypeSelected(data) {

View File

@ -61,7 +61,7 @@
</el-form>
<!-- 底部 -->
<div class="el-register-footer">
<span>Copyright © 2018-2025 ruoyi.vip All Rights Reserved.</span>
<span>Copyright © 2018-2025 unissense All Rights Reserved.</span>
</div>
</div>
</template>

View File

@ -7,7 +7,7 @@ function resolve(dir) {
const CompressionPlugin = require('compression-webpack-plugin')
const name = process.env.VUE_APP_TITLE || '若依管理系统' // 网页标题
const name = process.env.VUE_APP_TITLE || 'UNISSENSE-OMS' // 网页标题
const baseUrl = 'http://localhost:28080' // 后端接口

View File

@ -58,7 +58,7 @@ spring:
time-zone: GMT+8
date-format: yyyy-MM-dd HH:mm:ss
profiles:
active: prod
active: dev
# 文件上传
servlet:
multipart:

View File

@ -65,7 +65,7 @@
</div>
<div class="signup-footer">
<div class="pull-left">
Copyright © 2018-2025 ruoyi.vip All Rights Reserved. <br>
Copyright © 2018-2025 unissense All Rights Reserved. <br>
</div>
</div>
</div>

View File

@ -45,9 +45,18 @@ public class VueProjectOrderInfoController extends BaseController {
@RequiresPermissions(value = {"project:order:list", "project:order:approve"}, logical = Logical.OR)
@GetMapping("/list")
public TableDataInfo list(ProjectOrderInfo projectOrderInfo) {
if (StringUtils.isNotEmpty(projectOrderInfo.getApprove())) {
projectOrderInfo.setApprove(ShiroUtils.getUserId().toString());
}
startPage();
List<ProjectOrderInfo> list = projectOrderInfoService.selectProjectOrderInfoList(projectOrderInfo);
return getDataTable(list);
}
/**
*
*/
@RequiresPermissions(value = {"project:order:list", "project:order:approve"}, logical = Logical.OR)
@GetMapping("/approve/list")
public TableDataInfo listApprove(ProjectOrderInfo projectOrderInfo) {
projectOrderInfo.setApprove(ShiroUtils.getUserId().toString());
startPage();
List<ProjectOrderInfo> list = projectOrderInfoService.selectProjectOrderInfoList(projectOrderInfo);
return getDataTable(list);
@ -56,7 +65,7 @@ public class VueProjectOrderInfoController extends BaseController {
/**
*
*/
@RequiresPermissions("project:order:query")
@RequiresPermissions("project:order:list")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") Long id) {
Map<String,Object> mmap=new HashMap<>();
@ -84,7 +93,7 @@ public class VueProjectOrderInfoController extends BaseController {
mmap.put("updateFile", (ShiroUtils.getSubject().hasRole("sale_assistant")||ShiroUtils.getSubject().hasRole("business") ||ShiroUtils.getSysUser().isAdmin()) && updateFlag);
mmap.put("uploadFinalFile", (ShiroUtils.getSubject().hasRole("business") || ShiroUtils.getSysUser().isAdmin()) &&
ProjectOrderInfo.OrderStatus.APPROVE_COMPLETE.getCode().equals(projectOrderInfo.getOrderStatus()));
mmap.put("todo", todoService.selectTodo(todo));
return AjaxResult.success(mmap);