# 设计:自营管理菜单(票种 / 景点 / 自营分销商 / 销售日历) - 日期:2026-10-02 - 仓库:E:\Data\Code\tourism\ticket-ota(clear-admin,Vue 2.5 + Element UI 2.15.3) - 需求来源(m00042):「新增菜单 一级菜单 自营管理(放在产品中心下面) 二级菜单 票种管理 景点管理 自营分销商管理 用户端接口」 - 接口依据:`自营售票管理端接口.md`(仓库根,593 行,下文简称「管理端文档」) - 范围外:微信小程序/公众号 C 端流程(用户明确「微信小程序的不用管」);管理端统计/报表(文档第 7 节明确沿用现有数据中心页面,不新增菜单);现有订单模块(自营订单沿用) ## 1. 背景与现状 - 现有菜单由 `src/router/index.js` 的 `routerMap` 声明,登录后 `src/permission.js` 守卫触发 `GetInfo` → `GenerateRoutes`(`src/store/modules/permission.js`)按后台 `resourceList` 的 code 过滤,`addRoutes` 注入。 - 菜单图标用 iconfont(`static/font/iconfont.css`,index.html 引入),`meta.icon` → 类名 `icon-{name}`;可用字形含 `icon-ticket-manage`(门票图标),直接复用,零图标改动。 - 页面惯例:`.form-wrap > .searchBox(el-form inline + .btn-wrap)` + `.tableBox(el-table stripe + 操作列 fixed right)` + `el-pagination`;弹窗拆独立组件(参考 `src/views/goodsCenter/priceList/EditDialog.vue`、`KSNewDialog.vue`)。 - 请求层:`src/utils/request.js` axios 实例,请求头 `SaToken: localStorage.otaToken`,`code!==200` 弹 Message,401/403 登出。管理端文档确认新接口走同一体系(`/tenant` 前缀 + 现有 satoken),直接复用,无需新实例。 - 图片上传现成机制:`el-upload picture-card` + `window.config.uploadUrl || 'https://pres.cleartv.cn/upload'`(见 `src/views/goodsCenter/priceList/KSNewDialog.vue:699-702`)。 - 线下票种列表 `/tenant/otaTicketType` 已有封装:`src/api/ticketType.js` 的 `getTicketTypeList`。 ### 权限过滤的关键坑(决定菜单怎么声明) `src/store/modules/permission.js:19-25` 子路由过滤逻辑: ```js route.children = route.children.filter(i => { if (i.meta && i.meta.permissionName) { return permissionCodes.find((code) => i.meta.permissionName.indexOf(`:${code}`) > -1) } else { return true } }) ``` - 子路由 `meta.permissionName` 一旦有值,后台 code 必须以 `:code` 形式出现在其中才显示;写 `'all'` 也永远匹配不上 → 子菜单被隐藏。 - 父级显示条件(`src/router/index.js:281-296` 聚合后):`meta.permissionName` 数组含 `'all'` 则恒显示。 **结论:4 个新子路由均不写 permissionName** → 聚合后父级 `自营管理` 的 permissionName 数组全为 `'all'` → 菜单全员可见。后续若要按角色控制,再补 permissionName + 后端配置 resourceList code。 ## 2. 范围(YAGNI) 做: 1. 一级菜单「自营管理」(产品中心之后),二级 4 个页面:票种管理、景点管理、自营分销商管理、销售日历(销售日历是独立第 4 个菜单,用户已确认)。 2. `src/api/selfManage.js` 新 API 封装。 3. `src/const.js` 追加本模块常量。 不做: - 不引富文本库(用户已确认:图文描述/购买协议用 textarea 纯文本,图片用 el-upload)。 - 不做统计/报表新页面(沿用数据中心现有页)。 - 不改登录、权限过滤代码、request 拦截器。 - 不碰工作区既有未提交改动(`src/views/goodsCenter/priceList/EditDialog.vue`、`src/views/queryReport/orderList.vue`)。 - 零新增 npm 依赖(webpack 3/babel 6 老构建不冒险)。 ## 3. 路由(src/router/index.js) `routerMap` 中「产品中心」块之后、「数据中心」块之前插入: ```js { path: '/selfManage', component: Layout, name: '自营管理', meta: { title: '自营管理', icon: 'ticket-manage' }, redirect: 'noredirect', alwaysShow: true, children: [ { path: 'ticketType', name: 'selfTicketType', component: () => import('@/views/selfManage/ticketType'), meta: { title: '票种管理' } }, { path: 'scenic', name: 'selfScenic', component: () => import('@/views/selfManage/scenic'), meta: { title: '景点管理' } }, { path: 'thirdParty', name: 'selfThirdParty', component: () => import('@/views/selfManage/thirdParty'), meta: { title: '自营分销商管理' } }, { path: 'saleCalendar', name: 'selfSaleCalendar', component: () => import('@/views/selfManage/saleCalendar'), meta: { title: '销售日历' } } ] } ``` 注意:`name` 用英文(现有中文 name 与英文混用,新模块统一英文,避免路由跳转歧义);子路由不写 permissionName(见第 1 节)。 ## 4. API 层(src/api/selfManage.js,新文件) 统一 `http.post(url, { data: params })` 风格。函数清单(URL 均出自管理端文档): | 函数 | 方法+URL | 说明 | |---|---|---| | getSelfScenicList | POST /tenant/selfScenic | 筛选 name(模糊)/isOpen;分页 records/total | | addSelfScenic | POST /tenant/selfScenic/add | | | updateSelfScenic | POST /tenant/selfScenic/update | 多带 id | | deleteSelfScenic | POST /tenant/selfScenic/delete | data.idList | | getSelfTicketTypeList | POST /tenant/selfTicketType | 筛选 category/name/isSale;分页 | | getSelfTicketTypeDetail | POST /tenant/selfTicketType/{id} | 路径拼 id,body data 空对象 | | addSelfTicketType | POST /tenant/selfTicketType/add | | | updateSelfTicketType | POST /tenant/selfTicketType/update | 必填 id;ticketTypeId/category 不可改 | | deleteSelfTicketType | POST /tenant/selfTicketType/delete | data.idList | | getSaleCalendarList | POST /tenant/selfTicketSaleCalendar/list | 入参 selfTicketTypeId;返回数组(非分页) | | saveSaleCalendar | POST /tenant/selfTicketSaleCalendar/add | **data 是数组,全量覆盖**;空数组禁止调用 | | deleteSaleCalendar | POST /tenant/selfTicketSaleCalendar/delete | data.idList = 规则 id | | getUserThirdPartyList | POST /tenant/userThirdParty | 筛选 name(模糊)/contactName/phone/remark/status;分页 | | addUserThirdParty | POST /tenant/userThirdParty/add | | | updateUserThirdParty | POST /tenant/userThirdParty/update | id + 可选字段;name/loginName 不可改 | | bindUserThirdParty | POST /tenant/userThirdParty/bind | userThirdPartyIdList + selfTicketTypeIdList([] = 清空) | | rechargeUserThirdParty | POST /tenant/userThirdParty/recharge | userThirdPartyId + amount>0 | | decreaseUserThirdParty | POST /tenant/userThirdParty/decrease | 同充值 | | deleteUserThirdParty | POST /tenant/userThirdParty/delete | data.idList;余额>0/有未完结订单会失败 | | getThirdPartyBalanceLog | POST /tenant/thirdPartyBalanceLog | 筛选 userThirdPartyId/type/tradeTimeStart/tradeTimeEnd;分页 | 线下票种下拉复用 `src/api/ticketType.js` 的 `getTicketTypeList`(POST /tenant/otaTicketType,传 `{}` 拉全量或按 name 筛),不重复封装。 ## 5. 常量(src/const.js 追加) ```js SELF_TICKET_CATEGORY: ticket 景区门票 / batch 场次售票 / member 年卡次卡 SELF_REAL_NAME_TYPE: 0 不限 / 1 一证一人 / 2 一证多人 SELF_CHANNELS: wx 微信公众号 / wx-mini 微信小程序 / third-party 自营分销商 SELF_WEEKDAYS: 1 周日 … 7 周六(文档约定 1=周日) SELF_BALANCE_LOG_TYPE: 1 充值 / 2 人工扣款 / 3 消费 / 4 退款 SELF_USE_DATE_TYPE: 1 日历票 / 2 期票 SELF_MEMBER_USE_DATE_TYPE: 1 按天 / 2 固定日期 SELF_MEMBER_ACTIVE_TYPE: 1 购买即激活 / 2 首次使用激活 ``` ## 6. 页面设计 ### 6.1 票种管理 `src/views/selfManage/ticketType/index.vue` + `FormDialog.vue` 列表(POST /tenant/selfTicketType): - 搜索:大类(三选)、名称(模糊)、是否在售(1/0/全部);按钮:重置/搜索/新增/批量删除 - 表格列:名称 / 大类(tag) / 线下票种id(ticketTypeId) / 底价(price) / 划线价(highPrice) / 在售(tag) / 实名(tag 不限/一证一人/一证多人) / 可售日期(saleDateStart~saleDateEnd) / 渠道(tags) / 绑定景点(scenicList 名称拼接) / 可退 / 创建时间 - 操作列(fixed right):编辑、销售日历(`$router.push({path:'/selfManage/saleCalendar', query:{id}})`)、删除 - 删除:单条二次确认;顶部批量删除(selection 列) 表单弹窗 FormDialog(新增/编辑共用,`show(row?)` 模式): - 新增:选线下票种(getTicketTypeList 下拉,显示 name,提交 ticketTypeId + category);已创建过的后端报「票种已存在」,前端把已创建票种(getSelfTicketTypeList 全量)置 disabled 并提示 - 选定后 category 锁定为只读展示(创建后不可改) - 公共必填:name、price(底价,数字 >0)、realNameType、saleDateStart/saleDateEnd(date 选择器 yyyy-MM-dd)、saleTimeStart/saleTimeEnd(time 选择器 HH:mm:ss,默认 00:00:00~23:59:59) - 公共选填:isSale(switch 默认 1)、highPrice、saleMaxPreDays、saleMinPreDays、saleMinNums、saleMaxNums、dayStock、totalStock(0 不限)、isAllowCancel(switch 默认 1)、isAgreeProtocol(switch 默认 0)+ buyProtocol(textarea 纯文本) - 渠道:el-checkbox-group 勾 wx/wx-mini/third-party(要给分销商卖必须勾 third-party,文案提示) - 景点:el-select multiple,数据源 getSelfScenicList 全量,提交 scenicIds 数组 - 按大类条件块: - `ticket`:useDays(数字)、useDateType(1 日历票 / 2 期票);期票时 show useDateStart/useDateEnd - `member`:memberUseDateType(1 按天 / 2 固定日期);按天 show useDays;固定日期 show useDateStart/useDateEnd;memberActiveType(1/2) - `batch`:展示所选线下票种的 batchConfigList(只读表格:场次名称/时段/最大数量),说明「场次来自线下票种,不在此维护」 - 编辑:getSelfTicketTypeDetail 回填;ticketTypeId/category 只读;景点、渠道整表覆盖提交(清空传空数组) - 提交前前端校验:必填项、价格/库存数字、日期区间合法 ### 6.2 景点管理 `src/views/selfManage/scenic/index.vue` + `FormDialog.vue` 列表(POST /tenant/selfScenic): - 搜索:名称(模糊)、是否开放(1/0/全部);按钮:重置/搜索/新增/批量删除 - 表格列:名称 / 地址 / 开放时间(openingTime) / 轮播图(picture 首图缩略 + 张数) / 开放状态(tag 开放/关闭) / 公告(publicAnnouncement 截断,title 悬浮全文) / 创建时间 - 操作列:编辑(用列表行数据回填,无详情接口)、删除 - 删除二次确认提示:「已绑到票种的景点删除后,C 端景点列表可能不再展示该景点」 表单弹窗: - name 必填、isOpen(radio 1/0 必填)、address、openingTime(文本,如 08:00~18:00) - picture:el-upload picture-card,`:limit="6"`,action = `window.config.uploadUrl || 'https://pres.cleartv.cn/upload'`,上传成功取返回 URL(与 KSNewDialog 同款 on-change/on-remove 处理,失败提示重试),保存为英文逗号串;编辑时按逗号拆成 fileList 回填 - publicAnnouncement:el-input type=textarea - scenicDescription:el-input type=textarea rows≈8(纯文本,不做富文本) - 编辑提交 = update(多带 id) ### 6.3 自营分销商管理 `src/views/selfManage/thirdParty/index.vue` + 4 个子弹窗 列表(POST /tenant/userThirdParty): - 搜索:名称(模糊)、联系人(精确)、电话(精确)、状态(1/0/全部);按钮:重置/搜索/新增/批量授权票种/批量删除 - 表格列:名称 / 登录名(loginName) / 联系人 / 电话 / 余额(balance,两位小数) / 状态(tag 正常/禁用红色) / 已授权票种(selfTicketTypeList 数量 + 名称拼接,title 悬浮) / 最近登录(lastLoginTime) / 创建时间 / 备注 - 行操作列:编辑、充值、扣款、票种授权、流水、删除 - 顶部「批量授权票种」:表格 selection 多选分销商 → BindDialog(userThirdPartyIdList 多选) - 删除:前端先拦 `balance > 0`(提示先扣款);未完结订单前端不可见,靠后端报错弹 Message 子弹窗: - **FormDialog**(新增/编辑) - 新增:name 必填、loginName 必填、password 必填 + 前端校验(≥8 位且含大写、小写字母、数字三者中的字母大小写都有 + 数字,正则 `/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/`)、contactName、phone、remark;文案提示「新建余额为 0,创建后需单独授权票种」 - 编辑:name/loginName 只读展示;contactName/phone/remark 可改;password 选填(空 = 不改,填了同样校验);status(switch 正常/禁用,文案「禁用会立即踢下线」,切到禁用时二次确认) - **MoneyDialog**(充值/扣款共用,mode 参数区分) - amount 必填 >0 最小 0.01;扣款时显示「当前余额 X」并前端拦超余额;提交 recharge/decrease - **BindDialog**(票种授权,支持单行/多选) - 票种 el-select multiple(getSelfTicketTypeList 全量,label 用 name) - 文案:「提交将覆盖所选分销商的原有授权;被授权票种需勾 third-party 渠道分销商端才可见」 - 允许清空:选 0 个票种也可提交(selfTicketTypeIdList: [] = 清空授权),用 confirm 二次确认 - **BalanceLogDialog**(流水,只读) - 打开时 userThirdPartyId 固定为该行分销商 - 搜索:type(1充值/2扣款/3消费/4退款/全部)、交易时间范围(datetime,转 yyyy-MM-dd HH:mm:ss) - 表格:类型(tag) / 发生额(amount) / 变动前(beforeBalance) / 变动后(afterBalance) / 关联订单(orderId) / 操作人(operatorAdminId) / 交易时间(tradeTime);el-pagination ### 6.4 销售日历 `src/views/selfManage/saleCalendar/index.vue`(独立菜单) - 顶部工具条:自营票种下拉(getSelfTicketTypeList 全量,显示 name;支持 `this.$route.query.id` 预选并自动加载)+ 提示文案「规则覆盖该票种某天价格/是否可售;C 端列表显示底价,详情/下单走日历」 - 规则表(getSaleCalendarList,按 priority 升序展示): - 列:优先级(priority,带 ↑↓ 两按钮调整顺序) / 价格(price) / 可售(isSale tag) / 规则内容(特定日期:specificDate 原文;周期:startDate~endDate + 周几中文名)/ 删除(单条,二次确认) - 新增规则(行内小表单或弹窗,二选一实现为弹窗 CalendarRuleDialog): - price 必填 >0、isSale 必填(1/0) - 类型 radio 二选一: - 特定日期:可录入多个日期(el-date-picker type=date + 动态加行/或逗号分隔输入),提交拼 `specificDate` 逗号串 - 周期性:startDate/endDate 必填(date)+ weekdays el-checkbox-group 7 项(`1 周日…7 周六`,至少勾 1 个),提交拼逗号串 - 新规则追加到列表尾部(priority 按当前末尾 +1) - **保存(全量)**:把整表按当前顺序重排 priority = 1..n,`data` 以数组提交 saveSaleCalendar(每条含 selfTicketTypeId/price/isSale/priority + specificDate 或 startDate/endDate/weekdays;已有规则保留原 id) - **清空保护**:表内规则为 0 条时,「保存」按钮改为「恢复/清空」逻辑——若之前有规则且用户全部删掉,调用 deleteSaleCalendar(data.idList = 全部规则 id),**绝不用空数组调 /add**(后端取第 0 条会报错);无任何 id 可删时禁用保存 - 未选票种时表格区显示空态提示 ## 7. 错误处理与交互约定 - 全部走 `src/utils/request.js` 统一 Message 提示与 401/403 登出,页面内不重复弹错 - 二次确认:所有删除/清空授权/禁用分销商用 `this.$confirm` - 分页:`{ pageNum: 1, pageSize: 10 }`,`res.data.records / res.data.total`,`el-pagination :current-page.sync` + `@current-change` 重载(与现有页一致) - 时间显示统一 `parseTime`(src/utils/index.js) - 金额展示统一两位小数 ## 8. 文件清单(新增/修改) 新增: - src/api/selfManage.js - src/views/selfManage/ticketType/index.vue、src/views/selfManage/ticketType/FormDialog.vue - src/views/selfManage/scenic/index.vue、src/views/selfManage/scenic/FormDialog.vue - src/views/selfManage/thirdParty/index.vue、FormDialog.vue、MoneyDialog.vue、BindDialog.vue、BalanceLogDialog.vue - src/views/selfManage/saleCalendar/index.vue、CalendarRuleDialog.vue 修改: - src/router/index.js(routerMap 插入自营管理块) - src/const.js(追加常量) 不改:permission.js、request.js、login、layout、既有业务页。 ## 9. 验收标准 1. `npm run dev` 启动后侧边栏出现「自营管理」(产品中心之后),展开有 4 个二级菜单;任意已登录账号可见 2. 景点:新增(含 ≤6 张图上传、逗号串保存)→ 列表可见 → 编辑回填正确 → 删除(含已绑票种提示) 3. 票种:选线下票种建自营票种(三类字段块均正确)→ 重复选同一线下票种被拦 → 编辑回填(含渠道/景点整表覆盖)→ 从列表跳销售日历带参 4. 销售日历:选票种加载规则 → 增/删/调序 → 保存后 list 核对(priority 重排正确)→ 删空规则走 delete 不走 add 5. 分销商:新增(密码复杂度校验)→ 充值/扣款(超余额拦截)→ 授权覆盖生效 → 流水只读可见 → 禁用踢下线确认 → 余额>0 删除被拦 6. 所有接口失败时弹后端 msg,无白屏、无未捕获 Promise ## 10. 风险与备注 - 接口均为文档约定,后端如未部署 `/tenant/selfScenic` 等,页面表现为 Message 报错(开发期需先与后端确认环境已联调,见管理端文档联调顺序) - `el-upload` 返回结构沿用 KSNewDialog 的处理方式实现,联调时以实际 upload 响应字段为准 - 菜单全员可见是临时决策(用户确认「先用 all」);后续上权限时只需给 4 个子路由补 permissionName 并在后端 resourceList 配 code