让凭证数组与套餐作废任务进入可联调状态
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 7m52s
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 7m52s
Constraint: 后端订单套餐作废接口使用 /api/admin/order-package-invalidate-tasks,上传 purpose 只能使用 attachment。 Rejected: 继续提交逗号拼接凭证字符串 | 新提交路径必须使用 string[],历史字符串只做读取归一化。 Confidence: high Scope-risk: broad Directive: 新增订单套餐作废权限时需同时配置菜单 URL 与 order_package_invalidate_task:create/detail 按钮权限。 Tested: bun run build;development 真实后端联调上传、创建、列表、详情通过;复机接口未触发。 Not-tested: 未逐一手工覆盖所有历史凭证数据、全部角色权限组合和所有文件扩展名预览
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -21,3 +21,9 @@ dist-ssr
|
||||
# Logs
|
||||
*.log
|
||||
npminstall-debug.log
|
||||
|
||||
# plan
|
||||
.scratch
|
||||
|
||||
# ai
|
||||
.agents
|
||||
|
||||
14
CLAUDE.md
14
CLAUDE.md
@@ -19,3 +19,17 @@ Use `@/openspec/AGENTS.md` to learn:
|
||||
Keep this managed block so 'openspec update' can refresh the instructions.
|
||||
|
||||
<!-- OPENSPEC:END -->
|
||||
|
||||
## Agent skills
|
||||
|
||||
### Issue tracker
|
||||
|
||||
Issues and PRDs are tracked as local markdown files under `.scratch/`; external PRs are not a triage surface. See `docs/agents/issue-tracker.md`.
|
||||
|
||||
### Triage labels
|
||||
|
||||
Uses the default triage state vocabulary: `needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`. See `docs/agents/triage-labels.md`.
|
||||
|
||||
### Domain docs
|
||||
|
||||
Uses a single-context domain documentation layout. See `docs/agents/domain.md`.
|
||||
|
||||
15
CONTEXT.md
Normal file
15
CONTEXT.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# One Pipe System
|
||||
|
||||
This context captures the business language used by the admin system for orders, assets, finance operations, and operational batch tasks.
|
||||
|
||||
## Language
|
||||
|
||||
**Voucher attachment**: A user-provided proof file attached to an order, refund, recharge, or operational task. Voucher attachments may contain any file type and a business record may hold multiple voucher attachment keys. _Avoid_: Voucher image, comma-separated voucher string
|
||||
|
||||
**Voucher key list**: The canonical representation of voucher attachment object-storage keys at the frontend and API submission boundary. Legacy single-key strings or comma-separated strings may be read and normalized for display, but new submissions use key lists. _Avoid_: Comma-separated voucher string
|
||||
|
||||
**Batch import file**: The structured data file that drives an asynchronous operational batch task. Unlike voucher attachments, a batch import file is constrained by the task's required template format. _Avoid_: Voucher, attachment
|
||||
|
||||
**Operational batch task**: An asynchronous admin task created from an uploaded batch import file and tracked through task status, counts, operator identity, and failure details. _Avoid_: Inline order operation, synchronous import
|
||||
|
||||
**Operational remark**: A note captured when an admin creates a financial or operational record. Operational remarks are retained for later review and are not edited after creation. _Avoid_: Editable comment, processing note
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
- 退款详情中直接展示退款凭证图片,点击图片后可放大预览。
|
||||
- 创建退款申请的订单号搜索/选择请求必须增加 `payment_status: 2` 条件。
|
||||
- 退款管理凭证上传支持多张图片,并将所有上传后的文件 key 以英文逗号拼接后放入 `refund_voucher_key` 参数。
|
||||
- 退款管理凭证上传支持多张图片,并按 ADR-0001 将所有上传后的文件 key 以 `string[]` 放入 `refund_voucher_key` 参数。
|
||||
|
||||
## Impact
|
||||
|
||||
@@ -17,4 +17,4 @@
|
||||
- Refund detail voucher display and image preview behavior
|
||||
- Create refund request order selector query params
|
||||
- Refund management voucher upload component and submit payload
|
||||
- Breaking changes: Refund voucher payload field for this flow remains `refund_voucher_key` and supports one or more uploaded image keys separated by English commas.
|
||||
- Breaking changes: Refund voucher payload field for this flow remains `refund_voucher_key`, but new submissions use `string[]`. Legacy single-key strings and comma-separated strings are read-only compatibility inputs for display normalization.
|
||||
|
||||
@@ -75,7 +75,7 @@ The system SHALL allow the requested refund amount in create refund application
|
||||
|
||||
### Requirement: Refund Management Multi Image Voucher Upload
|
||||
|
||||
The system SHALL allow multiple refund voucher images in any browser-recognized image format to be uploaded in refund management and SHALL submit all uploaded file keys in the `refund_voucher_key` parameter as a comma-separated string.
|
||||
The system SHALL allow multiple refund voucher images in any browser-recognized image format to be uploaded in refund management and SHALL submit all uploaded file keys in the `refund_voucher_key` parameter as a `string[]`. The earlier comma-separated-string submission convention is superseded by ADR-0001; legacy single-key strings and comma-separated strings are only normalized when reading existing records for display.
|
||||
|
||||
#### Scenario: Submit multiple uploaded voucher images
|
||||
|
||||
@@ -83,9 +83,9 @@ The system SHALL allow multiple refund voucher images in any browser-recognized
|
||||
- **AND** 用户上传了多张退款凭证图片
|
||||
- **WHEN** 每张图片上传成功并返回文件 key
|
||||
- **AND** 用户提交表单
|
||||
- **THEN** 系统 MUST join the uploaded file keys with English commas
|
||||
- **AND** 系统 MUST put the joined string into `refund_voucher_key`
|
||||
- **AND** `refund_voucher_key` MUST look like `attachments/a.jpg,attachments/b.jpg` when multiple files exist
|
||||
- **THEN** 系统 MUST put the uploaded file keys into `refund_voucher_key` as a `string[]`
|
||||
- **AND** `refund_voucher_key` MUST look like `["attachments/a.jpg", "attachments/b.jpg"]` when multiple files exist
|
||||
- **AND** 系统 MUST NOT submit a comma-separated voucher string for new records
|
||||
|
||||
#### Scenario: Upload any image format refund voucher without frontend size limit
|
||||
|
||||
@@ -99,7 +99,7 @@ The system SHALL allow multiple refund voucher images in any browser-recognized
|
||||
- **GIVEN** 用户正在退款管理中创建或提交退款相关记录
|
||||
- **AND** 用户只上传了一张退款凭证图片
|
||||
- **WHEN** 用户提交表单
|
||||
- **THEN** 系统 MUST put that single file key into `refund_voucher_key` without a trailing comma
|
||||
- **THEN** 系统 MUST put that single file key into `refund_voucher_key` as a one-item `string[]`
|
||||
|
||||
#### Scenario: Block submission without required voucher image
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
- [x] 1.2 Add click-to-preview/enlarge behavior for refund detail voucher images.
|
||||
- [x] 1.3 Add `payment_status: 2` to the order number search/select request used by create refund application.
|
||||
- [x] 1.4 Update refund management voucher upload to allow multiple image uploads.
|
||||
- [x] 1.5 Submit uploaded voucher file keys joined by English commas in `refund_voucher_key`.
|
||||
- [x] 1.5 Submit uploaded voucher file keys as `string[]` in `refund_voucher_key`; do not submit comma-separated strings for new records.
|
||||
- [x] 1.6 Validate that at least one voucher image is uploaded before submission when voucher is required.
|
||||
- [x] 1.7 Verify refund detail display, order selector filtering, and multi-image payload behavior.
|
||||
- [x] 1.8 Allow requested refund amount `0` while blocking negative and over-actual amounts.
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
packages:
|
||||
- .
|
||||
|
||||
allowBuilds:
|
||||
'@parcel/watcher': true
|
||||
core-js: true
|
||||
|
||||
107
skills-lock.json
Normal file
107
skills-lock.json
Normal file
@@ -0,0 +1,107 @@
|
||||
{
|
||||
"version": 1,
|
||||
"skills": {
|
||||
"ask-matt": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/engineering/ask-matt/SKILL.md",
|
||||
"computedHash": "f3ac3351e2a242ce2591e4f8475424d58f62db3f5786332b78f330b15312d1e0"
|
||||
},
|
||||
"codebase-design": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/engineering/codebase-design/SKILL.md",
|
||||
"computedHash": "2426dc4accf1a85dedfb560edb3a877ec8828b7375ea08c970df2b6c900a2a22"
|
||||
},
|
||||
"diagnosing-bugs": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/engineering/diagnosing-bugs/SKILL.md",
|
||||
"computedHash": "1a993ce9b2aaa653ee441c8d7efb7f27de03493c722be6dfb8137bcb8db1bc72"
|
||||
},
|
||||
"domain-modeling": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/engineering/domain-modeling/SKILL.md",
|
||||
"computedHash": "67343881f5def98487d56243155716110afbcbf22ec92421c882d532b941cb17"
|
||||
},
|
||||
"grill-me": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/productivity/grill-me/SKILL.md",
|
||||
"computedHash": "f321507f77702a54af1db66549ec1685fc625390d06c57d7949cdcda8eb1b5c7"
|
||||
},
|
||||
"grill-with-docs": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/engineering/grill-with-docs/SKILL.md",
|
||||
"computedHash": "e7ef25bbee50cda2eb7b3a8ef541db0a0396dad7d369f7d14fb610671dd1864b"
|
||||
},
|
||||
"grilling": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/productivity/grilling/SKILL.md",
|
||||
"computedHash": "ef685a6fa0bbe73b05bbd8dc1ec97258191587f07c6aa7dbc9006675ceb8f90f"
|
||||
},
|
||||
"handoff": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/productivity/handoff/SKILL.md",
|
||||
"computedHash": "5d0f81e38abe6b984e1feaa731927de5291d040982106ac513cf54ec240e00ec"
|
||||
},
|
||||
"improve-codebase-architecture": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/engineering/improve-codebase-architecture/SKILL.md",
|
||||
"computedHash": "8bf292143ca93b00276a0de0fc5f84f2381f4690c5b0d32e99fa283a072f392f"
|
||||
},
|
||||
"prototype": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/engineering/prototype/SKILL.md",
|
||||
"computedHash": "40ff56e36bb13b5a9a16903efd6c78a02690ba430e10a8d1a65559100b77f475"
|
||||
},
|
||||
"setup-matt-pocock-skills": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/engineering/setup-matt-pocock-skills/SKILL.md",
|
||||
"computedHash": "400da4521ed701c99e8824ae7b71c7859632ee91481b3715890036cf8aec6857"
|
||||
},
|
||||
"tdd": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/engineering/tdd/SKILL.md",
|
||||
"computedHash": "d5ca5c8ae0615f343f1cfd71591f4a586046d77b1f36e8d67b8f643bad51043e"
|
||||
},
|
||||
"teach": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/productivity/teach/SKILL.md",
|
||||
"computedHash": "9cd31ea42b40915ea5fd3949ed229b217d1dfad07dd93d9b0db771dcdd6a6c8a"
|
||||
},
|
||||
"to-issues": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/engineering/to-issues/SKILL.md",
|
||||
"computedHash": "4e2a11d35f018490d42a79234432f779d7131b9920373ef24cfbc68229bfef6c"
|
||||
},
|
||||
"to-prd": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/engineering/to-prd/SKILL.md",
|
||||
"computedHash": "5a733e19d825f22e83de52e68b8840c9fd2fa7501282277a122c1aa28a922059"
|
||||
},
|
||||
"triage": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/engineering/triage/SKILL.md",
|
||||
"computedHash": "b77ea99c2e6e97815b373cc476ad4cc1835d3e736867b764602147be71f6c131"
|
||||
},
|
||||
"writing-great-skills": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/productivity/writing-great-skills/SKILL.md",
|
||||
"computedHash": "581f78b50b3d81b095de86b5c18a4d9fa932fbe414cc82bd26d627d4da518f07"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ export { ManualTriggerService } from './manualTrigger'
|
||||
export { PollingMonitorService } from './pollingMonitor'
|
||||
export { SuperAdminService } from './superAdmin'
|
||||
export { ExportTaskService } from './exportTask'
|
||||
export { OrderPackageInvalidateTaskService } from './orderPackageInvalidateTask'
|
||||
|
||||
// TODO: 按需添加其他业务模块
|
||||
// export { SettingService } from './setting'
|
||||
|
||||
36
src/api/modules/orderPackageInvalidateTask.ts
Normal file
36
src/api/modules/orderPackageInvalidateTask.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 订单套餐批量作废任务 API
|
||||
*/
|
||||
|
||||
import { BaseService } from '../BaseService'
|
||||
import type {
|
||||
CreateOrderPackageInvalidateTaskApiResponse,
|
||||
CreateOrderPackageInvalidateTaskRequest,
|
||||
OrderPackageInvalidateTaskDetailApiResponse,
|
||||
OrderPackageInvalidateTaskListApiResponse,
|
||||
OrderPackageInvalidateTaskQueryParams
|
||||
} from '@/types/api'
|
||||
|
||||
export class OrderPackageInvalidateTaskService extends BaseService {
|
||||
static getTasks(
|
||||
params?: OrderPackageInvalidateTaskQueryParams
|
||||
): Promise<OrderPackageInvalidateTaskListApiResponse> {
|
||||
return this.get<OrderPackageInvalidateTaskListApiResponse>(
|
||||
'/api/admin/order-package-invalidate-tasks',
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
static createTask(
|
||||
data: CreateOrderPackageInvalidateTaskRequest
|
||||
): Promise<CreateOrderPackageInvalidateTaskApiResponse> {
|
||||
return this.post<CreateOrderPackageInvalidateTaskApiResponse>(
|
||||
'/api/admin/order-package-invalidate-tasks',
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
static getTaskDetail(id: number): Promise<OrderPackageInvalidateTaskDetailApiResponse> {
|
||||
return this.getOne(`/api/admin/order-package-invalidate-tasks/${id}`)
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
<!-- 选择下拉项表单项 -->
|
||||
<ElFormItem v-if="showSelect" :label="selectLabel" :prop="selectProp">
|
||||
<ElSelect
|
||||
v-model="formData[selectProp]"
|
||||
v-model="formData[selectField]"
|
||||
:placeholder="selectPlaceholder"
|
||||
filterable
|
||||
remote
|
||||
@@ -80,8 +80,7 @@
|
||||
ElOption,
|
||||
ElInput,
|
||||
ElButton,
|
||||
ElTag,
|
||||
ElMessage
|
||||
ElTag
|
||||
} from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
@@ -145,12 +144,20 @@
|
||||
const showCardList = ref(false)
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
type FormData = Record<string, string> & {
|
||||
selectedValue: string
|
||||
amount: string
|
||||
remark: string
|
||||
}
|
||||
|
||||
const formData = reactive<FormData>({
|
||||
selectedValue: '',
|
||||
amount: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const selectField = computed(() => props.selectProp || 'selectedValue')
|
||||
|
||||
// 下拉选项
|
||||
const selectOptions = ref<SelectOption[]>([])
|
||||
|
||||
@@ -159,7 +166,7 @@
|
||||
const rules: FormRules = {}
|
||||
|
||||
if (props.showSelect) {
|
||||
rules[props.selectProp!] = [
|
||||
rules[selectField.value] = [
|
||||
{ required: true, message: `请选择${props.selectLabel}`, trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
import { RefundService, OrderService } from '@/api/modules'
|
||||
import type { CreateRefundRequest, Order } from '@/types/api'
|
||||
import { formatMoney, yuanToFen } from '@/utils/business/format'
|
||||
import { getErrorMessage } from '@/utils/business'
|
||||
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
||||
|
||||
interface Props {
|
||||
@@ -110,13 +111,13 @@
|
||||
requested_refund_amount?: number
|
||||
actual_received_amount: number
|
||||
refund_reason: string
|
||||
refund_voucher_key?: string
|
||||
refund_voucher_key: string[]
|
||||
}>({
|
||||
order_id: null,
|
||||
requested_refund_amount: undefined,
|
||||
actual_received_amount: 0,
|
||||
refund_reason: '',
|
||||
refund_voucher_key: undefined
|
||||
refund_voucher_key: []
|
||||
})
|
||||
|
||||
const validateRefundAmount = (rule: any, value: number, callback: any) => {
|
||||
@@ -200,7 +201,7 @@
|
||||
formData.requested_refund_amount = undefined
|
||||
formData.actual_received_amount = 0
|
||||
formData.refund_reason = ''
|
||||
formData.refund_voucher_key = undefined
|
||||
formData.refund_voucher_key = []
|
||||
orderSearchOptions.value = []
|
||||
uploadRef.value?.clearFiles(false)
|
||||
}
|
||||
@@ -220,7 +221,7 @@
|
||||
order_id: formData.order_id!,
|
||||
requested_refund_amount: yuanToFen(formData.requested_refund_amount) ?? 0,
|
||||
actual_received_amount: formData.actual_received_amount,
|
||||
refund_voucher_key: formData.refund_voucher_key!,
|
||||
refund_voucher_key: formData.refund_voucher_key,
|
||||
refund_reason: formData.refund_reason || undefined
|
||||
}
|
||||
await RefundService.createRefund(data)
|
||||
@@ -229,6 +230,7 @@
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error(getErrorMessage(error, '退款申请创建失败'))
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ElMessage, ElTag } from 'element-plus'
|
||||
import { ElTag } from 'element-plus'
|
||||
import { AccountService } from '@/api/modules'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import type { PlatformAccount } from '@/types/api'
|
||||
@@ -175,13 +175,13 @@
|
||||
prop: 'shop_id',
|
||||
label: '店铺ID',
|
||||
width: 100,
|
||||
formatter: (row: PlatformAccount) => row.shop_id || '-'
|
||||
formatter: (row: PlatformAccount) => String(row.shop_id || '-')
|
||||
},
|
||||
{
|
||||
prop: 'enterprise_id',
|
||||
label: '企业ID',
|
||||
width: 100,
|
||||
formatter: (row: PlatformAccount) => row.enterprise_id || '-'
|
||||
formatter: (row: PlatformAccount) => String(row.enterprise_id || '-')
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
|
||||
@@ -13,11 +13,16 @@
|
||||
@change="handleChange"
|
||||
@visible-change="handleVisibleChange"
|
||||
>
|
||||
<el-option v-for="item in packageList" :key="item.id" :label="item.name" :value="item.id">
|
||||
<el-option
|
||||
v-for="item in packageList"
|
||||
:key="item.id"
|
||||
:label="item.packageName"
|
||||
:value="item.id"
|
||||
>
|
||||
<div class="package-option">
|
||||
<span class="package-name">{{ item.name }}</span>
|
||||
<span class="package-name">{{ item.packageName }}</span>
|
||||
<span class="package-info">
|
||||
{{ formatFlow(item.flow) }} / {{ formatMoney(item.price) }}
|
||||
{{ formatFlow(item.totalFlow) }} / {{ formatMoney(item.price) }}
|
||||
</span>
|
||||
</div>
|
||||
</el-option>
|
||||
|
||||
@@ -13,10 +13,13 @@
|
||||
preview-teleported
|
||||
/>
|
||||
<div v-else class="voucher-preview__file">
|
||||
<ElIcon><Document /></ElIcon>
|
||||
<ElIcon><component :is="getFileIcon(file)" /></ElIcon>
|
||||
<span class="voucher-preview__file-type">{{ file.fileType }}</span>
|
||||
</div>
|
||||
<div class="voucher-preview__meta">
|
||||
<div class="voucher-preview__name" :title="file.name">{{ file.name }}</div>
|
||||
<div class="voucher-preview__name" :title="file.name">{{
|
||||
getDisplayName(file.name)
|
||||
}}</div>
|
||||
<ElButton link type="primary" @click="openFile(file)">查看/下载</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -27,12 +30,24 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { Document } from '@element-plus/icons-vue'
|
||||
import {
|
||||
Box,
|
||||
DataAnalysis,
|
||||
Document,
|
||||
Film,
|
||||
Headset,
|
||||
Memo,
|
||||
Picture,
|
||||
Tickets
|
||||
} from '@element-plus/icons-vue'
|
||||
import { ElButton, ElDialog, ElEmpty, ElIcon, ElImage } from 'element-plus'
|
||||
import type { Component } from 'vue'
|
||||
import { StorageService } from '@/api/modules'
|
||||
import { toVoucherKeyList } from '@/utils/business'
|
||||
|
||||
interface Props {
|
||||
fileKey?: string
|
||||
fileKey?: string | string[]
|
||||
fileKeys?: string[]
|
||||
}
|
||||
|
||||
interface VoucherFile {
|
||||
@@ -40,6 +55,8 @@
|
||||
name: string
|
||||
url: string
|
||||
isImage: boolean
|
||||
extension: string
|
||||
fileType: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
@@ -56,14 +73,47 @@
|
||||
files.value.filter((file) => file.isImage).map((file) => file.url)
|
||||
)
|
||||
|
||||
const getVoucherKeys = (value: string) =>
|
||||
value
|
||||
.split(',')
|
||||
.map((key) => key.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
const getFileName = (fileKey: string) => fileKey.split('/').pop() || fileKey
|
||||
|
||||
const getFileExtension = (fileName: string) => {
|
||||
const match = fileName.toLowerCase().match(/\.([a-z0-9]+)(?:[?#].*)?$/)
|
||||
return match?.[1] || ''
|
||||
}
|
||||
|
||||
const getFileType = (extension: string) => {
|
||||
if (!extension) return '文件'
|
||||
const typeMap: Record<string, string> = {
|
||||
csv: 'CSV',
|
||||
xls: 'Excel',
|
||||
xlsx: 'Excel',
|
||||
doc: 'Word',
|
||||
docx: 'Word',
|
||||
pdf: 'PDF',
|
||||
txt: 'TXT',
|
||||
json: 'JSON',
|
||||
zip: 'ZIP',
|
||||
rar: 'RAR',
|
||||
'7z': '7Z',
|
||||
mp4: '视频',
|
||||
mov: '视频',
|
||||
avi: '视频',
|
||||
mp3: '音频',
|
||||
wav: '音频'
|
||||
}
|
||||
return typeMap[extension] || extension.toUpperCase()
|
||||
}
|
||||
|
||||
const getDisplayName = (fileName: string) => {
|
||||
if (fileName.length <= 28) return fileName
|
||||
|
||||
const extension = getFileExtension(fileName)
|
||||
const suffix = extension ? `.${extension}` : ''
|
||||
const baseName = suffix ? fileName.slice(0, -suffix.length) : fileName
|
||||
if (baseName.length <= 22) return fileName
|
||||
|
||||
return `${baseName.slice(0, 12)}...${baseName.slice(-8)}${suffix}`
|
||||
}
|
||||
|
||||
const isImageFile = (fileKey: string, url: string) => {
|
||||
const target = `${fileKey} ${url}`.toLowerCase()
|
||||
return /\.(png|jpe?g|gif|webp|bmp|svg)(\?|#|$)/.test(target)
|
||||
@@ -71,21 +121,45 @@
|
||||
|
||||
const getImageIndex = (url: string) => Math.max(0, imageUrls.value.indexOf(url))
|
||||
|
||||
const getFileIcon = (file: VoucherFile): Component => {
|
||||
const iconMap: Record<string, Component> = {
|
||||
csv: DataAnalysis,
|
||||
xls: DataAnalysis,
|
||||
xlsx: DataAnalysis,
|
||||
doc: Tickets,
|
||||
docx: Tickets,
|
||||
pdf: Document,
|
||||
txt: Memo,
|
||||
json: Memo,
|
||||
zip: Box,
|
||||
rar: Box,
|
||||
'7z': Box,
|
||||
mp4: Film,
|
||||
mov: Film,
|
||||
avi: Film,
|
||||
mp3: Headset,
|
||||
wav: Headset,
|
||||
png: Picture,
|
||||
jpg: Picture,
|
||||
jpeg: Picture,
|
||||
gif: Picture,
|
||||
webp: Picture,
|
||||
bmp: Picture,
|
||||
svg: Picture
|
||||
}
|
||||
return iconMap[file.extension] || Document
|
||||
}
|
||||
|
||||
const openFile = (file: VoucherFile) => {
|
||||
window.open(file.url, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.fileKey,
|
||||
async (val) => {
|
||||
if (!val) {
|
||||
visible.value = false
|
||||
files.value = []
|
||||
return
|
||||
}
|
||||
() => [props.fileKeys, props.fileKey] as const,
|
||||
async ([fileKeys, fileKey]) => {
|
||||
const keys = fileKeys?.length ? toVoucherKeyList(fileKeys) : toVoucherKeyList(fileKey)
|
||||
|
||||
const fileKeys = getVoucherKeys(val)
|
||||
if (!fileKeys.length) {
|
||||
if (!keys.length) {
|
||||
visible.value = false
|
||||
files.value = []
|
||||
return
|
||||
@@ -97,10 +171,10 @@
|
||||
|
||||
try {
|
||||
const res = await StorageService.batchDownloadUrls({
|
||||
file_keys: fileKeys
|
||||
file_keys: keys
|
||||
})
|
||||
if (res.code === 0) {
|
||||
files.value = fileKeys
|
||||
files.value = keys
|
||||
.map((key) => {
|
||||
const url = res.data.urls[key]
|
||||
if (!url) return null
|
||||
@@ -108,7 +182,9 @@
|
||||
key,
|
||||
name: getFileName(key),
|
||||
url,
|
||||
isImage: isImageFile(key, url)
|
||||
isImage: isImageFile(key, url),
|
||||
extension: getFileExtension(getFileName(key)),
|
||||
fileType: getFileType(getFileExtension(getFileName(key)))
|
||||
}
|
||||
})
|
||||
.filter((file): file is VoucherFile => !!file)
|
||||
@@ -162,6 +238,8 @@
|
||||
|
||||
&__file {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 40px;
|
||||
@@ -169,6 +247,17 @@
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
&__file-type {
|
||||
max-width: 84px;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
color: var(--el-text-color-regular);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
width: 120px;
|
||||
margin-top: 8px;
|
||||
@@ -177,6 +266,7 @@
|
||||
&__name {
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: var(--el-text-color-regular);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { genFileId } from 'element-plus'
|
||||
import { Close, Document, UploadFilled } from '@element-plus/icons-vue'
|
||||
@@ -58,7 +58,7 @@
|
||||
import { StorageService } from '@/api/modules'
|
||||
|
||||
interface Props {
|
||||
modelValue?: string
|
||||
modelValue?: string[] | string
|
||||
voucherName?: string
|
||||
maxCount?: number
|
||||
}
|
||||
@@ -69,9 +69,9 @@
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string | undefined]
|
||||
'update:modelValue': [value: string[]]
|
||||
'uploading-change': [value: boolean]
|
||||
change: [value: string | undefined]
|
||||
change: [value: string[]]
|
||||
}>()
|
||||
|
||||
const rootRef = ref<HTMLElement>()
|
||||
@@ -91,9 +91,8 @@
|
||||
|
||||
const emitVoucherKeys = () => {
|
||||
const keys = Array.from(voucherFileKeyMap.values())
|
||||
const value = keys.length ? keys.join(',') : undefined
|
||||
emit('update:modelValue', value)
|
||||
emit('change', value)
|
||||
emit('update:modelValue', keys)
|
||||
emit('change', keys)
|
||||
}
|
||||
|
||||
const setUploadingCount = (count: number) => {
|
||||
@@ -114,6 +113,26 @@
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
const keys = Array.isArray(value)
|
||||
? value
|
||||
: value
|
||||
? value
|
||||
.split(',')
|
||||
.map((key) => key.trim())
|
||||
.filter(Boolean)
|
||||
: []
|
||||
|
||||
voucherFileKeyMap.clear()
|
||||
keys.slice(0, getMaxCount()).forEach((key, index) => {
|
||||
voucherFileKeyMap.set(index + 1, key)
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const revokeFileObjectUrl = (uid: number) => {
|
||||
const url = fileObjectUrlMap.get(uid)
|
||||
if (!url) return
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import chinaMapJson from '@/mock/json/chinaMap.json'
|
||||
|
||||
// 响应式引用与主题
|
||||
const chinaMapRef = ref<HTMLElement | null>(null)
|
||||
const chinaMapRef = shallowRef<HTMLElement | null>(null)
|
||||
const chartInstance = ref<echarts.ECharts | null>(null)
|
||||
const settingStore = useSettingStore()
|
||||
const { isDark } = storeToRefs(settingStore)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<el-radio-group v-model="value" v-bind="config" @change="(val) => changeValue(val)">
|
||||
<el-radio v-for="v in options" :key="v.value" :value="v.value">{{ v.label }}</el-radio>
|
||||
<el-radio v-for="v in options" :key="String(v.value)" :value="v.value">{{ v.label }}</el-radio>
|
||||
</el-radio-group>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<el-select v-model="value" v-bind="config" @change="(val) => changeValue(val)">
|
||||
<el-option
|
||||
v-for="v in options"
|
||||
:key="v.value"
|
||||
:key="String(v.value)"
|
||||
:label="v.label"
|
||||
:value="v.value"
|
||||
:disabled="v.disabled || false"
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
}
|
||||
]
|
||||
|
||||
const menus = userStore.menus.value || []
|
||||
const menus = userStore.menus || []
|
||||
|
||||
const hasMenuPermission = (permCode: string): boolean => {
|
||||
const findMenu = (items: any[]): boolean => {
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<div v-if="showCardSelection" class="card-selector-top-left" @click.stop>
|
||||
<ElCheckbox
|
||||
:model-value="isCardSelected(item)"
|
||||
@change="handleCardSelect(item, $event)"
|
||||
@change="handleCardSelect(item, Boolean($event))"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { ElDialog, ElForm, ElFormItem, ElRadioGroup, ElRadio, ElTag } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type { FormInstance, FormRules, TagProps } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
@@ -84,9 +84,9 @@
|
||||
return map[policy] || '未知'
|
||||
}
|
||||
|
||||
const getPolicyTagType = (policy?: string) => {
|
||||
const getPolicyTagType = (policy?: string): TagProps['type'] => {
|
||||
if (!policy || policy === '') return 'info'
|
||||
const map: Record<string, string> = {
|
||||
const map: Record<string, TagProps['type']> = {
|
||||
none: 'success',
|
||||
before_order: 'warning',
|
||||
after_order: 'info'
|
||||
|
||||
@@ -21,6 +21,16 @@ export function useAgentManagement() {
|
||||
// 分页
|
||||
const { pagination, setTotal, handleCurrentChange, handleSizeChange } = usePagination()
|
||||
|
||||
const agentService = AccountService as unknown as {
|
||||
getAgents: (params?: AgentQueryParams) => Promise<any>
|
||||
getAgentTree: () => Promise<any>
|
||||
getAgent: (id: string | number) => Promise<any>
|
||||
createAgent: (data: CreateAgentParams) => Promise<any>
|
||||
updateAgent: (id: string | number, data: Partial<CreateAgentParams>) => Promise<any>
|
||||
toggleAgentStatus: (id: string | number, status: 0 | 1) => Promise<any>
|
||||
getSubAgents: (id: string | number) => Promise<any>
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取代理商列表
|
||||
* @param params 查询参数
|
||||
@@ -28,7 +38,7 @@ export function useAgentManagement() {
|
||||
const fetchAgentList = async (params?: AgentQueryParams) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await AccountService.getAgents({
|
||||
const res = await agentService.getAgents({
|
||||
current: pagination.current,
|
||||
size: pagination.size,
|
||||
...params
|
||||
@@ -50,7 +60,7 @@ export function useAgentManagement() {
|
||||
const fetchAgentTree = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await AccountService.getAgentTree()
|
||||
const res = await agentService.getAgentTree()
|
||||
treeData.value = res.data
|
||||
} catch (error) {
|
||||
console.error('获取代理商树失败:', error)
|
||||
@@ -67,7 +77,7 @@ export function useAgentManagement() {
|
||||
const fetchAgentDetail = async (id: string | number) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await AccountService.getAgent(id)
|
||||
const res = await agentService.getAgent(id)
|
||||
return res.data
|
||||
} catch (error) {
|
||||
console.error('获取代理商详情失败:', error)
|
||||
@@ -85,7 +95,7 @@ export function useAgentManagement() {
|
||||
const createAgent = async (data: CreateAgentParams) => {
|
||||
loading.value = true
|
||||
try {
|
||||
await AccountService.createAgent(data)
|
||||
await agentService.createAgent(data)
|
||||
ElMessage.success('创建成功')
|
||||
return true
|
||||
} catch (error) {
|
||||
@@ -105,7 +115,7 @@ export function useAgentManagement() {
|
||||
const updateAgent = async (id: string | number, data: Partial<CreateAgentParams>) => {
|
||||
loading.value = true
|
||||
try {
|
||||
await AccountService.updateAgent(id, data)
|
||||
await agentService.updateAgent(id, data)
|
||||
ElMessage.success('更新成功')
|
||||
return true
|
||||
} catch (error) {
|
||||
@@ -125,7 +135,7 @@ export function useAgentManagement() {
|
||||
const toggleStatus = async (id: string | number, status: 0 | 1) => {
|
||||
loading.value = true
|
||||
try {
|
||||
await AccountService.toggleAgentStatus(id, status)
|
||||
await agentService.toggleAgentStatus(id, status)
|
||||
ElMessage.success('状态更新成功')
|
||||
return true
|
||||
} catch (error) {
|
||||
@@ -144,7 +154,7 @@ export function useAgentManagement() {
|
||||
const fetchSubAgents = async (id: string | number) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await AccountService.getSubAgents(id)
|
||||
const res = await agentService.getSubAgents(id)
|
||||
return res.data
|
||||
} catch (error) {
|
||||
console.error('获取下级代理失败:', error)
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { CardService } from '@/api/modules'
|
||||
import type { Card, CardQueryParams, CardOperationParams } from '@/types/api'
|
||||
import { getErrorMessage } from '@/utils/business'
|
||||
import { usePagination } from './usePagination'
|
||||
import { useTableSelection } from './useTableSelection'
|
||||
|
||||
@@ -116,7 +117,7 @@ export function useCardManagement() {
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('复机失败:', error)
|
||||
console.log('复机失败')
|
||||
ElMessage.error(getErrorMessage(error, '复机失败'))
|
||||
return false
|
||||
} finally {
|
||||
loading.value = false
|
||||
|
||||
@@ -29,7 +29,7 @@ export interface ColumnOption {
|
||||
fixed?: boolean | 'left' | 'right'
|
||||
sortable?: boolean
|
||||
disabled?: boolean
|
||||
formatter?: (row: any) => any
|
||||
formatter?: (row: any, ...args: any[]) => any
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
* 表格选择管理 Composable
|
||||
*/
|
||||
|
||||
import { ref, computed } from 'vue'
|
||||
import { computed, shallowRef } from 'vue'
|
||||
|
||||
export function useTableSelection<T = any>() {
|
||||
// 选中的行
|
||||
const selectedRows = ref<T[]>([])
|
||||
const selectedRows = shallowRef<T[]>([])
|
||||
|
||||
// 选中的ID列表
|
||||
const selectedIds = computed(() => {
|
||||
@@ -38,9 +38,9 @@ export function useTableSelection<T = any>() {
|
||||
const toggleSelection = (row: T) => {
|
||||
const index = selectedRows.value.findIndex((item: any) => item.id === (row as any).id)
|
||||
if (index > -1) {
|
||||
selectedRows.value.splice(index, 1)
|
||||
selectedRows.value = selectedRows.value.filter((_, itemIndex) => itemIndex !== index)
|
||||
} else {
|
||||
selectedRows.value.push(row)
|
||||
selectedRows.value = [...selectedRows.value, row]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -432,6 +432,8 @@
|
||||
"standaloneCardList": "IoT Card Management",
|
||||
"iotCardTask": "IoT Card Tasks",
|
||||
"deviceTask": "Device Tasks",
|
||||
"orderPackageInvalidateTask": "Order Package Void Tasks",
|
||||
"orderPackageInvalidateTaskDetail": "Order Package Void Task Detail",
|
||||
"taskDetail": "Task Details",
|
||||
"devices": "Device Management",
|
||||
"deviceDetail": "Device Details",
|
||||
|
||||
@@ -370,6 +370,8 @@
|
||||
"standaloneCardList": "IoT卡管理",
|
||||
"iotCardTask": "IoT卡任务",
|
||||
"deviceTask": "设备任务",
|
||||
"orderPackageInvalidateTask": "订单套餐批量作废",
|
||||
"orderPackageInvalidateTaskDetail": "订单套餐作废任务详情",
|
||||
"taskDetail": "任务详情",
|
||||
"devices": "设备管理",
|
||||
"assetAssign": "分配记录",
|
||||
|
||||
@@ -84,7 +84,7 @@ export const hasRoutePermission = (
|
||||
if (permissions.includes('*:*:*')) {
|
||||
return true
|
||||
}
|
||||
return permissions.some((userPermission) => {
|
||||
return permissions.some((userPermission: string) => {
|
||||
if (userPermission.endsWith('*')) {
|
||||
const prefix = userPermission.slice(0, -1)
|
||||
return permission.startsWith(prefix)
|
||||
|
||||
@@ -405,9 +405,30 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
title: 'menus.assetManagement.deviceTask',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
// 订单套餐批量作废任务
|
||||
{
|
||||
path: 'order-package-invalidate-task',
|
||||
name: 'OrderPackageInvalidateTask',
|
||||
component: RoutesAlias.OrderPackageInvalidateTask,
|
||||
meta: {
|
||||
title: 'menus.assetManagement.orderPackageInvalidateTask',
|
||||
keepAlive: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
// 订单套餐作废任务详情
|
||||
{
|
||||
path: 'task-management/order-package-invalidate-task/detail',
|
||||
name: 'OrderPackageInvalidateTaskDetail',
|
||||
component: RoutesAlias.OrderPackageInvalidateTaskDetail,
|
||||
meta: {
|
||||
title: 'menus.assetManagement.orderPackageInvalidateTaskDetail',
|
||||
isHide: true,
|
||||
keepAlive: false
|
||||
}
|
||||
},
|
||||
// 任务详情(放在 asset-management 层级)
|
||||
{
|
||||
path: 'task-management/task-detail',
|
||||
|
||||
@@ -57,6 +57,8 @@ export enum RoutesAlias {
|
||||
// 任务管理
|
||||
IotCardTask = '/asset-management/task-management/iot-card-task', // IoT卡任务
|
||||
DeviceTask = '/asset-management/task-management/device-task', // 设备任务
|
||||
OrderPackageInvalidateTask = '/asset-management/task-management/order-package-invalidate-task', // 订单套餐批量作废任务
|
||||
OrderPackageInvalidateTaskDetail = '/asset-management/task-management/order-package-invalidate-task/detail', // 订单套餐作废任务详情
|
||||
TaskDetail = '/asset-management/task-management/task-detail', // 任务详情(IoT卡/设备任务详情)
|
||||
ExportTaskList = '/asset-management/export-task-management/export-task-list', // 导出列表
|
||||
ExportTaskDetail = '/asset-management/export-task-management/export-task-detail', // 导出任务详情
|
||||
|
||||
@@ -179,7 +179,7 @@ export const useUserStore = defineStore(
|
||||
persist: {
|
||||
key: 'user',
|
||||
storage: localStorage,
|
||||
paths: [
|
||||
pick: [
|
||||
'accessToken',
|
||||
'refreshToken',
|
||||
'isLogin',
|
||||
|
||||
@@ -502,7 +502,7 @@ export const useWorktabStore = defineStore(
|
||||
persist: {
|
||||
key: 'worktab',
|
||||
storage: localStorage,
|
||||
paths: ['opened'],
|
||||
pick: ['opened'],
|
||||
// 自定义序列化,只保存固定标签页
|
||||
serializer: {
|
||||
serialize: (value: any) => {
|
||||
|
||||
@@ -31,7 +31,8 @@ export interface AgentRecharge {
|
||||
payment_channel: AgentRechargePaymentChannel
|
||||
payment_config_id: number | null
|
||||
payment_transaction_id: string
|
||||
payment_voucher_key?: string // 凭证 file_key(仅 offline 支付时有值)
|
||||
payment_voucher_key?: string[] | string // 凭证附件列表;历史数据可能为单字符串或逗号字符串
|
||||
remark?: string // 运营备注
|
||||
created_at: string
|
||||
paid_at: string | null
|
||||
completed_at: string | null
|
||||
@@ -63,7 +64,8 @@ export interface CreateAgentRechargeRequest {
|
||||
amount: number // 充值金额(单位:分),最小 1(即 ¥0.01)
|
||||
payment_method: AgentRechargePaymentMethod
|
||||
shop_id: number
|
||||
payment_voucher_key?: string // 线下支付凭证(payment_method=offline 时必填)
|
||||
payment_voucher_key?: string[] // 线下支付凭证附件列表(payment_method=offline 时必填)
|
||||
remark?: string // 运营备注,最多 1000 字
|
||||
}
|
||||
|
||||
// 确认线下充值请求
|
||||
|
||||
@@ -39,6 +39,8 @@ export interface UserInfo {
|
||||
enterprise_name?: string // 企业名称(可选)
|
||||
shop_id?: number // 店铺ID(可选)
|
||||
shop_name?: string // 店铺名称(可选)
|
||||
roles?: string[]
|
||||
permissions?: string[]
|
||||
}
|
||||
|
||||
// 获取用户信息响应数据 (GET /api/admin/me)
|
||||
|
||||
@@ -296,6 +296,7 @@ export interface IotCardImportTask {
|
||||
carrier_name: string // 运营商名称
|
||||
file_name: string // 文件名
|
||||
file_key?: string // 原始导入文件存储键
|
||||
creator_name?: string // 创建人姓名
|
||||
card_category?: CardCategory // 卡业务类型
|
||||
status: IotCardImportTaskStatus // 任务状态
|
||||
status_text: string // 任务状态文本
|
||||
@@ -343,6 +344,7 @@ export enum StandaloneCardStatus {
|
||||
|
||||
// 单卡查询参数
|
||||
export interface StandaloneCardQueryParams extends PaginationParams {
|
||||
keyword?: string // 通用关键字搜索
|
||||
has_active_package?: boolean // has active package filter
|
||||
carrier_name?: string // carrier name filter
|
||||
is_standalone?: boolean // standalone filter
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface BaseResponse<T = any> {
|
||||
export interface PaginationParams {
|
||||
page?: number
|
||||
page_size?: number
|
||||
current?: number
|
||||
size?: number
|
||||
}
|
||||
|
||||
// 分页响应数据
|
||||
|
||||
@@ -217,6 +217,7 @@ export interface DeviceImportTask {
|
||||
batch_no: string // 批次号
|
||||
file_name: string // 文件名
|
||||
file_key?: string // 原始导入文件存储键
|
||||
creator_name?: string // 创建人姓名
|
||||
status: DeviceImportTaskStatus // 任务状态
|
||||
status_text: string // 任务状态文本
|
||||
total_count: number // 总数
|
||||
@@ -309,6 +310,7 @@ export interface SwitchCardRequest {
|
||||
|
||||
// 设置WiFi请求参数
|
||||
export interface SetWiFiRequest {
|
||||
card_no?: string // 设备资产标识
|
||||
enabled: boolean // 启用状态
|
||||
ssid: string // WiFi 名称(1-32字符)
|
||||
password: string // WiFi 密码(8-63字符)
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface EnterprisePageResult {
|
||||
|
||||
// 查询企业客户列表参数
|
||||
export interface EnterpriseQueryParams {
|
||||
id?: number
|
||||
page?: number
|
||||
page_size?: number
|
||||
enterprise_name?: string
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
// 通用类型
|
||||
export * from './common'
|
||||
export * from './request'
|
||||
|
||||
// 认证相关
|
||||
export * from './auth'
|
||||
@@ -90,7 +91,16 @@ export * from './alert'
|
||||
export * from './pollingConcurrency'
|
||||
|
||||
// 轮询配置管理相关
|
||||
export * from './pollingConfig'
|
||||
export type {
|
||||
CardCategory as PollingCardCategory,
|
||||
CardCondition,
|
||||
PollingConfig,
|
||||
PollingConfigQueryParams,
|
||||
PollingConfigListResponse,
|
||||
CreatePollingConfigRequest,
|
||||
UpdatePollingConfigRequest,
|
||||
UpdatePollingConfigStatusRequest
|
||||
} from './pollingConfig'
|
||||
|
||||
// 手动触发相关
|
||||
export * from './manualTrigger'
|
||||
@@ -100,3 +110,6 @@ export * from './pollingMonitor'
|
||||
|
||||
// 导出任务相关
|
||||
export * from './exportTask'
|
||||
|
||||
// 订单套餐批量作废任务相关
|
||||
export * from './orderPackageInvalidateTask'
|
||||
|
||||
@@ -83,7 +83,7 @@ export interface Order {
|
||||
seller_shop_id?: number | null // 销售店铺ID
|
||||
seller_shop_name?: string // 销售店铺名称
|
||||
items: OrderItem[] | null
|
||||
payment_voucher_key?: string // 支付凭证(线下支付时才有值)
|
||||
payment_voucher_key?: string[] | string // 支付凭证附件列表;历史数据可能为单字符串或逗号字符串
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
@@ -123,7 +123,7 @@ export interface CreateOrderRequest {
|
||||
identifier?: string // 资产标识符(ICCID 或 VirtualNo)
|
||||
package_ids: number[] // 套餐ID列表(前端限制单选,后端支持1-10个)
|
||||
payment_method: OrderPaymentMethod // 支付方式
|
||||
payment_voucher_key?: string // 线下支付凭证(仅 offline 必填)
|
||||
payment_voucher_key?: string[] // 线下支付凭证附件列表(仅 offline 必填)
|
||||
}
|
||||
|
||||
// 创建订单响应 (返回订单详情)
|
||||
|
||||
64
src/types/api/orderPackageInvalidateTask.ts
Normal file
64
src/types/api/orderPackageInvalidateTask.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { BaseResponse, PaginationParams } from './common'
|
||||
|
||||
export enum OrderPackageInvalidateTaskStatus {
|
||||
PENDING = 1,
|
||||
PROCESSING = 2,
|
||||
COMPLETED = 3,
|
||||
FAILED = 4
|
||||
}
|
||||
|
||||
export interface OrderPackageInvalidateTaskQueryParams extends PaginationParams {
|
||||
status?: OrderPackageInvalidateTaskStatus
|
||||
}
|
||||
|
||||
export interface OrderPackageInvalidateFailedItem {
|
||||
line: number
|
||||
order_no: string
|
||||
reason: string
|
||||
}
|
||||
|
||||
export interface OrderPackageInvalidateTask {
|
||||
id: number
|
||||
task_no: string
|
||||
status: OrderPackageInvalidateTaskStatus
|
||||
status_text?: string
|
||||
status_name?: string
|
||||
total_count: number
|
||||
success_count: number
|
||||
fail_count: number
|
||||
skip_count?: number
|
||||
file_name: string
|
||||
file_key?: string
|
||||
voucher_keys?: string[]
|
||||
remark?: string
|
||||
creator_name?: string
|
||||
error_message?: string
|
||||
created_at: string
|
||||
started_at?: string | null
|
||||
completed_at?: string | null
|
||||
}
|
||||
|
||||
export interface OrderPackageInvalidateTaskDetail extends OrderPackageInvalidateTask {
|
||||
failed_items?: OrderPackageInvalidateFailedItem[] | null
|
||||
}
|
||||
|
||||
export interface OrderPackageInvalidateTaskListResponse {
|
||||
items: OrderPackageInvalidateTask[]
|
||||
page: number
|
||||
page_size: number
|
||||
total: number
|
||||
total_pages?: number
|
||||
}
|
||||
|
||||
export interface CreateOrderPackageInvalidateTaskRequest {
|
||||
file_key: string
|
||||
file_name: string
|
||||
voucher_keys?: string[]
|
||||
remark?: string
|
||||
}
|
||||
|
||||
export type OrderPackageInvalidateTaskListApiResponse =
|
||||
BaseResponse<OrderPackageInvalidateTaskListResponse>
|
||||
export type OrderPackageInvalidateTaskDetailApiResponse =
|
||||
BaseResponse<OrderPackageInvalidateTaskDetail>
|
||||
export type CreateOrderPackageInvalidateTaskApiResponse = BaseResponse<OrderPackageInvalidateTask>
|
||||
@@ -26,7 +26,7 @@ export interface Refund {
|
||||
actual_received_amount?: number | null // 实收金额(分)
|
||||
status: RefundStatus
|
||||
refund_reason: string
|
||||
refund_voucher_key?: string // 退款凭证 file_key,多个以英文逗号分隔
|
||||
refund_voucher_key?: string[] | string // 退款凭证附件列表;历史数据可能为单字符串或逗号字符串
|
||||
reject_reason: string
|
||||
remark: string
|
||||
asset_reset: boolean
|
||||
@@ -64,7 +64,7 @@ export interface CreateRefundRequest {
|
||||
order_id: number
|
||||
requested_refund_amount: number // 申请退款金额(分)
|
||||
actual_received_amount: number // 实收金额(分)
|
||||
refund_voucher_key: string // 退款凭证对象存储 file_key,多个以英文逗号分隔
|
||||
refund_voucher_key: string[] // 退款凭证附件列表
|
||||
refund_reason?: string
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ export interface RejectRefundRequest {
|
||||
export interface ResubmitRefundRequest {
|
||||
requested_refund_amount?: number
|
||||
actual_received_amount?: number
|
||||
refund_voucher_key?: string[]
|
||||
refund_reason?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface ShopResponse {
|
||||
|
||||
// 店铺列表查询参数
|
||||
export interface ShopQueryParams extends PaginationParams {
|
||||
id?: number // 店铺ID
|
||||
shop_name?: string // 店铺名称模糊查询
|
||||
shop_code?: string // 店铺编号模糊查询
|
||||
parent_shop_name?: string // 上级店铺名称模糊查询
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
export * from './api'
|
||||
export * from './common'
|
||||
export * from './component'
|
||||
export * from './store'
|
||||
export type {
|
||||
UserInfo as StoreUserInfo,
|
||||
SystemThemeType,
|
||||
SystemThemeTypes,
|
||||
MenuThemeType,
|
||||
SettingState,
|
||||
WorkTab,
|
||||
UserState,
|
||||
SettingStoreState,
|
||||
WorkTabState,
|
||||
MenuState,
|
||||
RootState
|
||||
} from './store'
|
||||
export * from './router'
|
||||
export * from './config'
|
||||
|
||||
@@ -5,3 +5,4 @@
|
||||
export * from './format'
|
||||
export * from './validate'
|
||||
export * from './calculate'
|
||||
export * from './voucher'
|
||||
|
||||
37
src/utils/business/voucher.ts
Normal file
37
src/utils/business/voucher.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 凭证附件相关业务工具
|
||||
*/
|
||||
|
||||
export const toVoucherKeyList = (value?: string | string[]): string[] => {
|
||||
const values = Array.isArray(value) ? value : value?.split(',') || []
|
||||
|
||||
return values.map((key) => key.trim()).filter(Boolean)
|
||||
}
|
||||
|
||||
export const hasVoucherKeys = (value?: string | string[]): boolean =>
|
||||
toVoucherKeyList(value).length > 0
|
||||
|
||||
export const getErrorMessage = (error: unknown, fallback: string): string => {
|
||||
if (!error || typeof error !== 'object') return fallback
|
||||
|
||||
const maybeError = error as {
|
||||
msg?: unknown
|
||||
message?: unknown
|
||||
response?: {
|
||||
data?: {
|
||||
msg?: unknown
|
||||
message?: unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const candidates = [
|
||||
maybeError.response?.data?.msg,
|
||||
maybeError.response?.data?.message,
|
||||
maybeError.msg,
|
||||
maybeError.message
|
||||
]
|
||||
|
||||
const message = candidates.find((item): item is string => typeof item === 'string' && !!item)
|
||||
return message || fallback
|
||||
}
|
||||
@@ -91,7 +91,7 @@
|
||||
<ElFormItem label="所在地区" prop="region">
|
||||
<ElCascader
|
||||
v-model="form.region"
|
||||
:options="regionData"
|
||||
:options="regionData as CascaderOption[]"
|
||||
placeholder="请选择省/市/区"
|
||||
clearable
|
||||
filterable
|
||||
@@ -206,7 +206,7 @@
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { EnterpriseService, ShopService } from '@/api/modules'
|
||||
import { ElMessage, ElSwitch, ElCascader } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type { CascaderOption, CascaderValue, FormInstance, FormRules } from 'element-plus'
|
||||
import type { EnterpriseItem } from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
@@ -649,11 +649,12 @@
|
||||
}
|
||||
|
||||
// 处理地区选择变化
|
||||
const handleRegionChange = (value: string[]) => {
|
||||
if (value && value.length === 3) {
|
||||
form.province = value[0]
|
||||
form.city = value[1]
|
||||
form.district = value[2]
|
||||
const handleRegionChange = (value: CascaderValue | null | undefined) => {
|
||||
const region = Array.isArray(value) ? value.map(String) : []
|
||||
if (region.length === 3) {
|
||||
form.province = region[0]
|
||||
form.city = region[1]
|
||||
form.district = region[2]
|
||||
} else {
|
||||
form.province = ''
|
||||
form.city = ''
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
style="width: 100%"
|
||||
>
|
||||
<template v-if="!props.seriesId">
|
||||
<ElOption :value="null" disabled>
|
||||
<ElOption value="__NO_PACKAGE__" disabled>
|
||||
<span style="color: var(--el-text-color-warning)">
|
||||
该设备未关联套餐系列,无法购买套餐
|
||||
</span>
|
||||
@@ -125,6 +125,7 @@
|
||||
import type { CreateOrderRequest, PackageResponse } from '@/types/api'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
||||
import { hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
@@ -154,7 +155,7 @@
|
||||
const form = reactive({
|
||||
package_id: undefined as number | undefined,
|
||||
payment_method: 'wallet' as 'wallet' | 'offline',
|
||||
payment_voucher_key: undefined as string | undefined
|
||||
payment_voucher_key: [] as string[]
|
||||
})
|
||||
|
||||
const rules = computed<FormRules>(() => {
|
||||
@@ -317,7 +318,7 @@
|
||||
return
|
||||
}
|
||||
|
||||
if (form.payment_method === 'offline' && !form.payment_voucher_key) {
|
||||
if (form.payment_method === 'offline' && !hasVoucherKeys(form.payment_voucher_key)) {
|
||||
ElMessage.error('线下支付必须上传支付凭证')
|
||||
return
|
||||
}
|
||||
@@ -331,8 +332,8 @@
|
||||
payment_method: form.payment_method
|
||||
}
|
||||
|
||||
if (form.payment_method === 'offline' && form.payment_voucher_key) {
|
||||
data.payment_voucher_key = form.payment_voucher_key
|
||||
if (form.payment_method === 'offline' && hasVoucherKeys(form.payment_voucher_key)) {
|
||||
data.payment_voucher_key = toVoucherKeyList(form.payment_voucher_key)
|
||||
}
|
||||
|
||||
await OrderService.createOrder(data)
|
||||
@@ -360,7 +361,7 @@
|
||||
formRef.value?.resetFields()
|
||||
form.package_id = undefined
|
||||
form.payment_method = 'wallet'
|
||||
form.payment_voucher_key = undefined
|
||||
form.payment_voucher_key = []
|
||||
voucherUploading.value = false
|
||||
packageOptions.value = []
|
||||
packagesEmptyWithSeriesId.value = false
|
||||
|
||||
@@ -198,6 +198,7 @@
|
||||
import { useAssetOperations } from './composables/useAssetOperations'
|
||||
|
||||
// 引入类型
|
||||
import type { AssetPackageUsageRecord } from '@/types/api'
|
||||
import type { BindingCard, PackageInfo, SwitchCardForm, WiFiForm } from './types'
|
||||
|
||||
defineOptions({ name: 'SingleCard' })
|
||||
@@ -613,10 +614,10 @@
|
||||
/**
|
||||
* 显示流量详单
|
||||
*/
|
||||
const handleShowDailyRecords = (payload: { packageRow: PackageInfo }) => {
|
||||
const handleShowDailyRecords = (payload: { packageRow: AssetPackageUsageRecord }) => {
|
||||
const { packageRow } = payload
|
||||
selectedPackageUsageId.value = packageRow.package_usage_id ?? packageRow.id
|
||||
selectedPackageRow.value = packageRow
|
||||
selectedPackageUsageId.value = packageRow.package_usage_id ?? packageRow.id ?? null
|
||||
selectedPackageRow.value = packageRow as PackageInfo
|
||||
dailyRecordsDialogVisible.value = true
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
import { ExchangeService } from '@/api/modules'
|
||||
import type { ExchangeResponse } from '@/api/modules/exchange'
|
||||
import { ElTag, ElCard, ElButton, ElIcon } from 'element-plus'
|
||||
import type { TagProps } from 'element-plus'
|
||||
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import DetailPage from '@/components/common/DetailPage.vue'
|
||||
@@ -44,8 +45,8 @@
|
||||
const exchangeDetail = ref<ExchangeResponse | null>(null)
|
||||
const exchangeId = ref<number>(0)
|
||||
|
||||
const getStatusType = (status: number) => {
|
||||
const types: Record<number, string> = {
|
||||
const getStatusType = (status: number): TagProps['type'] => {
|
||||
const types: Record<number, TagProps['type']> = {
|
||||
1: 'warning',
|
||||
2: 'info',
|
||||
3: 'primary',
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
:label-width="90"
|
||||
label-width="90px"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import type { AssetAllocationRecord, AllocationTypeEnum, AssetTypeEnum } from '@/types/api/card'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
@@ -347,7 +346,7 @@
|
||||
const getTableData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
const params: Record<string, any> = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.pageSize,
|
||||
...formFilters
|
||||
|
||||
@@ -370,6 +370,13 @@
|
||||
minWidth: 180,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'creator_name',
|
||||
label: '操作人',
|
||||
width: 140,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: DeviceImportTask) => row.creator_name || '-'
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
|
||||
@@ -500,6 +500,13 @@
|
||||
minWidth: 250,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'creator_name',
|
||||
label: '操作人',
|
||||
width: 140,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: IotCardImportTask) => row.creator_name || '-'
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<div class="order-package-invalidate-task-detail-page">
|
||||
<ElCard shadow="never">
|
||||
<div class="detail-header">
|
||||
<ElButton @click="router.back()">返回</ElButton>
|
||||
<h2 class="detail-title">订单套餐作废任务详情</h2>
|
||||
</div>
|
||||
|
||||
<DetailPage v-if="taskDetail" :sections="detailSections" :data="taskDetail" />
|
||||
|
||||
<ElTable
|
||||
v-if="taskDetail?.failed_items?.length"
|
||||
:data="taskDetail.failed_items"
|
||||
border
|
||||
style="width: 100%; margin-top: 16px"
|
||||
>
|
||||
<ElTableColumn prop="line" label="行号" width="100" />
|
||||
<ElTableColumn prop="order_no" label="订单号" min-width="180" show-overflow-tooltip />
|
||||
<ElTableColumn prop="reason" label="失败原因" min-width="240" show-overflow-tooltip />
|
||||
</ElTable>
|
||||
<ElEmpty v-else-if="taskDetail" description="暂无失败明细" />
|
||||
</ElCard>
|
||||
|
||||
<PaymentVoucherDialog :file-keys="voucherPreviewKeys" @close="voucherPreviewKeys = []" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElButton, ElCard, ElEmpty, ElMessage, ElTable, ElTableColumn, ElTag } from 'element-plus'
|
||||
import DetailPage from '@/components/common/DetailPage.vue'
|
||||
import type { DetailSection } from '@/components/common/DetailPage.vue'
|
||||
import { OrderPackageInvalidateTaskService } from '@/api/modules'
|
||||
import type {
|
||||
OrderPackageInvalidateTaskDetail,
|
||||
OrderPackageInvalidateTaskStatus
|
||||
} from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
|
||||
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
|
||||
|
||||
defineOptions({ name: 'OrderPackageInvalidateTaskDetail' })
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const taskDetail = ref<OrderPackageInvalidateTaskDetail | null>(null)
|
||||
const voucherPreviewKeys = ref<string[]>([])
|
||||
|
||||
const getStatusType = (status: OrderPackageInvalidateTaskStatus) => {
|
||||
const statusMap: Record<number, 'info' | 'warning' | 'success' | 'danger'> = {
|
||||
1: 'info',
|
||||
2: 'warning',
|
||||
3: 'success',
|
||||
4: 'danger'
|
||||
}
|
||||
return statusMap[status] || 'info'
|
||||
}
|
||||
|
||||
const getStatusName = (task: OrderPackageInvalidateTaskDetail) =>
|
||||
task.status_text || task.status_name || '-'
|
||||
|
||||
const detailSections = computed<DetailSection[]>(() => [
|
||||
{
|
||||
title: '任务信息',
|
||||
columns: 2,
|
||||
fields: [
|
||||
{ label: '任务编号', prop: 'task_no' },
|
||||
{
|
||||
label: '任务状态',
|
||||
render: (data: OrderPackageInvalidateTaskDetail) =>
|
||||
h(ElTag, { type: getStatusType(data.status) }, () => getStatusName(data))
|
||||
},
|
||||
{ label: '文件名', prop: 'file_name', formatter: (value: string) => value || '-' },
|
||||
{ label: '操作人', prop: 'creator_name', formatter: (value: string) => value || '-' },
|
||||
{
|
||||
label: '凭证附件',
|
||||
render: (data: OrderPackageInvalidateTaskDetail) =>
|
||||
hasVoucherKeys(data.voucher_keys)
|
||||
? h(
|
||||
ElButton,
|
||||
{
|
||||
link: true,
|
||||
type: 'primary',
|
||||
onClick: () => (voucherPreviewKeys.value = toVoucherKeyList(data.voucher_keys))
|
||||
},
|
||||
() => '查看凭证'
|
||||
)
|
||||
: h('span', '-')
|
||||
},
|
||||
{
|
||||
label: '运营备注',
|
||||
prop: 'remark',
|
||||
formatter: (value: string) => value || '-',
|
||||
fullWidth: true
|
||||
},
|
||||
{
|
||||
label: '错误信息',
|
||||
prop: 'error_message',
|
||||
formatter: (value: string) => value || '-',
|
||||
fullWidth: true
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'created_at',
|
||||
formatter: (value: string) => formatDateTime(value)
|
||||
},
|
||||
{
|
||||
label: '开始时间',
|
||||
prop: 'started_at',
|
||||
formatter: (value: string) => (value ? formatDateTime(value) : '-')
|
||||
},
|
||||
{
|
||||
label: '完成时间',
|
||||
prop: 'completed_at',
|
||||
formatter: (value: string) => (value ? formatDateTime(value) : '-')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '统计信息',
|
||||
columns: 2,
|
||||
fields: [
|
||||
{ label: '总数', prop: 'total_count' },
|
||||
{ label: '成功数', prop: 'success_count' },
|
||||
{ label: '失败数', prop: 'fail_count' },
|
||||
{ label: '跳过数', prop: 'skip_count', formatter: (value: number) => String(value ?? 0) }
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
const getTaskDetail = async () => {
|
||||
const taskId = Number(route.query.id)
|
||||
if (!taskId) {
|
||||
ElMessage.error('任务ID无效')
|
||||
return
|
||||
}
|
||||
const res = await OrderPackageInvalidateTaskService.getTaskDetail(taskId)
|
||||
if (res.code === 0) {
|
||||
taskDetail.value = res.data
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getTaskDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.order-package-invalidate-task-detail-page {
|
||||
padding: 20px;
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,476 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="order-package-invalidate-task-page" id="table-full-screen">
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
:show-expand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:icon="Upload"
|
||||
@click="createDialogVisible = true"
|
||||
v-permission="'order_package_invalidate_task:create'"
|
||||
>
|
||||
新建作废任务
|
||||
</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="taskList"
|
||||
:currentPage="pagination.page"
|
||||
:pageSize="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:actions="getActions"
|
||||
:actionsWidth="180"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<ElDialog
|
||||
v-model="createDialogVisible"
|
||||
title="新建订单套餐作废任务"
|
||||
width="560px"
|
||||
@closed="handleCreateDialogClosed"
|
||||
>
|
||||
<ElForm ref="createFormRef" :model="createForm" :rules="createRules" label-width="100px">
|
||||
<ElFormItem label="CSV文件" prop="file">
|
||||
<ElUpload
|
||||
ref="uploadRef"
|
||||
drag
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
accept=".csv,text/csv"
|
||||
:on-change="handleFileChange"
|
||||
:on-remove="handleFileRemove"
|
||||
>
|
||||
<ElIcon class="el-icon--upload"><UploadFilled /></ElIcon>
|
||||
<div class="el-upload__text">将 CSV 文件拖到此处,或<em>点击选择</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">仅支持 .csv 文件,必填列:order_no</div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="凭证附件">
|
||||
<VoucherUpload
|
||||
ref="voucherUploadRef"
|
||||
v-model="createForm.voucher_keys"
|
||||
voucher-name="凭证附件"
|
||||
@uploading-change="voucherUploading = $event"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="运营备注" prop="remark">
|
||||
<ElInput
|
||||
v-model="createForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
placeholder="请输入运营备注(可选)"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="createDialogVisible = false">取消</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:loading="creating || voucherUploading"
|
||||
:disabled="voucherUploading"
|
||||
@click="handleCreateTask"
|
||||
>
|
||||
{{ voucherUploading ? '凭证上传中...' : '确认创建' }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<PaymentVoucherDialog :file-keys="voucherPreviewKeys" @close="voucherPreviewKeys = []" />
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElIcon,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElTag,
|
||||
ElUpload
|
||||
} from 'element-plus'
|
||||
import { Upload, UploadFilled } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules, UploadFile, UploadInstance } from 'element-plus'
|
||||
import { OrderPackageInvalidateTaskService, StorageService } from '@/api/modules'
|
||||
import type {
|
||||
CreateOrderPackageInvalidateTaskRequest,
|
||||
OrderPackageInvalidateTask,
|
||||
OrderPackageInvalidateTaskQueryParams,
|
||||
OrderPackageInvalidateTaskStatus
|
||||
} from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { getErrorMessage, hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
|
||||
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
||||
|
||||
defineOptions({ name: 'OrderPackageInvalidateTask' })
|
||||
|
||||
const router = useRouter()
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
const loading = ref(false)
|
||||
const creating = ref(false)
|
||||
const voucherUploading = ref(false)
|
||||
const tableRef = ref()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
const voucherUploadRef = ref<InstanceType<typeof VoucherUpload>>()
|
||||
const createFormRef = ref<FormInstance>()
|
||||
const createDialogVisible = ref(false)
|
||||
const selectedFile = ref<File | null>(null)
|
||||
const taskList = ref<OrderPackageInvalidateTask[]>([])
|
||||
const voucherPreviewKeys = ref<string[]>([])
|
||||
|
||||
const searchForm = reactive<OrderPackageInvalidateTaskQueryParams>({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
status: undefined
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const createForm = reactive({
|
||||
file: '',
|
||||
voucher_keys: [] as string[],
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const createRules = reactive<FormRules>({
|
||||
file: [{ required: true, message: '请选择 CSV 文件', trigger: 'change' }],
|
||||
remark: [{ max: 1000, message: '运营备注最多1000字', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const searchFormItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '任务状态',
|
||||
prop: 'status',
|
||||
type: 'select',
|
||||
config: { clearable: true, placeholder: '全部' },
|
||||
options: [
|
||||
{ label: '待处理', value: 1 },
|
||||
{ label: '处理中', value: 2 },
|
||||
{ label: '已完成', value: 3 },
|
||||
{ label: '失败', value: 4 }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const columnOptions = [
|
||||
{ label: '任务编号', prop: 'task_no' },
|
||||
{ label: '任务状态', prop: 'status' },
|
||||
{ label: '总数', prop: 'total_count' },
|
||||
{ label: '成功数', prop: 'success_count' },
|
||||
{ label: '失败数', prop: 'fail_count' },
|
||||
{ label: '跳过数', prop: 'skip_count' },
|
||||
{ label: '文件名', prop: 'file_name' },
|
||||
{ label: '凭证', prop: 'voucher_keys' },
|
||||
{ label: '运营备注', prop: 'remark' },
|
||||
{ label: '操作人', prop: 'creator_name' },
|
||||
{ label: '错误信息', prop: 'error_message' },
|
||||
{ label: '创建时间', prop: 'created_at' },
|
||||
{ label: '开始时间', prop: 'started_at' },
|
||||
{ label: '完成时间', prop: 'completed_at' }
|
||||
]
|
||||
|
||||
const getStatusType = (status: OrderPackageInvalidateTaskStatus) => {
|
||||
const statusMap: Record<number, 'info' | 'warning' | 'success' | 'danger'> = {
|
||||
1: 'info',
|
||||
2: 'warning',
|
||||
3: 'success',
|
||||
4: 'danger'
|
||||
}
|
||||
return statusMap[status] || 'info'
|
||||
}
|
||||
|
||||
const getStatusName = (task: OrderPackageInvalidateTask) =>
|
||||
task.status_text || task.status_name || '-'
|
||||
|
||||
const handleNameClick = (row: OrderPackageInvalidateTask) => {
|
||||
if (!hasAuth('order_package_invalidate_task:detail')) {
|
||||
ElMessage.warning('您没有查看详情的权限')
|
||||
return
|
||||
}
|
||||
router.push({
|
||||
path: RoutesAlias.OrderPackageInvalidateTaskDetail,
|
||||
query: { id: row.id }
|
||||
})
|
||||
}
|
||||
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'task_no',
|
||||
label: '任务编号',
|
||||
width: 220,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: OrderPackageInvalidateTask) =>
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
|
||||
onClick: (event: MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
handleNameClick(row)
|
||||
}
|
||||
},
|
||||
row.task_no
|
||||
)
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '任务状态',
|
||||
width: 100,
|
||||
formatter: (row: OrderPackageInvalidateTask) =>
|
||||
h(ElTag, { type: getStatusType(row.status) }, () => getStatusName(row))
|
||||
},
|
||||
{ prop: 'total_count', label: '总数', width: 80 },
|
||||
{
|
||||
prop: 'success_count',
|
||||
label: '成功数',
|
||||
width: 80,
|
||||
formatter: (row: OrderPackageInvalidateTask) =>
|
||||
h('span', { style: { color: 'var(--el-color-success)' } }, row.success_count)
|
||||
},
|
||||
{
|
||||
prop: 'fail_count',
|
||||
label: '失败数',
|
||||
width: 80,
|
||||
formatter: (row: OrderPackageInvalidateTask) =>
|
||||
h('span', { style: { color: 'var(--el-color-danger)' } }, row.fail_count)
|
||||
},
|
||||
{
|
||||
prop: 'skip_count',
|
||||
label: '跳过数',
|
||||
width: 80,
|
||||
formatter: (row: OrderPackageInvalidateTask) => row.skip_count ?? 0
|
||||
},
|
||||
{ prop: 'file_name', label: '文件名', minWidth: 180, showOverflowTooltip: true },
|
||||
{
|
||||
prop: 'voucher_keys',
|
||||
label: '凭证',
|
||||
width: 100,
|
||||
formatter: (row: OrderPackageInvalidateTask) =>
|
||||
hasVoucherKeys(row.voucher_keys)
|
||||
? h(
|
||||
ElButton,
|
||||
{
|
||||
link: true,
|
||||
type: 'primary',
|
||||
onClick: () => (voucherPreviewKeys.value = toVoucherKeyList(row.voucher_keys))
|
||||
},
|
||||
() => '查看'
|
||||
)
|
||||
: '-'
|
||||
},
|
||||
{
|
||||
prop: 'remark',
|
||||
label: '运营备注',
|
||||
minWidth: 180,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: OrderPackageInvalidateTask) => row.remark || '-'
|
||||
},
|
||||
{
|
||||
prop: 'creator_name',
|
||||
label: '操作人',
|
||||
width: 140,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: OrderPackageInvalidateTask) => row.creator_name || '-'
|
||||
},
|
||||
{
|
||||
prop: 'error_message',
|
||||
label: '错误信息',
|
||||
minWidth: 200,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: OrderPackageInvalidateTask) => row.error_message || '-'
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
width: 180,
|
||||
formatter: (row: OrderPackageInvalidateTask) => formatDateTime(row.created_at)
|
||||
},
|
||||
{
|
||||
prop: 'started_at',
|
||||
label: '开始时间',
|
||||
width: 180,
|
||||
formatter: (row: OrderPackageInvalidateTask) =>
|
||||
row.started_at ? formatDateTime(row.started_at) : '-'
|
||||
},
|
||||
{
|
||||
prop: 'completed_at',
|
||||
label: '完成时间',
|
||||
width: 180,
|
||||
formatter: (row: OrderPackageInvalidateTask) =>
|
||||
row.completed_at ? formatDateTime(row.completed_at) : '-'
|
||||
}
|
||||
])
|
||||
|
||||
const getTableData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await OrderPackageInvalidateTaskService.getTasks({
|
||||
page: pagination.page,
|
||||
page_size: pagination.page_size,
|
||||
status: searchForm.status
|
||||
})
|
||||
if (res.code === 0) {
|
||||
taskList.value = res.data.items || []
|
||||
pagination.total = res.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载订单套餐作废任务失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.status = undefined
|
||||
pagination.page = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleRefresh = () => getTableData()
|
||||
const handleSizeChange = (pageSize: number) => {
|
||||
pagination.page_size = pageSize
|
||||
getTableData()
|
||||
}
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleFileChange = (uploadFile: UploadFile) => {
|
||||
const file = uploadFile.raw
|
||||
if (!file) return
|
||||
if (!/\.csv$/i.test(file.name)) {
|
||||
ElMessage.error('仅支持 CSV 文件')
|
||||
uploadRef.value?.clearFiles()
|
||||
selectedFile.value = null
|
||||
createForm.file = ''
|
||||
return
|
||||
}
|
||||
selectedFile.value = file
|
||||
createForm.file = file.name
|
||||
createFormRef.value?.validateField('file')
|
||||
}
|
||||
|
||||
const handleFileRemove = () => {
|
||||
selectedFile.value = null
|
||||
createForm.file = ''
|
||||
createFormRef.value?.validateField('file')
|
||||
}
|
||||
|
||||
const uploadCsvFile = async (file: File) => {
|
||||
const contentType = file.type || 'text/csv'
|
||||
const uploadUrlRes = await StorageService.getUploadUrl({
|
||||
file_name: file.name,
|
||||
content_type: contentType,
|
||||
purpose: 'attachment'
|
||||
})
|
||||
if (uploadUrlRes.code !== 0) {
|
||||
throw new Error(uploadUrlRes.msg || '获取上传地址失败')
|
||||
}
|
||||
|
||||
await StorageService.uploadFile(uploadUrlRes.data.upload_url, file, contentType)
|
||||
return uploadUrlRes.data.file_key
|
||||
}
|
||||
|
||||
const handleCreateTask = async () => {
|
||||
if (!createFormRef.value) return
|
||||
if (voucherUploading.value) {
|
||||
ElMessage.warning('凭证上传中,请稍候')
|
||||
return
|
||||
}
|
||||
|
||||
const valid = await createFormRef.value.validate().catch(() => false)
|
||||
if (!valid || !selectedFile.value) return
|
||||
|
||||
creating.value = true
|
||||
try {
|
||||
const fileKey = await uploadCsvFile(selectedFile.value)
|
||||
const data: CreateOrderPackageInvalidateTaskRequest = {
|
||||
file_key: fileKey,
|
||||
file_name: selectedFile.value.name,
|
||||
voucher_keys: toVoucherKeyList(createForm.voucher_keys),
|
||||
remark: createForm.remark || undefined
|
||||
}
|
||||
await OrderPackageInvalidateTaskService.createTask(data)
|
||||
ElMessage.success('任务创建成功')
|
||||
createDialogVisible.value = false
|
||||
await getTableData()
|
||||
} catch (error) {
|
||||
console.error('创建订单套餐作废任务失败:', error)
|
||||
ElMessage.error(getErrorMessage(error, '任务创建失败'))
|
||||
} finally {
|
||||
creating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreateDialogClosed = () => {
|
||||
createFormRef.value?.resetFields()
|
||||
uploadRef.value?.clearFiles()
|
||||
voucherUploadRef.value?.clearFiles(false)
|
||||
selectedFile.value = null
|
||||
createForm.file = ''
|
||||
createForm.voucher_keys = []
|
||||
createForm.remark = ''
|
||||
voucherUploading.value = false
|
||||
}
|
||||
|
||||
const getActions = (row: OrderPackageInvalidateTask) => {
|
||||
const actions: any[] = []
|
||||
if (hasAuth('order_package_invalidate_task:detail')) {
|
||||
actions.push({ label: '详情', handler: () => handleNameClick(row), type: 'primary' })
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
})
|
||||
</script>
|
||||
@@ -21,7 +21,7 @@
|
||||
<ElDivider content-position="left">
|
||||
<span class="section-title">失败记录 ({{ taskDetail.fail_count }})</span>
|
||||
</ElDivider>
|
||||
<ElTable :data="taskDetail.failed_items" border style="width: 100%">
|
||||
<ElTable :data="taskDetail.failed_items || []" border style="width: 100%">
|
||||
<ElTableColumn prop="line" label="行号" width="100" />
|
||||
<ElTableColumn v-if="taskType === 'card'" prop="iccid" label="ICCID" min-width="180" />
|
||||
<ElTableColumn v-else prop="virtual_no" label="设备号" min-width="180" />
|
||||
@@ -34,7 +34,7 @@
|
||||
<ElDivider content-position="left">
|
||||
<span class="section-title">跳过记录 ({{ taskDetail.skip_count }})</span>
|
||||
</ElDivider>
|
||||
<ElTable :data="taskDetail.skipped_items" border style="width: 100%">
|
||||
<ElTable :data="taskDetail.skipped_items || []" border style="width: 100%">
|
||||
<ElTableColumn prop="line" label="行号" width="100" />
|
||||
<ElTableColumn v-if="taskType === 'card'" prop="iccid" label="ICCID" min-width="180" />
|
||||
<ElTableColumn v-else prop="virtual_no" label="设备号" min-width="180" />
|
||||
@@ -52,9 +52,10 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { CardService, DeviceService } from '@/api/modules'
|
||||
import { ElDivider, ElIcon, ElMessage, ElTable, ElTableColumn, ElTag } from 'element-plus'
|
||||
import type { TagProps } from 'element-plus'
|
||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import type { IotCardImportTaskDetail, IotCardImportTaskStatus } from '@/types/api/card'
|
||||
import type { IotCardImportTaskDetail } from '@/types/api/card'
|
||||
import type { DeviceImportTaskDetail } from '@/types/api/device'
|
||||
import type { DetailSection } from '@/components/common/DetailPage.vue'
|
||||
import DetailPage from '@/components/common/DetailPage.vue'
|
||||
@@ -72,7 +73,7 @@
|
||||
const taskType = ref<TaskType>('card')
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusType = (status?: IotCardImportTaskStatus) => {
|
||||
const getStatusType = (status?: number): TagProps['type'] => {
|
||||
if (!status) return 'info'
|
||||
switch (status) {
|
||||
case 1:
|
||||
@@ -107,6 +108,11 @@
|
||||
},
|
||||
{ label: '批次号', prop: 'batch_no', formatter: (value: string) => value || '-' },
|
||||
{ label: '文件名', prop: 'file_name', formatter: (value: string) => value || '-' },
|
||||
{
|
||||
label: '操作人',
|
||||
prop: 'creator_name',
|
||||
formatter: (value: string) => value || '-'
|
||||
},
|
||||
{
|
||||
label: '任务状态',
|
||||
render: (data: TaskDetail) => {
|
||||
|
||||
@@ -106,7 +106,10 @@
|
||||
"
|
||||
>
|
||||
{{
|
||||
scope.row.status_name || CommissionStatusMap[scope.row.status]?.label || '-'
|
||||
scope.row.status_name ||
|
||||
CommissionStatusMap[scope.row.status as keyof typeof CommissionStatusMap]
|
||||
?.label ||
|
||||
'-'
|
||||
}}
|
||||
</ElTag>
|
||||
</template>
|
||||
@@ -1057,13 +1060,13 @@
|
||||
.main-wallet-filter__actions {
|
||||
:deep(.el-form-item__content) {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
@media (width <= 1200px) {
|
||||
.main-wallet-filter__grid {
|
||||
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
||||
}
|
||||
@@ -1075,7 +1078,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (width <= 768px) {
|
||||
.main-wallet-filter__grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -210,13 +210,11 @@
|
||||
import { WithdrawalStatusMap, WithdrawalMethodMap } from '@/config/constants/commission'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { formatDateTime, formatMoney } from '@/utils/business/format'
|
||||
|
||||
defineOptions({ name: 'MyCommission' })
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 获取当前用户的 shop_id
|
||||
@@ -365,14 +363,6 @@
|
||||
one_time: { label: '一次性佣金', type: 'success' as const }
|
||||
}
|
||||
|
||||
// 状态映射
|
||||
const CommissionRecordStatusMap = {
|
||||
1: { label: '已冻结', type: 'info' as const },
|
||||
2: { label: '解冻中', type: 'warning' as const },
|
||||
3: { label: '已发放', type: 'success' as const },
|
||||
4: { label: '已失效', type: 'danger' as const }
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks: commissionColumnChecks, columns: commissionColumns } = useCheckedColumns(
|
||||
() => [
|
||||
@@ -809,7 +799,7 @@
|
||||
params.bank_name = withdrawalForm.bank_name
|
||||
}
|
||||
|
||||
await CommissionService.submitShopWithdrawalRequest(currentShopId.value, params)
|
||||
await CommissionService.submitShopWithdrawalRequest(currentShopId.value!, params)
|
||||
ElMessage.success('提现申请提交成功')
|
||||
withdrawalDialogVisible.value = false
|
||||
|
||||
|
||||
@@ -351,8 +351,8 @@
|
||||
}
|
||||
|
||||
// 列配置
|
||||
const columns = computed(() => {
|
||||
const baseColumns = [
|
||||
const columns = computed<any[]>(() => {
|
||||
const baseColumns: any[] = [
|
||||
{
|
||||
prop: 'username',
|
||||
label: '用户名'
|
||||
@@ -378,14 +378,14 @@
|
||||
baseColumns.push({
|
||||
prop: 'shop_name',
|
||||
label: '店铺名称',
|
||||
formatter: (row: PlatformAccount) => (row as any).shop_name || '-'
|
||||
formatter: (row: PlatformAccount) => h('span', (row as any).shop_name || '-')
|
||||
})
|
||||
} else {
|
||||
// 企业类型:显示企业名称
|
||||
baseColumns.push({
|
||||
prop: 'enterprise_name',
|
||||
label: '企业名称',
|
||||
formatter: (row: PlatformAccount) => (row as any).enterprise_name || '-'
|
||||
formatter: (row: PlatformAccount) => h('span', (row as any).enterprise_name || '-')
|
||||
})
|
||||
}
|
||||
|
||||
@@ -401,7 +401,7 @@
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
formatter: (row: PlatformAccount) => formatDateTime((row as any).created_at)
|
||||
formatter: (row: PlatformAccount) => h('span', formatDateTime((row as any).created_at))
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
|
||||
@@ -22,7 +22,10 @@
|
||||
</div>
|
||||
</ElCard>
|
||||
|
||||
<PaymentVoucherDialog :file-key="paymentVoucherFileKey" @close="paymentVoucherFileKey = ''" />
|
||||
<PaymentVoucherDialog
|
||||
:file-keys="paymentVoucherFileKeys"
|
||||
@close="paymentVoucherFileKeys = []"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -36,6 +39,7 @@
|
||||
import { AgentRechargeService } from '@/api/modules'
|
||||
import type { AgentRecharge, AgentRechargeStatus, AgentRechargePaymentMethod } from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
|
||||
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
|
||||
|
||||
defineOptions({ name: 'AgentRechargeDetail' })
|
||||
@@ -45,7 +49,7 @@
|
||||
|
||||
const loading = ref(false)
|
||||
const detailData = ref<AgentRecharge | null>(null)
|
||||
const paymentVoucherFileKey = ref('')
|
||||
const paymentVoucherFileKeys = ref<string[]>([])
|
||||
|
||||
const pageTitle = computed(() => `充值订单详情`)
|
||||
|
||||
@@ -132,19 +136,25 @@
|
||||
label: '支付凭证',
|
||||
fullWidth: true,
|
||||
render: (data) =>
|
||||
data.payment_voucher_key
|
||||
hasVoucherKeys(data.payment_voucher_key)
|
||||
? h(
|
||||
ElButton,
|
||||
{
|
||||
type: 'primary',
|
||||
link: true,
|
||||
onClick: () => {
|
||||
paymentVoucherFileKey.value = data.payment_voucher_key
|
||||
paymentVoucherFileKeys.value = toVoucherKeyList(data.payment_voucher_key)
|
||||
}
|
||||
},
|
||||
() => '查看支付凭证'
|
||||
)
|
||||
: h('span', '-')
|
||||
},
|
||||
{
|
||||
label: '运营备注',
|
||||
prop: 'remark',
|
||||
formatter: (value) => value || '-',
|
||||
fullWidth: true
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -103,6 +103,16 @@
|
||||
@change="createFormRef?.validateField('payment_voucher_key')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="运营备注" prop="remark">
|
||||
<ElInput
|
||||
v-model="createForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
placeholder="请输入运营备注(可选)"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
@@ -159,8 +169,8 @@
|
||||
|
||||
<!-- 支付凭证预览 -->
|
||||
<PaymentVoucherDialog
|
||||
:file-key="paymentVoucherFileKey"
|
||||
@close="paymentVoucherFileKey = ''"
|
||||
:file-keys="paymentVoucherFileKeys"
|
||||
@close="paymentVoucherFileKeys = []"
|
||||
/>
|
||||
</ElCard>
|
||||
</div>
|
||||
@@ -176,6 +186,7 @@
|
||||
ElTag,
|
||||
ElButton,
|
||||
ElCascader,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElSelect,
|
||||
ElOption
|
||||
@@ -192,6 +203,7 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
|
||||
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
||||
@@ -209,7 +221,7 @@
|
||||
const createDialogVisible = ref(false)
|
||||
const confirmPayDialogVisible = ref(false)
|
||||
const currentRecharge = ref<AgentRecharge | null>(null)
|
||||
const paymentVoucherFileKey = ref('')
|
||||
const paymentVoucherFileKeys = ref<string[]>([])
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState: AgentRechargeQueryParams = {
|
||||
@@ -366,12 +378,14 @@
|
||||
amount: number
|
||||
payment_method: string
|
||||
shop_id: number | null
|
||||
payment_voucher_key?: string
|
||||
payment_voucher_key: string[]
|
||||
remark: string
|
||||
}>({
|
||||
amount: MIN_RECHARGE_AMOUNT,
|
||||
payment_method: '',
|
||||
shop_id: null,
|
||||
payment_voucher_key: undefined
|
||||
payment_voucher_key: [],
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const confirmPayForm = reactive<ConfirmOfflinePaymentRequest>({
|
||||
@@ -475,6 +489,13 @@
|
||||
width: 120,
|
||||
formatter: (row: AgentRecharge) => row.payment_channel || '-'
|
||||
},
|
||||
{
|
||||
prop: 'remark',
|
||||
label: '运营备注',
|
||||
minWidth: 180,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: AgentRecharge) => row.remark || '-'
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
@@ -628,7 +649,8 @@
|
||||
createForm.amount = MIN_RECHARGE_AMOUNT
|
||||
createForm.payment_method = ''
|
||||
createForm.shop_id = null
|
||||
createForm.payment_voucher_key = undefined
|
||||
createForm.payment_voucher_key = []
|
||||
createForm.remark = ''
|
||||
voucherUploading.value = false
|
||||
uploadRef.value?.clearFiles(false)
|
||||
}
|
||||
@@ -649,11 +671,15 @@
|
||||
const data: CreateAgentRechargeRequest = {
|
||||
amount: Math.round(createForm.amount * 100), // 元转分
|
||||
payment_method: createForm.payment_method as AgentRechargePaymentMethod,
|
||||
shop_id: createForm.shop_id!
|
||||
shop_id: createForm.shop_id!,
|
||||
remark: createForm.remark || undefined
|
||||
}
|
||||
|
||||
if (createForm.payment_method === 'offline' && createForm.payment_voucher_key) {
|
||||
data.payment_voucher_key = createForm.payment_voucher_key
|
||||
if (
|
||||
createForm.payment_method === 'offline' &&
|
||||
hasVoucherKeys(createForm.payment_voucher_key)
|
||||
) {
|
||||
data.payment_voucher_key = toVoucherKeyList(createForm.payment_voucher_key)
|
||||
}
|
||||
|
||||
await AgentRechargeService.createAgentRecharge(data)
|
||||
@@ -733,7 +759,7 @@
|
||||
// 线下支付且有凭证可以查看
|
||||
if (
|
||||
row.payment_method === 'offline' &&
|
||||
row.payment_voucher_key &&
|
||||
hasVoucherKeys(row.payment_voucher_key) &&
|
||||
hasAuth('agent_recharge:view_payment_voucher')
|
||||
) {
|
||||
actions.push({
|
||||
@@ -761,8 +787,8 @@
|
||||
|
||||
// 查看支付凭证
|
||||
const handleViewPaymentVoucher = (row: AgentRecharge) => {
|
||||
if (!row.payment_voucher_key) return
|
||||
paymentVoucherFileKey.value = row.payment_voucher_key
|
||||
if (!hasVoucherKeys(row.payment_voucher_key)) return
|
||||
paymentVoucherFileKeys.value = toVoucherKeyList(row.payment_voucher_key)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
</div>
|
||||
</ElCard>
|
||||
|
||||
<PaymentVoucherDialog :file-key="refundVoucherFileKey" @close="refundVoucherFileKey = ''" />
|
||||
<PaymentVoucherDialog :file-keys="refundVoucherFileKeys" @close="refundVoucherFileKeys = []" />
|
||||
|
||||
<!-- 审批通过对话框 -->
|
||||
<ElDialog
|
||||
@@ -150,12 +150,26 @@
|
||||
placeholder="请输入退款原因"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="退款凭证" prop="refund_voucher_key">
|
||||
<VoucherUpload
|
||||
ref="resubmitUploadRef"
|
||||
v-model="resubmitForm.refund_voucher_key"
|
||||
voucher-name="退款凭证"
|
||||
@uploading-change="resubmitVoucherUploading = $event"
|
||||
@change="resubmitFormRef?.validateField('refund_voucher_key')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="resubmitDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleResubmitRefund" :loading="resubmitLoading">
|
||||
确认提交
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="handleResubmitRefund"
|
||||
:loading="resubmitLoading || resubmitVoucherUploading"
|
||||
:disabled="resubmitVoucherUploading"
|
||||
>
|
||||
{{ resubmitVoucherUploading ? '凭证上传中...' : '确认提交' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
@@ -179,7 +193,9 @@
|
||||
ResubmitRefundRequest
|
||||
} from '@/types/api'
|
||||
import { formatDateTime, yuanToFen } from '@/utils/business/format'
|
||||
import { hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
|
||||
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
|
||||
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
||||
|
||||
defineOptions({ name: 'RefundDetail' })
|
||||
|
||||
@@ -188,7 +204,7 @@
|
||||
|
||||
const loading = ref(false)
|
||||
const refund = ref<Refund | null>(null)
|
||||
const refundVoucherFileKey = ref('')
|
||||
const refundVoucherFileKeys = ref<string[]>([])
|
||||
|
||||
const pageTitle = computed(() => `退款详情`)
|
||||
|
||||
@@ -199,10 +215,12 @@
|
||||
const approveLoading = ref(false)
|
||||
const rejectLoading = ref(false)
|
||||
const resubmitLoading = ref(false)
|
||||
const resubmitVoucherUploading = ref(false)
|
||||
|
||||
const approveFormRef = ref()
|
||||
const rejectFormRef = ref()
|
||||
const resubmitFormRef = ref()
|
||||
const resubmitUploadRef = ref<InstanceType<typeof VoucherUpload>>()
|
||||
|
||||
const approveRules = ref()
|
||||
const rejectRules = ref({
|
||||
@@ -222,10 +240,12 @@
|
||||
const resubmitForm = reactive<{
|
||||
requested_refund_amount?: number
|
||||
actual_received_amount?: number
|
||||
refund_voucher_key: string[]
|
||||
refund_reason?: string
|
||||
}>({
|
||||
requested_refund_amount: undefined,
|
||||
actual_received_amount: undefined,
|
||||
refund_voucher_key: [],
|
||||
refund_reason: ''
|
||||
})
|
||||
|
||||
@@ -295,14 +315,14 @@
|
||||
label: '退款凭证',
|
||||
fullWidth: true,
|
||||
render: (data) =>
|
||||
data.refund_voucher_key
|
||||
hasVoucherKeys(data.refund_voucher_key)
|
||||
? h(
|
||||
ElButton,
|
||||
{
|
||||
type: 'primary',
|
||||
link: true,
|
||||
onClick: () => {
|
||||
refundVoucherFileKey.value = data.refund_voucher_key
|
||||
refundVoucherFileKeys.value = toVoucherKeyList(data.refund_voucher_key)
|
||||
}
|
||||
},
|
||||
() => '查看退款凭证'
|
||||
@@ -411,7 +431,10 @@
|
||||
resubmitFormRef.value?.resetFields()
|
||||
resubmitForm.requested_refund_amount = undefined
|
||||
resubmitForm.actual_received_amount = undefined
|
||||
resubmitForm.refund_voucher_key = []
|
||||
resubmitForm.refund_reason = ''
|
||||
resubmitVoucherUploading.value = false
|
||||
resubmitUploadRef.value?.clearFiles(false)
|
||||
}
|
||||
|
||||
const handleApproveRefund = async () => {
|
||||
@@ -463,6 +486,10 @@
|
||||
|
||||
const handleResubmitRefund = async () => {
|
||||
if (!resubmitFormRef.value || !refund.value) return
|
||||
if (resubmitVoucherUploading.value) {
|
||||
ElMessage.warning('退款凭证上传中,请稍候')
|
||||
return
|
||||
}
|
||||
|
||||
const refundId = refund.value.id
|
||||
await resubmitFormRef.value.validate(async (valid: boolean) => {
|
||||
@@ -474,6 +501,9 @@
|
||||
actual_received_amount: yuanToFen(resubmitForm.actual_received_amount),
|
||||
refund_reason: resubmitForm.refund_reason || undefined
|
||||
}
|
||||
if (hasVoucherKeys(resubmitForm.refund_voucher_key)) {
|
||||
data.refund_voucher_key = toVoucherKeyList(resubmitForm.refund_voucher_key)
|
||||
}
|
||||
|
||||
await RefundService.resubmitRefund(refundId, data)
|
||||
ElMessage.success('重新提交成功')
|
||||
|
||||
@@ -49,7 +49,10 @@
|
||||
<CreateRefundDialog v-model="createDialogVisible" @success="handleCreateSuccess" />
|
||||
|
||||
<!-- 退款凭证预览 -->
|
||||
<PaymentVoucherDialog :file-key="refundVoucherFileKey" @close="refundVoucherFileKey = ''" />
|
||||
<PaymentVoucherDialog
|
||||
:file-keys="refundVoucherFileKeys"
|
||||
@close="refundVoucherFileKeys = []"
|
||||
/>
|
||||
|
||||
<!-- 审批通过对话框 -->
|
||||
<ElDialog
|
||||
@@ -217,12 +220,26 @@
|
||||
placeholder="请输入退款原因"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="退款凭证" prop="refund_voucher_key">
|
||||
<VoucherUpload
|
||||
ref="resubmitUploadRef"
|
||||
v-model="resubmitForm.refund_voucher_key"
|
||||
voucher-name="退款凭证"
|
||||
@uploading-change="resubmitVoucherUploading = $event"
|
||||
@change="resubmitFormRef?.validateField('refund_voucher_key')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="resubmitDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleResubmitRefund" :loading="resubmitLoading">
|
||||
确认提交
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="handleResubmitRefund"
|
||||
:loading="resubmitLoading || resubmitVoucherUploading"
|
||||
:disabled="resubmitVoucherUploading"
|
||||
>
|
||||
{{ resubmitVoucherUploading ? '凭证上传中...' : '确认提交' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
@@ -250,8 +267,10 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { fenToYuan, formatDateTime, yuanToFen } from '@/utils/business/format'
|
||||
import { hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
|
||||
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
||||
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
|
||||
@@ -266,14 +285,16 @@
|
||||
const rejectLoading = ref(false)
|
||||
const returnLoading = ref(false)
|
||||
const resubmitLoading = ref(false)
|
||||
const resubmitVoucherUploading = ref(false)
|
||||
const tableRef = ref()
|
||||
const resubmitUploadRef = ref<InstanceType<typeof VoucherUpload>>()
|
||||
const createDialogVisible = ref(false)
|
||||
const approveDialogVisible = ref(false)
|
||||
const rejectDialogVisible = ref(false)
|
||||
const returnDialogVisible = ref(false)
|
||||
const resubmitDialogVisible = ref(false)
|
||||
const currentRefund = ref<Refund | null>(null)
|
||||
const refundVoucherFileKey = ref('')
|
||||
const refundVoucherFileKeys = ref<string[]>([])
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState: RefundQueryParams = {
|
||||
@@ -410,10 +431,12 @@
|
||||
const resubmitForm = reactive<{
|
||||
requested_refund_amount?: number
|
||||
actual_received_amount?: number
|
||||
refund_voucher_key: string[]
|
||||
refund_reason?: string
|
||||
}>({
|
||||
requested_refund_amount: undefined,
|
||||
actual_received_amount: undefined,
|
||||
refund_voucher_key: [],
|
||||
refund_reason: ''
|
||||
})
|
||||
|
||||
@@ -812,6 +835,7 @@
|
||||
// 预填充原有数据
|
||||
resubmitForm.requested_refund_amount = fenToYuan(row.requested_refund_amount) || undefined
|
||||
resubmitForm.actual_received_amount = fenToYuan(row.actual_received_amount) || undefined
|
||||
resubmitForm.refund_voucher_key = []
|
||||
resubmitForm.refund_reason = row.refund_reason
|
||||
resubmitDialogVisible.value = true
|
||||
}
|
||||
@@ -821,13 +845,20 @@
|
||||
resubmitFormRef.value?.resetFields()
|
||||
resubmitForm.requested_refund_amount = undefined
|
||||
resubmitForm.actual_received_amount = undefined
|
||||
resubmitForm.refund_voucher_key = []
|
||||
resubmitForm.refund_reason = ''
|
||||
resubmitVoucherUploading.value = false
|
||||
resubmitUploadRef.value?.clearFiles(false)
|
||||
currentRefund.value = null
|
||||
}
|
||||
|
||||
// 重新提交
|
||||
const handleResubmitRefund = async () => {
|
||||
if (!resubmitFormRef.value || !currentRefund.value) return
|
||||
if (resubmitVoucherUploading.value) {
|
||||
ElMessage.warning('退款凭证上传中,请稍候')
|
||||
return
|
||||
}
|
||||
|
||||
await resubmitFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
@@ -840,6 +871,9 @@
|
||||
actual_received_amount: yuanToFen(resubmitForm.actual_received_amount),
|
||||
refund_reason: resubmitForm.refund_reason || undefined
|
||||
}
|
||||
if (hasVoucherKeys(resubmitForm.refund_voucher_key)) {
|
||||
data.refund_voucher_key = toVoucherKeyList(resubmitForm.refund_voucher_key)
|
||||
}
|
||||
|
||||
await RefundService.resubmitRefund(refundId, data)
|
||||
ElMessage.success('重新提交成功')
|
||||
@@ -873,7 +907,7 @@
|
||||
// 获取操作按钮
|
||||
const getActions = (row: Refund) => {
|
||||
const actions: any[] = []
|
||||
const voucherKey = row.refund_voucher_key
|
||||
const voucherKeys = toVoucherKeyList(row.refund_voucher_key)
|
||||
|
||||
// 待审批状态可以打回重填、审批通过或审批拒绝
|
||||
if (row.status === 1) {
|
||||
@@ -912,7 +946,7 @@
|
||||
})
|
||||
}
|
||||
|
||||
if (voucherKey && hasAuth('refund:view_voucher')) {
|
||||
if (voucherKeys.length && hasAuth('refund:view_voucher')) {
|
||||
actions.push({
|
||||
label: '查看退款凭证',
|
||||
handler: () => handleViewRefundVoucher(row),
|
||||
@@ -925,8 +959,8 @@
|
||||
|
||||
const handleViewRefundVoucher = (row: Refund) => {
|
||||
const voucherKey = row.refund_voucher_key
|
||||
if (!voucherKey) return
|
||||
refundVoucherFileKey.value = voucherKey
|
||||
if (!hasVoucherKeys(voucherKey)) return
|
||||
refundVoucherFileKeys.value = toVoucherKeyList(voucherKey)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -56,7 +56,10 @@
|
||||
</div>
|
||||
</ElCard>
|
||||
|
||||
<PaymentVoucherDialog :file-key="paymentVoucherFileKey" @close="paymentVoucherFileKey = ''" />
|
||||
<PaymentVoucherDialog
|
||||
:file-keys="paymentVoucherFileKeys"
|
||||
@close="paymentVoucherFileKeys = []"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -66,7 +69,7 @@
|
||||
import { ElButton, ElCard, ElIcon, ElMessage, ElTable, ElTableColumn } 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 type { DetailField, DetailSection } from '@/components/common/DetailPage.vue'
|
||||
import { OrderService } from '@/api/modules'
|
||||
import type {
|
||||
Order,
|
||||
@@ -77,6 +80,7 @@
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
|
||||
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
|
||||
|
||||
defineOptions({ name: 'OrderDetail' })
|
||||
@@ -88,7 +92,7 @@
|
||||
|
||||
const loading = ref(false)
|
||||
const detailData = ref<Order | null>(null)
|
||||
const paymentVoucherFileKey = ref('')
|
||||
const paymentVoucherFileKeys = ref<string[]>([])
|
||||
|
||||
// 格式化货币 - 将分转换为元
|
||||
const formatCurrency = (amount: number): string => {
|
||||
@@ -164,20 +168,20 @@
|
||||
|
||||
// 详情页配置
|
||||
const detailSections = computed<DetailSection[]>(() => {
|
||||
const baseFields = [
|
||||
const baseFields: DetailField[] = [
|
||||
{ label: '订单号', prop: 'order_no' },
|
||||
{
|
||||
label: '订单类型',
|
||||
formatter: (_, data) => getOrderTypeText(data.order_type)
|
||||
formatter: (_: any, data: any) => getOrderTypeText(data.order_type)
|
||||
},
|
||||
{
|
||||
label: '支付状态',
|
||||
formatter: (_, data) => data.payment_status_text || '-'
|
||||
formatter: (_: any, data: any) => data.payment_status_text || '-'
|
||||
},
|
||||
{
|
||||
label: '订单金额',
|
||||
prop: 'total_amount',
|
||||
formatter: (value) => formatCurrency(value)
|
||||
formatter: (value: any) => formatCurrency(value)
|
||||
}
|
||||
]
|
||||
|
||||
@@ -186,7 +190,8 @@
|
||||
baseFields.push({
|
||||
label: '实付金额',
|
||||
prop: 'actual_paid_amount',
|
||||
formatter: (value) => (value !== undefined && value !== null ? formatCurrency(value) : '-')
|
||||
formatter: (value: any) =>
|
||||
value !== undefined && value !== null ? formatCurrency(value) : '-'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -194,8 +199,11 @@
|
||||
{
|
||||
label: '支付凭证',
|
||||
fullWidth: true,
|
||||
render: (data) => {
|
||||
if (!data.payment_voucher_key || !hasAuth('orders:view_payment_voucher')) {
|
||||
render: (data: any) => {
|
||||
if (
|
||||
!hasVoucherKeys(data.payment_voucher_key) ||
|
||||
!hasAuth('orders:view_payment_voucher')
|
||||
) {
|
||||
return h('span', '-')
|
||||
}
|
||||
|
||||
@@ -205,7 +213,7 @@
|
||||
type: 'primary',
|
||||
link: true,
|
||||
onClick: () => {
|
||||
paymentVoucherFileKey.value = data.payment_voucher_key
|
||||
paymentVoucherFileKeys.value = toVoucherKeyList(data.payment_voucher_key)
|
||||
}
|
||||
},
|
||||
() => '查看支付凭证'
|
||||
|
||||
@@ -225,8 +225,8 @@
|
||||
|
||||
<!-- 支付凭证预览 -->
|
||||
<PaymentVoucherDialog
|
||||
:file-key="paymentVoucherFileKey"
|
||||
@close="paymentVoucherFileKey = ''"
|
||||
:file-keys="paymentVoucherFileKeys"
|
||||
@close="paymentVoucherFileKeys = []"
|
||||
/>
|
||||
|
||||
<!-- 导出任务对话框 -->
|
||||
@@ -273,6 +273,7 @@
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
|
||||
import ExportTaskCreateDialog from '@/components/business/ExportTaskCreateDialog.vue'
|
||||
@@ -294,7 +295,7 @@
|
||||
const tableRef = ref()
|
||||
const createDialogVisible = ref(false)
|
||||
const exportDialogVisible = ref(false)
|
||||
const paymentVoucherFileKey = ref('')
|
||||
const paymentVoucherFileKeys = ref<string[]>([])
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState: OrderSearchFormState = {
|
||||
@@ -570,7 +571,7 @@
|
||||
device_id: null as number | null,
|
||||
device_series_id: null as number | null, // 设备的套餐系列ID
|
||||
payment_method: 'wallet' as OrderPaymentMethod, // 默认使用钱包支付
|
||||
payment_voucher_key: undefined as string | undefined // 线下支付凭证
|
||||
payment_voucher_key: [] as string[] // 线下支付凭证
|
||||
})
|
||||
|
||||
const orderList = ref<Order[]>([])
|
||||
@@ -1142,7 +1143,7 @@
|
||||
createForm.iot_card_id = null
|
||||
createForm.device_id = null
|
||||
createForm.payment_method = 'wallet'
|
||||
createForm.payment_voucher_key = undefined
|
||||
createForm.payment_voucher_key = []
|
||||
voucherUploading.value = false
|
||||
|
||||
// 清空上传文件列表
|
||||
@@ -1203,7 +1204,10 @@
|
||||
}
|
||||
|
||||
// 线下支付时,支付凭证为必填
|
||||
if (createForm.payment_method === 'offline' && !createForm.payment_voucher_key) {
|
||||
if (
|
||||
createForm.payment_method === 'offline' &&
|
||||
!hasVoucherKeys(createForm.payment_voucher_key)
|
||||
) {
|
||||
ElMessage.error('线下支付必须上传支付凭证')
|
||||
return
|
||||
}
|
||||
@@ -1215,9 +1219,12 @@
|
||||
payment_method: createForm.payment_method
|
||||
}
|
||||
|
||||
// 线下支付时,添加支付凭证
|
||||
if (createForm.payment_method === 'offline' && createForm.payment_voucher_key) {
|
||||
data.payment_voucher_key = createForm.payment_voucher_key
|
||||
// 线下支付时,添加支付凭证附件列表
|
||||
if (
|
||||
createForm.payment_method === 'offline' &&
|
||||
hasVoucherKeys(createForm.payment_voucher_key)
|
||||
) {
|
||||
data.payment_voucher_key = toVoucherKeyList(createForm.payment_voucher_key)
|
||||
}
|
||||
|
||||
const res = await OrderService.createOrder(data)
|
||||
@@ -1296,7 +1303,7 @@
|
||||
// 线下支付且有凭证可以查看
|
||||
if (
|
||||
row.payment_method === 'offline' &&
|
||||
row.payment_voucher_key &&
|
||||
hasVoucherKeys(row.payment_voucher_key) &&
|
||||
hasAuth('orders:view_payment_voucher')
|
||||
) {
|
||||
actions.push({
|
||||
@@ -1320,8 +1327,8 @@
|
||||
|
||||
// 查看支付凭证
|
||||
const handleViewPaymentVoucher = (row: Order) => {
|
||||
if (!row.payment_voucher_key) return
|
||||
paymentVoucherFileKey.value = row.payment_voucher_key
|
||||
if (!hasVoucherKeys(row.payment_voucher_key)) return
|
||||
paymentVoucherFileKeys.value = toVoucherKeyList(row.payment_voucher_key)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
{
|
||||
label: '成本价',
|
||||
formatter: (_: unknown, data: PackageResponse) => {
|
||||
return `¥${(data.cost_price / 100).toFixed(2)}`
|
||||
return `¥${((data.cost_price || 0) / 100).toFixed(2)}`
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -190,10 +190,11 @@
|
||||
{
|
||||
label: '总流量',
|
||||
formatter: (_: unknown, data: PackageResponse) => {
|
||||
if (data.real_data_mb >= 1024) {
|
||||
return `${(data.real_data_mb / 1024).toFixed(2)} GB`
|
||||
const realDataMb = data.real_data_mb || 0
|
||||
if (realDataMb >= 1024) {
|
||||
return `${(realDataMb / 1024).toFixed(2)} GB`
|
||||
}
|
||||
return `${data.real_data_mb} MB`
|
||||
return `${realDataMb} MB`
|
||||
}
|
||||
},
|
||||
...(shouldHideVirtualTrafficFields.value
|
||||
@@ -209,10 +210,11 @@
|
||||
label: '虚流量额度',
|
||||
formatter: (_: unknown, data: PackageResponse) => {
|
||||
if (!data.enable_virtual_data) return '-'
|
||||
if (data.virtual_data_mb >= 1024) {
|
||||
return `${(data.virtual_data_mb / 1024).toFixed(2)} GB`
|
||||
const virtualDataMb = data.virtual_data_mb || 0
|
||||
if (virtualDataMb >= 1024) {
|
||||
return `${(virtualDataMb / 1024).toFixed(2)} GB`
|
||||
}
|
||||
return `${data.virtual_data_mb} MB`
|
||||
return `${virtualDataMb} MB`
|
||||
}
|
||||
}
|
||||
])
|
||||
@@ -243,7 +245,7 @@
|
||||
label: '下一档位比例',
|
||||
formatter: (_: unknown, data: PackageResponse) => {
|
||||
if (!data.tier_info || !data.tier_info.next_rate) return '-'
|
||||
return data.tier_info.next_rate
|
||||
return String(data.tier_info.next_rate)
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -255,7 +257,7 @@
|
||||
data.tier_info.next_threshold === undefined
|
||||
)
|
||||
return '-'
|
||||
return data.tier_info.next_threshold
|
||||
return String(data.tier_info.next_threshold)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -231,14 +231,16 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, nextTick, h } from 'vue'
|
||||
import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { PollingConfigService, CarrierService } from '@/api/modules'
|
||||
import type {
|
||||
PollingConfig,
|
||||
PollingConfigQueryParams,
|
||||
CreatePollingConfigRequest,
|
||||
UpdatePollingConfigRequest
|
||||
UpdatePollingConfigRequest,
|
||||
PollingCardCategory,
|
||||
CardCondition
|
||||
} from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
@@ -262,8 +264,8 @@
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
config_name: '',
|
||||
card_category: undefined as string | undefined,
|
||||
card_condition: undefined as string | undefined,
|
||||
card_category: undefined as PollingCardCategory | undefined,
|
||||
card_condition: undefined as CardCondition | undefined,
|
||||
carrier_id: undefined as number | undefined,
|
||||
status: undefined as number | undefined
|
||||
})
|
||||
@@ -530,7 +532,7 @@
|
||||
const { data } = await PollingConfigService.getPollingConfigs(queryParams)
|
||||
tableData.value = data.items
|
||||
pagination.total = data.total
|
||||
} catch (error) {
|
||||
} catch {
|
||||
console.log('加载配置列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -612,7 +614,7 @@
|
||||
nextTick(() => {
|
||||
formRef.value?.clearValidate()
|
||||
})
|
||||
} catch (error) {
|
||||
} catch {
|
||||
console.log('获取配置详情失败')
|
||||
}
|
||||
}
|
||||
@@ -623,7 +625,7 @@
|
||||
await PollingConfigService.updatePollingConfigStatus(row.id, { status: newStatus })
|
||||
ElMessage.success(newStatus === 1 ? '已启用' : '已禁用')
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
} catch {
|
||||
console.log('状态更新失败')
|
||||
}
|
||||
}
|
||||
@@ -677,7 +679,7 @@
|
||||
|
||||
dialogVisible.value = false
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
} catch {
|
||||
console.log(dialogType.value === 'create' ? '创建失败' : '更新失败')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
:show-expand="false"
|
||||
label-width="100"
|
||||
label-width="100px"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
@@ -63,11 +63,11 @@
|
||||
<div style="display: flex; gap: 8px; align-items: center">
|
||||
<el-progress
|
||||
:percentage="getProgressPercentage(scope.row)"
|
||||
stroke-width="6"
|
||||
:stroke-width="6"
|
||||
:show-text="false"
|
||||
style="flex: 1"
|
||||
/>
|
||||
<span style=" width: 50px;font-size: 12px"
|
||||
<span style="width: 50px; font-size: 12px"
|
||||
>{{ getProgressPercentage(scope.row) }}%</span
|
||||
>
|
||||
</div>
|
||||
@@ -103,7 +103,7 @@
|
||||
label="任务类型"
|
||||
prop="task_type"
|
||||
label-width="80"
|
||||
style=" width: 300px;margin-bottom: 0"
|
||||
style="width: 300px; margin-bottom: 0"
|
||||
>
|
||||
<el-select
|
||||
v-model="batchForm.task_type"
|
||||
@@ -757,28 +757,6 @@
|
||||
batchCardPagination.pageSize = 20
|
||||
}
|
||||
|
||||
// 获取卡状态类型
|
||||
const getCardStatusType = (status: number) => {
|
||||
const typeMap: Record<number, 'success' | 'warning' | 'danger' | 'info'> = {
|
||||
0: 'info',
|
||||
1: 'success',
|
||||
2: 'warning',
|
||||
3: 'danger'
|
||||
}
|
||||
return typeMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 获取卡状态名称
|
||||
const getCardStatusName = (status: number) => {
|
||||
const nameMap: Record<number, string> = {
|
||||
0: '未激活',
|
||||
1: '已激活',
|
||||
2: '停机',
|
||||
3: '异常'
|
||||
}
|
||||
return nameMap[status] || '未知'
|
||||
}
|
||||
|
||||
// 获取卡业务类型文本
|
||||
const getCardCategoryText = (category: string) => {
|
||||
const categoryMap: Record<string, string> = {
|
||||
@@ -790,7 +768,7 @@
|
||||
|
||||
// 条件触发表单
|
||||
const conditionForm = reactive<ConditionManualTriggerRequest>({
|
||||
task_type: '' as TaskType | '',
|
||||
task_type: undefined as unknown as TaskType,
|
||||
limit: 100,
|
||||
card_status: undefined,
|
||||
card_type: undefined,
|
||||
@@ -838,16 +816,6 @@
|
||||
}
|
||||
])
|
||||
|
||||
const getStatusTag = (status: string) => {
|
||||
const typeMap: Record<string, 'info' | 'warning' | 'success' | 'danger'> = {
|
||||
pending: 'info',
|
||||
processing: 'warning',
|
||||
completed: 'success',
|
||||
cancelled: 'danger'
|
||||
}
|
||||
return typeMap[status] || 'info'
|
||||
}
|
||||
|
||||
const getProgressPercentage = (row: ManualTriggerLog) => {
|
||||
if (row.total_count === 0) return 0
|
||||
return Math.round((row.processed_count / row.total_count) * 100)
|
||||
@@ -918,7 +886,7 @@
|
||||
const { data } = await ManualTriggerService.getHistory(queryParams)
|
||||
tableData.value = data.items
|
||||
pagination.total = data.total
|
||||
} catch (error) {
|
||||
} catch {
|
||||
console.log('加载触发历史失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -1046,7 +1014,7 @@
|
||||
ElMessage.success('批量触发成功')
|
||||
batchDialogVisible.value = false
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
} catch {
|
||||
console.log('批量触发失败')
|
||||
}
|
||||
})
|
||||
@@ -1073,7 +1041,7 @@
|
||||
ElMessage.success('条件触发成功')
|
||||
conditionDialogVisible.value = false
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
} catch {
|
||||
console.log('条件触发失败')
|
||||
}
|
||||
})
|
||||
@@ -1092,7 +1060,7 @@
|
||||
ElMessage.success('单卡触发成功')
|
||||
singleDialogVisible.value = false
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
} catch {
|
||||
console.log('单卡触发失败')
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user