From 5207d14daa88be8347d73ec4ebad25c1e2637e8b Mon Sep 17 00:00:00 2001 From: liuwenxin <1009266660@qq.com> Date: Tue, 13 Oct 2026 12:25:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(base):=20=E5=89=8D=E7=AB=AF=E5=9F=BA?= =?UTF-8?q?=E7=A1=80=E6=95=B0=E6=8D=AE=E4=B8=89=E9=A1=B5=E4=B8=8E=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E8=AF=84=E7=BA=A7=E8=AE=A1=E7=AE=97=E5=99=A8=EF=BC=88?= =?UTF-8?q?=E4=BA=BA=E5=91=98/=E4=BA=A7=E5=93=81/=E5=AE=A2=E6=88=B7/?= =?UTF-8?q?=E8=AF=84=E7=BA=A7=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 人员页:编辑抽屉内区域/计划子表编辑;产品页:类别阈值提示条; 客户页:归属变更二次确认含「不回溯历史」+变更记录抽屉 - 评级页:五项评分实时出等级(纯前端判定,不落库 #25)+ 等级映射维护抽屉 - 菜单两组:基础数据(8-10)/业务数据(14);金额字符串直出禁 float Co-Authored-By: Claude Code --- frontend/src/api/customers.ts | 127 ++++ frontend/src/api/personnel.ts | 150 +++++ frontend/src/api/products.ts | 91 +++ frontend/src/api/ratings.ts | 60 ++ frontend/src/router/index.ts | 27 + frontend/src/utils/constants.ts | 10 +- frontend/src/views/base/customers/index.vue | 562 ++++++++++++++++ frontend/src/views/base/personnel/index.vue | 659 +++++++++++++++++++ frontend/src/views/base/products/index.vue | 351 ++++++++++ frontend/src/views/business/rating/index.vue | 482 ++++++++++++++ 10 files changed, 2518 insertions(+), 1 deletion(-) create mode 100644 frontend/src/api/customers.ts create mode 100644 frontend/src/api/personnel.ts create mode 100644 frontend/src/api/products.ts create mode 100644 frontend/src/api/ratings.ts create mode 100644 frontend/src/views/base/customers/index.vue create mode 100644 frontend/src/views/base/personnel/index.vue create mode 100644 frontend/src/views/base/products/index.vue create mode 100644 frontend/src/views/business/rating/index.vue diff --git a/frontend/src/api/customers.ts b/frontend/src/api/customers.ts new file mode 100644 index 0000000..812721c --- /dev/null +++ b/frontend/src/api/customers.ts @@ -0,0 +1,127 @@ +import { request } from './request' +import type { PageData } from './types' + +/** + * 客户列表项(对应后端 `CustomerOut`)。 + * + * owner_personnel_name 由联表带出:归属人员不可考(被物理删除)时为 null, + * owner_personnel_id 仍保留原值供追溯。 + */ +export interface CustomerItem { + id: number + company_name: string + /** 客户公司的负责人姓名(非系统内营销人员) */ + owner_name: string + contact_person: string | null + contact_phone: string | null + address: string | null + industry: string | null + /** personal 个人户 / manager_personal 经理个人户 / enterprise 企业户(计提规则锚点) */ + customer_type: string + owner_personnel_id: number | null + owner_personnel_name: string | null + remark: string | null +} + +export interface CustomerQueryParams { + pageNum?: number + pageSize?: number + /** 公司名称或客户负责人,模糊匹配 */ + keyword?: string + customer_type?: string + owner_personnel_id?: number +} + +/** 新增客户(owner_personnel_id 为初始归属,可空:先建档后派归属也合法) */ +export interface CustomerCreateParams { + company_name: string + owner_name: string + contact_person?: string | null + contact_phone?: string | null + address?: string | null + industry?: string | null + customer_type: string + owner_personnel_id?: number | null + remark?: string | null +} + +/** + * 编辑客户:不含归属字段 —— 归属调整统一走 `PUT /{id}/ownership` + * (唯一写归属留痕的入口),编辑表单即使提交归属也会被后端忽略。 + */ +export type CustomerUpdateParams = Omit + +/** 归属变更入参(new_owner_personnel_id 空 = 清空归属) */ +export interface CustomerOwnershipParams { + new_owner_personnel_id?: number | null + reason?: string | null +} + +/** 归属变更留痕条目(对应后端 `OwnershipChangeLogOut`;姓名可随人员消失,ID 固化) */ +export interface OwnershipLogItem { + id: number + customer_id: number + old_owner_personnel_id: number | null + new_owner_personnel_id: number | null + old_owner_name: string | null + new_owner_name: string | null + change_reason: string | null + operator_id: number | null + operator_name: string + create_time: string +} + +/** 客户分类下拉项(与后端 `core/constants.py` 的值域一致) */ +export const CUSTOMER_TYPE_OPTIONS = [ + { label: '个人户', value: 'personal' }, + { label: '经理个人户', value: 'manager_personal' }, + { label: '企业户', value: 'enterprise' }, +] as const + +/** 分类编码 → 展示名(表格列展示用,未知编码原样回显) */ +export function customerTypeLabel(code: string): string { + return CUSTOMER_TYPE_OPTIONS.find((o) => o.value === code)?.label ?? code +} + +/** 分页查询客户(keyword 对公司名称/客户负责人模糊匹配,附归属人员姓名) */ +export function listCustomers(params: CustomerQueryParams): Promise> { + return request({ url: '/v1/base/customers', method: 'get', params }) +} + +/** 新增客户(公司名称+负责人组合唯一,冲突由后端拦截) */ +export function createCustomer(data: CustomerCreateParams): Promise { + return request({ url: '/v1/base/customers', method: 'post', data }) +} + +/** 编辑客户档案(不含归属;全量提交) */ +export function updateCustomer( + customerId: number, + data: CustomerUpdateParams, +): Promise { + return request({ url: `/v1/base/customers/${customerId}`, method: 'put', data }) +} + +/** 删除客户(物理删除) */ +export function deleteCustomer(customerId: number): Promise { + return request({ url: `/v1/base/customers/${customerId}`, method: 'delete' }) +} + +/** 归属变更(唯一写归属留痕的入口;新旧相同后端报「归属未变化」) */ +export function changeOwnership( + customerId: number, + data: CustomerOwnershipParams, +): Promise { + return request({ url: `/v1/base/customers/${customerId}/ownership`, method: 'put', data }) +} + +/** 查某客户的归属变更留痕(分页,按变更时间倒序) */ +export function listOwnershipLogs( + customerId: number, + params: { pageNum?: number; pageSize?: number }, +): Promise> { + return request({ + url: `/v1/base/customers/${customerId}/ownership-logs`, + method: 'get', + params, + }) +} diff --git a/frontend/src/api/personnel.ts b/frontend/src/api/personnel.ts new file mode 100644 index 0000000..00b821d --- /dev/null +++ b/frontend/src/api/personnel.ts @@ -0,0 +1,150 @@ +import { request } from './request' +import type { PageData } from './types' + +/** 辐射区域条目(对应后端 `PersonnelRegionOut`) */ +export interface PersonnelRegionItem { + id: number + region_name: string + remark: string | null +} + +/** 拓展计划条目(对应后端 `PersonnelPlanOut`) */ +export interface PersonnelPlanItem { + id: number + plan_period: string + plan_content: string + target_desc: string | null +} + +/** 人员列表项(对应后端 `PersonnelOut`) */ +export interface PersonnelItem { + id: number + personnel_name: string + business_role_id: number + /** 岗位名称由联表带出 */ + role_name: string | null + manager_id: number | null + manager_name: string | null + phone: string | null + /** 入职日期,ISO 串(YYYY-MM-DD) */ + hire_date: string | null + /** 1试用期 0正式 */ + is_probation: number + /** 月工资标准(元),字符串直出 —— 金额红线:不经 float */ + salary_standard: string | null + /** 1在职 0离职 */ + status: number + remark: string | null +} + +/** 人员详情:附辐射区域与拓展计划,供编辑抽屉一次回显 */ +export interface PersonnelDetail extends PersonnelItem { + regions: PersonnelRegionItem[] + plans: PersonnelPlanItem[] +} + +export interface PersonnelQueryParams { + pageNum?: number + pageSize?: number + /** 姓名,模糊匹配 */ + keyword?: string + /** 1在职 0离职 */ + status?: number + /** 岗位角色ID */ + business_role_id?: number +} + +/** + * 新增/编辑人员(全量提交)。 + * + * 不含状态 —— 在职/离职是独立操作(PUT /{id}/status),避免编辑表单 + * 误触离职。可空字段传 `null` 表示清空,与后端 `PersonnelCreateIn` 对齐。 + */ +export interface PersonnelSaveParams { + personnel_name: string + business_role_id: number + /** 直属经理ID,空=无;后端校验不能是自己且须在职 */ + manager_id?: number | null + phone?: string | null + /** ISO 串(YYYY-MM-DD) */ + hire_date?: string | null + /** 1试用期 0正式 */ + is_probation?: number + /** 月工资标准(元),字符串形式的数字,最多两位小数 */ + salary_standard?: string | null + remark?: string | null +} + +/** 辐射区域条目入参(全量替换用) */ +export interface RegionItemParams { + region_name: string + remark?: string | null +} + +/** 拓展计划条目入参(全量替换用) */ +export interface PlanItemParams { + plan_period: string + plan_content: string + target_desc?: string | null +} + +/** 分页查询人员(含离职),姓名模糊匹配 */ +export function listPersonnel(params: PersonnelQueryParams): Promise> { + return request({ url: '/v1/base/personnel', method: 'get', params }) +} + +/** 新增人员(默认在职;岗位须存在且启用) */ +export function createPersonnel(data: PersonnelSaveParams): Promise { + return request({ url: '/v1/base/personnel', method: 'post', data }) +} + +/** 人员详情(附辐射区域与拓展计划) */ +export function getPersonnelDetail(personnelId: number): Promise { + return request({ url: `/v1/base/personnel/${personnelId}`, method: 'get' }) +} + +/** 编辑人员(全量提交,不含状态) */ +export function updatePersonnel( + personnelId: number, + data: PersonnelSaveParams, +): Promise { + return request({ url: `/v1/base/personnel/${personnelId}`, method: 'put', data }) +} + +/** 删除人员(被团队成员或客户归属引用时后端拒绝,应改为离职) */ +export function deletePersonnel(personnelId: number): Promise { + return request({ url: `/v1/base/personnel/${personnelId}`, method: 'delete' }) +} + +/** 设为在职 / 离职(离职人员不可再被业务与客户归属引用) */ +export function setPersonnelStatus(personnelId: number, status: number): Promise { + return request({ + url: `/v1/base/personnel/${personnelId}/status`, + method: 'put', + data: { status }, + }) +} + +/** 全量替换辐射区域(空列表即清空该人员的全部区域) */ +export function replacePersonnelRegions( + personnelId: number, + items: RegionItemParams[], +): Promise { + return request({ + url: `/v1/base/personnel/${personnelId}/regions`, + method: 'put', + data: { items }, + }) +} + +/** 全量替换拓展计划(空列表即清空该人员的全部计划) */ +export function replacePersonnelPlans( + personnelId: number, + items: PlanItemParams[], +): Promise { + return request({ + url: `/v1/base/personnel/${personnelId}/plans`, + method: 'put', + data: { items }, + }) +} diff --git a/frontend/src/api/products.ts b/frontend/src/api/products.ts new file mode 100644 index 0000000..8d14290 --- /dev/null +++ b/frontend/src/api/products.ts @@ -0,0 +1,91 @@ +import { request } from './request' +import type { PageData } from './types' + +/** + * 产品列表项(对应后端 `ProductOut`)。 + * + * 数量/单价为字符串直出 —— 金额红线:前端禁用 float 解析与 toFixed, + * 展示原串、提交原串,Decimal 转换收敛在后端 service 层。 + */ +export interface ProductItem { + id: number + product_name: string + /** 规格型号,可空串(与名称构成组合唯一键) */ + spec_model: string + unit: string | null + stock_quantity: string | null + sale_price: string | null + /** municipal_water 市政水利 / rail_transit 轨道交通 —— 折算阈值的行级锚点 */ + product_category: string + /** 1启用 0停用 */ + status: number + remark: string | null + create_time: string + update_time: string +} + +export interface ProductQueryParams { + pageNum?: number + pageSize?: number + /** 名称或规格,模糊匹配 */ + keyword?: string + product_category?: string + status?: number +} + +/** 新增/编辑产品(全量提交;不含状态,启停走独立端点) */ +export interface ProductSaveParams { + product_name: string + /** 空串与 None 同义(后端归一为空串以保组合唯一键生效) */ + spec_model?: string | null + unit?: string | null + /** 非负数字字符串 */ + stock_quantity?: string | null + /** 非负数字字符串 */ + sale_price?: string | null + product_category: string + remark?: string | null +} + +/** 产品类别下拉项(与后端 `core/constants.py` 的值域一致) */ +export const PRODUCT_CATEGORY_OPTIONS = [ + { label: '市政水利', value: 'municipal_water' }, + { label: '轨道交通', value: 'rail_transit' }, +] as const + +/** 类别编码 → 展示名(表格列展示用,未知编码原样回显) */ +export function productCategoryLabel(code: string): string { + return PRODUCT_CATEGORY_OPTIONS.find((o) => o.value === code)?.label ?? code +} + +/** 分页查询产品(含停用) */ +export function listProducts(params: ProductQueryParams): Promise> { + return request({ url: '/v1/base/products', method: 'get', params }) +} + +/** 新增产品(默认启用;类别须在合法值域内) */ +export function createProduct(data: ProductSaveParams): Promise { + return request({ url: '/v1/base/products', method: 'post', data }) +} + +/** 编辑产品(全量提交,不含状态;名称+规格组合唯一,冲突由后端拦截) */ +export function updateProduct( + productId: number, + data: ProductSaveParams, +): Promise { + return request({ url: `/v1/base/products/${productId}`, method: 'put', data }) +} + +/** 删除产品(物理删除,仅限误录数据;常规下架用停用) */ +export function deleteProduct(productId: number): Promise { + return request({ url: `/v1/base/products/${productId}`, method: 'delete' }) +} + +/** 启用 / 停用产品 */ +export function setProductStatus(productId: number, status: number): Promise { + return request({ + url: `/v1/base/products/${productId}/status`, + method: 'put', + data: { status }, + }) +} diff --git a/frontend/src/api/ratings.ts b/frontend/src/api/ratings.ts new file mode 100644 index 0000000..434a2d0 --- /dev/null +++ b/frontend/src/api/ratings.ts @@ -0,0 +1,60 @@ +import { request } from './request' + +/** + * 评级等级配置项(对应后端 `RatingGradeOut`)。 + * + * min_score/max_score 按分值红线以**字符串**出参(后端 str 直出), + * 前端参与数值比较前必须显式 parseFloat 转数值,禁止直接做字符串比大小。 + */ +export interface RatingGradeItem { + id: number + grade_name: string + min_score: string + max_score: string + /** 判定顺序,升序;判定取首个 min_score ≤ 总分的等级 */ + sort_order: number + remark: string | null + create_time: string + update_time: string +} + +/** + * 新增 / 编辑等级入参(编辑为全量替换语义)。 + * + * 分值以字符串承载(如 "90.0"):number 无法精确表示全部十进制小数, + * 字符串是分值红线的入口闸门,Decimal 转换统一收敛在后端 service 层。 + */ +export interface RatingGradeParams { + grade_name: string + min_score: string + max_score: string + sort_order?: number + remark?: string +} + +/** + * 全量等级列表(按 sort_order 升序返回,不分页)。 + * + * 等级映射是评级计算器的判定表,数量个位数,分页无意义。 + */ +export function listRatingGrades(): Promise { + return request({ url: '/v1/business/rating-grades', method: 'get' }) +} + +/** 新增评级等级(区间合法与不重叠由后端校验兜底,前端校验仅是展示约束) */ +export function createRatingGrade(data: RatingGradeParams): Promise { + return request({ url: '/v1/business/rating-grades', method: 'post', data }) +} + +/** 编辑评级等级(全量替换,字段与新增一致) */ +export function updateRatingGrade( + gradeId: number, + data: RatingGradeParams, +): Promise { + return request({ url: `/v1/business/rating-grades/${gradeId}`, method: 'put', data }) +} + +/** 删除评级等级(物理删除,等级表无下游引用,允许清空后由列表空态提示) */ +export function deleteRatingGrade(gradeId: number): Promise { + return request({ url: `/v1/business/rating-grades/${gradeId}`, method: 'delete' }) +} diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 08e4639..b8f8836 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -70,6 +70,33 @@ const routes: RouteRecordRaw[] = [ component: () => import('@/views/system/logs/index.vue'), meta: { title: '日志查询', group: '系统管理', requiresAdmin: true, order: 7 }, }, + { + // 基础数据属业务操作:登录即可访问,不设 requiresAdmin(对照后端 api/v1/base) + path: '/base/personnel', + name: 'base-personnel', + component: () => import('@/views/base/personnel/index.vue'), + meta: { title: '人员管理', group: '基础数据', order: 8 }, + }, + { + path: '/base/products', + name: 'base-products', + component: () => import('@/views/base/products/index.vue'), + meta: { title: '产品管理', group: '基础数据', order: 9 }, + }, + { + path: '/base/customers', + name: 'base-customers', + component: () => import('@/views/base/customers/index.vue'), + meta: { title: '客户管理', group: '基础数据', order: 10 }, + }, + { + // 业务数据组首个页面:评级计算器是打分工具,不影响绩效金额, + // 登录用户即可用,不设 requiresAdmin(区别于系统管理组) + path: '/business/rating', + name: 'business-rating', + component: () => import('@/views/business/rating/index.vue'), + meta: { title: '客户评级计算器', group: '业务数据', order: 14 }, + }, ], }, // 未匹配路由回首页;若未登录会被守卫拦到登录页 diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts index 37ec5e0..939f9b2 100644 --- a/frontend/src/utils/constants.ts +++ b/frontend/src/utils/constants.ts @@ -7,7 +7,15 @@ export const ROLE_ADMIN = 'admin' * 操作日志的模块 / 操作词表 —— 与后端 `core/constants.py` 的 * `MODULE_*` / `ACTION_*` 保持一致;新增模块时两处同步(后端是权威来源)。 */ -export const MODULE_OPTIONS = ['用户管理', '角色管理', '岗位角色', '计提参数'] as const +export const MODULE_OPTIONS = [ + '用户管理', + '角色管理', + '岗位角色', + '计提参数', + '人员管理', + '产品管理', + '客户管理', +] as const export const ACTION_OPTIONS = ['新增', '修改', '删除', '重置密码', '启用', '禁用'] as const /** 启用/禁用筛选选项(文案与 StatusTag 的展示一致) */ diff --git a/frontend/src/views/base/customers/index.vue b/frontend/src/views/base/customers/index.vue new file mode 100644 index 0000000..88eba5f --- /dev/null +++ b/frontend/src/views/base/customers/index.vue @@ -0,0 +1,562 @@ + + + + + diff --git a/frontend/src/views/base/personnel/index.vue b/frontend/src/views/base/personnel/index.vue new file mode 100644 index 0000000..0af2bd2 --- /dev/null +++ b/frontend/src/views/base/personnel/index.vue @@ -0,0 +1,659 @@ + + + + + diff --git a/frontend/src/views/base/products/index.vue b/frontend/src/views/base/products/index.vue new file mode 100644 index 0000000..c156c88 --- /dev/null +++ b/frontend/src/views/base/products/index.vue @@ -0,0 +1,351 @@ + + + + + diff --git a/frontend/src/views/business/rating/index.vue b/frontend/src/views/business/rating/index.vue new file mode 100644 index 0000000..b60ea56 --- /dev/null +++ b/frontend/src/views/business/rating/index.vue @@ -0,0 +1,482 @@ + + + + +