fetch(add): 账户管理

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

View File

@@ -31,6 +31,9 @@
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
:default-expand-all="false"
:pagination="false"
@selection-change="handleSelectionChange"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@@ -62,8 +65,24 @@
<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 label="上级店铺" prop="parent_id">
<ElSelect
v-model="formData.parent_id"
placeholder="一级店铺可不选"
filterable
remote
:remote-method="searchParentShops"
:loading="parentShopLoading"
clearable
style="width: 100%"
>
<ElOption
v-for="shop in parentShopList"
:key="shop.id"
:label="shop.shop_name"
:value="shop.id"
/>
</ElSelect>
</ElFormItem>
</ElCol>
</ElRow>
@@ -102,7 +121,11 @@
</ElCol>
<ElCol :span="12">
<ElFormItem label="联系电话" prop="contact_phone">
<ElInput v-model="formData.contact_phone" placeholder="请输入联系电话" maxlength="11" />
<ElInput
v-model="formData.contact_phone"
placeholder="请输入联系电话"
maxlength="11"
/>
</ElFormItem>
</ElCol>
</ElRow>
@@ -118,14 +141,23 @@
</ElCol>
<ElCol :span="12">
<ElFormItem label="密码" prop="init_password">
<ElInput v-model="formData.init_password" type="password" placeholder="请输入初始账号密码" show-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" />
<ElInput
v-model="formData.init_phone"
placeholder="请输入初始账号手机号"
maxlength="11"
/>
</ElFormItem>
</ElCol>
</ElRow>
@@ -142,7 +174,9 @@
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading">提交</ElButton>
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading"
>提交</ElButton
>
</div>
</template>
</ElDialog>
@@ -153,7 +187,15 @@
<script setup lang="ts">
import { h } from 'vue'
import { FormInstance, ElMessage, ElMessageBox, ElTag, ElSwitch } from 'element-plus'
import {
FormInstance,
ElMessage,
ElMessageBox,
ElTag,
ElSwitch,
ElSelect,
ElOption
} from 'element-plus'
import type { FormRules } from 'element-plus'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
@@ -169,6 +211,9 @@
const dialogVisible = ref(false)
const loading = ref(false)
const submitLoading = ref(false)
const parentShopLoading = ref(false)
const parentShopList = ref<ShopResponse[]>([])
const searchParentShopList = ref<ShopResponse[]>([])
// 定义表单搜索初始值
const initialSearchState = {
@@ -211,7 +256,7 @@
}
// 表单配置项
const searchFormItems: SearchFormItem[] = [
const searchFormItems = computed<SearchFormItem[]>(() => [
{
label: '店铺名称',
prop: 'shop_name',
@@ -231,12 +276,20 @@
}
},
{
label: '上级ID',
label: '上级店铺',
prop: 'parent_id',
type: 'input',
type: 'select',
options: searchParentShopList.value.map((shop) => ({
label: shop.shop_name,
value: shop.id
})),
config: {
clearable: true,
placeholder: '请输入上级店铺ID'
filterable: true,
remote: true,
remoteMethod: handleSearchParentShop,
loading: parentShopLoading.value,
placeholder: '请选择或搜索上级店铺'
}
},
{
@@ -267,15 +320,13 @@
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' },
@@ -286,19 +337,13 @@
// 显示对话框
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.parent_id = undefined
formData.province = row.province || ''
formData.city = row.city || ''
formData.district = row.district || ''
@@ -313,7 +358,7 @@
formData.id = 0
formData.shop_name = ''
formData.shop_code = ''
formData.parent_id = null
formData.parent_id = undefined
formData.province = ''
formData.city = ''
formData.district = ''
@@ -325,6 +370,13 @@
formData.init_password = ''
formData.init_phone = ''
}
// 重置表单验证状态
nextTick(() => {
formRef.value?.clearValidate()
})
dialogVisible.value = true
}
// 删除店铺
@@ -350,11 +402,6 @@
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'id',
label: 'ID',
width: 80
},
{
prop: 'shop_name',
label: '店铺名称',
@@ -373,12 +420,6 @@
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: '所在地区',
@@ -388,6 +429,7 @@
if (row.province) parts.push(row.province)
if (row.city) parts.push(row.city)
if (row.district) parts.push(row.district)
if (row.address) parts.push(row.address)
return parts.length > 0 ? parts.join(' / ') : '-'
}
},
@@ -413,7 +455,7 @@
activeText: getStatusText(CommonStatus.ENABLED),
inactiveText: getStatusText(CommonStatus.DISABLED),
inlinePrompt: true,
'onUpdate:modelValue': (val: number) => handleStatusChange(row, val)
'onUpdate:modelValue': (val: string | number | boolean) => handleStatusChange(row, val as number)
})
}
},
@@ -451,7 +493,7 @@
id: 0,
shop_name: '',
shop_code: '',
parent_id: null as number | null,
parent_id: undefined as number | undefined,
province: '',
city: '',
district: '',
@@ -466,15 +508,76 @@
onMounted(() => {
getShopList()
loadParentShopList()
loadSearchParentShopList()
})
// 加载上级店铺列表(用于新增对话框,默认加载20条)
const loadParentShopList = async (shopName?: string) => {
parentShopLoading.value = true
try {
const params: any = {
page: 1,
pageSize: 20
}
if (shopName) {
params.shop_name = shopName
}
const res = await ShopService.getShops(params)
if (res.code === 0) {
parentShopList.value = res.data.items || []
}
} catch (error) {
console.error('获取上级店铺列表失败:', error)
} finally {
parentShopLoading.value = false
}
}
// 加载搜索栏上级店铺列表(默认加载20条)
const loadSearchParentShopList = async (shopName?: string) => {
try {
const params: any = {
page: 1,
pageSize: 20
}
if (shopName) {
params.shop_name = shopName
}
const res = await ShopService.getShops(params)
if (res.code === 0) {
searchParentShopList.value = res.data.items || []
}
} catch (error) {
console.error('获取上级店铺列表失败:', error)
}
}
// 搜索上级店铺(用于新增对话框)
const searchParentShops = (query: string) => {
if (query) {
loadParentShopList(query)
} else {
loadParentShopList()
}
}
// 搜索上级店铺(用于搜索栏)
const handleSearchParentShop = (query: string) => {
if (query) {
loadSearchParentShopList(query)
} else {
loadSearchParentShopList()
}
}
// 获取店铺列表
const getShopList = async () => {
loading.value = true
try {
const params = {
page: pagination.currentPage,
page_size: pagination.pageSize,
page: 1,
page_size: 9999, // 获取所有数据用于构建树形结构
shop_name: searchForm.shop_name || undefined,
shop_code: searchForm.shop_code || undefined,
parent_id: searchForm.parent_id,
@@ -483,7 +586,8 @@
}
const res = await ShopService.getShops(params)
if (res.code === 0) {
tableData.value = res.data.items || []
const items = res.data.items || []
tableData.value = buildTreeData(items)
pagination.total = res.data.total || 0
}
} catch (error) {
@@ -493,6 +597,33 @@
}
}
// 构建树形数据
const buildTreeData = (items: ShopResponse[]) => {
const map = new Map<number, ShopResponse & { children?: ShopResponse[] }>()
const tree: ShopResponse[] = []
// 先将所有项放入 map
items.forEach((item) => {
map.set(item.id, { ...item, children: [] })
})
// 构建树形结构
items.forEach((item) => {
const node = map.get(item.id)!
if (item.parent_id && map.has(item.parent_id)) {
// 有父节点,添加到父节点的 children 中
const parent = map.get(item.parent_id)!
if (!parent.children) parent.children = []
parent.children.push(node)
} else {
// 没有父节点或父节点不存在,作为根节点
tree.push(node)
}
})
return tree
}
const handleRefresh = () => {
getShopList()
}
@@ -512,19 +643,15 @@
{ 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' }
],
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' }
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
],
init_password: [
{ required: true, message: '请输入初始账号密码', trigger: 'blur' },
@@ -611,7 +738,10 @@
// 先更新UI
row.status = newStatus
try {
await ShopService.updateShop(row.id, { status: newStatus })
await ShopService.updateShop(row.id, {
shop_name: row.shop_name,
status: newStatus
})
ElMessage.success('状态切换成功')
} catch (error) {
// 切换失败,恢复原状态

View File

@@ -27,7 +27,12 @@
</ElRow>
<!-- 号卡产品列表 -->
<ArtTable :data="filteredData" index style="margin-top: 20px" @selection-change="handleSelectionChange">
<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 />
@@ -96,7 +101,7 @@
:max="currentProduct.stock"
style="width: 100%"
/>
<div style="color: var(--el-text-color-secondary); font-size: 12px; margin-top: 4px">
<div style="margin-top: 4px; font-size: 12px; color: var(--el-text-color-secondary)">
当前库存{{ currentProduct.stock }}
</div>
</ElFormItem>
@@ -109,18 +114,41 @@
</ElRadioGroup>
</ElFormItem>
<ElFormItem v-if="assignForm.commissionMode === 'fixed'" label="固定金额" prop="fixedAmount">
<ElInputNumber v-model="assignForm.fixedAmount" :min="0" :precision="2" style="width: 100%" />
<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%" />
<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%">
<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"
@@ -128,7 +156,7 @@
:value="template.id"
>
<span>{{ template.templateName }}</span>
<span style="float: right; color: var(--el-text-color-secondary); font-size: 12px">
<span style="float: right; font-size: 12px; color: var(--el-text-color-secondary)">
{{ template.mode === 'fixed' ? `¥${template.value}元/张` : `${template.value}%` }}
</span>
</ElOption>
@@ -136,15 +164,26 @@
</ElFormItem>
<ElFormItem label="特殊折扣" prop="discount">
<ElInputNumber v-model="assignForm.discount" :min="0" :max="100" :precision="2" style="width: 100%" />
<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">
<div style="margin-top: 4px; font-size: 12px; color: var(--el-text-color-secondary)">
0表示无折扣设置后代理商可以此折扣价格销售
</div>
</ElFormItem>
<ElFormItem label="备注" prop="remark">
<ElInput v-model="assignForm.remark" type="textarea" :rows="3" placeholder="请输入备注信息" />
<ElInput
v-model="assignForm.remark"
type="textarea"
:rows="3"
placeholder="请输入备注信息"
/>
</ElFormItem>
</ElForm>
@@ -165,7 +204,9 @@
<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-if="scope.row.commissionMode === 'percent'" type="success"
>比例佣金</ElTag
>
<ElTag v-else>模板佣金</ElTag>
</template>
</ElTableColumn>
@@ -179,7 +220,9 @@
<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>
<el-button link type="danger" @click="handleCancelAssign(scope.row)"
>取消分配</el-button
>
</template>
</ElTableColumn>
</template>
@@ -263,7 +306,7 @@
productName: '移动4G流量卡-月包100GB',
operator: 'CMCC',
packageSpec: '100GB/月有效期1年',
price: 80.00,
price: 80.0,
stock: 1000,
assignedCount: 500
},
@@ -272,7 +315,7 @@
productName: '联通5G流量卡-季包300GB',
operator: 'CUCC',
packageSpec: '300GB/季有效期1年',
price: 220.00,
price: 220.0,
stock: 500,
assignedCount: 200
},
@@ -281,7 +324,7 @@
productName: '电信物联网卡-年包1TB',
operator: 'CTCC',
packageSpec: '1TB/年有效期2年',
price: 800.00,
price: 800.0,
stock: 80,
assignedCount: 0
}

View File

@@ -96,7 +96,12 @@
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="月租(元)" prop="monthlyFee">
<ElInputNumber v-model="form.monthlyFee" :min="0" :precision="2" style="width: 100%" />
<ElInputNumber
v-model="form.monthlyFee"
:min="0"
:precision="2"
style="width: 100%"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
@@ -106,7 +111,12 @@
</ElCol>
</ElRow>
<ElFormItem label="号卡描述" prop="description">
<ElInput v-model="form.description" type="textarea" :rows="3" placeholder="请输入号卡描述" />
<ElInput
v-model="form.description"
type="textarea"
:rows="3"
placeholder="请输入号卡描述"
/>
</ElFormItem>
<ElFormItem label="状态">
<ElSwitch v-model="form.status" active-value="online" inactive-value="offline" />
@@ -199,7 +209,8 @@
let data = mockData.value
if (searchQuery.value) {
data = data.filter(
(item) => item.cardName.includes(searchQuery.value) || item.cardCode.includes(searchQuery.value)
(item) =>
item.cardName.includes(searchQuery.value) || item.cardCode.includes(searchQuery.value)
)
}
if (operatorFilter.value) {