From 1f0a0a7059d3ac240857ecf8612f9e6cb422b4d8 Mon Sep 17 00:00:00 2001
From: ZN <zhang_12370@163.com>
Date: 星期二, 10 三月 2026 15:53:35 +0800
Subject: [PATCH] feat: 新增项目管理模块和销售退款页面
---
src/components/SearchPanel/index.vue | 257 +++++
src/views/financialManagement/salesRefund/components/ReceiptandRefundPopupWindow.vue | 226 +++++
src/api/projectManagement/project.js | 95 ++
src/views/projectManagement/Management/components/formDia.vue | 1503 ++++++++++++++++++++++++++++++++++
src/views/financialManagement/salesRefund/index.vue | 134 +++
src/views/salesManagement/returnOrder/index.vue | 2
src/views/projectManagement/Management/index.vue | 333 +++++++
7 files changed, 2,549 insertions(+), 1 deletions(-)
diff --git a/src/api/projectManagement/project.js b/src/api/projectManagement/project.js
new file mode 100644
index 0000000..d940e46
--- /dev/null
+++ b/src/api/projectManagement/project.js
@@ -0,0 +1,95 @@
+import request from '@/utils/request'
+
+export function listProject(data) {
+ return request({
+ url: '/projectManagement/info/listPage',
+ method: 'post',
+ data: data
+ })
+}
+
+export function getProject(id) {
+ return request({
+ url: `/projectManagement/info/${id}`,
+ method: 'post'
+ })
+}
+
+export function addProject(data) {
+ return request({
+ url: '/projectManagement/info/save',
+ method: 'post',
+ data: data
+ })
+}
+
+export function updateProject(data) {
+ return request({
+ url: '/projectManagement/info/save',
+ method: 'post',
+ data: data
+ })
+}
+
+export function delProject(ids) {
+ return request({
+ url: '/projectManagement/info/remove',
+ method: 'delete',
+ data: ids
+ })
+}
+
+export function updateStatus(data) {
+ return request({
+ url: '/projectManagement/info/updateStatus',
+ method: 'post',
+ data: data
+ })
+}
+
+export function submitProject(data) {
+ return request({
+ url: '/projectManagement/info/updateStatus',
+ method: 'post',
+ data: { ...data, reviewStatus: 0 }
+ })
+}
+
+export function auditProject(data) {
+ return request({
+ url: '/projectManagement/info/updateStatus',
+ method: 'post',
+ data: { ...data, reviewStatus: 1 }
+ })
+}
+
+export function reverseAuditProject(data) {
+ return request({
+ url: '/projectManagement/info/updateStatus',
+ method: 'post',
+ data: { ...data, reviewStatus: 0 }
+ })
+}
+
+export function listPlan(data) {
+ return request({
+ url: '/projectManagement/plan/listPage',
+ method: 'post',
+ data: data
+ })
+}
+
+export function addPlan(data) {
+ return request({
+ url: '/projectManagement/plan/save',
+ method: 'post',
+ data: data
+ })
+}
+
+export function delPlan(id) {
+ return request({
+ url: `/projectManagement/plan/delete/${id}`,
+ method: 'post'
+ })
+}
diff --git a/src/components/SearchPanel/index.vue b/src/components/SearchPanel/index.vue
new file mode 100644
index 0000000..a1859d2
--- /dev/null
+++ b/src/components/SearchPanel/index.vue
@@ -0,0 +1,257 @@
+<template>
+ <div class="search-panel-container">
+ <el-form
+ ref="formRef"
+ :model="modelValue"
+ class="search-form"
+ label-width="0"
+ >
+ <el-row :gutter="10" class="form-row">
+ <!-- 娓叉煋琛ㄥ崟椤� -->
+ <el-col
+ v-for="(item, index) in visibleSchema"
+ :key="item.prop || index"
+ :xs="24"
+ :sm="12"
+ :md="8"
+ :lg="4"
+ :xl="4"
+ class="search-col"
+ >
+ <el-form-item :prop="item.prop" :rules="item.rules" class="search-form-item">
+ <!-- 鑷畾涔夋彃妲� -->
+ <slot v-if="item.slot" :name="item.slot" :item="item"></slot>
+ <!-- 榛樿娓叉煋绫诲瀷 -->
+ <template v-else>
+ <!-- 杈撳叆妗� -->
+ <el-input
+ v-if="item.type === 'input'"
+ v-model="modelValue[item.prop]"
+ :placeholder="item.placeholder || '璇疯緭鍏�'"
+ clearable
+ class="full-width"
+ v-bind="item.props"
+ @keyup.enter="handleSearch"
+ />
+
+ <!-- 涓嬫媺妗� -->
+ <el-select
+ v-else-if="item.type === 'select'"
+ v-model="modelValue[item.prop]"
+ :placeholder="item.placeholder || '璇烽�夋嫨'"
+ clearable
+ class="full-width"
+ v-bind="item.props"
+ >
+ {{ item || '璇烽�夋嫨' }}
+ <!-- <el-option
+ v-for="(opt,idx) in getOptions(item)"
+ :key="idx"
+ :label="opt.label"
+ :value="opt.value"
+ /> -->
+ </el-select>
+
+ <!-- 鏃ユ湡閫夋嫨鍣� -->
+ <el-date-picker
+ v-else-if="item.type === 'date'"
+ v-model="modelValue[item.prop]"
+ type="date"
+ :placeholder="item.placeholder || '閫夋嫨鏃ユ湡'"
+ style="width: 100%"
+ value-format="YYYY-MM-DD"
+ class="full-width"
+ v-bind="item.props"
+ />
+
+ <!-- 鏃ユ湡鑼冨洿閫夋嫨鍣� -->
+ <el-date-picker
+ v-else-if="item.type === 'daterange'"
+ v-model="modelValue[item.prop]"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ class="full-width"
+ v-bind="item.props"
+ />
+ </template>
+ </el-form-item>
+ </el-col>
+
+ <!-- 鎸夐挳鍖哄煙 -->
+ <el-col :xs="24" :sm="12" :md="8" :lg="4" :xl="4" class="search-actions-col">
+ <el-form-item class="search-actions">
+ <el-button style="background: #002FA7; color: white;" icon="Search" @click="handleSearch">鎼滅储</el-button>
+ <el-button icon="Refresh" @click="handleReset">閲嶇疆</el-button>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <!-- 灞曞紑/鏀惰捣鎸夐挳 -->
+ <div v-if="schema.length > 5" class="expand-toggle" @click="toggleExpand">
+ <span>{{ isExpanded ? '鏀惰捣' : '灞曞紑' }}</span>
+ <el-icon :class="{ 'is-reverse': isExpanded }">
+ <ArrowDown />
+ </el-icon>
+ </div>
+ </el-form>
+ </div>
+</template>
+
+<script setup name="SearchPanel">
+import { ref, reactive, computed, getCurrentInstance, onMounted } from 'vue';
+import { ArrowDown, Search, Refresh } from '@element-plus/icons-vue';
+
+const { proxy } = getCurrentInstance();
+
+const props = defineProps({
+ // 琛ㄥ崟鏁版嵁瀵硅薄
+ modelValue: {
+ type: Object,
+ required: true
+ },
+ // 琛ㄥ崟閰嶇疆椤�
+ schema: {
+ type: Array,
+ default: () => []
+ }
+});
+
+const emit = defineEmits(['update:modelValue', 'search', 'reset']);
+
+// 鏄惁灞曞紑
+const isExpanded = ref(false);
+const formRef = ref(null);
+const dictMap = reactive({});
+
+// 璁$畻鍙鐨� schema 椤�
+const visibleSchema = computed(() => {
+ if (isExpanded.value || props.schema.length <= 5) {
+ return props.schema;
+ }
+ return props.schema.slice(0, 5);
+});
+
+// 鍒濆鍖栧瓧鍏告暟鎹�
+onMounted(() => {
+ const dicts = props.schema.filter(item => item.dict).map(item => item.dict);
+ if (dicts.length > 0 && proxy.useDict) {
+ const dictData = proxy.useDict(...dicts);
+ Object.keys(dictData).forEach(key => {
+ dictMap[key] = dictData[key];
+ });
+ }
+});
+
+// 鑾峰彇涓嬫媺閫夐」 (鏀寔闈欐�� options 鍜� 瀛楀吀 dict)
+function getOptions(item) {
+ if (item.options) return item.options;
+ if (item.dict && dictMap[item.dict]) {
+ return dictMap[item.dict].value || [];
+ }
+ return [];
+}
+
+// 鎼滅储
+function handleSearch() {
+ emit('search', props.modelValue);
+}
+
+// 閲嶇疆
+function handleReset() {
+ if (formRef.value) {
+ formRef.value.resetFields();
+ }
+ const keys = props.schema.map(item => item.prop).filter(Boolean);
+ keys.forEach(key => {
+ props.modelValue[key] = undefined;
+ });
+ emit('update:modelValue', props.modelValue);
+ emit('reset');
+}
+
+// 鍒囨崲灞曞紑/鏀惰捣
+function toggleExpand() {
+ isExpanded.value = !isExpanded.value;
+}
+</script>
+
+<style scoped lang="scss">
+.search-panel-container {
+ background: #fff;
+ padding: 15px 15px 5px;
+ border-radius: 8px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+ margin-bottom: 15px;
+
+ .search-form {
+ .form-row {
+ width: 100%;
+ }
+
+ .search-col {
+ margin-bottom: 10px;
+ }
+
+ .search-form-item {
+ margin-right: 0;
+ margin-bottom: 0;
+ width: 100%;
+
+ :deep(.el-form-item__content) {
+ width: 100%;
+ }
+ }
+
+ .full-width {
+ width: 100% !important;
+ }
+
+ .search-actions-col {
+ margin-left: auto;
+ display: flex;
+ justify-content: flex-end;
+ margin-bottom: 10px;
+ }
+
+ .search-actions {
+ margin-bottom: 0;
+ margin-right: 0;
+
+ :deep(.el-button--primary) {
+ background-color: #409eff;
+ border-color: #409eff;
+ }
+ }
+
+ .expand-toggle {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ font-size: 13px;
+ color: #909399;
+ cursor: pointer;
+ padding: 5px 0;
+ user-select: none;
+ width: 100%;
+ border-top: 1px solid #f0f2f5;
+ margin-top: 5px;
+
+ &:hover {
+ color: #409eff;
+ }
+
+ .el-icon {
+ transition: transform 0.3s;
+ &.is-reverse {
+ transform: rotate(180deg);
+ }
+ }
+ }
+ }
+}
+</style>
+
diff --git a/src/views/financialManagement/salesRefund/components/ReceiptandRefundPopupWindow.vue b/src/views/financialManagement/salesRefund/components/ReceiptandRefundPopupWindow.vue
new file mode 100644
index 0000000..d8218c1
--- /dev/null
+++ b/src/views/financialManagement/salesRefund/components/ReceiptandRefundPopupWindow.vue
@@ -0,0 +1,226 @@
+<template>
+ <el-dialog v-model="visible" title="鏀舵/閫�娆�" width="90%" append-to-body>
+ <div class="section">
+ <div class="section-title descriptions">鍩虹璧勬枡</div>
+ <el-form :model="form" label-width="100px">
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-form-item label="鍗曟嵁缂栧彿">
+ <el-input v-model="form.billNo" placeholder="浣跨敤绯荤粺缂栧彿" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="瀹㈡埛">
+ <el-select v-model="form.customerId" placeholder="璇烽�夋嫨">
+ <el-option v-for="c in customerOptions" :key="c.value" :label="c.label" :value="c.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="鍒跺崟浜�">
+ <el-select v-model="form.makerId" placeholder="璇烽�夋嫨">
+ <el-option v-for="u in userOptions" :key="u.value" :label="u.label" :value="u.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="鍒跺崟鏃ユ湡">
+ <el-date-picker v-model="form.makeDate" type="date" value-format="YYYY-MM-DD" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="鐢宠閮ㄩ棬">
+ <el-select v-model="form.applyDeptId" placeholder="璇烽�夋嫨">
+ <el-option v-for="d in deptOptions" :key="d.value" :label="d.label" :value="d.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="澶囨敞">
+ <el-input v-model="form.remark" maxlength="100" show-word-limit placeholder="璇疯緭鍏�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="闄勪欢">
+ <el-upload :action="uploadUrl" :headers="uploadHeaders" name="files" :on-success="onUploadSuccess">
+ <el-button>涓婁紶鏂囦欢</el-button>
+ </el-upload>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ </div>
+
+ <div class="section">
+ <div class="toolbar">
+ <div class="section-title descriptions">浠樻鍒楄〃</div>
+ <el-input v-model="form.discountAmount" placeholder="浼樻儬閲戦" style="width:240px" />
+ </div>
+ <el-table :data="form.paymentList" border>
+ <el-table-column label="浠樻璐﹀彿" minWidth="160">
+ <template #default="scope">
+ <el-input v-model="scope.row.accountNo" placeholder="璇疯緭鍏�" />
+ </template>
+ </el-table-column>
+ <el-table-column label="浠樻璐﹀彿鍚嶇О" minWidth="180">
+ <template #default="scope">
+ <el-select v-model="scope.row.accountName" placeholder="璇烽�夋嫨">
+ <el-option v-for="a in accountOptions" :key="a.value" :label="a.label" :value="a.label" />
+ </el-select>
+ </template>
+ </el-table-column>
+ <el-table-column label="浠樻鏂瑰紡" minWidth="140">
+ <template #default="scope">
+ <el-select v-model="scope.row.payMethod" placeholder="璇烽�夋嫨">
+ <el-option v-for="m in payMethodOptions" :key="m.value" :label="m.label" :value="m.value" />
+ </el-select>
+ </template>
+ </el-table-column>
+ <el-table-column label="瀹為檯浠樻閲戦" minWidth="160">
+ <template #default="scope">
+ <el-input v-model="scope.row.amount" placeholder="璇疯緭鍏�" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎵嬬画璐�" minWidth="140">
+ <template #default="scope">
+ <el-input v-model="scope.row.fee" placeholder="璇疯緭鍏�" />
+ </template>
+ </el-table-column>
+ <el-table-column label="浜ゆ槗鍙�/绁ㄦ嵁鍙�" minWidth="180">
+ <template #default="scope">
+ <el-input v-model="scope.row.txNo" placeholder="璇疯緭鍏�" />
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞" minWidth="200">
+ <template #default="scope">
+ <el-input v-model="scope.row.remark" maxlength="30" show-word-limit placeholder="璇疯緭鍏�" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" minWidth="120" fixed="right">
+ <template #default="scope">
+ <el-button link type="primary" @click="addPayment">鏂板涓�琛�</el-button>
+ <el-button link type="danger" @click="removePayment(scope.$index)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <div class="summary">鍚堣</div>
+ </div>
+
+ <div class="section">
+ <div class="section-container">
+ <div class="section-title descriptions">婧愬崟淇℃伅</div>
+ <div class="source-toolbar">
+ <el-button @click="clearSource">娓呯┖</el-button>
+ <el-button @click="selectSource">閫夋嫨婧愬崟</el-button>
+ <el-button type="primary" @click="autoWriteOff">鑷姩鏍搁攢</el-button>
+ </div>
+ </div>
+ <el-table :data="form.sourceList" border>
+ <el-table-column label="鍗曟嵁鏃ユ湡" minWidth="160" prop="billDate" />
+ <el-table-column label="鍗曟嵁绫诲瀷" minWidth="160" prop="billType" />
+ <el-table-column label="鍗曟嵁缂栧彿" minWidth="200" prop="billNo" />
+ <el-table-column label="鍗曟嵁閲戦" minWidth="120" prop="billAmount" />
+ <el-table-column label="宸叉牳閿�閲戦" minWidth="120" prop="wroteAmount" />
+ <el-table-column label="鏈牳閿�閲戦" minWidth="120" prop="unWroteAmount" />
+ <el-table-column label="鏈鏍搁攢閲戦" minWidth="160">
+ <template #default="scope">
+ <el-input v-model="scope.row.thisWriteOffAmount" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="100" fixed="right">
+ <template #default="scope">
+ <el-button link type="danger" @click="removeSource(scope.$index)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <div class="summary">鍚堣</div>
+ </div>
+
+ <template #footer>
+ <el-button type="primary" @click="submit">纭</el-button>
+ <el-button @click="visible=false">鍙栨秷</el-button>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import { getToken } from '@/utils/auth';
+
+const visible = ref(false);
+const form = ref({
+ billNo: '',
+ customerId: undefined,
+ makerId: undefined,
+ makeDate: '',
+ applyDeptId: undefined,
+ remark: '',
+ discountAmount: '',
+ paymentList: [{ accountNo: '', accountName: '', payMethod: '', amount: '', fee: '', txNo: '', remark: '' }],
+ sourceList: [{ billDate: '', billType: '', billNo: '', billAmount: 0, wroteAmount: 0, unWroteAmount: 0, thisWriteOffAmount: '' }]
+});
+
+const customerOptions = ref([]);
+const userOptions = ref([]);
+const deptOptions = ref([]);
+const accountOptions = ref([]);
+const payMethodOptions = ref([]);
+
+const uploadUrl = import.meta.env.VITE_APP_BASE_API + '/basic/customer-follow/upload';
+const uploadHeaders = { Authorization: 'Bearer ' + getToken() };
+
+function addPayment() {
+ form.value.paymentList.push({ accountNo: '', accountName: '', payMethod: '', amount: '', fee: '', txNo: '', remark: '' });
+}
+function removePayment(i) {
+ form.value.paymentList.splice(i, 1);
+}
+function removeSource(i) {
+ form.value.sourceList.splice(i, 1);
+}
+function clearSource() {
+ form.value.sourceList = [];
+}
+function selectSource() {}
+function autoWriteOff() {}
+function onUploadSuccess() {}
+
+function open(payload) {
+ visible.value = true;
+}
+function submit() {
+ visible.value = false;
+ emit('submitted');
+}
+
+defineExpose({ open });
+</script>
+
+<style scoped>
+.section { background: #fff; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05); padding: 16px; margin-bottom: 16px; }
+.section-title { font-weight: 600; margin-bottom: 12px; }
+.descriptions {
+ margin-bottom: 20px;
+ display: inline-block;
+ font-size: 1rem;
+ font-weight: 600;
+ padding-left: 12px;
+ position: relative;
+}
+.descriptions::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 4px;
+ height: 1rem;
+ background-color: #002FA7;
+ border-radius: 2px;
+}
+.toolbar { margin-bottom: 10px; display: flex; justify-content: space-between;
+ align-items: center; }
+.source-toolbar { margin-bottom: 10px; display: flex; gap: 8px; }
+.summary { padding: 8px 12px; background: #fff7e6; color: #ad6800; }
+.section-container{display: flex;align-items: center;justify-content: space-between; }
+</style>
diff --git a/src/views/financialManagement/salesRefund/index.vue b/src/views/financialManagement/salesRefund/index.vue
new file mode 100644
index 0000000..b4a792f
--- /dev/null
+++ b/src/views/financialManagement/salesRefund/index.vue
@@ -0,0 +1,134 @@
+<template>
+ <div class="app-container">
+ <!-- 浣跨敤鍏叡鎼滅储缁勪欢 -->
+ <SearchPanel
+ v-model="queryParams"
+ :schema="searchSchema"
+ @search="handleQuery"
+ @reset="resetQuery"
+ />
+
+ <!-- 琛ㄦ牸鍖哄煙 -->
+ <el-card class="table-card">
+ <el-table :data="refundList" v-loading="loading" border>
+ <el-table-column label="閫�璐у崟鍙�" prop="returnManagementNo" align="center" />
+ <el-table-column label="瀹㈡埛鍚嶇О" prop="customerName" align="center" />
+ <el-table-column label="閿�鍞崟鍙�" prop="salesContractNo" align="center" />
+ <el-table-column label="搴旈��娆鹃噾棰�" prop="refundAmount" align="center" />
+ <el-table-column label="宸查��娆鹃噾棰�" prop="refundedAmount" align="center" />
+ <el-table-column label="鏈��娆鹃噾棰�" prop="notRefundedAmount" align="center" />
+ <el-table-column label="鐘舵��" prop="status" align="center">
+ <template #default="scope">
+ <dict-tag :options="dictRef.sales_refund_status.value" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍒涘缓浜�" prop="createUserName" align="center" />
+ <el-table-column label="鍒涘缓鏃堕棿" prop="createTime" align="center" />
+ <el-table-column label="鎿嶄綔" align="center" width="150">
+ <template #default="scope">
+ <el-button link type="primary" @click="openDetail(scope.row)">璇︽儏</el-button>
+ <el-button link type="primary" @click="openConfirm(scope.row)">纭</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ v-model:page="queryParams.pageNum"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </el-card>
+<ReceiptandRefundPopupWindow ref="popupRef" @submitted="getList" />
+ </div>
+</template>
+
+<script setup name="SalesRefund">
+import { ref, reactive, onMounted, computed, getCurrentInstance } from 'vue';
+const { proxy } = getCurrentInstance();
+import { listPage, add, update, del } from '@/api/financialManagement/salesRefund';
+import SearchPanel from '@/components/SearchPanel/index.vue';
+import ReceiptandRefundPopupWindow from './components/ReceiptandRefundPopupWindow.vue';
+
+// 鏌ヨ鍙傛暟
+const queryParams = reactive({
+ pageNum: 1,
+ pageSize: 10,
+ returnManagementNo: undefined,
+ customerName: undefined,
+ salesContractNo: undefined,
+ createUserName: undefined,
+ status: undefined
+});
+
+const dictRef = proxy.useDict('sales_refund_status');
+const salesRefundStatusOptions = computed(() => dictRef.sales_refund_status.value || []);
+
+// 鎼滅储鏍忛厤缃�
+const searchSchema = [
+ { type: 'input', prop: 'returnManagementNo', placeholder: '閫�璐у崟鍙�' },
+ { type: 'input', prop: 'customerName', placeholder: '瀹㈡埛鍚嶇О' },
+ { type: 'input', prop: 'salesContractNo', placeholder: '閿�鍞崟鍙�' },
+ { type: 'input', prop: 'createUserName', placeholder: '鍒涘缓浜哄悕绉�' },
+ { type: 'select', prop: 'status', placeholder: '鐘舵��', options: salesRefundStatusOptions }
+];
+
+const loading = ref(false);
+const total = ref(0);
+const refundList = ref([]);
+const popupRef = ref(null);
+
+/** 鏌ヨ鍒楄〃 */
+function getList() {
+ loading.value = true;
+ const { pageNum, pageSize, ...filters } = queryParams;
+ listPlan({
+ current: pageNum,
+ size: pageSize,
+ ...filters
+ })
+ .then(res => {
+ refundList.value = res?.data?.records || res?.rows || [];
+ total.value = res?.data?.total || res?.total || 0;
+ })
+ .finally(() => {
+ loading.value = false;
+ });
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+function handleQuery() {
+ queryParams.pageNum = 1;
+ getList();
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+function resetQuery() {
+ handleQuery();
+}
+
+function openDetail(row) {
+ if (popupRef.value) {
+ popupRef.value.open({ mode: 'detail', row });
+ }
+}
+function openConfirm(row) {
+ if (popupRef.value) {
+ popupRef.value.open({ mode: 'confirm', row });
+ }
+}
+
+onMounted(() => {
+ getList();
+});
+</script>
+
+<style scoped lang="scss">
+.table-card {
+ border-radius: 8px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+}
+</style>
+
+<!-- keep-alive child -->
diff --git a/src/views/projectManagement/Management/components/formDia.vue b/src/views/projectManagement/Management/components/formDia.vue
new file mode 100644
index 0000000..eca4f33
--- /dev/null
+++ b/src/views/projectManagement/Management/components/formDia.vue
@@ -0,0 +1,1503 @@
+<template>
+ <el-dialog
+ v-model="dialogVisible"
+ :title="dialogTitle"
+ width="95%"
+ top="5vh"
+ destroy-on-close
+ @close="closeDialog"
+ >
+ <el-form
+ ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-position="top"
+ label-width="120px"
+ :disabled="isView"
+ >
+ <div class="section">
+ <div class="section-header" @click="toggleSection('base')">
+ <div class="section-title">
+ <span class="section-bar" />
+ <span>鍩虹璧勬枡</span>
+ </div>
+ <el-icon class="toggle-icon">
+ <ArrowDown v-if="sectionCollapsed.base" />
+ <ArrowUp v-else />
+ </el-icon>
+ </div>
+ <div v-show="!sectionCollapsed.base" class="section-body">
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-form-item label="鍗曟嵁缂栧彿" prop="billNo">
+ <el-input v-model="form.billNo" placeholder="绯荤粺鐢熸垚" disabled />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="椤圭洰鍚嶇О" prop="projectName">
+ <el-input v-model="form.projectName" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="瀹㈡埛鍚嶇О" prop="customerName">
+ <el-input v-model="form.customerName" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-form-item label="绔嬮」鏃ユ湡" prop="setupDate">
+ <el-date-picker
+ v-model="form.setupDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="椤圭洰鏉ユ簮" prop="projectSource">
+ <el-input v-model="form.projectSource" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="绔嬮」浜�" prop="creatorName">
+ <el-input v-model="form.creatorName" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-form-item label="棰勮宸ユ湡(澶�)" prop="estimatedDays">
+ <el-input-number v-model="form.estimatedDays" :min="0" controls-position="right" style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="璁″垝寮�濮嬫棩鏈�" prop="planStartDate">
+ <el-date-picker
+ v-model="form.planStartDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="璁″垝瀹屾垚鏃ユ湡" prop="planEndDate">
+ <el-date-picker
+ v-model="form.planEndDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-form-item label="椤圭洰绫诲瀷" prop="projectManagementPlanId">
+ <el-select v-model="form.projectManagementPlanId" placeholder="璇烽�夋嫨" clearable style="width: 100%">
+ <el-option v-for="opt in projectTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="椤圭洰閲戦" prop="projectAmount">
+ <el-input-number v-model="form.projectAmount" :min="0" controls-position="right" style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="瀹℃牳鐘舵��" prop="auditStatus">
+ <el-select v-model="form.auditStatus" placeholder="璇烽�夋嫨" clearable style="width: 100%">
+ <el-option v-for="d in project_management" :key="d.value" :label="d.label" :value="d.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+
+ </el-row>
+ <el-row :gutter="10" >
+ <el-col :span="24">
+ <el-upload
+ v-model:file-list="fileList"
+ :action="upload.url"
+ :headers="upload.headers"
+ multiple
+ :disabled="isView"
+ :before-upload="beforeUpload"
+ :on-success="handleUploadSuccess"
+ :on-error="handleUploadError"
+ name="files"
+ :on-remove="handleRemove"
+ >
+ <el-button type="primary" :disabled="isView">涓婁紶鏂囦欢</el-button>
+ </el-upload>
+ <div v-if="existingAttachments.length > 0" class="attachment-list">
+ <div
+ v-for="(att, idx) in existingAttachments"
+ :key="att.id || att.url || idx"
+ class="attachment-item"
+ >
+ <el-icon><Document /></el-icon>
+ <span class="attachment-name">{{ att.name || att.fileName || att.url || '闄勪欢' }}</span>
+ <el-button link type="primary" size="small" @click="downloadAttachment(att)">涓嬭浇</el-button>
+ </div>
+ </div>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="24">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="璇疯緭鍏�" maxlength="100" show-word-limit />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </div>
+ </div>
+
+ <div class="section">
+ <div class="section-header" @click="toggleSection('product')">
+ <div class="section-title">
+ <span class="section-bar" />
+ <span>浜у搧淇℃伅</span>
+ </div>
+ <div class="section-actions" @click.stop>
+ <el-button v-if="!isView" type="primary" @click="openProductForm('add')">娣诲姞</el-button>
+ <el-button v-if="!isView" plain type="danger" @click="deleteProduct">鍒犻櫎</el-button>
+ <el-icon class="toggle-icon" @click="toggleSection('product')">
+ <ArrowDown v-if="sectionCollapsed.product" />
+ <ArrowUp v-else />
+ </el-icon>
+ </div>
+ </div>
+ <div v-show="!sectionCollapsed.product" class="section-body">
+ <el-table
+ :data="productData"
+ border
+ show-summary
+ :summary-method="summarizeProductTable"
+ @selection-change="productSelected"
+ >
+ <el-table-column v-if="!isView" align="center" type="selection" width="55" />
+ <el-table-column align="center" label="搴忓彿" type="index" width="60" />
+ <el-table-column label="浜у搧澶х被" prop="productCategory" />
+ <el-table-column label="瑙勬牸鍨嬪彿" prop="specificationModel" />
+ <el-table-column label="鍗曚綅" prop="unit" />
+ <el-table-column label="鏁伴噺" prop="quantity" />
+ <el-table-column label="绋庣巼(%)" prop="taxRate" />
+ <el-table-column label="鍚◣鍗曚环(鍏�)" prop="taxInclusiveUnitPrice" :formatter="formattedNumber" />
+ <el-table-column label="鍚◣鎬讳环(鍏�)" prop="taxInclusiveTotalPrice" :formatter="formattedNumber" />
+ <el-table-column label="涓嶅惈绋庢�讳环(鍏�)" prop="taxExclusiveTotalPrice" :formatter="formattedNumber" />
+ <el-table-column v-if="!isView" fixed="right" label="鎿嶄綔" min-width="60" align="center">
+ <template #default="scope">
+ <el-button link type="primary" size="small" @click="openProductForm('edit', scope.row, scope.$index)">缂栬緫</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </div>
+
+ <div class="section">
+ <div class="section-header" @click="toggleSection('team')">
+ <div class="section-title">
+ <span class="section-bar" />
+ <span>椤圭洰鍥㈤槦</span>
+ </div>
+ <div class="section-actions" @click.stop>
+ <el-button v-if="!isView" type="primary" :icon="Plus" @click="addTeamRow">鏂板琛�</el-button>
+ <el-icon class="toggle-icon" @click="toggleSection('team')">
+ <ArrowDown v-if="sectionCollapsed.team" />
+ <ArrowUp v-else />
+ </el-icon>
+ </div>
+ </div>
+ <div v-show="!sectionCollapsed.team" class="section-body">
+ <PIMTable
+ :column="teamColumns"
+ :tableData="form.teamList"
+ :tableLoading="false"
+ :isSelection="false"
+ :isShowPagination="false"
+ height="220"
+ >
+ <template #memberId="{ row }">
+ <el-select v-model="row.memberId" placeholder="璇烽�夋嫨" filterable clearable style="width: 100%" :disabled="isView">
+ <el-option v-for="u in userOptions" :key="u.value" :label="u.label" :value="u.value" />
+ </el-select>
+ </template>
+ <template #roleId="{ row }">
+ <el-select v-model="row.roleId" placeholder="璇烽�夋嫨" clearable style="width: 100%" :disabled="isView">
+ <el-option v-for="r in roleOptions" :key="r.value" :label="r.label" :value="r.value" />
+ </el-select>
+ </template>
+ <template #enterDate="{ row }">
+ <el-date-picker
+ v-model="row.enterDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ :disabled="isView"
+ />
+ </template>
+ <template #leaveDate="{ row }">
+ <el-date-picker
+ v-model="row.leaveDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ :disabled="isView"
+ />
+ </template>
+ <template #phone="{ row }">
+ <el-input v-model="row.phone" placeholder="璇疯緭鍏�" clearable :disabled="isView" />
+ </template>
+ <template #teamRemark="{ row }">
+ <el-input v-model="row.remark" placeholder="璇疯緭鍏�" clearable :disabled="isView" />
+ </template>
+ <template #teamAction="{ row, index }">
+ <el-button v-if="!isView" link type="danger" :icon="Delete" @click="removeTeamRow(index)">鍒犻櫎</el-button>
+ <span v-else>鈥�</span>
+ </template>
+ </PIMTable>
+ </div>
+ </div>
+
+ <!-- <div class="section">
+ <div class="section-header" @click="toggleSection('phase')">
+ <div class="section-title">
+ <span class="section-bar" />
+ <span>椤圭洰闃舵</span>
+ </div>
+ <div class="section-actions" @click.stop>
+ <el-button v-if="!isView" type="primary" :icon="Plus" @click="addPhaseRow">鏂板琛�</el-button>
+ <el-icon class="toggle-icon" @click="toggleSection('phase')">
+ <ArrowDown v-if="sectionCollapsed.phase" />
+ <ArrowUp v-else />
+ </el-icon>
+ </div>
+ </div>
+ <div v-show="!sectionCollapsed.phase" class="section-body">
+ <PIMTable
+ :column="phaseColumns"
+ :tableData="form.phaseList"
+ :tableLoading="false"
+ :isSelection="false"
+ :isShowPagination="false"
+ height="240"
+ >
+ <template #phaseName="{ row }">
+ <el-input v-model="row.phaseName" placeholder="璇疯緭鍏�" clearable :disabled="isView" />
+ </template>
+ <template #phaseDesc="{ row }">
+ <el-input v-model="row.description" placeholder="璇疯緭鍏�" clearable :disabled="isView" />
+ </template>
+ <template #ownerId="{ row }">
+ <el-select v-model="row.ownerId" placeholder="璇烽�夋嫨" filterable clearable style="width: 100%" :disabled="isView">
+ <el-option v-for="u in userOptions" :key="u.value" :label="u.label" :value="u.value" />
+ </el-select>
+ </template>
+ <template #planDays="{ row }">
+ <el-input-number v-model="row.planDays" :min="0" controls-position="right" style="width: 100%" :disabled="isView" />
+ </template>
+ <template #planStart="{ row }">
+ <el-date-picker
+ v-model="row.planStartDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ :disabled="isView"
+ />
+ </template>
+ <template #planEnd="{ row }">
+ <el-date-picker
+ v-model="row.planEndDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ :disabled="isView"
+ />
+ </template>
+ <template #progress="{ row }">
+ <el-input-number v-model="row.progress" :min="0" :max="100" controls-position="right" style="width: 100%" :disabled="isView" />
+ </template>
+ <template #actualStart="{ row }">
+ <el-date-picker
+ v-model="row.actualStartDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ :disabled="isView"
+ />
+ </template>
+ <template #actualEnd="{ row }">
+ <el-date-picker
+ v-model="row.actualEndDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ :disabled="isView"
+ />
+ </template>
+ <template #overdueDays="{ row }">
+ <el-input-number v-model="row.overdueDays" :min="0" controls-position="right" style="width: 100%" :disabled="isView" />
+ </template>
+ <template #completion="{ row }">
+ <el-input v-model="row.completionRemark" placeholder="璇疯緭鍏�" clearable :disabled="isView" />
+ </template>
+ <template #phaseAction="{ row, index }">
+ <el-button v-if="!isView" link type="danger" :icon="Delete" @click="removePhaseRow(index)">鍒犻櫎</el-button>
+ <span v-else>鈥�</span>
+ </template>
+ </PIMTable>
+ </div>
+ </div> -->
+
+ <div class="section">
+ <div class="section-header" @click="toggleSection('address')">
+ <div class="section-title">
+ <span class="section-bar" />
+ <span>鏀惰揣鍦板潃</span>
+ </div>
+ <div class="section-actions" @click.stop>
+ <el-button v-if="!isView" type="primary" :icon="Plus" @click="addAddressRow">鏂板琛�</el-button>
+ <el-icon class="toggle-icon" @click="toggleSection('address')">
+ <ArrowDown v-if="sectionCollapsed.address" />
+ <ArrowUp v-else />
+ </el-icon>
+ </div>
+ </div>
+ <div v-show="!sectionCollapsed.address" class="section-body">
+ <PIMTable
+ :column="addressColumns"
+ :tableData="form.addressList"
+ :tableLoading="false"
+ :isSelection="false"
+ :isShowPagination="false"
+ height="200"
+ >
+ <template #receiver="{ row }">
+ <el-input v-model="row.receiver" placeholder="璇疯緭鍏�" clearable :disabled="isView" />
+ </template>
+ <template #receiverPhone="{ row }">
+ <el-input v-model="row.phone" placeholder="璇疯緭鍏�" clearable :disabled="isView" />
+ </template>
+ <template #receiverAddress="{ row }">
+ <el-input v-model="row.address" placeholder="璇疯緭鍏�" clearable :disabled="isView" />
+ </template>
+ <template #addressAction="{ row, index }">
+ <el-button v-if="!isView" link type="danger" :icon="Delete" @click="removeAddressRow(index)">鍒犻櫎</el-button>
+ <span v-else>鈥�</span>
+ </template>
+ </PIMTable>
+ </div>
+ </div>
+
+ <div class="section">
+ <div class="section-header" @click="toggleSection('contact')">
+ <div class="section-title">
+ <span class="section-bar" />
+ <span>鑱旂郴淇℃伅</span>
+ </div>
+ <el-icon class="toggle-icon">
+ <ArrowDown v-if="sectionCollapsed.contact" />
+ <ArrowUp v-else />
+ </el-icon>
+ </div>
+ <div v-show="!sectionCollapsed.contact" class="section-body">
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-form-item label="鑱旂郴浜哄鍚�" prop="contactName">
+ <el-input v-model="form.contactName" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="鎬у埆" prop="contactGender">
+ <el-select v-model="form.contactGender" placeholder="璇烽�夋嫨" clearable style="width: 100%">
+ <el-option label="鐢�" value="1" />
+ <el-option label="濂�" value="2" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="鐢熸棩" prop="contactBirthday">
+ <el-date-picker
+ v-model="form.contactBirthday"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="閭" prop="contactEmail">
+ <el-input v-model="form.contactEmail" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-form-item label="閮ㄩ棬" prop="contactDept">
+ <el-input v-model="form.contactDept" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="鑱屽姟" prop="contactJob">
+ <el-input v-model="form.contactJob" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="鎵嬫満鍙风爜" prop="contactMobile">
+ <el-input v-model="form.contactMobile" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="寰俊鍙风爜" prop="contactWechat">
+ <el-input v-model="form.contactWechat" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-form-item label="QQ" prop="contactQq">
+ <el-input v-model="form.contactQq" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="浼佷笟寰俊" prop="contactWorkWechat">
+ <el-input v-model="form.contactWorkWechat" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍦板潃" prop="contactAddress">
+ <el-input v-model="form.contactAddress" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="24">
+ <el-form-item label="澶囨敞" prop="contactRemark">
+ <el-input v-model="form.contactRemark" type="textarea" :rows="2" placeholder="璇疯緭鍏�" maxlength="200" show-word-limit />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </div>
+ </div>
+ </el-form>
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button v-if="!isView" type="primary" @click="submitForm">纭</el-button>
+ <el-button @click="closeDialog">{{ isView ? '鍏抽棴' : '鍙栨秷' }}</el-button>
+ </div>
+ </template>
+ </el-dialog>
+
+ <FormDialog
+ v-model="productFormVisible"
+ :title="productOperationType === 'add' ? '鏂板浜у搧' : '缂栬緫浜у搧'"
+ :width="'40%'"
+ :operation-type="productOperationType"
+ @close="closeProductDia"
+ @confirm="submitProduct"
+ @cancel="closeProductDia"
+ >
+ <el-form ref="productFormRef" :model="productForm" label-width="140px" label-position="top" :rules="productRules">
+ <el-row :gutter="30">
+ <el-col :span="24">
+ <el-form-item label="浜у搧澶х被锛�" prop="productCategoryId">
+ <el-tree-select
+ v-model="productForm.productCategoryId"
+ placeholder="璇烽�夋嫨"
+ clearable
+ check-strictly
+ :data="productCategoryOptions"
+ :render-after-expand="false"
+ style="width: 100%"
+ @change="getModels"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="24">
+ <el-form-item label="瑙勬牸鍨嬪彿锛�" prop="productModelId">
+ <el-select v-model="productForm.productModelId" placeholder="璇烽�夋嫨" clearable filterable @change="getProductModel">
+ <el-option v-for="item in modelOptions" :key="item.id" :label="item.model" :value="item.id" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鍗曚綅锛�" prop="unit">
+ <el-input v-model="productForm.unit" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="绋庣巼(%)锛�" prop="taxRate">
+ <el-select v-model="productForm.taxRate" placeholder="璇烽�夋嫨" clearable @change="calculateFromTaxRate">
+ <el-option label="1" value="1" />
+ <el-option label="6" value="6" />
+ <el-option label="13" value="13" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鍚◣鍗曚环(鍏�)锛�" prop="taxInclusiveUnitPrice">
+ <el-input-number
+ v-model="productForm.taxInclusiveUnitPrice"
+ :step="0.01"
+ :min="0"
+ :precision="2"
+ style="width: 100%"
+ placeholder="璇疯緭鍏�"
+ clearable
+ @change="calculateFromUnitPrice"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏁伴噺锛�" prop="quantity">
+ <el-input-number
+ v-model="productForm.quantity"
+ :step="0.1"
+ :min="0"
+ :precision="2"
+ style="width: 100%"
+ placeholder="璇疯緭鍏�"
+ clearable
+ @change="calculateFromQuantity"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鍚◣鎬讳环(鍏�)锛�" prop="taxInclusiveTotalPrice">
+ <el-input v-model="productForm.taxInclusiveTotalPrice" placeholder="璇疯緭鍏�" clearable @change="calculateFromTotalPrice" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="涓嶅惈绋庢�讳环(鍏�)锛�" prop="taxExclusiveTotalPrice">
+ <el-input v-model="productForm.taxExclusiveTotalPrice" placeholder="璇疯緭鍏�" clearable @change="calculateFromExclusiveTotalPrice" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鍙戠エ绫诲瀷锛�" prop="invoiceType">
+ <el-select v-model="productForm.invoiceType" placeholder="璇烽�夋嫨" clearable>
+ <el-option label="澧炴櫘绁�" value="澧炴櫘绁�" />
+ <el-option label="澧炰笓绁�" value="澧炰笓绁�" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ </FormDialog>
+</template>
+
+<script setup name="ProjectManagementFormDia">
+import { computed, getCurrentInstance, reactive, ref, toRefs } from 'vue'
+import { ArrowDown, ArrowUp, Delete, Plus, Document } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+import { getToken } from '@/utils/auth'
+import PIMTable from '@/components/PIMTable/PIMTable.vue'
+import FormDialog from '@/components/Dialog/FormDialog.vue'
+import { listPlan } from '@/api/projectManagement/projectType'
+import { findRoleListPage } from '@/api/projectManagement/role'
+import { userListAll } from '@/api/publicApi'
+import { addProject, getProject, updateProject } from '@/api/projectManagement/project'
+import { modelList, productTreeList } from '@/api/basicData/product'
+import { delProduct as delSalesProduct } from '@/api/salesManagement/salesLedger'
+
+const emit = defineEmits(['completed'])
+const { proxy } = getCurrentInstance()
+const { bill_status, project_management, plan_status } = proxy.useDict('bill_status', 'project_management', 'plan_status')
+
+const dialogVisible = ref(false)
+const operationType = ref('add')
+const formRef = ref()
+const fileList = ref([])
+const existingAttachments = ref([])
+const upload = reactive({
+ url: import.meta.env.VITE_APP_BASE_API + '/basic/customer-follow/upload',
+ headers: { Authorization: 'Bearer ' + getToken() }
+})
+
+const projectTypeOptions = ref([])
+const roleOptions = ref([])
+const userOptions = ref([])
+const productData = ref([])
+const productSelectedRows = ref([])
+const productCategoryOptions = ref([])
+const modelOptions = ref([])
+const productFormVisible = ref(false)
+const productOperationType = ref('add')
+const productFormRef = ref()
+const productIndex = ref(0)
+const isCalculating = ref(false)
+
+const productFormData = reactive({
+ productForm: {
+ productCategoryId: undefined,
+ productCategory: '',
+ productModelId: undefined,
+ specificationModel: '',
+ unit: '',
+ quantity: '',
+ taxInclusiveUnitPrice: '',
+ taxRate: '',
+ taxInclusiveTotalPrice: '',
+ taxExclusiveTotalPrice: '',
+ invoiceType: ''
+ },
+ productRules: {
+ productCategoryId: [{ required: true, message: '璇烽�夋嫨', trigger: 'change' }],
+ productModelId: [{ required: true, message: '璇烽�夋嫨', trigger: 'change' }],
+ unit: [{ required: true, message: '璇疯緭鍏�', trigger: 'blur' }],
+ quantity: [{ required: true, message: '璇疯緭鍏�', trigger: 'blur' }],
+ taxInclusiveUnitPrice: [{ required: true, message: '璇疯緭鍏�', trigger: 'blur' }],
+ taxRate: [{ required: true, message: '璇烽�夋嫨', trigger: 'change' }],
+ taxInclusiveTotalPrice: [{ required: true, message: '璇疯緭鍏�', trigger: 'blur' }],
+ taxExclusiveTotalPrice: [{ required: true, message: '璇疯緭鍏�', trigger: 'blur' }],
+ invoiceType: [{ required: true, message: '璇烽�夋嫨', trigger: 'change' }]
+ }
+})
+
+const { productForm, productRules } = toRefs(productFormData)
+
+const data = reactive({
+ form: {
+ id: undefined,
+ clientId: undefined,
+ parentProjectId: undefined,
+ projectManagementPlanId: undefined,
+ managerId: undefined,
+ salesmanId: undefined,
+ salesmanName: '',
+ actualStartDate: '',
+ actualEndDate: '',
+ departmentId: undefined,
+ departmentName: '',
+ orderDate: '',
+ billNo: '',
+ projectName: '',
+ customerName: '',
+ parentProjectName: '',
+ setupDate: '',
+ projectSource: '',
+ creatorName: '',
+ billStatus: '',
+ projectStage: '',
+ estimatedDays: 0,
+ planStartDate: '',
+ planEndDate: '',
+ projectManagementPlanId: undefined,
+ projectAmount: 0,
+ auditStatus: '',
+ remark: '',
+ attachmentIds: [],
+ teamList: [],
+ phaseList: [],
+ addressList: [],
+ contactName: '',
+ contactGender: '',
+ contactBirthday: '',
+ contactEmail: '',
+ contactDept: '',
+ contactJob: '',
+ contactMobile: '',
+ contactWechat: '',
+ contactQq: '',
+ contactWorkWechat: '',
+ contactAddress: '',
+ contactRemark: ''
+ },
+ rules: {
+ projectName: [{ required: true, message: '璇疯緭鍏ラ」鐩悕绉�', trigger: 'blur' }]
+ }
+})
+
+const { form, rules } = toRefs(data)
+
+const sectionCollapsed = reactive({
+ base: false,
+ product: false,
+ team: false,
+ phase: false,
+ address: false,
+ contact: false
+})
+
+const isView = computed(() => operationType.value === 'view')
+const dialogTitle = computed(() => {
+ if (operationType.value === 'add') return '鏂板椤圭洰'
+ if (operationType.value === 'edit') return '缂栬緫椤圭洰'
+ return '椤圭洰璇︽儏'
+})
+
+const teamColumns = [
+ { label: '濮撳悕', prop: 'memberId', align: 'center', width: 180, dataType: 'slot', slot: 'memberId' },
+ { label: '椤圭洰缁勮鑹�', prop: 'roleId', align: 'center', width: 160, dataType: 'slot', slot: 'roleId' },
+ { label: '杩涘叆鏃ユ湡', prop: 'enterDate', align: 'center', width: 160, dataType: 'slot', slot: 'enterDate' },
+ { label: '绂诲紑鏃ユ湡', prop: 'leaveDate', align: 'center', width: 160, dataType: 'slot', slot: 'leaveDate' },
+ { label: '鑱旂郴鏂瑰紡', prop: 'phone', align: 'center', width: 180, dataType: 'slot', slot: 'phone' },
+ { label: '澶囨敞', prop: 'remark', align: 'center', dataType: 'slot', slot: 'teamRemark' },
+ { label: '鎿嶄綔', prop: 'action', align: 'center', width: 100, dataType: 'slot', slot: 'teamAction', fixed: 'right' }
+]
+
+const phaseColumns = [
+ { label: '闃舵鍚嶇О', prop: 'phaseName', align: 'center', width: 160, dataType: 'slot', slot: 'phaseName' },
+ { label: '鎻忚堪', prop: 'description', align: 'center', width: 200, dataType: 'slot', slot: 'phaseDesc' },
+ { label: '璐熻矗浜�', prop: 'ownerId', align: 'center', width: 160, dataType: 'slot', slot: 'ownerId' },
+ { label: '棰勮宸ユ湡(澶�)', prop: 'planDays', align: 'center', width: 140, dataType: 'slot', slot: 'planDays' },
+ { label: '璁″垝寮�濮嬫棩鏈�', prop: 'planStartDate', align: 'center', width: 160, dataType: 'slot', slot: 'planStart' },
+ { label: '璁″垝缁撴潫鏃ユ湡', prop: 'planEndDate', align: 'center', width: 160, dataType: 'slot', slot: 'planEnd' },
+ { label: '杩涘害(%)', prop: 'progress', align: 'center', width: 120, dataType: 'slot', slot: 'progress' },
+ { label: '瀹為檯寮�濮嬫棩鏈�', prop: 'actualStartDate', align: 'center', width: 160, dataType: 'slot', slot: 'actualStart' },
+ { label: '瀹為檯缁撴潫鏃ユ湡', prop: 'actualEndDate', align: 'center', width: 160, dataType: 'slot', slot: 'actualEnd' },
+ { label: '閫炬湡澶╂暟', prop: 'overdueDays', align: 'center', width: 120, dataType: 'slot', slot: 'overdueDays' },
+ { label: '瀹屾垚鎯呭喌', prop: 'completionRemark', align: 'center', width: 200, dataType: 'slot', slot: 'completion' },
+ { label: '鎿嶄綔', prop: 'action', align: 'center', width: 100, dataType: 'slot', slot: 'phaseAction', fixed: 'right' }
+]
+
+const addressColumns = [
+ { label: '鏀惰揣浜�', prop: 'receiver', align: 'center', width: 180, dataType: 'slot', slot: 'receiver' },
+ { label: '鑱旂郴鏂瑰紡', prop: 'phone', align: 'center', width: 180, dataType: 'slot', slot: 'receiverPhone' },
+ { label: '鏀惰揣鍦板潃', prop: 'address', align: 'center', dataType: 'slot', slot: 'receiverAddress' },
+ { label: '鎿嶄綔', prop: 'action', align: 'center', width: 100, dataType: 'slot', slot: 'addressAction', fixed: 'right' }
+]
+
+function toggleSection(key) {
+ sectionCollapsed[key] = !sectionCollapsed[key]
+}
+
+function resetFormData() {
+ Object.assign(form.value, {
+ id: undefined,
+ clientId: undefined,
+ parentProjectId: undefined,
+ projectManagementPlanId: undefined,
+ managerId: undefined,
+ salesmanId: undefined,
+ salesmanName: '',
+ actualStartDate: '',
+ actualEndDate: '',
+ departmentId: undefined,
+ departmentName: '',
+ orderDate: '',
+ billNo: '',
+ projectName: '',
+ customerName: '',
+ parentProjectName: '',
+ setupDate: '',
+ projectSource: '',
+ creatorName: '',
+ billStatus: '',
+ projectStage: '',
+ estimatedDays: 0,
+ planStartDate: '',
+ planEndDate: '',
+ projectManagementPlanId: undefined,
+ projectAmount: 0,
+ auditStatus: '',
+ remark: '',
+ attachmentIds: [],
+ teamList: [],
+ phaseList: [],
+ addressList: [],
+ contactName: '',
+ contactGender: '',
+ contactBirthday: '',
+ contactEmail: '',
+ contactDept: '',
+ contactJob: '',
+ contactMobile: '',
+ contactWechat: '',
+ contactQq: '',
+ contactWorkWechat: '',
+ contactAddress: '',
+ contactRemark: ''
+ })
+ fileList.value = []
+ productData.value = []
+}
+
+function formattedNumber(row, column, cellValue) {
+ const val = Number(cellValue ?? 0)
+ return Number.isFinite(val) ? val.toFixed(2) : '0.00'
+}
+
+function summarizeProductTable(param) {
+ return proxy.summarizeTable(param, ['taxInclusiveTotalPrice', 'taxExclusiveTotalPrice'])
+}
+
+function productSelected(selection) {
+ productSelectedRows.value = selection
+}
+
+function convertIdToValue(data) {
+ return (Array.isArray(data) ? data : []).map(item => {
+ const { id, children, ...rest } = item
+ const newItem = {
+ ...rest,
+ value: id
+ }
+ if (children && children.length > 0) {
+ newItem.children = convertIdToValue(children)
+ }
+ return newItem
+ })
+}
+
+function findNodeById(nodes, productId) {
+ for (let i = 0; i < (nodes || []).length; i++) {
+ if (nodes[i].value === productId) {
+ return nodes[i].label
+ }
+ if (nodes[i].children && nodes[i].children.length > 0) {
+ const foundNode = findNodeById(nodes[i].children, productId)
+ if (foundNode) return foundNode
+ }
+ }
+ return null
+}
+
+function findNodeIdByLabel(nodes, label) {
+ if (!label) return null
+ for (let i = 0; i < (nodes || []).length; i++) {
+ const node = nodes[i]
+ if (node.label === label) return node.value
+ if (node.children && node.children.length > 0) {
+ const found = findNodeIdByLabel(node.children, label)
+ if (found !== null && found !== undefined) return found
+ }
+ }
+ return null
+}
+
+function getProductOptions() {
+ return productTreeList().then(res => {
+ const list = res?.data || res
+ productCategoryOptions.value = convertIdToValue(list)
+ return productCategoryOptions.value
+ })
+}
+
+function getModels(value) {
+ const categoryLabel = findNodeById(productCategoryOptions.value, value)
+ productForm.value.productCategory = categoryLabel || ''
+ modelList({ id: value }).then(res => {
+ modelOptions.value = res?.data || res || []
+ })
+}
+
+function getProductModel(value) {
+ const index = (modelOptions.value || []).findIndex(item => item.id === value)
+ if (index !== -1) {
+ productForm.value.specificationModel = modelOptions.value[index].model
+ productForm.value.unit = modelOptions.value[index].unit
+ } else {
+ productForm.value.specificationModel = ''
+ productForm.value.unit = ''
+ }
+}
+
+async function openProductForm(type, row, index) {
+ productOperationType.value = type
+ productIndex.value = index || 0
+ productForm.value = {}
+ proxy.resetForm('productFormRef')
+
+ if (!productCategoryOptions.value || productCategoryOptions.value.length === 0) {
+ await getProductOptions()
+ }
+
+ if (type === 'edit' && row) {
+ productForm.value = { ...row }
+ try {
+ const categoryId = findNodeIdByLabel(productCategoryOptions.value, productForm.value.productCategory)
+ if (categoryId) {
+ productForm.value.productCategoryId = categoryId
+ const models = await modelList({ id: categoryId })
+ modelOptions.value = models?.data || models || []
+ const currentModel = (modelOptions.value || []).find(m => m.model === productForm.value.specificationModel)
+ if (currentModel) {
+ productForm.value.productModelId = currentModel.id
+ }
+ }
+ } catch {}
+ } else {
+ productForm.value = {
+ productCategoryId: undefined,
+ productCategory: '',
+ productModelId: undefined,
+ specificationModel: '',
+ unit: '',
+ quantity: '',
+ taxInclusiveUnitPrice: '',
+ taxRate: '',
+ taxInclusiveTotalPrice: '',
+ taxExclusiveTotalPrice: '',
+ invoiceType: ''
+ }
+ }
+
+ productFormVisible.value = true
+}
+
+function closeProductDia() {
+ proxy.resetForm('productFormRef')
+ productFormVisible.value = false
+}
+
+function submitProduct() {
+ productFormRef.value?.validate?.(valid => {
+ if (!valid) return
+ const payload = { ...productForm.value }
+ if (productOperationType.value === 'add') {
+ productData.value.push(payload)
+ } else {
+ productData.value[productIndex.value] = payload
+ }
+ closeProductDia()
+ })
+}
+
+function deleteProduct() {
+ if (!productSelectedRows.value || productSelectedRows.value.length === 0) {
+ proxy.$modal?.msgWarning?.('璇烽�夋嫨鏁版嵁')
+ return
+ }
+ const selectedIds = productSelectedRows.value.map(r => r?.id).filter(Boolean)
+ if (operationType.value !== 'add' && selectedIds.length > 0) {
+ delSalesProduct(selectedIds)
+ .then(() => {
+ proxy.$modal?.msgSuccess?.('鍒犻櫎鎴愬姛')
+ productData.value = productData.value.filter(row => !selectedIds.includes(row?.id))
+ productSelectedRows.value = []
+ })
+ .catch(() => {})
+ return
+ }
+
+ productData.value = productData.value.filter(row => !productSelectedRows.value.includes(row))
+ productSelectedRows.value = []
+}
+
+function calculateFromTotalPrice() {
+ if (isCalculating.value) return
+ const totalPrice = parseFloat(productForm.value.taxInclusiveTotalPrice)
+ const quantity = parseFloat(productForm.value.quantity)
+ if (!totalPrice || !quantity || quantity <= 0) return
+ isCalculating.value = true
+ productForm.value.taxInclusiveUnitPrice = (totalPrice / quantity).toFixed(2)
+ if (productForm.value.taxRate) {
+ productForm.value.taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice(totalPrice, productForm.value.taxRate)
+ }
+ isCalculating.value = false
+}
+
+function calculateFromExclusiveTotalPrice() {
+ if (!productForm.value.taxRate) {
+ proxy.$modal?.msgWarning?.('璇峰厛閫夋嫨绋庣巼')
+ return
+ }
+ if (isCalculating.value) return
+ const exclusiveTotalPrice = parseFloat(productForm.value.taxExclusiveTotalPrice)
+ const quantity = parseFloat(productForm.value.quantity)
+ const taxRate = parseFloat(productForm.value.taxRate)
+ if (!exclusiveTotalPrice || !quantity || quantity <= 0 || !taxRate) return
+ isCalculating.value = true
+ const taxRateDecimal = taxRate / 100
+ const inclusiveTotalPrice = exclusiveTotalPrice / (1 - taxRateDecimal)
+ productForm.value.taxInclusiveTotalPrice = inclusiveTotalPrice.toFixed(2)
+ productForm.value.taxInclusiveUnitPrice = (inclusiveTotalPrice / quantity).toFixed(2)
+ isCalculating.value = false
+}
+
+function calculateFromQuantity() {
+ if (!productForm.value.taxRate) {
+ proxy.$modal?.msgWarning?.('璇峰厛閫夋嫨绋庣巼')
+ return
+ }
+ if (isCalculating.value) return
+ const quantity = parseFloat(productForm.value.quantity)
+ const unitPrice = parseFloat(productForm.value.taxInclusiveUnitPrice)
+ if (!quantity || quantity <= 0 || !unitPrice) return
+ isCalculating.value = true
+ productForm.value.taxInclusiveTotalPrice = (unitPrice * quantity).toFixed(2)
+ if (productForm.value.taxRate) {
+ productForm.value.taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice(
+ productForm.value.taxInclusiveTotalPrice,
+ productForm.value.taxRate
+ )
+ }
+ isCalculating.value = false
+}
+
+function calculateFromUnitPrice() {
+ if (!productForm.value.taxRate) {
+ proxy.$modal?.msgWarning?.('璇峰厛閫夋嫨绋庣巼')
+ return
+ }
+ if (isCalculating.value) return
+ const quantity = parseFloat(productForm.value.quantity)
+ const unitPrice = parseFloat(productForm.value.taxInclusiveUnitPrice)
+ if (!quantity || quantity <= 0 || !unitPrice) return
+ isCalculating.value = true
+ productForm.value.taxInclusiveTotalPrice = (unitPrice * quantity).toFixed(2)
+ if (productForm.value.taxRate) {
+ productForm.value.taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice(
+ productForm.value.taxInclusiveTotalPrice,
+ productForm.value.taxRate
+ )
+ }
+ isCalculating.value = false
+}
+
+function calculateFromTaxRate() {
+ if (!productForm.value.taxRate) {
+ proxy.$modal?.msgWarning?.('璇峰厛閫夋嫨绋庣巼')
+ return
+ }
+ if (isCalculating.value) return
+ const inclusiveTotalPrice = parseFloat(productForm.value.taxInclusiveTotalPrice)
+ const taxRate = parseFloat(productForm.value.taxRate)
+ if (!inclusiveTotalPrice || !taxRate) return
+ isCalculating.value = true
+ productForm.value.taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice(inclusiveTotalPrice, taxRate)
+ isCalculating.value = false
+}
+
+async function loadProjectTypeOptions() {
+ try {
+ const res = await listPlan({ current: 1, size: 999 })
+ const records = res?.data?.records || res?.records || res?.rows || []
+ projectTypeOptions.value = records.map(item => ({ label: item.name, value: item.id }))
+ } catch {
+ projectTypeOptions.value = []
+ }
+}
+
+async function loadRoleOptions() {
+ try {
+ const res = await findRoleListPage({ pageNum: 1, pageSize: 999 })
+ const records = res?.data?.records || res?.rows || res?.records || []
+ roleOptions.value = records.map(item => ({ label: item.roleName || item.name, value: item.id }))
+ } catch {
+ roleOptions.value = []
+ }
+}
+
+async function loadUserOptions() {
+ try {
+ const res = await userListAll()
+ const list = res?.data || res?.rows || res || []
+ userOptions.value = (Array.isArray(list) ? list : []).map(u => ({
+ label: u.nickName || u.userName || u.username || u.name,
+ value: u.userId || u.id
+ }))
+ } catch {
+ userOptions.value = []
+ }
+}
+
+function addTeamRow() {
+ form.value.teamList.push({
+ memberId: undefined,
+ roleId: undefined,
+ enterDate: '',
+ leaveDate: '',
+ phone: '',
+ remark: ''
+ })
+}
+
+function removeTeamRow(index) {
+ if (index > -1) form.value.teamList.splice(index, 1)
+}
+
+function addPhaseRow() {
+ form.value.phaseList.push({
+ phaseName: '',
+ description: '',
+ ownerId: undefined,
+ planDays: 0,
+ planStartDate: '',
+ planEndDate: '',
+ progress: 0,
+ actualStartDate: '',
+ actualEndDate: '',
+ overdueDays: 0,
+ completionRemark: ''
+ })
+}
+
+function removePhaseRow(index) {
+ if (index > -1) form.value.phaseList.splice(index, 1)
+}
+
+function addAddressRow() {
+ form.value.addressList.push({
+ receiver: '',
+ phone: '',
+ address: ''
+ })
+}
+
+function removeAddressRow(index) {
+ if (index > -1) form.value.addressList.splice(index, 1)
+}
+
+function beforeUpload() {
+ if (isView.value) return false
+ proxy.$modal?.loading?.('姝e湪涓婁紶鏂囦欢锛岃绋嶅��...')
+ return true
+}
+
+function handleUploadError() {
+ proxy.$modal?.closeLoading?.()
+ ElMessage.error('涓婁紶鏂囦欢澶辫触')
+}
+
+function handleUploadSuccess(res, file) {
+ console.log(res, file)
+ proxy.$modal?.closeLoading?.()
+ if (res?.code !== 200) {
+ ElMessage.error(res?.msg || '涓婁紶澶辫触')
+ return
+ }
+ const attachmentId = res?.data?.[0]?.id ?? ""
+ if (!attachmentId) return
+ form.value.attachmentIds.push(attachmentId)
+ console.log(form.value.attachmentIds)
+ ElMessage.success('涓婁紶鎴愬姛')
+}
+
+function handleRemove(file) {
+ const attachmentId = file?.attachmentId
+ if (!attachmentId) return
+ form.value.attachmentIds = (form.value.attachmentIds || []).filter(id => id !== attachmentId)
+}
+
+async function openDialog(payload = {}) {
+ operationType.value = payload.operationType || 'add'
+ resetFormData()
+ await Promise.all([loadProjectTypeOptions(), loadRoleOptions(), loadUserOptions(), getProductOptions()])
+ if (payload.row?.id) {
+ try {
+ const res = await getProject(payload.row.id)
+ const detail = res?.data?.data ?? res?.data ?? res
+ const info = detail?.info || {}
+ const shippingAddress = detail?.shippingAddress || {}
+ const contractInfo = detail?.contractInfo || {}
+
+ const normalizeId = v => {
+ if (v === undefined || v === null || v === '') return undefined
+ const n = Number(v)
+ return Number.isNaN(n) ? v : n
+ }
+
+ const normalizeDictValue = v => {
+ if (v === undefined || v === null || v === '') return ''
+ return String(v)
+ }
+
+ const computeEstimatedDays = (start, end) => {
+ if (!start || !end) return 0
+ const startTime = new Date(`${start}T00:00:00`).getTime()
+ const endTime = new Date(`${end}T00:00:00`).getTime()
+ if (!Number.isFinite(startTime) || !Number.isFinite(endTime)) return 0
+ if (endTime < startTime) return 0
+ return Math.floor((endTime - startTime) / (24 * 60 * 60 * 1000)) + 1
+ }
+
+ Object.assign(form.value, {
+ id: info.id,
+ billNo: info.no ?? '',
+ projectManagementPlanId: info.projectManagementPlanId ?? '',
+ estimatedDays: Number(info.estimatedDays) || computeEstimatedDays(info.planStartTime, info.planEndTime) || 0,
+ projectName: info.title ?? '',
+ customerName: info.clientName ?? '',
+ parentProjectName: info.projectManagementInfoParentName ?? '',
+ setupDate: info.establishTime ?? '',
+ projectSource: info.source ?? '',
+ creatorName: info.managerName ?? '',
+ billStatus: normalizeDictValue(info.status),
+ projectStage: normalizeDictValue(info.stage ?? info.projectStage),
+ planStartDate: info.planStartTime ?? '',
+ planEndDate: info.planEndTime ?? '',
+ projectAmount: info.orderAmount ?? 0,
+ auditStatus: normalizeDictValue(info.reviewStatus),
+ remark: info.remark ?? '',
+ attachmentIds: Array.isArray(info.attachmentIds) ? info.attachmentIds : [],
+ teamList: Array.isArray(info.teamList) ? info.teamList.map(t => ({
+ memberId: normalizeId(t.userId),
+ roleId: normalizeId(t.userRoleId),
+ enterDate: t.joinTime,
+ leaveDate: t.departTime,
+ phone: t.contact,
+ remark: t.remark
+ })) : [],
+ addressList: shippingAddress?.address
+ ? [{
+ receiver: shippingAddress.consignee,
+ phone: shippingAddress.contract,
+ address: shippingAddress.address
+ }]
+ : [],
+ contactName: contractInfo.name ?? '',
+ contactGender: contractInfo.sex === '鐢�' ? '1' : contractInfo.sex === '濂�' ? '2' : '',
+ contactBirthday: contractInfo.birthday ?? '',
+ contactDept: contractInfo.department ?? '',
+ contactJob: contractInfo.job ?? '',
+ contactMobile: contractInfo.phoneNumber ?? '',
+ contactEmail: contractInfo.email ?? '',
+ contactQq: contractInfo.qq ?? '',
+ contactWechat: contractInfo.wx ?? '',
+ contactWorkWechat: contractInfo.lineaFissa ?? '',
+ contactAddress: contractInfo.origineEtnica ?? '',
+ contactRemark: contractInfo.rappresentanteLegale ?? ''
+ })
+
+ existingAttachments.value = Array.isArray(info.attachmentList)
+ ? info.attachmentList.map(a => ({
+ id: a.id ?? a.fileId,
+ name: a.fileName ?? a.name,
+ url: a.url ?? a.fileUrl ?? a.path
+ }))
+ : []
+
+ const rawPhaseList =
+ detail?.phaseList ||
+ detail?.projectPhaseList ||
+ detail?.projectStageList ||
+ info?.phaseList ||
+ info?.projectPhaseList ||
+ []
+ form.value.phaseList = Array.isArray(rawPhaseList)
+ ? rawPhaseList.map(p => ({
+ phaseName: p.phaseName ?? p.name ?? p.title ?? '',
+ description: p.description ?? p.workContent ?? p.desc ?? '',
+ ownerId: normalizeId(p.ownerId ?? p.leaderId ?? p.userId),
+ planDays: Number(p.planDays ?? p.estimatedDuration ?? p.estimatedDays) || 0,
+ planStartDate: p.planStartDate ?? p.planStartTime ?? p.startDate ?? '',
+ planEndDate: p.planEndDate ?? p.planEndTime ?? p.endDate ?? '',
+ progress: Number(p.progress ?? p.schedule) || 0,
+ actualStartDate: p.actualStartDate ?? p.actualStartTime ?? '',
+ actualEndDate: p.actualEndDate ?? p.actualEndTime ?? '',
+ overdueDays: Number(p.overdueDays ?? p.overDays) || 0,
+ completionRemark: p.completionRemark ?? p.remark ?? ''
+ }))
+ : []
+
+ productData.value = detail?.salesLedgerProductList || detail?.productData || []
+ } catch {}
+ }
+ if (form.value.teamList.length === 0 && !isView.value) addTeamRow()
+ if (form.value.phaseList.length === 0 && !isView.value) addPhaseRow()
+ dialogVisible.value = true
+}
+
+function downloadAttachment(att) {
+ if (att?.name) {
+ try {
+ proxy.$download.name(att.url);
+ return
+ } catch (e) {}
+ }
+ ElMessage.warning('闄勪欢鏆傛棤涓嬭浇鍦板潃')
+}
+function closeDialog() {
+ dialogVisible.value = false
+}
+
+async function submitForm() {
+ if (isView.value) {
+ closeDialog()
+ return
+ }
+ await formRef.value?.validate?.()
+ if (!productData.value || productData.value.length === 0) {
+ proxy.$modal?.msgWarning?.('璇锋坊鍔犱骇鍝佷俊鎭�')
+ return
+ }
+ const findLabel = (list, value) => (list || []).find(i => String(i.value) === String(value))?.label
+ const teamList = (form.value.teamList || []).map(t => ({
+ userId: t.memberId,
+ userName: findLabel(userOptions.value, t.memberId),
+ userRoleId: t.roleId,
+ userRoleName: findLabel(roleOptions.value, t.roleId),
+ joinTime: t.enterDate,
+ departTime: t.leaveDate,
+ contact: t.phone,
+ remark: t.remark
+ }))
+
+ const shippingRow = (form.value.addressList || [])[0] || {}
+ const shippingAddress = {
+ id: undefined,
+ consignee: shippingRow.receiver,
+ contract: shippingRow.phone,
+ address: shippingRow.address
+ }
+
+ const contractInfo = {
+ id: undefined,
+ name: form.value.contactName,
+ sex: form.value.contactGender === '1' ? '鐢�' : form.value.contactGender === '2' ? '濂�' : '',
+ birthday: form.value.contactBirthday,
+ department: form.value.contactDept,
+ job: form.value.contactJob,
+ phoneNumber: form.value.contactMobile,
+ email: form.value.contactEmail,
+ qq: form.value.contactQq,
+ lineaFissa: form.value.contactWorkWechat,
+ wx: form.value.contactWechat,
+ origineEtnica: form.value.contactAddress,
+ rappresentanteLegale: form.value.contactRemark
+ }
+ const info = {
+ id: form.value.id ?? null,
+ no: form.value.billNo,
+ title: form.value.projectName,
+ clientId: form.value.clientId ?? null,
+ clientName: form.value.customerName,
+ projectManagementInfoParentId: form.value.parentProjectId ?? null,
+ projectManagementPlanId: form.value.projectManagementPlanId ?? null,
+ establishTime: form.value.setupDate,
+ source: form.value.projectSource,
+ managerId: form.value.managerId ?? null,
+ managerName: form.value.creatorName,
+ salesmanId: form.value.salesmanId ?? null,
+ salesmanName: form.value.salesmanName ?? '',
+ planStartTime: form.value.planStartDate,
+ planEndTime: form.value.planEndDate,
+ actualStartTime: form.value.actualStartDate,
+ actualEndTime: form.value.actualEndDate,
+ status: form.value.billStatus === '' || form.value.billStatus === undefined || form.value.billStatus === null ? null : Number(form.value.billStatus),
+ departmentId: form.value.departmentId ?? null,
+ departmentName: form.value.departmentName ?? '',
+ orderDate: form.value.orderDate,
+ orderAmount: form.value.projectAmount,
+ reviewStatus: form.value.auditStatus === '' || form.value.auditStatus === undefined || form.value.auditStatus === null ? null : Number(form.value.auditStatus),
+ stage: form.value.projectStage === '' || form.value.projectStage === undefined || form.value.projectStage === null ? null : Number(form.value.projectStage),
+ remark: form.value.remark,
+ attachmentIds: Array.isArray(form.value.attachmentIds) ? form.value.attachmentIds : [],
+ teamList
+ }
+
+ const payload = {
+ info,
+ shippingAddress,
+ contractInfo,
+ salesLedgerProductList: productData.value
+ }
+
+ const req = operationType.value === 'edit' ? updateProject : addProject
+ const res = await req(payload)
+ if (res?.code === 200) {
+ ElMessage.success('淇濆瓨鎴愬姛')
+ closeDialog()
+ emit('completed')
+ return
+ }
+ ElMessage.error(res?.msg || '淇濆瓨澶辫触')
+}
+
+defineExpose({ openDialog })
+</script>
+
+<style scoped lang="scss">
+.section {
+ border: 1px solid #ebeef5;
+ border-radius: 8px;
+ margin-bottom: 14px;
+ background: #fff;
+}
+
+.section-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 14px;
+ cursor: pointer;
+}
+
+.section-title {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-weight: 600;
+ color: #303133;
+}
+
+.section-bar {
+ width: 3px;
+ height: 14px;
+ background: #e61e1e;
+ border-radius: 2px;
+}
+
+.section-actions {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.toggle-icon {
+ color: #909399;
+}
+
+.section-body {
+ padding: 0 14px 14px;
+}
+
+.dialog-footer {
+ display: flex;
+ justify-content: center;
+ gap: 12px;
+}
+.attachment-upload{
+
+}
+</style>
diff --git a/src/views/projectManagement/Management/index.vue b/src/views/projectManagement/Management/index.vue
new file mode 100644
index 0000000..1a48db5
--- /dev/null
+++ b/src/views/projectManagement/Management/index.vue
@@ -0,0 +1,333 @@
+<template>
+ <div class="app-container">
+ <SearchPanel
+ v-model="queryParams"
+ :schema="searchSchema"
+ @search="handleQuery"
+ @reset="resetQuery"
+ >
+ <template #billStatus="{ item }">
+ <el-select v-model="queryParams[item.prop]" placeholder="璇烽�夋嫨鍗曟嵁鐘舵��" clearable style="width: 100%">
+ <el-option v-for="dict in bill_status" :key="dict.value" :label="dict.label" :value="dict.value" />
+ </el-select>
+ </template>
+ <template #auditStatus="{ item }">
+ <el-select v-model="queryParams[item.prop]" placeholder="璇烽�夋嫨璁″垝鐘舵��" clearable style="width: 100%">
+ <el-option v-for="dict in project_management" :key="dict.value" :label="dict.label" :value="dict.value" />
+ </el-select>
+ </template>
+ <template #projectStage="{ item }">
+ <el-select v-model="queryParams[item.prop]" placeholder="璇烽�夋嫨瀹℃牳鐘舵��" clearable style="width: 100%">
+ <el-option v-for="dict in plan_status" :key="dict.value" :label="dict.label" :value="dict.value" />
+ </el-select>
+ </template>
+ </SearchPanel>
+
+ <div class="table-container">
+ <div class="table-actions">
+ <el-button style="background-color: #002FA7; color: #fff" @click="handleAdd">鏂板</el-button>
+ <!-- <el-dropdown split-button type="default" @command="handleGenerateBill" style="margin-left: 10px;">
+ 鐢熸垚鍗曟嵁
+ <template #dropdown>
+ <el-dropdown-menu>
+ <el-dropdown-item command="1">鐢熸垚鍗曟嵁1</el-dropdown-item>
+ <el-dropdown-item command="2">鐢熸垚鍗曟嵁2</el-dropdown-item>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown> -->
+ <el-button @click="handleSubmit">鎻愪氦</el-button>
+ <el-button @click="handleAudit">瀹℃牳</el-button>
+ <el-button @click="handleReverseAudit">鍙嶅鏍�</el-button>
+ <el-button @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+
+ <PIMTable
+ :column="columns"
+ :tableData="tableData"
+ :page="pagination"
+ :tableLoading="loading"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ @pagination="handlePagination"
+ >
+ <template #auditStatus="{ row }">
+ <dict-tag :options="project_management" :value="row.auditStatus" />
+ </template>
+ <template #projectStage="{ row }">
+ <dict-tag :options="plan_status" :value="row.projectStage" />
+ </template>
+ <template #action="{ row }">
+ <el-button link type="primary" @click="handleEdit(row)">缂栬緫</el-button>
+ <el-button link type="primary" @click="handleProgressReport(row)">杩涘害姹囨姤</el-button>
+ <el-button link type="primary" @click="handleDiscussProgress(row)">娲借皥杩涘睍</el-button>
+ <el-button link type="primary" @click="handleDetail(row)">璇︽儏</el-button>
+ </template>
+ </PIMTable>
+ </div>
+
+ <FormDia ref="formDiaRef" @completed="getList" />
+ </div>
+</template>
+
+<script setup name="ProjectManagement">
+import { ref, reactive, toRefs, onMounted, getCurrentInstance } from 'vue'
+import SearchPanel from '@/components/SearchPanel/index.vue'
+import PIMTable from '@/components/PIMTable/PIMTable.vue'
+import FormDia from './components/formDia.vue'
+import {
+ listProject,
+ delProject,
+ submitProject,
+ auditProject,
+ reverseAuditProject
+} from '@/api/projectManagement/project'
+import { ElMessage, ElMessageBox } from 'element-plus'
+
+const { proxy } = getCurrentInstance()
+const { bill_status, project_management, plan_status } = proxy.useDict('bill_status', 'project_management', 'plan_status')
+
+const loading = ref(false)
+const ids = ref([])
+const tableData = ref([])
+const formDiaRef = ref()
+
+const data = reactive({
+ queryParams: {
+ projectNameOrCode: undefined,
+ customerName: undefined,
+ billStatus: undefined,
+ projectStage: undefined,
+ auditStatus: undefined,
+ salesperson: undefined,
+ pageNum: 1,
+ pageSize: 10
+ },
+ pagination: {
+ current: 1,
+ size: 10,
+ total: 0,
+ layout: 'total, sizes, prev, pager, next, jumper'
+ }
+})
+
+const { queryParams, pagination } = toRefs(data)
+
+const searchSchema = [
+ { prop: 'projectNameOrCode', label: '椤圭洰鍚嶇О/缂栧彿', type: 'input', placeholder: '璇疯緭鍏ラ」鐩悕绉�/缂栧彿' },
+ { prop: 'customerName', label: '瀹㈡埛鍚嶇О', type: 'input', placeholder: '璇疯緭鍏ュ鎴峰悕绉�' },
+ { prop: 'billStatus', label: '鍗曟嵁鐘舵��', slot: 'billStatus' },
+ { prop: 'projectStage', label: '璁″垝鐘舵��', slot: 'projectStage' },
+ { prop: 'auditStatus', label: '瀹℃牳鐘舵��', slot: 'auditStatus' },
+ { prop: 'salesperson', label: '涓氬姟浜哄憳', type: 'input', placeholder: '璇疯緭鍏ヤ笟鍔′汉鍛�' }
+]
+
+const columns = [
+ { label: '鍗曟嵁缂栧彿', prop: 'billNo', align: 'center', width: '150' },
+ { label: '椤圭洰鍚嶇О', prop: 'projectName', align: 'center' },
+ { label: '瀹℃牳鐘舵��', prop: 'auditStatus', align: 'center', dataType: 'slot', slot: 'auditStatus' },
+ { label: '瀹㈡埛鍚嶇О', prop: 'customerName', align: 'center' },
+ { label: '绔嬮」鏃ユ湡', prop: 'setupDate', align: 'center', width: '120' },
+ { label: '椤圭洰鏉ユ簮', prop: 'projectSource', align: 'center' },
+ { label: '椤圭洰鍒嗙被', prop: 'projectClassification', align: 'center' },
+ { label: '鎿嶄綔', prop: 'action', align: 'center', width: '250', dataType: 'slot', slot: 'action', fixed: 'right' }
+]
+
+function getList() {
+ loading.value = true
+ const params = {
+ noOrName: queryParams.value.projectNameOrCode,
+ clientName: queryParams.value.customerName,
+ salesmanName: queryParams.value.salesperson,
+ reviewStatus: queryParams.value.auditStatus,
+ stage: queryParams.value.projectStage,
+ current: queryParams.value.pageNum,
+ size: queryParams.value.pageSize
+ }
+ listProject(params)
+ .then(response => {
+ const records = response?.data?.records || response?.rows || response?.records || []
+ const billFilter = queryParams.value.billStatus
+ const filtered = billFilter === undefined || billFilter === null || billFilter === ''
+ ? records
+ : records.filter(r => String(r.billStatus ?? r.status) === String(billFilter))
+ tableData.value = filtered.map(r => ({
+ id: r.id,
+ billNo: r.no ?? r.billNo,
+ projectName: r.title ?? r.projectName,
+ billStatus: r.billStatus ?? r.status,
+ auditStatus: r.reviewStatus ?? r.auditStatus,
+ projectStage: r.stage ?? r.projectStage,
+ customerName: r.clientName ?? r.customerName,
+ parentProject: r.parentTitle ?? r.parentName ?? r.parentProject,
+ setupDate: r.establishTime ?? r.setupDate,
+ projectType: r.planName ?? r.projectType,
+ projectSource: r.source ?? r.projectSource,
+ projectClassification: r.departmentName ?? r.projectClassification,
+ raw: r
+ }))
+ pagination.value.total = response?.total || response?.data?.total || 0
+ })
+ .finally(() => {
+ loading.value = false
+ })
+}
+
+function handleQuery() {
+ queryParams.value.pageNum = 1
+ pagination.value.current = 1
+ getList()
+}
+
+function resetQuery() {
+ queryParams.value = {
+ projectNameOrCode: undefined,
+ customerName: undefined,
+ billStatus: undefined,
+ projectStage: undefined,
+ auditStatus: undefined,
+ salesperson: undefined,
+ pageNum: 1,
+ pageSize: 10
+ }
+ handleQuery()
+}
+
+function handleSelectionChange(selection) {
+ ids.value = selection.map(item => item.id)
+}
+
+function handlePagination({ page, limit }) {
+ queryParams.value.pageNum = page
+ queryParams.value.pageSize = limit
+ pagination.value.current = page
+ pagination.value.size = limit
+ getList()
+}
+
+function handleAdd() {
+ formDiaRef.value?.openDialog({ operationType: 'add' })
+}
+
+function handleDelete() {
+ const delIds = ids.value
+ if (delIds.length === 0) {
+ ElMessage.warning('璇烽�夋嫨瑕佸垹闄ょ殑鏁版嵁椤�')
+ return
+ }
+ ElMessageBox.confirm('鏄惁纭鍒犻櫎鎵�閫夋暟鎹」?', '璀﹀憡', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ .then(() => delProject(delIds))
+ .then(() => {
+ getList()
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ })
+ .catch(() => {})
+}
+
+function handleSubmit() {
+ const submitIds = ids.value
+ if (submitIds.length === 0) {
+ ElMessage.warning('璇烽�夋嫨瑕佹彁浜ょ殑鏁版嵁椤�')
+ return
+ }
+ ElMessageBox.confirm('鏄惁纭鎻愪氦鎵�閫夋暟鎹」?', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ .then(async () => {
+ await Promise.all(submitIds.map(id => submitProject({ id })))
+ })
+ .then(() => {
+ getList()
+ ElMessage.success('鎻愪氦鎴愬姛')
+ })
+ .catch(() => {})
+}
+
+function handleAudit() {
+ const auditIds = ids.value
+ if (auditIds.length === 0) {
+ ElMessage.warning('璇烽�夋嫨瑕佸鏍哥殑鏁版嵁椤�')
+ return
+ }
+ ElMessageBox.confirm('鏄惁纭瀹℃牳鎵�閫夋暟鎹」?', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ .then(async () => {
+ await Promise.all(auditIds.map(id => auditProject({ id })))
+ })
+ .then(() => {
+ getList()
+ ElMessage.success('瀹℃牳鎴愬姛')
+ })
+ .catch(() => {})
+}
+
+function handleReverseAudit() {
+ const reverseAuditIds = ids.value
+ if (reverseAuditIds.length === 0) {
+ ElMessage.warning('璇烽�夋嫨瑕佸弽瀹℃牳鐨勬暟鎹」')
+ return
+ }
+ ElMessageBox.confirm('鏄惁纭鍙嶅鏍告墍閫夋暟鎹」?', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ .then(async () => {
+ await Promise.all(reverseAuditIds.map(id => reverseAuditProject({ id })))
+ })
+ .then(() => {
+ getList()
+ ElMessage.success('鍙嶅鏍告垚鍔�')
+ })
+ .catch(() => {})
+}
+
+function handleGenerateBill(command) {
+ ElMessage.info(`鐢熸垚鍗曟嵁: ${command}`)
+}
+
+function handleProgressReport(row) {
+ formDiaRef.value?.openDialog({ operationType: 'view', row })
+}
+
+function handleDiscussProgress(row) {
+ formDiaRef.value?.openDialog({ operationType: 'view', row })
+}
+
+function handleDetail(row) {
+ formDiaRef.value?.openDialog({ operationType: 'view', row })
+}
+
+function handleEdit(row) {
+ formDiaRef.value?.openDialog({ operationType: 'edit', row })
+}
+
+onMounted(() => {
+ getList()
+})
+</script>
+
+<style scoped lang="scss">
+.app-container {
+ padding: 20px;
+}
+.table-container {
+ background-color: #fff;
+ padding: 20px;
+ border-radius: 4px;
+}
+.table-actions {
+ margin-bottom: 15px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+</style>
diff --git a/src/views/salesManagement/returnOrder/index.vue b/src/views/salesManagement/returnOrder/index.vue
index a83dd24..a5aa019 100644
--- a/src/views/salesManagement/returnOrder/index.vue
+++ b/src/views/salesManagement/returnOrder/index.vue
@@ -95,7 +95,7 @@
cancelButtonText: "鍙栨秷",
type: "warning",
}).then(() => {
- returnManagementDel([row.id]).then(() => {
+ returnManagementDel({ ids: [row.id] }).then(() => {
proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
getList();
});
--
Gitblit v1.9.3