fix: 回调配置, 调整信用位置, 套餐
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 6m55s
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 6m55s
This commit is contained in:
@@ -20,10 +20,19 @@
|
|||||||
- Decision: `docs/所需接口文档` 是本变更的接口契约索引;页面 API 类型、请求参数、枚举和响应字段必须以对应模块文档为准,不从七月说明中自行扩展路径。
|
- Decision: `docs/所需接口文档` 是本变更的接口契约索引;页面 API 类型、请求参数、枚举和响应字段必须以对应模块文档为准,不从七月说明中自行扩展路径。
|
||||||
- Decision: 文件类业务统一采用 `POST /api/admin/storage/upload-url` 获取预签名 URL,直传成功后仅把 `file_key` 传给批量订购或设备分配接口。
|
- Decision: 文件类业务统一采用 `POST /api/admin/storage/upload-url` 获取预签名 URL,直传成功后仅把 `file_key` 传给批量订购或设备分配接口。
|
||||||
- Decision: 批量订购、设备分配和导出均保存服务端返回的任务 ID;页面恢复时查询原任务,终态后停止轮询,不能因刷新或超时重复创建。
|
- Decision: 批量订购、设备分配和导出均保存服务端返回的任务 ID;页面恢复时查询原任务,终态后停止轮询,不能因刷新或超时重复创建。
|
||||||
|
- Decision: 代理钱包在线充值和平台线下代充共用 `POST /api/admin/agent-recharges`,但使用两套互斥的表单模型。在线表单只允许 `amount`、`payment_method` 和 `request_id`;线下表单才允许 `shop_id`、`payment_voucher_key` 和 `remark`,不能把线下表单对象整体复用到在线请求中。
|
||||||
|
- Decision: 在线充值弹窗打开时先请求 `/api/admin/agent-recharges/payment-methods`,支付方式和金额上下限完全由响应驱动。金额在界面显示为元,提交前转换为整数分;支付方式为空时禁止提交并显示无可用支付方式提示。
|
||||||
|
- Decision: 在线创建成功后只使用返回的 `recharge_id` 查询 `/api/admin/agent-recharges/{id}/payment-status`,把原始 `qr_content` 交给二维码组件。二维码弹窗可见且订单非终态时每 3 秒轮询;弹窗隐藏、页面进入后台或状态进入 3/4/5/6 后停止轮询。
|
||||||
|
- Decision: `request_id` 是一次主动在线充值的幂等键。网络超时或断开只用同一个 ID 重试一次并复用后端返回的原订单/二维码;用户关闭弹窗后重新发起充值或改变金额/支付方式时生成新的 ID,冲突响应不得覆盖原表单或自动创建新订单。
|
||||||
|
- Decision: 充值列表和详情均以 `recharge_source` 做业务分支,并优先展示 `recharge_source_name`。在线单展示支付方式、第三方流水和支付/完成时间;线下单展示凭证、备注、提交人和审批状态。存在企微审批来源或实例时只读展示审批进度,不显示确认/驳回操作。
|
||||||
|
- Decision: 充值相关导出权限按场景拆分:业务列表入口使用 `agent_recharge:export`,导出任务详情使用 `export_task:agent_recharge_detail`,下载文件使用 `export_task:agent_recharge_download`。权限不足时隐藏入口或操作,不通过前端绕过权限。
|
||||||
|
|
||||||
## Risks / Trade-offs
|
## Risks / Trade-offs
|
||||||
|
|
||||||
- 接口 DTO 若未按说明返回,页面无法可靠展示审批人、历史资产标识或新旧字段;通过联调清单阻断实现前确认。
|
- 接口 DTO 若未按说明返回,页面无法可靠展示审批人、历史资产标识或新旧字段;通过联调清单阻断实现前确认。
|
||||||
|
- 支付配置为空或二维码内容为空时,在线充值无法继续;通过动态支付方式加载、空状态和联调环境中微信/支付宝二维码内容非空校验阻断上线。
|
||||||
|
- 在线创建请求若错误生成新的 `request_id`,可能产生重复充值单;通过提交态保存 ID、幂等重试和冲突保护降低重复支付风险。
|
||||||
|
- `status=2` 只代表第三方已支付,不代表钱包已经入账;通过以 `status=3` 作为余额刷新条件避免提前展示到账。
|
||||||
- 设备批量分配接口文档已明确任务列表和详情仅平台用户可操作,前端不得向代理账号展示任务入口或通过前端绕过权限。
|
- 设备批量分配接口文档已明确任务列表和详情仅平台用户可操作,前端不得向代理账号展示任务入口或通过前端绕过权限。
|
||||||
- H5/C 端不在本次实现范围,后台只能依赖后端提供的最终字段,跨端验收需要单独安排。
|
- H5/C 端不在本次实现范围,后台只能依赖后端提供的最终字段,跨端验收需要单独安排。
|
||||||
- 部分接口文档只给出字段模型,缺少业务错误码、权限编码和分页默认值;这些列入待确认清单。
|
- 部分接口文档只给出字段模型,缺少业务错误码、权限编码和分页默认值;这些列入待确认清单。
|
||||||
@@ -38,5 +47,5 @@
|
|||||||
## Open Questions
|
## Open Questions
|
||||||
|
|
||||||
- 退款和代理充值详情是否需要直接返回企微审批节点人员列表?
|
- 退款和代理充值详情是否需要直接返回企微审批节点人员列表?
|
||||||
- 各新增后台页面和按钮的最终权限编码是什么?
|
- 代理在线充值创建和查询的最终权限编码是否与现有 `agent_recharge:*` 权限保持一致;导出权限编码已由接口文档确定。
|
||||||
- 批量任务、导出任务、企微审批和 Gateway 超时的稳定错误码及重试语义是什么?
|
- 批量任务、导出任务、企微审批和 Gateway 超时的稳定错误码及重试语义是什么?
|
||||||
|
|||||||
@@ -7,9 +7,13 @@
|
|||||||
## What Changes
|
## What Changes
|
||||||
|
|
||||||
- 按 `00-七月迭代实现与接口对接说明.md` 更新后台管理端的接口调用和页面展示契约。
|
- 按 `00-七月迭代实现与接口对接说明.md` 更新后台管理端的接口调用和页面展示契约。
|
||||||
|
- 按 `产品迭代7月份/000-代理钱包扫码充值接口对接说明.md` 和 `所需接口文档/000-代理钱包扫码充值接口对接.md` 补齐代理钱包在线扫码充值、线下代充和充值导出权限的前端契约。
|
||||||
- 以 `docs/所需接口文档/01-店铺.md` 至 `21-设备批量分配.md` 为接口参数、响应字段、枚举、权限和错误语义的具体来源。
|
- 以 `docs/所需接口文档/01-店铺.md` 至 `21-设备批量分配.md` 为接口参数、响应字段、枚举、权限和错误语义的具体来源。
|
||||||
- 对资产、店铺、订单、换货、套餐、资金、系列授权、审批、通知、批量任务、导出和 Gateway 相关页面补齐前端适配要求。
|
- 对资产、店铺、订单、换货、套餐、资金、系列授权、审批、通知、批量任务、导出和 Gateway 相关页面补齐前端适配要求。
|
||||||
- 统一使用后端返回的实名策略、支付方式、预计到期、审批状态、金额计算结果和任务状态,不在前端重复推导。
|
- 统一使用后端返回的实名策略、支付方式、预计到期、审批状态、金额计算结果和任务状态,不在前端重复推导。
|
||||||
|
- 代理在线充值只提交金额、动态可用支付方式和幂等 `request_id`,用后端返回的原始 `qr_content` 渲染二维码,并按充值状态轮询到帐结果。
|
||||||
|
- 按 `recharge_source` 区分代理在线自充与平台线下代充;线下单展示凭证和审批状态,在线单隐藏线下字段和人工审批操作。
|
||||||
|
- 补齐代理充值列表导出入口、导出任务详情和下载的权限编码。
|
||||||
- 补齐 CSV 上传模板、异步任务恢复、权限按钮、错误处理和并发冲突处理要求。
|
- 补齐 CSV 上传模板、异步任务恢复、权限按钮、错误处理和并发冲突处理要求。
|
||||||
- 明确预签名上传先取 `file_key`、再调用批量业务接口的两阶段流程,并固定批量订购、设备分配和导出任务的终态处理规则。
|
- 明确预签名上传先取 `file_key`、再调用批量业务接口的两阶段流程,并固定批量订购、设备分配和导出任务的终态处理规则。
|
||||||
- **不实施 H5/C 端页面改动**,但在规格和联调清单中标注其依赖的接口及外部工作项。
|
- **不实施 H5/C 端页面改动**,但在规格和联调清单中标注其依赖的接口及外部工作项。
|
||||||
@@ -18,6 +22,6 @@
|
|||||||
## Impact
|
## Impact
|
||||||
|
|
||||||
- Affected specs: `july-iteration-frontend-integration`
|
- Affected specs: `july-iteration-frontend-integration`
|
||||||
- Affected code: 后台资产、店铺、订单、换货、套餐、代理资金、系列授权、审批配置、通知、批量任务和导出页面
|
- Affected code: 后台资产、店铺、订单、换货、套餐、代理充值与资金、系列授权、审批配置、通知、批量任务和导出页面;重点涉及 `src/views/finance/agent-recharge`、`src/api/modules/agentRecharge.ts` 和 `src/types/api/agentRecharge.ts`
|
||||||
- External follow-up: H5/C 端认证限制、实名流程、支付按钮、C 端订单续费、C 端通知和 C 端资产展示
|
- External follow-up: H5/C 端认证限制、实名流程、支付按钮、C 端订单续费、C 端通知和 C 端资产展示
|
||||||
- Approval gate: 接口待确认项完成前不进入大范围实现
|
- Approval gate: 接口待确认项完成前不进入大范围实现
|
||||||
|
|||||||
@@ -191,3 +191,129 @@ The admin frontend SHALL enforce the documented permission boundary for device a
|
|||||||
|
|
||||||
- **WHEN** an agent or enterprise account loads the device allocation area
|
- **WHEN** an agent or enterprise account loads the device allocation area
|
||||||
- **THEN** the frontend hides the entry and does not call `/api/admin/devices/import/tasks` or `/api/admin/devices/import/tasks/{id}`
|
- **THEN** the frontend hides the entry and does not call `/api/admin/devices/import/tasks` or `/api/admin/devices/import/tasks/{id}`
|
||||||
|
|
||||||
|
### Requirement: Agent Wallet Online Recharge Contract
|
||||||
|
|
||||||
|
The admin frontend SHALL support the documented agent wallet online QR recharge flow through `/api/admin/agent-recharges/payment-methods` and `/api/admin/agent-recharges`. The available payment methods and amount limits SHALL come from the payment-methods response; the online create request SHALL contain only `amount` in integer fen, `payment_method` from the returned methods, and a required `request_id`. Online agent recharge requests MUST NOT send `shop_id`, `payment_voucher_key`, `remark`, merchant credentials, or payment configuration identifiers.
|
||||||
|
|
||||||
|
#### Scenario: Load available online payment methods
|
||||||
|
|
||||||
|
- **WHEN** an agent opens the online recharge dialog
|
||||||
|
- **THEN** the frontend calls `GET /api/admin/agent-recharges/payment-methods`
|
||||||
|
- **AND** it displays only the returned `methods` values (`wechat` and/or `alipay`)
|
||||||
|
- **AND** it validates the integer-fen amount against the returned `min_amount` and `max_amount`
|
||||||
|
|
||||||
|
#### Scenario: No online payment method is available
|
||||||
|
|
||||||
|
- **WHEN** the payment-methods response returns an empty `methods` array
|
||||||
|
- **THEN** the frontend hides or disables the submit action
|
||||||
|
- **AND** it displays that no online payment method is currently available
|
||||||
|
- **AND** it does not call the online create endpoint
|
||||||
|
|
||||||
|
#### Scenario: Create an online QR recharge
|
||||||
|
|
||||||
|
- **WHEN** an authorized agent submits a valid online recharge
|
||||||
|
- **THEN** the frontend calls `POST /api/admin/agent-recharges` with `amount`, `payment_method`, and `request_id`
|
||||||
|
- **AND** it converts the displayed yuan amount to an integer amount in fen before submission
|
||||||
|
- **AND** it uses the returned `recharge_id` and does not derive the target shop or wallet from user-editable form fields
|
||||||
|
|
||||||
|
#### Scenario: Render the returned QR content
|
||||||
|
|
||||||
|
- **WHEN** online recharge creation succeeds with a non-empty `qr_content`
|
||||||
|
- **THEN** the frontend passes `qr_content` unchanged to the QR-code component
|
||||||
|
- **AND** it does not parse, concatenate, replace, upload, persist, or log the QR content
|
||||||
|
|
||||||
|
### Requirement: Idempotent Online Recharge Submission
|
||||||
|
|
||||||
|
The frontend SHALL preserve one `request_id` for the lifetime of one active online recharge submission. A timeout or network interruption MAY retry the same submission once with the same `request_id`, but a user-initiated new recharge MUST generate a new request ID. A conflict caused by reusing the same request ID with a different amount or payment method SHALL preserve the existing form state and require an explicit new recharge attempt.
|
||||||
|
|
||||||
|
#### Scenario: Retry after an uncertain create result
|
||||||
|
|
||||||
|
- **WHEN** the online create request times out or the network disconnects
|
||||||
|
- **THEN** the frontend retries at most once with the original `request_id`
|
||||||
|
- **AND** it does not create a new `request_id` or blindly open a second recharge dialog
|
||||||
|
|
||||||
|
#### Scenario: Start a new recharge after closing the dialog
|
||||||
|
|
||||||
|
- **WHEN** the user closes the QR dialog and clicks online recharge again
|
||||||
|
- **THEN** the frontend creates a new `request_id`
|
||||||
|
- **AND** the new amount and payment method are treated as a new active submission
|
||||||
|
|
||||||
|
#### Scenario: Reused request ID conflicts
|
||||||
|
|
||||||
|
- **WHEN** the backend rejects a request because the same `request_id` is paired with different recharge parameters
|
||||||
|
- **THEN** the frontend displays the backend business message
|
||||||
|
- **AND** it does not overwrite the original request or automatically submit another amount
|
||||||
|
|
||||||
|
### Requirement: Online Payment And Wallet Arrival Polling
|
||||||
|
|
||||||
|
The online QR recharge dialog SHALL poll only the documented local payment-status endpoint `GET /api/admin/agent-recharges/{id}/payment-status`. While the QR dialog is visible and the recharge is non-terminal, the frontend SHALL poll at approximately three-second intervals. The frontend SHALL stop polling when the dialog is hidden, the page is not visible, or the recharge status is `3` (completed), `4` (closed), `5` (refunded), or `6` (rejected). It MUST treat `status=2` as third-party payment completed but wallet arrival still processing; only `status=3` permits refreshing the wallet balance and recharge list.
|
||||||
|
|
||||||
|
#### Scenario: Payment is waiting for the user
|
||||||
|
|
||||||
|
- **WHEN** payment-status returns `status=1` and `payment_status=0`
|
||||||
|
- **THEN** the dialog displays that the user is waiting to scan and pay
|
||||||
|
- **AND** polling continues while the dialog is visible
|
||||||
|
|
||||||
|
#### Scenario: Payment is received but wallet arrival is processing
|
||||||
|
|
||||||
|
- **WHEN** payment-status returns `status=2` and `payment_status=1`
|
||||||
|
- **THEN** the dialog displays payment success and wallet arrival processing
|
||||||
|
- **AND** the frontend does not increase or refresh the wallet balance as completed
|
||||||
|
- **AND** polling continues
|
||||||
|
|
||||||
|
#### Scenario: Wallet arrival completes
|
||||||
|
|
||||||
|
- **WHEN** payment-status returns `status=3`
|
||||||
|
- **THEN** the frontend displays that the recharge has arrived
|
||||||
|
- **AND** it stops polling
|
||||||
|
- **AND** it refreshes the current wallet balance and recharge list
|
||||||
|
|
||||||
|
#### Scenario: Online recharge reaches a terminal failure state
|
||||||
|
|
||||||
|
- **WHEN** payment-status returns `status=4`, `status=5`, or `status=6`
|
||||||
|
- **THEN** the frontend displays the backend status name or an appropriate failure message
|
||||||
|
- **AND** it stops polling without automatically creating a replacement recharge
|
||||||
|
|
||||||
|
### Requirement: Recharge Source And Operation Boundaries
|
||||||
|
|
||||||
|
The recharge list and detail pages SHALL use `recharge_source` as the only business branch: `agent_online` for agent online self-recharge and `platform_offline` for platform offline代充. They SHALL prefer `recharge_source_name` for display and use the documented list filters `shop_id`, `status`, `recharge_source`, `start_date`, and `end_date`. Online records SHALL show payment information and payment/completion timestamps while hiding offline vouchers and manual approval operations. Offline records SHALL show vouchers, remarks, submitter, and approval status; they SHALL not show QR content or invoke the online payment-status endpoint.
|
||||||
|
|
||||||
|
#### Scenario: Filter online recharge records
|
||||||
|
|
||||||
|
- **WHEN** a user selects online recharge source or status filters
|
||||||
|
- **THEN** the frontend requests `GET /api/admin/agent-recharges` with the documented filter names and values
|
||||||
|
- **AND** it displays the backend recharge source name, amount, payment method, status, submitter, and timestamps
|
||||||
|
|
||||||
|
#### Scenario: Display a platform offline recharge
|
||||||
|
|
||||||
|
- **WHEN** a platform user opens a record with `recharge_source=platform_offline`
|
||||||
|
- **THEN** the detail view displays the payment voucher, remark, submitter, and approval status
|
||||||
|
- **AND** it does not render a QR code or start payment-status polling
|
||||||
|
|
||||||
|
#### Scenario: Confirm or reject an offline recharge
|
||||||
|
|
||||||
|
- **WHEN** a pending offline recharge has no WeCom approval provider or approval instance
|
||||||
|
- **THEN** an authorized platform user MAY confirm it through `POST /api/admin/agent-recharges/{id}/offline-pay` with `operation_password`
|
||||||
|
- **AND** an authorized platform user MAY reject it through `POST /api/admin/agent-recharges/{id}/reject` with a required `rejection_reason` of at most 500 characters
|
||||||
|
|
||||||
|
#### Scenario: WeCom approval controls the offline record
|
||||||
|
|
||||||
|
- **WHEN** an offline recharge has `approval_provider=wecom` or an `approval_instance_id`
|
||||||
|
- **THEN** the frontend displays approval progress as read-only
|
||||||
|
- **AND** it hides confirm and reject operations
|
||||||
|
|
||||||
|
### Requirement: Agent Recharge Export Permissions
|
||||||
|
|
||||||
|
The agent recharge list SHALL show its export entry only with `agent_recharge:export`. The export-task page SHALL protect the `agent_recharge` detail and download operations with `export_task:agent_recharge_detail` and `export_task:agent_recharge_download`, respectively. The frontend SHALL hide unauthorized controls and SHALL NOT invoke the corresponding API as a fallback.
|
||||||
|
|
||||||
|
#### Scenario: An authorized user exports agent recharges
|
||||||
|
|
||||||
|
- **WHEN** the current account has `agent_recharge:export`
|
||||||
|
- **THEN** the agent recharge page displays the export entry and opens the `agent_recharge` export flow
|
||||||
|
|
||||||
|
#### Scenario: An unauthorized user accesses export actions
|
||||||
|
|
||||||
|
- **WHEN** the current account lacks an export permission
|
||||||
|
- **THEN** the related entry, detail action, or download action is hidden
|
||||||
|
- **AND** the frontend does not call the export-task detail or download endpoint
|
||||||
|
|||||||
@@ -54,7 +54,17 @@
|
|||||||
- [x] 6.3 验证代理数据隔离、平台权限、敏感字段脱敏和财务操作审计入口。
|
- [x] 6.3 验证代理数据隔离、平台权限、敏感字段脱敏和财务操作审计入口。
|
||||||
- [ ] 6.4 完成后台页面的接口联调、权限组合、空数据、错误、并发冲突和任务恢复验收。
|
- [ ] 6.4 完成后台页面的接口联调、权限组合、空数据、错误、并发冲突和任务恢复验收。
|
||||||
|
|
||||||
## 7. Explicitly Out Of Scope
|
## 7. Agent Wallet QR Recharge
|
||||||
|
|
||||||
|
- [ ] 7.1 Extend `AgentRecharge` types and `AgentRechargeService` for `alipay`, dynamic payment methods, amount bounds, `request_id`, `qr_content`, `payment_status`, and the documented payment-status endpoint while preserving nullable fields.
|
||||||
|
- [ ] 7.2 Add the agent online recharge dialog flow that loads `/api/admin/agent-recharges/payment-methods`, renders only returned payment methods, validates the returned fen bounds, and disables submission when no method is available.
|
||||||
|
- [ ] 7.3 Split online and offline create payloads; submit online recharge with only `amount`, `payment_method`, and a generated `request_id`, render the returned raw `qr_content`, and prevent leakage of shop, voucher, remark, or payment configuration fields.
|
||||||
|
- [ ] 7.4 Implement idempotent online-create retry and conflict handling: reuse the original `request_id` for one uncertain retry, generate a new ID for a new user action, and preserve the original form on request conflicts.
|
||||||
|
- [ ] 7.5 Implement visible-dialog payment polling with a three-second interval, stop on hidden/background or terminal status, distinguish `status=2` payment success from `status=3` wallet arrival, and refresh the wallet balance/list only after completion.
|
||||||
|
- [ ] 7.6 Update recharge list/detail branches to use `recharge_source`, document the online/offline field visibility rules, keep offline confirmation/rejection conditional on the absence of WeCom approval, and prevent online records from invoking offline operations.
|
||||||
|
- [ ] 7.7 Apply agent recharge export permissions: `agent_recharge:export`, `export_task:agent_recharge_detail`, and `export_task:agent_recharge_download`; add focused tests for role visibility, payload exclusion, idempotent retry, polling termination, and status transitions.
|
||||||
|
|
||||||
|
## 8. Explicitly Out Of Scope
|
||||||
|
|
||||||
- H5/C 端登录限制、实名流程、支付按钮、资产详情、订单续费和 C 端通知不在本次实现任务内。
|
- H5/C 端登录限制、实名流程、支付按钮、资产详情、订单续费和 C 端通知不在本次实现任务内。
|
||||||
- H5 首页隐藏设备下 ICCID 仍按最新说明标记为不做。
|
- H5 首页隐藏设备下 ICCID 仍按最新说明标记为不做。
|
||||||
|
|||||||
@@ -50,13 +50,13 @@ async function importBundledActions() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test('pending agent recharge exposes reject action for admins with rejection permission', async () => {
|
test('pending offline recharge exposes reject action for admins with rejection permission', async () => {
|
||||||
const { module, cleanup } = await importBundledActions()
|
const { module, cleanup } = await importBundledActions()
|
||||||
const { buildAgentRechargeActions } = module
|
const { buildAgentRechargeActions } = module
|
||||||
const row = {
|
const row = {
|
||||||
id: 42,
|
id: 42,
|
||||||
status: 1,
|
status: 1,
|
||||||
payment_method: 'wechat',
|
payment_method: 'offline',
|
||||||
payment_voucher_key: []
|
payment_voucher_key: []
|
||||||
}
|
}
|
||||||
const handledRows = []
|
const handledRows = []
|
||||||
@@ -108,3 +108,27 @@ test('non-pending agent recharge does not expose reject action', async () => {
|
|||||||
await cleanup()
|
await cleanup()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('pending online recharge does not expose offline reject action', async () => {
|
||||||
|
const { module, cleanup } = await importBundledActions()
|
||||||
|
const { buildAgentRechargeActions } = module
|
||||||
|
const row = {
|
||||||
|
id: 42,
|
||||||
|
status: 1,
|
||||||
|
payment_method: 'wechat',
|
||||||
|
payment_voucher_key: []
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const actions = buildAgentRechargeActions(row, {
|
||||||
|
hasAuth: (permission) => permission === 'agent_recharge:reject',
|
||||||
|
onViewPaymentVoucher: () => {},
|
||||||
|
onConfirmPayment: () => {},
|
||||||
|
onReject: () => {}
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.deepEqual(actions, [])
|
||||||
|
} finally {
|
||||||
|
await cleanup()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|||||||
72
scripts/tests/agentRechargeOnline.test.mjs
Normal file
72
scripts/tests/agentRechargeOnline.test.mjs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import test from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { mkdtemp, rm, stat } from 'node:fs/promises'
|
||||||
|
import { tmpdir } from 'node:os'
|
||||||
|
import { join } from 'node:path'
|
||||||
|
import { pathToFileURL } from 'node:url'
|
||||||
|
import * as esbuild from 'esbuild'
|
||||||
|
|
||||||
|
async function importBundledOnlineHelpers() {
|
||||||
|
const tempDir = await mkdtemp(join(tmpdir(), 'agent-recharge-online-'))
|
||||||
|
const bundlePath = join(tempDir, 'agentRechargeOnline.mjs')
|
||||||
|
|
||||||
|
await esbuild.build({
|
||||||
|
entryPoints: ['src/views/finance/agent-recharge/agentRechargeOnline.ts'],
|
||||||
|
bundle: true,
|
||||||
|
outfile: bundlePath,
|
||||||
|
format: 'esm',
|
||||||
|
platform: 'node',
|
||||||
|
absWorkingDir: process.cwd(),
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
name: 'alias-src',
|
||||||
|
setup(build) {
|
||||||
|
build.onResolve({ filter: /^@\// }, async (args) => {
|
||||||
|
const resolvedPath = join(process.cwd(), 'src', args.path.slice(2))
|
||||||
|
try {
|
||||||
|
if ((await stat(resolvedPath)).isDirectory())
|
||||||
|
return { path: join(resolvedPath, 'index.ts') }
|
||||||
|
} catch {
|
||||||
|
// Let esbuild report the original resolution failure.
|
||||||
|
}
|
||||||
|
return { path: resolvedPath }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const module = await import(pathToFileURL(bundlePath).href)
|
||||||
|
return {
|
||||||
|
module,
|
||||||
|
cleanup: () => rm(tempDir, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('online recharge helpers convert yuan to integer fen and identify terminal states', async () => {
|
||||||
|
const { module, cleanup } = await importBundledOnlineHelpers()
|
||||||
|
|
||||||
|
try {
|
||||||
|
assert.equal(module.amountYuanToFen(100.01), 10001)
|
||||||
|
assert.equal(module.isOnlineRechargeTerminal(1), false)
|
||||||
|
assert.equal(module.isOnlineRechargeTerminal(2), false)
|
||||||
|
assert.equal(module.isOnlineRechargeTerminal(3), true)
|
||||||
|
assert.equal(module.isOnlineRechargeTerminal(6), true)
|
||||||
|
} finally {
|
||||||
|
await cleanup()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('online recharge request IDs are unique and namespaced', async () => {
|
||||||
|
const { module, cleanup } = await importBundledOnlineHelpers()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const first = module.createOnlineRechargeRequestId()
|
||||||
|
const second = module.createOnlineRechargeRequestId()
|
||||||
|
assert.match(first, /^recharge-/)
|
||||||
|
assert.match(second, /^recharge-/)
|
||||||
|
assert.notEqual(first, second)
|
||||||
|
} finally {
|
||||||
|
await cleanup()
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -20,6 +20,17 @@ test('agent recharge rejection posts the rejection reason to the order rejection
|
|||||||
post(options) {
|
post(options) {
|
||||||
calls.push(options)
|
calls.push(options)
|
||||||
return Promise.resolve({ code: 0, data: undefined })
|
return Promise.resolve({ code: 0, data: undefined })
|
||||||
|
},
|
||||||
|
get(options) {
|
||||||
|
calls.push(options)
|
||||||
|
return Promise.resolve({
|
||||||
|
code: 0,
|
||||||
|
data: {
|
||||||
|
methods: ['wechat', 'alipay'],
|
||||||
|
min_amount: 10000,
|
||||||
|
max_amount: 100000000
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -53,6 +64,8 @@ test('agent recharge rejection posts the rejection reason to the order rejection
|
|||||||
await AgentRechargeService.rejectAgentRecharge(42, {
|
await AgentRechargeService.rejectAgentRecharge(42, {
|
||||||
rejection_reason: '支付凭证不符合要求'
|
rejection_reason: '支付凭证不符合要求'
|
||||||
})
|
})
|
||||||
|
await AgentRechargeService.getPaymentMethods()
|
||||||
|
await AgentRechargeService.getPaymentStatus(42)
|
||||||
|
|
||||||
assert.deepEqual(calls, [
|
assert.deepEqual(calls, [
|
||||||
{
|
{
|
||||||
@@ -60,6 +73,14 @@ test('agent recharge rejection posts the rejection reason to the order rejection
|
|||||||
data: {
|
data: {
|
||||||
rejection_reason: '支付凭证不符合要求'
|
rejection_reason: '支付凭证不符合要求'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/api/admin/agent-recharges/payment-methods',
|
||||||
|
params: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/api/admin/agent-recharges/42/payment-status',
|
||||||
|
params: undefined
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -8,12 +8,23 @@ import type {
|
|||||||
AgentRechargeQueryParams,
|
AgentRechargeQueryParams,
|
||||||
AgentRechargeListResponse,
|
AgentRechargeListResponse,
|
||||||
CreateAgentRechargeRequest,
|
CreateAgentRechargeRequest,
|
||||||
|
AgentRechargePaymentMethods,
|
||||||
|
AgentRechargePaymentStatusResponse,
|
||||||
ConfirmOfflinePaymentRequest,
|
ConfirmOfflinePaymentRequest,
|
||||||
RejectAgentRechargeRequest,
|
RejectAgentRechargeRequest,
|
||||||
BaseResponse
|
BaseResponse
|
||||||
} from '@/types/api'
|
} from '@/types/api'
|
||||||
|
|
||||||
export class AgentRechargeService extends BaseService {
|
export class AgentRechargeService extends BaseService {
|
||||||
|
/**
|
||||||
|
* 获取代理在线充值可用支付方式和金额限制
|
||||||
|
*/
|
||||||
|
static getPaymentMethods(): Promise<BaseResponse<AgentRechargePaymentMethods>> {
|
||||||
|
return this.get<BaseResponse<AgentRechargePaymentMethods>>(
|
||||||
|
'/api/admin/agent-recharges/payment-methods'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取代理充值订单列表
|
* 获取代理充值订单列表
|
||||||
* @param params 查询参数
|
* @param params 查询参数
|
||||||
@@ -32,6 +43,15 @@ export class AgentRechargeService extends BaseService {
|
|||||||
return this.getOne<AgentRecharge>(`/api/admin/agent-recharges/${id}`)
|
return this.getOne<AgentRecharge>(`/api/admin/agent-recharges/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询在线充值支付及钱包到账状态
|
||||||
|
*/
|
||||||
|
static getPaymentStatus(id: number): Promise<BaseResponse<AgentRechargePaymentStatusResponse>> {
|
||||||
|
return this.getOne<AgentRechargePaymentStatusResponse>(
|
||||||
|
`/api/admin/agent-recharges/${id}/payment-status`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建代理充值订单
|
* 创建代理充值订单
|
||||||
* @param data 创建充值订单请求参数
|
* @param data 创建充值订单请求参数
|
||||||
|
|||||||
290
src/components/business/SeriesGrantPackageDialog.vue
Normal file
290
src/components/business/SeriesGrantPackageDialog.vue
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
<template>
|
||||||
|
<ElDialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="props.dialogType === 'add' ? '添加套餐' : '编辑套餐'"
|
||||||
|
width="40%"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@closed="emit('closed')"
|
||||||
|
>
|
||||||
|
<ElForm ref="formRef" :model="props.form" :rules="props.rules" label-width="130px">
|
||||||
|
<ElFormItem v-if="props.dialogType === 'add'" label="选择套餐" prop="package_ids">
|
||||||
|
<ElSelect
|
||||||
|
:model-value="props.form.package_ids"
|
||||||
|
@update:model-value="updateForm({ package_ids: $event })"
|
||||||
|
placeholder="请选择套餐(可多选)"
|
||||||
|
style="width: 100%"
|
||||||
|
filterable
|
||||||
|
remote
|
||||||
|
:remote-method="(query: string) => emit('search', query)"
|
||||||
|
:loading="props.packageLoading"
|
||||||
|
clearable
|
||||||
|
multiple
|
||||||
|
:multiple-limit="100"
|
||||||
|
popper-class="package-select-dropdown"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-if="props.availablePackages.length === 0 && !props.packageLoading && props.hasSeries"
|
||||||
|
>
|
||||||
|
<ElOption disabled value="" label="该系列没有可选套餐" />
|
||||||
|
</template>
|
||||||
|
<ElOption
|
||||||
|
v-for="pkg in props.availablePackages"
|
||||||
|
:key="pkg.id"
|
||||||
|
:label="getPackageOptionLabel(pkg)"
|
||||||
|
:value="pkg.id"
|
||||||
|
:disabled="pkg.is_authorized"
|
||||||
|
/>
|
||||||
|
</ElSelect>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<ElFormItem
|
||||||
|
v-if="props.dialogType === 'add' && props.form.packages.length"
|
||||||
|
label="套餐配置"
|
||||||
|
prop="packages"
|
||||||
|
>
|
||||||
|
<div class="package-config-list">
|
||||||
|
<div v-for="pkg in props.form.packages" :key="pkg.package_id" class="package-config-item">
|
||||||
|
<div class="package-config-name" :title="pkg.package_name || ''">
|
||||||
|
{{ pkg.package_name || '套餐名称不可用' }}
|
||||||
|
</div>
|
||||||
|
<div class="package-config-cost">
|
||||||
|
<span class="package-config-field-label">套餐成本价(元)</span>
|
||||||
|
<ElInputNumber
|
||||||
|
:model-value="pkg.cost_price_yuan"
|
||||||
|
:min="pkg.original_cost_price || 0"
|
||||||
|
:max="getPackageCostPriceMax(pkg)"
|
||||||
|
:precision="2"
|
||||||
|
:step="0.01"
|
||||||
|
:controls="false"
|
||||||
|
placeholder="请输入成本价"
|
||||||
|
style="width: 150px"
|
||||||
|
@update:model-value="updatePackageCost(pkg.package_id, $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<ElFormItem v-if="props.dialogType === 'edit'" label="套餐名称">
|
||||||
|
<span>{{ props.form.package_name }}</span>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem v-if="props.dialogType === 'edit'" label="套餐编码">
|
||||||
|
<span>{{ props.form.package_code }}</span>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<ElFormItem v-if="props.dialogType === 'edit'" label="成本价(元)" prop="cost_price_yuan">
|
||||||
|
<ElInputNumber
|
||||||
|
:model-value="props.form.cost_price_yuan"
|
||||||
|
:precision="2"
|
||||||
|
:step="0.01"
|
||||||
|
:controls="false"
|
||||||
|
style="width: 100%"
|
||||||
|
placeholder="请输入成本价"
|
||||||
|
@update:model-value="updateForm({ cost_price_yuan: $event ?? 0 })"
|
||||||
|
/>
|
||||||
|
<div v-if="props.form.original_cost_price" class="form-tip">
|
||||||
|
请参考{{ props.form.package_name || '套餐' }} - 套餐成本价: ¥{{
|
||||||
|
props.form.original_cost_price.toFixed(2)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<ElFormItem v-if="props.dialogType === 'add' || props.canUpdateExpiryBase" label="生效条件">
|
||||||
|
<ElSelect
|
||||||
|
:model-value="props.form.expiry_base_override"
|
||||||
|
placeholder="请选择生效条件"
|
||||||
|
style="width: 100%"
|
||||||
|
@update:model-value="updateForm({ expiry_base_override: $event })"
|
||||||
|
>
|
||||||
|
<ElOption label="跟随套餐默认" value="default" />
|
||||||
|
<ElOption label="购买即生效" value="from_purchase" />
|
||||||
|
<ElOption label="实名激活时生效" value="from_activation" />
|
||||||
|
</ElSelect>
|
||||||
|
<div class="form-tip">仅影响后续新订单,不影响已购买套餐</div>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<template v-if="props.dialogType === 'edit'">
|
||||||
|
<ElFormItem label="套餐默认生效条件">
|
||||||
|
{{ props.form.default_expiry_base_name || '-' }}
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="覆盖生效条件">
|
||||||
|
{{ props.form.expiry_base_override_name || '跟随套餐默认' }}
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="最终生效条件">
|
||||||
|
{{ props.form.effective_expiry_base_name || '-' }}
|
||||||
|
</ElFormItem>
|
||||||
|
</template>
|
||||||
|
</ElForm>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||||
|
<ElButton type="primary" :loading="props.submitLoading" @click="handleSubmit">
|
||||||
|
保存
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, nextTick, ref, watch } from 'vue'
|
||||||
|
import {
|
||||||
|
ElButton,
|
||||||
|
ElDialog,
|
||||||
|
ElForm,
|
||||||
|
ElFormItem,
|
||||||
|
ElInputNumber,
|
||||||
|
ElOption,
|
||||||
|
ElSelect,
|
||||||
|
type FormInstance,
|
||||||
|
type FormRules
|
||||||
|
} from 'element-plus'
|
||||||
|
import type {
|
||||||
|
GrantPackageCandidate,
|
||||||
|
SeriesGrantPackageForm
|
||||||
|
} from '@/utils/business/seriesGrantPackage'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
modelValue: boolean
|
||||||
|
dialogType: 'add' | 'edit'
|
||||||
|
form: SeriesGrantPackageForm
|
||||||
|
rules: FormRules
|
||||||
|
availablePackages: GrantPackageCandidate[]
|
||||||
|
packageLoading?: boolean
|
||||||
|
submitLoading?: boolean
|
||||||
|
canUpdateExpiryBase?: boolean
|
||||||
|
hasSeries?: boolean
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
packageLoading: false,
|
||||||
|
submitLoading: false,
|
||||||
|
canUpdateExpiryBase: false,
|
||||||
|
hasSeries: false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: boolean]
|
||||||
|
'update:form': [form: SeriesGrantPackageForm]
|
||||||
|
search: [query: string]
|
||||||
|
submit: []
|
||||||
|
closed: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const dialogVisible = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (value: boolean) => emit('update:modelValue', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const formatPackageOptionPrice = (price?: number | null) => {
|
||||||
|
if (price === undefined || price === null) return '未配置'
|
||||||
|
return `¥${(price / 100).toFixed(2)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPackageOptionLabel = (pkg: GrantPackageCandidate) => {
|
||||||
|
const packageName = pkg.package_name || '套餐名称不可用'
|
||||||
|
return `${packageName} - 成本价 ${formatPackageOptionPrice(pkg.cost_price)} - 建议售价 ${formatPackageOptionPrice(pkg.suggested_retail_price)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPackageCostPriceMax = (pkg: {
|
||||||
|
original_cost_price?: number
|
||||||
|
suggested_retail_price?: number | null
|
||||||
|
}) => {
|
||||||
|
if (pkg.suggested_retail_price !== undefined && pkg.suggested_retail_price !== null) {
|
||||||
|
return Number((pkg.suggested_retail_price * 1.5).toFixed(2))
|
||||||
|
}
|
||||||
|
if (pkg.original_cost_price !== undefined && pkg.original_cost_price !== null) {
|
||||||
|
return Number((pkg.original_cost_price * 1.5).toFixed(2))
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateForm = (values: Partial<SeriesGrantPackageForm>) => {
|
||||||
|
emit('update:form', { ...props.form, ...values })
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePackageCost = (packageId: number, costPrice: number | undefined) => {
|
||||||
|
updateForm({
|
||||||
|
packages: props.form.packages.map((pkg) =>
|
||||||
|
pkg.package_id === packageId ? { ...pkg, cost_price_yuan: costPrice ?? 0 } : pkg
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
try {
|
||||||
|
const valid = await formRef.value.validate()
|
||||||
|
if (valid) emit('submit')
|
||||||
|
} catch {
|
||||||
|
// 表单校验失败时由表单项展示提示
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(visible) => {
|
||||||
|
if (visible) nextTick(() => formRef.value?.clearValidate())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.form-tip {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.package-select-dropdown .el-select-dropdown__item) {
|
||||||
|
height: auto;
|
||||||
|
min-height: 34px;
|
||||||
|
padding-top: 6px;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
line-height: 1.5;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-config-list {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-config-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-config-name {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-config-cost {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-config-field-label {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
80
src/components/business/SeriesGrantPackageTable.vue
Normal file
80
src/components/business/SeriesGrantPackageTable.vue
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<template>
|
||||||
|
<ElTable
|
||||||
|
v-loading="props.loading"
|
||||||
|
:data="props.data"
|
||||||
|
row-key="package_id"
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
:empty-text="props.emptyText"
|
||||||
|
>
|
||||||
|
<ElTableColumn prop="package_name" label="套餐名称" min-width="220" show-overflow-tooltip />
|
||||||
|
<ElTableColumn prop="package_code" label="套餐编码" min-width="180" show-overflow-tooltip />
|
||||||
|
<ElTableColumn label="成本价" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="amount-value">¥{{ (row.cost_price / 100).toFixed(2) }}</span>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn label="套餐默认生效条件" min-width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.default_expiry_base_name || '-' }}
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn label="覆盖生效条件" min-width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.expiry_base_override_name || '跟随套餐默认' }}
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn label="最终生效条件" min-width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.effective_expiry_base_name || '-' }}
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn label="上架状态" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<ElTag v-if="row.shelf_status === 1" type="success" size="small">上架</ElTag>
|
||||||
|
<ElTag v-else-if="row.shelf_status === 2" type="info" size="small">下架</ElTag>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn label="状态" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<ElTag v-if="row.status === 1" type="success" size="small">启用</ElTag>
|
||||||
|
<ElTag v-else-if="row.status === 2" type="danger" size="small">禁用</ElTag>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn v-if="props.showActions" label="操作" :width="props.actionsWidth" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<slot name="actions" :row="row" />
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
</ElTable>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ElTable, ElTableColumn, ElTag } from 'element-plus'
|
||||||
|
import type { GrantPackageInfo } from '@/types/api'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
data: GrantPackageInfo[]
|
||||||
|
loading?: boolean
|
||||||
|
emptyText?: string
|
||||||
|
showActions?: boolean
|
||||||
|
actionsWidth?: number | string
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
loading: false,
|
||||||
|
emptyText: '暂无套餐',
|
||||||
|
showActions: false,
|
||||||
|
actionsWidth: 240
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.amount-value {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-color-warning);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
242
src/components/business/ShopCreditLimitDialog.vue
Normal file
242
src/components/business/ShopCreditLimitDialog.vue
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
<template>
|
||||||
|
<ElDialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="`调整实际信用额度 - ${currentShop?.shop_name || ''}`"
|
||||||
|
width="520px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@closed="resetDialog"
|
||||||
|
>
|
||||||
|
<ElDescriptions :column="1" border class="credit-preview">
|
||||||
|
<ElDescriptionsItem label="店铺名称">
|
||||||
|
{{ currentShop?.shop_name || '-' }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="当前版本">
|
||||||
|
{{ currentShop?.version ?? '-' }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="修改前">
|
||||||
|
{{ formatCreditPreview(currentShop?.credit_enabled, currentShop?.credit_limit) }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="修改后">
|
||||||
|
{{ formatCreditPreview(creditForm.credit_enabled, creditLimitFen) }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
</ElDescriptions>
|
||||||
|
|
||||||
|
<ElAlert
|
||||||
|
title="实际可用金额、欠款金额和欠款状态以后端刷新后的资金概况为准。"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
class="credit-dialog-alert"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ElForm ref="creditFormRef" :model="creditForm" :rules="creditRules" label-width="110px">
|
||||||
|
<ElFormItem label="启用信用">
|
||||||
|
<ElSwitch v-model="creditForm.credit_enabled" @change="handleCreditEnabledChange" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="实际信用额度" prop="credit_limit_yuan">
|
||||||
|
<ElInputNumber
|
||||||
|
v-model="creditForm.credit_limit_yuan"
|
||||||
|
:disabled="!creditForm.credit_enabled"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
:step="100"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%"
|
||||||
|
placeholder="请输入实际信用额度"
|
||||||
|
/>
|
||||||
|
<div class="credit-dialog-tip">单位:元;关闭信用时额度将自动归零</div>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElForm>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||||
|
<ElButton type="primary" :loading="submitting" @click="handleSubmit">确认调整</ElButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, nextTick, reactive, ref, watch } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
import { CommissionService, ShopService } from '@/api/modules'
|
||||||
|
import type { ShopFundSummaryItem } from '@/types/api/commission'
|
||||||
|
import { fenToYuan, formatMoney, yuanToFen } from '@/utils/business/format'
|
||||||
|
import { normalizeApiError } from '@/utils/business/apiError'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: boolean
|
||||||
|
shop: ShopFundSummaryItem | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: boolean]
|
||||||
|
submitted: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const dialogVisible = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (value: boolean) => emit('update:modelValue', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const creditFormRef = ref<FormInstance>()
|
||||||
|
const submitting = ref(false)
|
||||||
|
const currentShop = ref<ShopFundSummaryItem | null>(null)
|
||||||
|
const creditForm = reactive({
|
||||||
|
credit_enabled: false,
|
||||||
|
credit_limit_yuan: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const creditLimitFen = computed(() =>
|
||||||
|
creditForm.credit_enabled ? yuanToFen(creditForm.credit_limit_yuan) || 0 : 0
|
||||||
|
)
|
||||||
|
|
||||||
|
const creditRules = computed<FormRules>(() => ({
|
||||||
|
credit_limit_yuan: [
|
||||||
|
{
|
||||||
|
validator: (
|
||||||
|
_rule: unknown,
|
||||||
|
value: number | undefined,
|
||||||
|
callback: (error?: Error) => void
|
||||||
|
) => {
|
||||||
|
if (!creditForm.credit_enabled) {
|
||||||
|
callback()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (value === undefined || value === null || Number.isNaN(value)) {
|
||||||
|
callback(new Error('请输入实际信用额度'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (value <= 0) {
|
||||||
|
callback(new Error('实际信用额度必须大于0'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
callback()
|
||||||
|
},
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
|
||||||
|
const formatCreditPreview = (enabled?: boolean, creditLimit?: number) =>
|
||||||
|
enabled ? `启用 / ${formatMoney(creditLimit || 0)}` : '关闭 / ¥0.00'
|
||||||
|
|
||||||
|
const syncForm = (summary: ShopFundSummaryItem) => {
|
||||||
|
creditForm.credit_enabled = Boolean(summary.credit_enabled)
|
||||||
|
creditForm.credit_limit_yuan = summary.credit_enabled ? fenToYuan(summary.credit_limit) : 0
|
||||||
|
nextTick(() => creditFormRef.value?.clearValidate())
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.modelValue, props.shop] as const,
|
||||||
|
([visible, shop]) => {
|
||||||
|
if (visible && shop) {
|
||||||
|
currentShop.value = shop
|
||||||
|
syncForm(shop)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleCreditEnabledChange = (enabled: boolean | string | number) => {
|
||||||
|
if (!enabled) {
|
||||||
|
creditForm.credit_limit_yuan = 0
|
||||||
|
creditFormRef.value?.clearValidate('credit_limit_yuan')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetDialog = () => {
|
||||||
|
creditFormRef.value?.resetFields()
|
||||||
|
currentShop.value = null
|
||||||
|
creditForm.credit_enabled = false
|
||||||
|
creditForm.credit_limit_yuan = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadLatestSummary = async () => {
|
||||||
|
if (!currentShop.value) return null
|
||||||
|
const res = await CommissionService.getShopFundSummary({
|
||||||
|
page: 1,
|
||||||
|
page_size: 100,
|
||||||
|
shop_name: currentShop.value.shop_name
|
||||||
|
})
|
||||||
|
if (res.code !== 0) return null
|
||||||
|
return (
|
||||||
|
(res.data.items || []).find((item) => item.shop_id === currentShop.value?.shop_id) || null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isConflict = (error: unknown) => {
|
||||||
|
const normalized = normalizeApiError(error)
|
||||||
|
return normalized.kind === 'conflict' || normalized.status === 409
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!currentShop.value || !creditFormRef.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await creditFormRef.value.validate()
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const res = await ShopService.updateShopCreditLimit(currentShop.value.shop_id, {
|
||||||
|
credit_enabled: creditForm.credit_enabled,
|
||||||
|
credit_limit: creditLimitFen.value,
|
||||||
|
version: currentShop.value.version
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.code === 0) {
|
||||||
|
ElMessage.success('实际信用额度调整成功')
|
||||||
|
dialogVisible.value = false
|
||||||
|
emit('submitted')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.code === 409) {
|
||||||
|
const latest = await loadLatestSummary()
|
||||||
|
if (latest) {
|
||||||
|
currentShop.value = latest
|
||||||
|
syncForm(latest)
|
||||||
|
}
|
||||||
|
ElMessage.warning('资金概况已被其他操作更新,已刷新最新版本;请确认后重新提交')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.error(res.msg || '实际信用额度调整失败')
|
||||||
|
} catch (error) {
|
||||||
|
if (isConflict(error)) {
|
||||||
|
const latest = await loadLatestSummary()
|
||||||
|
if (latest) {
|
||||||
|
currentShop.value = latest
|
||||||
|
syncForm(latest)
|
||||||
|
}
|
||||||
|
ElMessage.warning('资金概况已被其他操作更新,已刷新最新版本;请确认后重新提交')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(normalizeApiError(error).message)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.credit-preview {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credit-dialog-alert {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credit-dialog-tip {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -400,17 +400,6 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
keepAlive: false
|
keepAlive: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 授权记录详情(放在 asset-management 层级)
|
|
||||||
{
|
|
||||||
path: 'record-management/authorization-records/detail/:id',
|
|
||||||
name: 'AuthorizationRecordDetail',
|
|
||||||
component: RoutesAlias.AuthorizationRecordDetail,
|
|
||||||
meta: {
|
|
||||||
title: 'menus.assetManagement.authorizationRecordDetail',
|
|
||||||
isHide: true,
|
|
||||||
keepAlive: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// 任务管理
|
// 任务管理
|
||||||
{
|
{
|
||||||
path: 'task-management',
|
path: 'task-management',
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ export enum RoutesAlias {
|
|||||||
|
|
||||||
// 记录管理
|
// 记录管理
|
||||||
AuthorizationRecords = '/asset-management/record-management/authorization-records', // 授权记录
|
AuthorizationRecords = '/asset-management/record-management/authorization-records', // 授权记录
|
||||||
AuthorizationRecordDetail = '/asset-management/record-management/authorization-records/detail', // 授权记录详情
|
|
||||||
AssetAssign = '/asset-management/record-management/asset-assign', // 分配记录
|
AssetAssign = '/asset-management/record-management/asset-assign', // 分配记录
|
||||||
AssetAssignDetail = '/asset-management/record-management/asset-assign/detail', // 分配记录详情
|
AssetAssignDetail = '/asset-management/record-management/asset-assign/detail', // 分配记录详情
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ export interface PlatformAccount {
|
|||||||
enterprise_name?: string // ⭐ 新增:企业名称
|
enterprise_name?: string // ⭐ 新增:企业名称
|
||||||
shop_id?: number | null // 关联店铺ID
|
shop_id?: number | null // 关联店铺ID
|
||||||
shop_name?: string // ⭐ 新增:店铺名称
|
shop_name?: string // ⭐ 新增:店铺名称
|
||||||
|
wecom_bound?: boolean // 是否已绑定企业微信账号
|
||||||
|
wecom_corp_id?: string // 企业微信 CorpID
|
||||||
|
wecom_name?: string // 企业微信成员名称
|
||||||
|
wecom_userid?: string // 企业微信成员 UserID
|
||||||
status: AccountStatus // 状态 (0:禁用, 1:启用)
|
status: AccountStatus // 状态 (0:禁用, 1:启用)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,10 +13,17 @@ export enum AgentRechargeStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 支付方式
|
// 支付方式
|
||||||
export type AgentRechargePaymentMethod = 'wechat' | 'offline'
|
export type AgentRechargeOnlinePaymentMethod = 'wechat' | 'alipay'
|
||||||
|
export type AgentRechargePaymentMethod = AgentRechargeOnlinePaymentMethod | 'offline'
|
||||||
|
|
||||||
|
// 充值来源
|
||||||
|
export type AgentRechargeSource = 'agent_online' | 'platform_offline'
|
||||||
|
|
||||||
|
// 第三方支付状态
|
||||||
|
export type AgentRechargePaymentStatus = 0 | 1 | 2 | 3
|
||||||
|
|
||||||
// 支付通道
|
// 支付通道
|
||||||
export type AgentRechargePaymentChannel = 'wechat_direct' | 'fuyou' | 'offline'
|
export type AgentRechargePaymentChannel = string
|
||||||
|
|
||||||
// 代理充值订单
|
// 代理充值订单
|
||||||
export interface AgentRecharge {
|
export interface AgentRecharge {
|
||||||
@@ -31,7 +38,14 @@ export interface AgentRecharge {
|
|||||||
payment_method: AgentRechargePaymentMethod
|
payment_method: AgentRechargePaymentMethod
|
||||||
payment_channel: AgentRechargePaymentChannel
|
payment_channel: AgentRechargePaymentChannel
|
||||||
payment_config_id: number | null
|
payment_config_id: number | null
|
||||||
payment_transaction_id: string
|
payment_transaction_id: string | null
|
||||||
|
payment_no?: string | null
|
||||||
|
request_id?: string | null
|
||||||
|
qr_content?: string | null
|
||||||
|
payment_status?: AgentRechargePaymentStatus | null
|
||||||
|
payment_status_name?: string | null
|
||||||
|
recharge_source?: AgentRechargeSource | null
|
||||||
|
recharge_source_name?: string | null
|
||||||
payment_voucher_key?: string[] | string // 凭证附件列表;历史数据可能为单字符串或逗号字符串
|
payment_voucher_key?: string[] | string // 凭证附件列表;历史数据可能为单字符串或逗号字符串
|
||||||
rejection_reason?: string | null // 拒绝原因
|
rejection_reason?: string | null // 拒绝原因
|
||||||
remark?: string // 运营备注
|
remark?: string // 运营备注
|
||||||
@@ -57,6 +71,7 @@ export interface AgentRechargeQueryParams {
|
|||||||
page_size?: number
|
page_size?: number
|
||||||
shop_id?: number
|
shop_id?: number
|
||||||
status?: AgentRechargeStatus
|
status?: AgentRechargeStatus
|
||||||
|
recharge_source?: AgentRechargeSource
|
||||||
start_date?: string
|
start_date?: string
|
||||||
end_date?: string
|
end_date?: string
|
||||||
dateRange?: string[] // For date range picker in UI
|
dateRange?: string[] // For date range picker in UI
|
||||||
@@ -72,14 +87,45 @@ export interface AgentRechargeListResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 创建代理充值订单请求
|
// 创建代理充值订单请求
|
||||||
export interface CreateAgentRechargeRequest {
|
export interface CreateAgentRechargeOnlineRequest {
|
||||||
amount: number // 充值金额(单位:分),最小 1(即 ¥0.01)
|
amount: number // 充值金额(单位:分),最小 1(即 ¥0.01)
|
||||||
payment_method: AgentRechargePaymentMethod
|
payment_method: AgentRechargeOnlinePaymentMethod
|
||||||
|
request_id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateAgentRechargeOfflineRequest {
|
||||||
|
amount: number // 充值金额(单位:分),最小 1(即 ¥0.01)
|
||||||
|
payment_method: 'offline'
|
||||||
shop_id: number
|
shop_id: number
|
||||||
payment_voucher_key?: string[] // 线下支付凭证附件列表(payment_method=offline 时必填)
|
payment_voucher_key: string[] // 线下支付凭证附件列表(payment_method=offline 时必填)
|
||||||
remark?: string // 运营备注,最多 1000 字
|
remark?: string // 运营备注,最多 1000 字
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CreateAgentRechargeRequest =
|
||||||
|
| CreateAgentRechargeOnlineRequest
|
||||||
|
| CreateAgentRechargeOfflineRequest
|
||||||
|
|
||||||
|
// 在线充值可用支付方式和金额范围
|
||||||
|
export interface AgentRechargePaymentMethods {
|
||||||
|
methods: AgentRechargeOnlinePaymentMethod[]
|
||||||
|
min_amount: number
|
||||||
|
max_amount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在线充值支付及钱包到账状态
|
||||||
|
export interface AgentRechargePaymentStatusResponse {
|
||||||
|
recharge_id: number
|
||||||
|
recharge_no: string
|
||||||
|
recharge_source: AgentRechargeSource
|
||||||
|
recharge_source_name?: string | null
|
||||||
|
status: AgentRechargeStatus
|
||||||
|
status_name?: string | null
|
||||||
|
payment_status: AgentRechargePaymentStatus
|
||||||
|
payment_status_name?: string | null
|
||||||
|
paid_at: string | null
|
||||||
|
completed_at: string | null
|
||||||
|
}
|
||||||
|
|
||||||
// 确认线下充值请求
|
// 确认线下充值请求
|
||||||
export interface ConfirmOfflinePaymentRequest {
|
export interface ConfirmOfflinePaymentRequest {
|
||||||
operation_password: string
|
operation_password: string
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export interface SystemConfigItem {
|
|||||||
value_type: SystemConfigValueType
|
value_type: SystemConfigValueType
|
||||||
description: string
|
description: string
|
||||||
control: string
|
control: string
|
||||||
enum_values: string[]
|
enum_values?: string[]
|
||||||
min: number | null
|
min: number | null
|
||||||
max: number | null
|
max: number | null
|
||||||
readonly: boolean
|
readonly: boolean
|
||||||
|
|||||||
3
src/types/components.d.ts
vendored
3
src/types/components.d.ts
vendored
@@ -148,9 +148,12 @@ declare module 'vue' {
|
|||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
SectionTitle: typeof import('./../components/core/layouts/art-settings-panel/widget/SectionTitle.vue')['default']
|
SectionTitle: typeof import('./../components/core/layouts/art-settings-panel/widget/SectionTitle.vue')['default']
|
||||||
|
SeriesGrantPackageDialog: typeof import('./../components/business/SeriesGrantPackageDialog.vue')['default']
|
||||||
|
SeriesGrantPackageTable: typeof import('./../components/business/SeriesGrantPackageTable.vue')['default']
|
||||||
SettingDrawer: typeof import('./../components/core/layouts/art-settings-panel/widget/SettingDrawer.vue')['default']
|
SettingDrawer: typeof import('./../components/core/layouts/art-settings-panel/widget/SettingDrawer.vue')['default']
|
||||||
SettingHeader: typeof import('./../components/core/layouts/art-settings-panel/widget/SettingHeader.vue')['default']
|
SettingHeader: typeof import('./../components/core/layouts/art-settings-panel/widget/SettingHeader.vue')['default']
|
||||||
SettingItem: typeof import('./../components/core/layouts/art-settings-panel/widget/SettingItem.vue')['default']
|
SettingItem: typeof import('./../components/core/layouts/art-settings-panel/widget/SettingItem.vue')['default']
|
||||||
|
ShopCreditLimitDialog: typeof import('./../components/business/ShopCreditLimitDialog.vue')['default']
|
||||||
SidebarSubmenu: typeof import('./../components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue')['default']
|
SidebarSubmenu: typeof import('./../components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue')['default']
|
||||||
SpeedLimitDialog: typeof import('./../components/device/SpeedLimitDialog.vue')['default']
|
SpeedLimitDialog: typeof import('./../components/device/SpeedLimitDialog.vue')['default']
|
||||||
SwitchCardDialog: typeof import('./../components/device/SwitchCardDialog.vue')['default']
|
SwitchCardDialog: typeof import('./../components/device/SwitchCardDialog.vue')['default']
|
||||||
|
|||||||
@@ -1,4 +1,36 @@
|
|||||||
import type { GrantPackageInfo, PackageResponse } from '@/types/api'
|
import type {
|
||||||
|
GrantPackageInfo,
|
||||||
|
PackageAllocationExpiryBaseOverride,
|
||||||
|
PackageResponse
|
||||||
|
} from '@/types/api'
|
||||||
|
|
||||||
|
export type ExpiryBaseSelection = PackageAllocationExpiryBaseOverride | 'default'
|
||||||
|
|
||||||
|
export type PackageCostFormItem = {
|
||||||
|
package_id: number
|
||||||
|
package_name?: string
|
||||||
|
package_code?: string
|
||||||
|
cost_price_yuan: number
|
||||||
|
original_cost_price?: number
|
||||||
|
suggested_retail_price?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SeriesGrantPackageForm = {
|
||||||
|
package_id?: number
|
||||||
|
allocation_id?: number
|
||||||
|
package_ids: number[]
|
||||||
|
package_name?: string
|
||||||
|
package_code?: string
|
||||||
|
cost_price_yuan: number
|
||||||
|
original_cost_price?: number
|
||||||
|
suggested_retail_price?: number
|
||||||
|
expiry_base_override: ExpiryBaseSelection
|
||||||
|
initial_expiry_base_override: ExpiryBaseSelection
|
||||||
|
default_expiry_base_name?: string | null
|
||||||
|
expiry_base_override_name?: string | null
|
||||||
|
effective_expiry_base_name?: string | null
|
||||||
|
packages: PackageCostFormItem[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface GrantPackageCandidate {
|
export interface GrantPackageCandidate {
|
||||||
id: number
|
id: number
|
||||||
|
|||||||
@@ -512,6 +512,8 @@
|
|||||||
{ label: '账号类型', prop: 'user_type' },
|
{ label: '账号类型', prop: 'user_type' },
|
||||||
{ label: '店铺名称', prop: 'shop_name' },
|
{ label: '店铺名称', prop: 'shop_name' },
|
||||||
{ label: '企业名称', prop: 'enterprise_name' },
|
{ label: '企业名称', prop: 'enterprise_name' },
|
||||||
|
{ label: '企微绑定', prop: 'wecom_bound' },
|
||||||
|
{ label: '企微用户名称', prop: 'wecom_name' },
|
||||||
...(canModifyAccountStatus ? [{ label: '状态', prop: 'status' }] : []),
|
...(canModifyAccountStatus ? [{ label: '状态', prop: 'status' }] : []),
|
||||||
{ label: '创建时间', prop: 'created_at' }
|
{ label: '创建时间', prop: 'created_at' }
|
||||||
]
|
]
|
||||||
@@ -620,6 +622,21 @@
|
|||||||
return row.enterprise_name || '-'
|
return row.enterprise_name || '-'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
prop: 'wecom_bound',
|
||||||
|
label: '企微绑定',
|
||||||
|
width: 100,
|
||||||
|
formatter: (row: any) =>
|
||||||
|
h(ElTag, { type: row.wecom_bound ? 'success' : 'info', size: 'small' }, () =>
|
||||||
|
row.wecom_bound ? '已绑定' : '未绑定'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'wecom_name',
|
||||||
|
label: '企微用户名称',
|
||||||
|
minWidth: 130,
|
||||||
|
formatter: (row: any) => row.wecom_name || '-'
|
||||||
|
},
|
||||||
...(canModifyAccountStatus
|
...(canModifyAccountStatus
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@@ -688,7 +705,7 @@
|
|||||||
return actions
|
return actions
|
||||||
}
|
}
|
||||||
|
|
||||||
const showWecomBindingDialog = (row: any) => {
|
const showWecomBindingDialog = async (row: any) => {
|
||||||
if (row.user_type !== 2) {
|
if (row.user_type !== 2) {
|
||||||
ElMessage.warning('只有平台用户可以绑定企微账号')
|
ElMessage.warning('只有平台用户可以绑定企微账号')
|
||||||
return
|
return
|
||||||
@@ -699,7 +716,17 @@
|
|||||||
wecomBindingForm.userid = ''
|
wecomBindingForm.userid = ''
|
||||||
wecomMembers.value = []
|
wecomMembers.value = []
|
||||||
wecomBindingDialogVisible.value = true
|
wecomBindingDialogVisible.value = true
|
||||||
void loadWecomApplications()
|
await loadWecomApplications()
|
||||||
|
|
||||||
|
if (row.wecom_bound && row.wecom_corp_id) {
|
||||||
|
const boundApplication = wecomApplications.value.find(
|
||||||
|
(application) => application.corp_id === row.wecom_corp_id
|
||||||
|
)
|
||||||
|
if (boundApplication) {
|
||||||
|
wecomBindingForm.application_id = boundApplication.id
|
||||||
|
await handleWecomApplicationChange(boundApplication.id, row.wecom_userid || '')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadWecomApplications = async () => {
|
const loadWecomApplications = async () => {
|
||||||
@@ -712,8 +739,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleWecomApplicationChange = async (applicationId?: number) => {
|
const handleWecomApplicationChange = async (applicationId?: number, selectedUserid = '') => {
|
||||||
wecomBindingForm.userid = ''
|
wecomBindingForm.userid = selectedUserid
|
||||||
wecomMembers.value = []
|
wecomMembers.value = []
|
||||||
if (!applicationId) return
|
if (!applicationId) return
|
||||||
|
|
||||||
@@ -723,7 +750,15 @@
|
|||||||
page: 1,
|
page: 1,
|
||||||
page_size: 100
|
page_size: 100
|
||||||
})
|
})
|
||||||
if (response.code === 0) wecomMembers.value = response.data.items || []
|
if (response.code === 0) {
|
||||||
|
wecomMembers.value = response.data.items || []
|
||||||
|
if (
|
||||||
|
selectedUserid &&
|
||||||
|
!wecomMembers.value.some((member) => member.userid === selectedUserid)
|
||||||
|
) {
|
||||||
|
wecomBindingForm.userid = selectedUserid
|
||||||
|
}
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
wecomMembersLoading.value = false
|
wecomMembersLoading.value = false
|
||||||
}
|
}
|
||||||
@@ -753,6 +788,7 @@
|
|||||||
if (response.code === 0) {
|
if (response.code === 0) {
|
||||||
ElMessage.success('账号企微绑定成功')
|
ElMessage.success('账号企微绑定成功')
|
||||||
wecomBindingDialogVisible.value = false
|
wecomBindingDialogVisible.value = false
|
||||||
|
await getAccountList()
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
wecomBindingSubmitting.value = false
|
wecomBindingSubmitting.value = false
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
<ElFormItem label="固定档位" prop="code">
|
<ElFormItem label="固定档位" prop="code">
|
||||||
<ElSelect
|
<ElSelect
|
||||||
v-model="form.code"
|
v-model="form.code"
|
||||||
|
placeholder="请选择限速档位"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
:teleported="true"
|
:teleported="true"
|
||||||
append-to="body"
|
append-to="body"
|
||||||
@@ -50,9 +51,11 @@
|
|||||||
confirmLoading?: boolean
|
confirmLoading?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SpeedTierCode = -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
(e: 'update:modelValue', value: boolean): void
|
(e: 'update:modelValue', value: boolean): void
|
||||||
(e: 'confirm', data: { code: -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 }): void
|
(e: 'confirm', data: { code: SpeedTierCode }): void
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@@ -74,7 +77,7 @@
|
|||||||
{ code: 8, label: '100Mbps' }
|
{ code: 8, label: '100Mbps' }
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
const form = reactive({ code: 3 as (typeof speedTiers)[number]['code'] })
|
const form = reactive<{ code: SpeedTierCode | undefined }>({ code: undefined })
|
||||||
|
|
||||||
const rules: FormRules = {
|
const rules: FormRules = {
|
||||||
code: [{ required: true, message: '请选择限速档位', trigger: 'change' }]
|
code: [{ required: true, message: '请选择限速档位', trigger: 'change' }]
|
||||||
@@ -97,7 +100,7 @@
|
|||||||
(newVal) => {
|
(newVal) => {
|
||||||
setLayoutScrollLock(newVal)
|
setLayoutScrollLock(newVal)
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
form.code = 3
|
form.code = undefined
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
@@ -117,6 +120,7 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await formRef.value.validate()
|
await formRef.value.validate()
|
||||||
|
if (form.code === undefined) return
|
||||||
emit('confirm', {
|
emit('confirm', {
|
||||||
code: form.code
|
code: form.code
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -32,6 +32,14 @@
|
|||||||
>
|
>
|
||||||
批量回收
|
批量回收
|
||||||
</ElButton>
|
</ElButton>
|
||||||
|
<ElButton
|
||||||
|
type="info"
|
||||||
|
:disabled="!selectedDevices.length"
|
||||||
|
@click="handleBatchSetSeries"
|
||||||
|
v-permission="'device:batch_set_series'"
|
||||||
|
>
|
||||||
|
批量设置套餐系列
|
||||||
|
</ElButton>
|
||||||
<ElButton
|
<ElButton
|
||||||
type="primary"
|
type="primary"
|
||||||
:disabled="!selectedDevices.length"
|
:disabled="!selectedDevices.length"
|
||||||
@@ -2656,6 +2664,18 @@
|
|||||||
await loadPackageSeriesList(query || undefined)
|
await loadPackageSeriesList(query || undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 显示批量设置套餐系列对话框
|
||||||
|
const handleBatchSetSeries = async () => {
|
||||||
|
seriesBindingResult.value = null
|
||||||
|
Object.assign(seriesBindingForm, createInitialSeriesBindingState(), {
|
||||||
|
selection_type:
|
||||||
|
selectedDevices.value.length > 0 ? DeviceSelectionType.LIST : DeviceSelectionType.FILTER
|
||||||
|
})
|
||||||
|
await Promise.all([loadPackageSeriesList(), loadSearchBatchNoOptions()])
|
||||||
|
seriesBindingDialogVisible.value = true
|
||||||
|
seriesBindingFormRef.value?.clearValidate()
|
||||||
|
}
|
||||||
|
|
||||||
const buildSeriesBindingRequest = (): BatchSetDeviceSeriesBindingRequest | null => {
|
const buildSeriesBindingRequest = (): BatchSetDeviceSeriesBindingRequest | null => {
|
||||||
const request: BatchSetDeviceSeriesBindingRequest = {
|
const request: BatchSetDeviceSeriesBindingRequest = {
|
||||||
selection_type: seriesBindingForm.selection_type,
|
selection_type: seriesBindingForm.selection_type,
|
||||||
|
|||||||
@@ -136,6 +136,7 @@
|
|||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
clearable
|
clearable
|
||||||
@change="handleOldIdentifierChange"
|
@change="handleOldIdentifierChange"
|
||||||
|
@focus="handleOldIotCardsFocus"
|
||||||
>
|
>
|
||||||
<ElOption
|
<ElOption
|
||||||
v-for="card in oldIotCardOptions"
|
v-for="card in oldIotCardOptions"
|
||||||
@@ -151,6 +152,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</ElOption>
|
</ElOption>
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
|
<div v-if="oldIotCardEmptyMessage" class="exchange-field-error">
|
||||||
|
{{ oldIotCardEmptyMessage }}
|
||||||
|
</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem
|
<ElFormItem
|
||||||
v-if="createForm.old_asset_type === 'device'"
|
v-if="createForm.old_asset_type === 'device'"
|
||||||
@@ -169,6 +173,7 @@
|
|||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
clearable
|
clearable
|
||||||
@change="handleOldIdentifierChange"
|
@change="handleOldIdentifierChange"
|
||||||
|
@focus="handleOldDevicesFocus"
|
||||||
>
|
>
|
||||||
<ElOption
|
<ElOption
|
||||||
v-for="device in oldDeviceOptions"
|
v-for="device in oldDeviceOptions"
|
||||||
@@ -184,6 +189,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</ElOption>
|
</ElOption>
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
|
<div v-if="oldDeviceEmptyMessage" class="exchange-field-error">
|
||||||
|
{{ oldDeviceEmptyMessage }}
|
||||||
|
</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem v-if="createOldAssetShopDisplay" label="旧资产所属店铺">
|
<ElFormItem v-if="createOldAssetShopDisplay" label="旧资产所属店铺">
|
||||||
<ElInput :model-value="createOldAssetShopDisplay" readonly disabled />
|
<ElInput :model-value="createOldAssetShopDisplay" readonly disabled />
|
||||||
@@ -204,6 +212,7 @@
|
|||||||
no-data-text="暂无同店铺可用的新资产"
|
no-data-text="暂无同店铺可用的新资产"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
clearable
|
clearable
|
||||||
|
@focus="handleCreateNewIotCardsFocus"
|
||||||
>
|
>
|
||||||
<ElOption
|
<ElOption
|
||||||
v-for="card in newIotCardOptions"
|
v-for="card in newIotCardOptions"
|
||||||
@@ -219,6 +228,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</ElOption>
|
</ElOption>
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
|
<div v-if="createNewIotCardEmptyMessage" class="exchange-field-error">
|
||||||
|
{{ createNewIotCardEmptyMessage }}
|
||||||
|
</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem
|
<ElFormItem
|
||||||
v-if="createForm.flow_type === 'direct' && createForm.old_asset_type === 'device'"
|
v-if="createForm.flow_type === 'direct' && createForm.old_asset_type === 'device'"
|
||||||
@@ -236,6 +248,7 @@
|
|||||||
no-data-text="暂无同店铺可用的新资产"
|
no-data-text="暂无同店铺可用的新资产"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
clearable
|
clearable
|
||||||
|
@focus="handleCreateNewDevicesFocus"
|
||||||
>
|
>
|
||||||
<ElOption
|
<ElOption
|
||||||
v-for="device in newDeviceOptions"
|
v-for="device in newDeviceOptions"
|
||||||
@@ -251,6 +264,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</ElOption>
|
</ElOption>
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
|
<div v-if="createNewDeviceEmptyMessage" class="exchange-field-error">
|
||||||
|
{{ createNewDeviceEmptyMessage }}
|
||||||
|
</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem v-if="createDirectInheritedShopHint" label="归属提示">
|
<ElFormItem v-if="createDirectInheritedShopHint" label="归属提示">
|
||||||
<div class="exchange-shop-hint">{{ createDirectInheritedShopHint }}</div>
|
<div class="exchange-shop-hint">{{ createDirectInheritedShopHint }}</div>
|
||||||
@@ -308,6 +324,8 @@
|
|||||||
no-data-text="暂无可用的新资产"
|
no-data-text="暂无可用的新资产"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
clearable
|
clearable
|
||||||
|
@change="handleShipNewIdentifierChange"
|
||||||
|
@focus="handleShipNewIotCardsFocus"
|
||||||
>
|
>
|
||||||
<ElOption
|
<ElOption
|
||||||
v-for="card in newIotCardOptions"
|
v-for="card in newIotCardOptions"
|
||||||
@@ -323,6 +341,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</ElOption>
|
</ElOption>
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
|
<div v-if="shipNewIotCardEmptyMessage" class="exchange-field-error">
|
||||||
|
{{ shipNewIotCardEmptyMessage }}
|
||||||
|
</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem
|
<ElFormItem
|
||||||
v-else-if="shipAssetType === 'device'"
|
v-else-if="shipAssetType === 'device'"
|
||||||
@@ -340,6 +361,8 @@
|
|||||||
no-data-text="暂无可用的新资产"
|
no-data-text="暂无可用的新资产"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
clearable
|
clearable
|
||||||
|
@change="handleShipNewIdentifierChange"
|
||||||
|
@focus="handleShipNewDevicesFocus"
|
||||||
>
|
>
|
||||||
<ElOption
|
<ElOption
|
||||||
v-for="device in newDeviceOptions"
|
v-for="device in newDeviceOptions"
|
||||||
@@ -355,6 +378,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</ElOption>
|
</ElOption>
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
|
<div v-if="shipNewDeviceEmptyMessage" class="exchange-field-error">
|
||||||
|
{{ shipNewDeviceEmptyMessage }}
|
||||||
|
</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem v-if="shipInheritedShopDisplayText" label="归属提示">
|
<ElFormItem v-if="shipInheritedShopDisplayText" label="归属提示">
|
||||||
<div class="exchange-shop-hint">{{ shipInheritedShopDisplayText }}</div>
|
<div class="exchange-shop-hint">{{ shipInheritedShopDisplayText }}</div>
|
||||||
@@ -534,6 +560,42 @@
|
|||||||
const newDeviceSearchLoading = ref(false)
|
const newDeviceSearchLoading = ref(false)
|
||||||
const completeLoadingId = ref<number | null>(null)
|
const completeLoadingId = ref<number | null>(null)
|
||||||
|
|
||||||
|
type AssetFilterOption = {
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldAssetFilterOptions = ref<AssetFilterOption[]>([])
|
||||||
|
const newAssetFilterOptions = ref<AssetFilterOption[]>([])
|
||||||
|
const oldAssetFilterLoading = ref(false)
|
||||||
|
const newAssetFilterLoading = ref(false)
|
||||||
|
|
||||||
|
const oldIotCardEmptyMessage = computed(() =>
|
||||||
|
oldCardSearchLoading.value || oldIotCardOptions.value.length > 0 ? '' : '暂无可用的旧资产'
|
||||||
|
)
|
||||||
|
|
||||||
|
const oldDeviceEmptyMessage = computed(() =>
|
||||||
|
oldDeviceSearchLoading.value || oldDeviceOptions.value.length > 0 ? '' : '暂无可用的旧资产'
|
||||||
|
)
|
||||||
|
|
||||||
|
const createNewIotCardEmptyMessage = computed(() => {
|
||||||
|
if (newCardSearchLoading.value || newIotCardOptions.value.length > 0) return ''
|
||||||
|
return createForm.old_identifier ? '暂无同店铺可用的新资产' : '请先选择旧资产'
|
||||||
|
})
|
||||||
|
|
||||||
|
const createNewDeviceEmptyMessage = computed(() => {
|
||||||
|
if (newDeviceSearchLoading.value || newDeviceOptions.value.length > 0) return ''
|
||||||
|
return createForm.old_identifier ? '暂无同店铺可用的新资产' : '请先选择旧资产'
|
||||||
|
})
|
||||||
|
|
||||||
|
const shipNewIotCardEmptyMessage = computed(() =>
|
||||||
|
newCardSearchLoading.value || newIotCardOptions.value.length > 0 ? '' : '暂无可用的新资产'
|
||||||
|
)
|
||||||
|
|
||||||
|
const shipNewDeviceEmptyMessage = computed(() =>
|
||||||
|
newDeviceSearchLoading.value || newDeviceOptions.value.length > 0 ? '' : '暂无可用的新资产'
|
||||||
|
)
|
||||||
|
|
||||||
// 发货表单
|
// 发货表单
|
||||||
const shipForm = reactive({
|
const shipForm = reactive({
|
||||||
new_identifier: '',
|
new_identifier: '',
|
||||||
@@ -543,11 +605,90 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
const shipRules = reactive<FormRules>({
|
const shipRules = reactive<FormRules>({
|
||||||
new_identifier: [{ required: true, message: '请输入新资产标识符', trigger: 'blur' }],
|
new_identifier: [{ required: true, message: '请选择新资产标识符', trigger: 'change' }],
|
||||||
express_company: [{ required: true, message: '请输入快递公司', trigger: 'blur' }],
|
express_company: [{ required: true, message: '请输入快递公司', trigger: 'blur' }],
|
||||||
express_no: [{ required: true, message: '请输入快递单号', trigger: 'blur' }]
|
express_no: [{ required: true, message: '请输入快递单号', trigger: 'blur' }]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const handleShipNewIdentifierChange = () => {
|
||||||
|
shipFormRef.value?.clearValidate('new_identifier')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleShipNewIotCardsFocus = async () => {
|
||||||
|
if (newCardSearchLoading.value || newIotCardOptions.value.length > 0) return
|
||||||
|
await loadDefaultShipNewIotCards()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleShipNewDevicesFocus = async () => {
|
||||||
|
if (newDeviceSearchLoading.value || newDeviceOptions.value.length > 0) return
|
||||||
|
await loadDefaultShipNewDevices()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOldIotCardsFocus = async () => {
|
||||||
|
if (oldCardSearchLoading.value || oldIotCardOptions.value.length > 0) return
|
||||||
|
await loadDefaultIotCards()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOldDevicesFocus = async () => {
|
||||||
|
if (oldDeviceSearchLoading.value || oldDeviceOptions.value.length > 0) return
|
||||||
|
await loadDefaultDevices()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCreateNewIotCardsFocus = async () => {
|
||||||
|
if (newCardSearchLoading.value || newIotCardOptions.value.length > 0) return
|
||||||
|
await loadDefaultNewIotCards()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCreateNewDevicesFocus = async () => {
|
||||||
|
if (newDeviceSearchLoading.value || newDeviceOptions.value.length > 0) return
|
||||||
|
await loadDefaultNewDevices()
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchFilterAssets = async (query: string, target: 'old' | 'new') => {
|
||||||
|
const options = target === 'old' ? oldAssetFilterOptions : newAssetFilterOptions
|
||||||
|
const loading = target === 'old' ? oldAssetFilterLoading : newAssetFilterLoading
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const keyword = query.trim() || undefined
|
||||||
|
const [cardRes, deviceRes] = await Promise.all([
|
||||||
|
CardService.getStandaloneIotCards({
|
||||||
|
is_standalone: true,
|
||||||
|
keyword,
|
||||||
|
page: 1,
|
||||||
|
page_size: 10
|
||||||
|
}),
|
||||||
|
DeviceService.getDevices({
|
||||||
|
keyword,
|
||||||
|
page: 1,
|
||||||
|
page_size: 10
|
||||||
|
})
|
||||||
|
])
|
||||||
|
|
||||||
|
const cardOptions: AssetFilterOption[] =
|
||||||
|
cardRes.code === 0
|
||||||
|
? (cardRes.data?.items || []).map((card) => ({
|
||||||
|
label: `${card.iccid} (IoT卡)`,
|
||||||
|
value: card.iccid
|
||||||
|
}))
|
||||||
|
: []
|
||||||
|
const deviceOptions: AssetFilterOption[] =
|
||||||
|
deviceRes.code === 0
|
||||||
|
? (deviceRes.data?.items || []).map((device) => ({
|
||||||
|
label: `${device.virtual_no} (设备)`,
|
||||||
|
value: device.virtual_no
|
||||||
|
}))
|
||||||
|
: []
|
||||||
|
|
||||||
|
options.value = [...cardOptions, ...deviceOptions]
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载资产筛选选项失败:', error)
|
||||||
|
options.value = []
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type ShopSelectableOption = {
|
type ShopSelectableOption = {
|
||||||
shop_id?: number | null
|
shop_id?: number | null
|
||||||
shop_name?: string | null
|
shop_name?: string | null
|
||||||
@@ -644,7 +785,7 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 搜索表单配置
|
// 搜索表单配置
|
||||||
const searchFormItems: SearchFormItem[] = [
|
const searchFormItems = computed<SearchFormItem[]>(() => [
|
||||||
{
|
{
|
||||||
label: '换货状态',
|
label: '换货状态',
|
||||||
prop: 'status',
|
prop: 'status',
|
||||||
@@ -677,19 +818,31 @@
|
|||||||
{
|
{
|
||||||
label: '旧资产',
|
label: '旧资产',
|
||||||
prop: 'old_asset_keyword',
|
prop: 'old_asset_keyword',
|
||||||
type: 'input',
|
type: 'select',
|
||||||
|
options: () => oldAssetFilterOptions.value,
|
||||||
config: {
|
config: {
|
||||||
placeholder: '请输入旧资产标识',
|
placeholder: '请选择或搜索旧资产标识',
|
||||||
clearable: true
|
clearable: true,
|
||||||
|
filterable: true,
|
||||||
|
remote: true,
|
||||||
|
reserveKeyword: true,
|
||||||
|
loading: oldAssetFilterLoading.value,
|
||||||
|
remoteMethod: (query: string) => searchFilterAssets(query, 'old')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '新资产',
|
label: '新资产',
|
||||||
prop: 'new_asset_keyword',
|
prop: 'new_asset_keyword',
|
||||||
type: 'input',
|
type: 'select',
|
||||||
|
options: () => newAssetFilterOptions.value,
|
||||||
config: {
|
config: {
|
||||||
placeholder: '请输入新资产标识',
|
placeholder: '请选择或搜索新资产标识',
|
||||||
clearable: true
|
clearable: true,
|
||||||
|
filterable: true,
|
||||||
|
remote: true,
|
||||||
|
reserveKeyword: true,
|
||||||
|
loading: newAssetFilterLoading.value,
|
||||||
|
remoteMethod: (query: string) => searchFilterAssets(query, 'new')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -704,7 +857,7 @@
|
|||||||
valueFormat: 'YYYY-MM-DD'
|
valueFormat: 'YYYY-MM-DD'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
])
|
||||||
|
|
||||||
// 动态列配置
|
// 动态列配置
|
||||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||||
@@ -1037,9 +1190,6 @@
|
|||||||
})
|
})
|
||||||
if (res.code === 0 && res.data) {
|
if (res.code === 0 && res.data) {
|
||||||
oldIotCardOptions.value = res.data.items || []
|
oldIotCardOptions.value = res.data.items || []
|
||||||
if (oldIotCardOptions.value.length === 0) {
|
|
||||||
ElMessage.info('暂无可用的IoT卡')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载IoT卡列表失败:', error)
|
console.error('加载IoT卡列表失败:', error)
|
||||||
@@ -1059,9 +1209,6 @@
|
|||||||
})
|
})
|
||||||
if (res.code === 0 && res.data) {
|
if (res.code === 0 && res.data) {
|
||||||
oldDeviceOptions.value = res.data.items || []
|
oldDeviceOptions.value = res.data.items || []
|
||||||
if (oldDeviceOptions.value.length === 0) {
|
|
||||||
ElMessage.info('暂无可用的设备')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载设备列表失败:', error)
|
console.error('加载设备列表失败:', error)
|
||||||
@@ -1089,9 +1236,6 @@
|
|||||||
})
|
})
|
||||||
if (res.code === 0 && res.data) {
|
if (res.code === 0 && res.data) {
|
||||||
oldIotCardOptions.value = res.data.items || []
|
oldIotCardOptions.value = res.data.items || []
|
||||||
if (oldIotCardOptions.value.length === 0) {
|
|
||||||
ElMessage.info('未找到匹配的IoT卡')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('搜索IoT卡失败:', error)
|
console.error('搜索IoT卡失败:', error)
|
||||||
@@ -1118,9 +1262,6 @@
|
|||||||
})
|
})
|
||||||
if (res.code === 0 && res.data) {
|
if (res.code === 0 && res.data) {
|
||||||
oldDeviceOptions.value = res.data.items || []
|
oldDeviceOptions.value = res.data.items || []
|
||||||
if (oldDeviceOptions.value.length === 0) {
|
|
||||||
ElMessage.info('未找到匹配的设备')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('搜索设备失败:', error)
|
console.error('搜索设备失败:', error)
|
||||||
@@ -1141,7 +1282,6 @@
|
|||||||
const shopId = getCreateOldAssetShopId()
|
const shopId = getCreateOldAssetShopId()
|
||||||
if (shopId === undefined) {
|
if (shopId === undefined) {
|
||||||
newIotCardOptions.value = []
|
newIotCardOptions.value = []
|
||||||
ElMessage.warning('请先选择旧资产')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1155,9 +1295,6 @@
|
|||||||
})
|
})
|
||||||
if (res.code === 0 && res.data) {
|
if (res.code === 0 && res.data) {
|
||||||
newIotCardOptions.value = res.data.items || []
|
newIotCardOptions.value = res.data.items || []
|
||||||
if (newIotCardOptions.value.length === 0) {
|
|
||||||
ElMessage.info('未找到匹配的IoT卡')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('搜索IoT卡失败:', error)
|
console.error('搜索IoT卡失败:', error)
|
||||||
@@ -1184,9 +1321,6 @@
|
|||||||
})
|
})
|
||||||
if (res.code === 0 && res.data) {
|
if (res.code === 0 && res.data) {
|
||||||
newIotCardOptions.value = res.data.items || []
|
newIotCardOptions.value = res.data.items || []
|
||||||
if (newIotCardOptions.value.length === 0) {
|
|
||||||
ElMessage.info('未找到匹配的IoT卡')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('搜索IoT卡失败:', error)
|
console.error('搜索IoT卡失败:', error)
|
||||||
@@ -1213,9 +1347,6 @@
|
|||||||
})
|
})
|
||||||
if (res.code === 0 && res.data) {
|
if (res.code === 0 && res.data) {
|
||||||
newIotCardOptions.value = res.data.items || []
|
newIotCardOptions.value = res.data.items || []
|
||||||
if (newIotCardOptions.value.length === 0) {
|
|
||||||
ElMessage.info('暂无同店铺可用的新资产')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载IoT卡列表失败:', error)
|
console.error('加载IoT卡列表失败:', error)
|
||||||
@@ -1234,8 +1365,8 @@
|
|||||||
page: 1,
|
page: 1,
|
||||||
page_size: 10
|
page_size: 10
|
||||||
})
|
})
|
||||||
if (res.code === 0 && res.data) {
|
if (res.code === 0) {
|
||||||
newIotCardOptions.value = res.data.items || []
|
newIotCardOptions.value = res.data?.items || []
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载IoT卡列表失败:', error)
|
console.error('加载IoT卡列表失败:', error)
|
||||||
@@ -1256,7 +1387,6 @@
|
|||||||
const shopId = getCreateOldAssetShopId()
|
const shopId = getCreateOldAssetShopId()
|
||||||
if (shopId === undefined) {
|
if (shopId === undefined) {
|
||||||
newDeviceOptions.value = []
|
newDeviceOptions.value = []
|
||||||
ElMessage.warning('请先选择旧资产')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1269,9 +1399,6 @@
|
|||||||
})
|
})
|
||||||
if (res.code === 0 && res.data) {
|
if (res.code === 0 && res.data) {
|
||||||
newDeviceOptions.value = res.data.items || []
|
newDeviceOptions.value = res.data.items || []
|
||||||
if (newDeviceOptions.value.length === 0) {
|
|
||||||
ElMessage.info('未找到匹配的设备')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('搜索设备失败:', error)
|
console.error('搜索设备失败:', error)
|
||||||
@@ -1297,9 +1424,6 @@
|
|||||||
})
|
})
|
||||||
if (res.code === 0 && res.data) {
|
if (res.code === 0 && res.data) {
|
||||||
newDeviceOptions.value = res.data.items || []
|
newDeviceOptions.value = res.data.items || []
|
||||||
if (newDeviceOptions.value.length === 0) {
|
|
||||||
ElMessage.info('未找到匹配的设备')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('搜索设备失败:', error)
|
console.error('搜索设备失败:', error)
|
||||||
@@ -1325,9 +1449,6 @@
|
|||||||
})
|
})
|
||||||
if (res.code === 0 && res.data) {
|
if (res.code === 0 && res.data) {
|
||||||
newDeviceOptions.value = res.data.items || []
|
newDeviceOptions.value = res.data.items || []
|
||||||
if (newDeviceOptions.value.length === 0) {
|
|
||||||
ElMessage.info('暂无同店铺可用的新资产')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载设备列表失败:', error)
|
console.error('加载设备列表失败:', error)
|
||||||
@@ -1345,8 +1466,8 @@
|
|||||||
page: 1,
|
page: 1,
|
||||||
page_size: 10
|
page_size: 10
|
||||||
})
|
})
|
||||||
if (res.code === 0 && res.data) {
|
if (res.code === 0) {
|
||||||
newDeviceOptions.value = res.data.items || []
|
newDeviceOptions.value = res.data?.items || []
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载设备列表失败:', error)
|
console.error('加载设备列表失败:', error)
|
||||||
@@ -1641,6 +1762,8 @@
|
|||||||
// 初始化
|
// 初始化
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadExchangeList()
|
loadExchangeList()
|
||||||
|
searchFilterAssets('', 'old')
|
||||||
|
searchFilterAssets('', 'new')
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -1657,5 +1780,13 @@
|
|||||||
border: 1px solid var(--el-color-primary-light-7);
|
border: 1px solid var(--el-color-primary-light-7);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.exchange-field-error {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 4px;
|
||||||
|
color: var(--el-color-danger);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,140 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="authorization-record-detail">
|
|
||||||
<ElCard shadow="never">
|
|
||||||
<!-- 页面头部 -->
|
|
||||||
<div class="detail-header">
|
|
||||||
<ElButton @click="handleBack">
|
|
||||||
<template #icon>
|
|
||||||
<ElIcon><ArrowLeft /></ElIcon>
|
|
||||||
</template>
|
|
||||||
返回
|
|
||||||
</ElButton>
|
|
||||||
<h2 class="detail-title">授权记录详情</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 详情内容 -->
|
|
||||||
<DetailPage v-if="detailData" :sections="detailSections" :data="detailData" />
|
|
||||||
|
|
||||||
<!-- 加载中 -->
|
|
||||||
<div v-if="loading" class="loading-container">
|
|
||||||
<ElIcon class="is-loading"><Loading /></ElIcon>
|
|
||||||
<span>加载中...</span>
|
|
||||||
</div>
|
|
||||||
</ElCard>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, onMounted } from 'vue'
|
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
|
||||||
import { ElCard, ElButton, ElIcon, ElMessage } from 'element-plus'
|
|
||||||
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
|
|
||||||
import DetailPage from '@/components/common/DetailPage.vue'
|
|
||||||
import type { DetailSection } from '@/components/common/DetailPage.vue'
|
|
||||||
import { AuthorizationService } from '@/api/modules'
|
|
||||||
import type { AuthorizationItem } from '@/types/api/authorization'
|
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
|
||||||
|
|
||||||
defineOptions({ name: 'AuthorizationRecordDetail' })
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const loading = ref(false)
|
|
||||||
const detailData = ref<AuthorizationItem | null>(null)
|
|
||||||
|
|
||||||
// 详情页配置
|
|
||||||
const detailSections: DetailSection[] = [
|
|
||||||
{
|
|
||||||
title: '基本信息',
|
|
||||||
fields: [
|
|
||||||
{ label: '企业ID', prop: 'enterprise_id' },
|
|
||||||
{ label: '企业名称', prop: 'enterprise_name' },
|
|
||||||
{ label: '卡ID', prop: 'card_id' },
|
|
||||||
{ label: 'ICCID', prop: 'iccid' },
|
|
||||||
{ label: '手机号', prop: 'msisdn', formatter: (value) => value || '-' },
|
|
||||||
{ label: '授权人ID', prop: 'authorized_by' },
|
|
||||||
{ label: '授权人', prop: 'authorizer_name' },
|
|
||||||
{
|
|
||||||
label: '授权人类型',
|
|
||||||
formatter: (_, data) => {
|
|
||||||
return data.authorizer_type === 2 ? '平台' : '代理'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ label: '授权时间', prop: 'authorized_at', formatter: (value) => formatDateTime(value) },
|
|
||||||
{
|
|
||||||
label: '状态',
|
|
||||||
formatter: (_, data) => {
|
|
||||||
return data.status === 1 ? '有效' : '已回收'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ label: '备注', prop: 'remark', formatter: (value) => value || '-' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
// 返回上一页
|
|
||||||
const handleBack = () => {
|
|
||||||
router.back()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取详情数据
|
|
||||||
const fetchDetail = async () => {
|
|
||||||
const id = Number(route.params.id)
|
|
||||||
if (!id) {
|
|
||||||
ElMessage.error('缺少ID参数')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const res = await AuthorizationService.getAuthorizationDetail(id)
|
|
||||||
if (res.code === 0) {
|
|
||||||
detailData.value = res.data
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
console.log('获取授权记录详情失败')
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
fetchDetail()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.authorization-record-detail {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-header {
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
align-items: center;
|
|
||||||
padding-bottom: 16px;
|
|
||||||
|
|
||||||
.detail-title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 60px 20px;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
|
|
||||||
.el-icon {
|
|
||||||
font-size: 32px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -28,29 +28,16 @@
|
|||||||
:pageSize="pagination.pageSize"
|
:pageSize="pagination.pageSize"
|
||||||
:total="pagination.total"
|
:total="pagination.total"
|
||||||
:marginTop="10"
|
:marginTop="10"
|
||||||
:row-class-name="getRowClassName"
|
:actions="getActions"
|
||||||
|
:actionsWidth="100"
|
||||||
@size-change="handleSizeChange"
|
@size-change="handleSizeChange"
|
||||||
@current-change="handleCurrentChange"
|
@current-change="handleCurrentChange"
|
||||||
@row-contextmenu="handleRowContextMenu"
|
|
||||||
@cell-mouse-enter="handleCellMouseEnter"
|
|
||||||
@cell-mouse-leave="handleCellMouseLeave"
|
|
||||||
>
|
>
|
||||||
<template #default>
|
<template #default>
|
||||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||||
</template>
|
</template>
|
||||||
</ArtTable>
|
</ArtTable>
|
||||||
|
|
||||||
<!-- 鼠标悬浮提示 -->
|
|
||||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
|
||||||
|
|
||||||
<!-- 右键菜单 -->
|
|
||||||
<ArtMenuRight
|
|
||||||
ref="contextMenuRef"
|
|
||||||
:menu-items="contextMenuItems"
|
|
||||||
:menu-width="120"
|
|
||||||
@select="handleContextMenuSelect"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 修改备注对话框 -->
|
<!-- 修改备注对话框 -->
|
||||||
<ElDialog v-model="remarkDialogVisible" title="修改备注" width="500px">
|
<ElDialog v-model="remarkDialogVisible" title="修改备注" width="500px">
|
||||||
<ElForm ref="remarkFormRef" :model="remarkForm" :rules="remarkRules" label-width="80px">
|
<ElForm ref="remarkFormRef" :model="remarkForm" :rules="remarkRules" label-width="80px">
|
||||||
@@ -81,40 +68,22 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h } from 'vue'
|
import { h } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { AuthorizationService, EnterpriseService } from '@/api/modules'
|
import { AuthorizationService, EnterpriseService } from '@/api/modules'
|
||||||
import { ElMessage, ElTag } from 'element-plus'
|
import { ElMessage, ElTag } from 'element-plus'
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
import type { SearchFormItem } from '@/types'
|
import type { SearchFormItem } from '@/types'
|
||||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||||
import { useAuth } from '@/composables/useAuth'
|
import { useAuth } from '@/composables/useAuth'
|
||||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
|
||||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
|
||||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
|
||||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
|
||||||
import type {
|
import type {
|
||||||
AuthorizationItem,
|
AuthorizationItem,
|
||||||
AuthorizationStatus,
|
AuthorizationStatus,
|
||||||
AuthorizerType
|
AuthorizerType
|
||||||
} from '@/types/api/authorization'
|
} from '@/types/api/authorization'
|
||||||
import { CommonStatus } from '@/config/constants'
|
|
||||||
import { RoutesAlias } from '@/router/routesAlias'
|
|
||||||
|
|
||||||
defineOptions({ name: 'AuthorizationRecords' })
|
defineOptions({ name: 'AuthorizationRecords' })
|
||||||
|
|
||||||
const { hasAuth } = useAuth()
|
const { hasAuth } = useAuth()
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
// 使用表格右键菜单功能
|
|
||||||
const {
|
|
||||||
showContextMenuHint,
|
|
||||||
hintPosition,
|
|
||||||
getRowClassName,
|
|
||||||
handleCellMouseEnter,
|
|
||||||
handleCellMouseLeave
|
|
||||||
} = useTableContextMenu()
|
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const remarkDialogVisible = ref(false)
|
const remarkDialogVisible = ref(false)
|
||||||
@@ -122,8 +91,6 @@
|
|||||||
const tableRef = ref()
|
const tableRef = ref()
|
||||||
const remarkFormRef = ref<FormInstance>()
|
const remarkFormRef = ref<FormInstance>()
|
||||||
const currentRecordId = ref<number>(0)
|
const currentRecordId = ref<number>(0)
|
||||||
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
|
||||||
const currentRow = ref<AuthorizationItem | null>(null)
|
|
||||||
|
|
||||||
// 企业搜索选项
|
// 企业搜索选项
|
||||||
const enterpriseOptions = ref<any[]>([])
|
const enterpriseOptions = ref<any[]>([])
|
||||||
@@ -252,7 +219,8 @@
|
|||||||
{ label: '授权人类型', prop: 'authorizer_type' },
|
{ label: '授权人类型', prop: 'authorizer_type' },
|
||||||
{ label: '授权时间', prop: 'authorized_at' },
|
{ label: '授权时间', prop: 'authorized_at' },
|
||||||
{ label: '状态', prop: 'status' },
|
{ label: '状态', prop: 'status' },
|
||||||
{ label: '备注', prop: 'remark' }
|
{ label: '备注', prop: 'remark' },
|
||||||
|
{ label: '操作', prop: 'operation' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const authorizationList = ref<AuthorizationItem[]>([])
|
const authorizationList = ref<AuthorizationItem[]>([])
|
||||||
@@ -284,17 +252,7 @@
|
|||||||
label: 'ICCID',
|
label: 'ICCID',
|
||||||
width: 200,
|
width: 200,
|
||||||
formatter: (row: AuthorizationItem) => {
|
formatter: (row: AuthorizationItem) => {
|
||||||
return h(
|
return row.iccid || '-'
|
||||||
'span',
|
|
||||||
{
|
|
||||||
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
|
|
||||||
onClick: (e: MouseEvent) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
handleNameClick(row)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
row.iccid
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -346,6 +304,21 @@
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
|
// 操作列配置,样式与店铺列表保持一致
|
||||||
|
const getActions = (row: AuthorizationItem) => {
|
||||||
|
const actions: any[] = []
|
||||||
|
|
||||||
|
if (hasAuth('authorization_records:update_remark')) {
|
||||||
|
actions.push({
|
||||||
|
label: '编辑',
|
||||||
|
handler: () => showRemarkDialog(row),
|
||||||
|
type: 'primary'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getTableData()
|
getTableData()
|
||||||
searchEnterprises('')
|
searchEnterprises('')
|
||||||
@@ -417,22 +390,6 @@
|
|||||||
getTableData()
|
getTableData()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查看详情
|
|
||||||
const viewDetail = (row: AuthorizationItem) => {
|
|
||||||
router.push({
|
|
||||||
path: `${RoutesAlias.AuthorizationRecords}/detail/${row.id}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理名称点击
|
|
||||||
const handleNameClick = (row: AuthorizationItem) => {
|
|
||||||
if (hasAuth('authorization_records:view_detail')) {
|
|
||||||
viewDetail(row)
|
|
||||||
} else {
|
|
||||||
ElMessage.warning('您没有查看详情的权限')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示修改备注对话框
|
// 显示修改备注对话框
|
||||||
const showRemarkDialog = (row: AuthorizationItem) => {
|
const showRemarkDialog = (row: AuthorizationItem) => {
|
||||||
currentRecordId.value = row.id
|
currentRecordId.value = row.id
|
||||||
@@ -462,44 +419,10 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 右键菜单项配置
|
|
||||||
const contextMenuItems = computed((): MenuItemType[] => {
|
|
||||||
const items: MenuItemType[] = []
|
|
||||||
|
|
||||||
if (hasAuth('authorization_records:update_remark')) {
|
|
||||||
items.push({ key: 'edit', label: '编辑' })
|
|
||||||
}
|
|
||||||
|
|
||||||
return items
|
|
||||||
})
|
|
||||||
|
|
||||||
// 处理表格行右键菜单
|
|
||||||
const handleRowContextMenu = (row: AuthorizationItem, column: any, event: MouseEvent) => {
|
|
||||||
event.preventDefault()
|
|
||||||
event.stopPropagation()
|
|
||||||
currentRow.value = row
|
|
||||||
contextMenuRef.value?.show(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理右键菜单选择
|
|
||||||
const handleContextMenuSelect = (item: MenuItemType) => {
|
|
||||||
if (!currentRow.value) return
|
|
||||||
|
|
||||||
switch (item.key) {
|
|
||||||
case 'edit':
|
|
||||||
showRemarkDialog(currentRow.value)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.authorization-records-page {
|
.authorization-records-page {
|
||||||
// Authorization records page styles
|
// Authorization records page styles
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-table__row.table-row-with-context-menu) {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,12 +5,12 @@
|
|||||||
v-model="createDrawerVisible"
|
v-model="createDrawerVisible"
|
||||||
title="批量订购套餐"
|
title="批量订购套餐"
|
||||||
direction="rtl"
|
direction="rtl"
|
||||||
size="520px"
|
size="40%"
|
||||||
:close-on-click-modal="!createDrawerBusy"
|
:close-on-click-modal="!createDrawerBusy"
|
||||||
:close-on-press-escape="!createDrawerBusy"
|
:close-on-press-escape="!createDrawerBusy"
|
||||||
@closed="handleCreateDrawerClosed"
|
@closed="handleCreateDrawerClosed"
|
||||||
>
|
>
|
||||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="90px" class="create-form">
|
<ElForm ref="formRef" :model="form" :rules="rules" label-width="100px" class="create-form">
|
||||||
<ElFormItem label="套餐系列" prop="series_id">
|
<ElFormItem label="套餐系列" prop="series_id">
|
||||||
<ElSelect
|
<ElSelect
|
||||||
v-model="form.series_id"
|
v-model="form.series_id"
|
||||||
|
|||||||
@@ -5,12 +5,12 @@
|
|||||||
v-model="createDrawerVisible"
|
v-model="createDrawerVisible"
|
||||||
title="设备批量任务"
|
title="设备批量任务"
|
||||||
direction="rtl"
|
direction="rtl"
|
||||||
size="520px"
|
size="40%"
|
||||||
:close-on-click-modal="!createDrawerBusy"
|
:close-on-click-modal="!createDrawerBusy"
|
||||||
:close-on-press-escape="!createDrawerBusy"
|
:close-on-press-escape="!createDrawerBusy"
|
||||||
@closed="handleCreateDrawerClosed"
|
@closed="handleCreateDrawerClosed"
|
||||||
>
|
>
|
||||||
<ElForm ref="formRef" :model="form" label-width="110px" class="create-form">
|
<ElForm ref="formRef" :model="form" label-width="80px" class="create-form">
|
||||||
<ElFormItem label="操作类型" prop="operation_type">
|
<ElFormItem label="操作类型" prop="operation_type">
|
||||||
<ElSelect
|
<ElSelect
|
||||||
v-model="form.operation_type"
|
v-model="form.operation_type"
|
||||||
|
|||||||
@@ -31,8 +31,6 @@
|
|||||||
:pageSize="pagination.pageSize"
|
:pageSize="pagination.pageSize"
|
||||||
:total="pagination.total"
|
:total="pagination.total"
|
||||||
:marginTop="10"
|
:marginTop="10"
|
||||||
:actions="getActions"
|
|
||||||
:actionsWidth="180"
|
|
||||||
@size-change="handleSizeChange"
|
@size-change="handleSizeChange"
|
||||||
@current-change="handleCurrentChange"
|
@current-change="handleCurrentChange"
|
||||||
>
|
>
|
||||||
@@ -429,74 +427,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ElDialog>
|
</ElDialog>
|
||||||
|
|
||||||
<!-- 店铺实际信用额度调整弹框 -->
|
|
||||||
<ElDialog
|
|
||||||
v-model="creditDialogVisible"
|
|
||||||
:title="`调整实际信用额度 - ${currentCreditShop?.shop_name || ''}`"
|
|
||||||
width="520px"
|
|
||||||
@closed="resetCreditDialog"
|
|
||||||
>
|
|
||||||
<ElDescriptions :column="1" border class="credit-preview">
|
|
||||||
<ElDescriptionsItem label="店铺名称">
|
|
||||||
{{ currentCreditShop?.shop_name || '-' }}
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="当前版本">
|
|
||||||
{{ currentCreditShop?.version ?? '-' }}
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="修改前">
|
|
||||||
{{
|
|
||||||
formatCreditPreview(currentCreditShop?.credit_enabled, currentCreditShop?.credit_limit)
|
|
||||||
}}
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="修改后">
|
|
||||||
{{ formatCreditPreview(creditForm.credit_enabled, creditFormCreditLimitFen) }}
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
</ElDescriptions>
|
|
||||||
|
|
||||||
<ElAlert
|
|
||||||
title="实际可用金额、欠款金额和欠款状态以后端刷新后的资金概况为准。"
|
|
||||||
type="info"
|
|
||||||
:closable="false"
|
|
||||||
show-icon
|
|
||||||
class="credit-dialog-alert"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ElForm ref="creditFormRef" :model="creditForm" :rules="creditRules" label-width="110px">
|
|
||||||
<ElFormItem label="启用信用">
|
|
||||||
<ElSwitch v-model="creditForm.credit_enabled" @change="handleCreditEnabledChange" />
|
|
||||||
</ElFormItem>
|
|
||||||
<ElFormItem label="实际信用额度" prop="credit_limit_yuan">
|
|
||||||
<ElInputNumber
|
|
||||||
v-model="creditForm.credit_limit_yuan"
|
|
||||||
:disabled="!creditForm.credit_enabled"
|
|
||||||
:min="0"
|
|
||||||
:precision="2"
|
|
||||||
:step="100"
|
|
||||||
controls-position="right"
|
|
||||||
style="width: 100%"
|
|
||||||
placeholder="请输入实际信用额度"
|
|
||||||
/>
|
|
||||||
<div class="credit-dialog-tip">单位:元;关闭信用时额度将自动归零</div>
|
|
||||||
</ElFormItem>
|
|
||||||
</ElForm>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<div class="dialog-footer">
|
|
||||||
<ElButton @click="creditDialogVisible = false">取消</ElButton>
|
|
||||||
<ElButton type="primary" :loading="creditSubmitting" @click="handleCreditSubmit">
|
|
||||||
确认调整
|
|
||||||
</ElButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h, watch, onBeforeUnmount, ref, reactive, onMounted, computed, nextTick } from 'vue'
|
import { h, watch, onBeforeUnmount, ref, reactive, onMounted, computed } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { CommissionService, ShopService } from '@/api/modules'
|
import { CommissionService } from '@/api/modules'
|
||||||
import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
|
import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
import { MainWalletTransactionType } from '@/types/api/commission'
|
import { MainWalletTransactionType } from '@/types/api/commission'
|
||||||
@@ -512,7 +449,7 @@
|
|||||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||||
import { useAuth } from '@/composables/useAuth'
|
import { useAuth } from '@/composables/useAuth'
|
||||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||||
import { fenToYuan, formatDateTime, formatMoney, yuanToFen } from '@/utils/business/format'
|
import { formatDateTime, formatMoney } from '@/utils/business/format'
|
||||||
import {
|
import {
|
||||||
CommissionStatusMap,
|
CommissionStatusMap,
|
||||||
WithdrawalStatusMap,
|
WithdrawalStatusMap,
|
||||||
@@ -520,7 +457,6 @@
|
|||||||
CommissionSourceMap
|
CommissionSourceMap
|
||||||
} from '@/config/constants/commission'
|
} from '@/config/constants/commission'
|
||||||
import { RoutesAlias } from '@/router/routesAlias'
|
import { RoutesAlias } from '@/router/routesAlias'
|
||||||
import { normalizeApiError } from '@/utils/business/apiError'
|
|
||||||
import ExportTaskCreateDialog from '@/components/business/ExportTaskCreateDialog.vue'
|
import ExportTaskCreateDialog from '@/components/business/ExportTaskCreateDialog.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'AgentCommission' })
|
defineOptions({ name: 'AgentCommission' })
|
||||||
@@ -584,20 +520,6 @@
|
|||||||
remark: ''
|
remark: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
// 实际信用额度调整弹框状态
|
|
||||||
const creditDialogVisible = ref(false)
|
|
||||||
const creditFormRef = ref<FormInstance>()
|
|
||||||
const creditSubmitting = ref(false)
|
|
||||||
const currentCreditShop = ref<ShopFundSummaryItem | null>(null)
|
|
||||||
const creditForm = reactive({
|
|
||||||
credit_enabled: false,
|
|
||||||
credit_limit_yuan: 0
|
|
||||||
})
|
|
||||||
|
|
||||||
const creditFormCreditLimitFen = computed(() =>
|
|
||||||
creditForm.credit_enabled ? yuanToFen(creditForm.credit_limit_yuan) || 0 : 0
|
|
||||||
)
|
|
||||||
|
|
||||||
// 佣金修正表单验证规则
|
// 佣金修正表单验证规则
|
||||||
const resolveRules = computed<FormRules>(() => ({
|
const resolveRules = computed<FormRules>(() => ({
|
||||||
amount:
|
amount:
|
||||||
@@ -615,36 +537,6 @@
|
|||||||
remark: [{ max: 500, message: '备注最多500字符', trigger: 'blur' }]
|
remark: [{ max: 500, message: '备注最多500字符', trigger: 'blur' }]
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const creditRules = computed<FormRules>(() => ({
|
|
||||||
credit_limit_yuan: [
|
|
||||||
{
|
|
||||||
validator: (
|
|
||||||
_rule: unknown,
|
|
||||||
value: number | undefined,
|
|
||||||
callback: (error?: Error) => void
|
|
||||||
) => {
|
|
||||||
if (!creditForm.credit_enabled) {
|
|
||||||
callback()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value === undefined || value === null || Number.isNaN(value)) {
|
|
||||||
callback(new Error('请输入实际信用额度'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value <= 0) {
|
|
||||||
callback(new Error('实际信用额度必须大于0'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
callback()
|
|
||||||
},
|
|
||||||
trigger: 'blur'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}))
|
|
||||||
|
|
||||||
// 预充值钱包流水状态
|
// 预充值钱包流水状态
|
||||||
const mainWalletLoading = ref(false)
|
const mainWalletLoading = ref(false)
|
||||||
const mainWalletTableRef = ref()
|
const mainWalletTableRef = ref()
|
||||||
@@ -987,120 +879,6 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 获取操作按钮
|
// 获取操作按钮
|
||||||
const getActions = (row: ShopFundSummaryItem) => {
|
|
||||||
const actions: any[] = []
|
|
||||||
|
|
||||||
if (hasAuth('shop:credit_limit_manage')) {
|
|
||||||
actions.push({
|
|
||||||
label: '调整额度',
|
|
||||||
handler: () => showCreditDialog(row),
|
|
||||||
type: 'primary'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return actions
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatCreditPreview = (enabled?: boolean, creditLimit?: number) => {
|
|
||||||
return enabled ? `启用 / ${formatMoney(creditLimit || 0)}` : '关闭 / ¥0.00'
|
|
||||||
}
|
|
||||||
|
|
||||||
const showCreditDialog = (row: ShopFundSummaryItem) => {
|
|
||||||
currentCreditShop.value = row
|
|
||||||
creditForm.credit_enabled = Boolean(row.credit_enabled)
|
|
||||||
creditForm.credit_limit_yuan = row.credit_enabled ? fenToYuan(row.credit_limit) : 0
|
|
||||||
creditDialogVisible.value = true
|
|
||||||
nextTick(() => {
|
|
||||||
creditFormRef.value?.clearValidate()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCreditEnabledChange = (enabled: boolean | string | number) => {
|
|
||||||
if (!enabled) {
|
|
||||||
creditForm.credit_limit_yuan = 0
|
|
||||||
creditFormRef.value?.clearValidate('credit_limit_yuan')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetCreditDialog = () => {
|
|
||||||
creditFormRef.value?.resetFields()
|
|
||||||
currentCreditShop.value = null
|
|
||||||
creditForm.credit_enabled = false
|
|
||||||
creditForm.credit_limit_yuan = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const isCreditConflictMessage = (message?: string) => {
|
|
||||||
if (!message) return false
|
|
||||||
return /版本|冲突|并发|过期|conflict/i.test(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
const isCreditConflictResponse = (response: any) => {
|
|
||||||
return response?.code === 409 || isCreditConflictMessage(response?.msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
const isCreditConflictError = (error: any) => {
|
|
||||||
return (
|
|
||||||
error?.response?.status === 409 ||
|
|
||||||
error?.response?.data?.code === 409 ||
|
|
||||||
isCreditConflictMessage(error?.response?.data?.msg || error?.message)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCreditConflict = async () => {
|
|
||||||
const shopId = currentCreditShop.value?.shop_id
|
|
||||||
await getTableData()
|
|
||||||
|
|
||||||
if (shopId) {
|
|
||||||
const latest = summaryList.value.find((item) => item.shop_id === shopId)
|
|
||||||
if (latest) {
|
|
||||||
// 仅刷新后端版本和当前余额,保留用户刚填写的表单值,方便确认后重试。
|
|
||||||
currentCreditShop.value = latest
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ElMessage.warning('资金概况已被其他操作更新,已刷新最新版本;请确认后重新提交')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCreditSubmit = async () => {
|
|
||||||
if (!creditFormRef.value || !currentCreditShop.value) return
|
|
||||||
|
|
||||||
await creditFormRef.value.validate(async (valid) => {
|
|
||||||
if (!valid || !currentCreditShop.value) return
|
|
||||||
|
|
||||||
creditSubmitting.value = true
|
|
||||||
try {
|
|
||||||
const res = await ShopService.updateShopCreditLimit(currentCreditShop.value.shop_id, {
|
|
||||||
credit_enabled: creditForm.credit_enabled,
|
|
||||||
credit_limit: creditFormCreditLimitFen.value,
|
|
||||||
version: currentCreditShop.value.version
|
|
||||||
})
|
|
||||||
|
|
||||||
if (res.code === 0) {
|
|
||||||
ElMessage.success('实际信用额度调整成功')
|
|
||||||
creditDialogVisible.value = false
|
|
||||||
await getTableData()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCreditConflictResponse(res)) {
|
|
||||||
await handleCreditConflict()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ElMessage.error(res.msg || '实际信用额度调整失败')
|
|
||||||
} catch (error: any) {
|
|
||||||
if (isCreditConflictError(error)) {
|
|
||||||
await handleCreditConflict()
|
|
||||||
} else {
|
|
||||||
console.error('实际信用额度调整失败:', error)
|
|
||||||
ElMessage.error(normalizeApiError(error).message)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
creditSubmitting.value = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听tab切换
|
// 监听tab切换
|
||||||
watch(activeTab, (newTab) => {
|
watch(activeTab, (newTab) => {
|
||||||
if (newTab === 'commission') {
|
if (newTab === 'commission') {
|
||||||
@@ -1379,21 +1157,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.credit-preview {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.credit-dialog-alert {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.credit-dialog-tip {
|
|
||||||
margin-top: 6px;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.4;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (width <= 1200px) {
|
@media (width <= 1200px) {
|
||||||
.main-wallet-filter__grid {
|
.main-wallet-filter__grid {
|
||||||
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
||||||
|
|||||||
@@ -120,7 +120,11 @@
|
|||||||
<ElEmpty v-if="filteredAvailableRoles.length === 0" description="暂无角色" />
|
<ElEmpty v-if="filteredAvailableRoles.length === 0" description="暂无角色" />
|
||||||
<template v-else-if="isPlatformUser">
|
<template v-else-if="isPlatformUser">
|
||||||
<ElCheckboxGroup v-model="rolesToAdd" class="role-list">
|
<ElCheckboxGroup v-model="rolesToAdd" class="role-list">
|
||||||
<div v-for="role in filteredAvailableRoles" :key="getRoleId(role)" class="role-item">
|
<div
|
||||||
|
v-for="role in filteredAvailableRoles"
|
||||||
|
:key="getRoleId(role)"
|
||||||
|
class="role-item"
|
||||||
|
>
|
||||||
<ElCheckbox
|
<ElCheckbox
|
||||||
:label="getRoleId(role)"
|
:label="getRoleId(role)"
|
||||||
:disabled="selectedRoles.includes(getRoleId(role))"
|
:disabled="selectedRoles.includes(getRoleId(role))"
|
||||||
@@ -137,7 +141,11 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<ElRadioGroup v-model="roleToAdd" class="role-list">
|
<ElRadioGroup v-model="roleToAdd" class="role-list">
|
||||||
<div v-for="role in filteredAvailableRoles" :key="getRoleId(role)" class="role-item">
|
<div
|
||||||
|
v-for="role in filteredAvailableRoles"
|
||||||
|
:key="getRoleId(role)"
|
||||||
|
class="role-item"
|
||||||
|
>
|
||||||
<ElRadio
|
<ElRadio
|
||||||
:label="getRoleId(role)"
|
:label="getRoleId(role)"
|
||||||
:disabled="selectedRoles.includes(getRoleId(role))"
|
:disabled="selectedRoles.includes(getRoleId(role))"
|
||||||
@@ -404,6 +412,22 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
baseColumns.push(
|
||||||
|
{
|
||||||
|
prop: 'wecom_bound',
|
||||||
|
label: '企微绑定',
|
||||||
|
formatter: (row: PlatformAccount) =>
|
||||||
|
h(ElTag, { type: row.wecom_bound ? 'success' : 'info' }, () =>
|
||||||
|
row.wecom_bound ? '已绑定' : '未绑定'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'wecom_name',
|
||||||
|
label: '企微用户名称',
|
||||||
|
formatter: (row: PlatformAccount) => h('span', row.wecom_name || '-')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// 添加状态和创建时间
|
// 添加状态和创建时间
|
||||||
baseColumns.push(
|
baseColumns.push(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export const buildAgentRechargeActions = (
|
|||||||
if (
|
if (
|
||||||
!hasApprovalRecord &&
|
!hasApprovalRecord &&
|
||||||
row.status === AgentRechargeStatus.PENDING &&
|
row.status === AgentRechargeStatus.PENDING &&
|
||||||
|
row.payment_method === 'offline' &&
|
||||||
options.hasAuth('agent_recharge:reject')
|
options.hasAuth('agent_recharge:reject')
|
||||||
) {
|
) {
|
||||||
actions.push({
|
actions.push({
|
||||||
|
|||||||
23
src/views/finance/agent-recharge/agentRechargeOnline.ts
Normal file
23
src/views/finance/agent-recharge/agentRechargeOnline.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { normalizeApiError } from '@/utils/business/apiError'
|
||||||
|
import type { AgentRechargeStatus } from '@/types/api'
|
||||||
|
|
||||||
|
export const ONLINE_RECHARGE_TERMINAL_STATUSES: AgentRechargeStatus[] = [3, 4, 5, 6]
|
||||||
|
|
||||||
|
export const isOnlineRechargeTerminal = (status: AgentRechargeStatus): boolean => {
|
||||||
|
return ONLINE_RECHARGE_TERMINAL_STATUSES.includes(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createOnlineRechargeRequestId = (): string => {
|
||||||
|
const randomId = globalThis.crypto?.randomUUID?.()
|
||||||
|
const suffix = randomId || Math.random().toString(36).slice(2, 12)
|
||||||
|
return `recharge-${Date.now()}-${suffix}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const amountYuanToFen = (amount: number): number => {
|
||||||
|
return Math.round(amount * 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const shouldRetryOnlineCreate = (error: unknown): boolean => {
|
||||||
|
const kind = normalizeApiError(error).kind
|
||||||
|
return kind === 'timeout' || kind === 'server' || kind === 'unknown'
|
||||||
|
}
|
||||||
@@ -78,11 +78,19 @@
|
|||||||
const getPaymentMethodText = (method: AgentRechargePaymentMethod): string => {
|
const getPaymentMethodText = (method: AgentRechargePaymentMethod): string => {
|
||||||
const methodMap: Record<AgentRechargePaymentMethod, string> = {
|
const methodMap: Record<AgentRechargePaymentMethod, string> = {
|
||||||
wechat: '微信在线支付',
|
wechat: '微信在线支付',
|
||||||
|
alipay: '支付宝在线支付',
|
||||||
offline: '线下转账'
|
offline: '线下转账'
|
||||||
}
|
}
|
||||||
return methodMap[method] || method
|
return methodMap[method] || method
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getRechargeSourceText = (data: AgentRecharge): string => {
|
||||||
|
if (data.recharge_source_name) return data.recharge_source_name
|
||||||
|
if (data.recharge_source === 'agent_online') return '代理在线自充'
|
||||||
|
if (data.recharge_source === 'platform_offline') return '平台线下代充'
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
|
||||||
const getApprovalProviderText = (data: AgentRecharge) => {
|
const getApprovalProviderText = (data: AgentRecharge) => {
|
||||||
if (data.approval_provider === 'wecom' || data.approval_source === 'wecom') return '企微'
|
if (data.approval_provider === 'wecom' || data.approval_source === 'wecom') return '企微'
|
||||||
if (data.approval_source === 'legacy') return '历史审批'
|
if (data.approval_source === 'legacy') return '历史审批'
|
||||||
@@ -90,12 +98,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 详情配置
|
// 详情配置
|
||||||
const detailSections = computed((): DetailSection[] => [
|
const detailSections = computed((): DetailSection[] => {
|
||||||
|
if (!detailData.value) return []
|
||||||
|
|
||||||
|
const isOfflineRecharge =
|
||||||
|
detailData.value.recharge_source === 'platform_offline' ||
|
||||||
|
detailData.value.payment_method === 'offline'
|
||||||
|
|
||||||
|
const sections: DetailSection[] = [
|
||||||
{
|
{
|
||||||
title: '订单信息',
|
title: '订单信息',
|
||||||
fields: [
|
fields: [
|
||||||
{ label: '充值单号', prop: 'recharge_no' },
|
{ label: '充值单号', prop: 'recharge_no' },
|
||||||
{ label: '店铺名称', prop: 'shop_name' },
|
{ label: '店铺名称', prop: 'shop_name' },
|
||||||
|
{ label: '充值来源', formatter: (_, data) => getRechargeSourceText(data) },
|
||||||
{ label: '提交人', prop: 'submitter_name', formatter: (value) => value || '-' },
|
{ label: '提交人', prop: 'submitter_name', formatter: (value) => value || '-' },
|
||||||
{
|
{
|
||||||
label: '充值金额',
|
label: '充值金额',
|
||||||
@@ -114,21 +130,29 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
...(isOfflineRecharge
|
||||||
|
? [
|
||||||
{
|
{
|
||||||
title: '企微审批信息',
|
title: '企微审批信息',
|
||||||
fields: [
|
fields: [
|
||||||
{ label: '审批渠道', formatter: (_, data) => getApprovalProviderText(data) },
|
{
|
||||||
|
label: '审批渠道',
|
||||||
|
formatter: (_: unknown, data: AgentRecharge) => getApprovalProviderText(data)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '审批状态',
|
label: '审批状态',
|
||||||
formatter: (_, data) => data.approval_status_name || '-'
|
formatter: (_: unknown, data: AgentRecharge) => data.approval_status_name || '-'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '当前审批人摘要',
|
label: '当前审批人摘要',
|
||||||
formatter: (_, data) => data.current_approver_summary || '-',
|
formatter: (_: unknown, data: AgentRecharge) =>
|
||||||
|
data.current_approver_summary || '-',
|
||||||
fullWidth: true
|
fullWidth: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
title: '业务处理结果',
|
title: '业务处理结果',
|
||||||
fields: [
|
fields: [
|
||||||
@@ -156,10 +180,18 @@
|
|||||||
formatter: (value) => value || '-',
|
formatter: (value) => value || '-',
|
||||||
fullWidth: true
|
fullWidth: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '支付单号',
|
||||||
|
prop: 'payment_no',
|
||||||
|
formatter: (value) => value || '-',
|
||||||
|
fullWidth: true
|
||||||
|
},
|
||||||
|
...(isOfflineRecharge
|
||||||
|
? [
|
||||||
{
|
{
|
||||||
label: '支付凭证',
|
label: '支付凭证',
|
||||||
fullWidth: true,
|
fullWidth: true,
|
||||||
render: (data) =>
|
render: (data: AgentRecharge) =>
|
||||||
hasVoucherKeys(data.payment_voucher_key)
|
hasVoucherKeys(data.payment_voucher_key)
|
||||||
? h(
|
? h(
|
||||||
ElButton,
|
ElButton,
|
||||||
@@ -167,13 +199,17 @@
|
|||||||
type: 'primary',
|
type: 'primary',
|
||||||
link: true,
|
link: true,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
paymentVoucherFileKeys.value = toVoucherKeyList(data.payment_voucher_key)
|
paymentVoucherFileKeys.value = toVoucherKeyList(
|
||||||
|
data.payment_voucher_key
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
() => '查看支付凭证'
|
() => '查看支付凭证'
|
||||||
)
|
)
|
||||||
: h('span', '-')
|
: h('span', '-')
|
||||||
},
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
label: '运营备注',
|
label: '运营备注',
|
||||||
prop: 'remark',
|
prop: 'remark',
|
||||||
@@ -207,7 +243,10 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
])
|
]
|
||||||
|
|
||||||
|
return sections
|
||||||
|
})
|
||||||
|
|
||||||
// 加载详情数据
|
// 加载详情数据
|
||||||
const loadDetailData = async () => {
|
const loadDetailData = async () => {
|
||||||
|
|||||||
@@ -21,8 +21,8 @@
|
|||||||
<ElButton
|
<ElButton
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="showCreateDialog"
|
@click="showCreateDialog"
|
||||||
v-if="hasAuth('agent_recharge:create')"
|
v-if="hasAuth('agent_recharge:create') && canCreateRecharge"
|
||||||
>创建充值订单</ElButton
|
>{{ createButtonLabel }}</ElButton
|
||||||
>
|
>
|
||||||
<ElButton v-if="hasAuth('agent_recharge:export')" @click="exportDialogVisible = true">
|
<ElButton v-if="hasAuth('agent_recharge:export')" @click="exportDialogVisible = true">
|
||||||
导出
|
导出
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
<!-- 创建充值订单对话框 -->
|
<!-- 创建充值订单对话框 -->
|
||||||
<ElDialog
|
<ElDialog
|
||||||
v-model="createDialogVisible"
|
v-model="createDialogVisible"
|
||||||
title="创建充值订单"
|
:title="createDialogTitle"
|
||||||
width="500px"
|
width="500px"
|
||||||
@closed="handleCreateDialogClosed"
|
@closed="handleCreateDialogClosed"
|
||||||
>
|
>
|
||||||
@@ -62,14 +62,17 @@
|
|||||||
<ElFormItem label="充值金额" prop="amount">
|
<ElFormItem label="充值金额" prop="amount">
|
||||||
<ElInputNumber
|
<ElInputNumber
|
||||||
v-model="createForm.amount"
|
v-model="createForm.amount"
|
||||||
:min="0.01"
|
:min="minimumAmountYuan"
|
||||||
|
:max="maximumAmountYuan"
|
||||||
:precision="2"
|
:precision="2"
|
||||||
:step="0.01"
|
:step="0.01"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
placeholder="请输入充值金额(元)"
|
placeholder="请输入充值金额(元)"
|
||||||
/>
|
/>
|
||||||
<div style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary)">
|
<div style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary)">
|
||||||
最小: ¥0.01,最大: ¥1,000,000.00
|
金额范围:¥{{ minimumAmountYuan.toFixed(2) }} - ¥{{
|
||||||
|
maximumAmountYuan.toLocaleString('zh-CN', { minimumFractionDigits: 2 })
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="支付方式" prop="payment_method">
|
<ElFormItem label="支付方式" prop="payment_method">
|
||||||
@@ -77,11 +80,27 @@
|
|||||||
v-model="createForm.payment_method"
|
v-model="createForm.payment_method"
|
||||||
placeholder="请选择支付方式"
|
placeholder="请选择支付方式"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
:loading="paymentMethodsLoading"
|
||||||
>
|
>
|
||||||
<ElOption label="线下转账" value="offline" />
|
<ElOption
|
||||||
|
v-for="option in availablePaymentMethodOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:label="option.label"
|
||||||
|
:value="option.value"
|
||||||
|
/>
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
createMode === 'online' &&
|
||||||
|
!paymentMethodsLoading &&
|
||||||
|
onlinePaymentMethods.length === 0
|
||||||
|
"
|
||||||
|
class="online-recharge-empty-hint"
|
||||||
|
>
|
||||||
|
当前暂无可用在线支付方式,暂时无法提交充值。
|
||||||
|
</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="店铺" prop="shop_id">
|
<ElFormItem v-if="createMode === 'offline'" label="店铺" prop="shop_id">
|
||||||
<ElCascader
|
<ElCascader
|
||||||
v-model="createForm.shop_id"
|
v-model="createForm.shop_id"
|
||||||
:options="shopCascadeOptions"
|
:options="shopCascadeOptions"
|
||||||
@@ -106,7 +125,7 @@
|
|||||||
@change="createFormRef?.validateField('payment_voucher_key')"
|
@change="createFormRef?.validateField('payment_voucher_key')"
|
||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="运营备注" prop="remark">
|
<ElFormItem v-if="createMode === 'offline'" label="运营备注" prop="remark">
|
||||||
<ElInput
|
<ElInput
|
||||||
v-model="createForm.remark"
|
v-model="createForm.remark"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
@@ -123,15 +142,54 @@
|
|||||||
<ElButton
|
<ElButton
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="handleCreateRecharge"
|
@click="handleCreateRecharge"
|
||||||
:loading="createLoading || voucherUploading"
|
:loading="createLoading || voucherUploading || paymentMethodsLoading"
|
||||||
:disabled="voucherUploading"
|
:disabled="createSubmitDisabled"
|
||||||
>
|
>
|
||||||
{{ voucherUploading ? '凭证上传中...' : '确认创建' }}
|
{{ voucherUploading ? '凭证上传中...' : createButtonLabel }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ElDialog>
|
</ElDialog>
|
||||||
|
|
||||||
|
<!-- 在线充值二维码对话框 -->
|
||||||
|
<ElDialog
|
||||||
|
v-model="onlineQrDialogVisible"
|
||||||
|
title="扫码完成充值"
|
||||||
|
width="430px"
|
||||||
|
align-center
|
||||||
|
@closed="handleQrDialogClosed"
|
||||||
|
>
|
||||||
|
<div class="online-recharge-qr-dialog">
|
||||||
|
<QrcodeVue
|
||||||
|
v-if="qrContent"
|
||||||
|
:value="qrContent"
|
||||||
|
:size="240"
|
||||||
|
level="H"
|
||||||
|
render-as="canvas"
|
||||||
|
/>
|
||||||
|
<ElEmpty v-else description="暂未获取到支付二维码" />
|
||||||
|
<div v-if="onlineQrRecharge" class="online-recharge-qr-dialog__summary">
|
||||||
|
<div>充值单号:{{ onlineQrRecharge.recharge_no || '-' }}</div>
|
||||||
|
<div>充值金额:{{ formatCurrency(onlineQrRecharge.amount) }}</div>
|
||||||
|
<div v-if="onlineWalletBalance !== null">
|
||||||
|
当前主钱包余额:{{ formatCurrency(onlineWalletBalance) }}
|
||||||
|
</div>
|
||||||
|
<ElTag :type="getStatusType(onlinePaymentStatus?.status || onlineQrRecharge.status)">
|
||||||
|
{{ onlinePaymentStatus?.status_name || onlineQrRecharge.status_name || '-' }}
|
||||||
|
</ElTag>
|
||||||
|
<div class="online-recharge-qr-dialog__hint">
|
||||||
|
{{ onlinePaymentStatusMessage }}
|
||||||
|
</div>
|
||||||
|
<div v-if="paymentStatusLoading" class="online-recharge-qr-dialog__loading">
|
||||||
|
正在查询支付状态...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<ElButton @click="onlineQrDialogVisible = false">关闭</ElButton>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
|
|
||||||
<!-- 导出任务对话框 -->
|
<!-- 导出任务对话框 -->
|
||||||
<ExportTaskCreateDialog
|
<ExportTaskCreateDialog
|
||||||
v-model="exportDialogVisible"
|
v-model="exportDialogVisible"
|
||||||
@@ -224,7 +282,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h } from 'vue'
|
import { h } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { AgentRechargeService, ShopService } from '@/api/modules'
|
import QrcodeVue from 'qrcode.vue'
|
||||||
|
import { AgentRechargeService, CommissionService, ShopService } from '@/api/modules'
|
||||||
import {
|
import {
|
||||||
ElMessage,
|
ElMessage,
|
||||||
ElTag,
|
ElTag,
|
||||||
@@ -240,7 +299,9 @@
|
|||||||
AgentRecharge,
|
AgentRecharge,
|
||||||
AgentRechargeQueryParams,
|
AgentRechargeQueryParams,
|
||||||
AgentRechargeStatus,
|
AgentRechargeStatus,
|
||||||
|
AgentRechargeOnlinePaymentMethod,
|
||||||
AgentRechargePaymentMethod,
|
AgentRechargePaymentMethod,
|
||||||
|
AgentRechargePaymentStatusResponse,
|
||||||
CreateAgentRechargeRequest,
|
CreateAgentRechargeRequest,
|
||||||
ConfirmOfflinePaymentRequest,
|
ConfirmOfflinePaymentRequest,
|
||||||
RejectAgentRechargeRequest
|
RejectAgentRechargeRequest
|
||||||
@@ -248,6 +309,7 @@
|
|||||||
import type { SearchFormItem } from '@/types'
|
import type { SearchFormItem } from '@/types'
|
||||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
|
import { normalizeApiError } from '@/utils/business/apiError'
|
||||||
import {
|
import {
|
||||||
getApprovalStatusText,
|
getApprovalStatusText,
|
||||||
getCurrentApproverSummaryText,
|
getCurrentApproverSummaryText,
|
||||||
@@ -255,16 +317,36 @@
|
|||||||
} from '@/utils/business/approvalSummary'
|
} from '@/utils/business/approvalSummary'
|
||||||
import { hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
|
import { hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
|
||||||
import { useAuth } from '@/composables/useAuth'
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
import { useUserStore } from '@/store/modules/user'
|
||||||
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
|
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
|
||||||
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
||||||
import ExportTaskCreateDialog from '@/components/business/ExportTaskCreateDialog.vue'
|
import ExportTaskCreateDialog from '@/components/business/ExportTaskCreateDialog.vue'
|
||||||
import { buildAgentRechargeActions } from './agentRechargeActions'
|
import { buildAgentRechargeActions } from './agentRechargeActions'
|
||||||
import { formatRejectionReason } from './agentRechargeDisplay'
|
import { formatRejectionReason } from './agentRechargeDisplay'
|
||||||
|
import {
|
||||||
|
amountYuanToFen,
|
||||||
|
createOnlineRechargeRequestId,
|
||||||
|
isOnlineRechargeTerminal,
|
||||||
|
shouldRetryOnlineCreate
|
||||||
|
} from './agentRechargeOnline'
|
||||||
|
|
||||||
defineOptions({ name: 'AgentRechargeList' })
|
defineOptions({ name: 'AgentRechargeList' })
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { hasAuth } = useAuth()
|
const { hasAuth } = useAuth()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const isAgentAccount = computed(() => Number(userStore.info.user_type) === 3)
|
||||||
|
const isPlatformAccount = computed(() => [1, 2].includes(Number(userStore.info.user_type)))
|
||||||
|
const canViewRecharge = computed(() => [1, 2, 3].includes(Number(userStore.info.user_type)))
|
||||||
|
const canCreateRecharge = computed(() => isAgentAccount.value || isPlatformAccount.value)
|
||||||
|
const createMode = ref<'online' | 'offline'>('offline')
|
||||||
|
const createDialogTitle = computed(() =>
|
||||||
|
createMode.value === 'online' ? '代理钱包在线扫码充值' : '创建平台线下代充'
|
||||||
|
)
|
||||||
|
const createButtonLabel = computed(() =>
|
||||||
|
createMode.value === 'online' ? '立即充值' : '创建充值订单'
|
||||||
|
)
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const createLoading = ref(false)
|
const createLoading = ref(false)
|
||||||
@@ -276,13 +358,29 @@
|
|||||||
const exportDialogVisible = ref(false)
|
const exportDialogVisible = ref(false)
|
||||||
const confirmPayDialogVisible = ref(false)
|
const confirmPayDialogVisible = ref(false)
|
||||||
const rejectDialogVisible = ref(false)
|
const rejectDialogVisible = ref(false)
|
||||||
|
const onlineQrDialogVisible = ref(false)
|
||||||
const currentRecharge = ref<AgentRecharge | null>(null)
|
const currentRecharge = ref<AgentRecharge | null>(null)
|
||||||
|
const onlineQrRecharge = ref<AgentRecharge | null>(null)
|
||||||
|
const onlinePaymentStatus = ref<AgentRechargePaymentStatusResponse | null>(null)
|
||||||
|
const onlineWalletBalance = ref<number | null>(null)
|
||||||
|
const onlinePaymentMethods = ref<AgentRechargeOnlinePaymentMethod[]>([])
|
||||||
|
const paymentMethodsLoading = ref(false)
|
||||||
|
const paymentStatusLoading = ref(false)
|
||||||
|
const qrContent = ref('')
|
||||||
|
const onlineRequestId = ref<string | null>(null)
|
||||||
|
const onlineCreateRetried = ref(false)
|
||||||
|
const paymentStatusTimer = ref<ReturnType<typeof setInterval> | null>(null)
|
||||||
|
const paymentMethodsBounds = reactive({
|
||||||
|
min_amount: 10000,
|
||||||
|
max_amount: 100000000
|
||||||
|
})
|
||||||
const paymentVoucherFileKeys = ref<string[]>([])
|
const paymentVoucherFileKeys = ref<string[]>([])
|
||||||
|
|
||||||
// 搜索表单初始值
|
// 搜索表单初始值
|
||||||
const initialSearchState: AgentRechargeQueryParams = {
|
const initialSearchState: AgentRechargeQueryParams = {
|
||||||
shop_id: undefined,
|
shop_id: undefined,
|
||||||
status: undefined,
|
status: undefined,
|
||||||
|
recharge_source: undefined,
|
||||||
dateRange: [],
|
dateRange: [],
|
||||||
start_date: '',
|
start_date: '',
|
||||||
end_date: ''
|
end_date: ''
|
||||||
@@ -325,8 +423,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 搜索表单配置
|
// 搜索表单配置
|
||||||
const searchFormItems: SearchFormItem[] = [
|
const searchFormItems = computed<SearchFormItem[]>(() => {
|
||||||
{
|
const items: SearchFormItem[] = []
|
||||||
|
|
||||||
|
if (isPlatformAccount.value) {
|
||||||
|
items.push({
|
||||||
label: '店铺名称',
|
label: '店铺名称',
|
||||||
prop: 'shop_id',
|
prop: 'shop_id',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
@@ -342,9 +443,12 @@
|
|||||||
remote: true,
|
remote: true,
|
||||||
remoteMethod: (query: string) => searchShops(query)
|
remoteMethod: (query: string) => searchShops(query)
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push(
|
||||||
{
|
{
|
||||||
label: '支付状态',
|
label: '充值状态',
|
||||||
prop: 'status',
|
prop: 'status',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
placeholder: '请选择状态',
|
placeholder: '请选择状态',
|
||||||
@@ -356,9 +460,18 @@
|
|||||||
{ label: '已退款', value: 5 },
|
{ label: '已退款', value: 5 },
|
||||||
{ label: '已驳回', value: 6 }
|
{ label: '已驳回', value: 6 }
|
||||||
],
|
],
|
||||||
config: {
|
config: { clearable: true }
|
||||||
clearable: true
|
},
|
||||||
}
|
{
|
||||||
|
label: '充值来源',
|
||||||
|
prop: 'recharge_source',
|
||||||
|
type: 'select',
|
||||||
|
placeholder: '请选择充值来源',
|
||||||
|
options: [
|
||||||
|
{ label: '代理在线自充', value: 'agent_online' },
|
||||||
|
{ label: '平台线下代充', value: 'platform_offline' }
|
||||||
|
],
|
||||||
|
config: { clearable: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '起止时间',
|
label: '起止时间',
|
||||||
@@ -372,7 +485,10 @@
|
|||||||
valueFormat: 'YYYY-MM-DD'
|
valueFormat: 'YYYY-MM-DD'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
)
|
||||||
|
|
||||||
|
return items
|
||||||
|
})
|
||||||
|
|
||||||
// 分页
|
// 分页
|
||||||
const pagination = reactive({
|
const pagination = reactive({
|
||||||
@@ -385,6 +501,7 @@
|
|||||||
const columnOptions = [
|
const columnOptions = [
|
||||||
{ label: '充值单号', prop: 'recharge_no' },
|
{ label: '充值单号', prop: 'recharge_no' },
|
||||||
{ label: '店铺名称', prop: 'shop_name' },
|
{ label: '店铺名称', prop: 'shop_name' },
|
||||||
|
{ label: '充值来源', prop: 'recharge_source' },
|
||||||
{ label: '充值金额', prop: 'amount' },
|
{ label: '充值金额', prop: 'amount' },
|
||||||
{ label: '状态', prop: 'status' },
|
{ label: '状态', prop: 'status' },
|
||||||
{ label: '审批渠道', prop: 'approval_provider' },
|
{ label: '审批渠道', prop: 'approval_provider' },
|
||||||
@@ -406,8 +523,43 @@
|
|||||||
const confirmPayFormRef = ref<FormInstance>()
|
const confirmPayFormRef = ref<FormInstance>()
|
||||||
const rejectFormRef = ref<FormInstance>()
|
const rejectFormRef = ref<FormInstance>()
|
||||||
const uploadRef = ref<InstanceType<typeof VoucherUpload>>()
|
const uploadRef = ref<InstanceType<typeof VoucherUpload>>()
|
||||||
const MIN_RECHARGE_AMOUNT = 0.01
|
const OFFLINE_MIN_RECHARGE_AMOUNT = 0.01
|
||||||
const MAX_RECHARGE_AMOUNT = 1_000_000
|
const OFFLINE_MAX_RECHARGE_AMOUNT = 1_000_000
|
||||||
|
|
||||||
|
const minimumAmountYuan = computed(() =>
|
||||||
|
createMode.value === 'online'
|
||||||
|
? paymentMethodsBounds.min_amount / 100
|
||||||
|
: OFFLINE_MIN_RECHARGE_AMOUNT
|
||||||
|
)
|
||||||
|
const maximumAmountYuan = computed(() =>
|
||||||
|
createMode.value === 'online'
|
||||||
|
? paymentMethodsBounds.max_amount / 100
|
||||||
|
: OFFLINE_MAX_RECHARGE_AMOUNT
|
||||||
|
)
|
||||||
|
const availablePaymentMethodOptions = computed(() => {
|
||||||
|
if (createMode.value === 'offline') return [{ label: '线下转账', value: 'offline' }]
|
||||||
|
|
||||||
|
return onlinePaymentMethods.value.map((method) => ({
|
||||||
|
label: method === 'wechat' ? '微信支付' : '支付宝',
|
||||||
|
value: method
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
const createSubmitDisabled = computed(
|
||||||
|
() =>
|
||||||
|
voucherUploading.value ||
|
||||||
|
paymentMethodsLoading.value ||
|
||||||
|
(createMode.value === 'online' && onlinePaymentMethods.value.length === 0)
|
||||||
|
)
|
||||||
|
const onlinePaymentStatusMessage = computed(() => {
|
||||||
|
const status = onlinePaymentStatus.value?.status ?? onlineQrRecharge.value?.status
|
||||||
|
if (status === 1) return '请使用对应支付方式扫码完成支付'
|
||||||
|
if (status === 2) return '支付已成功,钱包正在入账,请稍候'
|
||||||
|
if (status === 3) return '充值成功,钱包已到账'
|
||||||
|
if (status === 4) return '充值订单已关闭,请重新发起充值'
|
||||||
|
if (status === 5) return '充值订单已退款'
|
||||||
|
if (status === 6) return '充值订单已驳回'
|
||||||
|
return '正在获取支付状态'
|
||||||
|
})
|
||||||
|
|
||||||
const createRules = computed<FormRules>(() => {
|
const createRules = computed<FormRules>(() => {
|
||||||
const rules: FormRules = {
|
const rules: FormRules = {
|
||||||
@@ -419,12 +571,16 @@
|
|||||||
callback()
|
callback()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (Number(value) < MIN_RECHARGE_AMOUNT) {
|
if (Number(value) < minimumAmountYuan.value) {
|
||||||
callback(new Error(`充值金额最小为 ¥${MIN_RECHARGE_AMOUNT.toFixed(2)}`))
|
callback(new Error(`充值金额最小为 ¥${minimumAmountYuan.value.toFixed(2)}`))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (Number(value) > MAX_RECHARGE_AMOUNT) {
|
if (Number(value) > maximumAmountYuan.value) {
|
||||||
callback(new Error(`充值金额最大为 ¥${MAX_RECHARGE_AMOUNT.toFixed(2)}`))
|
callback(
|
||||||
|
new Error(
|
||||||
|
`充值金额最大为 ¥${maximumAmountYuan.value.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}`
|
||||||
|
)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
callback()
|
callback()
|
||||||
@@ -435,9 +591,10 @@
|
|||||||
payment_method: [{ required: true, message: '请选择支付方式', trigger: 'change' }],
|
payment_method: [{ required: true, message: '请选择支付方式', trigger: 'change' }],
|
||||||
shop_id: [{ required: true, message: '请选择目标店铺', trigger: 'change' }]
|
shop_id: [{ required: true, message: '请选择目标店铺', trigger: 'change' }]
|
||||||
}
|
}
|
||||||
if (createForm.payment_method === 'offline') {
|
if (createMode.value === 'offline') {
|
||||||
rules.payment_voucher_key = [{ required: true, message: '请上传支付凭证', trigger: 'change' }]
|
rules.payment_voucher_key = [{ required: true, message: '请上传支付凭证', trigger: 'change' }]
|
||||||
}
|
}
|
||||||
|
if (createMode.value === 'online') delete rules.shop_id
|
||||||
return rules
|
return rules
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -452,13 +609,13 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
const createForm = reactive<{
|
const createForm = reactive<{
|
||||||
amount: number
|
amount: number | null
|
||||||
payment_method: string
|
payment_method: AgentRechargePaymentMethod | ''
|
||||||
shop_id: number | null
|
shop_id: number | null
|
||||||
payment_voucher_key: string[]
|
payment_voucher_key: string[]
|
||||||
remark: string
|
remark: string
|
||||||
}>({
|
}>({
|
||||||
amount: MIN_RECHARGE_AMOUNT,
|
amount: OFFLINE_MIN_RECHARGE_AMOUNT,
|
||||||
payment_method: '',
|
payment_method: '',
|
||||||
shop_id: null,
|
shop_id: null,
|
||||||
payment_voucher_key: [],
|
payment_voucher_key: [],
|
||||||
@@ -499,11 +656,19 @@
|
|||||||
const getPaymentMethodText = (method: AgentRechargePaymentMethod): string => {
|
const getPaymentMethodText = (method: AgentRechargePaymentMethod): string => {
|
||||||
const methodMap: Record<AgentRechargePaymentMethod, string> = {
|
const methodMap: Record<AgentRechargePaymentMethod, string> = {
|
||||||
wechat: '微信在线支付',
|
wechat: '微信在线支付',
|
||||||
|
alipay: '支付宝在线支付',
|
||||||
offline: '线下转账'
|
offline: '线下转账'
|
||||||
}
|
}
|
||||||
return methodMap[method] || method
|
return methodMap[method] || method
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getRechargeSourceText = (row: AgentRecharge): string => {
|
||||||
|
if (row.recharge_source_name) return row.recharge_source_name
|
||||||
|
if (row.recharge_source === 'agent_online') return '代理在线自充'
|
||||||
|
if (row.recharge_source === 'platform_offline') return '平台线下代充'
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
|
||||||
// 动态列配置
|
// 动态列配置
|
||||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||||
{
|
{
|
||||||
@@ -530,6 +695,12 @@
|
|||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
showOverflowTooltip: true
|
showOverflowTooltip: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
prop: 'recharge_source',
|
||||||
|
label: '充值来源',
|
||||||
|
width: 140,
|
||||||
|
formatter: (row: AgentRecharge) => getRechargeSourceText(row)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
prop: 'amount',
|
prop: 'amount',
|
||||||
label: '充值金额',
|
label: '充值金额',
|
||||||
@@ -635,7 +806,17 @@
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getTableData()
|
getTableData()
|
||||||
loadShops()
|
if (isPlatformAccount.value) loadShops()
|
||||||
|
document.addEventListener('visibilitychange', handleDocumentVisibilityChange)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
stopPaymentStatusPolling()
|
||||||
|
document.removeEventListener('visibilitychange', handleDocumentVisibilityChange)
|
||||||
|
})
|
||||||
|
|
||||||
|
onDeactivated(() => {
|
||||||
|
stopPaymentStatusPolling()
|
||||||
})
|
})
|
||||||
|
|
||||||
let isFirstActivation = true
|
let isFirstActivation = true
|
||||||
@@ -643,6 +824,9 @@
|
|||||||
if (!isFirstActivation) {
|
if (!isFirstActivation) {
|
||||||
getTableData()
|
getTableData()
|
||||||
}
|
}
|
||||||
|
if (onlineQrDialogVisible.value && document.visibilityState === 'visible') {
|
||||||
|
startPaymentStatusPolling()
|
||||||
|
}
|
||||||
isFirstActivation = false
|
isFirstActivation = false
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -693,13 +877,20 @@
|
|||||||
|
|
||||||
// 获取充值订单列表
|
// 获取充值订单列表
|
||||||
const getTableData = async () => {
|
const getTableData = async () => {
|
||||||
|
if (!canViewRecharge.value) {
|
||||||
|
rechargeList.value = []
|
||||||
|
pagination.total = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const params: AgentRechargeQueryParams = {
|
const params: AgentRechargeQueryParams = {
|
||||||
page: pagination.page,
|
page: pagination.page,
|
||||||
page_size: pagination.page_size,
|
page_size: pagination.page_size,
|
||||||
shop_id: searchForm.shop_id,
|
shop_id: isPlatformAccount.value ? searchForm.shop_id : undefined,
|
||||||
status: searchForm.status,
|
status: searchForm.status,
|
||||||
|
recharge_source: searchForm.recharge_source,
|
||||||
start_date: searchForm.start_date || undefined,
|
start_date: searchForm.start_date || undefined,
|
||||||
end_date: searchForm.end_date || undefined
|
end_date: searchForm.end_date || undefined
|
||||||
}
|
}
|
||||||
@@ -737,8 +928,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const exportQuery = computed(() => ({
|
const exportQuery = computed(() => ({
|
||||||
shop_id: searchForm.shop_id,
|
shop_id: isPlatformAccount.value ? searchForm.shop_id : undefined,
|
||||||
status: searchForm.status,
|
status: searchForm.status,
|
||||||
|
recharge_source: searchForm.recharge_source,
|
||||||
start_date: searchForm.start_date || searchForm.dateRange?.[0],
|
start_date: searchForm.start_date || searchForm.dateRange?.[0],
|
||||||
end_date: searchForm.end_date || searchForm.dateRange?.[1]
|
end_date: searchForm.end_date || searchForm.dateRange?.[1]
|
||||||
}))
|
}))
|
||||||
@@ -761,22 +953,56 @@
|
|||||||
|
|
||||||
// 显示创建订单对话框
|
// 显示创建订单对话框
|
||||||
const showCreateDialog = async () => {
|
const showCreateDialog = async () => {
|
||||||
|
createMode.value = isAgentAccount.value ? 'online' : 'offline'
|
||||||
|
resetCreateForm()
|
||||||
|
|
||||||
|
if (createMode.value === 'online') {
|
||||||
|
await loadPaymentMethods()
|
||||||
|
} else {
|
||||||
// 重新加载店铺列表,确保获取最新数据
|
// 重新加载店铺列表,确保获取最新数据
|
||||||
await loadShops()
|
await loadShops()
|
||||||
createForm.payment_method = ''
|
}
|
||||||
|
|
||||||
createDialogVisible.value = true
|
createDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resetCreateForm = () => {
|
||||||
|
createForm.amount = minimumAmountYuan.value
|
||||||
|
createForm.payment_method = ''
|
||||||
|
createForm.shop_id = null
|
||||||
|
createForm.payment_voucher_key = []
|
||||||
|
createForm.remark = ''
|
||||||
|
onlineRequestId.value = null
|
||||||
|
onlineCreateRetried.value = false
|
||||||
|
voucherUploading.value = false
|
||||||
|
uploadRef.value?.clearFiles(false)
|
||||||
|
}
|
||||||
|
|
||||||
// 对话框关闭后的清理
|
// 对话框关闭后的清理
|
||||||
const handleCreateDialogClosed = () => {
|
const handleCreateDialogClosed = () => {
|
||||||
createFormRef.value?.resetFields()
|
createFormRef.value?.resetFields()
|
||||||
createForm.amount = MIN_RECHARGE_AMOUNT
|
resetCreateForm()
|
||||||
createForm.payment_method = ''
|
}
|
||||||
createForm.shop_id = null
|
|
||||||
createForm.payment_voucher_key = []
|
// 加载代理在线充值可用支付方式
|
||||||
createForm.remark = ''
|
const loadPaymentMethods = async () => {
|
||||||
voucherUploading.value = false
|
paymentMethodsLoading.value = true
|
||||||
uploadRef.value?.clearFiles(false)
|
onlinePaymentMethods.value = []
|
||||||
|
try {
|
||||||
|
const res = await AgentRechargeService.getPaymentMethods()
|
||||||
|
if (res.code === 0) {
|
||||||
|
onlinePaymentMethods.value = Array.isArray(res.data?.methods) ? res.data.methods : []
|
||||||
|
paymentMethodsBounds.min_amount = Number(res.data?.min_amount) || 10000
|
||||||
|
paymentMethodsBounds.max_amount = Number(res.data?.max_amount) || 100000000
|
||||||
|
createForm.amount = minimumAmountYuan.value
|
||||||
|
} else {
|
||||||
|
ElMessage.warning(res.msg || '当前暂无可用在线支付方式')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载在线支付方式失败:', error)
|
||||||
|
} finally {
|
||||||
|
paymentMethodsLoading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建充值订单
|
// 创建充值订单
|
||||||
@@ -788,28 +1014,43 @@
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await formRef.validate(async (valid) => {
|
const valid = await formRef.validate().catch(() => false)
|
||||||
if (valid) {
|
if (!valid || createForm.amount == null || !createForm.payment_method) return
|
||||||
|
|
||||||
createLoading.value = true
|
createLoading.value = true
|
||||||
try {
|
try {
|
||||||
|
if (createMode.value === 'online') {
|
||||||
|
await handleOnlineRechargeCreate(createForm.amount, createForm.payment_method)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!createForm.shop_id) {
|
||||||
|
ElMessage.warning('请选择目标店铺')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const voucherKeys = toVoucherKeyList(createForm.payment_voucher_key)
|
||||||
|
if (!hasVoucherKeys(voucherKeys)) {
|
||||||
|
ElMessage.warning('请上传支付凭证')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const data: CreateAgentRechargeRequest = {
|
const data: CreateAgentRechargeRequest = {
|
||||||
amount: Math.round(createForm.amount * 100), // 元转分
|
amount: amountYuanToFen(createForm.amount),
|
||||||
payment_method: createForm.payment_method as AgentRechargePaymentMethod,
|
payment_method: 'offline',
|
||||||
shop_id: createForm.shop_id!,
|
shop_id: createForm.shop_id,
|
||||||
|
payment_voucher_key: voucherKeys,
|
||||||
remark: createForm.remark || undefined
|
remark: createForm.remark || undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
const res = await AgentRechargeService.createAgentRecharge(data)
|
||||||
createForm.payment_method === 'offline' &&
|
if (res.code !== 0) {
|
||||||
hasVoucherKeys(createForm.payment_voucher_key)
|
ElMessage.error(res.msg || '充值订单创建失败')
|
||||||
) {
|
return
|
||||||
data.payment_voucher_key = toVoucherKeyList(createForm.payment_voucher_key)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await AgentRechargeService.createAgentRecharge(data)
|
|
||||||
ElMessage.success('充值订单创建成功')
|
ElMessage.success('充值订单创建成功')
|
||||||
createDialogVisible.value = false
|
createDialogVisible.value = false
|
||||||
formRef.resetFields()
|
|
||||||
await getTableData()
|
await getTableData()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
@@ -817,7 +1058,155 @@
|
|||||||
createLoading.value = false
|
createLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleOnlineRechargeCreate = async (
|
||||||
|
amountYuan: number,
|
||||||
|
paymentMethod: AgentRechargePaymentMethod
|
||||||
|
) => {
|
||||||
|
if (paymentMethod === 'offline') return
|
||||||
|
|
||||||
|
const requestId = onlineRequestId.value || createOnlineRechargeRequestId()
|
||||||
|
onlineRequestId.value = requestId
|
||||||
|
onlineCreateRetried.value = false
|
||||||
|
|
||||||
|
const request: CreateAgentRechargeRequest = {
|
||||||
|
amount: amountYuanToFen(amountYuan),
|
||||||
|
payment_method: paymentMethod,
|
||||||
|
request_id: requestId
|
||||||
|
}
|
||||||
|
|
||||||
|
let attempt = 0
|
||||||
|
while (attempt < 2) {
|
||||||
|
try {
|
||||||
|
const res = await AgentRechargeService.createAgentRecharge(request)
|
||||||
|
if (res.code !== 0) {
|
||||||
|
ElMessage.error(res.msg || '在线充值创建失败')
|
||||||
|
onlineRequestId.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.data?.qr_content) {
|
||||||
|
ElMessage.error('支付接口未返回有效二维码,请刷新后重试')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
onlineQrRecharge.value = res.data
|
||||||
|
onlinePaymentStatus.value = null
|
||||||
|
qrContent.value = res.data.qr_content
|
||||||
|
onlineQrDialogVisible.value = true
|
||||||
|
createDialogVisible.value = false
|
||||||
|
onlineRequestId.value = null
|
||||||
|
await getTableData()
|
||||||
|
return
|
||||||
|
} catch (error) {
|
||||||
|
if (attempt === 0 && shouldRetryOnlineCreate(error)) {
|
||||||
|
attempt += 1
|
||||||
|
onlineCreateRetried.value = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = normalizeApiError(error)
|
||||||
|
if (normalized.kind === 'validation' || normalized.kind === 'conflict') {
|
||||||
|
ElMessage.error(normalized.message)
|
||||||
|
} else {
|
||||||
|
ElMessage.warning('在线充值请求结果未知,请勿重复提交;可关闭后刷新列表确认')
|
||||||
|
}
|
||||||
|
onlineRequestId.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopPaymentStatusPolling = () => {
|
||||||
|
if (paymentStatusTimer.value) {
|
||||||
|
clearInterval(paymentStatusTimer.value)
|
||||||
|
paymentStatusTimer.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadOnlinePaymentStatus = async () => {
|
||||||
|
const rechargeId = onlineQrRecharge.value?.id
|
||||||
|
if (!rechargeId || !onlineQrDialogVisible.value || document.visibilityState !== 'visible')
|
||||||
|
return
|
||||||
|
|
||||||
|
paymentStatusLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await AgentRechargeService.getPaymentStatus(rechargeId)
|
||||||
|
if (res.code !== 0) return
|
||||||
|
|
||||||
|
onlinePaymentStatus.value = res.data
|
||||||
|
if (onlineQrRecharge.value) {
|
||||||
|
onlineQrRecharge.value = {
|
||||||
|
...onlineQrRecharge.value,
|
||||||
|
status: res.data.status,
|
||||||
|
status_name: res.data.status_name || onlineQrRecharge.value.status_name,
|
||||||
|
paid_at: res.data.paid_at,
|
||||||
|
completed_at: res.data.completed_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOnlineRechargeTerminal(res.data.status)) {
|
||||||
|
stopPaymentStatusPolling()
|
||||||
|
if (res.data.status === 3) {
|
||||||
|
await Promise.all([getTableData(), refreshAgentWalletBalance()])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('查询在线充值状态失败:', error)
|
||||||
|
} finally {
|
||||||
|
paymentStatusLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startPaymentStatusPolling = () => {
|
||||||
|
stopPaymentStatusPolling()
|
||||||
|
void loadOnlinePaymentStatus()
|
||||||
|
paymentStatusTimer.value = setInterval(() => {
|
||||||
|
if (!onlineQrDialogVisible.value || document.visibilityState !== 'visible') {
|
||||||
|
stopPaymentStatusPolling()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
void loadOnlinePaymentStatus()
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDocumentVisibilityChange = () => {
|
||||||
|
if (document.visibilityState === 'visible' && onlineQrDialogVisible.value) {
|
||||||
|
startPaymentStatusPolling()
|
||||||
|
} else if (document.visibilityState === 'hidden') {
|
||||||
|
stopPaymentStatusPolling()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(onlineQrDialogVisible, (visible) => {
|
||||||
|
if (visible) {
|
||||||
|
startPaymentStatusPolling()
|
||||||
|
} else {
|
||||||
|
stopPaymentStatusPolling()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const handleQrDialogClosed = () => {
|
||||||
|
stopPaymentStatusPolling()
|
||||||
|
qrContent.value = ''
|
||||||
|
onlineQrRecharge.value = null
|
||||||
|
onlinePaymentStatus.value = null
|
||||||
|
onlineWalletBalance.value = null
|
||||||
|
paymentStatusLoading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshAgentWalletBalance = async () => {
|
||||||
|
if (!isAgentAccount.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await CommissionService.getShopFundSummary({ page: 1, page_size: 100 })
|
||||||
|
const currentShopId = Number(userStore.info.shop_id)
|
||||||
|
const currentShop =
|
||||||
|
res.code === 0 ? res.data.items?.find((item) => item.shop_id === currentShopId) : null
|
||||||
|
if (currentShop) onlineWalletBalance.value = currentShop.main_balance
|
||||||
|
} catch (error) {
|
||||||
|
console.error('刷新代理钱包余额失败:', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示确认支付对话框
|
// 显示确认支付对话框
|
||||||
@@ -935,4 +1324,38 @@
|
|||||||
.agent-recharge-page {
|
.agent-recharge-page {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.online-recharge-empty-hint {
|
||||||
|
margin-top: 8px;
|
||||||
|
color: var(--el-color-danger);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-recharge-qr-dialog {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
&__summary {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__hint {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__loading {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -727,10 +727,10 @@
|
|||||||
package_name: '',
|
package_name: '',
|
||||||
series_id: undefined,
|
series_id: undefined,
|
||||||
package_type: '',
|
package_type: '',
|
||||||
calendar_type: undefined,
|
calendar_type: null,
|
||||||
duration_days: undefined,
|
duration_days: undefined,
|
||||||
duration_months: 1,
|
duration_months: 1,
|
||||||
data_reset_cycle: undefined,
|
data_reset_cycle: null,
|
||||||
enable_virtual_data: false,
|
enable_virtual_data: false,
|
||||||
expiry_base: 'from_activation',
|
expiry_base: 'from_activation',
|
||||||
real_data_mb: 0,
|
real_data_mb: 0,
|
||||||
@@ -1345,10 +1345,10 @@
|
|||||||
form.package_name = data.package_name
|
form.package_name = data.package_name
|
||||||
form.series_id = data.series_id
|
form.series_id = data.series_id
|
||||||
form.package_type = data.package_type
|
form.package_type = data.package_type
|
||||||
form.calendar_type = data.calendar_type || undefined
|
form.calendar_type = data.calendar_type ?? null
|
||||||
form.duration_days = data.duration_days
|
form.duration_days = data.duration_days
|
||||||
form.duration_months = data.duration_months
|
form.duration_months = data.duration_months
|
||||||
form.data_reset_cycle = data.data_reset_cycle || undefined
|
form.data_reset_cycle = data.data_reset_cycle ?? null
|
||||||
form.enable_virtual_data = data.enable_virtual_data || false
|
form.enable_virtual_data = data.enable_virtual_data || false
|
||||||
form.expiry_base = data.expiry_base || 'from_activation'
|
form.expiry_base = data.expiry_base || 'from_activation'
|
||||||
form.real_data_mb = data.real_data_mb || 0
|
form.real_data_mb = data.real_data_mb || 0
|
||||||
@@ -1384,10 +1384,10 @@
|
|||||||
form.package_name = ''
|
form.package_name = ''
|
||||||
form.series_id = undefined
|
form.series_id = undefined
|
||||||
form.package_type = ''
|
form.package_type = ''
|
||||||
form.calendar_type = undefined
|
form.calendar_type = null
|
||||||
form.duration_days = undefined
|
form.duration_days = undefined
|
||||||
form.duration_months = 1
|
form.duration_months = 1
|
||||||
form.data_reset_cycle = undefined
|
form.data_reset_cycle = null
|
||||||
form.enable_virtual_data = false
|
form.enable_virtual_data = false
|
||||||
form.expiry_base = 'from_activation'
|
form.expiry_base = 'from_activation'
|
||||||
form.real_data_mb = 0
|
form.real_data_mb = 0
|
||||||
@@ -1428,10 +1428,10 @@
|
|||||||
form.package_name = ''
|
form.package_name = ''
|
||||||
form.series_id = undefined
|
form.series_id = undefined
|
||||||
form.package_type = ''
|
form.package_type = ''
|
||||||
form.calendar_type = undefined
|
form.calendar_type = null
|
||||||
form.duration_days = undefined
|
form.duration_days = undefined
|
||||||
form.duration_months = 1
|
form.duration_months = 1
|
||||||
form.data_reset_cycle = undefined
|
form.data_reset_cycle = null
|
||||||
form.enable_virtual_data = false
|
form.enable_virtual_data = false
|
||||||
form.expiry_base = 'from_activation'
|
form.expiry_base = 'from_activation'
|
||||||
form.real_data_mb = 0
|
form.real_data_mb = 0
|
||||||
@@ -1483,6 +1483,8 @@
|
|||||||
package_type: form.package_type,
|
package_type: form.package_type,
|
||||||
duration_months: form.duration_months,
|
duration_months: form.duration_months,
|
||||||
cost_price: costPriceInCents,
|
cost_price: costPriceInCents,
|
||||||
|
calendar_type: form.calendar_type ?? null,
|
||||||
|
data_reset_cycle: form.data_reset_cycle ?? null,
|
||||||
is_gift: form.is_gift
|
is_gift: form.is_gift
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1494,12 +1496,6 @@
|
|||||||
if (form.series_id !== undefined) {
|
if (form.series_id !== undefined) {
|
||||||
data.series_id = form.series_id
|
data.series_id = form.series_id
|
||||||
}
|
}
|
||||||
if (form.calendar_type) {
|
|
||||||
data.calendar_type = form.calendar_type
|
|
||||||
}
|
|
||||||
if (form.data_reset_cycle) {
|
|
||||||
data.data_reset_cycle = form.data_reset_cycle
|
|
||||||
}
|
|
||||||
// 只有 calendar_type 为 by_day 时才传 duration_days
|
// 只有 calendar_type 为 by_day 时才传 duration_days
|
||||||
if (form.calendar_type === 'by_day' && form.duration_days !== undefined) {
|
if (form.calendar_type === 'by_day' && form.duration_days !== undefined) {
|
||||||
data.duration_days = form.duration_days
|
data.duration_days = form.duration_days
|
||||||
|
|||||||
@@ -143,45 +143,23 @@
|
|||||||
|
|
||||||
<!-- 套餐列表 -->
|
<!-- 套餐列表 -->
|
||||||
<div v-if="detailData.packages && detailData.packages.length > 0" class="package-table">
|
<div v-if="detailData.packages && detailData.packages.length > 0" class="package-table">
|
||||||
<ElTable :data="detailData.packages" border stripe>
|
<SeriesGrantPackageTable
|
||||||
<ElTableColumn prop="package_name" label="套餐名称" />
|
:data="detailData.packages"
|
||||||
<ElTableColumn prop="package_code" label="套餐编码" />
|
:loading="loading"
|
||||||
<ElTableColumn label="成本价">
|
:show-actions="true"
|
||||||
<template #default="{ row }">
|
:actions-width="240"
|
||||||
<span class="amount-value">¥{{ (row.cost_price / 100).toFixed(2) }}</span>
|
>
|
||||||
</template>
|
<template #actions="{ row }">
|
||||||
</ElTableColumn>
|
<div class="package-table-actions">
|
||||||
<ElTableColumn label="套餐默认生效条件">
|
<ElButton
|
||||||
<template #default="{ row }">
|
v-if="canUpdateExpiryBase"
|
||||||
{{ row.default_expiry_base_name || '-' }}
|
type="primary"
|
||||||
</template>
|
size="small"
|
||||||
</ElTableColumn>
|
link
|
||||||
<ElTableColumn label="覆盖生效条件">
|
@click="showExpiryBaseDialog(row)"
|
||||||
<template #default="{ row }">
|
>
|
||||||
{{ row.expiry_base_override_name || '跟随套餐默认' }}
|
修改生效条件
|
||||||
</template>
|
</ElButton>
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn label="最终生效条件">
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ row.effective_expiry_base_name || '-' }}
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn label="上架状态" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<ElTag v-if="row.shelf_status === 1" type="success" size="small">上架</ElTag>
|
|
||||||
<ElTag v-else-if="row.shelf_status === 2" type="info" size="small">下架</ElTag>
|
|
||||||
<span v-else>-</span>
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn label="状态" width="100" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<ElTag v-if="row.status === 1" type="success" size="small">启用</ElTag>
|
|
||||||
<ElTag v-else-if="row.status === 2" type="danger" size="small">禁用</ElTag>
|
|
||||||
<span v-else>-</span>
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn label="操作" width="150" align="center" fixed="right">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<ElButton
|
<ElButton
|
||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -200,9 +178,9 @@
|
|||||||
>
|
>
|
||||||
删除
|
删除
|
||||||
</ElButton>
|
</ElButton>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</SeriesGrantPackageTable>
|
||||||
</ElTable>
|
|
||||||
</div>
|
</div>
|
||||||
<ElEmpty v-else description="暂无套餐" :image-size="80" />
|
<ElEmpty v-else description="暂无套餐" :image-size="80" />
|
||||||
</div>
|
</div>
|
||||||
@@ -214,95 +192,58 @@
|
|||||||
<span>加载中...</span>
|
<span>加载中...</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 添加/编辑套餐对话框 -->
|
<SeriesGrantPackageDialog
|
||||||
<ElDialog
|
|
||||||
v-model="packageDialogVisible"
|
v-model="packageDialogVisible"
|
||||||
:title="packageDialogType === 'add' ? '添加套餐' : '编辑套餐'"
|
v-model:form="packageForm"
|
||||||
width="500px"
|
:dialog-type="packageDialogType"
|
||||||
:close-on-click-modal="false"
|
:rules="packageRules"
|
||||||
|
:available-packages="availablePackages"
|
||||||
|
:package-loading="packageLoading"
|
||||||
|
:submit-loading="submitLoading"
|
||||||
|
:can-update-expiry-base="canUpdateExpiryBase"
|
||||||
|
:has-series="Boolean(detailData?.series_id)"
|
||||||
|
@search="searchPackages"
|
||||||
|
@submit="handleSavePackage"
|
||||||
@closed="handlePackageDialogClosed"
|
@closed="handlePackageDialogClosed"
|
||||||
>
|
|
||||||
<ElForm ref="packageFormRef" :model="packageForm" :rules="packageRules" label-width="100px">
|
|
||||||
<!-- 添加模式:选择套餐 -->
|
|
||||||
<ElFormItem label="选择套餐" prop="package_id" v-if="packageDialogType === 'add'">
|
|
||||||
<ElSelect
|
|
||||||
v-model="packageForm.package_id"
|
|
||||||
placeholder="请选择套餐"
|
|
||||||
style="width: 100%"
|
|
||||||
filterable
|
|
||||||
remote
|
|
||||||
:remote-method="searchPackages"
|
|
||||||
:loading="packageLoading"
|
|
||||||
clearable
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
v-if="availablePackages.length === 0 && !packageLoading && detailData?.series_id"
|
|
||||||
>
|
|
||||||
<ElOption disabled value="" label="该系列没有可选套餐" />
|
|
||||||
</template>
|
|
||||||
<ElOption
|
|
||||||
v-for="pkg in availablePackages"
|
|
||||||
:key="pkg.id"
|
|
||||||
:label="`${pkg.package_name} (${pkg.package_code})`"
|
|
||||||
:value="pkg.id"
|
|
||||||
:disabled="pkg.is_authorized"
|
|
||||||
/>
|
/>
|
||||||
</ElSelect>
|
|
||||||
</ElFormItem>
|
|
||||||
|
|
||||||
<!-- 编辑模式:显示套餐信息 -->
|
<ElDialog
|
||||||
<ElFormItem label="套餐名称" v-if="packageDialogType === 'edit'">
|
v-model="expiryDialogVisible"
|
||||||
<span>{{ packageForm.package_name }}</span>
|
title="修改生效条件"
|
||||||
</ElFormItem>
|
width="460px"
|
||||||
<ElFormItem label="套餐编码" v-if="packageDialogType === 'edit'">
|
:close-on-click-modal="false"
|
||||||
<span>{{ packageForm.package_code }}</span>
|
|
||||||
</ElFormItem>
|
|
||||||
|
|
||||||
<!-- 成本价 -->
|
|
||||||
<ElFormItem label="成本价(元)" prop="cost_price_yuan">
|
|
||||||
<ElInputNumber
|
|
||||||
v-model="packageForm.cost_price_yuan"
|
|
||||||
:precision="2"
|
|
||||||
:step="0.01"
|
|
||||||
:controls="false"
|
|
||||||
style="width: 100%"
|
|
||||||
placeholder="请输入成本价"
|
|
||||||
/>
|
|
||||||
<div v-if="packageForm.original_cost_price" class="form-tip">
|
|
||||||
请参考{{ packageForm.package_name || '套餐' }} - 套餐成本价: ¥{{
|
|
||||||
packageForm.original_cost_price.toFixed(2)
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</ElFormItem>
|
|
||||||
<ElFormItem v-if="packageDialogType === 'add' || canUpdateExpiryBase" label="生效条件">
|
|
||||||
<ElSelect
|
|
||||||
v-model="packageForm.expiry_base_override"
|
|
||||||
placeholder="请选择生效条件"
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
>
|
||||||
|
<ElForm label-width="130px">
|
||||||
|
<ElFormItem label="套餐名称">
|
||||||
|
<span class="ellipsis-value" :title="expiryForm.package_name || ''">
|
||||||
|
{{ expiryForm.package_name || '-' }}
|
||||||
|
</span>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="套餐编码">
|
||||||
|
<span class="ellipsis-value" :title="expiryForm.package_code || ''">
|
||||||
|
{{ expiryForm.package_code || '-' }}
|
||||||
|
</span>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="套餐默认生效条件">
|
||||||
|
{{ expiryForm.default_expiry_base_name || '-' }}
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="覆盖生效条件">
|
||||||
|
<ElSelect v-model="expiryForm.expiry_base_override" style="width: 100%">
|
||||||
<ElOption label="跟随套餐默认" value="default" />
|
<ElOption label="跟随套餐默认" value="default" />
|
||||||
<ElOption label="购买即生效" value="from_purchase" />
|
<ElOption label="购买即生效" value="from_purchase" />
|
||||||
<ElOption label="实名激活时生效" value="from_activation" />
|
<ElOption label="实名激活时生效" value="from_activation" />
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
<div class="form-tip">仅影响后续新订单,不影响已购买套餐</div>
|
|
||||||
</ElFormItem>
|
|
||||||
<template v-if="packageDialogType === 'edit'">
|
|
||||||
<ElFormItem label="套餐默认生效条件">
|
|
||||||
{{ packageForm.default_expiry_base_name || '-' }}
|
|
||||||
</ElFormItem>
|
|
||||||
<ElFormItem label="覆盖生效条件">
|
|
||||||
{{ packageForm.expiry_base_override_name || '跟随套餐默认' }}
|
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="最终生效条件">
|
<ElFormItem label="最终生效条件">
|
||||||
{{ packageForm.effective_expiry_base_name || '-' }}
|
{{ expiryForm.effective_expiry_base_name || '-' }}
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</template>
|
<div class="form-tip">仅影响后续新订单,不影响已购买套餐</div>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<ElButton @click="packageDialogVisible = false">取消</ElButton>
|
<ElButton @click="expiryDialogVisible = false">取消</ElButton>
|
||||||
<ElButton type="primary" @click="handleSavePackage" :loading="submitLoading">
|
<ElButton type="primary" :loading="expirySubmitLoading" @click="saveExpiryBase">
|
||||||
保存
|
保存
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -330,10 +271,8 @@
|
|||||||
ElDialog,
|
ElDialog,
|
||||||
ElSelect,
|
ElSelect,
|
||||||
ElOption,
|
ElOption,
|
||||||
ElInputNumber,
|
|
||||||
ElForm,
|
ElForm,
|
||||||
ElFormItem,
|
ElFormItem,
|
||||||
FormInstance,
|
|
||||||
FormRules
|
FormRules
|
||||||
} from 'element-plus'
|
} from 'element-plus'
|
||||||
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
|
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
|
||||||
@@ -354,9 +293,13 @@
|
|||||||
import { formatDateTime } from '@/utils/business/format'
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
import { getEnableStatusText, JULY_PERMISSIONS } from '@/config/constants'
|
import { getEnableStatusText, JULY_PERMISSIONS } from '@/config/constants'
|
||||||
import { useAuth } from '@/composables/useAuth'
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
import SeriesGrantPackageTable from '@/components/business/SeriesGrantPackageTable.vue'
|
||||||
|
import SeriesGrantPackageDialog from '@/components/business/SeriesGrantPackageDialog.vue'
|
||||||
import {
|
import {
|
||||||
mergeGrantPackageCandidates,
|
mergeGrantPackageCandidates,
|
||||||
type GrantPackageCandidate
|
type GrantPackageCandidate,
|
||||||
|
type PackageCostFormItem,
|
||||||
|
type SeriesGrantPackageForm
|
||||||
} from '@/utils/business/seriesGrantPackage'
|
} from '@/utils/business/seriesGrantPackage'
|
||||||
|
|
||||||
defineOptions({ name: 'SeriesGrantsDetail' })
|
defineOptions({ name: 'SeriesGrantsDetail' })
|
||||||
@@ -374,61 +317,79 @@
|
|||||||
const packageDialogVisible = ref(false)
|
const packageDialogVisible = ref(false)
|
||||||
const packageDialogType = ref<'add' | 'edit'>('add')
|
const packageDialogType = ref<'add' | 'edit'>('add')
|
||||||
const availablePackages = ref<GrantPackageCandidate[]>([])
|
const availablePackages = ref<GrantPackageCandidate[]>([])
|
||||||
const packageFormRef = ref<FormInstance>()
|
const expiryDialogVisible = ref(false)
|
||||||
|
const expirySubmitLoading = ref(false)
|
||||||
|
|
||||||
type ExpiryBaseSelection = PackageAllocationExpiryBaseOverride | 'default'
|
type ExpiryBaseSelection = PackageAllocationExpiryBaseOverride | 'default'
|
||||||
|
|
||||||
// 套餐表单
|
type ExpiryFormState = {
|
||||||
const packageForm = ref<{
|
|
||||||
package_id?: number
|
package_id?: number
|
||||||
allocation_id?: number
|
allocation_id?: number
|
||||||
package_name?: string
|
package_name?: string
|
||||||
package_code?: string
|
package_code?: string
|
||||||
original_cost_price?: number
|
|
||||||
cost_price_yuan: number
|
|
||||||
expiry_base_override: ExpiryBaseSelection
|
|
||||||
initial_expiry_base_override: ExpiryBaseSelection
|
|
||||||
default_expiry_base_name?: string | null
|
default_expiry_base_name?: string | null
|
||||||
|
expiry_base_override: ExpiryBaseSelection
|
||||||
expiry_base_override_name?: string | null
|
expiry_base_override_name?: string | null
|
||||||
effective_expiry_base_name?: string | null
|
effective_expiry_base_name?: string | null
|
||||||
}>({
|
}
|
||||||
|
|
||||||
|
const expiryForm = ref<ExpiryFormState>({ expiry_base_override: 'default' })
|
||||||
|
|
||||||
|
const createDefaultPackageForm = (): SeriesGrantPackageForm => ({
|
||||||
|
package_id: undefined,
|
||||||
|
allocation_id: undefined,
|
||||||
|
package_ids: [],
|
||||||
|
package_name: undefined,
|
||||||
|
package_code: undefined,
|
||||||
cost_price_yuan: 0,
|
cost_price_yuan: 0,
|
||||||
expiry_base_override: 'default',
|
expiry_base_override: 'default',
|
||||||
initial_expiry_base_override: 'default'
|
initial_expiry_base_override: 'default',
|
||||||
|
packages: []
|
||||||
})
|
})
|
||||||
|
const packageForm = ref<SeriesGrantPackageForm>(createDefaultPackageForm())
|
||||||
|
|
||||||
// 表单验证规则
|
// 表单验证规则
|
||||||
const packageRules = computed<FormRules>(() => ({
|
const packageRules = computed<FormRules>(() => ({
|
||||||
package_id: [
|
package_ids: [
|
||||||
{ required: packageDialogType.value === 'add', message: '请选择套餐', trigger: 'change' }
|
{
|
||||||
|
validator: (_rule, value, callback) => {
|
||||||
|
if (packageDialogType.value !== 'add') {
|
||||||
|
callback()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!Array.isArray(value) || value.length === 0) {
|
||||||
|
callback(new Error('请至少选择一个套餐'))
|
||||||
|
} else if (value.length > 100) {
|
||||||
|
callback(new Error('最多选择100个套餐'))
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: 'change'
|
||||||
|
}
|
||||||
],
|
],
|
||||||
cost_price_yuan: [
|
cost_price_yuan: [
|
||||||
{ required: true, message: '请输入成本价', trigger: 'blur' },
|
{
|
||||||
{ type: 'number', min: 0, message: '成本价不能小于0', trigger: 'blur' }
|
required: packageDialogType.value === 'edit',
|
||||||
|
message: '请输入成本价',
|
||||||
|
trigger: 'blur'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
validator: (_rule, value, callback) => {
|
||||||
|
if (value === undefined || value === null || value === '') {
|
||||||
|
callback(new Error('请输入成本价'))
|
||||||
|
} else if (value < 0) {
|
||||||
|
callback(new Error('成本价不能小于0'))
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// 梯度配置表格数据(合并授权数据和系列数据)
|
// 梯度配置表格数据(合并授权数据和系列数据)
|
||||||
watch(
|
|
||||||
() => packageForm.value.package_id,
|
|
||||||
(packageId) => {
|
|
||||||
if (packageDialogType.value !== 'add' || !packageId) return
|
|
||||||
|
|
||||||
const selectedPackage = availablePackages.value.find((pkg) => pkg.id === packageId)
|
|
||||||
if (!selectedPackage) return
|
|
||||||
|
|
||||||
const originalCostPrice =
|
|
||||||
selectedPackage.cost_price !== undefined && selectedPackage.cost_price !== null
|
|
||||||
? selectedPackage.cost_price / 100
|
|
||||||
: 0
|
|
||||||
|
|
||||||
packageForm.value.package_name = selectedPackage.package_name
|
|
||||||
packageForm.value.package_code = selectedPackage.package_code
|
|
||||||
packageForm.value.original_cost_price = originalCostPrice
|
|
||||||
packageForm.value.cost_price_yuan = originalCostPrice
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const tierTableData = computed(() => {
|
const tierTableData = computed(() => {
|
||||||
if (
|
if (
|
||||||
!detailData.value?.commission_tiers ||
|
!detailData.value?.commission_tiers ||
|
||||||
@@ -507,19 +468,31 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 显示添加套餐对话框
|
||||||
|
const assignPackageForm = (values: Partial<SeriesGrantPackageForm> = {}) => {
|
||||||
|
Object.assign(packageForm.value, createDefaultPackageForm(), values)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvePackagePricing = (row?: GrantPackageInfo) => {
|
||||||
|
const pkgOption = availablePackages.value.find((pkg) => pkg.id === row?.package_id)
|
||||||
|
const currentCostPrice = row?.cost_price !== undefined ? row.cost_price / 100 : 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
original_cost_price:
|
||||||
|
pkgOption?.cost_price !== undefined && pkgOption.cost_price !== null
|
||||||
|
? pkgOption.cost_price / 100
|
||||||
|
: currentCostPrice,
|
||||||
|
suggested_retail_price:
|
||||||
|
pkgOption?.suggested_retail_price !== undefined && pkgOption.suggested_retail_price !== null
|
||||||
|
? pkgOption.suggested_retail_price / 100
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 显示添加套餐对话框
|
// 显示添加套餐对话框
|
||||||
const showAddPackageDialog = () => {
|
const showAddPackageDialog = () => {
|
||||||
packageDialogType.value = 'add'
|
packageDialogType.value = 'add'
|
||||||
packageForm.value = {
|
assignPackageForm({ package_ids: [], packages: [] })
|
||||||
package_id: undefined,
|
|
||||||
allocation_id: undefined,
|
|
||||||
package_name: undefined,
|
|
||||||
package_code: undefined,
|
|
||||||
original_cost_price: undefined,
|
|
||||||
cost_price_yuan: 0,
|
|
||||||
expiry_base_override: 'default',
|
|
||||||
initial_expiry_base_override: 'default'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载可用套餐
|
// 加载可用套餐
|
||||||
if (detailData.value?.series_id) {
|
if (detailData.value?.series_id) {
|
||||||
@@ -532,23 +505,44 @@
|
|||||||
// 显示编辑套餐对话框
|
// 显示编辑套餐对话框
|
||||||
const showEditPackageDialog = (row: GrantPackageInfo) => {
|
const showEditPackageDialog = (row: GrantPackageInfo) => {
|
||||||
packageDialogType.value = 'edit'
|
packageDialogType.value = 'edit'
|
||||||
packageForm.value = {
|
const packagePricing = resolvePackagePricing(row)
|
||||||
|
|
||||||
|
assignPackageForm({
|
||||||
package_id: row.package_id,
|
package_id: row.package_id,
|
||||||
allocation_id: row.allocation_id,
|
allocation_id: row.allocation_id,
|
||||||
package_name: row.package_name,
|
package_name: row.package_name,
|
||||||
package_code: row.package_code,
|
package_code: row.package_code,
|
||||||
original_cost_price: row.cost_price / 100,
|
original_cost_price: packagePricing.original_cost_price,
|
||||||
cost_price_yuan: row.cost_price / 100,
|
cost_price_yuan: row.cost_price / 100,
|
||||||
|
suggested_retail_price: packagePricing.suggested_retail_price,
|
||||||
expiry_base_override: row.expiry_base_override ?? 'default',
|
expiry_base_override: row.expiry_base_override ?? 'default',
|
||||||
initial_expiry_base_override: row.expiry_base_override ?? 'default',
|
initial_expiry_base_override: row.expiry_base_override ?? 'default',
|
||||||
default_expiry_base_name: row.default_expiry_base_name,
|
default_expiry_base_name: row.default_expiry_base_name,
|
||||||
expiry_base_override_name: row.expiry_base_override_name,
|
expiry_base_override_name: row.expiry_base_override_name,
|
||||||
effective_expiry_base_name: row.effective_expiry_base_name
|
effective_expiry_base_name: row.effective_expiry_base_name
|
||||||
}
|
})
|
||||||
|
|
||||||
packageDialogVisible.value = true
|
packageDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showExpiryBaseDialog = (row: GrantPackageInfo) => {
|
||||||
|
if (row.allocation_id === undefined || row.allocation_id === null) {
|
||||||
|
ElMessage.error('该套餐分配记录缺少套餐分配 ID,无法修改生效条件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
expiryForm.value = {
|
||||||
|
package_id: row.package_id,
|
||||||
|
allocation_id: row.allocation_id,
|
||||||
|
package_name: row.package_name,
|
||||||
|
package_code: row.package_code,
|
||||||
|
default_expiry_base_name: row.default_expiry_base_name,
|
||||||
|
expiry_base_override: row.expiry_base_override ?? 'default',
|
||||||
|
expiry_base_override_name: row.expiry_base_override_name,
|
||||||
|
effective_expiry_base_name: row.effective_expiry_base_name
|
||||||
|
}
|
||||||
|
expiryDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
// 加载可用套餐
|
// 加载可用套餐
|
||||||
const loadAvailablePackages = async (packageName?: string) => {
|
const loadAvailablePackages = async (packageName?: string) => {
|
||||||
if (!detailData.value?.series_id) return
|
if (!detailData.value?.series_id) return
|
||||||
@@ -596,33 +590,98 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createPackageCostFormItem = (packageId: number): PackageCostFormItem => {
|
||||||
|
const selectedPackage = availablePackages.value.find((pkg) => pkg.id === packageId)
|
||||||
|
const originalCostPrice = selectedPackage?.cost_price ? selectedPackage.cost_price / 100 : 0
|
||||||
|
return {
|
||||||
|
package_id: packageId,
|
||||||
|
package_name: selectedPackage?.package_name,
|
||||||
|
package_code: selectedPackage?.package_code,
|
||||||
|
cost_price_yuan: originalCostPrice,
|
||||||
|
original_cost_price: originalCostPrice,
|
||||||
|
suggested_retail_price:
|
||||||
|
selectedPackage?.suggested_retail_price !== undefined &&
|
||||||
|
selectedPackage.suggested_retail_price !== null
|
||||||
|
? selectedPackage.suggested_retail_price / 100
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPackageCostPriceMax = (pkg?: {
|
||||||
|
original_cost_price?: number
|
||||||
|
suggested_retail_price?: number | null
|
||||||
|
}) => {
|
||||||
|
if (pkg?.suggested_retail_price !== undefined && pkg.suggested_retail_price !== null) {
|
||||||
|
return Number((pkg.suggested_retail_price * 1.5).toFixed(2))
|
||||||
|
}
|
||||||
|
if (pkg?.original_cost_price !== undefined && pkg.original_cost_price !== null) {
|
||||||
|
return Number((pkg.original_cost_price * 1.5).toFixed(2))
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => packageForm.value.package_ids,
|
||||||
|
(packageIds) => {
|
||||||
|
if (packageDialogType.value !== 'add') return
|
||||||
|
const existingItems = new Map(
|
||||||
|
packageForm.value.packages.map((item) => [item.package_id, item])
|
||||||
|
)
|
||||||
|
packageForm.value.packages = packageIds.map(
|
||||||
|
(packageId) => existingItems.get(packageId) || createPackageCostFormItem(packageId)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
// 保存套餐
|
// 保存套餐
|
||||||
const handleSavePackage = async () => {
|
const handleSavePackage = async () => {
|
||||||
if (!packageFormRef.value || !detailData.value) return
|
if (!detailData.value) return
|
||||||
|
|
||||||
const grantId = detailData.value.id // 保存到常量中避免类型检查问题
|
|
||||||
await packageFormRef.value.validate(async (valid) => {
|
|
||||||
if (!valid) return
|
|
||||||
|
|
||||||
|
const grantId = detailData.value.id
|
||||||
submitLoading.value = true
|
submitLoading.value = true
|
||||||
try {
|
try {
|
||||||
if (packageDialogType.value === 'add') {
|
if (packageDialogType.value === 'add') {
|
||||||
|
if (packageForm.value.packages.length === 0) {
|
||||||
|
ElMessage.error('请至少选择一个套餐')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const packages: GrantPackageItem[] = packageForm.value.packages.map((pkg) => {
|
||||||
|
const currentCostPrice = Number(pkg.cost_price_yuan)
|
||||||
|
if (!Number.isFinite(currentCostPrice) || currentCostPrice < 0) {
|
||||||
|
throw new Error(`套餐 ${pkg.package_name || pkg.package_id} 的成本价无效`)
|
||||||
|
}
|
||||||
|
const maxCostPrice = getPackageCostPriceMax(pkg)
|
||||||
|
if (maxCostPrice !== undefined && currentCostPrice > maxCostPrice) {
|
||||||
|
throw new Error(
|
||||||
|
`套餐 ${pkg.package_name || pkg.package_id} 的成本价不能高于 ¥${maxCostPrice.toFixed(2)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (pkg.original_cost_price !== undefined && currentCostPrice < pkg.original_cost_price) {
|
||||||
|
throw new Error(
|
||||||
|
`套餐 ${pkg.package_name || pkg.package_id} 的成本价不能低于 ¥${pkg.original_cost_price.toFixed(2)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
package_id: pkg.package_id,
|
||||||
|
cost_price: Math.round(currentCostPrice * 100)
|
||||||
|
}
|
||||||
|
})
|
||||||
await ShopSeriesGrantService.manageGrantPackages(grantId, {
|
await ShopSeriesGrantService.manageGrantPackages(grantId, {
|
||||||
expiry_base_override:
|
expiry_base_override:
|
||||||
packageForm.value.expiry_base_override === 'default'
|
packageForm.value.expiry_base_override === 'default'
|
||||||
? null
|
? null
|
||||||
: packageForm.value.expiry_base_override,
|
: packageForm.value.expiry_base_override,
|
||||||
packages: [
|
packages
|
||||||
{
|
|
||||||
package_id: packageForm.value.package_id!,
|
|
||||||
cost_price: Math.round(packageForm.value.cost_price_yuan * 100)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
const currentCostPriceYuan = Number(packageForm.value.cost_price_yuan)
|
||||||
|
if (Number.isNaN(currentCostPriceYuan)) {
|
||||||
|
throw new Error('当前成本价无效,无法保存')
|
||||||
|
}
|
||||||
|
const costPrice = Math.round(currentCostPriceYuan * 100)
|
||||||
const expiryBaseChanged =
|
const expiryBaseChanged =
|
||||||
packageForm.value.expiry_base_override !==
|
packageForm.value.expiry_base_override !== packageForm.value.initial_expiry_base_override
|
||||||
packageForm.value.initial_expiry_base_override
|
|
||||||
if (
|
if (
|
||||||
expiryBaseChanged &&
|
expiryBaseChanged &&
|
||||||
(packageForm.value.allocation_id === undefined ||
|
(packageForm.value.allocation_id === undefined ||
|
||||||
@@ -633,10 +692,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const packages: GrantPackageItem[] = [
|
const packages: GrantPackageItem[] = [
|
||||||
{
|
{ package_id: packageForm.value.package_id!, cost_price: costPrice }
|
||||||
package_id: packageForm.value.package_id!,
|
|
||||||
cost_price: Math.round(packageForm.value.cost_price_yuan * 100)
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
if (expiryBaseChanged) {
|
if (expiryBaseChanged) {
|
||||||
await ShopPackageAllocationService.updateShopPackageAllocationExpiryBase(
|
await ShopPackageAllocationService.updateShopPackageAllocationExpiryBase(
|
||||||
@@ -663,12 +719,10 @@
|
|||||||
// 刷新详情数据
|
// 刷新详情数据
|
||||||
await fetchDetail()
|
await fetchDetail()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
ElMessage.error(error instanceof Error ? error.message : '保存套餐失败')
|
||||||
console.log(packageDialogType.value === 'add' ? '添加失败' : '更新失败')
|
|
||||||
} finally {
|
} finally {
|
||||||
submitLoading.value = false
|
submitLoading.value = false
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除套餐
|
// 删除套餐
|
||||||
@@ -690,7 +744,10 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
await ShopSeriesGrantService.manageGrantPackages(detailData.value.id, { packages })
|
await ShopSeriesGrantService.manageGrantPackages(detailData.value.id, {
|
||||||
|
expiry_base_override: row.expiry_base_override ?? null,
|
||||||
|
packages
|
||||||
|
})
|
||||||
ElMessage.success('删除成功')
|
ElMessage.success('删除成功')
|
||||||
|
|
||||||
// 刷新详情数据
|
// 刷新详情数据
|
||||||
@@ -707,17 +764,35 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const saveExpiryBase = async () => {
|
||||||
|
if (expiryForm.value.allocation_id === undefined || expiryForm.value.allocation_id === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
expirySubmitLoading.value = true
|
||||||
|
try {
|
||||||
|
const response = await ShopPackageAllocationService.updateShopPackageAllocationExpiryBase(
|
||||||
|
expiryForm.value.allocation_id,
|
||||||
|
{
|
||||||
|
expiry_base_override:
|
||||||
|
expiryForm.value.expiry_base_override === 'default'
|
||||||
|
? null
|
||||||
|
: expiryForm.value.expiry_base_override
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (response.code === 0) {
|
||||||
|
ElMessage.success('生效条件更新成功')
|
||||||
|
expiryDialogVisible.value = false
|
||||||
|
await fetchDetail()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
expirySubmitLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 关闭对话框
|
// 关闭对话框
|
||||||
const handlePackageDialogClosed = () => {
|
const handlePackageDialogClosed = () => {
|
||||||
packageFormRef.value?.resetFields()
|
assignPackageForm()
|
||||||
packageForm.value = {
|
|
||||||
package_name: undefined,
|
|
||||||
package_code: undefined,
|
|
||||||
original_cost_price: undefined,
|
|
||||||
cost_price_yuan: 0,
|
|
||||||
expiry_base_override: 'default',
|
|
||||||
initial_expiry_base_override: 'default'
|
|
||||||
}
|
|
||||||
availablePackages.value = []
|
availablePackages.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -764,6 +839,13 @@
|
|||||||
|
|
||||||
.package-table {
|
.package-table {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
|
|
||||||
|
.package-table-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tier-table-wrapper {
|
.tier-table-wrapper {
|
||||||
@@ -794,6 +876,21 @@
|
|||||||
color: var(--el-color-warning);
|
color: var(--el-color-warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ellipsis-value {
|
||||||
|
display: block;
|
||||||
|
width: 260px;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-tip {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
.dialog-footer {
|
.dialog-footer {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,189 +21,60 @@
|
|||||||
</ElButton>
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ArtTable
|
<SeriesGrantPackageTable
|
||||||
ref="tableRef"
|
|
||||||
row-key="package_id"
|
|
||||||
:loading="loading"
|
|
||||||
:data="packageList"
|
:data="packageList"
|
||||||
:marginTop="10"
|
:loading="loading"
|
||||||
:pagination="false"
|
:show-actions="true"
|
||||||
:actions="getActions"
|
:actions-width="240"
|
||||||
:actionsWidth="240"
|
|
||||||
:inlineActionsCount="3"
|
|
||||||
>
|
>
|
||||||
<template #default>
|
<template #actions="{ row }">
|
||||||
<ElTableColumn
|
<div class="package-table-actions">
|
||||||
prop="package_name"
|
<ElButton
|
||||||
label="套餐名称"
|
v-if="canUpdateExpiryBase"
|
||||||
minWidth="220"
|
type="primary"
|
||||||
show-overflow-tooltip
|
size="small"
|
||||||
/>
|
link
|
||||||
<ElTableColumn
|
@click="showExpiryBaseDialog(row)"
|
||||||
prop="package_code"
|
|
||||||
label="套餐编码"
|
|
||||||
minWidth="180"
|
|
||||||
show-overflow-tooltip
|
|
||||||
/>
|
|
||||||
<ElTableColumn label="成本价" width="120">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<span class="amount-value">¥{{ (row.cost_price / 100).toFixed(2) }}</span>
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn label="套餐默认生效条件" minWidth="150">
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ row.default_expiry_base_name || '-' }}
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn label="覆盖生效条件" minWidth="150">
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ row.expiry_base_override_name || '跟随套餐默认' }}
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn label="最终生效条件" minWidth="150">
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ row.effective_expiry_base_name || '-' }}
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn label="上架状态" width="100" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<ElTag v-if="row.shelf_status === 1" type="success" size="small">上架</ElTag>
|
|
||||||
<ElTag v-else-if="row.shelf_status === 2" type="info" size="small">下架</ElTag>
|
|
||||||
<span v-else>-</span>
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn label="状态" width="100" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<ElTag v-if="row.status === 1" type="success" size="small">启用</ElTag>
|
|
||||||
<ElTag v-else-if="row.status === 2" type="danger" size="small">禁用</ElTag>
|
|
||||||
<span v-else>-</span>
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
</template>
|
|
||||||
</ArtTable>
|
|
||||||
</ElCard>
|
|
||||||
|
|
||||||
<ElDialog
|
|
||||||
v-model="packageDialogVisible"
|
|
||||||
:title="packageDialogType === 'add' ? '添加套餐' : '编辑套餐'"
|
|
||||||
width="40%"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
@closed="handlePackageDialogClosed"
|
|
||||||
>
|
>
|
||||||
<ElForm ref="packageFormRef" :model="packageForm" :rules="packageRules" label-width="140px">
|
修改生效条件
|
||||||
<ElFormItem label="选择套餐" prop="package_ids" v-if="packageDialogType === 'add'">
|
</ElButton>
|
||||||
<ElSelect
|
<ElButton
|
||||||
v-model="packageForm.package_ids"
|
v-if="hasAuth('series_grants:edit_packages_list')"
|
||||||
placeholder="请选择套餐(可多选)"
|
type="primary"
|
||||||
style="width: 100%"
|
size="small"
|
||||||
filterable
|
link
|
||||||
remote
|
@click="showEditPackageDialog(row)"
|
||||||
:remote-method="searchAvailablePackages"
|
|
||||||
:loading="packageLoading"
|
|
||||||
clearable
|
|
||||||
multiple
|
|
||||||
:multiple-limit="100"
|
|
||||||
>
|
>
|
||||||
<template v-if="availablePackages.length === 0 && !packageLoading && seriesId">
|
编辑
|
||||||
<ElOption disabled value="" label="该系列没有可选套餐" />
|
</ElButton>
|
||||||
</template>
|
<ElButton
|
||||||
<ElOption
|
v-if="hasAuth('series_grants:delete_packages_list')"
|
||||||
v-for="pkg in availablePackages"
|
type="danger"
|
||||||
:key="pkg.id"
|
size="small"
|
||||||
:label="`${pkg.package_name} (${pkg.package_code})`"
|
link
|
||||||
:value="pkg.id"
|
@click="handleDeletePackage(row)"
|
||||||
:disabled="pkg.is_authorized"
|
|
||||||
/>
|
|
||||||
</ElSelect>
|
|
||||||
</ElFormItem>
|
|
||||||
|
|
||||||
<ElFormItem
|
|
||||||
v-if="packageDialogType === 'add' && packageForm.packages.length"
|
|
||||||
label="套餐配置"
|
|
||||||
prop="packages"
|
|
||||||
>
|
>
|
||||||
<div class="package-config-list">
|
删除
|
||||||
<div
|
|
||||||
v-for="pkg in packageForm.packages"
|
|
||||||
:key="pkg.package_id"
|
|
||||||
class="package-config-item"
|
|
||||||
>
|
|
||||||
<div class="package-config-name" :title="pkg.package_name || ''">
|
|
||||||
{{ pkg.package_name || '套餐名称不可用' }}
|
|
||||||
</div>
|
|
||||||
<div class="package-config-cost">
|
|
||||||
<span class="package-config-field-label">套餐成本价(元)</span>
|
|
||||||
<ElInputNumber
|
|
||||||
v-model="pkg.cost_price_yuan"
|
|
||||||
:min="pkg.original_cost_price || 0"
|
|
||||||
:max="getPackageCostPriceMax(pkg)"
|
|
||||||
:precision="2"
|
|
||||||
:step="0.01"
|
|
||||||
:controls="false"
|
|
||||||
placeholder="请输入成本价"
|
|
||||||
style="width: 150px"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ElFormItem>
|
|
||||||
|
|
||||||
<ElFormItem label="套餐名称" v-if="packageDialogType === 'edit'">
|
|
||||||
<span>{{ packageForm.package_name }}</span>
|
|
||||||
</ElFormItem>
|
|
||||||
<ElFormItem label="套餐编码" v-if="packageDialogType === 'edit'">
|
|
||||||
<span>{{ packageForm.package_code }}</span>
|
|
||||||
</ElFormItem>
|
|
||||||
|
|
||||||
<ElFormItem label="成本价(元)" prop="cost_price_yuan">
|
|
||||||
<ElInputNumber
|
|
||||||
v-model="packageForm.cost_price_yuan"
|
|
||||||
:precision="2"
|
|
||||||
:step="0.01"
|
|
||||||
:controls="false"
|
|
||||||
style="width: 100%"
|
|
||||||
placeholder="请输入成本价"
|
|
||||||
/>
|
|
||||||
<div v-if="packageForm.original_cost_price" class="form-tip">
|
|
||||||
请参考{{ packageForm.package_name || '套餐' }} - 套餐成本价: ¥{{
|
|
||||||
packageForm.original_cost_price.toFixed(2)
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</ElFormItem>
|
|
||||||
<ElFormItem v-if="packageDialogType === 'add' || canUpdateExpiryBase" label="生效条件">
|
|
||||||
<ElSelect
|
|
||||||
v-model="packageForm.expiry_base_override"
|
|
||||||
placeholder="请选择生效条件"
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<ElOption label="跟随套餐默认" value="default" />
|
|
||||||
<ElOption label="购买即生效" value="from_purchase" />
|
|
||||||
<ElOption label="实名激活时生效" value="from_activation" />
|
|
||||||
</ElSelect>
|
|
||||||
<div class="form-tip">仅影响后续新订单,不影响已购买套餐</div>
|
|
||||||
</ElFormItem>
|
|
||||||
<template v-if="packageDialogType === 'edit'">
|
|
||||||
<ElFormItem label="套餐默认生效条件">
|
|
||||||
{{ packageForm.default_expiry_base_name || '-' }}
|
|
||||||
</ElFormItem>
|
|
||||||
<ElFormItem label="覆盖生效条件">
|
|
||||||
{{ packageForm.expiry_base_override_name || '跟随套餐默认' }}
|
|
||||||
</ElFormItem>
|
|
||||||
<ElFormItem label="最终生效条件">
|
|
||||||
{{ packageForm.effective_expiry_base_name || '-' }}
|
|
||||||
</ElFormItem>
|
|
||||||
</template>
|
|
||||||
</ElForm>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<div class="dialog-footer">
|
|
||||||
<ElButton @click="packageDialogVisible = false">取消</ElButton>
|
|
||||||
<ElButton type="primary" @click="handleSavePackage" :loading="submitLoading">
|
|
||||||
保存
|
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ElDialog>
|
</SeriesGrantPackageTable>
|
||||||
|
</ElCard>
|
||||||
|
|
||||||
|
<SeriesGrantPackageDialog
|
||||||
|
v-model="packageDialogVisible"
|
||||||
|
v-model:form="packageForm"
|
||||||
|
:dialog-type="packageDialogType"
|
||||||
|
:rules="packageRules"
|
||||||
|
:available-packages="availablePackages"
|
||||||
|
:package-loading="packageLoading"
|
||||||
|
:submit-loading="submitLoading"
|
||||||
|
:can-update-expiry-base="canUpdateExpiryBase"
|
||||||
|
:has-series="Boolean(seriesId)"
|
||||||
|
@search="searchAvailablePackages"
|
||||||
|
@submit="handleSavePackage"
|
||||||
|
@closed="handlePackageDialogClosed"
|
||||||
|
/>
|
||||||
|
|
||||||
<ElDialog
|
<ElDialog
|
||||||
v-model="expiryDialogVisible"
|
v-model="expiryDialogVisible"
|
||||||
@@ -252,22 +123,18 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import {
|
import {
|
||||||
ElCard,
|
ElCard,
|
||||||
ElButton,
|
ElButton,
|
||||||
ElIcon,
|
ElIcon,
|
||||||
ElTableColumn,
|
|
||||||
ElTag,
|
|
||||||
ElDialog,
|
ElDialog,
|
||||||
ElSelect,
|
ElSelect,
|
||||||
ElOption,
|
ElOption,
|
||||||
ElInputNumber,
|
|
||||||
ElForm,
|
ElForm,
|
||||||
ElFormItem,
|
ElFormItem,
|
||||||
ElMessage,
|
ElMessage,
|
||||||
FormInstance,
|
|
||||||
FormRules
|
FormRules
|
||||||
} from 'element-plus'
|
} from 'element-plus'
|
||||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||||
@@ -283,9 +150,13 @@
|
|||||||
} from '@/types/api'
|
} from '@/types/api'
|
||||||
import { useAuth } from '@/composables/useAuth'
|
import { useAuth } from '@/composables/useAuth'
|
||||||
import { JULY_PERMISSIONS } from '@/config/constants'
|
import { JULY_PERMISSIONS } from '@/config/constants'
|
||||||
|
import SeriesGrantPackageTable from '@/components/business/SeriesGrantPackageTable.vue'
|
||||||
|
import SeriesGrantPackageDialog from '@/components/business/SeriesGrantPackageDialog.vue'
|
||||||
import {
|
import {
|
||||||
mergeGrantPackageCandidates,
|
mergeGrantPackageCandidates,
|
||||||
type GrantPackageCandidate
|
type GrantPackageCandidate,
|
||||||
|
type SeriesGrantPackageForm,
|
||||||
|
type PackageCostFormItem
|
||||||
} from '@/utils/business/seriesGrantPackage'
|
} from '@/utils/business/seriesGrantPackage'
|
||||||
|
|
||||||
defineOptions({ name: 'SeriesGrantPackages' })
|
defineOptions({ name: 'SeriesGrantPackages' })
|
||||||
@@ -298,15 +169,11 @@
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const submitLoading = ref(false)
|
const submitLoading = ref(false)
|
||||||
const packageLoading = ref(false)
|
const packageLoading = ref(false)
|
||||||
const tableRef = ref()
|
|
||||||
|
|
||||||
const grantId = computed(() => Number(route.params.id))
|
const grantId = computed(() => Number(route.params.id))
|
||||||
const seriesId = ref<number>(0)
|
const seriesId = ref<number>(0)
|
||||||
const grantName = ref<string>('')
|
const grantName = ref<string>('')
|
||||||
const packageList = ref<GrantPackageInfo[]>([])
|
const packageList = ref<GrantPackageInfo[]>([])
|
||||||
const availablePackages = ref<GrantPackageCandidate[]>([])
|
const availablePackages = ref<GrantPackageCandidate[]>([])
|
||||||
const packageFormRef = ref<FormInstance>()
|
|
||||||
|
|
||||||
const packageDialogVisible = ref(false)
|
const packageDialogVisible = ref(false)
|
||||||
const packageDialogType = ref<'add' | 'edit'>('add')
|
const packageDialogType = ref<'add' | 'edit'>('add')
|
||||||
const expiryDialogVisible = ref(false)
|
const expiryDialogVisible = ref(false)
|
||||||
@@ -314,32 +181,6 @@
|
|||||||
|
|
||||||
type ExpiryBaseSelection = PackageAllocationExpiryBaseOverride | 'default'
|
type ExpiryBaseSelection = PackageAllocationExpiryBaseOverride | 'default'
|
||||||
|
|
||||||
type PackageCostFormItem = {
|
|
||||||
package_id: number
|
|
||||||
package_name?: string
|
|
||||||
package_code?: string
|
|
||||||
cost_price_yuan: number
|
|
||||||
original_cost_price?: number
|
|
||||||
suggested_retail_price?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
type PackageFormState = {
|
|
||||||
package_id?: number
|
|
||||||
allocation_id?: number
|
|
||||||
package_ids: number[]
|
|
||||||
package_name?: string
|
|
||||||
package_code?: string
|
|
||||||
cost_price_yuan: number
|
|
||||||
original_cost_price?: number
|
|
||||||
suggested_retail_price?: number
|
|
||||||
expiry_base_override: ExpiryBaseSelection
|
|
||||||
initial_expiry_base_override: ExpiryBaseSelection
|
|
||||||
default_expiry_base_name?: string | null
|
|
||||||
expiry_base_override_name?: string | null
|
|
||||||
effective_expiry_base_name?: string | null
|
|
||||||
packages: PackageCostFormItem[]
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExpiryFormState = {
|
type ExpiryFormState = {
|
||||||
package_id?: number
|
package_id?: number
|
||||||
allocation_id?: number
|
allocation_id?: number
|
||||||
@@ -351,7 +192,7 @@
|
|||||||
effective_expiry_base_name?: string | null
|
effective_expiry_base_name?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const createDefaultPackageForm = (): PackageFormState => ({
|
const createDefaultPackageForm = (): SeriesGrantPackageForm => ({
|
||||||
package_id: undefined,
|
package_id: undefined,
|
||||||
allocation_id: undefined,
|
allocation_id: undefined,
|
||||||
package_ids: [],
|
package_ids: [],
|
||||||
@@ -362,10 +203,10 @@
|
|||||||
initial_expiry_base_override: 'default',
|
initial_expiry_base_override: 'default',
|
||||||
packages: []
|
packages: []
|
||||||
})
|
})
|
||||||
const packageForm = ref<PackageFormState>(createDefaultPackageForm())
|
const packageForm = ref<SeriesGrantPackageForm>(createDefaultPackageForm())
|
||||||
const expiryForm = ref<ExpiryFormState>({ expiry_base_override: 'default' })
|
const expiryForm = ref<ExpiryFormState>({ expiry_base_override: 'default' })
|
||||||
|
|
||||||
const assignPackageForm = (values: Partial<PackageFormState> = {}) => {
|
const assignPackageForm = (values: Partial<SeriesGrantPackageForm> = {}) => {
|
||||||
Object.assign(packageForm.value, createDefaultPackageForm(), values)
|
Object.assign(packageForm.value, createDefaultPackageForm(), values)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,36 +267,6 @@
|
|||||||
]
|
]
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const getActions = (row: GrantPackageInfo) => {
|
|
||||||
const actions: any[] = []
|
|
||||||
|
|
||||||
if (canUpdateExpiryBase) {
|
|
||||||
actions.push({
|
|
||||||
label: '修改生效条件',
|
|
||||||
handler: () => showExpiryBaseDialog(row),
|
|
||||||
type: 'primary'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasAuth('series_grants:edit_packages_list')) {
|
|
||||||
actions.push({
|
|
||||||
label: '编辑',
|
|
||||||
handler: () => showEditPackageDialog(row),
|
|
||||||
type: 'primary'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasAuth('series_grants:delete_packages_list')) {
|
|
||||||
actions.push({
|
|
||||||
label: '删除',
|
|
||||||
handler: () => handleDeletePackage(row),
|
|
||||||
type: 'danger'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return actions
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回上一页
|
// 返回上一页
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
router.back()
|
router.back()
|
||||||
@@ -484,7 +295,6 @@
|
|||||||
assignPackageForm({ package_ids: [], packages: [] })
|
assignPackageForm({ package_ids: [], packages: [] })
|
||||||
loadAvailablePackages()
|
loadAvailablePackages()
|
||||||
packageDialogVisible.value = true
|
packageDialogVisible.value = true
|
||||||
nextTick(() => packageFormRef.value?.clearValidate())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const showEditPackageDialog = (row: GrantPackageInfo) => {
|
const showEditPackageDialog = (row: GrantPackageInfo) => {
|
||||||
@@ -506,7 +316,6 @@
|
|||||||
effective_expiry_base_name: row.effective_expiry_base_name
|
effective_expiry_base_name: row.effective_expiry_base_name
|
||||||
})
|
})
|
||||||
packageDialogVisible.value = true
|
packageDialogVisible.value = true
|
||||||
nextTick(() => packageFormRef.value?.clearValidate())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const showExpiryBaseDialog = (row: GrantPackageInfo) => {
|
const showExpiryBaseDialog = (row: GrantPackageInfo) => {
|
||||||
@@ -605,41 +414,11 @@
|
|||||||
{ deep: true }
|
{ deep: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
|
||||||
() => packageForm.value.package_id,
|
|
||||||
(packageId) => {
|
|
||||||
if (packageDialogType.value === 'add' && packageId) {
|
|
||||||
const selectedPackage = availablePackages.value.find((p) => p.id === packageId)
|
|
||||||
if (selectedPackage) {
|
|
||||||
const originalCostPrice = selectedPackage.cost_price
|
|
||||||
? selectedPackage.cost_price / 100
|
|
||||||
: 0
|
|
||||||
const suggestedRetailPrice =
|
|
||||||
selectedPackage.suggested_retail_price !== undefined &&
|
|
||||||
selectedPackage.suggested_retail_price !== null
|
|
||||||
? selectedPackage.suggested_retail_price / 100
|
|
||||||
: undefined
|
|
||||||
packageForm.value.original_cost_price = originalCostPrice
|
|
||||||
packageForm.value.suggested_retail_price = suggestedRetailPrice
|
|
||||||
packageForm.value.cost_price_yuan = originalCostPrice
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleSavePackage = async () => {
|
const handleSavePackage = async () => {
|
||||||
if (!packageFormRef.value || !grantId.value) return
|
if (!grantId.value) return
|
||||||
|
|
||||||
await packageFormRef.value.validate(async (valid) => {
|
|
||||||
if (!valid) return
|
|
||||||
|
|
||||||
submitLoading.value = true
|
submitLoading.value = true
|
||||||
try {
|
try {
|
||||||
const currentCostPriceYuan = Number(packageForm.value.cost_price_yuan)
|
|
||||||
if (Number.isNaN(currentCostPriceYuan)) {
|
|
||||||
throw new Error('当前成本价无效,无法保存')
|
|
||||||
}
|
|
||||||
const costPrice = Math.round(currentCostPriceYuan * 100)
|
|
||||||
if (packageDialogType.value === 'add') {
|
if (packageDialogType.value === 'add') {
|
||||||
if (packageForm.value.packages.length === 0) {
|
if (packageForm.value.packages.length === 0) {
|
||||||
ElMessage.error('请至少选择一个套餐')
|
ElMessage.error('请至少选择一个套餐')
|
||||||
@@ -656,10 +435,7 @@
|
|||||||
`套餐 ${pkg.package_name || pkg.package_id} 的成本价不能高于 ¥${maxCostPrice.toFixed(2)}`
|
`套餐 ${pkg.package_name || pkg.package_id} 的成本价不能高于 ¥${maxCostPrice.toFixed(2)}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (
|
if (pkg.original_cost_price !== undefined && currentCostPrice < pkg.original_cost_price) {
|
||||||
pkg.original_cost_price !== undefined &&
|
|
||||||
currentCostPrice < pkg.original_cost_price
|
|
||||||
) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`套餐 ${pkg.package_name || pkg.package_id} 的成本价不能低于 ¥${pkg.original_cost_price.toFixed(2)}`
|
`套餐 ${pkg.package_name || pkg.package_id} 的成本价不能低于 ¥${pkg.original_cost_price.toFixed(2)}`
|
||||||
)
|
)
|
||||||
@@ -677,9 +453,13 @@
|
|||||||
packages
|
packages
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
const currentCostPriceYuan = Number(packageForm.value.cost_price_yuan)
|
||||||
|
if (Number.isNaN(currentCostPriceYuan)) {
|
||||||
|
throw new Error('当前成本价无效,无法保存')
|
||||||
|
}
|
||||||
|
const costPrice = Math.round(currentCostPriceYuan * 100)
|
||||||
const expiryBaseChanged =
|
const expiryBaseChanged =
|
||||||
packageForm.value.expiry_base_override !==
|
packageForm.value.expiry_base_override !== packageForm.value.initial_expiry_base_override
|
||||||
packageForm.value.initial_expiry_base_override
|
|
||||||
if (
|
if (
|
||||||
expiryBaseChanged &&
|
expiryBaseChanged &&
|
||||||
(packageForm.value.allocation_id === undefined ||
|
(packageForm.value.allocation_id === undefined ||
|
||||||
@@ -718,7 +498,6 @@
|
|||||||
} finally {
|
} finally {
|
||||||
submitLoading.value = false
|
submitLoading.value = false
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveExpiryBase = async () => {
|
const saveExpiryBase = async () => {
|
||||||
@@ -779,7 +558,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handlePackageDialogClosed = () => {
|
const handlePackageDialogClosed = () => {
|
||||||
packageFormRef.value?.clearValidate()
|
|
||||||
assignPackageForm()
|
assignPackageForm()
|
||||||
availablePackages.value = []
|
availablePackages.value = []
|
||||||
}
|
}
|
||||||
@@ -827,9 +605,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.amount-value {
|
.package-table-actions {
|
||||||
font-weight: 600;
|
display: flex;
|
||||||
color: var(--el-color-warning);
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-tip {
|
.form-tip {
|
||||||
@@ -838,51 +618,15 @@
|
|||||||
color: var(--el-text-color-secondary);
|
color: var(--el-text-color-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.package-config-list {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.package-config-item {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.package-config-name,
|
|
||||||
.ellipsis-value {
|
.ellipsis-value {
|
||||||
|
display: block;
|
||||||
|
width: 260px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.package-config-name {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.package-config-cost {
|
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.package-config-field-label {
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
font-size: 13px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ellipsis-value {
|
|
||||||
display: block;
|
|
||||||
width: 260px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-footer {
|
.dialog-footer {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,10 +40,10 @@
|
|||||||
<ElDialog
|
<ElDialog
|
||||||
v-model="dialogVisible"
|
v-model="dialogVisible"
|
||||||
title="编辑系统配置"
|
title="编辑系统配置"
|
||||||
width="560px"
|
width="30%"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
>
|
>
|
||||||
<ElForm label-width="120px">
|
<ElForm label-width="80px">
|
||||||
<ElFormItem label="配置说明">
|
<ElFormItem label="配置说明">
|
||||||
<span>{{ currentConfig?.description || '-' }}</span>
|
<span>{{ currentConfig?.description || '-' }}</span>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
@@ -90,7 +90,14 @@
|
|||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||||
<ElButton type="primary" :loading="submitLoading" @click="handleSubmit"> 保存 </ElButton>
|
<ElButton
|
||||||
|
type="primary"
|
||||||
|
:loading="submitLoading"
|
||||||
|
:disabled="submitLoading"
|
||||||
|
@click="handleSubmit"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</ElButton>
|
||||||
</template>
|
</template>
|
||||||
</ElDialog>
|
</ElDialog>
|
||||||
</div>
|
</div>
|
||||||
@@ -111,6 +118,7 @@
|
|||||||
type SystemConfigModule
|
type SystemConfigModule
|
||||||
} from '@/types/api/systemConfig'
|
} from '@/types/api/systemConfig'
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
|
import { normalizeApiError } from '@/utils/business/apiError'
|
||||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||||
import { JULY_PERMISSIONS } from '@/config/constants'
|
import { JULY_PERMISSIONS } from '@/config/constants'
|
||||||
import { useAuth } from '@/composables/useAuth'
|
import { useAuth } from '@/composables/useAuth'
|
||||||
@@ -258,7 +266,8 @@
|
|||||||
const validateValue = (value: string) => {
|
const validateValue = (value: string) => {
|
||||||
const config = currentConfig.value
|
const config = currentConfig.value
|
||||||
if (!config) return '配置不存在'
|
if (!config) return '配置不存在'
|
||||||
if (config.enum_values.length > 0 && !config.enum_values.includes(value)) {
|
const enumValues = config.enum_values ?? []
|
||||||
|
if (enumValues.length > 0 && !enumValues.includes(value)) {
|
||||||
return '配置值不在允许的枚举范围内'
|
return '配置值不在允许的枚举范围内'
|
||||||
}
|
}
|
||||||
if (config.value_type === 'int') {
|
if (config.value_type === 'int') {
|
||||||
@@ -324,7 +333,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!currentConfig.value || currentConfig.value.readonly) return
|
if (!currentConfig.value) {
|
||||||
|
ElMessage.error('未选择要编辑的系统配置')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (currentConfig.value.readonly) {
|
||||||
|
ElMessage.warning('当前配置为只读配置,不能保存')
|
||||||
|
return
|
||||||
|
}
|
||||||
const value = getSubmittedValue()
|
const value = getSubmittedValue()
|
||||||
if (currentConfig.value.sensitive && value === originalValue.value) {
|
if (currentConfig.value.sensitive && value === originalValue.value) {
|
||||||
ElMessage.warning('敏感配置未输入新值,无需保存')
|
ElMessage.warning('敏感配置未输入新值,无需保存')
|
||||||
@@ -351,6 +367,7 @@
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('更新系统配置失败:', error)
|
console.error('更新系统配置失败:', error)
|
||||||
|
ElMessage.error(normalizeApiError(error).message)
|
||||||
} finally {
|
} finally {
|
||||||
submitLoading.value = false
|
submitLoading.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -303,6 +303,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ElDialog>
|
</ElDialog>
|
||||||
|
|
||||||
|
<ShopCreditLimitDialog
|
||||||
|
v-model="creditDialogVisible"
|
||||||
|
:shop="currentCreditShop"
|
||||||
|
@submitted="getShopList"
|
||||||
|
/>
|
||||||
</ElCard>
|
</ElCard>
|
||||||
</div>
|
</div>
|
||||||
</ArtTableFullScreen>
|
</ArtTableFullScreen>
|
||||||
@@ -326,7 +332,8 @@
|
|||||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||||
import { useAuth } from '@/composables/useAuth'
|
import { useAuth } from '@/composables/useAuth'
|
||||||
import { generateShopCode } from '@/utils/codeGenerator'
|
import { generateShopCode } from '@/utils/codeGenerator'
|
||||||
import { ShopService, RoleService } from '@/api/modules'
|
import { CommissionService, ShopService, RoleService } from '@/api/modules'
|
||||||
|
import ShopCreditLimitDialog from '@/components/business/ShopCreditLimitDialog.vue'
|
||||||
import type { SearchFormItem } from '@/types'
|
import type { SearchFormItem } from '@/types'
|
||||||
import type {
|
import type {
|
||||||
CreateShopParams,
|
CreateShopParams,
|
||||||
@@ -335,6 +342,7 @@
|
|||||||
ShopRoleResponse,
|
ShopRoleResponse,
|
||||||
UpdateShopParams
|
UpdateShopParams
|
||||||
} from '@/types/api'
|
} from '@/types/api'
|
||||||
|
import type { ShopFundSummaryItem } from '@/types/api/commission'
|
||||||
import { RoleType, RoleStatus } from '@/types/api'
|
import { RoleType, RoleStatus } from '@/types/api'
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
import { getCompatibleNumericId } from '@/utils/business/id'
|
import { getCompatibleNumericId } from '@/utils/business/id'
|
||||||
@@ -356,6 +364,9 @@
|
|||||||
const defaultRoleList = ref<any[]>([])
|
const defaultRoleList = ref<any[]>([])
|
||||||
const salespersonLoading = ref(false)
|
const salespersonLoading = ref(false)
|
||||||
const salespersonOptions = ref<ShopBusinessOwnerCandidate[]>([])
|
const salespersonOptions = ref<ShopBusinessOwnerCandidate[]>([])
|
||||||
|
const creditDialogVisible = ref(false)
|
||||||
|
const currentCreditShop = ref<ShopFundSummaryItem | null>(null)
|
||||||
|
const creditLoadingShopId = ref<number | null>(null)
|
||||||
|
|
||||||
// 级联选择相关
|
// 级联选择相关
|
||||||
const parentShopCascadeOptions = ref<any[]>([])
|
const parentShopCascadeOptions = ref<any[]>([])
|
||||||
@@ -803,6 +814,14 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasAuth('shop:credit_limit_manage')) {
|
||||||
|
actions.push({
|
||||||
|
label: '调整实际信用额度',
|
||||||
|
handler: () => showCreditDialog(row),
|
||||||
|
type: 'primary'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (hasAuth('shop:edit')) {
|
if (hasAuth('shop:edit')) {
|
||||||
actions.push({
|
actions.push({
|
||||||
label: '编辑',
|
label: '编辑',
|
||||||
@@ -822,6 +841,34 @@
|
|||||||
return actions
|
return actions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showCreditDialog = async (row: ShopResponse) => {
|
||||||
|
if (creditLoadingShopId.value === row.id) return
|
||||||
|
|
||||||
|
creditLoadingShopId.value = row.id
|
||||||
|
try {
|
||||||
|
const res = await CommissionService.getShopFundSummary({
|
||||||
|
page: 1,
|
||||||
|
page_size: 100,
|
||||||
|
shop_name: row.shop_name
|
||||||
|
})
|
||||||
|
const summary =
|
||||||
|
res.code === 0 ? (res.data.items || []).find((item) => item.shop_id === row.id) : null
|
||||||
|
|
||||||
|
if (!summary) {
|
||||||
|
ElMessage.warning('未获取到该店铺的资金概况,请刷新店铺列表后重试')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentCreditShop.value = summary
|
||||||
|
creditDialogVisible.value = true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取店铺资金概况失败:', error)
|
||||||
|
ElMessage.error('获取店铺资金概况失败,请稍后重试')
|
||||||
|
} finally {
|
||||||
|
creditLoadingShopId.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 表单实例
|
// 表单实例
|
||||||
const formRef = ref<FormInstance>()
|
const formRef = ref<FormInstance>()
|
||||||
|
|
||||||
|
|||||||
@@ -21,10 +21,10 @@
|
|||||||
<ElDescriptionsItem label="状态">
|
<ElDescriptionsItem label="状态">
|
||||||
{{ role.status === 1 ? '启用' : '禁用' }}
|
{{ role.status === 1 ? '启用' : '禁用' }}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="新建代理默认信用">
|
<ElDescriptionsItem label="代理默认信用">
|
||||||
{{ role.default_credit_enabled ? '启用' : '未启用' }}
|
{{ role.default_credit_enabled ? '启用' : '未启用' }}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="默认信用额度">
|
<ElDescriptionsItem label="默认信用额度(元)">
|
||||||
{{ formatCredit(role.default_credit_limit) }}
|
{{ formatCredit(role.default_credit_limit) }}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="信用生效范围">
|
<ElDescriptionsItem label="信用生效范围">
|
||||||
@@ -60,7 +60,10 @@
|
|||||||
|
|
||||||
const formatCredit = (value?: number) => {
|
const formatCredit = (value?: number) => {
|
||||||
if (value === undefined || value === null) return '-'
|
if (value === undefined || value === null) return '-'
|
||||||
return `${(value / 100).toFixed(2)} 元`
|
return (value / 100).toLocaleString('zh-CN', {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleBack = () => router.back()
|
const handleBack = () => router.back()
|
||||||
|
|||||||
@@ -88,6 +88,7 @@
|
|||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="默认信用额度">
|
<ElFormItem label="默认信用额度">
|
||||||
|
<div class="credit-form-unit">单位:元</div>
|
||||||
<ElInputNumber
|
<ElInputNumber
|
||||||
v-model="form.credit_limit_yuan"
|
v-model="form.credit_limit_yuan"
|
||||||
:disabled="!form.credit_enabled"
|
:disabled="!form.credit_enabled"
|
||||||
@@ -97,8 +98,10 @@
|
|||||||
controls-position="right"
|
controls-position="right"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
placeholder="请输入默认信用额度"
|
placeholder="请输入默认信用额度"
|
||||||
|
:formatter="formatCreditInput"
|
||||||
|
:parser="parseCreditInput"
|
||||||
/>
|
/>
|
||||||
<div class="credit-form-tip">单位:元。修改后不会影响已有店铺</div>
|
<div class="credit-form-tip">修改后不会影响已有店铺</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</template>
|
</template>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
@@ -349,8 +352,8 @@
|
|||||||
{ label: '角色名称', prop: 'role_name' },
|
{ label: '角色名称', prop: 'role_name' },
|
||||||
{ label: '角色描述', prop: 'role_desc' },
|
{ label: '角色描述', prop: 'role_desc' },
|
||||||
{ label: '角色类型', prop: 'role_type' },
|
{ label: '角色类型', prop: 'role_type' },
|
||||||
{ label: '新建代理默认信用', prop: 'default_credit_enabled' },
|
{ label: '代理默认信用', prop: 'default_credit_enabled' },
|
||||||
{ label: '默认信用额度', prop: 'default_credit_limit' },
|
{ label: '默认信用额度(元)', prop: 'default_credit_limit' },
|
||||||
...(canUpdateRoleStatus ? [{ label: '状态', prop: 'status' }] : []),
|
...(canUpdateRoleStatus ? [{ label: '状态', prop: 'status' }] : []),
|
||||||
{ label: '创建时间', prop: 'created_at' }
|
{ label: '创建时间', prop: 'created_at' }
|
||||||
]
|
]
|
||||||
@@ -414,6 +417,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatCreditInput = (value: string | number | undefined) => {
|
||||||
|
if (value === undefined || value === null || value === '') return ''
|
||||||
|
|
||||||
|
const normalizedValue = String(value).replace(/,/g, '')
|
||||||
|
const [integerPart, decimalPart] = normalizedValue.split('.')
|
||||||
|
const formattedIntegerPart = (integerPart || '0').replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||||
|
|
||||||
|
return decimalPart === undefined
|
||||||
|
? formattedIntegerPart
|
||||||
|
: `${formattedIntegerPart}.${decimalPart}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseCreditInput = (value: string) => value.replace(/,/g, '')
|
||||||
|
|
||||||
|
const formatCreditYuan = (value?: number | null) => {
|
||||||
|
if (value === undefined || value === null) return '-'
|
||||||
|
|
||||||
|
return fenToYuan(value).toLocaleString('zh-CN', {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const roleList = ref<PlatformRole[]>([])
|
const roleList = ref<PlatformRole[]>([])
|
||||||
|
|
||||||
const getRoleId = (role: PlatformRole | any) => getCompatibleNumericId(role) ?? 0
|
const getRoleId = (role: PlatformRole | any) => getCompatibleNumericId(role) ?? 0
|
||||||
@@ -463,7 +489,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'default_credit_enabled',
|
prop: 'default_credit_enabled',
|
||||||
label: '新建代理默认信用',
|
label: '代理默认信用',
|
||||||
width: 150,
|
width: 150,
|
||||||
formatter: (row: PlatformRole) => {
|
formatter: (row: PlatformRole) => {
|
||||||
if (row.role_type !== RoleType.CUSTOMER) return '-'
|
if (row.role_type !== RoleType.CUSTOMER) return '-'
|
||||||
@@ -474,11 +500,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'default_credit_limit',
|
prop: 'default_credit_limit',
|
||||||
label: '默认信用额度',
|
label: '默认信用额度(元)',
|
||||||
width: 140,
|
width: 160,
|
||||||
formatter: (row: PlatformRole) => {
|
formatter: (row: PlatformRole) => {
|
||||||
if (row.role_type !== RoleType.CUSTOMER || !row.default_credit_enabled) return '-'
|
if (row.role_type !== RoleType.CUSTOMER || !row.default_credit_enabled) return '-'
|
||||||
return `${fenToYuan(row.default_credit_limit)} 元`
|
return formatCreditYuan(row.default_credit_limit)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
...(canUpdateRoleStatus
|
...(canUpdateRoleStatus
|
||||||
@@ -1267,6 +1293,13 @@
|
|||||||
color: var(--el-text-color-secondary);
|
color: var(--el-text-color-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.credit-form-unit {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
.permission-tree-transfer-container {
|
.permission-tree-transfer-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
|||||||
Reference in New Issue
Block a user