From 17d2eeebc5727c6dd9fd67bbc987482b372af843 Mon Sep 17 00:00:00 2001 From: luo Date: Wed, 29 Jul 2026 18:20:42 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=9B=9E=E8=B0=83=E9=85=8D=E7=BD=AE,=20?= =?UTF-8?q?=E8=B0=83=E6=95=B4=E4=BF=A1=E7=94=A8=E4=BD=8D=E7=BD=AE,=20?= =?UTF-8?q?=E5=A5=97=E9=A4=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../design.md | 11 +- .../proposal.md | 6 +- .../spec.md | 126 ++++ .../tasks.md | 12 +- scripts/tests/agentRechargeActions.test.mjs | 28 +- scripts/tests/agentRechargeOnline.test.mjs | 72 ++ scripts/tests/agentRechargeService.test.mjs | 21 + src/api/modules/agentRecharge.ts | 20 + .../business/SeriesGrantPackageDialog.vue | 290 ++++++++ .../business/SeriesGrantPackageTable.vue | 80 +++ .../business/ShopCreditLimitDialog.vue | 242 +++++++ src/router/routes/asyncRoutes.ts | 11 - src/router/routesAlias.ts | 1 - src/types/api/account.ts | 4 + src/types/api/agentRecharge.ts | 58 +- src/types/api/systemConfig.ts | 2 +- src/types/components.d.ts | 3 + src/utils/business/seriesGrantPackage.ts | 34 +- .../account-management/account/index.vue | 46 +- .../components/dialogs/SpeedLimitDialog.vue | 10 +- .../asset-management/device-list/index.vue | 20 + .../exchange-management/index.vue | 221 ++++-- .../authorization-records/detail.vue | 140 ---- .../authorization-records/index.vue | 117 +--- .../task-management/bulk-purchase/index.vue | 4 +- .../device-batch-allocation/index.vue | 4 +- .../agent-fund-overview/index.vue | 243 +------ src/views/common/account-list.vue | 28 +- .../agent-recharge/agentRechargeActions.ts | 1 + .../agent-recharge/agentRechargeOnline.ts | 23 + src/views/finance/agent-recharge/detail.vue | 275 ++++---- src/views/finance/agent-recharge/index.vue | 639 +++++++++++++++--- .../package-management/package-list/index.vue | 24 +- .../series-grants/detail.vue | 575 +++++++++------- .../series-grants/packages.vue | 532 ++++----------- src/views/settings/system-configs/index.vue | 27 +- src/views/shop-management/list/index.vue | 213 +++--- src/views/system/role/detail/index.vue | 9 +- src/views/system/role/index.vue | 47 +- 39 files changed, 2687 insertions(+), 1532 deletions(-) create mode 100644 scripts/tests/agentRechargeOnline.test.mjs create mode 100644 src/components/business/SeriesGrantPackageDialog.vue create mode 100644 src/components/business/SeriesGrantPackageTable.vue create mode 100644 src/components/business/ShopCreditLimitDialog.vue delete mode 100644 src/views/asset-management/record-management/authorization-records/detail.vue create mode 100644 src/views/finance/agent-recharge/agentRechargeOnline.ts diff --git a/openspec/changes/update-july-iteration-frontend-integration/design.md b/openspec/changes/update-july-iteration-frontend-integration/design.md index ef7d3bc..9ba8a59 100644 --- a/openspec/changes/update-july-iteration-frontend-integration/design.md +++ b/openspec/changes/update-july-iteration-frontend-integration/design.md @@ -20,10 +20,19 @@ - Decision: `docs/所需接口文档` 是本变更的接口契约索引;页面 API 类型、请求参数、枚举和响应字段必须以对应模块文档为准,不从七月说明中自行扩展路径。 - Decision: 文件类业务统一采用 `POST /api/admin/storage/upload-url` 获取预签名 URL,直传成功后仅把 `file_key` 传给批量订购或设备分配接口。 - 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 - 接口 DTO 若未按说明返回,页面无法可靠展示审批人、历史资产标识或新旧字段;通过联调清单阻断实现前确认。 +- 支付配置为空或二维码内容为空时,在线充值无法继续;通过动态支付方式加载、空状态和联调环境中微信/支付宝二维码内容非空校验阻断上线。 +- 在线创建请求若错误生成新的 `request_id`,可能产生重复充值单;通过提交态保存 ID、幂等重试和冲突保护降低重复支付风险。 +- `status=2` 只代表第三方已支付,不代表钱包已经入账;通过以 `status=3` 作为余额刷新条件避免提前展示到账。 - 设备批量分配接口文档已明确任务列表和详情仅平台用户可操作,前端不得向代理账号展示任务入口或通过前端绕过权限。 - H5/C 端不在本次实现范围,后台只能依赖后端提供的最终字段,跨端验收需要单独安排。 - 部分接口文档只给出字段模型,缺少业务错误码、权限编码和分页默认值;这些列入待确认清单。 @@ -38,5 +47,5 @@ ## Open Questions - 退款和代理充值详情是否需要直接返回企微审批节点人员列表? -- 各新增后台页面和按钮的最终权限编码是什么? +- 代理在线充值创建和查询的最终权限编码是否与现有 `agent_recharge:*` 权限保持一致;导出权限编码已由接口文档确定。 - 批量任务、导出任务、企微审批和 Gateway 超时的稳定错误码及重试语义是什么? diff --git a/openspec/changes/update-july-iteration-frontend-integration/proposal.md b/openspec/changes/update-july-iteration-frontend-integration/proposal.md index 460086e..d753c38 100644 --- a/openspec/changes/update-july-iteration-frontend-integration/proposal.md +++ b/openspec/changes/update-july-iteration-frontend-integration/proposal.md @@ -7,9 +7,13 @@ ## What Changes - 按 `00-七月迭代实现与接口对接说明.md` 更新后台管理端的接口调用和页面展示契约。 +- 按 `产品迭代7月份/000-代理钱包扫码充值接口对接说明.md` 和 `所需接口文档/000-代理钱包扫码充值接口对接.md` 补齐代理钱包在线扫码充值、线下代充和充值导出权限的前端契约。 - 以 `docs/所需接口文档/01-店铺.md` 至 `21-设备批量分配.md` 为接口参数、响应字段、枚举、权限和错误语义的具体来源。 - 对资产、店铺、订单、换货、套餐、资金、系列授权、审批、通知、批量任务、导出和 Gateway 相关页面补齐前端适配要求。 - 统一使用后端返回的实名策略、支付方式、预计到期、审批状态、金额计算结果和任务状态,不在前端重复推导。 +- 代理在线充值只提交金额、动态可用支付方式和幂等 `request_id`,用后端返回的原始 `qr_content` 渲染二维码,并按充值状态轮询到帐结果。 +- 按 `recharge_source` 区分代理在线自充与平台线下代充;线下单展示凭证和审批状态,在线单隐藏线下字段和人工审批操作。 +- 补齐代理充值列表导出入口、导出任务详情和下载的权限编码。 - 补齐 CSV 上传模板、异步任务恢复、权限按钮、错误处理和并发冲突处理要求。 - 明确预签名上传先取 `file_key`、再调用批量业务接口的两阶段流程,并固定批量订购、设备分配和导出任务的终态处理规则。 - **不实施 H5/C 端页面改动**,但在规格和联调清单中标注其依赖的接口及外部工作项。 @@ -18,6 +22,6 @@ ## Impact - 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 端资产展示 - Approval gate: 接口待确认项完成前不进入大范围实现 diff --git a/openspec/changes/update-july-iteration-frontend-integration/specs/july-iteration-frontend-integration/spec.md b/openspec/changes/update-july-iteration-frontend-integration/specs/july-iteration-frontend-integration/spec.md index 230db79..929d601 100644 --- a/openspec/changes/update-july-iteration-frontend-integration/specs/july-iteration-frontend-integration/spec.md +++ b/openspec/changes/update-july-iteration-frontend-integration/specs/july-iteration-frontend-integration/spec.md @@ -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 - **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 diff --git a/openspec/changes/update-july-iteration-frontend-integration/tasks.md b/openspec/changes/update-july-iteration-frontend-integration/tasks.md index ddf1789..f224fc7 100644 --- a/openspec/changes/update-july-iteration-frontend-integration/tasks.md +++ b/openspec/changes/update-july-iteration-frontend-integration/tasks.md @@ -54,7 +54,17 @@ - [x] 6.3 验证代理数据隔离、平台权限、敏感字段脱敏和财务操作审计入口。 - [ ] 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 首页隐藏设备下 ICCID 仍按最新说明标记为不做。 diff --git a/scripts/tests/agentRechargeActions.test.mjs b/scripts/tests/agentRechargeActions.test.mjs index d910d68..ea63813 100644 --- a/scripts/tests/agentRechargeActions.test.mjs +++ b/scripts/tests/agentRechargeActions.test.mjs @@ -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 { buildAgentRechargeActions } = module const row = { id: 42, status: 1, - payment_method: 'wechat', + payment_method: 'offline', payment_voucher_key: [] } const handledRows = [] @@ -108,3 +108,27 @@ test('non-pending agent recharge does not expose reject action', async () => { 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() + } +}) diff --git a/scripts/tests/agentRechargeOnline.test.mjs b/scripts/tests/agentRechargeOnline.test.mjs new file mode 100644 index 0000000..bee89b5 --- /dev/null +++ b/scripts/tests/agentRechargeOnline.test.mjs @@ -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() + } +}) diff --git a/scripts/tests/agentRechargeService.test.mjs b/scripts/tests/agentRechargeService.test.mjs index 0179c7f..6010096 100644 --- a/scripts/tests/agentRechargeService.test.mjs +++ b/scripts/tests/agentRechargeService.test.mjs @@ -20,6 +20,17 @@ test('agent recharge rejection posts the rejection reason to the order rejection post(options) { calls.push(options) 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, { rejection_reason: '支付凭证不符合要求' }) + await AgentRechargeService.getPaymentMethods() + await AgentRechargeService.getPaymentStatus(42) assert.deepEqual(calls, [ { @@ -60,6 +73,14 @@ test('agent recharge rejection posts the rejection reason to the order rejection data: { rejection_reason: '支付凭证不符合要求' } + }, + { + url: '/api/admin/agent-recharges/payment-methods', + params: undefined + }, + { + url: '/api/admin/agent-recharges/42/payment-status', + params: undefined } ]) } finally { diff --git a/src/api/modules/agentRecharge.ts b/src/api/modules/agentRecharge.ts index 84413ca..84e87b9 100644 --- a/src/api/modules/agentRecharge.ts +++ b/src/api/modules/agentRecharge.ts @@ -8,12 +8,23 @@ import type { AgentRechargeQueryParams, AgentRechargeListResponse, CreateAgentRechargeRequest, + AgentRechargePaymentMethods, + AgentRechargePaymentStatusResponse, ConfirmOfflinePaymentRequest, RejectAgentRechargeRequest, BaseResponse } from '@/types/api' export class AgentRechargeService extends BaseService { + /** + * 获取代理在线充值可用支付方式和金额限制 + */ + static getPaymentMethods(): Promise> { + return this.get>( + '/api/admin/agent-recharges/payment-methods' + ) + } + /** * 获取代理充值订单列表 * @param params 查询参数 @@ -32,6 +43,15 @@ export class AgentRechargeService extends BaseService { return this.getOne(`/api/admin/agent-recharges/${id}`) } + /** + * 查询在线充值支付及钱包到账状态 + */ + static getPaymentStatus(id: number): Promise> { + return this.getOne( + `/api/admin/agent-recharges/${id}/payment-status` + ) + } + /** * 创建代理充值订单 * @param data 创建充值订单请求参数 diff --git a/src/components/business/SeriesGrantPackageDialog.vue b/src/components/business/SeriesGrantPackageDialog.vue new file mode 100644 index 0000000..9a22a53 --- /dev/null +++ b/src/components/business/SeriesGrantPackageDialog.vue @@ -0,0 +1,290 @@ + + + + + diff --git a/src/components/business/SeriesGrantPackageTable.vue b/src/components/business/SeriesGrantPackageTable.vue new file mode 100644 index 0000000..327b651 --- /dev/null +++ b/src/components/business/SeriesGrantPackageTable.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/src/components/business/ShopCreditLimitDialog.vue b/src/components/business/ShopCreditLimitDialog.vue new file mode 100644 index 0000000..93a9b39 --- /dev/null +++ b/src/components/business/ShopCreditLimitDialog.vue @@ -0,0 +1,242 @@ + + + + + diff --git a/src/router/routes/asyncRoutes.ts b/src/router/routes/asyncRoutes.ts index cf38408..3094cf0 100644 --- a/src/router/routes/asyncRoutes.ts +++ b/src/router/routes/asyncRoutes.ts @@ -400,17 +400,6 @@ export const asyncRoutes: AppRouteRecord[] = [ 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', diff --git a/src/router/routesAlias.ts b/src/router/routesAlias.ts index 6e06647..fb73378 100644 --- a/src/router/routesAlias.ts +++ b/src/router/routesAlias.ts @@ -52,7 +52,6 @@ export enum RoutesAlias { // 记录管理 AuthorizationRecords = '/asset-management/record-management/authorization-records', // 授权记录 - AuthorizationRecordDetail = '/asset-management/record-management/authorization-records/detail', // 授权记录详情 AssetAssign = '/asset-management/record-management/asset-assign', // 分配记录 AssetAssignDetail = '/asset-management/record-management/asset-assign/detail', // 分配记录详情 diff --git a/src/types/api/account.ts b/src/types/api/account.ts index 4a0b34a..00ea592 100644 --- a/src/types/api/account.ts +++ b/src/types/api/account.ts @@ -26,6 +26,10 @@ export interface PlatformAccount { enterprise_name?: string // ⭐ 新增:企业名称 shop_id?: number | null // 关联店铺ID shop_name?: string // ⭐ 新增:店铺名称 + wecom_bound?: boolean // 是否已绑定企业微信账号 + wecom_corp_id?: string // 企业微信 CorpID + wecom_name?: string // 企业微信成员名称 + wecom_userid?: string // 企业微信成员 UserID status: AccountStatus // 状态 (0:禁用, 1:启用) } diff --git a/src/types/api/agentRecharge.ts b/src/types/api/agentRecharge.ts index 06165eb..e8b45e1 100644 --- a/src/types/api/agentRecharge.ts +++ b/src/types/api/agentRecharge.ts @@ -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 { @@ -31,7 +38,14 @@ export interface AgentRecharge { payment_method: AgentRechargePaymentMethod payment_channel: AgentRechargePaymentChannel 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 // 凭证附件列表;历史数据可能为单字符串或逗号字符串 rejection_reason?: string | null // 拒绝原因 remark?: string // 运营备注 @@ -57,6 +71,7 @@ export interface AgentRechargeQueryParams { page_size?: number shop_id?: number status?: AgentRechargeStatus + recharge_source?: AgentRechargeSource start_date?: string end_date?: string 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) - payment_method: AgentRechargePaymentMethod + payment_method: AgentRechargeOnlinePaymentMethod + request_id: string +} + +export interface CreateAgentRechargeOfflineRequest { + amount: number // 充值金额(单位:分),最小 1(即 ¥0.01) + payment_method: 'offline' shop_id: number - payment_voucher_key?: string[] // 线下支付凭证附件列表(payment_method=offline 时必填) + payment_voucher_key: string[] // 线下支付凭证附件列表(payment_method=offline 时必填) 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 { operation_password: string diff --git a/src/types/api/systemConfig.ts b/src/types/api/systemConfig.ts index 645484c..6fcf653 100644 --- a/src/types/api/systemConfig.ts +++ b/src/types/api/systemConfig.ts @@ -17,7 +17,7 @@ export interface SystemConfigItem { value_type: SystemConfigValueType description: string control: string - enum_values: string[] + enum_values?: string[] min: number | null max: number | null readonly: boolean diff --git a/src/types/components.d.ts b/src/types/components.d.ts index e898e2d..4e17e70 100644 --- a/src/types/components.d.ts +++ b/src/types/components.d.ts @@ -148,9 +148,12 @@ declare module 'vue' { RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] 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'] 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'] + ShopCreditLimitDialog: typeof import('./../components/business/ShopCreditLimitDialog.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'] SwitchCardDialog: typeof import('./../components/device/SwitchCardDialog.vue')['default'] diff --git a/src/utils/business/seriesGrantPackage.ts b/src/utils/business/seriesGrantPackage.ts index 7372c40..81fafed 100644 --- a/src/utils/business/seriesGrantPackage.ts +++ b/src/utils/business/seriesGrantPackage.ts @@ -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 { id: number diff --git a/src/views/account-management/account/index.vue b/src/views/account-management/account/index.vue index 32a91e9..cd25acd 100644 --- a/src/views/account-management/account/index.vue +++ b/src/views/account-management/account/index.vue @@ -512,6 +512,8 @@ { label: '账号类型', prop: 'user_type' }, { label: '店铺名称', prop: 'shop_name' }, { label: '企业名称', prop: 'enterprise_name' }, + { label: '企微绑定', prop: 'wecom_bound' }, + { label: '企微用户名称', prop: 'wecom_name' }, ...(canModifyAccountStatus ? [{ label: '状态', prop: 'status' }] : []), { label: '创建时间', prop: 'created_at' } ] @@ -620,6 +622,21 @@ 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 ? [ { @@ -688,7 +705,7 @@ return actions } - const showWecomBindingDialog = (row: any) => { + const showWecomBindingDialog = async (row: any) => { if (row.user_type !== 2) { ElMessage.warning('只有平台用户可以绑定企微账号') return @@ -699,7 +716,17 @@ wecomBindingForm.userid = '' wecomMembers.value = [] 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 () => { @@ -712,8 +739,8 @@ } } - const handleWecomApplicationChange = async (applicationId?: number) => { - wecomBindingForm.userid = '' + const handleWecomApplicationChange = async (applicationId?: number, selectedUserid = '') => { + wecomBindingForm.userid = selectedUserid wecomMembers.value = [] if (!applicationId) return @@ -723,7 +750,15 @@ page: 1, 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 { wecomMembersLoading.value = false } @@ -753,6 +788,7 @@ if (response.code === 0) { ElMessage.success('账号企微绑定成功') wecomBindingDialogVisible.value = false + await getAccountList() } } finally { wecomBindingSubmitting.value = false diff --git a/src/views/asset-management/asset-information/components/dialogs/SpeedLimitDialog.vue b/src/views/asset-management/asset-information/components/dialogs/SpeedLimitDialog.vue index 6697835..04a2785 100644 --- a/src/views/asset-management/asset-information/components/dialogs/SpeedLimitDialog.vue +++ b/src/views/asset-management/asset-information/components/dialogs/SpeedLimitDialog.vue @@ -11,6 +11,7 @@ (), { @@ -74,7 +77,7 @@ { code: 8, label: '100Mbps' } ] as const - const form = reactive({ code: 3 as (typeof speedTiers)[number]['code'] }) + const form = reactive<{ code: SpeedTierCode | undefined }>({ code: undefined }) const rules: FormRules = { code: [{ required: true, message: '请选择限速档位', trigger: 'change' }] @@ -97,7 +100,7 @@ (newVal) => { setLayoutScrollLock(newVal) if (newVal) { - form.code = 3 + form.code = undefined } }, { immediate: true } @@ -117,6 +120,7 @@ try { await formRef.value.validate() + if (form.code === undefined) return emit('confirm', { code: form.code }) diff --git a/src/views/asset-management/device-list/index.vue b/src/views/asset-management/device-list/index.vue index e345795..de7672e 100644 --- a/src/views/asset-management/device-list/index.vue +++ b/src/views/asset-management/device-list/index.vue @@ -32,6 +32,14 @@ > 批量回收 + + 批量设置套餐系列 + { + 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 request: BatchSetDeviceSeriesBindingRequest = { selection_type: seriesBindingForm.selection_type, diff --git a/src/views/asset-management/exchange-management/index.vue b/src/views/asset-management/exchange-management/index.vue index 64ccd68..c29b48d 100644 --- a/src/views/asset-management/exchange-management/index.vue +++ b/src/views/asset-management/exchange-management/index.vue @@ -136,6 +136,7 @@ style="width: 100%" clearable @change="handleOldIdentifierChange" + @focus="handleOldIotCardsFocus" > +
+ {{ oldIotCardEmptyMessage }} +
+
+ {{ oldDeviceEmptyMessage }} +
@@ -204,6 +212,7 @@ no-data-text="暂无同店铺可用的新资产" style="width: 100%" clearable + @focus="handleCreateNewIotCardsFocus" > +
+ {{ createNewIotCardEmptyMessage }} +
+
+ {{ createNewDeviceEmptyMessage }} +
{{ createDirectInheritedShopHint }}
@@ -308,6 +324,8 @@ no-data-text="暂无可用的新资产" style="width: 100%" clearable + @change="handleShipNewIdentifierChange" + @focus="handleShipNewIotCardsFocus" > +
+ {{ shipNewIotCardEmptyMessage }} +
+
+ {{ shipNewDeviceEmptyMessage }} +
{{ shipInheritedShopDisplayText }}
@@ -534,6 +560,42 @@ const newDeviceSearchLoading = ref(false) const completeLoadingId = ref(null) + type AssetFilterOption = { + label: string + value: string + } + + const oldAssetFilterOptions = ref([]) + const newAssetFilterOptions = ref([]) + 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({ new_identifier: '', @@ -543,11 +605,90 @@ }) const shipRules = reactive({ - new_identifier: [{ required: true, message: '请输入新资产标识符', trigger: 'blur' }], + new_identifier: [{ required: true, message: '请选择新资产标识符', trigger: 'change' }], express_company: [{ 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 = { shop_id?: number | null shop_name?: string | null @@ -644,7 +785,7 @@ }) // 搜索表单配置 - const searchFormItems: SearchFormItem[] = [ + const searchFormItems = computed(() => [ { label: '换货状态', prop: 'status', @@ -677,19 +818,31 @@ { label: '旧资产', prop: 'old_asset_keyword', - type: 'input', + type: 'select', + options: () => oldAssetFilterOptions.value, config: { - placeholder: '请输入旧资产标识', - clearable: true + placeholder: '请选择或搜索旧资产标识', + clearable: true, + filterable: true, + remote: true, + reserveKeyword: true, + loading: oldAssetFilterLoading.value, + remoteMethod: (query: string) => searchFilterAssets(query, 'old') } }, { label: '新资产', prop: 'new_asset_keyword', - type: 'input', + type: 'select', + options: () => newAssetFilterOptions.value, config: { - placeholder: '请输入新资产标识', - clearable: true + placeholder: '请选择或搜索新资产标识', + 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' } } - ] + ]) // 动态列配置 const { columnChecks, columns } = useCheckedColumns(() => [ @@ -1037,9 +1190,6 @@ }) if (res.code === 0 && res.data) { oldIotCardOptions.value = res.data.items || [] - if (oldIotCardOptions.value.length === 0) { - ElMessage.info('暂无可用的IoT卡') - } } } catch (error) { console.error('加载IoT卡列表失败:', error) @@ -1059,9 +1209,6 @@ }) if (res.code === 0 && res.data) { oldDeviceOptions.value = res.data.items || [] - if (oldDeviceOptions.value.length === 0) { - ElMessage.info('暂无可用的设备') - } } } catch (error) { console.error('加载设备列表失败:', error) @@ -1089,9 +1236,6 @@ }) if (res.code === 0 && res.data) { oldIotCardOptions.value = res.data.items || [] - if (oldIotCardOptions.value.length === 0) { - ElMessage.info('未找到匹配的IoT卡') - } } } catch (error) { console.error('搜索IoT卡失败:', error) @@ -1118,9 +1262,6 @@ }) if (res.code === 0 && res.data) { oldDeviceOptions.value = res.data.items || [] - if (oldDeviceOptions.value.length === 0) { - ElMessage.info('未找到匹配的设备') - } } } catch (error) { console.error('搜索设备失败:', error) @@ -1141,7 +1282,6 @@ const shopId = getCreateOldAssetShopId() if (shopId === undefined) { newIotCardOptions.value = [] - ElMessage.warning('请先选择旧资产') return } @@ -1155,9 +1295,6 @@ }) if (res.code === 0 && res.data) { newIotCardOptions.value = res.data.items || [] - if (newIotCardOptions.value.length === 0) { - ElMessage.info('未找到匹配的IoT卡') - } } } catch (error) { console.error('搜索IoT卡失败:', error) @@ -1184,9 +1321,6 @@ }) if (res.code === 0 && res.data) { newIotCardOptions.value = res.data.items || [] - if (newIotCardOptions.value.length === 0) { - ElMessage.info('未找到匹配的IoT卡') - } } } catch (error) { console.error('搜索IoT卡失败:', error) @@ -1213,9 +1347,6 @@ }) if (res.code === 0 && res.data) { newIotCardOptions.value = res.data.items || [] - if (newIotCardOptions.value.length === 0) { - ElMessage.info('暂无同店铺可用的新资产') - } } } catch (error) { console.error('加载IoT卡列表失败:', error) @@ -1234,8 +1365,8 @@ page: 1, page_size: 10 }) - if (res.code === 0 && res.data) { - newIotCardOptions.value = res.data.items || [] + if (res.code === 0) { + newIotCardOptions.value = res.data?.items || [] } } catch (error) { console.error('加载IoT卡列表失败:', error) @@ -1256,7 +1387,6 @@ const shopId = getCreateOldAssetShopId() if (shopId === undefined) { newDeviceOptions.value = [] - ElMessage.warning('请先选择旧资产') return } @@ -1269,9 +1399,6 @@ }) if (res.code === 0 && res.data) { newDeviceOptions.value = res.data.items || [] - if (newDeviceOptions.value.length === 0) { - ElMessage.info('未找到匹配的设备') - } } } catch (error) { console.error('搜索设备失败:', error) @@ -1297,9 +1424,6 @@ }) if (res.code === 0 && res.data) { newDeviceOptions.value = res.data.items || [] - if (newDeviceOptions.value.length === 0) { - ElMessage.info('未找到匹配的设备') - } } } catch (error) { console.error('搜索设备失败:', error) @@ -1325,9 +1449,6 @@ }) if (res.code === 0 && res.data) { newDeviceOptions.value = res.data.items || [] - if (newDeviceOptions.value.length === 0) { - ElMessage.info('暂无同店铺可用的新资产') - } } } catch (error) { console.error('加载设备列表失败:', error) @@ -1345,8 +1466,8 @@ page: 1, page_size: 10 }) - if (res.code === 0 && res.data) { - newDeviceOptions.value = res.data.items || [] + if (res.code === 0) { + newDeviceOptions.value = res.data?.items || [] } } catch (error) { console.error('加载设备列表失败:', error) @@ -1641,6 +1762,8 @@ // 初始化 onMounted(() => { loadExchangeList() + searchFilterAssets('', 'old') + searchFilterAssets('', 'new') }) @@ -1657,5 +1780,13 @@ border: 1px solid var(--el-color-primary-light-7); border-radius: 6px; } + + .exchange-field-error { + width: 100%; + margin-top: 4px; + color: var(--el-color-danger); + font-size: 12px; + line-height: 1.2; + } } diff --git a/src/views/asset-management/record-management/authorization-records/detail.vue b/src/views/asset-management/record-management/authorization-records/detail.vue deleted file mode 100644 index 9d44818..0000000 --- a/src/views/asset-management/record-management/authorization-records/detail.vue +++ /dev/null @@ -1,140 +0,0 @@ - - - - - diff --git a/src/views/asset-management/record-management/authorization-records/index.vue b/src/views/asset-management/record-management/authorization-records/index.vue index ab800f0..1010c35 100644 --- a/src/views/asset-management/record-management/authorization-records/index.vue +++ b/src/views/asset-management/record-management/authorization-records/index.vue @@ -28,29 +28,16 @@ :pageSize="pagination.pageSize" :total="pagination.total" :marginTop="10" - :row-class-name="getRowClassName" + :actions="getActions" + :actionsWidth="100" @size-change="handleSizeChange" @current-change="handleCurrentChange" - @row-contextmenu="handleRowContextMenu" - @cell-mouse-enter="handleCellMouseEnter" - @cell-mouse-leave="handleCellMouseLeave" > - - - - - - @@ -81,40 +68,22 @@ diff --git a/src/views/asset-management/task-management/bulk-purchase/index.vue b/src/views/asset-management/task-management/bulk-purchase/index.vue index 3c49a69..586f746 100644 --- a/src/views/asset-management/task-management/bulk-purchase/index.vue +++ b/src/views/asset-management/task-management/bulk-purchase/index.vue @@ -5,12 +5,12 @@ v-model="createDrawerVisible" title="批量订购套餐" direction="rtl" - size="520px" + size="40%" :close-on-click-modal="!createDrawerBusy" :close-on-press-escape="!createDrawerBusy" @closed="handleCreateDrawerClosed" > - + - + @@ -429,74 +427,13 @@ - - - - - - {{ currentCreditShop?.shop_name || '-' }} - - - {{ currentCreditShop?.version ?? '-' }} - - - {{ - formatCreditPreview(currentCreditShop?.credit_enabled, currentCreditShop?.credit_limit) - }} - - - {{ formatCreditPreview(creditForm.credit_enabled, creditFormCreditLimitFen) }} - - - - - - - - - - - -
单位:元;关闭信用时额度将自动归零
-
-
- - -