2026-10-02-self-manage-menu-design.md 18 KB

设计:自营管理菜单(票种 / 景点 / 自营分销商 / 销售日历)

  • 日期: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 子路由过滤逻辑:

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 中「产品中心」块之后、「数据中心」块之前插入:

{
  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 追加)

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