Files
im-uniapp/pages/workbench/profit/cost.vue
砂糖 307b46b213 feat: 新增客户管理、项目进度和财务中心功能模块
新增客户管理、项目进度和财务中心相关页面及API接口
添加项目明细页面和启动图资源
重构请求基础URL和更新逻辑
引入uni-badge和uni-list组件
优化工作台首页功能入口布局
更新版本号至5.0.0并修改启动图配置
2025-11-06 16:56:35 +08:00

849 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="container">
<!-- 项目基本信息展示 -->
<view class="project-header" v-if="this.projectId != 0">
<view class="project-info-item">
<text class="info-label">项目名称</text>
<text class="info-value">{{ currentProject.projectName || '未知项目' }}</text>
</view>
<view class="project-info-item">
<text class="info-label">项目编号</text>
<text class="info-value">{{ currentProject.projectNo || '无编号' }}</text>
</view>
<view class="project-info-item">
<text class="info-label">项目总金额</text>
<text class="info-value">¥ {{ formatCurrency(currentProject.funds || 0) }}</text>
</view>
</view>
<view class="project-header" v-if="this.projectId == 0">
<view class="project-info-item">
<text class="info-value">其他收支</text>
</view>
</view>
<!-- 新增按钮 -->
<view class="btn-add-container">
<uni-button type="primary" @click="openAddPopup">
<uni-icons type="plus" size="18" class="btn-icon"></uni-icons>
新增财务记录
</uni-button>
</view>
<!-- 财务明细列表 -->
<view class="finance-list-container">
<view class="list-title">财务明细记录</view>
<uni-list v-if="finances.length > 0" border>
<uni-list-item
v-for="(item, index) in finances"
:key="item.financeId"
:title="item.financeTitle"
:note="`${item.financeType === '1' ? '出账' : '入账'} · ${formatDate(item.financeTime)} · 共${item.detailList.length || 0}条明细`"
showArrow
:clickable="true"
@click="openDetailPopup(item)"
>
<template v-slot:extra>
<view class="amount-tag" :class="item.financeType === '1' ? 'expense' : 'income'">
{{ item.financeType === '1' ? '-' : '+' }}¥{{ calculateFinanceTotal(item.detailList) }}
</view>
</template>
</uni-list-item>
</uni-list>
<view v-else class="empty-list">
<uni-icons type="empty" size="60" color="#ccc"></uni-icons>
<text class="empty-text">暂无财务记录</text>
</view>
</view>
<!-- 新增财务记录弹窗 -->
<uni-popup ref="addPopup" type="bottom" :style="{ width: '100%' }">
<view class="popup-content">
<view class="popup-title">{{ form.financeId ? '编辑财务记录' : '新增财务记录' }}</view>
<uni-forms ref="financeForm" :model="form" :rules="rules" label-width="150rpx">
<uni-forms-item label="财务类型" name="financeType">
<uni-data-checkbox mode="tag" v-model="form.financeType" :localdata="[
{ value: "1", text: '出账' },
{ value: '2', text: '入账' }
]"></uni-data-checkbox>
</uni-forms-item>
<uni-forms-item label="记录名称" name="financeTitle">
<uni-easyinput
v-model="form.financeTitle"
placeholder="请输入记录名称"
clearable
></uni-easyinput>
</uni-forms-item>
<uni-forms-item label="交易时间" name="financeTime">
<uni-datetime-picker
v-model="form.financeTime"
type="datetime"
placeholder="请选择交易时间"
:start="minDate"
></uni-datetime-picker>
</uni-forms-item>
<uni-forms-item label="交易方" name="financeParties">
<uni-easyinput
v-model="form.financeParties"
placeholder="请输入交易方"
clearable
></uni-easyinput>
</uni-forms-item>
<uni-forms-item label="支付类型" name="payType">
<oa-dict-select v-model="form.payType" dict-type='sys_pay_type'></oa-dict-select>
</uni-forms-item>
<!-- 明细列表区域替换uni-table -->
<view class="detail-section">
<view class="detail-header">
<text class="detail-title">明细列表</text>
<uni-button type="primary" size="mini" @click="addDetail">
<uni-icons type="plus" size="16"></uni-icons>添加明细
</uni-button>
</view>
<!-- 明细标题行 -->
<view class="detail-row header-row">
<view class="detail-col index-col">序号</view>
<view class="detail-col name-col">明细名称</view>
<view class="detail-col price-col">金额()</view>
<view class="detail-col action-col">操作</view>
</view>
<!-- 明细内容行 -->
<view
class="detail-row content-row"
v-for="(item, index) in form.detailList"
:key="item.detailId"
>
<view class="detail-col index-col">{{ index + 1 }}</view>
<view class="detail-col name-col">
<uni-easyinput
v-model="item.detailTitle"
placeholder="请输入明细名称"
class="detail-input"
></uni-easyinput>
</view>
<view class="detail-col price-col">
<uni-easyinput
v-model="item.price"
placeholder="0.00"
type="number"
class="detail-input"
@input="calculateTotal"
></uni-easyinput>
</view>
<view class="detail-col action-col">
<uni-button
type="warn"
size="mini"
plain
@click="removeDetail(index)"
v-if="form.detailList.length > 1"
>
<uni-icons type="trash" size="16"></uni-icons>
</uni-button>
</view>
</view>
<!-- 合计金额 -->
<view class="total-amount" v-if="form.detailList.length > 0">
<text class="total-label">合计金额</text>
<text class="total-value" :class="form.financeType === '1' ? 'text-expense' : 'text-income'">
¥{{ formatCurrency(totalAmount) }}
</text>
</view>
</view>
<uni-forms-item label="备注" name="remark">
<uni-easyinput
v-model="form.remark"
placeholder="请输入备注信息"
type="textarea"
rows="3"
></uni-easyinput>
</uni-forms-item>
</uni-forms>
<view class="popup-btn-group">
<uni-button type="default" @click="closeAddPopup">取消</uni-button>
<uni-button type="primary" @click="submitFinanceForm">确认提交</uni-button>
</view>
</view>
</uni-popup>
<uni-popup ref="detailPopup" type="bottom" :style="{ width: '100%' }">
<view class="popup-content">
<view class="popup-title">财务记录详情</view>
<view class="detail-info">
<view class="detail-item">
<text class="detail-label">记录名称</text>
<text class="detail-value">{{ currentFinance.financeTitle || '-' }}</text>
</view>
<view class="detail-item">
<text class="detail-label">记录类型</text>
<text class="detail-value">{{ currentFinance.financeType === '1' ? '出账' : '入账' }}</text>
</view>
<view class="detail-item">
<text class="detail-label">交易时间</text>
<text class="detail-value">{{ formatDate(currentFinance.financeTime) || '-' }}</text>
</view>
<view class="detail-item">
<text class="detail-label">交易方</text>
<text class="detail-value">{{ currentFinance.financeParties || '-' }}</text>
</view>
<view class="detail-item">
<text class="detail-label">支付类型</text>
<text class="detail-value">{{ getPayTypeName(currentFinance.payType) || '-' }}</text>
</view>
<view class="detail-item">
<text class="detail-label">总金额</text>
<text class="detail-value" :class="currentFinance.financeType === '1' ? 'text-expense' : 'text-income'">
{{ currentFinance.financeType === '1' ? '-' : '+' }}¥{{ calculateFinanceTotal(currentFinance.detailList) }}
</text>
</view>
<!-- 明细列表展示替换uni-table -->
<view class="detail-section-view">
<view class="detail-header-view">
<text class="detail-title-view">明细列表 ({{ currentFinance.detailList.length || 0 }})</text>
</view>
<!-- 明细标题行 -->
<view class="detail-row header-row">
<view class="detail-col index-col">序号</view>
<view class="detail-col name-col">明细名称</view>
<view class="detail-col price-col">金额()</view>
</view>
<!-- 明细内容行 -->
<view
class="detail-row content-row"
v-for="(item, index) in currentFinance.detailList"
:key="index"
>
<view class="detail-col index-col">{{ index + 1 }}</view>
<view class="detail-col name-col">{{ item.detailTitle || '-' }}</view>
<view class="detail-col price-col">¥{{ formatCurrency(item.price || 0) }}</view>
</view>
<view v-if="!currentFinance.detailList || currentFinance.detailList.length === 0" class="empty-detail">
<text>无明细记录</text>
</view>
</view>
<view class="detail-item">
<text class="detail-label">备注</text>
<text class="detail-value">{{ currentFinance.remark || '无' }}</text>
</view>
<view class="detail-item">
<text class="detail-label">创建人</text>
<text class="detail-value">{{ currentFinance.createBy || '-' }}</text>
</view>
<view class="detail-item">
<text class="detail-label">创建时间</text>
<text class="detail-value">{{ formatDate(currentFinance.createTime) || '-' }}</text>
</view>
</view>
<view class="popup-btn-group">
<uni-button type="default" @click="closeDetailPopup">关闭</uni-button>
<uni-button type="primary" @click="handleEdit">编辑</uni-button>
<uni-button type="warn" @click="handleDelete">删除</uni-button>
</view>
</view>
</uni-popup>
</view>
</template>
<script>
import { getProject } from '@/api/oa/project';
import {
addFinance,
delFinance,
listFinance,
getFinance,
updateFinance
} from "@/api/oa/finance/finance.js";
export default {
data() {
return {
projectId: '',
currentProject: {},
finances: [],
currentFinance: {
detailList: []
},
loading: false,
// 表单数据 - 默认包含一条空明细
form: {
financeId: '',
projectId: '',
financeType: '1', // 1-出账 2-入账
financeTitle: '',
financeTime: '',
financeParties: '',
payType: '',
remark: '',
detailList: [] // 明细列表将在打开弹窗时初始化
},
// 合计金额(前端计算)
totalAmount: 0,
// 表单验证规则
rules: {
financeType: [{ required: true, message: '请选择记录类型', trigger: 'change' }],
financeTitle: [{ required: true, message: '请输入记录名称', trigger: 'blur' }],
financeTime: [{ required: true, message: '请选择交易时间', trigger: 'change' }],
financeParties: [{ required: true, message: '请输入交易方', trigger: 'blur' }],
payType: [{ required: true, message: '请选择支付类型', trigger: 'change' }]
},
// 支付类型选项
payTypes: [
{ value: '1', label: '微信支付' },
{ value: '2', label: '支付宝' },
{ value: '3', label: '银行转账' },
{ value: '4', label: '现金' },
{ value: '5', label: '其他' }
],
// 最小日期限制
minDate: ''
};
},
onLoad(options) {
// 获取项目ID
this.projectId = options.id;
if (!this.projectId && this.projectId !== '0') {
uni.showToast({ title: '项目ID不存在', icon: 'none' });
return;
}
// 初始化最小日期3年前
const now = new Date();
this.minDate = new Date(now.getFullYear() - 3, now.getMonth(), now.getDate()).getTime();
// 加载项目信息和财务记录
this.loadProjectInfo();
this.loadFinanceList();
},
methods: {
// 加载项目信息
loadProjectInfo() {
this.loading = true;
getProject(this.projectId).then(res => {
if (res.code === 200) {
this.currentProject = res.data;
} else {
uni.showToast({ title: res.msg || '获取项目信息失败', icon: 'none' });
}
}).catch(err => {
console.error('获取项目信息错误:', err);
uni.showToast({ title: '获取项目信息失败', icon: 'none' });
}).finally(() => {
this.loading = false;
});
},
// 加载财务记录列表
loadFinanceList() {
this.loading = true;
listFinance({
projectId: this.projectId,
pageNum: 1,
pageSize: 100
}).then(res => {
if (res.code === 200) {
this.finances = res.rows || [];
} else {
uni.showToast({ title: res.msg || '获取财务记录失败', icon: 'none' });
}
}).catch(err => {
console.error('获取财务记录错误:', err);
uni.showToast({ title: '获取财务记录失败', icon: 'none' });
}).finally(() => {
this.loading = false;
});
},
// 添加明细
addDetail() {
this.form.detailList.push({
detailId: Date.now(), // 临时ID
detailTitle: '',
price: ''
});
},
// 移除明细
removeDetail(index) {
// 确保至少保留一条明细
if (this.form.detailList.length <= 1) {
uni.showToast({ title: '至少保留一条明细', icon: 'none' });
return;
}
uni.showModal({
title: '确认删除',
content: '确定要删除这条明细吗?',
success: (res) => {
if (res.confirm) {
this.form.detailList.splice(index, 1);
this.calculateTotal();
}
}
});
},
// 计算表单总金额
calculateTotal() {
this.totalAmount = this.form.detailList.reduce((sum, item) => {
const price = Number(item.price) || 0;
return sum + price;
}, 0);
},
// 计算财务记录总金额(通用方法)
calculateFinanceTotal(detailList) {
if (!detailList || !detailList.length) return '0.00';
const total = detailList.reduce((sum, item) => {
const price = Number(item.price) || 0;
return sum + price;
}, 0);
return this.formatCurrency(total);
},
// 打开新增弹窗 - 默认添加一条空明细
openAddPopup() {
// 重置表单并默认添加一条空明细
this.form = {
financeId: '',
projectId: this.projectId,
financeType: '1',
financeTitle: '',
financeTime: new Date().toISOString().slice(0, 16),
financeParties: '',
payType: '',
remark: '',
detailList: [
{
detailId: Date.now(),
detailTitle: '',
price: ''
}
]
};
this.totalAmount = 0;
this.$refs.addPopup.open();
},
// 关闭新增弹窗
closeAddPopup() {
this.$refs.addPopup.close();
},
// 打开详情弹窗
openDetailPopup(item) {
this.currentFinance = { ...item };
this.$refs.detailPopup.open();
},
// 关闭详情弹窗
closeDetailPopup() {
this.$refs.detailPopup.close();
},
// 提交表单
submitFinanceForm() {
// 验证每条明细
const validDetail = this.form.detailList.every(item => {
return item.detailTitle && item.price && !isNaN(Number(item.price));
});
if (!validDetail) {
uni.showToast({ title: '请完善所有明细的名称和金额', icon: 'none' });
return;
}
this.$refs.financeForm.validate().then(() => {
this.loading = true;
const params = { ...this.form };
const submitApi = params.financeId ? updateFinance : addFinance;
submitApi(params).then(res => {
if (res.code === 200) {
uni.showToast({ title: params.financeId ? '修改成功' : '新增成功', icon: 'success' });
this.closeAddPopup();
this.loadFinanceList();
} else {
uni.showToast({ title: res.msg || '操作失败', icon: 'none' });
}
}).catch(err => {
console.error('提交财务记录错误:', err);
uni.showToast({ title: '操作失败', icon: 'none' });
}).finally(() => {
this.loading = false;
});
}).catch(err => {
console.log('表单验证失败:', err);
});
},
// 编辑记录
handleEdit() {
this.closeDetailPopup();
// 确保编辑时有至少一条明细
const detailList = this.currentFinance.detailList && this.currentFinance.detailList.length
? [...this.currentFinance.detailList]
: [{ detailId: Date.now(), detailTitle: '', price: '' }];
this.form = {
financeId: this.currentFinance.financeId,
projectId: this.projectId,
financeType: this.currentFinance.financeType,
financeTitle: this.currentFinance.financeTitle,
financeTime: this.currentFinance.financeTime ? this.formatDateTimeForInput(this.currentFinance.financeTime) : '',
financeParties: this.currentFinance.financeParties,
payType: this.currentFinance.payType,
remark: this.currentFinance.remark,
detailList: detailList
};
this.calculateTotal();
this.$refs.addPopup.open();
},
// 删除记录
handleDelete() {
uni.showModal({
title: '确认删除',
content: '确定要删除这条财务记录吗?',
success: (res) => {
if (res.confirm) {
this.loading = true;
delFinance(this.currentFinance.financeId).then(res => {
if (res.code === 200) {
uni.showToast({ title: '删除成功', icon: 'success' });
this.closeDetailPopup();
this.loadFinanceList();
} else {
uni.showToast({ title: res.msg || '删除失败', icon: 'none' });
}
}).catch(err => {
console.error('删除财务记录错误:', err);
uni.showToast({ title: '删除失败', icon: 'none' });
}).finally(() => {
this.loading = false;
});
}
}
});
},
// 格式化金额
formatCurrency(value) {
return parseFloat(value || 0).toFixed(2);
},
// 格式化日期
formatDate(dateStr) {
if (!dateStr) return '';
const date = new Date(dateStr);
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
},
// 格式化日期时间(用于输入框)
formatDateTimeForInput(dateStr) {
if (!dateStr) return '';
const date = new Date(dateStr);
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
},
// 获取支付类型名称
getPayTypeName(value) {
const type = this.payTypes.find(item => item.value === value);
return type ? type.label : '';
}
}
};
</script>
<style scoped>
.container {
padding: 16rpx;
background-color: #f5f5f5;
min-height: 100vh;
}
.project-header {
background-color: #fff;
border-radius: 8rpx;
padding: 20rpx;
margin-bottom: 20rpx;
}
.project-info-item {
display: flex;
padding: 12rpx 0;
border-bottom: 1px solid #f5f5f5;
}
.project-info-item:last-child {
border-bottom: none;
}
.info-label {
color: #666;
width: 200rpx;
}
.info-value {
color: #333;
flex: 1;
}
.btn-add-container {
margin-bottom: 20rpx;
}
.btn-icon {
margin-right: 8rpx;
}
.finance-list-container {
background-color: #fff;
border-radius: 8rpx;
padding: 20rpx;
}
.list-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
padding-bottom: 10rpx;
border-bottom: 1px solid #f5f5f5;
}
.empty-list {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx 0;
}
.empty-text {
color: #999;
font-size: 28rpx;
margin-top: 20rpx;
}
.amount-tag {
padding: 4rpx 12rpx;
border-radius: 16rpx;
font-size: 24rpx;
}
.amount-tag.income {
background-color: #e8f4e9;
color: #2e7d32;
}
.amount-tag.expense {
background-color: #ffebee;
color: #c62828;
}
.popup-content {
background-color: #fff;
border-radius: 16rpx 16rpx 0 0;
padding: 30rpx 20rpx;
max-height: 90vh;
overflow-y: auto;
}
.popup-title {
font-size: 36rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
padding-bottom: 20rpx;
border-bottom: 1px solid #f5f5f5;
}
.popup-btn-group {
display: flex;
justify-content: flex-end;
gap: 16rpx;
margin-top: 30rpx;
padding-top: 20rpx;
border-top: 1px solid #f5f5f5;
}
.detail-info {
margin-top: 10rpx;
}
.detail-item {
display: flex;
padding: 16rpx 0;
border-bottom: 1px solid #f5f5f5;
}
.detail-item:last-child {
border-bottom: none;
}
.detail-label {
color: #666;
width: 180rpx;
}
.detail-value {
color: #333;
flex: 1;
word-break: break-all;
}
.text-income {
color: #2e7d32;
}
.text-expense {
color: #c62828;
}
/* 明细区域样式替换table */
.detail-section {
margin: 30rpx 0;
padding: 20rpx;
background-color: #f9f9f9;
border-radius: 8rpx;
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.detail-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
}
/* 明细行样式 */
.detail-row {
display: flex;
width: 100%;
padding: 12rpx 0;
}
.header-row {
background-color: #f0f0f0;
font-weight: bold;
border-radius: 4rpx 4rpx 0 0;
}
.content-row {
border-bottom: 1px solid #f0f0f0;
align-items: center;
}
.content-row:last-child {
border-bottom: none;
}
/* 明细列样式 */
.detail-col {
display: flex;
justify-content: center;
align-items: center;
}
.index-col {
width: 10%;
font-size: 26rpx;
}
.name-col {
width: 40%;
padding: 0 10rpx;
}
.price-col {
width: 30%;
padding: 0 10rpx;
}
.action-col {
width: 20%;
}
/* 明细输入框样式 */
.detail-input {
width: 100%;
}
.empty-detail {
text-align: center;
color: #999;
padding: 40rpx 0;
font-size: 26rpx;
}
.total-amount {
display: flex;
justify-content: flex-end;
align-items: center;
margin-top: 20rpx;
padding-top: 10rpx;
border-top: 1px dashed #ddd;
}
.total-label {
font-size: 28rpx;
color: #666;
margin-right: 10rpx;
}
.total-value {
font-size: 28rpx;
font-weight: bold;
}
/* 详情弹窗中的明细样式 */
.detail-section-view {
margin: 20rpx 0;
padding: 10rpx;
background-color: #f9f9f9;
border-radius: 8rpx;
}
.detail-header-view {
margin-bottom: 15rpx;
}
.detail-title-view {
font-size: 26rpx;
font-weight: bold;
color: #333;
}
</style>