<template>
|
<div class="app-container">
|
<div class="search_form">
|
<el-form :model="searchForm" :inline="true">
|
<el-form-item label="省份">
|
<el-select
|
v-model="searchForm.provinceId"
|
placeholder="请选择省份"
|
clearable
|
filterable
|
style="width: 180px"
|
@change="handleSearchProvinceChange"
|
>
|
<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="城市">
|
<el-select
|
v-model="searchForm.cityId"
|
placeholder="请选择城市"
|
clearable
|
filterable
|
style="width: 180px"
|
@change="handleQuery"
|
>
|
<el-option
|
v-for="item in searchCityOptions"
|
:key="item.id"
|
:label="item.name"
|
:value="item.id"
|
/>
|
</el-select>
|
</el-form-item>
|
<el-form-item label="客户名称">
|
<el-select
|
v-model="searchForm.customerName"
|
placeholder="请选择客户名称"
|
clearable
|
filterable
|
style="width: 200px"
|
@change="handleQuery"
|
>
|
<el-option
|
v-for="item in customerOptions"
|
:key="item.id || item.customerName"
|
:label="item.customerName"
|
:value="item.customerName"
|
>
|
{{ item.customerName }}{{ item.taxpayerIdentificationNumber ? ` - ${item.taxpayerIdentificationNumber}` : "" }}
|
</el-option>
|
</el-select>
|
</el-form-item>
|
<el-form-item label="录入人">
|
<el-input
|
v-model="searchForm.entryPerson"
|
placeholder="请输入录入人"
|
clearable
|
style="width: 180px"
|
@change="handleQuery"
|
/>
|
</el-form-item>
|
<el-form-item label="录入日期">
|
<el-date-picker
|
v-model="searchForm.entryDateRange"
|
type="daterange"
|
value-format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
range-separator="-"
|
start-placeholder="开始日期"
|
end-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
|
stripe
|
v-loading="tableLoading"
|
height="calc(100vh - 18.5em)"
|
@selection-change="handleSelectionChange"
|
>
|
<el-table-column type="selection" width="55" align="center" fixed="left" />
|
<el-table-column type="index" label="序号" width="60" align="center" fixed="left" />
|
<el-table-column prop="province" label="省份" min-width="120" />
|
<el-table-column prop="city" label="城市" min-width="120" />
|
<el-table-column prop="customerName" label="客户名称" min-width="220" show-overflow-tooltip />
|
<el-table-column prop="contractAmount" label="中标金额" min-width="130" />
|
<el-table-column prop="bidBond" label="投标保证金" min-width="130" />
|
<el-table-column prop="winningServiceFee" label="中标服务费" min-width="130" />
|
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
|
<el-table-column prop="entryPerson" label="录入人" min-width="100" />
|
<el-table-column prop="entryDate" label="录入日期" min-width="120" />
|
<el-table-column prop="updateTime" label="修改时间" min-width="170" />
|
<el-table-column fixed="right" label="操作" width="180" 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="handleDetail(row)">
|
详情
|
</el-button>
|
<el-button link type="danger" size="small" @click="handleDelete(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="dialogVisible"
|
:title="dialogTitle"
|
width="900px"
|
@close="closeDialog"
|
>
|
<el-form
|
ref="formRef"
|
:model="form"
|
:rules="rules"
|
label-width="100px"
|
:disabled="dialogMode === 'detail'"
|
>
|
<el-row :gutter="20">
|
<el-col :span="24">
|
<el-form-item label="客户名称" prop="customerName">
|
<el-select
|
v-model="form.customerName"
|
placeholder="请选择客户名称"
|
clearable
|
filterable
|
style="width: 100%"
|
>
|
<el-option
|
v-for="item in customerOptions"
|
:key="item.id || item.customerName"
|
:label="item.customerName"
|
:value="item.customerName"
|
>
|
{{ item.customerName }}{{ item.taxpayerIdentificationNumber ? ` - ${item.taxpayerIdentificationNumber}` : "" }}
|
</el-option>
|
</el-select>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
|
<el-row :gutter="20">
|
<el-col :span="12">
|
<el-form-item label="省份" prop="provinceId">
|
<el-select
|
v-model="form.provinceId"
|
placeholder="请选择省份"
|
filterable
|
style="width: 100%"
|
@change="handleFormProvinceChange"
|
>
|
<el-option
|
v-for="item in provinceOptions"
|
:key="item.id"
|
:label="item.name"
|
:value="item.id"
|
/>
|
</el-select>
|
</el-form-item>
|
</el-col>
|
<el-col :span="12">
|
<el-form-item label="城市" prop="cityId">
|
<el-select
|
v-model="form.cityId"
|
placeholder="请选择城市"
|
filterable
|
style="width: 100%"
|
>
|
<el-option
|
v-for="item in cityOptions"
|
:key="item.id"
|
:label="item.name"
|
:value="item.id"
|
/>
|
</el-select>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
|
<el-row :gutter="20">
|
<el-col :span="12">
|
<el-form-item label="中标金额" prop="contractAmount">
|
<el-input-number
|
v-model="form.contractAmount"
|
:min="0"
|
:precision="2"
|
style="width: 100%"
|
controls-position="right"
|
/>
|
</el-form-item>
|
</el-col>
|
<el-col :span="12">
|
<el-form-item label="投标保证金" prop="bidBond">
|
<el-input-number
|
v-model="form.bidBond"
|
:min="0"
|
:precision="2"
|
style="width: 100%"
|
controls-position="right"
|
/>
|
</el-form-item>
|
</el-col>
|
<el-col :span="12">
|
<el-form-item label="中标服务费" prop="winningServiceFee">
|
<el-input-number
|
v-model="form.winningServiceFee"
|
:min="0"
|
:precision="2"
|
style="width: 100%"
|
controls-position="right"
|
/>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
|
<el-row :gutter="20">
|
<el-col :span="12">
|
<el-form-item label="录入人" prop="entryPerson">
|
<el-select
|
v-model="form.entryPerson"
|
placeholder="请选择录入人"
|
filterable
|
allow-create
|
default-first-option
|
style="width: 100%"
|
>
|
<el-option
|
v-for="item in userOptions"
|
:key="item.userId || 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
|
v-model="form.entryDate"
|
type="date"
|
value-format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
placeholder="请选择录入日期"
|
style="width: 100%"
|
/>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
|
<el-form-item label="备注" prop="remark">
|
<el-input
|
v-model="form.remark"
|
type="textarea"
|
:rows="3"
|
placeholder="请输入备注"
|
/>
|
</el-form-item>
|
|
<el-form-item label="附件" v-if="dialogMode !== 'detail'">
|
<el-upload
|
ref="fileUploadRef"
|
v-model:file-list="fileList"
|
:action="upload.url"
|
:headers="upload.headers"
|
:data="upload.data"
|
multiple
|
auto-upload
|
:before-upload="handleBeforeUpload"
|
:on-success="handleUploadSuccess"
|
:on-error="handleUploadError"
|
:on-remove="handleRemove"
|
>
|
<el-button type="primary">上传附件</el-button>
|
<template #tip>
|
<div class="el-upload__tip">支持常见办公文档与图片格式</div>
|
</template>
|
</el-upload>
|
</el-form-item>
|
|
</el-form>
|
|
<div v-if="dialogMode === 'detail'">
|
<el-divider content-position="left">附件</el-divider>
|
<el-table :data="form.commonFiles || []" border stripe empty-text="暂无附件">
|
<el-table-column prop="name" label="附件名称" min-width="300" show-overflow-tooltip />
|
<el-table-column label="操作" width="160" align="center">
|
<template #default="{ row }">
|
<el-button link type="primary" size="small" @click="downloadFile(row)">
|
下载
|
</el-button>
|
<el-button link type="primary" size="small" @click="previewFile(row)">
|
预览
|
</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
</div>
|
<filePreview ref="filePreviewRef" />
|
|
<template #footer>
|
<div class="dialog-footer">
|
<el-button @click="closeDialog">取消</el-button>
|
<el-button type="primary" v-if="dialogMode !== 'detail'" @click="submitForm">
|
确定
|
</el-button>
|
</div>
|
</template>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script setup>
|
import { computed, getCurrentInstance, onMounted, reactive, ref } from "vue";
|
import dayjs from "dayjs";
|
import { ElMessageBox } from "element-plus";
|
import pagination from "@/components/PIMTable/Pagination.vue";
|
import filePreview from "@/components/filePreview/index.vue";
|
import useUserStore from "@/store/modules/user";
|
import { getToken } from "@/utils/auth";
|
import { userListNoPage } from "@/api/system/user";
|
import { customerList } from "@/api/salesManagement/salesLedger";
|
import {
|
bidWinningLedgerAdd,
|
bidWinningLedgerDelete,
|
bidWinningLedgerDetail,
|
bidWinningLedgerListPage,
|
bidWinningLedgerUpdate,
|
getCityList,
|
getProvinceList,
|
} from "@/api/salesManagement/bidWinningLedger";
|
|
const { proxy } = getCurrentInstance();
|
const userStore = useUserStore();
|
|
const tableData = ref([]);
|
const tableLoading = ref(false);
|
const selectedRows = ref([]);
|
const total = ref(0);
|
const userOptions = ref([]);
|
const customerOptions = ref([]);
|
const provinceOptions = ref([]);
|
const cityOptions = ref([]);
|
const searchCityOptions = ref([]);
|
const dialogVisible = ref(false);
|
const dialogMode = ref("add");
|
const formRef = ref();
|
const fileUploadRef = ref();
|
const filePreviewRef = ref();
|
const fileList = ref([]);
|
|
const upload = reactive({
|
url: `${import.meta.env.VITE_APP_BASE_API}/file/upload`,
|
headers: {
|
Authorization: `Bearer ${getToken()}`,
|
},
|
data: {
|
type: 10,
|
},
|
});
|
|
const page = reactive({
|
current: 1,
|
size: 20,
|
});
|
|
const createSearchForm = () => ({
|
provinceId: "",
|
cityId: "",
|
customerName: "",
|
entryPerson: "",
|
entryDateRange: [],
|
entryDateStart: "",
|
entryDateEnd: "",
|
});
|
|
const createFormData = () => ({
|
id: undefined,
|
province: "",
|
city: "",
|
provinceId: "",
|
cityId: "",
|
customerName: "",
|
contractAmount: undefined,
|
bidBond: undefined,
|
winningServiceFee: undefined,
|
remark: "",
|
entryPerson: userStore.nickName || "",
|
entryDate: dayjs().format("YYYY-MM-DD"),
|
commonFiles: [],
|
tempFileIds: [],
|
});
|
|
const searchForm = reactive(createSearchForm());
|
const form = reactive(createFormData());
|
|
const rules = reactive({
|
provinceId: [{ required: true, message: "请选择省份", trigger: "change" }],
|
cityId: [{ required: true, message: "请选择城市", trigger: "change" }],
|
customerName: [{ required: true, message: "请选择客户名称", trigger: "change" }],
|
entryPerson: [{ required: true, message: "请选择录入人", trigger: "change" }],
|
entryDate: [{ required: true, message: "请选择录入日期", trigger: "change" }],
|
});
|
|
const dialogTitle = computed(() => {
|
if (dialogMode.value === "add") return "新增中标台账";
|
if (dialogMode.value === "edit") return "编辑中标台账";
|
return "中标台账详情";
|
});
|
|
const getOptionName = (options, id) => {
|
const target = options.find((item) => String(item.id) === String(id));
|
return target?.name || "";
|
};
|
|
const buildQueryParams = () => {
|
const params = {
|
current: page.current,
|
size: page.size,
|
customerName: searchForm.customerName,
|
entryPerson: searchForm.entryPerson,
|
province: getOptionName(provinceOptions.value, searchForm.provinceId),
|
city: getOptionName(searchCityOptions.value, searchForm.cityId),
|
};
|
|
if (searchForm.entryDateStart) {
|
params.entryDateStart = searchForm.entryDateStart;
|
}
|
if (searchForm.entryDateEnd) {
|
params.entryDateEnd = searchForm.entryDateEnd;
|
}
|
|
Object.keys(params).forEach((key) => {
|
if (params[key] === "" || params[key] === undefined || params[key] === null) {
|
delete params[key];
|
}
|
});
|
|
return params;
|
};
|
|
const getList = async () => {
|
tableLoading.value = true;
|
try {
|
const res = await bidWinningLedgerListPage(buildQueryParams());
|
tableData.value = res?.data?.records || [];
|
total.value = res?.data?.total || 0;
|
} finally {
|
tableLoading.value = false;
|
}
|
};
|
|
const loadProvinceOptions = async () => {
|
const res = await getProvinceList();
|
provinceOptions.value = res?.data || [];
|
};
|
|
const loadCityOptions = async (provinceId, target = "form") => {
|
if (!provinceId) {
|
if (target === "form") {
|
cityOptions.value = [];
|
} else {
|
searchCityOptions.value = [];
|
}
|
return [];
|
}
|
const res = await getCityList({ provinceId });
|
const list = res?.data || [];
|
if (target === "form") {
|
cityOptions.value = list;
|
} else {
|
searchCityOptions.value = list;
|
}
|
return list;
|
};
|
|
const loadUserOptions = async () => {
|
const res = await userListNoPage();
|
userOptions.value = res?.data || [];
|
};
|
|
const loadCustomerOptions = async () => {
|
const res = await customerList();
|
customerOptions.value = res || [];
|
};
|
|
const changeDateRange = (value) => {
|
if (value?.length === 2) {
|
searchForm.entryDateStart = value[0];
|
searchForm.entryDateEnd = value[1];
|
} else {
|
searchForm.entryDateStart = "";
|
searchForm.entryDateEnd = "";
|
}
|
handleQuery();
|
};
|
|
const handleSearchProvinceChange = async () => {
|
searchForm.cityId = "";
|
await loadCityOptions(searchForm.provinceId, "search");
|
handleQuery();
|
};
|
|
const handleFormProvinceChange = async () => {
|
form.cityId = "";
|
await loadCityOptions(form.provinceId, "form");
|
};
|
|
const handleQuery = () => {
|
page.current = 1;
|
getList();
|
};
|
|
const resetQuery = async () => {
|
Object.assign(searchForm, createSearchForm());
|
searchCityOptions.value = [];
|
page.current = 1;
|
await getList();
|
};
|
|
const paginationChange = ({ page: current, limit }) => {
|
page.current = current;
|
page.size = limit;
|
getList();
|
};
|
|
const handleSelectionChange = (rows) => {
|
selectedRows.value = rows;
|
};
|
|
const resetFormState = () => {
|
Object.assign(form, createFormData());
|
cityOptions.value = [];
|
fileList.value = [];
|
formRef.value?.clearValidate();
|
};
|
|
const mapNameToId = (options, name) => {
|
const target = options.find((item) => item.name === name);
|
return target?.id || "";
|
};
|
|
const openDialog = async (mode, row) => {
|
dialogMode.value = mode;
|
resetFormState();
|
await Promise.all([loadUserOptions(), loadProvinceOptions(), loadCustomerOptions()]);
|
|
if (mode === "add") {
|
dialogVisible.value = true;
|
return;
|
}
|
|
const res = await bidWinningLedgerDetail({ id: row.id });
|
const detail = res?.data || {};
|
|
Object.assign(form, detail, {
|
entryDate: detail.entryDate ? dayjs(detail.entryDate).format("YYYY-MM-DD") : "",
|
commonFiles: detail.commonFiles || [],
|
});
|
|
form.provinceId = mapNameToId(provinceOptions.value, detail.province);
|
await loadCityOptions(form.provinceId, "form");
|
form.cityId = mapNameToId(cityOptions.value, detail.city);
|
fileList.value = (detail.commonFiles || []).map((item) => ({
|
name: item.name,
|
url: item.url,
|
id: item.id,
|
}));
|
dialogVisible.value = true;
|
};
|
|
const handleAdd = () => {
|
openDialog("add");
|
};
|
|
const handleEdit = (row) => {
|
openDialog("edit", row);
|
};
|
|
const handleDetail = (row) => {
|
openDialog("detail", row);
|
};
|
|
const buildSubmitData = () => {
|
const tempFileIds = fileList.value
|
.map((item) => item.tempId)
|
.filter((item) => item !== undefined && item !== null && item !== "");
|
|
return {
|
id: form.id,
|
province: getOptionName(provinceOptions.value, form.provinceId),
|
city: getOptionName(cityOptions.value, form.cityId),
|
customerName: form.customerName,
|
contractAmount: form.contractAmount,
|
bidBond: form.bidBond,
|
winningServiceFee: form.winningServiceFee,
|
remark: form.remark,
|
entryPerson: form.entryPerson,
|
entryDate: form.entryDate,
|
tempFileIds,
|
};
|
};
|
|
const submitForm = () => {
|
formRef.value.validate(async (valid) => {
|
if (!valid) {
|
return;
|
}
|
|
const api = dialogMode.value === "edit" ? bidWinningLedgerUpdate : bidWinningLedgerAdd;
|
await api(buildSubmitData());
|
proxy.$modal.msgSuccess(dialogMode.value === "edit" ? "修改成功" : "新增成功");
|
closeDialog();
|
getList();
|
});
|
};
|
|
const handleDelete = (row) => {
|
const ids = row ? [row.id] : selectedRows.value.map((item) => item.id);
|
if (!ids.length) {
|
proxy.$modal.msgWarning("请选择要删除的中标台账");
|
return;
|
}
|
|
ElMessageBox.confirm("确认删除选中的中标台账吗?", "提示", {
|
confirmButtonText: "确定",
|
cancelButtonText: "取消",
|
type: "warning",
|
})
|
.then(async () => {
|
await bidWinningLedgerDelete(ids);
|
proxy.$modal.msgSuccess("删除成功");
|
getList();
|
})
|
.catch(() => {});
|
};
|
|
const closeDialog = () => {
|
dialogVisible.value = false;
|
resetFormState();
|
};
|
|
const handleBeforeUpload = () => {
|
proxy.$modal.loading("正在上传文件,请稍候...");
|
return true;
|
};
|
|
const handleUploadSuccess = (res, file) => {
|
proxy.$modal.closeLoading();
|
if (res.code === 200) {
|
file.tempId = res.data?.tempId;
|
proxy.$modal.msgSuccess("上传成功");
|
} else {
|
proxy.$modal.msgError(res.msg || "上传失败");
|
fileUploadRef.value?.handleRemove(file);
|
}
|
};
|
|
const handleUploadError = () => {
|
proxy.$modal.closeLoading();
|
proxy.$modal.msgError("上传失败");
|
};
|
|
const handleRemove = () => {};
|
|
const downloadFile = (row) => {
|
if (row?.url) {
|
proxy.$download.name(row.url);
|
}
|
};
|
|
const previewFile = (row) => {
|
if (row?.url) {
|
filePreviewRef.value.open(row.url);
|
}
|
};
|
|
onMounted(async () => {
|
await Promise.all([loadProvinceOptions(), loadUserOptions(), loadCustomerOptions()]);
|
getList();
|
});
|
</script>
|
|
<style scoped lang="scss">
|
.table_list {
|
margin-top: unset;
|
}
|
|
.dialog-footer {
|
text-align: right;
|
}
|
|
:deep(.el-input-number) {
|
width: 100%;
|
}
|
</style>
|