fetch(add): 账户管理

This commit is contained in:
sexygoat
2026-01-23 17:18:24 +08:00
parent 339abca4c0
commit b53fea43c6
93 changed files with 7094 additions and 3153 deletions

View File

@@ -24,104 +24,104 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import type { Agent } from '@/types/api'
import { ref, computed, onMounted } from 'vue'
import type { Agent } from '@/types/api'
interface Props {
modelValue?: string | number | (string | number)[]
placeholder?: string
clearable?: boolean
disabled?: boolean
filterable?: boolean
checkStrictly?: boolean
multiple?: boolean
size?: 'large' | 'default' | 'small'
// 预加载的代理商树数据
agents?: Agent[]
// 远程获取方法
fetchMethod?: () => Promise<Agent[]>
}
interface Emits {
(e: 'update:modelValue', value: string | number | (string | number)[] | undefined): void
(e: 'change', value: string | number | (string | number)[] | undefined): void
}
const props = withDefaults(defineProps<Props>(), {
placeholder: '请选择代理商',
clearable: true,
disabled: false,
filterable: true,
checkStrictly: false,
multiple: false,
size: 'default'
})
const emit = defineEmits<Emits>()
const loading = ref(false)
const agentTreeData = ref<Agent[]>([])
const treeProps = {
value: 'id',
label: 'name',
children: 'children'
}
const selectedValue = computed({
get: () => props.modelValue,
set: (val) => {
emit('update:modelValue', val)
interface Props {
modelValue?: string | number | (string | number)[]
placeholder?: string
clearable?: boolean
disabled?: boolean
filterable?: boolean
checkStrictly?: boolean
multiple?: boolean
size?: 'large' | 'default' | 'small'
// 预加载的代理商树数据
agents?: Agent[]
// 远程获取方法
fetchMethod?: () => Promise<Agent[]>
}
})
const handleChange = (value: string | number | (string | number)[] | undefined) => {
emit('change', value)
}
const handleVisibleChange = (visible: boolean) => {
if (visible && agentTreeData.value.length === 0) {
loadAgents()
interface Emits {
(e: 'update:modelValue', value: string | number | (string | number)[] | undefined): void
(e: 'change', value: string | number | (string | number)[] | undefined): void
}
}
const loadAgents = async () => {
if (props.agents) {
agentTreeData.value = props.agents
} else if (props.fetchMethod) {
loading.value = true
try {
agentTreeData.value = await props.fetchMethod()
} finally {
loading.value = false
const props = withDefaults(defineProps<Props>(), {
placeholder: '请选择代理商',
clearable: true,
disabled: false,
filterable: true,
checkStrictly: false,
multiple: false,
size: 'default'
})
const emit = defineEmits<Emits>()
const loading = ref(false)
const agentTreeData = ref<Agent[]>([])
const treeProps = {
value: 'id',
label: 'name',
children: 'children'
}
const selectedValue = computed({
get: () => props.modelValue,
set: (val) => {
emit('update:modelValue', val)
}
})
const handleChange = (value: string | number | (string | number)[] | undefined) => {
emit('change', value)
}
const handleVisibleChange = (visible: boolean) => {
if (visible && agentTreeData.value.length === 0) {
loadAgents()
}
}
}
onMounted(() => {
if (props.agents) {
agentTreeData.value = props.agents
const loadAgents = async () => {
if (props.agents) {
agentTreeData.value = props.agents
} else if (props.fetchMethod) {
loading.value = true
try {
agentTreeData.value = await props.fetchMethod()
} finally {
loading.value = false
}
}
}
})
onMounted(() => {
if (props.agents) {
agentTreeData.value = props.agents
}
})
</script>
<style scoped lang="scss">
.agent-node {
display: flex;
align-items: center;
gap: 8px;
.agent-node {
display: flex;
gap: 8px;
align-items: center;
.agent-name {
font-weight: 500;
color: var(--el-text-color-primary);
}
.agent-name {
font-weight: 500;
color: var(--el-text-color-primary);
}
.agent-level {
font-size: 12px;
padding: 2px 8px;
border-radius: 4px;
background-color: var(--el-color-primary-light-9);
color: var(--el-color-primary);
.agent-level {
padding: 2px 8px;
font-size: 12px;
color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
border-radius: 4px;
}
}
}
</style>

View File

@@ -42,112 +42,106 @@
<template #footer>
<div class="dialog-footer">
<el-button @click="handleCancel">取消</el-button>
<el-button
type="primary"
:loading="loading"
@click="handleConfirm"
>
确定
</el-button>
<el-button type="primary" :loading="loading" @click="handleConfirm"> 确定 </el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage } from 'element-plus'
import { ref, computed } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage } from 'element-plus'
interface Props {
modelValue: boolean
title: string
width?: string | number
selectedCount?: number
confirmMessage?: string
formRules?: FormRules
}
interface Emits {
(e: 'update:modelValue', value: boolean): void
(e: 'confirm', formData: Record<string, any>): void | Promise<void>
(e: 'cancel'): void
}
const props = withDefaults(defineProps<Props>(), {
width: '600px',
selectedCount: 0
})
const emit = defineEmits<Emits>()
const formRef = ref<FormInstance>()
const formData = ref<Record<string, any>>({})
const loading = ref(false)
const visible = computed({
get: () => props.modelValue,
set: (val) => {
emit('update:modelValue', val)
interface Props {
modelValue: boolean
title: string
width?: string | number
selectedCount?: number
confirmMessage?: string
formRules?: FormRules
}
})
const handleConfirm = async () => {
if (!formRef.value) return
interface Emits {
(e: 'update:modelValue', value: boolean): void
(e: 'confirm', formData: Record<string, any>): void | Promise<void>
(e: 'cancel'): void
}
try {
await formRef.value.validate()
const props = withDefaults(defineProps<Props>(), {
width: '600px',
selectedCount: 0
})
loading.value = true
try {
await emit('confirm', formData.value)
visible.value = false
ElMessage.success('操作成功')
} catch (error) {
console.error('批量操作失败:', error)
ElMessage.error('操作失败')
} finally {
loading.value = false
const emit = defineEmits<Emits>()
const formRef = ref<FormInstance>()
const formData = ref<Record<string, any>>({})
const loading = ref(false)
const visible = computed({
get: () => props.modelValue,
set: (val) => {
emit('update:modelValue', val)
}
})
const handleConfirm = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
loading.value = true
try {
await emit('confirm', formData.value)
visible.value = false
ElMessage.success('操作成功')
} catch (error) {
console.error('批量操作失败:', error)
ElMessage.error('操作失败')
} finally {
loading.value = false
}
} catch {
ElMessage.warning('请检查表单填写')
}
} catch {
ElMessage.warning('请检查表单填写')
}
}
const handleCancel = () => {
visible.value = false
emit('cancel')
}
const handleCancel = () => {
visible.value = false
emit('cancel')
}
const handleClosed = () => {
formRef.value?.resetFields()
formData.value = {}
}
const handleClosed = () => {
formRef.value?.resetFields()
formData.value = {}
}
// 暴露方法供父组件调用
defineExpose({
formData
})
// 暴露方法供父组件调用
defineExpose({
formData
})
</script>
<style scoped lang="scss">
.batch-operation-content {
display: flex;
flex-direction: column;
gap: 16px;
.batch-operation-content {
display: flex;
flex-direction: column;
gap: 16px;
.operation-form {
margin-top: 16px;
.operation-form {
margin-top: 16px;
}
.confirm-alert {
margin-top: 8px;
}
}
.confirm-alert {
margin-top: 8px;
.dialog-footer {
display: flex;
gap: 12px;
justify-content: flex-end;
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
</style>

View File

@@ -6,18 +6,9 @@
align-center
:before-close="handleClose"
>
<ElForm
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
>
<ElForm ref="formRef" :model="formData" :rules="formRules" label-width="100px">
<!-- 选择下拉项表单项 -->
<ElFormItem
v-if="showSelect"
:label="selectLabel"
:prop="selectProp"
>
<ElFormItem v-if="showSelect" :label="selectLabel" :prop="selectProp">
<ElSelect
v-model="formData[selectProp]"
:placeholder="selectPlaceholder"
@@ -38,25 +29,14 @@
</ElFormItem>
<!-- 充值金额输入框 -->
<ElFormItem
v-if="showAmount"
label="充值金额"
prop="amount"
>
<ElInput
v-model="formData.amount"
placeholder="请输入充值金额"
>
<ElFormItem v-if="showAmount" label="充值金额" prop="amount">
<ElInput v-model="formData.amount" placeholder="请输入充值金额">
<template #append></template>
</ElInput>
</ElFormItem>
<!-- 备注信息 -->
<ElFormItem
v-if="showRemark"
label="备注"
prop="remark"
>
<ElFormItem v-if="showRemark" label="备注" prop="remark">
<ElInput
v-model="formData.remark"
type="textarea"
@@ -66,26 +46,15 @@
</ElFormItem>
<!-- 选中的网卡信息展示 -->
<ElFormItem
v-if="selectedCards.length > 0"
label="选中网卡"
>
<ElFormItem v-if="selectedCards.length > 0" label="选中网卡">
<div class="selected-cards-info">
<ElTag type="info">已选择 {{ selectedCards.length }} 张网卡</ElTag>
<ElButton
type="text"
size="small"
@click="showCardList = !showCardList"
>
<ElButton type="text" size="small" @click="showCardList = !showCardList">
{{ showCardList ? '收起' : '查看详情' }}
</ElButton>
</div>
<div v-if="showCardList" class="card-list">
<div
v-for="card in selectedCards.slice(0, 5)"
:key="card.id"
class="card-item"
>
<div v-for="card in selectedCards.slice(0, 5)" :key="card.id" class="card-item">
{{ card.iccid }}
</div>
<div v-if="selectedCards.length > 5" class="more-cards">
@@ -97,15 +66,23 @@
<template #footer>
<ElButton @click="handleClose">取消</ElButton>
<ElButton type="primary" @click="handleConfirm" :loading="confirmLoading">
确认
</ElButton>
<ElButton type="primary" @click="handleConfirm" :loading="confirmLoading"> 确认 </ElButton>
</template>
</ElDialog>
</template>
<script setup lang="ts">
import { ElDialog, ElForm, ElFormItem, ElSelect, ElOption, ElInput, ElButton, ElTag, ElMessage } from 'element-plus'
import {
ElDialog,
ElForm,
ElFormItem,
ElSelect,
ElOption,
ElInput,
ElButton,
ElTag,
ElMessage
} from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
interface SelectOption {
@@ -237,7 +214,6 @@
}
emit('confirm', submitData)
} catch (error) {
console.error('表单验证失败:', error)
} finally {
@@ -264,18 +240,18 @@
<style lang="scss" scoped>
.selected-cards-info {
display: flex;
align-items: center;
gap: 12px;
align-items: center;
margin-bottom: 8px;
}
.card-list {
margin-top: 8px;
max-height: 120px;
padding: 8px;
margin-top: 8px;
overflow-y: auto;
background-color: var(--el-fill-color-lighter);
border-radius: 4px;
max-height: 120px;
overflow-y: auto;
.card-item {
padding: 2px 0;
@@ -286,8 +262,8 @@
.more-cards {
padding: 2px 0;
font-size: 12px;
color: var(--el-text-color-placeholder);
font-style: italic;
color: var(--el-text-color-placeholder);
}
}
</style>
</style>

View File

@@ -5,21 +5,21 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { getCardStatusLabel, getCardStatusType } from '@/config/constants'
import type { CardStatus } from '@/types/api'
import { computed } from 'vue'
import { getCardStatusLabel, getCardStatusType } from '@/config/constants'
import type { CardStatus } from '@/types/api'
interface Props {
status: CardStatus
effect?: 'dark' | 'light' | 'plain'
size?: 'large' | 'default' | 'small'
}
interface Props {
status: CardStatus
effect?: 'dark' | 'light' | 'plain'
size?: 'large' | 'default' | 'small'
}
const props = withDefaults(defineProps<Props>(), {
effect: 'light',
size: 'default'
})
const props = withDefaults(defineProps<Props>(), {
effect: 'light',
size: 'default'
})
const statusLabel = computed(() => getCardStatusLabel(props.status))
const tagType = computed(() => getCardStatusType(props.status))
const statusLabel = computed(() => getCardStatusLabel(props.status))
const tagType = computed(() => getCardStatusType(props.status))
</script>

View File

@@ -27,92 +27,91 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { formatMoney } from '@/utils/business'
import {
CommissionType,
WithdrawStatus,
getCommissionTypeLabel,
getWithdrawStatusLabel,
getWithdrawStatusType
} from '@/config/constants'
import { computed } from 'vue'
import { formatMoney } from '@/utils/business'
import { WithdrawalStatus } from '@/types/api/commission'
import {
getCommissionTypeLabel,
getWithdrawalStatusLabel,
getWithdrawalStatusType
} from '@/config/constants'
interface Props {
// 佣金金额(单位:分)
amount: number
// 佣金类型
type?: CommissionType
// 佣金比例
rate?: number
// 状态(用于提现记录)
status?: WithdrawStatus
// 是否显示比例
showRate?: boolean
// 是否显示状态
showStatus?: boolean
// 紧凑模式(只显示金额)
compact?: boolean
}
interface Props {
// 佣金金额(单位:分)
amount: number
// 佣金类型
type?: string
// 佣金比例
rate?: number
// 状态(用于提现记录)
status?: WithdrawalStatus
// 是否显示比例
showRate?: boolean
// 是否显示状态
showStatus?: boolean
// 紧凑模式(只显示金额)
compact?: boolean
}
const props = withDefaults(defineProps<Props>(), {
showRate: true,
showStatus: false,
compact: false
})
const props = withDefaults(defineProps<Props>(), {
showRate: true,
showStatus: false,
compact: false
})
const formattedAmount = computed(() => formatMoney(props.amount))
const formattedAmount = computed(() => formatMoney(props.amount))
const commissionTypeLabel = computed(() => {
return props.type !== undefined ? getCommissionTypeLabel(props.type) : '-'
})
const commissionTypeLabel = computed(() => {
return props.type !== undefined ? getCommissionTypeLabel(props.type) : '-'
})
const statusLabel = computed(() => {
return props.status !== undefined ? getWithdrawStatusLabel(props.status) : '-'
})
const statusLabel = computed(() => {
return props.status !== undefined ? getWithdrawalStatusLabel(props.status) : '-'
})
const statusType = computed(() => {
return props.status !== undefined ? getWithdrawStatusType(props.status) : 'info'
})
const statusType = computed(() => {
return props.status !== undefined ? getWithdrawalStatusType(props.status) : 'info'
})
</script>
<style scoped lang="scss">
.commission-display {
display: flex;
flex-direction: column;
gap: 8px;
&.is-compact {
flex-direction: row;
align-items: center;
}
.commission-item {
.commission-display {
display: flex;
align-items: center;
font-size: 14px;
flex-direction: column;
gap: 8px;
.commission-label {
color: var(--el-text-color-secondary);
margin-right: 8px;
white-space: nowrap;
&.is-compact {
flex-direction: row;
align-items: center;
}
.commission-value {
color: var(--el-text-color-primary);
font-weight: 500;
.commission-item {
display: flex;
align-items: center;
font-size: 14px;
&.commission-amount {
color: var(--el-color-success);
font-size: 16px;
font-weight: 600;
.commission-label {
margin-right: 8px;
color: var(--el-text-color-secondary);
white-space: nowrap;
}
.commission-value {
font-weight: 500;
color: var(--el-text-color-primary);
&.commission-amount {
font-size: 16px;
font-weight: 600;
color: var(--el-color-success);
}
}
}
&.is-compact .commission-item {
.commission-label {
display: none;
}
}
}
&.is-compact .commission-item {
.commission-label {
display: none;
}
}
}
</style>

View File

@@ -17,11 +17,7 @@
show-icon
>
<template #default>
<el-button
type="primary"
link
@click="handleDownloadTemplate"
>
<el-button type="primary" link @click="handleDownloadTemplate">
<el-icon><Download /></el-icon>
下载模板
</el-button>
@@ -79,11 +75,7 @@
<div v-if="importResult.errors && importResult.errors.length > 0" class="error-list">
<el-divider content-position="left">失败详情</el-divider>
<el-scrollbar max-height="200px">
<div
v-for="(error, index) in importResult.errors"
:key="index"
class="error-item"
>
<div v-for="(error, index) in importResult.errors" :key="index" class="error-item">
<span class="error-row"> {{ error.row }} </span>
<span class="error-msg">{{ error.message }}</span>
</div>
@@ -109,12 +101,7 @@
<el-button @click="handleCancel" :disabled="uploading">
{{ importResult ? '关闭' : '取消' }}
</el-button>
<el-button
v-if="!importResult"
type="primary"
:loading="uploading"
@click="handleConfirm"
>
<el-button v-if="!importResult" type="primary" :loading="uploading" @click="handleConfirm">
开始导入
</el-button>
</div>
@@ -123,267 +110,273 @@
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { FormInstance, FormRules, UploadInstance, UploadUserFile, UploadProgressEvent } from 'element-plus'
import { ElMessage } from 'element-plus'
import { Download, UploadFilled } from '@element-plus/icons-vue'
import { ref, computed } from 'vue'
import type {
FormInstance,
FormRules,
UploadInstance,
UploadUserFile,
UploadProgressEvent
} from 'element-plus'
import { ElMessage } from 'element-plus'
import { Download, UploadFilled } from '@element-plus/icons-vue'
interface ImportResult {
success: boolean
message: string
detail?: {
total?: number
success?: number
failed?: number
}
errors?: Array<{
row: number
interface ImportResult {
success: boolean
message: string
}>
}
interface Props {
modelValue: boolean
title?: string
width?: string | number
// 模板下载地址
templateUrl?: string
// 文件上传地址(如果使用 action 方式)
uploadAction?: string
// 接受的文件类型
accept?: string
// 最大文件大小MB
maxSize?: number
// 表单验证规则
formRules?: FormRules
}
interface Emits {
(e: 'update:modelValue', value: boolean): void
(e: 'confirm', file: File, formData?: Record<string, any>): Promise<ImportResult> | void
(e: 'cancel'): void
}
const props = withDefaults(defineProps<Props>(), {
title: '导入数据',
width: '600px',
uploadAction: '#',
accept: '.xlsx,.xls',
maxSize: 10
})
const emit = defineEmits<Emits>()
const uploadRef = ref<UploadInstance>()
const formRef = ref<FormInstance>()
const fileList = ref<UploadUserFile[]>([])
const formData = ref<Record<string, any>>({})
const uploading = ref(false)
const uploadProgress = ref(0)
const importResult = ref<ImportResult>()
const visible = computed({
get: () => props.modelValue,
set: (val) => {
emit('update:modelValue', val)
detail?: {
total?: number
success?: number
failed?: number
}
errors?: Array<{
row: number
message: string
}>
}
})
const maxSizeMB = computed(() => props.maxSize)
const acceptText = computed(() => props.accept.replace(/\./g, '').toUpperCase())
const handleDownloadTemplate = () => {
if (props.templateUrl) {
window.open(props.templateUrl, '_blank')
interface Props {
modelValue: boolean
title?: string
width?: string | number
// 模板下载地址
templateUrl?: string
// 文件上传地址(如果使用 action 方式)
uploadAction?: string
// 接受的文件类型
accept?: string
// 最大文件大小MB
maxSize?: number
// 表单验证规则
formRules?: FormRules
}
}
const handleBeforeUpload = (file: File) => {
const isValidType = props.accept.split(',').some(type => {
const ext = type.trim()
return file.name.toLowerCase().endsWith(ext)
interface Emits {
(e: 'update:modelValue', value: boolean): void
(e: 'confirm', file: File, formData?: Record<string, any>): Promise<ImportResult> | void
(e: 'cancel'): void
}
const props = withDefaults(defineProps<Props>(), {
title: '导入数据',
width: '600px',
uploadAction: '#',
accept: '.xlsx,.xls',
maxSize: 10
})
if (!isValidType) {
ElMessage.error(`只能上传 ${acceptText.value} 格式的文件`)
return false
const emit = defineEmits<Emits>()
const uploadRef = ref<UploadInstance>()
const formRef = ref<FormInstance>()
const fileList = ref<UploadUserFile[]>([])
const formData = ref<Record<string, any>>({})
const uploading = ref(false)
const uploadProgress = ref(0)
const importResult = ref<ImportResult>()
const visible = computed({
get: () => props.modelValue,
set: (val) => {
emit('update:modelValue', val)
}
})
const maxSizeMB = computed(() => props.maxSize)
const acceptText = computed(() => props.accept.replace(/\./g, '').toUpperCase())
const handleDownloadTemplate = () => {
if (props.templateUrl) {
window.open(props.templateUrl, '_blank')
}
}
const isLtSize = file.size / 1024 / 1024 < props.maxSize
if (!isLtSize) {
ElMessage.error(`文件大小不能超过 ${props.maxSize}MB`)
return false
const handleBeforeUpload = (file: File) => {
const isValidType = props.accept.split(',').some((type) => {
const ext = type.trim()
return file.name.toLowerCase().endsWith(ext)
})
if (!isValidType) {
ElMessage.error(`只能上传 ${acceptText.value} 格式的文件`)
return false
}
const isLtSize = file.size / 1024 / 1024 < props.maxSize
if (!isLtSize) {
ElMessage.error(`文件大小不能超过 ${props.maxSize}MB`)
return false
}
return true
}
return true
}
const handleUploadProgress = (event: UploadProgressEvent) => {
uploadProgress.value = Math.round(event.percent)
}
const handleUploadSuccess = () => {
uploading.value = false
ElMessage.success('导入成功')
}
const handleUploadError = () => {
uploading.value = false
ElMessage.error('导入失败')
}
const handleConfirm = async () => {
if (fileList.value.length === 0) {
ElMessage.warning('请选择要导入的文件')
return
const handleUploadProgress = (event: UploadProgressEvent) => {
uploadProgress.value = Math.round(event.percent)
}
// 验证表单(如果有)
if (formRef.value) {
try {
await formRef.value.validate()
} catch {
ElMessage.warning('请检查表单填写')
const handleUploadSuccess = () => {
uploading.value = false
ElMessage.success('导入成功')
}
const handleUploadError = () => {
uploading.value = false
ElMessage.error('导入失败')
}
const handleConfirm = async () => {
if (fileList.value.length === 0) {
ElMessage.warning('请选择要导入的文件')
return
}
// 验证表单(如果有)
if (formRef.value) {
try {
await formRef.value.validate()
} catch {
ElMessage.warning('请检查表单填写')
return
}
}
const file = fileList.value[0].raw
if (!file) return
uploading.value = true
uploadProgress.value = 0
try {
const result = await emit('confirm', file, formData.value)
if (result) {
importResult.value = result
}
} catch (error: any) {
importResult.value = {
success: false,
message: error.message || '导入失败'
}
} finally {
uploading.value = false
}
}
const file = fileList.value[0].raw
if (!file) return
uploading.value = true
uploadProgress.value = 0
try {
const result = await emit('confirm', file, formData.value)
if (result) {
importResult.value = result
}
} catch (error: any) {
importResult.value = {
success: false,
message: error.message || '导入失败'
}
} finally {
uploading.value = false
const handleCancel = () => {
visible.value = false
emit('cancel')
}
}
const handleCancel = () => {
visible.value = false
emit('cancel')
}
const handleClosed = () => {
fileList.value = []
formRef.value?.resetFields()
formData.value = {}
uploadProgress.value = 0
importResult.value = undefined
}
const handleClosed = () => {
fileList.value = []
formRef.value?.resetFields()
formData.value = {}
uploadProgress.value = 0
importResult.value = undefined
}
defineExpose({
formData
})
defineExpose({
formData
})
</script>
<style scoped lang="scss">
.import-dialog-content {
display: flex;
flex-direction: column;
gap: 16px;
.import-dialog-content {
display: flex;
flex-direction: column;
gap: 16px;
.template-section {
margin-bottom: 8px;
}
.upload-area {
:deep(.el-upload) {
width: 100%;
.template-section {
margin-bottom: 8px;
}
:deep(.el-upload-dragger) {
padding: 40px 20px;
}
.upload-area {
:deep(.el-upload) {
width: 100%;
}
.upload-icon {
font-size: 48px;
color: var(--el-color-primary);
margin-bottom: 16px;
}
:deep(.el-upload-dragger) {
padding: 40px 20px;
}
.upload-text {
font-size: 14px;
color: var(--el-text-color-regular);
em {
.upload-icon {
margin-bottom: 16px;
font-size: 48px;
color: var(--el-color-primary);
font-style: normal;
}
.upload-tip {
font-size: 12px;
color: var(--el-text-color-secondary);
margin-top: 8px;
.upload-text {
font-size: 14px;
color: var(--el-text-color-regular);
em {
font-style: normal;
color: var(--el-color-primary);
}
.upload-tip {
margin-top: 8px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
}
}
}
.upload-progress {
text-align: center;
padding: 20px 0;
p {
margin-top: 12px;
font-size: 14px;
color: var(--el-text-color-secondary);
}
}
.import-result {
.result-detail {
margin-top: 8px;
font-size: 14px;
line-height: 1.6;
.upload-progress {
padding: 20px 0;
text-align: center;
p {
margin: 4px 0;
margin-top: 12px;
font-size: 14px;
color: var(--el-text-color-secondary);
}
}
.error-list {
margin-top: 16px;
.error-item {
padding: 8px 12px;
font-size: 13px;
.import-result {
.result-detail {
margin-top: 8px;
font-size: 14px;
line-height: 1.6;
background-color: var(--el-fill-color-light);
border-radius: 4px;
margin-bottom: 8px;
.error-row {
font-weight: 600;
color: var(--el-color-danger);
p {
margin: 4px 0;
}
}
.error-msg {
color: var(--el-text-color-regular);
.error-list {
margin-top: 16px;
.error-item {
padding: 8px 12px;
margin-bottom: 8px;
font-size: 13px;
line-height: 1.6;
background-color: var(--el-fill-color-light);
border-radius: 4px;
.error-row {
font-weight: 600;
color: var(--el-color-danger);
}
.error-msg {
color: var(--el-text-color-regular);
}
}
}
}
.import-form {
padding-top: 16px;
margin-top: 16px;
border-top: 1px solid var(--el-border-color-lighter);
}
}
.import-form {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--el-border-color-lighter);
.dialog-footer {
display: flex;
gap: 12px;
justify-content: flex-end;
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
</style>

View File

@@ -21,46 +21,46 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { OPERATOR_OPTIONS } from '@/config/constants'
import type { Operator } from '@/types/api'
import { computed } from 'vue'
import { OPERATOR_OPTIONS } from '@/config/constants'
import type { Operator } from '@/types/api'
interface Props {
modelValue?: Operator | Operator[]
placeholder?: string
clearable?: boolean
disabled?: boolean
multiple?: boolean
filterable?: boolean
size?: 'large' | 'default' | 'small'
}
interface Emits {
(e: 'update:modelValue', value: Operator | Operator[] | undefined): void
(e: 'change', value: Operator | Operator[] | undefined): void
}
const props = withDefaults(defineProps<Props>(), {
placeholder: '请选择运营商',
clearable: true,
disabled: false,
multiple: false,
filterable: true,
size: 'default'
})
const emit = defineEmits<Emits>()
const operatorOptions = OPERATOR_OPTIONS
const selectedValue = computed({
get: () => props.modelValue,
set: (val) => {
emit('update:modelValue', val)
interface Props {
modelValue?: Operator | Operator[]
placeholder?: string
clearable?: boolean
disabled?: boolean
multiple?: boolean
filterable?: boolean
size?: 'large' | 'default' | 'small'
}
})
const handleChange = (value: Operator | Operator[] | undefined) => {
emit('change', value)
}
interface Emits {
(e: 'update:modelValue', value: Operator | Operator[] | undefined): void
(e: 'change', value: Operator | Operator[] | undefined): void
}
const props = withDefaults(defineProps<Props>(), {
placeholder: '请选择运营商',
clearable: true,
disabled: false,
multiple: false,
filterable: true,
size: 'default'
})
const emit = defineEmits<Emits>()
const operatorOptions = OPERATOR_OPTIONS
const selectedValue = computed({
get: () => props.modelValue,
set: (val) => {
emit('update:modelValue', val)
}
})
const handleChange = (value: Operator | Operator[] | undefined) => {
emit('change', value)
}
</script>

View File

@@ -13,12 +13,7 @@
@change="handleChange"
@visible-change="handleVisibleChange"
>
<el-option
v-for="item in packageList"
:key="item.id"
:label="item.name"
:value="item.id"
>
<el-option v-for="item in packageList" :key="item.id" :label="item.name" :value="item.id">
<div class="package-option">
<span class="package-name">{{ item.name }}</span>
<span class="package-info">
@@ -30,108 +25,108 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import type { Package } from '@/types/api'
import { formatFlow, formatMoney } from '@/utils/business'
import { ref, computed, onMounted } from 'vue'
import type { Package } from '@/types/api'
import { formatFlow, formatMoney } from '@/utils/business'
interface Props {
modelValue?: string | number | (string | number)[]
placeholder?: string
clearable?: boolean
disabled?: boolean
multiple?: boolean
filterable?: boolean
remote?: boolean
size?: 'large' | 'default' | 'small'
// 预加载的套餐列表
packages?: Package[]
// 远程搜索方法
fetchMethod?: (query: string) => Promise<Package[]>
}
interface Emits {
(e: 'update:modelValue', value: string | number | (string | number)[] | undefined): void
(e: 'change', value: string | number | (string | number)[] | undefined): void
}
const props = withDefaults(defineProps<Props>(), {
placeholder: '请选择套餐',
clearable: true,
disabled: false,
multiple: false,
filterable: true,
remote: false,
size: 'default'
})
const emit = defineEmits<Emits>()
const loading = ref(false)
const packageList = ref<Package[]>([])
const selectedValue = computed({
get: () => props.modelValue,
set: (val) => {
emit('update:modelValue', val)
interface Props {
modelValue?: string | number | (string | number)[]
placeholder?: string
clearable?: boolean
disabled?: boolean
multiple?: boolean
filterable?: boolean
remote?: boolean
size?: 'large' | 'default' | 'small'
// 预加载的套餐列表
packages?: Package[]
// 远程搜索方法
fetchMethod?: (query: string) => Promise<Package[]>
}
})
const handleChange = (value: string | number | (string | number)[] | undefined) => {
emit('change', value)
}
const handleVisibleChange = (visible: boolean) => {
if (visible && !props.remote && packageList.value.length === 0) {
loadPackages()
interface Emits {
(e: 'update:modelValue', value: string | number | (string | number)[] | undefined): void
(e: 'change', value: string | number | (string | number)[] | undefined): void
}
}
const loadPackages = async () => {
if (props.packages) {
packageList.value = props.packages
} else if (props.fetchMethod) {
const props = withDefaults(defineProps<Props>(), {
placeholder: '请选择套餐',
clearable: true,
disabled: false,
multiple: false,
filterable: true,
remote: false,
size: 'default'
})
const emit = defineEmits<Emits>()
const loading = ref(false)
const packageList = ref<Package[]>([])
const selectedValue = computed({
get: () => props.modelValue,
set: (val) => {
emit('update:modelValue', val)
}
})
const handleChange = (value: string | number | (string | number)[] | undefined) => {
emit('change', value)
}
const handleVisibleChange = (visible: boolean) => {
if (visible && !props.remote && packageList.value.length === 0) {
loadPackages()
}
}
const loadPackages = async () => {
if (props.packages) {
packageList.value = props.packages
} else if (props.fetchMethod) {
loading.value = true
try {
packageList.value = await props.fetchMethod('')
} finally {
loading.value = false
}
}
}
const remoteMethod = async (query: string) => {
if (!props.fetchMethod) return
loading.value = true
try {
packageList.value = await props.fetchMethod('')
packageList.value = await props.fetchMethod(query)
} finally {
loading.value = false
}
}
}
const remoteMethod = async (query: string) => {
if (!props.fetchMethod) return
loading.value = true
try {
packageList.value = await props.fetchMethod(query)
} finally {
loading.value = false
}
}
onMounted(() => {
if (props.packages) {
packageList.value = props.packages
}
})
onMounted(() => {
if (props.packages) {
packageList.value = props.packages
}
})
</script>
<style scoped lang="scss">
.package-option {
display: flex;
justify-content: space-between;
align-items: center;
.package-option {
display: flex;
align-items: center;
justify-content: space-between;
.package-name {
font-weight: 500;
color: var(--el-text-color-primary);
}
.package-name {
font-weight: 500;
color: var(--el-text-color-primary);
}
.package-info {
font-size: 12px;
color: var(--el-text-color-secondary);
margin-left: 12px;
.package-info {
margin-left: 12px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
}
}
</style>