ZN
4 天以前 1f0a0a7059d3ac240857ecf8612f9e6cb422b4d8
feat: 新增项目管理模块和销售退款页面

- 新增项目管理模块,包含项目列表、增删改查、提交审核等功能
- 新增销售退款管理页面,支持退款单查询、确认和详情查看
- 新增通用搜索组件 SearchPanel,支持多种表单类型和展开收起
- 新增收款/退款弹窗组件,支持多付款方式和源单核销
- 修复退货单删除接口参数格式问题
已添加6个文件
已修改1个文件
2550 ■■■■■ 文件已修改
src/api/projectManagement/project.js 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SearchPanel/index.vue 257 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/salesRefund/components/ReceiptandRefundPopupWindow.vue 226 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/salesRefund/index.vue 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/Management/components/formDia.vue 1503 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/Management/index.vue 333 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/returnOrder/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
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'
  })
}
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>
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>
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 -->
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?.('正在上传文件,请稍候...')
  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>
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>
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();
    });