gaoluyang
2026-05-08 f5806d04b59df7a4079119392031ab99c381e0a5
浪潮
1.商机管理前端页面开发与联调
已添加7个文件
已修改3个文件
2602 ■■■■■ 文件已修改
src/api/basicData/contact.js 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/collaborativeApproval/journal.js 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/salesManagement/opportunityManagement.js 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/filePreview/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/contact/index.vue 514 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/customerFile/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/customerFileOpenSea/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/journal/index.vue 703 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/opportunityManagement/fileList.vue 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/opportunityManagement/index.vue 1122 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/basicData/contact.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,53 @@
import request from '@/utils/request'
// åˆ†é¡µæŸ¥è¯¢è”系人列表
export function listContact(query) {
  return request({
    url: '/customerContact/listPage',
    method: 'get',
    params: query
  })
}
// æ ¹æ®ID查询联系人详情
export function getContactById(id) {
  return request({
    url: `/customerContact/getById/${id}`,
    method: 'get'
  })
}
// æ–°å¢žè”系人
export function addContact(data) {
  return request({
    url: '/customerContact/add',
    method: 'post',
    data: data
  })
}
// ä¿®æ”¹è”系人
export function updateContact(data) {
  return request({
    url: '/customerContact/update',
    method: 'put',
    data: data
  })
}
// åˆ é™¤è”系人
export function delContact(id) {
  return request({
    url: `/customerContact/delete/${id}`,
    method: 'delete'
  })
}
// æ‰¹é‡åˆ é™¤è”系人
export function delContacts(ids) {
  return request({
    url: '/customerContact/delete',
    method: 'delete',
    data: ids
  })
}
src/api/collaborativeApproval/journal.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,62 @@
import request from "@/utils/request";
// æŸ¥è¯¢æ—¥å¿—列表
export function listJournal(query) {
  return request({
    url: "/collaborativeApproval/journal/page",
    method: "get",
    params: query,
  });
}
// æŸ¥è¯¢æ—¥å¿—详细
export function getJournal(journalId) {
  return request({
    url: "/collaborativeApproval/journal/" + journalId,
    method: "get",
  });
}
// æ–°å¢žæ—¥å¿—
export function addJournal(data) {
  return request({
    url: "/collaborativeApproval/journal/add",
    method: "post",
    data: data,
  });
}
// ä¿®æ”¹æ—¥å¿—
export function updateJournal(data) {
  return request({
    url: "/collaborativeApproval/journal/update",
    method: "put",
    data: data,
  });
}
// åˆ é™¤æ—¥å¿—
export function delJournal(ids) {
  return request({
    url: "/collaborativeApproval/journal/" + ids,
    method: "delete",
  });
}
// æŽ¨é€æ—¥å¿—
export function pushJournal(data) {
  return request({
    url: "/collaborativeApproval/journal/push",
    method: "post",
    data: data,
  });
}
// èŽ·å–ç”¨æˆ·åˆ—è¡¨ï¼ˆç”¨äºŽæŽ¨é€é€‰æ‹©ï¼‰
export function listUser(query) {
  return request({
    url: "/system/user/list",
    method: "get",
    params: query,
  });
}
src/api/salesManagement/opportunityManagement.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,61 @@
// å•†æœºç®¡ç†æŽ¥å£
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢å•†æœºåˆ—表
export function opportunityListPage(query) {
  return request({
    url: "/businessOpportunity/listPage",
    method: "get",
    params: query,
  });
}
// æ–°å¢žå•†æœº
export function addOpportunity(data) {
  return request({
    url: "/businessOpportunity/add",
    method: "post",
    data: data,
  });
}
// ä¿®æ”¹å•†æœº
export function updateOpportunity(data) {
  return request({
    url: "/businessOpportunity/update",
    method: "put",
    data: data,
  });
}
// æ·»åŠ å•†æœº
export function addDescription(data) {
  return request({
    url: "/businessOpportunity/addDescription",
    method: "post",
    data: data,
  });
}
// åˆ é™¤å•†æœº
export function delOpportunity(ids) {
  return request({
    url: "/businessOpportunity/delete",
    method: "delete",
    data: ids,
  });
}
// æŸ¥è¯¢çœ
export function getProvinceList() {
  return request({
    url: "/businessOpportunity/getProvinceList",
    method: "get",
  });
}
// æŸ¥è¯¢å¸‚
export function getCityList(id) {
  return request({
    url: "/businessOpportunity/getCityList",
    method: "get",
    params: id,
  });
}
src/components/filePreview/index.vue
@@ -164,7 +164,7 @@
};
const open = (url) => {
  fileUrl.value = window.location.protocol+'//'+window.location.host+ url;
  fileUrl.value = url;
  dialogVisible.value = true;
};
const handleClose = () => {
src/views/basicData/contact/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,514 @@
<template>
  <div class="app-container">
    <div class="search_form" style="margin-bottom: 20px;">
      <div>
        <span class="search_title">联系人姓名:</span>
        <el-input v-model="searchForm.contactPerson"
                  style="width: 240px;margin-right: 10px"
                  placeholder="请输入"
                  @keyup.enter="handleQuery"
                  clearable
                  :prefix-icon="Search" />
        <span class="search_title">联系电话:</span>
        <el-input v-model="searchForm.contactPhone"
                  style="width: 240px;margin-right: 10px"
                  placeholder="请输入"
                  @keyup.enter="handleQuery"
                  clearable />
        <span class="search_title">所属客户:</span>
        <el-select v-model="searchForm.customerId"
                   placeholder="请选择"
                   style="width: 240px"
                   clearable
                   filterable
                   @change="handleQuery">
          <el-option v-for="item in customerList"
                     :key="item.id"
                     :label="item.customerName"
                     :value="item.id" />
        </el-select>
        <el-button type="primary"
                   @click="handleQuery"
                   style="margin-left: 10px">搜索</el-button>
        <el-button @click="resetSearch">重置</el-button>
      </div>
      <div>
        <el-button type="primary"
                   @click="openForm('add')">新增联系人</el-button>
        <el-button type="danger"
                   plain
                   @click="handleDelete">删除</el-button>
      </div>
    </div>
    <div class="table_list">
      <PIMTable rowKey="id"
                :column="tableColumn"
                :tableData="tableData"
                :page="page"
                :isSelection="true"
                @selection-change="handleSelectionChange"
                :tableLoading="tableLoading"
                @pagination="pagination">
        <template #customerName="{ row }">
          <div class="customer-tags">
            <el-tag v-for="(name, index) in formatCustomerNames(row.customerNames)"
                    :key="index"
                    size="small"
                    type="info"
                    class="customer-tag">
              {{ name }}
            </el-tag>
          </div>
        </template>
      </PIMTable>
    </div>
    <!-- è”系人表单对话框 -->
    <FormDialog v-model="dialogFormVisible"
                :title="dialogTitle"
                :operation-type="operationType"
                width="600px"
                @close="closeDia"
                @confirm="submitForm"
                @cancel="closeDia">
      <el-form :model="form"
               label-width="100px"
               :rules="rules"
               ref="formRef">
        <el-form-item label="联系人:"
                      prop="contactPerson">
          <el-input v-model="form.contactPerson"
                    placeholder="请输入联系人姓名"
                    clearable />
        </el-form-item>
        <el-form-item label="联系电话:"
                      prop="contactPhone">
          <el-input v-model="form.contactPhone"
                    placeholder="请输入联系电话"
                    clearable />
        </el-form-item>
        <el-form-item label="所属客户:"
                      prop="customerIdList">
          <el-select v-model="form.customerIdList"
                     placeholder="请选择所属客户(可多选)"
                     style="width: 100%"
                     filterable
                     clearable
                     multiple>
            <el-option v-for="item in customerList"
                       :key="item.id"
                       :label="item.customerName"
                       :value="item.id" />
          </el-select>
        </el-form-item>
        <el-form-item label="备注:"
                      prop="remark">
          <el-input v-model="form.remark"
                    type="textarea"
                    :rows="3"
                    placeholder="请输入备注"
                    clearable />
        </el-form-item>
      </el-form>
    </FormDialog>
    <!-- å®¢æˆ·è¯¦æƒ…对话框 -->
    <FormDialog v-model="customerDetailVisible"
                title="绑定客户详情"
                operation-type="detail"
                width="700px"
                @close="closeCustomerDetail"
                @cancel="closeCustomerDetail">
      <div class="customer-detail">
        <el-table :data="currentCustomerList"
                  border
                  style="width: 100%">
          <el-table-column prop="customerName"
                           label="客户名称"
                           min-width="150" />
          <el-table-column prop="customerType"
                           label="客户分类"
                           width="120" />
          <el-table-column prop="companyPhone"
                           label="公司电话"
                           width="150" />
          <el-table-column prop="companyAddress"
                           label="公司地址"
                           min-width="200"
                           show-overflow-tooltip />
        </el-table>
      </div>
    </FormDialog>
  </div>
</template>
<script setup>
import { onMounted, ref, reactive, getCurrentInstance, computed } from "vue";
import { Search } from "@element-plus/icons-vue";
import { listContact, getContactById, addContact, updateContact, delContacts } from "@/api/basicData/contact.js";
import { listCustomer } from "@/api/basicData/customer.js";
import { ElMessageBox } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
const { proxy } = getCurrentInstance();
// å¯¹è¯æ¡†æ ‡é¢˜
const dialogTitle = computed(() => {
  return operationType.value === "add" ? "新增联系人" : "编辑联系人";
});
// è¡¨æ ¼åˆ—定义
const tableColumn = ref([
  {
    label: "联系人姓名",
    prop: "contactPerson",
  },
  {
    label: "联系电话",
    prop: "contactPhone",
  },
  {
    label: "所属客户",
    prop: "customerNames",
    dataType: "slot",
    slot: "customerName",
  },
  {
    label: "备注",
    prop: "remark",
    showOverflowTooltip: true,
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: "right",
    width: 200,
    operation: [
      {
        name: "编辑",
        type: "text",
        clickFun: row => {
          openForm("edit", row);
        },
      },
      {
        name: "查看客户",
        type: "text",
        clickFun: row => {
          viewCustomerDetail(row);
        },
      },
      {
        name: "删除",
        type: "text",
        style: "color: #f56c6c",
        clickFun: row => {
          handleDeleteRow(row);
        },
      },
    ],
  },
]);
const tableData = ref([]);
const selectedRows = ref([]);
const customerList = ref([]);
const customerMap = ref(new Map());
const tableLoading = ref(false);
const page = reactive({
  current: 1,
  size: 10,
  total: 0,
});
// æœç´¢è¡¨å•
const searchForm = reactive({
  contactPerson: "",
  contactPhone: "",
  customerId: "",
});
// å¯¹è¯æ¡†ç›¸å…³
const operationType = ref("");
const dialogFormVisible = ref(false);
const formRef = ref();
// è¡¨å•数据
const form = reactive({
  id: null,
  contactPerson: "",
  contactPhone: "",
  customerIdList: [],
  remark: "",
});
// è¡¨å•验证规则
const rules = {
  contactPerson: [{ required: true, message: "请输入联系人姓名", trigger: "blur" }],
  contactPhone: [{ required: true, message: "请输入联系电话", trigger: "blur" }],
  customerIdList: [{ required: true, message: "请选择所属客户", trigger: "change", type: "array" }],
};
// å®¢æˆ·è¯¦æƒ…相关
const customerDetailVisible = ref(false);
const currentCustomerList = ref([]);
// èŽ·å–å®¢æˆ·åˆ—è¡¨ï¼ˆç”¨äºŽä¸‹æ‹‰é€‰æ‹©ï¼‰
const getCustomerList = () => {
  listCustomer({ current: -1, size: -1 }).then(res => {
    if (res.data && res.data.records) {
      customerList.value = res.data.records;
      // æž„建客户ID到客户信息的映射
      customerMap.value = new Map();
      res.data.records.forEach(item => {
        customerMap.value.set(item.id, item);
      });
    }
  });
};
// æ ¼å¼åŒ–客户名称(处理字符串或数组格式)
const formatCustomerNames = (customerNames) => {
  if (!customerNames) return [];
  if (Array.isArray(customerNames)) return customerNames;
  if (typeof customerNames === 'string') {
    return customerNames.split(',').map(s => s.trim()).filter(s => s);
  }
  return [];
};
// é‡ç½®æœç´¢è¡¨å•
const resetSearch = () => {
  searchForm.contactPerson = "";
  searchForm.contactPhone = "";
  searchForm.customerId = "";
  handleQuery();
};
// æŸ¥è¯¢åˆ—表
const handleQuery = () => {
  page.current = 1;
  getList();
};
const pagination = obj => {
  page.current = obj.page;
  page.size = obj.limit;
  getList();
};
// èŽ·å–åˆ—è¡¨æ•°æ®
const getList = () => {
  tableLoading.value = true;
  const params = {
    ...searchForm,
    current: page.current,
    size: page.size
  };
  listContact(params).then(res => {
    tableLoading.value = false;
    if (res.data) {
      tableData.value = res.data.records || [];
      page.total = res.data.total || 0;
    }
  }).catch(() => {
    tableLoading.value = false;
  });
};
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = selection => {
  selectedRows.value = selection;
};
// æ‰“开表单对话框
const openForm = (type, row) => {
  operationType.value = type;
  resetForm();
  if (type === "edit" && row) {
    form.id = row.id;
    form.contactPerson = row.contactPerson;
    form.contactPhone = row.contactPhone;
    form.customerIdList = row.customerId ? row.customerId.toString().split(',').map(id => Number(id.trim())) : [];
    form.remark = row.remark || "";
  }
  dialogFormVisible.value = true;
};
// é‡ç½®è¡¨å•
const resetForm = () => {
  form.id = null;
  form.contactPerson = "";
  form.contactPhone = "";
  form.customerIdList = [];
  form.remark = "";
  if (formRef.value) {
    formRef.value.resetFields();
  }
};
// å…³é—­å¯¹è¯æ¡†
const closeDia = () => {
  dialogFormVisible.value = false;
  resetForm();
};
// æäº¤è¡¨å•
const submitForm = () => {
  formRef.value.validate(valid => {
    if (valid) {
      if (operationType.value === "edit") {
        submitEdit();
      } else {
        submitAdd();
      }
    }
  });
};
// æäº¤æ–°å¢ž
const submitAdd = () => {
  const submitData = {
    contactPerson: form.contactPerson,
    contactPhone: form.contactPhone,
    customerIdList: form.customerIdList,
    remark: form.remark,
  };
  addContact(submitData).then(res => {
    if (res.code === 200) {
      proxy.$modal.msgSuccess("添加成功");
      closeDia();
      getList();
    }
  });
};
// æäº¤ç¼–辑
const submitEdit = () => {
  const submitData = {
    id: form.id,
    contactPerson: form.contactPerson,
    contactPhone: form.contactPhone,
    customerIdList: form.customerIdList,
    remark: form.remark,
  };
  updateContact(submitData).then(res => {
    if (res.code === 200) {
      proxy.$modal.msgSuccess("修改成功");
      closeDia();
      getList();
    }
  });
};
// åˆ é™¤å•行
const handleDeleteRow = (row) => {
  ElMessageBox.confirm(
    `确定要删除联系人"${row.contactPerson}"吗?`,
    "提示",
    {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    }
  ).then(() => {
    delContacts([row.id]).then(res => {
      if (res.code === 200) {
        proxy.$modal.msgSuccess("删除成功");
        getList();
      }
    });
  });
};
// æ‰¹é‡åˆ é™¤
const handleDelete = () => {
  if (selectedRows.value.length === 0) {
    proxy.$modal.msgWarning("请选择要删除的数据");
    return;
  }
  ElMessageBox.confirm(
    `确定要删除选中的 ${selectedRows.value.length} ä¸ªè”系人吗?`,
    "提示",
    {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    }
  ).then(() => {
    const ids = selectedRows.value.map(row => row.id);
    delContacts(ids).then(res => {
      if (res.code === 200) {
        proxy.$modal.msgSuccess("批量删除成功");
        getList();
      }
    });
  });
};
// æŸ¥çœ‹å®¢æˆ·è¯¦æƒ…
const viewCustomerDetail = (row) => {
  const customerIdStr = row.customerId;
  if (!customerIdStr) {
    proxy.$modal.msgError("该联系人未绑定客户");
    return;
  }
  // èŽ·å–æ‰€æœ‰ç»‘å®šçš„å®¢æˆ·
  const customerIds = customerIdStr.toString().split(',').map(id => Number(id.trim()));
  const customers = customerIds.map(id => customerMap.value.get(id)).filter(item => item);
  if (customers.length > 0) {
    currentCustomerList.value = customers;
    customerDetailVisible.value = true;
  } else {
    proxy.$modal.msgError("客户信息不存在");
  }
};
// å…³é—­å®¢æˆ·è¯¦æƒ…
const closeCustomerDetail = () => {
  customerDetailVisible.value = false;
  currentCustomerList.value = [];
};
onMounted(() => {
  getList();
  getCustomerList();
});
</script>
<style scoped>
.search_form {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.search_title {
  font-size: 14px;
  color: #606266;
  margin-right: 5px;
}
.customer-detail {
  padding: 10px;
}
:deep(.el-descriptions__label) {
  width: 120px;
  font-weight: bold;
}
.customer-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
}
.customer-tag {
  margin-right: 0;
}
</style>
src/views/basicData/customerFile/index.vue
@@ -133,6 +133,7 @@
            </el-form-item>
          </el-col>
        </el-row>
        <!-- è”系人功能已迁移到联系人管理页面
        <el-row :gutter="30"
                v-for="(contact, index) in formYYs.contactList"
                :key="index">
@@ -165,6 +166,7 @@
        </el-row>
        <el-button @click="addNewContact"
                   style="margin-bottom: 10px;">+ æ–°å¢žè”系人</el-button>
        -->
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="维护人:"
@@ -430,6 +432,7 @@
              </div>
            </el-col>
          </el-row>
          <!-- è”系人信息已迁移到联系人管理页面
          <el-row :gutter="20">
            <el-col :span="12">
              <div class="info-item">
@@ -444,6 +447,7 @@
              </div>
            </el-col>
          </el-row>
          -->
          <el-row :gutter="20">
            <el-col :span="12">
              <div class="info-item">
src/views/basicData/customerFileOpenSea/index.vue
@@ -133,6 +133,7 @@
            </el-form-item>
          </el-col>
        </el-row>
        <!-- è”系人功能已迁移到联系人管理页面
        <el-row :gutter="30"
                v-for="(contact, index) in formYYs.contactList"
                :key="index">
@@ -165,6 +166,7 @@
        </el-row>
        <el-button @click="addNewContact"
                   style="margin-bottom: 10px;">+ æ–°å¢žè”系人</el-button>
        -->
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="维护人:"
@@ -499,6 +501,7 @@
              </div>
            </el-col>
          </el-row>
          <!-- è”系人信息已迁移到联系人管理页面
          <el-row :gutter="20">
            <el-col :span="12">
              <div class="info-item">
@@ -513,6 +516,7 @@
              </div>
            </el-col>
          </el-row>
          -->
          <el-row :gutter="20">
            <el-col :span="12">
              <div class="info-item">
src/views/collaborativeApproval/journal/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,703 @@
<template>
  <div class="app-container">
    <!-- æœç´¢è¡¨å• -->
    <div class="search_form">
      <el-form :model="searchForm" inline>
        <el-form-item label="日志标题">
          <el-input v-model="searchForm.title" placeholder="请输入日志标题" clearable />
        </el-form-item>
        <el-form-item label="创建人">
          <el-input v-model="searchForm.createUserName" placeholder="请输入创建人" clearable />
        </el-form-item>
        <el-form-item label="创建时间">
          <el-date-picker
            v-model="searchForm.dateRange"
            type="daterange"
            range-separator="至"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
            value-format="YYYY-MM-DD"
          />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleQuery">搜索</el-button>
          <el-button @click="resetQuery">重置</el-button>
        </el-form-item>
      </el-form>
      <div>
        <el-button type="primary" @click="openForm('add')">编写日志</el-button>
      </div>
    </div>
    <!-- æ—¥å¿—管理Tab -->
    <div class="journal-board">
      <el-tabs v-model="activeJournalType" @tab-change="handleTabChange">
        <el-tab-pane label="日报" name="daily">
          <template #label>
            <span>
              <el-icon><Calendar /></el-icon>
              æ—¥æŠ¥
              <span class="tab-count" v-if="journalCount.daily > 0">({{ journalCount.daily }})</span>
            </span>
          </template>
        </el-tab-pane>
        <el-tab-pane label="周报" name="weekly">
          <template #label>
            <span>
              <el-icon><Document /></el-icon>
              å‘¨æŠ¥
              <span class="tab-count" v-if="journalCount.weekly > 0">({{ journalCount.weekly }})</span>
            </span>
          </template>
        </el-tab-pane>
        <el-tab-pane label="月报" name="monthly">
          <template #label>
            <span>
              <el-icon><DataAnalysis /></el-icon>
              æœˆæŠ¥
              <span class="tab-count" v-if="journalCount.monthly > 0">({{ journalCount.monthly }})</span>
            </span>
          </template>
        </el-tab-pane>
      </el-tabs>
      <!-- æ—¥å¿—表格 -->
      <el-table :data="journalList" v-loading="loading" border style="width: 100%">
        <el-table-column type="index" label="序号" width="60" align="center" />
        <el-table-column prop="title" label="日志标题" min-width="200" show-overflow-tooltip>
          <template #default="scope">
            <el-link type="primary" @click="handleView(scope.row)">{{ scope.row.title }}</el-link>
          </template>
        </el-table-column>
        <el-table-column prop="type" label="日志类型" width="100" align="center">
          <template #default="scope">
            <el-tag :type="getTypeTagType(scope.row.type)">{{ getTypeText(scope.row.type) }}</el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="content" label="日志内容" min-width="300" show-overflow-tooltip />
        <el-table-column prop="createUserName" label="创建人" width="120" align="center" />
        <el-table-column prop="createTime" label="创建时间" width="160" align="center" />
        <el-table-column prop="pushStatus" label="推送状态" width="100" align="center">
          <template #default="scope">
            <el-tag :type="scope.row.pushStatus === 1 ? 'success' : 'info'">
              {{ scope.row.pushStatus === 1 ? '已推送' : '未推送' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="250" align="center" fixed="right">
          <template #default="scope">
            <el-button link type="primary" @click="handleView(scope.row)">查看</el-button>
            <el-button link type="primary" @click="handleEdit(scope.row)">编辑</el-button>
            <el-button link type="success" @click="handlePush(scope.row)" v-if="scope.row.pushStatus !== 1">推送</el-button>
            <el-button link type="danger" @click="handleDelete(scope.row.id)">删除</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"
      />
      <!-- ç©ºçŠ¶æ€ -->
      <div class="empty-state" v-if="journalList.length === 0 && !loading">
        <el-empty description="暂无日志数据" />
      </div>
    </div>
    <!-- ç¼–写/编辑日志对话框 -->
    <el-dialog
      :title="dialogTitle"
      v-model="dialogVisible"
      width="900px"
      append-to-body
      @close="resetForm"
    >
      <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
        <el-row>
          <el-col :span="12">
            <el-form-item label="日志类型" prop="type">
              <el-select v-model="form.type" placeholder="请选择日志类型" style="width: 100%">
                <el-option label="日报" value="daily" />
                <el-option label="周报" value="weekly" />
                <el-option label="月报" value="monthly" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="日志标题" prop="title">
              <el-input v-model="form.title" placeholder="请输入日志标题" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="24">
            <el-form-item label="日志内容" prop="content">
              <el-input
                v-model="form.content"
                type="textarea"
                :rows="10"
                placeholder="请输入日志内容"
                maxlength="2000"
                show-word-limit
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="24">
            <el-form-item label="备注">
              <el-input
                v-model="form.remark"
                type="textarea"
                :rows="3"
                placeholder="请输入备注信息"
                maxlength="500"
                show-word-limit
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">保 å­˜</el-button>
          <el-button type="success" @click="submitAndPushForm">保存并推送</el-button>
          <el-button @click="dialogVisible = false">取 æ¶ˆ</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- æŸ¥çœ‹æ—¥å¿—对话框 -->
    <el-dialog
      title="查看日志"
      v-model="viewDialogVisible"
      width="800px"
      append-to-body
    >
      <div class="journal-detail">
        <div class="detail-header">
          <h3 class="detail-title">{{ currentJournal.title }}</h3>
          <div class="detail-meta">
            <el-tag :type="getTypeTagType(currentJournal.type)">{{ getTypeText(currentJournal.type) }}</el-tag>
            <el-tag :type="currentJournal.pushStatus === 1 ? 'success' : 'info'">
              {{ currentJournal.pushStatus === 1 ? '已推送' : '未推送' }}
            </el-tag>
          </div>
        </div>
        <div class="detail-info">
          <span><el-icon><User /></el-icon> åˆ›å»ºäººï¼š{{ currentJournal.createUserName }}</span>
          <span><el-icon><Timer /></el-icon> åˆ›å»ºæ—¶é—´ï¼š{{ currentJournal.createTime }}</span>
        </div>
        <el-divider />
        <div class="detail-content">
          <div class="content-label">日志内容:</div>
          <div class="content-text">{{ currentJournal.content }}</div>
        </div>
        <div class="detail-remark" v-if="currentJournal.remark">
          <div class="content-label">备注:</div>
          <div class="content-text">{{ currentJournal.remark }}</div>
        </div>
      </div>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="handleEditFromView" v-if="currentJournal.pushStatus !== 1">编 è¾‘</el-button>
          <el-button type="success" @click="handlePushFromView" v-if="currentJournal.pushStatus !== 1">推 é€</el-button>
          <el-button @click="viewDialogVisible = false">关 é—­</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- æŽ¨é€æ—¥å¿—对话框 -->
    <el-dialog
      title="推送日志"
      v-model="pushDialogVisible"
      width="600px"
      append-to-body
    >
      <div class="push-info">
        <p><strong>日志标题:</strong>{{ currentJournal.title }}</p>
        <p><strong>日志类型:</strong>{{ getTypeText(currentJournal.type) }}</p>
      </div>
      <el-form ref="pushFormRef" :model="pushForm" :rules="pushRules" label-width="100px">
        <el-form-item label="推送人员" prop="userIds">
          <el-select
            v-model="pushForm.userIds"
            multiple
            filterable
            remote
            reserve-keyword
            placeholder="请输入用户名搜索"
            :remote-method="remoteSearchUser"
            :loading="userLoading"
            style="width: 100%"
          >
            <el-option
              v-for="item in userOptions"
              :key="item.userId"
              :label="item.userName"
              :value="item.userId"
            >
              <span>{{ item.userName }}</span>
              <span style="float: right; color: #8492a6; font-size: 13px">{{ item.deptName }}</span>
            </el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="推送备注">
          <el-input
            v-model="pushForm.remark"
            type="textarea"
            :rows="3"
            placeholder="请输入推送备注信息"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitPush">ç¡® å®š</el-button>
          <el-button @click="pushDialogVisible = false">取 æ¶ˆ</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { Calendar, Document, DataAnalysis, User, Timer } from "@element-plus/icons-vue";
import { onMounted, ref, reactive, toRefs } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import useUserStore from "@/store/modules/user";
import {
  listJournal,
  getJournal,
  addJournal,
  updateJournal,
  delJournal,
  pushJournal,
  listUser
} from "@/api/collaborativeApproval/journal.js";
import Pagination from "@/components/Pagination/index.vue";
const userStore = useUserStore();
// å“åº”式数据
const data = reactive({
  searchForm: {
    title: "",
    createUserName: "",
    dateRange: []
  },
  form: {
    id: undefined,
    type: "daily",
    title: "",
    content: "",
    remark: "",
    pushStatus: 0
  },
  rules: {
    type: [
      { required: true, message: "请选择日志类型", trigger: "change" }
    ],
    title: [
      { required: true, message: "日志标题不能为空", trigger: "blur" }
    ],
    content: [
      { required: true, message: "日志内容不能为空", trigger: "blur" }
    ]
  }
});
const { searchForm, form, rules } = toRefs(data);
// é¡µé¢çŠ¶æ€
const loading = ref(false);
const journalList = ref([]);
const total = ref(0);
const activeJournalType = ref("daily");
const dialogVisible = ref(false);
const dialogTitle = ref("");
const formRef = ref();
const viewDialogVisible = ref(false);
const pushDialogVisible = ref(false);
const pushFormRef = ref();
const currentJournal = ref({});
const userLoading = ref(false);
const userOptions = ref([]);
// æŸ¥è¯¢å‚æ•°
const queryParams = reactive({
  pageNum: 1,
  pageSize: 10
});
// æŽ¨é€è¡¨å•
const pushForm = reactive({
  journalId: undefined,
  userIds: [],
  remark: ""
});
const pushRules = {
  userIds: [
    { required: true, message: "请选择推送人员", trigger: "change", type: "array" }
  ]
};
// æ—¥å¿—数量统计
const journalCount = reactive({
  daily: 0,
  weekly: 0,
  monthly: 0
});
// èŽ·å–æ—¥å¿—åˆ—è¡¨
const getList = () => {
  loading.value = true;
  const params = {
    pageNum: queryParams.pageNum,
    pageSize: queryParams.pageSize,
    type: activeJournalType.value
  };
  if (searchForm.value.title) {
    params.title = searchForm.value.title;
  }
  if (searchForm.value.createUserName) {
    params.createUserName = searchForm.value.createUserName;
  }
  if (searchForm.value.dateRange && searchForm.value.dateRange.length === 2) {
    params.startTime = searchForm.value.dateRange[0];
    params.endTime = searchForm.value.dateRange[1];
  }
  listJournal(params).then(res => {
    if (res.code === 200) {
      journalList.value = res.data.records || [];
      total.value = res.data.total || 0;
      // æ›´æ–°å½“前类型的数量
      journalCount[activeJournalType.value] = res.data.total || 0;
    }
    loading.value = false;
  }).catch(() => {
    loading.value = false;
  });
};
// èŽ·å–æ‰€æœ‰ç±»åž‹æ•°é‡
const getAllTypeCounts = () => {
  ['daily', 'weekly', 'monthly'].forEach(type => {
    listJournal({ pageNum: 1, pageSize: 1, type }).then(res => {
      if (res.code === 200) {
        journalCount[type] = res.data.total || 0;
      }
    });
  });
};
// Tab切换
const handleTabChange = (tabName) => {
  activeJournalType.value = tabName;
  queryParams.pageNum = 1;
  getList();
};
// æœç´¢
const handleQuery = () => {
  queryParams.pageNum = 1;
  getList();
};
// é‡ç½®æœç´¢
const resetQuery = () => {
  searchForm.value = {
    title: "",
    createUserName: "",
    dateRange: []
  };
  handleQuery();
};
// èŽ·å–ç±»åž‹æ–‡æœ¬
const getTypeText = (type) => {
  const typeMap = { daily: "日报", weekly: "周报", monthly: "月报" };
  return typeMap[type] || type;
};
// èŽ·å–ç±»åž‹æ ‡ç­¾æ ·å¼
const getTypeTagType = (type) => {
  const typeMap = { daily: "primary", weekly: "success", monthly: "warning" };
  return typeMap[type] || "";
};
// æ‰“开表单
const openForm = (type) => {
  if (type === 'add') {
    dialogTitle.value = "编写日志";
    form.value = {
      id: undefined,
      type: activeJournalType.value,
      title: "",
      content: "",
      remark: "",
      pushStatus: 0
    };
  }
  dialogVisible.value = true;
};
// ç¼–辑日志
const handleEdit = (row) => {
  if (row.pushStatus === 1) {
    ElMessage.warning("已推送的日志不可编辑");
    return;
  }
  dialogTitle.value = "编辑日志";
  form.value = { ...row };
  dialogVisible.value = true;
};
// ä»ŽæŸ¥çœ‹é¡µé¢ç¼–辑
const handleEditFromView = () => {
  viewDialogVisible.value = false;
  handleEdit(currentJournal.value);
};
// æŸ¥çœ‹æ—¥å¿—
const handleView = (row) => {
  getJournal(row.id).then(res => {
    if (res.code === 200) {
      currentJournal.value = res.data;
      viewDialogVisible.value = true;
    }
  });
};
// åˆ é™¤æ—¥å¿—
const handleDelete = (id) => {
  ElMessageBox.confirm(
    "确认删除这条日志吗?",
    "提示",
    {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning"
    }
  ).then(() => {
    delJournal(id).then(res => {
      if (res.code === 200) {
        ElMessage.success("删除成功");
        getList();
        getAllTypeCounts();
      }
    });
  });
};
// æ‰“开推送弹窗
const handlePush = (row) => {
  currentJournal.value = row;
  pushForm.journalId = row.id;
  pushForm.userIds = [];
  pushForm.remark = "";
  pushDialogVisible.value = true;
  // åŠ è½½ç”¨æˆ·åˆ—è¡¨
  loadUserOptions();
};
// ä»ŽæŸ¥çœ‹é¡µé¢æŽ¨é€
const handlePushFromView = () => {
  viewDialogVisible.value = false;
  handlePush(currentJournal.value);
};
// åŠ è½½ç”¨æˆ·é€‰é¡¹
const loadUserOptions = () => {
  listUser({ pageNum: 1, pageSize: 100 }).then(res => {
    if (res.code === 200) {
      userOptions.value = res.data.records || res.data || [];
    }
  });
};
// è¿œç¨‹æœç´¢ç”¨æˆ·
const remoteSearchUser = (query) => {
  if (query) {
    userLoading.value = true;
    listUser({ userName: query, pageNum: 1, pageSize: 50 }).then(res => {
      if (res.code === 200) {
        userOptions.value = res.data.records || res.data || [];
      }
      userLoading.value = false;
    }).catch(() => {
      userLoading.value = false;
    });
  }
};
// æäº¤è¡¨å•
const submitForm = () => {
  formRef.value.validate((valid) => {
    if (valid) {
      const api = form.value.id ? updateJournal : addJournal;
      api(form.value).then(res => {
        if (res.code === 200) {
          ElMessage.success(form.value.id ? "修改成功" : "新增成功");
          dialogVisible.value = false;
          getList();
          getAllTypeCounts();
        }
      });
    }
  });
};
// æäº¤å¹¶æŽ¨é€
const submitAndPushForm = () => {
  formRef.value.validate((valid) => {
    if (valid) {
      const api = form.value.id ? updateJournal : addJournal;
      api(form.value).then(res => {
        if (res.code === 200) {
          const journalId = form.value.id || res.data;
          dialogVisible.value = false;
          // æ‰“开推送弹窗
          currentJournal.value = { ...form.value, id: journalId };
          pushForm.journalId = journalId;
          pushForm.userIds = [];
          pushForm.remark = "";
          pushDialogVisible.value = true;
          loadUserOptions();
          getList();
          getAllTypeCounts();
        }
      });
    }
  });
};
// æäº¤æŽ¨é€
const submitPush = () => {
  pushFormRef.value.validate((valid) => {
    if (valid) {
      pushJournal(pushForm).then(res => {
        if (res.code === 200) {
          ElMessage.success("推送成功");
          pushDialogVisible.value = false;
          getList();
          getAllTypeCounts();
        }
      });
    }
  });
};
// é‡ç½®è¡¨å•
const resetForm = () => {
  formRef.value?.resetFields();
};
onMounted(() => {
  getList();
  getAllTypeCounts();
});
</script>
<style scoped>
.search_form {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
}
.journal-board {
  background: #fff;
  padding: 20px;
  border-radius: 4px;
}
.tab-count {
  color: #f56c6c;
  font-size: 12px;
}
.empty-state {
  padding: 40px 0;
}
.journal-detail {
  padding: 20px;
}
.detail-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 16px;
}
.detail-title {
  margin: 0;
  font-size: 20px;
  color: #303133;
}
.detail-meta {
  display: flex;
  gap: 8px;
}
.detail-info {
  display: flex;
  gap: 24px;
  color: #606266;
  font-size: 14px;
}
.detail-info span {
  display: flex;
  align-items: center;
  gap: 4px;
}
.detail-content,
.detail-remark {
  margin-top: 16px;
}
.content-label {
  font-weight: bold;
  color: #303133;
  margin-bottom: 8px;
}
.content-text {
  color: #606266;
  line-height: 1.8;
  white-space: pre-wrap;
  word-wrap: break-word;
  background: #f5f7fa;
  padding: 16px;
  border-radius: 4px;
  min-height: 60px;
}
.push-info {
  padding: 0 20px 20px;
  color: #606266;
}
.push-info p {
  margin: 8px 0;
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}
</style>
src/views/salesManagement/opportunityManagement/fileList.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,77 @@
<template>
  <el-dialog v-model="dialogVisible" title="附件" width="40%" :before-close="handleClose" draggable>
    <el-table :data="tableData" border height="40vh">
      <el-table-column label="附件名称" prop="originalFilename" min-width="400" show-overflow-tooltip />
      <el-table-column fixed="right" label="操作" width="150" align="center">
        <template #default="scope">
          <el-button link type="primary" size="small" @click="downLoadFile(scope.row)">下载</el-button>
          <el-button link type="primary" size="small" @click="lookFile(scope.row)">预览</el-button>
          <el-button link type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
  </el-dialog>
  <filePreview ref="filePreviewRef" />
</template>
<script setup>
import { ref } from 'vue'
import filePreview from '@/components/filePreview/index.vue'
import { ElMessageBox, ElMessage } from 'element-plus'
import { deleteAttachment } from '@/api/basicData/storageAttachment.js'
const dialogVisible = ref(false)
const tableData = ref([])
const currentRowId = ref(null)
const { proxy } = getCurrentInstance();
const filePreviewRef = ref()
const emit = defineEmits(['refresh'])
const handleClose = () => {
  dialogVisible.value = false
}
const open = (list, rowId) => {
  dialogVisible.value = true
  tableData.value = list || []
  currentRowId.value = rowId
}
const downLoadFile = (row) => {
  proxy.$download.name(row.url);
}
const lookFile = (row) => {
  filePreviewRef.value.open(row.url)
}
// åˆ é™¤é™„ä»¶
const handleDelete = (row) => {
  ElMessageBox.confirm(`确认删除附件"${row.originalFilename}"吗?`, '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    deleteAttachment([row.storageAttachmentId]).then(() => {
      ElMessage.success('删除成功')
      // ä»Žåˆ—表中移除已删除的附件
      const index = tableData.value.findIndex(item => item.id === row.id)
      if (index !== -1) {
        tableData.value.splice(index, 1)
      }
      // è§¦å‘刷新事件
      emit('refresh', currentRowId.value)
    }).catch(() => {
      ElMessage.error('删除失败')
    })
  }).catch(() => {
    ElMessage.info('已取消删除')
  })
}
defineExpose({
  open
})
</script>
<style scoped></style>
src/views/salesManagement/opportunityManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1122 @@
<template>
  <div class="app-container">
    <!-- æœç´¢åŒºåŸŸ -->
    <div class="search_form">
      <el-form :model="searchForm" :inline="true">
        <el-form-item label="客户名称">
          <el-input
            v-model="searchForm.customerName"
            placeholder="请输入客户名称"
            clearable
            prefix-icon="Search"
            style="width: 200px;"
            @change="handleQuery"
          />
        </el-form-item>
        <el-form-item label="城市">
          <el-input
              v-model="searchForm.city"
              placeholder="请输入城市名称"
              clearable
              prefix-icon="Search"
              style="width: 200px"
              @change="handleQuery"
          />
        </el-form-item>
        <el-form-item label="状态">
          <el-select
            v-model="searchForm.status"
            placeholder="请选择状态"
            clearable
            style="width: 160px"
            @change="handleQuery"
          >
            <el-option
              v-for="item in statusOptions"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="录入人">
          <el-select
            v-model="searchForm.entryPerson"
            placeholder="请选择录入人"
            clearable
            filterable
            style="width: 200px"
            @change="handleQuery"
          >
            <el-option
              v-for="item in userList"
              :key="item.nickName"
              :label="item.nickName"
              :value="item.nickName"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="录入日期:">
          <el-date-picker v-model="searchForm.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange"
            placeholder="请选择" clearable @change="changeDaterange" />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleQuery">搜索</el-button>
          <el-button @click="resetQuery">重置</el-button>
          <el-button type="primary" @click="handleAdd">新建</el-button>
            <el-button type="danger" plain @click="handleDelete">删除</el-button>
        </el-form-item>
      </el-form>
    </div>
    <!-- è¡¨æ ¼åŒºåŸŸ -->
    <div class="table_list">
      <el-table
        :data="tableData"
        border
        v-loading="tableLoading"
        @selection-change="handleSelectionChange"
        :row-key="(row) => row.id"
        height="calc(100vh - 18.5em)"
        stripe
        show-summary
        :summary-method="contractAmountSummaryMethod"
      >
        <el-table-column align="center" type="selection" width="55" fixed="left"/>
        <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column label="状态" prop="status" width="120">
          <template #default="{ row }">
            <el-tag
              :type="getStatusTagType(row.status)"
              effect="light"
            >
              {{ getStatusText(row.status) }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="省份" prop="province" show-overflow-tooltip />
        <el-table-column label="市" prop="city" show-overflow-tooltip/>
        <el-table-column label="客户名称" prop="customerName" show-overflow-tooltip />
        <el-table-column label="行业" prop="industry" show-overflow-tooltip />
        <el-table-column label="商机来源" prop="businessSource" show-overflow-tooltip />
        <el-table-column label="签约金额" prop="contractAmount" show-overflow-tooltip width="150" />
        <!-- <el-table-column label="客户描述" prop="description" show-overflow-tooltip min-width="200" /> -->
        <el-table-column label="录入人" prop="entryPerson" show-overflow-tooltip width="120" />
        <el-table-column label="更新日期" prop="updateTime" width="120">
          <template #default="{ row }">
            {{ formatDate(row.updateTime) }}
          </template>
        </el-table-column>
        <el-table-column label="操作" fixed="right" width="240" align="center">
          <template #default="{ row }">
            <el-button
              link
              type="primary"
              size="small"
              @click="handleEdit(row)"
            >
              ç¼–辑
            </el-button>
            <el-button
              link
              type="primary"
              size="small"
              @click="handleAddOperation(row)"
            >
              æ·»åŠ æ‹œè®¿è®°å½•
            </el-button>
            <el-button
              link
              type="primary"
              size="small"
              @click="handleDetail(row)"
            >
              è¯¦æƒ…
            </el-button>
            <el-button
              link
              type="primary"
              size="small"
              @click="handleAttachment(row)"
            >
              é™„ä»¶
            </el-button>
          </template>
        </el-table-column>
      </el-table>
      <!-- åˆ†é¡µç»„ä»¶ -->
      <pagination
        v-show="total > 0"
        :total="total"
        layout="total, sizes, prev, pager, next, jumper"
        :page="page.current"
        :limit="page.size"
        @pagination="paginationChange"
      />
    </div>
    <!-- æ–°å¢ž/编辑对话框 -->
    <el-dialog
      v-model="dialogFormVisible"
      :title="operationType === 'add' ? '新建商机' : operationType === 'edit' ? '编辑商机' : operationType === 'addOperation' ? '添加商机' : '商机详情'"
      width="1000px"
      @close="closeDialog"
    >
      <el-form
        :model="form"
        :rules="rules"
        ref="formRef"
        label-width="100px"
        label-position="left"
      >
        <el-form-item label="状态" prop="status">
          <el-select v-model="form.status" placeholder="请选择状态" style="width: 100%" :disabled="operationType === 'detail'">
            <el-option
              v-for="item in statusOptions"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="省份" prop="province">
          <el-select v-model="form.province" filterable placeholder="请选择省份"
                                         @change="getCityListChange"
                                         style="width: 100%" :disabled="operationType === 'detail' || operationType === 'addOperation'">
            <el-option
              v-for="item in provinceOptions"
              :key="item.id"
              :label="item.name"
              :value="item.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="市" prop="city">
          <el-select v-model="form.city" filterable placeholder="请选择市"
                                         style="width: 100%" :disabled="operationType === 'detail' || operationType === 'addOperation'">
            <el-option
              v-for="item in cityOptions"
              :key="item.id"
              :label="item.name"
              :value="item.id"
            />
          </el-select>
        </el-form-item>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="客户名称" prop="customerName">
              <el-select
                v-model="form.customerName"
                placeholder="请选择"
                clearable
                style="width: 100%"
                :disabled="operationType === 'detail' || operationType === 'addOperation'"
              >
                <el-option
                  v-for="item in customerOption"
                  :key="item.customerName"
                  :label="item.customerName"
                  :value="item.customerName"
                >
                  {{
                    item.customerName + "——" + item.taxpayerIdentificationNumber
                  }}
                </el-option>
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="商机来源" prop="businessSource">
              <el-input
                v-model="form.businessSource"
                placeholder="请输入商机来源"
                clearable
                :disabled="operationType === 'detail' || operationType === 'addOperation'"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="行业">
              <el-input v-model="form.industry" placeholder="请输入行业" clearable :disabled="operationType === 'detail' || operationType === 'addOperation'" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="主营产品">
              <el-input v-model="form.mainProducts" placeholder="请输入主营产品" clearable :disabled="operationType === 'detail' || operationType === 'addOperation'" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="主营业务收入">
              <el-input v-model="form.mainBusinessRevenue" placeholder="请输入主营业务收入" clearable :disabled="operationType === 'detail' || operationType === 'addOperation'" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="客户规模">
              <el-input v-model="form.customerScale" placeholder="请输入客户规模" clearable :disabled="operationType === 'detail' || operationType === 'addOperation'" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="24">
            <el-form-item label="信息化现状">
              <el-input v-model="form.informationState" placeholder="请输入信息化现状" clearable :disabled="operationType === 'detail' || operationType === 'addOperation'" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="签约金额" prop="contractAmount">
          <el-input
            v-model="form.contractAmount"
            placeholder="请输入签约金额"
            clearable
            :disabled="operationType === 'detail' || operationType === 'addOperation'"
          >
            <template #append>元</template>
          </el-input>
        </el-form-item>
        <el-form-item label="拜访记录" prop="description">
          <el-input
            v-model="form.description"
            type="textarea"
            :autosize="{ minRows: 4, maxRows: 10 }"
            placeholder="请输入拜访记录"
            show-word-limit
            :disabled="operationType === 'detail'"
          />
        </el-form-item>
        <el-form-item label="改造内容" prop="renContent">
          <el-input
            v-model="form.renContent"
            type="textarea"
            :autosize="{ minRows: 3, maxRows: 8 }"
            :placeholder="renovationPlaceholder"
            show-word-limit
            :disabled="operationType === 'detail' || operationType === 'addOperation'"
          />
        </el-form-item>
        <el-form-item label="付款描述" prop="paymentDescription">
          <el-input
            v-model="form.paymentDescription"
            type="textarea"
            :autosize="{ minRows: 3, maxRows: 10 }"
            placeholder="是否垫资?企业是否开票?企业是否分补贴或额外出钱?"
            show-word-limit
            :disabled="operationType === 'detail' || operationType === 'addOperation'"
          />
        </el-form-item>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="录入人" prop="entryPerson">
              <el-select v-model="form.entryPerson" placeholder="请选择" clearable @change="changs" :disabled="operationType === 'detail' || operationType === 'addOperation'">
                <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName" :value="item.nickName" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
             <el-form-item label="录入日期" prop="entryDate">
               <el-date-picker style="width: 100%" v-model="form.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD"
                 type="date" placeholder="请选择" clearable :disabled="operationType === 'detail' || operationType === 'addOperation'" />
             </el-form-item>
           </el-col>
        </el-row>
        <!-- é™„件上传(非详情模式下显示) -->
        <el-row :gutter="30" v-if="operationType !== 'detail'">
          <el-col :span="24">
            <el-form-item label="附件材料:">
              <FileUpload v-model:file-list="fileList" />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <!-- é™„件查看(仅在详情模式下显示) -->
      <div v-if="operationType === 'detail'" class="attachment-section">
        <el-divider content-position="left">附件材料</el-divider>
        <div v-if="form.businessCommonFiles && form.businessCommonFiles.length > 0">
          <el-table :data="form.businessCommonFiles" border stripe style="width: 100%">
            <el-table-column label="附件名称" prop="originalFilename" min-width="400" show-overflow-tooltip />
            <el-table-column fixed="right" label="操作" width="150" align="center">
              <template #default="scope">
                <el-button link type="primary" size="small" @click="downloadAttachment(scope.row)">下载</el-button>
                <el-button link type="primary" size="small" @click="previewAttachment(scope.row)">预览</el-button>
              </template>
            </el-table-column>
          </el-table>
        </div>
        <div v-else style="text-align: center; padding: 20px; color: #999;">
          æš‚无附件
        </div>
      </div>
      <!-- å˜æ›´è®°å½•时间线(仅在详情模式下显示) -->
      <div v-if="operationType === 'detail'" class="change-history-section">
        <el-divider content-position="left">变更记录</el-divider>
        <el-timeline>
          <el-timeline-item
            v-for="record in changeHistory"
            :key="record.id"
            :timestamp="record.timestamp"
            :type="record.type === 'current' ? 'primary' : record.type === 'update' ? 'success' : 'info'"
            :hollow="record.type === 'current'"
            placement="top"
          >
            <el-card shadow="hover" class="timeline-card">
              <template #header>
                <div class="card-header">
                  <span class="action-type">{{ record.action }}</span>
                  <span class="operator">操作人:{{ record.operator }}</span>
                </div>
              </template>
              <div class="change-content">
                <div class="status-change" v-if="record.status">
                  <span class="label">状态:</span>
                  <el-tag :type="record.type === 'current' ? 'primary' : 'info'" size="small">
                    {{ getStatusLabel(record.status) }}
                  </el-tag>
                </div>
                <div class="description-change" v-if="record.description">
                  <span class="label">拜访记录:</span>
                  <span class="description-text">{{ record.description }}</span>
                </div>
              </div>
            </el-card>
          </el-timeline-item>
        </el-timeline>
      </div>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="closeDialog">取消</el-button>
          <el-button type="primary" @click="submitForm" v-if="operationType !== 'detail'">
            ç¡®å®š
          </el-button>
        </div>
      </template>
    </el-dialog>
    <!-- é™„件列表对话框 -->
    <FileList ref="fileListRef" @refresh="handleFileListRefresh" />
    <!-- æ–‡ä»¶é¢„览组件 -->
    <filePreview ref="filePreviewRef" />
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, getCurrentInstance } from 'vue'
import { Search } from '@element-plus/icons-vue'
import { ElMessageBox, ElMessage } from 'element-plus'
import pagination from '@/components/PIMTable/Pagination.vue'
import useUserStore from '@/store/modules/user'
import dayjs from 'dayjs'
import {
    opportunityListPage,
    addOpportunity,
    updateOpportunity,
    delOpportunity,
    addDescription, getProvinceList, getCityList
} from '@/api/salesManagement/opportunityManagement.js'
import { userListNoPage } from '@/api/system/user.js'
import {customerList, getSalesLedgerWithProducts} from '@/api/salesManagement/salesLedger.js'
import FileList from './fileList.vue'
import filePreview from '@/components/filePreview/index.vue'
const { proxy } = getCurrentInstance()
const userStore = useUserStore()
// è¡¨æ ¼æ•°æ®
const tableData = ref([])
const selectedRows = ref([])
const tableLoading = ref(false)
const userList = ref([])
const customerOption = ref([])
const DEFAULT_USER_QUERY = { postCode: 'Market_Sales' }
const DEFAULT_CUSTOMER_QUERY = { customerType: 2 }
let userListPromise = null
let customerListPromise = null
const loadUserList = async (query = DEFAULT_USER_QUERY) => {
  if (userListPromise) return userListPromise
  userListPromise = (async () => {
    try {
      const res = await userListNoPage(query)
      userList.value = res?.data || []
      return userList.value
    } catch (err) {
      console.error('获取用户列表失败:', err)
      userList.value = []
      userListPromise = null
      throw err
    }
  })()
  return userListPromise
}
const loadCustomerList = async (query = DEFAULT_CUSTOMER_QUERY) => {
  if (customerListPromise) return customerListPromise
  customerListPromise = (async () => {
    try {
      const res = await customerList(query)
      customerOption.value = res || []
      return customerOption.value
    } catch (err) {
      console.error('获取客户列表失败:', err)
      customerOption.value = []
      customerListPromise = null
      throw err
    }
  })()
  return customerListPromise
}
// åˆ†é¡µé…ç½®
const page = reactive({
  current: 1,
  size: 100,
})
const total = ref(0)
// æœç´¢è¡¨å•
const searchForm = reactive({
  customerName: '',
  city: '',
  status: '',
  entryPerson: '',
  entryDate: [],
  entryDateStart: '',
  entryDateEnd: ''
})
// å¯¹è¯æ¡†ç›¸å…³
const dialogFormVisible = ref(false)
const operationType = ref('') // add, detail
const formRef = ref()
const form = reactive({
  id: undefined,
  status: undefined,
  province: '',
    city: '',
  customerName: '',
  industry: '',
  informationState: '',
  mainBusinessRevenue: '',
  customerScale: '',
  mainProducts: '',
  businessSource: '',
  contractAmount: '',
  description: '',
  renContent: '',
  paymentDescription: '',
  entryPerson: userStore.nickName,
  entryDate: dayjs().format('YYYY-MM-DD')
})
const renovationPlaceholder = '1.标准化:\n2.定制化:\n3.外采:'
// å˜æ›´è®°å½•数据(模拟数据)
const changeHistory = ref([])
// æ–‡ä»¶åˆ—表
const fileList = ref([])
// FileList组件引用
const fileListRef = ref(null)
const currentAttachmentRow = ref(null)
const filePreviewRef = ref(null)
// èŽ·å–çŠ¶æ€æ ‡ç­¾
const getStatusLabel = (statusValue) => {
  const status = statusOptions.find(item => item.value === statusValue)
  return status ? status.label : statusValue
}
// è¡¨å•验证规则
const rules = reactive({
  customerName: [
    { required: true, message: '请选择客户', trigger: 'change' }
  ],
  status: [
    { required: true, message: '请选择状态', trigger: 'change' }
  ],
  entryPerson: [
    { required: true, message: '请选择录入人', trigger: 'change' }
  ],
  entryDate: [
    { required: true, message: '请选择录入日期', trigger: 'change' }
  ]
})
// çŠ¶æ€é€‰é¡¹
const statusOptions = [
  { value: '新建', label: '新建' },
  { value: '项目跟踪', label: '项目跟踪' },
  { value: '放弃', label: '放弃' },
  { value: '合同签约', label: '合同签约' },
  { value: '备案申报', label: '备案申报' },
  { value: '项目交付', label: '项目交付' },
  { value: '项目验收', label: '项目验收' },
  { value: '项目回款', label: '项目回款' },
  { value: '回补贴', label: '回补贴' }
]
// çœä»½é€‰é¡¹
const provinceOptions = ref([])
const cityOptions = ref([])
// èŽ·å–çŠ¶æ€æ ‡ç­¾ç±»åž‹
const getStatusTagType = (status) => {
  const typeMap = {
    '新建': 'info',
    '项目跟踪': 'primary',
    '放弃': 'danger',
    '合同签约': 'warning',
    '备案申报': 'primary',
    '项目交付': 'success',
    '项目验收': 'success',
    '项目回款': 'success',
    '回补贴': 'success'
  }
  return typeMap[status] || 'info'
}
// èŽ·å–çŠ¶æ€æ–‡æœ¬
const getStatusText = (status) => {
  const textMap = {
    '新建': '新建',
    '项目跟踪': '项目跟踪',
    '放弃': '放弃',
    '合同签约': '合同签约',
    '备案申报': '备案申报',
    '项目交付': '项目交付',
    '项目验收': '项目验收',
    '项目回款': '项目回款',
    '回补贴': '回补贴'
  }
  return textMap[status] || '未知'
}
// æ ¼å¼åŒ–日期
const formatDate = (date) => {
  if (!date) return ''
  return dayjs(date).format('YYYY-MM-DD')
}
// æŸ¥è¯¢åˆ—表
const handleQuery = () => {
  page.current = 1
  getList()
}
// é‡ç½®æŸ¥è¯¢
const resetQuery = () => {
  Object.assign(searchForm, {
    customerName: '',
    city: '',
    status: '',
    entryPerson: '',
    entryDate: [],
    entryDateStart: '',
    entryDateEnd: ''
  })
  handleQuery()
}
// æ—¥æœŸèŒƒå›´å˜åŒ–
const changeDaterange = (val) => {
  if (val && val.length === 2) {
    searchForm.entryDateStart = val[0]
    searchForm.entryDateEnd = val[1]
  } else {
    searchForm.entryDateStart = ''
    searchForm.entryDateEnd = ''
  }
  handleQuery()
}
// èŽ·å–åˆ—è¡¨æ•°æ®
const getList = () => {
  tableLoading.value = true
  // åˆ›å»ºæŸ¥è¯¢å‚数,排除entryDate字段,只使用entryDateStart和entryDateEnd
  const { entryDate, ...queryParams } = searchForm
  const params = {
    ...queryParams,
    ...page
  }
  // åˆ é™¤ç©ºå€¼å‚æ•°
  Object.keys(params).forEach(key => {
    if (params[key] === '' || params[key] === null || params[key] === undefined) {
      delete params[key]
    }
  })
  opportunityListPage(params).then(res => {
    tableData.value = res.data.records || []
    total.value = res.data.total || 0
  }).catch(err => {
    console.error('获取商机列表失败:', err)
    tableData.value = []
    total.value = 0
  }).finally(() => {
    tableLoading.value = false
  })
}
// ç­¾çº¦é‡‘额合计(复用全局 summarizeTable)
const contractAmountSummaryMethod = (param) => {
  return proxy.summarizeTable(param, ["contractAmount"]);
}
// åˆ†é¡µå˜åŒ–
const paginationChange = (pagination) => {
  page.current = pagination.page
  page.size = pagination.limit
  getList()
}
// é€‰æ‹©å˜åŒ–
const handleSelectionChange = (selection) => {
  selectedRows.value = selection
}
// æ–°å»ºå•†æœº
const handleAdd = async () => {
  operationType.value = 'add'
  resetForm()
  // åŠ è½½ç”¨æˆ·åˆ—è¡¨å’Œå®¢æˆ·åˆ—è¡¨
  await loadUserList()
  await loadCustomerList()
    getProvinceList().then(res => {
        provinceOptions.value = res.data
    })
  dialogFormVisible.value = true
}
const getCityListChange = (id) => {
    getCityList({provinceId: id}).then(res => {
        cityOptions.value = res.data
    })
}
// æ·»åŠ æ“ä½œ
const handleAddOperation = async (row) => {
  operationType.value = 'addOperation'
  // æ¸…空附件列表
  fileList.value = []
  // åŠ è½½ç”¨æˆ·åˆ—è¡¨å’Œå®¢æˆ·åˆ—è¡¨
  await loadUserList()
  await loadCustomerList()
  // ä½¿ç”¨å½“前行数据作为基础,但只能修改状态和拜访记录;付款描述、改造内容等保留反显
  Object.assign(form, row, {
    // ä¿ç•™åŽŸå§‹å•†æœºID,用于关联操作记录
    status: row.status,
    description: '', // æ¸…空拜访记录,允许重新填写
    entryPerson: userStore.nickName, // è®¾ç½®å½•入人为当前账号
    entryDate: dayjs().format('YYYY-MM-DD') // è®¾ç½®å½•入时间为当天
  })
  dialogFormVisible.value = true
}
// æŸ¥çœ‹è¯¦æƒ…
const handleDetail = async (row) => {
  operationType.value = 'detail'
  // åŠ è½½ç”¨æˆ·åˆ—è¡¨å’Œå®¢æˆ·åˆ—è¡¨
  await loadUserList()
  await loadCustomerList()
  // ä½¿ç”¨updateTime作为录入时间反显
  Object.assign(form, row, {
    entryDateStart: row.updateTime || row.entryDateStart
  })
  // ç”Ÿæˆæ¨¡æ‹Ÿå˜æ›´è®°å½•
  generateChangeHistory(row)
  dialogFormVisible.value = true
}
// ç”Ÿæˆå˜æ›´è®°å½•
const generateChangeHistory = (row) => {
  // ä½¿ç”¨businessDescription数组数据生成变更记录
  const history = []
  if (row.businessDescription && Array.isArray(row.businessDescription)) {
    row.businessDescription.forEach((item, index) => {
      history.push({
        id: item.id || index,
        timestamp: item.entryDate || item.updateTime || item.createTime,
        operator: item.entryPerson || '系统',
        status: item.status,
        description: item.description,
        type: index === 0 ? 'current' : 'info',
        action: index === 0 ? '当前状态' : '历史记录'
      })
    })
  }
  changeHistory.value = history
}
// ç¼–辑商机
const handleEdit = async (row) => {
  operationType.value = 'edit'
  // åŠ è½½ç”¨æˆ·åˆ—è¡¨å’Œå®¢æˆ·åˆ—è¡¨
  await loadUserList()
  await loadCustomerList()
  // åŠ è½½çœä»½åˆ—è¡¨
  await getProvinceList().then(res => {
    provinceOptions.value = res.data
  })
  // å¦‚果后端返回的是name,需要转换为id
  let provinceId = row.province
  let cityId = row.city
  // å¦‚æžœprovince是name字符串,查找对应的id
  if (row.province && typeof row.province === 'string' && !/^\d+$/.test(row.province)) {
    const provinceOption = provinceOptions.value.find(item => item.name === row.province)
    if (provinceOption) {
      provinceId = provinceOption.id
      // åŠ è½½å¯¹åº”çš„åŸŽå¸‚åˆ—è¡¨
      await getCityList({ provinceId: provinceId }).then(res => {
        cityOptions.value = res.data
        // å¦‚æžœcity是name字符串,查找对应的id
        if (row.city && typeof row.city === 'string' && !/^\d+$/.test(row.city)) {
          const cityOption = cityOptions.value.find(item => item.name === row.city)
          if (cityOption) {
            cityId = cityOption.id
          }
        }
      })
    }
  } else if (row.province) {
    // å¦‚æžœprovince是id,直接加载城市列表
    await getCityList({ id: row.province }).then(res => {
      cityOptions.value = res.data
    })
  }
  // ä½¿ç”¨å½“前账号和当天日期作为默认值
  Object.assign(form, row, {
    province: provinceId, // ä½¿ç”¨è½¬æ¢åŽçš„id
    city: cityId, // ä½¿ç”¨è½¬æ¢åŽçš„id
    entryPerson: userStore.nickName, // è®¾ç½®å½•入人为当前账号
    entryDate: dayjs().format('YYYY-MM-DD') // è®¾ç½®å½•入时间为当天
  })
  dialogFormVisible.value = true
}
// å½•入人变化处理
const changs = (value) => {
  // å¯ä»¥æ ¹æ®éœ€è¦æ·»åŠ å¤„ç†é€»è¾‘
}
// æäº¤è¡¨å•
const submitForm = () => {
  formRef.value.validate(valid => {
    if (valid) {
      // æ”¶é›†é™„件文件列表
      const businessCommonFiles = fileList.value || []
      // å°†çœä»½å’Œå¸‚çš„id转换为name
      const provinceName = form.province ? provinceOptions.value.find(item => item.id === form.province)?.name || form.province : ''
      const cityName = form.city ? cityOptions.value.find(item => item.id === form.city)?.name || form.city : ''
      let api
      let successMessage
      let submitData
      if (operationType.value === 'add') {
        api = addOpportunity
        successMessage = '新建成功'
        submitData = {
          ...form,
          province: provinceName, // ä¼ name而不是id
          city: cityName, // ä¼ name而不是id
          businessCommonFiles: businessCommonFiles,
          type: 9  // å•†æœºç®¡ç†çš„类型标识
        }
      } else if (operationType.value === 'addOperation') {
        api = addDescription
        successMessage = '添加操作成功'
        // æ·»åŠ æ“ä½œæ—¶ä¼ é€’çŠ¶æ€ã€æè¿°ã€å½•å…¥äººã€å½•å…¥æ—¥æœŸã€é™„ä»¶å’Œå•†æœºID
        submitData = {
          status: form.status,
          description: form.description,
          paymentDescription: form.paymentDescription,
          entryPerson: form.entryPerson,
          entryDate: form.entryDate,
          businessCommonFiles: businessCommonFiles,
          type: 9,  // å•†æœºç®¡ç†çš„类型标识
          businessOpportunityId: form.id  // ä¼ é€’商机ID
        }
      } else {
        api = updateOpportunity
        successMessage = '修改成功'
        submitData = {
          ...form,
          province: provinceName, // ä¼ name而不是id
          city: cityName, // ä¼ name而不是id
          businessCommonFiles: businessCommonFiles,
          type: 9  // å•†æœºç®¡ç†çš„类型标识
        }
      }
      api(submitData).then(res => {
        if (res.code === 200) {
          proxy.$modal.msgSuccess(successMessage)
          closeDialog()
          getList()
        } else {
          proxy.$modal.msgError(res.msg || '操作失败')
        }
      }).catch(err => {
        console.log(err);
      })
    }
  })
}
// åˆ é™¤å•†æœº
const handleDelete = () => {
  if (selectedRows.value.length === 0) {
    proxy.$modal.msgWarning('请选择要删除的商机')
    return
  }
  ElMessageBox.confirm('确定删除选中的商机吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    const ids = selectedRows.value.map(item => item.id)
    delOpportunity(ids).then(res => {
      if (res.code === 200) {
        proxy.$modal.msgSuccess('删除成功')
        getList()
      } else {
        proxy.$modal.msgError(res.msg || '删除失败')
      }
    }).catch(err => {
      proxy.$modal.msgError('删除失败')
    })
  }).catch(() => {
    // ç”¨æˆ·å–消删除
  })
}
// é‡ç½®è¡¨å•
const resetForm = () => {
  Object.assign(form, {
    id: undefined,
    status: '',
    province: '',
    city: '',
    customerName: '',
    industry: '',
    informationState: '',
    mainBusinessRevenue: '',
    customerScale: '',
    mainProducts: '',
    businessSource: '',
    contractAmount: '',
    description: '',
    renContent: '',
    paymentDescription: '',
    entryPerson: userStore.nickName,
    entryDate: dayjs().format('YYYY-MM-DD')
  })
  if (formRef.value) {
    formRef.value.clearValidate()
  }
}
// å…³é—­å¯¹è¯æ¡†
const closeDialog = () => {
  dialogFormVisible.value = false
  resetForm()
}
// æŸ¥çœ‹é™„ä»¶
function handleAttachment(row) {
    currentAttachmentRow.value = row
    fileListRef.value.open(row.businessCommonFiles, row.id)
}
// ä¸‹è½½é™„件(详情页面)
function downloadAttachment(row) {
    proxy.$download.name(row.url)
}
// é¢„览附件(详情页面)
function previewAttachment(row) {
    if (filePreviewRef.value) {
        filePreviewRef.value.open(row.previewURL)
    } else {
        // å¦‚果没有预览组件,直接打开链接
        window.open(row.url, '_blank')
    }
}
// é™„件列表刷新
function handleFileListRefresh(rowId) {
    // é‡æ–°èŽ·å–åˆ—è¡¨æ•°æ®
    getList()
    // ç­‰å¾…列表数据更新后,找到对应的行并更新附件列表
    setTimeout(() => {
        if (currentAttachmentRow.value && tableData.value) {
            const updatedRow = tableData.value.find(item => item.id === currentAttachmentRow.value.id)
            if (updatedRow && updatedRow.businessCommonFiles) {
                currentAttachmentRow.value = updatedRow
                fileListRef.value.open(updatedRow.businessCommonFiles, updatedRow.id)
            }
        }
    }, 300)
}
onMounted(async () => {
  // åŠ è½½ç”¨æˆ·åˆ—è¡¨ä¾›æœç´¢ä½¿ç”¨
  await loadUserList()
  getList()
})
</script>
<style scoped lang="scss">
.app-container {
  padding: 20px;
}
.search_form {
  display: flex;
  align-items: flex-start;
    justify-content: space-between;
}
.table_list {
  margin-top: unset;
}
.dialog-footer {
  text-align: right;
}
:deep(.el-form-item__label) {
  font-weight: 500;
}
:deep(.el-table) {
  .el-table__header-wrapper {
    th {
      background-color: #f0f2f5;
      color: #333;
      font-weight: 600;
    }
  }
}
/* å˜æ›´è®°å½•时间线样式 */
.change-history-section {
  margin-top: 20px;
  .el-divider {
    margin: 20px 0;
  }
  .timeline-card {
    margin: 8px 0;
    border-radius: 8px;
    .card-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 8px 0;
      .action-type {
        font-weight: 600;
        color: #333;
      }
      .operator {
        font-size: 12px;
        color: #666;
      }
    }
    .change-content {
      .status-change, .description-change {
        margin-bottom: 8px;
        .label {
          font-weight: 500;
          color: #666;
          margin-right: 8px;
        }
        .description-text {
          color: #333;
          line-height: 1.5;
        }
      }
    }
  }
  /* æ—¶é—´çº¿æ ·å¼ä¼˜åŒ– */
  :deep(.el-timeline) {
    padding-left: 0;
    .el-timeline-item {
      .el-timeline-item__node {
        background-color: #409eff;
        &.el-timeline-item__node--primary {
          background-color: #409eff;
        }
        &.el-timeline-item__node--success {
          background-color: #67c23a;
        }
        &.el-timeline-item__node--info {
          background-color: #909399;
        }
        &.el-timeline-item__node--hollow {
          background-color: transparent;
          border-color: #409eff;
        }
      }
      .el-timeline-item__timestamp {
        color: #666;
        font-size: 12px;
      }
    }
  }
}
</style>