From 6f106e43af7a822120af300770115037ead2652a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=AB=E5=9C=B0=E5=83=A7?= <357099073@qq.com> Date: Mon, 14 Sep 2026 00:09:58 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=8D=95=E4=BD=8D=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/api/crmPayback.js | 5 + backend/src/api/crmUnit.js | 36 ++ backend/src/router/index.js | 19 + .../contract/components/detail_payback.vue | 100 +++- backend/src/views/apps/crm/payback/index.vue | 103 +++- .../apps/crm/product/components/edit.vue | 37 +- backend/src/views/apps/crm/setting/index.vue | 104 +++++ .../apps/crm/setting/unit/components/edit.vue | 126 +++++ .../src/views/apps/crm/setting/unit/index.vue | 175 +++++++ .../components/contactDetailDialog.vue | 16 +- .../apps/oa/document/documentgraph/index.vue | 87 +++- .../components/fileDetailDrawer.vue | 106 +++-- go/controllers/backend_crm_payback.go | 48 ++ go/controllers/backend_crm_unit.go | 439 ++++++++++++++++++ go/docs/sql/create_backend_crm_unit.sql | 28 ++ go/models/init.go | 30 ++ go/models/tenant_crm_unit.go | 27 ++ go/routers/backend/backend.go | 9 +- .../accountpool/codex/components/detail.vue | 22 +- .../accountpool/cursor/components/detail.vue | 40 +- .../accountpool/kiro/components/detail.vue | 22 +- .../windsurf/components/detail.vue | 22 +- 22 files changed, 1473 insertions(+), 128 deletions(-) create mode 100644 backend/src/api/crmUnit.js create mode 100644 backend/src/views/apps/crm/setting/index.vue create mode 100644 backend/src/views/apps/crm/setting/unit/components/edit.vue create mode 100644 backend/src/views/apps/crm/setting/unit/index.vue create mode 100644 go/controllers/backend_crm_unit.go create mode 100644 go/docs/sql/create_backend_crm_unit.sql create mode 100644 go/models/tenant_crm_unit.go diff --git a/backend/src/api/crmPayback.js b/backend/src/api/crmPayback.js index 140d31f..1ced323 100644 --- a/backend/src/api/crmPayback.js +++ b/backend/src/api/crmPayback.js @@ -66,3 +66,8 @@ export function createPaybackRecord(data) { export function deletePaybackRecord(id) { return request({ url: `/backend/crm/payback/record/${id}`, method: "delete" }); } + +/** 更新回款记录回执(补传流水回执):仅更新回执,不改金额与状态 */ +export function updatePaybackRecord(id, data) { + return request({ url: `/backend/crm/payback/record/${id}`, method: "put", data }); +} diff --git a/backend/src/api/crmUnit.js b/backend/src/api/crmUnit.js new file mode 100644 index 0000000..b8e9684 --- /dev/null +++ b/backend/src/api/crmUnit.js @@ -0,0 +1,36 @@ +import request from "@/utils/request"; + +/** + * CRM 基本设置 - 计量单位接口。 + * 单位用于产品管理等业务(产品「单位」下拉取自本表)。 + */ + +export function getUnitList(params) { + return request({ url: "/backend/crm/unit/list", method: "get", params }); +} + +/** 下拉数据源:仅返回启用中的单位 */ +export function getAllUnits(params) { + return request({ url: "/backend/crm/unit/all", method: "get", params }); +} + +export function getUnitDetail(id) { + return request({ url: `/backend/crm/unit/${id}`, method: "get" }); +} + +export function createUnit(data) { + return request({ url: "/backend/crm/unit", method: "post", data }); +} + +export function updateUnit(id, data) { + return request({ url: `/backend/crm/unit/${id}`, method: "put", data }); +} + +export function deleteUnit(id) { + return request({ url: `/backend/crm/unit/${id}`, method: "delete" }); +} + +/** 补齐缺失的系统默认单位(套 / 台 / 个 …) */ +export function initDefaultUnits() { + return request({ url: "/backend/crm/unit/init-default", method: "post" }); +} diff --git a/backend/src/router/index.js b/backend/src/router/index.js index f3d3a8a..b41817f 100644 --- a/backend/src/router/index.js +++ b/backend/src/router/index.js @@ -82,6 +82,25 @@ const staticMainChildren = [ path: "/apps/crm/product/cate.vue", redirect: "/apps/crm/product/cate" }, + // CRM 基本设置(单位设置等基础数据维护入口) + { + path: "/apps/crm/setting", + name: "CrmSetting", + component: () => import("@/views/apps/crm/setting/index.vue"), + meta: { requiresAuth: true, title: "基本设置", modulePath: "/apps/crm" } + }, + // CRM 单位设置(产品等业务的「单位」下拉取自该表) + { + path: "/apps/crm/setting/unit", + name: "CrmSettingUnit", + component: () => import("@/views/apps/crm/setting/unit/index.vue"), + meta: { requiresAuth: true, title: "单位设置", modulePath: "/apps/crm" } + }, + // 兼容带 .vue 后缀的菜单路径 + { + path: "/apps/crm/setting/unit.vue", + redirect: "/apps/crm/setting/unit" + }, { path: "/user/userProfile", name: "userProfile", diff --git a/backend/src/views/apps/crm/contract/components/detail_payback.vue b/backend/src/views/apps/crm/contract/components/detail_payback.vue index e21c001..eed8488 100644 --- a/backend/src/views/apps/crm/contract/components/detail_payback.vue +++ b/backend/src/views/apps/crm/contract/components/detail_payback.vue @@ -90,8 +90,17 @@ {{ row.remark || "-" }} - + + + 补回执 + 保存 + + + + + + + + 上传流水回执 + + + + + {{ receiptForm.receipt_name || "流水回执" }} + + 移除 + + + + + + 取消 + 保存回执 + + @@ -203,6 +241,7 @@ import { getPaybackList, getPaybackRecordList, createPaybackRecord, + updatePaybackRecord, deletePaybackRecord, } from "@/api/crmPayback"; import { uploadFile } from "@/api/file"; @@ -404,6 +443,65 @@ function clearReceipt() { receiveForm.receipt_name = ""; } +/* ---------------------------- 补传回款回执(事后补凭证) ---------------------------- */ +const receiptVisible = ref(false); +const receiptUploading = ref(false); +const receiptForm = reactive({ id: null, receipt_url: "", receipt_name: "" }); + +/** 打开补回执:仅针对未上传回执的回款记录 */ +function openReceipt(row) { + receiptForm.id = row.id; + receiptForm.receipt_url = ""; + receiptForm.receipt_name = ""; + receiptVisible.value = true; +} + +async function handleReceiptFileUpload(options) { + const file = options.file; + receiptUploading.value = true; + try { + const fd = new FormData(); + fd.append("file", file); + const res = await uploadFile(fd); + const data = res?.data || {}; + receiptForm.receipt_url = data.url || ""; + receiptForm.receipt_name = data.name || file.name; + ElMessage.success("上传成功"); + } catch (e) { + ElMessage.error(e.message || "上传失败"); + } finally { + receiptUploading.value = false; + } +} + +function clearReceiptForm() { + receiptForm.receipt_url = ""; + receiptForm.receipt_name = ""; +} + +async function submitReceipt() { + if (!receiptForm.id) return; + if (!receiptForm.receipt_url) { + ElMessage.warning("请先上传回执文件"); + return; + } + receiptUploading.value = true; + try { + await updatePaybackRecord(receiptForm.id, { + receipt_url: receiptForm.receipt_url, + receipt_name: receiptForm.receipt_name, + }); + ElMessage.success("回执已补传"); + receiptVisible.value = false; + await fetchRecords(); + emit("saved"); + } catch (e) { + ElMessage.error(e.message || "补传失败"); + } finally { + receiptUploading.value = false; + } +} + async function submitReceive() { if (!receiveForm.payback_id) { ElMessage.warning("请选择回款计划"); diff --git a/backend/src/views/apps/crm/payback/index.vue b/backend/src/views/apps/crm/payback/index.vue index 76b9608..a74f867 100644 --- a/backend/src/views/apps/crm/payback/index.vue +++ b/backend/src/views/apps/crm/payback/index.vue @@ -3,7 +3,7 @@ 回款管理 - 针对于合同的回款计划创建与回款明细跟踪(点击「详情」登记一笔笔回款) + 针对于合同的回款计划创建与回款明细跟踪 刷新 @@ -163,6 +163,15 @@ > 回执 + + 补回执 + + + + + + + + + 上传流水回执 + + + + + {{ receiptForm.receipt_name || "流水回执" }} + + 移除 + + + + + + 取消 + 保存回执 + + diff --git a/backend/src/views/apps/crm/product/components/edit.vue b/backend/src/views/apps/crm/product/components/edit.vue index de4ee7a..b82107c 100644 --- a/backend/src/views/apps/crm/product/components/edit.vue +++ b/backend/src/views/apps/crm/product/components/edit.vue @@ -50,8 +50,15 @@ - - + + @@ -65,7 +72,7 @@ 📁 编辑模块树({{ (form.dev_tree || []).length }} 项 · ¥{{ formatMoney(treeSellTotal(form.dev_tree, form.dev_unit_price)) }}) - 叶子模块只填「人天」,人天单价统一;父级自动汇总金额 + @@ -122,6 +129,7 @@ import { ref, reactive, watch } from "vue"; import { ElMessage } from "element-plus"; import { createProduct, updateProduct } from "@/api/crmProduct"; import { getProductCategoryList } from "@/api/crmProductCategory"; +import { getUnitList } from "@/api/crmUnit"; import { PRODUCT_STATUS_OPTIONS, PRODUCT_UNIT_OPTIONS, formatMoney } from "../../dict"; import { treeSellTotal } from "../../contract/components/utils"; import DevModuleTree from "../../contract/components/DevModuleTree.vue"; @@ -137,6 +145,7 @@ const submitting = ref(false); const isEdit = ref(false); const internalId = ref(null); const categoryOptions = ref([]); +const unitOptions = ref([]); const devVisible = ref(false); const defaultForm = () => ({ @@ -205,6 +214,7 @@ watch( Object.assign(form, defaultForm()); } fetchCategories(); + fetchUnits(); } } ); @@ -219,6 +229,27 @@ async function fetchCategories() { } } +/** 单位下拉合并:单位设置中的启用项 + 内置默认单位 + 当前产品的历史单位值 */ +function mergeUnitOptions(names) { + const all = (names || []).filter(Boolean); + PRODUCT_UNIT_OPTIONS.forEach((i) => { + if (!all.includes(i.value)) all.push(i.value); + }); + if (form.unit && !all.includes(form.unit)) all.push(form.unit); + return all; +} + +/** 加载计量单位(取自「基本设置 - 单位设置」,取启用中的单位) */ +async function fetchUnits() { + try { + const res = await getUnitList({ page: 1, pageSize: 200, status: 1 }); + const names = (res?.data?.list || []).map((i) => i.name); + unitOptions.value = mergeUnitOptions(names); + } catch (e) { + unitOptions.value = mergeUnitOptions([]); + } +} + function handleClose() { emit("update:visible", false); } diff --git a/backend/src/views/apps/crm/setting/index.vue b/backend/src/views/apps/crm/setting/index.vue new file mode 100644 index 0000000..30d8bde --- /dev/null +++ b/backend/src/views/apps/crm/setting/index.vue @@ -0,0 +1,104 @@ + + + + + 基本设置 + 维护 CRM 各模块的基础数据,设置后立即在对应业务表单中生效 + + + + + + + + + + 单位设置 + 维护产品、报价等业务使用的计量单位(套 / 台 / 个 …) + + + + + + + + + + + + diff --git a/backend/src/views/apps/crm/setting/unit/components/edit.vue b/backend/src/views/apps/crm/setting/unit/components/edit.vue new file mode 100644 index 0000000..e1017b3 --- /dev/null +++ b/backend/src/views/apps/crm/setting/unit/components/edit.vue @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + 取消 + 保存 + + + + + diff --git a/backend/src/views/apps/crm/setting/unit/index.vue b/backend/src/views/apps/crm/setting/unit/index.vue new file mode 100644 index 0000000..6ff9a0b --- /dev/null +++ b/backend/src/views/apps/crm/setting/unit/index.vue @@ -0,0 +1,175 @@ + + + + + 单位设置 + 维护产品、报价等业务使用的计量单位,产品表单的「单位」下拉取自本表 + + + 刷新 + 恢复默认单位 + 新增单位 + + + + + + + + + + + + + + + 查询 + 重置 + + + + + + + + + {{ row.code || "-" }} + + + + + {{ productStatusText(row.status) }} + + + + + {{ row.used_count }} 个产品 + - + + + + {{ row.remark || "-" }} + + + + 编辑 + 删除 + + + + + + + + + + + + + + + + + diff --git a/backend/src/views/apps/oa/contact/components/contactDetailDialog.vue b/backend/src/views/apps/oa/contact/components/contactDetailDialog.vue index e49815c..f43ff29 100644 --- a/backend/src/views/apps/oa/contact/components/contactDetailDialog.vue +++ b/backend/src/views/apps/oa/contact/components/contactDetailDialog.vue @@ -143,15 +143,15 @@ const handleClose = () => { display: flex; gap: 16px; padding: 16px; - background: #f5f7fa; + background: var(--el-fill-color-light); border-radius: 8px; margin-bottom: 20px; } .detail-avatar { flex-shrink: 0; - background: #409eff; - color: #fff; + background: var(--el-color-primary); + color: var(--el-color-white); font-size: 22px; } @@ -171,12 +171,12 @@ const handleClose = () => { .detail-name { font-size: 18px; font-weight: 600; - color: #303133; + color: var(--el-text-color-primary); } .detail-sub { font-size: 13px; - color: #606266; + color: var(--el-text-color-regular); } .detail-section { @@ -187,11 +187,11 @@ const handleClose = () => { margin: 0 0 12px; font-size: 14px; font-weight: 600; - color: #303133; + color: var(--el-text-color-primary); } .contact-link { - color: #409eff; + color: var(--el-color-primary); text-decoration: none; } @@ -200,7 +200,7 @@ const handleClose = () => { } .empty-text { - color: #c0c4cc; + color: var(--el-text-color-placeholder); } .dialog-footer { diff --git a/backend/src/views/apps/oa/document/documentgraph/index.vue b/backend/src/views/apps/oa/document/documentgraph/index.vue index 2281cda..10ad5fb 100644 --- a/backend/src/views/apps/oa/document/documentgraph/index.vue +++ b/backend/src/views/apps/oa/document/documentgraph/index.vue @@ -16,7 +16,13 @@ - + - + 0)缩小,向上滚放大 + const inside = + e.clientX >= rect.left && + e.clientX <= rect.right && + e.clientY >= rect.top && + e.clientY <= rect.bottom; + + chart.dispatchAction({ + type: "graphRoam", + seriesIndex: 0, + zoom, + originX: inside ? e.clientX - rect.left : rect.width / 2, + originY: inside ? e.clientY - rect.top : rect.height / 2 + }); +} + function onPanMouseMove(e) { if (!panning.value) return; // 右键在画布外松开时 mouseup 不会触发,用 buttons 兜底结束拖拽 @@ -587,7 +632,7 @@ function onPanMouseUp(e) { function stopPan() { if (!panning.value) return; panning.value = false; - chartRef.value?.classList.remove("is-panning"); + graphCardRef.value?.classList.remove("is-panning"); window.removeEventListener("mousemove", onPanMouseMove); window.removeEventListener("mouseup", onPanMouseUp); } @@ -765,12 +810,12 @@ onBeforeUnmount(() => { .chart { width: 100%; height: 100%; +} - // 右键拖拽平移时强制抓手光标(zrender 会改写 canvas 内联 cursor) - &.is-panning, - &.is-panning * { - cursor: grabbing !important; - } +// 右键拖拽平移时整卡强制抓手光标(zrender 会改写 canvas 内联 cursor) +.graph-card.is-panning, +.graph-card.is-panning * { + cursor: grabbing !important; } .chart-empty { diff --git a/backend/src/views/apps/oa/employeefile/components/fileDetailDrawer.vue b/backend/src/views/apps/oa/employeefile/components/fileDetailDrawer.vue index 0a5ff68..4e43655 100644 --- a/backend/src/views/apps/oa/employeefile/components/fileDetailDrawer.vue +++ b/backend/src/views/apps/oa/employeefile/components/fileDetailDrawer.vue @@ -159,29 +159,18 @@ + 支持图片 / PDF,单个不超过 10MB,可多选批量上传,数量不限 - - {{ file.education_photo ? '替换学历照片' : '上传学历照片' }} + + 批量上传证照 - - 上传证照 - @@ -238,8 +227,9 @@
针对于合同的回款计划创建与回款明细跟踪(点击「详情」登记一笔笔回款)
针对于合同的回款计划创建与回款明细跟踪
维护 CRM 各模块的基础数据,设置后立即在对应业务表单中生效
维护产品、报价等业务使用的计量单位,产品表单的「单位」下拉取自本表