This commit is contained in:
@@ -27,10 +27,10 @@ import type {
|
||||
import type {
|
||||
EnterpriseDeviceListParams,
|
||||
EnterpriseDevicePageResult,
|
||||
AllocateDevicesRequest,
|
||||
AllocateDevicesResponse,
|
||||
RecallDevicesRequest,
|
||||
RecallDevicesResponse
|
||||
EnterpriseAllocateDevicesRequest,
|
||||
EnterpriseAllocateDevicesResponse,
|
||||
EnterpriseRecallDevicesRequest,
|
||||
EnterpriseRecallDevicesResponse
|
||||
} from '@/types/api/enterpriseDevice'
|
||||
|
||||
export class EnterpriseService extends BaseService {
|
||||
@@ -171,9 +171,9 @@ export class EnterpriseService extends BaseService {
|
||||
*/
|
||||
static allocateDevices(
|
||||
enterpriseId: number,
|
||||
data: AllocateDevicesRequest
|
||||
): Promise<BaseResponse<AllocateDevicesResponse>> {
|
||||
return this.post<BaseResponse<AllocateDevicesResponse>>(
|
||||
data: EnterpriseAllocateDevicesRequest
|
||||
): Promise<BaseResponse<EnterpriseAllocateDevicesResponse>> {
|
||||
return this.post<BaseResponse<EnterpriseAllocateDevicesResponse>>(
|
||||
`/api/admin/enterprises/${enterpriseId}/allocate-devices`,
|
||||
data
|
||||
)
|
||||
@@ -201,9 +201,9 @@ export class EnterpriseService extends BaseService {
|
||||
*/
|
||||
static recallDevices(
|
||||
enterpriseId: number,
|
||||
data: RecallDevicesRequest
|
||||
): Promise<BaseResponse<RecallDevicesResponse>> {
|
||||
return this.post<BaseResponse<RecallDevicesResponse>>(
|
||||
data: EnterpriseRecallDevicesRequest
|
||||
): Promise<BaseResponse<EnterpriseRecallDevicesResponse>> {
|
||||
return this.post<BaseResponse<EnterpriseRecallDevicesResponse>>(
|
||||
`/api/admin/enterprises/${enterpriseId}/recall-devices`,
|
||||
data
|
||||
)
|
||||
|
||||
@@ -47,23 +47,13 @@
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="退款凭证" prop="refund_voucher_key">
|
||||
<ElUpload
|
||||
<VoucherUpload
|
||||
ref="uploadRef"
|
||||
class="refund-voucher-upload"
|
||||
drag
|
||||
:auto-upload="false"
|
||||
:on-change="handleVoucherFileChange"
|
||||
:on-remove="handleRemoveVoucher"
|
||||
accept="image/*"
|
||||
list-type="picture"
|
||||
multiple
|
||||
>
|
||||
<ElIcon class="refund-voucher-upload__icon"><UploadFilled /></ElIcon>
|
||||
<div class="refund-voucher-upload__text">将退款凭证拖到此处,或<em>点击上传</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">支持所有图片格式,可上传多张</div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
v-model="formData.refund_voucher_key"
|
||||
voucher-name="退款凭证"
|
||||
@uploading-change="voucherUploading = $event"
|
||||
@change="formRef?.validateField('refund_voucher_key')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
@@ -75,7 +65,7 @@
|
||||
:loading="submitLoading || voucherUploading"
|
||||
:disabled="voucherUploading"
|
||||
>
|
||||
{{ voucherUploading ? '图片上传中...' : '确认创建' }}
|
||||
{{ voucherUploading ? '凭证上传中...' : '确认创建' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
@@ -83,13 +73,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch, nextTick } from 'vue'
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { UploadFilled } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules, UploadFile, UploadInstance } from 'element-plus'
|
||||
import { RefundService, OrderService, StorageService } from '@/api/modules'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { RefundService, OrderService } from '@/api/modules'
|
||||
import type { CreateRefundRequest, Order } from '@/types/api'
|
||||
import { formatMoney, yuanToFen } from '@/utils/business/format'
|
||||
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
@@ -109,13 +99,11 @@
|
||||
})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
const uploadRef = ref<InstanceType<typeof VoucherUpload>>()
|
||||
const submitLoading = ref(false)
|
||||
const voucherUploading = ref(false)
|
||||
const voucherUploadingCount = ref(0)
|
||||
const orderSearchLoading = ref(false)
|
||||
const orderSearchOptions = ref<Order[]>([])
|
||||
let activeVoucherUploadBatch = 0
|
||||
|
||||
const formData = reactive<{
|
||||
order_id: number | null
|
||||
@@ -207,96 +195,14 @@
|
||||
|
||||
const handleDialogClosed = () => {
|
||||
formRef.value?.resetFields()
|
||||
activeVoucherUploadBatch += 1
|
||||
voucherUploadingCount.value = 0
|
||||
voucherUploading.value = false
|
||||
formData.order_id = null
|
||||
formData.requested_refund_amount = undefined
|
||||
formData.actual_received_amount = 0
|
||||
formData.refund_reason = ''
|
||||
formData.refund_voucher_key = undefined
|
||||
voucherFileKeyMap.clear()
|
||||
removedVoucherUploadUids.clear()
|
||||
orderSearchOptions.value = []
|
||||
uploadRef.value?.clearFiles()
|
||||
}
|
||||
|
||||
const voucherFileKeyMap = new Map<number, string>()
|
||||
const removedVoucherUploadUids = new Set<number>()
|
||||
|
||||
const refreshPaymentVoucherKey = () => {
|
||||
const keys = Array.from(voucherFileKeyMap.values())
|
||||
formData.refund_voucher_key = keys.length ? keys.join(',') : undefined
|
||||
}
|
||||
|
||||
const removeUploadFile = (uploadFile: UploadFile) => {
|
||||
uploadRef.value?.handleRemove(uploadFile)
|
||||
}
|
||||
|
||||
const handleVoucherFileChange = async (uploadFile: UploadFile) => {
|
||||
const file = uploadFile.raw
|
||||
if (!file) return
|
||||
|
||||
if (!file.type.startsWith('image/')) {
|
||||
ElMessage.error('只能上传图片文件!')
|
||||
removeUploadFile(uploadFile)
|
||||
return
|
||||
}
|
||||
|
||||
const uploadBatch = activeVoucherUploadBatch
|
||||
removedVoucherUploadUids.delete(uploadFile.uid)
|
||||
voucherUploadingCount.value += 1
|
||||
voucherUploading.value = voucherUploadingCount.value > 0
|
||||
|
||||
try {
|
||||
ElMessage.info('正在上传退款凭证...')
|
||||
const uploadUrlRes = await StorageService.getUploadUrl({
|
||||
file_name: file.name,
|
||||
content_type: file.type,
|
||||
purpose: 'attachment'
|
||||
})
|
||||
|
||||
if (uploadUrlRes.code !== 0) {
|
||||
ElMessage.error(uploadUrlRes.msg || '获取上传地址失败')
|
||||
removeUploadFile(uploadFile)
|
||||
return
|
||||
}
|
||||
|
||||
const { upload_url, file_key } = uploadUrlRes.data
|
||||
await StorageService.uploadFile(upload_url, file, file.type)
|
||||
|
||||
if (
|
||||
uploadBatch !== activeVoucherUploadBatch ||
|
||||
removedVoucherUploadUids.has(uploadFile.uid)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
voucherFileKeyMap.set(uploadFile.uid, file_key)
|
||||
refreshPaymentVoucherKey()
|
||||
ElMessage.success('上传成功')
|
||||
await nextTick(() => {
|
||||
formRef.value?.validateField('refund_voucher_key')
|
||||
})
|
||||
} catch (error: any) {
|
||||
if (uploadBatch !== activeVoucherUploadBatch) return
|
||||
console.error('上传退款凭证失败:', error)
|
||||
ElMessage.error(error.message || '上传失败,请重试')
|
||||
voucherFileKeyMap.delete(uploadFile.uid)
|
||||
refreshPaymentVoucherKey()
|
||||
removeUploadFile(uploadFile)
|
||||
} finally {
|
||||
if (uploadBatch === activeVoucherUploadBatch) {
|
||||
voucherUploadingCount.value = Math.max(0, voucherUploadingCount.value - 1)
|
||||
voucherUploading.value = voucherUploadingCount.value > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveVoucher = (uploadFile: UploadFile) => {
|
||||
removedVoucherUploadUids.add(uploadFile.uid)
|
||||
voucherFileKeyMap.delete(uploadFile.uid)
|
||||
refreshPaymentVoucherKey()
|
||||
uploadRef.value?.clearFiles(false)
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
@@ -343,35 +249,3 @@
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.refund-voucher-upload {
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-upload) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-upload-dragger) {
|
||||
width: 100%;
|
||||
padding: 24px 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
margin-bottom: 12px;
|
||||
font-size: 28px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
|
||||
em {
|
||||
font-style: normal;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,22 +1,47 @@
|
||||
<template>
|
||||
<ElImageViewer
|
||||
v-if="visible"
|
||||
:url-list="urls"
|
||||
:initial-index="0"
|
||||
hide-on-click-modal
|
||||
@close="handleClose"
|
||||
/>
|
||||
<ElDialog v-model="visible" title="凭证查看" width="560px" @closed="handleClose">
|
||||
<div v-if="loading" class="voucher-preview__loading">加载中...</div>
|
||||
<div v-else-if="files.length" class="voucher-preview__list">
|
||||
<div v-for="file in files" :key="file.key" class="voucher-preview__item">
|
||||
<ElImage
|
||||
v-if="file.isImage"
|
||||
class="voucher-preview__image"
|
||||
:src="file.url"
|
||||
:preview-src-list="imageUrls"
|
||||
:initial-index="getImageIndex(file.url)"
|
||||
fit="cover"
|
||||
preview-teleported
|
||||
/>
|
||||
<div v-else class="voucher-preview__file">
|
||||
<ElIcon><Document /></ElIcon>
|
||||
</div>
|
||||
<div class="voucher-preview__meta">
|
||||
<div class="voucher-preview__name" :title="file.name">{{ file.name }}</div>
|
||||
<ElButton link type="primary" @click="openFile(file)">查看/下载</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ElEmpty v-else description="暂无凭证" />
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { ElImageViewer } from 'element-plus'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { Document } from '@element-plus/icons-vue'
|
||||
import { ElButton, ElDialog, ElEmpty, ElIcon, ElImage } from 'element-plus'
|
||||
import { StorageService } from '@/api/modules'
|
||||
|
||||
interface Props {
|
||||
fileKey?: string
|
||||
}
|
||||
|
||||
interface VoucherFile {
|
||||
key: string
|
||||
name: string
|
||||
url: string
|
||||
isImage: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -24,32 +49,74 @@
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const urls = ref<string[]>([])
|
||||
const loading = ref(false)
|
||||
const files = ref<VoucherFile[]>([])
|
||||
|
||||
const imageUrls = computed(() =>
|
||||
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 isImageFile = (fileKey: string, url: string) => {
|
||||
const target = `${fileKey} ${url}`.toLowerCase()
|
||||
return /\.(png|jpe?g|gif|webp|bmp|svg)(\?|#|$)/.test(target)
|
||||
}
|
||||
|
||||
const getImageIndex = (url: string) => Math.max(0, imageUrls.value.indexOf(url))
|
||||
|
||||
const openFile = (file: VoucherFile) => {
|
||||
window.open(file.url, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.fileKey,
|
||||
async (val) => {
|
||||
if (val) {
|
||||
urls.value = []
|
||||
if (!val) {
|
||||
visible.value = false
|
||||
const fileKeys = val
|
||||
.split(',')
|
||||
.map((key) => key.trim())
|
||||
.filter(Boolean)
|
||||
try {
|
||||
const res = await StorageService.batchDownloadUrls({
|
||||
file_keys: fileKeys
|
||||
})
|
||||
if (res.code === 0) {
|
||||
urls.value = fileKeys.map((key) => res.data.urls[key]).filter(Boolean)
|
||||
visible.value = urls.value.length > 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取支付凭证失败:', error)
|
||||
files.value = []
|
||||
return
|
||||
}
|
||||
|
||||
const fileKeys = getVoucherKeys(val)
|
||||
if (!fileKeys.length) {
|
||||
visible.value = false
|
||||
files.value = []
|
||||
return
|
||||
}
|
||||
|
||||
visible.value = true
|
||||
loading.value = true
|
||||
files.value = []
|
||||
|
||||
try {
|
||||
const res = await StorageService.batchDownloadUrls({
|
||||
file_keys: fileKeys
|
||||
})
|
||||
if (res.code === 0) {
|
||||
files.value = fileKeys
|
||||
.map((key) => {
|
||||
const url = res.data.urls[key]
|
||||
if (!url) return null
|
||||
return {
|
||||
key,
|
||||
name: getFileName(key),
|
||||
url,
|
||||
isImage: isImageFile(key, url)
|
||||
}
|
||||
})
|
||||
.filter((file): file is VoucherFile => !!file)
|
||||
}
|
||||
} else {
|
||||
visible.value = false
|
||||
urls.value = []
|
||||
} catch (error) {
|
||||
console.error('获取凭证失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
@@ -57,7 +124,62 @@
|
||||
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
urls.value = []
|
||||
loading.value = false
|
||||
files.value = []
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.voucher-preview {
|
||||
&__loading {
|
||||
padding: 32px 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
&__item {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__image,
|
||||
&__file {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
&__image {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
&__file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 40px;
|
||||
color: var(--el-text-color-secondary);
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
&__meta {
|
||||
width: 120px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
&__name {
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-regular);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
404
src/components/business/VoucherUpload.vue
Normal file
404
src/components/business/VoucherUpload.vue
Normal file
@@ -0,0 +1,404 @@
|
||||
<template>
|
||||
<div
|
||||
ref="rootRef"
|
||||
class="voucher-upload"
|
||||
tabindex="0"
|
||||
@click="focusUploadArea"
|
||||
@paste="handlePaste"
|
||||
>
|
||||
<ElUpload
|
||||
ref="uploadRef"
|
||||
class="voucher-upload__control"
|
||||
drag
|
||||
multiple
|
||||
:auto-upload="false"
|
||||
:on-change="handleFileChange"
|
||||
:on-remove="handleRemoveFile"
|
||||
list-type="picture"
|
||||
>
|
||||
<ElIcon class="voucher-upload__icon"><UploadFilled /></ElIcon>
|
||||
<div class="voucher-upload__text">将{{ voucherName }}拖到此处,或<em>点击上传</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
支持多张图片或附件,最多上传 {{ maxCount }} 个文件;点击上传区域后可粘贴文件
|
||||
</div>
|
||||
</template>
|
||||
<template #file="{ file }">
|
||||
<div class="voucher-upload__file-item">
|
||||
<img
|
||||
v-if="isUploadImage(file)"
|
||||
class="voucher-upload__file-cover"
|
||||
:src="getUploadFileUrl(file)"
|
||||
alt=""
|
||||
/>
|
||||
<div v-else class="voucher-upload__file-cover voucher-upload__file-cover--default">
|
||||
<ElIcon><Document /></ElIcon>
|
||||
</div>
|
||||
<span class="voucher-upload__file-name" :title="file.name">{{ file.name }}</span>
|
||||
<button
|
||||
class="voucher-upload__file-remove"
|
||||
type="button"
|
||||
aria-label="移除文件"
|
||||
@click.stop="removeUploadFile(file)"
|
||||
>
|
||||
<ElIcon><Close /></ElIcon>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { genFileId } from 'element-plus'
|
||||
import { Close, Document, UploadFilled } from '@element-plus/icons-vue'
|
||||
import type { UploadFile, UploadInstance, UploadRawFile } from 'element-plus'
|
||||
import { StorageService } from '@/api/modules'
|
||||
|
||||
interface Props {
|
||||
modelValue?: string
|
||||
voucherName?: string
|
||||
maxCount?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
voucherName: '凭证',
|
||||
maxCount: 5
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string | undefined]
|
||||
'uploading-change': [value: boolean]
|
||||
change: [value: string | undefined]
|
||||
}>()
|
||||
|
||||
const rootRef = ref<HTMLElement>()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
const uploadingCount = ref(0)
|
||||
const voucherFileKeyMap = new Map<number, string>()
|
||||
const removedUploadUids = new Set<number>()
|
||||
const fileObjectUrlMap = new Map<number, string>()
|
||||
const selectedUploadUids = new Set<number>()
|
||||
let activeUploadBatch = 0
|
||||
|
||||
const getMaxCount = () => Math.max(1, props.maxCount)
|
||||
|
||||
const showMaxCountWarning = () => {
|
||||
ElMessage.warning(`最多只能上传 ${getMaxCount()} 个文件`)
|
||||
}
|
||||
|
||||
const emitVoucherKeys = () => {
|
||||
const keys = Array.from(voucherFileKeyMap.values())
|
||||
const value = keys.length ? keys.join(',') : undefined
|
||||
emit('update:modelValue', value)
|
||||
emit('change', value)
|
||||
}
|
||||
|
||||
const setUploadingCount = (count: number) => {
|
||||
uploadingCount.value = Math.max(0, count)
|
||||
emit('uploading-change', uploadingCount.value > 0)
|
||||
}
|
||||
|
||||
const clearFiles = (emitValue = true) => {
|
||||
activeUploadBatch += 1
|
||||
setUploadingCount(0)
|
||||
voucherFileKeyMap.clear()
|
||||
removedUploadUids.clear()
|
||||
selectedUploadUids.clear()
|
||||
revokeAllFileObjectUrls()
|
||||
uploadRef.value?.clearFiles()
|
||||
if (emitValue) {
|
||||
emitVoucherKeys()
|
||||
}
|
||||
}
|
||||
|
||||
const revokeFileObjectUrl = (uid: number) => {
|
||||
const url = fileObjectUrlMap.get(uid)
|
||||
if (!url) return
|
||||
|
||||
URL.revokeObjectURL(url)
|
||||
fileObjectUrlMap.delete(uid)
|
||||
}
|
||||
|
||||
const revokeAllFileObjectUrls = () => {
|
||||
fileObjectUrlMap.forEach((url) => URL.revokeObjectURL(url))
|
||||
fileObjectUrlMap.clear()
|
||||
}
|
||||
|
||||
const isUploadImage = (file: UploadFile) => {
|
||||
if (file.raw?.type?.startsWith('image/')) return true
|
||||
return /\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(file.name)
|
||||
}
|
||||
|
||||
const getUploadFileUrl = (file: UploadFile) => {
|
||||
if (file.url) return file.url
|
||||
if (!file.raw) return ''
|
||||
|
||||
const cachedUrl = fileObjectUrlMap.get(file.uid)
|
||||
if (cachedUrl) return cachedUrl
|
||||
|
||||
const url = URL.createObjectURL(file.raw)
|
||||
fileObjectUrlMap.set(file.uid, url)
|
||||
return url
|
||||
}
|
||||
|
||||
const removeUploadFile = (uploadFile: UploadFile) => {
|
||||
uploadRef.value?.handleRemove(uploadFile)
|
||||
}
|
||||
|
||||
const handleFileChange = async (uploadFile: UploadFile) => {
|
||||
const file = uploadFile.raw
|
||||
if (!file) return
|
||||
|
||||
if (!selectedUploadUids.has(uploadFile.uid)) {
|
||||
if (selectedUploadUids.size >= getMaxCount()) {
|
||||
showMaxCountWarning()
|
||||
removeUploadFile(uploadFile)
|
||||
return
|
||||
}
|
||||
|
||||
selectedUploadUids.add(uploadFile.uid)
|
||||
}
|
||||
|
||||
const uploadBatch = activeUploadBatch
|
||||
removedUploadUids.delete(uploadFile.uid)
|
||||
setUploadingCount(uploadingCount.value + 1)
|
||||
|
||||
try {
|
||||
ElMessage.info(`正在上传${props.voucherName}...`)
|
||||
|
||||
const contentType = file.type || 'application/octet-stream'
|
||||
const uploadUrlRes = await StorageService.getUploadUrl({
|
||||
file_name: file.name,
|
||||
content_type: contentType,
|
||||
purpose: 'attachment'
|
||||
})
|
||||
|
||||
if (uploadUrlRes.code !== 0) {
|
||||
ElMessage.error(uploadUrlRes.msg || '获取上传地址失败')
|
||||
removeUploadFile(uploadFile)
|
||||
return
|
||||
}
|
||||
|
||||
const { upload_url, file_key } = uploadUrlRes.data
|
||||
await StorageService.uploadFile(upload_url, file, contentType)
|
||||
|
||||
if (uploadBatch !== activeUploadBatch || removedUploadUids.has(uploadFile.uid)) {
|
||||
return
|
||||
}
|
||||
|
||||
voucherFileKeyMap.set(uploadFile.uid, file_key)
|
||||
emitVoucherKeys()
|
||||
ElMessage.success('上传成功')
|
||||
} catch (error: any) {
|
||||
if (uploadBatch !== activeUploadBatch) return
|
||||
|
||||
console.error(`上传${props.voucherName}失败:`, error)
|
||||
ElMessage.error(error?.message || '上传失败,请重试')
|
||||
voucherFileKeyMap.delete(uploadFile.uid)
|
||||
emitVoucherKeys()
|
||||
removeUploadFile(uploadFile)
|
||||
} finally {
|
||||
if (uploadBatch === activeUploadBatch) {
|
||||
setUploadingCount(uploadingCount.value - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveFile = (uploadFile: UploadFile) => {
|
||||
removedUploadUids.add(uploadFile.uid)
|
||||
selectedUploadUids.delete(uploadFile.uid)
|
||||
revokeFileObjectUrl(uploadFile.uid)
|
||||
voucherFileKeyMap.delete(uploadFile.uid)
|
||||
emitVoucherKeys()
|
||||
}
|
||||
|
||||
const focusUploadArea = () => {
|
||||
nextTick(() => rootRef.value?.focus())
|
||||
}
|
||||
|
||||
const getExtensionByMimeType = (mimeType: string) => {
|
||||
const extensionMap: Record<string, string> = {
|
||||
'image/png': 'png',
|
||||
'image/jpeg': 'jpg',
|
||||
'image/gif': 'gif',
|
||||
'image/webp': 'webp',
|
||||
'application/pdf': 'pdf',
|
||||
'text/plain': 'txt'
|
||||
}
|
||||
return extensionMap[mimeType] || 'bin'
|
||||
}
|
||||
|
||||
const normalizeClipboardFile = (file: File, index: number) => {
|
||||
if (file.name) return file
|
||||
|
||||
const extension = getExtensionByMimeType(file.type)
|
||||
return new File([file], `clipboard-${Date.now()}-${index + 1}.${extension}`, {
|
||||
type: file.type || 'application/octet-stream'
|
||||
})
|
||||
}
|
||||
|
||||
const startUploadFile = (file: File) => {
|
||||
const uploadRawFile = file as UploadRawFile
|
||||
uploadRawFile.uid = genFileId()
|
||||
uploadRef.value?.handleStart(uploadRawFile)
|
||||
}
|
||||
|
||||
const getClipboardFiles = (event: ClipboardEvent) => {
|
||||
const clipboardData = event.clipboardData
|
||||
if (!clipboardData) return []
|
||||
|
||||
const files = Array.from(clipboardData.files || [])
|
||||
if (files.length) return files
|
||||
|
||||
return Array.from(clipboardData.items || [])
|
||||
.filter((item) => item.kind === 'file')
|
||||
.map((item) => item.getAsFile())
|
||||
.filter((file): file is File => !!file)
|
||||
}
|
||||
|
||||
const handlePaste = (event: ClipboardEvent) => {
|
||||
if (event.defaultPrevented) return
|
||||
if (!isUploadVisible()) return
|
||||
|
||||
const files = getClipboardFiles(event)
|
||||
if (!files.length) return
|
||||
|
||||
event.preventDefault()
|
||||
const remainingCount = getMaxCount() - selectedUploadUids.size
|
||||
if (remainingCount <= 0) {
|
||||
showMaxCountWarning()
|
||||
return
|
||||
}
|
||||
|
||||
if (files.length > remainingCount) {
|
||||
showMaxCountWarning()
|
||||
}
|
||||
|
||||
files.slice(0, remainingCount).map(normalizeClipboardFile).forEach(startUploadFile)
|
||||
}
|
||||
|
||||
const isUploadVisible = () => {
|
||||
const element = rootRef.value
|
||||
return (
|
||||
!!element &&
|
||||
!!(element.offsetWidth || element.offsetHeight || element.getClientRects().length)
|
||||
)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('paste', handlePaste)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('paste', handlePaste)
|
||||
revokeAllFileObjectUrls()
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
clearFiles
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.voucher-upload {
|
||||
width: 100%;
|
||||
outline: none;
|
||||
|
||||
&:focus-within,
|
||||
&:focus {
|
||||
:deep(.el-upload-dragger) {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-upload) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-upload-dragger) {
|
||||
width: 100%;
|
||||
padding: 24px 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
:deep(.el-upload-list__item) {
|
||||
height: auto;
|
||||
padding: 8px;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
margin-bottom: 12px;
|
||||
font-size: 28px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
|
||||
em {
|
||||
font-style: normal;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
&__file-item {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__file-cover {
|
||||
flex: 0 0 auto;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
object-fit: cover;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
&__file-cover--default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22px;
|
||||
color: var(--el-text-color-secondary);
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
&__file-name {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--el-text-color-regular);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__file-remove {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-danger);
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -301,28 +301,6 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
// 企业设备管理(设备授权)
|
||||
{
|
||||
path: 'enterprise-devices',
|
||||
name: 'EnterpriseDevices',
|
||||
component: RoutesAlias.EnterpriseDevices,
|
||||
meta: {
|
||||
title: 'menus.assetManagement.enterpriseDevices',
|
||||
isHide: true,
|
||||
keepAlive: false
|
||||
}
|
||||
},
|
||||
// 企业卡授权
|
||||
{
|
||||
path: 'enterprise-cards',
|
||||
name: 'EnterpriseCards',
|
||||
component: RoutesAlias.EnterpriseCards,
|
||||
meta: {
|
||||
title: 'menus.accountManagement.enterpriseCards',
|
||||
isHide: true,
|
||||
keepAlive: false
|
||||
}
|
||||
},
|
||||
// 换货管理
|
||||
{
|
||||
path: 'exchange-management',
|
||||
|
||||
@@ -40,8 +40,6 @@ export enum RoutesAlias {
|
||||
// 账号管理
|
||||
Account = '/account-management/account', // 账号列表
|
||||
EnterpriseCustomer = '/account-management/enterprise-customer', // 企业客户管理
|
||||
EnterpriseCards = '/asset-management/enterprise-cards', // 企业卡授权
|
||||
EnterpriseDevices = '/asset-management/enterprise-devices', // 企业设备授权
|
||||
|
||||
// 资产管理
|
||||
AssetInformation = '/asset-management/asset-information', // 资产信息
|
||||
|
||||
@@ -347,6 +347,9 @@ export interface StandaloneCardQueryParams extends PaginationParams {
|
||||
carrier_name?: string // carrier name filter
|
||||
is_standalone?: boolean // standalone filter
|
||||
status?: StandaloneCardStatus // 状态
|
||||
network_status?: number | null // 网络状态 (0:停机, 1:开机)
|
||||
authorized_enterprise_id?: number // 有效授权企业ID
|
||||
is_authorized_to_enterprise?: boolean | null // 是否已授权给企业
|
||||
carrier_id?: number // 运营商ID
|
||||
shop_id?: number // 分销商ID
|
||||
iccid?: string // ICCID(模糊查询)
|
||||
@@ -404,6 +407,8 @@ export interface StandaloneIotCard {
|
||||
series_id: number | null // 套餐系列ID
|
||||
series_name: string // 套餐系列名称
|
||||
realname_policy: string // 实名制策略
|
||||
authorized_enterprise_id?: number | null // 当前有效授权企业ID
|
||||
authorized_enterprise_name?: string | null // 当前有效授权企业名称
|
||||
}
|
||||
|
||||
// ========== 单卡批量分配和回收相关 ==========
|
||||
|
||||
@@ -310,6 +310,7 @@ export interface MainWalletTransactionQueryParams extends PaginationParams {
|
||||
transaction_type?: MainWalletTransactionType // 交易类型过滤
|
||||
start_date?: string // 开始日期 YYYY-MM-DD
|
||||
end_date?: string // 结束日期 YYYY-MM-DD
|
||||
asset_identifier?: string // 资产标识精确检索(ICCID 或设备虚拟号)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -65,6 +65,8 @@ export interface Device {
|
||||
imei?: string // IMEI(设备国际移动设备识别码)
|
||||
sn?: string // 设备序列号
|
||||
realname_policy?: string // 实名认证策略
|
||||
authorized_enterprise_id?: number | null // 当前有效授权企业ID
|
||||
authorized_enterprise_name?: string | null // 当前有效授权企业名称
|
||||
}
|
||||
|
||||
// 设备查询参数
|
||||
@@ -81,6 +83,8 @@ export interface DeviceQueryParams extends PaginationParams {
|
||||
created_at_end?: string // 创建时间结束
|
||||
series_id?: number // 套餐系列ID
|
||||
keyword?: string // 关键词搜索(虚拟号或IMEI)
|
||||
authorized_enterprise_id?: number // 有效授权企业ID
|
||||
is_authorized_to_enterprise?: boolean | null // 是否已授权给企业
|
||||
}
|
||||
|
||||
// 设备列表响应
|
||||
|
||||
@@ -30,8 +30,26 @@ export interface AllocatedDevice {
|
||||
* 授权卡给企业请求
|
||||
*/
|
||||
export interface AllocateCardsRequest {
|
||||
/** 卡片选取方式 */
|
||||
selection_type?: 'list' | 'range' | 'filter'
|
||||
/** 需要授权的 ICCID 列表 */
|
||||
iccids: string[]
|
||||
iccids?: string[]
|
||||
/** 起始 ICCID */
|
||||
iccid_start?: string
|
||||
/** 结束 ICCID */
|
||||
iccid_end?: string
|
||||
/** 运营商ID */
|
||||
carrier_id?: number
|
||||
/** 单个店铺ID */
|
||||
shop_id?: number
|
||||
/** 多个店铺ID */
|
||||
shop_ids?: number[]
|
||||
/** 批次号 */
|
||||
batch_no?: string
|
||||
/** ICCID模糊匹配 */
|
||||
iccid?: string
|
||||
/** 授权备注 */
|
||||
remark?: string
|
||||
/** 确认整体授权设备下所有卡 */
|
||||
confirm_device_bundles?: boolean
|
||||
}
|
||||
@@ -212,8 +230,22 @@ export interface RecalledDevice {
|
||||
* 回收卡授权请求
|
||||
*/
|
||||
export interface RecallCardsRequest {
|
||||
/** 卡片选取方式 */
|
||||
selection_type?: 'list' | 'range' | 'filter'
|
||||
/** 需要回收授权的 ICCID 列表 */
|
||||
iccids: string[]
|
||||
iccids?: string[]
|
||||
/** 起始 ICCID */
|
||||
iccid_start?: string
|
||||
/** 结束 ICCID */
|
||||
iccid_end?: string
|
||||
/** 运营商ID */
|
||||
carrier_id?: number
|
||||
/** 批次号 */
|
||||
batch_no?: string
|
||||
/** ICCID模糊匹配 */
|
||||
iccid?: string
|
||||
/** 回收备注 */
|
||||
remark?: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,8 +38,12 @@ export interface EnterpriseDevicePageResult {
|
||||
/**
|
||||
* 授权设备请求参数
|
||||
*/
|
||||
export interface AllocateDevicesRequest {
|
||||
device_nos: string[] | null // 设备号列表(最多100个)
|
||||
export interface EnterpriseAllocateDevicesRequest {
|
||||
selection_type?: 'list' | 'filter' // 设备选取方式
|
||||
device_nos?: string[] | null // 设备号列表(最多100个)
|
||||
batch_no?: string // 批次号
|
||||
shop_id?: number // 店铺ID
|
||||
virtual_no?: string // 设备虚拟号模糊匹配
|
||||
remark?: string // 授权备注
|
||||
}
|
||||
|
||||
@@ -63,7 +67,7 @@ export interface FailedDeviceItem {
|
||||
/**
|
||||
* 授权设备响应
|
||||
*/
|
||||
export interface AllocateDevicesResponse {
|
||||
export interface EnterpriseAllocateDevicesResponse {
|
||||
success_count: number // 成功数量
|
||||
fail_count: number // 失败数量
|
||||
authorized_devices: AuthorizedDeviceItem[] | null // 已授权设备列表
|
||||
@@ -75,14 +79,17 @@ export interface AllocateDevicesResponse {
|
||||
/**
|
||||
* 撤销设备授权请求参数
|
||||
*/
|
||||
export interface RecallDevicesRequest {
|
||||
device_nos: string[] | null // 设备号列表(最多100个)
|
||||
export interface EnterpriseRecallDevicesRequest {
|
||||
selection_type?: 'list' | 'filter' // 设备选取方式
|
||||
device_nos?: string[] | null // 设备号列表(最多100个)
|
||||
batch_no?: string // 批次号
|
||||
virtual_no?: string // 设备虚拟号模糊匹配
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销设备授权响应
|
||||
*/
|
||||
export interface RecallDevicesResponse {
|
||||
export interface EnterpriseRecallDevicesResponse {
|
||||
success_count: number // 成功数量
|
||||
fail_count: number // 失败数量
|
||||
failed_items: FailedDeviceItem[] | null // 失败项列表
|
||||
|
||||
@@ -47,6 +47,7 @@ export interface RefundQueryParams {
|
||||
order_id?: number
|
||||
shop_id?: number
|
||||
shop_name?: string // 店铺名称(模糊查询)
|
||||
asset_identifier?: string // 资产标识(ICCID 或 设备虚拟号,精确匹配)
|
||||
dateRange?: string[]
|
||||
}
|
||||
|
||||
|
||||
1
src/types/components.d.ts
vendored
1
src/types/components.d.ts
vendored
@@ -155,6 +155,7 @@ declare module 'vue' {
|
||||
TableContextMenuHint: typeof import('./../components/core/others/TableContextMenuHint.vue')['default']
|
||||
ThemeSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/ThemeSettings.vue')['default']
|
||||
UpdateRealnameStatusDialog: typeof import('./../components/business/UpdateRealnameStatusDialog.vue')['default']
|
||||
VoucherUpload: typeof import('./../components/business/VoucherUpload.vue')['default']
|
||||
}
|
||||
export interface ComponentCustomProperties {
|
||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||
|
||||
@@ -206,9 +206,9 @@
|
||||
import { useRouter } from 'vue-router'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { EnterpriseService, ShopService } from '@/api/modules'
|
||||
import { ElMessage, ElSwitch, ElCascader, ElTreeSelect } from 'element-plus'
|
||||
import { ElMessage, ElSwitch, ElCascader } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type { EnterpriseItem, ShopResponse } from '@/types/api'
|
||||
import type { EnterpriseItem } from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
@@ -236,8 +236,6 @@
|
||||
const shopLoading = ref(false)
|
||||
const tableRef = ref()
|
||||
const currentEnterpriseId = ref<number>(0)
|
||||
const shopTreeData = ref<ShopResponse[]>([])
|
||||
|
||||
// 店铺级联选择相关
|
||||
const shopCascadeOptions = ref<any[]>([])
|
||||
const shopCascadeProps = {
|
||||
@@ -499,22 +497,6 @@
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('enterprise_customer:card_authorization')) {
|
||||
actions.push({
|
||||
label: '卡授权',
|
||||
handler: () => manageCards(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('enterprise_customer:device_authorization')) {
|
||||
actions.push({
|
||||
label: '设备授权',
|
||||
handler: () => manageDevices(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('enterprise_customer:edit')) {
|
||||
actions.push({
|
||||
label: '编辑',
|
||||
@@ -582,33 +564,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 构建树形数据
|
||||
const buildTreeData = (items: ShopResponse[]) => {
|
||||
const map = new Map<number, ShopResponse & { children?: ShopResponse[] }>()
|
||||
const tree: ShopResponse[] = []
|
||||
|
||||
// 先将所有项放入 map
|
||||
items.forEach((item) => {
|
||||
map.set(item.id, { ...item, children: [] })
|
||||
})
|
||||
|
||||
// 构建树形结构
|
||||
items.forEach((item) => {
|
||||
const node = map.get(item.id)!
|
||||
if (item.parent_id && map.has(item.parent_id)) {
|
||||
// 有父节点,添加到父节点的 children 中
|
||||
const parent = map.get(item.parent_id)!
|
||||
if (!parent.children) parent.children = []
|
||||
parent.children.push(node)
|
||||
} else {
|
||||
// 没有父节点或父节点不存在,作为根节点
|
||||
tree.push(node)
|
||||
}
|
||||
})
|
||||
|
||||
return tree
|
||||
}
|
||||
|
||||
// 获取企业客户列表
|
||||
const getTableData = async () => {
|
||||
loading.value = true
|
||||
@@ -855,21 +810,6 @@
|
||||
})
|
||||
}
|
||||
|
||||
// 卡管理
|
||||
const manageCards = (row: EnterpriseItem) => {
|
||||
router.push({
|
||||
path: RoutesAlias.EnterpriseCards,
|
||||
query: { id: row.id }
|
||||
})
|
||||
}
|
||||
|
||||
const manageDevices = (row: EnterpriseItem) => {
|
||||
router.push({
|
||||
path: RoutesAlias.EnterpriseDevices,
|
||||
query: { id: row.id }
|
||||
})
|
||||
}
|
||||
|
||||
// 企业客户操作菜单项配置
|
||||
</script>
|
||||
|
||||
|
||||
@@ -297,8 +297,8 @@
|
||||
v-model="usedDataForm.data_usage"
|
||||
:min="0"
|
||||
:max="usedDataMaxValue"
|
||||
:precision="usedDataForm.unit === 'MB' ? 0 : 3"
|
||||
:step="usedDataForm.unit === 'MB' ? 1 : 0.1"
|
||||
:precision="3"
|
||||
:step="0.1"
|
||||
class="used-data-input__number"
|
||||
placeholder="请输入新的套餐真实已用量"
|
||||
/>
|
||||
@@ -447,8 +447,8 @@
|
||||
return
|
||||
}
|
||||
|
||||
if (dataUsageMb === undefined || dataUsageMb < 0 || !Number.isInteger(dataUsageMb)) {
|
||||
callback(new Error('请输入可换算为非负整数 MB 的已用量'))
|
||||
if (dataUsageMb === undefined || dataUsageMb < 0) {
|
||||
callback(new Error('请输入可换算为非负数字 MB 的已用量'))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
/>
|
||||
<ElButton type="primary" @click="handleQuery">查询</ElButton>
|
||||
<ElButton @click="handleReset()">重置</ElButton>
|
||||
<ElTag type="info" size="small">共 {{ total }} 条</ElTag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -66,6 +65,14 @@
|
||||
|
||||
<div v-else v-loading="loading" class="wallet-table-wrapper">
|
||||
<ElTable v-if="hasTransactions" :data="transactionList" class="wallet-table" border>
|
||||
<ElTableColumn prop="reference_no" label="业务编号">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.reference_no" class="reference-no-link">
|
||||
{{ scope.row.reference_no }}
|
||||
</span>
|
||||
<span v-else>--</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="交易类型" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<ElTag :type="getTransactionTypeTag(scope.row.transaction_type)" size="small">
|
||||
@@ -103,14 +110,6 @@
|
||||
<span v-else>--</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="reference_no" label="业务编号" min-width="180" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.reference_no" class="reference-no-link">
|
||||
{{ scope.row.reference_no }}
|
||||
</span>
|
||||
<span v-else>--</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="remark" label="备注" min-width="150" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
{{ scope.row.remark || '-' }}
|
||||
|
||||
@@ -44,17 +44,17 @@
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
<template v-if="noSeriesId">
|
||||
<div style=" margin-top: 4px; font-size: 12px;color: var(--el-text-color-warning)">
|
||||
<div style="margin-top: 4px; font-size: 12px; color: var(--el-text-color-warning)">
|
||||
该设备未关联套餐系列,无法购买套餐
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="packagesEmptyWithSeriesId">
|
||||
<div style=" margin-top: 4px; font-size: 12px;color: var(--el-text-color-info)">
|
||||
<div style="margin-top: 4px; font-size: 12px; color: var(--el-text-color-info)">
|
||||
该系列暂无可用套餐
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="selectedPackageIsGift">
|
||||
<div style=" margin-top: 4px; font-size: 12px;color: var(--el-color-warning)">
|
||||
<div style="margin-top: 4px; font-size: 12px; color: var(--el-color-warning)">
|
||||
提示: 赠送套餐只能使用线下支付方式,且必须上传支付凭证
|
||||
</div>
|
||||
</template>
|
||||
@@ -83,23 +83,13 @@
|
||||
label="支付凭证"
|
||||
prop="payment_voucher_key"
|
||||
>
|
||||
<ElUpload
|
||||
<VoucherUpload
|
||||
ref="uploadRef"
|
||||
class="payment-voucher-upload"
|
||||
drag
|
||||
:auto-upload="false"
|
||||
:on-change="handleVoucherFileChange"
|
||||
:on-remove="handleRemoveVoucher"
|
||||
accept="image/*"
|
||||
list-type="picture"
|
||||
:limit="1"
|
||||
>
|
||||
<el-icon class="payment-voucher-upload__icon"><UploadFilled /></el-icon>
|
||||
<div class="payment-voucher-upload__text"> 将支付凭证拖到此处,或<em>点击上传</em> </div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">支持 jpg、png 格式,文件大小不超过 5MB</div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
v-model="form.payment_voucher_key"
|
||||
voucher-name="支付凭证"
|
||||
@uploading-change="voucherUploading = $event"
|
||||
@change="formRef?.validateField('payment_voucher_key')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
@@ -111,7 +101,7 @@
|
||||
:loading="loading || voucherUploading"
|
||||
:disabled="voucherUploading"
|
||||
>
|
||||
{{ voucherUploading ? '图片上传中...' : '提交' }}
|
||||
{{ voucherUploading ? '凭证上传中...' : '提交' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
@@ -119,24 +109,22 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, reactive, ref, watch } from 'vue'
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import {
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElIcon,
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElTag,
|
||||
ElUpload
|
||||
ElTag
|
||||
} from 'element-plus'
|
||||
import { UploadFilled } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules, UploadFile, UploadInstance } from 'element-plus'
|
||||
import { OrderService, PackageManageService, StorageService } from '@/api/modules'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { OrderService, PackageManageService } from '@/api/modules'
|
||||
import type { CreateOrderRequest, PackageResponse } from '@/types/api'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
@@ -156,13 +144,12 @@
|
||||
|
||||
const userStore = useUserStore()
|
||||
const formRef = ref<FormInstance>()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
const uploadRef = ref<InstanceType<typeof VoucherUpload>>()
|
||||
const loading = ref(false)
|
||||
const voucherUploading = ref(false)
|
||||
const packageSearchLoading = ref(false)
|
||||
const packageOptions = ref<PackageResponse[]>([])
|
||||
const packagesEmptyWithSeriesId = ref(false)
|
||||
let activeVoucherUploadId = 0
|
||||
|
||||
const form = reactive({
|
||||
package_id: undefined as number | undefined,
|
||||
@@ -311,80 +298,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
const invalidateVoucherUploadState = (clearFileKey = false) => {
|
||||
activeVoucherUploadId += 1
|
||||
voucherUploading.value = false
|
||||
if (clearFileKey) {
|
||||
form.payment_voucher_key = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const handleVoucherFileChange = async (uploadFile: UploadFile) => {
|
||||
const file = uploadFile.raw
|
||||
if (!file) return
|
||||
|
||||
const isImage = file.type.startsWith('image/')
|
||||
if (!isImage) {
|
||||
ElMessage.error('只能上传图片文件!')
|
||||
uploadRef.value?.clearFiles()
|
||||
return
|
||||
}
|
||||
|
||||
const isLt5M = file.size / 1024 / 1024 < 5
|
||||
if (!isLt5M) {
|
||||
ElMessage.error('图片大小不能超过 5MB!')
|
||||
uploadRef.value?.clearFiles()
|
||||
return
|
||||
}
|
||||
|
||||
const requestId = ++activeVoucherUploadId
|
||||
voucherUploading.value = true
|
||||
form.payment_voucher_key = undefined
|
||||
|
||||
try {
|
||||
ElMessage.info('正在上传支付凭证...')
|
||||
|
||||
const uploadUrlRes = await StorageService.getUploadUrl({
|
||||
file_name: file.name,
|
||||
content_type: file.type,
|
||||
purpose: 'attachment'
|
||||
})
|
||||
|
||||
if (uploadUrlRes.code !== 0) {
|
||||
if (requestId !== activeVoucherUploadId) return
|
||||
ElMessage.error(uploadUrlRes.msg || '获取上传地址失败')
|
||||
uploadRef.value?.clearFiles()
|
||||
return
|
||||
}
|
||||
|
||||
const { upload_url, file_key } = uploadUrlRes.data
|
||||
await StorageService.uploadFile(upload_url, file, file.type)
|
||||
|
||||
if (requestId !== activeVoucherUploadId) return
|
||||
|
||||
form.payment_voucher_key = file_key
|
||||
ElMessage.success('上传成功')
|
||||
nextTick(() => {
|
||||
formRef.value?.validateField('payment_voucher_key')
|
||||
})
|
||||
} catch (error: any) {
|
||||
if (requestId !== activeVoucherUploadId) return
|
||||
|
||||
console.error('上传支付凭证失败:', error)
|
||||
ElMessage.error(error?.message || '上传失败,请重试')
|
||||
form.payment_voucher_key = undefined
|
||||
uploadRef.value?.clearFiles()
|
||||
} finally {
|
||||
if (requestId === activeVoucherUploadId) {
|
||||
voucherUploading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveVoucher = () => {
|
||||
invalidateVoucherUploadState(true)
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
visible.value = false
|
||||
}
|
||||
@@ -447,10 +360,11 @@
|
||||
formRef.value?.resetFields()
|
||||
form.package_id = undefined
|
||||
form.payment_method = 'wallet'
|
||||
invalidateVoucherUploadState(true)
|
||||
form.payment_voucher_key = undefined
|
||||
voucherUploading.value = false
|
||||
packageOptions.value = []
|
||||
packagesEmptyWithSeriesId.value = false
|
||||
uploadRef.value?.clearFiles()
|
||||
uploadRef.value?.clearFiles(false)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -460,34 +374,4 @@
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.payment-voucher-upload {
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-upload) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-upload-dragger) {
|
||||
width: 100%;
|
||||
padding: 24px 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
margin-bottom: 12px;
|
||||
font-size: 28px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
|
||||
em {
|
||||
font-style: normal;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
label-width="120"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
@@ -41,6 +40,22 @@
|
||||
>
|
||||
批量设置套餐系列
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:disabled="!selectedDevices.length"
|
||||
@click="showEnterpriseDeviceAuthorizeDialog"
|
||||
v-permission="'devices:authorize_enterprise'"
|
||||
>
|
||||
授权设备给企业
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="warning"
|
||||
:disabled="!selectedDevices.length"
|
||||
@click="showEnterpriseDeviceRecallDialog"
|
||||
v-permission="'devices:recall_enterprise_authorization'"
|
||||
>
|
||||
撤销设备授权
|
||||
</ElButton>
|
||||
<ElButton type="primary" @click="showExportDialog" v-permission="'devices:export'">
|
||||
导出
|
||||
</ElButton>
|
||||
@@ -677,6 +692,250 @@
|
||||
:identifier="operationLogsIdentifier"
|
||||
download-permission="device:download_log_file"
|
||||
/>
|
||||
<ElDialog
|
||||
v-model="enterpriseDeviceAuthorizeDialogVisible"
|
||||
title="授权设备给企业"
|
||||
width="620px"
|
||||
@close="handleEnterpriseDeviceAuthorizeDialogClose"
|
||||
>
|
||||
<ElForm
|
||||
ref="enterpriseDeviceAuthorizeFormRef"
|
||||
:model="enterpriseDeviceAuthorizeForm"
|
||||
:rules="enterpriseDeviceAuthorizeRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<ElFormItem label="授权企业" prop="enterprise_id">
|
||||
<ElSelect
|
||||
v-model="enterpriseDeviceAuthorizeForm.enterprise_id"
|
||||
placeholder="请选择或搜索企业"
|
||||
clearable
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
:remote-method="handleSearchEnterprise"
|
||||
:loading="enterpriseLoading"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in enterpriseOptions"
|
||||
:key="item.id"
|
||||
:label="item.enterprise_name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="选设备方式" prop="selection_type">
|
||||
<ElRadioGroup v-model="enterpriseDeviceAuthorizeForm.selection_type">
|
||||
<ElRadio label="list">设备号列表</ElRadio>
|
||||
<ElRadio label="filter">筛选条件</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="enterpriseDeviceAuthorizeForm.selection_type === DeviceSelectionType.LIST"
|
||||
label="设备号列表"
|
||||
>
|
||||
<div>已选择 {{ selectedDevices.length }} 台设备</div>
|
||||
</ElFormItem>
|
||||
<template
|
||||
v-if="enterpriseDeviceAuthorizeForm.selection_type === DeviceSelectionType.FILTER"
|
||||
>
|
||||
<ElFormItem label="设备号">
|
||||
<ElInput
|
||||
v-model="enterpriseDeviceAuthorizeForm.virtual_no"
|
||||
placeholder="请输入设备号"
|
||||
clearable
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="批次号">
|
||||
<ElSelect
|
||||
v-model="enterpriseDeviceAuthorizeForm.batch_no"
|
||||
placeholder="请选择或搜索批次号"
|
||||
clearable
|
||||
filterable
|
||||
remote
|
||||
:remote-method="handleSearchBatchNo"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in searchBatchNoOptions"
|
||||
:key="item.id"
|
||||
:label="item.batch_no"
|
||||
:value="item.batch_no"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="店铺名称">
|
||||
<ElCascader
|
||||
v-model="enterpriseDeviceAuthorizeForm.shop_id_path"
|
||||
:options="shopCascadeOptions"
|
||||
:props="shopCascadeProps"
|
||||
placeholder="请选择店铺名称"
|
||||
clearable
|
||||
filterable
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
<ElFormItem label="备注">
|
||||
<ElInput
|
||||
v-model="enterpriseDeviceAuthorizeForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注信息"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="enterpriseDeviceAuthorizeDialogVisible = false">取消</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="handleEnterpriseDeviceAuthorize"
|
||||
:loading="enterpriseDeviceAuthorizeLoading"
|
||||
>
|
||||
确认授权
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
<ElDialog
|
||||
v-model="enterpriseDeviceRecallDialogVisible"
|
||||
title="撤销设备授权"
|
||||
width="620px"
|
||||
@close="handleEnterpriseDeviceRecallDialogClose"
|
||||
>
|
||||
<ElForm
|
||||
ref="enterpriseDeviceRecallFormRef"
|
||||
:model="enterpriseDeviceRecallForm"
|
||||
:rules="enterpriseDeviceRecallRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<ElFormItem label="授权企业" prop="enterprise_id">
|
||||
<ElSelect
|
||||
v-model="enterpriseDeviceRecallForm.enterprise_id"
|
||||
placeholder="请选择或搜索企业"
|
||||
clearable
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
:remote-method="handleSearchEnterprise"
|
||||
:loading="enterpriseLoading"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in enterpriseOptions"
|
||||
:key="item.id"
|
||||
:label="item.enterprise_name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="选设备方式" prop="selection_type">
|
||||
<ElRadioGroup v-model="enterpriseDeviceRecallForm.selection_type">
|
||||
<ElRadio label="list">设备号列表</ElRadio>
|
||||
<ElRadio label="filter">筛选条件</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="enterpriseDeviceRecallForm.selection_type === DeviceSelectionType.LIST"
|
||||
label="设备号列表"
|
||||
>
|
||||
<div>已选择 {{ selectedDevices.length }} 台设备</div>
|
||||
</ElFormItem>
|
||||
<template
|
||||
v-if="enterpriseDeviceRecallForm.selection_type === DeviceSelectionType.FILTER"
|
||||
>
|
||||
<ElFormItem label="设备号">
|
||||
<ElInput
|
||||
v-model="enterpriseDeviceRecallForm.virtual_no"
|
||||
placeholder="请输入设备号"
|
||||
clearable
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="批次号">
|
||||
<ElSelect
|
||||
v-model="enterpriseDeviceRecallForm.batch_no"
|
||||
placeholder="请选择或搜索批次号"
|
||||
clearable
|
||||
filterable
|
||||
remote
|
||||
:remote-method="handleSearchBatchNo"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in searchBatchNoOptions"
|
||||
:key="item.id"
|
||||
:label="item.batch_no"
|
||||
:value="item.batch_no"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="enterpriseDeviceRecallDialogVisible = false">取消</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="handleEnterpriseDeviceRecall"
|
||||
:loading="enterpriseDeviceRecallLoading"
|
||||
>
|
||||
确认撤销
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
<ElDialog
|
||||
v-model="enterpriseDeviceResultDialogVisible"
|
||||
:title="enterpriseDeviceResultTitle"
|
||||
width="700px"
|
||||
>
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem label="成功数">
|
||||
<ElTag type="success">{{ enterpriseDeviceOperationResult.success_count }}</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="失败数">
|
||||
<ElTag type="danger">{{ enterpriseDeviceOperationResult.fail_count }}</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
<div
|
||||
v-if="
|
||||
enterpriseDeviceOperationResult.failed_items &&
|
||||
enterpriseDeviceOperationResult.failed_items.length > 0
|
||||
"
|
||||
style="margin-top: 20px"
|
||||
>
|
||||
<ElDivider content-position="left">失败项详情</ElDivider>
|
||||
<ElTable :data="enterpriseDeviceOperationResult.failed_items" border max-height="300">
|
||||
<ElTableColumn prop="virtual_no" label="设备号" width="180" />
|
||||
<ElTableColumn prop="reason" label="失败原因" />
|
||||
</ElTable>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
enterpriseDeviceOperationResult.authorized_devices &&
|
||||
enterpriseDeviceOperationResult.authorized_devices.length > 0
|
||||
"
|
||||
style="margin-top: 20px"
|
||||
>
|
||||
<ElDivider content-position="left">已授权设备</ElDivider>
|
||||
<ElTable
|
||||
:data="enterpriseDeviceOperationResult.authorized_devices"
|
||||
border
|
||||
max-height="220"
|
||||
>
|
||||
<ElTableColumn prop="virtual_no" label="设备号" width="180" />
|
||||
<ElTableColumn prop="card_count" label="绑定卡数" />
|
||||
</ElTable>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton type="primary" @click="enterpriseDeviceResultDialogVisible = false"
|
||||
>确定</ElButton
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
<ExportTaskCreateDialog
|
||||
v-model="exportDialogVisible"
|
||||
scene="device"
|
||||
@@ -699,7 +958,7 @@
|
||||
CardService,
|
||||
PackageSeriesService,
|
||||
AssetService,
|
||||
CarrierService
|
||||
EnterpriseService
|
||||
} from '@/api/modules'
|
||||
import { DeviceSelectionType } from '@/types/api'
|
||||
import { ElMessage, ElMessageBox, ElTag, ElIcon, ElRadioGroup, ElRadio } from 'element-plus'
|
||||
@@ -720,6 +979,11 @@
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import type { PackageSeriesResponse } from '@/types/api'
|
||||
import type { EnterpriseItem } from '@/types/api/enterprise'
|
||||
import type {
|
||||
EnterpriseAllocateDevicesResponse,
|
||||
EnterpriseRecallDevicesResponse
|
||||
} from '@/types/api/enterpriseDevice'
|
||||
import RealnamePolicyDialog from '@/components/device/RealnamePolicyDialog.vue'
|
||||
import OperationLogsDialog from '@/components/business/OperationLogsDialog.vue'
|
||||
import ExportTaskCreateDialog from '@/components/business/ExportTaskCreateDialog.vue'
|
||||
@@ -781,6 +1045,20 @@
|
||||
label: 'label',
|
||||
children: 'children'
|
||||
}
|
||||
const loadTopLevelShopCascadeOptions = async () => {
|
||||
try {
|
||||
const res = await ShopService.getShopsCascade({ parent_id: undefined })
|
||||
if (res.code === 0) {
|
||||
shopCascadeOptions.value = (res.data || []).map((item: any) => ({
|
||||
value: item.id,
|
||||
label: item.shop_name,
|
||||
leaf: !item.has_children
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载顶级店铺失败:', error)
|
||||
}
|
||||
}
|
||||
const seriesBindingShopCascadeOptions = ref<any[]>([])
|
||||
const seriesBindingShopCascadeProps = {
|
||||
lazy: true,
|
||||
@@ -815,6 +1093,27 @@
|
||||
}
|
||||
const allocateResult = ref<AllocateDevicesResponse | null>(null)
|
||||
const recallResult = ref<RecallDevicesResponse | null>(null)
|
||||
const enterpriseDeviceAuthorizeDialogVisible = ref(false)
|
||||
const enterpriseDeviceRecallDialogVisible = ref(false)
|
||||
const enterpriseDeviceResultDialogVisible = ref(false)
|
||||
const enterpriseDeviceAuthorizeLoading = ref(false)
|
||||
const enterpriseDeviceRecallLoading = ref(false)
|
||||
const enterpriseDeviceResultTitle = ref('')
|
||||
const enterpriseDeviceAuthorizeFormRef = ref<FormInstance>()
|
||||
const enterpriseDeviceRecallFormRef = ref<FormInstance>()
|
||||
const enterpriseOptions = ref<EnterpriseItem[]>([])
|
||||
const enterpriseLoading = ref(false)
|
||||
const enterpriseDeviceOperationResult = ref<
|
||||
Partial<EnterpriseAllocateDevicesResponse & EnterpriseRecallDevicesResponse> & {
|
||||
success_count: number
|
||||
fail_count: number
|
||||
}
|
||||
>({
|
||||
success_count: 0,
|
||||
fail_count: 0,
|
||||
failed_items: null,
|
||||
authorized_devices: null
|
||||
})
|
||||
|
||||
// 套餐系列绑定相关
|
||||
const seriesBindingDialogVisible = ref(false)
|
||||
@@ -840,6 +1139,22 @@
|
||||
created_at_end: ''
|
||||
})
|
||||
const seriesBindingForm = reactive(createInitialSeriesBindingState())
|
||||
const createInitialEnterpriseDeviceAuthorizeState = () => ({
|
||||
enterprise_id: undefined as number | undefined,
|
||||
selection_type: DeviceSelectionType.LIST,
|
||||
virtual_no: '',
|
||||
batch_no: '',
|
||||
shop_id_path: undefined as number[] | undefined,
|
||||
remark: ''
|
||||
})
|
||||
const createInitialEnterpriseDeviceRecallState = () => ({
|
||||
enterprise_id: undefined as number | undefined,
|
||||
selection_type: DeviceSelectionType.LIST,
|
||||
virtual_no: '',
|
||||
batch_no: ''
|
||||
})
|
||||
const enterpriseDeviceAuthorizeForm = reactive(createInitialEnterpriseDeviceAuthorizeState())
|
||||
const enterpriseDeviceRecallForm = reactive(createInitialEnterpriseDeviceRecallState())
|
||||
const parseNumericStringToBigInt = (value: string): bigint | null => {
|
||||
const normalizedValue = value.trim()
|
||||
if (!normalizedValue || !/^\d+$/.test(normalizedValue)) {
|
||||
@@ -931,6 +1246,13 @@
|
||||
}
|
||||
]
|
||||
})
|
||||
const createEnterpriseDeviceRules = () =>
|
||||
reactive<FormRules>({
|
||||
enterprise_id: [{ required: true, message: '请选择企业', trigger: 'change' }],
|
||||
selection_type: [{ required: true, message: '请选择选设备方式', trigger: 'change' }]
|
||||
})
|
||||
const enterpriseDeviceAuthorizeRules = createEnterpriseDeviceRules()
|
||||
const enterpriseDeviceRecallRules = createEnterpriseDeviceRules()
|
||||
const seriesBindingResult = ref<BatchSetDeviceSeriesBindingResponse | null>(null)
|
||||
|
||||
// 搜索表单店铺/系列选项
|
||||
@@ -999,13 +1321,15 @@
|
||||
shop_id: undefined as number | undefined,
|
||||
series_id: undefined as number | undefined,
|
||||
has_active_package: undefined as boolean | undefined,
|
||||
authorized_enterprise_id: undefined as number | undefined,
|
||||
is_authorized_to_enterprise: undefined as boolean | undefined,
|
||||
dateRange: [] as string[],
|
||||
created_at_start: '',
|
||||
created_at_end: ''
|
||||
}
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({ ...initialSearchState })
|
||||
const searchForm = reactive<Record<string, any>>({ ...initialSearchState })
|
||||
|
||||
// 搜索批次号(用于搜索表单)
|
||||
const loadSearchBatchNoOptions = async (batchNo?: string) => {
|
||||
@@ -1174,7 +1498,7 @@
|
||||
}))
|
||||
},
|
||||
{
|
||||
label: '起始至结束',
|
||||
label: '起止时间',
|
||||
prop: 'dateRange',
|
||||
type: 'date',
|
||||
config: {
|
||||
@@ -1189,7 +1513,7 @@
|
||||
|
||||
// 分页
|
||||
searchFormItems.splice(searchFormItems.length - 1, 0, {
|
||||
label: '是否有生效套餐',
|
||||
label: '有效套餐',
|
||||
prop: 'has_active_package',
|
||||
type: 'select',
|
||||
config: {
|
||||
@@ -1201,6 +1525,37 @@
|
||||
{ label: '否', value: false }
|
||||
]
|
||||
})
|
||||
searchFormItems.splice(searchFormItems.length - 1, 0, {
|
||||
label: '授权企业',
|
||||
prop: 'authorized_enterprise_id',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
remote: true,
|
||||
remoteMethod: (query: string) => handleSearchEnterprise(query),
|
||||
loading: enterpriseLoading.value,
|
||||
placeholder: '请选择或搜索企业'
|
||||
},
|
||||
options: () =>
|
||||
enterpriseOptions.value.map((enterprise) => ({
|
||||
label: enterprise.enterprise_name,
|
||||
value: enterprise.id
|
||||
}))
|
||||
})
|
||||
searchFormItems.splice(searchFormItems.length - 1, 0, {
|
||||
label: '授权状态',
|
||||
prop: 'is_authorized_to_enterprise',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '已授权', value: true },
|
||||
{ label: '未授权', value: false }
|
||||
]
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
@@ -1704,6 +2059,26 @@
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearchEnterprise = async (query?: string) => {
|
||||
try {
|
||||
enterpriseLoading.value = true
|
||||
const params = {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
enterprise_name: query || undefined,
|
||||
status: 1
|
||||
}
|
||||
const res = await EnterpriseService.getEnterprises(params)
|
||||
if (res.code === 0) {
|
||||
enterpriseOptions.value = res.data.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索企业失败:', error)
|
||||
} finally {
|
||||
enterpriseLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
let isFirstActivation = true
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
@@ -1735,6 +2110,8 @@
|
||||
shop_id: searchForm.shop_id || undefined,
|
||||
series_id: searchForm.series_id || undefined,
|
||||
has_active_package: searchForm.has_active_package ?? undefined,
|
||||
authorized_enterprise_id: searchForm.authorized_enterprise_id || undefined,
|
||||
is_authorized_to_enterprise: searchForm.is_authorized_to_enterprise ?? undefined,
|
||||
created_at_start: searchForm.created_at_start || undefined,
|
||||
created_at_end: searchForm.created_at_end || undefined
|
||||
}
|
||||
@@ -1763,6 +2140,8 @@
|
||||
shop_id: searchForm.shop_id || undefined,
|
||||
series_id: searchForm.series_id || undefined,
|
||||
has_active_package: searchForm.has_active_package ?? undefined,
|
||||
authorized_enterprise_id: searchForm.authorized_enterprise_id || undefined,
|
||||
is_authorized_to_enterprise: searchForm.is_authorized_to_enterprise ?? undefined,
|
||||
created_at_start: searchForm.created_at_start || undefined,
|
||||
created_at_end: searchForm.created_at_end || undefined
|
||||
}
|
||||
@@ -1993,6 +2372,160 @@
|
||||
recallForm.remark = ''
|
||||
}
|
||||
|
||||
const getInferredDeviceEnterpriseId = () => {
|
||||
const filterEnterpriseId = searchForm.authorized_enterprise_id
|
||||
if (filterEnterpriseId) return filterEnterpriseId
|
||||
|
||||
const selectedEnterpriseIds = Array.from(
|
||||
new Set(
|
||||
selectedDevices.value
|
||||
.map((device) => device.authorized_enterprise_id)
|
||||
.filter((id): id is number => typeof id === 'number')
|
||||
)
|
||||
)
|
||||
|
||||
return selectedEnterpriseIds.length === 1 ? selectedEnterpriseIds[0] : undefined
|
||||
}
|
||||
|
||||
const resetEnterpriseDeviceAuthorizeForm = () => {
|
||||
Object.assign(enterpriseDeviceAuthorizeForm, createInitialEnterpriseDeviceAuthorizeState(), {
|
||||
selection_type:
|
||||
selectedDevices.value.length > 0 ? DeviceSelectionType.LIST : DeviceSelectionType.FILTER,
|
||||
virtual_no: searchForm.virtual_no || '',
|
||||
batch_no: searchForm.batch_no || '',
|
||||
shop_id_path: undefined
|
||||
})
|
||||
}
|
||||
|
||||
const resetEnterpriseDeviceRecallForm = () => {
|
||||
Object.assign(enterpriseDeviceRecallForm, createInitialEnterpriseDeviceRecallState(), {
|
||||
enterprise_id: getInferredDeviceEnterpriseId(),
|
||||
selection_type:
|
||||
selectedDevices.value.length > 0 ? DeviceSelectionType.LIST : DeviceSelectionType.FILTER,
|
||||
virtual_no: searchForm.virtual_no || '',
|
||||
batch_no: searchForm.batch_no || ''
|
||||
})
|
||||
}
|
||||
|
||||
const showEnterpriseDeviceAuthorizeDialog = async () => {
|
||||
resetEnterpriseDeviceAuthorizeForm()
|
||||
await Promise.all([
|
||||
handleSearchEnterprise(),
|
||||
loadSearchBatchNoOptions(),
|
||||
loadTopLevelShopCascadeOptions()
|
||||
])
|
||||
enterpriseDeviceAuthorizeDialogVisible.value = true
|
||||
enterpriseDeviceAuthorizeFormRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
const showEnterpriseDeviceRecallDialog = async () => {
|
||||
resetEnterpriseDeviceRecallForm()
|
||||
await Promise.all([handleSearchEnterprise(), loadSearchBatchNoOptions()])
|
||||
enterpriseDeviceRecallDialogVisible.value = true
|
||||
enterpriseDeviceRecallFormRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
const handleEnterpriseDeviceAuthorizeDialogClose = () => {
|
||||
Object.assign(enterpriseDeviceAuthorizeForm, createInitialEnterpriseDeviceAuthorizeState())
|
||||
enterpriseDeviceAuthorizeFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
const handleEnterpriseDeviceRecallDialogClose = () => {
|
||||
Object.assign(enterpriseDeviceRecallForm, createInitialEnterpriseDeviceRecallState())
|
||||
enterpriseDeviceRecallFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
const buildEnterpriseDeviceParams = (form: any) => {
|
||||
const params: Record<string, unknown> = {
|
||||
selection_type: form.selection_type,
|
||||
remark: form.remark || undefined
|
||||
}
|
||||
|
||||
if (form.selection_type === DeviceSelectionType.LIST) {
|
||||
const deviceNos = selectedDevices.value.map((device) => device.virtual_no).filter(Boolean)
|
||||
if (!deviceNos.length) {
|
||||
ElMessage.warning('列表模式下请先选择设备')
|
||||
return null
|
||||
}
|
||||
params.device_nos = deviceNos
|
||||
} else {
|
||||
if (form.batch_no) params.batch_no = form.batch_no
|
||||
if (Array.isArray(form.shop_id_path) && form.shop_id_path.length > 0) {
|
||||
params.shop_id = form.shop_id_path[form.shop_id_path.length - 1]
|
||||
}
|
||||
if (form.virtual_no) params.virtual_no = form.virtual_no
|
||||
}
|
||||
|
||||
Object.keys(params).forEach((key) => {
|
||||
if (params[key] === undefined || params[key] === '') delete params[key]
|
||||
})
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
const applyEnterpriseDeviceOperationSuccess = async (
|
||||
title: string,
|
||||
result: EnterpriseAllocateDevicesResponse | EnterpriseRecallDevicesResponse
|
||||
) => {
|
||||
enterpriseDeviceOperationResult.value = result
|
||||
enterpriseDeviceResultTitle.value = title
|
||||
enterpriseDeviceResultDialogVisible.value = true
|
||||
selectedDevices.value = []
|
||||
await getTableData()
|
||||
}
|
||||
|
||||
const handleEnterpriseDeviceAuthorize = async () => {
|
||||
if (!enterpriseDeviceAuthorizeFormRef.value) return
|
||||
|
||||
await enterpriseDeviceAuthorizeFormRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
const params = buildEnterpriseDeviceParams(enterpriseDeviceAuthorizeForm)
|
||||
if (!params) return
|
||||
|
||||
enterpriseDeviceAuthorizeLoading.value = true
|
||||
try {
|
||||
const res = await EnterpriseService.allocateDevices(
|
||||
enterpriseDeviceAuthorizeForm.enterprise_id!,
|
||||
params as any
|
||||
)
|
||||
if (res.code === 0) {
|
||||
enterpriseDeviceAuthorizeDialogVisible.value = false
|
||||
await applyEnterpriseDeviceOperationSuccess('授权设备给企业结果', res.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('授权设备给企业失败:', error)
|
||||
} finally {
|
||||
enterpriseDeviceAuthorizeLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleEnterpriseDeviceRecall = async () => {
|
||||
if (!enterpriseDeviceRecallFormRef.value) return
|
||||
|
||||
await enterpriseDeviceRecallFormRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
const params = buildEnterpriseDeviceParams(enterpriseDeviceRecallForm)
|
||||
if (!params) return
|
||||
|
||||
enterpriseDeviceRecallLoading.value = true
|
||||
try {
|
||||
const res = await EnterpriseService.recallDevices(
|
||||
enterpriseDeviceRecallForm.enterprise_id!,
|
||||
params as any
|
||||
)
|
||||
if (res.code === 0) {
|
||||
enterpriseDeviceRecallDialogVisible.value = false
|
||||
await applyEnterpriseDeviceOperationSuccess('撤销设备授权结果', res.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('撤销设备授权失败:', error)
|
||||
} finally {
|
||||
enterpriseDeviceRecallLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 批量设置套餐系列
|
||||
const handleBatchSetSeries = async () => {
|
||||
seriesBindingResult.value = null
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
label-width="120"
|
||||
:label-width="90"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
@@ -42,6 +42,22 @@
|
||||
>
|
||||
批量设置套餐系列
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:disabled="selectedCards.length === 0"
|
||||
@click="showEnterpriseCardAuthorizeDialog"
|
||||
v-permission="'iot_card:authorize_enterprise'"
|
||||
>
|
||||
授权卡给企业
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="warning"
|
||||
:disabled="selectedCards.length === 0"
|
||||
@click="showEnterpriseCardRecallDialog"
|
||||
v-permission="'iot_card:recall_enterprise_authorization'"
|
||||
>
|
||||
回收卡授权
|
||||
</ElButton>
|
||||
<ElButton type="primary" @click="showExportDialog" v-permission="'iot_card:export'">
|
||||
导出
|
||||
</ElButton>
|
||||
@@ -357,6 +373,350 @@
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 企业卡授权对话框 -->
|
||||
<ElDialog
|
||||
v-model="enterpriseCardAuthorizeDialogVisible"
|
||||
title="授权卡给企业"
|
||||
width="640px"
|
||||
@close="handleEnterpriseCardAuthorizeDialogClose"
|
||||
>
|
||||
<ElForm
|
||||
ref="enterpriseCardAuthorizeFormRef"
|
||||
:model="enterpriseCardAuthorizeForm"
|
||||
:rules="enterpriseCardAuthorizeRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<ElFormItem label="授权企业" prop="enterprise_id">
|
||||
<ElSelect
|
||||
v-model="enterpriseCardAuthorizeForm.enterprise_id"
|
||||
placeholder="请选择或搜索企业"
|
||||
clearable
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
:remote-method="handleSearchEnterprise"
|
||||
:loading="enterpriseLoading"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in enterpriseOptions"
|
||||
:key="item.id"
|
||||
:label="item.enterprise_name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="选卡方式" prop="selection_type">
|
||||
<ElRadioGroup v-model="enterpriseCardAuthorizeForm.selection_type">
|
||||
<ElRadio label="list">ICCID列表</ElRadio>
|
||||
<ElRadio label="range">号段范围</ElRadio>
|
||||
<ElRadio label="filter">筛选条件</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="enterpriseCardAuthorizeForm.selection_type === CardSelectionType.LIST"
|
||||
label="ICCID列表"
|
||||
>
|
||||
<div>已选择 {{ selectedCards.length }} 张卡</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="enterpriseCardAuthorizeForm.selection_type === CardSelectionType.RANGE"
|
||||
label="起始ICCID"
|
||||
prop="iccid_start"
|
||||
>
|
||||
<ElInput
|
||||
v-model="enterpriseCardAuthorizeForm.iccid_start"
|
||||
placeholder="请输入起始ICCID"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="enterpriseCardAuthorizeForm.selection_type === CardSelectionType.RANGE"
|
||||
label="结束ICCID"
|
||||
prop="iccid_end"
|
||||
>
|
||||
<ElInput
|
||||
v-model="enterpriseCardAuthorizeForm.iccid_end"
|
||||
placeholder="请输入结束ICCID"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<div
|
||||
v-if="
|
||||
enterpriseCardAuthorizeForm.selection_type === CardSelectionType.RANGE &&
|
||||
enterpriseCardAuthorizeRangeCountText
|
||||
"
|
||||
class="range-allocation-hint range-allocation-hint--card-form"
|
||||
>
|
||||
共授权{{ enterpriseCardAuthorizeRangeCountText }}张卡
|
||||
</div>
|
||||
<template
|
||||
v-if="enterpriseCardAuthorizeForm.selection_type === CardSelectionType.FILTER"
|
||||
>
|
||||
<ElFormItem label="运营商">
|
||||
<ElSelect
|
||||
v-model="enterpriseCardAuthorizeForm.carrier_id"
|
||||
placeholder="请选择或搜索运营商"
|
||||
clearable
|
||||
filterable
|
||||
remote
|
||||
:remote-method="handleSearchAllocateCarrier"
|
||||
:loading="allocateCarrierLoading"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in allocateCarrierOptions"
|
||||
:key="item.id"
|
||||
:label="item.carrier_name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="店铺">
|
||||
<ElCascader
|
||||
v-model="enterpriseCardAuthorizeForm.shop_id_path"
|
||||
:options="shopCascadeOptions"
|
||||
:props="shopCascadeProps"
|
||||
placeholder="请选择店铺"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
filterable
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="批次号">
|
||||
<ElSelect
|
||||
v-model="enterpriseCardAuthorizeForm.batch_no"
|
||||
placeholder="请选择或搜索批次号"
|
||||
clearable
|
||||
filterable
|
||||
remote
|
||||
:remote-method="handleSearchAllocateBatchNo"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in allocateBatchNoOptions"
|
||||
:key="item.id"
|
||||
:label="item.batch_no"
|
||||
:value="item.batch_no"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="ICCID">
|
||||
<ElInput v-model="enterpriseCardAuthorizeForm.iccid" placeholder="请输入ICCID" />
|
||||
</ElFormItem>
|
||||
</template>
|
||||
<ElFormItem label="备注">
|
||||
<ElInput
|
||||
v-model="enterpriseCardAuthorizeForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注信息"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="enterpriseCardAuthorizeDialogVisible = false">取消</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="handleEnterpriseCardAuthorize"
|
||||
:loading="enterpriseCardAuthorizeLoading"
|
||||
>
|
||||
确认授权
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 企业卡授权回收对话框 -->
|
||||
<ElDialog
|
||||
v-model="enterpriseCardRecallDialogVisible"
|
||||
title="回收卡授权"
|
||||
width="640px"
|
||||
@close="handleEnterpriseCardRecallDialogClose"
|
||||
>
|
||||
<ElForm
|
||||
ref="enterpriseCardRecallFormRef"
|
||||
:model="enterpriseCardRecallForm"
|
||||
:rules="enterpriseCardRecallRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<ElFormItem label="授权企业" prop="enterprise_id">
|
||||
<ElSelect
|
||||
v-model="enterpriseCardRecallForm.enterprise_id"
|
||||
placeholder="请选择或搜索企业"
|
||||
clearable
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
:remote-method="handleSearchEnterprise"
|
||||
:loading="enterpriseLoading"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in enterpriseOptions"
|
||||
:key="item.id"
|
||||
:label="item.enterprise_name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="选卡方式" prop="selection_type">
|
||||
<ElRadioGroup v-model="enterpriseCardRecallForm.selection_type">
|
||||
<ElRadio label="list">ICCID列表</ElRadio>
|
||||
<ElRadio label="range">号段范围</ElRadio>
|
||||
<ElRadio label="filter">筛选条件</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="enterpriseCardRecallForm.selection_type === CardSelectionType.LIST"
|
||||
label="ICCID列表"
|
||||
>
|
||||
<div>已选择 {{ selectedCards.length }} 张卡</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="enterpriseCardRecallForm.selection_type === CardSelectionType.RANGE"
|
||||
label="起始ICCID"
|
||||
prop="iccid_start"
|
||||
>
|
||||
<ElInput
|
||||
v-model="enterpriseCardRecallForm.iccid_start"
|
||||
placeholder="请输入起始ICCID"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="enterpriseCardRecallForm.selection_type === CardSelectionType.RANGE"
|
||||
label="结束ICCID"
|
||||
prop="iccid_end"
|
||||
>
|
||||
<ElInput v-model="enterpriseCardRecallForm.iccid_end" placeholder="请输入结束ICCID" />
|
||||
</ElFormItem>
|
||||
<div
|
||||
v-if="
|
||||
enterpriseCardRecallForm.selection_type === CardSelectionType.RANGE &&
|
||||
enterpriseCardRecallRangeCountText
|
||||
"
|
||||
class="range-allocation-hint range-allocation-hint--card-form"
|
||||
>
|
||||
共回收{{ enterpriseCardRecallRangeCountText }}张卡
|
||||
</div>
|
||||
<template v-if="enterpriseCardRecallForm.selection_type === CardSelectionType.FILTER">
|
||||
<ElFormItem label="运营商">
|
||||
<ElSelect
|
||||
v-model="enterpriseCardRecallForm.carrier_id"
|
||||
placeholder="请选择或搜索运营商"
|
||||
clearable
|
||||
filterable
|
||||
remote
|
||||
:remote-method="handleSearchAllocateCarrier"
|
||||
:loading="allocateCarrierLoading"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in allocateCarrierOptions"
|
||||
:key="item.id"
|
||||
:label="item.carrier_name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="批次号">
|
||||
<ElSelect
|
||||
v-model="enterpriseCardRecallForm.batch_no"
|
||||
placeholder="请选择或搜索批次号"
|
||||
clearable
|
||||
filterable
|
||||
remote
|
||||
:remote-method="handleSearchAllocateBatchNo"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in allocateBatchNoOptions"
|
||||
:key="item.id"
|
||||
:label="item.batch_no"
|
||||
:value="item.batch_no"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="ICCID">
|
||||
<ElInput v-model="enterpriseCardRecallForm.iccid" placeholder="请输入ICCID" />
|
||||
</ElFormItem>
|
||||
</template>
|
||||
<ElFormItem label="备注">
|
||||
<ElInput
|
||||
v-model="enterpriseCardRecallForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注信息"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="enterpriseCardRecallDialogVisible = false">取消</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="handleEnterpriseCardRecall"
|
||||
:loading="enterpriseCardRecallLoading"
|
||||
>
|
||||
确认回收
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 企业卡授权操作结果 -->
|
||||
<ElDialog
|
||||
v-model="enterpriseCardResultDialogVisible"
|
||||
:title="enterpriseCardResultTitle"
|
||||
width="700px"
|
||||
>
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem label="成功数">
|
||||
<ElTag type="success">{{ enterpriseCardOperationResult.success_count }}</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="失败数">
|
||||
<ElTag type="danger">{{ enterpriseCardOperationResult.fail_count }}</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
<div
|
||||
v-if="
|
||||
enterpriseCardOperationResult.failed_items &&
|
||||
enterpriseCardOperationResult.failed_items.length > 0
|
||||
"
|
||||
style="margin-top: 20px"
|
||||
>
|
||||
<ElDivider content-position="left">失败项详情</ElDivider>
|
||||
<ElTable :data="enterpriseCardOperationResult.failed_items" border max-height="300">
|
||||
<ElTableColumn prop="iccid" label="ICCID" width="200" />
|
||||
<ElTableColumn prop="reason" label="失败原因" />
|
||||
</ElTable>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
enterpriseCardOperationResult.recalled_devices &&
|
||||
enterpriseCardOperationResult.recalled_devices.length > 0
|
||||
"
|
||||
style="margin-top: 20px"
|
||||
>
|
||||
<ElDivider content-position="left">连带回收设备</ElDivider>
|
||||
<ElTable :data="enterpriseCardOperationResult.recalled_devices" border max-height="220">
|
||||
<ElTableColumn prop="virtual_no" label="设备虚拟号" width="180" />
|
||||
<ElTableColumn prop="card_count" label="关联卡数" width="100" />
|
||||
<ElTableColumn label="ICCID列表">
|
||||
<template #default="{ row }">
|
||||
{{ row.iccids?.join(', ') || '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton type="primary" @click="enterpriseCardResultDialogVisible = false"
|
||||
>确定</ElButton
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 批量设置套餐系列绑定对话框 -->
|
||||
<ElDialog
|
||||
v-model="seriesBindingDialogVisible"
|
||||
@@ -652,7 +1012,8 @@
|
||||
ShopService,
|
||||
PackageSeriesService,
|
||||
AssetService,
|
||||
CarrierService
|
||||
CarrierService,
|
||||
EnterpriseService
|
||||
} from '@/api/modules'
|
||||
import { ElMessage, ElTag, ElIcon, ElMessageBox } from 'element-plus'
|
||||
import { Loading } from '@element-plus/icons-vue'
|
||||
@@ -677,6 +1038,8 @@
|
||||
} from '@/types/api/card'
|
||||
import { CardSelectionType } from '@/types/api/card'
|
||||
import type { PackageSeriesResponse } from '@/types/api'
|
||||
import type { EnterpriseItem } from '@/types/api/enterprise'
|
||||
import type { AllocateCardsResponse, RecallCardsResponse } from '@/types/api/enterpriseCard'
|
||||
import OperationLogsDialog from '@/components/business/OperationLogsDialog.vue'
|
||||
|
||||
defineOptions({ name: 'StandaloneCardList' })
|
||||
@@ -704,6 +1067,27 @@
|
||||
fail_count: 0,
|
||||
failed_items: null
|
||||
})
|
||||
const enterpriseCardAuthorizeDialogVisible = ref(false)
|
||||
const enterpriseCardRecallDialogVisible = ref(false)
|
||||
const enterpriseCardResultDialogVisible = ref(false)
|
||||
const enterpriseCardAuthorizeLoading = ref(false)
|
||||
const enterpriseCardRecallLoading = ref(false)
|
||||
const enterpriseCardResultTitle = ref('')
|
||||
const enterpriseCardAuthorizeFormRef = ref<FormInstance>()
|
||||
const enterpriseCardRecallFormRef = ref<FormInstance>()
|
||||
const enterpriseOptions = ref<EnterpriseItem[]>([])
|
||||
const enterpriseLoading = ref(false)
|
||||
const enterpriseCardOperationResult = ref<
|
||||
Partial<AllocateCardsResponse & RecallCardsResponse> & {
|
||||
success_count: number
|
||||
fail_count: number
|
||||
}
|
||||
>({
|
||||
success_count: 0,
|
||||
fail_count: 0,
|
||||
failed_items: null,
|
||||
recalled_devices: null
|
||||
})
|
||||
|
||||
// 操作审计日志弹窗
|
||||
const operationLogsDialogVisible = ref(false)
|
||||
@@ -727,6 +1111,29 @@
|
||||
batch_no: ''
|
||||
})
|
||||
const seriesBindingForm = reactive(createInitialSeriesBindingState())
|
||||
const createInitialEnterpriseCardAuthorizeState = () => ({
|
||||
enterprise_id: undefined as number | undefined,
|
||||
selection_type: CardSelectionType.LIST,
|
||||
iccid_start: '',
|
||||
iccid_end: '',
|
||||
carrier_id: undefined as number | undefined,
|
||||
shop_id_path: undefined as number[] | undefined,
|
||||
batch_no: '',
|
||||
iccid: '',
|
||||
remark: ''
|
||||
})
|
||||
const createInitialEnterpriseCardRecallState = () => ({
|
||||
enterprise_id: undefined as number | undefined,
|
||||
selection_type: CardSelectionType.LIST,
|
||||
iccid_start: '',
|
||||
iccid_end: '',
|
||||
carrier_id: undefined as number | undefined,
|
||||
batch_no: '',
|
||||
iccid: '',
|
||||
remark: ''
|
||||
})
|
||||
const enterpriseCardAuthorizeForm = reactive(createInitialEnterpriseCardAuthorizeState())
|
||||
const enterpriseCardRecallForm = reactive(createInitialEnterpriseCardRecallState())
|
||||
const parseNumericStringToBigInt = (value: string): bigint | null => {
|
||||
const normalizedValue = value.trim()
|
||||
if (!normalizedValue || !/^\d+$/.test(normalizedValue)) {
|
||||
@@ -761,9 +1168,54 @@
|
||||
const recallRangeCountText = computed(() =>
|
||||
getFormattedRangeCountText(recallForm.iccid_start, recallForm.iccid_end)
|
||||
)
|
||||
const enterpriseCardAuthorizeRangeCountText = computed(() =>
|
||||
getFormattedRangeCountText(
|
||||
enterpriseCardAuthorizeForm.iccid_start,
|
||||
enterpriseCardAuthorizeForm.iccid_end
|
||||
)
|
||||
)
|
||||
const enterpriseCardRecallRangeCountText = computed(() =>
|
||||
getFormattedRangeCountText(
|
||||
enterpriseCardRecallForm.iccid_start,
|
||||
enterpriseCardRecallForm.iccid_end
|
||||
)
|
||||
)
|
||||
const seriesBindingRangeCountText = computed(() =>
|
||||
getFormattedRangeCountText(seriesBindingForm.iccid_start, seriesBindingForm.iccid_end)
|
||||
)
|
||||
const createEnterpriseCardRules = (form: { selection_type: CardSelectionType }) =>
|
||||
reactive<FormRules>({
|
||||
enterprise_id: [{ required: true, message: '请选择企业', trigger: 'change' }],
|
||||
selection_type: [{ required: true, message: '请选择选卡方式', trigger: 'change' }],
|
||||
iccid_start: [
|
||||
{
|
||||
required: true,
|
||||
validator: (_rule, value, callback) => {
|
||||
if (form.selection_type === CardSelectionType.RANGE && !value) {
|
||||
callback(new Error('请输入起始ICCID'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
iccid_end: [
|
||||
{
|
||||
required: true,
|
||||
validator: (_rule, value, callback) => {
|
||||
if (form.selection_type === CardSelectionType.RANGE && !value) {
|
||||
callback(new Error('请输入结束ICCID'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
})
|
||||
const enterpriseCardAuthorizeRules = createEnterpriseCardRules(enterpriseCardAuthorizeForm)
|
||||
const enterpriseCardRecallRules = createEnterpriseCardRules(enterpriseCardRecallForm)
|
||||
const seriesBindingRules = reactive<FormRules>({
|
||||
selection_type: [{ required: true, message: '请选择选卡方式', trigger: 'change' }],
|
||||
series_id: [{ required: true, message: '请选择套餐系列', trigger: 'change' }],
|
||||
@@ -921,6 +1373,26 @@
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearchEnterprise = async (query?: string) => {
|
||||
try {
|
||||
enterpriseLoading.value = true
|
||||
const params = {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
enterprise_name: query || undefined,
|
||||
status: 1
|
||||
}
|
||||
const res = await EnterpriseService.getEnterprises(params)
|
||||
if (res.code === 0) {
|
||||
enterpriseOptions.value = res.data.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索企业失败:', error)
|
||||
} finally {
|
||||
enterpriseLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索套餐系列(用于搜索栏)
|
||||
const handleSearchSeries = async (query: string) => {
|
||||
try {
|
||||
@@ -1043,10 +1515,25 @@
|
||||
label: 'label',
|
||||
children: 'children'
|
||||
}
|
||||
const loadTopLevelShopCascadeOptions = async () => {
|
||||
try {
|
||||
const res = await ShopService.getShopsCascade({ parent_id: undefined })
|
||||
if (res.code === 0) {
|
||||
shopCascadeOptions.value = (res.data || []).map((item: any) => ({
|
||||
value: item.id,
|
||||
label: item.shop_name,
|
||||
leaf: !item.has_children
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载顶级店铺失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState: {
|
||||
status: undefined | number
|
||||
network_status: undefined | number
|
||||
iccid: string
|
||||
carrier_name: string
|
||||
msisdn: string
|
||||
@@ -1057,9 +1544,12 @@
|
||||
has_active_package: undefined | boolean
|
||||
is_distributed: undefined | number
|
||||
is_standalone: undefined | boolean
|
||||
authorized_enterprise_id: undefined | number
|
||||
is_authorized_to_enterprise: undefined | boolean
|
||||
[key: string]: any
|
||||
} = {
|
||||
status: undefined,
|
||||
network_status: undefined,
|
||||
iccid: '',
|
||||
carrier_name: '',
|
||||
msisdn: '',
|
||||
@@ -1070,6 +1560,8 @@
|
||||
has_active_package: undefined,
|
||||
is_distributed: undefined,
|
||||
is_standalone: undefined,
|
||||
authorized_enterprise_id: undefined,
|
||||
is_authorized_to_enterprise: undefined,
|
||||
is_replaced: undefined,
|
||||
iccid_start: '',
|
||||
iccid_end: ''
|
||||
@@ -1184,7 +1676,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '运营商名称',
|
||||
label: '运营商',
|
||||
prop: 'carrier_name',
|
||||
type: 'select',
|
||||
config: {
|
||||
@@ -1216,6 +1708,19 @@
|
||||
{ label: '已停用', value: 4 }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '网络状态',
|
||||
prop: 'network_status',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '停机', value: 0 },
|
||||
{ label: '正常', value: 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '卡虚拟号',
|
||||
prop: 'virtual_no',
|
||||
@@ -1235,7 +1740,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '绑定设备虚拟号',
|
||||
label: '设备虚拟号',
|
||||
prop: 'device_virtual_no',
|
||||
type: 'input',
|
||||
config: {
|
||||
@@ -1279,7 +1784,7 @@
|
||||
}))
|
||||
},
|
||||
{
|
||||
label: '是否已分销',
|
||||
label: '分销状态',
|
||||
prop: 'is_distributed',
|
||||
type: 'select',
|
||||
config: {
|
||||
@@ -1292,7 +1797,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '是否绑定设备',
|
||||
label: '是否绑设备',
|
||||
prop: 'is_standalone',
|
||||
type: 'select',
|
||||
config: {
|
||||
@@ -1305,7 +1810,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '是否有换卡记录',
|
||||
label: '有无换卡',
|
||||
prop: 'is_replaced',
|
||||
type: 'select',
|
||||
config: {
|
||||
@@ -1318,7 +1823,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'ICCID起始号',
|
||||
label: '起始号',
|
||||
prop: 'iccid_start',
|
||||
type: 'input',
|
||||
config: {
|
||||
@@ -1327,7 +1832,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'ICCID结束号',
|
||||
label: '结束号',
|
||||
prop: 'iccid_end',
|
||||
type: 'input',
|
||||
config: {
|
||||
@@ -1339,7 +1844,7 @@
|
||||
|
||||
// 搜索表单配置
|
||||
baseFormItems.splice(8, 0, {
|
||||
label: '是否有生效套餐',
|
||||
label: '有效套餐',
|
||||
prop: 'has_active_package',
|
||||
type: 'select',
|
||||
config: {
|
||||
@@ -1351,6 +1856,37 @@
|
||||
{ label: '否', value: false }
|
||||
]
|
||||
})
|
||||
baseFormItems.splice(9, 0, {
|
||||
label: '授权企业',
|
||||
prop: 'authorized_enterprise_id',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
remote: true,
|
||||
remoteMethod: handleSearchEnterprise,
|
||||
loading: enterpriseLoading.value,
|
||||
placeholder: '请选择或搜索企业'
|
||||
},
|
||||
options: () =>
|
||||
enterpriseOptions.value.map((enterprise) => ({
|
||||
label: enterprise.enterprise_name,
|
||||
value: enterprise.id
|
||||
}))
|
||||
})
|
||||
baseFormItems.splice(10, 0, {
|
||||
label: '授权状态',
|
||||
prop: 'is_authorized_to_enterprise',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '已授权', value: true },
|
||||
{ label: '未授权', value: false }
|
||||
]
|
||||
})
|
||||
|
||||
const formItems: SearchFormItem[] = baseFormItems
|
||||
|
||||
@@ -1999,6 +2535,175 @@
|
||||
})
|
||||
}
|
||||
|
||||
const getInferredCardEnterpriseId = () => {
|
||||
const filterEnterpriseId = formFilters.authorized_enterprise_id
|
||||
if (filterEnterpriseId) return filterEnterpriseId
|
||||
|
||||
const selectedEnterpriseIds = Array.from(
|
||||
new Set(
|
||||
selectedCards.value
|
||||
.map((card) => card.authorized_enterprise_id)
|
||||
.filter((id): id is number => typeof id === 'number')
|
||||
)
|
||||
)
|
||||
|
||||
return selectedEnterpriseIds.length === 1 ? selectedEnterpriseIds[0] : undefined
|
||||
}
|
||||
|
||||
const resetEnterpriseCardAuthorizeForm = () => {
|
||||
Object.assign(enterpriseCardAuthorizeForm, createInitialEnterpriseCardAuthorizeState(), {
|
||||
selection_type:
|
||||
selectedCards.value.length > 0 ? CardSelectionType.LIST : CardSelectionType.FILTER,
|
||||
carrier_id: undefined,
|
||||
shop_id_path: undefined,
|
||||
batch_no: formFilters.batch_no || '',
|
||||
iccid: formFilters.iccid || ''
|
||||
})
|
||||
}
|
||||
|
||||
const resetEnterpriseCardRecallForm = () => {
|
||||
Object.assign(enterpriseCardRecallForm, createInitialEnterpriseCardRecallState(), {
|
||||
enterprise_id: getInferredCardEnterpriseId(),
|
||||
selection_type:
|
||||
selectedCards.value.length > 0 ? CardSelectionType.LIST : CardSelectionType.FILTER,
|
||||
carrier_id: undefined,
|
||||
batch_no: formFilters.batch_no || '',
|
||||
iccid: formFilters.iccid || ''
|
||||
})
|
||||
}
|
||||
|
||||
const showEnterpriseCardAuthorizeDialog = async () => {
|
||||
resetEnterpriseCardAuthorizeForm()
|
||||
await Promise.all([
|
||||
handleSearchEnterprise(),
|
||||
loadAllocateCarrierOptions(),
|
||||
loadAllocateBatchNoOptions(),
|
||||
loadTopLevelShopCascadeOptions()
|
||||
])
|
||||
enterpriseCardAuthorizeDialogVisible.value = true
|
||||
enterpriseCardAuthorizeFormRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
const showEnterpriseCardRecallDialog = async () => {
|
||||
resetEnterpriseCardRecallForm()
|
||||
await Promise.all([
|
||||
handleSearchEnterprise(),
|
||||
loadAllocateCarrierOptions(),
|
||||
loadAllocateBatchNoOptions()
|
||||
])
|
||||
enterpriseCardRecallDialogVisible.value = true
|
||||
enterpriseCardRecallFormRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
const handleEnterpriseCardAuthorizeDialogClose = () => {
|
||||
Object.assign(enterpriseCardAuthorizeForm, createInitialEnterpriseCardAuthorizeState())
|
||||
enterpriseCardAuthorizeFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
const handleEnterpriseCardRecallDialogClose = () => {
|
||||
Object.assign(enterpriseCardRecallForm, createInitialEnterpriseCardRecallState())
|
||||
enterpriseCardRecallFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
const appendEnterpriseCardFilterParams = (params: Record<string, unknown>, form: any) => {
|
||||
if (form.carrier_id) params.carrier_id = form.carrier_id
|
||||
if (Array.isArray(form.shop_id_path) && form.shop_id_path.length > 0) {
|
||||
params.shop_id = form.shop_id_path[form.shop_id_path.length - 1]
|
||||
}
|
||||
if (form.batch_no) params.batch_no = form.batch_no
|
||||
if (form.iccid) params.iccid = form.iccid
|
||||
}
|
||||
|
||||
const buildEnterpriseCardParams = (form: any) => {
|
||||
const params: Record<string, unknown> = {
|
||||
selection_type: form.selection_type,
|
||||
remark: form.remark || undefined
|
||||
}
|
||||
|
||||
if (form.selection_type === CardSelectionType.LIST) {
|
||||
const iccids = selectedCards.value.map((card) => card.iccid)
|
||||
if (!iccids.length) {
|
||||
ElMessage.warning('列表模式下请先选择卡')
|
||||
return null
|
||||
}
|
||||
params.iccids = iccids
|
||||
} else if (form.selection_type === CardSelectionType.RANGE) {
|
||||
params.iccid_start = form.iccid_start
|
||||
params.iccid_end = form.iccid_end
|
||||
} else {
|
||||
appendEnterpriseCardFilterParams(params, form)
|
||||
}
|
||||
|
||||
Object.keys(params).forEach((key) => {
|
||||
if (params[key] === undefined || params[key] === '') delete params[key]
|
||||
})
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
const applyEnterpriseCardOperationSuccess = async (
|
||||
title: string,
|
||||
result: AllocateCardsResponse | RecallCardsResponse
|
||||
) => {
|
||||
enterpriseCardOperationResult.value = result
|
||||
enterpriseCardResultTitle.value = title
|
||||
enterpriseCardResultDialogVisible.value = true
|
||||
selectedCards.value = []
|
||||
await getTableData()
|
||||
}
|
||||
|
||||
const handleEnterpriseCardAuthorize = async () => {
|
||||
if (!enterpriseCardAuthorizeFormRef.value) return
|
||||
|
||||
await enterpriseCardAuthorizeFormRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
const params = buildEnterpriseCardParams(enterpriseCardAuthorizeForm)
|
||||
if (!params) return
|
||||
|
||||
enterpriseCardAuthorizeLoading.value = true
|
||||
try {
|
||||
const res = await EnterpriseService.allocateCards(
|
||||
enterpriseCardAuthorizeForm.enterprise_id!,
|
||||
params as any
|
||||
)
|
||||
if (res.code === 0) {
|
||||
enterpriseCardAuthorizeDialogVisible.value = false
|
||||
await applyEnterpriseCardOperationSuccess('授权卡给企业结果', res.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('授权卡给企业失败:', error)
|
||||
} finally {
|
||||
enterpriseCardAuthorizeLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleEnterpriseCardRecall = async () => {
|
||||
if (!enterpriseCardRecallFormRef.value) return
|
||||
|
||||
await enterpriseCardRecallFormRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
const params = buildEnterpriseCardParams(enterpriseCardRecallForm)
|
||||
if (!params) return
|
||||
|
||||
enterpriseCardRecallLoading.value = true
|
||||
try {
|
||||
const res = await EnterpriseService.recallCards(
|
||||
enterpriseCardRecallForm.enterprise_id!,
|
||||
params as any
|
||||
)
|
||||
if (res.code === 0) {
|
||||
enterpriseCardRecallDialogVisible.value = false
|
||||
await applyEnterpriseCardOperationSuccess('回收卡授权结果', res.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('回收卡授权失败:', error)
|
||||
} finally {
|
||||
enterpriseCardRecallLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 显示套餐系列绑定对话框
|
||||
const showSeriesBindingDialog = async () => {
|
||||
Object.assign(seriesBindingForm, createInitialSeriesBindingState(), {
|
||||
@@ -2118,47 +2823,6 @@
|
||||
})
|
||||
}
|
||||
|
||||
// 清除卡套餐系列绑定
|
||||
const handleClearCardSeries = async () => {
|
||||
if (selectedCards.value.length === 0) {
|
||||
ElMessage.warning('请先选择要清除关联的卡')
|
||||
return
|
||||
}
|
||||
|
||||
ElMessageBox.confirm(
|
||||
`确定要清除所选 ${selectedCards.value.length} 张卡的套餐系列关联吗?`,
|
||||
'确认清除',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
.then(async () => {
|
||||
try {
|
||||
const res = await CardService.batchSetCardSeriesBinding({
|
||||
selection_type: CardSelectionType.LIST,
|
||||
iccids: selectedCards.value.map((card) => card.iccid),
|
||||
series_id: 0
|
||||
})
|
||||
if (res.code === 0) {
|
||||
if (res.data.fail_count === 0) {
|
||||
ElMessage.success('清除关联成功')
|
||||
} else {
|
||||
ElMessage.warning(`部分失败:${res.data.failed_items?.[0]?.reason || '未知原因'}`)
|
||||
}
|
||||
selectedCards.value = []
|
||||
await getTableData()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('清除关联失败:', error)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// 用户取消
|
||||
})
|
||||
}
|
||||
|
||||
// IoT卡操作处理函数
|
||||
const handleCardOperation = (command: string, iccid: string, row?: any) => {
|
||||
switch (command) {
|
||||
@@ -2258,33 +2922,6 @@
|
||||
// 用户取消
|
||||
})
|
||||
}
|
||||
|
||||
// 手动停用 IoT 卡(资产层面的停用)
|
||||
const handleManualDeactivate = (iccid: string) => {
|
||||
ElMessageBox.confirm(
|
||||
`确定要手动停用卡片 ${iccid} 吗?此操作将在资产管理层面停用该卡。`,
|
||||
'确认手动停用',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
.then(async () => {
|
||||
try {
|
||||
const res = await AssetService.deactivateAsset(iccid)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('手动停用成功')
|
||||
await getTableData()
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('手动停用失败:', error)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// 用户取消
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -63,8 +63,8 @@
|
||||
:currentPage="commissionPagination.page"
|
||||
:pageSize="commissionPagination.pageSize"
|
||||
:total="commissionPagination.total"
|
||||
:marginTop="10"
|
||||
:height="500"
|
||||
:marginTop="0"
|
||||
:height="450"
|
||||
@size-change="handleCommissionSizeChange"
|
||||
@current-change="handleCommissionCurrentChange"
|
||||
>
|
||||
@@ -231,6 +231,45 @@
|
||||
|
||||
<!-- 预充值钱包流水 Tab -->
|
||||
<ElTabPane label="预充值钱包流水" name="mainWallet">
|
||||
<ElForm :model="mainWalletSearchForm" class="main-wallet-filter" label-position="top">
|
||||
<div class="main-wallet-filter__grid">
|
||||
<ElFormItem label="交易类型">
|
||||
<ElSelect
|
||||
v-model="mainWalletSearchForm.transaction_type"
|
||||
clearable
|
||||
placeholder="全部类型"
|
||||
>
|
||||
<ElOption label="充值入账" :value="MainWalletTransactionType.RECHARGE" />
|
||||
<ElOption label="套餐扣款" :value="MainWalletTransactionType.DEDUCT" />
|
||||
<ElOption label="退款" :value="MainWalletTransactionType.REFUND" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="交易日期">
|
||||
<ElDatePicker
|
||||
v-model="mainWalletSearchForm.date_range"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
unlink-panels
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="资产标识">
|
||||
<ElInput
|
||||
v-model="mainWalletSearchForm.asset_identifier"
|
||||
clearable
|
||||
maxlength="100"
|
||||
placeholder="请输入ICCID或设备虚拟号"
|
||||
@keyup.enter="handleMainWalletSearch"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem class="main-wallet-filter__actions">
|
||||
<ElButton type="primary" @click="handleMainWalletSearch">搜索</ElButton>
|
||||
<ElButton @click="handleMainWalletReset">重置</ElButton>
|
||||
</ElFormItem>
|
||||
</div>
|
||||
</ElForm>
|
||||
<ArtTable
|
||||
ref="mainWalletTableRef"
|
||||
row-key="id"
|
||||
@@ -384,12 +423,14 @@
|
||||
import { CommissionService } from '@/api/modules'
|
||||
import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { MainWalletTransactionType } from '@/types/api/commission'
|
||||
import type {
|
||||
ShopFundSummaryItem,
|
||||
ShopCommissionRecordItem,
|
||||
WithdrawalRequestItem,
|
||||
CommissionResolveAction,
|
||||
MainWalletTransactionItem
|
||||
MainWalletTransactionItem,
|
||||
MainWalletTransactionQueryParams
|
||||
} from '@/types/api/commission'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
@@ -491,6 +532,15 @@
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
const mainWalletSearchForm = reactive<{
|
||||
transaction_type: MainWalletTransactionType | undefined
|
||||
date_range: [string, string] | [] | null
|
||||
asset_identifier: string
|
||||
}>({
|
||||
transaction_type: undefined,
|
||||
date_range: [],
|
||||
asset_identifier: ''
|
||||
})
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
@@ -713,6 +763,9 @@
|
||||
commissionPagination.page = 1
|
||||
withdrawalPagination.page = 1
|
||||
mainWalletPagination.page = 1
|
||||
mainWalletPagination.total = 0
|
||||
mainWalletRecords.value = []
|
||||
resetMainWalletSearchForm()
|
||||
|
||||
// 加载佣金明细
|
||||
loadCommissionRecords()
|
||||
@@ -841,9 +894,14 @@
|
||||
|
||||
mainWalletLoading.value = true
|
||||
try {
|
||||
const params = {
|
||||
const [startDate, endDate] = mainWalletSearchForm.date_range || []
|
||||
const params: MainWalletTransactionQueryParams = {
|
||||
page: mainWalletPagination.page,
|
||||
page_size: mainWalletPagination.pageSize
|
||||
page_size: mainWalletPagination.pageSize,
|
||||
transaction_type: mainWalletSearchForm.transaction_type,
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
asset_identifier: mainWalletSearchForm.asset_identifier.trim() || undefined
|
||||
}
|
||||
const res = await CommissionService.getMainWalletTransactions(
|
||||
currentShop.value.shop_id,
|
||||
@@ -872,6 +930,23 @@
|
||||
loadMainWalletRecords()
|
||||
}
|
||||
|
||||
const resetMainWalletSearchForm = () => {
|
||||
mainWalletSearchForm.transaction_type = undefined
|
||||
mainWalletSearchForm.date_range = []
|
||||
mainWalletSearchForm.asset_identifier = ''
|
||||
}
|
||||
|
||||
const handleMainWalletSearch = () => {
|
||||
mainWalletPagination.page = 1
|
||||
loadMainWalletRecords()
|
||||
}
|
||||
|
||||
const handleMainWalletReset = () => {
|
||||
resetMainWalletSearchForm()
|
||||
mainWalletPagination.page = 1
|
||||
loadMainWalletRecords()
|
||||
}
|
||||
|
||||
// 处理佣金修正
|
||||
const handleResolveCommission = (
|
||||
row: ShopCommissionRecordItem,
|
||||
@@ -955,6 +1030,58 @@
|
||||
}
|
||||
}
|
||||
|
||||
.main-wallet-filter {
|
||||
padding: 16px 16px 4px;
|
||||
margin-bottom: 12px;
|
||||
background: var(--el-fill-color-lighter);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-select),
|
||||
:deep(.el-date-editor),
|
||||
:deep(.el-input) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.main-wallet-filter__grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(140px, 0.8fr) minmax(260px, 1.4fr) minmax(220px, 1fr) auto;
|
||||
gap: 12px 16px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.main-wallet-filter__actions {
|
||||
:deep(.el-form-item__content) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.main-wallet-filter__grid {
|
||||
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
||||
}
|
||||
|
||||
.main-wallet-filter__actions {
|
||||
:deep(.el-form-item__content) {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main-wallet-filter__grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.asset-identifier-link {
|
||||
color: var(--el-color-primary);
|
||||
text-decoration: underline;
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
</ElCard>
|
||||
|
||||
<PaymentVoucherDialog :file-key="paymentVoucherFileKey" @close="paymentVoucherFileKey = ''" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -34,7 +36,7 @@
|
||||
import { AgentRechargeService } from '@/api/modules'
|
||||
import type { AgentRecharge, AgentRechargeStatus, AgentRechargePaymentMethod } from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
|
||||
|
||||
defineOptions({ name: 'AgentRechargeDetail' })
|
||||
|
||||
@@ -43,6 +45,7 @@
|
||||
|
||||
const loading = ref(false)
|
||||
const detailData = ref<AgentRecharge | null>(null)
|
||||
const paymentVoucherFileKey = ref('')
|
||||
|
||||
const pageTitle = computed(() => `充值订单详情`)
|
||||
|
||||
@@ -124,6 +127,24 @@
|
||||
prop: 'payment_transaction_id',
|
||||
formatter: (value) => value || '-',
|
||||
fullWidth: true
|
||||
},
|
||||
{
|
||||
label: '支付凭证',
|
||||
fullWidth: true,
|
||||
render: (data) =>
|
||||
data.payment_voucher_key
|
||||
? h(
|
||||
ElButton,
|
||||
{
|
||||
type: 'primary',
|
||||
link: true,
|
||||
onClick: () => {
|
||||
paymentVoucherFileKey.value = data.payment_voucher_key
|
||||
}
|
||||
},
|
||||
() => '查看支付凭证'
|
||||
)
|
||||
: h('span', '-')
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -96,32 +96,25 @@
|
||||
label="支付凭证"
|
||||
prop="payment_voucher_key"
|
||||
>
|
||||
<ElUpload
|
||||
<VoucherUpload
|
||||
ref="uploadRef"
|
||||
class="payment-voucher-upload"
|
||||
drag
|
||||
:auto-upload="false"
|
||||
:on-change="handleVoucherFileChange"
|
||||
:on-remove="handleRemoveVoucher"
|
||||
accept="image/*"
|
||||
list-type="picture"
|
||||
:limit="1"
|
||||
>
|
||||
<el-icon class="payment-voucher-upload__icon"><UploadFilled /></el-icon>
|
||||
<div class="payment-voucher-upload__text">
|
||||
将支付凭证拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">支持 jpg、png 格式,文件大小不超过 5MB</div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
v-model="createForm.payment_voucher_key"
|
||||
voucher-name="支付凭证"
|
||||
@uploading-change="voucherUploading = $event"
|
||||
@change="createFormRef?.validateField('payment_voucher_key')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="createDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleCreateRecharge" :loading="createLoading">
|
||||
确认创建
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="handleCreateRecharge"
|
||||
:loading="createLoading || voucherUploading"
|
||||
:disabled="voucherUploading"
|
||||
>
|
||||
{{ voucherUploading ? '凭证上传中...' : '确认创建' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
@@ -178,19 +171,16 @@
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { AgentRechargeService, ShopService, StorageService } from '@/api/modules'
|
||||
import { AgentRechargeService, ShopService } from '@/api/modules'
|
||||
import {
|
||||
ElMessage,
|
||||
ElTag,
|
||||
ElTreeSelect,
|
||||
ElButton,
|
||||
ElCascader,
|
||||
ElInputNumber,
|
||||
ElSelect,
|
||||
ElOption,
|
||||
ElUpload
|
||||
ElOption
|
||||
} from 'element-plus'
|
||||
import { UploadFilled } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type {
|
||||
AgentRecharge,
|
||||
@@ -198,25 +188,23 @@
|
||||
AgentRechargeStatus,
|
||||
AgentRechargePaymentMethod,
|
||||
CreateAgentRechargeRequest,
|
||||
ConfirmOfflinePaymentRequest,
|
||||
ShopResponse
|
||||
ConfirmOfflinePaymentRequest
|
||||
} from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
|
||||
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
||||
|
||||
defineOptions({ name: 'AgentRechargeList' })
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
const loading = ref(false)
|
||||
const createLoading = ref(false)
|
||||
const voucherUploading = ref(false)
|
||||
const confirmPayLoading = ref(false)
|
||||
const tableRef = ref()
|
||||
const createDialogVisible = ref(false)
|
||||
@@ -238,8 +226,6 @@
|
||||
|
||||
// 店铺选项
|
||||
const shopOptions = ref<any[]>([])
|
||||
const shopTreeData = ref<ShopResponse[]>([])
|
||||
|
||||
// 店铺级联选择相关
|
||||
const shopCascadeOptions = ref<any[]>([])
|
||||
const shopCascadeProps = {
|
||||
@@ -340,7 +326,7 @@
|
||||
|
||||
const createFormRef = ref<FormInstance>()
|
||||
const confirmPayFormRef = ref<FormInstance>()
|
||||
const uploadRef = ref()
|
||||
const uploadRef = ref<InstanceType<typeof VoucherUpload>>()
|
||||
const MIN_RECHARGE_AMOUNT = 0.01
|
||||
|
||||
const createRules = computed<FormRules>(() => {
|
||||
@@ -523,33 +509,6 @@
|
||||
isFirstActivation = false
|
||||
})
|
||||
|
||||
// 构建树形数据
|
||||
const buildTreeData = (items: ShopResponse[]) => {
|
||||
const map = new Map<number, ShopResponse & { children?: ShopResponse[] }>()
|
||||
const tree: ShopResponse[] = []
|
||||
|
||||
// 先将所有项放入 map
|
||||
items.forEach((item) => {
|
||||
map.set(item.id, { ...item, children: [] })
|
||||
})
|
||||
|
||||
// 构建树形结构
|
||||
items.forEach((item) => {
|
||||
const node = map.get(item.id)!
|
||||
if (item.parent_id && map.has(item.parent_id)) {
|
||||
// 有父节点,添加到父节点的 children 中
|
||||
const parent = map.get(item.parent_id)!
|
||||
if (!parent.children) parent.children = []
|
||||
parent.children.push(node)
|
||||
} else {
|
||||
// 没有父节点或父节点不存在,作为根节点
|
||||
tree.push(node)
|
||||
}
|
||||
})
|
||||
|
||||
return tree
|
||||
}
|
||||
|
||||
// 加载店铺列表 - 改用级联查询
|
||||
const loadShops = async () => {
|
||||
try {
|
||||
@@ -671,60 +630,18 @@
|
||||
createForm.payment_method = ''
|
||||
createForm.shop_id = null
|
||||
createForm.payment_voucher_key = undefined
|
||||
uploadRef.value?.clearFiles()
|
||||
}
|
||||
|
||||
// 支付凭证文件变化
|
||||
const handleVoucherFileChange = async (file: any) => {
|
||||
const isImage = file.raw.type.startsWith('image/')
|
||||
const isLt5M = file.raw.size / 1024 / 1024 < 5
|
||||
|
||||
if (!isImage) {
|
||||
ElMessage.error('只能上传图片文件')
|
||||
uploadRef.value?.clearFiles()
|
||||
return
|
||||
}
|
||||
if (!isLt5M) {
|
||||
ElMessage.error('图片大小不能超过 5MB')
|
||||
uploadRef.value?.clearFiles()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const uploadUrlRes = await StorageService.getUploadUrl({
|
||||
file_name: file.name,
|
||||
content_type: file.raw.type,
|
||||
purpose: 'attachment'
|
||||
})
|
||||
|
||||
if (uploadUrlRes.code !== 0) {
|
||||
ElMessage.error(uploadUrlRes.msg || '获取上传地址失败')
|
||||
uploadRef.value?.clearFiles()
|
||||
return
|
||||
}
|
||||
|
||||
const { upload_url, file_key } = uploadUrlRes.data
|
||||
|
||||
await StorageService.uploadFile(upload_url, file.raw, file.raw.type)
|
||||
|
||||
createForm.payment_voucher_key = file_key
|
||||
ElMessage.success('上传成功')
|
||||
} catch (error: any) {
|
||||
console.error('上传失败:', error)
|
||||
createForm.payment_voucher_key = undefined
|
||||
uploadRef.value?.clearFiles()
|
||||
}
|
||||
}
|
||||
|
||||
// 删除支付凭证
|
||||
const handleRemoveVoucher = () => {
|
||||
createForm.payment_voucher_key = undefined
|
||||
voucherUploading.value = false
|
||||
uploadRef.value?.clearFiles(false)
|
||||
}
|
||||
|
||||
// 创建充值订单
|
||||
const handleCreateRecharge = async () => {
|
||||
const formRef = createFormRef.value
|
||||
if (!formRef) return
|
||||
if (voucherUploading.value) {
|
||||
ElMessage.warning('支付凭证上传中,请稍候')
|
||||
return
|
||||
}
|
||||
|
||||
await formRef.validate(async (valid) => {
|
||||
if (valid) {
|
||||
@@ -854,34 +771,4 @@
|
||||
.agent-recharge-page {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.payment-voucher-upload {
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-upload) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-upload-dragger) {
|
||||
width: 100%;
|
||||
padding: 24px 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
margin-bottom: 12px;
|
||||
font-size: 28px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
|
||||
em {
|
||||
font-style: normal;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -22,13 +22,7 @@
|
||||
</div>
|
||||
</ElCard>
|
||||
|
||||
<ElImageViewer
|
||||
v-if="voucherPreviewVisible"
|
||||
:url-list="refundVoucherUrls"
|
||||
:initial-index="voucherPreviewIndex"
|
||||
hide-on-click-modal
|
||||
@close="voucherPreviewVisible = false"
|
||||
/>
|
||||
<PaymentVoucherDialog :file-key="refundVoucherFileKey" @close="refundVoucherFileKey = ''" />
|
||||
|
||||
<!-- 审批通过对话框 -->
|
||||
<ElDialog
|
||||
@@ -172,11 +166,11 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed, h, reactive } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElCard, ElImageViewer, ElIcon, ElMessage, ElTag } from 'element-plus'
|
||||
import { ElButton, ElCard, ElIcon, ElMessage, ElTag } 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 { RefundService, StorageService } from '@/api/modules'
|
||||
import { RefundService } from '@/api/modules'
|
||||
import type {
|
||||
Refund,
|
||||
RefundStatus,
|
||||
@@ -185,6 +179,7 @@
|
||||
ResubmitRefundRequest
|
||||
} from '@/types/api'
|
||||
import { formatDateTime, yuanToFen } from '@/utils/business/format'
|
||||
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
|
||||
|
||||
defineOptions({ name: 'RefundDetail' })
|
||||
|
||||
@@ -193,9 +188,7 @@
|
||||
|
||||
const loading = ref(false)
|
||||
const refund = ref<Refund | null>(null)
|
||||
const refundVoucherUrls = ref<string[]>([])
|
||||
const voucherPreviewVisible = ref(false)
|
||||
const voucherPreviewIndex = ref(0)
|
||||
const refundVoucherFileKey = ref('')
|
||||
|
||||
const pageTitle = computed(() => `退款详情`)
|
||||
|
||||
@@ -262,14 +255,6 @@
|
||||
return statusMap[status] || '-'
|
||||
}
|
||||
|
||||
const getVoucherKeys = (data: Refund): string[] => {
|
||||
const voucherKey = data.refund_voucher_key || ''
|
||||
return voucherKey
|
||||
.split(',')
|
||||
.map((key) => key.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
const detailSections = computed((): DetailSection[] => [
|
||||
{
|
||||
title: '退款信息',
|
||||
@@ -309,18 +294,18 @@
|
||||
{
|
||||
label: '退款凭证',
|
||||
fullWidth: true,
|
||||
render: () =>
|
||||
refundVoucherUrls.value.length
|
||||
render: (data) =>
|
||||
data.refund_voucher_key
|
||||
? h(
|
||||
'div',
|
||||
{ class: 'refund-voucher-list' },
|
||||
refundVoucherUrls.value.map((url, index) =>
|
||||
h('img', {
|
||||
class: 'refund-voucher-image',
|
||||
src: url,
|
||||
onClick: () => handlePreviewRefundVoucher(index)
|
||||
})
|
||||
)
|
||||
ElButton,
|
||||
{
|
||||
type: 'primary',
|
||||
link: true,
|
||||
onClick: () => {
|
||||
refundVoucherFileKey.value = data.refund_voucher_key
|
||||
}
|
||||
},
|
||||
() => '查看退款凭证'
|
||||
)
|
||||
: h('span', '-')
|
||||
},
|
||||
@@ -391,7 +376,6 @@
|
||||
const res = await RefundService.getRefundById(id)
|
||||
if (res.code === 0) {
|
||||
refund.value = res.data
|
||||
await loadRefundVoucherUrls(res.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
@@ -405,26 +389,6 @@
|
||||
router.back()
|
||||
}
|
||||
|
||||
const loadRefundVoucherUrls = async (data: Refund) => {
|
||||
const fileKeys = getVoucherKeys(data)
|
||||
refundVoucherUrls.value = []
|
||||
if (!fileKeys.length) return
|
||||
|
||||
try {
|
||||
const res = await StorageService.batchDownloadUrls({ file_keys: fileKeys })
|
||||
if (res.code === 0) {
|
||||
refundVoucherUrls.value = fileKeys.map((key) => res.data.urls[key]).filter(Boolean)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取退款凭证失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePreviewRefundVoucher = (index: number) => {
|
||||
voucherPreviewIndex.value = index
|
||||
voucherPreviewVisible.value = true
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const id = route.params.id as string
|
||||
if (id) {
|
||||
@@ -556,21 +520,5 @@
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.refund-voucher-list) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
:deep(.refund-voucher-image) {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
cursor: zoom-in;
|
||||
object-fit: cover;
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
:show-expand="false"
|
||||
show-expand
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
@@ -279,7 +279,8 @@
|
||||
const initialSearchState: RefundQueryParams = {
|
||||
status: undefined,
|
||||
order_id: undefined,
|
||||
shop_id: undefined
|
||||
shop_id: undefined,
|
||||
asset_identifier: ''
|
||||
}
|
||||
|
||||
// 搜索表单
|
||||
@@ -325,6 +326,15 @@
|
||||
remoteMethod: (query: string) => searchOrdersForFilter(query)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '资产标识',
|
||||
prop: 'asset_identifier',
|
||||
type: 'input',
|
||||
placeholder: '请输入ICCID或设备虚拟号',
|
||||
config: {
|
||||
clearable: true
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
@@ -622,12 +632,14 @@
|
||||
const getTableData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const assetIdentifier = searchForm.asset_identifier?.trim()
|
||||
const params: RefundQueryParams = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.page_size,
|
||||
status: searchForm.status,
|
||||
order_id: searchForm.order_id,
|
||||
shop_id: searchForm.shop_id
|
||||
shop_id: searchForm.shop_id,
|
||||
asset_identifier: assetIdentifier || undefined
|
||||
}
|
||||
const res = await RefundService.getRefunds(params)
|
||||
if (res.code === 0) {
|
||||
|
||||
@@ -18,9 +18,7 @@
|
||||
<!-- 订单列表 -->
|
||||
<div v-if="detailData && detailData.items && detailData.items.length > 0">
|
||||
<ElCard style="margin-top: 20px" shadow="never">
|
||||
<div
|
||||
class="order-items-section"
|
||||
>
|
||||
<div class="order-items-section">
|
||||
<h3 class="section-title">订单列表</h3>
|
||||
<ElTable :data="detailData.items" border>
|
||||
<ElTableColumn prop="package_name" label="套餐名称" />
|
||||
@@ -57,35 +55,29 @@
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
</ElCard>
|
||||
|
||||
<PaymentVoucherDialog :file-key="paymentVoucherFileKey" @close="paymentVoucherFileKey = ''" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, onMounted, ref, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElIcon,
|
||||
ElImage,
|
||||
ElMessage,
|
||||
ElTable,
|
||||
ElTableColumn
|
||||
} from 'element-plus'
|
||||
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 { OrderService, StorageService } from '@/api/modules'
|
||||
import { OrderService } from '@/api/modules'
|
||||
import type {
|
||||
Order,
|
||||
OrderCommissionResult,
|
||||
OrderCommissionStatus,
|
||||
OrderOperatorType,
|
||||
OrderPaymentMethod
|
||||
OrderOperatorType
|
||||
} from '@/types/api'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
|
||||
|
||||
defineOptions({ name: 'OrderDetail' })
|
||||
|
||||
@@ -96,7 +88,7 @@
|
||||
|
||||
const loading = ref(false)
|
||||
const detailData = ref<Order | null>(null)
|
||||
const paymentVoucherUrl = ref('')
|
||||
const paymentVoucherFileKey = ref('')
|
||||
|
||||
// 格式化货币 - 将分转换为元
|
||||
const formatCurrency = (amount: number): string => {
|
||||
@@ -113,17 +105,6 @@
|
||||
return type === 'personal' ? '个人客户' : '代理商'
|
||||
}
|
||||
|
||||
// 获取支付方式文本
|
||||
const getPaymentMethodText = (method: OrderPaymentMethod): string => {
|
||||
const methodMap: Record<OrderPaymentMethod, string> = {
|
||||
wallet: '钱包支付',
|
||||
wechat: '微信支付',
|
||||
alipay: '支付宝支付',
|
||||
offline: '线下支付'
|
||||
}
|
||||
return methodMap[method] || method
|
||||
}
|
||||
|
||||
// 获取操作者类型文本
|
||||
const getOperatorTypeText = (type?: OrderOperatorType): string => {
|
||||
if (!type) return '-'
|
||||
@@ -174,17 +155,6 @@
|
||||
return roleMap[role] || role
|
||||
}
|
||||
|
||||
const loadPaymentVoucherUrl = async (fileKey?: string) => {
|
||||
paymentVoucherUrl.value = ''
|
||||
if (!fileKey || !hasAuth('orders:view_payment_voucher')) return
|
||||
|
||||
try {
|
||||
paymentVoucherUrl.value = await StorageService.getDownloadUrl(fileKey)
|
||||
} catch (error) {
|
||||
console.error('Get payment voucher failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查当前用户是否可以查看实付金额(代理账号和企业账号不能看到)
|
||||
const canViewActualPaidAmount = computed(() => {
|
||||
const userType = userStore.info.user_type
|
||||
@@ -229,21 +199,17 @@
|
||||
return h('span', '-')
|
||||
}
|
||||
|
||||
if (!paymentVoucherUrl.value) {
|
||||
return h('span', '加载中...')
|
||||
}
|
||||
|
||||
return h(ElImage, {
|
||||
src: paymentVoucherUrl.value,
|
||||
previewSrcList: [paymentVoucherUrl.value],
|
||||
fit: 'cover',
|
||||
style: {
|
||||
width: '120px',
|
||||
height: '120px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid var(--el-border-color-light)'
|
||||
}
|
||||
})
|
||||
return h(
|
||||
ElButton,
|
||||
{
|
||||
type: 'primary',
|
||||
link: true,
|
||||
onClick: () => {
|
||||
paymentVoucherFileKey.value = data.payment_voucher_key
|
||||
}
|
||||
},
|
||||
() => '查看支付凭证'
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -377,7 +343,6 @@
|
||||
const res = await OrderService.getOrderById(id)
|
||||
if (res.code === 0) {
|
||||
detailData.value = res.data
|
||||
await loadPaymentVoucherUrl(res.data.payment_voucher_key)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
@@ -199,25 +199,13 @@
|
||||
label="支付凭证"
|
||||
prop="payment_voucher_key"
|
||||
>
|
||||
<ElUpload
|
||||
<VoucherUpload
|
||||
ref="uploadRef"
|
||||
class="payment-voucher-upload"
|
||||
drag
|
||||
:auto-upload="false"
|
||||
:on-change="handleVoucherFileChange"
|
||||
:on-remove="handleRemoveVoucher"
|
||||
accept="image/*"
|
||||
list-type="picture"
|
||||
:limit="1"
|
||||
>
|
||||
<el-icon class="payment-voucher-upload__icon"><UploadFilled /></el-icon>
|
||||
<div class="payment-voucher-upload__text">
|
||||
将支付凭证拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">支持所有图片格式</div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
v-model="createForm.payment_voucher_key"
|
||||
voucher-name="支付凭证"
|
||||
@uploading-change="voucherUploading = $event"
|
||||
@change="createFormRef?.validateField('payment_voucher_key')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
@@ -229,7 +217,7 @@
|
||||
:loading="createLoading || voucherUploading"
|
||||
:disabled="voucherUploading"
|
||||
>
|
||||
{{ voucherUploading ? '图片上传中...' : '提交' }}
|
||||
{{ voucherUploading ? '凭证上传中...' : '提交' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
@@ -255,19 +243,17 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, nextTick } from 'vue'
|
||||
import { computed, h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
OrderService,
|
||||
CardService,
|
||||
DeviceService,
|
||||
PackageManageService,
|
||||
StorageService,
|
||||
ShopService
|
||||
} from '@/api/modules'
|
||||
import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
|
||||
import { UploadFilled } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules, UploadInstance, UploadFile } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type {
|
||||
Order,
|
||||
OrderQueryParams,
|
||||
@@ -290,6 +276,7 @@
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
|
||||
import ExportTaskCreateDialog from '@/components/business/ExportTaskCreateDialog.vue'
|
||||
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
||||
|
||||
defineOptions({ name: 'OrderList' })
|
||||
|
||||
@@ -308,7 +295,6 @@
|
||||
const createDialogVisible = ref(false)
|
||||
const exportDialogVisible = ref(false)
|
||||
const paymentVoucherFileKey = ref('')
|
||||
let activeVoucherUploadId = 0
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState: OrderSearchFormState = {
|
||||
@@ -536,82 +522,7 @@
|
||||
})
|
||||
|
||||
const createFormRef = ref<FormInstance>()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
|
||||
const invalidateVoucherUploadState = (clearFileKey = false) => {
|
||||
activeVoucherUploadId += 1
|
||||
voucherUploading.value = false
|
||||
if (clearFileKey) {
|
||||
createForm.payment_voucher_key = undefined
|
||||
}
|
||||
}
|
||||
|
||||
// 处理支付凭证文件选择
|
||||
const handleVoucherFileChange = async (uploadFile: UploadFile) => {
|
||||
const file = uploadFile.raw
|
||||
if (!file) return
|
||||
|
||||
// 验证文件类型
|
||||
const isImage = file.type.startsWith('image/')
|
||||
if (!isImage) {
|
||||
ElMessage.error('只能上传图片文件!')
|
||||
uploadRef.value?.clearFiles()
|
||||
return
|
||||
}
|
||||
|
||||
const requestId = ++activeVoucherUploadId
|
||||
voucherUploading.value = true
|
||||
createForm.payment_voucher_key = undefined
|
||||
|
||||
// 开始上传
|
||||
try {
|
||||
ElMessage.info('正在上传支付凭证...')
|
||||
|
||||
// 1. 获取上传 URL
|
||||
const uploadUrlRes = await StorageService.getUploadUrl({
|
||||
file_name: file.name,
|
||||
content_type: file.type,
|
||||
purpose: 'attachment'
|
||||
})
|
||||
|
||||
if (uploadUrlRes.code !== 0) {
|
||||
ElMessage.error(uploadUrlRes.msg || '获取上传地址失败')
|
||||
uploadRef.value?.clearFiles()
|
||||
return
|
||||
}
|
||||
|
||||
const { upload_url, file_key } = uploadUrlRes.data
|
||||
|
||||
// 2. 上传文件到对象存储
|
||||
await StorageService.uploadFile(upload_url, file, file.type)
|
||||
|
||||
if (requestId !== activeVoucherUploadId) return
|
||||
|
||||
// 3. 保存 file_key
|
||||
createForm.payment_voucher_key = file_key
|
||||
ElMessage.success('上传成功')
|
||||
// 触发表单验证
|
||||
await nextTick(() => {
|
||||
createFormRef.value?.validateField('payment_voucher_key')
|
||||
})
|
||||
} catch (error: any) {
|
||||
if (requestId !== activeVoucherUploadId) return
|
||||
|
||||
console.error('上传失败:', error)
|
||||
ElMessage.error(error.message || '上传失败,请重试')
|
||||
createForm.payment_voucher_key = undefined
|
||||
uploadRef.value?.clearFiles()
|
||||
} finally {
|
||||
if (requestId === activeVoucherUploadId) {
|
||||
voucherUploading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除支付凭证
|
||||
const handleRemoveVoucher = () => {
|
||||
invalidateVoucherUploadState(true)
|
||||
}
|
||||
const uploadRef = ref<InstanceType<typeof VoucherUpload>>()
|
||||
|
||||
const createRules = computed<FormRules>(() => {
|
||||
const baseRules: FormRules = {
|
||||
@@ -1231,10 +1142,11 @@
|
||||
createForm.iot_card_id = null
|
||||
createForm.device_id = null
|
||||
createForm.payment_method = 'wallet'
|
||||
invalidateVoucherUploadState(true)
|
||||
createForm.payment_voucher_key = undefined
|
||||
voucherUploading.value = false
|
||||
|
||||
// 清空上传文件列表
|
||||
uploadRef.value?.clearFiles()
|
||||
uploadRef.value?.clearFiles(false)
|
||||
|
||||
// 清空套餐、IoT卡和设备搜索结果
|
||||
packageOptions.value = []
|
||||
@@ -1418,36 +1330,6 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.payment-voucher-upload {
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-upload) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-upload-dragger) {
|
||||
width: 100%;
|
||||
padding: 24px 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
margin-bottom: 12px;
|
||||
font-size: 28px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
|
||||
em {
|
||||
font-style: normal;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user