Initial commit: One Pipe System
完整的管理系统,包含账户管理、卡片管理、套餐管理、财务管理等功能模块。 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
522
src/views/account-management/account/index.vue
Normal file
522
src/views/account-management/account/index.vue
Normal file
@@ -0,0 +1,522 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="account-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
:show-expand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton @click="showDialog('add')">新增账号</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="ID"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '添加账号' : '编辑账号'"
|
||||
width="500px"
|
||||
>
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="100px">
|
||||
<ElFormItem label="账号名称" prop="username">
|
||||
<ElInput v-model="formData.username" placeholder="请输入账号名称" />
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="dialogType === 'add'" label="密码" prop="password">
|
||||
<ElInput
|
||||
v-model="formData.password"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
show-password
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="手机号" prop="phone">
|
||||
<ElInput v-model="formData.phone" placeholder="请输入手机号" maxlength="11" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="账号类型" prop="user_type">
|
||||
<ElSelect
|
||||
v-model="formData.user_type"
|
||||
placeholder="请选择账号类型"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption label="超级管理员" :value="1" />
|
||||
<ElOption label="平台用户" :value="2" />
|
||||
<ElOption label="代理账号" :value="3" />
|
||||
<ElOption label="企业账号" :value="4" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 分配角色对话框 -->
|
||||
<ElDialog v-model="roleDialogVisible" title="分配角色" width="500px">
|
||||
<ElRadioGroup v-model="selectedRole" class="role-radio-group">
|
||||
<div v-for="role in allRoles" :key="role.ID" class="role-radio-item">
|
||||
<ElRadio :label="role.ID">
|
||||
{{ role.role_name }}
|
||||
<ElTag
|
||||
:type="role.role_type === 1 ? 'primary' : 'success'"
|
||||
size="small"
|
||||
style="margin-left: 8px"
|
||||
>
|
||||
{{ role.role_type === 1 ? '平台角色' : '客户角色' }}
|
||||
</ElTag>
|
||||
</ElRadio>
|
||||
</div>
|
||||
</ElRadioGroup>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="roleDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleAssignRoles" :loading="roleSubmitLoading"
|
||||
>提交</ElButton
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { FormInstance, ElSwitch } from 'element-plus'
|
||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { AccountService } from '@/api/modules/account'
|
||||
import { RoleService } from '@/api/modules/role'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import type { PlatformRole } from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { CommonStatus, getStatusText } from '@/config/constants'
|
||||
|
||||
defineOptions({ name: 'Account' }) // 定义组件名称,用于 KeepAlive 缓存控制
|
||||
|
||||
const dialogType = ref('add')
|
||||
const dialogVisible = ref(false)
|
||||
const roleDialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const roleSubmitLoading = ref(false)
|
||||
const currentAccountId = ref<number>(0)
|
||||
const selectedRole = ref<number | null>(null)
|
||||
const allRoles = ref<PlatformRole[]>([])
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
name: '',
|
||||
phone: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1 // 重置到第一页
|
||||
getAccountList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1 // 搜索时重置到第一页
|
||||
getAccountList()
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '账号名称',
|
||||
prop: 'name',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入账号名称'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '手机号',
|
||||
prop: 'phone',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入手机号'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: 'ID', prop: 'ID' },
|
||||
{ label: '账号名称', prop: 'username' },
|
||||
{ label: '手机号', prop: 'phone' },
|
||||
{ label: '账号类型', prop: 'user_type_name' },
|
||||
{ label: '状态', prop: 'status' },
|
||||
{ label: '创建时间', prop: 'CreatedAt' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 显示对话框
|
||||
const showDialog = (type: string, row?: any) => {
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
|
||||
// 重置表单验证状态
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
|
||||
if (type === 'edit' && row) {
|
||||
formData.id = row.ID
|
||||
formData.username = row.username
|
||||
formData.phone = row.phone
|
||||
formData.user_type = row.user_type
|
||||
formData.password = ''
|
||||
} else {
|
||||
formData.id = ''
|
||||
formData.username = ''
|
||||
formData.phone = ''
|
||||
formData.user_type = 2
|
||||
formData.password = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 删除账号
|
||||
const deleteAccount = (row: any) => {
|
||||
ElMessageBox.confirm(`确定要删除账号 ${row.username} 吗?`, '删除账号', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
await AccountService.deleteAccount(row.ID)
|
||||
ElMessage.success('删除成功')
|
||||
getAccountList()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// 账号取消删除
|
||||
})
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'ID',
|
||||
label: 'ID',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
prop: 'username',
|
||||
label: '账号名称',
|
||||
minWidth: 120
|
||||
},
|
||||
{
|
||||
prop: 'phone',
|
||||
label: '手机号',
|
||||
width: 130
|
||||
},
|
||||
{
|
||||
prop: 'user_type',
|
||||
label: '账号类型',
|
||||
width: 120,
|
||||
formatter: (row: any) => {
|
||||
const typeMap: Record<number, string> = {
|
||||
1: '超级管理员',
|
||||
2: '平台用户',
|
||||
3: '代理账号',
|
||||
4: '企业账号'
|
||||
}
|
||||
return typeMap[row.user_type] || '-'
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 100,
|
||||
formatter: (row: any) => {
|
||||
return h(ElSwitch, {
|
||||
modelValue: row.status,
|
||||
activeValue: CommonStatus.ENABLED,
|
||||
inactiveValue: CommonStatus.DISABLED,
|
||||
activeText: getStatusText(CommonStatus.ENABLED),
|
||||
inactiveText: getStatusText(CommonStatus.DISABLED),
|
||||
inlinePrompt: true,
|
||||
'onUpdate:modelValue': (val: number) => handleStatusChange(row, val)
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'CreatedAt',
|
||||
label: '创建时间',
|
||||
width: 180,
|
||||
formatter: (row: any) => formatDateTime(row.CreatedAt)
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
formatter: (row: any) => {
|
||||
return h('div', { style: 'display: flex; gap: 8px;' }, [
|
||||
h(ArtButtonTable, {
|
||||
icon: '',
|
||||
onClick: () => showRoleDialog(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
type: 'edit',
|
||||
onClick: () => showDialog('edit', row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
type: 'delete',
|
||||
onClick: () => deleteAccount(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// 表单实例
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
id: '',
|
||||
username: '',
|
||||
password: '',
|
||||
phone: '',
|
||||
user_type: 2
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getAccountList()
|
||||
loadAllRoles()
|
||||
})
|
||||
|
||||
// 加载所有角色列表
|
||||
const loadAllRoles = async () => {
|
||||
try {
|
||||
const res = await RoleService.getRoles({ page: 1, pageSize: 100 })
|
||||
if (res.code === 0) {
|
||||
allRoles.value = res.data.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取角色列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 显示分配角色对话框
|
||||
const showRoleDialog = async (row: any) => {
|
||||
currentAccountId.value = row.ID
|
||||
selectedRole.value = null
|
||||
|
||||
// 先加载当前账号的角色,再打开对话框
|
||||
try {
|
||||
const res = await AccountService.getAccountRoles(row.ID)
|
||||
if (res.code === 0) {
|
||||
// 提取角色ID(只取第一个角色)
|
||||
const roles = res.data || []
|
||||
selectedRole.value = roles.length > 0 ? roles[0].ID : null
|
||||
// 数据加载完成后再打开对话框
|
||||
roleDialogVisible.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取账号角色失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 提交分配角色
|
||||
const handleAssignRoles = async () => {
|
||||
if (selectedRole.value === null) {
|
||||
ElMessage.warning('请选择一个角色')
|
||||
return
|
||||
}
|
||||
|
||||
roleSubmitLoading.value = true
|
||||
try {
|
||||
// 将单个角色ID包装成数组传给后端
|
||||
await AccountService.assignRolesToAccount(currentAccountId.value, [selectedRole.value])
|
||||
ElMessage.success('分配角色成功')
|
||||
roleDialogVisible.value = false
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
roleSubmitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取账号列表
|
||||
const getAccountList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.currentPage,
|
||||
pageSize: pagination.pageSize,
|
||||
keyword: formFilters.name || formFilters.phone || undefined
|
||||
}
|
||||
const res = await AccountService.getAccounts(params)
|
||||
if (res.code === 0) {
|
||||
tableData.value = res.data.items || []
|
||||
pagination.total = res.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取账号列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getAccountList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
username: [
|
||||
{ required: true, message: '请输入账号名称', trigger: 'blur' },
|
||||
{ min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 8, max: 32, message: '密码长度为 8-32 个字符', trigger: 'blur' }
|
||||
],
|
||||
phone: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ len: 11, message: '手机号必须为 11 位', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
|
||||
],
|
||||
user_type: [{ required: true, message: '请选择账号类型', trigger: 'change' }]
|
||||
})
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const data: any = {
|
||||
username: formData.username,
|
||||
phone: formData.phone,
|
||||
user_type: formData.user_type
|
||||
}
|
||||
|
||||
if (dialogType.value === 'add') {
|
||||
data.password = formData.password
|
||||
await AccountService.createAccount(data)
|
||||
ElMessage.success('添加成功')
|
||||
} else {
|
||||
await AccountService.updateAccount(Number(formData.id), data)
|
||||
ElMessage.success('更新成功')
|
||||
}
|
||||
|
||||
dialogVisible.value = false
|
||||
getAccountList()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getAccountList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getAccountList()
|
||||
}
|
||||
|
||||
// 状态切换
|
||||
const handleStatusChange = async (row: any, newStatus: number) => {
|
||||
const oldStatus = row.status
|
||||
// 先更新UI
|
||||
row.status = newStatus
|
||||
try {
|
||||
await AccountService.updateAccount(row.ID, { status: newStatus })
|
||||
ElMessage.success('状态切换成功')
|
||||
} catch (error) {
|
||||
// 切换失败,恢复原状态
|
||||
row.status = oldStatus
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.account-page {
|
||||
// 账号管理页面样式
|
||||
}
|
||||
|
||||
.role-radio-group {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.role-radio-item {
|
||||
margin-bottom: 16px;
|
||||
padding: 8px;
|
||||
}
|
||||
</style>
|
||||
468
src/views/account-management/agent/index.vue
Normal file
468
src/views/account-management/agent/index.vue
Normal file
@@ -0,0 +1,468 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<ElRow>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElInput v-model="searchQuery" placeholder="代理商名称/联系人" clearable></ElInput>
|
||||
</ElCol>
|
||||
<div style="width: 12px"></div>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElSelect v-model="levelFilter" placeholder="代理商等级" clearable style="width: 100%">
|
||||
<ElOption label="一级代理" value="1" />
|
||||
<ElOption label="二级代理" value="2" />
|
||||
<ElOption label="三级代理" value="3" />
|
||||
</ElSelect>
|
||||
</ElCol>
|
||||
<div style="width: 12px"></div>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<ElButton v-ripple @click="handleSearch">搜索</ElButton>
|
||||
<ElButton v-ripple @click="showDialog('add')">新增代理商</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ArtTable :data="filteredData" index row-key="id">
|
||||
<template #default>
|
||||
<ElTableColumn label="代理商名称" prop="agentName" min-width="150" />
|
||||
<ElTableColumn label="代理商编码" prop="agentCode" />
|
||||
<ElTableColumn label="等级" prop="level">
|
||||
<template #default="scope">
|
||||
<ElTag :type="getLevelTagType(scope.row.level)">
|
||||
{{ getLevelText(scope.row.level) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="上级代理" prop="parentName" show-overflow-tooltip />
|
||||
<ElTableColumn label="联系人" prop="contactPerson" />
|
||||
<ElTableColumn label="联系电话" prop="contactPhone" />
|
||||
<ElTableColumn label="账号数量" prop="accountCount" />
|
||||
<ElTableColumn label="网卡数量" prop="simCardCount" />
|
||||
<ElTableColumn label="佣金总额" prop="totalCommission">
|
||||
<template #default="scope"> ¥{{ scope.row.totalCommission.toFixed(2) }} </template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" prop="status">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status === 'active' ? 'success' : 'info'">
|
||||
{{ scope.row.status === 'active' ? '启用' : '禁用' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="createTime" width="180" />
|
||||
<ElTableColumn fixed="right" label="操作" width="280">
|
||||
<template #default="scope">
|
||||
<el-button link @click="viewAccountList(scope.row)">账号管理</el-button>
|
||||
<el-button link @click="viewCommission(scope.row)">佣金配置</el-button>
|
||||
<el-button link @click="showDialog('edit', scope.row)">编辑</el-button>
|
||||
<el-button link @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增代理商' : '编辑代理商'"
|
||||
width="700px"
|
||||
align-center
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="代理商名称" prop="agentName">
|
||||
<ElInput v-model="form.agentName" placeholder="请输入代理商名称" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="代理商编码" prop="agentCode">
|
||||
<ElInput v-model="form.agentCode" placeholder="请输入代理商编码" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="代理商等级" prop="level">
|
||||
<ElSelect v-model="form.level" placeholder="请选择代理商等级" style="width: 100%">
|
||||
<ElOption label="一级代理" :value="1" />
|
||||
<ElOption label="二级代理" :value="2" />
|
||||
<ElOption label="三级代理" :value="3" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="上级代理" prop="parentId">
|
||||
<ElSelect v-model="form.parentId" placeholder="请选择上级代理" clearable style="width: 100%">
|
||||
<ElOption label="无" :value="null" />
|
||||
<ElOption
|
||||
v-for="agent in parentAgentOptions"
|
||||
:key="agent.id"
|
||||
:label="agent.agentName"
|
||||
:value="agent.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="联系人" prop="contactPerson">
|
||||
<ElInput v-model="form.contactPerson" placeholder="请输入联系人" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="联系电话" prop="contactPhone">
|
||||
<ElInput v-model="form.contactPhone" placeholder="请输入联系电话" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="邮箱" prop="email">
|
||||
<ElInput v-model="form.email" placeholder="请输入邮箱" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="公司地址" prop="address">
|
||||
<ElInput v-model="form.address" placeholder="请输入公司地址" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElFormItem label="备注" prop="remark">
|
||||
<ElInput v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注信息" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="状态">
|
||||
<ElSwitch v-model="form.status" active-value="active" inactive-value="inactive" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 账号管理对话框 -->
|
||||
<ElDialog v-model="accountDialogVisible" title="代理商账号管理" width="900px" align-center>
|
||||
<div style="margin-bottom: 16px">
|
||||
<ElButton type="primary" size="small" @click="addSubAccount">创建子账号</ElButton>
|
||||
</div>
|
||||
<ArtTable :data="currentAgentAccounts" index>
|
||||
<template #default>
|
||||
<ElTableColumn label="账号" prop="username" />
|
||||
<ElTableColumn label="真实姓名" prop="realName" />
|
||||
<ElTableColumn label="手机号" prop="phone" />
|
||||
<ElTableColumn label="角色" prop="role" />
|
||||
<ElTableColumn label="状态" prop="status">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status === 'active' ? 'success' : 'info'">
|
||||
{{ scope.row.status === 'active' ? '正常' : '禁用' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="createTime" />
|
||||
<ElTableColumn fixed="right" label="操作" width="150">
|
||||
<template #default="scope">
|
||||
<el-button link>编辑</el-button>
|
||||
<el-button link>禁用</el-button>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 佣金配置对话框 -->
|
||||
<ElDialog v-model="commissionDialogVisible" title="佣金配置" width="600px" align-center>
|
||||
<ElForm label-width="120px">
|
||||
<ElFormItem label="佣金模式">
|
||||
<ElRadioGroup v-model="commissionForm.mode">
|
||||
<ElRadio value="fixed">固定佣金</ElRadio>
|
||||
<ElRadio value="percent">比例佣金</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="commissionForm.mode === 'fixed'" label="固定金额">
|
||||
<ElInputNumber v-model="commissionForm.fixedAmount" :min="0" :precision="2" />
|
||||
<span style="margin-left: 8px">元/笔</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="commissionForm.mode === 'percent'" label="佣金比例">
|
||||
<ElInputNumber v-model="commissionForm.percent" :min="0" :max="100" :precision="2" />
|
||||
<span style="margin-left: 8px">%</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="结算周期">
|
||||
<ElSelect v-model="commissionForm.settlementCycle" style="width: 100%">
|
||||
<ElOption label="实时结算" value="realtime" />
|
||||
<ElOption label="日结" value="daily" />
|
||||
<ElOption label="周结" value="weekly" />
|
||||
<ElOption label="月结" value="monthly" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="commissionDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="saveCommissionConfig">保存</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'AgentManagement' })
|
||||
|
||||
interface Agent {
|
||||
id?: string
|
||||
agentName: string
|
||||
agentCode: string
|
||||
level: number
|
||||
parentId?: string | null
|
||||
parentName?: string
|
||||
contactPerson: string
|
||||
contactPhone: string
|
||||
email?: string
|
||||
address?: string
|
||||
remark?: string
|
||||
accountCount?: number
|
||||
simCardCount?: number
|
||||
totalCommission?: number
|
||||
status: 'active' | 'inactive'
|
||||
createTime?: string
|
||||
}
|
||||
|
||||
// Mock 数据
|
||||
const mockData = ref<Agent[]>([
|
||||
{
|
||||
id: '1',
|
||||
agentName: '华东区总代理',
|
||||
agentCode: 'AGENT_HD_001',
|
||||
level: 1,
|
||||
parentId: null,
|
||||
parentName: '无',
|
||||
contactPerson: '张三',
|
||||
contactPhone: '13800138001',
|
||||
email: 'zhangsan@example.com',
|
||||
address: '上海市浦东新区',
|
||||
accountCount: 5,
|
||||
simCardCount: 1000,
|
||||
totalCommission: 158900.50,
|
||||
status: 'active',
|
||||
createTime: '2026-01-01 10:00:00'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
agentName: '江苏省代理',
|
||||
agentCode: 'AGENT_JS_001',
|
||||
level: 2,
|
||||
parentId: '1',
|
||||
parentName: '华东区总代理',
|
||||
contactPerson: '李四',
|
||||
contactPhone: '13800138002',
|
||||
email: 'lisi@example.com',
|
||||
address: '江苏省南京市',
|
||||
accountCount: 3,
|
||||
simCardCount: 500,
|
||||
totalCommission: 78500.00,
|
||||
status: 'active',
|
||||
createTime: '2026-01-05 11:00:00'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
agentName: '南京市代理',
|
||||
agentCode: 'AGENT_NJ_001',
|
||||
level: 3,
|
||||
parentId: '2',
|
||||
parentName: '江苏省代理',
|
||||
contactPerson: '王五',
|
||||
contactPhone: '13800138003',
|
||||
email: 'wangwu@example.com',
|
||||
address: '江苏省南京市玄武区',
|
||||
accountCount: 2,
|
||||
simCardCount: 200,
|
||||
totalCommission: 32800.00,
|
||||
status: 'active',
|
||||
createTime: '2026-01-10 12:00:00'
|
||||
}
|
||||
])
|
||||
|
||||
const searchQuery = ref('')
|
||||
const levelFilter = ref('')
|
||||
const dialogVisible = ref(false)
|
||||
const accountDialogVisible = ref(false)
|
||||
const commissionDialogVisible = ref(false)
|
||||
const dialogType = ref<'add' | 'edit'>('add')
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const form = reactive<Agent>({
|
||||
agentName: '',
|
||||
agentCode: '',
|
||||
level: 1,
|
||||
parentId: null,
|
||||
contactPerson: '',
|
||||
contactPhone: '',
|
||||
email: '',
|
||||
address: '',
|
||||
remark: '',
|
||||
status: 'active'
|
||||
})
|
||||
|
||||
const commissionForm = reactive({
|
||||
mode: 'fixed',
|
||||
fixedAmount: 10,
|
||||
percent: 5,
|
||||
settlementCycle: 'monthly'
|
||||
})
|
||||
|
||||
const currentAgentAccounts = ref([
|
||||
{
|
||||
username: 'agent001',
|
||||
realName: '张三',
|
||||
phone: '13800138001',
|
||||
role: '管理员',
|
||||
status: 'active',
|
||||
createTime: '2026-01-01 10:00:00'
|
||||
}
|
||||
])
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
agentName: [{ required: true, message: '请输入代理商名称', trigger: 'blur' }],
|
||||
agentCode: [{ required: true, message: '请输入代理商编码', trigger: 'blur' }],
|
||||
level: [{ required: true, message: '请选择代理商等级', trigger: 'change' }],
|
||||
contactPerson: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
|
||||
contactPhone: [
|
||||
{ required: true, message: '请输入联系电话', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
const parentAgentOptions = computed(() => {
|
||||
return mockData.value.filter((item) => item.level < form.level)
|
||||
})
|
||||
|
||||
const filteredData = computed(() => {
|
||||
let data = mockData.value
|
||||
if (searchQuery.value) {
|
||||
data = data.filter(
|
||||
(item) =>
|
||||
item.agentName.includes(searchQuery.value) || item.contactPerson.includes(searchQuery.value)
|
||||
)
|
||||
}
|
||||
if (levelFilter.value) {
|
||||
data = data.filter((item) => item.level === parseInt(levelFilter.value))
|
||||
}
|
||||
return data
|
||||
})
|
||||
|
||||
const getLevelText = (level: number) => {
|
||||
const levelMap: Record<number, string> = { 1: '一级代理', 2: '二级代理', 3: '三级代理' }
|
||||
return levelMap[level] || '未知'
|
||||
}
|
||||
|
||||
const getLevelTagType = (level: number) => {
|
||||
const typeMap: Record<number, string> = { 1: '', 2: 'success', 3: 'warning' }
|
||||
return typeMap[level] || 'info'
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
// 搜索逻辑已通过 computed 实现
|
||||
}
|
||||
|
||||
const showDialog = (type: 'add' | 'edit', row?: Agent) => {
|
||||
dialogType.value = type
|
||||
dialogVisible.value = true
|
||||
|
||||
if (type === 'edit' && row) {
|
||||
Object.assign(form, row)
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(form, {
|
||||
agentName: '',
|
||||
agentCode: '',
|
||||
level: 1,
|
||||
parentId: null,
|
||||
contactPerson: '',
|
||||
contactPhone: '',
|
||||
email: '',
|
||||
address: '',
|
||||
remark: '',
|
||||
status: 'active'
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
|
||||
await formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
if (dialogType.value === 'add') {
|
||||
const parentAgent = mockData.value.find((item) => item.id === form.parentId)
|
||||
mockData.value.push({
|
||||
...form,
|
||||
id: Date.now().toString(),
|
||||
parentName: parentAgent ? parentAgent.agentName : '无',
|
||||
accountCount: 0,
|
||||
simCardCount: 0,
|
||||
totalCommission: 0,
|
||||
createTime: new Date().toLocaleString('zh-CN')
|
||||
})
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
const index = mockData.value.findIndex((item) => item.id === form.id)
|
||||
if (index !== -1) {
|
||||
const parentAgent = mockData.value.find((item) => item.id === form.parentId)
|
||||
mockData.value[index] = {
|
||||
...form,
|
||||
parentName: parentAgent ? parentAgent.agentName : '无'
|
||||
}
|
||||
}
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
formEl.resetFields()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = (row: Agent) => {
|
||||
ElMessageBox.confirm('确定删除该代理商吗?删除后其下属代理商和账号也将被删除。', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(() => {
|
||||
const index = mockData.value.findIndex((item) => item.id === row.id)
|
||||
if (index !== -1) {
|
||||
mockData.value.splice(index, 1)
|
||||
}
|
||||
ElMessage.success('删除成功')
|
||||
})
|
||||
}
|
||||
|
||||
const viewAccountList = (row: Agent) => {
|
||||
accountDialogVisible.value = true
|
||||
// 实际应用中应该根据代理商ID加载账号列表
|
||||
}
|
||||
|
||||
const viewCommission = (row: Agent) => {
|
||||
commissionDialogVisible.value = true
|
||||
// 实际应用中应该根据代理商ID加载佣金配置
|
||||
}
|
||||
|
||||
const addSubAccount = () => {
|
||||
ElMessage.info('创建子账号功能')
|
||||
}
|
||||
|
||||
const saveCommissionConfig = () => {
|
||||
ElMessage.success('佣金配置保存成功')
|
||||
commissionDialogVisible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
// 自定义样式
|
||||
}
|
||||
</style>
|
||||
276
src/views/account-management/customer-account/index.vue
Normal file
276
src/views/account-management/customer-account/index.vue
Normal file
@@ -0,0 +1,276 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<ElRow>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElInput v-model="searchQuery" placeholder="账号/姓名/手机号" clearable></ElInput>
|
||||
</ElCol>
|
||||
<div style="width: 12px"></div>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElSelect v-model="typeFilter" placeholder="客户类型" clearable style="width: 100%">
|
||||
<ElOption label="代理商" value="agent" />
|
||||
<ElOption label="企业客户" value="enterprise" />
|
||||
</ElSelect>
|
||||
</ElCol>
|
||||
<div style="width: 12px"></div>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElSelect v-model="statusFilter" placeholder="账号状态" clearable style="width: 100%">
|
||||
<ElOption label="正常" value="active" />
|
||||
<ElOption label="禁用" value="disabled" />
|
||||
<ElOption label="已锁定" value="locked" />
|
||||
</ElSelect>
|
||||
</ElCol>
|
||||
<div style="width: 12px"></div>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<ElButton v-ripple @click="handleSearch">搜索</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ArtTable :data="filteredData" index>
|
||||
<template #default>
|
||||
<ElTableColumn label="账号" prop="username" min-width="120" />
|
||||
<ElTableColumn label="真实姓名" prop="realName" />
|
||||
<ElTableColumn label="客户类型" prop="customerType">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.customerType === 'agent' ? '' : 'success'">
|
||||
{{ scope.row.customerType === 'agent' ? '代理商' : '企业客户' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="所属组织" prop="organizationName" show-overflow-tooltip />
|
||||
<ElTableColumn label="手机号" prop="phone" />
|
||||
<ElTableColumn label="邮箱" prop="email" show-overflow-tooltip />
|
||||
<ElTableColumn label="登录次数" prop="loginCount" />
|
||||
<ElTableColumn label="最后登录" prop="lastLoginTime" width="180" />
|
||||
<ElTableColumn label="状态" prop="status">
|
||||
<template #default="scope">
|
||||
<ElTag :type="getStatusTagType(scope.row.status)">
|
||||
{{ getStatusText(scope.row.status) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="createTime" width="180" />
|
||||
<ElTableColumn fixed="right" label="操作" width="280">
|
||||
<template #default="scope">
|
||||
<el-button link @click="viewDetail(scope.row)">详情</el-button>
|
||||
<el-button link @click="unbindPhone(scope.row)">解绑手机</el-button>
|
||||
<el-button link @click="resetPassword(scope.row)">重置密码</el-button>
|
||||
<el-button
|
||||
link
|
||||
:type="scope.row.status === 'active' ? 'danger' : 'primary'"
|
||||
@click="toggleStatus(scope.row)"
|
||||
>
|
||||
{{ scope.row.status === 'active' ? '禁用' : '启用' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 操作记录对话框 -->
|
||||
<ElDialog v-model="detailDialogVisible" title="账号详情" width="800px" align-center>
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem label="账号">{{ currentAccount?.username }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="真实姓名">{{ currentAccount?.realName }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="客户类型">{{
|
||||
currentAccount?.customerType === 'agent' ? '代理商' : '企业客户'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="所属组织">{{
|
||||
currentAccount?.organizationName
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="手机号">{{ currentAccount?.phone }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="邮箱">{{ currentAccount?.email }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="注册时间">{{ currentAccount?.createTime }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="最后登录">{{
|
||||
currentAccount?.lastLoginTime
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="登录次数">{{ currentAccount?.loginCount }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="状态">
|
||||
<ElTag :type="getStatusTagType(currentAccount?.status || 'active')">
|
||||
{{ getStatusText(currentAccount?.status || 'active') }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<ElDivider>操作记录</ElDivider>
|
||||
<ArtTable :data="operationRecords" index max-height="300">
|
||||
<template #default>
|
||||
<ElTableColumn label="操作类型" prop="operationType" />
|
||||
<ElTableColumn label="操作描述" prop="description" />
|
||||
<ElTableColumn label="操作人" prop="operator" />
|
||||
<ElTableColumn label="操作时间" prop="operateTime" width="180" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'CustomerAccount' })
|
||||
|
||||
interface CustomerAccount {
|
||||
id: string
|
||||
username: string
|
||||
realName: string
|
||||
customerType: 'agent' | 'enterprise'
|
||||
organizationName: string
|
||||
phone: string
|
||||
email: string
|
||||
loginCount: number
|
||||
lastLoginTime: string
|
||||
status: 'active' | 'disabled' | 'locked'
|
||||
createTime: string
|
||||
}
|
||||
|
||||
// Mock 数据
|
||||
const mockData = ref<CustomerAccount[]>([
|
||||
{
|
||||
id: '1',
|
||||
username: 'agent001',
|
||||
realName: '张三',
|
||||
customerType: 'agent',
|
||||
organizationName: '华东区总代理',
|
||||
phone: '13800138001',
|
||||
email: 'zhangsan@example.com',
|
||||
loginCount: 328,
|
||||
lastLoginTime: '2026-01-09 08:30:00',
|
||||
status: 'active',
|
||||
createTime: '2026-01-01 10:00:00'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
username: 'enterprise001',
|
||||
realName: '李四',
|
||||
customerType: 'enterprise',
|
||||
organizationName: '某某科技有限公司',
|
||||
phone: '13800138002',
|
||||
email: 'lisi@example.com',
|
||||
loginCount: 156,
|
||||
lastLoginTime: '2026-01-08 18:45:00',
|
||||
status: 'active',
|
||||
createTime: '2026-01-03 11:00:00'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
username: 'agent002',
|
||||
realName: '王五',
|
||||
customerType: 'agent',
|
||||
organizationName: '江苏省代理',
|
||||
phone: '13800138003',
|
||||
email: 'wangwu@example.com',
|
||||
loginCount: 89,
|
||||
lastLoginTime: '2026-01-07 14:20:00',
|
||||
status: 'disabled',
|
||||
createTime: '2026-01-05 12:00:00'
|
||||
}
|
||||
])
|
||||
|
||||
const operationRecords = ref([
|
||||
{
|
||||
operationType: '重置密码',
|
||||
description: '管理员重置登录密码',
|
||||
operator: 'admin',
|
||||
operateTime: '2026-01-08 10:00:00'
|
||||
},
|
||||
{
|
||||
operationType: '解绑手机',
|
||||
description: '解绑原手机号并重新绑定',
|
||||
operator: 'admin',
|
||||
operateTime: '2026-01-06 15:30:00'
|
||||
}
|
||||
])
|
||||
|
||||
const searchQuery = ref('')
|
||||
const typeFilter = ref('')
|
||||
const statusFilter = ref('')
|
||||
const detailDialogVisible = ref(false)
|
||||
const currentAccount = ref<CustomerAccount | null>(null)
|
||||
|
||||
const filteredData = computed(() => {
|
||||
let data = mockData.value
|
||||
if (searchQuery.value) {
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
data = data.filter(
|
||||
(item) =>
|
||||
item.username.toLowerCase().includes(query) ||
|
||||
item.realName.includes(query) ||
|
||||
item.phone.includes(query)
|
||||
)
|
||||
}
|
||||
if (typeFilter.value) {
|
||||
data = data.filter((item) => item.customerType === typeFilter.value)
|
||||
}
|
||||
if (statusFilter.value) {
|
||||
data = data.filter((item) => item.status === statusFilter.value)
|
||||
}
|
||||
return data
|
||||
})
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
active: '正常',
|
||||
disabled: '禁用',
|
||||
locked: '已锁定'
|
||||
}
|
||||
return statusMap[status] || '未知'
|
||||
}
|
||||
|
||||
const getStatusTagType = (status: string) => {
|
||||
const typeMap: Record<string, string> = {
|
||||
active: 'success',
|
||||
disabled: 'info',
|
||||
locked: 'danger'
|
||||
}
|
||||
return typeMap[status] || 'info'
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
// 搜索逻辑已通过 computed 实现
|
||||
}
|
||||
|
||||
const viewDetail = (row: CustomerAccount) => {
|
||||
currentAccount.value = row
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
const unbindPhone = (row: CustomerAccount) => {
|
||||
ElMessageBox.confirm(`确定要解绑账号 ${row.username} 的手机号吗?`, '解绑确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
ElMessage.success('手机号解绑成功')
|
||||
})
|
||||
}
|
||||
|
||||
const resetPassword = (row: CustomerAccount) => {
|
||||
ElMessageBox.confirm(`确定要重置账号 ${row.username} 的密码吗?新密码将发送至其手机。`, '重置密码', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
ElMessage.success('密码重置成功,新密码已发送至手机')
|
||||
})
|
||||
}
|
||||
|
||||
const toggleStatus = (row: CustomerAccount) => {
|
||||
const action = row.status === 'active' ? '禁用' : '启用'
|
||||
ElMessageBox.confirm(`确定要${action}账号 ${row.username} 吗?`, `${action}确认`, {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
row.status = row.status === 'active' ? 'disabled' : 'active'
|
||||
ElMessage.success(`${action}成功`)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
:deep(.el-descriptions__label) {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
441
src/views/account-management/customer-commission/index.vue
Normal file
441
src/views/account-management/customer-commission/index.vue
Normal file
@@ -0,0 +1,441 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<!-- 统计卡片 -->
|
||||
<ElRow :gutter="20" style="margin-bottom: 20px">
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover" class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">客户总数</div>
|
||||
<div class="stat-value">{{ statistics.totalCustomers }}</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon" style="color: var(--el-color-primary)"><User /></el-icon>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover" class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">累计佣金</div>
|
||||
<div class="stat-value" style="color: var(--el-color-success)">
|
||||
¥{{ statistics.totalCommission.toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon" style="color: var(--el-color-success)"><Money /></el-icon>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover" class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">已提现</div>
|
||||
<div class="stat-value" style="color: var(--el-color-warning)">
|
||||
¥{{ statistics.totalWithdrawn.toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon" style="color: var(--el-color-warning)"><WalletFilled /></el-icon>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover" class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">待提现</div>
|
||||
<div class="stat-value" style="color: var(--el-color-danger)">
|
||||
¥{{ statistics.pendingWithdrawal.toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon" style="color: var(--el-color-danger)"><Wallet /></el-icon>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<!-- 搜索和筛选区 -->
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElInput v-model="searchQuery" placeholder="客户名称/手机号" clearable />
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElSelect v-model="customerTypeFilter" placeholder="客户类型" clearable style="width: 100%">
|
||||
<ElOption label="全部" value="" />
|
||||
<ElOption label="代理商" value="agent" />
|
||||
<ElOption label="企业客户" value="enterprise" />
|
||||
</ElSelect>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElSelect v-model="commissionRangeFilter" placeholder="佣金范围" clearable style="width: 100%">
|
||||
<ElOption label="全部" value="" />
|
||||
<ElOption label="0-1000元" value="0-1000" />
|
||||
<ElOption label="1000-5000元" value="1000-5000" />
|
||||
<ElOption label="5000-10000元" value="5000-10000" />
|
||||
<ElOption label="10000元以上" value="10000+" />
|
||||
</ElSelect>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<ElButton v-ripple @click="handleSearch">搜索</ElButton>
|
||||
<ElButton v-ripple @click="exportData">导出</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<!-- 客户佣金列表 -->
|
||||
<ArtTable :data="filteredData" index style="margin-top: 20px">
|
||||
<template #default>
|
||||
<ElTableColumn label="客户名称" prop="customerName" min-width="150" show-overflow-tooltip />
|
||||
<ElTableColumn label="客户类型" prop="customerType" width="120">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.customerType === 'agent' ? 'warning' : 'primary'">
|
||||
{{ scope.row.customerType === 'agent' ? '代理商' : '企业客户' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="联系电话" prop="phone" width="130" />
|
||||
<ElTableColumn label="累计佣金" prop="totalCommission" width="130" sortable>
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-success); font-weight: 600">
|
||||
¥{{ scope.row.totalCommission.toFixed(2) }}
|
||||
</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="已提现" prop="withdrawnAmount" width="130" sortable>
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-warning)">
|
||||
¥{{ scope.row.withdrawnAmount.toFixed(2) }}
|
||||
</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="待提现" prop="pendingAmount" width="130" sortable>
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-danger); font-weight: 600">
|
||||
¥{{ scope.row.pendingAmount.toFixed(2) }}
|
||||
</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="提现次数" prop="withdrawalCount" width="100" align="center" />
|
||||
<ElTableColumn label="卡片数量" prop="cardCount" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-primary)">{{ scope.row.cardCount }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="最近提现" prop="lastWithdrawalTime" width="180" />
|
||||
<ElTableColumn label="注册时间" prop="registerTime" width="180" />
|
||||
<ElTableColumn fixed="right" label="操作" width="220">
|
||||
<template #default="scope">
|
||||
<el-button link :icon="View" @click="viewCommissionDetail(scope.row)">佣金详情</el-button>
|
||||
<el-button link :icon="List" @click="viewWithdrawalHistory(scope.row)">提现记录</el-button>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 佣金详情对话框 -->
|
||||
<ElDialog v-model="commissionDialogVisible" title="佣金详情" width="900px" align-center>
|
||||
<ElDescriptions :column="2" border style="margin-bottom: 20px">
|
||||
<ElDescriptionsItem label="客户名称">{{ currentCustomer.customerName }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="客户类型">
|
||||
<ElTag :type="currentCustomer.customerType === 'agent' ? 'warning' : 'primary'">
|
||||
{{ currentCustomer.customerType === 'agent' ? '代理商' : '企业客户' }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="累计佣金">
|
||||
<span style="color: var(--el-color-success); font-weight: 600">
|
||||
¥{{ currentCustomer.totalCommission.toFixed(2) }}
|
||||
</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="已提现">
|
||||
<span style="color: var(--el-color-warning)">
|
||||
¥{{ currentCustomer.withdrawnAmount.toFixed(2) }}
|
||||
</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="待提现">
|
||||
<span style="color: var(--el-color-danger); font-weight: 600">
|
||||
¥{{ currentCustomer.pendingAmount.toFixed(2) }}
|
||||
</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="提现次数">{{ currentCustomer.withdrawalCount }}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<ElDivider content-position="left">佣金明细</ElDivider>
|
||||
<ArtTable :data="commissionDetails" index>
|
||||
<template #default>
|
||||
<ElTableColumn label="来源" prop="source" width="120" />
|
||||
<ElTableColumn label="订单号" prop="orderNo" width="180" />
|
||||
<ElTableColumn label="佣金金额" prop="amount" width="120">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-success)">+¥{{ scope.row.amount.toFixed(2) }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="佣金比例" prop="rate" width="100" />
|
||||
<ElTableColumn label="订单金额" prop="orderAmount" width="120" />
|
||||
<ElTableColumn label="获得时间" prop="createTime" width="180" />
|
||||
<ElTableColumn label="状态" prop="status" width="100">
|
||||
<template #default="scope">
|
||||
<ElTag v-if="scope.row.status === 'completed'" type="success">已结算</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'pending'" type="warning">待结算</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="commissionDialogVisible = false">关闭</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 提现记录对话框 -->
|
||||
<ElDialog v-model="withdrawalDialogVisible" title="提现记录" width="900px" align-center>
|
||||
<ArtTable :data="withdrawalHistory" index>
|
||||
<template #default>
|
||||
<ElTableColumn label="提现单号" prop="withdrawalNo" width="180" />
|
||||
<ElTableColumn label="提现金额" prop="amount" width="120">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-danger); font-weight: 600">
|
||||
¥{{ scope.row.amount.toFixed(2) }}
|
||||
</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="手续费" prop="fee" width="100">
|
||||
<template #default="scope"> ¥{{ scope.row.fee.toFixed(2) }} </template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="实际到账" prop="actualAmount" width="120">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-success)">
|
||||
¥{{ scope.row.actualAmount.toFixed(2) }}
|
||||
</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="提现方式" prop="method" width="100" />
|
||||
<ElTableColumn label="状态" prop="status" width="100">
|
||||
<template #default="scope">
|
||||
<ElTag v-if="scope.row.status === 'completed'" type="success">已完成</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'processing'" type="warning">处理中</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'pending'" type="info">待审核</ElTag>
|
||||
<ElTag v-else type="danger">已拒绝</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="申请时间" prop="applyTime" width="180" />
|
||||
<ElTableColumn label="完成时间" prop="completeTime" width="180">
|
||||
<template #default="scope">
|
||||
{{ scope.row.completeTime || '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="withdrawalDialogVisible = false">关闭</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { View, List, User, Money, WalletFilled, Wallet } from '@element-plus/icons-vue'
|
||||
|
||||
defineOptions({ name: 'CustomerCommission' })
|
||||
|
||||
interface CustomerCommission {
|
||||
id: string
|
||||
customerName: string
|
||||
customerType: 'agent' | 'enterprise'
|
||||
phone: string
|
||||
totalCommission: number
|
||||
withdrawnAmount: number
|
||||
pendingAmount: number
|
||||
withdrawalCount: number
|
||||
cardCount: number
|
||||
lastWithdrawalTime: string
|
||||
registerTime: string
|
||||
}
|
||||
|
||||
const searchQuery = ref('')
|
||||
const customerTypeFilter = ref('')
|
||||
const commissionRangeFilter = ref('')
|
||||
const commissionDialogVisible = ref(false)
|
||||
const withdrawalDialogVisible = ref(false)
|
||||
|
||||
const statistics = reactive({
|
||||
totalCustomers: 156,
|
||||
totalCommission: 580000.00,
|
||||
totalWithdrawn: 420000.00,
|
||||
pendingWithdrawal: 160000.00
|
||||
})
|
||||
|
||||
const mockData = ref<CustomerCommission[]>([
|
||||
{
|
||||
id: '1',
|
||||
customerName: '华东区总代理',
|
||||
customerType: 'agent',
|
||||
phone: '13800138000',
|
||||
totalCommission: 85000.00,
|
||||
withdrawnAmount: 70000.00,
|
||||
pendingAmount: 15000.00,
|
||||
withdrawalCount: 12,
|
||||
cardCount: 500,
|
||||
lastWithdrawalTime: '2026-01-08 10:00:00',
|
||||
registerTime: '2025-06-01 09:00:00'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
customerName: '深圳市科技有限公司',
|
||||
customerType: 'enterprise',
|
||||
phone: '13900139000',
|
||||
totalCommission: 45000.00,
|
||||
withdrawnAmount: 30000.00,
|
||||
pendingAmount: 15000.00,
|
||||
withdrawalCount: 8,
|
||||
cardCount: 300,
|
||||
lastWithdrawalTime: '2026-01-05 14:30:00',
|
||||
registerTime: '2025-07-15 10:00:00'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
customerName: '北京智能制造',
|
||||
customerType: 'enterprise',
|
||||
phone: '13700137000',
|
||||
totalCommission: 68000.00,
|
||||
withdrawnAmount: 55000.00,
|
||||
pendingAmount: 13000.00,
|
||||
withdrawalCount: 10,
|
||||
cardCount: 450,
|
||||
lastWithdrawalTime: '2026-01-07 16:00:00',
|
||||
registerTime: '2025-08-20 11:00:00'
|
||||
}
|
||||
])
|
||||
|
||||
const currentCustomer = ref<CustomerCommission>({
|
||||
id: '',
|
||||
customerName: '',
|
||||
customerType: 'agent',
|
||||
phone: '',
|
||||
totalCommission: 0,
|
||||
withdrawnAmount: 0,
|
||||
pendingAmount: 0,
|
||||
withdrawalCount: 0,
|
||||
cardCount: 0,
|
||||
lastWithdrawalTime: '',
|
||||
registerTime: ''
|
||||
})
|
||||
|
||||
const commissionDetails = ref([
|
||||
{
|
||||
id: '1',
|
||||
source: '套餐销售',
|
||||
orderNo: 'ORD202601090001',
|
||||
amount: 150.00,
|
||||
rate: '10%',
|
||||
orderAmount: '¥1,500.00',
|
||||
createTime: '2026-01-09 09:30:00',
|
||||
status: 'completed'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
source: '卡片激活',
|
||||
orderNo: 'ORD202601080025',
|
||||
amount: 80.00,
|
||||
rate: '8%',
|
||||
orderAmount: '¥1,000.00',
|
||||
createTime: '2026-01-08 15:20:00',
|
||||
status: 'completed'
|
||||
}
|
||||
])
|
||||
|
||||
const withdrawalHistory = ref([
|
||||
{
|
||||
id: '1',
|
||||
withdrawalNo: 'WD202601080001',
|
||||
amount: 10000.00,
|
||||
fee: 20.00,
|
||||
actualAmount: 9980.00,
|
||||
method: '银行卡',
|
||||
status: 'completed',
|
||||
applyTime: '2026-01-08 10:00:00',
|
||||
completeTime: '2026-01-08 15:30:00'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
withdrawalNo: 'WD202601050002',
|
||||
amount: 5000.00,
|
||||
fee: 10.00,
|
||||
actualAmount: 4990.00,
|
||||
method: '支付宝',
|
||||
status: 'completed',
|
||||
applyTime: '2026-01-05 14:00:00',
|
||||
completeTime: '2026-01-05 18:20:00'
|
||||
}
|
||||
])
|
||||
|
||||
const filteredData = computed(() => {
|
||||
let data = mockData.value
|
||||
|
||||
if (searchQuery.value) {
|
||||
data = data.filter(
|
||||
(item) =>
|
||||
item.customerName.includes(searchQuery.value) ||
|
||||
item.phone.includes(searchQuery.value)
|
||||
)
|
||||
}
|
||||
|
||||
if (customerTypeFilter.value) {
|
||||
data = data.filter((item) => item.customerType === customerTypeFilter.value)
|
||||
}
|
||||
|
||||
if (commissionRangeFilter.value) {
|
||||
data = data.filter((item) => {
|
||||
const commission = item.totalCommission
|
||||
if (commissionRangeFilter.value === '0-1000') return commission >= 0 && commission < 1000
|
||||
if (commissionRangeFilter.value === '1000-5000') return commission >= 1000 && commission < 5000
|
||||
if (commissionRangeFilter.value === '5000-10000') return commission >= 5000 && commission < 10000
|
||||
if (commissionRangeFilter.value === '10000+') return commission >= 10000
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
return data
|
||||
})
|
||||
|
||||
const handleSearch = () => {}
|
||||
|
||||
const exportData = () => {
|
||||
ElMessage.success('数据导出中...')
|
||||
}
|
||||
|
||||
const viewCommissionDetail = (row: CustomerCommission) => {
|
||||
currentCustomer.value = { ...row }
|
||||
commissionDialogVisible.value = true
|
||||
}
|
||||
|
||||
const viewWithdrawalHistory = (row: CustomerCommission) => {
|
||||
currentCustomer.value = { ...row }
|
||||
withdrawalDialogVisible.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
.stat-card {
|
||||
:deep(.el-card__body) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 40px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
240
src/views/account-management/customer-role/index.vue
Normal file
240
src/views/account-management/customer-role/index.vue
Normal file
@@ -0,0 +1,240 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<ElRow>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElInput v-model="searchQuery" placeholder="角色名称" clearable></ElInput>
|
||||
</ElCol>
|
||||
<div style="width: 12px"></div>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<ElButton v-ripple @click="handleSearch">搜索</ElButton>
|
||||
<ElButton v-ripple @click="showDialog('add')">新增角色</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ArtTable :data="filteredData" index>
|
||||
<template #default>
|
||||
<ElTableColumn label="角色名称" prop="roleName" />
|
||||
<ElTableColumn label="角色编码" prop="roleCode" />
|
||||
<ElTableColumn label="能力边界" prop="abilities">
|
||||
<template #default="scope">
|
||||
<ElTag
|
||||
v-for="(ability, index) in scope.row.abilities"
|
||||
:key="index"
|
||||
size="small"
|
||||
style="margin-right: 4px"
|
||||
>
|
||||
{{ ability }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="描述" prop="description" show-overflow-tooltip />
|
||||
<ElTableColumn label="状态" prop="status">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status === 'active' ? 'success' : 'info'">
|
||||
{{ scope.row.status === 'active' ? '启用' : '禁用' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="createTime" width="180" />
|
||||
<ElTableColumn fixed="right" label="操作" width="180">
|
||||
<template #default="scope">
|
||||
<el-button link @click="showDialog('edit', scope.row)">编辑</el-button>
|
||||
<el-button link @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增客户角色' : '编辑客户角色'"
|
||||
width="600px"
|
||||
align-center
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<ElFormItem label="角色名称" prop="roleName">
|
||||
<ElInput v-model="form.roleName" placeholder="请输入角色名称" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="角色编码" prop="roleCode">
|
||||
<ElInput v-model="form.roleCode" placeholder="请输入角色编码" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="能力边界" prop="abilities">
|
||||
<ElCheckboxGroup v-model="form.abilities">
|
||||
<ElCheckbox label="查看网卡">查看网卡</ElCheckbox>
|
||||
<ElCheckbox label="操作网卡">操作网卡</ElCheckbox>
|
||||
<ElCheckbox label="查看套餐">查看套餐</ElCheckbox>
|
||||
<ElCheckbox label="购买套餐">购买套餐</ElCheckbox>
|
||||
<ElCheckbox label="查看设备">查看设备</ElCheckbox>
|
||||
<ElCheckbox label="管理设备">管理设备</ElCheckbox>
|
||||
<ElCheckbox label="查看佣金">查看佣金</ElCheckbox>
|
||||
<ElCheckbox label="提现佣金">提现佣金</ElCheckbox>
|
||||
</ElCheckboxGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="描述" prop="description">
|
||||
<ElInput v-model="form.description" type="textarea" :rows="3" placeholder="请输入角色描述" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="状态">
|
||||
<ElSwitch v-model="form.status" active-value="active" inactive-value="inactive" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'CustomerRole' })
|
||||
|
||||
interface CustomerRole {
|
||||
id?: string
|
||||
roleName: string
|
||||
roleCode: string
|
||||
abilities: string[]
|
||||
description: string
|
||||
status: 'active' | 'inactive'
|
||||
createTime?: string
|
||||
}
|
||||
|
||||
// Mock 数据
|
||||
const mockData = ref<CustomerRole[]>([
|
||||
{
|
||||
id: '1',
|
||||
roleName: '标准客户',
|
||||
roleCode: 'CUSTOMER_STANDARD',
|
||||
abilities: ['查看网卡', '查看套餐', '查看设备'],
|
||||
description: '标准客户角色,拥有基本查看权限',
|
||||
status: 'active',
|
||||
createTime: '2026-01-01 10:00:00'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
roleName: 'VIP客户',
|
||||
roleCode: 'CUSTOMER_VIP',
|
||||
abilities: ['查看网卡', '操作网卡', '查看套餐', '购买套餐', '查看设备', '管理设备'],
|
||||
description: 'VIP客户角色,拥有更多操作权限',
|
||||
status: 'active',
|
||||
createTime: '2026-01-02 11:00:00'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
roleName: '企业客户',
|
||||
roleCode: 'CUSTOMER_ENTERPRISE',
|
||||
abilities: ['查看网卡', '操作网卡', '查看套餐', '购买套餐', '查看设备', '管理设备', '查看佣金'],
|
||||
description: '企业客户角色,拥有完整业务权限',
|
||||
status: 'active',
|
||||
createTime: '2026-01-03 12:00:00'
|
||||
}
|
||||
])
|
||||
|
||||
const searchQuery = ref('')
|
||||
const dialogVisible = ref(false)
|
||||
const dialogType = ref<'add' | 'edit'>('add')
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const form = reactive<CustomerRole>({
|
||||
roleName: '',
|
||||
roleCode: '',
|
||||
abilities: [],
|
||||
description: '',
|
||||
status: 'active'
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
roleName: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
|
||||
roleCode: [{ required: true, message: '请输入角色编码', trigger: 'blur' }],
|
||||
abilities: [{ required: true, message: '请至少选择一项能力', trigger: 'change' }]
|
||||
})
|
||||
|
||||
const filteredData = computed(() => {
|
||||
if (!searchQuery.value) return mockData.value
|
||||
return mockData.value.filter((item) =>
|
||||
item.roleName.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||
)
|
||||
})
|
||||
|
||||
const handleSearch = () => {
|
||||
// 搜索逻辑已通过 computed 实现
|
||||
}
|
||||
|
||||
const showDialog = (type: 'add' | 'edit', row?: CustomerRole) => {
|
||||
dialogType.value = type
|
||||
dialogVisible.value = true
|
||||
|
||||
if (type === 'edit' && row) {
|
||||
form.id = row.id
|
||||
form.roleName = row.roleName
|
||||
form.roleCode = row.roleCode
|
||||
form.abilities = [...row.abilities]
|
||||
form.description = row.description
|
||||
form.status = row.status
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
form.id = undefined
|
||||
form.roleName = ''
|
||||
form.roleCode = ''
|
||||
form.abilities = []
|
||||
form.description = ''
|
||||
form.status = 'active'
|
||||
}
|
||||
|
||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
|
||||
await formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
if (dialogType.value === 'add') {
|
||||
mockData.value.push({
|
||||
...form,
|
||||
id: Date.now().toString(),
|
||||
createTime: new Date().toLocaleString('zh-CN')
|
||||
})
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
const index = mockData.value.findIndex((item) => item.id === form.id)
|
||||
if (index !== -1) {
|
||||
mockData.value[index] = { ...form }
|
||||
}
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
formEl.resetFields()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = (row: CustomerRole) => {
|
||||
ElMessageBox.confirm('确定删除该客户角色吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(() => {
|
||||
const index = mockData.value.findIndex((item) => item.id === row.id)
|
||||
if (index !== -1) {
|
||||
mockData.value.splice(index, 1)
|
||||
}
|
||||
ElMessage.success('删除成功')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
:deep(.el-checkbox) {
|
||||
margin-right: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
359
src/views/account-management/customer/index.vue
Normal file
359
src/views/account-management/customer/index.vue
Normal file
@@ -0,0 +1,359 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="customer-management-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton type="primary" @click="handleSearch">查询</ElButton>
|
||||
<ElButton @click="exportExcel">导出excel</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
|
||||
defineOptions({ name: 'CustomerManagement' })
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
distributorName: '',
|
||||
distributorAccount: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 模拟数据 - 客户管理(代理商)
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
distributorName: '北京优享科技有限公司',
|
||||
distributorAccount: 'bjyx2024',
|
||||
totalCommission: 12560.8,
|
||||
instantCommission: 8320.5,
|
||||
pendingCommission: 4240.3,
|
||||
prepaidCommission: 2400.0,
|
||||
withdrawnCommission: 6800.2,
|
||||
withdrawingCommission: 1520.3,
|
||||
availableCommission: 2000.0
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
distributorName: '上海智联通信技术公司',
|
||||
distributorAccount: 'shzl2024',
|
||||
totalCommission: 18750.25,
|
||||
instantCommission: 12500.75,
|
||||
pendingCommission: 6249.5,
|
||||
prepaidCommission: 3600.0,
|
||||
withdrawnCommission: 8900.25,
|
||||
withdrawingCommission: 2100.0,
|
||||
availableCommission: 3750.0
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
distributorName: '广州物联网络有限公司',
|
||||
distributorAccount: 'gzwl2024',
|
||||
totalCommission: 9876.4,
|
||||
instantCommission: 6584.3,
|
||||
pendingCommission: 3292.1,
|
||||
prepaidCommission: 1800.0,
|
||||
withdrawnCommission: 4938.2,
|
||||
withdrawingCommission: 980.2,
|
||||
availableCommission: 1958.0
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
distributorName: '深圳云联科技股份公司',
|
||||
distributorAccount: 'szyl2024',
|
||||
totalCommission: 24500.6,
|
||||
instantCommission: 16333.73,
|
||||
pendingCommission: 8166.87,
|
||||
prepaidCommission: 4800.0,
|
||||
withdrawnCommission: 11225.3,
|
||||
withdrawingCommission: 2450.06,
|
||||
availableCommission: 4900.12
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
distributorName: '杭州通达网络服务公司',
|
||||
distributorAccount: 'hztd2024',
|
||||
totalCommission: 15200.35,
|
||||
instantCommission: 10133.57,
|
||||
pendingCommission: 5066.78,
|
||||
prepaidCommission: 2900.0,
|
||||
withdrawnCommission: 7600.18,
|
||||
withdrawingCommission: 1520.04,
|
||||
availableCommission: 3040.07
|
||||
}
|
||||
]
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1
|
||||
getCustomerList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1
|
||||
getCustomerList()
|
||||
}
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '代理商名称',
|
||||
prop: 'distributorName',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入代理商名称'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '代理商账号',
|
||||
prop: 'distributorAccount',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入代理商账号'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: '代理商名称', prop: 'distributorName' },
|
||||
{ label: '代理商账号', prop: 'distributorAccount' },
|
||||
{ label: '总佣金', prop: 'totalCommission' },
|
||||
{ label: '已秒返佣金', prop: 'instantCommission' },
|
||||
{ label: '未秒返佣金', prop: 'pendingCommission' },
|
||||
{ label: '预存佣金', prop: 'prepaidCommission' },
|
||||
{ label: '已提现佣金', prop: 'withdrawnCommission' },
|
||||
{ label: '提现中佣金', prop: 'withdrawingCommission' },
|
||||
{ label: '可提现佣金', prop: 'availableCommission' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 导出Excel
|
||||
const exportExcel = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要导出的数据')
|
||||
return
|
||||
}
|
||||
ElMessage.success(`导出 ${selectedRows.value.length} 条客户记录`)
|
||||
}
|
||||
|
||||
// 查看提现详情
|
||||
const viewWithdrawDetail = (row: any) => {
|
||||
ElMessage.info(`查看 ${row.distributorName} 的提现详情`)
|
||||
}
|
||||
|
||||
// 查看佣金明细
|
||||
const viewCommissionDetail = (row: any) => {
|
||||
ElMessage.info(`查看 ${row.distributorName} 的佣金明细`)
|
||||
}
|
||||
|
||||
// 查看自动提现详情
|
||||
const viewAutoWithdrawDetail = (row: any) => {
|
||||
ElMessage.info(`查看 ${row.distributorName} 的自动提现详情`)
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'distributorName',
|
||||
label: '代理商名称',
|
||||
minWidth: 180
|
||||
},
|
||||
{
|
||||
prop: 'distributorAccount',
|
||||
label: '代理商账号',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'totalCommission',
|
||||
label: '总佣金',
|
||||
width: 120,
|
||||
formatter: (row) => `¥${row.totalCommission.toFixed(2)}`
|
||||
},
|
||||
{
|
||||
prop: 'instantCommission',
|
||||
label: '已秒返佣金',
|
||||
width: 120,
|
||||
formatter: (row) => `¥${row.instantCommission.toFixed(2)}`
|
||||
},
|
||||
{
|
||||
prop: 'pendingCommission',
|
||||
label: '未秒返佣金',
|
||||
width: 120,
|
||||
formatter: (row) => `¥${row.pendingCommission.toFixed(2)}`
|
||||
},
|
||||
{
|
||||
prop: 'prepaidCommission',
|
||||
label: '预存佣金',
|
||||
width: 120,
|
||||
formatter: (row) => `¥${row.prepaidCommission.toFixed(2)}`
|
||||
},
|
||||
{
|
||||
prop: 'withdrawnCommission',
|
||||
label: '已提现佣金',
|
||||
width: 120,
|
||||
formatter: (row) => `¥${row.withdrawnCommission.toFixed(2)}`
|
||||
},
|
||||
{
|
||||
prop: 'withdrawingCommission',
|
||||
label: '提现中佣金',
|
||||
width: 120,
|
||||
formatter: (row) => `¥${row.withdrawingCommission.toFixed(2)}`
|
||||
},
|
||||
{
|
||||
prop: 'availableCommission',
|
||||
label: '可提现佣金',
|
||||
width: 120,
|
||||
formatter: (row) => `¥${row.availableCommission.toFixed(2)}`
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 200,
|
||||
formatter: (row: any) => {
|
||||
return h('div', { style: 'display: flex; gap: 5px; flex-wrap: wrap;' }, [
|
||||
h(ArtButtonTable, {
|
||||
type: 'view',
|
||||
text: '提现详情',
|
||||
onClick: () => viewWithdrawDetail(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
type: 'detail',
|
||||
text: '佣金明细',
|
||||
onClick: () => viewCommissionDetail(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
type: 'info',
|
||||
text: '自动提现详情',
|
||||
onClick: () => viewAutoWithdrawDetail(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getCustomerList()
|
||||
})
|
||||
|
||||
// 获取客户列表
|
||||
const getCustomerList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
|
||||
const endIndex = startIndex + pagination.pageSize
|
||||
const paginatedData = mockData.slice(startIndex, endIndex)
|
||||
|
||||
tableData.value = paginatedData
|
||||
pagination.total = mockData.length
|
||||
loading.value = false
|
||||
} catch (error) {
|
||||
console.error('获取客户列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getCustomerList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getCustomerList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getCustomerList()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.customer-management-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
</style>
|
||||
498
src/views/account-management/enterprise-customer/index.vue
Normal file
498
src/views/account-management/enterprise-customer/index.vue
Normal file
@@ -0,0 +1,498 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<!-- 搜索和操作区 -->
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElInput v-model="searchQuery" placeholder="企业名称/联系人" clearable />
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElSelect v-model="statusFilter" placeholder="状态筛选" clearable style="width: 100%">
|
||||
<ElOption label="全部" value="" />
|
||||
<ElOption label="正常" value="active" />
|
||||
<ElOption label="禁用" value="disabled" />
|
||||
<ElOption label="待审核" value="pending" />
|
||||
</ElSelect>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElSelect v-model="roleFilter" placeholder="角色筛选" clearable style="width: 100%">
|
||||
<ElOption label="全部" value="" />
|
||||
<ElOption label="企业管理员" value="admin" />
|
||||
<ElOption label="企业用户" value="user" />
|
||||
</ElSelect>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<ElButton v-ripple @click="handleSearch">搜索</ElButton>
|
||||
<ElButton v-ripple type="primary" @click="showDialog('add')">新增企业客户</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<!-- 企业客户列表 -->
|
||||
<ArtTable :data="filteredData" index style="margin-top: 20px">
|
||||
<template #default>
|
||||
<ElTableColumn label="企业名称" prop="enterpriseName" min-width="180" show-overflow-tooltip />
|
||||
<ElTableColumn label="统一社会信用代码" prop="creditCode" width="180" />
|
||||
<ElTableColumn label="联系人" prop="contactPerson" width="120" />
|
||||
<ElTableColumn label="联系电话" prop="contactPhone" width="130" />
|
||||
<ElTableColumn label="所属角色" prop="roleName" width="120">
|
||||
<template #default="scope">
|
||||
<ElTag>{{ scope.row.roleName }}</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="卡片数量" prop="cardCount" width="100">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-primary)">{{ scope.row.cardCount }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="设备数量" prop="deviceCount" width="100">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-success)">{{ scope.row.deviceCount }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="账户余额" prop="balance" width="120">
|
||||
<template #default="scope"> ¥{{ scope.row.balance.toFixed(2) }} </template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" prop="status" width="100">
|
||||
<template #default="scope">
|
||||
<ElTag v-if="scope.row.status === 'active'" type="success">正常</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'disabled'" type="danger">禁用</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'pending'" type="warning">待审核</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="createTime" width="180" />
|
||||
<ElTableColumn fixed="right" label="操作" width="260">
|
||||
<template #default="scope">
|
||||
<el-button link :icon="View" @click="viewDetail(scope.row)">详情</el-button>
|
||||
<el-button link @click="showDialog('edit', scope.row)">编辑</el-button>
|
||||
<el-button
|
||||
link
|
||||
:type="scope.row.status === 'active' ? 'warning' : 'success'"
|
||||
@click="toggleStatus(scope.row)"
|
||||
>
|
||||
{{ scope.row.status === 'active' ? '禁用' : '启用' }}
|
||||
</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 新增/编辑企业客户对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增企业客户' : '编辑企业客户'"
|
||||
width="700px"
|
||||
align-center
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="140px">
|
||||
<ElDivider content-position="left">企业信息</ElDivider>
|
||||
|
||||
<ElFormItem label="企业名称" prop="enterpriseName">
|
||||
<ElInput v-model="form.enterpriseName" placeholder="请输入企业名称" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="统一社会信用代码" prop="creditCode">
|
||||
<ElInput v-model="form.creditCode" placeholder="请输入统一社会信用代码" maxlength="18" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="企业地址" prop="address">
|
||||
<ElInput v-model="form.address" placeholder="请输入企业地址" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="营业执照" prop="businessLicense">
|
||||
<ElUpload
|
||||
:action="uploadUrl"
|
||||
:limit="1"
|
||||
list-type="picture-card"
|
||||
accept="image/*"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
</ElUpload>
|
||||
<div style="color: var(--el-text-color-secondary); font-size: 12px">支持 JPG、PNG 格式</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElDivider content-position="left">联系人信息</ElDivider>
|
||||
|
||||
<ElFormItem label="联系人" prop="contactPerson">
|
||||
<ElInput v-model="form.contactPerson" placeholder="请输入联系人姓名" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="联系电话" prop="contactPhone">
|
||||
<ElInput v-model="form.contactPhone" placeholder="请输入联系电话" maxlength="11" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="联系邮箱" prop="contactEmail">
|
||||
<ElInput v-model="form.contactEmail" placeholder="请输入联系邮箱" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElDivider content-position="left">账号配置</ElDivider>
|
||||
|
||||
<ElFormItem label="登录账号" prop="username">
|
||||
<ElInput v-model="form.username" placeholder="请输入登录账号" :disabled="dialogType === 'edit'" />
|
||||
<div style="color: var(--el-text-color-secondary); font-size: 12px">只能登录企业端</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="dialogType === 'add'" label="登录密码" prop="password">
|
||||
<ElInput v-model="form.password" type="password" placeholder="请输入登录密码" show-password />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="分配角色" prop="roleId">
|
||||
<ElSelect v-model="form.roleId" placeholder="请选择客户角色" style="width: 100%">
|
||||
<ElOption
|
||||
v-for="role in availableRoles"
|
||||
:key="role.id"
|
||||
:label="role.roleName"
|
||||
:value="role.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
<div style="color: var(--el-text-color-secondary); font-size: 12px">角色决定企业客户的能力边界</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="初始余额" prop="initialBalance">
|
||||
<ElInputNumber v-model="form.initialBalance" :min="0" :precision="2" style="width: 100%" />
|
||||
<span style="margin-left: 8px">元</span>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="备注" prop="remark">
|
||||
<ElInput v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注信息" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="状态">
|
||||
<ElSwitch v-model="form.status" active-value="active" inactive-value="disabled" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 企业详情对话框 -->
|
||||
<ElDialog v-model="detailDialogVisible" title="企业客户详情" width="800px" align-center>
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem label="企业名称">{{ currentDetail.enterpriseName }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="统一社会信用代码">{{ currentDetail.creditCode }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="联系人">{{ currentDetail.contactPerson }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="联系电话">{{ currentDetail.contactPhone }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="联系邮箱">{{ currentDetail.contactEmail }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="企业地址" :span="2">{{ currentDetail.address }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="所属角色">
|
||||
<ElTag>{{ currentDetail.roleName }}</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="登录账号">{{ currentDetail.username }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="卡片数量">
|
||||
<span style="color: var(--el-color-primary)">{{ currentDetail.cardCount }}</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="设备数量">
|
||||
<span style="color: var(--el-color-success)">{{ currentDetail.deviceCount }}</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="账户余额">¥{{ currentDetail.balance.toFixed(2) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="状态">
|
||||
<ElTag v-if="currentDetail.status === 'active'" type="success">正常</ElTag>
|
||||
<ElTag v-else-if="currentDetail.status === 'disabled'" type="danger">禁用</ElTag>
|
||||
<ElTag v-else-if="currentDetail.status === 'pending'" type="warning">待审核</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="创建时间">{{ currentDetail.createTime }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="最后登录">{{ currentDetail.lastLoginTime || '未登录' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="备注" :span="2">{{ currentDetail.remark || '无' }}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="detailDialogVisible = false">关闭</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { View, Plus } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'EnterpriseCustomer' })
|
||||
|
||||
interface EnterpriseCustomer {
|
||||
id: string
|
||||
enterpriseName: string
|
||||
creditCode: string
|
||||
address: string
|
||||
contactPerson: string
|
||||
contactPhone: string
|
||||
contactEmail: string
|
||||
username: string
|
||||
roleId: string
|
||||
roleName: string
|
||||
cardCount: number
|
||||
deviceCount: number
|
||||
balance: number
|
||||
status: 'active' | 'disabled' | 'pending'
|
||||
createTime: string
|
||||
lastLoginTime?: string
|
||||
remark?: string
|
||||
}
|
||||
|
||||
const searchQuery = ref('')
|
||||
const statusFilter = ref('')
|
||||
const roleFilter = ref('')
|
||||
const dialogVisible = ref(false)
|
||||
const detailDialogVisible = ref(false)
|
||||
const dialogType = ref<'add' | 'edit'>('add')
|
||||
const formRef = ref<FormInstance>()
|
||||
const uploadUrl = ref('/api/upload/image')
|
||||
|
||||
const form = reactive({
|
||||
enterpriseName: '',
|
||||
creditCode: '',
|
||||
address: '',
|
||||
businessLicense: '',
|
||||
contactPerson: '',
|
||||
contactPhone: '',
|
||||
contactEmail: '',
|
||||
username: '',
|
||||
password: '',
|
||||
roleId: '',
|
||||
initialBalance: 0,
|
||||
remark: '',
|
||||
status: 'active'
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
enterpriseName: [{ required: true, message: '请输入企业名称', trigger: 'blur' }],
|
||||
creditCode: [
|
||||
{ required: true, message: '请输入统一社会信用代码', trigger: 'blur' },
|
||||
{ len: 18, message: '统一社会信用代码为18位', trigger: 'blur' }
|
||||
],
|
||||
contactPerson: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
|
||||
contactPhone: [
|
||||
{ required: true, message: '请输入联系电话', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
|
||||
],
|
||||
contactEmail: [
|
||||
{ required: true, message: '请输入联系邮箱', trigger: 'blur' },
|
||||
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
|
||||
],
|
||||
username: [{ required: true, message: '请输入登录账号', trigger: 'blur' }],
|
||||
password: [
|
||||
{ required: true, message: '请输入登录密码', trigger: 'blur' },
|
||||
{ min: 6, max: 20, message: '密码长度为6-20位', trigger: 'blur' }
|
||||
],
|
||||
roleId: [{ required: true, message: '请选择客户角色', trigger: 'change' }]
|
||||
})
|
||||
|
||||
const availableRoles = ref([
|
||||
{ id: '1', roleName: '企业管理员' },
|
||||
{ id: '2', roleName: '企业用户' },
|
||||
{ id: '3', roleName: '企业财务' }
|
||||
])
|
||||
|
||||
const mockData = ref<EnterpriseCustomer[]>([
|
||||
{
|
||||
id: '1',
|
||||
enterpriseName: '深圳市科技有限公司',
|
||||
creditCode: '91440300MA5DXXXX01',
|
||||
address: '深圳市南山区科技园',
|
||||
contactPerson: '张经理',
|
||||
contactPhone: '13800138000',
|
||||
contactEmail: 'zhang@company.com',
|
||||
username: 'shenzhen_tech',
|
||||
roleId: '1',
|
||||
roleName: '企业管理员',
|
||||
cardCount: 500,
|
||||
deviceCount: 450,
|
||||
balance: 15000.00,
|
||||
status: 'active',
|
||||
createTime: '2026-01-01 10:00:00',
|
||||
lastLoginTime: '2026-01-09 09:30:00',
|
||||
remark: '重要客户'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
enterpriseName: '北京智能制造有限公司',
|
||||
creditCode: '91110000MA5DXXXX02',
|
||||
address: '北京市海淀区中关村',
|
||||
contactPerson: '李总监',
|
||||
contactPhone: '13900139000',
|
||||
contactEmail: 'li@manufacturing.com',
|
||||
username: 'beijing_smart',
|
||||
roleId: '1',
|
||||
roleName: '企业管理员',
|
||||
cardCount: 800,
|
||||
deviceCount: 750,
|
||||
balance: 28000.00,
|
||||
status: 'active',
|
||||
createTime: '2026-01-03 14:00:00',
|
||||
lastLoginTime: '2026-01-09 08:15:00'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
enterpriseName: '上海物联网科技公司',
|
||||
creditCode: '91310000MA5DXXXX03',
|
||||
address: '上海市浦东新区张江高科技园区',
|
||||
contactPerson: '王主管',
|
||||
contactPhone: '13700137000',
|
||||
contactEmail: 'wang@iot-shanghai.com',
|
||||
username: 'shanghai_iot',
|
||||
roleId: '2',
|
||||
roleName: '企业用户',
|
||||
cardCount: 300,
|
||||
deviceCount: 280,
|
||||
balance: 8500.00,
|
||||
status: 'pending',
|
||||
createTime: '2026-01-08 16:00:00'
|
||||
}
|
||||
])
|
||||
|
||||
const currentDetail = ref<EnterpriseCustomer>({
|
||||
id: '',
|
||||
enterpriseName: '',
|
||||
creditCode: '',
|
||||
address: '',
|
||||
contactPerson: '',
|
||||
contactPhone: '',
|
||||
contactEmail: '',
|
||||
username: '',
|
||||
roleId: '',
|
||||
roleName: '',
|
||||
cardCount: 0,
|
||||
deviceCount: 0,
|
||||
balance: 0,
|
||||
status: 'active',
|
||||
createTime: ''
|
||||
})
|
||||
|
||||
const filteredData = computed(() => {
|
||||
let data = mockData.value
|
||||
|
||||
if (searchQuery.value) {
|
||||
data = data.filter(
|
||||
(item) =>
|
||||
item.enterpriseName.includes(searchQuery.value) ||
|
||||
item.contactPerson.includes(searchQuery.value) ||
|
||||
item.contactPhone.includes(searchQuery.value)
|
||||
)
|
||||
}
|
||||
|
||||
if (statusFilter.value) {
|
||||
data = data.filter((item) => item.status === statusFilter.value)
|
||||
}
|
||||
|
||||
if (roleFilter.value) {
|
||||
data = data.filter((item) => item.roleId === roleFilter.value)
|
||||
}
|
||||
|
||||
return data
|
||||
})
|
||||
|
||||
const handleSearch = () => {}
|
||||
|
||||
const showDialog = (type: 'add' | 'edit', row?: EnterpriseCustomer) => {
|
||||
dialogType.value = type
|
||||
dialogVisible.value = true
|
||||
if (type === 'edit' && row) {
|
||||
Object.assign(form, {
|
||||
enterpriseName: row.enterpriseName,
|
||||
creditCode: row.creditCode,
|
||||
address: row.address,
|
||||
contactPerson: row.contactPerson,
|
||||
contactPhone: row.contactPhone,
|
||||
contactEmail: row.contactEmail,
|
||||
username: row.username,
|
||||
roleId: row.roleId,
|
||||
initialBalance: row.balance,
|
||||
remark: row.remark,
|
||||
status: row.status
|
||||
})
|
||||
} else {
|
||||
Object.assign(form, {
|
||||
enterpriseName: '',
|
||||
creditCode: '',
|
||||
address: '',
|
||||
businessLicense: '',
|
||||
contactPerson: '',
|
||||
contactPhone: '',
|
||||
contactEmail: '',
|
||||
username: '',
|
||||
password: '',
|
||||
roleId: '',
|
||||
initialBalance: 0,
|
||||
remark: '',
|
||||
status: 'active'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
if (dialogType.value === 'add') {
|
||||
const selectedRole = availableRoles.value.find((r) => r.id === form.roleId)
|
||||
mockData.value.unshift({
|
||||
id: Date.now().toString(),
|
||||
enterpriseName: form.enterpriseName,
|
||||
creditCode: form.creditCode,
|
||||
address: form.address,
|
||||
contactPerson: form.contactPerson,
|
||||
contactPhone: form.contactPhone,
|
||||
contactEmail: form.contactEmail,
|
||||
username: form.username,
|
||||
roleId: form.roleId,
|
||||
roleName: selectedRole?.roleName || '',
|
||||
cardCount: 0,
|
||||
deviceCount: 0,
|
||||
balance: form.initialBalance,
|
||||
status: form.status as any,
|
||||
createTime: new Date().toLocaleString('zh-CN'),
|
||||
remark: form.remark
|
||||
})
|
||||
ElMessage.success('企业客户创建成功')
|
||||
} else {
|
||||
ElMessage.success('企业客户更新成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
formEl.resetFields()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const viewDetail = (row: EnterpriseCustomer) => {
|
||||
currentDetail.value = { ...row }
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
const toggleStatus = (row: EnterpriseCustomer) => {
|
||||
const action = row.status === 'active' ? '禁用' : '启用'
|
||||
ElMessageBox.confirm(`确定${action}该企业客户吗?`, `${action}确认`, {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
row.status = row.status === 'active' ? 'disabled' : 'active'
|
||||
ElMessage.success(`${action}成功`)
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = (row: EnterpriseCustomer) => {
|
||||
ElMessageBox.confirm('删除后无法恢复,确定要删除该企业客户吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(() => {
|
||||
const index = mockData.value.findIndex((item) => item.id === row.id)
|
||||
if (index !== -1) mockData.value.splice(index, 1)
|
||||
ElMessage.success('删除成功')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
:deep(.el-upload-list--picture-card .el-upload-list__item) {
|
||||
width: 148px;
|
||||
height: 148px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
669
src/views/account-management/platform-account/index.vue
Normal file
669
src/views/account-management/platform-account/index.vue
Normal file
@@ -0,0 +1,669 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="platform-account-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
:show-expand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton @click="showDialog('add')">新增平台账号</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="ID"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增平台账号' : '编辑平台账号'"
|
||||
width="500px"
|
||||
>
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<ElFormItem label="账号名称" prop="username">
|
||||
<ElInput
|
||||
v-model="formData.username"
|
||||
placeholder="请输入账号名称"
|
||||
:disabled="dialogType === 'edit'"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="dialogType === 'add'" label="密码" prop="password">
|
||||
<ElInput
|
||||
v-model="formData.password"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
show-password
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="手机号" prop="phone">
|
||||
<ElInput v-model="formData.phone" placeholder="请输入手机号" maxlength="11" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="账号类型" prop="user_type">
|
||||
<ElSelect
|
||||
v-model="formData.user_type"
|
||||
placeholder="请选择账号类型"
|
||||
style="width: 100%"
|
||||
:disabled="dialogType === 'edit'"
|
||||
>
|
||||
<ElOption label="超级管理员" :value="1" />
|
||||
<ElOption label="平台用户" :value="2" />
|
||||
<ElOption label="代理账号" :value="3" />
|
||||
<ElOption label="企业账号" :value="4" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="formData.user_type === 3" label="关联店铺ID" prop="shop_id">
|
||||
<ElInputNumber
|
||||
v-model="formData.shop_id"
|
||||
:min="1"
|
||||
placeholder="请输入店铺ID"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="formData.user_type === 4" label="关联企业ID" prop="enterprise_id">
|
||||
<ElInputNumber
|
||||
v-model="formData.enterprise_id"
|
||||
:min="1"
|
||||
placeholder="请输入企业ID"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="dialogType === 'edit'" label="状态">
|
||||
<ElSwitch
|
||||
v-model="formData.status"
|
||||
:active-value="CommonStatus.ENABLED"
|
||||
:inactive-value="CommonStatus.DISABLED"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading"
|
||||
>提交</ElButton
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 分配角色对话框 -->
|
||||
<ElDialog v-model="roleDialogVisible" title="分配角色" width="500px">
|
||||
<ElCheckboxGroup v-model="selectedRoles">
|
||||
<div v-for="role in allRoles" :key="role.ID" style="margin-bottom: 12px">
|
||||
<ElCheckbox :label="role.ID">{{ role.role_name }}</ElCheckbox>
|
||||
</div>
|
||||
</ElCheckboxGroup>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="roleDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleAssignRoles" :loading="roleSubmitLoading"
|
||||
>提交</ElButton
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 修改密码对话框 -->
|
||||
<ElDialog v-model="passwordDialogVisible" title="修改密码" width="400px">
|
||||
<ElForm ref="passwordFormRef" :model="passwordForm" :rules="passwordRules">
|
||||
<ElFormItem label="新密码" prop="new_password">
|
||||
<ElInput
|
||||
v-model="passwordForm.new_password"
|
||||
type="password"
|
||||
placeholder="请输入新密码"
|
||||
show-password
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="passwordDialogVisible = false">取消</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="handleChangePassword"
|
||||
:loading="passwordSubmitLoading"
|
||||
>提交</ElButton
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { FormInstance, ElMessage, ElMessageBox, ElTag, ElSwitch } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { PlatformAccountService, RoleService } from '@/api/modules'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import type { PlatformRole, PlatformAccountResponse } from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants'
|
||||
|
||||
defineOptions({ name: 'PlatformAccount' }) // 定义组件名称,用于 KeepAlive 缓存控制
|
||||
|
||||
const dialogType = ref('add')
|
||||
const dialogVisible = ref(false)
|
||||
const roleDialogVisible = ref(false)
|
||||
const passwordDialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const roleSubmitLoading = ref(false)
|
||||
const passwordSubmitLoading = ref(false)
|
||||
const currentAccountId = ref<number>(0)
|
||||
const selectedRoles = ref<number[]>([])
|
||||
const allRoles = ref<PlatformRole[]>([])
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
username: '',
|
||||
phone: '',
|
||||
status: undefined as number | undefined
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const searchForm = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<PlatformAccountResponse[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, { ...initialSearchState })
|
||||
pagination.currentPage = 1 // 重置到第一页
|
||||
getAccountList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', searchForm)
|
||||
pagination.currentPage = 1 // 搜索时重置到第一页
|
||||
getAccountList()
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const searchFormItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '账号名称',
|
||||
prop: 'username',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入账号名称'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '手机号',
|
||||
prop: 'phone',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入手机号'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
type: 'select',
|
||||
options: STATUS_SELECT_OPTIONS,
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请选择状态'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: 'ID', prop: 'ID' },
|
||||
{ label: '账号名称', prop: 'username' },
|
||||
{ label: '手机号', prop: 'phone' },
|
||||
{ label: '账号类型', prop: 'user_type' },
|
||||
{ label: '状态', prop: 'status' },
|
||||
{ label: '创建时间', prop: 'CreatedAt' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 显示对话框
|
||||
const showDialog = (type: string, row?: PlatformAccountResponse) => {
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
|
||||
// 重置表单验证状态
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
|
||||
if (type === 'edit' && row) {
|
||||
formData.id = row.ID
|
||||
formData.username = row.username
|
||||
formData.phone = row.phone
|
||||
formData.user_type = row.user_type
|
||||
formData.enterprise_id = row.enterprise_id || null
|
||||
formData.shop_id = row.shop_id || null
|
||||
formData.status = row.status
|
||||
formData.password = ''
|
||||
} else {
|
||||
formData.id = 0
|
||||
formData.username = ''
|
||||
formData.phone = ''
|
||||
formData.user_type = 2
|
||||
formData.enterprise_id = null
|
||||
formData.shop_id = null
|
||||
formData.status = CommonStatus.ENABLED
|
||||
formData.password = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 删除账号
|
||||
const deleteAccount = (row: PlatformAccountResponse) => {
|
||||
ElMessageBox.confirm(`确定要删除平台账号 ${row.username} 吗?`, '删除平台账号', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
await PlatformAccountService.deletePlatformAccount(row.ID)
|
||||
ElMessage.success('删除成功')
|
||||
getAccountList()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// 账号取消删除
|
||||
})
|
||||
}
|
||||
|
||||
// 显示修改密码对话框
|
||||
const showPasswordDialog = (row: PlatformAccountResponse) => {
|
||||
currentAccountId.value = row.ID
|
||||
passwordForm.new_password = ''
|
||||
passwordDialogVisible.value = true
|
||||
if (passwordFormRef.value) {
|
||||
passwordFormRef.value.resetFields()
|
||||
}
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'ID',
|
||||
label: 'ID',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
prop: 'username',
|
||||
label: '账号名称',
|
||||
minWidth: 120
|
||||
},
|
||||
{
|
||||
prop: 'phone',
|
||||
label: '手机号',
|
||||
width: 130
|
||||
},
|
||||
{
|
||||
prop: 'user_type',
|
||||
label: '账号类型',
|
||||
width: 120,
|
||||
formatter: (row: PlatformAccountResponse) => {
|
||||
const typeMap: Record<number, string> = {
|
||||
1: '超级管理员',
|
||||
2: '平台用户',
|
||||
3: '代理账号',
|
||||
4: '企业账号'
|
||||
}
|
||||
return typeMap[row.user_type] || '-'
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 100,
|
||||
formatter: (row: PlatformAccountResponse) => {
|
||||
return h(ElSwitch, {
|
||||
modelValue: row.status,
|
||||
activeValue: CommonStatus.ENABLED,
|
||||
inactiveValue: CommonStatus.DISABLED,
|
||||
activeText: getStatusText(CommonStatus.ENABLED),
|
||||
inactiveText: getStatusText(CommonStatus.DISABLED),
|
||||
inlinePrompt: true,
|
||||
'onUpdate:modelValue': (val: number) => handleStatusChange(row, val)
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'CreatedAt',
|
||||
label: '创建时间',
|
||||
width: 180,
|
||||
formatter: (row: PlatformAccountResponse) => formatDateTime(row.CreatedAt)
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 240,
|
||||
fixed: 'right',
|
||||
formatter: (row: PlatformAccountResponse) => {
|
||||
return h('div', { style: 'display: flex; gap: 8px;' }, [
|
||||
h(ArtButtonTable, {
|
||||
icon: '',
|
||||
onClick: () => showRoleDialog(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
icon: '',
|
||||
onClick: () => showPasswordDialog(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
type: 'edit',
|
||||
onClick: () => showDialog('edit', row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
type: 'delete',
|
||||
onClick: () => deleteAccount(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// 表单实例
|
||||
const formRef = ref<FormInstance>()
|
||||
const passwordFormRef = ref<FormInstance>()
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
id: 0,
|
||||
username: '',
|
||||
password: '',
|
||||
phone: '',
|
||||
user_type: 2,
|
||||
enterprise_id: null as number | null,
|
||||
shop_id: null as number | null,
|
||||
status: CommonStatus.ENABLED
|
||||
})
|
||||
|
||||
// 密码表单数据
|
||||
const passwordForm = reactive({
|
||||
new_password: ''
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getAccountList()
|
||||
loadAllRoles()
|
||||
})
|
||||
|
||||
// 加载所有角色列表
|
||||
const loadAllRoles = async () => {
|
||||
try {
|
||||
const res = await RoleService.getRoles({ page: 1, pageSize: 100 })
|
||||
if (res.code === 0) {
|
||||
allRoles.value = res.data.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取角色列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 显示分配角色对话框
|
||||
const showRoleDialog = async (row: PlatformAccountResponse) => {
|
||||
currentAccountId.value = row.ID
|
||||
selectedRoles.value = []
|
||||
|
||||
// 先加载当前账号的角色,再打开对话框
|
||||
try {
|
||||
const res = await PlatformAccountService.getPlatformAccountRoles(row.ID)
|
||||
if (res.code === 0) {
|
||||
// 提取角色ID数组
|
||||
const roles = res.data || []
|
||||
selectedRoles.value = roles.map((role: any) => role.ID)
|
||||
// 数据加载完成后再打开对话框
|
||||
roleDialogVisible.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取账号角色失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 提交分配角色
|
||||
const handleAssignRoles = async () => {
|
||||
roleSubmitLoading.value = true
|
||||
try {
|
||||
await PlatformAccountService.assignRolesToPlatformAccount(currentAccountId.value, {
|
||||
role_ids: selectedRoles.value
|
||||
})
|
||||
ElMessage.success('分配角色成功')
|
||||
roleDialogVisible.value = false
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
roleSubmitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 提交修改密码
|
||||
const handleChangePassword = async () => {
|
||||
if (!passwordFormRef.value) return
|
||||
|
||||
await passwordFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
passwordSubmitLoading.value = true
|
||||
try {
|
||||
await PlatformAccountService.changePlatformAccountPassword(currentAccountId.value, {
|
||||
new_password: passwordForm.new_password
|
||||
})
|
||||
ElMessage.success('修改密码成功')
|
||||
passwordDialogVisible.value = false
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
passwordSubmitLoading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取账号列表
|
||||
const getAccountList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.currentPage,
|
||||
page_size: pagination.pageSize,
|
||||
username: searchForm.username || undefined,
|
||||
phone: searchForm.phone || undefined,
|
||||
status: searchForm.status
|
||||
}
|
||||
const res = await PlatformAccountService.getPlatformAccounts(params)
|
||||
if (res.code === 0) {
|
||||
tableData.value = res.data.items || []
|
||||
pagination.total = res.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取平台账号列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getAccountList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
username: [
|
||||
{ required: true, message: '请输入账号名称', trigger: 'blur' },
|
||||
{ min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 8, max: 32, message: '密码长度为 8-32 个字符', trigger: 'blur' }
|
||||
],
|
||||
phone: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ len: 11, message: '手机号必须为 11 位', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
|
||||
],
|
||||
user_type: [{ required: true, message: '请选择账号类型', trigger: 'change' }],
|
||||
shop_id: [
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
if (formData.user_type === 3 && !value) {
|
||||
callback(new Error('代理账号必须关联店铺ID'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
enterprise_id: [
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
if (formData.user_type === 4 && !value) {
|
||||
callback(new Error('企业账号必须关联企业ID'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 密码验证规则
|
||||
const passwordRules = reactive<FormRules>({
|
||||
new_password: [
|
||||
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||||
{ min: 8, max: 32, message: '密码长度为 8-32 个字符', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true
|
||||
try {
|
||||
if (dialogType.value === 'add') {
|
||||
const data: any = {
|
||||
username: formData.username,
|
||||
password: formData.password,
|
||||
phone: formData.phone,
|
||||
user_type: formData.user_type
|
||||
}
|
||||
|
||||
// 根据账号类型添加相应的字段
|
||||
if (formData.user_type === 3) {
|
||||
data.shop_id = formData.shop_id
|
||||
} else if (formData.user_type === 4) {
|
||||
data.enterprise_id = formData.enterprise_id
|
||||
}
|
||||
|
||||
await PlatformAccountService.createPlatformAccount(data)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
const data: any = {
|
||||
username: formData.username,
|
||||
phone: formData.phone,
|
||||
status: formData.status
|
||||
}
|
||||
|
||||
await PlatformAccountService.updatePlatformAccount(formData.id, data)
|
||||
ElMessage.success('更新成功')
|
||||
}
|
||||
|
||||
dialogVisible.value = false
|
||||
getAccountList()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getAccountList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getAccountList()
|
||||
}
|
||||
|
||||
// 状态切换
|
||||
const handleStatusChange = async (row: any, newStatus: number) => {
|
||||
const oldStatus = row.status
|
||||
// 先更新UI
|
||||
row.status = newStatus
|
||||
try {
|
||||
await PlatformAccountService.updatePlatformAccount(row.ID, { status: newStatus })
|
||||
ElMessage.success('状态切换成功')
|
||||
} catch (error) {
|
||||
// 切换失败,恢复原状态
|
||||
row.status = oldStatus
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.platform-account-page {
|
||||
// 平台账号管理页面样式
|
||||
}
|
||||
</style>
|
||||
482
src/views/account-management/shop-account/index.vue
Normal file
482
src/views/account-management/shop-account/index.vue
Normal file
@@ -0,0 +1,482 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="shop-account-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton @click="showDialog('add')">新增代理账号</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增代理账号' : '编辑代理账号'"
|
||||
width="500px"
|
||||
>
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<ElFormItem label="用户名" prop="username">
|
||||
<ElInput v-model="formData.username" placeholder="请输入用户名" />
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="dialogType === 'add'" label="密码" prop="password">
|
||||
<ElInput v-model="formData.password" type="password" placeholder="请输入密码" show-password />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="手机号" prop="phone">
|
||||
<ElInput v-model="formData.phone" placeholder="请输入手机号" maxlength="11" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="店铺ID" prop="shop_id">
|
||||
<ElInputNumber v-model="formData.shop_id" :min="1" placeholder="请输入店铺ID" style="width: 100%" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 修改密码对话框 -->
|
||||
<ElDialog
|
||||
v-model="passwordDialogVisible"
|
||||
title="重置密码"
|
||||
width="400px"
|
||||
>
|
||||
<ElForm ref="passwordFormRef" :model="passwordForm" :rules="passwordRules">
|
||||
<ElFormItem label="新密码" prop="new_password">
|
||||
<ElInput v-model="passwordForm.new_password" type="password" placeholder="请输入新密码" show-password />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="passwordDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleChangePassword" :loading="passwordSubmitLoading">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { FormInstance, ElMessage, ElMessageBox, ElSwitch } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { ShopAccountService } from '@/api/modules'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import type { ShopAccountResponse } from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants'
|
||||
|
||||
defineOptions({ name: 'ShopAccount' }) // 定义组件名称,用于 KeepAlive 缓存控制
|
||||
|
||||
const dialogType = ref('add')
|
||||
const dialogVisible = ref(false)
|
||||
const passwordDialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const passwordSubmitLoading = ref(false)
|
||||
const currentAccountId = ref<number>(0)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
username: '',
|
||||
phone: '',
|
||||
shop_id: undefined as number | undefined,
|
||||
status: undefined as number | undefined
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const searchForm = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<ShopAccountResponse[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, { ...initialSearchState })
|
||||
pagination.currentPage = 1 // 重置到第一页
|
||||
getAccountList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', searchForm)
|
||||
pagination.currentPage = 1 // 搜索时重置到第一页
|
||||
getAccountList()
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const searchFormItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '用户名',
|
||||
prop: 'username',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入用户名'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '手机号',
|
||||
prop: 'phone',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入手机号'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '店铺ID',
|
||||
prop: 'shop_id',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入店铺ID'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
type: 'select',
|
||||
options: STATUS_SELECT_OPTIONS,
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请选择状态'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: 'ID', prop: 'id' },
|
||||
{ label: '用户名', prop: 'username' },
|
||||
{ label: '手机号', prop: 'phone' },
|
||||
{ label: '店铺ID', prop: 'shop_id' },
|
||||
{ label: '状态', prop: 'status' },
|
||||
{ label: '创建时间', prop: 'created_at' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 显示对话框
|
||||
const showDialog = (type: string, row?: ShopAccountResponse) => {
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
|
||||
// 重置表单验证状态
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
|
||||
if (type === 'edit' && row) {
|
||||
formData.id = row.id
|
||||
formData.username = row.username
|
||||
formData.phone = row.phone
|
||||
formData.shop_id = row.shop_id
|
||||
formData.password = ''
|
||||
} else {
|
||||
formData.id = 0
|
||||
formData.username = ''
|
||||
formData.phone = ''
|
||||
formData.shop_id = 0
|
||||
formData.password = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 显示修改密码对话框
|
||||
const showPasswordDialog = (row: ShopAccountResponse) => {
|
||||
currentAccountId.value = row.id
|
||||
passwordForm.new_password = ''
|
||||
passwordDialogVisible.value = true
|
||||
if (passwordFormRef.value) {
|
||||
passwordFormRef.value.resetFields()
|
||||
}
|
||||
}
|
||||
|
||||
// 状态切换处理
|
||||
const handleStatusChange = async (row: ShopAccountResponse, newStatus: number) => {
|
||||
const oldStatus = row.status
|
||||
// 先更新UI
|
||||
row.status = newStatus
|
||||
try {
|
||||
await ShopAccountService.updateShopAccountStatus(row.id, { status: newStatus })
|
||||
ElMessage.success('状态切换成功')
|
||||
} catch (error) {
|
||||
// 切换失败,恢复原状态
|
||||
row.status = oldStatus
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'id',
|
||||
label: 'ID',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
prop: 'username',
|
||||
label: '用户名',
|
||||
minWidth: 120
|
||||
},
|
||||
{
|
||||
prop: 'phone',
|
||||
label: '手机号',
|
||||
width: 130
|
||||
},
|
||||
{
|
||||
prop: 'shop_id',
|
||||
label: '店铺ID',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 100,
|
||||
formatter: (row: ShopAccountResponse) => {
|
||||
return h(ElSwitch, {
|
||||
modelValue: row.status,
|
||||
activeValue: CommonStatus.ENABLED,
|
||||
inactiveValue: CommonStatus.DISABLED,
|
||||
activeText: getStatusText(CommonStatus.ENABLED),
|
||||
inactiveText: getStatusText(CommonStatus.DISABLED),
|
||||
inlinePrompt: true,
|
||||
'onUpdate:modelValue': (val: number) => handleStatusChange(row, val)
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
width: 180,
|
||||
formatter: (row: ShopAccountResponse) => formatDateTime(row.created_at)
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 120,
|
||||
fixed: 'right',
|
||||
formatter: (row: ShopAccountResponse) => {
|
||||
return h('div', { style: 'display: flex; gap: 8px;' }, [
|
||||
h(ArtButtonTable, {
|
||||
icon: '',
|
||||
onClick: () => showPasswordDialog(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
type: 'edit',
|
||||
onClick: () => showDialog('edit', row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// 表单实例
|
||||
const formRef = ref<FormInstance>()
|
||||
const passwordFormRef = ref<FormInstance>()
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
id: 0,
|
||||
username: '',
|
||||
password: '',
|
||||
phone: '',
|
||||
shop_id: 0
|
||||
})
|
||||
|
||||
// 密码表单数据
|
||||
const passwordForm = reactive({
|
||||
new_password: ''
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getAccountList()
|
||||
})
|
||||
|
||||
// 提交修改密码
|
||||
const handleChangePassword = async () => {
|
||||
if (!passwordFormRef.value) return
|
||||
|
||||
await passwordFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
passwordSubmitLoading.value = true
|
||||
try {
|
||||
await ShopAccountService.updateShopAccountPassword(currentAccountId.value, {
|
||||
new_password: passwordForm.new_password
|
||||
})
|
||||
ElMessage.success('重置密码成功')
|
||||
passwordDialogVisible.value = false
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
passwordSubmitLoading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取账号列表
|
||||
const getAccountList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.currentPage,
|
||||
page_size: pagination.pageSize,
|
||||
username: searchForm.username || undefined,
|
||||
phone: searchForm.phone || undefined,
|
||||
shop_id: searchForm.shop_id,
|
||||
status: searchForm.status
|
||||
}
|
||||
const res = await ShopAccountService.getShopAccounts(params)
|
||||
if (res.code === 0) {
|
||||
tableData.value = res.data.items || []
|
||||
pagination.total = res.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取代理账号列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getAccountList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 8, max: 32, message: '密码长度为 8-32 个字符', trigger: 'blur' }
|
||||
],
|
||||
phone: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ len: 11, message: '手机号必须为 11 位', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
|
||||
],
|
||||
shop_id: [
|
||||
{ required: true, message: '请输入店铺ID', trigger: 'blur' },
|
||||
{ type: 'number', min: 1, message: '店铺ID必须大于0', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
// 密码验证规则
|
||||
const passwordRules = reactive<FormRules>({
|
||||
new_password: [
|
||||
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||||
{ min: 8, max: 32, message: '密码长度为 8-32 个字符', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true
|
||||
try {
|
||||
if (dialogType.value === 'add') {
|
||||
const data = {
|
||||
username: formData.username,
|
||||
password: formData.password,
|
||||
phone: formData.phone,
|
||||
shop_id: formData.shop_id
|
||||
}
|
||||
|
||||
await ShopAccountService.createShopAccount(data)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
const data = {
|
||||
username: formData.username
|
||||
}
|
||||
|
||||
await ShopAccountService.updateShopAccount(formData.id, data)
|
||||
ElMessage.success('更新成功')
|
||||
}
|
||||
|
||||
dialogVisible.value = false
|
||||
getAccountList()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getAccountList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getAccountList()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.shop-account-page {
|
||||
// 代理账号管理页面样式
|
||||
}
|
||||
</style>
|
||||
269
src/views/article/comment/index.vue
Normal file
269
src/views/article/comment/index.vue
Normal file
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<h1 class="title">留言墙</h1>
|
||||
<p class="desc">每一份留言都记录了您的想法,也为我们提供了珍贵的回忆</p>
|
||||
|
||||
<div class="list">
|
||||
<ul class="offset">
|
||||
<li
|
||||
class="comment-box"
|
||||
v-for="item in commentList"
|
||||
:key="item.id"
|
||||
:style="{ background: randomColor() }"
|
||||
@click="openDrawer(item)"
|
||||
>
|
||||
<p class="date">{{ item.date }}</p>
|
||||
<p class="content">{{ item.content }}</p>
|
||||
<div class="bottom">
|
||||
<div class="left">
|
||||
<span><i class="iconfont-sys"></i>{{ item.collection }}</span>
|
||||
<span><i class="iconfont-sys"></i>{{ item.comment }}</span>
|
||||
</div>
|
||||
<div class="right">
|
||||
<span>{{ item.userName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<ElDrawer
|
||||
lDrawer
|
||||
v-model="showDrawer"
|
||||
:lock-scroll="false"
|
||||
:size="360"
|
||||
modal-class="comment-modal"
|
||||
>
|
||||
<template #header>
|
||||
<h4>详情</h4>
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="drawer-default">
|
||||
<div class="comment-box" :style="{ background: randomColor() }">
|
||||
<p class="date">{{ clickItem.date }}</p>
|
||||
<p class="content">{{ clickItem.content }}</p>
|
||||
<div class="bottom">
|
||||
<div class="left">
|
||||
<span><i class="iconfont-sys"></i>{{ clickItem.collection }}</span>
|
||||
<span><i class="iconfont-sys"></i>{{ clickItem.comment }}</span>
|
||||
</div>
|
||||
<div class="right">
|
||||
<span>{{ clickItem.userName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 评论组件 -->
|
||||
<CommentWidget />
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div>
|
||||
<!-- <ElButton @click="cancelClick">cancel</ElButton> -->
|
||||
<!-- <ElButton type="primary" @click="confirmClick">confirm</ElButton> -->
|
||||
</div>
|
||||
</template>
|
||||
</ElDrawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { commentList } from '@/mock/temp/commentList'
|
||||
const showDrawer = ref(false)
|
||||
|
||||
defineOptions({ name: 'ArticleComment' })
|
||||
|
||||
// const colorList = reactive([
|
||||
// 'rgba(216, 248, 255, 0.8)',
|
||||
// 'rgba(253, 223, 217, 0.8)',
|
||||
// 'rgba(252, 230, 240, 0.8)',
|
||||
// 'rgba(211, 248, 240, 0.8)',
|
||||
// 'rgba(255, 234, 188, 0.8)',
|
||||
// 'rgba(245, 225, 255, 0.8)',
|
||||
// 'rgba(225, 230, 254, 0.8)'
|
||||
// ])
|
||||
|
||||
const colorList = reactive([
|
||||
'#D8F8FF',
|
||||
'#FDDFD9',
|
||||
'#FCE6F0',
|
||||
'#D3F8F0',
|
||||
'#FFEABC',
|
||||
'#F5E1FF',
|
||||
'#E1E6FE'
|
||||
])
|
||||
|
||||
let lastColor: string | null = null
|
||||
|
||||
const randomColor = () => {
|
||||
let newColor: string
|
||||
|
||||
do {
|
||||
const index = Math.floor(Math.random() * colorList.length)
|
||||
newColor = colorList[index]
|
||||
} while (newColor === lastColor)
|
||||
|
||||
lastColor = newColor
|
||||
return newColor
|
||||
}
|
||||
|
||||
const clickItem = ref({
|
||||
id: 1,
|
||||
date: '2024-9-3',
|
||||
content: '加油!学好Node 自己写个小Demo',
|
||||
collection: 5,
|
||||
comment: 8,
|
||||
userName: '匿名'
|
||||
})
|
||||
|
||||
const openDrawer = (item: any) => {
|
||||
showDrawer.value = true
|
||||
clickItem.value = item
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
background-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
:deep(.comment-modal) {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-top: 20px;
|
||||
font-size: 36px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin-top: 15px;
|
||||
font-size: 14px;
|
||||
color: var(--art-text-gray-600);
|
||||
}
|
||||
|
||||
.list {
|
||||
margin-top: 40px;
|
||||
|
||||
.offset {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: calc(100% + 16px);
|
||||
}
|
||||
}
|
||||
|
||||
.comment-box {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
width: calc(20% - 16px);
|
||||
aspect-ratio: 16 / 12;
|
||||
padding: 16px;
|
||||
margin: 0 16px 16px 0;
|
||||
cursor: pointer;
|
||||
background-color: #eae2cb;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 12px;
|
||||
color: #949494;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
left: 0;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 0 16px;
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 20px;
|
||||
font-size: 12px;
|
||||
color: #949494;
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
span {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-default {
|
||||
.comment-box {
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $device-notebook) {
|
||||
.page-content {
|
||||
.comment-box {
|
||||
width: calc(25% - 16px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $device-ipad-pro) {
|
||||
.page-content {
|
||||
.comment-box {
|
||||
width: calc(33.333% - 16px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $device-ipad) {
|
||||
.page-content {
|
||||
.comment-box {
|
||||
width: calc(50% - 16px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $device-phone) {
|
||||
.page-content {
|
||||
.comment-box {
|
||||
width: calc(100% - 16px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.page-content {
|
||||
.comment-box {
|
||||
color: #333 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
122
src/views/article/detail/index.vue
Normal file
122
src/views/article/detail/index.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div class="article-detail page-content">
|
||||
<div class="content">
|
||||
<h1>{{ articleTitle }}</h1>
|
||||
<div class="markdown-body" v-highlight v-html="articleHtml"></div>
|
||||
</div>
|
||||
<ArtBackToTop />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import '@/assets/styles/markdown.scss'
|
||||
import '@/assets/styles/one-dark-pro.scss'
|
||||
import { useCommon } from '@/composables/useCommon'
|
||||
import axios from 'axios'
|
||||
// import 'highlight.js/styles/atom-one-dark.css';
|
||||
// import 'highlight.js/styles/vs2015.css';
|
||||
|
||||
defineOptions({ name: 'ArticleDetail' })
|
||||
|
||||
const articleId = ref(0)
|
||||
const router = useRoute()
|
||||
const articleTitle = ref('')
|
||||
const articleHtml = ref('')
|
||||
|
||||
onMounted(() => {
|
||||
useCommon().scrollToTop()
|
||||
articleId.value = Number(router.query.id)
|
||||
getArticleDetail()
|
||||
})
|
||||
|
||||
const getArticleDetail = async () => {
|
||||
if (articleId.value) {
|
||||
const res = await axios.get('https://www.qiniu.lingchen.kim/blog_detail.json')
|
||||
if (res.data.code === 200) {
|
||||
articleTitle.value = res.data.data.title
|
||||
articleHtml.value = res.data.data.html_content
|
||||
}
|
||||
|
||||
// const res = await ArticleService.getArticleDetail(articleId.value)
|
||||
// if (res.code === ApiStatus.success) {
|
||||
// articleTitle.value = res.data.title;;
|
||||
// articleHtml.value = res.data.html_content;
|
||||
// }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.article-detail {
|
||||
.content {
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
margin-top: 60px;
|
||||
|
||||
.markdown-body {
|
||||
margin-top: 60px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
border: 1px solid var(--art-gray-200);
|
||||
}
|
||||
|
||||
pre {
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
.copy-button {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50px;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
content: '';
|
||||
background: #0a0a0e;
|
||||
}
|
||||
}
|
||||
|
||||
.code-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.line-number {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
margin-right: 10px;
|
||||
font-size: 14px;
|
||||
color: #9e9e9e;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
z-index: 1;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 20px;
|
||||
line-height: 40px;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
background-color: #000;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
opacity: 0;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
381
src/views/article/list/index.vue
Normal file
381
src/views/article/list/index.vue
Normal file
@@ -0,0 +1,381 @@
|
||||
<template>
|
||||
<div class="page-content article-list">
|
||||
<ElRow justify="space-between" :gutter="10">
|
||||
<ElCol :lg="6" :md="6" :sm="14" :xs="16">
|
||||
<ElInput
|
||||
v-model="searchVal"
|
||||
:prefix-icon="Search"
|
||||
clearable
|
||||
placeholder="输入文章标题查询"
|
||||
@keyup.enter="searchArticle"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :lg="12" :md="12" :sm="0" :xs="0">
|
||||
<div class="custom-segmented">
|
||||
<ElSegmented v-model="yearVal" :options="options" @change="searchArticleByYear" />
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :lg="6" :md="6" :sm="10" :xs="6" style="display: flex; justify-content: end">
|
||||
<ElButton @click="toAddArticle" v-auth="'add'">新增文章</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<div class="list">
|
||||
<div class="offset">
|
||||
<div class="item" v-for="item in articleList" :key="item.id" @click="toDetail(item)">
|
||||
<!-- 骨架屏 -->
|
||||
<ElSkeleton animated :loading="isLoading" style="width: 100%; height: 100%">
|
||||
<template #template>
|
||||
<div class="top">
|
||||
<ElSkeletonItem
|
||||
variant="image"
|
||||
style="width: 100%; height: 100%; border-radius: 10px"
|
||||
/>
|
||||
<div style="padding: 16px 0">
|
||||
<ElSkeletonItem variant="p" style="width: 80%" />
|
||||
<ElSkeletonItem variant="p" style="width: 40%; margin-top: 10px" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<div class="top">
|
||||
<ElImage class="cover" :src="item.home_img" lazy fit="cover">
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<ElIcon><icon-picture /></ElIcon>
|
||||
</div>
|
||||
</template>
|
||||
</ElImage>
|
||||
|
||||
<span class="type">{{ item.type_name }}</span>
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<h2>{{ item.title }}</h2>
|
||||
<div class="info">
|
||||
<div class="text">
|
||||
<i class="iconfont-sys"></i>
|
||||
<span>{{ useDateFormat(item.create_time, 'YYYY-MM-DD') }}</span>
|
||||
<div class="line"></div>
|
||||
<i class="iconfont-sys"></i>
|
||||
<span>{{ item.count }}</span>
|
||||
</div>
|
||||
<ElButton v-auth="'edit'" size="small" @click.stop="toEdit(item)">编辑</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ElSkeleton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 16vh" v-if="showEmpty">
|
||||
<ElEmpty :description="`未找到相关数据 ${EmojiText[0]}`" />
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: center; margin-top: 20px">
|
||||
<ElPagination
|
||||
size="default"
|
||||
background
|
||||
v-model:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:pager-count="9"
|
||||
layout="prev, pager, next, total,jumper"
|
||||
:total="total"
|
||||
:hide-on-single-page="true"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Picture as IconPicture } from '@element-plus/icons-vue'
|
||||
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { router } from '@/router'
|
||||
import { useDateFormat } from '@vueuse/core'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import EmojiText from '@/utils/ui/emojo'
|
||||
import { ArticleList } from '@/mock/temp/articleList'
|
||||
import { useCommon } from '@/composables/useCommon'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { ArticleType } from '@/api/modules'
|
||||
|
||||
defineOptions({ name: 'ArticleList' })
|
||||
|
||||
const yearVal = ref('All')
|
||||
|
||||
const options = ['All', '2024', '2023', '2022', '2021', '2020', '2019']
|
||||
|
||||
const searchVal = ref('')
|
||||
const articleList = ref<ArticleType[]>([])
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(40)
|
||||
// const lastPage = ref(0)
|
||||
const total = ref(0)
|
||||
const isLoading = ref(true)
|
||||
|
||||
const showEmpty = computed(() => {
|
||||
return articleList.value.length === 0 && !isLoading.value
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getArticleList({ backTop: false })
|
||||
})
|
||||
|
||||
// 搜索文章
|
||||
const searchArticle = () => {
|
||||
getArticleList({ backTop: true })
|
||||
}
|
||||
|
||||
// 根据年份查询文章
|
||||
const searchArticleByYear = () => {
|
||||
getArticleList({ backTop: true })
|
||||
}
|
||||
|
||||
const getArticleList = async ({ backTop = false }) => {
|
||||
isLoading.value = true
|
||||
// let year = yearVal.value
|
||||
|
||||
if (searchVal.value) {
|
||||
yearVal.value = 'All'
|
||||
}
|
||||
|
||||
if (yearVal.value === 'All') {
|
||||
// year = ''
|
||||
}
|
||||
|
||||
// const params = {
|
||||
// page: currentPage.value,
|
||||
// size: pageSize.value,
|
||||
// searchVal: searchVal.value,
|
||||
// year
|
||||
// }
|
||||
|
||||
articleList.value = ArticleList
|
||||
isLoading.value = false
|
||||
|
||||
if (backTop) {
|
||||
useCommon().scrollToTop()
|
||||
}
|
||||
|
||||
// const res = await ArticleService.getArticleList(params)
|
||||
// if (res.code === ApiStatus.success) {
|
||||
// currentPage.value = res.currentPage
|
||||
// pageSize.value = res.pageSize
|
||||
// lastPage.value = res.lastPage
|
||||
// total.value = res.total
|
||||
// articleList.value = res.data
|
||||
|
||||
// // setTimeout(() => {
|
||||
// isLoading.value = false
|
||||
// // }, 3000)
|
||||
|
||||
// if (searchVal.value) {
|
||||
// searchVal.value = ''
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val: number) => {
|
||||
currentPage.value = val
|
||||
getArticleList({ backTop: true })
|
||||
}
|
||||
|
||||
const toDetail = (item: ArticleType) => {
|
||||
router.push({
|
||||
path: RoutesAlias.ArticleDetail,
|
||||
query: {
|
||||
id: item.id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const toEdit = (item: ArticleType) => {
|
||||
router.push({
|
||||
path: RoutesAlias.ArticlePublish,
|
||||
query: {
|
||||
id: item.id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const toAddArticle = () => {
|
||||
router.push({
|
||||
path: RoutesAlias.ArticlePublish
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.article-list {
|
||||
.custom-segmented .el-segmented {
|
||||
height: 40px;
|
||||
padding: 6px;
|
||||
|
||||
--el-border-radius-base: 8px;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin-top: 20px;
|
||||
|
||||
.offset {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: calc(100% + 20px);
|
||||
|
||||
.item {
|
||||
box-sizing: border-box;
|
||||
width: calc(20% - 20px);
|
||||
margin: 0 20px 20px 0;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--art-border-color);
|
||||
border-radius: calc(var(--custom-radius) / 2 + 2px) !important;
|
||||
|
||||
&:hover {
|
||||
.el-button {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.top {
|
||||
position: relative;
|
||||
aspect-ratio: 16/9.5;
|
||||
|
||||
.cover {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
background: var(--art-gray-200);
|
||||
border-radius: calc(var(--custom-radius) / 2 + 2px)
|
||||
calc(var(--custom-radius) / 2 + 2px) 0 0;
|
||||
|
||||
.image-slot {
|
||||
font-size: 26px;
|
||||
color: var(--art-gray-400);
|
||||
}
|
||||
}
|
||||
|
||||
.type {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
padding: 5px 4px;
|
||||
font-size: 12px;
|
||||
color: rgba(#fff, 0.8);
|
||||
background: rgba($color: #000, $alpha: 60%);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
padding: 5px 10px;
|
||||
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
|
||||
@include ellipsis();
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 25px;
|
||||
margin-top: 6px;
|
||||
line-height: 25px;
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--art-text-gray-600);
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 13px;
|
||||
color: var(--art-gray-600);
|
||||
}
|
||||
|
||||
.line {
|
||||
width: 1px;
|
||||
height: 12px;
|
||||
margin: 0 15px;
|
||||
background-color: var(--art-border-dashed-color);
|
||||
}
|
||||
}
|
||||
|
||||
.el-button {
|
||||
opacity: 0;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $device-notebook) {
|
||||
.article-list {
|
||||
.list {
|
||||
.offset {
|
||||
.item {
|
||||
width: calc(25% - 20px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $device-ipad-pro) {
|
||||
.article-list {
|
||||
.list {
|
||||
.offset {
|
||||
.item {
|
||||
width: calc(33.333% - 20px);
|
||||
|
||||
.bottom {
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $device-ipad) {
|
||||
.article-list {
|
||||
.list {
|
||||
.offset {
|
||||
.item {
|
||||
width: calc(50% - 20px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $device-phone) {
|
||||
.article-list {
|
||||
.list {
|
||||
.offset {
|
||||
.item {
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
412
src/views/article/publish/index.vue
Normal file
412
src/views/article/publish/index.vue
Normal file
@@ -0,0 +1,412 @@
|
||||
<template>
|
||||
<div class="article-edit">
|
||||
<div>
|
||||
<div class="editor-wrap">
|
||||
<!-- 文章标题、类型 -->
|
||||
<ElRow :gutter="10">
|
||||
<ElCol :span="18">
|
||||
<ElInput
|
||||
v-model.trim="articleName"
|
||||
placeholder="请输入文章标题(最多100个字符)"
|
||||
maxlength="100"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :span="6">
|
||||
<ElSelect v-model="articleType" placeholder="请选择文章类型" filterable>
|
||||
<ElOption
|
||||
v-for="item in articleTypes"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<!-- 富文本编辑器 -->
|
||||
<ArtWangEditor class="el-top" v-model="editorHtml" />
|
||||
|
||||
<div class="form-wrap">
|
||||
<h2>发布设置</h2>
|
||||
<!-- 图片上传 -->
|
||||
<ElForm>
|
||||
<ElFormItem label="封面">
|
||||
<div class="el-top upload-container">
|
||||
<ElUpload
|
||||
class="cover-uploader"
|
||||
:action="uploadImageUrl"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:on-success="onSuccess"
|
||||
:on-error="onError"
|
||||
:before-upload="beforeUpload"
|
||||
>
|
||||
<div v-if="!cover" class="upload-placeholder">
|
||||
<ElIcon class="upload-icon"><Plus /></ElIcon>
|
||||
<div class="upload-text">点击上传封面</div>
|
||||
</div>
|
||||
<img v-else :src="cover" class="cover-image" />
|
||||
</ElUpload>
|
||||
<div class="el-upload__tip">建议尺寸 16:9,jpg/png 格式</div>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="可见">
|
||||
<ElSwitch v-model="visible" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<div style="display: flex; justify-content: flex-end">
|
||||
<ElButton type="primary" @click="submit" style="width: 100px">
|
||||
{{ pageMode === PageModeEnum.Edit ? '保存' : '发布' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="outline-wrap">
|
||||
<div class="item" v-for="(item, index) in outlineList" :key="index">
|
||||
<p :class="`level${item.level}`">{{ item.text }}</p>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { ArticleService } from '@/api/articleApi'
|
||||
import { ApiStatus } from '@/utils/http/status'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import EmojiText from '@/utils/ui/emojo'
|
||||
import { PageModeEnum } from '@/enums/formEnum'
|
||||
import axios from 'axios'
|
||||
import { useCommon } from '@/composables/useCommon'
|
||||
|
||||
defineOptions({ name: 'ArticlePublish' })
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const userStore = useUserStore()
|
||||
let { accessToken } = userStore
|
||||
|
||||
// 上传路径
|
||||
const uploadImageUrl = `${import.meta.env.VITE_API_URL}/api/common/upload`
|
||||
// 传递 token
|
||||
const uploadHeaders = { Authorization: accessToken }
|
||||
|
||||
let pageMode: PageModeEnum = PageModeEnum.Add // 页面类型 新增 | 编辑
|
||||
const articleName = ref('') // 文章标题
|
||||
const articleType = ref() // 文章类型
|
||||
const articleTypes = ref() // 类型列表
|
||||
const editorHtml = ref('') // 编辑器内容
|
||||
const createDate = ref('') // 创建时间
|
||||
const cover = ref('') // 图片
|
||||
const visible = ref(true) // 可见
|
||||
// const outlineList = ref()
|
||||
|
||||
onMounted(() => {
|
||||
useCommon().scrollToTop()
|
||||
getArticleTypes()
|
||||
initPageMode()
|
||||
})
|
||||
|
||||
// 初始化页面类型 新增 | 编辑
|
||||
const initPageMode = () => {
|
||||
const { id } = route.query
|
||||
pageMode = id ? PageModeEnum.Edit : PageModeEnum.Add
|
||||
if (pageMode === PageModeEnum.Edit && id) {
|
||||
initEditArticle(Number(id))
|
||||
} else {
|
||||
initAddArticle()
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化编辑文章的逻辑
|
||||
const initEditArticle = (id: number) => {
|
||||
articleId = id
|
||||
getArticleDetail()
|
||||
}
|
||||
|
||||
// 初始化新增文章逻辑
|
||||
const initAddArticle = () => {
|
||||
createDate.value = formDate(useNow().value)
|
||||
}
|
||||
|
||||
// 获取文章类型
|
||||
const getArticleTypes = async () => {
|
||||
try {
|
||||
const response = await axios.get('https://www.qiniu.lingchen.kim/classify.json')
|
||||
if (response.data.code === 200) {
|
||||
articleTypes.value = response.data.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching JSON data:', error)
|
||||
}
|
||||
// try {
|
||||
// const res = await ArticleService.getArticleTypes({})
|
||||
// if (res.code === ApiStatus.success) {
|
||||
// articleTypes.value = res.data
|
||||
// }
|
||||
// } catch (err) { }
|
||||
}
|
||||
|
||||
// 获取文章详情内容
|
||||
let articleId: number = 0
|
||||
const getArticleDetail = async () => {
|
||||
const res = await axios.get('https://www.qiniu.lingchen.kim/blog_list.json')
|
||||
|
||||
if (res.data.code === ApiStatus.success) {
|
||||
let { title, blog_class, html_content } = res.data.data
|
||||
articleName.value = title
|
||||
articleType.value = Number(blog_class)
|
||||
editorHtml.value = html_content
|
||||
}
|
||||
|
||||
// const res = await ArticleService.getArticleDetail(articleId)
|
||||
// if (res.code === ApiStatus.success) {
|
||||
// let { title, blog_class, create_time, home_img, html_content } = res.data
|
||||
|
||||
// articleName.value = title
|
||||
// articleType.value = Number(blog_class)
|
||||
// editorHtml.value = html_content
|
||||
// cover.value = home_img
|
||||
// createDate.value = formDate(create_time)
|
||||
|
||||
// // getOutline(html_content)
|
||||
// }
|
||||
}
|
||||
|
||||
// const getOutline = (content: string) => {
|
||||
// const regex = /<h([1-3])>(.*?)<\/h\1>/g
|
||||
// const headings = []
|
||||
// let match
|
||||
|
||||
// while ((match = regex.exec(content)) !== null) {
|
||||
// headings.push({ level: match[1], text: match[2] })
|
||||
// }
|
||||
// outlineList.value = headings
|
||||
// }
|
||||
|
||||
// 提交
|
||||
const submit = () => {
|
||||
if (pageMode === PageModeEnum.Edit) {
|
||||
editArticle()
|
||||
} else {
|
||||
addArticle()
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formDate = (date: string | Date): string => {
|
||||
return useDateFormat(date, 'YYYY-MM-DD').value
|
||||
}
|
||||
|
||||
// 验证输入
|
||||
const validateArticle = () => {
|
||||
if (!articleName.value) {
|
||||
ElMessage.error(`请输入文章标题`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (!articleType.value) {
|
||||
ElMessage.error(`请选择文章类型`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (editorHtml.value === '<p><br></p>') {
|
||||
ElMessage.error(`请输入文章内容`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (!cover.value) {
|
||||
ElMessage.error(`请上传图片`)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 构建参数
|
||||
const buildParams = () => {
|
||||
return {
|
||||
title: articleName.value,
|
||||
html_content: editorHtml.value,
|
||||
home_img: cover.value,
|
||||
blog_class: articleType.value,
|
||||
create_time: createDate.value
|
||||
}
|
||||
}
|
||||
|
||||
// 添加文章
|
||||
const addArticle = async () => {
|
||||
try {
|
||||
if (!validateArticle()) return
|
||||
|
||||
editorHtml.value = delCodeTrim(editorHtml.value)
|
||||
|
||||
const params = buildParams()
|
||||
const res = await ArticleService.addArticle(params)
|
||||
|
||||
if (res.code === ApiStatus.success) {
|
||||
ElMessage.success(`发布成功 ${EmojiText[200]}`)
|
||||
goBack()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑文章
|
||||
const editArticle = async () => {
|
||||
try {
|
||||
if (!validateArticle()) return
|
||||
|
||||
editorHtml.value = delCodeTrim(editorHtml.value)
|
||||
|
||||
const params = buildParams()
|
||||
const res = await ArticleService.editArticle(articleId, params)
|
||||
|
||||
if (res.code === ApiStatus.success) {
|
||||
ElMessage.success(`修改成功 ${EmojiText[200]}`)
|
||||
goBack()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
const delCodeTrim = (content: string): string => {
|
||||
return content.replace(/(\s*)<\/code>/g, '</code>')
|
||||
}
|
||||
|
||||
const onSuccess = (response: any) => {
|
||||
cover.value = response.data.url
|
||||
ElMessage.success(`图片上传成功 ${EmojiText[200]}`)
|
||||
}
|
||||
|
||||
const onError = () => {
|
||||
ElMessage.error(`图片上传失败 ${EmojiText[500]}`)
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
const goBack = () => {
|
||||
setTimeout(() => {
|
||||
router.go(-1)
|
||||
}, 800)
|
||||
}
|
||||
|
||||
// 添加上传前的校验
|
||||
const beforeUpload = (file: File) => {
|
||||
const isImage = file.type.startsWith('image/')
|
||||
const isLt2M = file.size / 1024 / 1024 < 2
|
||||
|
||||
if (!isImage) {
|
||||
ElMessage.error('只能上传图片文件!')
|
||||
return false
|
||||
}
|
||||
if (!isLt2M) {
|
||||
ElMessage.error('图片大小不能超过 2MB!')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.article-edit {
|
||||
.editor-wrap {
|
||||
max-width: 1000px;
|
||||
margin: 20px auto;
|
||||
|
||||
.el-top {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.form-wrap {
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
background-color: var(--art-main-bg-color);
|
||||
border: 1px solid var(--art-border-color);
|
||||
border-radius: calc(var(--custom-radius) / 2 + 2px) !important;
|
||||
|
||||
h2 {
|
||||
margin-bottom: 20px;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.outline-wrap {
|
||||
box-sizing: border-box;
|
||||
width: 280px;
|
||||
padding: 20px;
|
||||
border: 1px solid #e3e3e3;
|
||||
border-radius: 8px;
|
||||
|
||||
.item {
|
||||
p {
|
||||
height: 30px;
|
||||
font-size: 13px;
|
||||
line-height: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.level3 {
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-container {
|
||||
.cover-uploader {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: var(--el-transition-duration);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.upload-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 260px;
|
||||
height: 160px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
|
||||
.upload-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
color: #8c939d;
|
||||
}
|
||||
}
|
||||
|
||||
.cover-image {
|
||||
display: block;
|
||||
width: 260px;
|
||||
height: 160px;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.el-upload__tip {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
442
src/views/asset-management/asset-assign/index.vue
Normal file
442
src/views/asset-management/asset-assign/index.vue
Normal file
@@ -0,0 +1,442 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<!-- 操作提示 -->
|
||||
<ElAlert type="info" :closable="false" style="margin-bottom: 20px">
|
||||
<template #title>
|
||||
<div style="line-height: 1.8">
|
||||
<p><strong>资产分配说明:</strong></p>
|
||||
<p>1. <strong>网卡批量分配</strong>:仅分配选中的网卡资产</p>
|
||||
<p>2. <strong>设备批量分配</strong>:仅分配选中的设备资产</p>
|
||||
<p>3. <strong>网卡+设备分配</strong>:如果网卡有绑定设备,将同时分配网卡和设备</p>
|
||||
<p>4. 分配后资产所有权将转移至目标代理商</p>
|
||||
</div>
|
||||
</template>
|
||||
</ElAlert>
|
||||
|
||||
<!-- 分配模式选择 -->
|
||||
<ElCard shadow="never" style="margin-bottom: 20px">
|
||||
<ElRadioGroup v-model="assignMode" size="large">
|
||||
<ElRadioButton value="sim">网卡批量分配</ElRadioButton>
|
||||
<ElRadioButton value="device">设备批量分配</ElRadioButton>
|
||||
<ElRadioButton value="both">网卡+设备分配</ElRadioButton>
|
||||
</ElRadioGroup>
|
||||
</ElCard>
|
||||
|
||||
<!-- 网卡分配 -->
|
||||
<ElCard v-if="assignMode === 'sim' || assignMode === 'both'" shadow="never" style="margin-bottom: 20px">
|
||||
<template #header>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center">
|
||||
<span style="font-weight: 500">选择网卡资产</span>
|
||||
<ElButton type="primary" size="small" :disabled="selectedSims.length === 0" @click="showAssignDialog('sim')">
|
||||
分配选中的 {{ selectedSims.length }} 张网卡
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ElRow :gutter="12" style="margin-bottom: 16px">
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElInput v-model="simSearchQuery" placeholder="ICCID/IMSI" clearable />
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElSelect v-model="simStatusFilter" placeholder="状态筛选" clearable style="width: 100%">
|
||||
<ElOption label="全部" value="" />
|
||||
<ElOption label="激活" value="active" />
|
||||
<ElOption label="未激活" value="inactive" />
|
||||
<ElOption label="停机" value="suspended" />
|
||||
</ElSelect>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElButton v-ripple @click="searchSims">搜索</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ArtTable :data="filteredSimData" index @selection-change="handleSimSelectionChange">
|
||||
<template #default>
|
||||
<ElTableColumn type="selection" width="55" />
|
||||
<ElTableColumn label="ICCID" prop="iccid" width="200" />
|
||||
<ElTableColumn label="IMSI" prop="imsi" width="180" />
|
||||
<ElTableColumn label="运营商" prop="operator" width="100">
|
||||
<template #default="scope">
|
||||
<ElTag size="small">{{ scope.row.operator }}</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" prop="status" width="100">
|
||||
<template #default="scope">
|
||||
<ElTag v-if="scope.row.status === 'active'" type="success" size="small">激活</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'inactive'" type="info" size="small">未激活</ElTag>
|
||||
<ElTag v-else type="warning" size="small">停机</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="绑定设备" prop="deviceCode" width="150">
|
||||
<template #default="scope">
|
||||
<ElTag v-if="scope.row.deviceCode" type="primary" size="small">
|
||||
{{ scope.row.deviceCode }}
|
||||
</ElTag>
|
||||
<span v-else style="color: var(--el-text-color-secondary)">未绑定</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="剩余流量" prop="remainData" width="120" />
|
||||
<ElTableColumn label="到期时间" prop="expireTime" width="180" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 设备分配 -->
|
||||
<ElCard v-if="assignMode === 'device'" shadow="never">
|
||||
<template #header>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center">
|
||||
<span style="font-weight: 500">选择设备资产</span>
|
||||
<ElButton type="primary" size="small" :disabled="selectedDevices.length === 0" @click="showAssignDialog('device')">
|
||||
分配选中的 {{ selectedDevices.length }} 个设备
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ElRow :gutter="12" style="margin-bottom: 16px">
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElInput v-model="deviceSearchQuery" placeholder="设备编号/名称" clearable />
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElSelect v-model="deviceTypeFilter" placeholder="设备类型" clearable style="width: 100%">
|
||||
<ElOption label="全部" value="" />
|
||||
<ElOption label="GPS定位器" value="gps" />
|
||||
<ElOption label="智能水表" value="water_meter" />
|
||||
<ElOption label="智能电表" value="electric_meter" />
|
||||
</ElSelect>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElButton v-ripple @click="searchDevices">搜索</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ArtTable :data="filteredDeviceData" index @selection-change="handleDeviceSelectionChange">
|
||||
<template #default>
|
||||
<ElTableColumn type="selection" width="55" />
|
||||
<ElTableColumn label="设备编号" prop="deviceCode" width="180" />
|
||||
<ElTableColumn label="设备名称" prop="deviceName" min-width="180" />
|
||||
<ElTableColumn label="设备类型" prop="deviceType" width="120">
|
||||
<template #default="scope">
|
||||
<ElTag size="small">{{ getDeviceTypeText(scope.row.deviceType) }}</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="绑定ICCID" prop="iccid" width="200">
|
||||
<template #default="scope">
|
||||
<ElTag v-if="scope.row.iccid" type="success" size="small">
|
||||
{{ scope.row.iccid }}
|
||||
</ElTag>
|
||||
<span v-else style="color: var(--el-text-color-secondary)">未绑定</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="在线状态" prop="onlineStatus" width="100">
|
||||
<template #default="scope">
|
||||
<ElTag v-if="scope.row.onlineStatus === 'online'" type="success" size="small">在线</ElTag>
|
||||
<ElTag v-else type="info" size="small">离线</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="createTime" width="180" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 分配对话框 -->
|
||||
<ElDialog v-model="assignDialogVisible" title="资产分配" width="600px" align-center>
|
||||
<ElForm ref="formRef" :model="assignForm" :rules="assignRules" label-width="120px">
|
||||
<ElFormItem label="分配类型">
|
||||
<ElTag v-if="assignForm.type === 'sim'" type="primary">网卡资产</ElTag>
|
||||
<ElTag v-else-if="assignForm.type === 'device'" type="success">设备资产</ElTag>
|
||||
<ElTag v-else type="warning">网卡+设备</ElTag>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="分配数量">
|
||||
<div>
|
||||
<span v-if="assignForm.type === 'sim'" style="font-size: 18px; font-weight: 600; color: var(--el-color-primary)">
|
||||
{{ selectedSims.length }} 张网卡
|
||||
</span>
|
||||
<span v-else-if="assignForm.type === 'device'" style="font-size: 18px; font-weight: 600; color: var(--el-color-success)">
|
||||
{{ selectedDevices.length }} 个设备
|
||||
</span>
|
||||
<span v-else style="font-size: 18px; font-weight: 600">
|
||||
{{ selectedSims.length }} 张网卡 + {{ selectedSims.filter(s => s.deviceCode).length }} 个设备
|
||||
</span>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="目标代理商" prop="targetAgentId">
|
||||
<ElSelect
|
||||
v-model="assignForm.targetAgentId"
|
||||
placeholder="请选择目标代理商"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption
|
||||
v-for="agent in agentList"
|
||||
:key="agent.id"
|
||||
:label="`${agent.agentName} (等级${agent.level})`"
|
||||
:value="agent.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="分配说明" prop="remark">
|
||||
<ElInput
|
||||
v-model="assignForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入分配说明"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElAlert type="warning" :closable="false">
|
||||
分配后资产所有权将转移至目标代理商,原账号将无法管理这些资产!
|
||||
</ElAlert>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="assignDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleAssignSubmit">确认分配</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 分配记录 -->
|
||||
<ElCard shadow="never" style="margin-top: 20px">
|
||||
<template #header>
|
||||
<span style="font-weight: 500">最近分配记录</span>
|
||||
</template>
|
||||
|
||||
<ArtTable :data="assignHistory" index>
|
||||
<template #default>
|
||||
<ElTableColumn label="分配批次号" prop="batchNo" width="180" />
|
||||
<ElTableColumn label="分配类型" prop="type" width="120">
|
||||
<template #default="scope">
|
||||
<ElTag v-if="scope.row.type === 'sim'" type="primary" size="small">网卡</ElTag>
|
||||
<ElTag v-else-if="scope.row.type === 'device'" type="success" size="small">设备</ElTag>
|
||||
<ElTag v-else type="warning" size="small">网卡+设备</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="分配数量" prop="quantity" width="100" />
|
||||
<ElTableColumn label="目标代理商" prop="targetAgentName" min-width="150" />
|
||||
<ElTableColumn label="分配说明" prop="remark" min-width="200" show-overflow-tooltip />
|
||||
<ElTableColumn label="分配时间" prop="assignTime" width="180" />
|
||||
<ElTableColumn label="操作人" prop="operator" width="100" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'AssetAssign' })
|
||||
|
||||
interface SimCard {
|
||||
id: string
|
||||
iccid: string
|
||||
imsi: string
|
||||
operator: string
|
||||
status: string
|
||||
deviceCode?: string
|
||||
remainData: string
|
||||
expireTime: string
|
||||
}
|
||||
|
||||
interface Device {
|
||||
id: string
|
||||
deviceCode: string
|
||||
deviceName: string
|
||||
deviceType: string
|
||||
iccid?: string
|
||||
onlineStatus: string
|
||||
createTime: string
|
||||
}
|
||||
|
||||
const assignMode = ref('sim')
|
||||
const simSearchQuery = ref('')
|
||||
const simStatusFilter = ref('')
|
||||
const deviceSearchQuery = ref('')
|
||||
const deviceTypeFilter = ref('')
|
||||
const assignDialogVisible = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
const selectedSims = ref<SimCard[]>([])
|
||||
const selectedDevices = ref<Device[]>([])
|
||||
|
||||
const assignForm = reactive({
|
||||
type: 'sim',
|
||||
targetAgentId: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const assignRules = reactive<FormRules>({
|
||||
targetAgentId: [{ required: true, message: '请选择目标代理商', trigger: 'change' }]
|
||||
})
|
||||
|
||||
const agentList = ref([
|
||||
{ id: '1', agentName: '华东区总代理', level: 1 },
|
||||
{ id: '2', agentName: '华南区代理', level: 2 },
|
||||
{ id: '3', agentName: '华北区代理', level: 1 }
|
||||
])
|
||||
|
||||
const simMockData = ref<SimCard[]>([
|
||||
{
|
||||
id: '1',
|
||||
iccid: '89860123456789012345',
|
||||
imsi: '460012345678901',
|
||||
operator: '中国移动',
|
||||
status: 'active',
|
||||
deviceCode: 'DEV001',
|
||||
remainData: '50GB',
|
||||
expireTime: '2026-12-31'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
iccid: '89860123456789012346',
|
||||
imsi: '460012345678902',
|
||||
operator: '中国联通',
|
||||
status: 'active',
|
||||
remainData: '80GB',
|
||||
expireTime: '2026-11-30'
|
||||
}
|
||||
])
|
||||
|
||||
const deviceMockData = ref<Device[]>([
|
||||
{
|
||||
id: '1',
|
||||
deviceCode: 'DEV001',
|
||||
deviceName: 'GPS定位器-001',
|
||||
deviceType: 'gps',
|
||||
iccid: '89860123456789012345',
|
||||
onlineStatus: 'online',
|
||||
createTime: '2026-01-01 10:00:00'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
deviceCode: 'DEV002',
|
||||
deviceName: '智能水表-002',
|
||||
deviceType: 'water_meter',
|
||||
iccid: '89860123456789012346',
|
||||
onlineStatus: 'offline',
|
||||
createTime: '2026-01-02 11:00:00'
|
||||
}
|
||||
])
|
||||
|
||||
const assignHistory = ref([
|
||||
{
|
||||
id: '1',
|
||||
batchNo: 'ASSIGN202601090001',
|
||||
type: 'sim',
|
||||
quantity: 100,
|
||||
targetAgentName: '华东区总代理',
|
||||
remark: '批量分配给华东区',
|
||||
assignTime: '2026-01-09 10:00:00',
|
||||
operator: 'admin'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
batchNo: 'ASSIGN202601080001',
|
||||
type: 'both',
|
||||
quantity: 50,
|
||||
targetAgentName: '华南区代理',
|
||||
remark: '网卡和设备一起分配',
|
||||
assignTime: '2026-01-08 14:00:00',
|
||||
operator: 'admin'
|
||||
}
|
||||
])
|
||||
|
||||
const filteredSimData = computed(() => {
|
||||
let data = simMockData.value
|
||||
if (simSearchQuery.value) {
|
||||
data = data.filter(
|
||||
(item) => item.iccid.includes(simSearchQuery.value) || item.imsi.includes(simSearchQuery.value)
|
||||
)
|
||||
}
|
||||
if (simStatusFilter.value) {
|
||||
data = data.filter((item) => item.status === simStatusFilter.value)
|
||||
}
|
||||
return data
|
||||
})
|
||||
|
||||
const filteredDeviceData = computed(() => {
|
||||
let data = deviceMockData.value
|
||||
if (deviceSearchQuery.value) {
|
||||
data = data.filter(
|
||||
(item) => item.deviceCode.includes(deviceSearchQuery.value) || item.deviceName.includes(deviceSearchQuery.value)
|
||||
)
|
||||
}
|
||||
if (deviceTypeFilter.value) {
|
||||
data = data.filter((item) => item.deviceType === deviceTypeFilter.value)
|
||||
}
|
||||
return data
|
||||
})
|
||||
|
||||
const getDeviceTypeText = (type: string) => {
|
||||
const map: Record<string, string> = {
|
||||
gps: 'GPS定位器',
|
||||
water_meter: '智能水表',
|
||||
electric_meter: '智能电表'
|
||||
}
|
||||
return map[type] || type
|
||||
}
|
||||
|
||||
const searchSims = () => {}
|
||||
const searchDevices = () => {}
|
||||
|
||||
const handleSimSelectionChange = (rows: SimCard[]) => {
|
||||
selectedSims.value = rows
|
||||
}
|
||||
|
||||
const handleDeviceSelectionChange = (rows: Device[]) => {
|
||||
selectedDevices.value = rows
|
||||
}
|
||||
|
||||
const showAssignDialog = (type: string) => {
|
||||
assignForm.type = type
|
||||
assignDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleAssignSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
const agent = agentList.value.find((a) => a.id === assignForm.targetAgentId)
|
||||
let quantity = 0
|
||||
|
||||
if (assignForm.type === 'sim') {
|
||||
quantity = selectedSims.value.length
|
||||
} else if (assignForm.type === 'device') {
|
||||
quantity = selectedDevices.value.length
|
||||
} else {
|
||||
quantity = selectedSims.value.length
|
||||
}
|
||||
|
||||
assignHistory.value.unshift({
|
||||
id: Date.now().toString(),
|
||||
batchNo: `ASSIGN${Date.now()}`,
|
||||
type: assignForm.type,
|
||||
quantity,
|
||||
targetAgentName: agent?.agentName || '',
|
||||
remark: assignForm.remark,
|
||||
assignTime: new Date().toLocaleString('zh-CN'),
|
||||
operator: 'admin'
|
||||
})
|
||||
|
||||
assignDialogVisible.value = false
|
||||
formRef.value.resetFields()
|
||||
selectedSims.value = []
|
||||
selectedDevices.value = []
|
||||
ElMessage.success('资产分配成功')
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
:deep(.el-radio-button__inner) {
|
||||
padding: 12px 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
523
src/views/asset-management/card-replacement-request/index.vue
Normal file
523
src/views/asset-management/card-replacement-request/index.vue
Normal file
@@ -0,0 +1,523 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<!-- 搜索和筛选区 -->
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElInput v-model="searchQuery" placeholder="申请单号/ICCID" clearable />
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElSelect v-model="statusFilter" placeholder="状态筛选" clearable style="width: 100%">
|
||||
<ElOption label="全部" value="" />
|
||||
<ElOption label="待处理" value="pending" />
|
||||
<ElOption label="处理中" value="processing" />
|
||||
<ElOption label="已完成" value="completed" />
|
||||
<ElOption label="已拒绝" value="rejected" />
|
||||
</ElSelect>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElDatePicker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<ElButton v-ripple @click="handleSearch">搜索</ElButton>
|
||||
<ElButton v-ripple @click="exportData">导出</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<ElRow :gutter="20" style="margin: 20px 0">
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover" class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">待处理</div>
|
||||
<div class="stat-value" style="color: var(--el-color-warning)">{{ statistics.pending }}</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon" style="color: var(--el-color-warning)"><Clock /></el-icon>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover" class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">处理中</div>
|
||||
<div class="stat-value" style="color: var(--el-color-primary)">{{ statistics.processing }}</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon" style="color: var(--el-color-primary)"><Loading /></el-icon>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover" class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">已完成</div>
|
||||
<div class="stat-value" style="color: var(--el-color-success)">{{ statistics.completed }}</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon" style="color: var(--el-color-success)"><CircleCheck /></el-icon>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover" class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">已拒绝</div>
|
||||
<div class="stat-value" style="color: var(--el-color-danger)">{{ statistics.rejected }}</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon" style="color: var(--el-color-danger)"><CircleClose /></el-icon>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<!-- 换卡申请列表 -->
|
||||
<ArtTable :data="filteredData" index>
|
||||
<template #default>
|
||||
<ElTableColumn label="申请单号" prop="requestNo" width="180" />
|
||||
<ElTableColumn label="旧卡ICCID" prop="oldIccid" width="200" />
|
||||
<ElTableColumn label="申请人" prop="applicant" width="120" />
|
||||
<ElTableColumn label="联系电话" prop="phone" width="130" />
|
||||
<ElTableColumn label="换卡原因" prop="reason" min-width="180" show-overflow-tooltip />
|
||||
<ElTableColumn label="新卡ICCID" prop="newIccid" width="200">
|
||||
<template #default="scope">
|
||||
<ElTag v-if="scope.row.newIccid" type="success" size="small">
|
||||
{{ scope.row.newIccid }}
|
||||
</ElTag>
|
||||
<span v-else style="color: var(--el-text-color-secondary)">待填充</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" prop="status" width="100">
|
||||
<template #default="scope">
|
||||
<ElTag v-if="scope.row.status === 'pending'" type="warning">待处理</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'processing'" type="primary">处理中</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'completed'" type="success">已完成</ElTag>
|
||||
<ElTag v-else type="danger">已拒绝</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="申请时间" prop="applyTime" width="180" />
|
||||
<ElTableColumn fixed="right" label="操作" width="240">
|
||||
<template #default="scope">
|
||||
<el-button link :icon="View" @click="viewDetail(scope.row)">详情</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.status === 'pending'"
|
||||
link
|
||||
type="primary"
|
||||
@click="handleProcess(scope.row)"
|
||||
>
|
||||
处理
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.status === 'processing'"
|
||||
link
|
||||
type="success"
|
||||
@click="fillNewIccid(scope.row)"
|
||||
>
|
||||
填充新卡
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.status === 'pending'"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleReject(scope.row)"
|
||||
>
|
||||
拒绝
|
||||
</el-button>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<ElDialog v-model="detailDialogVisible" title="换卡申请详情" width="800px" align-center>
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem label="申请单号">{{ currentRequest.requestNo }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="申请人">{{ currentRequest.applicant }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="联系电话">{{ currentRequest.phone }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="申请时间">{{ currentRequest.applyTime }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="旧卡ICCID" :span="2">
|
||||
<ElTag type="warning">{{ currentRequest.oldIccid }}</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="新卡ICCID" :span="2">
|
||||
<ElTag v-if="currentRequest.newIccid" type="success">{{ currentRequest.newIccid }}</ElTag>
|
||||
<span v-else style="color: var(--el-text-color-secondary)">待填充</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="换卡原因" :span="2">
|
||||
{{ currentRequest.reason }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="详细说明" :span="2">
|
||||
{{ currentRequest.description || '无' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="状态">
|
||||
<ElTag v-if="currentRequest.status === 'pending'" type="warning">待处理</ElTag>
|
||||
<ElTag v-else-if="currentRequest.status === 'processing'" type="primary">处理中</ElTag>
|
||||
<ElTag v-else-if="currentRequest.status === 'completed'" type="success">已完成</ElTag>
|
||||
<ElTag v-else type="danger">已拒绝</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="处理人">
|
||||
{{ currentRequest.processor || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="处理时间" :span="2">
|
||||
{{ currentRequest.processTime || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem v-if="currentRequest.rejectReason" label="拒绝原因" :span="2">
|
||||
<span style="color: var(--el-color-danger)">{{ currentRequest.rejectReason }}</span>
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="detailDialogVisible = false">关闭</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 填充新卡对话框 -->
|
||||
<ElDialog v-model="fillDialogVisible" title="填充新卡ICCID" width="500px" align-center>
|
||||
<ElForm ref="fillFormRef" :model="fillForm" :rules="fillRules" label-width="100px">
|
||||
<ElFormItem label="旧卡ICCID">
|
||||
<ElInput :value="currentRequest.oldIccid" disabled />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="新卡ICCID" prop="newIccid">
|
||||
<ElInput v-model="fillForm.newIccid" placeholder="请输入新卡ICCID" maxlength="20" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="验证新卡">
|
||||
<ElButton @click="validateNewIccid">验证ICCID</ElButton>
|
||||
<div v-if="validationResult" style="margin-top: 8px">
|
||||
<ElTag v-if="validationResult === 'success'" type="success" size="small">
|
||||
验证通过,该卡可用
|
||||
</ElTag>
|
||||
<ElTag v-else type="danger" size="small">
|
||||
验证失败,{{ validationMessage }}
|
||||
</ElTag>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="备注">
|
||||
<ElInput v-model="fillForm.remark" type="textarea" :rows="3" placeholder="请输入备注信息" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElAlert type="info" :closable="false">
|
||||
填充新卡后,系统将自动完成换卡操作,旧卡将被停用
|
||||
</ElAlert>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="fillDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleFillSubmit">确认填充</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 拒绝对话框 -->
|
||||
<ElDialog v-model="rejectDialogVisible" title="拒绝换卡申请" width="500px" align-center>
|
||||
<ElForm ref="rejectFormRef" :model="rejectForm" :rules="rejectRules" label-width="100px">
|
||||
<ElFormItem label="拒绝原因" prop="reason">
|
||||
<ElInput
|
||||
v-model="rejectForm.reason"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入拒绝原因"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="rejectDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="danger" @click="handleRejectSubmit">确认拒绝</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { View, Clock, Loading as LoadingIcon, CircleCheck, CircleClose } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'CardReplacementRequest' })
|
||||
|
||||
interface ReplacementRequest {
|
||||
id: string
|
||||
requestNo: string
|
||||
oldIccid: string
|
||||
newIccid?: string
|
||||
applicant: string
|
||||
phone: string
|
||||
reason: string
|
||||
description?: string
|
||||
status: 'pending' | 'processing' | 'completed' | 'rejected'
|
||||
applyTime: string
|
||||
processor?: string
|
||||
processTime?: string
|
||||
rejectReason?: string
|
||||
}
|
||||
|
||||
const searchQuery = ref('')
|
||||
const statusFilter = ref('')
|
||||
const dateRange = ref<[Date, Date] | null>(null)
|
||||
const detailDialogVisible = ref(false)
|
||||
const fillDialogVisible = ref(false)
|
||||
const rejectDialogVisible = ref(false)
|
||||
const fillFormRef = ref<FormInstance>()
|
||||
const rejectFormRef = ref<FormInstance>()
|
||||
const validationResult = ref<string>('')
|
||||
const validationMessage = ref('')
|
||||
|
||||
const statistics = reactive({
|
||||
pending: 15,
|
||||
processing: 8,
|
||||
completed: 102,
|
||||
rejected: 5
|
||||
})
|
||||
|
||||
const fillForm = reactive({
|
||||
newIccid: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const fillRules = reactive<FormRules>({
|
||||
newIccid: [
|
||||
{ required: true, message: '请输入新卡ICCID', trigger: 'blur' },
|
||||
{ len: 20, message: 'ICCID长度必须为20位', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
const rejectForm = reactive({
|
||||
reason: ''
|
||||
})
|
||||
|
||||
const rejectRules = reactive<FormRules>({
|
||||
reason: [{ required: true, message: '请输入拒绝原因', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const mockData = ref<ReplacementRequest[]>([
|
||||
{
|
||||
id: '1',
|
||||
requestNo: 'REP202601090001',
|
||||
oldIccid: '89860123456789012345',
|
||||
applicant: '张三',
|
||||
phone: '13800138000',
|
||||
reason: '卡片损坏',
|
||||
description: '卡片物理损坏无法使用',
|
||||
status: 'pending',
|
||||
applyTime: '2026-01-09 09:30:00'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
requestNo: 'REP202601080002',
|
||||
oldIccid: '89860123456789012346',
|
||||
newIccid: '89860123456789012350',
|
||||
applicant: '李四',
|
||||
phone: '13900139000',
|
||||
reason: '信号不稳定',
|
||||
description: '长期信号不稳定,影响使用',
|
||||
status: 'processing',
|
||||
applyTime: '2026-01-08 14:20:00',
|
||||
processor: 'admin'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
requestNo: 'REP202601070003',
|
||||
oldIccid: '89860123456789012347',
|
||||
newIccid: '89860123456789012351',
|
||||
applicant: '王五',
|
||||
phone: '13700137000',
|
||||
reason: '卡片丢失',
|
||||
description: '卡片意外丢失',
|
||||
status: 'completed',
|
||||
applyTime: '2026-01-07 10:00:00',
|
||||
processor: 'admin',
|
||||
processTime: '2026-01-07 15:30:00'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
requestNo: 'REP202601060004',
|
||||
oldIccid: '89860123456789012348',
|
||||
applicant: '赵六',
|
||||
phone: '13600136000',
|
||||
reason: '套餐到期',
|
||||
description: '套餐到期需要换新卡',
|
||||
status: 'rejected',
|
||||
applyTime: '2026-01-06 11:00:00',
|
||||
processor: 'admin',
|
||||
processTime: '2026-01-06 12:00:00',
|
||||
rejectReason: '套餐到期应该续费而不是换卡'
|
||||
}
|
||||
])
|
||||
|
||||
const currentRequest = ref<ReplacementRequest>({
|
||||
id: '',
|
||||
requestNo: '',
|
||||
oldIccid: '',
|
||||
applicant: '',
|
||||
phone: '',
|
||||
reason: '',
|
||||
status: 'pending',
|
||||
applyTime: ''
|
||||
})
|
||||
|
||||
const filteredData = computed(() => {
|
||||
let data = mockData.value
|
||||
|
||||
if (searchQuery.value) {
|
||||
data = data.filter(
|
||||
(item) =>
|
||||
item.requestNo.includes(searchQuery.value) ||
|
||||
item.oldIccid.includes(searchQuery.value) ||
|
||||
(item.newIccid && item.newIccid.includes(searchQuery.value))
|
||||
)
|
||||
}
|
||||
|
||||
if (statusFilter.value) {
|
||||
data = data.filter((item) => item.status === statusFilter.value)
|
||||
}
|
||||
|
||||
return data
|
||||
})
|
||||
|
||||
const handleSearch = () => {}
|
||||
|
||||
const exportData = () => {
|
||||
ElMessage.success('数据导出中...')
|
||||
}
|
||||
|
||||
const viewDetail = (row: ReplacementRequest) => {
|
||||
currentRequest.value = { ...row }
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleProcess = (row: ReplacementRequest) => {
|
||||
ElMessageBox.confirm('确定要处理该换卡申请吗?', '处理确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info'
|
||||
}).then(() => {
|
||||
row.status = 'processing'
|
||||
row.processor = 'admin'
|
||||
ElMessage.success('已标记为处理中')
|
||||
})
|
||||
}
|
||||
|
||||
const fillNewIccid = (row: ReplacementRequest) => {
|
||||
currentRequest.value = { ...row }
|
||||
fillForm.newIccid = ''
|
||||
fillForm.remark = ''
|
||||
validationResult.value = ''
|
||||
fillDialogVisible.value = true
|
||||
}
|
||||
|
||||
const validateNewIccid = () => {
|
||||
if (!fillForm.newIccid) {
|
||||
ElMessage.warning('请先输入新卡ICCID')
|
||||
return
|
||||
}
|
||||
|
||||
if (fillForm.newIccid.length !== 20) {
|
||||
validationResult.value = 'error'
|
||||
validationMessage.value = 'ICCID长度必须为20位'
|
||||
return
|
||||
}
|
||||
|
||||
// 模拟验证
|
||||
setTimeout(() => {
|
||||
const exists = mockData.value.some((item) => item.oldIccid === fillForm.newIccid)
|
||||
if (exists) {
|
||||
validationResult.value = 'error'
|
||||
validationMessage.value = '该ICCID已被使用'
|
||||
} else {
|
||||
validationResult.value = 'success'
|
||||
validationMessage.value = ''
|
||||
ElMessage.success('验证通过')
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const handleFillSubmit = async () => {
|
||||
if (!fillFormRef.value) return
|
||||
|
||||
if (validationResult.value !== 'success') {
|
||||
ElMessage.warning('请先验证新卡ICCID')
|
||||
return
|
||||
}
|
||||
|
||||
await fillFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
const index = mockData.value.findIndex((item) => item.id === currentRequest.value.id)
|
||||
if (index !== -1) {
|
||||
mockData.value[index].newIccid = fillForm.newIccid
|
||||
mockData.value[index].status = 'completed'
|
||||
mockData.value[index].processTime = new Date().toLocaleString('zh-CN')
|
||||
|
||||
statistics.processing--
|
||||
statistics.completed++
|
||||
}
|
||||
|
||||
fillDialogVisible.value = false
|
||||
ElMessage.success('新卡填充成功,换卡操作已完成')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleReject = (row: ReplacementRequest) => {
|
||||
currentRequest.value = { ...row }
|
||||
rejectForm.reason = ''
|
||||
rejectDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleRejectSubmit = async () => {
|
||||
if (!rejectFormRef.value) return
|
||||
await rejectFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
const index = mockData.value.findIndex((item) => item.id === currentRequest.value.id)
|
||||
if (index !== -1) {
|
||||
mockData.value[index].status = 'rejected'
|
||||
mockData.value[index].rejectReason = rejectForm.reason
|
||||
mockData.value[index].processor = 'admin'
|
||||
mockData.value[index].processTime = new Date().toLocaleString('zh-CN')
|
||||
|
||||
statistics.pending--
|
||||
statistics.rejected++
|
||||
}
|
||||
|
||||
rejectDialogVisible.value = false
|
||||
ElMessage.success('已拒绝该换卡申请')
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
.stat-card {
|
||||
:deep(.el-card__body) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 40px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
63
src/views/auth/forget-password/index.vue
Normal file
63
src/views/auth/forget-password/index.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="login register">
|
||||
<LoginLeftView></LoginLeftView>
|
||||
<div class="right-wrap">
|
||||
<div class="header">
|
||||
<ArtLogo class="icon" />
|
||||
<h1>{{ systemName }}</h1>
|
||||
</div>
|
||||
<div class="login-wrap">
|
||||
<div class="form">
|
||||
<h3 class="title">{{ $t('forgetPassword.title') }}</h3>
|
||||
<p class="sub-title">{{ $t('forgetPassword.subTitle') }}</p>
|
||||
<div class="input-wrap">
|
||||
<span class="input-label" v-if="showInputLabel">账号</span>
|
||||
<ElInput :placeholder="$t('forgetPassword.placeholder')" v-model.trim="username" />
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 15px">
|
||||
<ElButton
|
||||
class="login-btn"
|
||||
type="primary"
|
||||
@click="register"
|
||||
:loading="loading"
|
||||
v-ripple
|
||||
>
|
||||
{{ $t('forgetPassword.submitBtnText') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 15px">
|
||||
<ElButton class="back-btn" plain @click="toLogin">
|
||||
{{ $t('forgetPassword.backBtnText') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AppConfig from '@/config'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
|
||||
defineOptions({ name: 'ForgetPassword' })
|
||||
|
||||
const router = useRouter()
|
||||
const showInputLabel = ref(false)
|
||||
|
||||
const systemName = AppConfig.systemInfo.name
|
||||
const username = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
const register = async () => {}
|
||||
|
||||
const toLogin = () => {
|
||||
router.push(RoutesAlias.Login)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use '../login/index';
|
||||
</style>
|
||||
232
src/views/auth/login/index.scss
Normal file
232
src/views/auth/login/index.scss
Normal file
@@ -0,0 +1,232 @@
|
||||
@use '@styles/variables.scss' as *;
|
||||
|
||||
.login {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
|
||||
.el-input__inner {
|
||||
&:focus {
|
||||
border: 1px solid #4e83fd;
|
||||
}
|
||||
}
|
||||
|
||||
.el-input--medium .el-input__inner {
|
||||
height: var(--el-component-custom-height);
|
||||
line-height: var(--el-component-custom-height);
|
||||
}
|
||||
|
||||
.right-wrap {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
|
||||
.top-right-wrap {
|
||||
position: fixed;
|
||||
top: 30px;
|
||||
right: 30px;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 5px;
|
||||
margin-left: 15px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all 0.3s;
|
||||
|
||||
i {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--main-color) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.login-wrap {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 440px;
|
||||
height: 610px;
|
||||
padding: 0 5px;
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
background-size: cover;
|
||||
border-radius: 5px;
|
||||
|
||||
.form {
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
padding: 40px 0;
|
||||
widows: 100%;
|
||||
|
||||
.title {
|
||||
margin-left: -2px;
|
||||
font-size: 34px;
|
||||
font-weight: 600;
|
||||
color: var(--art-text-gray-900) !important;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
color: var(--art-text-gray-500) !important;
|
||||
}
|
||||
|
||||
.input-wrap {
|
||||
margin-top: 25px;
|
||||
|
||||
.input-label {
|
||||
display: block;
|
||||
padding-bottom: 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: var(--art-text-gray-800);
|
||||
}
|
||||
}
|
||||
|
||||
.account-select :deep(.el-select__wrapper),
|
||||
.el-input,
|
||||
.login-btn {
|
||||
height: 40px !important;
|
||||
}
|
||||
|
||||
.drag-verify {
|
||||
position: relative;
|
||||
padding-bottom: 20px;
|
||||
margin-top: 25px;
|
||||
|
||||
.drag-verify-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.error {
|
||||
border-color: #f56c6c;
|
||||
}
|
||||
}
|
||||
|
||||
.error-text {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
margin-top: 10px;
|
||||
font-size: 13px;
|
||||
color: #f56c6c;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.show-error-text {
|
||||
transform: translateY(40px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.forget-password {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
color: var(--art-text-gray-500);
|
||||
|
||||
a {
|
||||
color: var(--main-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
height: 40px !important;
|
||||
color: #fff;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
width: 100%;
|
||||
height: 40px !important;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
color: var(--art-text-gray-800);
|
||||
|
||||
a {
|
||||
color: var(--main-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $device-ipad-pro) {
|
||||
.login {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
|
||||
.right-wrap {
|
||||
margin: auto;
|
||||
|
||||
.login-wrap {
|
||||
position: relative;
|
||||
width: 440px;
|
||||
height: auto;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
|
||||
.form {
|
||||
margin-top: 10vh;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $device-phone) {
|
||||
.login {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
||||
.right-wrap {
|
||||
box-sizing: border-box;
|
||||
width: 100% !important;
|
||||
padding: 0 30px;
|
||||
margin: auto;
|
||||
|
||||
.login-wrap {
|
||||
width: 100%;
|
||||
|
||||
.form {
|
||||
margin-top: 12vh;
|
||||
|
||||
.input-wrap {
|
||||
.input-label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.input-wrap,
|
||||
.drag-verify {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
184
src/views/auth/login/index.vue
Normal file
184
src/views/auth/login/index.vue
Normal file
@@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<div class="login">
|
||||
<LoginLeftView></LoginLeftView>
|
||||
|
||||
<div class="right-wrap">
|
||||
<div class="top-right-wrap">
|
||||
<div class="btn theme-btn" @click="toggleTheme">
|
||||
<i class="iconfont-sys">
|
||||
{{ isDark ? '' : '' }}
|
||||
</i>
|
||||
</div>
|
||||
<ElDropdown @command="changeLanguage" popper-class="langDropDownStyle">
|
||||
<div class="btn language-btn">
|
||||
<i class="iconfont-sys icon-language"></i>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<div v-for="lang in languageOptions" :key="lang.value" class="lang-btn-item">
|
||||
<ElDropdownItem
|
||||
:command="lang.value"
|
||||
:class="{ 'is-selected': locale === lang.value }"
|
||||
>
|
||||
<span class="menu-txt">{{ lang.label }}</span>
|
||||
<i v-if="locale === lang.value" class="iconfont-sys icon-check"></i>
|
||||
</ElDropdownItem>
|
||||
</div>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
<div class="header">
|
||||
<ArtLogo class="icon" />
|
||||
<h1>{{ systemName }}</h1>
|
||||
</div>
|
||||
<div class="login-wrap">
|
||||
<div class="form">
|
||||
<h3 class="title">{{ $t('login.title') }}</h3>
|
||||
<p class="sub-title">{{ $t('login.subTitle') }}</p>
|
||||
<ElForm
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
@keyup.enter="handleSubmit"
|
||||
style="margin-top: 25px"
|
||||
>
|
||||
<!-- Mock 账号选择(仅在开发模式且使用 Mock 时显示) -->
|
||||
<ElFormItem prop="account" v-if="false">
|
||||
<ElSelect v-model="formData.account" @change="setupAccount" class="account-select">
|
||||
<ElOption
|
||||
v-for="account in mockAccounts"
|
||||
:key="account.key"
|
||||
:label="account.label"
|
||||
:value="account.key"
|
||||
>
|
||||
<span>{{ account.label }}</span>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="username">
|
||||
<ElInput :placeholder="$t('login.placeholder[0]')" v-model.trim="formData.username" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="password">
|
||||
<ElInput
|
||||
:placeholder="$t('login.placeholder[1]')"
|
||||
v-model.trim="formData.password"
|
||||
type="password"
|
||||
show-password
|
||||
radius="8px"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<div class="drag-verify">
|
||||
<div class="drag-verify-content" :class="{ error: !isPassing && isClickPass }">
|
||||
<ArtDragVerify
|
||||
ref="dragVerify"
|
||||
v-model:value="isPassing"
|
||||
:width="width < 500 ? 328 : 438"
|
||||
:text="$t('login.sliderText')"
|
||||
textColor="var(--art-gray-800)"
|
||||
:successText="$t('login.sliderSuccessText')"
|
||||
:progressBarBg="getCssVar('--el-color-primary')"
|
||||
background="var(--art-gray-200)"
|
||||
handlerBg="var(--art-main-bg-color)"
|
||||
/>
|
||||
</div>
|
||||
<p class="error-text" :class="{ 'show-error-text': !isPassing && isClickPass }">{{
|
||||
$t('login.placeholder[2]')
|
||||
}}</p>
|
||||
</div>
|
||||
|
||||
<div class="forget-password">
|
||||
<ElCheckbox v-model="formData.rememberPassword">{{
|
||||
$t('login.rememberPwd')
|
||||
}}</ElCheckbox>
|
||||
<RouterLink :to="RoutesAlias.ForgetPassword">{{ $t('login.forgetPwd') }}</RouterLink>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 30px">
|
||||
<ElButton
|
||||
class="login-btn"
|
||||
type="primary"
|
||||
@click="handleSubmit"
|
||||
:loading="loading"
|
||||
v-ripple
|
||||
>
|
||||
{{ $t('login.btnText') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>
|
||||
{{ $t('login.noAccount') }}
|
||||
<RouterLink :to="RoutesAlias.Register">{{ $t('login.register') }}</RouterLink>
|
||||
</p>
|
||||
</div>
|
||||
</ElForm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AppConfig from '@/config'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { getCssVar } from '@/utils/ui'
|
||||
import { languageOptions } from '@/locales'
|
||||
import { LanguageEnum, SystemThemeEnum } from '@/enums/appEnum'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useSettingStore } from '@/store/modules/setting'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
import { useLogin } from '@/composables/useLogin'
|
||||
|
||||
defineOptions({ name: 'Login' })
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const settingStore = useSettingStore()
|
||||
const userStore = useUserStore()
|
||||
const { isDark, systemThemeType } = storeToRefs(settingStore)
|
||||
|
||||
// 使用登录 Composable
|
||||
const {
|
||||
formRef,
|
||||
formData,
|
||||
rules,
|
||||
loading,
|
||||
isPassing,
|
||||
isClickPass,
|
||||
mockAccounts,
|
||||
setupAccount,
|
||||
handleLogin
|
||||
} = useLogin()
|
||||
|
||||
const dragVerify = ref()
|
||||
const systemName = AppConfig.systemInfo.name
|
||||
const { width } = useWindowSize()
|
||||
|
||||
// 处理提交
|
||||
const handleSubmit = async () => {
|
||||
await handleLogin()
|
||||
// 重置拖拽验证
|
||||
if (dragVerify.value) {
|
||||
dragVerify.value.reset()
|
||||
}
|
||||
}
|
||||
|
||||
// 切换语言
|
||||
const changeLanguage = (lang: LanguageEnum) => {
|
||||
if (locale.value === lang) return
|
||||
locale.value = lang
|
||||
userStore.setLanguage(lang)
|
||||
}
|
||||
|
||||
// 切换主题
|
||||
const toggleTheme = () => {
|
||||
let { LIGHT, DARK } = SystemThemeEnum
|
||||
useTheme().switchThemeStyles(systemThemeType.value === LIGHT ? DARK : LIGHT)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use './index';
|
||||
</style>
|
||||
29
src/views/auth/register/index.scss
Normal file
29
src/views/auth/register/index.scss
Normal file
@@ -0,0 +1,29 @@
|
||||
.register {
|
||||
.right-wrap {
|
||||
.login-wrap {
|
||||
.form {
|
||||
.el-form {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.privacy-policy {
|
||||
margin-top: 15px;
|
||||
|
||||
:deep(.el-checkbox__label) {
|
||||
color: #333 !important;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--main-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.register-btn {
|
||||
width: 100%;
|
||||
height: 40px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
173
src/views/auth/register/index.vue
Normal file
173
src/views/auth/register/index.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<div class="login register">
|
||||
<LoginLeftView></LoginLeftView>
|
||||
<div class="right-wrap">
|
||||
<div class="header">
|
||||
<ArtLogo class="icon" />
|
||||
<h1>{{ systemName }}</h1>
|
||||
</div>
|
||||
<div class="login-wrap">
|
||||
<div class="form">
|
||||
<h3 class="title">{{ $t('register.title') }}</h3>
|
||||
<p class="sub-title">{{ $t('register.subTitle') }}</p>
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-position="top">
|
||||
<ElFormItem prop="username">
|
||||
<ElInput
|
||||
v-model.trim="formData.username"
|
||||
:placeholder="$t('register.placeholder[0]')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem prop="password">
|
||||
<ElInput
|
||||
v-model.trim="formData.password"
|
||||
:placeholder="$t('register.placeholder[1]')"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem prop="confirmPassword">
|
||||
<ElInput
|
||||
v-model.trim="formData.confirmPassword"
|
||||
:placeholder="$t('register.placeholder[2]')"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
@keyup.enter="register"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem prop="agreement">
|
||||
<ElCheckbox v-model="formData.agreement">
|
||||
{{ $t('register.agreeText') }}
|
||||
<router-link
|
||||
style="color: var(--main-color); text-decoration: none"
|
||||
to="/privacy-policy"
|
||||
>{{ $t('register.privacyPolicy') }}</router-link
|
||||
>
|
||||
</ElCheckbox>
|
||||
</ElFormItem>
|
||||
|
||||
<div style="margin-top: 15px">
|
||||
<ElButton
|
||||
class="register-btn"
|
||||
type="primary"
|
||||
@click="register"
|
||||
:loading="loading"
|
||||
v-ripple
|
||||
>
|
||||
{{ $t('register.submitBtnText') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>
|
||||
{{ $t('register.hasAccount') }}
|
||||
<router-link :to="RoutesAlias.Login">{{ $t('register.toLogin') }}</router-link>
|
||||
</p>
|
||||
</div>
|
||||
</ElForm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AppConfig from '@/config'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
defineOptions({ name: 'Register' })
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const router = useRouter()
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const systemName = AppConfig.systemInfo.name
|
||||
const loading = ref(false)
|
||||
|
||||
const formData = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
agreement: false
|
||||
})
|
||||
|
||||
const validatePass = (rule: any, value: string, callback: any) => {
|
||||
if (value === '') {
|
||||
callback(new Error(t('register.placeholder[1]')))
|
||||
} else {
|
||||
if (formData.confirmPassword !== '') {
|
||||
formRef.value?.validateField('confirmPassword')
|
||||
}
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const validatePass2 = (rule: any, value: string, callback: any) => {
|
||||
if (value === '') {
|
||||
callback(new Error(t('register.rule[0]')))
|
||||
} else if (value !== formData.password) {
|
||||
callback(new Error(t('register.rule[1]')))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
username: [
|
||||
{ required: true, message: t('register.placeholder[0]'), trigger: 'blur' },
|
||||
{ min: 3, max: 20, message: t('register.rule[2]'), trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, validator: validatePass, trigger: 'blur' },
|
||||
{ min: 6, message: t('register.rule[3]'), trigger: 'blur' }
|
||||
],
|
||||
confirmPassword: [{ required: true, validator: validatePass2, trigger: 'blur' }],
|
||||
agreement: [
|
||||
{
|
||||
validator: (rule: any, value: boolean, callback: any) => {
|
||||
if (!value) {
|
||||
callback(new Error(t('register.rule[4]')))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const register = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
loading.value = true
|
||||
|
||||
// 模拟注册请求
|
||||
setTimeout(() => {
|
||||
loading.value = false
|
||||
ElMessage.success('注册成功')
|
||||
toLogin()
|
||||
}, 1000)
|
||||
} catch (error) {
|
||||
console.log('验证失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
const toLogin = () => {
|
||||
setTimeout(() => {
|
||||
router.push(RoutesAlias.Login)
|
||||
}, 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use '../login/index' as login;
|
||||
@use './index' as register;
|
||||
</style>
|
||||
546
src/views/batch/card-change-notice/index.vue
Normal file
546
src/views/batch/card-change-notice/index.vue
Normal file
@@ -0,0 +1,546 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<!-- 搜索和操作区 -->
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElInput v-model="searchQuery" placeholder="通知标题/内容" clearable />
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElSelect v-model="statusFilter" placeholder="状态筛选" clearable style="width: 100%">
|
||||
<ElOption label="全部" value="" />
|
||||
<ElOption label="待发送" value="pending" />
|
||||
<ElOption label="发送中" value="sending" />
|
||||
<ElOption label="已发送" value="sent" />
|
||||
<ElOption label="发送失败" value="failed" />
|
||||
</ElSelect>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElSelect v-model="typeFilter" placeholder="通知类型" clearable style="width: 100%">
|
||||
<ElOption label="全部" value="" />
|
||||
<ElOption label="卡片更换" value="replace" />
|
||||
<ElOption label="卡片激活" value="activate" />
|
||||
<ElOption label="卡片停用" value="deactivate" />
|
||||
<ElOption label="套餐变更" value="plan_change" />
|
||||
</ElSelect>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<ElButton v-ripple @click="handleSearch">搜索</ElButton>
|
||||
<ElButton v-ripple type="primary" @click="showDialog('add')">新增通知</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<!-- 通知列表 -->
|
||||
<ArtTable :data="filteredData" index style="margin-top: 20px">
|
||||
<template #default>
|
||||
<ElTableColumn type="selection" width="55" />
|
||||
<ElTableColumn label="通知标题" prop="title" min-width="200" show-overflow-tooltip />
|
||||
<ElTableColumn label="通知类型" prop="type" width="120">
|
||||
<template #default="scope">
|
||||
<ElTag :type="getTypeTagType(scope.row.type)">
|
||||
{{ getTypeText(scope.row.type) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="目标用户数" prop="targetCount" width="120">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-primary)">{{ scope.row.targetCount }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="已发送" prop="sentCount" width="100">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-success)">{{ scope.row.sentCount }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="失败数" prop="failCount" width="100">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-danger)">{{ scope.row.failCount }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="发送进度" prop="progress" width="150">
|
||||
<template #default="scope">
|
||||
<ElProgress
|
||||
:percentage="scope.row.progress"
|
||||
:status="scope.row.status === 'failed' ? 'exception' : scope.row.status === 'sent' ? 'success' : undefined"
|
||||
/>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" prop="status" width="120">
|
||||
<template #default="scope">
|
||||
<ElTag v-if="scope.row.status === 'pending'" type="info">待发送</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'sending'" type="warning">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
发送中
|
||||
</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'sent'" type="success">已发送</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'failed'" type="danger">发送失败</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="发送方式" prop="sendMethod" width="120">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; gap: 4px">
|
||||
<ElTag v-for="method in scope.row.sendMethods" :key="method" size="small">
|
||||
{{ getSendMethodText(method) }}
|
||||
</ElTag>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="createTime" width="180" />
|
||||
<ElTableColumn label="发送时间" prop="sendTime" width="180">
|
||||
<template #default="scope">
|
||||
{{ scope.row.sendTime || '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn fixed="right" label="操作" width="240">
|
||||
<template #default="scope">
|
||||
<el-button link :icon="View" @click="viewDetail(scope.row)">查看详情</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.status === 'pending'"
|
||||
link
|
||||
type="primary"
|
||||
:icon="Promotion"
|
||||
@click="handleSend(scope.row)"
|
||||
>
|
||||
立即发送
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.status === 'pending'"
|
||||
link
|
||||
@click="showDialog('edit', scope.row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.status === 'pending' || scope.row.status === 'failed'"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 新增/编辑通知对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增换卡通知' : '编辑换卡通知'"
|
||||
width="700px"
|
||||
align-center
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<ElFormItem label="通知标题" prop="title">
|
||||
<ElInput v-model="form.title" placeholder="请输入通知标题" maxlength="50" show-word-limit />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="通知类型" prop="type">
|
||||
<ElRadioGroup v-model="form.type">
|
||||
<ElRadio value="replace">卡片更换</ElRadio>
|
||||
<ElRadio value="activate">卡片激活</ElRadio>
|
||||
<ElRadio value="deactivate">卡片停用</ElRadio>
|
||||
<ElRadio value="plan_change">套餐变更</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="通知内容" prop="content">
|
||||
<ElInput
|
||||
v-model="form.content"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入通知内容"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="目标用户" prop="targetType">
|
||||
<ElRadioGroup v-model="form.targetType">
|
||||
<ElRadio value="all">全部用户</ElRadio>
|
||||
<ElRadio value="specific">指定用户</ElRadio>
|
||||
<ElRadio value="batch">批量导入</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="form.targetType === 'specific'" label="用户列表">
|
||||
<ElSelect v-model="form.targetUsers" multiple placeholder="请选择目标用户" style="width: 100%">
|
||||
<ElOption label="张三 (13800138000)" value="user1" />
|
||||
<ElOption label="李四 (13900139000)" value="user2" />
|
||||
<ElOption label="王五 (13700137000)" value="user3" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="form.targetType === 'batch'" label="用户文件">
|
||||
<ElUpload
|
||||
:action="uploadUrl"
|
||||
:limit="1"
|
||||
:on-exceed="handleExceed"
|
||||
accept=".xlsx,.xls,.txt"
|
||||
>
|
||||
<ElButton type="primary">选择文件</ElButton>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">支持 Excel 或 TXT 格式,每行一个用户手机号</div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="发送方式" prop="sendMethods">
|
||||
<ElCheckboxGroup v-model="form.sendMethods">
|
||||
<ElCheckbox value="sms">短信</ElCheckbox>
|
||||
<ElCheckbox value="email">邮件</ElCheckbox>
|
||||
<ElCheckbox value="app">App推送</ElCheckbox>
|
||||
</ElCheckboxGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="定时发送">
|
||||
<ElSwitch v-model="form.scheduleSend" />
|
||||
<span style="margin-left: 8px; color: var(--el-text-color-secondary)">
|
||||
{{ form.scheduleSend ? '启用定时发送' : '立即发送' }}
|
||||
</span>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="form.scheduleSend" label="发送时间" prop="scheduleTime">
|
||||
<ElDatePicker
|
||||
v-model="form.scheduleTime"
|
||||
type="datetime"
|
||||
placeholder="选择发送时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit(formRef)">
|
||||
{{ form.scheduleSend ? '创建定时任务' : '保存' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<ElDialog v-model="detailDialogVisible" title="通知详情" width="800px" align-center>
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem label="通知标题">{{ currentDetail.title }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="通知类型">
|
||||
<ElTag :type="getTypeTagType(currentDetail.type)">
|
||||
{{ getTypeText(currentDetail.type) }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="目标用户数">{{ currentDetail.targetCount }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="已发送数">
|
||||
<span style="color: var(--el-color-success)">{{ currentDetail.sentCount }}</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="失败数">
|
||||
<span style="color: var(--el-color-danger)">{{ currentDetail.failCount }}</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="发送状态">
|
||||
<ElTag v-if="currentDetail.status === 'sent'" type="success">已发送</ElTag>
|
||||
<ElTag v-else-if="currentDetail.status === 'sending'" type="warning">发送中</ElTag>
|
||||
<ElTag v-else-if="currentDetail.status === 'failed'" type="danger">发送失败</ElTag>
|
||||
<ElTag v-else type="info">待发送</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="发送方式" :span="2">
|
||||
<ElTag v-for="method in currentDetail.sendMethods" :key="method" size="small" style="margin-right: 4px">
|
||||
{{ getSendMethodText(method) }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="创建时间">{{ currentDetail.createTime }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="发送时间">{{ currentDetail.sendTime || '-' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="通知内容" :span="2">
|
||||
{{ currentDetail.content }}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="detailDialogVisible = false">关闭</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { View, Loading, Promotion } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules, UploadProps } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'CardChangeNotice' })
|
||||
|
||||
interface Notice {
|
||||
id: string
|
||||
title: string
|
||||
type: 'replace' | 'activate' | 'deactivate' | 'plan_change'
|
||||
content: string
|
||||
targetCount: number
|
||||
sentCount: number
|
||||
failCount: number
|
||||
progress: number
|
||||
status: 'pending' | 'sending' | 'sent' | 'failed'
|
||||
sendMethods: string[]
|
||||
createTime: string
|
||||
sendTime?: string
|
||||
}
|
||||
|
||||
const searchQuery = ref('')
|
||||
const statusFilter = ref('')
|
||||
const typeFilter = ref('')
|
||||
const dialogVisible = ref(false)
|
||||
const detailDialogVisible = ref(false)
|
||||
const dialogType = ref<'add' | 'edit'>('add')
|
||||
const formRef = ref<FormInstance>()
|
||||
const uploadUrl = ref('/api/batch/upload-users')
|
||||
|
||||
const form = reactive({
|
||||
title: '',
|
||||
type: 'replace',
|
||||
content: '',
|
||||
targetType: 'all',
|
||||
targetUsers: [] as string[],
|
||||
sendMethods: ['sms'],
|
||||
scheduleSend: false,
|
||||
scheduleTime: null as Date | null
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
title: [{ required: true, message: '请输入通知标题', trigger: 'blur' }],
|
||||
type: [{ required: true, message: '请选择通知类型', trigger: 'change' }],
|
||||
content: [{ required: true, message: '请输入通知内容', trigger: 'blur' }],
|
||||
targetType: [{ required: true, message: '请选择目标用户类型', trigger: 'change' }],
|
||||
sendMethods: [
|
||||
{
|
||||
type: 'array',
|
||||
required: true,
|
||||
message: '请至少选择一种发送方式',
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const mockData = ref<Notice[]>([
|
||||
{
|
||||
id: '1',
|
||||
title: 'SIM卡更换通知-2026年1月批次',
|
||||
type: 'replace',
|
||||
content: '尊敬的用户,您的物联网卡将于近期进行更换,新卡将在3个工作日内寄出,请注意查收。',
|
||||
targetCount: 1000,
|
||||
sentCount: 980,
|
||||
failCount: 20,
|
||||
progress: 100,
|
||||
status: 'sent',
|
||||
sendMethods: ['sms', 'app'],
|
||||
createTime: '2026-01-09 09:00:00',
|
||||
sendTime: '2026-01-09 10:00:00'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: '卡片激活成功通知',
|
||||
type: 'activate',
|
||||
content: '恭喜!您的物联网卡已成功激活,现在可以正常使用了。',
|
||||
targetCount: 500,
|
||||
sentCount: 350,
|
||||
failCount: 5,
|
||||
progress: 71,
|
||||
status: 'sending',
|
||||
sendMethods: ['sms', 'email'],
|
||||
createTime: '2026-01-09 11:30:00'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: '套餐变更提醒',
|
||||
type: 'plan_change',
|
||||
content: '您好,您的套餐将于2026年2月1日起变更为新套餐,详情请登录系统查看。',
|
||||
targetCount: 800,
|
||||
sentCount: 0,
|
||||
failCount: 0,
|
||||
progress: 0,
|
||||
status: 'pending',
|
||||
sendMethods: ['sms'],
|
||||
createTime: '2026-01-09 14:00:00'
|
||||
}
|
||||
])
|
||||
|
||||
const currentDetail = ref<Notice>({
|
||||
id: '',
|
||||
title: '',
|
||||
type: 'replace',
|
||||
content: '',
|
||||
targetCount: 0,
|
||||
sentCount: 0,
|
||||
failCount: 0,
|
||||
progress: 0,
|
||||
status: 'pending',
|
||||
sendMethods: [],
|
||||
createTime: ''
|
||||
})
|
||||
|
||||
const filteredData = computed(() => {
|
||||
let data = mockData.value
|
||||
|
||||
if (searchQuery.value) {
|
||||
data = data.filter(
|
||||
(item) => item.title.includes(searchQuery.value) || item.content.includes(searchQuery.value)
|
||||
)
|
||||
}
|
||||
|
||||
if (statusFilter.value) {
|
||||
data = data.filter((item) => item.status === statusFilter.value)
|
||||
}
|
||||
|
||||
if (typeFilter.value) {
|
||||
data = data.filter((item) => item.type === typeFilter.value)
|
||||
}
|
||||
|
||||
return data
|
||||
})
|
||||
|
||||
const getTypeText = (type: string) => {
|
||||
const map: Record<string, string> = {
|
||||
replace: '卡片更换',
|
||||
activate: '卡片激活',
|
||||
deactivate: '卡片停用',
|
||||
plan_change: '套餐变更'
|
||||
}
|
||||
return map[type] || '未知'
|
||||
}
|
||||
|
||||
const getTypeTagType = (type: string) => {
|
||||
const map: Record<string, any> = {
|
||||
replace: 'warning',
|
||||
activate: 'success',
|
||||
deactivate: 'danger',
|
||||
plan_change: 'primary'
|
||||
}
|
||||
return map[type] || ''
|
||||
}
|
||||
|
||||
const getSendMethodText = (method: string) => {
|
||||
const map: Record<string, string> = {
|
||||
sms: '短信',
|
||||
email: '邮件',
|
||||
app: 'App'
|
||||
}
|
||||
return map[method] || method
|
||||
}
|
||||
|
||||
const handleSearch = () => {}
|
||||
|
||||
const showDialog = (type: 'add' | 'edit', row?: Notice) => {
|
||||
dialogType.value = type
|
||||
dialogVisible.value = true
|
||||
if (type === 'edit' && row) {
|
||||
Object.assign(form, {
|
||||
title: row.title,
|
||||
type: row.type,
|
||||
content: row.content,
|
||||
targetType: 'all',
|
||||
sendMethods: row.sendMethods,
|
||||
scheduleSend: false,
|
||||
scheduleTime: null
|
||||
})
|
||||
} else {
|
||||
Object.assign(form, {
|
||||
title: '',
|
||||
type: 'replace',
|
||||
content: '',
|
||||
targetType: 'all',
|
||||
targetUsers: [],
|
||||
sendMethods: ['sms'],
|
||||
scheduleSend: false,
|
||||
scheduleTime: null
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
if (dialogType.value === 'add') {
|
||||
mockData.value.unshift({
|
||||
id: Date.now().toString(),
|
||||
title: form.title,
|
||||
type: form.type as any,
|
||||
content: form.content,
|
||||
targetCount: form.targetType === 'all' ? 1000 : form.targetUsers.length,
|
||||
sentCount: 0,
|
||||
failCount: 0,
|
||||
progress: 0,
|
||||
status: 'pending',
|
||||
sendMethods: form.sendMethods,
|
||||
createTime: new Date().toLocaleString('zh-CN')
|
||||
})
|
||||
ElMessage.success('通知创建成功')
|
||||
} else {
|
||||
ElMessage.success('通知更新成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
formEl.resetFields()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleSend = (row: Notice) => {
|
||||
ElMessageBox.confirm(`确定立即发送通知"${row.title}"吗?`, '发送确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
row.status = 'sending'
|
||||
ElMessage.success('通知发送中...')
|
||||
|
||||
// 模拟发送进度
|
||||
const timer = setInterval(() => {
|
||||
if (row.progress < 100) {
|
||||
row.progress += 10
|
||||
row.sentCount = Math.floor((row.targetCount * row.progress) / 100)
|
||||
} else {
|
||||
clearInterval(timer)
|
||||
row.status = 'sent'
|
||||
row.sendTime = new Date().toLocaleString('zh-CN')
|
||||
ElMessage.success('通知发送完成')
|
||||
}
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = (row: Notice) => {
|
||||
ElMessageBox.confirm('确定删除该通知吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(() => {
|
||||
const index = mockData.value.findIndex((item) => item.id === row.id)
|
||||
if (index !== -1) mockData.value.splice(index, 1)
|
||||
ElMessage.success('删除成功')
|
||||
})
|
||||
}
|
||||
|
||||
const viewDetail = (row: Notice) => {
|
||||
currentDetail.value = { ...row }
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleExceed: UploadProps['onExceed'] = () => {
|
||||
ElMessage.warning('最多只能上传1个文件')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
:deep(.is-loading) {
|
||||
animation: rotating 2s linear infinite;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
@keyframes rotating {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
480
src/views/batch/device-import/index.vue
Normal file
480
src/views/batch/device-import/index.vue
Normal file
@@ -0,0 +1,480 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<!-- 导入操作区 -->
|
||||
<ElCard shadow="never">
|
||||
<template #header>
|
||||
<span style="font-weight: 500">批量导入设备</span>
|
||||
</template>
|
||||
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :xs="24" :lg="12">
|
||||
<ElAlert type="info" :closable="false" style="margin-bottom: 20px">
|
||||
<template #title>
|
||||
<div style="line-height: 1.8">
|
||||
<p><strong>导入说明:</strong></p>
|
||||
<p>1. 请先下载模板文件,按照模板格式填写设备信息</p>
|
||||
<p>2. 支持 Excel 格式(.xlsx, .xls),单次最多导入 500 条</p>
|
||||
<p>3. 必填字段:设备编号、设备名称、设备类型、ICCID(绑定网卡)</p>
|
||||
<p>4. ICCID 必须在系统中已存在,否则导入失败</p>
|
||||
<p>5. 设备编号重复将自动跳过</p>
|
||||
</div>
|
||||
</template>
|
||||
</ElAlert>
|
||||
|
||||
<ElButton type="primary" :icon="Download" @click="downloadTemplate">下载导入模板</ElButton>
|
||||
</ElCol>
|
||||
|
||||
<ElCol :xs="24" :lg="12">
|
||||
<div class="upload-area">
|
||||
<ElUpload
|
||||
ref="uploadRef"
|
||||
drag
|
||||
:action="uploadUrl"
|
||||
:on-change="handleFileChange"
|
||||
:on-success="handleUploadSuccess"
|
||||
:on-error="handleUploadError"
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
accept=".xlsx,.xls"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">只能上传 xlsx/xls 文件,且不超过 5MB</div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
<div style="margin-top: 16px; text-align: center">
|
||||
<ElButton type="success" :loading="uploading" :disabled="!fileList.length" @click="submitUpload">
|
||||
开始导入
|
||||
</ElButton>
|
||||
<ElButton @click="clearFiles">清空</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</ElCard>
|
||||
|
||||
<!-- 导入统计 -->
|
||||
<ElRow :gutter="20" style="margin-top: 20px">
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover" class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">今日导入</div>
|
||||
<div class="stat-value">1,250</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon" style="color: var(--el-color-primary)"><Upload /></el-icon>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover" class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">成功绑定</div>
|
||||
<div class="stat-value" style="color: var(--el-color-success)">1,180</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon" style="color: var(--el-color-success)"><SuccessFilled /></el-icon>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover" class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">导入失败</div>
|
||||
<div class="stat-value" style="color: var(--el-color-danger)">70</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon" style="color: var(--el-color-danger)"><CircleCloseFilled /></el-icon>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover" class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">成功率</div>
|
||||
<div class="stat-value">94.4%</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon" style="color: var(--el-color-warning)"><TrendCharts /></el-icon>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<!-- 导入记录 -->
|
||||
<ElCard shadow="never" style="margin-top: 20px">
|
||||
<template #header>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center">
|
||||
<span style="font-weight: 500">导入记录</span>
|
||||
<div>
|
||||
<ElSelect v-model="statusFilter" placeholder="状态筛选" style="width: 120px; margin-right: 12px" clearable>
|
||||
<ElOption label="全部" value="" />
|
||||
<ElOption label="处理中" value="processing" />
|
||||
<ElOption label="完成" value="success" />
|
||||
<ElOption label="失败" value="failed" />
|
||||
</ElSelect>
|
||||
<ElButton size="small" @click="refreshList">刷新</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ArtTable :data="filteredRecords" index>
|
||||
<template #default>
|
||||
<ElTableColumn label="导入批次号" prop="batchNo" width="180" />
|
||||
<ElTableColumn label="文件名" prop="fileName" min-width="200" show-overflow-tooltip />
|
||||
<ElTableColumn label="设备总数" prop="totalCount" width="100" />
|
||||
<ElTableColumn label="成功数" prop="successCount" width="100">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-success)">{{ scope.row.successCount }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="失败数" prop="failCount" width="100">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-danger)">{{ scope.row.failCount }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="已绑定ICCID" prop="bindCount" width="120">
|
||||
<template #default="scope">
|
||||
<ElTag type="success" size="small">{{ scope.row.bindCount }}</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="导入状态" prop="status" width="120">
|
||||
<template #default="scope">
|
||||
<ElTag v-if="scope.row.status === 'processing'" type="warning">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
处理中
|
||||
</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'success'" type="success">完成</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'failed'" type="danger">失败</ElTag>
|
||||
<ElTag v-else type="info">待处理</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="导入进度" prop="progress" width="150">
|
||||
<template #default="scope">
|
||||
<ElProgress
|
||||
:percentage="scope.row.progress"
|
||||
:status="scope.row.status === 'failed' ? 'exception' : undefined"
|
||||
/>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="导入时间" prop="importTime" width="180" />
|
||||
<ElTableColumn label="操作人" prop="operator" width="120" />
|
||||
<ElTableColumn fixed="right" label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<el-button link :icon="View" @click="viewDetail(scope.row)">查看详情</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.failCount > 0"
|
||||
link
|
||||
type="primary"
|
||||
:icon="Download"
|
||||
@click="downloadFailData(scope.row)"
|
||||
>
|
||||
失败数据
|
||||
</el-button>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 导入详情对话框 -->
|
||||
<ElDialog v-model="detailDialogVisible" title="设备导入详情" width="900px" align-center>
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem label="批次号">{{ currentDetail.batchNo }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="文件名">{{ currentDetail.fileName }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="设备总数">{{ currentDetail.totalCount }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="成功导入">
|
||||
<span style="color: var(--el-color-success)">{{ currentDetail.successCount }}</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="导入失败">
|
||||
<span style="color: var(--el-color-danger)">{{ currentDetail.failCount }}</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="已绑定ICCID">
|
||||
<ElTag type="success">{{ currentDetail.bindCount }}</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="导入时间">{{ currentDetail.importTime }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="操作人">{{ currentDetail.operator }}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<ElDivider content-position="left">失败明细</ElDivider>
|
||||
<div v-if="currentDetail.failReasons && currentDetail.failReasons.length" style="max-height: 300px; overflow-y: auto">
|
||||
<ElTable :data="currentDetail.failReasons" border size="small">
|
||||
<ElTableColumn label="行号" prop="row" width="80" />
|
||||
<ElTableColumn label="设备编号" prop="deviceCode" width="150" />
|
||||
<ElTableColumn label="ICCID" prop="iccid" width="200" />
|
||||
<ElTableColumn label="失败原因" prop="message" min-width="200" />
|
||||
</ElTable>
|
||||
</div>
|
||||
<ElEmpty v-else description="无失败记录" />
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="detailDialogVisible = false">关闭</ElButton>
|
||||
<ElButton v-if="currentDetail.failCount > 0" type="primary" :icon="Download" @click="downloadFailData(currentDetail)">
|
||||
下载失败数据
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
Download,
|
||||
UploadFilled,
|
||||
View,
|
||||
Loading,
|
||||
Upload,
|
||||
SuccessFilled,
|
||||
CircleCloseFilled,
|
||||
TrendCharts
|
||||
} from '@element-plus/icons-vue'
|
||||
import type { UploadInstance, UploadRawFile } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'DeviceImport' })
|
||||
|
||||
interface FailReason {
|
||||
row: number
|
||||
deviceCode: string
|
||||
iccid: string
|
||||
message: string
|
||||
}
|
||||
|
||||
interface ImportRecord {
|
||||
id: string
|
||||
batchNo: string
|
||||
fileName: string
|
||||
totalCount: number
|
||||
successCount: number
|
||||
failCount: number
|
||||
bindCount: number
|
||||
status: 'pending' | 'processing' | 'success' | 'failed'
|
||||
progress: number
|
||||
importTime: string
|
||||
operator: string
|
||||
failReasons?: FailReason[]
|
||||
}
|
||||
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
const uploadUrl = ref('/api/batch/device-import')
|
||||
const fileList = ref<UploadRawFile[]>([])
|
||||
const uploading = ref(false)
|
||||
const detailDialogVisible = ref(false)
|
||||
const statusFilter = ref('')
|
||||
|
||||
const importRecords = ref<ImportRecord[]>([
|
||||
{
|
||||
id: '1',
|
||||
batchNo: 'DEV20260109001',
|
||||
fileName: '设备导入模板_20260109.xlsx',
|
||||
totalCount: 300,
|
||||
successCount: 285,
|
||||
failCount: 15,
|
||||
bindCount: 285,
|
||||
status: 'success',
|
||||
progress: 100,
|
||||
importTime: '2026-01-09 11:30:00',
|
||||
operator: 'admin',
|
||||
failReasons: [
|
||||
{ row: 12, deviceCode: 'DEV001', iccid: '89860123456789012345', message: 'ICCID 不存在' },
|
||||
{ row: 23, deviceCode: 'DEV002', iccid: '89860123456789012346', message: '设备编号已存在' },
|
||||
{ row: 45, deviceCode: '', iccid: '89860123456789012347', message: '设备编号为空' },
|
||||
{ row: 67, deviceCode: 'DEV003', iccid: '', message: 'ICCID 为空' },
|
||||
{ row: 89, deviceCode: 'DEV004', iccid: '89860123456789012348', message: '设备类型不存在' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
batchNo: 'DEV20260108001',
|
||||
fileName: '智能水表设备批量导入.xlsx',
|
||||
totalCount: 150,
|
||||
successCount: 150,
|
||||
failCount: 0,
|
||||
bindCount: 150,
|
||||
status: 'success',
|
||||
progress: 100,
|
||||
importTime: '2026-01-08 14:20:00',
|
||||
operator: 'admin'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
batchNo: 'DEV20260107001',
|
||||
fileName: 'GPS定位器导入.xlsx',
|
||||
totalCount: 200,
|
||||
successCount: 180,
|
||||
failCount: 20,
|
||||
bindCount: 180,
|
||||
status: 'success',
|
||||
progress: 100,
|
||||
importTime: '2026-01-07 10:15:00',
|
||||
operator: 'operator01',
|
||||
failReasons: [
|
||||
{ row: 10, deviceCode: 'GPS001', iccid: '89860123456789012349', message: 'ICCID 已被其他设备绑定' },
|
||||
{ row: 20, deviceCode: 'GPS002', iccid: '89860123456789012350', message: 'ICCID 状态异常' }
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
const currentDetail = ref<ImportRecord>({
|
||||
id: '',
|
||||
batchNo: '',
|
||||
fileName: '',
|
||||
totalCount: 0,
|
||||
successCount: 0,
|
||||
failCount: 0,
|
||||
bindCount: 0,
|
||||
status: 'pending',
|
||||
progress: 0,
|
||||
importTime: '',
|
||||
operator: ''
|
||||
})
|
||||
|
||||
const filteredRecords = computed(() => {
|
||||
if (!statusFilter.value) return importRecords.value
|
||||
return importRecords.value.filter((item) => item.status === statusFilter.value)
|
||||
})
|
||||
|
||||
const downloadTemplate = () => {
|
||||
ElMessage.success('模板下载中...')
|
||||
setTimeout(() => {
|
||||
ElMessage.success('设备导入模板下载成功')
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const handleFileChange = (file: any, files: any[]) => {
|
||||
fileList.value = files
|
||||
}
|
||||
|
||||
const clearFiles = () => {
|
||||
uploadRef.value?.clearFiles()
|
||||
fileList.value = []
|
||||
}
|
||||
|
||||
const submitUpload = async () => {
|
||||
if (!fileList.value.length) {
|
||||
ElMessage.warning('请先选择文件')
|
||||
return
|
||||
}
|
||||
|
||||
uploading.value = true
|
||||
ElMessage.info('正在导入设备并绑定ICCID,请稍候...')
|
||||
|
||||
// 模拟上传和导入过程
|
||||
setTimeout(() => {
|
||||
const newRecord: ImportRecord = {
|
||||
id: Date.now().toString(),
|
||||
batchNo: `DEV${new Date().getTime()}`,
|
||||
fileName: fileList.value[0].name,
|
||||
totalCount: 100,
|
||||
successCount: 95,
|
||||
failCount: 5,
|
||||
bindCount: 95,
|
||||
status: 'success',
|
||||
progress: 100,
|
||||
importTime: new Date().toLocaleString('zh-CN'),
|
||||
operator: 'admin',
|
||||
failReasons: [
|
||||
{ row: 12, deviceCode: 'TEST001', iccid: '89860123456789012351', message: 'ICCID 不存在' },
|
||||
{ row: 34, deviceCode: 'TEST002', iccid: '89860123456789012352', message: '设备类型无效' }
|
||||
]
|
||||
}
|
||||
|
||||
importRecords.value.unshift(newRecord)
|
||||
uploading.value = false
|
||||
clearFiles()
|
||||
ElMessage.success(
|
||||
`导入完成!成功 ${newRecord.successCount} 条,失败 ${newRecord.failCount} 条,已绑定 ${newRecord.bindCount} 个ICCID`
|
||||
)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
const handleUploadSuccess = () => {
|
||||
ElMessage.success('上传成功')
|
||||
}
|
||||
|
||||
const handleUploadError = () => {
|
||||
uploading.value = false
|
||||
ElMessage.error('上传失败')
|
||||
}
|
||||
|
||||
const refreshList = () => {
|
||||
ElMessage.success('刷新成功')
|
||||
}
|
||||
|
||||
const viewDetail = (row: ImportRecord) => {
|
||||
currentDetail.value = { ...row }
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
const downloadFailData = (row: ImportRecord) => {
|
||||
ElMessage.info(`正在下载批次 ${row.batchNo} 的失败数据...`)
|
||||
setTimeout(() => {
|
||||
ElMessage.success('失败数据下载完成')
|
||||
}, 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
.upload-area {
|
||||
:deep(.el-upload) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-upload-dragger) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-icon--upload) {
|
||||
font-size: 67px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-upload__text) {
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 14px;
|
||||
|
||||
em {
|
||||
color: var(--el-color-primary);
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.is-loading) {
|
||||
animation: rotating 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotating {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
:deep(.el-card__body) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 40px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
366
src/views/batch/sim-import/index.vue
Normal file
366
src/views/batch/sim-import/index.vue
Normal file
@@ -0,0 +1,366 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<!-- 导入操作区 -->
|
||||
<ElCard shadow="never">
|
||||
<template #header>
|
||||
<span style="font-weight: 500">批量导入网卡</span>
|
||||
</template>
|
||||
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :xs="24" :lg="12">
|
||||
<ElAlert type="info" :closable="false" style="margin-bottom: 20px">
|
||||
<template #title>
|
||||
<div style="line-height: 1.8">
|
||||
<p><strong>导入说明:</strong></p>
|
||||
<p>1. 请先下载模板文件,按照模板格式填写网卡信息</p>
|
||||
<p>2. 支持 Excel 格式(.xlsx, .xls),单次最多导入 1000 条</p>
|
||||
<p>3. 必填字段:ICCID、运营商、套餐类型、流量规格</p>
|
||||
<p>4. 导入后系统将自动校验数据,重复 ICCID 将跳过</p>
|
||||
</div>
|
||||
</template>
|
||||
</ElAlert>
|
||||
|
||||
<ElButton type="primary" :icon="Download" @click="downloadTemplate">下载导入模板</ElButton>
|
||||
</ElCol>
|
||||
|
||||
<ElCol :xs="24" :lg="12">
|
||||
<div class="upload-area">
|
||||
<ElUpload
|
||||
ref="uploadRef"
|
||||
drag
|
||||
:action="uploadUrl"
|
||||
:on-change="handleFileChange"
|
||||
:on-success="handleUploadSuccess"
|
||||
:on-error="handleUploadError"
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
accept=".xlsx,.xls"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">只能上传 xlsx/xls 文件,且不超过 5MB</div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
<div style="margin-top: 16px; text-align: center">
|
||||
<ElButton type="success" :loading="uploading" :disabled="!fileList.length" @click="submitUpload">
|
||||
开始导入
|
||||
</ElButton>
|
||||
<ElButton @click="clearFiles">清空</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</ElCard>
|
||||
|
||||
<!-- 导入记录 -->
|
||||
<ElCard shadow="never" style="margin-top: 20px">
|
||||
<template #header>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center">
|
||||
<span style="font-weight: 500">导入记录</span>
|
||||
<ElButton size="small" @click="refreshList">刷新</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ArtTable :data="importRecords" index>
|
||||
<template #default>
|
||||
<ElTableColumn label="导入批次号" prop="batchNo" width="180" />
|
||||
<ElTableColumn label="文件名" prop="fileName" min-width="200" show-overflow-tooltip />
|
||||
<ElTableColumn label="总条数" prop="totalCount" width="100" />
|
||||
<ElTableColumn label="成功数" prop="successCount" width="100">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-success)">{{ scope.row.successCount }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="失败数" prop="failCount" width="100">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-danger)">{{ scope.row.failCount }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="导入状态" prop="status" width="120">
|
||||
<template #default="scope">
|
||||
<ElTag v-if="scope.row.status === 'processing'" type="warning">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
处理中
|
||||
</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'success'" type="success">完成</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'failed'" type="danger">失败</ElTag>
|
||||
<ElTag v-else type="info">待处理</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="导入进度" prop="progress" width="150">
|
||||
<template #default="scope">
|
||||
<ElProgress
|
||||
:percentage="scope.row.progress"
|
||||
:status="scope.row.status === 'failed' ? 'exception' : undefined"
|
||||
/>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="导入时间" prop="importTime" width="180" />
|
||||
<ElTableColumn label="操作人" prop="operator" width="120" />
|
||||
<ElTableColumn fixed="right" label="操作" width="180">
|
||||
<template #default="scope">
|
||||
<el-button link :icon="View" @click="viewDetail(scope.row)">查看详情</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.failCount > 0"
|
||||
link
|
||||
type="primary"
|
||||
:icon="Download"
|
||||
@click="downloadFailData(scope.row)"
|
||||
>
|
||||
下载失败数据
|
||||
</el-button>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 导入详情对话框 -->
|
||||
<ElDialog v-model="detailDialogVisible" title="导入详情" width="800px" align-center>
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem label="批次号">{{ currentDetail.batchNo }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="文件名">{{ currentDetail.fileName }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="总条数">{{ currentDetail.totalCount }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="成功数">
|
||||
<span style="color: var(--el-color-success)">{{ currentDetail.successCount }}</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="失败数">
|
||||
<span style="color: var(--el-color-danger)">{{ currentDetail.failCount }}</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="跳过数">{{ currentDetail.skipCount || 0 }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="导入时间">{{ currentDetail.importTime }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="操作人">{{ currentDetail.operator }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="失败原因" :span="2">
|
||||
<div v-if="currentDetail.failReasons && currentDetail.failReasons.length">
|
||||
<div v-for="(reason, index) in currentDetail.failReasons" :key="index" style="margin-bottom: 4px">
|
||||
<ElTag type="danger" size="small">行{{ reason.row }}</ElTag>
|
||||
{{ reason.message }}
|
||||
</div>
|
||||
</div>
|
||||
<span v-else style="color: var(--el-text-color-secondary)">无</span>
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="detailDialogVisible = false">关闭</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Download, UploadFilled, View, Loading } from '@element-plus/icons-vue'
|
||||
import type { UploadInstance, UploadRawFile } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'SimImport' })
|
||||
|
||||
interface ImportRecord {
|
||||
id: string
|
||||
batchNo: string
|
||||
fileName: string
|
||||
totalCount: number
|
||||
successCount: number
|
||||
failCount: number
|
||||
skipCount?: number
|
||||
status: 'pending' | 'processing' | 'success' | 'failed'
|
||||
progress: number
|
||||
importTime: string
|
||||
operator: string
|
||||
failReasons?: Array<{ row: number; message: string }>
|
||||
}
|
||||
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
const uploadUrl = ref('/api/batch/sim-import')
|
||||
const fileList = ref<UploadRawFile[]>([])
|
||||
const uploading = ref(false)
|
||||
const detailDialogVisible = ref(false)
|
||||
|
||||
const importRecords = ref<ImportRecord[]>([
|
||||
{
|
||||
id: '1',
|
||||
batchNo: 'IMP20260109001',
|
||||
fileName: '网卡导入模板_20260109.xlsx',
|
||||
totalCount: 500,
|
||||
successCount: 495,
|
||||
failCount: 5,
|
||||
skipCount: 0,
|
||||
status: 'success',
|
||||
progress: 100,
|
||||
importTime: '2026-01-09 10:30:00',
|
||||
operator: 'admin',
|
||||
failReasons: [
|
||||
{ row: 23, message: 'ICCID 格式错误' },
|
||||
{ row: 45, message: 'ICCID 已存在' },
|
||||
{ row: 67, message: '套餐类型不存在' },
|
||||
{ row: 89, message: '流量规格格式错误' },
|
||||
{ row: 123, message: '运营商代码无效' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
batchNo: 'IMP20260108001',
|
||||
fileName: '网卡批量导入.xlsx',
|
||||
totalCount: 1000,
|
||||
successCount: 1000,
|
||||
failCount: 0,
|
||||
skipCount: 0,
|
||||
status: 'success',
|
||||
progress: 100,
|
||||
importTime: '2026-01-08 15:20:00',
|
||||
operator: 'admin'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
batchNo: 'IMP20260107001',
|
||||
fileName: '测试数据.xlsx',
|
||||
totalCount: 200,
|
||||
successCount: 150,
|
||||
failCount: 50,
|
||||
skipCount: 0,
|
||||
status: 'success',
|
||||
progress: 100,
|
||||
importTime: '2026-01-07 09:15:00',
|
||||
operator: 'operator01',
|
||||
failReasons: [
|
||||
{ row: 10, message: 'ICCID 重复' },
|
||||
{ row: 20, message: '运营商字段为空' }
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
const currentDetail = ref<ImportRecord>({
|
||||
id: '',
|
||||
batchNo: '',
|
||||
fileName: '',
|
||||
totalCount: 0,
|
||||
successCount: 0,
|
||||
failCount: 0,
|
||||
status: 'pending',
|
||||
progress: 0,
|
||||
importTime: '',
|
||||
operator: ''
|
||||
})
|
||||
|
||||
const downloadTemplate = () => {
|
||||
ElMessage.success('模板下载中...')
|
||||
// 实际项目中应该调用下载接口
|
||||
setTimeout(() => {
|
||||
ElMessage.success('模板下载成功')
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const handleFileChange = (file: any, files: any[]) => {
|
||||
fileList.value = files
|
||||
}
|
||||
|
||||
const clearFiles = () => {
|
||||
uploadRef.value?.clearFiles()
|
||||
fileList.value = []
|
||||
}
|
||||
|
||||
const submitUpload = async () => {
|
||||
if (!fileList.value.length) {
|
||||
ElMessage.warning('请先选择文件')
|
||||
return
|
||||
}
|
||||
|
||||
uploading.value = true
|
||||
ElMessage.info('正在导入,请稍候...')
|
||||
|
||||
// 模拟上传和导入过程
|
||||
setTimeout(() => {
|
||||
const newRecord: ImportRecord = {
|
||||
id: Date.now().toString(),
|
||||
batchNo: `IMP${new Date().getTime()}`,
|
||||
fileName: fileList.value[0].name,
|
||||
totalCount: 300,
|
||||
successCount: 295,
|
||||
failCount: 5,
|
||||
skipCount: 0,
|
||||
status: 'success',
|
||||
progress: 100,
|
||||
importTime: new Date().toLocaleString('zh-CN'),
|
||||
operator: 'admin',
|
||||
failReasons: [
|
||||
{ row: 12, message: 'ICCID 格式错误' },
|
||||
{ row: 34, message: 'ICCID 已存在' }
|
||||
]
|
||||
}
|
||||
|
||||
importRecords.value.unshift(newRecord)
|
||||
uploading.value = false
|
||||
clearFiles()
|
||||
ElMessage.success(`导入完成!成功 ${newRecord.successCount} 条,失败 ${newRecord.failCount} 条`)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
const handleUploadSuccess = () => {
|
||||
ElMessage.success('上传成功')
|
||||
}
|
||||
|
||||
const handleUploadError = () => {
|
||||
uploading.value = false
|
||||
ElMessage.error('上传失败')
|
||||
}
|
||||
|
||||
const refreshList = () => {
|
||||
ElMessage.success('刷新成功')
|
||||
}
|
||||
|
||||
const viewDetail = (row: ImportRecord) => {
|
||||
currentDetail.value = { ...row }
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
const downloadFailData = (row: ImportRecord) => {
|
||||
ElMessage.info(`正在下载批次 ${row.batchNo} 的失败数据...`)
|
||||
setTimeout(() => {
|
||||
ElMessage.success('下载完成')
|
||||
}, 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
.upload-area {
|
||||
:deep(.el-upload) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-upload-dragger) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-icon--upload) {
|
||||
font-size: 67px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-upload__text) {
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 14px;
|
||||
|
||||
em {
|
||||
color: var(--el-color-primary);
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.is-loading) {
|
||||
animation: rotating 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotating {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
383
src/views/card-management/_template.vue
Normal file
383
src/views/card-management/_template.vue
Normal file
@@ -0,0 +1,383 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="card-template-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton type="primary" @click="showDialog('add')">新增</ElButton>
|
||||
<ElButton @click="batchOperation" :disabled="selectedRows.length === 0"
|
||||
>批量操作</ElButton
|
||||
>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 操作对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增' : '编辑'"
|
||||
width="400px"
|
||||
align-center
|
||||
>
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="100px">
|
||||
<ElFormItem label="名称" prop="name">
|
||||
<ElInput v-model="formData.name" placeholder="请输入名称" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="描述" prop="description">
|
||||
<ElInput
|
||||
v-model="formData.description"
|
||||
type="textarea"
|
||||
placeholder="请输入描述"
|
||||
:rows="3"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ElTag, ElMessageBox, ElMessage, FormInstance } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
|
||||
defineOptions({ name: 'CardTemplate' })
|
||||
|
||||
const dialogType = ref('add')
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
name: '',
|
||||
status: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
name: '示例项目1',
|
||||
description: '这是一个示例项目',
|
||||
status: '1',
|
||||
statusName: '正常',
|
||||
createTime: '2024-11-07 10:00:00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '示例项目2',
|
||||
description: '这是另一个示例项目',
|
||||
status: '2',
|
||||
statusName: '禁用',
|
||||
createTime: '2024-11-06 15:30:00'
|
||||
}
|
||||
]
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1
|
||||
getDataList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1
|
||||
getDataList()
|
||||
}
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '名称',
|
||||
prop: 'name',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入名称'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请选择状态'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '正常', value: '1' },
|
||||
{ label: '禁用', value: '2' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: 'ID', prop: 'id' },
|
||||
{ label: '名称', prop: 'name' },
|
||||
{ label: '描述', prop: 'description' },
|
||||
{ label: '状态', prop: 'status' },
|
||||
{ label: '创建时间', prop: 'createTime' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 获取标签类型
|
||||
const getTagType = (status: string) => {
|
||||
switch (status) {
|
||||
case '1':
|
||||
return 'success'
|
||||
case '2':
|
||||
return 'danger'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 构建标签文本
|
||||
const buildTagText = (status: string) => {
|
||||
switch (status) {
|
||||
case '1':
|
||||
return '正常'
|
||||
case '2':
|
||||
return '禁用'
|
||||
default:
|
||||
return '未知'
|
||||
}
|
||||
}
|
||||
|
||||
// 显示对话框
|
||||
const showDialog = (type: string, row?: any) => {
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
|
||||
if (type === 'edit' && row) {
|
||||
formData.name = row.name
|
||||
formData.description = row.description
|
||||
} else {
|
||||
formData.name = ''
|
||||
formData.description = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 删除操作
|
||||
const deleteItem = () => {
|
||||
ElMessageBox.confirm('确定要删除该项目吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(() => {
|
||||
ElMessage.success('删除成功')
|
||||
getDataList()
|
||||
})
|
||||
}
|
||||
|
||||
// 批量操作
|
||||
const batchOperation = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要操作的项目')
|
||||
return
|
||||
}
|
||||
ElMessage.info(`已选择 ${selectedRows.value.length} 个项目`)
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'id',
|
||||
label: 'ID',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
prop: 'name',
|
||||
label: '名称',
|
||||
minWidth: 120
|
||||
},
|
||||
{
|
||||
prop: 'description',
|
||||
label: '描述',
|
||||
minWidth: 200
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 100,
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getTagType(row.status) }, () => buildTagText(row.status))
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'createTime',
|
||||
label: '创建时间',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 150,
|
||||
formatter: (row: any) => {
|
||||
return h('div', [
|
||||
h(ArtButtonTable, {
|
||||
type: 'edit',
|
||||
onClick: () => showDialog('edit', row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
type: 'delete',
|
||||
onClick: () => deleteItem()
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// 表单实例
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
name: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getDataList()
|
||||
})
|
||||
|
||||
// 获取数据列表
|
||||
const getDataList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
|
||||
const endIndex = startIndex + pagination.pageSize
|
||||
const paginatedData = mockData.slice(startIndex, endIndex)
|
||||
|
||||
tableData.value = paginatedData
|
||||
pagination.total = mockData.length
|
||||
loading.value = false
|
||||
} catch (error) {
|
||||
console.error('获取数据列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getDataList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
name: [{ required: true, message: '请输入名称', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
ElMessage.success(dialogType.value === 'add' ? '添加成功' : '更新成功')
|
||||
dialogVisible.value = false
|
||||
getDataList()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getDataList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getDataList()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-template-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
</style>
|
||||
436
src/views/card-management/card-assign/index.vue
Normal file
436
src/views/card-management/card-assign/index.vue
Normal file
@@ -0,0 +1,436 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="card-assign-page" id="table-full-screen">
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton type="primary" @click="showDistributeDialog">分销网卡</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 分销网卡对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
title="分销网卡"
|
||||
width="500px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<ElFormItem label="上传Excel文件" prop="excelFile">
|
||||
<ElUpload
|
||||
ref="uploadRef"
|
||||
:limit="1"
|
||||
:on-exceed="handleExceed"
|
||||
:before-upload="beforeUpload"
|
||||
:on-change="handleFileChange"
|
||||
:auto-upload="false"
|
||||
accept=".xlsx,.xls"
|
||||
drag
|
||||
>
|
||||
<ElIcon class="el-icon--upload"><UploadFilled /></ElIcon>
|
||||
<div class="el-upload__text"> 将文件拖到此处,或<em>点击上传</em> </div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip"> 只能上传 xlsx/xls 文件,且不超过 10MB </div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="选择代理商" prop="agent">
|
||||
<ElSelect
|
||||
v-model="formData.agent"
|
||||
placeholder="请选择代理商"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<ElOption
|
||||
v-for="agent in agentOptions"
|
||||
:key="agent.value"
|
||||
:label="agent.label"
|
||||
:value="agent.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
确认分销
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import {
|
||||
ElTag,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElUpload,
|
||||
ElIcon,
|
||||
ElSelect,
|
||||
ElOption
|
||||
} from 'element-plus'
|
||||
import { UploadFilled } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, UploadInstance, UploadRawFile, UploadFile } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
|
||||
defineOptions({ name: 'CardAssign' })
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 表单实例
|
||||
const formRef = ref<FormInstance>()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
excelFile: null as File | null,
|
||||
agent: ''
|
||||
})
|
||||
|
||||
// 代理商选项
|
||||
const agentOptions = ref([
|
||||
{ label: '张丽丽', value: 'zhangll' },
|
||||
{ label: '王小明', value: 'wangxm' },
|
||||
{ label: 'HNSXKJ', value: 'hnsxkj' },
|
||||
{ label: '孔丽娟', value: 'konglj' },
|
||||
{ label: '李佳音', value: 'lijy' },
|
||||
{ label: '赵强', value: 'zhaoq' }
|
||||
])
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
importTime: '2025-11-07 15:01:37',
|
||||
packageName: '随意联畅玩年卡套餐(12个月)',
|
||||
importCount: 1000,
|
||||
successCount: 998,
|
||||
failCount: 2,
|
||||
distributor: '张丽丽',
|
||||
operator: '张若暄'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
importTime: '2025-11-05 12:07:41',
|
||||
packageName: '如意包年3G流量包',
|
||||
importCount: 500,
|
||||
successCount: 500,
|
||||
failCount: 0,
|
||||
distributor: '王小明',
|
||||
operator: '张若暄'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
importTime: '2025-11-03 11:21:42',
|
||||
packageName: 'Y-NB专享套餐',
|
||||
importCount: 200,
|
||||
successCount: 195,
|
||||
failCount: 5,
|
||||
distributor: 'HNSXKJ',
|
||||
operator: '张若暄'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
importTime: '2025-10-29 16:01:16',
|
||||
packageName: '100G全国流量月卡套餐',
|
||||
importCount: 2500,
|
||||
successCount: 2500,
|
||||
failCount: 0,
|
||||
distributor: '未分销',
|
||||
operator: '张若暄'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
importTime: '2025-10-22 15:44:31',
|
||||
packageName: '广电飞悦卡无预存50G(30天)',
|
||||
importCount: 800,
|
||||
successCount: 795,
|
||||
failCount: 5,
|
||||
distributor: '孔丽娟',
|
||||
operator: '孔丽娟'
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: '导入时间', prop: 'importTime' },
|
||||
{ label: '套餐名称', prop: 'packageName' },
|
||||
{ label: '导入张数(张)', prop: 'importCount' },
|
||||
{ label: '导入成功张数(张)', prop: 'successCount' },
|
||||
{ label: '导入失败张数(张)', prop: 'failCount' },
|
||||
{ label: '分销商', prop: 'distributor' },
|
||||
{ label: '操作人', prop: 'operator' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 查看详情
|
||||
const viewDetails = (row: any) => {
|
||||
ElMessage.info(`查看 ${row.packageName} 的分配详情`)
|
||||
}
|
||||
|
||||
// 删除记录
|
||||
const deleteRecord = (row: any) => {
|
||||
ElMessageBox.confirm(`确定要删除该分配记录吗?`, '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('删除成功')
|
||||
getCardAssignList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消删除')
|
||||
})
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'importTime',
|
||||
label: '导入时间',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
prop: 'packageName',
|
||||
label: '套餐名称',
|
||||
minWidth: 200
|
||||
},
|
||||
{
|
||||
prop: 'importCount',
|
||||
label: '导入张数(张)',
|
||||
width: 130
|
||||
},
|
||||
{
|
||||
prop: 'successCount',
|
||||
label: '导入成功张数(张)',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
prop: 'failCount',
|
||||
label: '导入失败张数(张)',
|
||||
width: 150,
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: row.failCount > 0 ? 'danger' : 'success' }, () => row.failCount)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'distributor',
|
||||
label: '分销商',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'operator',
|
||||
label: '操作人',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 150,
|
||||
formatter: (row: any) => {
|
||||
return h('div', { class: 'operation-buttons' }, [
|
||||
h(ArtButtonTable, {
|
||||
text: '查看',
|
||||
onClick: () => viewDetails(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '删除',
|
||||
onClick: () => deleteRecord(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getCardAssignList()
|
||||
})
|
||||
|
||||
// 获取网卡分配列表
|
||||
const getCardAssignList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
|
||||
const endIndex = startIndex + pagination.pageSize
|
||||
const paginatedData = mockData.slice(startIndex, endIndex)
|
||||
|
||||
tableData.value = paginatedData
|
||||
pagination.total = mockData.length
|
||||
loading.value = false
|
||||
} catch (error) {
|
||||
console.error('获取网卡分配列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getCardAssignList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getCardAssignList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getCardAssignList()
|
||||
}
|
||||
|
||||
// 显示分销对话框
|
||||
const showDistributeDialog = () => {
|
||||
dialogVisible.value = true
|
||||
// 重置表单
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
formData.excelFile = null
|
||||
formData.agent = ''
|
||||
}
|
||||
|
||||
// 文件上传限制
|
||||
const handleExceed = () => {
|
||||
ElMessage.warning('最多只能上传一个文件')
|
||||
}
|
||||
|
||||
// 文件上传前检查
|
||||
const beforeUpload = (file: UploadRawFile) => {
|
||||
const isExcel =
|
||||
file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
|
||||
file.type === 'application/vnd.ms-excel'
|
||||
const isLt10M = file.size / 1024 / 1024 < 10
|
||||
|
||||
if (!isExcel) {
|
||||
ElMessage.error('只能上传 Excel 文件!')
|
||||
return false
|
||||
}
|
||||
if (!isLt10M) {
|
||||
ElMessage.error('上传文件大小不能超过 10MB!')
|
||||
return false
|
||||
}
|
||||
return false // 阻止自动上传
|
||||
}
|
||||
|
||||
// 文件变化处理
|
||||
const handleFileChange = (file: UploadFile) => {
|
||||
if (file.raw) {
|
||||
formData.excelFile = file.raw
|
||||
}
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
excelFile: [{ required: true, message: '请上传Excel文件', trigger: 'change' }],
|
||||
agent: [{ required: true, message: '请选择代理商', trigger: 'change' }]
|
||||
})
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
// 检查文件是否上传
|
||||
if (!formData.excelFile) {
|
||||
ElMessage.error('请先上传Excel文件')
|
||||
return
|
||||
}
|
||||
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true
|
||||
|
||||
// 模拟提交过程
|
||||
setTimeout(() => {
|
||||
ElMessage.success(
|
||||
`已成功分销给代理商:${agentOptions.value.find((a) => a.value === formData.agent)?.label}`
|
||||
)
|
||||
dialogVisible.value = false
|
||||
submitLoading.value = false
|
||||
getCardAssignList()
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-assign-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
:deep(.operation-buttons) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.el-upload-dragger) {
|
||||
padding: 40px;
|
||||
}
|
||||
</style>
|
||||
549
src/views/card-management/card-change-card/index.vue
Normal file
549
src/views/card-management/card-change-card/index.vue
Normal file
@@ -0,0 +1,549 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="card-change-card-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton type="primary" @click="handleSearch">搜索</ElButton>
|
||||
<ElButton type="success" @click="showAddDialog">新增</ElButton>
|
||||
<ElButton @click="exportExcel">导出excel</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 新增换卡网卡对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
title="新增换卡网卡"
|
||||
width="500px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<ElFormItem label="ICCID" prop="iccid">
|
||||
<ElInput v-model="formData.iccid" placeholder="请输入ICCID" clearable />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="是否收费" prop="isCharged">
|
||||
<ElSelect
|
||||
v-model="formData.isCharged"
|
||||
placeholder="请选择是否收费"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
@change="handleChargeChange"
|
||||
>
|
||||
<ElOption label="收费" value="true" />
|
||||
<ElOption label="免费" value="false" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="收费金额" prop="chargeAmount" v-if="formData.isCharged === 'true'">
|
||||
<ElInputNumber
|
||||
v-model="formData.chargeAmount"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
:precision="2"
|
||||
placeholder="请输入收费金额"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
确认新增
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ElTag, ElMessage, ElMessageBox, ElSelect, ElOption, ElInputNumber } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
|
||||
defineOptions({ name: 'CardChangeCard' })
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
cardNumber: '',
|
||||
accessNumber: '',
|
||||
cardCompany: '',
|
||||
operationDateRange: '',
|
||||
isCharged: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 表单实例
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 新增表单数据
|
||||
const formData = reactive({
|
||||
iccid: '',
|
||||
isCharged: '',
|
||||
chargeAmount: 0
|
||||
})
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
cardNumber: '89860621370079892035',
|
||||
accessNumber: '1440012345678',
|
||||
cardCompany: '联通2',
|
||||
agentLevel: '一级代理',
|
||||
operator: '张若暄',
|
||||
operationTime: '2025-11-08 10:30:00',
|
||||
isCharged: '收费',
|
||||
amount: '50.00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
cardNumber: '89860621370079892036',
|
||||
accessNumber: '1440012345679',
|
||||
cardCompany: 'SXKJ-NB',
|
||||
agentLevel: '二级代理',
|
||||
operator: '孔丽娟',
|
||||
operationTime: '2025-11-07 14:15:00',
|
||||
isCharged: '免费',
|
||||
amount: '0.00'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
cardNumber: '89860621370079892037',
|
||||
accessNumber: '1440012345680',
|
||||
cardCompany: '联通36',
|
||||
agentLevel: '终端用户',
|
||||
operator: '李佳音',
|
||||
operationTime: '2025-11-06 09:45:00',
|
||||
isCharged: '收费',
|
||||
amount: '30.00'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
cardNumber: '89860621370079892038',
|
||||
accessNumber: '1440012345681',
|
||||
cardCompany: '联通1-1',
|
||||
agentLevel: '一级代理',
|
||||
operator: '赵强',
|
||||
operationTime: '2025-11-05 16:20:00',
|
||||
isCharged: '收费',
|
||||
amount: '100.00'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
cardNumber: '89860621370079892039',
|
||||
accessNumber: '1440012345682',
|
||||
cardCompany: '广电4',
|
||||
agentLevel: '分销商',
|
||||
operator: '张若暄',
|
||||
operationTime: '2025-11-04 11:30:00',
|
||||
isCharged: '免费',
|
||||
amount: '0.00'
|
||||
}
|
||||
]
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1
|
||||
getCardChangeCardList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1
|
||||
getCardChangeCardList()
|
||||
}
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '卡号',
|
||||
prop: 'cardNumber',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入卡号'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '接入号',
|
||||
prop: 'accessNumber',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入接入号'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '开卡公司',
|
||||
prop: 'cardCompany',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '联通2', value: 'unicom2' },
|
||||
{ label: '联通36', value: 'unicom36' },
|
||||
{ label: 'SXKJ-NB', value: 'sxkj_nb' },
|
||||
{ label: '联通1-1', value: 'unicom1_1' },
|
||||
{ label: '联通8', value: 'unicom8' },
|
||||
{ label: '移动21', value: 'mobile21' },
|
||||
{ label: '广电4', value: 'gdtv4' },
|
||||
{ label: '电信9', value: 'telecom9' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '操作时间',
|
||||
prop: 'operationDateRange',
|
||||
type: 'daterange',
|
||||
config: {
|
||||
type: 'daterange',
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '是否收费',
|
||||
prop: 'isCharged',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '收费', value: 'charged' },
|
||||
{ label: '免费', value: 'free' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: '卡号', prop: 'cardNumber' },
|
||||
{ label: '接入号', prop: 'accessNumber' },
|
||||
{ label: '开卡公司', prop: 'cardCompany' },
|
||||
{ label: '代理商层级', prop: 'agentLevel' },
|
||||
{ label: '操作人', prop: 'operator' },
|
||||
{ label: '操作时间', prop: 'operationTime' },
|
||||
{ label: '是否收费', prop: 'isCharged' },
|
||||
{ label: '金额', prop: 'amount' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 获取收费状态标签类型
|
||||
const getChargeStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case '收费':
|
||||
return 'warning'
|
||||
case '免费':
|
||||
return 'success'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 导出Excel
|
||||
const exportExcel = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要导出的数据')
|
||||
return
|
||||
}
|
||||
ElMessage.success(`导出 ${selectedRows.value.length} 条换卡网卡记录`)
|
||||
}
|
||||
|
||||
// 显示新增对话框
|
||||
const showAddDialog = () => {
|
||||
dialogVisible.value = true
|
||||
// 重置表单
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
formData.iccid = ''
|
||||
formData.isCharged = ''
|
||||
formData.chargeAmount = 0
|
||||
}
|
||||
|
||||
// 收费方式变化处理
|
||||
const handleChargeChange = (value: string) => {
|
||||
if (value === 'false') {
|
||||
formData.chargeAmount = 0
|
||||
}
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetails = (row: any) => {
|
||||
ElMessage.info(`查看换卡网卡详情: ${row.cardNumber}`)
|
||||
}
|
||||
|
||||
// 编辑记录
|
||||
const editRecord = (row: any) => {
|
||||
ElMessage.info(`编辑换卡网卡记录: ${row.cardNumber}`)
|
||||
}
|
||||
|
||||
// 删除记录
|
||||
const deleteRecord = (row: any) => {
|
||||
ElMessageBox.confirm(`确定要删除该换卡网卡记录吗?`, '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('删除成功')
|
||||
getCardChangeCardList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消删除')
|
||||
})
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'cardNumber',
|
||||
label: '卡号',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
prop: 'accessNumber',
|
||||
label: '接入号'
|
||||
},
|
||||
{
|
||||
prop: 'cardCompany',
|
||||
label: '开卡公司'
|
||||
},
|
||||
{
|
||||
prop: 'agentLevel',
|
||||
label: '代理商层级'
|
||||
},
|
||||
{
|
||||
prop: 'operator',
|
||||
label: '操作人'
|
||||
},
|
||||
{
|
||||
prop: 'operationTime',
|
||||
label: '操作时间',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
prop: 'isCharged',
|
||||
label: '是否收费',
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getChargeStatusType(row.isCharged) }, () => row.isCharged)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'amount',
|
||||
label: '金额',
|
||||
formatter: (row) => `¥${row.amount}`
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 220,
|
||||
formatter: (row: any) => {
|
||||
return h('div', { class: 'operation-buttons' }, [
|
||||
h(ArtButtonTable, {
|
||||
text: '查看',
|
||||
onClick: () => viewDetails(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '编辑',
|
||||
onClick: () => editRecord(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '删除',
|
||||
onClick: () => deleteRecord(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getCardChangeCardList()
|
||||
})
|
||||
|
||||
// 获取换卡网卡列表
|
||||
const getCardChangeCardList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
|
||||
const endIndex = startIndex + pagination.pageSize
|
||||
const paginatedData = mockData.slice(startIndex, endIndex)
|
||||
|
||||
tableData.value = paginatedData
|
||||
pagination.total = mockData.length
|
||||
loading.value = false
|
||||
} catch (error) {
|
||||
console.error('获取换卡网卡列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getCardChangeCardList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getCardChangeCardList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getCardChangeCardList()
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
iccid: [
|
||||
{ required: true, message: '请输入ICCID', trigger: 'blur' },
|
||||
{ min: 15, max: 20, message: 'ICCID长度在 15 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
isCharged: [{ required: true, message: '请选择是否收费', trigger: 'change' }],
|
||||
chargeAmount: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入收费金额',
|
||||
trigger: 'blur',
|
||||
validator: (rule, value, callback) => {
|
||||
if (formData.isCharged === 'true' && (!value || value <= 0)) {
|
||||
callback(new Error('收费时金额必须大于0'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true
|
||||
|
||||
// 模拟提交过程
|
||||
setTimeout(() => {
|
||||
const chargeText = formData.isCharged === 'true' ? '收费' : '免费'
|
||||
const amountText =
|
||||
formData.isCharged === 'true' ? `,金额:¥${formData.chargeAmount}` : ''
|
||||
ElMessage.success(
|
||||
`新增换卡网卡成功!ICCID:${formData.iccid},${chargeText}${amountText}`
|
||||
)
|
||||
dialogVisible.value = false
|
||||
submitLoading.value = false
|
||||
getCardChangeCardList()
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-change-card-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
:deep(.operation-buttons) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
1008
src/views/card-management/card-detail/index.vue
Normal file
1008
src/views/card-management/card-detail/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
607
src/views/card-management/card-list/index.vue
Normal file
607
src/views/card-management/card-list/index.vue
Normal file
@@ -0,0 +1,607 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="card-list-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 数据视图组件 -->
|
||||
<ArtDataViewer
|
||||
ref="dataViewerRef"
|
||||
:data="tableData"
|
||||
:loading="loading"
|
||||
:table-columns="columns"
|
||||
:descriptions-fields="descriptionsFields"
|
||||
:descriptions-columns="2"
|
||||
:pagination="pagination"
|
||||
:card-title-field="'importBatch'"
|
||||
:label-width="'150px'"
|
||||
:field-columns="columnChecks"
|
||||
:show-card-actions="true"
|
||||
:show-card-selection="true"
|
||||
:default-view="currentView"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@view-change="handleViewChange"
|
||||
>
|
||||
<template #header-left>
|
||||
<ElButton type="primary" @click="showDialog('add')">导入网卡</ElButton>
|
||||
<ElButton @click="showDialog('single')">单卡导入</ElButton>
|
||||
</template>
|
||||
|
||||
<template #header-right>
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
v-model:currentView="currentView"
|
||||
@refresh="handleRefresh"
|
||||
@viewChange="handleViewChange"
|
||||
:show-title="false"
|
||||
:show-view-toggle="true"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #card-actions="{ item }">
|
||||
<ArtButtonTable type="view" text="查看失败" @click="viewFailures(item)" />
|
||||
</template>
|
||||
</ArtDataViewer>
|
||||
|
||||
<!-- 网卡信息对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '导入网卡' : dialogType === 'single' ? '单卡导入' : '编辑'"
|
||||
width="40%"
|
||||
align-center
|
||||
>
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="100px">
|
||||
<ElFormItem label="ICCID" prop="iccid">
|
||||
<ElInput v-model="formData.iccid" placeholder="请输入ICCID" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="IMSI" prop="imsi">
|
||||
<ElInput v-model="formData.imsi" placeholder="请输入IMSI" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="手机号码" prop="msisdn">
|
||||
<ElInput v-model="formData.msisdn" placeholder="请输入手机号码" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="运营商" prop="operator">
|
||||
<ElSelect v-model="formData.operator" placeholder="请选择运营商">
|
||||
<ElOption label="中国移动" value="mobile" />
|
||||
<ElOption label="中国联通" value="unicom" />
|
||||
<ElOption label="中国电信" value="telecom" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="网络类型" prop="networkType">
|
||||
<ElSelect v-model="formData.networkType" placeholder="请选择网络类型">
|
||||
<ElOption label="2G" value="2G" />
|
||||
<ElOption label="3G" value="3G" />
|
||||
<ElOption label="4G" value="4G" />
|
||||
<ElOption label="5G" value="5G" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="状态" prop="status">
|
||||
<ElSelect v-model="formData.status" placeholder="请选择状态">
|
||||
<ElOption label="激活" value="1" />
|
||||
<ElOption label="停用" value="2" />
|
||||
<ElOption label="测试" value="3" />
|
||||
<ElOption label="库存" value="4" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ElDialog, FormInstance, ElTag } from 'element-plus'
|
||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import ArtDataViewer from '@/components/core/views/ArtDataViewer.vue'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
|
||||
defineOptions({ name: 'CardList' })
|
||||
|
||||
const dialogType = ref('add')
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
// 当前视图模式
|
||||
const currentView = ref('table')
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
operator: '',
|
||||
distributor: '',
|
||||
importDateRange: '',
|
||||
cardCompany: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 数据视图组件引用
|
||||
const dataViewerRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
importTime: '2025-11-07 15:01:37.0',
|
||||
importBatch: '650',
|
||||
importCount: 1,
|
||||
successCount: 1,
|
||||
failCount: 0,
|
||||
distributorName: '未分销',
|
||||
packageName: '随意联畅玩年卡套餐(12个月)',
|
||||
cardCompanyName: '联通2',
|
||||
operatorName: '中国联通',
|
||||
cardType: '月卡',
|
||||
operatorUser: '张若暄'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
importTime: '2025-11-05 12:07:41.0',
|
||||
importBatch: '649',
|
||||
importCount: 100,
|
||||
successCount: 100,
|
||||
failCount: 0,
|
||||
distributorName: '未分销',
|
||||
packageName: '如意包年3G流量包',
|
||||
cardCompanyName: '联通36',
|
||||
operatorName: '中国联通',
|
||||
cardType: '月卡',
|
||||
operatorUser: '张若暄'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
importTime: '2025-11-03 11:21:42.0',
|
||||
importBatch: '648',
|
||||
importCount: 200,
|
||||
successCount: 200,
|
||||
failCount: 0,
|
||||
distributorName: 'HNSXKJ',
|
||||
packageName: 'Y-NB专享套餐',
|
||||
cardCompanyName: 'SXKJ-NB',
|
||||
operatorName: 'GS移动',
|
||||
cardType: '月卡',
|
||||
operatorUser: '张若暄'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
importTime: '2025-10-29 16:01:16.0',
|
||||
importBatch: '647',
|
||||
importCount: 2500,
|
||||
successCount: 2500,
|
||||
failCount: 0,
|
||||
distributorName: '未分销',
|
||||
packageName: '100G全国流量月卡套餐',
|
||||
cardCompanyName: '联通1-1',
|
||||
operatorName: '中国联通',
|
||||
cardType: '月卡',
|
||||
operatorUser: '张若暄'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
importTime: '2025-10-22 15:44:31.0',
|
||||
importBatch: '644',
|
||||
importCount: 500,
|
||||
successCount: 499,
|
||||
failCount: 1,
|
||||
distributorName: '未分销',
|
||||
packageName: '广电飞悦卡无预存50G(30天)',
|
||||
cardCompanyName: '广电4',
|
||||
operatorName: 'GDWL',
|
||||
cardType: '月卡',
|
||||
operatorUser: '孔丽娟'
|
||||
}
|
||||
]
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1
|
||||
getCardList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1
|
||||
getCardList()
|
||||
}
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '运营商',
|
||||
prop: 'operator',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '中国移动', value: 'mobile' },
|
||||
{ label: '中国联通', value: 'unicom' },
|
||||
{ label: '中国电信', value: 'telecom' },
|
||||
{ label: 'GS移动', value: 'gs_mobile' },
|
||||
{ label: 'DC物联', value: 'dc_iot' },
|
||||
{ label: 'GDWL', value: 'gdwl' },
|
||||
{ label: 'GS联通', value: 'gs_unicom' },
|
||||
{ label: 'GS电信', value: 'gs_telecom' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '分销商',
|
||||
prop: 'distributor',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入分销商'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '导入时间',
|
||||
prop: 'importDateRange',
|
||||
type: 'daterange',
|
||||
config: {
|
||||
type: 'daterange',
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '开卡公司',
|
||||
prop: 'cardCompany',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '联通2', value: 'unicom2' },
|
||||
{ label: '联通36', value: 'unicom36' },
|
||||
{ label: 'SXKJ-NB', value: 'sxkj_nb' },
|
||||
{ label: '联通1-1', value: 'unicom1_1' },
|
||||
{ label: '联通8', value: 'unicom8' },
|
||||
{ label: '新移动22', value: 'new_mobile22' },
|
||||
{ label: '广电4', value: 'gdtv4' },
|
||||
{ label: '移动21', value: 'mobile21' },
|
||||
{ label: '联通38', value: 'unicom38' },
|
||||
{ label: '电信9', value: 'telecom9' },
|
||||
{ label: '联通10-1', value: 'unicom10_1' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: '导入时间', prop: 'importTime' },
|
||||
{ label: '导入批次', prop: 'importBatch' },
|
||||
{ label: '导入张数(张)', prop: 'importCount' },
|
||||
{ label: '导入成功张数(张)', prop: 'successCount' },
|
||||
{ label: '导入失败张数(张)', prop: 'failCount' },
|
||||
{ label: '分销代理商', prop: 'distributorName' },
|
||||
{ label: '套餐名称', prop: 'packageName' },
|
||||
{ label: '开卡公司', prop: 'cardCompanyName' },
|
||||
{ label: '运营商', prop: 'operatorName' },
|
||||
{ label: '卡类型', prop: 'cardType' },
|
||||
{ label: '操作人', prop: 'operatorUser' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 描述字段配置
|
||||
const descriptionsFields = [
|
||||
{ prop: 'importTime', label: '导入时间' },
|
||||
{ prop: 'importBatch', label: '导入批次' },
|
||||
{ prop: 'importCount', label: '导入张数(张)' },
|
||||
{ prop: 'successCount', label: '导入成功张数(张)' },
|
||||
{
|
||||
prop: 'failCount',
|
||||
label: '导入失败张数(张)',
|
||||
formatter: (row: any) => {
|
||||
const type = row.failCount > 0 ? 'danger' : 'success'
|
||||
return `<el-tag type="${type}" size="small">${row.failCount}</el-tag>`
|
||||
}
|
||||
},
|
||||
{ prop: 'distributorName', label: '分销代理商' },
|
||||
{ prop: 'packageName', label: '套餐名称', span: 1 },
|
||||
{ prop: 'cardCompanyName', label: '开卡公司' },
|
||||
{ prop: 'operatorName', label: '运营商' },
|
||||
{ prop: 'cardType', label: '卡类型' },
|
||||
{ prop: 'operatorUser', label: '操作人' }
|
||||
]
|
||||
|
||||
// 获取标签类型
|
||||
const getTagType = (status: string) => {
|
||||
switch (status) {
|
||||
case '1':
|
||||
return 'success'
|
||||
case '2':
|
||||
return 'danger'
|
||||
case '3':
|
||||
return 'warning'
|
||||
case '4':
|
||||
return 'info'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 构建标签文本
|
||||
const buildTagText = (status: string) => {
|
||||
switch (status) {
|
||||
case '1':
|
||||
return '激活'
|
||||
case '2':
|
||||
return '停用'
|
||||
case '3':
|
||||
return '测试'
|
||||
case '4':
|
||||
return '库存'
|
||||
default:
|
||||
return '未知'
|
||||
}
|
||||
}
|
||||
|
||||
// 显示对话框
|
||||
const showDialog = (type: string, row?: any) => {
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
|
||||
if (type === 'edit' && row) {
|
||||
formData.iccid = row.iccid
|
||||
formData.imsi = row.imsi
|
||||
formData.msisdn = row.msisdn
|
||||
formData.operator = row.operator
|
||||
formData.networkType = row.networkType
|
||||
formData.status = row.status
|
||||
} else {
|
||||
formData.iccid = ''
|
||||
formData.imsi = ''
|
||||
formData.msisdn = ''
|
||||
formData.operator = ''
|
||||
formData.networkType = ''
|
||||
formData.status = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 查看失败
|
||||
const viewFailures = (row: any) => {
|
||||
if (row.failCount > 0) {
|
||||
ElMessage.info(`查看导入批次 ${row.importBatch} 的失败记录`)
|
||||
// 这里可以跳转到失败详情页面或显示失败详情对话框
|
||||
} else {
|
||||
ElMessage.info('该批次没有失败记录')
|
||||
}
|
||||
}
|
||||
|
||||
// 批量操作
|
||||
const batchOperation = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要操作的导入记录')
|
||||
return
|
||||
}
|
||||
ElMessage.info(`已选择 ${selectedRows.value.length} 条导入记录`)
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'importTime',
|
||||
label: '导入时间',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
prop: 'importBatch',
|
||||
label: '导入批次',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'importCount',
|
||||
label: '导入张数(张)',
|
||||
width: 130
|
||||
},
|
||||
{
|
||||
prop: 'successCount',
|
||||
label: '导入成功张数(张)',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
prop: 'failCount',
|
||||
label: '导入失败张数(张)',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
prop: 'distributorName',
|
||||
label: '分销代理商',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'packageName',
|
||||
label: '套餐名称',
|
||||
minWidth: 200
|
||||
},
|
||||
{
|
||||
prop: 'cardCompanyName',
|
||||
label: '开卡公司',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'operatorName',
|
||||
label: '运营商',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'cardType',
|
||||
label: '卡类型',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
prop: 'operatorUser',
|
||||
label: '操作人',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 120,
|
||||
formatter: (row: any) => {
|
||||
return h('div', [
|
||||
h(ArtButtonTable, {
|
||||
type: 'view',
|
||||
text: '查看失败',
|
||||
onClick: () => viewFailures(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// 表单实例
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
iccid: '',
|
||||
imsi: '',
|
||||
msisdn: '',
|
||||
operator: '',
|
||||
networkType: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getCardList()
|
||||
})
|
||||
|
||||
// 获取网卡列表
|
||||
const getCardList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
|
||||
const endIndex = startIndex + pagination.pageSize
|
||||
const paginatedData = mockData.slice(startIndex, endIndex)
|
||||
|
||||
tableData.value = paginatedData
|
||||
pagination.total = mockData.length
|
||||
loading.value = false
|
||||
} catch (error) {
|
||||
console.error('获取网卡列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getCardList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
iccid: [
|
||||
{ required: true, message: '请输入ICCID', trigger: 'blur' },
|
||||
{ min: 19, max: 20, message: 'ICCID长度应为19-20位', trigger: 'blur' }
|
||||
],
|
||||
imsi: [
|
||||
{ required: true, message: '请输入IMSI', trigger: 'blur' },
|
||||
{ min: 15, max: 15, message: 'IMSI长度应为15位', trigger: 'blur' }
|
||||
],
|
||||
msisdn: [
|
||||
{ required: true, message: '请输入手机号码', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
|
||||
],
|
||||
operator: [{ required: true, message: '请选择运营商', trigger: 'change' }],
|
||||
networkType: [{ required: true, message: '请选择网络类型', trigger: 'change' }],
|
||||
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
|
||||
})
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
ElMessage.success(dialogType.value === 'add' ? '添加成功' : '更新成功')
|
||||
dialogVisible.value = false
|
||||
getCardList()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getCardList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getCardList()
|
||||
}
|
||||
|
||||
// 处理视图切换
|
||||
const handleViewChange = (view: string) => {
|
||||
console.log('视图切换到:', view)
|
||||
// 可以在这里添加视图切换的额外逻辑,比如保存用户偏好
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-list-page {
|
||||
// Card list page styles
|
||||
}
|
||||
</style>
|
||||
695
src/views/card-management/card-replacement/index.vue
Normal file
695
src/views/card-management/card-replacement/index.vue
Normal file
@@ -0,0 +1,695 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="card-replacement-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton type="primary" @click="handleSearch">搜索</ElButton>
|
||||
<ElButton @click="exportExcel">导出excel</ElButton>
|
||||
<ElButton type="success" @click="showImportDialog">导入excel</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 导入Excel对话框 -->
|
||||
<ElDialog
|
||||
v-model="importDialogVisible"
|
||||
title="导入换卡记录"
|
||||
width="500px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<!-- 顶部下载模板按钮 -->
|
||||
<div class="template-section">
|
||||
<ElButton type="primary" @click="downloadTemplate" icon="Download"> 下载模板 </ElButton>
|
||||
<span class="template-tip">请先下载模板,按模板格式填写后上传</span>
|
||||
</div>
|
||||
|
||||
<ElDivider />
|
||||
|
||||
<ElForm
|
||||
ref="importFormRef"
|
||||
:model="importFormData"
|
||||
:rules="importRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<ElFormItem label="上传Excel文件" prop="excelFile">
|
||||
<ElUpload
|
||||
ref="uploadRef"
|
||||
:limit="1"
|
||||
:on-exceed="handleExceed"
|
||||
:before-upload="beforeUpload"
|
||||
:on-change="handleFileChange"
|
||||
:auto-upload="false"
|
||||
accept=".xlsx,.xls"
|
||||
drag
|
||||
>
|
||||
<ElIcon class="el-icon--upload"><UploadFilled /></ElIcon>
|
||||
<div class="el-upload__text"> 将文件拖到此处,或<em>点击上传</em> </div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip"> 只能上传 xlsx/xls 文件,且不超过 10MB </div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="备注说明" prop="remark">
|
||||
<ElInput
|
||||
v-model="importFormData.remark"
|
||||
type="textarea"
|
||||
placeholder="请输入备注说明(可选)"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="importDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleImportSubmit" :loading="importLoading">
|
||||
确认导入
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ElTag, ElMessage, ElMessageBox, ElUpload, ElIcon, ElDivider } from 'element-plus'
|
||||
import { UploadFilled, Download } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, UploadInstance, UploadRawFile, UploadFile } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
|
||||
defineOptions({ name: 'CardReplacement' })
|
||||
|
||||
const importDialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const importLoading = ref(false)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
processResult: '',
|
||||
cardCompany: '',
|
||||
oldSimNumber: '',
|
||||
newSimNumber: '',
|
||||
newCardOperator: '',
|
||||
submitDateRange: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 导入表单实例
|
||||
const importFormRef = ref<FormInstance>()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
|
||||
// 导入表单数据
|
||||
const importFormData = reactive({
|
||||
excelFile: null as File | null,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
iccid: '89860621370079892035',
|
||||
currentPackage: '随意联畅玩年卡套餐(12个月)',
|
||||
cardCompany: '联通2',
|
||||
hierarchyRelation: '一级代理->二级代理->终端用户',
|
||||
recipientName: '张丽丽',
|
||||
recipientPhone: '138****5678',
|
||||
recipientAddress: '北京市朝阳区建国门外大街1号',
|
||||
newSimNumber: '1440012345778',
|
||||
processor: '张若暄',
|
||||
processTime: '2025-11-08 14:30:00',
|
||||
processDescription: '用户申请换卡,原卡损坏',
|
||||
processStatus: '处理完成',
|
||||
submitTime: '2025-11-08 10:00:00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
iccid: '89860621370079892036',
|
||||
currentPackage: '如意包年3G流量包',
|
||||
cardCompany: '联通36',
|
||||
hierarchyRelation: '总代理->分销商',
|
||||
recipientName: '王小明',
|
||||
recipientPhone: '139****1234',
|
||||
recipientAddress: '上海市浦东新区陆家嘴环路1000号',
|
||||
newSimNumber: '1440012345779',
|
||||
processor: '孔丽娟',
|
||||
processTime: '2025-11-07 16:15:00',
|
||||
processDescription: '卡槽损坏需要更换新卡',
|
||||
processStatus: '处理中',
|
||||
submitTime: '2025-11-07 09:30:00'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
iccid: '89860621370079892037',
|
||||
currentPackage: 'Y-NB专享套餐',
|
||||
cardCompany: 'SXKJ-NB',
|
||||
hierarchyRelation: '直营',
|
||||
recipientName: 'HNSXKJ',
|
||||
recipientPhone: '135****9876',
|
||||
recipientAddress: '广州市天河区珠江新城花城大道5号',
|
||||
newSimNumber: '1440012345780',
|
||||
processor: '李佳音',
|
||||
processTime: '2025-11-06 11:20:00',
|
||||
processDescription: '设备升级需要更换新规格SIM卡',
|
||||
processStatus: '审核中',
|
||||
submitTime: '2025-11-06 08:45:00'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
iccid: '89860621370079892038',
|
||||
currentPackage: '100G全国流量月卡套餐',
|
||||
cardCompany: '联通1-1',
|
||||
hierarchyRelation: '一级代理->终端用户',
|
||||
recipientName: '赵强',
|
||||
recipientPhone: '137****5555',
|
||||
recipientAddress: '深圳市南山区科技园南区深南大道10000号',
|
||||
newSimNumber: '1440012345781',
|
||||
processor: '张若暄',
|
||||
processTime: '',
|
||||
processDescription: '',
|
||||
processStatus: '待处理',
|
||||
submitTime: '2025-11-05 15:10:00'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
iccid: '89860621370079892039',
|
||||
currentPackage: '广电飞悦卡无预存50G(30天)',
|
||||
cardCompany: '广电4',
|
||||
hierarchyRelation: '二级代理->终端用户',
|
||||
recipientName: '李丽',
|
||||
recipientPhone: '133****7777',
|
||||
recipientAddress: '杭州市西湖区文三路90号',
|
||||
newSimNumber: '1440012345782',
|
||||
processor: '赵强',
|
||||
processTime: '2025-11-04 09:30:00',
|
||||
processDescription: '换卡申请已拒绝,原因:信息不完整',
|
||||
processStatus: '已拒绝',
|
||||
submitTime: '2025-11-04 08:00:00'
|
||||
}
|
||||
]
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1
|
||||
getCardReplacementList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1
|
||||
getCardReplacementList()
|
||||
}
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '处理结果',
|
||||
prop: 'processResult',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '处理完成', value: 'completed' },
|
||||
{ label: '处理中', value: 'processing' },
|
||||
{ label: '审核中', value: 'reviewing' },
|
||||
{ label: '待处理', value: 'pending' },
|
||||
{ label: '已拒绝', value: 'rejected' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '开卡公司',
|
||||
prop: 'cardCompany',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '联通2', value: 'unicom2' },
|
||||
{ label: '联通36', value: 'unicom36' },
|
||||
{ label: 'SXKJ-NB', value: 'sxkj_nb' },
|
||||
{ label: '联通1-1', value: 'unicom1_1' },
|
||||
{ label: '联通8', value: 'unicom8' },
|
||||
{ label: '移动21', value: 'mobile21' },
|
||||
{ label: '广电4', value: 'gdtv4' },
|
||||
{ label: '电信9', value: 'telecom9' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '换卡SIM号',
|
||||
prop: 'oldSimNumber',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入原SIM号'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '新SIM号',
|
||||
prop: 'newSimNumber',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入新SIM号'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '新卡运营商',
|
||||
prop: 'newCardOperator',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '中国移动', value: 'mobile' },
|
||||
{ label: '中国联通', value: 'unicom' },
|
||||
{ label: '中国电信', value: 'telecom' },
|
||||
{ label: 'GS移动', value: 'gs_mobile' },
|
||||
{ label: 'DC物联', value: 'dc_iot' },
|
||||
{ label: 'GDWL', value: 'gdwl' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '提交时间',
|
||||
prop: 'submitDateRange',
|
||||
type: 'daterange',
|
||||
config: {
|
||||
type: 'daterange',
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: 'ICCID号', prop: 'iccid' },
|
||||
{ label: '当前套餐', prop: 'currentPackage' },
|
||||
{ label: '开卡公司', prop: 'cardCompany' },
|
||||
{ label: '层级关系', prop: 'hierarchyRelation' },
|
||||
{ label: '收货人姓名', prop: 'recipientName' },
|
||||
{ label: '收货人电话', prop: 'recipientPhone' },
|
||||
{ label: '收货人地址', prop: 'recipientAddress' },
|
||||
{ label: '新sim号', prop: 'newSimNumber' },
|
||||
{ label: '处理人', prop: 'processor' },
|
||||
{ label: '处理时间', prop: 'processTime' },
|
||||
{ label: '处理描述', prop: 'processDescription' },
|
||||
{ label: '处理状态', prop: 'processStatus' },
|
||||
{ label: '提交时间', prop: 'submitTime' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 获取处理状态标签类型
|
||||
const getProcessStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case '处理完成':
|
||||
return 'success'
|
||||
case '处理中':
|
||||
return 'warning'
|
||||
case '审核中':
|
||||
return 'info'
|
||||
case '待处理':
|
||||
return 'primary'
|
||||
case '已拒绝':
|
||||
return 'danger'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 导出Excel
|
||||
const exportExcel = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要导出的数据')
|
||||
return
|
||||
}
|
||||
ElMessage.success(`导出 ${selectedRows.value.length} 条换卡记录`)
|
||||
}
|
||||
|
||||
// 显示导入对话框
|
||||
const showImportDialog = () => {
|
||||
importDialogVisible.value = true
|
||||
// 重置表单
|
||||
if (importFormRef.value) {
|
||||
importFormRef.value.resetFields()
|
||||
}
|
||||
importFormData.excelFile = null
|
||||
importFormData.remark = ''
|
||||
}
|
||||
|
||||
// 下载模板
|
||||
const downloadTemplate = () => {
|
||||
ElMessage.success('正在下载换卡记录导入模板...')
|
||||
// 这里可以实现实际的模板下载功能
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetails = (row: any) => {
|
||||
ElMessage.info(`查看换卡详情: ${row.iccid}`)
|
||||
}
|
||||
|
||||
// 编辑记录
|
||||
const editRecord = (row: any) => {
|
||||
ElMessage.info(`编辑换卡记录: ${row.iccid}`)
|
||||
}
|
||||
|
||||
// 删除记录
|
||||
const deleteRecord = (row: any) => {
|
||||
ElMessageBox.confirm(`确定要删除该换卡记录吗?`, '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('删除成功')
|
||||
getCardReplacementList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消删除')
|
||||
})
|
||||
}
|
||||
|
||||
// 处理换卡申请
|
||||
const processApplication = (row: any) => {
|
||||
if (row.processStatus === '处理完成' || row.processStatus === '已拒绝') {
|
||||
ElMessage.warning('该申请已处理完成')
|
||||
return
|
||||
}
|
||||
ElMessage.info(`处理换卡申请: ${row.iccid}`)
|
||||
// 这里可以打开处理对话框或跳转到处理页面
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'iccid',
|
||||
label: 'ICCID号',
|
||||
minWidth: 180
|
||||
},
|
||||
{
|
||||
prop: 'currentPackage',
|
||||
label: '当前套餐',
|
||||
minWidth: 200
|
||||
},
|
||||
{
|
||||
prop: 'cardCompany',
|
||||
label: '开卡公司',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'hierarchyRelation',
|
||||
label: '层级关系',
|
||||
minWidth: 150
|
||||
},
|
||||
{
|
||||
prop: 'recipientName',
|
||||
label: '收货人姓名',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'recipientPhone',
|
||||
label: '收货人电话',
|
||||
width: 130
|
||||
},
|
||||
{
|
||||
prop: 'recipientAddress',
|
||||
label: '收货人地址',
|
||||
minWidth: 200
|
||||
},
|
||||
{
|
||||
prop: 'newSimNumber',
|
||||
label: '新sim号',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
prop: 'processor',
|
||||
label: '处理人',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'processTime',
|
||||
label: '处理时间',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
prop: 'processDescription',
|
||||
label: '处理描述',
|
||||
minWidth: 200
|
||||
},
|
||||
{
|
||||
prop: 'processStatus',
|
||||
label: '处理状态',
|
||||
width: 120,
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getProcessStatusType(row.processStatus) }, () => row.processStatus)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'submitTime',
|
||||
label: '提交时间',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 280,
|
||||
formatter: (row: any) => {
|
||||
return h('div', { class: 'operation-buttons' }, [
|
||||
h(ArtButtonTable, {
|
||||
text: '查看',
|
||||
onClick: () => viewDetails(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '处理',
|
||||
disabled: row.processStatus === '处理完成' || row.processStatus === '已拒绝',
|
||||
onClick: () => processApplication(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '编辑',
|
||||
onClick: () => editRecord(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '删除',
|
||||
onClick: () => deleteRecord(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getCardReplacementList()
|
||||
})
|
||||
|
||||
// 获取换卡管理列表
|
||||
const getCardReplacementList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
|
||||
const endIndex = startIndex + pagination.pageSize
|
||||
const paginatedData = mockData.slice(startIndex, endIndex)
|
||||
|
||||
tableData.value = paginatedData
|
||||
pagination.total = mockData.length
|
||||
loading.value = false
|
||||
} catch (error) {
|
||||
console.error('获取换卡管理列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getCardReplacementList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getCardReplacementList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getCardReplacementList()
|
||||
}
|
||||
|
||||
// 文件上传限制
|
||||
const handleExceed = () => {
|
||||
ElMessage.warning('最多只能上传一个文件')
|
||||
}
|
||||
|
||||
// 文件上传前检查
|
||||
const beforeUpload = (file: UploadRawFile) => {
|
||||
const isExcel =
|
||||
file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
|
||||
file.type === 'application/vnd.ms-excel'
|
||||
const isLt10M = file.size / 1024 / 1024 < 10
|
||||
|
||||
if (!isExcel) {
|
||||
ElMessage.error('只能上传 Excel 文件!')
|
||||
return false
|
||||
}
|
||||
if (!isLt10M) {
|
||||
ElMessage.error('上传文件大小不能超过 10MB!')
|
||||
return false
|
||||
}
|
||||
return false // 阻止自动上传
|
||||
}
|
||||
|
||||
// 文件变化处理
|
||||
const handleFileChange = (file: UploadFile) => {
|
||||
if (file.raw) {
|
||||
importFormData.excelFile = file.raw
|
||||
}
|
||||
}
|
||||
|
||||
// 导入表单验证规则
|
||||
const importRules = reactive<FormRules>({
|
||||
excelFile: [{ required: true, message: '请上传Excel文件', trigger: 'change' }]
|
||||
})
|
||||
|
||||
// 提交导入
|
||||
const handleImportSubmit = async () => {
|
||||
if (!importFormRef.value) return
|
||||
|
||||
// 检查文件是否上传
|
||||
if (!importFormData.excelFile) {
|
||||
ElMessage.error('请先上传Excel文件')
|
||||
return
|
||||
}
|
||||
|
||||
await importFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
importLoading.value = true
|
||||
|
||||
// 模拟导入过程
|
||||
setTimeout(() => {
|
||||
ElMessage.success('换卡记录导入成功!')
|
||||
importDialogVisible.value = false
|
||||
importLoading.value = false
|
||||
getCardReplacementList()
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-replacement-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
.template-section {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.template-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.operation-buttons) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.el-upload-dragger) {
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
460
src/views/card-management/card-shutdown/index.vue
Normal file
460
src/views/card-management/card-shutdown/index.vue
Normal file
@@ -0,0 +1,460 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="card-shutdown-page" id="table-full-screen">
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton type="primary" @click="showShutdownDialog">设置停机时间</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 设置停机时间对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
title="设置停机时间"
|
||||
width="500px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<ElFormItem label="停机类型" prop="shutdownType">
|
||||
<ElSelect
|
||||
v-model="formData.shutdownType"
|
||||
placeholder="请选择停机类型"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<ElOption label="立即停机" value="immediate" />
|
||||
<ElOption label="定时停机" value="scheduled" />
|
||||
<ElOption label="批量停机" value="batch" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
label="停机时间"
|
||||
prop="shutdownTime"
|
||||
v-if="formData.shutdownType === 'scheduled'"
|
||||
>
|
||||
<ElDatePicker
|
||||
v-model="formData.shutdownTime"
|
||||
type="datetime"
|
||||
placeholder="请选择停机时间"
|
||||
style="width: 100%"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="停机原因" prop="reason">
|
||||
<ElInput
|
||||
v-model="formData.reason"
|
||||
type="textarea"
|
||||
placeholder="请输入停机原因"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="影响范围" prop="affectedCards">
|
||||
<ElInputNumber
|
||||
v-model="formData.affectedCards"
|
||||
:min="1"
|
||||
:max="10000"
|
||||
placeholder="请输入影响的网卡数量"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="通知方式" prop="notifyMethod">
|
||||
<ElCheckboxGroup v-model="formData.notifyMethod">
|
||||
<ElCheckbox label="短信通知">短信通知</ElCheckbox>
|
||||
<ElCheckbox label="邮件通知">邮件通知</ElCheckbox>
|
||||
<ElCheckbox label="系统通知">系统通知</ElCheckbox>
|
||||
</ElCheckboxGroup>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
确认设置
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import {
|
||||
ElTag,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElSelect,
|
||||
ElOption,
|
||||
ElDatePicker,
|
||||
ElInputNumber,
|
||||
ElCheckboxGroup,
|
||||
ElCheckbox
|
||||
} from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
|
||||
defineOptions({ name: 'CardShutdown' })
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 表单实例
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
shutdownType: '',
|
||||
shutdownTime: '',
|
||||
reason: '',
|
||||
affectedCards: 1,
|
||||
notifyMethod: [] as string[]
|
||||
})
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
totalCount: 1000,
|
||||
operationTime: '2025-11-08 14:30:00',
|
||||
successCount: 998,
|
||||
failureCount: 2,
|
||||
operationType: '立即停机',
|
||||
operator: '张若暄',
|
||||
reason: '系统维护'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
totalCount: 500,
|
||||
operationTime: '2025-11-07 10:15:00',
|
||||
successCount: 500,
|
||||
failureCount: 0,
|
||||
operationType: '定时停机',
|
||||
operator: '孔丽娟',
|
||||
reason: '网络升级'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
totalCount: 200,
|
||||
operationTime: '2025-11-06 16:45:00',
|
||||
successCount: 195,
|
||||
failureCount: 5,
|
||||
operationType: '批量停机',
|
||||
operator: '张若暄',
|
||||
reason: '安全检查'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
totalCount: 1500,
|
||||
operationTime: '2025-11-05 09:30:00',
|
||||
successCount: 1500,
|
||||
failureCount: 0,
|
||||
operationType: '立即停机',
|
||||
operator: '李佳音',
|
||||
reason: '应急处理'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
totalCount: 800,
|
||||
operationTime: '2025-11-04 13:20:00',
|
||||
successCount: 792,
|
||||
failureCount: 8,
|
||||
operationType: '定时停机',
|
||||
operator: '张若暄',
|
||||
reason: '定期维护'
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: '总数', prop: 'totalCount' },
|
||||
{ label: '时间', prop: 'operationTime' },
|
||||
{ label: '成功', prop: 'successCount' },
|
||||
{ label: '失败', prop: 'failureCount' },
|
||||
{ label: '操作类型', prop: 'operationType' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 获取操作类型标签类型
|
||||
const getOperationTagType = (type: string) => {
|
||||
switch (type) {
|
||||
case '立即停机':
|
||||
return 'danger'
|
||||
case '定时停机':
|
||||
return 'warning'
|
||||
case '批量停机':
|
||||
return 'info'
|
||||
default:
|
||||
return 'primary'
|
||||
}
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetails = (row: any) => {
|
||||
ElMessage.info(`查看停机操作的详细信息: ${row.operationType}`)
|
||||
}
|
||||
|
||||
// 删除记录
|
||||
const deleteRecord = (row: any) => {
|
||||
ElMessageBox.confirm(`确定要删除该停机记录吗?`, '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('删除成功')
|
||||
getShutdownList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消删除')
|
||||
})
|
||||
}
|
||||
|
||||
// 重新执行
|
||||
const retryOperation = (row: any) => {
|
||||
ElMessageBox.confirm(`确定要重新执行该停机操作吗?`, '操作确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('重新执行成功')
|
||||
getShutdownList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消操作')
|
||||
})
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'totalCount',
|
||||
label: '总数',
|
||||
formatter: (row) => `${row.totalCount} `
|
||||
},
|
||||
{
|
||||
prop: 'operationTime',
|
||||
label: '时间'
|
||||
},
|
||||
{
|
||||
prop: 'successCount',
|
||||
label: '成功',
|
||||
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: 'success' }, () => `${row.successCount} 张`)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'failureCount',
|
||||
label: '失败',
|
||||
|
||||
formatter: (row) => {
|
||||
return h(
|
||||
ElTag,
|
||||
{ type: row.failureCount > 0 ? 'danger' : 'success' },
|
||||
() => `${row.failureCount} 张`
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'operationType',
|
||||
label: '操作类型',
|
||||
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getOperationTagType(row.operationType) }, () => row.operationType)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 220,
|
||||
formatter: (row: any) => {
|
||||
return h('div', { class: 'operation-buttons' }, [
|
||||
h(ArtButtonTable, {
|
||||
text: '查看',
|
||||
onClick: () => viewDetails(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '重试',
|
||||
onClick: () => retryOperation(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '删除',
|
||||
onClick: () => deleteRecord(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getShutdownList()
|
||||
})
|
||||
|
||||
// 获取停机管理列表
|
||||
const getShutdownList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
|
||||
const endIndex = startIndex + pagination.pageSize
|
||||
const paginatedData = mockData.slice(startIndex, endIndex)
|
||||
|
||||
tableData.value = paginatedData
|
||||
pagination.total = mockData.length
|
||||
loading.value = false
|
||||
} catch (error) {
|
||||
console.error('获取停机管理列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getShutdownList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getShutdownList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getShutdownList()
|
||||
}
|
||||
|
||||
// 显示停机设置对话框
|
||||
const showShutdownDialog = () => {
|
||||
dialogVisible.value = true
|
||||
// 重置表单
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
formData.shutdownType = ''
|
||||
formData.shutdownTime = ''
|
||||
formData.reason = ''
|
||||
formData.affectedCards = 1
|
||||
formData.notifyMethod = []
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
shutdownType: [{ required: true, message: '请选择停机类型', trigger: 'change' }],
|
||||
shutdownTime: [{ required: true, message: '请选择停机时间', trigger: 'change' }],
|
||||
reason: [
|
||||
{ required: true, message: '请输入停机原因', trigger: 'blur' },
|
||||
{ min: 5, max: 200, message: '停机原因长度在 5 到 200 个字符', trigger: 'blur' }
|
||||
],
|
||||
affectedCards: [{ required: true, message: '请输入影响的网卡数量', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
// 如果是定时停机,验证停机时间
|
||||
if (formData.shutdownType === 'scheduled' && !formData.shutdownTime) {
|
||||
ElMessage.error('定时停机必须选择停机时间')
|
||||
return
|
||||
}
|
||||
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true
|
||||
|
||||
// 模拟提交过程
|
||||
setTimeout(() => {
|
||||
ElMessage.success(
|
||||
`停机设置成功!类型:${formData.shutdownType},影响网卡:${formData.affectedCards} 张`
|
||||
)
|
||||
dialogVisible.value = false
|
||||
submitLoading.value = false
|
||||
getShutdownList()
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-shutdown-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
:deep(.operation-buttons) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
739
src/views/card-management/card-transfer/index.vue
Normal file
739
src/views/card-management/card-transfer/index.vue
Normal file
@@ -0,0 +1,739 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="card-transfer-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton type="primary" @click="handleSearch">搜索</ElButton>
|
||||
<ElButton type="success" @click="showBatchTransferDialog">批量转卡</ElButton>
|
||||
<ElButton @click="exportData">导出</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 批量转卡对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
title="批量转卡"
|
||||
width="600px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<ElFormItem label="转卡类型" prop="transferType">
|
||||
<ElSelect
|
||||
v-model="formData.transferType"
|
||||
placeholder="请选择转卡类型"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<ElOption label="运营商转接" value="operator_transfer" />
|
||||
<ElOption label="套餐转换" value="package_transfer" />
|
||||
<ElOption label="紧急转接" value="emergency_transfer" />
|
||||
<ElOption label="升级转接" value="upgrade_transfer" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="目标运营商" prop="targetOperator">
|
||||
<ElSelect
|
||||
v-model="formData.targetOperator"
|
||||
placeholder="请选择目标运营商"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<ElOption
|
||||
v-for="operator in operatorOptions"
|
||||
:key="operator.value"
|
||||
:label="operator.label"
|
||||
:value="operator.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="目标套餐" prop="targetPackage">
|
||||
<ElSelect
|
||||
v-model="formData.targetPackage"
|
||||
placeholder="请选择目标套餐"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<ElOption
|
||||
v-for="pkg in packageOptions"
|
||||
:key="pkg.value"
|
||||
:label="pkg.label"
|
||||
:value="pkg.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="生效时间" prop="effectiveTime">
|
||||
<ElDatePicker
|
||||
v-model="formData.effectiveTime"
|
||||
type="datetime"
|
||||
placeholder="请选择生效时间"
|
||||
style="width: 100%"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="是否保留流量" prop="keepTraffic">
|
||||
<ElRadioGroup v-model="formData.keepTraffic">
|
||||
<ElRadio :label="true">保留剩余流量</ElRadio>
|
||||
<ElRadio :label="false">清零重新计算</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="转卡原因" prop="reason">
|
||||
<ElInput
|
||||
v-model="formData.reason"
|
||||
type="textarea"
|
||||
placeholder="请输入转卡原因"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
确认转卡
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import {
|
||||
ElTag,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElSelect,
|
||||
ElOption,
|
||||
ElDatePicker,
|
||||
ElRadioGroup,
|
||||
ElRadio
|
||||
} from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
|
||||
defineOptions({ name: 'CardTransfer' })
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
oldOperator: '',
|
||||
transferType: '',
|
||||
iccid: '',
|
||||
transferDateRange: '',
|
||||
newOperator: '',
|
||||
status: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 表单实例
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
transferType: '',
|
||||
targetOperator: '',
|
||||
targetPackage: '',
|
||||
effectiveTime: '',
|
||||
keepTraffic: true,
|
||||
reason: ''
|
||||
})
|
||||
|
||||
// 运营商选项
|
||||
const operatorOptions = ref([
|
||||
{ label: '中国移动', value: 'mobile' },
|
||||
{ label: '中国联通', value: 'unicom' },
|
||||
{ label: '中国电信', value: 'telecom' },
|
||||
{ label: 'GS移动', value: 'gs_mobile' },
|
||||
{ label: 'DC物联', value: 'dc_iot' },
|
||||
{ label: 'GDWL', value: 'gdwl' },
|
||||
{ label: 'GS联通', value: 'gs_unicom' },
|
||||
{ label: 'GS电信', value: 'gs_telecom' }
|
||||
])
|
||||
|
||||
// 套餐选项
|
||||
const packageOptions = ref([
|
||||
{ label: '随意联畅玩年卡套餐(12个月)', value: 'package1' },
|
||||
{ label: '如意包年3G流量包', value: 'package2' },
|
||||
{ label: 'Y-NB专享套餐', value: 'package3' },
|
||||
{ label: '100G全国流量月卡套餐', value: 'package4' },
|
||||
{ label: '广电飞悦卡无预存50G(30天)', value: 'package5' },
|
||||
{ label: '5G畅享套餐', value: 'package6' },
|
||||
{ label: '移动物联网专用套餐', value: 'package7' },
|
||||
{ label: '电信天翼套餐', value: 'package8' }
|
||||
])
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
oldCardNumber: '89860621370079892035',
|
||||
oldAccessNumber: '1440012345678',
|
||||
newCardNumber: '89860621370079892135',
|
||||
newAccessNumber: '1440012345778',
|
||||
transferType: '运营商转接',
|
||||
oldOperator: '中国移动',
|
||||
newOperator: '中国联通',
|
||||
packageName: '随意联畅玩年卡套餐(12个月)',
|
||||
totalTraffic: '100GB',
|
||||
usedTraffic: '25.5GB',
|
||||
transferTime: '2025-11-08 10:30:00',
|
||||
operator: '张若暄',
|
||||
status: '转接成功'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
oldCardNumber: '89860621370079892036',
|
||||
oldAccessNumber: '1440012345679',
|
||||
newCardNumber: '89860621370079892136',
|
||||
newAccessNumber: '1440012345779',
|
||||
transferType: '套餐转换',
|
||||
oldOperator: '中国联通',
|
||||
newOperator: '中国联通',
|
||||
packageName: '如意包年3G流量包',
|
||||
totalTraffic: '50GB',
|
||||
usedTraffic: '12.8GB',
|
||||
transferTime: '2025-11-07 14:15:00',
|
||||
operator: '孔丽娟',
|
||||
status: '转接进行中'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
oldCardNumber: '89860621370079892037',
|
||||
oldAccessNumber: '1440012345680',
|
||||
newCardNumber: '89860621370079892137',
|
||||
newAccessNumber: '1440012345780',
|
||||
transferType: '紧急转接',
|
||||
oldOperator: 'GS移动',
|
||||
newOperator: '中国移动',
|
||||
packageName: 'Y-NB专享套餐',
|
||||
totalTraffic: '30GB',
|
||||
usedTraffic: '28.9GB',
|
||||
transferTime: '2025-11-06 09:45:00',
|
||||
operator: '李佳音',
|
||||
status: '转接失败'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
oldCardNumber: '89860621370079892038',
|
||||
oldAccessNumber: '1440012345681',
|
||||
newCardNumber: '89860621370079892138',
|
||||
newAccessNumber: '1440012345781',
|
||||
transferType: '升级转接',
|
||||
oldOperator: '中国电信',
|
||||
newOperator: '中国电信',
|
||||
packageName: '100G全国流量月卡套餐',
|
||||
totalTraffic: '100GB',
|
||||
usedTraffic: '0GB',
|
||||
transferTime: '2025-11-05 16:20:00',
|
||||
operator: '赵强',
|
||||
status: '等待确认'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
oldCardNumber: '89860621370079892039',
|
||||
oldAccessNumber: '1440012345682',
|
||||
newCardNumber: '89860621370079892139',
|
||||
newAccessNumber: '1440012345782',
|
||||
transferType: '运营商转接',
|
||||
oldOperator: 'GDWL',
|
||||
newOperator: '中国联通',
|
||||
packageName: '广电飞悦卡无预存50G(30天)',
|
||||
totalTraffic: '50GB',
|
||||
usedTraffic: '35.2GB',
|
||||
transferTime: '2025-11-04 11:30:00',
|
||||
operator: '张若暄',
|
||||
status: '转接成功'
|
||||
}
|
||||
]
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1
|
||||
getCardTransferList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1
|
||||
getCardTransferList()
|
||||
}
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '老卡运营商',
|
||||
prop: 'oldOperator',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => operatorOptions.value,
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '类型',
|
||||
prop: 'transferType',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '运营商转接', value: 'operator_transfer' },
|
||||
{ label: '套餐转换', value: 'package_transfer' },
|
||||
{ label: '紧急转接', value: 'emergency_transfer' },
|
||||
{ label: '升级转接', value: 'upgrade_transfer' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: 'ICCID',
|
||||
prop: 'iccid',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入ICCID'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '转卡时间',
|
||||
prop: 'transferDateRange',
|
||||
type: 'daterange',
|
||||
config: {
|
||||
type: 'daterange',
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '新卡运营商',
|
||||
prop: 'newOperator',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => operatorOptions.value,
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '转接成功', value: 'success' },
|
||||
{ label: '转接进行中', value: 'in_progress' },
|
||||
{ label: '转接失败', value: 'failed' },
|
||||
{ label: '等待确认', value: 'pending' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: '老卡卡号', prop: 'oldCardNumber' },
|
||||
{ label: '老卡接入号', prop: 'oldAccessNumber' },
|
||||
{ label: '新卡卡号', prop: 'newCardNumber' },
|
||||
{ label: '新卡接入号', prop: 'newAccessNumber' },
|
||||
{ label: '类型', prop: 'transferType' },
|
||||
{ label: '老卡运营商', prop: 'oldOperator' },
|
||||
{ label: '新卡运营商', prop: 'newOperator' },
|
||||
{ label: '套餐名称', prop: 'packageName' },
|
||||
{ label: '总流量', prop: 'totalTraffic' },
|
||||
{ label: '转卡已使用流量', prop: 'usedTraffic' },
|
||||
{ label: '转卡时间', prop: 'transferTime' },
|
||||
{ label: '操作人', prop: 'operator' },
|
||||
{ label: '状态', prop: 'status' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case '转接成功':
|
||||
return 'success'
|
||||
case '转接进行中':
|
||||
return 'warning'
|
||||
case '转接失败':
|
||||
return 'danger'
|
||||
case '等待确认':
|
||||
return 'info'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取转卡类型标签类型
|
||||
const getTransferTypeTagType = (type: string) => {
|
||||
switch (type) {
|
||||
case '运营商转接':
|
||||
return 'primary'
|
||||
case '套餐转换':
|
||||
return 'success'
|
||||
case '紧急转接':
|
||||
return 'danger'
|
||||
case '升级转接':
|
||||
return 'warning'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 导出数据
|
||||
const exportData = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要导出的数据')
|
||||
return
|
||||
}
|
||||
ElMessage.success(`导出 ${selectedRows.value.length} 条转卡记录`)
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetails = (row: any) => {
|
||||
ElMessage.info(`查看转卡详情: ${row.oldCardNumber} -> ${row.newCardNumber}`)
|
||||
}
|
||||
|
||||
// 取消转卡
|
||||
const cancelTransfer = (row: any) => {
|
||||
if (row.status === '转接成功') {
|
||||
ElMessage.warning('已成功转接的记录无法取消')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`确定要取消该转卡操作吗?`, '取消确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('转卡操作已取消')
|
||||
getCardTransferList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消操作')
|
||||
})
|
||||
}
|
||||
|
||||
// 重新转卡
|
||||
const retryTransfer = (row: any) => {
|
||||
if (row.status !== '转接失败') {
|
||||
ElMessage.warning('只有失败的记录才能重新转卡')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`确定要重新执行转卡操作吗?`, '操作确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('重新转卡操作已开始')
|
||||
getCardTransferList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消操作')
|
||||
})
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'oldCardNumber',
|
||||
label: '老卡卡号',
|
||||
minWidth: 180
|
||||
},
|
||||
{
|
||||
prop: 'oldAccessNumber',
|
||||
label: '老卡接入号',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
prop: 'newCardNumber',
|
||||
label: '新卡卡号',
|
||||
minWidth: 180
|
||||
},
|
||||
{
|
||||
prop: 'newAccessNumber',
|
||||
label: '新卡接入号',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
prop: 'transferType',
|
||||
label: '类型',
|
||||
width: 120,
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getTransferTypeTagType(row.transferType) }, () => row.transferType)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'oldOperator',
|
||||
label: '老卡运营商',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'newOperator',
|
||||
label: '新卡运营商',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'packageName',
|
||||
label: '套餐名称',
|
||||
minWidth: 200
|
||||
},
|
||||
{
|
||||
prop: 'totalTraffic',
|
||||
label: '总流量',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'usedTraffic',
|
||||
label: '转卡已使用流量',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
prop: 'transferTime',
|
||||
label: '转卡时间',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
prop: 'operator',
|
||||
label: '操作人',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 120,
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getStatusType(row.status) }, () => row.status)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 220,
|
||||
formatter: (row: any) => {
|
||||
return h('div', { class: 'operation-buttons' }, [
|
||||
h(ArtButtonTable, {
|
||||
text: '查看',
|
||||
onClick: () => viewDetails(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '重试',
|
||||
disabled: row.status !== '转接失败',
|
||||
onClick: () => retryTransfer(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '取消',
|
||||
disabled: row.status === '转接成功',
|
||||
onClick: () => cancelTransfer(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getCardTransferList()
|
||||
})
|
||||
|
||||
// 获取网卡转接列表
|
||||
const getCardTransferList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
|
||||
const endIndex = startIndex + pagination.pageSize
|
||||
const paginatedData = mockData.slice(startIndex, endIndex)
|
||||
|
||||
tableData.value = paginatedData
|
||||
pagination.total = mockData.length
|
||||
loading.value = false
|
||||
} catch (error) {
|
||||
console.error('获取网卡转接列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getCardTransferList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getCardTransferList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getCardTransferList()
|
||||
}
|
||||
|
||||
// 显示批量转卡对话框
|
||||
const showBatchTransferDialog = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要转卡的记录')
|
||||
return
|
||||
}
|
||||
dialogVisible.value = true
|
||||
// 重置表单
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
formData.transferType = ''
|
||||
formData.targetOperator = ''
|
||||
formData.targetPackage = ''
|
||||
formData.effectiveTime = ''
|
||||
formData.keepTraffic = true
|
||||
formData.reason = ''
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
transferType: [{ required: true, message: '请选择转卡类型', trigger: 'change' }],
|
||||
targetOperator: [{ required: true, message: '请选择目标运营商', trigger: 'change' }],
|
||||
targetPackage: [{ required: true, message: '请选择目标套餐', trigger: 'change' }],
|
||||
effectiveTime: [{ required: true, message: '请选择生效时间', trigger: 'change' }],
|
||||
reason: [
|
||||
{ required: true, message: '请输入转卡原因', trigger: 'blur' },
|
||||
{ min: 5, max: 200, message: '转卡原因长度在 5 到 200 个字符', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true
|
||||
|
||||
// 模拟提交过程
|
||||
setTimeout(() => {
|
||||
const targetOperatorName = operatorOptions.value.find(
|
||||
(op) => op.value === formData.targetOperator
|
||||
)?.label
|
||||
const targetPackageName = packageOptions.value.find(
|
||||
(pkg) => pkg.value === formData.targetPackage
|
||||
)?.label
|
||||
ElMessage.success(
|
||||
`批量转卡操作提交成功!目标运营商:${targetOperatorName},目标套餐:${targetPackageName},影响${selectedRows.value.length}张网卡`
|
||||
)
|
||||
dialogVisible.value = false
|
||||
submitLoading.value = false
|
||||
getCardTransferList()
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-transfer-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
:deep(.operation-buttons) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
699
src/views/card-management/my-cards/index.vue
Normal file
699
src/views/card-management/my-cards/index.vue
Normal file
@@ -0,0 +1,699 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="my-cards-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton @click="exportExcel">导出excel</ElButton>
|
||||
<ElButton @click="cardDistribution">网卡分销</ElButton>
|
||||
<ElButton @click="batchRecharge">批量充值</ElButton>
|
||||
<ElButton @click="cardRecycle">网卡回收</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 网卡分销弹框 -->
|
||||
<CardOperationDialog
|
||||
v-model:visible="distributionDialogVisible"
|
||||
title="网卡分销"
|
||||
select-label="代理商"
|
||||
select-placeholder="请搜索并选择代理商"
|
||||
:selected-cards="selectedRows"
|
||||
:remote-search="searchAgents"
|
||||
@confirm="handleDistributionConfirm"
|
||||
@close="handleDialogClose"
|
||||
/>
|
||||
|
||||
<!-- 批量充值弹框 -->
|
||||
<CardOperationDialog
|
||||
v-model:visible="rechargeDialogVisible"
|
||||
title="批量充值"
|
||||
select-label="套餐"
|
||||
select-placeholder="请搜索并选择套餐"
|
||||
:show-amount="true"
|
||||
:selected-cards="selectedRows"
|
||||
:remote-search="searchPackages"
|
||||
@confirm="handleRechargeConfirm"
|
||||
@close="handleDialogClose"
|
||||
/>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ElTag, ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
import CardOperationDialog from '@/components/business/CardOperationDialog.vue'
|
||||
|
||||
defineOptions({ name: 'MyCards' })
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
// 弹框状态
|
||||
const distributionDialogVisible = ref(false)
|
||||
const rechargeDialogVisible = ref(false)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
distributor: '',
|
||||
cardStatus: '',
|
||||
importDateRange: '',
|
||||
cardCompany: '',
|
||||
virtualNumber: '',
|
||||
iccid: '',
|
||||
iccidRange: '',
|
||||
cardPackage: '',
|
||||
virtualNumberRange: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
cardCompany: '联通2',
|
||||
iccid: '89860621370079892035',
|
||||
virtualNumber: '10655001234',
|
||||
expireTime: '2025-12-31',
|
||||
packageName: '随意联畅玩年卡套餐(12个月)',
|
||||
distributorName: '张丽丽',
|
||||
cardStatus: '正常'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
cardCompany: '联通36',
|
||||
iccid: '89860621370079892036',
|
||||
virtualNumber: '10655001235',
|
||||
expireTime: '2025-11-30',
|
||||
packageName: '如意包年3G流量包',
|
||||
distributorName: '王小明',
|
||||
cardStatus: '正常'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
cardCompany: 'SXKJ-NB',
|
||||
iccid: '89860621370079892037',
|
||||
virtualNumber: '10655001236',
|
||||
expireTime: '2025-10-31',
|
||||
packageName: 'Y-NB专享套餐',
|
||||
distributorName: 'HNSXKJ',
|
||||
cardStatus: '待激活'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
cardCompany: '联通1-1',
|
||||
iccid: '89860621370079892038',
|
||||
virtualNumber: '10655001237',
|
||||
expireTime: '2026-01-31',
|
||||
packageName: '100G全国流量月卡套餐',
|
||||
distributorName: '未分销',
|
||||
cardStatus: '正常'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
cardCompany: '广电4',
|
||||
iccid: '89860621370079892039',
|
||||
virtualNumber: '10655001238',
|
||||
expireTime: '2025-08-31',
|
||||
packageName: '广电飞悦卡无预存50G(30天)',
|
||||
distributorName: '孔丽娟',
|
||||
cardStatus: '停机'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
cardCompany: '联通8',
|
||||
iccid: '89860621370079892040',
|
||||
virtualNumber: '10655001239',
|
||||
expireTime: '2025-09-15',
|
||||
packageName: '5G畅享套餐',
|
||||
distributorName: '李佳音',
|
||||
cardStatus: '正常'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
cardCompany: '移动21',
|
||||
iccid: '89860621370079892041',
|
||||
virtualNumber: '10655001240',
|
||||
expireTime: '2025-07-20',
|
||||
packageName: '移动物联网专用套餐',
|
||||
distributorName: '赵强',
|
||||
cardStatus: '已过期'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
cardCompany: '电信9',
|
||||
iccid: '89860621370079892042',
|
||||
virtualNumber: '10655001241',
|
||||
expireTime: '2026-02-28',
|
||||
packageName: '电信天翼套餐',
|
||||
distributorName: '未分销',
|
||||
cardStatus: '正常'
|
||||
}
|
||||
]
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1
|
||||
getMyCardsList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1
|
||||
getMyCardsList()
|
||||
}
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '分销商',
|
||||
prop: 'distributor',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入分销商'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '网卡状态',
|
||||
prop: 'cardStatus',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '正常', value: 'normal' },
|
||||
{ label: '待激活', value: 'pending' },
|
||||
{ label: '停机', value: 'suspended' },
|
||||
{ label: '已过期', value: 'expired' },
|
||||
{ label: '注销', value: 'cancelled' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '导入时间',
|
||||
prop: 'importDateRange',
|
||||
type: 'daterange',
|
||||
config: {
|
||||
type: 'daterange',
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '开卡公司',
|
||||
prop: 'cardCompany',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '联通2', value: 'unicom2' },
|
||||
{ label: '联通36', value: 'unicom36' },
|
||||
{ label: 'SXKJ-NB', value: 'sxkj_nb' },
|
||||
{ label: '联通1-1', value: 'unicom1_1' },
|
||||
{ label: '联通8', value: 'unicom8' },
|
||||
{ label: '移动21', value: 'mobile21' },
|
||||
{ label: '广电4', value: 'gdtv4' },
|
||||
{ label: '电信9', value: 'telecom9' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '虚拟号',
|
||||
prop: 'virtualNumber',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入虚拟号'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: 'ICCID号',
|
||||
prop: 'iccid',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入ICCID号'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: 'ICCID号段',
|
||||
prop: 'iccidRange',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入ICCID号段,如:8986001-8986999'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '卡套餐',
|
||||
prop: 'cardPackage',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '随意联畅玩年卡套餐', value: 'package1' },
|
||||
{ label: '如意包年3G流量包', value: 'package2' },
|
||||
{ label: 'Y-NB专享套餐', value: 'package3' },
|
||||
{ label: '100G全国流量月卡套餐', value: 'package4' },
|
||||
{ label: '广电飞悦卡无预存50G', value: 'package5' },
|
||||
{ label: '5G畅享套餐', value: 'package6' },
|
||||
{ label: '移动物联网专用套餐', value: 'package7' },
|
||||
{ label: '电信天翼套餐', value: 'package8' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '虚拟号段',
|
||||
prop: 'virtualNumberRange',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入虚拟号段,如:10655001-10655999'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: '开卡公司', prop: 'cardCompany' },
|
||||
{ label: 'ICCID卡号', prop: 'iccid' },
|
||||
{ label: '虚拟号', prop: 'virtualNumber' },
|
||||
{ label: '到期时间', prop: 'expireTime' },
|
||||
{ label: '套餐', prop: 'packageName' },
|
||||
{ label: '分销商姓名', prop: 'distributorName' },
|
||||
{ label: '卡状态', prop: 'cardStatus' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 获取卡状态标签类型
|
||||
const getCardStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case '正常':
|
||||
return 'success'
|
||||
case '待激活':
|
||||
return 'warning'
|
||||
case '停机':
|
||||
case '注销':
|
||||
return 'danger'
|
||||
case '已过期':
|
||||
return 'info'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 导出Excel
|
||||
const exportExcel = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要导出的数据')
|
||||
return
|
||||
}
|
||||
ElMessage.success(`导出 ${selectedRows.value.length} 条记录到Excel`)
|
||||
}
|
||||
|
||||
// 网卡分销
|
||||
const cardDistribution = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要分销的网卡')
|
||||
return
|
||||
}
|
||||
distributionDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 批量充值
|
||||
const batchRecharge = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要充值的网卡')
|
||||
return
|
||||
}
|
||||
rechargeDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 网卡回收
|
||||
const cardRecycle = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要回收的网卡')
|
||||
return
|
||||
}
|
||||
|
||||
ElMessageBox.confirm(
|
||||
`确定要回收所选的 ${selectedRows.value.length} 张网卡吗?回收后将无法恢复。`,
|
||||
'网卡回收确认',
|
||||
{
|
||||
confirmButtonText: '确认回收',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
dangerouslyUseHTMLString: false
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
handleCardRecycle()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消回收操作')
|
||||
})
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetails = (row: any) => {
|
||||
ElMessage.info(`查看网卡 ${row.iccid} 的详细信息`)
|
||||
}
|
||||
|
||||
// 编辑网卡
|
||||
const editCard = (row: any) => {
|
||||
ElMessage.info(`编辑网卡 ${row.iccid}`)
|
||||
}
|
||||
|
||||
// 删除网卡
|
||||
const deleteCard = (row: any) => {
|
||||
ElMessageBox.confirm(`确定要删除ICCID为 ${row.iccid} 的网卡吗?`, '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success(`已删除网卡 ${row.iccid}`)
|
||||
getMyCardsList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消删除')
|
||||
})
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'cardCompany',
|
||||
label: '开卡公司',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'iccid',
|
||||
label: 'ICCID卡号',
|
||||
minWidth: 180
|
||||
},
|
||||
{
|
||||
prop: 'virtualNumber',
|
||||
label: '虚拟号',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'expireTime',
|
||||
label: '到期时间',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'packageName',
|
||||
label: '套餐',
|
||||
minWidth: 200
|
||||
},
|
||||
{
|
||||
prop: 'distributorName',
|
||||
label: '分销商姓名',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'cardStatus',
|
||||
label: '卡状态',
|
||||
width: 100,
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getCardStatusType(row.cardStatus) }, () => row.cardStatus)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 220,
|
||||
formatter: (row: any) => {
|
||||
return h('div', { class: 'operation-buttons' }, [
|
||||
h(ArtButtonTable, {
|
||||
text: '查看',
|
||||
onClick: () => viewDetails(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '编辑',
|
||||
onClick: () => editCard(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '删除',
|
||||
onClick: () => deleteCard(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getMyCardsList()
|
||||
})
|
||||
|
||||
// 获取我的网卡列表
|
||||
const getMyCardsList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
|
||||
const endIndex = startIndex + pagination.pageSize
|
||||
const paginatedData = mockData.slice(startIndex, endIndex)
|
||||
|
||||
tableData.value = paginatedData
|
||||
pagination.total = mockData.length
|
||||
loading.value = false
|
||||
} catch (error) {
|
||||
console.error('获取我的网卡列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getMyCardsList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getMyCardsList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getMyCardsList()
|
||||
}
|
||||
|
||||
// 远程搜索代理商
|
||||
const searchAgents = async (query: string) => {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 300))
|
||||
|
||||
// 模拟代理商数据
|
||||
const allAgents = [
|
||||
{ label: '张丽丽代理商', value: 'agent_001' },
|
||||
{ label: '王小明代理商', value: 'agent_002' },
|
||||
{ label: 'HNSXKJ代理商', value: 'agent_003' },
|
||||
{ label: '孔丽娟代理商', value: 'agent_004' },
|
||||
{ label: '李四代理商', value: 'agent_005' },
|
||||
{ label: '赵六代理商', value: 'agent_006' },
|
||||
{ label: '刘备代理商', value: 'agent_007' },
|
||||
{ label: '关羽代理商', value: 'agent_008' },
|
||||
{ label: '张飞代理商', value: 'agent_009' },
|
||||
{ label: '赵云代理商', value: 'agent_010' },
|
||||
{ label: '黄忠代理商', value: 'agent_011' },
|
||||
{ label: '马超代理商', value: 'agent_012' },
|
||||
{ label: '诸葛亮代理商', value: 'agent_013' },
|
||||
{ label: '周瑜代理商', value: 'agent_014' },
|
||||
{ label: '孙权代理商', value: 'agent_015' }
|
||||
]
|
||||
|
||||
// 根据查询条件过滤
|
||||
let filteredAgents = allAgents
|
||||
if (query) {
|
||||
filteredAgents = allAgents.filter((agent) =>
|
||||
agent.label.toLowerCase().includes(query.toLowerCase())
|
||||
)
|
||||
}
|
||||
|
||||
// 返回前10条数据
|
||||
return filteredAgents.slice(0, 10)
|
||||
}
|
||||
|
||||
// 远程搜索套餐
|
||||
const searchPackages = async (query: string) => {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 300))
|
||||
|
||||
// 模拟套餐数据
|
||||
const allPackages = [
|
||||
{ label: '随意联畅玩年卡套餐(12个月)', value: 'package_001' },
|
||||
{ label: '如意包年3G流量包', value: 'package_002' },
|
||||
{ label: 'Y-NB专享套餐', value: 'package_003' },
|
||||
{ label: '100G全国流量月卡套餐', value: 'package_004' },
|
||||
{ label: '广电飞悦卡无预存50G(30天)', value: 'package_005' },
|
||||
{ label: '移动畅享套餐50G', value: 'package_006' },
|
||||
{ label: '联通大王卡19元', value: 'package_007' },
|
||||
{ label: '电信星卡29元', value: 'package_008' },
|
||||
{ label: '移动无限流量卡99元', value: 'package_009' },
|
||||
{ label: '联通冰淇淋套餐199元', value: 'package_010' },
|
||||
{ label: '电信天翼云卡59元', value: 'package_011' },
|
||||
{ label: '移动神州行卡39元', value: 'package_012' },
|
||||
{ label: '联通腾讯王卡免流', value: 'package_013' },
|
||||
{ label: '电信达量降速套餐', value: 'package_014' },
|
||||
{ label: '广电5G畅享套餐', value: 'package_015' }
|
||||
]
|
||||
|
||||
// 根据查询条件过滤
|
||||
let filteredPackages = allPackages
|
||||
if (query) {
|
||||
filteredPackages = allPackages.filter((pkg) =>
|
||||
pkg.label.toLowerCase().includes(query.toLowerCase())
|
||||
)
|
||||
}
|
||||
|
||||
// 返回前10条数据
|
||||
return filteredPackages.slice(0, 10)
|
||||
}
|
||||
|
||||
// 处理网卡分销确认
|
||||
const handleDistributionConfirm = async (data: any) => {
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
ElMessage.success(`成功将 ${data.selectedCards.length} 张网卡分销给代理商`)
|
||||
distributionDialogVisible.value = false
|
||||
|
||||
// 刷新列表
|
||||
getMyCardsList()
|
||||
} catch (error) {
|
||||
ElMessage.error('分销操作失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 处理批量充值确认
|
||||
const handleRechargeConfirm = async (data: any) => {
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
ElMessage.success(`成功为 ${data.selectedCards.length} 张网卡充值 ${data.amount} 元`)
|
||||
rechargeDialogVisible.value = false
|
||||
|
||||
// 刷新列表
|
||||
getMyCardsList()
|
||||
} catch (error) {
|
||||
ElMessage.error('充值操作失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 处理网卡回收
|
||||
const handleCardRecycle = async () => {
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
ElMessage.success(`成功回收 ${selectedRows.value.length} 张网卡`)
|
||||
|
||||
// 从列表中移除回收的网卡
|
||||
const recycledIds = selectedRows.value.map((row) => row.id)
|
||||
tableData.value = tableData.value.filter((item) => !recycledIds.includes(item.id))
|
||||
selectedRows.value = []
|
||||
} catch (error) {
|
||||
ElMessage.error('回收操作失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 处理弹框关闭
|
||||
const handleDialogClose = () => {
|
||||
// 可以在这里添加额外的关闭逻辑
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.my-cards-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
:deep(.operation-buttons) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
714
src/views/card-management/offline-batch-recharge/index.vue
Normal file
714
src/views/card-management/offline-batch-recharge/index.vue
Normal file
@@ -0,0 +1,714 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="offline-batch-recharge-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 数据视图组件 -->
|
||||
<ArtDataViewer
|
||||
ref="dataViewerRef"
|
||||
:data="tableData"
|
||||
:loading="loading"
|
||||
:table-columns="columns"
|
||||
:descriptions-fields="descriptionsFields"
|
||||
:descriptions-columns="2"
|
||||
:pagination="pagination"
|
||||
:label-width="'120px'"
|
||||
:field-columns="columnChecks"
|
||||
:show-card-actions="true"
|
||||
:show-card-selection="true"
|
||||
:default-view="currentView"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@view-change="handleViewChange"
|
||||
>
|
||||
<template #header-left>
|
||||
<ElButton type="primary" @click="handleSearch">搜索查询</ElButton>
|
||||
<ElButton type="success" @click="showBatchRechargeDialog">批量充值</ElButton>
|
||||
<ElButton @click="exportExcel">导出excel</ElButton>
|
||||
</template>
|
||||
|
||||
<template #header-right>
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
v-model:currentView="currentView"
|
||||
@refresh="handleRefresh"
|
||||
@viewChange="handleViewChange"
|
||||
:show-title="false"
|
||||
:show-view-toggle="true"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #card-actions="{ item }">
|
||||
<ArtButtonTable
|
||||
text="查看"
|
||||
@click="viewDetails(item)"
|
||||
/>
|
||||
<ArtButtonTable
|
||||
text="重试"
|
||||
@click="retryRecharge(item)"
|
||||
/>
|
||||
<ArtButtonTable
|
||||
text="删除"
|
||||
@click="deleteRecord(item)"
|
||||
/>
|
||||
</template>
|
||||
</ArtDataViewer>
|
||||
|
||||
<!-- 批量充值对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
title="批量充值"
|
||||
width="600px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<!-- 顶部下载模板按钮 -->
|
||||
<div class="template-section">
|
||||
<ElButton type="primary" @click="downloadTemplate" icon="Download"> 下载模板 </ElButton>
|
||||
<span class="template-tip">请先下载模板,按模板格式填写后上传</span>
|
||||
</div>
|
||||
|
||||
<ElDivider />
|
||||
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<ElFormItem label="上传Excel文件" prop="excelFile">
|
||||
<ElUpload
|
||||
ref="uploadRef"
|
||||
:limit="1"
|
||||
:on-exceed="handleExceed"
|
||||
:before-upload="beforeUpload"
|
||||
:on-change="handleFileChange"
|
||||
:auto-upload="false"
|
||||
accept=".xlsx,.xls"
|
||||
drag
|
||||
>
|
||||
<ElIcon class="el-icon--upload"><UploadFilled /></ElIcon>
|
||||
<div class="el-upload__text"> 将文件拖到此处,或<em>点击上传</em> </div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip"> 只能上传 xlsx/xls 文件,且不超过 10MB </div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="选择套餐" prop="packageId">
|
||||
<ElSelect
|
||||
v-model="formData.packageId"
|
||||
placeholder="请选择套餐"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<ElOption
|
||||
v-for="pkg in packageOptions"
|
||||
:key="pkg.value"
|
||||
:label="pkg.label"
|
||||
:value="pkg.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="生效方式" prop="effectiveMethod">
|
||||
<ElSelect
|
||||
v-model="formData.effectiveMethod"
|
||||
placeholder="请选择生效方式"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<ElOption
|
||||
v-for="method in effectiveMethods"
|
||||
:key="method.value"
|
||||
:label="method.label"
|
||||
:value="method.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
label="生效时间"
|
||||
prop="effectiveTime"
|
||||
v-if="formData.effectiveMethod === 'scheduled'"
|
||||
>
|
||||
<ElDatePicker
|
||||
v-model="formData.effectiveTime"
|
||||
type="datetime"
|
||||
placeholder="请选择生效时间"
|
||||
style="width: 100%"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="备注说明" prop="remark">
|
||||
<ElInput
|
||||
v-model="formData.remark"
|
||||
type="textarea"
|
||||
placeholder="请输入备注说明(可选)"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
确认充值
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import {
|
||||
ElTag,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElUpload,
|
||||
ElIcon,
|
||||
ElSelect,
|
||||
ElOption,
|
||||
ElDatePicker,
|
||||
ElDivider,
|
||||
ElButton
|
||||
} from 'element-plus'
|
||||
import { UploadFilled, Download } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, UploadInstance, UploadRawFile, UploadFile } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import ArtDataViewer from '@/components/core/views/ArtDataViewer.vue'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
|
||||
defineOptions({ name: 'OfflineBatchRecharge' })
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
|
||||
// 当前视图模式
|
||||
const currentView = ref('table')
|
||||
|
||||
// 数据视图组件引用
|
||||
const dataViewerRef = ref()
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
importDateRange: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 表单实例
|
||||
const formRef = ref<FormInstance>()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
excelFile: null as File | null,
|
||||
packageId: '',
|
||||
effectiveMethod: '',
|
||||
effectiveTime: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 套餐选项
|
||||
const packageOptions = ref([
|
||||
{ label: '随意联畅玩年卡套餐(12个月)', value: 'package1' },
|
||||
{ label: '如意包年3G流量包', value: 'package2' },
|
||||
{ label: 'Y-NB专享套餐', value: 'package3' },
|
||||
{ label: '100G全国流量月卡套餐', value: 'package4' },
|
||||
{ label: '广电飞悦卡无预存50G(30天)', value: 'package5' },
|
||||
{ label: '5G畅享套餐', value: 'package6' },
|
||||
{ label: '移动物联网专用套餐', value: 'package7' },
|
||||
{ label: '电信天翼套餐', value: 'package8' }
|
||||
])
|
||||
|
||||
// 生效方式选项
|
||||
const effectiveMethods = ref([
|
||||
{ label: '立即生效', value: 'immediate' },
|
||||
{ label: '定时生效', value: 'scheduled' },
|
||||
{ label: '次月生效', value: 'next_month' },
|
||||
{ label: '手动激活', value: 'manual' }
|
||||
])
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
iccidStart: '89860621370079892001',
|
||||
iccidEnd: '89860621370079892100',
|
||||
importTime: '2025-11-08 10:30:00',
|
||||
importCount: 100,
|
||||
successCount: 98,
|
||||
failureCount: 2,
|
||||
packageName: '随意联畅玩年卡套餐(12个月)',
|
||||
packageCount: 100,
|
||||
operator: '张若暄'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
iccidStart: '89860621370079892101',
|
||||
iccidEnd: '89860621370079892200',
|
||||
importTime: '2025-11-07 14:15:00',
|
||||
importCount: 100,
|
||||
successCount: 100,
|
||||
failureCount: 0,
|
||||
packageName: '如意包年3G流量包',
|
||||
packageCount: 100,
|
||||
operator: '孔丽娟'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
iccidStart: '89860621370079892201',
|
||||
iccidEnd: '89860621370079892250',
|
||||
importTime: '2025-11-06 09:45:00',
|
||||
importCount: 50,
|
||||
successCount: 45,
|
||||
failureCount: 5,
|
||||
packageName: 'Y-NB专享套餐',
|
||||
packageCount: 50,
|
||||
operator: '李佳音'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
iccidStart: '89860621370079892251',
|
||||
iccidEnd: '89860621370079892350',
|
||||
importTime: '2025-11-05 16:20:00',
|
||||
importCount: 100,
|
||||
successCount: 97,
|
||||
failureCount: 3,
|
||||
packageName: '100G全国流量月卡套餐',
|
||||
packageCount: 100,
|
||||
operator: '张若暄'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
iccidStart: '89860621370079892351',
|
||||
iccidEnd: '89860621370079892400',
|
||||
importTime: '2025-11-04 11:30:00',
|
||||
importCount: 50,
|
||||
successCount: 50,
|
||||
failureCount: 0,
|
||||
packageName: '广电飞悦卡无预存50G(30天)',
|
||||
packageCount: 50,
|
||||
operator: '赵强'
|
||||
}
|
||||
]
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1
|
||||
getOfflineBatchRechargeList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1
|
||||
getOfflineBatchRechargeList()
|
||||
}
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '导入时间',
|
||||
prop: 'importDateRange',
|
||||
type: 'daterange',
|
||||
config: {
|
||||
type: 'daterange',
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: 'ICCID开始号段', prop: 'iccidStart' },
|
||||
{ label: 'ICCID结束号段', prop: 'iccidEnd' },
|
||||
{ label: '导入时间', prop: 'importTime' },
|
||||
{ label: '导入数量', prop: 'importCount' },
|
||||
{ label: '成功数量', prop: 'successCount' },
|
||||
{ label: '失败数量', prop: 'failureCount' },
|
||||
{ label: '套餐名称', prop: 'packageName' },
|
||||
{ label: '套餐数量', prop: 'packageCount' },
|
||||
{ label: '操作人', prop: 'operator' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 描述字段配置
|
||||
const descriptionsFields = [
|
||||
{ prop: 'iccidStart', label: 'ICCID开始号段' },
|
||||
{ prop: 'iccidEnd', label: 'ICCID结束号段' },
|
||||
{ prop: 'importTime', label: '导入时间' },
|
||||
{ prop: 'importCount', label: '导入数量', formatter: (row: any) => `${row.importCount} 张` },
|
||||
{
|
||||
prop: 'successCount',
|
||||
label: '成功数量',
|
||||
formatter: (row: any) => {
|
||||
return `<el-tag type="success" size="small">${row.successCount} 张</el-tag>`
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'failureCount',
|
||||
label: '失败数量',
|
||||
formatter: (row: any) => {
|
||||
const type = row.failureCount > 0 ? 'danger' : 'success'
|
||||
return `<el-tag type="${type}" size="small">${row.failureCount} 张</el-tag>`
|
||||
}
|
||||
},
|
||||
{ prop: 'packageName', label: '套餐名称', span: 2 },
|
||||
{ prop: 'packageCount', label: '套餐数量', formatter: (row: any) => `${row.packageCount} 个` },
|
||||
{ prop: 'operator', label: '操作人' }
|
||||
]
|
||||
|
||||
// 导出Excel
|
||||
const exportExcel = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要导出的数据')
|
||||
return
|
||||
}
|
||||
ElMessage.success(`导出 ${selectedRows.value.length} 条记录到Excel`)
|
||||
}
|
||||
|
||||
// 下载模板
|
||||
const downloadTemplate = () => {
|
||||
ElMessage.success('正在下载充值模板文件...')
|
||||
// 这里可以实现实际的模板下载功能
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetails = (row: any) => {
|
||||
ElMessage.info(`查看充值记录详情: ${row.iccidStart} - ${row.iccidEnd}`)
|
||||
}
|
||||
|
||||
// 删除记录
|
||||
const deleteRecord = (row: any) => {
|
||||
ElMessageBox.confirm(`确定要删除该充值记录吗?`, '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('删除成功')
|
||||
getOfflineBatchRechargeList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消删除')
|
||||
})
|
||||
}
|
||||
|
||||
// 重新充值
|
||||
const retryRecharge = (row: any) => {
|
||||
ElMessageBox.confirm(`确定要重新执行该批次的充值操作吗?`, '操作确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('重新充值操作已开始')
|
||||
getOfflineBatchRechargeList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消操作')
|
||||
})
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'iccidStart',
|
||||
label: 'ICCID开始号段',
|
||||
minWidth: 220
|
||||
},
|
||||
{
|
||||
prop: 'iccidEnd',
|
||||
label: 'ICCID结束号段',
|
||||
minWidth: 220
|
||||
},
|
||||
{
|
||||
prop: 'importTime',
|
||||
label: '导入时间',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
prop: 'importCount',
|
||||
label: '导入数量',
|
||||
width: 100,
|
||||
formatter: (row) => `${row.importCount} 张`
|
||||
},
|
||||
{
|
||||
prop: 'successCount',
|
||||
label: '成功数量',
|
||||
width: 100,
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: 'success' }, () => `${row.successCount} 张`)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'failureCount',
|
||||
label: '失败数量',
|
||||
width: 100,
|
||||
formatter: (row) => {
|
||||
return h(
|
||||
ElTag,
|
||||
{ type: row.failureCount > 0 ? 'danger' : 'success' },
|
||||
() => `${row.failureCount} 张`
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'packageName',
|
||||
label: '套餐名称',
|
||||
minWidth: 200
|
||||
},
|
||||
{
|
||||
prop: 'packageCount',
|
||||
label: '套餐数量',
|
||||
width: 100,
|
||||
formatter: (row) => `${row.packageCount} 个`
|
||||
},
|
||||
{
|
||||
prop: 'operator',
|
||||
label: '操作人',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 220,
|
||||
formatter: (row: any) => {
|
||||
return h('div', { class: 'operation-buttons' }, [
|
||||
h(ArtButtonTable, {
|
||||
text: '查看',
|
||||
onClick: () => viewDetails(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '重试',
|
||||
onClick: () => retryRecharge(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '删除',
|
||||
onClick: () => deleteRecord(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getOfflineBatchRechargeList()
|
||||
})
|
||||
|
||||
// 获取线下批量充值列表
|
||||
const getOfflineBatchRechargeList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
|
||||
const endIndex = startIndex + pagination.pageSize
|
||||
const paginatedData = mockData.slice(startIndex, endIndex)
|
||||
|
||||
tableData.value = paginatedData
|
||||
pagination.total = mockData.length
|
||||
loading.value = false
|
||||
} catch (error) {
|
||||
console.error('获取线下批量充值列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getOfflineBatchRechargeList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getOfflineBatchRechargeList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getOfflineBatchRechargeList()
|
||||
}
|
||||
|
||||
// 处理视图切换
|
||||
const handleViewChange = (view: string) => {
|
||||
console.log('视图切换到:', view)
|
||||
// 可以在这里添加视图切换的额外逻辑,比如保存用户偏好
|
||||
}
|
||||
|
||||
// 显示批量充值对话框
|
||||
const showBatchRechargeDialog = () => {
|
||||
dialogVisible.value = true
|
||||
// 重置表单
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
formData.excelFile = null
|
||||
formData.packageId = ''
|
||||
formData.effectiveMethod = ''
|
||||
formData.effectiveTime = ''
|
||||
formData.remark = ''
|
||||
}
|
||||
|
||||
// 文件上传限制
|
||||
const handleExceed = () => {
|
||||
ElMessage.warning('最多只能上传一个文件')
|
||||
}
|
||||
|
||||
// 文件上传前检查
|
||||
const beforeUpload = (file: UploadRawFile) => {
|
||||
const isExcel =
|
||||
file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
|
||||
file.type === 'application/vnd.ms-excel'
|
||||
const isLt10M = file.size / 1024 / 1024 < 10
|
||||
|
||||
if (!isExcel) {
|
||||
ElMessage.error('只能上传 Excel 文件!')
|
||||
return false
|
||||
}
|
||||
if (!isLt10M) {
|
||||
ElMessage.error('上传文件大小不能超过 10MB!')
|
||||
return false
|
||||
}
|
||||
return false // 阻止自动上传
|
||||
}
|
||||
|
||||
// 文件变化处理
|
||||
const handleFileChange = (file: UploadFile) => {
|
||||
if (file.raw) {
|
||||
formData.excelFile = file.raw
|
||||
}
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
excelFile: [{ required: true, message: '请上传Excel文件', trigger: 'change' }],
|
||||
packageId: [{ required: true, message: '请选择套餐', trigger: 'change' }],
|
||||
effectiveMethod: [{ required: true, message: '请选择生效方式', trigger: 'change' }],
|
||||
effectiveTime: [{ required: true, message: '请选择生效时间', trigger: 'change' }]
|
||||
})
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
// 检查文件是否上传
|
||||
if (!formData.excelFile) {
|
||||
ElMessage.error('请先上传Excel文件')
|
||||
return
|
||||
}
|
||||
|
||||
// 如果是定时生效,验证生效时间
|
||||
if (formData.effectiveMethod === 'scheduled' && !formData.effectiveTime) {
|
||||
ElMessage.error('定时生效必须选择生效时间')
|
||||
return
|
||||
}
|
||||
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true
|
||||
|
||||
// 模拟提交过程
|
||||
setTimeout(() => {
|
||||
const packageName = packageOptions.value.find(
|
||||
(p) => p.value === formData.packageId
|
||||
)?.label
|
||||
const effectiveMethodName = effectiveMethods.value.find(
|
||||
(m) => m.value === formData.effectiveMethod
|
||||
)?.label
|
||||
ElMessage.success(
|
||||
`批量充值提交成功!套餐:${packageName},生效方式:${effectiveMethodName}`
|
||||
)
|
||||
dialogVisible.value = false
|
||||
submitLoading.value = false
|
||||
getOfflineBatchRechargeList()
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.offline-batch-recharge-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
.template-section {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.template-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.operation-buttons) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.el-upload-dragger) {
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
689
src/views/card-management/package-gift/index.vue
Normal file
689
src/views/card-management/package-gift/index.vue
Normal file
@@ -0,0 +1,689 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="package-gift-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton type="primary" @click="handleSearch">搜索</ElButton>
|
||||
<ElButton type="success" @click="showBatchImportDialog">批量导入</ElButton>
|
||||
<ElButton type="danger" @click="batchDelete">批量删除</ElButton>
|
||||
<ElButton @click="exportExcel">导出excel</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 批量导入对话框 -->
|
||||
<ElDialog
|
||||
v-model="importDialogVisible"
|
||||
title="批量导入套餐赠送"
|
||||
width="500px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<!-- 顶部下载模板按钮 -->
|
||||
<div class="template-section">
|
||||
<ElButton type="primary" @click="downloadTemplate" icon="Download"> 下载模板 </ElButton>
|
||||
<span class="template-tip">请先下载模板,按模板格式填写后上传</span>
|
||||
</div>
|
||||
|
||||
<ElDivider />
|
||||
|
||||
<ElForm
|
||||
ref="importFormRef"
|
||||
:model="importFormData"
|
||||
:rules="importRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<ElFormItem label="上传Excel文件" prop="excelFile">
|
||||
<ElUpload
|
||||
ref="uploadRef"
|
||||
:limit="1"
|
||||
:on-exceed="handleExceed"
|
||||
:before-upload="beforeUpload"
|
||||
:on-change="handleFileChange"
|
||||
:auto-upload="false"
|
||||
accept=".xlsx,.xls"
|
||||
drag
|
||||
>
|
||||
<ElIcon class="el-icon--upload"><UploadFilled /></ElIcon>
|
||||
<div class="el-upload__text"> 将文件拖到此处,或<em>点击上传</em> </div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip"> 只能上传 xlsx/xls 文件,且不超过 10MB </div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="备注说明" prop="remark">
|
||||
<ElInput
|
||||
v-model="importFormData.remark"
|
||||
type="textarea"
|
||||
placeholder="请输入备注说明(可选)"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="importDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleImportSubmit" :loading="importLoading">
|
||||
确认导入
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ElTag, ElMessage, ElMessageBox, ElUpload, ElIcon, ElDivider } from 'element-plus'
|
||||
import { UploadFilled, Download } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, UploadInstance, UploadRawFile, UploadFile } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
|
||||
defineOptions({ name: 'PackageGift' })
|
||||
|
||||
const importDialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const importLoading = ref(false)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
iccid: '',
|
||||
accessNumber: '',
|
||||
cardCompany: '',
|
||||
isReceived: '',
|
||||
endDateRange: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 导入表单实例
|
||||
const importFormRef = ref<FormInstance>()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
|
||||
// 导入表单数据
|
||||
const importFormData = reactive({
|
||||
excelFile: null as File | null,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
iccid: '89860621370079892035',
|
||||
accessNumber: '1440012345678',
|
||||
giftPackage: '随意联畅玩年卡套餐(12个月)',
|
||||
cardCompany: '联通2',
|
||||
isReceived: '已领取',
|
||||
operator: '张若暄',
|
||||
operationTime: '2025-11-08 10:30:00',
|
||||
receiveTime: '2025-11-08 14:20:00',
|
||||
importStatus: '导入成功',
|
||||
failureReason: ''
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
iccid: '89860621370079892036',
|
||||
accessNumber: '1440012345679',
|
||||
giftPackage: 'Y-NB专享套餐',
|
||||
cardCompany: 'SXKJ-NB',
|
||||
isReceived: '未领取',
|
||||
operator: '孔丽娟',
|
||||
operationTime: '2025-11-07 14:15:00',
|
||||
receiveTime: '',
|
||||
importStatus: '导入成功',
|
||||
failureReason: ''
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
iccid: '89860621370079892037',
|
||||
accessNumber: '1440012345680',
|
||||
giftPackage: '如意包年3G流量包',
|
||||
cardCompany: '联通36',
|
||||
isReceived: '已过期',
|
||||
operator: '李佳音',
|
||||
operationTime: '2025-11-06 09:45:00',
|
||||
receiveTime: '',
|
||||
importStatus: '导入成功',
|
||||
failureReason: ''
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
iccid: '89860621370079892038',
|
||||
accessNumber: '1440012345681',
|
||||
giftPackage: '100G全国流量月卡套餐',
|
||||
cardCompany: '联通1-1',
|
||||
isReceived: '未领取',
|
||||
operator: '赵强',
|
||||
operationTime: '2025-11-05 16:20:00',
|
||||
receiveTime: '',
|
||||
importStatus: '导入失败',
|
||||
failureReason: 'ICCID格式错误'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
iccid: '89860621370079892039',
|
||||
accessNumber: '1440012345682',
|
||||
giftPackage: '广电飞悦卡无预存50G(30天)',
|
||||
cardCompany: '广电4',
|
||||
isReceived: '已领取',
|
||||
operator: '张若暄',
|
||||
operationTime: '2025-11-04 11:30:00',
|
||||
receiveTime: '2025-11-05 08:15:00',
|
||||
importStatus: '导入成功',
|
||||
failureReason: ''
|
||||
}
|
||||
]
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1
|
||||
getPackageGiftList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1
|
||||
getPackageGiftList()
|
||||
}
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: 'ICCID号',
|
||||
prop: 'iccid',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入ICCID号'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '接入号',
|
||||
prop: 'accessNumber',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入接入号'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '开卡公司',
|
||||
prop: 'cardCompany',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '联通2', value: 'unicom2' },
|
||||
{ label: '联通36', value: 'unicom36' },
|
||||
{ label: 'SXKJ-NB', value: 'sxkj_nb' },
|
||||
{ label: '联通1-1', value: 'unicom1_1' },
|
||||
{ label: '联通8', value: 'unicom8' },
|
||||
{ label: '移动21', value: 'mobile21' },
|
||||
{ label: '广电4', value: 'gdtv4' },
|
||||
{ label: '电信9', value: 'telecom9' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '是否领取',
|
||||
prop: 'isReceived',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '已领取', value: 'received' },
|
||||
{ label: '未领取', value: 'not_received' },
|
||||
{ label: '已过期', value: 'expired' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '结束时间',
|
||||
prop: 'endDateRange',
|
||||
type: 'daterange',
|
||||
config: {
|
||||
type: 'daterange',
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: 'ICCID', prop: 'iccid' },
|
||||
{ label: '接入号码', prop: 'accessNumber' },
|
||||
{ label: '赠送套餐', prop: 'giftPackage' },
|
||||
{ label: '开卡公司', prop: 'cardCompany' },
|
||||
{ label: '是否领取', prop: 'isReceived' },
|
||||
{ label: '操作人', prop: 'operator' },
|
||||
{ label: '操作时间', prop: 'operationTime' },
|
||||
{ label: '领取时间', prop: 'receiveTime' },
|
||||
{ label: '导入状态', prop: 'importStatus' },
|
||||
{ label: '失败原因', prop: 'failureReason' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 获取是否领取标签类型
|
||||
const getReceiveStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case '已领取':
|
||||
return 'success'
|
||||
case '未领取':
|
||||
return 'warning'
|
||||
case '已过期':
|
||||
return 'danger'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取导入状态标签类型
|
||||
const getImportStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case '导入成功':
|
||||
return 'success'
|
||||
case '导入失败':
|
||||
return 'danger'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 导出Excel
|
||||
const exportExcel = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要导出的数据')
|
||||
return
|
||||
}
|
||||
ElMessage.success(`导出 ${selectedRows.value.length} 条套餐赠送记录`)
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
const batchDelete = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要删除的数据')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(
|
||||
`确定要删除选中的 ${selectedRows.value.length} 条套餐赠送记录吗?`,
|
||||
'批量删除确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
ElMessage.success(`批量删除 ${selectedRows.value.length} 条记录成功`)
|
||||
getPackageGiftList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消删除')
|
||||
})
|
||||
}
|
||||
|
||||
// 显示导入对话框
|
||||
const showBatchImportDialog = () => {
|
||||
importDialogVisible.value = true
|
||||
// 重置表单
|
||||
if (importFormRef.value) {
|
||||
importFormRef.value.resetFields()
|
||||
}
|
||||
importFormData.excelFile = null
|
||||
importFormData.remark = ''
|
||||
}
|
||||
|
||||
// 下载模板
|
||||
const downloadTemplate = () => {
|
||||
ElMessage.success('正在下载套餐赠送导入模板...')
|
||||
// 这里可以实现实际的模板下载功能
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetails = (row: any) => {
|
||||
ElMessage.info(`查看套餐赠送详情: ${row.iccid}`)
|
||||
}
|
||||
|
||||
// 编辑记录
|
||||
const editRecord = (row: any) => {
|
||||
ElMessage.info(`编辑套餐赠送记录: ${row.iccid}`)
|
||||
}
|
||||
|
||||
// 删除记录
|
||||
const deleteRecord = (row: any) => {
|
||||
ElMessageBox.confirm(`确定要删除该套餐赠送记录吗?`, '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('删除成功')
|
||||
getPackageGiftList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消删除')
|
||||
})
|
||||
}
|
||||
|
||||
// 手动发放
|
||||
const manualGrant = (row: any) => {
|
||||
if (row.isReceived === '已领取') {
|
||||
ElMessage.warning('该套餐已被领取')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`确定要手动发放该套餐吗?`, '发放确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('套餐发放成功')
|
||||
getPackageGiftList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消发放')
|
||||
})
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'iccid',
|
||||
label: 'ICCID',
|
||||
minWidth: 180
|
||||
},
|
||||
{
|
||||
prop: 'accessNumber',
|
||||
label: '接入号码',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
prop: 'giftPackage',
|
||||
label: '赠送套餐',
|
||||
minWidth: 200
|
||||
},
|
||||
{
|
||||
prop: 'cardCompany',
|
||||
label: '开卡公司',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'isReceived',
|
||||
label: '是否领取',
|
||||
width: 100,
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getReceiveStatusType(row.isReceived) }, () => row.isReceived)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'operator',
|
||||
label: '操作人',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'operationTime',
|
||||
label: '操作时间',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
prop: 'receiveTime',
|
||||
label: '领取时间',
|
||||
width: 160,
|
||||
formatter: (row) => row.receiveTime || '未领取'
|
||||
},
|
||||
{
|
||||
prop: 'importStatus',
|
||||
label: '导入状态',
|
||||
width: 100,
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getImportStatusType(row.importStatus) }, () => row.importStatus)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'failureReason',
|
||||
label: '失败原因',
|
||||
width: 140,
|
||||
formatter: (row) => row.failureReason || '-'
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 280,
|
||||
formatter: (row: any) => {
|
||||
return h('div', { class: 'operation-buttons' }, [
|
||||
h(ArtButtonTable, {
|
||||
text: '查看',
|
||||
onClick: () => viewDetails(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '发放',
|
||||
disabled: row.isReceived === '已领取',
|
||||
onClick: () => manualGrant(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '编辑',
|
||||
onClick: () => editRecord(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '删除',
|
||||
onClick: () => deleteRecord(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getPackageGiftList()
|
||||
})
|
||||
|
||||
// 获取套餐赠送列表
|
||||
const getPackageGiftList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
|
||||
const endIndex = startIndex + pagination.pageSize
|
||||
const paginatedData = mockData.slice(startIndex, endIndex)
|
||||
|
||||
tableData.value = paginatedData
|
||||
pagination.total = mockData.length
|
||||
loading.value = false
|
||||
} catch (error) {
|
||||
console.error('获取套餐赠送列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getPackageGiftList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getPackageGiftList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getPackageGiftList()
|
||||
}
|
||||
|
||||
// 文件上传限制
|
||||
const handleExceed = () => {
|
||||
ElMessage.warning('最多只能上传一个文件')
|
||||
}
|
||||
|
||||
// 文件上传前检查
|
||||
const beforeUpload = (file: UploadRawFile) => {
|
||||
const isExcel =
|
||||
file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
|
||||
file.type === 'application/vnd.ms-excel'
|
||||
const isLt10M = file.size / 1024 / 1024 < 10
|
||||
|
||||
if (!isExcel) {
|
||||
ElMessage.error('只能上传 Excel 文件!')
|
||||
return false
|
||||
}
|
||||
if (!isLt10M) {
|
||||
ElMessage.error('上传文件大小不能超过 10MB!')
|
||||
return false
|
||||
}
|
||||
return false // 阻止自动上传
|
||||
}
|
||||
|
||||
// 文件变化处理
|
||||
const handleFileChange = (file: UploadFile) => {
|
||||
if (file.raw) {
|
||||
importFormData.excelFile = file.raw
|
||||
}
|
||||
}
|
||||
|
||||
// 导入表单验证规则
|
||||
const importRules = reactive<FormRules>({
|
||||
excelFile: [{ required: true, message: '请上传Excel文件', trigger: 'change' }]
|
||||
})
|
||||
|
||||
// 提交导入
|
||||
const handleImportSubmit = async () => {
|
||||
if (!importFormRef.value) return
|
||||
|
||||
// 检查文件是否上传
|
||||
if (!importFormData.excelFile) {
|
||||
ElMessage.error('请先上传Excel文件')
|
||||
return
|
||||
}
|
||||
|
||||
await importFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
importLoading.value = true
|
||||
|
||||
// 模拟导入过程
|
||||
setTimeout(() => {
|
||||
ElMessage.success('套餐赠送导入成功!')
|
||||
importDialogVisible.value = false
|
||||
importLoading.value = false
|
||||
getPackageGiftList()
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.package-gift-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
.template-section {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.template-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.operation-buttons) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.el-upload-dragger) {
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
468
src/views/card-management/single-card/index.vue
Normal file
468
src/views/card-management/single-card/index.vue
Normal file
@@ -0,0 +1,468 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="single-card-page" id="table-full-screen">
|
||||
<!-- 网卡信息卡片 -->
|
||||
<ElCard shadow="never" class="card-info-card" style="margin-bottom: 16px">
|
||||
<template #header>
|
||||
<span>网卡信息</span>
|
||||
</template>
|
||||
<ElForm :model="cardInfo" label-width="120px" :inline="false">
|
||||
<ElRow :gutter="24">
|
||||
<ElCol :span="8">
|
||||
<ElFormItem label="ICCID:">
|
||||
<span>{{ cardInfo.iccid }}</span>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="8">
|
||||
<ElFormItem label="IMSI:">
|
||||
<span>{{ cardInfo.imsi }}</span>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="8">
|
||||
<ElFormItem label="手机号码:">
|
||||
<span>{{ cardInfo.msisdn }}</span>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="24">
|
||||
<ElCol :span="8">
|
||||
<ElFormItem label="运营商:">
|
||||
<ElTag :type="getOperatorTagType(cardInfo.operator)">{{
|
||||
cardInfo.operatorName
|
||||
}}</ElTag>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="8">
|
||||
<ElFormItem label="网络类型:">
|
||||
<span>{{ cardInfo.networkType }}</span>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="8">
|
||||
<ElFormItem label="状态:">
|
||||
<ElTag :type="getStatusTagType(cardInfo.status)">{{ cardInfo.statusName }}</ElTag>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</ElForm>
|
||||
</ElCard>
|
||||
|
||||
<!-- 操作区域 -->
|
||||
<ElCard shadow="never" class="operation-card" style="margin-bottom: 16px">
|
||||
<template #header>
|
||||
<span>操作区域</span>
|
||||
</template>
|
||||
<div class="operation-buttons">
|
||||
<ElButton type="primary" @click="activateCard" :disabled="cardInfo.status === '1'"
|
||||
>激活网卡</ElButton
|
||||
>
|
||||
<ElButton type="warning" @click="suspendCard" :disabled="cardInfo.status === '2'"
|
||||
>停用网卡</ElButton
|
||||
>
|
||||
<ElButton type="success" @click="showRechargeDialog">充值</ElButton>
|
||||
<ElButton type="info" @click="queryTraffic">流量查询</ElButton>
|
||||
<ElButton type="danger" @click="resetCard">重置网卡</ElButton>
|
||||
<ElButton @click="diagnoseCard">网卡诊断</ElButton>
|
||||
</div>
|
||||
</ElCard>
|
||||
|
||||
<!-- 流量信息 -->
|
||||
<ElCard shadow="never" class="traffic-card" style="margin-bottom: 16px">
|
||||
<template #header>
|
||||
<span>流量信息</span>
|
||||
</template>
|
||||
<ElRow :gutter="24">
|
||||
<ElCol :span="6">
|
||||
<div class="traffic-item">
|
||||
<div class="traffic-value">{{ trafficInfo.totalTraffic }}</div>
|
||||
<div class="traffic-label">总流量</div>
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :span="6">
|
||||
<div class="traffic-item">
|
||||
<div class="traffic-value used">{{ trafficInfo.usedTraffic }}</div>
|
||||
<div class="traffic-label">已用流量</div>
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :span="6">
|
||||
<div class="traffic-item">
|
||||
<div class="traffic-value remaining">{{ trafficInfo.remainingTraffic }}</div>
|
||||
<div class="traffic-label">剩余流量</div>
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :span="6">
|
||||
<div class="traffic-item">
|
||||
<div class="traffic-value percentage">{{ trafficInfo.usagePercentage }}%</div>
|
||||
<div class="traffic-label">使用率</div>
|
||||
</div>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElProgress :percentage="parseInt(trafficInfo.usagePercentage)" style="margin-top: 16px" />
|
||||
</ElCard>
|
||||
|
||||
<!-- 使用记录 -->
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<template #header>
|
||||
<span>使用记录</span>
|
||||
<ElButton style="float: right" @click="refreshUsageRecords">刷新</ElButton>
|
||||
</template>
|
||||
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="usageRecords"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in usageColumns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 充值对话框 -->
|
||||
<ElDialog v-model="rechargeDialogVisible" title="网卡充值" width="400px" align-center>
|
||||
<ElForm
|
||||
ref="rechargeFormRef"
|
||||
:model="rechargeForm"
|
||||
:rules="rechargeRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<ElFormItem label="充值金额" prop="amount">
|
||||
<ElInput v-model="rechargeForm.amount" placeholder="请输入充值金额">
|
||||
<template #append>元</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="充值方式" prop="method">
|
||||
<ElSelect v-model="rechargeForm.method" placeholder="请选择充值方式">
|
||||
<ElOption label="支付宝" value="alipay" />
|
||||
<ElOption label="微信支付" value="wechat" />
|
||||
<ElOption label="银联支付" value="unionpay" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="rechargeDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleRecharge">确认充值</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ElTag, ElMessageBox, ElMessage, FormInstance } from 'element-plus'
|
||||
import { useRoute } from 'vue-router'
|
||||
import type { FormRules } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'SingleCard' })
|
||||
|
||||
const loading = ref(false)
|
||||
const rechargeDialogVisible = ref(false)
|
||||
const route = useRoute()
|
||||
|
||||
// 网卡信息
|
||||
const cardInfo = reactive({
|
||||
iccid: '89860123456789012345',
|
||||
imsi: '460012345678901',
|
||||
msisdn: '13800138001',
|
||||
operator: 'mobile',
|
||||
operatorName: '中国移动',
|
||||
networkType: '4G',
|
||||
status: '1',
|
||||
statusName: '激活',
|
||||
activatedDate: '2024-01-15',
|
||||
expiryDate: '2025-01-15'
|
||||
})
|
||||
|
||||
// 流量信息
|
||||
const trafficInfo = reactive({
|
||||
totalTraffic: '10GB',
|
||||
usedTraffic: '2.5GB',
|
||||
remainingTraffic: '7.5GB',
|
||||
usagePercentage: '25'
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 使用记录
|
||||
const usageRecords = ref([
|
||||
{
|
||||
id: 1,
|
||||
date: '2024-11-07',
|
||||
time: '14:30:25',
|
||||
dataUsage: '125.6MB',
|
||||
fee: '0.12',
|
||||
location: '北京市朝阳区'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
date: '2024-11-07',
|
||||
time: '13:45:12',
|
||||
dataUsage: '256.8MB',
|
||||
fee: '0.26',
|
||||
location: '北京市朝阳区'
|
||||
}
|
||||
])
|
||||
|
||||
// 充值表单
|
||||
const rechargeForm = reactive({
|
||||
amount: '',
|
||||
method: ''
|
||||
})
|
||||
|
||||
const rechargeFormRef = ref<FormInstance>()
|
||||
|
||||
// 表格列配置
|
||||
const usageColumns = [
|
||||
{
|
||||
prop: 'date',
|
||||
label: '日期',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'time',
|
||||
label: '时间',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'dataUsage',
|
||||
label: '流量使用量',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'fee',
|
||||
label: '费用(元)',
|
||||
width: 100,
|
||||
formatter: (row: any) => `¥${row.fee}`
|
||||
},
|
||||
{
|
||||
prop: 'location',
|
||||
label: '位置',
|
||||
minWidth: 140
|
||||
}
|
||||
]
|
||||
|
||||
// 获取运营商标签类型
|
||||
const getOperatorTagType = (operator: string) => {
|
||||
switch (operator) {
|
||||
case 'mobile':
|
||||
return 'success'
|
||||
case 'unicom':
|
||||
return 'primary'
|
||||
case 'telecom':
|
||||
return 'warning'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusTagType = (status: string) => {
|
||||
switch (status) {
|
||||
case '1':
|
||||
return 'success'
|
||||
case '2':
|
||||
return 'danger'
|
||||
case '3':
|
||||
return 'warning'
|
||||
case '4':
|
||||
return 'info'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 激活网卡
|
||||
const activateCard = () => {
|
||||
ElMessageBox.confirm('确定要激活该网卡吗?', '激活网卡', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info'
|
||||
}).then(() => {
|
||||
cardInfo.status = '1'
|
||||
cardInfo.statusName = '激活'
|
||||
ElMessage.success('网卡激活成功')
|
||||
})
|
||||
}
|
||||
|
||||
// 停用网卡
|
||||
const suspendCard = () => {
|
||||
ElMessageBox.confirm('确定要停用该网卡吗?', '停用网卡', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
cardInfo.status = '2'
|
||||
cardInfo.statusName = '停用'
|
||||
ElMessage.success('网卡停用成功')
|
||||
})
|
||||
}
|
||||
|
||||
// 显示充值对话框
|
||||
const showRechargeDialog = () => {
|
||||
rechargeDialogVisible.value = true
|
||||
if (rechargeFormRef.value) {
|
||||
rechargeFormRef.value.resetFields()
|
||||
}
|
||||
}
|
||||
|
||||
// 流量查询
|
||||
const queryTraffic = () => {
|
||||
ElMessage.info('正在查询流量信息...')
|
||||
// 这里可以调用API查询最新的流量信息
|
||||
}
|
||||
|
||||
// 重置网卡
|
||||
const resetCard = () => {
|
||||
ElMessageBox.confirm('确定要重置该网卡吗?重置后网卡需要重新激活。', '重置网卡', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(() => {
|
||||
ElMessage.success('网卡重置成功')
|
||||
})
|
||||
}
|
||||
|
||||
// 网卡诊断
|
||||
const diagnoseCard = () => {
|
||||
ElMessage.info('正在进行网卡诊断...')
|
||||
setTimeout(() => {
|
||||
ElMessage.success('网卡诊断完成,网卡状态正常')
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
// 充值验证规则
|
||||
const rechargeRules: FormRules = {
|
||||
amount: [
|
||||
{ required: true, message: '请输入充值金额', trigger: 'blur' },
|
||||
{ pattern: /^\d+(\.\d{1,2})?$/, message: '请输入正确的金额格式', trigger: 'blur' }
|
||||
],
|
||||
method: [{ required: true, message: '请选择充值方式', trigger: 'change' }]
|
||||
}
|
||||
|
||||
// 处理充值
|
||||
const handleRecharge = async () => {
|
||||
if (!rechargeFormRef.value) return
|
||||
|
||||
await rechargeFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
ElMessage.success('充值成功')
|
||||
rechargeDialogVisible.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 刷新使用记录
|
||||
const refreshUsageRecords = () => {
|
||||
ElMessage.info('正在刷新使用记录...')
|
||||
// 这里可以调用API获取最新的使用记录
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
// 重新获取数据
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
// 重新获取数据
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 检查是否有传递的ICCID参数
|
||||
const iccidFromQuery = route.query.iccid as string
|
||||
if (iccidFromQuery) {
|
||||
// 如果有ICCID参数,更新卡片信息
|
||||
cardInfo.iccid = iccidFromQuery
|
||||
// 可以在这里根据ICCID获取完整的卡片信息
|
||||
console.log('从网卡明细跳转,ICCID:', iccidFromQuery)
|
||||
// 模拟根据ICCID获取卡片详细信息
|
||||
loadCardInfoByIccid(iccidFromQuery)
|
||||
}
|
||||
|
||||
// 初始化数据
|
||||
pagination.total = usageRecords.value.length
|
||||
})
|
||||
|
||||
// 根据ICCID加载卡片信息
|
||||
const loadCardInfoByIccid = async (iccid: string) => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 这里应该调用API根据ICCID获取卡片详细信息
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
// 模拟更新卡片信息(实际应该从API获取)
|
||||
Object.assign(cardInfo, {
|
||||
iccid: iccid,
|
||||
imsi: '460012345678901',
|
||||
msisdn: '13800138001',
|
||||
operator: 'mobile',
|
||||
operatorName: '中国移动',
|
||||
networkType: '4G',
|
||||
status: '1',
|
||||
statusName: '激活',
|
||||
activatedDate: '2024-01-15',
|
||||
expiryDate: '2025-01-15'
|
||||
})
|
||||
|
||||
ElMessage.success(`已加载ICCID ${iccid} 的详细信息`)
|
||||
} catch (error) {
|
||||
console.error('获取卡片信息失败:', error)
|
||||
ElMessage.error('获取卡片信息失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.single-card-page {
|
||||
.operation-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.traffic-card {
|
||||
.traffic-item {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
|
||||
.traffic-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: var(--el-color-primary);
|
||||
|
||||
&.used {
|
||||
color: var(--el-color-warning);
|
||||
}
|
||||
|
||||
&.remaining {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
|
||||
&.percentage {
|
||||
color: var(--el-color-info);
|
||||
}
|
||||
}
|
||||
|
||||
.traffic-label {
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
52
src/views/change/log/index.vue
Normal file
52
src/views/change/log/index.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<h3 class="table-title"><i class="iconfont-sys"></i>更新日志</h3>
|
||||
|
||||
<ArtTable :data="upgradeLogList" :pagination="false">
|
||||
<ElTableColumn label="版本号" prop="version" width="200" />
|
||||
<ElTableColumn label="内容">
|
||||
<template #default="scope">
|
||||
<div class="title">{{ scope.row.title }}</div>
|
||||
<div v-if="scope.row.detail" style="margin-top: 10px">
|
||||
<div class="detail-item" v-for="(item, index) in scope.row.detail" :key="index">
|
||||
{{ index + 1 }}. {{ item }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="时间" prop="date" />
|
||||
</ArtTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ArtTable from '@/components/core/tables/ArtTable.vue'
|
||||
import { upgradeLogList } from '@/mock/upgrade/changeLog'
|
||||
|
||||
defineOptions({ name: 'ChangeLog' })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
.table-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 0 0;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
|
||||
i {
|
||||
margin-right: 10px;
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--art-gray-800);
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
color: var(--art-gray-600);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
53
src/views/dashboard/analysis/index.vue
Normal file
53
src/views/dashboard/analysis/index.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div class="analysis-dashboard">
|
||||
<el-row :gutter="20">
|
||||
<el-col :xl="14" :lg="15" :xs="24">
|
||||
<TodaySales />
|
||||
</el-col>
|
||||
<el-col :xl="10" :lg="9" :xs="24">
|
||||
<VisitorInsights />
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" class="mt-20">
|
||||
<el-col :xl="10" :lg="10" :xs="24">
|
||||
<TotalRevenue />
|
||||
</el-col>
|
||||
<el-col :xl="7" :lg="7" :xs="24">
|
||||
<CustomerSatisfaction />
|
||||
</el-col>
|
||||
<el-col :xl="7" :lg="7" :xs="24">
|
||||
<TargetVsReality />
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" class="mt-20">
|
||||
<el-col :xl="10" :lg="10" :xs="24">
|
||||
<TopProducts />
|
||||
</el-col>
|
||||
<el-col :xl="7" :lg="7" :xs="24">
|
||||
<SalesMappingByCountry />
|
||||
</el-col>
|
||||
<el-col :xl="7" :lg="7" :xs="24">
|
||||
<VolumeServiceLevel />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TodaySales from './widget/TodaySales.vue'
|
||||
import VisitorInsights from './widget/VisitorInsights.vue'
|
||||
import TotalRevenue from './widget/TotalRevenue.vue'
|
||||
import CustomerSatisfaction from './widget/CustomerSatisfaction.vue'
|
||||
import TargetVsReality from './widget/TargetVsReality.vue'
|
||||
import TopProducts from './widget/TopProducts.vue'
|
||||
import SalesMappingByCountry from './widget/SalesMappingByCountry.vue'
|
||||
import VolumeServiceLevel from './widget/VolumeServiceLevel.vue'
|
||||
|
||||
defineOptions({ name: 'Analysis' })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use './style';
|
||||
</style>
|
||||
61
src/views/dashboard/analysis/style.scss
Normal file
61
src/views/dashboard/analysis/style.scss
Normal file
@@ -0,0 +1,61 @@
|
||||
.analysis-dashboard {
|
||||
padding-bottom: 20px;
|
||||
|
||||
:deep(.custom-card) {
|
||||
background: var(--art-main-bg-color);
|
||||
border-radius: calc(var(--custom-radius) + 4px) !important;
|
||||
}
|
||||
|
||||
// 卡片头部
|
||||
:deep(.custom-card-header) {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 18px 20px;
|
||||
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
color: var(--art-text-gray-900);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
left: 21px;
|
||||
font-size: 13px;
|
||||
color: var(--art-gray-600);
|
||||
}
|
||||
}
|
||||
|
||||
.el-card {
|
||||
border: 1px solid #e8ebf1;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.mt-20 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.analysis-dashboard {
|
||||
:deep(.custom-card) {
|
||||
box-shadow: 0 4px 20px rgb(0 0 0 / 50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 1200px) {
|
||||
.analysis-dashboard {
|
||||
.mt-20 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
:deep(.custom-card) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
134
src/views/dashboard/analysis/widget/CustomerSatisfaction.vue
Normal file
134
src/views/dashboard/analysis/widget/CustomerSatisfaction.vue
Normal file
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<div class="custom-card art-custom-card customer-satisfaction">
|
||||
<div class="custom-card-header">
|
||||
<span class="title">{{ t('analysis.customerSatisfaction.title') }}</span>
|
||||
</div>
|
||||
<div class="custom-card-body">
|
||||
<div ref="chartRef" style="height: 300px; margin-top: 10px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as echarts from 'echarts'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useChart } from '@/composables/useChart'
|
||||
const { t } = useI18n()
|
||||
|
||||
const { chartRef, isDark, initChart } = useChart()
|
||||
|
||||
const options: () => echarts.EChartsOption = () => ({
|
||||
grid: {
|
||||
top: 30,
|
||||
right: 20,
|
||||
bottom: 50,
|
||||
left: 20,
|
||||
containLabel: true
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
confine: true
|
||||
},
|
||||
legend: {
|
||||
data: [
|
||||
t('analysis.customerSatisfaction.legend.lastMonth'),
|
||||
t('analysis.customerSatisfaction.legend.thisMonth')
|
||||
],
|
||||
bottom: 0,
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: isDark.value ? '#808290' : '#222B45'
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: ['Week 0', 'Week 1', 'Week 2', 'Week 3', 'Week 4', 'Week 5', 'Week 6'],
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { show: false } // 隐藏 x 轴标签
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { show: false },
|
||||
splitLine: {
|
||||
show: false // 将 show 设置为 false 以去除水平线条
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: t('analysis.customerSatisfaction.legend.lastMonth'),
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [1800, 2800, 1800, 2300, 2600, 2500, 3000],
|
||||
areaStyle: {
|
||||
opacity: 0.8,
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(0,157,255,0.33)' },
|
||||
{ offset: 1, color: 'rgba(255,255,255,0)' }
|
||||
])
|
||||
},
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: '#0086E1'
|
||||
},
|
||||
symbol: 'none',
|
||||
itemStyle: {
|
||||
color: '#0095FF'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t('analysis.customerSatisfaction.legend.thisMonth'),
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [4000, 3500, 4300, 3700, 4500, 3500, 4000],
|
||||
areaStyle: {
|
||||
opacity: 0.8,
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(147,241,180,0.33)' },
|
||||
{ offset: 1, color: 'rgba(255,255,255,0)' }
|
||||
])
|
||||
},
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: '#14DEB9'
|
||||
},
|
||||
symbol: 'none',
|
||||
itemStyle: {
|
||||
color: '#14DEB9'
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
watch(isDark, () => {
|
||||
initChart(options())
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
initChart(options())
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.custom-card {
|
||||
height: 400px;
|
||||
|
||||
&-body {
|
||||
padding: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $device-notebook) {
|
||||
.custom-card {
|
||||
height: 350px;
|
||||
|
||||
&-body {
|
||||
> div {
|
||||
height: 260px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div class="custom-card art-custom-card sales-mapping-country">
|
||||
<div class="custom-card-header">
|
||||
<span class="title">{{ t('analysis.salesMappingCountry.title') }}</span>
|
||||
</div>
|
||||
<div class="custom-card-body">
|
||||
<div ref="chartRef" class="sales-mapping-chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { EChartsOption } from 'echarts'
|
||||
import { useChart } from '@/composables/useChart'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { chartRef, initChart, isDark } = useChart()
|
||||
|
||||
const chartData = [
|
||||
{ value: 1048, name: 'Beijing', itemStyle: { color: 'rgba(99, 102, 241, 0.9)' } },
|
||||
{ value: 735, name: 'Shanghai', itemStyle: { color: 'rgba(134, 239, 172, 0.9)' } },
|
||||
{ value: 580, name: 'Guangzhou', itemStyle: { color: 'rgba(253, 224, 71, 0.9)' } },
|
||||
{ value: 484, name: 'Shenzhen', itemStyle: { color: 'rgba(248, 113, 113, 0.9)' } },
|
||||
{ value: 300, name: 'Chengdu', itemStyle: { color: 'rgba(125, 211, 252, 0.9)' } }
|
||||
]
|
||||
|
||||
const options: () => EChartsOption = () => ({
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'Sales Mapping',
|
||||
type: 'pie',
|
||||
radius: ['40%', '60%'],
|
||||
avoidLabelOverlap: false,
|
||||
padAngle: 5,
|
||||
itemStyle: {
|
||||
borderRadius: 10
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: chartData
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
watch(isDark, () => {
|
||||
initChart(options())
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
initChart(options())
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sales-mapping-country {
|
||||
height: 330px;
|
||||
|
||||
.sales-mapping-chart {
|
||||
width: 100%;
|
||||
height: 260px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
230
src/views/dashboard/analysis/widget/TargetVsReality.vue
Normal file
230
src/views/dashboard/analysis/widget/TargetVsReality.vue
Normal file
@@ -0,0 +1,230 @@
|
||||
<template>
|
||||
<div class="custom-card art-custom-card target-vs-reality">
|
||||
<div class="custom-card-header">
|
||||
<span class="title">{{ t('analysis.targetVsReality.title') }}</span>
|
||||
</div>
|
||||
<div class="custom-card-body">
|
||||
<div ref="chartRef" style="height: 160px"></div>
|
||||
</div>
|
||||
<div class="custom-card-footer">
|
||||
<div class="total-item">
|
||||
<div class="label">
|
||||
<i class="iconfont-sys"></i>
|
||||
<div class="label-text">
|
||||
<span>{{ t('analysis.targetVsReality.realitySales.label') }}</span>
|
||||
<span>{{ t('analysis.targetVsReality.realitySales.sublabel') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="value text-color-green">8,823</div>
|
||||
</div>
|
||||
<div class="total-item">
|
||||
<div class="label">
|
||||
<i class="iconfont-sys"></i>
|
||||
<div class="label-text">
|
||||
<span>{{ t('analysis.targetVsReality.targetSales.label') }}</span>
|
||||
<span>{{ t('analysis.targetVsReality.targetSales.sublabel') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="value text-color-orange">12,122</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useChart } from '@/composables/useChart'
|
||||
import { EChartsOption } from 'echarts'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
const { t } = useI18n()
|
||||
|
||||
const { chartRef, isDark, initChart } = useChart()
|
||||
|
||||
const options: () => EChartsOption = () => ({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
top: 10,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'June', 'July'],
|
||||
axisLabel: {
|
||||
color: '#7B91B0'
|
||||
},
|
||||
axisLine: {
|
||||
show: false // 隐藏 x 轴线
|
||||
},
|
||||
axisTick: {
|
||||
show: false // 隐藏刻度线
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
show: false // 隐藏 y 轴文字
|
||||
},
|
||||
splitLine: {
|
||||
show: false // 隐藏 y 轴分割线
|
||||
},
|
||||
axisLine: {
|
||||
show: false // 隐藏 y 轴线
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'Reality Sales',
|
||||
type: 'bar',
|
||||
data: [8000, 7000, 6000, 8500, 9000, 10000, 9500],
|
||||
barWidth: '15',
|
||||
itemStyle: {
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
color: '#2B8DFA'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Target Sales',
|
||||
type: 'bar',
|
||||
data: [10000, 9000, 11000, 10000, 12000, 12500, 11500],
|
||||
barWidth: '15',
|
||||
itemStyle: {
|
||||
borderRadius: [4, 4, 4, 4],
|
||||
color: '#95E0FB'
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
watch(isDark, () => {
|
||||
initChart(options())
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
initChart(options())
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.custom-card {
|
||||
height: 400px;
|
||||
|
||||
&-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
&-footer {
|
||||
box-sizing: border-box;
|
||||
padding: 0 20px;
|
||||
margin-top: 15px;
|
||||
|
||||
.total-item {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
|
||||
&:first-of-type .label .iconfont-sys {
|
||||
color: #2b8dfa !important;
|
||||
background-color: #e6f7ff !important;
|
||||
}
|
||||
|
||||
&:last-of-type .label .iconfont-sys {
|
||||
color: #1cb8fc !important;
|
||||
background-color: #e6f7ff !important;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 60%;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
|
||||
.iconfont-sys {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-right: 12px;
|
||||
font-size: 18px;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
background-color: #f2f2f2;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.label-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
span {
|
||||
&:first-of-type {
|
||||
font-size: 16px;
|
||||
color: var(--art-text-gray-800);
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: #737791;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
|
||||
&.text-color-green {
|
||||
color: #2b8dfa !important;
|
||||
}
|
||||
|
||||
&.text-color-orange {
|
||||
color: #1cb8fc !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $device-notebook) {
|
||||
.custom-card {
|
||||
height: 350px;
|
||||
|
||||
&-body {
|
||||
padding-top: 10px;
|
||||
|
||||
> div {
|
||||
height: 140px !important;
|
||||
}
|
||||
}
|
||||
|
||||
&-footer {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.custom-card {
|
||||
&-footer {
|
||||
.total-item {
|
||||
&:first-of-type .label .iconfont-sys {
|
||||
background-color: #222 !important;
|
||||
}
|
||||
|
||||
&:last-of-type .label .iconfont-sys {
|
||||
background-color: #222 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
196
src/views/dashboard/analysis/widget/TodaySales.vue
Normal file
196
src/views/dashboard/analysis/widget/TodaySales.vue
Normal file
@@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<div class="custom-card art-custom-card today-sales">
|
||||
<div class="custom-card-header">
|
||||
<span class="title">{{ t('analysis.todaySales.title') }}</span>
|
||||
<span class="subtitle">{{ t('analysis.todaySales.subtitle') }}</span>
|
||||
<div class="export-btn">
|
||||
<i class="iconfont-sys"></i>
|
||||
<span>{{ t('analysis.todaySales.export') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sales-summary">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6" :xs="24" v-for="(item, index) in salesData" :key="index">
|
||||
<div :class="['sales-card art-custom-card']">
|
||||
<i class="iconfont-sys" :class="item.class" v-html="item.iconfont"></i>
|
||||
<h2>
|
||||
<CountTo
|
||||
class="number box-title"
|
||||
:endVal="item.value"
|
||||
:duration="1000"
|
||||
separator=""
|
||||
></CountTo>
|
||||
</h2>
|
||||
<p>{{ item.label }}</p>
|
||||
<small>{{ item.change }} {{ t('analysis.todaySales.fromYesterday') }}</small>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { CountTo } from 'vue3-count-to'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const salesData = ref([
|
||||
{
|
||||
label: t('analysis.todaySales.cards.totalSales.label'),
|
||||
value: 999,
|
||||
change: t('analysis.todaySales.cards.totalSales.change'),
|
||||
iconfont: '',
|
||||
class: 'bg-primary'
|
||||
},
|
||||
{
|
||||
label: t('analysis.todaySales.cards.totalOrder.label'),
|
||||
value: 300,
|
||||
change: t('analysis.todaySales.cards.totalOrder.change'),
|
||||
iconfont: '',
|
||||
class: 'bg-warning'
|
||||
},
|
||||
{
|
||||
label: t('analysis.todaySales.cards.productSold.label'),
|
||||
value: 56,
|
||||
change: t('analysis.todaySales.cards.productSold.change'),
|
||||
iconfont: '',
|
||||
class: 'bg-error'
|
||||
},
|
||||
{
|
||||
label: t('analysis.todaySales.cards.newCustomers.label'),
|
||||
value: 68,
|
||||
change: t('analysis.todaySales.cards.newCustomers.change'),
|
||||
iconfont: '',
|
||||
class: 'bg-success'
|
||||
}
|
||||
])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.today-sales {
|
||||
height: 330px;
|
||||
|
||||
.export-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 66px;
|
||||
padding: 6px 0;
|
||||
color: var(--art-gray-600);
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--art-border-dashed-color);
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: var(--main-color);
|
||||
border-color: var(--main-color);
|
||||
}
|
||||
|
||||
.iconfont-sys {
|
||||
margin-right: 5px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.sales-summary {
|
||||
padding: 20px;
|
||||
|
||||
.sales-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 220px;
|
||||
padding: 0 20px;
|
||||
overflow: hidden;
|
||||
border-radius: calc(var(--custom-radius) / 2 + 4px) !important;
|
||||
|
||||
.iconfont-sys {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
font-size: 20px;
|
||||
line-height: 48px;
|
||||
color: #fff;
|
||||
color: var(--el-color-primary);
|
||||
text-align: center;
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 10px;
|
||||
font-size: 26px;
|
||||
font-weight: 400;
|
||||
color: var(--art-text-gray-900) !important;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 10px;
|
||||
font-size: 16px;
|
||||
color: var(--art-text-gray-700) !important;
|
||||
|
||||
@include ellipsis;
|
||||
}
|
||||
|
||||
small {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
color: var(--art-text-gray-500) !important;
|
||||
|
||||
@include ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 暗黑模式降低颜色强度
|
||||
.dark {
|
||||
.today-sales {
|
||||
.sales-summary {
|
||||
.sales-card {
|
||||
.iconfont-sys {
|
||||
&.red,
|
||||
&.yellow,
|
||||
&.green,
|
||||
&.purple {
|
||||
background-color: #222 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $device-notebook) {
|
||||
.today-sales {
|
||||
height: 280px;
|
||||
|
||||
.sales-summary {
|
||||
.sales-card {
|
||||
height: 170px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 768px) {
|
||||
.today-sales {
|
||||
height: auto;
|
||||
|
||||
.sales-summary {
|
||||
padding-bottom: 0;
|
||||
|
||||
.sales-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
115
src/views/dashboard/analysis/widget/TopProducts.vue
Normal file
115
src/views/dashboard/analysis/widget/TopProducts.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div class="custom-card art-custom-card top-products">
|
||||
<div class="custom-card-header">
|
||||
<span class="title">{{ t('analysis.topProducts.title') }}</span>
|
||||
</div>
|
||||
<div class="custom-card-body">
|
||||
<art-table
|
||||
:data="products"
|
||||
style="width: 100%"
|
||||
:pagination="false"
|
||||
size="large"
|
||||
:border="false"
|
||||
:stripe="false"
|
||||
:show-header-background="false"
|
||||
>
|
||||
<el-table-column prop="name" :label="t('analysis.topProducts.columns.name')" width="200" />
|
||||
<el-table-column prop="popularity" :label="t('analysis.topProducts.columns.popularity')">
|
||||
<template #default="scope">
|
||||
<el-progress
|
||||
:percentage="scope.row.popularity"
|
||||
:color="getColor(scope.row.popularity)"
|
||||
:stroke-width="5"
|
||||
:show-text="false"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sales" :label="t('analysis.topProducts.columns.sales')" width="80">
|
||||
<template #default="scope">
|
||||
<span
|
||||
:style="{
|
||||
color: getColor(scope.row.popularity),
|
||||
backgroundColor: `rgba(${hexToRgb(getColor(scope.row.popularity))}, 0.08)`,
|
||||
border: '1px solid',
|
||||
padding: '3px 6px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px'
|
||||
}"
|
||||
>{{ scope.row.sales }}</span
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</art-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { hexToRgb } from '@/utils/ui'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
const { t } = useI18n()
|
||||
|
||||
// 使用 computed 来创建响应式的产品数据
|
||||
const products = computed(() => [
|
||||
{
|
||||
name: t('analysis.topProducts.products.homeDecor.name'),
|
||||
popularity: 10,
|
||||
sales: t('analysis.topProducts.products.homeDecor.sales')
|
||||
},
|
||||
{
|
||||
name: t('analysis.topProducts.products.disneyBag.name'),
|
||||
popularity: 29,
|
||||
sales: t('analysis.topProducts.products.disneyBag.sales')
|
||||
},
|
||||
{
|
||||
name: t('analysis.topProducts.products.bathroom.name'),
|
||||
popularity: 65,
|
||||
sales: t('analysis.topProducts.products.bathroom.sales')
|
||||
},
|
||||
{
|
||||
name: t('analysis.topProducts.products.smartwatch.name'),
|
||||
popularity: 32,
|
||||
sales: t('analysis.topProducts.products.smartwatch.sales')
|
||||
},
|
||||
{
|
||||
name: t('analysis.topProducts.products.fitness.name'),
|
||||
popularity: 78,
|
||||
sales: t('analysis.topProducts.products.fitness.sales')
|
||||
},
|
||||
{
|
||||
name: t('analysis.topProducts.products.earbuds.name'),
|
||||
popularity: 41,
|
||||
sales: t('analysis.topProducts.products.earbuds.sales')
|
||||
}
|
||||
])
|
||||
|
||||
const getColor = (percentage: number) => {
|
||||
if (percentage < 25) return '#00E096'
|
||||
if (percentage < 50) return '#0095FF'
|
||||
if (percentage < 75) return '#884CFF'
|
||||
return '#FE8F0E'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.custom-card {
|
||||
height: 330px;
|
||||
overflow-y: scroll;
|
||||
|
||||
// 隐藏滚动条
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-body {
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 1200px) {
|
||||
.custom-card {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
129
src/views/dashboard/analysis/widget/TotalRevenue.vue
Normal file
129
src/views/dashboard/analysis/widget/TotalRevenue.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<div class="custom-card art-custom-card total-revenue">
|
||||
<div class="custom-card-header">
|
||||
<span class="title">{{ t('analysis.totalRevenue.title') }}</span>
|
||||
</div>
|
||||
<div class="custom-card-body">
|
||||
<div ref="chartRef" style="height: 300px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { EChartsOption } from 'echarts'
|
||||
import { useChart } from '@/composables/useChart'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { chartRef, isDark, initChart } = useChart()
|
||||
|
||||
// 创建图表选项
|
||||
const options: () => EChartsOption = () => ({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
top: 20,
|
||||
right: 3,
|
||||
bottom: 40,
|
||||
left: 3,
|
||||
containLabel: true
|
||||
},
|
||||
legend: {
|
||||
data: [
|
||||
t('analysis.totalRevenue.legend.onlineSales'),
|
||||
t('analysis.totalRevenue.legend.offlineSales')
|
||||
],
|
||||
bottom: 0,
|
||||
icon: 'circle',
|
||||
itemWidth: 10,
|
||||
itemHeight: 10,
|
||||
itemGap: 15,
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: isDark.value ? '#808290' : '#222B45'
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'],
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
color: isDark.value ? '#808290' : '#7B91B0'
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: isDark.value ? 'rgba(255, 255, 255, 0.1)' : '#EFF1F3',
|
||||
width: 0.8
|
||||
}
|
||||
},
|
||||
axisLabel: { color: isDark.value ? '#808290' : '#7B91B0' }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: t('analysis.totalRevenue.legend.onlineSales'),
|
||||
type: 'bar',
|
||||
data: [12, 13, 5, 15, 10, 15, 18],
|
||||
barWidth: '15',
|
||||
itemStyle: {
|
||||
color: '#0095FF',
|
||||
borderRadius: [4, 4, 4, 4]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t('analysis.totalRevenue.legend.offlineSales'),
|
||||
type: 'bar',
|
||||
data: [10, 11, 20, 5, 11, 13, 10],
|
||||
barWidth: '15',
|
||||
itemStyle: {
|
||||
color: '#95E0FB',
|
||||
borderRadius: [4, 4, 4, 4]
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
watch(isDark, () => {
|
||||
initChart(options())
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
initChart(options())
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.custom-card {
|
||||
height: 400px;
|
||||
|
||||
&-body {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $device-notebook) {
|
||||
.custom-card {
|
||||
height: 350px;
|
||||
|
||||
&-body {
|
||||
> div {
|
||||
height: 260px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
143
src/views/dashboard/analysis/widget/VisitorInsights.vue
Normal file
143
src/views/dashboard/analysis/widget/VisitorInsights.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<div class="custom-card art-custom-card visitor-insights">
|
||||
<div class="custom-card-header">
|
||||
<span class="title">{{ t('analysis.visitorInsights.title') }}</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div ref="chartRef" style="height: 250px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useChart } from '@/composables/useChart'
|
||||
import { EChartsOption } from 'echarts'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { chartRef, isDark, initChart } = useChart()
|
||||
|
||||
const { width } = useWindowSize()
|
||||
|
||||
const options: () => EChartsOption = () => {
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
grid: {
|
||||
top: 20,
|
||||
right: 20,
|
||||
bottom: width.value < 600 ? 80 : 40,
|
||||
left: 20,
|
||||
containLabel: true
|
||||
},
|
||||
legend: {
|
||||
data: [
|
||||
t('analysis.visitorInsights.legend.loyalCustomers'),
|
||||
t('analysis.visitorInsights.legend.newCustomers')
|
||||
],
|
||||
bottom: 0,
|
||||
left: 'center',
|
||||
itemWidth: 14,
|
||||
itemHeight: 14,
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: isDark.value ? '#808290' : '#222B45'
|
||||
},
|
||||
icon: 'roundRect',
|
||||
itemStyle: {
|
||||
borderRadius: 4
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: isDark.value ? '#808290' : '#7B91B0' }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: isDark.value ? 'rgba(255, 255, 255, 0.1)' : '#EFF1F3',
|
||||
width: 0.8
|
||||
}
|
||||
},
|
||||
axisLabel: { color: isDark.value ? '#808290' : '#7B91B0' }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: t('analysis.visitorInsights.legend.loyalCustomers'),
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'none',
|
||||
data: [260, 200, 150, 130, 180, 270, 340, 380, 300, 220, 170, 130],
|
||||
lineStyle: {
|
||||
color: '#2B8DFA',
|
||||
width: 3
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#2B8DFA'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t('analysis.visitorInsights.legend.newCustomers'),
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'none',
|
||||
data: [280, 350, 300, 250, 230, 210, 240, 280, 320, 350, 300, 200],
|
||||
lineStyle: {
|
||||
color: '#49BEFF',
|
||||
width: 3
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#49BEFF'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
watch(isDark, () => {
|
||||
initChart(options())
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
initChart(options())
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.visitor-insights {
|
||||
height: 330px;
|
||||
}
|
||||
|
||||
@media (max-width: $device-notebook) {
|
||||
.visitor-insights {
|
||||
height: 280px;
|
||||
|
||||
.card-body {
|
||||
> div {
|
||||
height: 210px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $device-phone) {
|
||||
.visitor-insights {
|
||||
height: 315px;
|
||||
|
||||
.card-body {
|
||||
> div {
|
||||
height: 240px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
125
src/views/dashboard/analysis/widget/VolumeServiceLevel.vue
Normal file
125
src/views/dashboard/analysis/widget/VolumeServiceLevel.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<div class="custom-card art-custom-card volume-service-level">
|
||||
<div class="custom-card-header">
|
||||
<span class="title">{{ t('analysis.volumeServiceLevel.title') }}</span>
|
||||
</div>
|
||||
<div class="custom-card-body">
|
||||
<div ref="chartRef" class="chart-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useChart } from '@/composables/useChart'
|
||||
import { EChartsOption } from 'echarts'
|
||||
const { t } = useI18n()
|
||||
|
||||
const { chartRef, isDark, initChart } = useChart()
|
||||
|
||||
// 模拟数据
|
||||
const chartData = [
|
||||
{ volume: 800, services: 400 },
|
||||
{ volume: 1000, services: 600 },
|
||||
{ volume: 750, services: 300 },
|
||||
{ volume: 600, services: 250 },
|
||||
{ volume: 450, services: 200 },
|
||||
{ volume: 500, services: 300 }
|
||||
]
|
||||
|
||||
const options: () => EChartsOption = () => ({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: [
|
||||
t('analysis.volumeServiceLevel.legend.volume'),
|
||||
t('analysis.volumeServiceLevel.legend.services')
|
||||
],
|
||||
bottom: 20,
|
||||
itemWidth: 10,
|
||||
itemHeight: 10,
|
||||
icon: 'circle',
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: isDark.value ? '#808290' : '#222B45'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '20',
|
||||
right: '20',
|
||||
bottom: '60',
|
||||
top: '30',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: chartData.map((_, index) => `${index + 1}`),
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: isDark.value ? 'rgba(255, 255, 255, 0.1)' : '#EFF1F3',
|
||||
width: 0.8
|
||||
}
|
||||
},
|
||||
axisTick: { show: false },
|
||||
axisLabel: { show: false }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { show: false },
|
||||
axisLabel: { show: false }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: t('analysis.volumeServiceLevel.legend.volume'),
|
||||
type: 'bar',
|
||||
stack: 'total',
|
||||
data: chartData.map((item) => item.volume),
|
||||
itemStyle: {
|
||||
color: '#0095FF',
|
||||
borderRadius: [0, 0, 4, 4]
|
||||
},
|
||||
barWidth: '15'
|
||||
},
|
||||
{
|
||||
name: t('analysis.volumeServiceLevel.legend.services'),
|
||||
type: 'bar',
|
||||
stack: 'total',
|
||||
data: chartData.map((item) => item.services),
|
||||
itemStyle: {
|
||||
color: '#95E0FB',
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
},
|
||||
barWidth: '50%'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
watch(isDark, () => {
|
||||
initChart(options())
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
initChart(options())
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.volume-service-level {
|
||||
height: 330px;
|
||||
|
||||
.custom-card-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 250px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
47
src/views/dashboard/console/index.vue
Normal file
47
src/views/dashboard/console/index.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div class="console">
|
||||
<CardList></CardList>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :sm="24" :md="12" :lg="10">
|
||||
<ActiveUser />
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12" :lg="14">
|
||||
<SalesOverview />
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :sm="24" :md="24" :lg="12">
|
||||
<NewUser />
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12" :lg="6">
|
||||
<Dynamic />
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12" :lg="6">
|
||||
<TodoList />
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<AboutProject />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import CardList from './widget/CardList.vue'
|
||||
import ActiveUser from './widget/ActiveUser.vue'
|
||||
import SalesOverview from './widget/SalesOverview.vue'
|
||||
import NewUser from './widget/NewUser.vue'
|
||||
import Dynamic from './widget/Dynamic.vue'
|
||||
import TodoList from './widget/TodoList.vue'
|
||||
import AboutProject from './widget/AboutProject.vue'
|
||||
import { useCommon } from '@/composables/useCommon'
|
||||
|
||||
defineOptions({ name: 'Console' })
|
||||
|
||||
useCommon().scrollToTop()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use './style';
|
||||
</style>
|
||||
43
src/views/dashboard/console/style.scss
Normal file
43
src/views/dashboard/console/style.scss
Normal file
@@ -0,0 +1,43 @@
|
||||
@use '@styles/variables.scss' as *;
|
||||
|
||||
.console {
|
||||
--card-spacing: 20px;
|
||||
|
||||
// 卡片头部
|
||||
:deep(.card-header) {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 20px 25px 5px 0;
|
||||
|
||||
.title {
|
||||
h4 {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: var(--art-gray-900) !important;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 3px;
|
||||
font-size: 13px;
|
||||
color: var(--art-gray-600) !important;
|
||||
|
||||
span {
|
||||
margin-left: 10px;
|
||||
color: #52c41a;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置卡片背景色、圆角、间隙
|
||||
:deep(.card-list .card),
|
||||
.card {
|
||||
margin-bottom: var(--card-spacing);
|
||||
background: var(--art-main-bg-color);
|
||||
border-radius: calc(var(--custom-radius) + 4px) !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $device-phone) {
|
||||
--card-spacing: 15px;
|
||||
}
|
||||
}
|
||||
139
src/views/dashboard/console/widget/AboutProject.vue
Normal file
139
src/views/dashboard/console/widget/AboutProject.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div class="card about-project art-custom-card">
|
||||
<div>
|
||||
<h2 class="box-title">关于项目</h2>
|
||||
<p>{{ systemName }} 是一款专注于用户体验和视觉设计的后台管理系统模版</p>
|
||||
<p>使用了 Vue3、TypeScript、Vite、Element Plus 等前沿技术</p>
|
||||
|
||||
<div class="button-wrap">
|
||||
<div class="btn art-custom-card" @click="goPage(WEB_LINKS.DOCS)">
|
||||
<span>项目官网</span>
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="btn art-custom-card" @click="goPage(WEB_LINKS.INTRODUCE)">
|
||||
<span>文档</span>
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="btn art-custom-card" @click="goPage(WEB_LINKS.GITHUB_HOME)">
|
||||
<span>Github</span>
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="btn art-custom-card" @click="goPage(WEB_LINKS.BLOG)">
|
||||
<span>博客</span>
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<img class="right-img" src="@imgs/draw/draw1.png" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AppConfig from '@/config'
|
||||
import { WEB_LINKS } from '@/utils/constants'
|
||||
|
||||
const systemName = AppConfig.systemInfo.name
|
||||
|
||||
const goPage = (url: string) => {
|
||||
window.open(url)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.about-project {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 300px;
|
||||
padding: 20px;
|
||||
|
||||
h2 {
|
||||
margin-top: 10px;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: var(--art-gray-900) !important;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 5px;
|
||||
font-size: 14px;
|
||||
color: var(--art-gray-600);
|
||||
}
|
||||
}
|
||||
|
||||
.button-wrap {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 600px;
|
||||
margin-top: 35px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 240px;
|
||||
height: 50px;
|
||||
padding: 0 15px;
|
||||
margin: 0 15px 15px 0;
|
||||
font-size: 14px;
|
||||
color: var(--art-gray-800);
|
||||
cursor: pointer;
|
||||
background: var(--art-bg-color);
|
||||
border-radius: calc(var(--custom-radius) / 2 + 2px) !important;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 5px 10px rgb(0 0 0 / 5%);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计 - iPad Pro及以下
|
||||
@media screen and (max-width: $device-ipad-pro) {
|
||||
.about-project {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.button-wrap {
|
||||
width: 470px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.right-img {
|
||||
width: 300px;
|
||||
height: 230px;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计 - iPad垂直及以下
|
||||
@media screen and (max-width: $device-ipad-vertical) {
|
||||
.button-wrap {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 190px;
|
||||
}
|
||||
|
||||
.right-img {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计 - 手机端
|
||||
@media screen and (max-width: $device-phone) {
|
||||
.about-project {
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
176
src/views/dashboard/console/widget/ActiveUser.vue
Normal file
176
src/views/dashboard/console/widget/ActiveUser.vue
Normal file
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<div class="card art-custom-card">
|
||||
<div class="chart" ref="chartRef"></div>
|
||||
<div class="text">
|
||||
<h3 class="box-title">用户概述</h3>
|
||||
<p class="subtitle">比上周 <span class="text-success">+23%</span></p>
|
||||
<p class="subtitle">我们为您创建了多个选项,可将它们组合在一起并定制为像素完美的页面</p>
|
||||
</div>
|
||||
<div class="list">
|
||||
<div v-for="(item, index) in list" :key="index">
|
||||
<p>{{ item.num }}</p>
|
||||
<p class="subtitle">{{ item.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as echarts from 'echarts'
|
||||
import { getCssVar } from '@/utils/ui'
|
||||
import { useChart } from '@/composables/useChart'
|
||||
import { EChartsOption } from 'echarts'
|
||||
|
||||
const {
|
||||
chartRef,
|
||||
isDark,
|
||||
initChart,
|
||||
getAxisLineStyle,
|
||||
getAxisLabelStyle,
|
||||
getAxisTickStyle,
|
||||
getSplitLineStyle
|
||||
} = useChart()
|
||||
|
||||
const list = [
|
||||
{ name: '总用户量', num: '32k' },
|
||||
{ name: '总访问量', num: '128k' },
|
||||
{ name: '日访问量', num: '1.2k' },
|
||||
{ name: '周同比', num: '+5%' }
|
||||
]
|
||||
|
||||
const options: () => EChartsOption = () => {
|
||||
return {
|
||||
grid: {
|
||||
top: 15,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
containLabel: true
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: [1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||
axisTick: getAxisTickStyle(),
|
||||
axisLine: getAxisLineStyle(true),
|
||||
axisLabel: getAxisLabelStyle(true)
|
||||
},
|
||||
yAxis: {
|
||||
axisLabel: getAxisLabelStyle(true),
|
||||
axisLine: getAxisLineStyle(!isDark.value),
|
||||
splitLine: getSplitLineStyle(true)
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: [160, 100, 150, 80, 190, 100, 175, 120, 160],
|
||||
type: 'bar',
|
||||
itemStyle: {
|
||||
borderRadius: 4,
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: getCssVar('--el-color-primary-light-4')
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: getCssVar('--el-color-primary')
|
||||
}
|
||||
])
|
||||
},
|
||||
barWidth: '50%',
|
||||
animationDelay: (idx) => idx * 50 + 300,
|
||||
animationDuration: (idx) => 1500 - idx * 50,
|
||||
animationEasing: 'quarticOut' // 推荐动画: quarticOut exponentialOut quinticOut backOut
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
watch(isDark, () => {
|
||||
initChart(options())
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
initChart(options())
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 420px;
|
||||
padding: 16px;
|
||||
|
||||
.chart {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 220px;
|
||||
padding: 20px 0 20px 20px;
|
||||
border-radius: calc(var(--custom-radius) / 2 + 4px) !important;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-left: 3px;
|
||||
|
||||
h3 {
|
||||
margin-top: 20px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 5px;
|
||||
font-size: 14px;
|
||||
|
||||
&:last-of-type {
|
||||
height: 42px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-left: 3px;
|
||||
|
||||
> div {
|
||||
flex: 1;
|
||||
|
||||
p {
|
||||
font-weight: 400;
|
||||
|
||||
&:first-of-type {
|
||||
font-size: 24px;
|
||||
color: var(--art-gray-900);
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.card {
|
||||
.chart {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $device-phone) {
|
||||
.dark {
|
||||
.card {
|
||||
.chart {
|
||||
padding: 15px 0 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
161
src/views/dashboard/console/widget/CardList.vue
Normal file
161
src/views/dashboard/console/widget/CardList.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<el-row :gutter="20" :style="{ marginTop: showWorkTab ? '0' : '10px' }" class="card-list">
|
||||
<el-col v-for="(item, index) in dataList" :key="index" :sm="12" :md="6" :lg="6">
|
||||
<div class="card art-custom-card">
|
||||
<span class="des subtitle">{{ item.des }}</span>
|
||||
<CountTo
|
||||
class="number box-title"
|
||||
:endVal="item.num"
|
||||
:duration="1000"
|
||||
separator=""
|
||||
></CountTo>
|
||||
<div class="change-box">
|
||||
<span class="change-text">较上周</span>
|
||||
<span
|
||||
class="change"
|
||||
:class="[item.change.indexOf('+') === -1 ? 'text-danger' : 'text-success']"
|
||||
>
|
||||
{{ item.change }}
|
||||
</span>
|
||||
</div>
|
||||
<i class="iconfont-sys" v-html="item.icon"></i>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useSettingStore } from '@/store/modules/setting'
|
||||
import { CountTo } from 'vue3-count-to'
|
||||
|
||||
const { showWorkTab } = storeToRefs(useSettingStore())
|
||||
|
||||
const dataList = reactive([
|
||||
{
|
||||
des: '总访问次数',
|
||||
icon: '',
|
||||
startVal: 0,
|
||||
duration: 1000,
|
||||
num: 9120,
|
||||
change: '+20%'
|
||||
},
|
||||
{
|
||||
des: '在线访客数',
|
||||
icon: '',
|
||||
startVal: 0,
|
||||
duration: 1000,
|
||||
num: 182,
|
||||
change: '+10%'
|
||||
},
|
||||
{
|
||||
des: '点击量',
|
||||
icon: '',
|
||||
startVal: 0,
|
||||
duration: 1000,
|
||||
num: 9520,
|
||||
change: '-12%'
|
||||
},
|
||||
{
|
||||
des: '新用户',
|
||||
icon: '',
|
||||
startVal: 0,
|
||||
duration: 1000,
|
||||
num: 156,
|
||||
change: '+30%'
|
||||
}
|
||||
])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-list {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
background-color: transparent !important;
|
||||
|
||||
.art-custom-card {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 140px;
|
||||
padding: 0 18px;
|
||||
list-style: none;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
$icon-size: 52px;
|
||||
|
||||
.iconfont-sys {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 20px;
|
||||
bottom: 0;
|
||||
width: $icon-size;
|
||||
height: $icon-size;
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
font-size: 22px;
|
||||
line-height: $icon-size;
|
||||
color: var(--el-color-primary) !important;
|
||||
text-align: center;
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.des {
|
||||
display: block;
|
||||
height: 14px;
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.number {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
font-size: 28px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.change-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
|
||||
.change-text {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
color: var(--art-text-gray-600);
|
||||
}
|
||||
|
||||
.change {
|
||||
display: block;
|
||||
margin-left: 5px;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
|
||||
&.text-success {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
|
||||
&.text-danger {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.card-list {
|
||||
.art-custom-card {
|
||||
.iconfont-sys {
|
||||
background-color: #232323 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
100
src/views/dashboard/console/widget/Dynamic.vue
Normal file
100
src/views/dashboard/console/widget/Dynamic.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div class="card art-custom-card">
|
||||
<div class="card-header">
|
||||
<div class="title">
|
||||
<h4 class="box-title">动态</h4>
|
||||
<p class="subtitle">新增<span class="text-success">+6</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list">
|
||||
<div v-for="(item, index) in list" :key="index">
|
||||
<span class="user">{{ item.username }}</span>
|
||||
<span class="type">{{ item.type }}</span>
|
||||
<span class="target">{{ item.target }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue-demi'
|
||||
|
||||
const list = reactive([
|
||||
{
|
||||
username: '中小鱼',
|
||||
type: '关注了',
|
||||
target: '誶誶淰'
|
||||
},
|
||||
{
|
||||
username: '何小荷',
|
||||
type: '发表文章',
|
||||
target: 'Vue3 + Typescript + Vite 项目实战笔记'
|
||||
},
|
||||
{
|
||||
username: '誶誶淰',
|
||||
type: '提出问题',
|
||||
target: '主题可以配置吗'
|
||||
},
|
||||
{
|
||||
username: '发呆草',
|
||||
type: '兑换了物品',
|
||||
target: '《奇特的一生》'
|
||||
},
|
||||
{
|
||||
username: '甜筒',
|
||||
type: '关闭了问题',
|
||||
target: '发呆草'
|
||||
},
|
||||
{
|
||||
username: '冷月呆呆',
|
||||
type: '兑换了物品',
|
||||
target: '《高效人士的七个习惯》'
|
||||
}
|
||||
])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 510px;
|
||||
padding: 0 25px;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 20px 0 0;
|
||||
}
|
||||
|
||||
.list {
|
||||
height: calc(100% - 100px);
|
||||
margin-top: 10px;
|
||||
overflow: hidden;
|
||||
|
||||
> div {
|
||||
height: 70px;
|
||||
overflow: hidden;
|
||||
line-height: 70px;
|
||||
border-bottom: 1px solid var(--art-border-color);
|
||||
|
||||
span {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.user {
|
||||
font-weight: 500;
|
||||
color: var(--art-text-gray-800);
|
||||
}
|
||||
|
||||
.type {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.target {
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
176
src/views/dashboard/console/widget/NewUser.vue
Normal file
176
src/views/dashboard/console/widget/NewUser.vue
Normal file
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<div class="card art-custom-card">
|
||||
<div class="card-header">
|
||||
<div class="title">
|
||||
<h4 class="box-title">新用户</h4>
|
||||
<p class="subtitle">这个月增长<span class="text-success">+20%</span></p>
|
||||
</div>
|
||||
<el-radio-group v-model="radio2">
|
||||
<el-radio-button value="本月" label="本月"></el-radio-button>
|
||||
<el-radio-button value="上月" label="上月"></el-radio-button>
|
||||
<el-radio-button value="今年" label="今年"></el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<art-table
|
||||
class="table"
|
||||
:data="tableData"
|
||||
:pagination="false"
|
||||
size="large"
|
||||
:border="false"
|
||||
:stripe="false"
|
||||
:show-header-background="false"
|
||||
>
|
||||
<template #default>
|
||||
<el-table-column label="头像" prop="avatar" width="150px">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center">
|
||||
<img class="avatar" :src="scope.row.avatar" />
|
||||
<span class="user-name">{{ scope.row.username }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="地区" prop="province" />
|
||||
<el-table-column label="性别" prop="avatar">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center">
|
||||
<span style="margin-left: 10px">{{ scope.row.sex === 1 ? '男' : '女' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="进度" width="240">
|
||||
<template #default="scope">
|
||||
<el-progress :percentage="scope.row.pro" :color="scope.row.color" :stroke-width="4" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
</art-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, reactive } from 'vue-demi'
|
||||
import avatar1 from '@/assets/img/avatar/avatar1.webp'
|
||||
import avatar2 from '@/assets/img/avatar/avatar2.webp'
|
||||
import avatar3 from '@/assets/img/avatar/avatar3.webp'
|
||||
import avatar4 from '@/assets/img/avatar/avatar4.webp'
|
||||
import avatar5 from '@/assets/img/avatar/avatar5.webp'
|
||||
import avatar6 from '@/assets/img/avatar/avatar6.webp'
|
||||
|
||||
const radio2 = ref('本月')
|
||||
|
||||
const tableData = reactive([
|
||||
{
|
||||
username: '中小鱼',
|
||||
province: '北京',
|
||||
sex: 0,
|
||||
age: 22,
|
||||
percentage: 60,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-primary)) !important',
|
||||
avatar: avatar1
|
||||
},
|
||||
{
|
||||
username: '何小荷',
|
||||
province: '深圳',
|
||||
sex: 1,
|
||||
age: 21,
|
||||
percentage: 20,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-secondary)) !important',
|
||||
avatar: avatar2
|
||||
},
|
||||
{
|
||||
username: '誶誶淰',
|
||||
province: '上海',
|
||||
sex: 1,
|
||||
age: 23,
|
||||
percentage: 60,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-warning)) !important',
|
||||
avatar: avatar3
|
||||
},
|
||||
{
|
||||
username: '发呆草',
|
||||
province: '长沙',
|
||||
sex: 0,
|
||||
age: 28,
|
||||
percentage: 50,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-info)) !important',
|
||||
avatar: avatar4
|
||||
},
|
||||
{
|
||||
username: '甜筒',
|
||||
province: '浙江',
|
||||
sex: 1,
|
||||
age: 26,
|
||||
percentage: 70,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-error)) !important',
|
||||
avatar: avatar5
|
||||
},
|
||||
{
|
||||
username: '冷月呆呆',
|
||||
province: '湖北',
|
||||
sex: 1,
|
||||
age: 25,
|
||||
percentage: 90,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-success)) !important',
|
||||
avatar: avatar6
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
addAnimation()
|
||||
})
|
||||
|
||||
const addAnimation = () => {
|
||||
setTimeout(() => {
|
||||
for (let i = 0; i < tableData.length; i++) {
|
||||
let item = tableData[i]
|
||||
tableData[i].pro = item.percentage
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.card {
|
||||
// 进度动画
|
||||
.el-progress-bar__inner {
|
||||
transition: all 1s !important;
|
||||
}
|
||||
|
||||
.el-radio-button__original-radio:checked + .el-radio-button__inner {
|
||||
color: var(--el-color-primary) !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card {
|
||||
width: 100%;
|
||||
height: 510px;
|
||||
overflow: hidden;
|
||||
|
||||
.card-header {
|
||||
padding-left: 25px !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr:last-child td) {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
143
src/views/dashboard/console/widget/SalesOverview.vue
Normal file
143
src/views/dashboard/console/widget/SalesOverview.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<div class="card art-custom-card">
|
||||
<div class="card-header">
|
||||
<div class="title">
|
||||
<h4 class="box-title">访问量</h4>
|
||||
<p class="subtitle">今年增长<span class="text-success">+15%</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart" ref="chartRef"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as echarts from 'echarts'
|
||||
import { hexToRgba, getCssVar } from '@/utils/ui'
|
||||
import { EChartsOption } from 'echarts'
|
||||
import { useChart } from '@/composables/useChart'
|
||||
|
||||
const {
|
||||
chartRef,
|
||||
isDark,
|
||||
initChart,
|
||||
updateChart,
|
||||
getAxisLabelStyle,
|
||||
getAxisLineStyle,
|
||||
getAxisTickStyle,
|
||||
getSplitLineStyle
|
||||
} = useChart()
|
||||
|
||||
// 定义真实数据
|
||||
const realData = [50, 25, 40, 20, 70, 35, 65, 30, 35, 20, 40, 44]
|
||||
|
||||
// 初始化动画函数
|
||||
const initChartWithAnimation = () => {
|
||||
// 首先初始化图表,数据为0
|
||||
initChart(options(true))
|
||||
updateChart(options(false))
|
||||
}
|
||||
|
||||
watch(isDark, () => {
|
||||
initChart(options())
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
initChartWithAnimation()
|
||||
})
|
||||
|
||||
const options: (isInitial?: boolean) => EChartsOption = (isInitial) => {
|
||||
const isInit = isInitial || false
|
||||
return {
|
||||
// 添加动画配置
|
||||
animation: true,
|
||||
animationDuration: 0,
|
||||
animationDurationUpdate: 0,
|
||||
grid: {
|
||||
left: '2.2%',
|
||||
right: '3%',
|
||||
bottom: '0%',
|
||||
top: '5px',
|
||||
containLabel: true
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: [
|
||||
'1月',
|
||||
'2月',
|
||||
'3月',
|
||||
'4月',
|
||||
'5月',
|
||||
'6月',
|
||||
'7月',
|
||||
'8月',
|
||||
'9月',
|
||||
'10月',
|
||||
'11月',
|
||||
'12月'
|
||||
],
|
||||
axisTick: getAxisTickStyle(),
|
||||
axisLabel: getAxisLabelStyle(true),
|
||||
axisLine: getAxisLineStyle(true)
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: realData.reduce((prev, curr) => Math.max(prev, curr), 0),
|
||||
axisLabel: getAxisLabelStyle(true),
|
||||
axisLine: getAxisLineStyle(!isDark.value),
|
||||
splitLine: getSplitLineStyle(true)
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '访客',
|
||||
color: getCssVar('--main-color'),
|
||||
type: 'line',
|
||||
stack: '总量',
|
||||
data: isInit ? new Array(12).fill(0) : realData,
|
||||
smooth: true,
|
||||
symbol: 'none',
|
||||
lineStyle: {
|
||||
width: 2.2
|
||||
},
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: hexToRgba(getCssVar('--el-color-primary'), 0.15).rgba
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: hexToRgba(getCssVar('--el-color-primary'), 0.01).rgba
|
||||
}
|
||||
])
|
||||
},
|
||||
animationDuration: 0,
|
||||
animationDurationUpdate: 1500
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 420px;
|
||||
padding: 20px 0 30px;
|
||||
|
||||
.card-header {
|
||||
padding: 0 18px !important;
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: calc(100% - 80px);
|
||||
margin-top: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
102
src/views/dashboard/console/widget/TodoList.vue
Normal file
102
src/views/dashboard/console/widget/TodoList.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div class="card art-custom-card">
|
||||
<div class="card-header">
|
||||
<div class="title">
|
||||
<h4 class="box-title">代办事项</h4>
|
||||
<p class="subtitle">待处理<span class="text-danger">3</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list">
|
||||
<div v-for="(item, index) in list" :key="index">
|
||||
<p class="title">{{ item.username }}</p>
|
||||
<p class="date subtitle">{{ item.date }}</p>
|
||||
<el-checkbox v-model="item.complate" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue-demi'
|
||||
|
||||
const list = reactive([
|
||||
{
|
||||
username: '查看今天工作内容',
|
||||
date: '上午 09:30',
|
||||
complate: true
|
||||
},
|
||||
{
|
||||
username: '回复邮件',
|
||||
date: '上午 10:30',
|
||||
complate: true
|
||||
},
|
||||
{
|
||||
username: '工作汇报整理',
|
||||
date: '上午 11:00',
|
||||
complate: true
|
||||
},
|
||||
{
|
||||
username: '产品需求会议',
|
||||
date: '下午 02:00',
|
||||
complate: false
|
||||
},
|
||||
{
|
||||
username: '整理会议内容',
|
||||
date: '下午 03:30',
|
||||
complate: false
|
||||
},
|
||||
{
|
||||
username: '明天工作计划',
|
||||
date: '下午 06:30',
|
||||
complate: false
|
||||
}
|
||||
])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 510px;
|
||||
padding: 0 25px;
|
||||
|
||||
.list {
|
||||
height: calc(100% - 90px);
|
||||
margin-top: 10px;
|
||||
overflow: hidden;
|
||||
|
||||
> div {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 70px;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid var(--art-border-color);
|
||||
|
||||
p {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.date {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.el-checkbox {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 10px;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
80
src/views/dashboard/ecommerce/index.vue
Normal file
80
src/views/dashboard/ecommerce/index.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div class="ecommerce">
|
||||
<el-row :gutter="20">
|
||||
<el-col :sm="24" :md="24" :lg="16">
|
||||
<Banner />
|
||||
</el-col>
|
||||
<el-col :sm="12" :md="12" :lg="4">
|
||||
<TotalOrderVolume />
|
||||
</el-col>
|
||||
<el-col :sm="12" :md="12" :lg="4">
|
||||
<TotalProducts />
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :sm="12" :md="12" :lg="8">
|
||||
<SalesTrend />
|
||||
</el-col>
|
||||
<el-col :sm="12" :md="12" :lg="8">
|
||||
<SalesClassification />
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="24" :lg="8">
|
||||
<el-row :gutter="20">
|
||||
<el-col :sm="24" :md="12" :lg="12">
|
||||
<ProductSales />
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12" :lg="12">
|
||||
<SalesGrowth />
|
||||
</el-col>
|
||||
<el-col :span="24" class="no-margin-bottom">
|
||||
<CartConversionRate />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :sm="24" :md="12" :lg="8">
|
||||
<HotCommodity />
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12" :lg="8">
|
||||
<AnnualSales />
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="24" :lg="8">
|
||||
<TransactionList />
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :md="24" :lg="8">
|
||||
<RecentTransaction />
|
||||
</el-col>
|
||||
<el-col :md="24" :lg="16" class="no-margin-bottom">
|
||||
<HotProductsList />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Banner from './widget/Banner.vue'
|
||||
import TotalOrderVolume from './widget/TotalOrderVolume.vue'
|
||||
import TotalProducts from './widget/TotalProducts.vue'
|
||||
import SalesTrend from './widget/SalesTrend.vue'
|
||||
import SalesClassification from './widget/SalesClassification.vue'
|
||||
import TransactionList from './widget/TransactionList.vue'
|
||||
import HotCommodity from './widget/HotCommodity.vue'
|
||||
import RecentTransaction from './widget/RecentTransaction.vue'
|
||||
import AnnualSales from './widget/AnnualSales.vue'
|
||||
import ProductSales from './widget/ProductSales.vue'
|
||||
import SalesGrowth from './widget/SalesGrowth.vue'
|
||||
import CartConversionRate from './widget/CartConversionRate.vue'
|
||||
import HotProductsList from './widget/HotProductsList.vue'
|
||||
|
||||
defineOptions({ name: 'Ecommerce' })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use './style';
|
||||
</style>
|
||||
72
src/views/dashboard/ecommerce/style.scss
Normal file
72
src/views/dashboard/ecommerce/style.scss
Normal file
@@ -0,0 +1,72 @@
|
||||
.ecommerce {
|
||||
:deep(.card) {
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
background-color: var(--art-main-bg-color);
|
||||
border-radius: var(--custom-radius);
|
||||
|
||||
.card-header {
|
||||
padding-bottom: 15px;
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: var(--art-gray-900);
|
||||
|
||||
i {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--art-gray-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.icon-text-widget) {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
margin-right: 10px;
|
||||
line-height: 42px;
|
||||
color: var(--main-color);
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border-radius: 8px;
|
||||
|
||||
i {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
p {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-margin-bottom {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.el-col {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
37
src/views/dashboard/ecommerce/widget/AnnualSales.vue
Normal file
37
src/views/dashboard/ecommerce/widget/AnnualSales.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div class="card art-custom-card yearly-card" style="height: 28.2rem">
|
||||
<div class="card-header">
|
||||
<p class="title">年度销售额</p>
|
||||
<p class="subtitle">按季度统计</p>
|
||||
</div>
|
||||
|
||||
<ArtBarChart
|
||||
:showAxisLabel="false"
|
||||
:showAxisLine="false"
|
||||
:showSplitLine="false"
|
||||
:data="[50, 80, 50, 90, 60, 70, 50]"
|
||||
barWidth="26px"
|
||||
height="16rem"
|
||||
/>
|
||||
<div class="icon-text-widget" style="margin-top: 50px">
|
||||
<div class="item">
|
||||
<div class="icon">
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>¥200,858</p>
|
||||
<span>线上销售</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="icon">
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>¥102,927</p>
|
||||
<span>线下销售</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
73
src/views/dashboard/ecommerce/widget/Banner.vue
Normal file
73
src/views/dashboard/ecommerce/widget/Banner.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<ArtBasicBanner
|
||||
class="banner"
|
||||
:title="`欢迎回来 ${userInfo.userName}`"
|
||||
:showButton="false"
|
||||
backgroundColor="var(--el-color-primary-light-9)"
|
||||
titleColor="var(--art-gray-900)"
|
||||
subtitleColor="#666"
|
||||
style="height: 13.3rem"
|
||||
:backgroundImage="bannerCover"
|
||||
:showDecoration="false"
|
||||
imgWidth="18rem"
|
||||
imgBottom="-7.5rem"
|
||||
:showMeteors="true"
|
||||
@click="handleBannerClick"
|
||||
>
|
||||
<div class="banner-slot">
|
||||
<div class="item">
|
||||
<p class="title">¥2,340<i class="iconfont-sys text-success"></i></p>
|
||||
<p class="subtitle">今日销售额</p>
|
||||
</div>
|
||||
<div class="item">
|
||||
<p class="title">35%<i class="iconfont-sys text-success"></i></p>
|
||||
<p class="subtitle">较昨日</p>
|
||||
</div>
|
||||
</div>
|
||||
</ArtBasicBanner>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import bannerCover from '@imgs/login/lf_icon2.webp'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
const userStore = useUserStore()
|
||||
|
||||
const userInfo = computed(() => userStore.getUserInfo)
|
||||
|
||||
const handleBannerClick = () => {}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.banner {
|
||||
.banner-slot {
|
||||
display: flex;
|
||||
|
||||
.item {
|
||||
margin-right: 30px;
|
||||
|
||||
&:first-of-type {
|
||||
padding-right: 30px;
|
||||
border-right: 1px solid var(--art-gray-300);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 30px;
|
||||
color: var(--art-gray-900) !important;
|
||||
|
||||
i {
|
||||
position: relative;
|
||||
top: -10px;
|
||||
margin-left: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin-top: 4px;
|
||||
font-size: 14px;
|
||||
color: var(--art-gray-700) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
11
src/views/dashboard/ecommerce/widget/CartConversionRate.vue
Normal file
11
src/views/dashboard/ecommerce/widget/CartConversionRate.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<ArtLineChartCard
|
||||
class="margin-bottom-0"
|
||||
:value="2545"
|
||||
label="购物车转化率"
|
||||
:percentage="1.2"
|
||||
:height="13.5"
|
||||
:chartData="[120, 132, 101, 134, 90, 230, 210]"
|
||||
:showAreaColor="true"
|
||||
/>
|
||||
</template>
|
||||
107
src/views/dashboard/ecommerce/widget/HotCommodity.vue
Normal file
107
src/views/dashboard/ecommerce/widget/HotCommodity.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="card art-custom-card weekly-card" style="height: 28.2rem">
|
||||
<div class="card-header">
|
||||
<p class="title">热销商品</p>
|
||||
<p class="subtitle">本周销售排行</p>
|
||||
</div>
|
||||
<ArtLineChart
|
||||
:showAxisLabel="false"
|
||||
:showAxisLine="false"
|
||||
:showSplitLine="false"
|
||||
:showAreaColor="true"
|
||||
:data="[8, 40, 82, 35, 90, 52, 35]"
|
||||
height="9rem"
|
||||
/>
|
||||
<div class="content">
|
||||
<div class="item" v-for="item in weeklyList" :key="item.title">
|
||||
<div class="icon" :class="item.color">
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="text">
|
||||
<p class="title">{{ item.title }}</p>
|
||||
<span class="subtitle">{{ item.subtitle }}</span>
|
||||
</div>
|
||||
<div class="value" :class="item.color">
|
||||
<span>+{{ item.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const weeklyList = [
|
||||
{
|
||||
icon: '',
|
||||
title: '智能手表Pro',
|
||||
subtitle: '电子产品',
|
||||
value: '1,286件',
|
||||
color: 'bg-primary'
|
||||
},
|
||||
{
|
||||
icon: '',
|
||||
title: '时尚连衣裙',
|
||||
subtitle: '女装服饰',
|
||||
value: '892件',
|
||||
color: 'bg-success'
|
||||
},
|
||||
{
|
||||
icon: '',
|
||||
title: '厨房小家电',
|
||||
subtitle: '家居用品',
|
||||
value: '756件',
|
||||
color: 'bg-error'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.weekly-card {
|
||||
.content {
|
||||
margin-top: 40px;
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
|
||||
.icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
line-height: 42px;
|
||||
text-align: center;
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border-radius: 8px;
|
||||
|
||||
i {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-left: 10px;
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--art-gray-800);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--art-gray-600);
|
||||
}
|
||||
}
|
||||
|
||||
.value {
|
||||
padding: 6px 12px;
|
||||
margin-left: auto;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
231
src/views/dashboard/ecommerce/widget/HotProductsList.vue
Normal file
231
src/views/dashboard/ecommerce/widget/HotProductsList.vue
Normal file
@@ -0,0 +1,231 @@
|
||||
<template>
|
||||
<div class="card art-custom-card" style="height: 27.8rem">
|
||||
<div class="card-header">
|
||||
<p class="title">热销产品</p>
|
||||
<p class="subtitle">本月销售情况</p>
|
||||
</div>
|
||||
<div class="table">
|
||||
<el-scrollbar style="height: 21.55rem">
|
||||
<art-table
|
||||
:data="tableData"
|
||||
:pagination="false"
|
||||
style="margin-top: 0 !important"
|
||||
size="large"
|
||||
:border="false"
|
||||
:stripe="false"
|
||||
:show-header-background="false"
|
||||
>
|
||||
<template #default>
|
||||
<el-table-column label="产品" prop="product" width="220px">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center">
|
||||
<img class="product-image" :src="scope.row.image" />
|
||||
<div class="product-info">
|
||||
<div class="product-name">{{ scope.row.name }}</div>
|
||||
<div class="product-category">{{ scope.row.category }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="价格" prop="price">
|
||||
<template #default="scope">
|
||||
<span class="price">¥{{ scope.row.price.toLocaleString() }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="库存" prop="stock">
|
||||
<template #default="scope">
|
||||
<div class="stock-badge" :class="getStockClass(scope.row.stock)">
|
||||
{{ getStockStatus(scope.row.stock) }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="销量" prop="sales" />
|
||||
<el-table-column label="销售趋势" width="240">
|
||||
<template #default="scope">
|
||||
<el-progress
|
||||
:percentage="scope.row.pro"
|
||||
:color="scope.row.color"
|
||||
:stroke-width="4"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
</art-table>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, onMounted } from 'vue'
|
||||
|
||||
// 导入产品图片
|
||||
import product1 from '@/assets/img/3d/icon1.webp'
|
||||
import product2 from '@/assets/img/3d/icon2.webp'
|
||||
import product3 from '@/assets/img/3d/icon3.webp'
|
||||
import product4 from '@/assets/img/3d/icon4.webp'
|
||||
import product5 from '@/assets/img/3d/icon5.webp'
|
||||
import product6 from '@/assets/img/3d/icon6.webp'
|
||||
|
||||
const tableData = reactive([
|
||||
{
|
||||
name: '智能手表 Pro',
|
||||
category: '电子设备',
|
||||
price: 1299,
|
||||
stock: 156,
|
||||
sales: 423,
|
||||
percentage: 75,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-primary)) !important',
|
||||
image: product1
|
||||
},
|
||||
{
|
||||
name: '无线蓝牙耳机',
|
||||
category: '音频设备',
|
||||
price: 499,
|
||||
stock: 89,
|
||||
sales: 652,
|
||||
percentage: 85,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-success)) !important',
|
||||
image: product2
|
||||
},
|
||||
{
|
||||
name: '机械键盘',
|
||||
category: '电脑配件',
|
||||
price: 399,
|
||||
stock: 12,
|
||||
sales: 238,
|
||||
percentage: 45,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-warning)) !important',
|
||||
image: product3
|
||||
},
|
||||
{
|
||||
name: '超薄笔记本电脑',
|
||||
category: '电子设备',
|
||||
price: 5999,
|
||||
stock: 0,
|
||||
sales: 126,
|
||||
percentage: 30,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-error)) !important',
|
||||
image: product4
|
||||
},
|
||||
{
|
||||
name: '智能音箱',
|
||||
category: '智能家居',
|
||||
price: 799,
|
||||
stock: 45,
|
||||
sales: 321,
|
||||
percentage: 60,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-info)) !important',
|
||||
image: product5
|
||||
},
|
||||
{
|
||||
name: '游戏手柄',
|
||||
category: '游戏配件',
|
||||
price: 299,
|
||||
stock: 78,
|
||||
sales: 489,
|
||||
percentage: 70,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-secondary)) !important',
|
||||
image: product6
|
||||
}
|
||||
])
|
||||
|
||||
// 根据库存获取状态文本
|
||||
const getStockStatus = (stock: number) => {
|
||||
if (stock === 0) return '缺货'
|
||||
if (stock < 20) return '低库存'
|
||||
if (stock < 50) return '适中'
|
||||
return '充足'
|
||||
}
|
||||
|
||||
// 根据库存获取状态类名
|
||||
const getStockClass = (stock: number) => {
|
||||
if (stock === 0) return 'out-of-stock'
|
||||
if (stock < 20) return 'low-stock'
|
||||
if (stock < 50) return 'medium-stock'
|
||||
return 'in-stock'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
addAnimation()
|
||||
})
|
||||
|
||||
const addAnimation = () => {
|
||||
setTimeout(() => {
|
||||
for (let i = 0; i < tableData.length; i++) {
|
||||
let item = tableData[i]
|
||||
tableData[i].pro = item.percentage
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.table {
|
||||
width: 100%;
|
||||
|
||||
.card-header {
|
||||
padding-left: 25px !important;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.product-category {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.price {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stock-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.in-stock {
|
||||
color: rgb(var(--art-success));
|
||||
background-color: rgba(var(--art-success-rgb), 0.1);
|
||||
}
|
||||
|
||||
.medium-stock {
|
||||
color: rgb(var(--art-info));
|
||||
background-color: rgba(var(--art-info-rgb), 0.1);
|
||||
}
|
||||
|
||||
.low-stock {
|
||||
color: rgb(var(--art-warning));
|
||||
background-color: rgba(var(--art-warning-rgb), 0.1);
|
||||
}
|
||||
|
||||
.out-of-stock {
|
||||
color: rgb(var(--art-error));
|
||||
background-color: rgba(var(--art-error-rgb), 0.1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
19
src/views/dashboard/ecommerce/widget/ProductSales.vue
Normal file
19
src/views/dashboard/ecommerce/widget/ProductSales.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div class="card art-custom-card" style="height: 11rem">
|
||||
<div class="card-header">
|
||||
<p class="title" style="font-size: 24px"
|
||||
>14.5k<i class="iconfont-sys text-success"></i></p
|
||||
>
|
||||
<p class="subtitle">销售量</p>
|
||||
</div>
|
||||
|
||||
<ArtBarChart
|
||||
:showAxisLabel="false"
|
||||
:showAxisLine="false"
|
||||
:showSplitLine="false"
|
||||
:data="[50, 80, 50, 90, 60, 70, 50]"
|
||||
barWidth="16px"
|
||||
height="4rem"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
41
src/views/dashboard/ecommerce/widget/RecentTransaction.vue
Normal file
41
src/views/dashboard/ecommerce/widget/RecentTransaction.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<ArtTimelineListCard :list="timelineData" title="最近交易" subtitle="今日订单动态" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const timelineData = [
|
||||
{
|
||||
time: '上午 09:30',
|
||||
status: 'rgb(73, 190, 255)',
|
||||
content: '收到订单 #38291 支付 ¥385.90'
|
||||
},
|
||||
{
|
||||
time: '上午 10:00',
|
||||
status: 'rgb(54, 158, 255)',
|
||||
content: '新商品上架',
|
||||
code: 'SKU-3467'
|
||||
},
|
||||
{
|
||||
time: '上午 12:00',
|
||||
status: 'rgb(103, 232, 207)',
|
||||
content: '向供应商支付了 ¥6495.00'
|
||||
},
|
||||
{
|
||||
time: '下午 14:30',
|
||||
status: 'rgb(255, 193, 7)',
|
||||
content: '促销活动开始',
|
||||
code: 'PROMO-2023'
|
||||
},
|
||||
{
|
||||
time: '下午 15:45',
|
||||
status: 'rgb(255, 105, 105)',
|
||||
content: '订单取消提醒',
|
||||
code: 'ORD-9876'
|
||||
},
|
||||
{
|
||||
time: '下午 17:00',
|
||||
status: 'rgb(103, 232, 207)',
|
||||
content: '完成日销售报表'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
41
src/views/dashboard/ecommerce/widget/SalesClassification.vue
Normal file
41
src/views/dashboard/ecommerce/widget/SalesClassification.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div class="card art-custom-card sales-card" style="height: 26rem">
|
||||
<div class="card-header">
|
||||
<p class="title">销售分类</p>
|
||||
<p class="subtitle">按产品类别</p>
|
||||
</div>
|
||||
<ArtRingChart
|
||||
:data="[
|
||||
{ value: 30, name: '电子产品' },
|
||||
{ value: 55, name: '服装鞋包' },
|
||||
{ value: 36, name: '家居用品' }
|
||||
]"
|
||||
:color="['#4C87F3', '#EDF2FF', '#8BD8FC']"
|
||||
:radius="['70%', '80%']"
|
||||
height="16.5rem"
|
||||
:showLabel="false"
|
||||
:borderRadius="0"
|
||||
centerText="¥300,458"
|
||||
/>
|
||||
<div class="icon-text-widget">
|
||||
<div class="item">
|
||||
<div class="icon">
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>¥500,458</p>
|
||||
<span>总收入</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="icon">
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>¥130,580</p>
|
||||
<span>净利润</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
20
src/views/dashboard/ecommerce/widget/SalesGrowth.vue
Normal file
20
src/views/dashboard/ecommerce/widget/SalesGrowth.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="card art-custom-card" style="height: 11rem">
|
||||
<div class="card-header">
|
||||
<p class="title" style="font-size: 24px"
|
||||
>12%<i class="iconfont-sys text-success"></i></p
|
||||
>
|
||||
<p class="subtitle">增长</p>
|
||||
</div>
|
||||
|
||||
<ArtLineChart
|
||||
:showAreaColor="true"
|
||||
:showAxisLabel="false"
|
||||
:showAxisLine="false"
|
||||
:showSplitLine="false"
|
||||
:data="[50, 85, 65, 95, 75, 130, 180]"
|
||||
barWidth="16px"
|
||||
height="4rem"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
15
src/views/dashboard/ecommerce/widget/SalesTrend.vue
Normal file
15
src/views/dashboard/ecommerce/widget/SalesTrend.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="card art-custom-card" style="height: 26rem">
|
||||
<div class="card-header">
|
||||
<p class="title">销售趋势</p>
|
||||
<p class="subtitle">月度销售对比</p>
|
||||
</div>
|
||||
<ArtDualBarCompareChart
|
||||
:topData="[50, 80, 120, 90, 60]"
|
||||
:bottomData="[30, 60, 90, 70, 40]"
|
||||
:xAxisData="['一月', '二月', '三月', '四月', '五月']"
|
||||
height="18rem"
|
||||
:barWidth="16"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
20
src/views/dashboard/ecommerce/widget/TotalOrderVolume.vue
Normal file
20
src/views/dashboard/ecommerce/widget/TotalOrderVolume.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="card art-custom-card" style="height: 13.3rem">
|
||||
<div class="card-header">
|
||||
<p class="title" style="font-size: 24px">205,216</p>
|
||||
<p class="subtitle">总订单量</p>
|
||||
</div>
|
||||
<ArtRingChart
|
||||
:data="[
|
||||
{ value: 30, name: '已完成' },
|
||||
{ value: 25, name: '处理中' },
|
||||
{ value: 45, name: '待发货' }
|
||||
]"
|
||||
:color="['#4C87F3', '#93F1B4', '#8BD8FC']"
|
||||
:radius="['56%', '76%']"
|
||||
height="7rem"
|
||||
:showLabel="false"
|
||||
:borderRadius="0"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
16
src/views/dashboard/ecommerce/widget/TotalProducts.vue
Normal file
16
src/views/dashboard/ecommerce/widget/TotalProducts.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div class="card art-custom-card" style="height: 13.3rem">
|
||||
<div class="card-header">
|
||||
<p class="title" style="font-size: 24px">55,231</p>
|
||||
<p class="subtitle">商品总数</p>
|
||||
</div>
|
||||
<ArtBarChart
|
||||
:showAxisLabel="false"
|
||||
:showAxisLine="false"
|
||||
:showSplitLine="false"
|
||||
:data="[50, 80, 40, 90, 60, 70]"
|
||||
height="7rem"
|
||||
barWidth="18px"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
52
src/views/dashboard/ecommerce/widget/TransactionList.vue
Normal file
52
src/views/dashboard/ecommerce/widget/TransactionList.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<ArtDataListCard
|
||||
:maxCount="4"
|
||||
:list="dataList"
|
||||
title="最近活动"
|
||||
subtitle="订单处理状态"
|
||||
:showMoreButton="true"
|
||||
@more="handleMore"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const dataList = [
|
||||
{
|
||||
title: '新订单 #38291',
|
||||
status: '待处理',
|
||||
time: '5分钟',
|
||||
class: 'bg-primary',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
title: '退款申请 #12845',
|
||||
status: '处理中',
|
||||
time: '10分钟',
|
||||
class: 'bg-secondary',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
title: '客户投诉处理',
|
||||
status: '待处理',
|
||||
time: '15分钟',
|
||||
class: 'bg-warning',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
title: '库存不足提醒',
|
||||
status: '紧急',
|
||||
time: '20分钟',
|
||||
class: 'bg-danger',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
title: '订单 #29384 已发货',
|
||||
status: '已完成',
|
||||
time: '20分钟',
|
||||
class: 'bg-success',
|
||||
icon: ''
|
||||
}
|
||||
]
|
||||
|
||||
const handleMore = () => {}
|
||||
</script>
|
||||
2022
src/views/device-management/devices/index.vue
Normal file
2022
src/views/device-management/devices/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
15
src/views/exception/403/index.vue
Normal file
15
src/views/exception/403/index.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<ArtException
|
||||
:data="{
|
||||
title: '403',
|
||||
desc: $t('exceptionPage.403'),
|
||||
btnText: $t('exceptionPage.gohome'),
|
||||
imgUrl
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import imgUrl from '@imgs/state/403.png'
|
||||
defineOptions({ name: 'Exception403' })
|
||||
</script>
|
||||
15
src/views/exception/404/index.vue
Normal file
15
src/views/exception/404/index.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<ArtException
|
||||
:data="{
|
||||
title: '404',
|
||||
desc: $t('exceptionPage.404'),
|
||||
btnText: $t('exceptionPage.gohome'),
|
||||
imgUrl
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import imgUrl from '@imgs/state/404.png'
|
||||
defineOptions({ name: 'Exception404' })
|
||||
</script>
|
||||
15
src/views/exception/500/index.vue
Normal file
15
src/views/exception/500/index.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<ArtException
|
||||
:data="{
|
||||
title: '500',
|
||||
desc: $t('exceptionPage.500'),
|
||||
btnText: $t('exceptionPage.gohome'),
|
||||
imgUrl
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import imgUrl from '@imgs/state/500.png'
|
||||
defineOptions({ name: 'Exception500' })
|
||||
</script>
|
||||
210
src/views/finance/customer-account/index.vue
Normal file
210
src/views/finance/customer-account/index.vue
Normal file
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<ElCard shadow="never">
|
||||
<!-- 搜索栏 -->
|
||||
<ElForm :inline="true" :model="searchForm" class="search-form">
|
||||
<ElFormItem label="客户账号">
|
||||
<ElInput v-model="searchForm.accountNo" placeholder="请输入客户账号" clearable style="width: 200px" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="客户名称">
|
||||
<ElInput v-model="searchForm.customerName" placeholder="请输入客户名称" clearable style="width: 200px" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="客户类型">
|
||||
<ElSelect v-model="searchForm.customerType" placeholder="请选择" clearable style="width: 150px">
|
||||
<ElOption label="代理商" value="agent" />
|
||||
<ElOption label="企业客户" value="enterprise" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem>
|
||||
<ElButton type="primary" @click="handleSearch">查询</ElButton>
|
||||
<ElButton @click="handleReset">重置</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<ArtTable :data="tableData" index stripe>
|
||||
<template #default>
|
||||
<ElTableColumn label="客户账号" prop="accountNo" min-width="150" />
|
||||
<ElTableColumn label="客户名称" prop="customerName" min-width="150" />
|
||||
<ElTableColumn label="客户类型" prop="customerType" width="120">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.customerType === 'agent' ? 'success' : 'primary'">
|
||||
{{ scope.row.customerType === 'agent' ? '代理商' : '企业客户' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="佣金总额" prop="totalCommission" width="150">
|
||||
<template #default="scope"> ¥{{ scope.row.totalCommission.toFixed(2) }} </template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="可提现金额" prop="availableAmount" width="150">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-success)"> ¥{{ scope.row.availableAmount.toFixed(2) }} </span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="待入账金额" prop="pendingAmount" width="150">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-warning)"> ¥{{ scope.row.pendingAmount.toFixed(2) }} </span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="已提现金额" prop="withdrawnAmount" width="150">
|
||||
<template #default="scope"> ¥{{ scope.row.withdrawnAmount.toFixed(2) }} </template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="提现次数" prop="withdrawCount" width="100" />
|
||||
<ElTableColumn label="最后提现时间" prop="lastWithdrawTime" width="180" />
|
||||
<ElTableColumn label="操作" width="180" fixed="right">
|
||||
<template #default="scope">
|
||||
<ElButton link type="primary" @click="handleViewDetail(scope.row)">查看详情</ElButton>
|
||||
<ElButton link type="primary" @click="handleViewFlow(scope.row)">流水记录</ElButton>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 分页 -->
|
||||
<ElPagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
style="margin-top: 20px; justify-content: flex-end"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</ElCard>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<ElDialog v-model="detailDialogVisible" title="客户账号详情" width="800px" align-center>
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem label="客户账号">{{ currentRow?.accountNo }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="客户名称">{{ currentRow?.customerName }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="客户类型">
|
||||
<ElTag :type="currentRow?.customerType === 'agent' ? 'success' : 'primary'">
|
||||
{{ currentRow?.customerType === 'agent' ? '代理商' : '企业客户' }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="联系电话">{{ currentRow?.phone }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="佣金总额">¥{{ currentRow?.totalCommission.toFixed(2) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="可提现金额">
|
||||
<span style="color: var(--el-color-success)"> ¥{{ currentRow?.availableAmount.toFixed(2) }} </span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="待入账金额">
|
||||
<span style="color: var(--el-color-warning)"> ¥{{ currentRow?.pendingAmount.toFixed(2) }} </span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="已提现金额">¥{{ currentRow?.withdrawnAmount.toFixed(2) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="提现次数">{{ currentRow?.withdrawCount }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="最后提现时间">{{ currentRow?.lastWithdrawTime }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="注册时间">{{ currentRow?.createTime }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="备注" :span="2">{{ currentRow?.remark || '无' }}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'CustomerAccount' })
|
||||
|
||||
const searchForm = reactive({
|
||||
accountNo: '',
|
||||
customerName: '',
|
||||
customerType: ''
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 100
|
||||
})
|
||||
|
||||
const detailDialogVisible = ref(false)
|
||||
const currentRow = ref<any>(null)
|
||||
|
||||
// 模拟表格数据
|
||||
const tableData = ref([
|
||||
{
|
||||
id: '1',
|
||||
accountNo: 'ACC20260001',
|
||||
customerName: '深圳市科技有限公司',
|
||||
customerType: 'agent',
|
||||
phone: '13800138000',
|
||||
totalCommission: 158900.5,
|
||||
availableAmount: 58900.5,
|
||||
pendingAmount: 50000.0,
|
||||
withdrawnAmount: 50000.0,
|
||||
withdrawCount: 12,
|
||||
lastWithdrawTime: '2026-01-08 15:00:00',
|
||||
createTime: '2025-06-01 10:00:00',
|
||||
remark: '优质代理商'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
accountNo: 'ACC20260002',
|
||||
customerName: '广州智能设备公司',
|
||||
customerType: 'enterprise',
|
||||
phone: '13900139000',
|
||||
totalCommission: 89600.0,
|
||||
availableAmount: 35600.0,
|
||||
pendingAmount: 24000.0,
|
||||
withdrawnAmount: 30000.0,
|
||||
withdrawCount: 8,
|
||||
lastWithdrawTime: '2026-01-05 10:30:00',
|
||||
createTime: '2025-07-15 14:20:00',
|
||||
remark: ''
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
accountNo: 'ACC20260003',
|
||||
customerName: '北京物联网代理',
|
||||
customerType: 'agent',
|
||||
phone: '13700137000',
|
||||
totalCommission: 256700.0,
|
||||
availableAmount: 106700.0,
|
||||
pendingAmount: 80000.0,
|
||||
withdrawnAmount: 70000.0,
|
||||
withdrawCount: 15,
|
||||
lastWithdrawTime: '2026-01-09 09:15:00',
|
||||
createTime: '2025-05-10 09:00:00',
|
||||
remark: '金牌代理商'
|
||||
}
|
||||
])
|
||||
|
||||
const handleSearch = () => {
|
||||
ElMessage.success('查询成功')
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.accountNo = ''
|
||||
searchForm.customerName = ''
|
||||
searchForm.customerType = ''
|
||||
ElMessage.info('已重置')
|
||||
}
|
||||
|
||||
const handleViewDetail = (row: any) => {
|
||||
currentRow.value = row
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleViewFlow = (row: any) => {
|
||||
ElMessage.info(`查看 ${row.customerName} 的流水记录`)
|
||||
}
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.pageSize = size
|
||||
ElMessage.info(`每页显示 ${size} 条`)
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page
|
||||
ElMessage.info(`当前第 ${page} 页`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
305
src/views/finance/my-account/index.vue
Normal file
305
src/views/finance/my-account/index.vue
Normal file
@@ -0,0 +1,305 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<!-- 统计卡片 -->
|
||||
<ElRow :gutter="20" style="margin-bottom: 20px">
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%)">
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">佣金总额</div>
|
||||
<div class="stat-value">¥{{ accountInfo.totalCommission.toFixed(2) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%)">
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">可提现金额</div>
|
||||
<div class="stat-value">¥{{ accountInfo.availableAmount.toFixed(2) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)">
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">待入账金额</div>
|
||||
<div class="stat-value">¥{{ accountInfo.pendingAmount.toFixed(2) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%)">
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">已提现金额</div>
|
||||
<div class="stat-value">¥{{ accountInfo.withdrawnAmount.toFixed(2) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<ElRow style="margin-bottom: 20px">
|
||||
<ElButton type="primary" @click="showWithdrawDialog">申请提现</ElButton>
|
||||
<ElButton @click="viewWithdrawHistory">提现记录</ElButton>
|
||||
</ElRow>
|
||||
|
||||
<!-- 收支流水 -->
|
||||
<ElCard shadow="never">
|
||||
<template #header>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center">
|
||||
<span style="font-weight: 500">收支流水</span>
|
||||
<ElRadioGroup v-model="flowType" size="small">
|
||||
<ElRadioButton value="all">全部</ElRadioButton>
|
||||
<ElRadioButton value="income">收入</ElRadioButton>
|
||||
<ElRadioButton value="withdraw">提现</ElRadioButton>
|
||||
</ElRadioGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ArtTable :data="filteredFlowData" index max-height="500">
|
||||
<template #default>
|
||||
<ElTableColumn label="流水号" prop="flowNo" min-width="180" />
|
||||
<ElTableColumn label="类型" prop="type">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.type === 'income' ? 'success' : 'warning'">
|
||||
{{ scope.row.type === 'income' ? '收入' : '提现' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="金额" prop="amount">
|
||||
<template #default="scope">
|
||||
<span :style="{ color: scope.row.type === 'income' ? 'var(--el-color-success)' : 'var(--el-color-danger)' }">
|
||||
{{ scope.row.type === 'income' ? '+' : '-' }}¥{{ scope.row.amount.toFixed(2) }}
|
||||
</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="余额" prop="balance">
|
||||
<template #default="scope"> ¥{{ scope.row.balance.toFixed(2) }} </template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="说明" prop="description" show-overflow-tooltip />
|
||||
<ElTableColumn label="时间" prop="createTime" width="180" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 提现申请对话框 -->
|
||||
<ElDialog v-model="withdrawDialogVisible" title="申请提现" width="600px" align-center>
|
||||
<ElForm ref="withdrawFormRef" :model="withdrawForm" :rules="withdrawRules" label-width="120px">
|
||||
<ElFormItem label="可提现金额">
|
||||
<span style="color: var(--el-color-success); font-size: 20px; font-weight: 500">
|
||||
¥{{ accountInfo.availableAmount.toFixed(2) }}
|
||||
</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="提现金额" prop="amount">
|
||||
<ElInputNumber
|
||||
v-model="withdrawForm.amount"
|
||||
:min="1"
|
||||
:max="accountInfo.availableAmount"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="手续费">
|
||||
<span>¥{{ calculatedFee.toFixed(2) }}</span>
|
||||
<span style="margin-left: 8px; color: var(--el-text-color-secondary)">
|
||||
(费率: {{ feeRate * 100 }}%)
|
||||
</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="实际到账">
|
||||
<span style="color: var(--el-color-success); font-size: 18px; font-weight: 500">
|
||||
¥{{ actualAmount.toFixed(2) }}
|
||||
</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="收款银行" prop="bankName">
|
||||
<ElSelect v-model="withdrawForm.bankName" placeholder="请选择收款银行" style="width: 100%">
|
||||
<ElOption label="中国工商银行" value="工商银行" />
|
||||
<ElOption label="中国建设银行" value="建设银行" />
|
||||
<ElOption label="中国农业银行" value="农业银行" />
|
||||
<ElOption label="中国银行" value="中国银行" />
|
||||
<ElOption label="招商银行" value="招商银行" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="银行账户" prop="bankAccount">
|
||||
<ElInput v-model="withdrawForm.bankAccount" placeholder="请输入银行账户" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="开户姓名" prop="accountName">
|
||||
<ElInput v-model="withdrawForm.accountName" placeholder="请输入开户姓名" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="withdrawDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="submitWithdraw">提交申请</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'MyAccount' })
|
||||
|
||||
const accountInfo = reactive({
|
||||
totalCommission: 158900.5,
|
||||
availableAmount: 58900.5,
|
||||
pendingAmount: 50000.0,
|
||||
withdrawnAmount: 50000.0
|
||||
})
|
||||
|
||||
const flowType = ref('all')
|
||||
const withdrawDialogVisible = ref(false)
|
||||
const withdrawFormRef = ref<FormInstance>()
|
||||
const feeRate = 0.002 // 手续费率 0.2%
|
||||
|
||||
const withdrawForm = reactive({
|
||||
amount: 0,
|
||||
bankName: '',
|
||||
bankAccount: '',
|
||||
accountName: ''
|
||||
})
|
||||
|
||||
const withdrawRules = reactive<FormRules>({
|
||||
amount: [
|
||||
{ required: true, message: '请输入提现金额', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (value > accountInfo.availableAmount) {
|
||||
callback(new Error('提现金额不能大于可提现金额'))
|
||||
} else if (value < 1) {
|
||||
callback(new Error('提现金额不能小于1元'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
bankName: [{ required: true, message: '请选择收款银行', trigger: 'change' }],
|
||||
bankAccount: [{ required: true, message: '请输入银行账户', trigger: 'blur' }],
|
||||
accountName: [{ required: true, message: '请输入开户姓名', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const flowData = ref([
|
||||
{
|
||||
id: '1',
|
||||
flowNo: 'FL202601090001',
|
||||
type: 'income',
|
||||
amount: 1580.0,
|
||||
balance: 58900.5,
|
||||
description: '套餐销售佣金',
|
||||
createTime: '2026-01-09 10:30:00'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
flowNo: 'FL202601080001',
|
||||
type: 'withdraw',
|
||||
amount: 5000.0,
|
||||
balance: 57320.5,
|
||||
description: '提现到账',
|
||||
createTime: '2026-01-08 15:00:00'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
flowNo: 'FL202601070001',
|
||||
type: 'income',
|
||||
amount: 2360.0,
|
||||
balance: 62320.5,
|
||||
description: '号卡分配佣金',
|
||||
createTime: '2026-01-07 14:20:00'
|
||||
}
|
||||
])
|
||||
|
||||
const calculatedFee = computed(() => {
|
||||
return withdrawForm.amount * feeRate
|
||||
})
|
||||
|
||||
const actualAmount = computed(() => {
|
||||
return withdrawForm.amount - calculatedFee.value
|
||||
})
|
||||
|
||||
const filteredFlowData = computed(() => {
|
||||
if (flowType.value === 'all') return flowData.value
|
||||
return flowData.value.filter((item) => item.type === flowType.value)
|
||||
})
|
||||
|
||||
const showWithdrawDialog = () => {
|
||||
withdrawForm.amount = 0
|
||||
withdrawForm.bankName = ''
|
||||
withdrawForm.bankAccount = ''
|
||||
withdrawForm.accountName = ''
|
||||
withdrawDialogVisible.value = true
|
||||
}
|
||||
|
||||
const submitWithdraw = async () => {
|
||||
if (!withdrawFormRef.value) return
|
||||
await withdrawFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
ElMessage.success('提现申请提交成功,请等待审核')
|
||||
withdrawDialogVisible.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const viewWithdrawHistory = () => {
|
||||
ElMessage.info('查看提现记录')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
.stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.stat-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
220
src/views/finance/withdrawal-settings/index.vue
Normal file
220
src/views/finance/withdrawal-settings/index.vue
Normal file
@@ -0,0 +1,220 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<ElCard shadow="never">
|
||||
<template #header>
|
||||
<span style="font-weight: 500">提现参数配置</span>
|
||||
</template>
|
||||
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="150px" style="max-width: 800px">
|
||||
<ElFormItem label="最低提现金额" prop="minAmount">
|
||||
<ElInputNumber v-model="form.minAmount" :min="1" :precision="2" />
|
||||
<span style="margin-left: 8px">元</span>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="手续费模式" prop="feeMode">
|
||||
<ElRadioGroup v-model="form.feeMode">
|
||||
<ElRadio value="fixed">固定手续费</ElRadio>
|
||||
<ElRadio value="percent">比例手续费</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="form.feeMode === 'fixed'" label="固定手续费" prop="fixedFee">
|
||||
<ElInputNumber v-model="form.fixedFee" :min="0" :precision="2" />
|
||||
<span style="margin-left: 8px">元/笔</span>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="form.feeMode === 'percent'" label="手续费比例" prop="feePercent">
|
||||
<ElInputNumber v-model="form.feePercent" :min="0" :max="100" :precision="2" />
|
||||
<span style="margin-left: 8px">%</span>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="单日提现次数" prop="dailyLimit">
|
||||
<ElInputNumber v-model="form.dailyLimit" :min="1" :max="10" />
|
||||
<span style="margin-left: 8px">次</span>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="提现到账时间" prop="arrivalTime">
|
||||
<ElSelect v-model="form.arrivalTime" style="width: 200px">
|
||||
<ElOption label="实时到账" value="realtime" />
|
||||
<ElOption label="2小时内到账" value="2hours" />
|
||||
<ElOption label="24小时内到账" value="24hours" />
|
||||
<ElOption label="T+1到账" value="t1" />
|
||||
<ElOption label="T+3到账" value="t3" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="工作日提现" prop="workdayOnly">
|
||||
<ElSwitch v-model="form.workdayOnly" />
|
||||
<span style="margin-left: 8px; color: var(--el-text-color-secondary)">
|
||||
{{ form.workdayOnly ? '仅工作日可提现' : '每天都可提现' }}
|
||||
</span>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="提现时间段" prop="timeRange">
|
||||
<ElTimePicker
|
||||
v-model="form.timeRange"
|
||||
is-range
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
format="HH:mm"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="配置说明" prop="description">
|
||||
<ElInput
|
||||
v-model="form.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入配置说明,如配置生效时间等"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem>
|
||||
<ElButton type="primary" @click="handleSave">保存配置</ElButton>
|
||||
<ElButton @click="resetForm">重置</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</ElCard>
|
||||
|
||||
<ElCard shadow="never" style="margin-top: 20px">
|
||||
<template #header>
|
||||
<span style="font-weight: 500">配置历史记录</span>
|
||||
</template>
|
||||
|
||||
<ArtTable :data="historyData" index>
|
||||
<template #default>
|
||||
<ElTableColumn label="配置时间" prop="configTime" width="180" />
|
||||
<ElTableColumn label="最低金额" prop="minAmount">
|
||||
<template #default="scope"> ¥{{ scope.row.minAmount.toFixed(2) }} </template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="手续费" prop="fee">
|
||||
<template #default="scope">
|
||||
{{
|
||||
scope.row.feeMode === 'fixed'
|
||||
? `¥${scope.row.fixedFee.toFixed(2)}/笔`
|
||||
: `${scope.row.feePercent}%`
|
||||
}}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="单日限制" prop="dailyLimit">
|
||||
<template #default="scope"> {{ scope.row.dailyLimit }}次/天 </template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="到账时间" prop="arrivalTime">
|
||||
<template #default="scope">
|
||||
{{ getArrivalTimeText(scope.row.arrivalTime) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="配置人" prop="operator" />
|
||||
<ElTableColumn label="状态" prop="status">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status === 'active' ? 'success' : 'info'">
|
||||
{{ scope.row.status === 'active' ? '当前生效' : '已过期' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'WithdrawalSettings' })
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const form = reactive({
|
||||
minAmount: 100,
|
||||
feeMode: 'percent',
|
||||
fixedFee: 2,
|
||||
feePercent: 0.2,
|
||||
dailyLimit: 3,
|
||||
arrivalTime: '24hours',
|
||||
workdayOnly: false,
|
||||
timeRange: [new Date(2024, 0, 1, 0, 0), new Date(2024, 0, 1, 23, 59)],
|
||||
description: ''
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
minAmount: [{ required: true, message: '请输入最低提现金额', trigger: 'blur' }],
|
||||
feeMode: [{ required: true, message: '请选择手续费模式', trigger: 'change' }],
|
||||
dailyLimit: [{ required: true, message: '请输入单日提现次数', trigger: 'blur' }],
|
||||
arrivalTime: [{ required: true, message: '请选择到账时间', trigger: 'change' }]
|
||||
})
|
||||
|
||||
const historyData = ref([
|
||||
{
|
||||
id: '1',
|
||||
configTime: '2026-01-09 10:00:00',
|
||||
minAmount: 100,
|
||||
feeMode: 'percent',
|
||||
fixedFee: 0,
|
||||
feePercent: 0.2,
|
||||
dailyLimit: 3,
|
||||
arrivalTime: '24hours',
|
||||
operator: 'admin',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
configTime: '2026-01-01 10:00:00',
|
||||
minAmount: 50,
|
||||
feeMode: 'fixed',
|
||||
fixedFee: 2.0,
|
||||
feePercent: 0,
|
||||
dailyLimit: 5,
|
||||
arrivalTime: 't1',
|
||||
operator: 'admin',
|
||||
status: 'expired'
|
||||
}
|
||||
])
|
||||
|
||||
const getArrivalTimeText = (value: string) => {
|
||||
const map: Record<string, string> = {
|
||||
realtime: '实时到账',
|
||||
'2hours': '2小时内',
|
||||
'24hours': '24小时内',
|
||||
t1: 'T+1',
|
||||
t3: 'T+3'
|
||||
}
|
||||
return map[value] || '未知'
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
// 添加到历史记录
|
||||
historyData.value.unshift({
|
||||
id: Date.now().toString(),
|
||||
configTime: new Date().toLocaleString('zh-CN'),
|
||||
...form,
|
||||
operator: 'admin',
|
||||
status: 'active'
|
||||
})
|
||||
// 将之前的配置标记为过期
|
||||
historyData.value.slice(1).forEach((item) => {
|
||||
item.status = 'expired'
|
||||
})
|
||||
ElMessage.success('配置保存成功')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
:deep(.el-card__header) {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--el-border-color-light);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
351
src/views/finance/withdrawal/index.vue
Normal file
351
src/views/finance/withdrawal/index.vue
Normal file
@@ -0,0 +1,351 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<ElRow>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElInput v-model="searchQuery" placeholder="申请人/手机号" clearable></ElInput>
|
||||
</ElCol>
|
||||
<div style="width: 12px"></div>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElSelect v-model="statusFilter" placeholder="审核状态" clearable style="width: 100%">
|
||||
<ElOption label="待审核" value="pending" />
|
||||
<ElOption label="已通过" value="approved" />
|
||||
<ElOption label="已拒绝" value="rejected" />
|
||||
</ElSelect>
|
||||
</ElCol>
|
||||
<div style="width: 12px"></div>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElDatePicker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElCol>
|
||||
<div style="width: 12px"></div>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<ElButton v-ripple @click="handleSearch">搜索</ElButton>
|
||||
<ElButton v-ripple @click="handleBatchApprove" :disabled="selectedIds.length === 0"
|
||||
>批量审核</ElButton
|
||||
>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ArtTable :data="filteredData" index @selection-change="handleSelectionChange">
|
||||
<template #default>
|
||||
<ElTableColumn type="selection" width="55" />
|
||||
<ElTableColumn label="申请单号" prop="orderNo" min-width="180" />
|
||||
<ElTableColumn label="申请人" prop="applicantName" />
|
||||
<ElTableColumn label="客户类型" prop="customerType">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.customerType === 'agent' ? '' : 'success'">
|
||||
{{ scope.row.customerType === 'agent' ? '代理商' : '企业客户' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="提现金额" prop="amount">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-danger); font-weight: 500">
|
||||
¥{{ scope.row.amount.toFixed(2) }}
|
||||
</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="手续费" prop="fee">
|
||||
<template #default="scope"> ¥{{ scope.row.fee.toFixed(2) }} </template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="实际到账" prop="actualAmount">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-success); font-weight: 500">
|
||||
¥{{ scope.row.actualAmount.toFixed(2) }}
|
||||
</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="收款账户" prop="bankAccount" show-overflow-tooltip />
|
||||
<ElTableColumn label="状态" prop="status">
|
||||
<template #default="scope">
|
||||
<ElTag :type="getStatusTagType(scope.row.status)">
|
||||
{{ getStatusText(scope.row.status) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="申请时间" prop="createTime" width="180" />
|
||||
<ElTableColumn fixed="right" label="操作" width="180">
|
||||
<template #default="scope">
|
||||
<el-button link @click="viewDetail(scope.row)">详情</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.status === 'pending'"
|
||||
link
|
||||
type="success"
|
||||
@click="handleApprove(scope.row)"
|
||||
>通过</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="scope.row.status === 'pending'"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleReject(scope.row)"
|
||||
>拒绝</el-button
|
||||
>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<ElDialog v-model="detailDialogVisible" title="提现申请详情" width="700px" align-center>
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem label="申请单号">{{ currentItem?.orderNo }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="申请人">{{ currentItem?.applicantName }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="手机号">{{ currentItem?.phone }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="客户类型">
|
||||
{{ currentItem?.customerType === 'agent' ? '代理商' : '企业客户' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="提现金额">
|
||||
<span style="color: var(--el-color-danger); font-weight: 500">
|
||||
¥{{ currentItem?.amount.toFixed(2) }}
|
||||
</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="手续费">¥{{ currentItem?.fee.toFixed(2) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="实际到账">
|
||||
<span style="color: var(--el-color-success); font-weight: 500">
|
||||
¥{{ currentItem?.actualAmount.toFixed(2) }}
|
||||
</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="收款银行">{{ currentItem?.bankName }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="收款账户" :span="2">{{
|
||||
currentItem?.bankAccount
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="开户姓名">{{ currentItem?.accountName }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="申请时间">{{ currentItem?.createTime }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="审核状态">
|
||||
<ElTag :type="getStatusTagType(currentItem?.status || 'pending')">
|
||||
{{ getStatusText(currentItem?.status || 'pending') }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem v-if="currentItem?.status !== 'pending'" label="审核时间">{{
|
||||
currentItem?.auditTime
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem v-if="currentItem?.rejectReason" label="拒绝原因" :span="2">{{
|
||||
currentItem?.rejectReason
|
||||
}}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 拒绝对话框 -->
|
||||
<ElDialog v-model="rejectDialogVisible" title="拒绝提现" width="500px" align-center>
|
||||
<ElForm ref="rejectFormRef" :model="rejectForm" :rules="rejectRules" label-width="100px">
|
||||
<ElFormItem label="拒绝原因" prop="reason">
|
||||
<ElInput
|
||||
v-model="rejectForm.reason"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入拒绝原因"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="rejectDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="confirmReject">确认拒绝</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'WithdrawalManagement' })
|
||||
|
||||
interface WithdrawalItem {
|
||||
id: string
|
||||
orderNo: string
|
||||
applicantName: string
|
||||
phone: string
|
||||
customerType: 'agent' | 'enterprise'
|
||||
amount: number
|
||||
fee: number
|
||||
actualAmount: number
|
||||
bankName: string
|
||||
bankAccount: string
|
||||
accountName: string
|
||||
status: 'pending' | 'approved' | 'rejected'
|
||||
createTime: string
|
||||
auditTime?: string
|
||||
rejectReason?: string
|
||||
}
|
||||
|
||||
const mockData = ref<WithdrawalItem[]>([
|
||||
{
|
||||
id: '1',
|
||||
orderNo: 'WD202601090001',
|
||||
applicantName: '张三',
|
||||
phone: '13800138001',
|
||||
customerType: 'agent',
|
||||
amount: 5000.0,
|
||||
fee: 10.0,
|
||||
actualAmount: 4990.0,
|
||||
bankName: '中国工商银行',
|
||||
bankAccount: '6222 **** **** 1234',
|
||||
accountName: '张三',
|
||||
status: 'pending',
|
||||
createTime: '2026-01-09 10:00:00'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
orderNo: 'WD202601090002',
|
||||
applicantName: '李四',
|
||||
phone: '13800138002',
|
||||
customerType: 'enterprise',
|
||||
amount: 3000.0,
|
||||
fee: 6.0,
|
||||
actualAmount: 2994.0,
|
||||
bankName: '中国建设银行',
|
||||
bankAccount: '6227 **** **** 5678',
|
||||
accountName: '李四',
|
||||
status: 'approved',
|
||||
createTime: '2026-01-08 14:30:00',
|
||||
auditTime: '2026-01-08 15:00:00'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
orderNo: 'WD202601090003',
|
||||
applicantName: '王五',
|
||||
phone: '13800138003',
|
||||
customerType: 'agent',
|
||||
amount: 2000.0,
|
||||
fee: 4.0,
|
||||
actualAmount: 1996.0,
|
||||
bankName: '中国农业银行',
|
||||
bankAccount: '6228 **** **** 9012',
|
||||
accountName: '王五',
|
||||
status: 'rejected',
|
||||
createTime: '2026-01-07 16:20:00',
|
||||
auditTime: '2026-01-07 17:00:00',
|
||||
rejectReason: '账户信息与实名不符'
|
||||
}
|
||||
])
|
||||
|
||||
const searchQuery = ref('')
|
||||
const statusFilter = ref('')
|
||||
const dateRange = ref<[Date, Date] | null>(null)
|
||||
const selectedIds = ref<string[]>([])
|
||||
const detailDialogVisible = ref(false)
|
||||
const rejectDialogVisible = ref(false)
|
||||
const currentItem = ref<WithdrawalItem | null>(null)
|
||||
const rejectFormRef = ref<FormInstance>()
|
||||
|
||||
const rejectForm = reactive({
|
||||
reason: ''
|
||||
})
|
||||
|
||||
const rejectRules = reactive<FormRules>({
|
||||
reason: [{ required: true, message: '请输入拒绝原因', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const filteredData = computed(() => {
|
||||
let data = mockData.value
|
||||
if (searchQuery.value) {
|
||||
data = data.filter(
|
||||
(item) => item.applicantName.includes(searchQuery.value) || item.phone.includes(searchQuery.value)
|
||||
)
|
||||
}
|
||||
if (statusFilter.value) {
|
||||
data = data.filter((item) => item.status === statusFilter.value)
|
||||
}
|
||||
return data
|
||||
})
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
pending: '待审核',
|
||||
approved: '已通过',
|
||||
rejected: '已拒绝'
|
||||
}
|
||||
return map[status] || '未知'
|
||||
}
|
||||
|
||||
const getStatusTagType = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
pending: 'warning',
|
||||
approved: 'success',
|
||||
rejected: 'danger'
|
||||
}
|
||||
return map[status] || 'info'
|
||||
}
|
||||
|
||||
const handleSearch = () => {}
|
||||
|
||||
const handleSelectionChange = (selection: WithdrawalItem[]) => {
|
||||
selectedIds.value = selection.map((item) => item.id)
|
||||
}
|
||||
|
||||
const viewDetail = (row: WithdrawalItem) => {
|
||||
currentItem.value = row
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleApprove = (row: WithdrawalItem) => {
|
||||
ElMessageBox.confirm(
|
||||
`确认通过提现申请?金额:¥${row.amount.toFixed(2)},实际到账:¥${row.actualAmount.toFixed(2)}`,
|
||||
'审核确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'success'
|
||||
}
|
||||
).then(() => {
|
||||
row.status = 'approved'
|
||||
row.auditTime = new Date().toLocaleString('zh-CN')
|
||||
ElMessage.success('审核通过')
|
||||
})
|
||||
}
|
||||
|
||||
const handleReject = (row: WithdrawalItem) => {
|
||||
currentItem.value = row
|
||||
rejectForm.reason = ''
|
||||
rejectDialogVisible.value = true
|
||||
}
|
||||
|
||||
const confirmReject = async () => {
|
||||
if (!rejectFormRef.value) return
|
||||
await rejectFormRef.value.validate((valid) => {
|
||||
if (valid && currentItem.value) {
|
||||
currentItem.value.status = 'rejected'
|
||||
currentItem.value.auditTime = new Date().toLocaleString('zh-CN')
|
||||
currentItem.value.rejectReason = rejectForm.reason
|
||||
rejectDialogVisible.value = false
|
||||
ElMessage.success('已拒绝提现申请')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleBatchApprove = () => {
|
||||
ElMessageBox.confirm(`确认批量审核 ${selectedIds.value.length} 条提现申请吗?`, '批量审核', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
selectedIds.value.forEach((id) => {
|
||||
const item = mockData.value.find((i) => i.id === id)
|
||||
if (item && item.status === 'pending') {
|
||||
item.status = 'approved'
|
||||
item.auditTime = new Date().toLocaleString('zh-CN')
|
||||
}
|
||||
})
|
||||
ElMessage.success('批量审核成功')
|
||||
selectedIds.value = []
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
:deep(.el-descriptions__label) {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
26
src/views/index/index.vue
Normal file
26
src/views/index/index.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<ArtLayouts>
|
||||
<!-- 顶栏、水平/混合菜单 -->
|
||||
<ArtHeaderBar />
|
||||
<!-- 左侧/双列菜单 -->
|
||||
<ArtSidebarMenu />
|
||||
<!-- 页面内容 -->
|
||||
<ArtPageContent />
|
||||
<!-- 设置面板 -->
|
||||
<ArtSettingsPanel />
|
||||
<!-- 全局搜索 -->
|
||||
<ArtGlobalSearch />
|
||||
<!-- 屏幕锁定 -->
|
||||
<ArtScreenLock />
|
||||
<!-- 聊天窗口 -->
|
||||
<ArtChatWindow />
|
||||
<!-- 礼花效果 -->
|
||||
<ArtFireworksEffect />
|
||||
<!-- 水印效果 -->
|
||||
<ArtWatermark />
|
||||
</ArtLayouts>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use './style';
|
||||
</style>
|
||||
47
src/views/index/style.scss
Normal file
47
src/views/index/style.scss
Normal file
@@ -0,0 +1,47 @@
|
||||
@use '@/assets/styles/variables' as *;
|
||||
|
||||
.layouts {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
background: var(--art-bg-color);
|
||||
transition: padding 0.3s ease-in-out;
|
||||
|
||||
.layout-content {
|
||||
box-sizing: border-box;
|
||||
width: calc(100% - 40px);
|
||||
margin: auto;
|
||||
|
||||
// 子页面默认style
|
||||
:deep(.page-content) {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
background: var(--art-main-bg-color);
|
||||
border-radius: calc(var(--custom-radius) / 2 + 2px) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $device-ipad) {
|
||||
.layouts {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
padding-left: 0 !important;
|
||||
overflow-y: scroll;
|
||||
|
||||
.layout-content {
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $device-phone) {
|
||||
.layouts {
|
||||
.layout-content {
|
||||
width: calc(100% - 32px);
|
||||
}
|
||||
}
|
||||
}
|
||||
867
src/views/my-simcard/single-card/index.vue
Normal file
867
src/views/my-simcard/single-card/index.vue
Normal file
@@ -0,0 +1,867 @@
|
||||
<template>
|
||||
<div class="single-card-page">
|
||||
<!-- 卡片内容区域 -->
|
||||
<div v-if="cardInfo" class="card-content-area slide-in">
|
||||
<!-- 主要内容区域 -->
|
||||
<div class="main-content-layout">
|
||||
<!-- 第一行:流量统计 -->
|
||||
<div class="row full-width">
|
||||
<ElCard shadow="never" class="info-card traffic-info">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>流量统计</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 流量使用情况 -->
|
||||
<div class="traffic-overview horizontal">
|
||||
<!-- 左侧:主要流量指标 -->
|
||||
<div class="traffic-left">
|
||||
<div class="traffic-stats-grid">
|
||||
<ElCard shadow="never" class="stat-card">
|
||||
<div class="stat-label">套餐系列</div>
|
||||
<div class="stat-value">{{ cardInfo.packageSeries || '--' }}</div>
|
||||
</ElCard>
|
||||
<ElCard shadow="never" class="stat-card">
|
||||
<div class="stat-label">套餐总流量</div>
|
||||
<div class="stat-value">{{ cardInfo.packageTotalFlow || '--' }}</div>
|
||||
</ElCard>
|
||||
<ElCard shadow="never" class="stat-card">
|
||||
<div class="stat-label">已使用流量</div>
|
||||
<div class="stat-value">{{ cardInfo.usedFlow || '--' }}</div>
|
||||
</ElCard>
|
||||
<ElCard shadow="never" class="stat-card">
|
||||
<div class="stat-label">已使用流量(真)</div>
|
||||
<div class="stat-value">{{ cardInfo.realUsedFlow || '--' }}</div>
|
||||
</ElCard>
|
||||
<ElCard shadow="never" class="stat-card">
|
||||
<div class="stat-label">实际流量</div>
|
||||
<div class="stat-value">{{ cardInfo.actualFlow || '--' }}</div>
|
||||
</ElCard>
|
||||
<ElCard shadow="never" class="stat-card">
|
||||
<div class="stat-label">剩余流量</div>
|
||||
<div class="stat-value">{{ cardInfo.remainFlow || '--' }}</div>
|
||||
</ElCard>
|
||||
<ElCard shadow="never" class="stat-card">
|
||||
<div class="stat-label">已使用流量百分比</div>
|
||||
<div class="stat-value">{{ cardInfo.usedFlowPercentage || '未设置' }}</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<!-- 第二行:设备信息和业务信息 -->
|
||||
<div class="row two-columns">
|
||||
<!-- 左侧:设备信息 -->
|
||||
<div class="col">
|
||||
<ElCard shadow="never" class="info-card basic-info">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>设备信息</span>
|
||||
</div>
|
||||
</template>
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem label="ICCID">{{ cardInfo?.iccid || '--' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="接入号">{{
|
||||
cardInfo?.accessNumber || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="IMEI">{{ cardInfo?.imei || '--' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="网卡状态">
|
||||
<ElTag :type="getStatusType(cardInfo?.cardStatus)" size="small">
|
||||
{{ cardInfo?.cardStatus || '--' }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="状态">{{
|
||||
cardInfo?.operatorStatus || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="卡类型">{{
|
||||
cardInfo?.cardType || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="虚拟号">{{
|
||||
cardInfo?.virtualNumber || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="手机绑定">{{
|
||||
cardInfo?.phoneBind || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="过期时间">{{
|
||||
cardInfo?.expireTime || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:业务信息 -->
|
||||
<div class="col">
|
||||
<ElCard shadow="never" class="info-card business-info">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>业务信息</span>
|
||||
</div>
|
||||
</template>
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem label="运营商">{{
|
||||
cardInfo?.operator || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="运营商实名">{{
|
||||
cardInfo?.operatorRealName || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="国政通实名">
|
||||
<ElTag :type="cardInfo?.realNameAuth ? 'success' : 'danger'" size="small">
|
||||
{{ cardInfo?.realNameAuth ? '是' : '否' }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="供应商">{{
|
||||
cardInfo?.supplier || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="代理商">{{
|
||||
cardInfo?.agent || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="流量池子">{{
|
||||
cardInfo?.trafficPool || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="钱包余额">{{
|
||||
cardInfo?.walletBalance || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="钱包支付密码状态">{{
|
||||
cardInfo?.walletPasswordStatus || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="导入时间">{{
|
||||
cardInfo?.importTime || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</ElCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第三行:当前套餐 -->
|
||||
<div class="row full-width">
|
||||
<ElCard shadow="never" class="info-card package-info">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>当前套餐</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="package-table-wrapper">
|
||||
<ElTable :data="cardInfo.packageList || []" class="package-table">
|
||||
<ElTableColumn
|
||||
prop="packageName"
|
||||
label="套餐名称"
|
||||
min-width="200"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<ElTableColumn prop="packageType" label="类型" width="100" />
|
||||
<ElTableColumn prop="totalFlow" label="总流量" width="100" />
|
||||
<ElTableColumn prop="usedFlow" label="已用" width="100" />
|
||||
<ElTableColumn prop="remainFlow" label="剩余" width="100" />
|
||||
<ElTableColumn prop="expireTime" label="到期时间" width="120" />
|
||||
<ElTableColumn prop="status" label="状态" width="100">
|
||||
<template #default="scope">
|
||||
<ElTag :type="getPackageStatusType(scope.row.status)" size="small">
|
||||
{{ scope.row.status }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<!-- 第四行:常规操作 -->
|
||||
<div class="row two-columns">
|
||||
<!-- 左侧操作 -->
|
||||
<div class="col">
|
||||
<ElCard shadow="never" class="info-card operation-card">
|
||||
<div class="operations-grid">
|
||||
<!-- 主要操作 -->
|
||||
<div class="operation-group primary-operations">
|
||||
<h4 class="group-title">主要操作</h4>
|
||||
<div class="operation-buttons">
|
||||
<ElButton @click="handleOperation('recharge')" class="operation-btn">
|
||||
套餐充值
|
||||
</ElButton>
|
||||
<ElButton @click="handleOperation('activate')" class="operation-btn">
|
||||
激活
|
||||
</ElButton>
|
||||
<ElButton @click="handleOperation('suspend')" class="operation-btn">
|
||||
保号停机
|
||||
</ElButton>
|
||||
<ElButton @click="handleOperation('resume')" class="operation-btn">
|
||||
保号复机
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 管理操作 -->
|
||||
<div class="operation-group management-operations">
|
||||
<h4 class="group-title">管理操作</h4>
|
||||
<div class="operation-buttons">
|
||||
<ElButton @click="handleOperation('rebind')" class="operation-btn"
|
||||
>机卡重绑</ElButton
|
||||
>
|
||||
<ElButton @click="handleOperation('changeExpire')" class="operation-btn"
|
||||
>更改过期时间</ElButton
|
||||
>
|
||||
<ElButton @click="handleOperation('transferCard')" class="operation-btn"
|
||||
>转新卡</ElButton
|
||||
>
|
||||
<ElButton @click="handleOperation('adjustTraffic')" class="operation-btn"
|
||||
>增减流量</ElButton
|
||||
>
|
||||
<ElButton @click="handleOperation('speedLimit')" class="operation-btn"
|
||||
>单卡限速</ElButton
|
||||
>
|
||||
<ElButton @click="handleOperation('instantLimit')" class="operation-btn"
|
||||
>即时限速</ElButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<!-- 右侧操作 -->
|
||||
<div class="col">
|
||||
<ElCard shadow="never" class="info-card operation-card">
|
||||
<div class="operations-grid">
|
||||
<!-- 查询操作 -->
|
||||
<div class="operation-group query-operations">
|
||||
<h4 class="group-title">查询记录</h4>
|
||||
<div class="operation-buttons">
|
||||
<ElButton @click="handleOperation('trafficDetail')" class="operation-btn"
|
||||
>流量详单</ElButton
|
||||
>
|
||||
<ElButton @click="handleOperation('suspendRecord')" class="operation-btn"
|
||||
>停复机记录</ElButton
|
||||
>
|
||||
<ElButton @click="handleOperation('orderHistory')" class="operation-btn"
|
||||
>往期订单</ElButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 其他操作 -->
|
||||
<div class="operation-group other-operations">
|
||||
<h4 class="group-title">其他操作</h4>
|
||||
<div class="operation-buttons">
|
||||
<ElButton @click="handleOperation('changeBalance')" class="operation-btn"
|
||||
>变更钱包余额</ElButton
|
||||
>
|
||||
<ElButton @click="handleOperation('resetPassword')" class="operation-btn"
|
||||
>重置支付密码</ElButton
|
||||
>
|
||||
<ElButton @click="handleOperation('renewRecharge')" class="operation-btn"
|
||||
>续充</ElButton
|
||||
>
|
||||
<ElButton @click="handleOperation('deviceOperation')" class="operation-btn"
|
||||
>设备操作</ElButton
|
||||
>
|
||||
<ElButton @click="handleOperation('recoverFromRoaming')" class="operation-btn"
|
||||
>窜卡复机</ElButton
|
||||
>
|
||||
<ElButton @click="handleOperation('roaming')" class="operation-btn"
|
||||
>窜卡</ElButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<!--<div v-else-if="loading" class="loading-state">-->
|
||||
<!-- <ElSkeleton :rows="10" animated />-->
|
||||
<!--</div>-->
|
||||
|
||||
<!-- 空状态 -->
|
||||
<!--<div v-else class="empty-state">-->
|
||||
<!-- <ElEmpty description="暂无卡片数据" />-->
|
||||
<!--</div>-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ElTag,
|
||||
ElMessage,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElProgress,
|
||||
ElEmpty,
|
||||
ElSkeleton,
|
||||
ElDescriptions,
|
||||
ElDescriptionsItem
|
||||
} from 'element-plus'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
defineOptions({ name: 'SingleCard' })
|
||||
|
||||
const route = useRoute()
|
||||
const loading = ref(true)
|
||||
|
||||
// 卡片信息
|
||||
const cardInfo = ref<any>(null)
|
||||
|
||||
// 模拟卡片数据
|
||||
const mockCardData = {
|
||||
iccid: '8986062357007989203',
|
||||
accessNumber: '1440012345678',
|
||||
imei: '860123456789012',
|
||||
expireTime: '2025-12-31',
|
||||
operator: '中国联通',
|
||||
cardStatus: '正常',
|
||||
cardType: '流量卡',
|
||||
supplier: '华为技术有限公司',
|
||||
importTime: '2024-01-15 10:30:00',
|
||||
phoneBind: '138****5678',
|
||||
trafficPool: '全国流量池',
|
||||
agent: '张丽丽',
|
||||
operatorStatus: '激活',
|
||||
operatorRealName: '已实名',
|
||||
walletBalance: '50.00元',
|
||||
walletPasswordStatus: '已设置',
|
||||
realNameAuth: true,
|
||||
virtualNumber: '10655****1234',
|
||||
// 流量信息 - 根据提供的数据更新
|
||||
packageSeries: 'UFI设备',
|
||||
packageTotalFlow: '3072000MB', // 套餐总流量
|
||||
usedFlow: '196.16MB', // 已使用流量
|
||||
remainFlow: '3071803.84MB', // 剩余流量
|
||||
usedFlowPercentage: '未设置', // 增加已使用流量百分比
|
||||
realUsedFlow: '196.16MB', // 已使用流量(真)
|
||||
actualFlow: '196.16MB', // 实际流量
|
||||
packageList: [
|
||||
{
|
||||
packageName: '随意联畅玩年卡套餐(12个月)',
|
||||
packageType: '年卡套餐',
|
||||
totalFlow: '3072000MB',
|
||||
usedFlow: '196.16MB',
|
||||
remainFlow: '3071803.84MB',
|
||||
expireTime: '2026-11-07',
|
||||
status: '正常'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 获取卡片详情
|
||||
const fetchCardDetail = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
// 模拟API调用
|
||||
setTimeout(() => {
|
||||
cardInfo.value = { ...mockCardData }
|
||||
loading.value = false
|
||||
}, 500)
|
||||
} catch (error) {
|
||||
ElMessage.error('获取卡片详情失败')
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 页面初始化时加载数据
|
||||
onMounted(() => {
|
||||
fetchCardDetail()
|
||||
})
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case '正常':
|
||||
case '激活':
|
||||
return 'success'
|
||||
case '停机':
|
||||
case '暂停':
|
||||
return 'warning'
|
||||
case '注销':
|
||||
case '异常':
|
||||
return 'danger'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取套餐状态标签类型
|
||||
const getPackageStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case '正常':
|
||||
case '生效':
|
||||
return 'success'
|
||||
case '未生效':
|
||||
return 'warning'
|
||||
case '已过期':
|
||||
return 'danger'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取进度条颜色
|
||||
const getProgressColor = (percentage: number) => {
|
||||
if (percentage < 50) return '#67c23a'
|
||||
if (percentage < 80) return '#e6a23c'
|
||||
return '#f56c6c'
|
||||
}
|
||||
|
||||
// 处理刷新操作
|
||||
const handleRefresh = () => {
|
||||
fetchCardDetail()
|
||||
}
|
||||
|
||||
// 处理操作按钮点击
|
||||
const handleOperation = (operation: string) => {
|
||||
if (!cardInfo.value) {
|
||||
ElMessage.warning('请先查询卡片信息')
|
||||
return
|
||||
}
|
||||
|
||||
const operationNames: Record<string, string> = {
|
||||
recharge: '套餐充值',
|
||||
activate: '激活',
|
||||
resume: '保号复机',
|
||||
suspend: '保号停机',
|
||||
rebind: '机卡重绑',
|
||||
trafficDetail: '流量详单',
|
||||
changeExpire: '更改过期时间',
|
||||
transferCard: '转新卡',
|
||||
suspendRecord: '停复机记录',
|
||||
orderHistory: '往期订单',
|
||||
speedLimit: '单卡限速',
|
||||
recoverFromRoaming: '窜卡复机',
|
||||
roaming: '窜卡',
|
||||
adjustTraffic: '增减流量',
|
||||
changeBalance: '变更钱包余额',
|
||||
resetPassword: '重置支付密码',
|
||||
instantLimit: '即时限速',
|
||||
renewRecharge: '续充',
|
||||
deviceOperation: '设备操作'
|
||||
}
|
||||
|
||||
ElMessage.info(`执行${operationNames[operation] || operation}操作`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.single-card-page {
|
||||
padding: 20px 0;
|
||||
// 卡片内容区域
|
||||
.card-content-area {
|
||||
&.slide-in {
|
||||
animation: slideInUp 0.6s ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
// 主内容布局
|
||||
.main-content-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
|
||||
// 行布局
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
|
||||
&.full-width {
|
||||
.info-card {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.two-columns {
|
||||
.col {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.info-card {
|
||||
margin-bottom: 0;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式调整
|
||||
@media (max-width: 1200px) {
|
||||
.row.two-columns {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.col {
|
||||
.info-card {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
gap: 16px;
|
||||
|
||||
.row {
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 信息卡片通用样式
|
||||
.info-card {
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-card__header) {
|
||||
padding: 20px 24px 16px;
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary, #1f2937);
|
||||
|
||||
i {
|
||||
font-size: 18px;
|
||||
color: #667eea;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
// 流量信息卡片
|
||||
.traffic-info {
|
||||
// 流量概览
|
||||
.traffic-overview {
|
||||
&.horizontal {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
// 左侧:流量统计网格
|
||||
.traffic-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.traffic-stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 12px;
|
||||
|
||||
.stat-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px 16px;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-regular);
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
line-height: 1.2;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 中间:使用率显示
|
||||
.traffic-center {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.usage-display {
|
||||
background: var(--el-bg-color, #ffffff);
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 12px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
|
||||
.usage-title {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
margin-bottom: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.usage-chart {
|
||||
.chart-value {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #667eea;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 右侧:套餐信息
|
||||
.traffic-right {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.package-display {
|
||||
background: var(--el-bg-color, #ffffff);
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 12px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
|
||||
.package-label {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.package-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.package-actual {
|
||||
font-size: 14px;
|
||||
color: #667eea;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 套餐信息卡片
|
||||
.package-info {
|
||||
.package-table-wrapper {
|
||||
.package-table {
|
||||
:deep(.el-table__header) {
|
||||
th {
|
||||
color: var(--el-text-color-primary, #374151);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 操作卡片
|
||||
.operation-card {
|
||||
:deep(.el-card__body) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.operations-grid {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.operation-group {
|
||||
margin-bottom: 24px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary, #374151);
|
||||
margin: 0 0 16px 0;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 2px solid var(--el-border-color-light, #e5e7eb);
|
||||
}
|
||||
|
||||
.operation-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
.operation-btn {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载和空状态
|
||||
.loading-state,
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--el-bg-color, #ffffff);
|
||||
border-radius: 16px;
|
||||
margin: 24px auto;
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
padding: 16px;
|
||||
|
||||
.main-content-layout {
|
||||
gap: 16px;
|
||||
|
||||
.row {
|
||||
gap: 16px;
|
||||
|
||||
&.two-columns {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-card {
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-card__header) {
|
||||
padding: 16px 16px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.traffic-info .traffic-overview {
|
||||
&.horizontal {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.traffic-left .traffic-stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
|
||||
.stat-card {
|
||||
padding: 12px 8px;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.traffic-center .usage-display {
|
||||
padding: 16px;
|
||||
|
||||
.usage-chart .chart-value {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.traffic-right .package-display {
|
||||
padding: 16px;
|
||||
|
||||
.package-name {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.package-actual {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//.operation-card .operations-grid .operation-group .operation-buttons {
|
||||
// grid-template-columns: 1fr;
|
||||
//}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.traffic-left .traffic-stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 6px;
|
||||
|
||||
.stat-card {
|
||||
padding: 10px 6px;
|
||||
|
||||
.stat-label {
|
||||
font-size: 11px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 动画效果
|
||||
@keyframes slideInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载动画
|
||||
.loading-state {
|
||||
:deep(.el-skeleton) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// 深色模式特定样式
|
||||
html.dark & {
|
||||
background: var(--el-bg-color-page);
|
||||
|
||||
.traffic-details .info-item {
|
||||
background: var(--el-fill-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
47
src/views/outside/Iframe.vue
Normal file
47
src/views/outside/Iframe.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div class="iframe-container" v-loading="isLoading">
|
||||
<iframe
|
||||
ref="iframeRef"
|
||||
:src="iframeUrl"
|
||||
frameborder="0"
|
||||
class="iframe-content"
|
||||
@load="handleIframeLoad"
|
||||
></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getIframeRoutes } from '@/router/utils/menuToRouter'
|
||||
|
||||
const route = useRoute()
|
||||
const isLoading = ref(true)
|
||||
const iframeUrl = ref('')
|
||||
const iframeRef = ref<HTMLIFrameElement | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
const iframeRoute = getIframeRoutes().find((item: any) => item.path === route.path)
|
||||
|
||||
if (iframeRoute?.meta) {
|
||||
iframeUrl.value = iframeRoute.meta.link || ''
|
||||
}
|
||||
})
|
||||
|
||||
const handleIframeLoad = () => {
|
||||
isLoading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.iframe-container {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.iframe-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: calc(100vh - 120px);
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
611
src/views/package-management/package-assign/index.vue
Normal file
611
src/views/package-management/package-assign/index.vue
Normal file
@@ -0,0 +1,611 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="package-assign-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton type="primary" @click="handleSearch">查询</ElButton>
|
||||
<ElButton type="success" @click="showAssignDialog">分配套餐</ElButton>
|
||||
<ElButton @click="exportExcel">导出excel</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 分配套餐对话框 -->
|
||||
<ElDialog
|
||||
v-model="assignDialogVisible"
|
||||
title="分配套餐"
|
||||
width="600px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm
|
||||
ref="assignFormRef"
|
||||
:model="assignFormData"
|
||||
:rules="assignRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<ElFormItem label="分销商" prop="distributor">
|
||||
<ElSelect
|
||||
v-model="assignFormData.distributor"
|
||||
placeholder="请选择分销商"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<ElOption label="北京优享科技有限公司" value="beijing_youxiang" />
|
||||
<ElOption label="上海智联通信技术公司" value="shanghai_zhilian" />
|
||||
<ElOption label="广州物联网络有限公司" value="guangzhou_wulian" />
|
||||
<ElOption label="深圳云联科技股份公司" value="shenzhen_yunlian" />
|
||||
<ElOption label="杭州通达网络服务公司" value="hangzhou_tongda" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="套餐系列" prop="packageSeries">
|
||||
<ElSelect
|
||||
v-model="assignFormData.packageSeries"
|
||||
placeholder="请选择套餐系列"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<ElOption label="畅玩系列" value="changwan" />
|
||||
<ElOption label="如意系列" value="ruyi" />
|
||||
<ElOption label="NB专享" value="nb_special" />
|
||||
<ElOption label="大流量系列" value="big_data" />
|
||||
<ElOption label="广电系列" value="gdtv" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="套餐名称" prop="packageName">
|
||||
<ElSelect
|
||||
v-model="assignFormData.packageName"
|
||||
placeholder="请选择套餐名称"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<ElOption label="随意联畅玩年卡套餐" value="changwan_yearly" />
|
||||
<ElOption label="如意包年3G流量包" value="ruyi_3g" />
|
||||
<ElOption label="Y-NB专享套餐" value="nb_special_package" />
|
||||
<ElOption label="100G全国流量月卡套餐" value="big_data_100g" />
|
||||
<ElOption label="广电飞悦卡无预存50G" value="gdtv_50g" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="分销金额" prop="distributionAmount">
|
||||
<ElInputNumber
|
||||
v-model="assignFormData.distributionAmount"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
:precision="2"
|
||||
placeholder="请输入分销金额"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="分销比例" prop="distributionRatio">
|
||||
<ElInputNumber
|
||||
v-model="assignFormData.distributionRatio"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:precision="1"
|
||||
placeholder="请输入分销比例"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<span style="margin-left: 8px">%</span>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="备注说明" prop="remark">
|
||||
<ElInput
|
||||
v-model="assignFormData.remark"
|
||||
type="textarea"
|
||||
placeholder="请输入备注说明(可选)"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="assignDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleAssignSubmit" :loading="assignLoading">
|
||||
确认分配
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ElTag, ElMessage, ElMessageBox, ElInputNumber } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
|
||||
defineOptions({ name: 'PackageAssign' })
|
||||
|
||||
const assignDialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const assignLoading = ref(false)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
distributor: '',
|
||||
assignStatus: '',
|
||||
packageName: '',
|
||||
packageSeries: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 分配表单实例
|
||||
const assignFormRef = ref<FormInstance>()
|
||||
|
||||
// 分配表单数据
|
||||
const assignFormData = reactive({
|
||||
distributor: '',
|
||||
packageSeries: '',
|
||||
packageName: '',
|
||||
distributionAmount: 0,
|
||||
distributionRatio: 0,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
packageSeries: '畅玩系列',
|
||||
packageName: '随意联畅玩年卡套餐',
|
||||
packageTraffic: 120,
|
||||
packageType: '年套餐',
|
||||
salesAmount: 168.0,
|
||||
distributionAmount: 150.0,
|
||||
distributionRatio: 10.7,
|
||||
costPrice: 120.0,
|
||||
assignStatus: '已分配',
|
||||
distributorName: '北京优享科技有限公司'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
packageSeries: '如意系列',
|
||||
packageName: '如意包年3G流量包',
|
||||
packageTraffic: 36,
|
||||
packageType: '年套餐',
|
||||
salesAmount: 98.0,
|
||||
distributionAmount: 85.0,
|
||||
distributionRatio: 13.3,
|
||||
costPrice: 80.0,
|
||||
assignStatus: '未分配',
|
||||
distributorName: ''
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
packageSeries: 'NB专享',
|
||||
packageName: 'Y-NB专享套餐',
|
||||
packageTraffic: 24,
|
||||
packageType: '月套餐',
|
||||
salesAmount: 25.0,
|
||||
distributionAmount: 22.0,
|
||||
distributionRatio: 12.0,
|
||||
costPrice: 20.0,
|
||||
assignStatus: '已分配',
|
||||
distributorName: '广州物联网络有限公司'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
packageSeries: '大流量系列',
|
||||
packageName: '100G全国流量月卡套餐',
|
||||
packageTraffic: 100,
|
||||
packageType: '月套餐',
|
||||
salesAmount: 59.0,
|
||||
distributionAmount: 50.0,
|
||||
distributionRatio: 15.3,
|
||||
costPrice: 45.0,
|
||||
assignStatus: '已分配',
|
||||
distributorName: '深圳云联科技股份公司'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
packageSeries: '广电系列',
|
||||
packageName: '广电飞悦卡无预存50G',
|
||||
packageTraffic: 50,
|
||||
packageType: '月套餐',
|
||||
salesAmount: 39.0,
|
||||
distributionAmount: 33.0,
|
||||
distributionRatio: 15.4,
|
||||
costPrice: 30.0,
|
||||
assignStatus: '未分配',
|
||||
distributorName: ''
|
||||
}
|
||||
]
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1
|
||||
getPackageAssignList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1
|
||||
getPackageAssignList()
|
||||
}
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '分销商',
|
||||
prop: 'distributor',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '北京优享科技有限公司', value: 'beijing_youxiang' },
|
||||
{ label: '上海智联通信技术公司', value: 'shanghai_zhilian' },
|
||||
{ label: '广州物联网络有限公司', value: 'guangzhou_wulian' },
|
||||
{ label: '深圳云联科技股份公司', value: 'shenzhen_yunlian' },
|
||||
{ label: '杭州通达网络服务公司', value: 'hangzhou_tongda' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '分配状态',
|
||||
prop: 'assignStatus',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '已分配', value: 'assigned' },
|
||||
{ label: '未分配', value: 'unassigned' },
|
||||
{ label: '分配中', value: 'assigning' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '套餐名称',
|
||||
prop: 'packageName',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入套餐名称'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '套餐系列',
|
||||
prop: 'packageSeries',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '畅玩系列', value: 'changwan' },
|
||||
{ label: '如意系列', value: 'ruyi' },
|
||||
{ label: 'NB专享', value: 'nb_special' },
|
||||
{ label: '大流量系列', value: 'big_data' },
|
||||
{ label: '广电系列', value: 'gdtv' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: '套餐系列', prop: 'packageSeries' },
|
||||
{ label: '套餐名称', prop: 'packageName' },
|
||||
{ label: '套餐流量(GB)', prop: 'packageTraffic' },
|
||||
{ label: '套餐类型', prop: 'packageType' },
|
||||
{ label: '套餐销售金额', prop: 'salesAmount' },
|
||||
{ label: '套餐分销金额', prop: 'distributionAmount' },
|
||||
{ label: '分销比例', prop: 'distributionRatio' },
|
||||
{ label: '成本价', prop: 'costPrice' },
|
||||
{ label: '分配状态', prop: 'assignStatus' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 获取分配状态标签类型
|
||||
const getAssignStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case '已分配':
|
||||
return 'success'
|
||||
case '未分配':
|
||||
return 'warning'
|
||||
case '分配中':
|
||||
return 'primary'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 导出Excel
|
||||
const exportExcel = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要导出的数据')
|
||||
return
|
||||
}
|
||||
ElMessage.success(`导出 ${selectedRows.value.length} 条套餐分配记录`)
|
||||
}
|
||||
|
||||
// 显示分配对话框
|
||||
const showAssignDialog = () => {
|
||||
assignDialogVisible.value = true
|
||||
// 重置表单
|
||||
if (assignFormRef.value) {
|
||||
assignFormRef.value.resetFields()
|
||||
}
|
||||
assignFormData.distributor = ''
|
||||
assignFormData.packageSeries = ''
|
||||
assignFormData.packageName = ''
|
||||
assignFormData.distributionAmount = 0
|
||||
assignFormData.distributionRatio = 0
|
||||
assignFormData.remark = ''
|
||||
}
|
||||
|
||||
// 编辑分配
|
||||
const editAssign = (row: any) => {
|
||||
ElMessage.info(`编辑分配: ${row.packageName}`)
|
||||
}
|
||||
|
||||
// 取消分配
|
||||
const cancelAssign = (row: any) => {
|
||||
ElMessageBox.confirm(`确定要取消分配套餐"${row.packageName}"吗?`, '取消分配确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('取消分配成功')
|
||||
getPackageAssignList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消操作')
|
||||
})
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'packageSeries',
|
||||
label: '套餐系列',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'packageName',
|
||||
label: '套餐名称',
|
||||
minWidth: 180
|
||||
},
|
||||
{
|
||||
prop: 'packageTraffic',
|
||||
label: '套餐流量(GB)',
|
||||
width: 120,
|
||||
formatter: (row) => `${row.packageTraffic}GB`
|
||||
},
|
||||
{
|
||||
prop: 'packageType',
|
||||
label: '套餐类型',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'salesAmount',
|
||||
label: '套餐销售金额',
|
||||
width: 120,
|
||||
formatter: (row) => `¥${row.salesAmount.toFixed(2)}`
|
||||
},
|
||||
{
|
||||
prop: 'distributionAmount',
|
||||
label: '套餐分销金额',
|
||||
width: 120,
|
||||
formatter: (row) => (row.distributionAmount ? `¥${row.distributionAmount.toFixed(2)}` : '-')
|
||||
},
|
||||
{
|
||||
prop: 'distributionRatio',
|
||||
label: '分销比例',
|
||||
width: 100,
|
||||
formatter: (row) => (row.distributionRatio ? `${row.distributionRatio}%` : '-')
|
||||
},
|
||||
{
|
||||
prop: 'costPrice',
|
||||
label: '成本价',
|
||||
width: 100,
|
||||
formatter: (row) => `¥${row.costPrice.toFixed(2)}`
|
||||
},
|
||||
{
|
||||
prop: 'assignStatus',
|
||||
label: '分配状态',
|
||||
width: 100,
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getAssignStatusType(row.assignStatus) }, () => row.assignStatus)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 180,
|
||||
formatter: (row: any) => {
|
||||
const buttons = []
|
||||
|
||||
if (row.assignStatus === '已分配') {
|
||||
buttons.push(
|
||||
h(ArtButtonTable, {
|
||||
text: '编辑',
|
||||
onClick: () => editAssign(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '取消分配',
|
||||
onClick: () => cancelAssign(row)
|
||||
})
|
||||
)
|
||||
} else {
|
||||
buttons.push(
|
||||
h(ArtButtonTable, {
|
||||
text: '分配',
|
||||
onClick: () => showAssignDialog()
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return h('div', { class: 'operation-buttons' }, buttons)
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getPackageAssignList()
|
||||
})
|
||||
|
||||
// 获取套餐分配列表
|
||||
const getPackageAssignList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
|
||||
const endIndex = startIndex + pagination.pageSize
|
||||
const paginatedData = mockData.slice(startIndex, endIndex)
|
||||
|
||||
tableData.value = paginatedData
|
||||
pagination.total = mockData.length
|
||||
loading.value = false
|
||||
} catch (error) {
|
||||
console.error('获取套餐分配列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getPackageAssignList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getPackageAssignList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getPackageAssignList()
|
||||
}
|
||||
|
||||
// 分配表单验证规则
|
||||
const assignRules = reactive<FormRules>({
|
||||
distributor: [{ required: true, message: '请选择分销商', trigger: 'change' }],
|
||||
packageSeries: [{ required: true, message: '请选择套餐系列', trigger: 'change' }],
|
||||
packageName: [{ required: true, message: '请选择套餐名称', trigger: 'change' }],
|
||||
distributionAmount: [{ required: true, message: '请输入分销金额', trigger: 'blur' }],
|
||||
distributionRatio: [{ required: true, message: '请输入分销比例', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
// 提交分配
|
||||
const handleAssignSubmit = async () => {
|
||||
if (!assignFormRef.value) return
|
||||
|
||||
await assignFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
assignLoading.value = true
|
||||
|
||||
// 模拟分配过程
|
||||
setTimeout(() => {
|
||||
ElMessage.success(
|
||||
`套餐分配成功!分销商:${assignFormData.distributor},套餐:${assignFormData.packageName}`
|
||||
)
|
||||
assignDialogVisible.value = false
|
||||
assignLoading.value = false
|
||||
getPackageAssignList()
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.package-assign-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
:deep(.operation-buttons) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
523
src/views/package-management/package-batch/index.vue
Normal file
523
src/views/package-management/package-batch/index.vue
Normal file
@@ -0,0 +1,523 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="package-batch-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton type="primary" @click="handleSearch">查询</ElButton>
|
||||
<ElButton type="success" @click="showImportDialog">导入</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 导入对话框 -->
|
||||
<ElDialog
|
||||
v-model="importDialogVisible"
|
||||
title="批量导入套餐"
|
||||
width="500px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<!-- 顶部下载模板按钮 -->
|
||||
<div class="template-section">
|
||||
<ElButton type="primary" @click="downloadTemplate" icon="Download"> 下载模板 </ElButton>
|
||||
<span class="template-tip">请先下载模板,按模板格式填写后上传</span>
|
||||
</div>
|
||||
|
||||
<ElDivider />
|
||||
|
||||
<ElForm
|
||||
ref="importFormRef"
|
||||
:model="importFormData"
|
||||
:rules="importRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<ElFormItem label="导入类型" prop="importType">
|
||||
<ElSelect
|
||||
v-model="importFormData.importType"
|
||||
placeholder="请选择导入类型"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<ElOption label="套餐批量导入" value="package_batch" />
|
||||
<ElOption label="套餐更新导入" value="package_update" />
|
||||
<ElOption label="套餐停用导入" value="package_disable" />
|
||||
<ElOption label="套餐启用导入" value="package_enable" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="上传Excel文件" prop="excelFile">
|
||||
<ElUpload
|
||||
ref="uploadRef"
|
||||
:limit="1"
|
||||
:on-exceed="handleExceed"
|
||||
:before-upload="beforeUpload"
|
||||
:on-change="handleFileChange"
|
||||
:auto-upload="false"
|
||||
accept=".xlsx,.xls"
|
||||
drag
|
||||
>
|
||||
<ElIcon class="el-icon--upload"><UploadFilled /></ElIcon>
|
||||
<div class="el-upload__text"> 将文件拖到此处,或<em>点击上传</em> </div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip"> 只能上传 xlsx/xls 文件,且不超过 10MB </div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="备注说明" prop="remark">
|
||||
<ElInput
|
||||
v-model="importFormData.remark"
|
||||
type="textarea"
|
||||
placeholder="请输入备注说明(可选)"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="importDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleImportSubmit" :loading="importLoading">
|
||||
确认导入
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ElTag, ElMessage, ElUpload, ElIcon, ElDivider } from 'element-plus'
|
||||
import { UploadFilled, Download } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, UploadInstance, UploadRawFile, UploadFile } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
|
||||
defineOptions({ name: 'PackageBatch' })
|
||||
|
||||
const importDialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const importLoading = ref(false)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
importType: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 导入表单实例
|
||||
const importFormRef = ref<FormInstance>()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
|
||||
// 导入表单数据
|
||||
const importFormData = reactive({
|
||||
importType: '',
|
||||
excelFile: null as File | null,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
importTotal: 1000,
|
||||
success: 980,
|
||||
failed: 20,
|
||||
importType: '套餐批量导入',
|
||||
operator: '张若暄',
|
||||
operationTime: '2025-11-08 10:30:00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
importTotal: 500,
|
||||
success: 500,
|
||||
failed: 0,
|
||||
importType: '套餐更新导入',
|
||||
operator: '孔丽娟',
|
||||
operationTime: '2025-11-07 14:15:00'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
importTotal: 300,
|
||||
success: 285,
|
||||
failed: 15,
|
||||
importType: '套餐停用导入',
|
||||
operator: '李佳音',
|
||||
operationTime: '2025-11-06 09:45:00'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
importTotal: 800,
|
||||
success: 795,
|
||||
failed: 5,
|
||||
importType: '套餐启用导入',
|
||||
operator: '赵强',
|
||||
operationTime: '2025-11-05 16:20:00'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
importTotal: 150,
|
||||
success: 145,
|
||||
failed: 5,
|
||||
importType: '套餐批量导入',
|
||||
operator: '张若暄',
|
||||
operationTime: '2025-11-04 11:30:00'
|
||||
}
|
||||
]
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1
|
||||
getBatchImportList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1
|
||||
getBatchImportList()
|
||||
}
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '导入类型',
|
||||
prop: 'importType',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '套餐批量导入', value: 'package_batch' },
|
||||
{ label: '套餐更新导入', value: 'package_update' },
|
||||
{ label: '套餐停用导入', value: 'package_disable' },
|
||||
{ label: '套餐启用导入', value: 'package_enable' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: '导入总数', prop: 'importTotal' },
|
||||
{ label: '成功', prop: 'success' },
|
||||
{ label: '失败', prop: 'failed' },
|
||||
{ label: '导入类型', prop: 'importType' },
|
||||
{ label: '操作人', prop: 'operator' },
|
||||
{ label: '操作时间', prop: 'operationTime' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 获取导入类型标签类型
|
||||
const getImportTypeTagType = (type: string) => {
|
||||
switch (type) {
|
||||
case '套餐批量导入':
|
||||
return 'primary'
|
||||
case '套餐更新导入':
|
||||
return 'success'
|
||||
case '套餐停用导入':
|
||||
return 'danger'
|
||||
case '套餐启用导入':
|
||||
return 'warning'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 显示导入对话框
|
||||
const showImportDialog = () => {
|
||||
importDialogVisible.value = true
|
||||
// 重置表单
|
||||
if (importFormRef.value) {
|
||||
importFormRef.value.resetFields()
|
||||
}
|
||||
importFormData.importType = ''
|
||||
importFormData.excelFile = null
|
||||
importFormData.remark = ''
|
||||
}
|
||||
|
||||
// 下载模板
|
||||
const downloadTemplate = () => {
|
||||
ElMessage.success('正在下载批量导入模板...')
|
||||
// 这里可以实现实际的模板下载功能
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetails = (row: any) => {
|
||||
ElMessage.info(`查看导入详情: ${row.importType} - 导入总数:${row.importTotal}`)
|
||||
}
|
||||
|
||||
// 重新导入
|
||||
const retryImport = (row: any) => {
|
||||
ElMessage.info(`重新导入: ${row.importType}`)
|
||||
}
|
||||
|
||||
// 删除记录
|
||||
const deleteRecord = (row: any) => {
|
||||
ElMessage.info(`删除导入记录: ${row.id}`)
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'importTotal',
|
||||
label: '导入总数',
|
||||
formatter: (row) => `${row.importTotal} `
|
||||
},
|
||||
{
|
||||
prop: 'success',
|
||||
label: '成功',
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: 'success' }, () => `${row.success}`)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'failed',
|
||||
label: '失败',
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: row.failed > 0 ? 'danger' : 'success' }, () => `${row.failed}`)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'importType',
|
||||
label: '导入类型',
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getImportTypeTagType(row.importType) }, () => row.importType)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'operator',
|
||||
label: '操作人'
|
||||
},
|
||||
{
|
||||
prop: 'operationTime',
|
||||
label: '操作时间'
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 220,
|
||||
formatter: (row: any) => {
|
||||
return h('div', { class: 'operation-buttons' }, [
|
||||
h(ArtButtonTable, {
|
||||
text: '查看',
|
||||
onClick: () => viewDetails(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '重试',
|
||||
onClick: () => retryImport(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '删除',
|
||||
onClick: () => deleteRecord(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getBatchImportList()
|
||||
})
|
||||
|
||||
// 获取批量导入列表
|
||||
const getBatchImportList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
|
||||
const endIndex = startIndex + pagination.pageSize
|
||||
const paginatedData = mockData.slice(startIndex, endIndex)
|
||||
|
||||
tableData.value = paginatedData
|
||||
pagination.total = mockData.length
|
||||
loading.value = false
|
||||
} catch (error) {
|
||||
console.error('获取批量导入列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getBatchImportList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getBatchImportList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getBatchImportList()
|
||||
}
|
||||
|
||||
// 文件上传限制
|
||||
const handleExceed = () => {
|
||||
ElMessage.warning('最多只能上传一个文件')
|
||||
}
|
||||
|
||||
// 文件上传前检查
|
||||
const beforeUpload = (file: UploadRawFile) => {
|
||||
const isExcel =
|
||||
file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
|
||||
file.type === 'application/vnd.ms-excel'
|
||||
const isLt10M = file.size / 1024 / 1024 < 10
|
||||
|
||||
if (!isExcel) {
|
||||
ElMessage.error('只能上传 Excel 文件!')
|
||||
return false
|
||||
}
|
||||
if (!isLt10M) {
|
||||
ElMessage.error('上传文件大小不能超过 10MB!')
|
||||
return false
|
||||
}
|
||||
return false // 阻止自动上传
|
||||
}
|
||||
|
||||
// 文件变化处理
|
||||
const handleFileChange = (file: UploadFile) => {
|
||||
if (file.raw) {
|
||||
importFormData.excelFile = file.raw
|
||||
}
|
||||
}
|
||||
|
||||
// 导入表单验证规则
|
||||
const importRules = reactive<FormRules>({
|
||||
importType: [{ required: true, message: '请选择导入类型', trigger: 'change' }],
|
||||
excelFile: [{ required: true, message: '请上传Excel文件', trigger: 'change' }]
|
||||
})
|
||||
|
||||
// 提交导入
|
||||
const handleImportSubmit = async () => {
|
||||
if (!importFormRef.value) return
|
||||
|
||||
// 检查文件是否上传
|
||||
if (!importFormData.excelFile) {
|
||||
ElMessage.error('请先上传Excel文件')
|
||||
return
|
||||
}
|
||||
|
||||
await importFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
importLoading.value = true
|
||||
|
||||
// 模拟导入过程
|
||||
setTimeout(() => {
|
||||
ElMessage.success(`${importFormData.importType}批量导入提交成功!`)
|
||||
importDialogVisible.value = false
|
||||
importLoading.value = false
|
||||
getBatchImportList()
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.package-batch-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
.template-section {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.template-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.operation-buttons) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.el-upload-dragger) {
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
585
src/views/package-management/package-change/index.vue
Normal file
585
src/views/package-management/package-change/index.vue
Normal file
@@ -0,0 +1,585 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="package-change-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton type="primary" @click="handleSearch">查询</ElButton>
|
||||
<ElButton @click="exportExcel">导出excel</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 套餐变更对话框 -->
|
||||
<ElDialog
|
||||
v-model="changeDialogVisible"
|
||||
title="套餐变更"
|
||||
width="600px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm
|
||||
ref="changeFormRef"
|
||||
:model="changeFormData"
|
||||
:rules="changeRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<ElFormItem label="当前套餐" prop="currentPackage">
|
||||
<ElInput
|
||||
v-model="changeFormData.currentPackage"
|
||||
placeholder="当前套餐"
|
||||
readonly
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="变更套餐" prop="newPackage">
|
||||
<ElSelect
|
||||
v-model="changeFormData.newPackage"
|
||||
placeholder="请选择要变更的套餐"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<ElOption label="随意联畅玩年卡套餐" value="changwan_yearly" />
|
||||
<ElOption label="如意包年3G流量包" value="ruyi_3g" />
|
||||
<ElOption label="Y-NB专享套餐" value="nb_special" />
|
||||
<ElOption label="100G全国流量月卡套餐" value="big_data_100g" />
|
||||
<ElOption label="广电飞悦卡无预存50G" value="gdtv_50g" />
|
||||
<ElOption label="联通大王卡" value="unicom_dawang" />
|
||||
<ElOption label="移动花卡宝藏版" value="mobile_huaka" />
|
||||
<ElOption label="电信星卡" value="telecom_star" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="变更原因" prop="changeReason">
|
||||
<ElSelect
|
||||
v-model="changeFormData.changeReason"
|
||||
placeholder="请选择变更原因"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<ElOption label="用户主动变更" value="user_request" />
|
||||
<ElOption label="套餐到期升级" value="package_upgrade" />
|
||||
<ElOption label="业务需求调整" value="business_adjustment" />
|
||||
<ElOption label="系统自动变更" value="system_auto" />
|
||||
<ElOption label="其他原因" value="other" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="备注说明" prop="remark">
|
||||
<ElInput
|
||||
v-model="changeFormData.remark"
|
||||
type="textarea"
|
||||
placeholder="请输入备注说明(可选)"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="changeDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleChangeSubmit" :loading="changeLoading">
|
||||
确认变更
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ElTag, ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
|
||||
defineOptions({ name: 'PackageChange' })
|
||||
|
||||
const changeDialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const changeLoading = ref(false)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
isDistribution: '',
|
||||
cardStatus: '',
|
||||
cardType: '',
|
||||
cardCompany: '',
|
||||
distributor: '',
|
||||
iccid: '',
|
||||
accessNumber: '',
|
||||
virtualNumber: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 套餐变更表单实例
|
||||
const changeFormRef = ref<FormInstance>()
|
||||
|
||||
// 套餐变更表单数据
|
||||
const changeFormData = reactive({
|
||||
currentPackage: '',
|
||||
newPackage: '',
|
||||
changeReason: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRowData = ref<any>({})
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
iccid: '89860621370079892035',
|
||||
accessNumber: '1440012345678',
|
||||
virtualNumber: '100001',
|
||||
cardCompany: '联通',
|
||||
expiryDate: '2025-12-31',
|
||||
packageName: '随意联畅玩年卡套餐',
|
||||
cardType: '2G/3G/4G',
|
||||
distributorName: '北京优享科技有限公司',
|
||||
cardStatus: '正常'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
iccid: '89860621370079892036',
|
||||
accessNumber: '1440012345679',
|
||||
virtualNumber: '100002',
|
||||
cardCompany: '移动',
|
||||
expiryDate: '2025-11-30',
|
||||
packageName: '如意包年3G流量包',
|
||||
cardType: 'NB-IoT',
|
||||
distributorName: '上海智联通信技术公司',
|
||||
cardStatus: '停机'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
iccid: '89860621370079892037',
|
||||
accessNumber: '1440012345680',
|
||||
virtualNumber: '100003',
|
||||
cardCompany: '电信',
|
||||
expiryDate: '2026-01-15',
|
||||
packageName: 'Y-NB专享套餐',
|
||||
cardType: '4G/5G',
|
||||
distributorName: '广州物联网络有限公司',
|
||||
cardStatus: '正常'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
iccid: '89860621370079892038',
|
||||
accessNumber: '1440012345681',
|
||||
virtualNumber: '100004',
|
||||
cardCompany: '联通',
|
||||
expiryDate: '2025-10-20',
|
||||
packageName: '100G全国流量月卡套餐',
|
||||
cardType: '2G/3G/4G',
|
||||
distributorName: '深圳云联科技股份公司',
|
||||
cardStatus: '欠费停机'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
iccid: '89860621370079892039',
|
||||
accessNumber: '1440012345682',
|
||||
virtualNumber: '100005',
|
||||
cardCompany: '广电',
|
||||
expiryDate: '2025-09-10',
|
||||
packageName: '广电飞悦卡无预存50G',
|
||||
cardType: '4G/5G',
|
||||
distributorName: '杭州通达网络服务公司',
|
||||
cardStatus: '正常'
|
||||
}
|
||||
]
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1
|
||||
getPackageChangeList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1
|
||||
getPackageChangeList()
|
||||
}
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '是否分销',
|
||||
prop: 'isDistribution',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '是', value: 'yes' },
|
||||
{ label: '否', value: 'no' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '卡状态',
|
||||
prop: 'cardStatus',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '正常', value: 'normal' },
|
||||
{ label: '停机', value: 'suspended' },
|
||||
{ label: '欠费停机', value: 'overdue_suspended' },
|
||||
{ label: '已销户', value: 'closed' },
|
||||
{ label: '测试期', value: 'testing' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '卡片类型',
|
||||
prop: 'cardType',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '2G/3G/4G', value: '2g_3g_4g' },
|
||||
{ label: '4G/5G', value: '4g_5g' },
|
||||
{ label: 'NB-IoT', value: 'nb_iot' },
|
||||
{ label: 'eMTC', value: 'emtc' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '开卡公司',
|
||||
prop: 'cardCompany',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '联通', value: 'unicom' },
|
||||
{ label: '移动', value: 'mobile' },
|
||||
{ label: '电信', value: 'telecom' },
|
||||
{ label: '广电', value: 'gdtv' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '分销商',
|
||||
prop: 'distributor',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入分销商'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: 'ICCID号',
|
||||
prop: 'iccid',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入ICCID号'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '接入号',
|
||||
prop: 'accessNumber',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入接入号'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '虚拟号',
|
||||
prop: 'virtualNumber',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入虚拟号'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: 'ICCID号', prop: 'iccid' },
|
||||
{ label: '接入号码', prop: 'accessNumber' },
|
||||
{ label: '虚拟号', prop: 'virtualNumber' },
|
||||
{ label: '开卡公司', prop: 'cardCompany' },
|
||||
{ label: '到期时间', prop: 'expiryDate' },
|
||||
{ label: '套餐', prop: 'packageName' },
|
||||
{ label: '卡片类型', prop: 'cardType' },
|
||||
{ label: '分销商姓名', prop: 'distributorName' },
|
||||
{ label: '卡状态', prop: 'cardStatus' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 获取卡状态标签类型
|
||||
const getCardStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case '正常':
|
||||
return 'success'
|
||||
case '停机':
|
||||
return 'warning'
|
||||
case '欠费停机':
|
||||
return 'danger'
|
||||
case '已销户':
|
||||
return 'info'
|
||||
case '测试期':
|
||||
return 'primary'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 导出Excel
|
||||
const exportExcel = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要导出的数据')
|
||||
return
|
||||
}
|
||||
ElMessage.success(`导出 ${selectedRows.value.length} 条套餐变更记录`)
|
||||
}
|
||||
|
||||
// 显示套餐变更对话框
|
||||
const showChangeDialog = (row: any) => {
|
||||
changeDialogVisible.value = true
|
||||
selectedRowData.value = row
|
||||
// 重置表单
|
||||
if (changeFormRef.value) {
|
||||
changeFormRef.value.resetFields()
|
||||
}
|
||||
changeFormData.currentPackage = row.packageName
|
||||
changeFormData.newPackage = ''
|
||||
changeFormData.changeReason = ''
|
||||
changeFormData.remark = ''
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'iccid',
|
||||
label: 'ICCID号',
|
||||
minWidth: 180
|
||||
},
|
||||
{
|
||||
prop: 'accessNumber',
|
||||
label: '接入号码',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
prop: 'virtualNumber',
|
||||
label: '虚拟号',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'cardCompany',
|
||||
label: '开卡公司',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'expiryDate',
|
||||
label: '到期时间',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'packageName',
|
||||
label: '套餐',
|
||||
minWidth: 180
|
||||
},
|
||||
{
|
||||
prop: 'cardType',
|
||||
label: '卡片类型',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'distributorName',
|
||||
label: '分销商姓名',
|
||||
minWidth: 180
|
||||
},
|
||||
{
|
||||
prop: 'cardStatus',
|
||||
label: '卡状态',
|
||||
width: 100,
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getCardStatusType(row.cardStatus) }, () => row.cardStatus)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 120,
|
||||
formatter: (row: any) => {
|
||||
return h('div', { class: 'operation-buttons' }, [
|
||||
h(ArtButtonTable, {
|
||||
text: '套餐变更',
|
||||
onClick: () => showChangeDialog(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getPackageChangeList()
|
||||
})
|
||||
|
||||
// 获取套餐变更列表
|
||||
const getPackageChangeList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
|
||||
const endIndex = startIndex + pagination.pageSize
|
||||
const paginatedData = mockData.slice(startIndex, endIndex)
|
||||
|
||||
tableData.value = paginatedData
|
||||
pagination.total = mockData.length
|
||||
loading.value = false
|
||||
} catch (error) {
|
||||
console.error('获取套餐变更列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getPackageChangeList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getPackageChangeList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getPackageChangeList()
|
||||
}
|
||||
|
||||
// 套餐变更表单验证规则
|
||||
const changeRules = reactive<FormRules>({
|
||||
newPackage: [{ required: true, message: '请选择要变更的套餐', trigger: 'change' }],
|
||||
changeReason: [{ required: true, message: '请选择变更原因', trigger: 'change' }]
|
||||
})
|
||||
|
||||
// 提交套餐变更
|
||||
const handleChangeSubmit = async () => {
|
||||
if (!changeFormRef.value) return
|
||||
|
||||
await changeFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
changeLoading.value = true
|
||||
|
||||
// 模拟变更过程
|
||||
setTimeout(() => {
|
||||
ElMessage.success(
|
||||
`套餐变更成功!ICCID:${selectedRowData.value.iccid},从${changeFormData.currentPackage}变更为${changeFormData.newPackage}`
|
||||
)
|
||||
changeDialogVisible.value = false
|
||||
changeLoading.value = false
|
||||
getPackageChangeList()
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.package-change-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
:deep(.operation-buttons) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
452
src/views/package-management/package-commission/index.vue
Normal file
452
src/views/package-management/package-commission/index.vue
Normal file
@@ -0,0 +1,452 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="package-commission-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton type="primary" @click="handleSearch">搜索</ElButton>
|
||||
<ElButton type="success" @click="showAddDialog">新增</ElButton>
|
||||
<ElButton @click="exportExcel">导出excel</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 新增套餐佣金网卡对话框 -->
|
||||
<ElDialog
|
||||
v-model="addDialogVisible"
|
||||
title="新增套餐佣金网卡"
|
||||
width="500px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm ref="addFormRef" :model="addFormData" :rules="addRules" label-width="120px">
|
||||
<ElFormItem label="卡号" prop="cardNumber">
|
||||
<ElInput v-model="addFormData.cardNumber" placeholder="请输入卡号" clearable />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="月份" prop="month">
|
||||
<ElDatePicker
|
||||
v-model="addFormData.month"
|
||||
type="month"
|
||||
placeholder="请选择月份"
|
||||
style="width: 100%"
|
||||
format="YYYY-MM"
|
||||
value-format="YYYY-MM"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="addDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleAddSubmit" :loading="addLoading">
|
||||
确认新增
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ElTag, ElMessage, ElMessageBox, ElDatePicker } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
|
||||
defineOptions({ name: 'PackageCommission' })
|
||||
|
||||
const addDialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const addLoading = ref(false)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
cardNumber: '',
|
||||
virtualNumber: '',
|
||||
createDateRange: []
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 新增表单实例
|
||||
const addFormRef = ref<FormInstance>()
|
||||
|
||||
// 新增表单数据
|
||||
const addFormData = reactive({
|
||||
cardNumber: '',
|
||||
month: ''
|
||||
})
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
cardNumber: '89860621370079892035',
|
||||
accessNumber: '1440012345678',
|
||||
virtualNumber: '100001',
|
||||
cardCompany: '联通',
|
||||
month: '2025-11',
|
||||
createTime: '2025-11-08 10:30:00',
|
||||
creator: '张若暄'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
cardNumber: '89860621370079892036',
|
||||
accessNumber: '1440012345679',
|
||||
virtualNumber: '100002',
|
||||
cardCompany: '移动',
|
||||
month: '2025-11',
|
||||
createTime: '2025-11-07 14:15:00',
|
||||
creator: '孔丽娟'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
cardNumber: '89860621370079892037',
|
||||
accessNumber: '1440012345680',
|
||||
virtualNumber: '100003',
|
||||
cardCompany: '电信',
|
||||
month: '2025-11',
|
||||
createTime: '2025-11-06 09:45:00',
|
||||
creator: '李佳音'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
cardNumber: '89860621370079892038',
|
||||
accessNumber: '1440012345681',
|
||||
virtualNumber: '100004',
|
||||
cardCompany: '联通',
|
||||
month: '2025-10',
|
||||
createTime: '2025-10-20 16:20:00',
|
||||
creator: '赵强'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
cardNumber: '89860621370079892039',
|
||||
accessNumber: '1440012345682',
|
||||
virtualNumber: '100005',
|
||||
cardCompany: '广电',
|
||||
month: '2025-10',
|
||||
createTime: '2025-10-15 11:30:00',
|
||||
creator: '张若暄'
|
||||
}
|
||||
]
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1
|
||||
getCommissionCardList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1
|
||||
getCommissionCardList()
|
||||
}
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '卡号',
|
||||
prop: 'cardNumber',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入卡号'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '虚拟号',
|
||||
prop: 'virtualNumber',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入虚拟号'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'createDateRange',
|
||||
type: 'daterange',
|
||||
config: {
|
||||
type: 'daterange',
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: '卡号', prop: 'cardNumber' },
|
||||
{ label: '接入号', prop: 'accessNumber' },
|
||||
{ label: '虚拟号', prop: 'virtualNumber' },
|
||||
{ label: '开卡公司', prop: 'cardCompany' },
|
||||
{ label: '月份', prop: 'month' },
|
||||
{ label: '创建时间', prop: 'createTime' },
|
||||
{ label: '创建人', prop: 'creator' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 导出Excel
|
||||
const exportExcel = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要导出的数据')
|
||||
return
|
||||
}
|
||||
ElMessage.success(`导出 ${selectedRows.value.length} 条套餐佣金网卡记录`)
|
||||
}
|
||||
|
||||
// 显示新增对话框
|
||||
const showAddDialog = () => {
|
||||
addDialogVisible.value = true
|
||||
// 重置表单
|
||||
if (addFormRef.value) {
|
||||
addFormRef.value.resetFields()
|
||||
}
|
||||
addFormData.cardNumber = ''
|
||||
addFormData.month = ''
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetails = (row: any) => {
|
||||
ElMessage.info(`查看详情: ${row.cardNumber}`)
|
||||
}
|
||||
|
||||
// 编辑记录
|
||||
const editRecord = (row: any) => {
|
||||
ElMessage.info(`编辑记录: ${row.cardNumber}`)
|
||||
}
|
||||
|
||||
// 删除记录
|
||||
const deleteRecord = (row: any) => {
|
||||
ElMessageBox.confirm(`确定要删除卡号为"${row.cardNumber}"的佣金记录吗?`, '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('删除成功')
|
||||
getCommissionCardList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消删除')
|
||||
})
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'cardNumber',
|
||||
label: '卡号',
|
||||
minWidth: 180
|
||||
},
|
||||
{
|
||||
prop: 'accessNumber',
|
||||
label: '接入号',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
prop: 'virtualNumber',
|
||||
label: '虚拟号',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'cardCompany',
|
||||
label: '开卡公司',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'month',
|
||||
label: '月份',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'createTime',
|
||||
label: '创建时间',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
prop: 'creator',
|
||||
label: '创建人',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 220,
|
||||
formatter: (row: any) => {
|
||||
return h('div', { class: 'operation-buttons' }, [
|
||||
h(ArtButtonTable, {
|
||||
text: '查看',
|
||||
onClick: () => viewDetails(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '编辑',
|
||||
onClick: () => editRecord(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '删除',
|
||||
onClick: () => deleteRecord(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getCommissionCardList()
|
||||
})
|
||||
|
||||
// 获取套餐佣金网卡列表
|
||||
const getCommissionCardList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
|
||||
const endIndex = startIndex + pagination.pageSize
|
||||
const paginatedData = mockData.slice(startIndex, endIndex)
|
||||
|
||||
tableData.value = paginatedData
|
||||
pagination.total = mockData.length
|
||||
loading.value = false
|
||||
} catch (error) {
|
||||
console.error('获取套餐佣金网卡列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getCommissionCardList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getCommissionCardList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getCommissionCardList()
|
||||
}
|
||||
|
||||
// 新增表单验证规则
|
||||
const addRules = reactive<FormRules>({
|
||||
cardNumber: [
|
||||
{ required: true, message: '请输入卡号', trigger: 'blur' },
|
||||
{ min: 15, max: 20, message: '卡号长度在 15 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
month: [{ required: true, message: '请选择月份', trigger: 'change' }]
|
||||
})
|
||||
|
||||
// 提交新增
|
||||
const handleAddSubmit = async () => {
|
||||
if (!addFormRef.value) return
|
||||
|
||||
await addFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
addLoading.value = true
|
||||
|
||||
// 模拟新增过程
|
||||
setTimeout(() => {
|
||||
ElMessage.success(
|
||||
`新增套餐佣金网卡成功!卡号:${addFormData.cardNumber},月份:${addFormData.month}`
|
||||
)
|
||||
addDialogVisible.value = false
|
||||
addLoading.value = false
|
||||
getCommissionCardList()
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.package-commission-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
:deep(.operation-buttons) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
953
src/views/package-management/package-create/index.vue
Normal file
953
src/views/package-management/package-create/index.vue
Normal file
@@ -0,0 +1,953 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="package-create-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 数据视图组件 -->
|
||||
<ArtDataViewer
|
||||
ref="dataViewerRef"
|
||||
:data="tableData"
|
||||
:loading="loading"
|
||||
:table-columns="columns"
|
||||
:descriptions-fields="descriptionsFields"
|
||||
:descriptions-columns="2"
|
||||
:pagination="pagination"
|
||||
:label-width="'140px'"
|
||||
:field-columns="columnChecks"
|
||||
:show-card-actions="true"
|
||||
:show-card-selection="true"
|
||||
:default-view="currentView"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@view-change="handleViewChange"
|
||||
>
|
||||
<template #header-left>
|
||||
<ElButton type="success" @click="showCreateDialog">新增套餐</ElButton>
|
||||
<ElButton @click="exportExcel">导出excel</ElButton>
|
||||
</template>
|
||||
|
||||
<template #header-right>
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
v-model:currentView="currentView"
|
||||
@refresh="handleRefresh"
|
||||
@viewChange="handleViewChange"
|
||||
:show-title="false"
|
||||
:show-view-toggle="true"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #card-actions="{ item }">
|
||||
<ElButton type="primary" size="small" @click="editPackage(item)">
|
||||
编辑
|
||||
</ElButton>
|
||||
<ElButton type="warning" size="small" @click="offlinePackage(item)">
|
||||
下架
|
||||
</ElButton>
|
||||
<ElButton type="success" size="small" @click="copyPackage(item)">
|
||||
复制套餐
|
||||
</ElButton>
|
||||
</template>
|
||||
</ArtDataViewer>
|
||||
|
||||
<!-- 新增套餐对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
title="新增套餐"
|
||||
width="800px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="140px">
|
||||
<ElRow :gutter="24">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="套餐系列" prop="series">
|
||||
<ElSelect
|
||||
v-model="formData.series"
|
||||
placeholder="请选择套餐系列"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption label="畅玩系列" value="changwan" />
|
||||
<ElOption label="如意系列" value="ruyi" />
|
||||
<ElOption label="NB专享" value="nb_special" />
|
||||
<ElOption label="大流量系列" value="big_data" />
|
||||
<ElOption label="广电系列" value="gdtv" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="余额充值梯度" prop="balanceRechargeGradient">
|
||||
<ElSelect
|
||||
v-model="formData.balanceRechargeGradient"
|
||||
placeholder="请选择充值梯度"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption label="10,20,50,100" value="10,20,50,100" />
|
||||
<ElOption label="5,10,20,50" value="5,10,20,50" />
|
||||
<ElOption label="10,50,100" value="10,50,100" />
|
||||
<ElOption label="20,50,100,200" value="20,50,100,200" />
|
||||
<ElOption label="10,30,50" value="10,30,50" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElRow :gutter="24">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="套餐名称" prop="name">
|
||||
<ElInput v-model="formData.name" placeholder="请输入套餐名称" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="套餐类型" prop="type">
|
||||
<ElSelect
|
||||
v-model="formData.type"
|
||||
placeholder="请选择套餐类型"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption label="月套餐" value="monthly" />
|
||||
<ElOption label="年套餐" value="yearly" />
|
||||
<ElOption label="流量包" value="data_package" />
|
||||
<ElOption label="季度套餐" value="quarterly" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElRow :gutter="24">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="本月次月加餐包" prop="monthlyNextMonthAddon">
|
||||
<ElSelect
|
||||
v-model="formData.monthlyNextMonthAddon"
|
||||
placeholder="请选择加餐包支持"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption label="支持" value="support" />
|
||||
<ElOption label="不支持" value="not_support" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="本月最大充值数量" prop="maxMonthlyRecharge">
|
||||
<ElInputNumber
|
||||
v-model="formData.maxMonthlyRecharge"
|
||||
:min="1"
|
||||
:max="1000"
|
||||
placeholder="请输入本月最大充值数量"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElRow :gutter="24">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="次月最大充值数量" prop="maxNextMonthRecharge">
|
||||
<ElInputNumber
|
||||
v-model="formData.maxNextMonthRecharge"
|
||||
:min="1"
|
||||
:max="1000"
|
||||
placeholder="请输入次月最大充值数量"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="套餐数量" prop="packageCount">
|
||||
<ElInputNumber
|
||||
v-model="formData.packageCount"
|
||||
:min="1"
|
||||
:max="999999"
|
||||
placeholder="请输入套餐数量"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElRow :gutter="24">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="成本价" prop="costPrice">
|
||||
<ElInputNumber
|
||||
v-model="formData.costPrice"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
:precision="2"
|
||||
placeholder="请输入成本价"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="套餐金额" prop="packageAmount">
|
||||
<ElInputNumber
|
||||
v-model="formData.packageAmount"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
:precision="2"
|
||||
placeholder="请输入套餐金额"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
确认新增
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ElTag, ElMessage, ElMessageBox, ElInputNumber } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import ArtDataViewer from '@/components/core/views/ArtDataViewer.vue'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
|
||||
defineOptions({ name: 'PackageCreate' })
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
|
||||
// 当前视图模式
|
||||
const currentView = ref('table')
|
||||
|
||||
// 数据视图组件引用
|
||||
const dataViewerRef = ref()
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
packageType: '',
|
||||
packageName: '',
|
||||
packageCode: '',
|
||||
status: '',
|
||||
packageSeries: '',
|
||||
levelOneMerge: '',
|
||||
levelTwoMerge: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 表单实例
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 新增表单数据
|
||||
const formData = reactive({
|
||||
series: '',
|
||||
balanceRechargeGradient: '',
|
||||
name: '',
|
||||
type: '',
|
||||
monthlyNextMonthAddon: '',
|
||||
maxMonthlyRecharge: 0,
|
||||
maxNextMonthRecharge: 0,
|
||||
packageCount: 0,
|
||||
costPrice: 0,
|
||||
packageAmount: 0
|
||||
})
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
packageSeries: '畅玩系列',
|
||||
packageName: '随意联畅玩年卡套餐',
|
||||
packageTraffic: 120,
|
||||
packageCount: 1000,
|
||||
packageCountType: '张',
|
||||
packageType: '年套餐',
|
||||
packageAmount: '168.00',
|
||||
description: '年卡套餐,含120GB流量',
|
||||
frontendSort: 1,
|
||||
sort: 1,
|
||||
maxMonthlyRecharge: 50,
|
||||
maxNextMonthRecharge: 50,
|
||||
monthlyNextMonthAddon: '支持',
|
||||
status: '启用',
|
||||
levelOneMerge: '是',
|
||||
levelOneMergeCode: 'L1_001',
|
||||
levelTwoMerge: '否',
|
||||
levelTwoMergeCode: '',
|
||||
isSpecial: '否',
|
||||
creator: '张若暄',
|
||||
modifier: '张若暄',
|
||||
addTime: '2025-11-08 10:30:00',
|
||||
balanceRechargeGradient: '10,20,50,100',
|
||||
packageCode: 'PKG_001'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
packageSeries: '如意系列',
|
||||
packageName: '如意包年3G流量包',
|
||||
packageTraffic: 36,
|
||||
packageCount: 800,
|
||||
packageCountType: '张',
|
||||
packageType: '年套餐',
|
||||
packageAmount: '98.00',
|
||||
description: '年包3GB月套餐',
|
||||
frontendSort: 2,
|
||||
sort: 2,
|
||||
maxMonthlyRecharge: 30,
|
||||
maxNextMonthRecharge: 30,
|
||||
monthlyNextMonthAddon: '支持',
|
||||
status: '启用',
|
||||
levelOneMerge: '是',
|
||||
levelOneMergeCode: 'L1_002',
|
||||
levelTwoMerge: '是',
|
||||
levelTwoMergeCode: 'L2_001',
|
||||
isSpecial: '否',
|
||||
creator: '孔丽娟',
|
||||
modifier: '孔丽娟',
|
||||
addTime: '2025-11-07 14:15:00',
|
||||
balanceRechargeGradient: '5,10,20,50',
|
||||
packageCode: 'PKG_002'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
packageSeries: 'NB专享',
|
||||
packageName: 'Y-NB专享套餐',
|
||||
packageTraffic: 24,
|
||||
packageCount: 500,
|
||||
packageCountType: '张',
|
||||
packageType: '月套餐',
|
||||
packageAmount: '25.00',
|
||||
description: 'NB-IoT专用套餐',
|
||||
frontendSort: 3,
|
||||
sort: 3,
|
||||
maxMonthlyRecharge: 100,
|
||||
maxNextMonthRecharge: 100,
|
||||
monthlyNextMonthAddon: '不支持',
|
||||
status: '停用',
|
||||
levelOneMerge: '否',
|
||||
levelOneMergeCode: '',
|
||||
levelTwoMerge: '否',
|
||||
levelTwoMergeCode: '',
|
||||
isSpecial: '是',
|
||||
creator: '李佳音',
|
||||
modifier: '李佳音',
|
||||
addTime: '2025-11-06 09:45:00',
|
||||
balanceRechargeGradient: '10,50,100',
|
||||
packageCode: 'PKG_003'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
packageSeries: '大流量系列',
|
||||
packageName: '100G全国流量月卡套餐',
|
||||
packageTraffic: 100,
|
||||
packageCount: 1200,
|
||||
packageCountType: '张',
|
||||
packageType: '月套餐',
|
||||
packageAmount: '59.00',
|
||||
description: '100GB大流量月卡',
|
||||
frontendSort: 4,
|
||||
sort: 4,
|
||||
maxMonthlyRecharge: 80,
|
||||
maxNextMonthRecharge: 80,
|
||||
monthlyNextMonthAddon: '支持',
|
||||
status: '启用',
|
||||
levelOneMerge: '是',
|
||||
levelOneMergeCode: 'L1_003',
|
||||
levelTwoMerge: '否',
|
||||
levelTwoMergeCode: '',
|
||||
isSpecial: '否',
|
||||
creator: '赵强',
|
||||
modifier: '赵强',
|
||||
addTime: '2025-11-05 16:20:00',
|
||||
balanceRechargeGradient: '20,50,100,200',
|
||||
packageCode: 'PKG_004'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
packageSeries: '广电系列',
|
||||
packageName: '广电飞悦卡无预存50G',
|
||||
packageTraffic: 50,
|
||||
packageCount: 600,
|
||||
packageCountType: '张',
|
||||
packageType: '月套餐',
|
||||
packageAmount: '39.00',
|
||||
description: '广电30天50GB流量',
|
||||
frontendSort: 5,
|
||||
sort: 5,
|
||||
maxMonthlyRecharge: 60,
|
||||
maxNextMonthRecharge: 60,
|
||||
monthlyNextMonthAddon: '支持',
|
||||
status: '启用',
|
||||
levelOneMerge: '否',
|
||||
levelOneMergeCode: '',
|
||||
levelTwoMerge: '是',
|
||||
levelTwoMergeCode: 'L2_002',
|
||||
isSpecial: '是',
|
||||
creator: '张若暄',
|
||||
modifier: '张若暄',
|
||||
addTime: '2025-11-04 11:30:00',
|
||||
balanceRechargeGradient: '10,30,50',
|
||||
packageCode: 'PKG_005'
|
||||
}
|
||||
]
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1
|
||||
getPackageList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1
|
||||
getPackageList()
|
||||
}
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '套餐类型',
|
||||
prop: 'packageType',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '月套餐', value: 'monthly' },
|
||||
{ label: '年套餐', value: 'yearly' },
|
||||
{ label: '流量包', value: 'data_package' },
|
||||
{ label: '季度套餐', value: 'quarterly' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '套餐名称',
|
||||
prop: 'packageName',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入套餐名称'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '套餐编码',
|
||||
prop: 'packageCode',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入套餐编码'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '启用', value: 'active' },
|
||||
{ label: '停用', value: 'inactive' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '套餐系列',
|
||||
prop: 'packageSeries',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入套餐系列'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '一级合并',
|
||||
prop: 'levelOneMerge',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '是', value: 'yes' },
|
||||
{ label: '否', value: 'no' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '二级合并',
|
||||
prop: 'levelTwoMerge',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '是', value: 'yes' },
|
||||
{ label: '否', value: 'no' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: '套餐系列', prop: 'packageSeries' },
|
||||
{ label: '套餐名称', prop: 'packageName' },
|
||||
{ label: '套餐流量(GB)', prop: 'packageTraffic' },
|
||||
{ label: '套餐数量', prop: 'packageCount' },
|
||||
{ label: '套餐数量类型', prop: 'packageCountType' },
|
||||
{ label: '套餐类型', prop: 'packageType' },
|
||||
{ label: '套餐金额', prop: 'packageAmount' },
|
||||
{ label: '描述', prop: 'description' },
|
||||
{ label: '前端排序', prop: 'frontendSort' },
|
||||
{ label: '排序', prop: 'sort' },
|
||||
{ label: '本月最大充值数量', prop: 'maxMonthlyRecharge' },
|
||||
{ label: '次月最大充值数量', prop: 'maxNextMonthRecharge' },
|
||||
{ label: '本月次月加餐包', prop: 'monthlyNextMonthAddon' },
|
||||
{ label: '状态', prop: 'status' },
|
||||
{ label: '一级是否合并', prop: 'levelOneMerge' },
|
||||
{ label: '一级合并编号', prop: 'levelOneMergeCode' },
|
||||
{ label: '二级是否合并', prop: 'levelTwoMerge' },
|
||||
{ label: '二级合并编号', prop: 'levelTwoMergeCode' },
|
||||
{ label: '是否特殊', prop: 'isSpecial' },
|
||||
{ label: '创建人', prop: 'creator' },
|
||||
{ label: '修改人', prop: 'modifier' },
|
||||
{ label: '添加时间', prop: 'addTime' },
|
||||
{ label: '余额充值梯度', prop: 'balanceRechargeGradient' },
|
||||
{ label: '套餐编码', prop: 'packageCode' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 描述字段配置(用于卡片视图)
|
||||
const descriptionsFields = [
|
||||
{ prop: 'packageName', label: '套餐名称', span: 2 },
|
||||
{ prop: 'packageSeries', label: '套餐系列' },
|
||||
{ prop: 'packageCode', label: '套餐编码' },
|
||||
{
|
||||
prop: 'packageTraffic',
|
||||
label: '套餐流量',
|
||||
formatter: (row: any) => `${row.packageTraffic}GB`
|
||||
},
|
||||
{
|
||||
prop: 'packageCount',
|
||||
label: '套餐数量',
|
||||
formatter: (row: any) => `${row.packageCount}${row.packageCountType}`
|
||||
},
|
||||
{ prop: 'packageType', label: '套餐类型' },
|
||||
{
|
||||
prop: 'packageAmount',
|
||||
label: '套餐金额',
|
||||
formatter: (row: any) => `¥${row.packageAmount}`
|
||||
},
|
||||
{ prop: 'description', label: '描述', span: 2 },
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
formatter: (row: any) => {
|
||||
const type = getStatusType(row.status)
|
||||
return `<el-tag type="${type}" size="small">${row.status}</el-tag>`
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'monthlyNextMonthAddon',
|
||||
label: '本月次月加餐包',
|
||||
formatter: (row: any) => {
|
||||
const type = row.monthlyNextMonthAddon === '支持' ? 'success' : 'info'
|
||||
return `<el-tag type="${type}" size="small">${row.monthlyNextMonthAddon}</el-tag>`
|
||||
}
|
||||
},
|
||||
{ prop: 'maxMonthlyRecharge', label: '本月最大充值数量' },
|
||||
{ prop: 'maxNextMonthRecharge', label: '次月最大充值数量' },
|
||||
{
|
||||
prop: 'levelOneMerge',
|
||||
label: '一级是否合并',
|
||||
formatter: (row: any) => {
|
||||
const type = getMergeType(row.levelOneMerge)
|
||||
return `<el-tag type="${type}" size="small">${row.levelOneMerge}</el-tag>`
|
||||
}
|
||||
},
|
||||
{ prop: 'levelOneMergeCode', label: '一级合并编号', formatter: (row: any) => row.levelOneMergeCode || '-' },
|
||||
{
|
||||
prop: 'levelTwoMerge',
|
||||
label: '二级是否合并',
|
||||
formatter: (row: any) => {
|
||||
const type = getMergeType(row.levelTwoMerge)
|
||||
return `<el-tag type="${type}" size="small">${row.levelTwoMerge}</el-tag>`
|
||||
}
|
||||
},
|
||||
{ prop: 'levelTwoMergeCode', label: '二级合并编号', formatter: (row: any) => row.levelTwoMergeCode || '-' },
|
||||
{
|
||||
prop: 'isSpecial',
|
||||
label: '是否特殊',
|
||||
formatter: (row: any) => {
|
||||
const type = getSpecialType(row.isSpecial)
|
||||
return `<el-tag type="${type}" size="small">${row.isSpecial}</el-tag>`
|
||||
}
|
||||
},
|
||||
{ prop: 'creator', label: '创建人' },
|
||||
{ prop: 'addTime', label: '添加时间', span: 2 },
|
||||
{ prop: 'balanceRechargeGradient', label: '余额充值梯度' },
|
||||
{ prop: 'frontendSort', label: '前端排序' },
|
||||
{ prop: 'sort', label: '排序' }
|
||||
]
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case '启用':
|
||||
return 'success'
|
||||
case '停用':
|
||||
return 'danger'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取是否合并标签类型
|
||||
const getMergeType = (merge: string) => {
|
||||
switch (merge) {
|
||||
case '是':
|
||||
return 'success'
|
||||
case '否':
|
||||
return 'info'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取是否特殊标签类型
|
||||
const getSpecialType = (special: string) => {
|
||||
switch (special) {
|
||||
case '是':
|
||||
return 'warning'
|
||||
case '否':
|
||||
return 'info'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 导出Excel
|
||||
const exportExcel = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要导出的数据')
|
||||
return
|
||||
}
|
||||
ElMessage.success(`导出 ${selectedRows.value.length} 条套餐记录`)
|
||||
}
|
||||
|
||||
// 显示新增对话框
|
||||
const showCreateDialog = () => {
|
||||
dialogVisible.value = true
|
||||
// 重置表单
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
formData.series = ''
|
||||
formData.balanceRechargeGradient = ''
|
||||
formData.name = ''
|
||||
formData.type = ''
|
||||
formData.monthlyNextMonthAddon = ''
|
||||
formData.maxMonthlyRecharge = 0
|
||||
formData.maxNextMonthRecharge = 0
|
||||
formData.packageCount = 0
|
||||
formData.costPrice = 0
|
||||
formData.packageAmount = 0
|
||||
}
|
||||
|
||||
// 编辑套餐
|
||||
const editPackage = (row: any) => {
|
||||
ElMessage.info(`编辑套餐: ${row.packageName}`)
|
||||
}
|
||||
|
||||
// 下架套餐
|
||||
const offlinePackage = (row: any) => {
|
||||
ElMessageBox.confirm(`确定要下架套餐"${row.packageName}"吗?`, '下架确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('套餐下架成功')
|
||||
getPackageList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消下架')
|
||||
})
|
||||
}
|
||||
|
||||
// 复制套餐
|
||||
const copyPackage = (row: any) => {
|
||||
ElMessage.info(`复制套餐: ${row.packageName}`)
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'packageSeries',
|
||||
label: '套餐系列',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'packageName',
|
||||
label: '套餐名称',
|
||||
minWidth: 180
|
||||
},
|
||||
{
|
||||
prop: 'packageTraffic',
|
||||
label: '套餐流量(GB)',
|
||||
width: 120,
|
||||
formatter: (row) => `${row.packageTraffic}GB`
|
||||
},
|
||||
{
|
||||
prop: 'packageCount',
|
||||
label: '套餐数量',
|
||||
width: 100,
|
||||
formatter: (row) => `${row.packageCount}${row.packageCountType}`
|
||||
},
|
||||
{
|
||||
prop: 'packageType',
|
||||
label: '套餐类型',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'packageAmount',
|
||||
label: '套餐金额',
|
||||
width: 100,
|
||||
formatter: (row) => `¥${row.packageAmount}`
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 80,
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getStatusType(row.status) }, () => row.status)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'levelOneMerge',
|
||||
label: '一级是否合并',
|
||||
width: 120,
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getMergeType(row.levelOneMerge) }, () => row.levelOneMerge)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'levelOneMergeCode',
|
||||
label: '一级合并编号',
|
||||
width: 120,
|
||||
formatter: (row) => row.levelOneMergeCode || '-'
|
||||
},
|
||||
{
|
||||
prop: 'levelTwoMerge',
|
||||
label: '二级是否合并',
|
||||
width: 120,
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getMergeType(row.levelTwoMerge) }, () => row.levelTwoMerge)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'levelTwoMergeCode',
|
||||
label: '二级合并编号',
|
||||
width: 120,
|
||||
formatter: (row) => row.levelTwoMergeCode || '-'
|
||||
},
|
||||
{
|
||||
prop: 'isSpecial',
|
||||
label: '是否特殊',
|
||||
width: 100,
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getSpecialType(row.isSpecial) }, () => row.isSpecial)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'creator',
|
||||
label: '创建人',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'addTime',
|
||||
label: '添加时间',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
prop: 'packageCode',
|
||||
label: '套餐编码',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 240,
|
||||
formatter: (row: any) => {
|
||||
return h('div', { class: 'operation-buttons' }, [
|
||||
h(ArtButtonTable, {
|
||||
text: '编辑',
|
||||
onClick: () => editPackage(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '下架',
|
||||
onClick: () => offlinePackage(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '复制套餐',
|
||||
onClick: () => copyPackage(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getPackageList()
|
||||
})
|
||||
|
||||
// 获取套餐列表
|
||||
const getPackageList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
|
||||
const endIndex = startIndex + pagination.pageSize
|
||||
const paginatedData = mockData.slice(startIndex, endIndex)
|
||||
|
||||
tableData.value = paginatedData
|
||||
pagination.total = mockData.length
|
||||
loading.value = false
|
||||
} catch (error) {
|
||||
console.error('获取套餐列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getPackageList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getPackageList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getPackageList()
|
||||
}
|
||||
|
||||
// 处理视图切换
|
||||
const handleViewChange = (view: string) => {
|
||||
console.log('视图切换到:', view)
|
||||
// 可以在这里添加视图切换的额外逻辑,比如保存用户偏好
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
series: [{ required: true, message: '请选择套餐系列', trigger: 'change' }],
|
||||
balanceRechargeGradient: [{ required: true, message: '请选择余额充值梯度', trigger: 'change' }],
|
||||
name: [
|
||||
{ required: true, message: '请输入套餐名称', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '套餐名称长度在 2 到 50 个字符', trigger: 'blur' }
|
||||
],
|
||||
type: [{ required: true, message: '请选择套餐类型', trigger: 'change' }],
|
||||
monthlyNextMonthAddon: [{ required: true, message: '请选择本月次月加餐包', trigger: 'change' }],
|
||||
maxMonthlyRecharge: [{ required: true, message: '请输入本月最大充值数量', trigger: 'blur' }],
|
||||
maxNextMonthRecharge: [{ required: true, message: '请输入次月最大充值数量', trigger: 'blur' }],
|
||||
packageCount: [{ required: true, message: '请输入套餐数量', trigger: 'blur' }],
|
||||
costPrice: [{ required: true, message: '请输入成本价', trigger: 'blur' }],
|
||||
packageAmount: [{ required: true, message: '请输入套餐金额', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true
|
||||
|
||||
// 模拟提交过程
|
||||
setTimeout(() => {
|
||||
ElMessage.success(
|
||||
`新增套餐成功!套餐名称:${formData.name},套餐系列:${formData.series}`
|
||||
)
|
||||
dialogVisible.value = false
|
||||
submitLoading.value = false
|
||||
getPackageList()
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.package-create-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
:deep(.operation-buttons) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
510
src/views/package-management/package-list/index.vue
Normal file
510
src/views/package-management/package-list/index.vue
Normal file
@@ -0,0 +1,510 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="package-list-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton type="primary" @click="handleSearch">查询</ElButton>
|
||||
<ElButton @click="exportExcel">导出excel</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ElTag, ElMessageBox, ElMessage } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
|
||||
defineOptions({ name: 'PackageList' })
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
agentName: '',
|
||||
packageType: '',
|
||||
status: '',
|
||||
packageName: '',
|
||||
category: '',
|
||||
agentAccount: '',
|
||||
packageSeries: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
packageSeries: '畅玩系列',
|
||||
agentName: '北京优享科技有限公司',
|
||||
agentAccount: 'BJ_YXKJ_001',
|
||||
packageName: '随意联畅玩年卡套餐',
|
||||
packageTraffic: 120,
|
||||
packageType: '年套餐',
|
||||
costPrice: 120.0,
|
||||
officialSuggestedPrice: 168.0,
|
||||
mySalesAmount: 158.0,
|
||||
sort: 1,
|
||||
category: '物联网套餐',
|
||||
packageCode: 'PKG_001',
|
||||
status: '上架'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
packageSeries: '如意系列',
|
||||
agentName: '上海智联通信技术公司',
|
||||
agentAccount: 'SH_ZLTX_002',
|
||||
packageName: '如意包年3G流量包',
|
||||
packageTraffic: 36,
|
||||
packageType: '年套餐',
|
||||
costPrice: 80.0,
|
||||
officialSuggestedPrice: 98.0,
|
||||
mySalesAmount: 95.0,
|
||||
sort: 2,
|
||||
category: '流量套餐',
|
||||
packageCode: 'PKG_002',
|
||||
status: '下架'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
packageSeries: 'NB专享',
|
||||
agentName: '广州物联网络有限公司',
|
||||
agentAccount: 'GZ_WLWL_003',
|
||||
packageName: 'Y-NB专享套餐',
|
||||
packageTraffic: 24,
|
||||
packageType: '月套餐',
|
||||
costPrice: 20.0,
|
||||
officialSuggestedPrice: 25.0,
|
||||
mySalesAmount: 24.0,
|
||||
sort: 3,
|
||||
category: 'NB-IoT套餐',
|
||||
packageCode: 'PKG_003',
|
||||
status: '上架'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
packageSeries: '大流量系列',
|
||||
agentName: '深圳云联科技股份公司',
|
||||
agentAccount: 'SZ_YLKJ_004',
|
||||
packageName: '100G全国流量月卡套餐',
|
||||
packageTraffic: 100,
|
||||
packageType: '月套餐',
|
||||
costPrice: 45.0,
|
||||
officialSuggestedPrice: 59.0,
|
||||
mySalesAmount: 55.0,
|
||||
sort: 4,
|
||||
category: '大流量套餐',
|
||||
packageCode: 'PKG_004',
|
||||
status: '上架'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
packageSeries: '广电系列',
|
||||
agentName: '杭州通达网络服务公司',
|
||||
agentAccount: 'HZ_TDWL_005',
|
||||
packageName: '广电飞悦卡无预存50G',
|
||||
packageTraffic: 50,
|
||||
packageType: '月套餐',
|
||||
costPrice: 30.0,
|
||||
officialSuggestedPrice: 39.0,
|
||||
mySalesAmount: 36.0,
|
||||
sort: 5,
|
||||
category: '广电套餐',
|
||||
packageCode: 'PKG_005',
|
||||
status: '下架'
|
||||
}
|
||||
]
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1
|
||||
getPackageList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1
|
||||
getPackageList()
|
||||
}
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '代理商名称',
|
||||
prop: 'agentName',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入代理商名称'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '套餐类型',
|
||||
prop: 'packageType',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '月套餐', value: 'monthly' },
|
||||
{ label: '年套餐', value: 'yearly' },
|
||||
{ label: '流量包', value: 'data_package' },
|
||||
{ label: '季度套餐', value: 'quarterly' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '上架', value: 'online' },
|
||||
{ label: '下架', value: 'offline' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '套餐名称',
|
||||
prop: 'packageName',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入套餐名称'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '类别',
|
||||
prop: 'category',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '物联网套餐', value: 'iot_package' },
|
||||
{ label: '流量套餐', value: 'data_package' },
|
||||
{ label: 'NB-IoT套餐', value: 'nb_iot_package' },
|
||||
{ label: '大流量套餐', value: 'big_data_package' },
|
||||
{ label: '广电套餐', value: 'gdtv_package' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '代理商账号',
|
||||
prop: 'agentAccount',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入代理商账号'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '套餐系列',
|
||||
prop: 'packageSeries',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入套餐系列'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: '套餐系列', prop: 'packageSeries' },
|
||||
{ label: '代理商名称', prop: 'agentName' },
|
||||
{ label: '代理商账号', prop: 'agentAccount' },
|
||||
{ label: '套餐名称', prop: 'packageName' },
|
||||
{ label: '套餐流量(GB)', prop: 'packageTraffic' },
|
||||
{ label: '套餐类型', prop: 'packageType' },
|
||||
{ label: '套餐成本价', prop: 'costPrice' },
|
||||
{ label: '官方建议销售价格', prop: 'officialSuggestedPrice' },
|
||||
{ label: '我的销售金额', prop: 'mySalesAmount' },
|
||||
{ label: '排序', prop: 'sort' },
|
||||
{ label: '所属类别', prop: 'category' },
|
||||
{ label: '套餐编码', prop: 'packageCode' },
|
||||
{ label: '状态', prop: 'status' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case '上架':
|
||||
return 'success'
|
||||
case '下架':
|
||||
return 'danger'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 导出Excel
|
||||
const exportExcel = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要导出的数据')
|
||||
return
|
||||
}
|
||||
ElMessage.success(`导出 ${selectedRows.value.length} 条套餐记录`)
|
||||
}
|
||||
|
||||
// 编辑套餐
|
||||
const editPackage = (row: any) => {
|
||||
ElMessage.info(`编辑套餐: ${row.packageName}`)
|
||||
}
|
||||
|
||||
// 上架套餐
|
||||
const onlinePackage = (row: any) => {
|
||||
if (row.status === '上架') {
|
||||
ElMessage.warning('该套餐已经上架')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`确定要上架套餐"${row.packageName}"吗?`, '上架确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('套餐上架成功')
|
||||
getPackageList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消上架')
|
||||
})
|
||||
}
|
||||
|
||||
// 下架套餐
|
||||
const offlinePackage = (row: any) => {
|
||||
if (row.status === '下架') {
|
||||
ElMessage.warning('该套餐已经下架')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`确定要下架套餐"${row.packageName}"吗?`, '下架确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('套餐下架成功')
|
||||
getPackageList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消下架')
|
||||
})
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'packageSeries',
|
||||
label: '套餐系列',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'agentName',
|
||||
label: '代理商名称',
|
||||
minWidth: 200
|
||||
},
|
||||
{
|
||||
prop: 'agentAccount',
|
||||
label: '代理商账号',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
prop: 'packageName',
|
||||
label: '套餐名称',
|
||||
minWidth: 180
|
||||
},
|
||||
{
|
||||
prop: 'packageTraffic',
|
||||
label: '套餐流量(GB)',
|
||||
width: 120,
|
||||
formatter: (row) => `${row.packageTraffic}GB`
|
||||
},
|
||||
{
|
||||
prop: 'packageType',
|
||||
label: '套餐类型',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'costPrice',
|
||||
label: '套餐成本价',
|
||||
width: 110,
|
||||
formatter: (row) => `¥${row.costPrice.toFixed(2)}`
|
||||
},
|
||||
{
|
||||
prop: 'officialSuggestedPrice',
|
||||
label: '官方建议销售价格',
|
||||
width: 150,
|
||||
formatter: (row) => `¥${row.officialSuggestedPrice.toFixed(2)}`
|
||||
},
|
||||
{
|
||||
prop: 'mySalesAmount',
|
||||
label: '我的销售金额',
|
||||
width: 120,
|
||||
formatter: (row) => `¥${row.mySalesAmount.toFixed(2)}`
|
||||
},
|
||||
{
|
||||
prop: 'sort',
|
||||
label: '排序',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
prop: 'category',
|
||||
label: '所属类别',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'packageCode',
|
||||
label: '套餐编码',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 80,
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getStatusType(row.status) }, () => row.status)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 150,
|
||||
formatter: (row: any) => {
|
||||
return h('div', { class: 'operation-buttons' }, [
|
||||
h(ArtButtonTable, {
|
||||
text: '编辑',
|
||||
onClick: () => editPackage(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: row.status === '上架' ? '下架' : '上架',
|
||||
onClick: () => (row.status === '上架' ? offlinePackage(row) : onlinePackage(row))
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getPackageList()
|
||||
})
|
||||
|
||||
// 获取套餐列表
|
||||
const getPackageList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
|
||||
const endIndex = startIndex + pagination.pageSize
|
||||
const paginatedData = mockData.slice(startIndex, endIndex)
|
||||
|
||||
tableData.value = paginatedData
|
||||
pagination.total = mockData.length
|
||||
loading.value = false
|
||||
} catch (error) {
|
||||
console.error('获取套餐列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getPackageList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getPackageList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getPackageList()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-template-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
</style>
|
||||
462
src/views/package-management/package-series/index.vue
Normal file
462
src/views/package-management/package-series/index.vue
Normal file
@@ -0,0 +1,462 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="package-series-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton type="primary" @click="handleSearch">搜索</ElButton>
|
||||
<ElButton type="success" @click="showAddDialog">新增</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 新增套餐系列对话框 -->
|
||||
<ElDialog
|
||||
v-model="addDialogVisible"
|
||||
title="新增套餐系列"
|
||||
width="500px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm ref="addFormRef" :model="addFormData" :rules="addRules" label-width="120px">
|
||||
<ElFormItem label="系列名称" prop="seriesName">
|
||||
<ElInput v-model="addFormData.seriesName" placeholder="请输入系列名称" clearable />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="包含套餐" prop="packageNames">
|
||||
<ElSelect
|
||||
v-model="addFormData.packageNames"
|
||||
placeholder="请选择要包含的套餐"
|
||||
style="width: 100%"
|
||||
multiple
|
||||
clearable
|
||||
>
|
||||
<ElOption label="随意联畅玩年卡套餐" value="changwan_yearly" />
|
||||
<ElOption label="随意联畅玩月卡套餐" value="changwan_monthly" />
|
||||
<ElOption label="如意包年3G流量包" value="ruyi_3g" />
|
||||
<ElOption label="如意包月流量包" value="ruyi_monthly" />
|
||||
<ElOption label="Y-NB专享套餐" value="nb_special" />
|
||||
<ElOption label="NB-IoT基础套餐" value="nb_basic" />
|
||||
<ElOption label="100G全国流量月卡套餐" value="big_data_100g" />
|
||||
<ElOption label="200G超值流量包" value="big_data_200g" />
|
||||
<ElOption label="广电飞悦卡无预存50G" value="gdtv_50g" />
|
||||
<ElOption label="广电天翼卡" value="gdtv_tianyi" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="系列描述" prop="description">
|
||||
<ElInput
|
||||
v-model="addFormData.description"
|
||||
type="textarea"
|
||||
placeholder="请输入系列描述(可选)"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="addDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleAddSubmit" :loading="addLoading">
|
||||
确认新增
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ElTag, ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
|
||||
defineOptions({ name: 'PackageSeries' })
|
||||
|
||||
const addDialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const addLoading = ref(false)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
seriesName: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 新增表单实例
|
||||
const addFormRef = ref<FormInstance>()
|
||||
|
||||
// 新增表单数据
|
||||
const addFormData = reactive({
|
||||
seriesName: '',
|
||||
packageNames: [] as string[],
|
||||
description: ''
|
||||
})
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
seriesName: '畅玩系列',
|
||||
operator: '张若暄',
|
||||
operationTime: '2025-11-08 10:30:00',
|
||||
status: '启用'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
seriesName: '如意系列',
|
||||
operator: '孔丽娟',
|
||||
operationTime: '2025-11-07 14:15:00',
|
||||
status: '启用'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
seriesName: 'NB专享',
|
||||
operator: '李佳音',
|
||||
operationTime: '2025-11-06 09:45:00',
|
||||
status: '禁用'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
seriesName: '大流量系列',
|
||||
operator: '赵强',
|
||||
operationTime: '2025-11-05 16:20:00',
|
||||
status: '启用'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
seriesName: '广电系列',
|
||||
operator: '张若暄',
|
||||
operationTime: '2025-11-04 11:30:00',
|
||||
status: '禁用'
|
||||
}
|
||||
]
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1
|
||||
getPackageSeriesList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1
|
||||
getPackageSeriesList()
|
||||
}
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '系列名称',
|
||||
prop: 'seriesName',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入系列名称'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: '系列名称', prop: 'seriesName' },
|
||||
{ label: '操作人', prop: 'operator' },
|
||||
{ label: '操作时间', prop: 'operationTime' },
|
||||
{ label: '状态', prop: 'status' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case '启用':
|
||||
return 'success'
|
||||
case '禁用':
|
||||
return 'danger'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 显示新增对话框
|
||||
const showAddDialog = () => {
|
||||
addDialogVisible.value = true
|
||||
// 重置表单
|
||||
if (addFormRef.value) {
|
||||
addFormRef.value.resetFields()
|
||||
}
|
||||
addFormData.seriesName = ''
|
||||
addFormData.packageNames = []
|
||||
addFormData.description = ''
|
||||
}
|
||||
|
||||
// 启用系列
|
||||
const enableSeries = (row: any) => {
|
||||
ElMessageBox.confirm(`确定要启用套餐系列"${row.seriesName}"吗?`, '启用确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('启用成功')
|
||||
getPackageSeriesList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消启用')
|
||||
})
|
||||
}
|
||||
|
||||
// 禁用系列
|
||||
const disableSeries = (row: any) => {
|
||||
ElMessageBox.confirm(`确定要禁用套餐系列"${row.seriesName}"吗?`, '禁用确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('禁用成功')
|
||||
getPackageSeriesList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消禁用')
|
||||
})
|
||||
}
|
||||
|
||||
// 删除系列
|
||||
const deleteSeries = (row: any) => {
|
||||
ElMessageBox.confirm(
|
||||
`确定要删除套餐系列"${row.seriesName}"吗?删除后将无法恢复。`,
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
ElMessage.success('删除成功')
|
||||
getPackageSeriesList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消删除')
|
||||
})
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'seriesName',
|
||||
label: '系列名称',
|
||||
minWidth: 180
|
||||
},
|
||||
{
|
||||
prop: 'operator',
|
||||
label: '操作人',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'operationTime',
|
||||
label: '操作时间',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 100,
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getStatusType(row.status) }, () => row.status)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 180,
|
||||
formatter: (row: any) => {
|
||||
const buttons = []
|
||||
|
||||
if (row.status === '启用') {
|
||||
buttons.push(
|
||||
h(ArtButtonTable, {
|
||||
text: '禁用',
|
||||
onClick: () => disableSeries(row)
|
||||
})
|
||||
)
|
||||
} else {
|
||||
buttons.push(
|
||||
h(ArtButtonTable, {
|
||||
text: '启用',
|
||||
onClick: () => enableSeries(row)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
buttons.push(
|
||||
h(ArtButtonTable, {
|
||||
text: '删除',
|
||||
onClick: () => deleteSeries(row)
|
||||
})
|
||||
)
|
||||
|
||||
return h('div', { class: 'operation-buttons' }, buttons)
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getPackageSeriesList()
|
||||
})
|
||||
|
||||
// 获取套餐系列列表
|
||||
const getPackageSeriesList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
|
||||
const endIndex = startIndex + pagination.pageSize
|
||||
const paginatedData = mockData.slice(startIndex, endIndex)
|
||||
|
||||
tableData.value = paginatedData
|
||||
pagination.total = mockData.length
|
||||
loading.value = false
|
||||
} catch (error) {
|
||||
console.error('获取套餐系列列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getPackageSeriesList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getPackageSeriesList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getPackageSeriesList()
|
||||
}
|
||||
|
||||
// 新增表单验证规则
|
||||
const addRules = reactive<FormRules>({
|
||||
seriesName: [
|
||||
{ required: true, message: '请输入系列名称', trigger: 'blur' },
|
||||
{ min: 2, max: 20, message: '系列名称长度在 2 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
packageNames: [{ required: true, message: '请选择要包含的套餐', trigger: 'change' }]
|
||||
})
|
||||
|
||||
// 提交新增
|
||||
const handleAddSubmit = async () => {
|
||||
if (!addFormRef.value) return
|
||||
|
||||
await addFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
addLoading.value = true
|
||||
|
||||
// 模拟新增过程
|
||||
setTimeout(() => {
|
||||
ElMessage.success(
|
||||
`新增套餐系列成功!系列名称:${addFormData.seriesName},包含套餐:${addFormData.packageNames.length} 个`
|
||||
)
|
||||
addDialogVisible.value = false
|
||||
addLoading.value = false
|
||||
getPackageSeriesList()
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.package-series-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
:deep(.operation-buttons) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
628
src/views/product/shop/index.vue
Normal file
628
src/views/product/shop/index.vue
Normal file
@@ -0,0 +1,628 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="shop-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton @click="showDialog('add')">新增店铺</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增店铺' : '编辑店铺'"
|
||||
width="800px"
|
||||
>
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="100px">
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="店铺名称" prop="shop_name">
|
||||
<ElInput v-model="formData.shop_name" placeholder="请输入店铺名称" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12" v-if="dialogType === 'add'">
|
||||
<ElFormItem label="店铺编号" prop="shop_code">
|
||||
<ElInput v-model="formData.shop_code" placeholder="请输入店铺编号" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElRow :gutter="20" v-if="dialogType === 'add'">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="上级店铺ID" prop="parent_id">
|
||||
<ElInputNumber v-model="formData.parent_id" :min="1" placeholder="一级店铺可不填" style="width: 100%" clearable />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="省份" prop="province">
|
||||
<ElInput v-model="formData.province" placeholder="请输入省份" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="城市" prop="city">
|
||||
<ElInput v-model="formData.city" placeholder="请输入城市" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="区县" prop="district">
|
||||
<ElInput v-model="formData.district" placeholder="请输入区县" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="详细地址" prop="address">
|
||||
<ElInput v-model="formData.address" placeholder="请输入详细地址" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="联系人" prop="contact_name">
|
||||
<ElInput v-model="formData.contact_name" placeholder="请输入联系人姓名" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="联系电话" prop="contact_phone">
|
||||
<ElInput v-model="formData.contact_phone" placeholder="请输入联系电话" maxlength="11" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<!-- 新增店铺时的初始账号信息 -->
|
||||
<template v-if="dialogType === 'add'">
|
||||
<ElDivider content-position="left">初始账号信息</ElDivider>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="用户名" prop="init_username">
|
||||
<ElInput v-model="formData.init_username" placeholder="请输入初始账号用户名" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="密码" prop="init_password">
|
||||
<ElInput v-model="formData.init_password" type="password" placeholder="请输入初始账号密码" show-password />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="手机号" prop="init_phone">
|
||||
<ElInput v-model="formData.init_phone" placeholder="请输入初始账号手机号" maxlength="11" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</template>
|
||||
|
||||
<ElFormItem v-if="dialogType === 'edit'" label="状态">
|
||||
<ElSwitch
|
||||
v-model="formData.status"
|
||||
:active-value="CommonStatus.ENABLED"
|
||||
:inactive-value="CommonStatus.DISABLED"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { FormInstance, ElMessage, ElMessageBox, ElTag, ElSwitch } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { ShopService } from '@/api/modules'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import type { ShopResponse } from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants'
|
||||
|
||||
defineOptions({ name: 'Shop' })
|
||||
|
||||
const dialogType = ref('add')
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
shop_name: '',
|
||||
shop_code: '',
|
||||
parent_id: undefined as number | undefined,
|
||||
level: undefined as number | undefined,
|
||||
status: undefined as number | undefined
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const searchForm = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<ShopResponse[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, { ...initialSearchState })
|
||||
pagination.currentPage = 1
|
||||
getShopList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
pagination.currentPage = 1
|
||||
getShopList()
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const searchFormItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '店铺名称',
|
||||
prop: 'shop_name',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入店铺名称'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '店铺编号',
|
||||
prop: 'shop_code',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入店铺编号'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '上级ID',
|
||||
prop: 'parent_id',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入上级店铺ID'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '店铺层级',
|
||||
prop: 'level',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '1级', value: 1 },
|
||||
{ label: '2级', value: 2 },
|
||||
{ label: '3级', value: 3 },
|
||||
{ label: '4级', value: 4 },
|
||||
{ label: '5级', value: 5 },
|
||||
{ label: '6级', value: 6 },
|
||||
{ label: '7级', value: 7 }
|
||||
],
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请选择店铺层级'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
type: 'select',
|
||||
options: STATUS_SELECT_OPTIONS,
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请选择状态'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: 'ID', prop: 'id' },
|
||||
{ label: '店铺名称', prop: 'shop_name' },
|
||||
{ label: '店铺编号', prop: 'shop_code' },
|
||||
{ label: '层级', prop: 'level' },
|
||||
{ label: '上级ID', prop: 'parent_id' },
|
||||
{ label: '所在地区', prop: 'region' },
|
||||
{ label: '联系人', prop: 'contact_name' },
|
||||
{ label: '联系电话', prop: 'contact_phone' },
|
||||
{ label: '状态', prop: 'status' },
|
||||
{ label: '创建时间', prop: 'created_at' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 显示对话框
|
||||
const showDialog = (type: string, row?: ShopResponse) => {
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
|
||||
// 重置表单验证状态
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
|
||||
if (type === 'edit' && row) {
|
||||
formData.id = row.id
|
||||
formData.shop_name = row.shop_name
|
||||
formData.shop_code = ''
|
||||
formData.parent_id = null
|
||||
formData.province = row.province || ''
|
||||
formData.city = row.city || ''
|
||||
formData.district = row.district || ''
|
||||
formData.address = row.address || ''
|
||||
formData.contact_name = row.contact_name || ''
|
||||
formData.contact_phone = row.contact_phone || ''
|
||||
formData.status = row.status
|
||||
formData.init_username = ''
|
||||
formData.init_password = ''
|
||||
formData.init_phone = ''
|
||||
} else {
|
||||
formData.id = 0
|
||||
formData.shop_name = ''
|
||||
formData.shop_code = ''
|
||||
formData.parent_id = null
|
||||
formData.province = ''
|
||||
formData.city = ''
|
||||
formData.district = ''
|
||||
formData.address = ''
|
||||
formData.contact_name = ''
|
||||
formData.contact_phone = ''
|
||||
formData.status = CommonStatus.ENABLED
|
||||
formData.init_username = ''
|
||||
formData.init_password = ''
|
||||
formData.init_phone = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 删除店铺
|
||||
const deleteShop = (row: ShopResponse) => {
|
||||
ElMessageBox.confirm(`确定要删除店铺 ${row.shop_name} 吗?`, '删除店铺', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
await ShopService.deleteShop(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
getShopList()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// 用户取消删除
|
||||
})
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'id',
|
||||
label: 'ID',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
prop: 'shop_name',
|
||||
label: '店铺名称',
|
||||
minWidth: 150
|
||||
},
|
||||
{
|
||||
prop: 'shop_code',
|
||||
label: '店铺编号',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'level',
|
||||
label: '层级',
|
||||
width: 80,
|
||||
formatter: (row: ShopResponse) => {
|
||||
return h(ElTag, { type: 'info', size: 'small' }, () => `${row.level}级`)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'parent_id',
|
||||
label: '上级ID',
|
||||
width: 100,
|
||||
formatter: (row: ShopResponse) => row.parent_id || '-'
|
||||
},
|
||||
{
|
||||
prop: 'region',
|
||||
label: '所在地区',
|
||||
minWidth: 180,
|
||||
formatter: (row: ShopResponse) => {
|
||||
const parts: string[] = []
|
||||
if (row.province) parts.push(row.province)
|
||||
if (row.city) parts.push(row.city)
|
||||
if (row.district) parts.push(row.district)
|
||||
return parts.length > 0 ? parts.join(' / ') : '-'
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'contact_name',
|
||||
label: '联系人',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'contact_phone',
|
||||
label: '联系电话',
|
||||
width: 130
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 100,
|
||||
formatter: (row: ShopResponse) => {
|
||||
return h(ElSwitch, {
|
||||
modelValue: row.status,
|
||||
activeValue: CommonStatus.ENABLED,
|
||||
inactiveValue: CommonStatus.DISABLED,
|
||||
activeText: getStatusText(CommonStatus.ENABLED),
|
||||
inactiveText: getStatusText(CommonStatus.DISABLED),
|
||||
inlinePrompt: true,
|
||||
'onUpdate:modelValue': (val: number) => handleStatusChange(row, val)
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
width: 180,
|
||||
formatter: (row: ShopResponse) => formatDateTime(row.created_at)
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 150,
|
||||
fixed: 'right',
|
||||
formatter: (row: ShopResponse) => {
|
||||
return h('div', { style: 'display: flex; gap: 8px;' }, [
|
||||
h(ArtButtonTable, {
|
||||
type: 'edit',
|
||||
onClick: () => showDialog('edit', row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
type: 'delete',
|
||||
onClick: () => deleteShop(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// 表单实例
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
id: 0,
|
||||
shop_name: '',
|
||||
shop_code: '',
|
||||
parent_id: null as number | null,
|
||||
province: '',
|
||||
city: '',
|
||||
district: '',
|
||||
address: '',
|
||||
contact_name: '',
|
||||
contact_phone: '',
|
||||
status: CommonStatus.ENABLED,
|
||||
init_username: '',
|
||||
init_password: '',
|
||||
init_phone: ''
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getShopList()
|
||||
})
|
||||
|
||||
// 获取店铺列表
|
||||
const getShopList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.currentPage,
|
||||
page_size: pagination.pageSize,
|
||||
shop_name: searchForm.shop_name || undefined,
|
||||
shop_code: searchForm.shop_code || undefined,
|
||||
parent_id: searchForm.parent_id,
|
||||
level: searchForm.level,
|
||||
status: searchForm.status
|
||||
}
|
||||
const res = await ShopService.getShops(params)
|
||||
if (res.code === 0) {
|
||||
tableData.value = res.data.items || []
|
||||
pagination.total = res.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取店铺列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getShopList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
shop_name: [
|
||||
{ required: true, message: '请输入店铺名称', trigger: 'blur' },
|
||||
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' }
|
||||
],
|
||||
shop_code: [
|
||||
{ required: true, message: '请输入店铺编号', trigger: 'blur' },
|
||||
{ min: 1, max: 50, message: '长度在 1 到 50 个字符', trigger: 'blur' }
|
||||
],
|
||||
address: [
|
||||
{ max: 255, message: '地址不能超过255个字符', trigger: 'blur' }
|
||||
],
|
||||
contact_name: [
|
||||
{ max: 50, message: '联系人姓名不能超过50个字符', trigger: 'blur' }
|
||||
],
|
||||
contact_phone: [
|
||||
{ len: 11, message: '联系电话必须为 11 位', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
|
||||
],
|
||||
init_username: [
|
||||
{ required: true, message: '请输入初始账号用户名', trigger: 'blur' },
|
||||
{ min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur' }
|
||||
],
|
||||
init_password: [
|
||||
{ required: true, message: '请输入初始账号密码', trigger: 'blur' },
|
||||
{ min: 8, max: 32, message: '密码长度为 8-32 个字符', trigger: 'blur' }
|
||||
],
|
||||
init_phone: [
|
||||
{ required: true, message: '请输入初始账号手机号', trigger: 'blur' },
|
||||
{ len: 11, message: '手机号必须为 11 位', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true
|
||||
try {
|
||||
if (dialogType.value === 'add') {
|
||||
const data: any = {
|
||||
shop_name: formData.shop_name,
|
||||
shop_code: formData.shop_code,
|
||||
init_username: formData.init_username,
|
||||
init_password: formData.init_password,
|
||||
init_phone: formData.init_phone
|
||||
}
|
||||
|
||||
// 可选字段
|
||||
if (formData.parent_id) data.parent_id = formData.parent_id
|
||||
if (formData.province) data.province = formData.province
|
||||
if (formData.city) data.city = formData.city
|
||||
if (formData.district) data.district = formData.district
|
||||
if (formData.address) data.address = formData.address
|
||||
if (formData.contact_name) data.contact_name = formData.contact_name
|
||||
if (formData.contact_phone) data.contact_phone = formData.contact_phone
|
||||
|
||||
await ShopService.createShop(data)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
const data: any = {
|
||||
shop_name: formData.shop_name,
|
||||
status: formData.status
|
||||
}
|
||||
|
||||
// 可选字段
|
||||
if (formData.province) data.province = formData.province
|
||||
if (formData.city) data.city = formData.city
|
||||
if (formData.district) data.district = formData.district
|
||||
if (formData.address) data.address = formData.address
|
||||
if (formData.contact_name) data.contact_name = formData.contact_name
|
||||
if (formData.contact_phone) data.contact_phone = formData.contact_phone
|
||||
|
||||
await ShopService.updateShop(formData.id, data)
|
||||
ElMessage.success('更新成功')
|
||||
}
|
||||
|
||||
dialogVisible.value = false
|
||||
getShopList()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getShopList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getShopList()
|
||||
}
|
||||
|
||||
// 状态切换
|
||||
const handleStatusChange = async (row: ShopResponse, newStatus: number) => {
|
||||
const oldStatus = row.status
|
||||
// 先更新UI
|
||||
row.status = newStatus
|
||||
try {
|
||||
await ShopService.updateShop(row.id, { status: newStatus })
|
||||
ElMessage.success('状态切换成功')
|
||||
} catch (error) {
|
||||
// 切换失败,恢复原状态
|
||||
row.status = oldStatus
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.shop-page {
|
||||
// 店铺管理页面样式
|
||||
}
|
||||
</style>
|
||||
430
src/views/product/sim-card-assign/index.vue
Normal file
430
src/views/product/sim-card-assign/index.vue
Normal file
@@ -0,0 +1,430 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<!-- 搜索和操作区 -->
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElInput v-model="searchQuery" placeholder="产品名称/ICCID" clearable />
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElSelect v-model="operatorFilter" placeholder="运营商筛选" clearable style="width: 100%">
|
||||
<ElOption label="全部" value="" />
|
||||
<ElOption label="中国移动" value="CMCC" />
|
||||
<ElOption label="中国联通" value="CUCC" />
|
||||
<ElOption label="中国电信" value="CTCC" />
|
||||
</ElSelect>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElSelect v-model="statusFilter" placeholder="分配状态" clearable style="width: 100%">
|
||||
<ElOption label="全部" value="" />
|
||||
<ElOption label="已分配" value="assigned" />
|
||||
<ElOption label="未分配" value="unassigned" />
|
||||
</ElSelect>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<ElButton v-ripple @click="handleSearch">搜索</ElButton>
|
||||
<ElButton v-ripple type="primary" @click="showAssignDialog">批量分配</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<!-- 号卡产品列表 -->
|
||||
<ArtTable :data="filteredData" index style="margin-top: 20px" @selection-change="handleSelectionChange">
|
||||
<template #default>
|
||||
<ElTableColumn type="selection" width="55" />
|
||||
<ElTableColumn label="产品名称" prop="productName" min-width="180" show-overflow-tooltip />
|
||||
<ElTableColumn label="运营商" prop="operator" width="100">
|
||||
<template #default="scope">
|
||||
<ElTag :type="getOperatorTagType(scope.row.operator)">
|
||||
{{ getOperatorText(scope.row.operator) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="套餐规格" prop="packageSpec" min-width="150" />
|
||||
<ElTableColumn label="产品价格" prop="price" width="120">
|
||||
<template #default="scope"> ¥{{ scope.row.price.toFixed(2) }} </template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="库存数量" prop="stock" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<span :style="{ color: scope.row.stock < 100 ? 'var(--el-color-danger)' : '' }">
|
||||
{{ scope.row.stock }}
|
||||
</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="已分配数量" prop="assignedCount" width="120" align="center">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-primary)">{{ scope.row.assignedCount }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="分配状态" prop="assignStatus" width="100">
|
||||
<template #default="scope">
|
||||
<ElTag v-if="scope.row.assignedCount > 0" type="success">已分配</ElTag>
|
||||
<ElTag v-else type="info">未分配</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn fixed="right" label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<el-button link :icon="View" @click="viewAssignDetail(scope.row)">分配记录</el-button>
|
||||
<el-button link type="primary" @click="assignToAgent(scope.row)">分配</el-button>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 分配对话框 -->
|
||||
<ElDialog v-model="assignDialogVisible" title="分配号卡产品" width="600px" align-center>
|
||||
<ElForm ref="formRef" :model="assignForm" :rules="assignRules" label-width="120px">
|
||||
<ElFormItem label="选择代理商" prop="agentId">
|
||||
<ElSelect
|
||||
v-model="assignForm.agentId"
|
||||
placeholder="请选择代理商"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
@change="handleAgentChange"
|
||||
>
|
||||
<ElOption
|
||||
v-for="agent in agentList"
|
||||
:key="agent.id"
|
||||
:label="`${agent.agentName} (${agent.phone})`"
|
||||
:value="agent.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="分配数量" prop="quantity">
|
||||
<ElInputNumber
|
||||
v-model="assignForm.quantity"
|
||||
:min="1"
|
||||
:max="currentProduct.stock"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div style="color: var(--el-text-color-secondary); font-size: 12px; margin-top: 4px">
|
||||
当前库存:{{ currentProduct.stock }} 张
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="分佣模式" prop="commissionMode">
|
||||
<ElRadioGroup v-model="assignForm.commissionMode">
|
||||
<ElRadio value="fixed">固定佣金</ElRadio>
|
||||
<ElRadio value="percent">比例佣金</ElRadio>
|
||||
<ElRadio value="template">使用模板</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="assignForm.commissionMode === 'fixed'" label="固定金额" prop="fixedAmount">
|
||||
<ElInputNumber v-model="assignForm.fixedAmount" :min="0" :precision="2" style="width: 100%" />
|
||||
<span style="margin-left: 8px">元/张</span>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="assignForm.commissionMode === 'percent'" label="佣金比例" prop="percent">
|
||||
<ElInputNumber v-model="assignForm.percent" :min="0" :max="100" :precision="2" style="width: 100%" />
|
||||
<span style="margin-left: 8px">%</span>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="assignForm.commissionMode === 'template'" label="分佣模板" prop="templateId">
|
||||
<ElSelect v-model="assignForm.templateId" placeholder="请选择分佣模板" style="width: 100%">
|
||||
<ElOption
|
||||
v-for="template in commissionTemplates"
|
||||
:key="template.id"
|
||||
:label="template.templateName"
|
||||
:value="template.id"
|
||||
>
|
||||
<span>{{ template.templateName }}</span>
|
||||
<span style="float: right; color: var(--el-text-color-secondary); font-size: 12px">
|
||||
{{ template.mode === 'fixed' ? `¥${template.value}元/张` : `${template.value}%` }}
|
||||
</span>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="特殊折扣" prop="discount">
|
||||
<ElInputNumber v-model="assignForm.discount" :min="0" :max="100" :precision="2" style="width: 100%" />
|
||||
<span style="margin-left: 8px">%</span>
|
||||
<div style="color: var(--el-text-color-secondary); font-size: 12px; margin-top: 4px">
|
||||
0表示无折扣,设置后代理商可以此折扣价格销售
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="备注" prop="remark">
|
||||
<ElInput v-model="assignForm.remark" type="textarea" :rows="3" placeholder="请输入备注信息" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="assignDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleAssignSubmit">确认分配</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 分配记录对话框 -->
|
||||
<ElDialog v-model="detailDialogVisible" title="分配记录" width="900px" align-center>
|
||||
<ArtTable :data="assignRecords" index>
|
||||
<template #default>
|
||||
<ElTableColumn label="代理商名称" prop="agentName" min-width="150" />
|
||||
<ElTableColumn label="分配数量" prop="quantity" width="100" align="center" />
|
||||
<ElTableColumn label="分佣模式" prop="commissionMode" width="120">
|
||||
<template #default="scope">
|
||||
<ElTag v-if="scope.row.commissionMode === 'fixed'" type="warning">固定佣金</ElTag>
|
||||
<ElTag v-else-if="scope.row.commissionMode === 'percent'" type="success">比例佣金</ElTag>
|
||||
<ElTag v-else>模板佣金</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="佣金规则" prop="commissionRule" width="150" />
|
||||
<ElTableColumn label="特殊折扣" prop="discount" width="100">
|
||||
<template #default="scope">
|
||||
{{ scope.row.discount > 0 ? `${scope.row.discount}%` : '无' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="分配时间" prop="assignTime" width="180" />
|
||||
<ElTableColumn label="操作人" prop="operator" width="100" />
|
||||
<ElTableColumn fixed="right" label="操作" width="120">
|
||||
<template #default="scope">
|
||||
<el-button link type="danger" @click="handleCancelAssign(scope.row)">取消分配</el-button>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="detailDialogVisible = false">关闭</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { View } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'SimCardAssign' })
|
||||
|
||||
interface SimCardProduct {
|
||||
id: string
|
||||
productName: string
|
||||
operator: string
|
||||
packageSpec: string
|
||||
price: number
|
||||
stock: number
|
||||
assignedCount: number
|
||||
}
|
||||
|
||||
const searchQuery = ref('')
|
||||
const operatorFilter = ref('')
|
||||
const statusFilter = ref('')
|
||||
const assignDialogVisible = ref(false)
|
||||
const detailDialogVisible = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
const selectedRows = ref<SimCardProduct[]>([])
|
||||
|
||||
const currentProduct = ref<SimCardProduct>({
|
||||
id: '',
|
||||
productName: '',
|
||||
operator: '',
|
||||
packageSpec: '',
|
||||
price: 0,
|
||||
stock: 0,
|
||||
assignedCount: 0
|
||||
})
|
||||
|
||||
const assignForm = reactive({
|
||||
agentId: '',
|
||||
quantity: 1,
|
||||
commissionMode: 'percent',
|
||||
fixedAmount: 0,
|
||||
percent: 10,
|
||||
templateId: '',
|
||||
discount: 0,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const assignRules = reactive<FormRules>({
|
||||
agentId: [{ required: true, message: '请选择代理商', trigger: 'change' }],
|
||||
quantity: [{ required: true, message: '请输入分配数量', trigger: 'blur' }],
|
||||
commissionMode: [{ required: true, message: '请选择分佣模式', trigger: 'change' }]
|
||||
})
|
||||
|
||||
const agentList = ref([
|
||||
{ id: '1', agentName: '华东区总代理', phone: '13800138000', level: 1 },
|
||||
{ id: '2', agentName: '华南区代理', phone: '13900139000', level: 2 },
|
||||
{ id: '3', agentName: '华北区代理', phone: '13700137000', level: 1 }
|
||||
])
|
||||
|
||||
const commissionTemplates = ref([
|
||||
{ id: '1', templateName: '标准代理商佣金', mode: 'percent', value: 10 },
|
||||
{ id: '2', templateName: '特殊套餐固定佣金', mode: 'fixed', value: 50 },
|
||||
{ id: '3', templateName: '高端代理商佣金', mode: 'percent', value: 15 }
|
||||
])
|
||||
|
||||
const mockData = ref<SimCardProduct[]>([
|
||||
{
|
||||
id: '1',
|
||||
productName: '移动4G流量卡-月包100GB',
|
||||
operator: 'CMCC',
|
||||
packageSpec: '100GB/月,有效期1年',
|
||||
price: 80.00,
|
||||
stock: 1000,
|
||||
assignedCount: 500
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
productName: '联通5G流量卡-季包300GB',
|
||||
operator: 'CUCC',
|
||||
packageSpec: '300GB/季,有效期1年',
|
||||
price: 220.00,
|
||||
stock: 500,
|
||||
assignedCount: 200
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
productName: '电信物联网卡-年包1TB',
|
||||
operator: 'CTCC',
|
||||
packageSpec: '1TB/年,有效期2年',
|
||||
price: 800.00,
|
||||
stock: 80,
|
||||
assignedCount: 0
|
||||
}
|
||||
])
|
||||
|
||||
const assignRecords = ref([
|
||||
{
|
||||
id: '1',
|
||||
agentName: '华东区总代理',
|
||||
quantity: 200,
|
||||
commissionMode: 'percent',
|
||||
commissionRule: '10%',
|
||||
discount: 5,
|
||||
assignTime: '2026-01-08 10:00:00',
|
||||
operator: 'admin'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
agentName: '华南区代理',
|
||||
quantity: 150,
|
||||
commissionMode: 'fixed',
|
||||
commissionRule: '¥5.00/张',
|
||||
discount: 0,
|
||||
assignTime: '2026-01-07 14:30:00',
|
||||
operator: 'admin'
|
||||
}
|
||||
])
|
||||
|
||||
const filteredData = computed(() => {
|
||||
let data = mockData.value
|
||||
|
||||
if (searchQuery.value) {
|
||||
data = data.filter((item) => item.productName.includes(searchQuery.value))
|
||||
}
|
||||
|
||||
if (operatorFilter.value) {
|
||||
data = data.filter((item) => item.operator === operatorFilter.value)
|
||||
}
|
||||
|
||||
if (statusFilter.value) {
|
||||
if (statusFilter.value === 'assigned') {
|
||||
data = data.filter((item) => item.assignedCount > 0)
|
||||
} else if (statusFilter.value === 'unassigned') {
|
||||
data = data.filter((item) => item.assignedCount === 0)
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
})
|
||||
|
||||
const getOperatorText = (operator: string) => {
|
||||
const map: Record<string, string> = {
|
||||
CMCC: '中国移动',
|
||||
CUCC: '中国联通',
|
||||
CTCC: '中国电信'
|
||||
}
|
||||
return map[operator] || operator
|
||||
}
|
||||
|
||||
const getOperatorTagType = (operator: string) => {
|
||||
const map: Record<string, any> = {
|
||||
CMCC: 'success',
|
||||
CUCC: 'primary',
|
||||
CTCC: 'warning'
|
||||
}
|
||||
return map[operator] || ''
|
||||
}
|
||||
|
||||
const handleSearch = () => {}
|
||||
|
||||
const handleSelectionChange = (rows: SimCardProduct[]) => {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
const showAssignDialog = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要分配的产品')
|
||||
return
|
||||
}
|
||||
if (selectedRows.value.length > 1) {
|
||||
ElMessage.warning('批量分配功能开发中,请单个选择')
|
||||
return
|
||||
}
|
||||
currentProduct.value = selectedRows.value[0]
|
||||
assignDialogVisible.value = true
|
||||
}
|
||||
|
||||
const assignToAgent = (row: SimCardProduct) => {
|
||||
currentProduct.value = row
|
||||
assignDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleAgentChange = () => {
|
||||
// 可以根据代理商自动填充默认佣金设置
|
||||
}
|
||||
|
||||
const handleAssignSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
if (assignForm.quantity > currentProduct.value.stock) {
|
||||
ElMessage.error('分配数量不能超过库存数量')
|
||||
return
|
||||
}
|
||||
|
||||
// 更新分配数量
|
||||
currentProduct.value.assignedCount += assignForm.quantity
|
||||
currentProduct.value.stock -= assignForm.quantity
|
||||
|
||||
assignDialogVisible.value = false
|
||||
formRef.value.resetFields()
|
||||
ElMessage.success('分配成功')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const viewAssignDetail = (row: SimCardProduct) => {
|
||||
currentProduct.value = row
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleCancelAssign = (row: any) => {
|
||||
ElMessageBox.confirm('取消分配后,该代理商将无法继续销售此产品,确定取消吗?', '取消分配', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
// 恢复库存
|
||||
currentProduct.value.stock += row.quantity
|
||||
currentProduct.value.assignedCount -= row.quantity
|
||||
|
||||
const index = assignRecords.value.findIndex((item) => item.id === row.id)
|
||||
if (index !== -1) assignRecords.value.splice(index, 1)
|
||||
ElMessage.success('取消分配成功')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
:deep(.el-select-dropdown__item) {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
275
src/views/product/sim-card/index.vue
Normal file
275
src/views/product/sim-card/index.vue
Normal file
@@ -0,0 +1,275 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<ElRow>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElInput v-model="searchQuery" placeholder="号卡名称/编码" clearable></ElInput>
|
||||
</ElCol>
|
||||
<div style="width: 12px"></div>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElSelect v-model="operatorFilter" placeholder="运营商" clearable style="width: 100%">
|
||||
<ElOption label="中国移动" value="cmcc" />
|
||||
<ElOption label="中国联通" value="cucc" />
|
||||
<ElOption label="中国电信" value="ctcc" />
|
||||
</ElSelect>
|
||||
</ElCol>
|
||||
<div style="width: 12px"></div>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<ElButton v-ripple @click="handleSearch">搜索</ElButton>
|
||||
<ElButton v-ripple @click="showDialog('add')">新增号卡</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ArtTable :data="filteredData" index>
|
||||
<template #default>
|
||||
<ElTableColumn label="号卡名称" prop="cardName" min-width="150" />
|
||||
<ElTableColumn label="号卡编码" prop="cardCode" />
|
||||
<ElTableColumn label="运营商" prop="operator">
|
||||
<template #default="scope">
|
||||
<ElTag :type="getOperatorTagType(scope.row.operator)">
|
||||
{{ getOperatorText(scope.row.operator) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="套餐类型" prop="packageType" />
|
||||
<ElTableColumn label="月租(元)" prop="monthlyFee">
|
||||
<template #default="scope"> ¥{{ scope.row.monthlyFee.toFixed(2) }} </template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="库存" prop="stock" />
|
||||
<ElTableColumn label="状态" prop="status">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status === 'online' ? 'success' : 'info'">
|
||||
{{ scope.row.status === 'online' ? '上架' : '下架' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="createTime" width="180" />
|
||||
<ElTableColumn fixed="right" label="操作" width="180">
|
||||
<template #default="scope">
|
||||
<el-button link @click="showDialog('edit', scope.row)">编辑</el-button>
|
||||
<el-button
|
||||
link
|
||||
:type="scope.row.status === 'online' ? 'danger' : 'primary'"
|
||||
@click="toggleStatus(scope.row)"
|
||||
>
|
||||
{{ scope.row.status === 'online' ? '下架' : '上架' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增号卡' : '编辑号卡'"
|
||||
width="700px"
|
||||
align-center
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="号卡名称" prop="cardName">
|
||||
<ElInput v-model="form.cardName" placeholder="请输入号卡名称" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="号卡编码" prop="cardCode">
|
||||
<ElInput v-model="form.cardCode" placeholder="请输入号卡编码" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="运营商" prop="operator">
|
||||
<ElSelect v-model="form.operator" placeholder="请选择运营商" style="width: 100%">
|
||||
<ElOption label="中国移动" value="cmcc" />
|
||||
<ElOption label="中国联通" value="cucc" />
|
||||
<ElOption label="中国电信" value="ctcc" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="套餐类型" prop="packageType">
|
||||
<ElInput v-model="form.packageType" placeholder="例如:流量套餐" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="月租(元)" prop="monthlyFee">
|
||||
<ElInputNumber v-model="form.monthlyFee" :min="0" :precision="2" style="width: 100%" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="初始库存" prop="stock">
|
||||
<ElInputNumber v-model="form.stock" :min="0" style="width: 100%" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElFormItem label="号卡描述" prop="description">
|
||||
<ElInput v-model="form.description" type="textarea" :rows="3" placeholder="请输入号卡描述" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="状态">
|
||||
<ElSwitch v-model="form.status" active-value="online" inactive-value="offline" />
|
||||
<span style="margin-left: 8px; color: var(--el-text-color-secondary)">{{
|
||||
form.status === 'online' ? '上架' : '下架'
|
||||
}}</span>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'SimCard' })
|
||||
|
||||
interface SimCard {
|
||||
id?: string
|
||||
cardName: string
|
||||
cardCode: string
|
||||
operator: string
|
||||
packageType: string
|
||||
monthlyFee: number
|
||||
stock: number
|
||||
description?: string
|
||||
status: 'online' | 'offline'
|
||||
createTime?: string
|
||||
}
|
||||
|
||||
const mockData = ref<SimCard[]>([
|
||||
{
|
||||
id: '1',
|
||||
cardName: '移动流量卡30GB',
|
||||
cardCode: 'CARD_CMCC_30GB',
|
||||
operator: 'cmcc',
|
||||
packageType: '流量套餐',
|
||||
monthlyFee: 29.9,
|
||||
stock: 1000,
|
||||
description: '移动30GB流量卡,全国通用',
|
||||
status: 'online',
|
||||
createTime: '2026-01-01 10:00:00'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
cardName: '联通流量卡50GB',
|
||||
cardCode: 'CARD_CUCC_50GB',
|
||||
operator: 'cucc',
|
||||
packageType: '流量套餐',
|
||||
monthlyFee: 49.9,
|
||||
stock: 800,
|
||||
description: '联通50GB流量卡,全国通用',
|
||||
status: 'online',
|
||||
createTime: '2026-01-02 11:00:00'
|
||||
}
|
||||
])
|
||||
|
||||
const searchQuery = ref('')
|
||||
const operatorFilter = ref('')
|
||||
const dialogVisible = ref(false)
|
||||
const dialogType = ref<'add' | 'edit'>('add')
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const form = reactive<SimCard>({
|
||||
cardName: '',
|
||||
cardCode: '',
|
||||
operator: '',
|
||||
packageType: '',
|
||||
monthlyFee: 0,
|
||||
stock: 0,
|
||||
description: '',
|
||||
status: 'online'
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
cardName: [{ required: true, message: '请输入号卡名称', trigger: 'blur' }],
|
||||
cardCode: [{ required: true, message: '请输入号卡编码', trigger: 'blur' }],
|
||||
operator: [{ required: true, message: '请选择运营商', trigger: 'change' }],
|
||||
monthlyFee: [{ required: true, message: '请输入月租', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const filteredData = computed(() => {
|
||||
let data = mockData.value
|
||||
if (searchQuery.value) {
|
||||
data = data.filter(
|
||||
(item) => item.cardName.includes(searchQuery.value) || item.cardCode.includes(searchQuery.value)
|
||||
)
|
||||
}
|
||||
if (operatorFilter.value) {
|
||||
data = data.filter((item) => item.operator === operatorFilter.value)
|
||||
}
|
||||
return data
|
||||
})
|
||||
|
||||
const getOperatorText = (operator: string) => {
|
||||
const map: Record<string, string> = { cmcc: '中国移动', cucc: '中国联通', ctcc: '中国电信' }
|
||||
return map[operator] || '未知'
|
||||
}
|
||||
|
||||
const getOperatorTagType = (operator: string) => {
|
||||
const map: Record<string, string> = { cmcc: '', cucc: 'success', ctcc: 'warning' }
|
||||
return map[operator] || 'info'
|
||||
}
|
||||
|
||||
const handleSearch = () => {}
|
||||
|
||||
const showDialog = (type: 'add' | 'edit', row?: SimCard) => {
|
||||
dialogType.value = type
|
||||
dialogVisible.value = true
|
||||
if (type === 'edit' && row) {
|
||||
Object.assign(form, row)
|
||||
} else {
|
||||
Object.assign(form, {
|
||||
cardName: '',
|
||||
cardCode: '',
|
||||
operator: '',
|
||||
packageType: '',
|
||||
monthlyFee: 0,
|
||||
stock: 0,
|
||||
description: '',
|
||||
status: 'online'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
if (dialogType.value === 'add') {
|
||||
mockData.value.push({
|
||||
...form,
|
||||
id: Date.now().toString(),
|
||||
createTime: new Date().toLocaleString('zh-CN')
|
||||
})
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
const index = mockData.value.findIndex((item) => item.id === form.id)
|
||||
if (index !== -1) mockData.value[index] = { ...form }
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
formEl.resetFields()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const toggleStatus = (row: SimCard) => {
|
||||
const action = row.status === 'online' ? '下架' : '上架'
|
||||
ElMessageBox.confirm(`确定要${action}该号卡吗?`, `${action}确认`, {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
row.status = row.status === 'online' ? 'offline' : 'online'
|
||||
ElMessage.success(`${action}成功`)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
22
src/views/result/fail/index.vue
Normal file
22
src/views/result/fail/index.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<ArtResultPage
|
||||
type="fail"
|
||||
title="提交失败"
|
||||
message="请核对并修改以下信息后,再重新提交。"
|
||||
iconCode=""
|
||||
>
|
||||
<template #result-content>
|
||||
<p>您提交的内容有如下错误:</p>
|
||||
<p><i class="icon iconfont-sys"></i>您的账户已被冻结</p>
|
||||
<p><i class="icon iconfont-sys"></i>您的账户还不具备申请资格</p>
|
||||
</template>
|
||||
<template #buttons>
|
||||
<el-button type="primary" v-ripple>返回修改</el-button>
|
||||
<el-button v-ripple>查看</el-button>
|
||||
</template>
|
||||
</ArtResultPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: 'ResultFail' })
|
||||
</script>
|
||||
21
src/views/result/success/index.vue
Normal file
21
src/views/result/success/index.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<ArtResultPage
|
||||
type="success"
|
||||
title="提交成功"
|
||||
message="提交结果页用于反馈一系列操作任务的处理结果,如果仅是简单操作,使用 Message 全局提示反馈即可。灰色区域可以显示一些补充的信息。"
|
||||
iconCode=""
|
||||
>
|
||||
<template #result-content>
|
||||
<p>已提交申请,等待部门审核。</p>
|
||||
</template>
|
||||
<template #buttons>
|
||||
<el-button type="primary" v-ripple>返回修改</el-button>
|
||||
<el-button v-ripple>查看</el-button>
|
||||
<el-button v-ripple>打印</el-button>
|
||||
</template>
|
||||
</ArtResultPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: 'ResultSuccess' })
|
||||
</script>
|
||||
287
src/views/safeguard/server/index.vue
Normal file
287
src/views/safeguard/server/index.vue
Normal file
@@ -0,0 +1,287 @@
|
||||
<template>
|
||||
<div class="page-content server">
|
||||
<div class="list">
|
||||
<div class="middle">
|
||||
<div class="item" v-for="item in serverList" :key="item.name">
|
||||
<div class="header">
|
||||
<span class="name">{{ item.name }}</span>
|
||||
<span class="ip">{{ item.ip }}</span>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="left">
|
||||
<img src="@imgs/safeguard/server.png" alt="服务器" />
|
||||
<ElButtonGroup class="ml-4">
|
||||
<ElButton type="primary" size="default">开机</ElButton>
|
||||
<ElButton type="danger" size="default">关机</ElButton>
|
||||
<ElButton type="warning" size="default">重启</ElButton>
|
||||
</ElButtonGroup>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div>
|
||||
<p>CPU</p>
|
||||
<ElProgress :percentage="item.cup" :text-inside="true" :stroke-width="17" />
|
||||
</div>
|
||||
<div>
|
||||
<p>RAM</p>
|
||||
<ElProgress
|
||||
:percentage="item.memory"
|
||||
status="success"
|
||||
:text-inside="true"
|
||||
:stroke-width="17"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p>SWAP</p>
|
||||
<ElProgress
|
||||
:percentage="item.swap"
|
||||
status="warning"
|
||||
:text-inside="true"
|
||||
:stroke-width="17"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p>DISK</p>
|
||||
<ElProgress
|
||||
:percentage="item.disk"
|
||||
status="success"
|
||||
:text-inside="true"
|
||||
:stroke-width="17"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
defineOptions({ name: 'SafeguardServer' })
|
||||
|
||||
interface ServerInfo {
|
||||
name: string
|
||||
ip: string
|
||||
cup: number
|
||||
memory: number
|
||||
swap: number
|
||||
disk: number
|
||||
}
|
||||
|
||||
const serverList = reactive<ServerInfo[]>([
|
||||
{
|
||||
name: '开发服务器',
|
||||
ip: '192.168.1.100',
|
||||
cup: 85,
|
||||
memory: 65,
|
||||
swap: 45,
|
||||
disk: 92
|
||||
},
|
||||
{
|
||||
name: '测试服务器',
|
||||
ip: '192.168.1.101',
|
||||
cup: 32,
|
||||
memory: 78,
|
||||
swap: 90,
|
||||
disk: 45
|
||||
},
|
||||
{
|
||||
name: '预发布服务器',
|
||||
ip: '192.168.1.102',
|
||||
cup: 95,
|
||||
memory: 42,
|
||||
swap: 67,
|
||||
disk: 88
|
||||
},
|
||||
{
|
||||
name: '线上服务器',
|
||||
ip: '192.168.1.103',
|
||||
cup: 58,
|
||||
memory: 93,
|
||||
swap: 25,
|
||||
disk: 73
|
||||
}
|
||||
])
|
||||
|
||||
// 生成随机数据的函数
|
||||
function generateRandomValue(min = 0, max = 100): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||
}
|
||||
|
||||
// 更新服务器数据
|
||||
function updateServerData() {
|
||||
serverList.forEach((server) => {
|
||||
server.cup = generateRandomValue()
|
||||
server.memory = generateRandomValue()
|
||||
server.swap = generateRandomValue()
|
||||
server.disk = generateRandomValue()
|
||||
})
|
||||
}
|
||||
|
||||
// 修改 timer 类型为 number | null
|
||||
let timer: number | null = null
|
||||
|
||||
onMounted(() => {
|
||||
timer = window.setInterval(updateServerData, 3000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer !== null) {
|
||||
window.clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.server {
|
||||
.list {
|
||||
width: 100%;
|
||||
|
||||
.middle {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: calc(100% + 20px);
|
||||
|
||||
.item {
|
||||
box-sizing: border-box;
|
||||
width: calc(50% - 20px);
|
||||
margin: 0 20px 20px 0;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 4px;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid var(--el-border-color-light);
|
||||
|
||||
.name {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ip {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 40px;
|
||||
|
||||
.left {
|
||||
margin: 0 40px;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 190px;
|
||||
}
|
||||
|
||||
.el-button-group {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: -10px;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
flex: 1;
|
||||
margin-top: 5px;
|
||||
|
||||
> div {
|
||||
margin: 15px 0;
|
||||
|
||||
p {
|
||||
margin-bottom: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $device-notebook) {
|
||||
.server {
|
||||
.list {
|
||||
.middle {
|
||||
.item {
|
||||
.header {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.box {
|
||||
padding: 20px;
|
||||
|
||||
.left {
|
||||
margin: 0 20px 0 0;
|
||||
}
|
||||
|
||||
.right {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $device-ipad-pro) {
|
||||
.server {
|
||||
.list {
|
||||
.middle {
|
||||
.item {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $device-phone) {
|
||||
.server {
|
||||
.list {
|
||||
.middle {
|
||||
.item {
|
||||
width: 100%;
|
||||
|
||||
.header {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.box {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
|
||||
.left {
|
||||
margin: 0;
|
||||
|
||||
img {
|
||||
width: 150px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.el-button-group {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
margin-top: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
295
src/views/settings/commission-template/index.vue
Normal file
295
src/views/settings/commission-template/index.vue
Normal file
@@ -0,0 +1,295 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<ElRow>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElInput v-model="searchQuery" placeholder="模板名称" clearable></ElInput>
|
||||
</ElCol>
|
||||
<div style="width: 12px"></div>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<ElButton v-ripple @click="handleSearch">搜索</ElButton>
|
||||
<ElButton v-ripple @click="showDialog('add')">新增模板</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ArtTable :data="filteredData" index>
|
||||
<template #default>
|
||||
<ElTableColumn label="模板名称" prop="templateName" />
|
||||
<ElTableColumn label="分佣模式" prop="commissionMode">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.commissionMode === 'fixed' ? '' : 'success'">
|
||||
{{ scope.row.commissionMode === 'fixed' ? '固定佣金' : '比例佣金' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="佣金规则" prop="rule">
|
||||
<template #default="scope">
|
||||
{{
|
||||
scope.row.commissionMode === 'fixed'
|
||||
? `¥${scope.row.fixedAmount.toFixed(2)}/笔`
|
||||
: `${scope.row.percent}%`
|
||||
}}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="适用范围" prop="scope" show-overflow-tooltip />
|
||||
<ElTableColumn label="应用次数" prop="usageCount" />
|
||||
<ElTableColumn label="状态" prop="status">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status === 'active' ? 'success' : 'info'">
|
||||
{{ scope.row.status === 'active' ? '启用' : '禁用' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="createTime" width="180" />
|
||||
<ElTableColumn fixed="right" label="操作" width="220">
|
||||
<template #default="scope">
|
||||
<el-button link @click="viewUsage(scope.row)">应用记录</el-button>
|
||||
<el-button link @click="showDialog('edit', scope.row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增分佣模板' : '编辑分佣模板'"
|
||||
width="600px"
|
||||
align-center
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<ElFormItem label="模板名称" prop="templateName">
|
||||
<ElInput v-model="form.templateName" placeholder="请输入模板名称" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="分佣模式" prop="commissionMode">
|
||||
<ElRadioGroup v-model="form.commissionMode">
|
||||
<ElRadio value="fixed">固定佣金</ElRadio>
|
||||
<ElRadio value="percent">比例佣金</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="form.commissionMode === 'fixed'" label="固定金额" prop="fixedAmount">
|
||||
<ElInputNumber v-model="form.fixedAmount" :min="0" :precision="2" style="width: 100%" />
|
||||
<span style="margin-left: 8px">元/笔</span>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="form.commissionMode === 'percent'" label="佣金比例" prop="percent">
|
||||
<ElInputNumber v-model="form.percent" :min="0" :max="100" :precision="2" style="width: 100%" />
|
||||
<span style="margin-left: 8px">%</span>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="适用范围" prop="scope">
|
||||
<ElInput v-model="form.scope" placeholder="例如:全部套餐、特定代理商等" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="分佣说明" prop="description">
|
||||
<ElInput v-model="form.description" type="textarea" :rows="3" placeholder="请输入分佣规则说明" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="状态">
|
||||
<ElSwitch v-model="form.status" active-value="active" inactive-value="inactive" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 应用记录对话框 -->
|
||||
<ElDialog v-model="usageDialogVisible" title="模板应用记录" width="900px" align-center>
|
||||
<ArtTable :data="usageRecords" index>
|
||||
<template #default>
|
||||
<ElTableColumn label="应用对象" prop="targetName" />
|
||||
<ElTableColumn label="对象类型" prop="targetType">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.targetType === 'agent' ? '' : 'success'">
|
||||
{{ scope.row.targetType === 'agent' ? '代理商' : '套餐' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="应用时间" prop="applyTime" width="180" />
|
||||
<ElTableColumn label="操作人" prop="operator" />
|
||||
<ElTableColumn label="状态" prop="status">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status === 'active' ? 'success' : 'info'">
|
||||
{{ scope.row.status === 'active' ? '生效中' : '已失效' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'CommissionTemplate' })
|
||||
|
||||
interface Template {
|
||||
id?: string
|
||||
templateName: string
|
||||
commissionMode: 'fixed' | 'percent'
|
||||
fixedAmount: number
|
||||
percent: number
|
||||
scope: string
|
||||
description?: string
|
||||
usageCount?: number
|
||||
status: 'active' | 'inactive'
|
||||
createTime?: string
|
||||
}
|
||||
|
||||
const mockData = ref<Template[]>([
|
||||
{
|
||||
id: '1',
|
||||
templateName: '标准代理商佣金',
|
||||
commissionMode: 'percent',
|
||||
fixedAmount: 0,
|
||||
percent: 10,
|
||||
scope: '全部套餐',
|
||||
description: '适用于一级代理商的标准佣金模板',
|
||||
usageCount: 25,
|
||||
status: 'active',
|
||||
createTime: '2026-01-01 10:00:00'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
templateName: '特殊套餐固定佣金',
|
||||
commissionMode: 'fixed',
|
||||
fixedAmount: 50,
|
||||
percent: 0,
|
||||
scope: '高端套餐系列',
|
||||
description: '适用于高端套餐的固定佣金',
|
||||
usageCount: 8,
|
||||
status: 'active',
|
||||
createTime: '2026-01-05 11:00:00'
|
||||
}
|
||||
])
|
||||
|
||||
const usageRecords = ref([
|
||||
{
|
||||
id: '1',
|
||||
targetName: '华东区总代理',
|
||||
targetType: 'agent',
|
||||
applyTime: '2026-01-02 10:00:00',
|
||||
operator: 'admin',
|
||||
status: 'active'
|
||||
}
|
||||
])
|
||||
|
||||
const searchQuery = ref('')
|
||||
const dialogVisible = ref(false)
|
||||
const usageDialogVisible = ref(false)
|
||||
const dialogType = ref<'add' | 'edit'>('add')
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const form = reactive<Template>({
|
||||
templateName: '',
|
||||
commissionMode: 'percent',
|
||||
fixedAmount: 0,
|
||||
percent: 0,
|
||||
scope: '',
|
||||
description: '',
|
||||
status: 'active'
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
templateName: [{ required: true, message: '请输入模板名称', trigger: 'blur' }],
|
||||
commissionMode: [{ required: true, message: '请选择分佣模式', trigger: 'change' }],
|
||||
fixedAmount: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (form.commissionMode === 'fixed' && value <= 0) {
|
||||
callback(new Error('固定金额必须大于0'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
percent: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (form.commissionMode === 'percent' && (value <= 0 || value > 100)) {
|
||||
callback(new Error('比例必须在0-100之间'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
scope: [{ required: true, message: '请输入适用范围', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const filteredData = computed(() => {
|
||||
if (!searchQuery.value) return mockData.value
|
||||
return mockData.value.filter((item) => item.templateName.includes(searchQuery.value))
|
||||
})
|
||||
|
||||
const handleSearch = () => {}
|
||||
|
||||
const showDialog = (type: 'add' | 'edit', row?: Template) => {
|
||||
dialogType.value = type
|
||||
dialogVisible.value = true
|
||||
if (type === 'edit' && row) {
|
||||
Object.assign(form, row)
|
||||
} else {
|
||||
Object.assign(form, {
|
||||
templateName: '',
|
||||
commissionMode: 'percent',
|
||||
fixedAmount: 0,
|
||||
percent: 0,
|
||||
scope: '',
|
||||
description: '',
|
||||
status: 'active'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
if (dialogType.value === 'add') {
|
||||
mockData.value.push({
|
||||
...form,
|
||||
id: Date.now().toString(),
|
||||
usageCount: 0,
|
||||
createTime: new Date().toLocaleString('zh-CN')
|
||||
})
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
const index = mockData.value.findIndex((item) => item.id === form.id)
|
||||
if (index !== -1) mockData.value[index] = { ...form }
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
formEl.resetFields()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = (row: Template) => {
|
||||
ElMessageBox.confirm('确定删除该模板吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(() => {
|
||||
const index = mockData.value.findIndex((item) => item.id === row.id)
|
||||
if (index !== -1) mockData.value.splice(index, 1)
|
||||
ElMessage.success('删除成功')
|
||||
})
|
||||
}
|
||||
|
||||
const viewUsage = (row: Template) => {
|
||||
usageDialogVisible.value = true
|
||||
}
|
||||
</script>
|
||||
301
src/views/settings/developer-api/index.vue
Normal file
301
src/views/settings/developer-api/index.vue
Normal file
@@ -0,0 +1,301 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<!-- API密钥管理 -->
|
||||
<ElCard shadow="never">
|
||||
<template #header>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center">
|
||||
<span style="font-weight: 500">API密钥管理</span>
|
||||
<ElButton type="primary" size="small" @click="showCreateDialog">生成新密钥</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ArtTable :data="apiKeyList" index>
|
||||
<template #default>
|
||||
<ElTableColumn label="密钥名称" prop="keyName" />
|
||||
<ElTableColumn label="AppKey" prop="appKey" min-width="200">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center; gap: 8px">
|
||||
<code style="color: var(--el-color-primary)">{{ scope.row.appKey }}</code>
|
||||
<ElButton link :icon="CopyDocument" @click="copyToClipboard(scope.row.appKey)" />
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="AppSecret" prop="appSecret" min-width="200">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center; gap: 8px">
|
||||
<code>{{ scope.row.showSecret ? scope.row.appSecret : '••••••••••••••••' }}</code>
|
||||
<ElButton link :icon="View" @click="scope.row.showSecret = !scope.row.showSecret" />
|
||||
<ElButton link :icon="CopyDocument" @click="copyToClipboard(scope.row.appSecret)" />
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="权限" prop="permissions">
|
||||
<template #default="scope">
|
||||
<ElTag v-for="(perm, index) in scope.row.permissions" :key="index" size="small" style="margin-right: 4px">
|
||||
{{ perm }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" prop="status">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status === 'active' ? 'success' : 'info'">
|
||||
{{ scope.row.status === 'active' ? '启用' : '禁用' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="createTime" width="180" />
|
||||
<ElTableColumn fixed="right" label="操作" width="180">
|
||||
<template #default="scope">
|
||||
<el-button link @click="handleResetKey(scope.row)">重置密钥</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- Webhook配置 -->
|
||||
<ElCard shadow="never" style="margin-top: 20px">
|
||||
<template #header>
|
||||
<span style="font-weight: 500">Webhook配置</span>
|
||||
</template>
|
||||
|
||||
<ElForm :model="webhookForm" label-width="120px" style="max-width: 800px">
|
||||
<ElFormItem label="回调地址">
|
||||
<ElInput v-model="webhookForm.url" placeholder="https://your-domain.com/webhook">
|
||||
<template #prepend>POST</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="签名密钥">
|
||||
<ElInput
|
||||
v-model="webhookForm.secret"
|
||||
:type="showWebhookSecret ? 'text' : 'password'"
|
||||
placeholder="用于验证webhook请求签名"
|
||||
>
|
||||
<template #append>
|
||||
<ElButton :icon="View" @click="showWebhookSecret = !showWebhookSecret" />
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="事件订阅">
|
||||
<ElCheckboxGroup v-model="webhookForm.events">
|
||||
<ElCheckbox value="order.created">订单创建</ElCheckbox>
|
||||
<ElCheckbox value="order.paid">订单支付</ElCheckbox>
|
||||
<ElCheckbox value="card.activated">卡片激活</ElCheckbox>
|
||||
<ElCheckbox value="card.expired">卡片过期</ElCheckbox>
|
||||
<ElCheckbox value="recharge.success">充值成功</ElCheckbox>
|
||||
</ElCheckboxGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem>
|
||||
<ElButton type="primary" @click="saveWebhook">保存配置</ElButton>
|
||||
<ElButton @click="testWebhook">测试推送</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</ElCard>
|
||||
|
||||
<!-- API调用统计 -->
|
||||
<ElCard shadow="never" style="margin-top: 20px">
|
||||
<template #header>
|
||||
<span style="font-weight: 500">API调用统计(最近7天)</span>
|
||||
</template>
|
||||
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<div class="stat-box">
|
||||
<div class="stat-label">总调用次数</div>
|
||||
<div class="stat-value">12,580</div>
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<div class="stat-box">
|
||||
<div class="stat-label">成功次数</div>
|
||||
<div class="stat-value" style="color: var(--el-color-success)">12,453</div>
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<div class="stat-box">
|
||||
<div class="stat-label">失败次数</div>
|
||||
<div class="stat-value" style="color: var(--el-color-danger)">127</div>
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<div class="stat-box">
|
||||
<div class="stat-label">成功率</div>
|
||||
<div class="stat-value">99.0%</div>
|
||||
</div>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</ElCard>
|
||||
|
||||
<!-- 生成密钥对话框 -->
|
||||
<ElDialog v-model="createDialogVisible" title="生成新密钥" width="500px" align-center>
|
||||
<ElForm ref="createFormRef" :model="createForm" :rules="createRules" label-width="100px">
|
||||
<ElFormItem label="密钥名称" prop="keyName">
|
||||
<ElInput v-model="createForm.keyName" placeholder="请输入密钥名称" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="权限设置">
|
||||
<ElCheckboxGroup v-model="createForm.permissions">
|
||||
<ElCheckbox value="读取">读取</ElCheckbox>
|
||||
<ElCheckbox value="写入">写入</ElCheckbox>
|
||||
<ElCheckbox value="删除">删除</ElCheckbox>
|
||||
</ElCheckboxGroup>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="createDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleCreateKey">生成</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { CopyDocument, View } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'DeveloperApi' })
|
||||
|
||||
interface ApiKey {
|
||||
id: string
|
||||
keyName: string
|
||||
appKey: string
|
||||
appSecret: string
|
||||
permissions: string[]
|
||||
status: 'active' | 'inactive'
|
||||
createTime: string
|
||||
showSecret?: boolean
|
||||
}
|
||||
|
||||
const apiKeyList = ref<ApiKey[]>([
|
||||
{
|
||||
id: '1',
|
||||
keyName: '生产环境密钥',
|
||||
appKey: 'ak_prod_1234567890abcdef',
|
||||
appSecret: 'sk_prod_abcdefghijklmnopqrstuvwxyz123456',
|
||||
permissions: ['读取', '写入'],
|
||||
status: 'active',
|
||||
createTime: '2026-01-01 10:00:00',
|
||||
showSecret: false
|
||||
}
|
||||
])
|
||||
|
||||
const webhookForm = reactive({
|
||||
url: 'https://your-domain.com/webhook',
|
||||
secret: 'webhook_secret_key_123456',
|
||||
events: ['order.created', 'order.paid']
|
||||
})
|
||||
|
||||
const showWebhookSecret = ref(false)
|
||||
const createDialogVisible = ref(false)
|
||||
const createFormRef = ref<FormInstance>()
|
||||
|
||||
const createForm = reactive({
|
||||
keyName: '',
|
||||
permissions: ['读取']
|
||||
})
|
||||
|
||||
const createRules = reactive<FormRules>({
|
||||
keyName: [{ required: true, message: '请输入密钥名称', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
ElMessage.success('已复制到剪贴板')
|
||||
})
|
||||
}
|
||||
|
||||
const showCreateDialog = () => {
|
||||
createForm.keyName = ''
|
||||
createForm.permissions = ['读取']
|
||||
createDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleCreateKey = async () => {
|
||||
if (!createFormRef.value) return
|
||||
await createFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
const newKey: ApiKey = {
|
||||
id: Date.now().toString(),
|
||||
keyName: createForm.keyName,
|
||||
appKey: `ak_${Date.now()}`,
|
||||
appSecret: `sk_${Math.random().toString(36).substring(2)}`,
|
||||
permissions: createForm.permissions,
|
||||
status: 'active',
|
||||
createTime: new Date().toLocaleString('zh-CN'),
|
||||
showSecret: false
|
||||
}
|
||||
apiKeyList.value.push(newKey)
|
||||
createDialogVisible.value = false
|
||||
ElMessage.success('密钥生成成功,请妥善保管')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleResetKey = (row: ApiKey) => {
|
||||
ElMessageBox.confirm('重置后原密钥将失效,确定要重置吗?', '重置密钥', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
row.appSecret = `sk_${Math.random().toString(36).substring(2)}`
|
||||
ElMessage.success('密钥重置成功')
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = (row: ApiKey) => {
|
||||
ElMessageBox.confirm('删除后无法恢复,确定要删除吗?', '删除密钥', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(() => {
|
||||
const index = apiKeyList.value.findIndex((item) => item.id === row.id)
|
||||
if (index !== -1) apiKeyList.value.splice(index, 1)
|
||||
ElMessage.success('删除成功')
|
||||
})
|
||||
}
|
||||
|
||||
const saveWebhook = () => {
|
||||
ElMessage.success('Webhook配置保存成功')
|
||||
}
|
||||
|
||||
const testWebhook = () => {
|
||||
ElMessage.info('正在发送测试推送...')
|
||||
setTimeout(() => {
|
||||
ElMessage.success('测试推送成功')
|
||||
}, 1500)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
.stat-box {
|
||||
padding: 20px;
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-checkbox) {
|
||||
margin-right: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
207
src/views/settings/payment-merchant/index.vue
Normal file
207
src/views/settings/payment-merchant/index.vue
Normal file
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<ElCard shadow="never">
|
||||
<template #header>
|
||||
<span style="font-weight: 500">支付商户配置</span>
|
||||
</template>
|
||||
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="150px" style="max-width: 900px">
|
||||
<ElDivider content-position="left">基础信息</ElDivider>
|
||||
|
||||
<ElFormItem label="商户名称" prop="merchantName">
|
||||
<ElInput v-model="form.merchantName" placeholder="请输入商户名称" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="商户编号" prop="merchantId">
|
||||
<ElInput v-model="form.merchantId" placeholder="请输入商户编号" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElDivider content-position="left">API配置</ElDivider>
|
||||
|
||||
<ElFormItem label="AppID" prop="appId">
|
||||
<ElInput v-model="form.appId" placeholder="请输入AppID">
|
||||
<template #append>
|
||||
<ElButton :icon="View" @click="toggleShow('appId')">
|
||||
{{ showFields.appId ? '隐藏' : '显示' }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="AppSecret" prop="appSecret">
|
||||
<ElInput
|
||||
v-model="form.appSecret"
|
||||
:type="showFields.appSecret ? 'text' : 'password'"
|
||||
placeholder="请输入AppSecret"
|
||||
>
|
||||
<template #append>
|
||||
<ElButton :icon="View" @click="toggleShow('appSecret')">
|
||||
{{ showFields.appSecret ? '隐藏' : '显示' }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="API密钥" prop="apiKey">
|
||||
<ElInput
|
||||
v-model="form.apiKey"
|
||||
:type="showFields.apiKey ? 'text' : 'password'"
|
||||
placeholder="请输入API密钥"
|
||||
>
|
||||
<template #append>
|
||||
<ElButton :icon="View" @click="toggleShow('apiKey')">
|
||||
{{ showFields.apiKey ? '隐藏' : '显示' }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
|
||||
<ElDivider content-position="left">回调配置</ElDivider>
|
||||
|
||||
<ElFormItem label="支付回调地址" prop="notifyUrl">
|
||||
<ElInput v-model="form.notifyUrl" placeholder="https://your-domain.com/api/notify">
|
||||
<template #prepend>POST</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="退款回调地址" prop="refundNotifyUrl">
|
||||
<ElInput v-model="form.refundNotifyUrl" placeholder="https://your-domain.com/api/refund-notify">
|
||||
<template #prepend>POST</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
|
||||
<ElDivider content-position="left">支付方式</ElDivider>
|
||||
|
||||
<ElFormItem label="启用的支付方式">
|
||||
<ElCheckboxGroup v-model="form.paymentMethods">
|
||||
<ElCheckbox value="wechat">
|
||||
<div style="display: flex; align-items: center; gap: 8px">
|
||||
<span style="color: #09bb07; font-size: 20px">💬</span>
|
||||
<span>微信支付</span>
|
||||
</div>
|
||||
</ElCheckbox>
|
||||
<ElCheckbox value="alipay">
|
||||
<div style="display: flex; align-items: center; gap: 8px">
|
||||
<span style="color: #1677ff; font-size: 20px">💳</span>
|
||||
<span>支付宝</span>
|
||||
</div>
|
||||
</ElCheckbox>
|
||||
<ElCheckbox value="bank">
|
||||
<div style="display: flex; align-items: center; gap: 8px">
|
||||
<span style="font-size: 20px">🏦</span>
|
||||
<span>银行卡</span>
|
||||
</div>
|
||||
</ElCheckbox>
|
||||
</ElCheckboxGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="测试模式">
|
||||
<ElSwitch v-model="form.testMode" />
|
||||
<span style="margin-left: 8px; color: var(--el-text-color-secondary)">
|
||||
{{ form.testMode ? '开启(使用沙箱环境)' : '关闭(生产环境)' }}
|
||||
</span>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem>
|
||||
<ElButton type="primary" @click="handleSave">保存配置</ElButton>
|
||||
<ElButton @click="handleTest">测试连接</ElButton>
|
||||
<ElButton @click="resetForm">重置</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</ElCard>
|
||||
|
||||
<ElCard shadow="never" style="margin-top: 20px">
|
||||
<template #header>
|
||||
<span style="font-weight: 500">配置说明</span>
|
||||
</template>
|
||||
<ElAlert type="info" :closable="false">
|
||||
<template #title>
|
||||
<div style="line-height: 1.8">
|
||||
<p><strong>AppID/AppSecret</strong>: 从支付服务商后台获取</p>
|
||||
<p><strong>API密钥</strong>: 用于签名验证,请妥善保管</p>
|
||||
<p><strong>回调地址</strong>: 支付完成后,支付平台会向该地址发送支付结果通知</p>
|
||||
<p><strong>测试模式</strong>: 开启后使用沙箱环境,不会产生真实交易</p>
|
||||
</div>
|
||||
</template>
|
||||
</ElAlert>
|
||||
</ElCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { View } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'PaymentMerchant' })
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const form = reactive({
|
||||
merchantName: '某某科技有限公司',
|
||||
merchantId: 'MCH123456789',
|
||||
appId: 'wx1234567890abcdef',
|
||||
appSecret: '********************************',
|
||||
apiKey: '********************************',
|
||||
notifyUrl: 'https://your-domain.com/api/payment/notify',
|
||||
refundNotifyUrl: 'https://your-domain.com/api/payment/refund-notify',
|
||||
paymentMethods: ['wechat', 'alipay'],
|
||||
testMode: true
|
||||
})
|
||||
|
||||
const showFields = reactive({
|
||||
appId: false,
|
||||
appSecret: false,
|
||||
apiKey: false
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
merchantName: [{ required: true, message: '请输入商户名称', trigger: 'blur' }],
|
||||
merchantId: [{ required: true, message: '请输入商户编号', trigger: 'blur' }],
|
||||
appId: [{ required: true, message: '请输入AppID', trigger: 'blur' }],
|
||||
appSecret: [{ required: true, message: '请输入AppSecret', trigger: 'blur' }],
|
||||
apiKey: [{ required: true, message: '请输入API密钥', trigger: 'blur' }],
|
||||
notifyUrl: [
|
||||
{ required: true, message: '请输入支付回调地址', trigger: 'blur' },
|
||||
{ type: 'url', message: '请输入正确的URL地址', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
const toggleShow = (field: 'appId' | 'appSecret' | 'apiKey') => {
|
||||
showFields[field] = !showFields[field]
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
ElMessage.success('配置保存成功')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleTest = () => {
|
||||
ElMessage.info('正在测试连接...')
|
||||
setTimeout(() => {
|
||||
ElMessage.success('连接测试成功')
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
:deep(.el-checkbox) {
|
||||
margin-right: 30px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-divider__text) {
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
661
src/views/system/menu/index.vue
Normal file
661
src/views/system/menu/index.vue
Normal file
@@ -0,0 +1,661 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="menu-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
:showExpand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
:showZebra="false"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<!-- 按钮权限:后端控制模式,使用自定义指令 -->
|
||||
<ElButton v-auth="'add'" @click="showModel('menu', null, true)" v-ripple>
|
||||
添加菜单
|
||||
</ElButton>
|
||||
<ElButton @click="toggleExpand" v-ripple>
|
||||
{{ isExpanded ? '收起' : '展开' }}
|
||||
</ElButton>
|
||||
<!-- 按钮权限:前端控制模式,使用 hasAuth 方法 -->
|
||||
<!-- <ElButton v-if="hasAuth('B_CODE1')" @click="showModel('menu', null, true)" v-ripple>
|
||||
添加菜单
|
||||
</ElButton> -->
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
rowKey="path"
|
||||
ref="tableRef"
|
||||
:loading="loading"
|
||||
:data="filteredTableData"
|
||||
:marginTop="10"
|
||||
:stripe="false"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<ElDialog :title="dialogTitle" v-model="dialogVisible" width="700px" align-center>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="85px">
|
||||
<ElFormItem label="菜单类型">
|
||||
<ElRadioGroup v-model="labelPosition" :disabled="disableMenuType">
|
||||
<ElRadioButton value="menu" label="menu">菜单</ElRadioButton>
|
||||
<ElRadioButton value="button" label="button">权限</ElRadioButton>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<template v-if="labelPosition === 'menu'">
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="菜单名称" prop="name">
|
||||
<ElInput v-model="form.name" placeholder="菜单名称"></ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="路由地址" prop="path">
|
||||
<ElInput v-model="form.path" placeholder="路由地址"></ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="权限标识" prop="label">
|
||||
<ElInput v-model="form.label" placeholder="权限标识"></ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="图标" prop="icon">
|
||||
<ArtIconSelector :iconType="iconType" :defaultIcon="form.icon" width="229px" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="菜单排序" prop="sort" style="width: 100%">
|
||||
<ElInputNumber
|
||||
v-model="form.sort"
|
||||
style="width: 100%"
|
||||
@change="handleChange"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="外部链接" prop="link">
|
||||
<ElInput
|
||||
v-model="form.link"
|
||||
placeholder="外部链接/内嵌地址(https://www.baidu.com)"
|
||||
></ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="5">
|
||||
<ElFormItem label="是否启用" prop="isEnable">
|
||||
<ElSwitch v-model="form.isEnable"></ElSwitch>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="5">
|
||||
<ElFormItem label="页面缓存" prop="keepAlive">
|
||||
<ElSwitch v-model="form.keepAlive"></ElSwitch>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="5">
|
||||
<ElFormItem label="是否显示" prop="isHidden">
|
||||
<ElSwitch v-model="form.isHidden"></ElSwitch>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="5">
|
||||
<ElFormItem label="是否内嵌" prop="isMenu">
|
||||
<ElSwitch v-model="form.isIframe"></ElSwitch>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</template>
|
||||
|
||||
<template v-if="labelPosition === 'button'">
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="权限名称" prop="authName">
|
||||
<ElInput v-model="form.authName" placeholder="权限名称"></ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="权限标识" prop="authLabel">
|
||||
<ElInput v-model="form.authLabel" placeholder="权限标识"></ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="权限排序" prop="authSort" style="width: 100%">
|
||||
<ElInputNumber
|
||||
v-model="form.authSort"
|
||||
style="width: 100%"
|
||||
@change="handleChange"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取 消</ElButton>
|
||||
<ElButton type="primary" @click="submitForm()">确 定</ElButton>
|
||||
</span>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useMenuStore } from '@/store/modules/menu'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { IconTypeEnum } from '@/enums/appEnum'
|
||||
import { formatMenuTitle } from '@/router/utils/utils'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { ElPopover, ElButton } from 'element-plus'
|
||||
import { AppRouteRecord } from '@/types/router'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { SearchFormItem } from '@/types'
|
||||
|
||||
defineOptions({ name: 'Menus' })
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
const { menuList } = storeToRefs(useMenuStore())
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
name: '',
|
||||
route: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
// 增加实际应用的搜索条件状态
|
||||
const appliedFilters = reactive({ ...initialSearchState })
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
Object.assign(appliedFilters, { ...initialSearchState })
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
// 将当前输入的筛选条件应用到实际搜索
|
||||
Object.assign(appliedFilters, { ...formFilters })
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '菜单名称',
|
||||
prop: 'name',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '路由地址',
|
||||
prop: 'route',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: '用户名', prop: 'avatar' },
|
||||
{ label: '手机号', prop: 'mobile' },
|
||||
{ label: '性别', prop: 'sex' },
|
||||
{ label: '部门', prop: 'dep' },
|
||||
{ label: '状态', prop: 'status' },
|
||||
{ label: '创建日期', prop: 'create_time' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 构建菜单类型标签
|
||||
const buildMenuTypeTag = (row: AppRouteRecord) => {
|
||||
if (row.children && row.children.length > 0) {
|
||||
return 'info'
|
||||
} else if (row.meta?.link && row.meta?.isIframe) {
|
||||
return 'success'
|
||||
} else if (row.path) {
|
||||
return 'primary'
|
||||
} else if (row.meta?.link) {
|
||||
return 'warning'
|
||||
}
|
||||
}
|
||||
|
||||
// 构建菜单类型文本
|
||||
const buildMenuTypeText = (row: AppRouteRecord) => {
|
||||
if (row.children && row.children.length > 0) {
|
||||
return '目录'
|
||||
} else if (row.meta?.link && row.meta?.isIframe) {
|
||||
return '内嵌'
|
||||
} else if (row.path) {
|
||||
return '菜单'
|
||||
} else if (row.meta?.link) {
|
||||
return '外链'
|
||||
}
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'meta.title',
|
||||
label: '菜单名称',
|
||||
minWidth: 120,
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
return formatMenuTitle(row.meta?.title)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'type',
|
||||
label: '菜单类型',
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
return h(ElTag, { type: buildMenuTypeTag(row) }, () => buildMenuTypeText(row))
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'path',
|
||||
label: '路由',
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
return row.meta?.link || row.path || ''
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'meta.authList',
|
||||
label: '可操作权限',
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
return h(
|
||||
'div',
|
||||
{},
|
||||
row.meta.authList?.map((item: { title: string; auth_mark: string }, index: number) => {
|
||||
return h(
|
||||
ElPopover,
|
||||
{
|
||||
placement: 'top-start',
|
||||
title: '操作',
|
||||
width: 200,
|
||||
trigger: 'click',
|
||||
key: index
|
||||
},
|
||||
{
|
||||
default: () =>
|
||||
h('div', { style: 'margin: 0; text-align: right' }, [
|
||||
h(
|
||||
ElButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
onClick: () => showModel('button', item)
|
||||
},
|
||||
{ default: () => '编辑' }
|
||||
),
|
||||
h(
|
||||
ElButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'danger',
|
||||
onClick: () => deleteAuth()
|
||||
},
|
||||
{ default: () => '删除' }
|
||||
)
|
||||
]),
|
||||
reference: () => h(ElButton, { class: 'small-btn' }, { default: () => item.title })
|
||||
}
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'date',
|
||||
label: '编辑时间',
|
||||
formatter: () => '2022-3-12 12:00:00'
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '隐藏菜单',
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: row.meta.isHide ? 'danger' : 'info' }, () =>
|
||||
row.meta.isHide ? '是' : '否'
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 180,
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
return h('div', [
|
||||
hasAuth('B_CODE1') &&
|
||||
h(ArtButtonTable, {
|
||||
type: 'add',
|
||||
onClick: () => showModel('menu')
|
||||
}),
|
||||
hasAuth('B_CODE2') &&
|
||||
h(ArtButtonTable, {
|
||||
type: 'edit',
|
||||
onClick: () => showDialog('edit', row)
|
||||
}),
|
||||
hasAuth('B_CODE3') &&
|
||||
h(ArtButtonTable, {
|
||||
type: 'delete',
|
||||
onClick: () => deleteMenu()
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
const handleRefresh = () => {
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const form = reactive({
|
||||
// 菜单
|
||||
name: '',
|
||||
path: '',
|
||||
label: '',
|
||||
icon: '',
|
||||
isEnable: true,
|
||||
sort: 1,
|
||||
isMenu: true,
|
||||
keepAlive: true,
|
||||
isHidden: true,
|
||||
link: '',
|
||||
isIframe: false,
|
||||
// 权限 (修改这部分)
|
||||
authName: '',
|
||||
authLabel: '',
|
||||
authIcon: '',
|
||||
authSort: 1
|
||||
})
|
||||
const iconType = ref(IconTypeEnum.UNICODE)
|
||||
|
||||
const labelPosition = ref('menu')
|
||||
const rules = reactive<FormRules>({
|
||||
name: [
|
||||
{ required: true, message: '请输入菜单名称', trigger: 'blur' },
|
||||
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
path: [{ required: true, message: '请输入路由地址', trigger: 'blur' }],
|
||||
label: [{ required: true, message: '输入权限标识', trigger: 'blur' }],
|
||||
// 修改这部分
|
||||
authName: [{ required: true, message: '请输入权限名称', trigger: 'blur' }],
|
||||
authLabel: [{ required: true, message: '请输入权限权限标识', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const tableData = ref<AppRouteRecord[]>([])
|
||||
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
})
|
||||
|
||||
const getTableData = () => {
|
||||
loading.value = true
|
||||
setTimeout(() => {
|
||||
tableData.value = menuList.value
|
||||
loading.value = false
|
||||
}, 500)
|
||||
}
|
||||
|
||||
// 过滤后的表格数据
|
||||
const filteredTableData = computed(() => {
|
||||
// 递归搜索函数
|
||||
const searchMenu = (items: AppRouteRecord[]): AppRouteRecord[] => {
|
||||
return items.filter((item) => {
|
||||
// 获取搜索关键词,转换为小写并去除首尾空格
|
||||
const searchName = appliedFilters.name?.toLowerCase().trim() || ''
|
||||
const searchRoute = appliedFilters.route?.toLowerCase().trim() || ''
|
||||
|
||||
// 获取菜单标题和路径,确保它们存在
|
||||
const menuTitle = formatMenuTitle(item.meta?.title || '').toLowerCase()
|
||||
const menuPath = (item.path || '').toLowerCase()
|
||||
|
||||
// 使用 includes 进行模糊匹配
|
||||
const nameMatch = !searchName || menuTitle.includes(searchName)
|
||||
const routeMatch = !searchRoute || menuPath.includes(searchRoute)
|
||||
|
||||
// 如果有子菜单,递归搜索
|
||||
if (item.children && item.children.length > 0) {
|
||||
const matchedChildren = searchMenu(item.children)
|
||||
// 如果子菜单有匹配项,保留当前菜单
|
||||
if (matchedChildren.length > 0) {
|
||||
item.children = matchedChildren
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return nameMatch && routeMatch
|
||||
})
|
||||
}
|
||||
|
||||
return searchMenu(tableData.value)
|
||||
})
|
||||
|
||||
const isEdit = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
const dialogTitle = computed(() => {
|
||||
const type = labelPosition.value === 'menu' ? '菜单' : '权限'
|
||||
return isEdit.value ? `编辑${type}` : `新建${type}`
|
||||
})
|
||||
|
||||
const showDialog = (type: string, row: AppRouteRecord) => {
|
||||
showModel('menu', row, true)
|
||||
}
|
||||
|
||||
const handleChange = () => {}
|
||||
|
||||
const submitForm = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
// const menuStore = useMenuStore()
|
||||
// const params =
|
||||
// labelPosition.value === 'menu'
|
||||
// ? {
|
||||
// title: form.name,
|
||||
// path: form.path,
|
||||
// name: form.label,
|
||||
// icon: form.icon,
|
||||
// sort: form.sort,
|
||||
// isEnable: form.isEnable,
|
||||
// isMenu: form.isMenu,
|
||||
// keepAlive: form.keepAlive,
|
||||
// isHidden: form.isHidden,
|
||||
// link: form.link
|
||||
// }
|
||||
// : {
|
||||
// title: form.authName,
|
||||
// name: form.authLabel,
|
||||
// icon: form.authIcon,
|
||||
// sort: form.authSort
|
||||
// }
|
||||
|
||||
if (isEdit.value) {
|
||||
// await menuStore.updateMenu(params)
|
||||
} else {
|
||||
// await menuStore.addMenu(params)
|
||||
}
|
||||
|
||||
ElMessage.success(`${isEdit.value ? '编辑' : '新增'}成功`)
|
||||
dialogVisible.value = false
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const showModel = (type: string, row?: any, lock: boolean = false) => {
|
||||
dialogVisible.value = true
|
||||
labelPosition.value = type
|
||||
isEdit.value = false
|
||||
lockMenuType.value = lock
|
||||
resetForm()
|
||||
|
||||
if (row) {
|
||||
isEdit.value = true
|
||||
nextTick(() => {
|
||||
// 回显数据
|
||||
if (type === 'menu') {
|
||||
// 菜单数据回显
|
||||
form.name = formatMenuTitle(row.meta.title)
|
||||
form.path = row.path
|
||||
form.label = row.name
|
||||
form.icon = row.meta.icon
|
||||
form.sort = row.meta.sort || 1
|
||||
form.isMenu = row.meta.isMenu
|
||||
form.keepAlive = row.meta.keepAlive
|
||||
form.isHidden = row.meta.isHidden || true
|
||||
form.isEnable = row.meta.isEnable || true
|
||||
form.link = row.meta.link
|
||||
form.isIframe = row.meta.isIframe || false
|
||||
} else {
|
||||
// 权限按钮数据回显
|
||||
form.authName = row.title
|
||||
form.authLabel = row.auth_mark
|
||||
form.authIcon = row.icon || ''
|
||||
form.authSort = row.sort || 1
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
formRef.value?.resetFields()
|
||||
Object.assign(form, {
|
||||
// 菜单
|
||||
name: '',
|
||||
path: '',
|
||||
label: '',
|
||||
icon: '',
|
||||
sort: 1,
|
||||
isMenu: true,
|
||||
keepAlive: true,
|
||||
isHidden: true,
|
||||
link: '',
|
||||
isIframe: false,
|
||||
// 权限
|
||||
authName: '',
|
||||
authLabel: '',
|
||||
authIcon: '',
|
||||
authSort: 1
|
||||
})
|
||||
}
|
||||
|
||||
const deleteMenu = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该菜单吗?删除后无法恢复', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
ElMessage.success('删除成功')
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const deleteAuth = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该权限吗?删除后无法恢复', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
ElMessage.success('删除成功')
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 修改计算属性,增加锁定控制参数
|
||||
const disableMenuType = computed(() => {
|
||||
// 编辑权限时锁定为权限类型
|
||||
if (isEdit.value && labelPosition.value === 'button') return true
|
||||
// 编辑菜单时锁定为菜单类型
|
||||
if (isEdit.value && labelPosition.value === 'menu') return true
|
||||
// 顶部添加菜单按钮时锁定为菜单类型
|
||||
if (!isEdit.value && labelPosition.value === 'menu' && lockMenuType.value) return true
|
||||
return false
|
||||
})
|
||||
|
||||
// 添加一个控制变量
|
||||
const lockMenuType = ref(false)
|
||||
|
||||
const isExpanded = ref(false)
|
||||
const tableRef = ref()
|
||||
|
||||
const toggleExpand = () => {
|
||||
isExpanded.value = !isExpanded.value
|
||||
nextTick(() => {
|
||||
if (tableRef.value) {
|
||||
tableRef.value[isExpanded.value ? 'expandAll' : 'collapseAll']()
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.menu-page {
|
||||
.svg-icon {
|
||||
width: 1.8em;
|
||||
height: 1.8em;
|
||||
overflow: hidden;
|
||||
vertical-align: -8px;
|
||||
fill: currentcolor;
|
||||
}
|
||||
|
||||
:deep(.small-btn) {
|
||||
height: 30px !important;
|
||||
padding: 0 10px !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
5
src/views/system/nested/menu1/index.vue
Normal file
5
src/views/system/nested/menu1/index.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<h1>菜单-1</h1>
|
||||
</div>
|
||||
</template>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user