小如首页
This commit is contained in:
1 parent
6a9abdce9f
commit
cd7f291892
13 files changed
+1264
-36
No files matched your search
@@ -66,6 +66,7 @@ export interface Spu {
|
||||
deviceId?: number // 设备号
|
||||
purchaseLimitType?: number // 限购类型:0-不限购,1-每天限购,2-每周限购,3-终身限购
|
||||
purchaseLimitCount?: number // 限购数量
|
||||
hotRecommend?: boolean // 是否热门推荐(分页筛选:不传则不筛;传 true/false 精确匹配)
|
||||
}
|
||||
|
||||
// 获得 Spu 列表
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface DiscountActivityVO {
|
||||
id?: number
|
||||
spuId?: number
|
||||
name?: string
|
||||
picUrl?: string
|
||||
status?: number
|
||||
remark?: string
|
||||
startTime?: Date
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
/** 榜单内商品(响应) */
|
||||
export interface RankingSpuResp {
|
||||
id: number // 关联表主键
|
||||
spuId: number
|
||||
sort: number
|
||||
}
|
||||
|
||||
/** 榜单详情/列表行 */
|
||||
export interface RankingRespVO {
|
||||
id: number
|
||||
title: string
|
||||
subtitle?: string
|
||||
iconUrl?: string
|
||||
sort: number
|
||||
status: number // 0 开启 1 关闭
|
||||
remark?: string
|
||||
createTime?: string
|
||||
spus: RankingSpuResp[]
|
||||
}
|
||||
|
||||
/** 提交时榜单商品项 */
|
||||
export interface RankingSpuItem {
|
||||
spuId: number
|
||||
sort: number
|
||||
}
|
||||
|
||||
export interface RankingCreateBody {
|
||||
title: string
|
||||
subtitle?: string
|
||||
iconUrl?: string
|
||||
sort: number
|
||||
status: number
|
||||
remark?: string
|
||||
spus: RankingSpuItem[]
|
||||
}
|
||||
|
||||
export type RankingUpdateBody = RankingCreateBody & { id: number }
|
||||
|
||||
/** 分页查询 */
|
||||
export const getRankingPage = async (params: any) => {
|
||||
return await request.get({ url: '/promotion/ranking/page', params })
|
||||
}
|
||||
|
||||
/** 详情 */
|
||||
export const getRanking = async (id: number) => {
|
||||
return await request.get({ url: '/promotion/ranking/get', params: { id } })
|
||||
}
|
||||
|
||||
/** 创建 */
|
||||
export const createRanking = async (data: RankingCreateBody) => {
|
||||
return await request.post({ url: '/promotion/ranking/create', data })
|
||||
}
|
||||
|
||||
/** 更新 */
|
||||
export const updateRanking = async (data: RankingUpdateBody) => {
|
||||
return await request.put({ url: '/promotion/ranking/update', data })
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
export const deleteRanking = async (id: number) => {
|
||||
return await request.delete({ url: '/promotion/ranking/delete', params: { id } })
|
||||
}
|
||||
@@ -25,16 +25,6 @@ v-for="(item, index) in tableHeaders" :key="index" :label="item.label" align="ce
|
||||
<el-input v-model="row.barCode" class="w-100%" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="货道/柜门编号" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.cabinetChannel" class="w-100%" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="通道类型" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.cabinetType" class="w-100%" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="销售价" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
@@ -135,16 +125,6 @@ v-for="(item, index) in tableHeaders" :key="index" :label="item.label" align="ce
|
||||
{{ row.barCode }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="货道/柜门编号" min-width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.cabinetChannel }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="通道类型" min-width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.cabinetType }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="销售价(元)" min-width="80">
|
||||
<template #default="{ row }">
|
||||
{{ row.price }}
|
||||
@@ -221,16 +201,6 @@ v-for="(item, index) in tableHeaders" :key="index" :label="item.label" align="ce
|
||||
{{ row.barCode }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="货道/柜门编号" min-width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.cabinetChannel }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="通道类型" min-width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.cabinetType }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="销售价(元)" min-width="80">
|
||||
<template #default="{ row }">
|
||||
{{ formatToFraction(row.price) }}
|
||||
|
||||
@@ -25,6 +25,16 @@
|
||||
class="w-80!"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="热门推荐" prop="hotRecommend">
|
||||
<el-switch
|
||||
v-model="formData.hotRecommend"
|
||||
:active-value="true"
|
||||
:inactive-value="false"
|
||||
active-text="是"
|
||||
inactive-text="否"
|
||||
inline-prompt
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
@@ -50,7 +60,8 @@ const formRef = ref() // 表单Ref
|
||||
const formData = ref<Spu>({
|
||||
sort: 0, // 商品排序
|
||||
giveIntegral: 0, // 赠送积分
|
||||
virtualSalesCount: 0 // 虚拟销量
|
||||
virtualSalesCount: 0, // 虚拟销量
|
||||
hotRecommend: false // 热门推荐
|
||||
})
|
||||
// 表单规则
|
||||
const rules = reactive({
|
||||
|
||||
@@ -109,7 +109,8 @@ const formData = ref<ProductSpuApi.Spu>({
|
||||
description: '', // 商品详情
|
||||
sort: 0, // 商品排序
|
||||
giveIntegral: 0, // 赠送积分
|
||||
virtualSalesCount: 0 // 虚拟销量
|
||||
virtualSalesCount: 0, // 虚拟销量
|
||||
hotRecommend: false // 热门推荐
|
||||
})
|
||||
|
||||
/** 获得详情 */
|
||||
@@ -139,6 +140,9 @@ const getDetail = async () => {
|
||||
}
|
||||
})
|
||||
formData.value = res
|
||||
if (formData.value.hotRecommend === undefined) {
|
||||
formData.value.hotRecommend = false
|
||||
}
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
@@ -167,6 +171,9 @@ const submitForm = async () => {
|
||||
item.costPrice = convertToInteger(item.costPrice)
|
||||
item.firstBrokeragePrice = convertToInteger(item.firstBrokeragePrice)
|
||||
item.secondBrokeragePrice = convertToInteger(item.secondBrokeragePrice)
|
||||
// 商城商品(types=0)SKU 不含售货机货道/通道,避免误带历史脏数据
|
||||
delete (item as any).cabinetChannel
|
||||
delete (item as any).cabinetType
|
||||
})
|
||||
// 处理轮播图列表
|
||||
const newSliderPicUrls: any[] = []
|
||||
|
||||
@@ -42,6 +42,17 @@
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="热门推荐" prop="hotRecommend">
|
||||
<el-select
|
||||
v-model="queryParams.hotRecommend"
|
||||
class="!w-240px"
|
||||
clearable
|
||||
placeholder="全部"
|
||||
>
|
||||
<el-option label="是" :value="true" />
|
||||
<el-option label="否" :value="false" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery">
|
||||
<Icon class="mr-5px" icon="ep:search" />
|
||||
@@ -155,6 +166,12 @@
|
||||
<el-table-column align="center" label="销量" min-width="90" prop="salesCount" />
|
||||
<el-table-column align="center" label="库存" min-width="90" prop="stock" />
|
||||
<el-table-column align="center" label="排序" min-width="70" prop="sort" />
|
||||
<el-table-column align="center" label="热门推荐" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.hotRecommend === true" size="small" type="success">是</el-tag>
|
||||
<el-tag v-else size="small" type="info">否</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="销售状态" min-width="80">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.status >= 0">
|
||||
@@ -289,15 +306,26 @@ const queryParams = ref({
|
||||
name: '',
|
||||
categoryId: undefined,
|
||||
createTime: undefined,
|
||||
types:0
|
||||
types: 0,
|
||||
/** 不传则不参与筛选;仅 true/false 参与后端 eq 查询 */
|
||||
hotRecommend: undefined as boolean | undefined
|
||||
}) // 查询参数
|
||||
|
||||
/** 分页/导出参数:未选「热门推荐」时不传 hotRecommend,避免 false 被误判为空 */
|
||||
const buildSpuPageParams = () => {
|
||||
const p: Record<string, any> = { ...queryParams.value }
|
||||
if (p.hotRecommend === undefined || p.hotRecommend === null) {
|
||||
delete p.hotRecommend
|
||||
}
|
||||
return p
|
||||
}
|
||||
const queryFormRef = ref() // 搜索的表单Ref
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await ProductSpuApi.getSpuPage(queryParams.value)
|
||||
const data = await ProductSpuApi.getSpuPage(buildSpuPageParams())
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
@@ -413,7 +441,7 @@ const handleExport = async () => {
|
||||
await message.exportConfirm()
|
||||
// 发起导出
|
||||
exportLoading.value = true
|
||||
const data = await ProductSpuApi.exportSpu(queryParams.value)
|
||||
const data = await ProductSpuApi.exportSpu(buildSpuPageParams())
|
||||
download.excel(data, '商品列表.xls')
|
||||
} catch {
|
||||
} finally {
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
:rules="rules"
|
||||
:schema="allSchemas.formSchema"
|
||||
>
|
||||
<template #picUrl="model">
|
||||
<UploadImg v-model="model.picUrl" height="80px" />
|
||||
</template>
|
||||
<!-- 先选择 -->
|
||||
<template #spuId>
|
||||
<el-button @click="spuSelectRef.open()">选择商品</el-button>
|
||||
@@ -63,6 +66,7 @@ import * as ProductSpuApi from '@/api/mall/product/spu'
|
||||
import { getPropertyList, RuleConfig } from '@/views/mall/product/spu/components'
|
||||
import { convertToInteger, erpCalculatePercentage, fenToYuan, yuanToFen } from '@/utils'
|
||||
import { PromotionDiscountTypeEnum } from '@/utils/constants'
|
||||
import { UploadImg } from '@/components/UploadFile'
|
||||
|
||||
defineOptions({ name: 'PromotionDiscountActivityForm' })
|
||||
|
||||
@@ -246,7 +250,8 @@ const resetForm = async () => {
|
||||
spuPropertyList.value = []
|
||||
spuIds.value = []
|
||||
await nextTick()
|
||||
formRef.value.getElFormRef().resetFields()
|
||||
// Dialog 首次打开时 formRef 可能尚未挂载
|
||||
formRef.value?.getElFormRef?.()?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,6 +24,17 @@ const crudSchemas = reactive<CrudSchema[]>([
|
||||
width: 120
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '活动图片',
|
||||
field: 'picUrl',
|
||||
isSearch: false,
|
||||
isTable: false,
|
||||
form: {
|
||||
colProps: {
|
||||
span: 24
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '活动开始时间',
|
||||
field: 'startTime',
|
||||
|
||||
@@ -63,6 +63,18 @@
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
||||
<el-table-column label="活动编号" prop="id" min-width="80" />
|
||||
<el-table-column label="活动图片" min-width="80">
|
||||
<template #default="scope">
|
||||
<el-image
|
||||
v-if="scope.row.picUrl"
|
||||
:src="scope.row.picUrl"
|
||||
class="h-40px w-40px"
|
||||
:preview-src-list="[scope.row.picUrl]"
|
||||
preview-teleported
|
||||
/>
|
||||
<span v-else class="text-gray-400">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="活动名称" prop="name" min-width="140" />
|
||||
<el-table-column label="活动时间" min-width="210">
|
||||
<template #default="scope">
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
<template>
|
||||
<Dialog v-model="dialogVisible" :title="dialogTitle" width="720px">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
v-loading="formLoading"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="110px"
|
||||
>
|
||||
<el-form-item label="标题" prop="title">
|
||||
<el-input v-model="formData.title" placeholder="请输入榜单标题" maxlength="64" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="副标题" prop="subtitle">
|
||||
<el-input v-model="formData.subtitle" placeholder="选填" />
|
||||
</el-form-item>
|
||||
<el-form-item label="榜单图标" prop="iconUrl">
|
||||
<UploadImg v-model="formData.iconUrl" height="80px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="榜单排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" :min="0" class="!w-240px" controls-position="right" />
|
||||
<span class="ml-12px text-12px text-gray-500">数值越大越靠前</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="formData.remark" placeholder="选填" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
<el-form-item label="榜单商品" required>
|
||||
<div class="mb-8px">
|
||||
<el-button type="primary" plain @click="spuSelectRef?.open()">
|
||||
<Icon class="mr-5px" icon="ep:plus" />
|
||||
添加商品
|
||||
</el-button>
|
||||
<span class="ml-8px text-12px text-gray-500">至少 1 个商品,排序越大越靠前</span>
|
||||
</div>
|
||||
<el-table :data="spuRows" border max-height="360" size="small">
|
||||
<el-table-column align="center" label="商品图" width="72">
|
||||
<template #default="{ row }">
|
||||
<el-image v-if="row.picUrl" :src="row.picUrl" class="h-36px w-36px" fit="cover" />
|
||||
<span v-else class="text-gray-400">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品名称" min-width="160" prop="name" show-overflow-tooltip />
|
||||
<el-table-column align="center" label="SPU编号" prop="spuId" width="100" />
|
||||
<el-table-column align="center" label="排序" width="140">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.sort" :min="0" class="!w-110px" controls-position="right" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" fixed="right" label="操作" width="80">
|
||||
<template #default="{ $index }">
|
||||
<el-button link type="danger" @click="removeSpuRow($index)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</template>
|
||||
<SpuSelect ref="spuSelectRef" :is-select-sku="false" @confirm="onSpuSelected" />
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { UploadImg } from '@/components/UploadFile'
|
||||
import * as RankingApi from '@/api/mall/promotion/ranking'
|
||||
import * as ProductSpuApi from '@/api/mall/product/spu'
|
||||
import { SpuSelect } from '../components'
|
||||
|
||||
defineOptions({ name: 'PromotionRankingForm' })
|
||||
|
||||
interface SpuRow {
|
||||
spuId: number
|
||||
sort: number
|
||||
name?: string
|
||||
picUrl?: string
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const formLoading = ref(false)
|
||||
const formType = ref('')
|
||||
const formRef = ref()
|
||||
const spuSelectRef = ref<InstanceType<typeof SpuSelect>>()
|
||||
|
||||
const formData = ref({
|
||||
id: undefined as number | undefined,
|
||||
title: '',
|
||||
subtitle: '',
|
||||
iconUrl: '',
|
||||
sort: 0,
|
||||
status: 0,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const spuRows = ref<SpuRow[]>([])
|
||||
|
||||
const formRules = reactive({
|
||||
title: [{ required: true, message: '标题不能为空', trigger: 'blur' }],
|
||||
sort: [{ required: true, message: '排序不能为空', trigger: 'change' }],
|
||||
status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
|
||||
})
|
||||
|
||||
const nextSort = () => {
|
||||
if (!spuRows.value.length) return 100
|
||||
return Math.max(...spuRows.value.map((r) => r.sort)) + 1
|
||||
}
|
||||
|
||||
const onSpuSelected = async (spuId: number) => {
|
||||
if (spuRows.value.some((r) => r.spuId === spuId)) {
|
||||
message.warning('该商品已在榜单中')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const spu = (await ProductSpuApi.getSpu(spuId)) as ProductSpuApi.Spu
|
||||
spuRows.value.push({
|
||||
spuId,
|
||||
sort: nextSort(),
|
||||
name: spu.name,
|
||||
picUrl: spu.picUrl
|
||||
})
|
||||
} catch {
|
||||
message.error('获取商品信息失败')
|
||||
}
|
||||
}
|
||||
|
||||
const removeSpuRow = (index: number) => {
|
||||
spuRows.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
title: '',
|
||||
subtitle: '',
|
||||
iconUrl: '',
|
||||
sort: 0,
|
||||
status: 0,
|
||||
remark: ''
|
||||
}
|
||||
spuRows.value = []
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
const open = async (type: string, id?: number) => {
|
||||
dialogVisible.value = true
|
||||
dialogTitle.value = t('action.' + type)
|
||||
formType.value = type
|
||||
resetForm()
|
||||
if (id) {
|
||||
formLoading.value = true
|
||||
try {
|
||||
const detail = (await RankingApi.getRanking(id)) as RankingApi.RankingRespVO
|
||||
if (!detail) return
|
||||
formData.value = {
|
||||
id: detail.id,
|
||||
title: detail.title,
|
||||
subtitle: detail.subtitle ?? '',
|
||||
iconUrl: detail.iconUrl ?? '',
|
||||
sort: detail.sort,
|
||||
status: detail.status,
|
||||
remark: detail.remark ?? ''
|
||||
}
|
||||
const ordered = [...(detail.spus || [])].sort((a, b) => b.sort - a.sort)
|
||||
const spuIds = ordered.map((s) => s.spuId)
|
||||
if (spuIds.length) {
|
||||
const list = (await ProductSpuApi.getSpuDetailList(spuIds)) as ProductSpuApi.Spu[]
|
||||
const map = new Map(list.map((s) => [s.id!, s]))
|
||||
spuRows.value = ordered.map((link) => {
|
||||
const spu = map.get(link.spuId)
|
||||
return {
|
||||
spuId: link.spuId,
|
||||
sort: link.sort,
|
||||
name: spu?.name,
|
||||
picUrl: spu?.picUrl
|
||||
}
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
|
||||
const submitForm = async () => {
|
||||
await formRef.value?.validate()
|
||||
if (!spuRows.value.length) {
|
||||
message.warning('请至少添加一个榜单商品')
|
||||
return
|
||||
}
|
||||
const ids = spuRows.value.map((r) => r.spuId)
|
||||
if (new Set(ids).size !== ids.length) {
|
||||
message.warning('榜单内存在重复的 SPU')
|
||||
return
|
||||
}
|
||||
const spus: RankingApi.RankingSpuItem[] = spuRows.value.map((r) => ({
|
||||
spuId: r.spuId,
|
||||
sort: r.sort
|
||||
}))
|
||||
const payload = {
|
||||
title: formData.value.title,
|
||||
subtitle: formData.value.subtitle || undefined,
|
||||
iconUrl: formData.value.iconUrl || undefined,
|
||||
sort: formData.value.sort,
|
||||
status: formData.value.status,
|
||||
remark: formData.value.remark || undefined,
|
||||
spus
|
||||
}
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (formType.value === 'create') {
|
||||
await RankingApi.createRanking(payload)
|
||||
message.success(t('common.createSuccess'))
|
||||
} else {
|
||||
await RankingApi.updateRanking({ ...payload, id: formData.value.id! })
|
||||
message.success(t('common.updateSuccess'))
|
||||
}
|
||||
dialogVisible.value = false
|
||||
emit('success')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<el-form
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
:model="queryParams"
|
||||
class="-mb-15px"
|
||||
label-width="88px"
|
||||
>
|
||||
<el-form-item label="榜单标题" prop="title">
|
||||
<el-input
|
||||
v-model="queryParams.title"
|
||||
class="!w-240px"
|
||||
clearable
|
||||
placeholder="模糊搜索标题"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="queryParams.status" class="!w-240px" clearable placeholder="全部">
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间" prop="createTime">
|
||||
<el-date-picker
|
||||
v-model="queryParams.createTime"
|
||||
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
|
||||
class="!w-240px"
|
||||
end-placeholder="结束日期"
|
||||
start-placeholder="开始日期"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery">
|
||||
<Icon class="mr-5px" icon="ep:search" />
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetQuery">
|
||||
<Icon class="mr-5px" icon="ep:refresh" />
|
||||
重置
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['promotion:ranking:create']"
|
||||
plain
|
||||
type="primary"
|
||||
@click="openForm('create')"
|
||||
>
|
||||
<Icon class="mr-5px" icon="ep:plus" />
|
||||
新增
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<ContentWrap>
|
||||
<div class="mb-10px text-13px text-gray-500">排序数值越大越靠前(榜单与榜单内商品规则一致)</div>
|
||||
<el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
|
||||
<el-table-column align="center" label="编号" prop="id" width="80" />
|
||||
<el-table-column align="center" label="标题" min-width="140" prop="title" />
|
||||
<el-table-column align="center" label="副标题" min-width="120" prop="subtitle" />
|
||||
<el-table-column align="center" label="图标" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
v-if="row.iconUrl"
|
||||
:src="row.iconUrl"
|
||||
class="h-32px w-32px"
|
||||
fit="cover"
|
||||
@click="imagePreview(row.iconUrl)"
|
||||
/>
|
||||
<span v-else class="text-gray-400">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="排序" prop="sort" width="80" />
|
||||
<el-table-column align="center" label="状态" prop="status" width="100">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="商品数量" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.spus?.length ?? 0 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:formatter="dateFormatter"
|
||||
align="center"
|
||||
label="创建时间"
|
||||
prop="createTime"
|
||||
width="170"
|
||||
/>
|
||||
<el-table-column align="center" fixed="right" label="操作" width="160">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-hasPermi="['promotion:ranking:update']"
|
||||
link
|
||||
type="primary"
|
||||
@click="openForm('update', scope.row.id)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['promotion:ranking:delete']"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<Pagination
|
||||
v-model:limit="queryParams.pageSize"
|
||||
v-model:page="queryParams.pageNo"
|
||||
:total="total"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<RankingForm ref="formRef" @success="getList" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import { createImageViewer } from '@/components/ImageViewer'
|
||||
import * as RankingApi from '@/api/mall/promotion/ranking'
|
||||
import RankingForm from './RankingForm.vue'
|
||||
|
||||
defineOptions({ name: 'PromotionRanking' })
|
||||
|
||||
const message = useMessage()
|
||||
const { t } = useI18n()
|
||||
|
||||
const loading = ref(true)
|
||||
const total = ref(0)
|
||||
const list = ref<RankingApi.RankingRespVO[]>([])
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
title: undefined as string | undefined,
|
||||
status: undefined as number | undefined,
|
||||
createTime: [] as string[]
|
||||
})
|
||||
const queryFormRef = ref()
|
||||
|
||||
const imagePreview = (url: string) => {
|
||||
createImageViewer({ urlList: [url] })
|
||||
}
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await RankingApi.getRankingPage(queryParams)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
const formRef = ref()
|
||||
const openForm = (type: string, id?: number) => {
|
||||
formRef.value?.open(type, id)
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await message.delConfirm()
|
||||
await RankingApi.deleteRanking(id)
|
||||
message.success(t('common.delSuccess'))
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,679 @@
|
||||
# 用户设备绑定功能 API 文档
|
||||
|
||||
## 概述
|
||||
|
||||
本功能实现了系统用户与设备的多对多绑定关系,一个用户账号可以绑定多个设备进行管理。设备下拉列表会根据当前登录用户绑定的设备自动过滤。
|
||||
|
||||
## 数据库表结构
|
||||
|
||||
### system_user_device(用户设备关联表)
|
||||
|
||||
| 字段名 | 类型 | 说明 | 备注 |
|
||||
|--------|------|------|------|
|
||||
| id | bigint | 自增主键 | PRIMARY KEY |
|
||||
| user_id | bigint | 用户ID | 关联 system_users.id |
|
||||
| device_id | bigint | 设备ID | 关联 product_device.id |
|
||||
| creator | varchar(64) | 创建者 | |
|
||||
| create_time | datetime | 创建时间 | |
|
||||
| updater | varchar(64) | 更新者 | |
|
||||
| update_time | datetime | 更新时间 | |
|
||||
| deleted | bit(1) | 是否删除 | 默认 0 |
|
||||
| tenant_id | bigint | 租户编号 | 默认 0 |
|
||||
|
||||
**唯一索引**:`uk_user_device` (user_id, device_id, tenant_id, deleted) - 防止重复绑定
|
||||
|
||||
---
|
||||
|
||||
## API 接口列表
|
||||
|
||||
### 1. 绑定用户设备
|
||||
|
||||
**接口地址**:`POST /system/user/bind-devices`
|
||||
|
||||
**接口说明**:为指定用户绑定一个或多个设备。如果设备已绑定,会自动跳过(去重)。
|
||||
|
||||
**权限要求**:`system:user:update`
|
||||
|
||||
**请求参数**:
|
||||
|
||||
```json
|
||||
{
|
||||
"userId": 1,
|
||||
"deviceIds": [1, 2, 3]
|
||||
}
|
||||
```
|
||||
|
||||
**参数说明**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 | 示例 |
|
||||
|--------|------|------|------|------|
|
||||
| userId | Long | 是 | 用户ID | 1 |
|
||||
| deviceIds | List<Long> | 是 | 设备ID列表 | [1, 2, 3] |
|
||||
|
||||
**请求示例**:
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8080/system/user/bind-devices" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-d '{
|
||||
"userId": 1,
|
||||
"deviceIds": [1, 2, 3]
|
||||
}'
|
||||
```
|
||||
|
||||
**响应示例**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": true,
|
||||
"msg": "操作成功"
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 1_003_000_001,
|
||||
"data": null,
|
||||
"msg": "用户不存在"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 解绑用户设备
|
||||
|
||||
**接口地址**:`DELETE /system/user/unbind-devices`
|
||||
|
||||
**接口说明**:为指定用户解绑一个或多个设备。
|
||||
|
||||
**权限要求**:`system:user:update`
|
||||
|
||||
**请求参数**:
|
||||
|
||||
```json
|
||||
{
|
||||
"userId": 1,
|
||||
"deviceIds": [2, 3]
|
||||
}
|
||||
```
|
||||
|
||||
**参数说明**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 | 示例 |
|
||||
|--------|------|------|------|------|
|
||||
| userId | Long | 是 | 用户ID | 1 |
|
||||
| deviceIds | List<Long> | 是 | 要解绑的设备ID列表 | [2, 3] |
|
||||
|
||||
**请求示例**:
|
||||
|
||||
```bash
|
||||
curl -X DELETE "http://localhost:8080/system/user/unbind-devices" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-d '{
|
||||
"userId": 1,
|
||||
"deviceIds": [2, 3]
|
||||
}'
|
||||
```
|
||||
|
||||
**响应示例**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": true,
|
||||
"msg": "操作成功"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 获取用户绑定的设备ID列表
|
||||
|
||||
**接口地址**:`GET /system/user/get-device-ids`
|
||||
|
||||
**接口说明**:查询指定用户已绑定的所有设备ID列表。
|
||||
|
||||
**权限要求**:`system:user:query`
|
||||
|
||||
**请求参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 | 示例 |
|
||||
|--------|------|------|------|------|
|
||||
| userId | Long | 是 | 用户ID | 1 |
|
||||
|
||||
**请求示例**:
|
||||
|
||||
```bash
|
||||
curl -X GET "http://localhost:8080/system/user/get-device-ids?userId=1" \
|
||||
-H "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
**响应示例**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"userId": 1,
|
||||
"deviceIds": [1, 2, 3]
|
||||
},
|
||||
"msg": "操作成功"
|
||||
}
|
||||
```
|
||||
|
||||
**响应字段说明**:
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| userId | Long | 用户ID |
|
||||
| deviceIds | List<Long> | 绑定的设备ID列表 |
|
||||
|
||||
---
|
||||
|
||||
### 4. 获取设备下拉列表(精简)
|
||||
|
||||
**接口地址**:`GET /product/device/list-all-simple`
|
||||
|
||||
**接口说明**:获取设备下拉列表,**自动根据当前登录用户绑定的设备进行过滤**。如果用户未绑定任何设备,返回空列表。
|
||||
|
||||
**权限要求**:`product:device:query`
|
||||
|
||||
**请求参数**:无
|
||||
|
||||
**请求示例**:
|
||||
|
||||
```bash
|
||||
curl -X GET "http://localhost:8080/product/device/list-all-simple" \
|
||||
-H "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
**响应示例**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "贩卖机001",
|
||||
"deviceId": "123456789012345"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "贩卖机002",
|
||||
"deviceId": "123456789012346"
|
||||
}
|
||||
],
|
||||
"msg": "操作成功"
|
||||
}
|
||||
```
|
||||
|
||||
**响应字段说明**:
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| id | Long | 设备编号 |
|
||||
| name | String | 设备名称 |
|
||||
| deviceId | String | 设备IMEI |
|
||||
|
||||
**重要说明**:
|
||||
- 此接口会根据当前登录用户自动过滤设备
|
||||
- 只返回当前用户已绑定的设备
|
||||
- 如果用户未绑定任何设备,返回空数组 `[]`
|
||||
- 兼容旧逻辑:如果无法获取用户ID,则根据部门ID查询
|
||||
|
||||
---
|
||||
|
||||
## 前端实现建议
|
||||
|
||||
### 1. 用户管理页面 - 设备绑定功能
|
||||
|
||||
在用户详情或编辑页面,添加设备绑定功能:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<el-form>
|
||||
<!-- 其他用户信息字段 -->
|
||||
|
||||
<!-- 设备绑定 -->
|
||||
<el-form-item label="绑定设备">
|
||||
<el-select
|
||||
v-model="form.deviceIds"
|
||||
multiple
|
||||
placeholder="请选择设备"
|
||||
style="width: 100%"
|
||||
@change="handleDeviceChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="device in deviceList"
|
||||
:key="device.id"
|
||||
:label="`${device.name} (${device.deviceId})`"
|
||||
:value="device.id"
|
||||
/>
|
||||
</el-select>
|
||||
<div class="tip">可绑定多个设备,设备下拉列表会根据当前用户绑定的设备自动过滤</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
userId: null,
|
||||
deviceIds: []
|
||||
},
|
||||
deviceList: [] // 所有可用设备列表
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 加载用户已绑定的设备
|
||||
async loadUserDevices(userId) {
|
||||
const res = await this.$api.get(`/system/user/get-device-ids?userId=${userId}`)
|
||||
if (res.code === 0) {
|
||||
this.form.deviceIds = res.data.deviceIds || []
|
||||
}
|
||||
},
|
||||
|
||||
// 保存设备绑定
|
||||
async saveDeviceBinding() {
|
||||
await this.$api.post('/system/user/bind-devices', {
|
||||
userId: this.form.userId,
|
||||
deviceIds: this.form.deviceIds
|
||||
})
|
||||
this.$message.success('设备绑定成功')
|
||||
},
|
||||
|
||||
// 解绑设备
|
||||
async unbindDevices(deviceIds) {
|
||||
await this.$api.delete('/system/user/unbind-devices', {
|
||||
userId: this.form.userId,
|
||||
deviceIds: deviceIds
|
||||
})
|
||||
this.$message.success('设备解绑成功')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 2. 设备下拉选择组件
|
||||
|
||||
在需要使用设备下拉的地方(如商品管理、订单管理等),使用设备下拉接口:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<el-select
|
||||
v-model="selectedDeviceId"
|
||||
placeholder="请选择设备"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="device in deviceList"
|
||||
:key="device.id"
|
||||
:label="`${device.name} (${device.deviceId})`"
|
||||
:value="device.id"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
selectedDeviceId: null,
|
||||
deviceList: []
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadDeviceList()
|
||||
},
|
||||
methods: {
|
||||
// 加载设备下拉列表(自动根据当前用户过滤)
|
||||
async loadDeviceList() {
|
||||
const res = await this.$api.get('/product/device/list-all-simple')
|
||||
if (res.code === 0) {
|
||||
this.deviceList = res.data || []
|
||||
if (this.deviceList.length === 0) {
|
||||
this.$message.warning('当前用户未绑定任何设备,请先绑定设备')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 3. 用户设备管理页面(可选)
|
||||
|
||||
可以创建一个专门的页面来管理用户设备绑定关系:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="user-device-management">
|
||||
<el-card>
|
||||
<div slot="header">
|
||||
<span>用户设备绑定管理</span>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
style="float: right"
|
||||
@click="showBindDialog = true"
|
||||
>
|
||||
绑定设备
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 用户选择 -->
|
||||
<el-select
|
||||
v-model="selectedUserId"
|
||||
placeholder="请选择用户"
|
||||
@change="loadUserDevices"
|
||||
style="width: 300px; margin-bottom: 20px"
|
||||
>
|
||||
<el-option
|
||||
v-for="user in userList"
|
||||
:key="user.id"
|
||||
:label="user.nickname"
|
||||
:value="user.id"
|
||||
/>
|
||||
</el-select>
|
||||
|
||||
<!-- 已绑定设备列表 -->
|
||||
<el-table :data="boundDevices" v-loading="loading">
|
||||
<el-table-column prop="id" label="设备ID" width="100" />
|
||||
<el-table-column prop="name" label="设备名称" />
|
||||
<el-table-column prop="deviceId" label="设备IMEI" />
|
||||
<el-table-column label="操作" width="150">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
type="danger"
|
||||
size="mini"
|
||||
@click="handleUnbind(scope.row.id)"
|
||||
>
|
||||
解绑
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 绑定设备对话框 -->
|
||||
<el-dialog
|
||||
title="绑定设备"
|
||||
:visible.sync="showBindDialog"
|
||||
width="500px"
|
||||
>
|
||||
<el-select
|
||||
v-model="selectedDeviceIds"
|
||||
multiple
|
||||
placeholder="请选择要绑定的设备"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="device in availableDevices"
|
||||
:key="device.id"
|
||||
:label="`${device.name} (${device.deviceId})`"
|
||||
:value="device.id"
|
||||
/>
|
||||
</el-select>
|
||||
<div slot="footer">
|
||||
<el-button @click="showBindDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleBind">确定</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
selectedUserId: null,
|
||||
userList: [],
|
||||
boundDevices: [],
|
||||
availableDevices: [],
|
||||
selectedDeviceIds: [],
|
||||
showBindDialog: false,
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadUserList()
|
||||
this.loadAvailableDevices()
|
||||
},
|
||||
methods: {
|
||||
// 加载用户列表
|
||||
async loadUserList() {
|
||||
const res = await this.$api.get('/system/user/list-all-simple')
|
||||
if (res.code === 0) {
|
||||
this.userList = res.data || []
|
||||
}
|
||||
},
|
||||
|
||||
// 加载可用设备列表(所有设备)
|
||||
async loadAvailableDevices() {
|
||||
// 这里需要调用获取所有设备的接口,或者使用分页接口
|
||||
const res = await this.$api.get('/product/device/page', {
|
||||
pageNo: 1,
|
||||
pageSize: 1000
|
||||
})
|
||||
if (res.code === 0) {
|
||||
this.availableDevices = res.data.list || []
|
||||
}
|
||||
},
|
||||
|
||||
// 加载用户已绑定的设备
|
||||
async loadUserDevices(userId) {
|
||||
if (!userId) return
|
||||
this.loading = true
|
||||
try {
|
||||
const res = await this.$api.get(`/system/user/get-device-ids?userId=${userId}`)
|
||||
if (res.code === 0) {
|
||||
const deviceIds = res.data.deviceIds || []
|
||||
// 根据设备ID获取设备详情
|
||||
if (deviceIds.length > 0) {
|
||||
const deviceRes = await this.$api.get('/product/device/list', {
|
||||
deviceIds: deviceIds
|
||||
})
|
||||
if (deviceRes.code === 0) {
|
||||
this.boundDevices = deviceRes.data || []
|
||||
}
|
||||
} else {
|
||||
this.boundDevices = []
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 绑定设备
|
||||
async handleBind() {
|
||||
if (!this.selectedUserId) {
|
||||
this.$message.warning('请先选择用户')
|
||||
return
|
||||
}
|
||||
if (this.selectedDeviceIds.length === 0) {
|
||||
this.$message.warning('请选择要绑定的设备')
|
||||
return
|
||||
}
|
||||
const res = await this.$api.post('/system/user/bind-devices', {
|
||||
userId: this.selectedUserId,
|
||||
deviceIds: this.selectedDeviceIds
|
||||
})
|
||||
if (res.code === 0) {
|
||||
this.$message.success('绑定成功')
|
||||
this.showBindDialog = false
|
||||
this.selectedDeviceIds = []
|
||||
this.loadUserDevices(this.selectedUserId)
|
||||
}
|
||||
},
|
||||
|
||||
// 解绑设备
|
||||
async handleUnbind(deviceId) {
|
||||
await this.$confirm('确定要解绑该设备吗?', '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
const res = await this.$api.delete('/system/user/unbind-devices', {
|
||||
userId: this.selectedUserId,
|
||||
deviceIds: [deviceId]
|
||||
})
|
||||
if (res.code === 0) {
|
||||
this.$message.success('解绑成功')
|
||||
this.loadUserDevices(this.selectedUserId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 业务逻辑说明
|
||||
|
||||
### 1. 设备绑定逻辑
|
||||
|
||||
- **去重处理**:绑定设备时会自动检查已绑定的设备,只绑定新设备,避免重复
|
||||
- **事务支持**:绑定操作在事务中执行,保证数据一致性
|
||||
- **用户校验**:绑定前会校验用户是否存在
|
||||
|
||||
### 2. 设备下拉过滤逻辑
|
||||
|
||||
- **优先级**:优先根据用户绑定的设备过滤
|
||||
- **兼容性**:如果无法获取用户ID,则回退到根据部门ID查询(兼容旧逻辑)
|
||||
- **空列表处理**:如果用户未绑定任何设备,返回空列表
|
||||
|
||||
### 3. 数据清理
|
||||
|
||||
- **删除用户时**:自动删除该用户的所有设备绑定关系
|
||||
- **删除设备时**:建议同时删除相关的用户设备绑定关系(需要在删除设备接口中实现)
|
||||
|
||||
---
|
||||
|
||||
## 错误码说明
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|--------|------|
|
||||
| 1_003_000_001 | 用户不存在 |
|
||||
| 1_008_009_000 | 设备不存在 |
|
||||
|
||||
---
|
||||
|
||||
## 接口调用流程示例
|
||||
|
||||
### 场景:为用户绑定设备
|
||||
|
||||
1. **获取用户信息**
|
||||
```
|
||||
GET /system/user/get?id=1
|
||||
```
|
||||
|
||||
2. **获取所有可用设备列表**(用于选择)
|
||||
```
|
||||
GET /product/device/page?pageNo=1&pageSize=100
|
||||
```
|
||||
|
||||
3. **获取用户已绑定的设备**(用于回显)
|
||||
```
|
||||
GET /system/user/get-device-ids?userId=1
|
||||
```
|
||||
|
||||
4. **绑定设备**
|
||||
```
|
||||
POST /system/user/bind-devices
|
||||
{
|
||||
"userId": 1,
|
||||
"deviceIds": [1, 2, 3]
|
||||
}
|
||||
```
|
||||
|
||||
### 场景:使用设备下拉(自动过滤)
|
||||
|
||||
1. **获取设备下拉列表**(自动根据当前用户过滤)
|
||||
```
|
||||
GET /product/device/list-all-simple
|
||||
```
|
||||
返回结果只包含当前登录用户已绑定的设备
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **权限控制**:
|
||||
- 绑定/解绑设备需要 `system:user:update` 权限
|
||||
- 查询设备列表需要 `product:device:query` 权限
|
||||
|
||||
2. **数据一致性**:
|
||||
- 绑定操作支持事务,失败会自动回滚
|
||||
- 建议在删除设备时,同步删除用户设备关联
|
||||
|
||||
3. **性能优化**:
|
||||
- 设备下拉接口已优化,只返回用户绑定的设备,减少数据量
|
||||
- 如果用户未绑定设备,直接返回空列表,避免不必要的查询
|
||||
|
||||
4. **兼容性**:
|
||||
- 设备下拉接口兼容旧逻辑,如果无法获取用户ID,会回退到部门ID查询
|
||||
- 新老逻辑可以平滑过渡
|
||||
|
||||
---
|
||||
|
||||
## 前端开发检查清单
|
||||
|
||||
- [ ] 用户管理页面添加设备绑定功能
|
||||
- [ ] 设备下拉组件使用新的接口(自动过滤)
|
||||
- [ ] 处理用户未绑定设备的情况(提示信息)
|
||||
- [ ] 实现设备绑定的增删改查功能
|
||||
- [ ] 添加权限校验
|
||||
- [ ] 添加错误处理和提示
|
||||
- [ ] 添加加载状态显示
|
||||
- [ ] 实现设备列表的搜索和筛选(如需要)
|
||||
|
||||
---
|
||||
|
||||
## 测试用例
|
||||
|
||||
### 测试场景 1:绑定设备
|
||||
|
||||
1. 选择用户ID = 1
|
||||
2. 选择设备ID = [1, 2, 3]
|
||||
3. 调用绑定接口
|
||||
4. 验证返回成功
|
||||
5. 查询用户设备列表,验证设备已绑定
|
||||
|
||||
### 测试场景 2:重复绑定
|
||||
|
||||
1. 用户已绑定设备 [1, 2]
|
||||
2. 再次绑定设备 [2, 3]
|
||||
3. 验证设备 [2] 不会重复绑定
|
||||
4. 验证最终绑定设备为 [1, 2, 3]
|
||||
|
||||
### 测试场景 3:设备下拉过滤
|
||||
|
||||
1. 用户A绑定设备 [1, 2]
|
||||
2. 用户B绑定设备 [2, 3]
|
||||
3. 用户A登录,调用设备下拉接口
|
||||
4. 验证只返回设备 [1, 2]
|
||||
5. 用户B登录,调用设备下拉接口
|
||||
6. 验证只返回设备 [2, 3]
|
||||
|
||||
### 测试场景 4:解绑设备
|
||||
|
||||
1. 用户绑定设备 [1, 2, 3]
|
||||
2. 解绑设备 [2]
|
||||
3. 验证用户剩余绑定设备为 [1, 3]
|
||||
|
||||
---
|
||||
|
||||
## 更新日志
|
||||
|
||||
- 2025-01-XX:初始版本,实现用户设备多对多绑定功能
|
||||
|
||||
Reference in new issue
Block a user