代理功能+web客户端权限

This commit is contained in:
2025-12-05 21:10:27 +08:00
parent e8ab8252f8
commit df9df3144b
33 changed files with 2294 additions and 162 deletions

View File

@@ -4,10 +4,9 @@ NODE_ENV=development
VITE_DEV=true
# 请求路径
# VITE_BASE_URL='http://172.16.214.222:48080'
VITE_BASE_URL='http://192.168.1.144:48080'
# VITE_BASE_URL='http://47.79.98.113:48080'
# VITE_BASE_URL='https://backstageapi.yolozs.com'
# VITE_BASE_URL='http://192.168.2.21:48080'
VITE_BASE_URL='https://backstageapi.yolozs.com'
# VITE_BASE_URL='https://testapi.tknb.net'
# 文件上传类型server - 后端上传, client - 前端直连上传,仅支持 S3 服务
VITE_UPLOAD_TYPE=server

View File

@@ -5,6 +5,7 @@ VITE_DEV=false
# 请求路径
VITE_BASE_URL='https://backstageapi.yolozs.com'
# VITE_BASE_URL='https://testapi.tknb.net'
# 文件上传类型server - 后端上传, client - 前端直连上传仅支持S3服务
VITE_UPLOAD_TYPE=server

View File

@@ -4,7 +4,7 @@ NODE_ENV=production
VITE_DEV=false
# 请求路径
VITE_BASE_URL='http://localhost:48080'
VITE_BASE_URL='http://testapi.tknb.net'
# 文件上传类型server - 后端上传, client - 前端直连上传仅支持S3服务
VITE_UPLOAD_TYPE=server

View File

@@ -49,6 +49,10 @@ export const BigBrotherApi = {
deleteBigBrother: async (id: number) => {
return await request.delete({ url: `/server/big-brother/delete?id=` + id })
},
// 批量删除大哥数据
deleteBigBrotherList: async (ids: number[]) => {
return await request.delete({ url: `/server/big-brother/delete-list` , params: { ids: ids.join(',') } })
},
// 导出大哥数据 Excel
exportBigBrother: async (params) => {

View File

@@ -52,6 +52,10 @@ export const EmployeeBigBrotherApi = {
deleteEmployeeBigBrother: async (id: number) => {
return await request.delete({ url: `/server/employee-big-brother/delete?id=` + id })
},
// 批量删除大哥数据员工业务
deleteEmployeeBigBrotherList: async (ids: number[]) => {
return await request.delete({ url: `/server/employee-big-brother/delete-list` , params: { ids: ids.join(',') } })
},
// 导出大哥数据员工业务 Excel
exportEmployeeBigBrother: async (params) => {

View File

@@ -36,14 +36,14 @@ export const EmployeeHostsApi = {
getEmployeeHostsPageDsec: async (params: any) => {
return await request.get({ url: `/server/employee-big-brother/self_complete` })
},
// 查询员工分配主播分页
// 查询爬大哥建联
employeeCompleteBarChart: async (params: number[], days: number) => {
return await request.post({
url: `/server/employee-hosts/employeeCompleteBarChart`,
data: { userId: params, days: days }
})
},
// 查询员工分配大哥分页
// 查询爬大哥建联
employeeCompleteBarChartDsec: async (params: number[], days: number) => {
return await request.post({
url: `/server/employee-big-brother/employeeCompleteBarChart`,

View File

@@ -35,6 +35,14 @@ export interface TenantExportReqVO {
export const getTenantPage = (params: TenantPageReqVO) => {
return request.get({ url: '/system/tenant/page', params })
}
// 查询租户列表(代理)
export const getTenantPageSelf = (params: TenantPageReqVO) => {
return request.get({ url: '/system/tenant/page_self', params })
}
// 查询下级租户列表(代理)
export const getTenantChildren = (params: TenantPageReqVO) => {
return request.get({ url: '/system/tenant/get-children', params })
}
// 查询租户详情
export const getTenant = (id: number) => {
@@ -70,3 +78,12 @@ export const deleteTenantList = (ids: number[]) => {
export const exportTenant = (params: TenantExportReqVO) => {
return request.download({ url: '/system/tenant/export-excel', params })
}
// 获取租户等级
export const getSelfTenantLevel = () => {
return request.get({ url: '/system/tenant/getSelfTenantLevel' })
}
// 续费
export const renewal = (data) => {
return request.put({ url: '/system/tenant/renewal',data})
}

View File

@@ -10,6 +10,8 @@ export interface TenantPackageVO {
updateTime: string
menuIds: number[]
createTime: Date
packageType: number
days: number
}
// 查询租户套餐列表
@@ -46,3 +48,7 @@ export const deleteTenantPackageList = (ids: number[]) => {
export const getTenantPackageList = () => {
return request.get({ url: '/system/tenant-package/simple-list' })
}
// 获取非总后台租户套餐精简信息列表
export const getTenantPackageListagency = () => {
return request.get({ url: '/system/tenant-agency-package/get-simple-list' })
}

View File

@@ -0,0 +1,49 @@
import request from '@/config/axios'
// 代理租户套餐 VO
export interface TenantAgencyPackageVO {
id: number // 套餐编号
name: string // 套餐名
status: number // 租户状态0正常 1停用
remark: string // 备注
menuIds: string // 关联的菜单编号
days: number // 套餐天数
price: number // 套餐价格
hostslClient: number // 爬主播客户端 0 关闭 1 开启
brotherClient: number // 爬大哥客户端 0 关闭 1 开启
aiClient: number // AI 客户端 0 关闭 1 开启
packageType: number // 套餐类型
}
// 代理租户套餐 API
export const TenantAgencyPackageApi = {
// 查询代理租户套餐分页
getTenantAgencyPackagePage: async (params: any) => {
return await request.get({ url: `/system/tenant-agency-package/page`, params })
},
// 查询代理租户套餐详情
getTenantAgencyPackage: async (id: number) => {
return await request.get({ url: `/system/tenant-agency-package/get?id=` + id })
},
// 新增代理租户套餐
createTenantAgencyPackage: async (data: TenantAgencyPackageVO) => {
return await request.post({ url: `/system/tenant-agency-package/create`, data })
},
// 修改代理租户套餐
updateTenantAgencyPackage: async (data: TenantAgencyPackageVO) => {
return await request.put({ url: `/system/tenant-agency-package/update`, data })
},
// 删除代理租户套餐
deleteTenantAgencyPackage: async (id: number) => {
return await request.delete({ url: `/system/tenant-agency-package/delete?id=` + id })
},
// 导出代理租户套餐 Excel
exportTenantAgencyPackage: async (params) => {
return await request.download({ url: `/system/tenant-agency-package/export-excel`, params })
}
}

View File

@@ -0,0 +1,83 @@
import request from '@/config/axios'
// 租户余额 VO
export interface TenantBalanceVO {
id: number // 租户 Id
balance: number // 当前积分余额
version: number // 乐观锁版本号
updatedAt: Date // 更新时间
}
// 租户余额 API
export const TenantBalanceApi = {
// 查询租户余额分页
getTenantBalancePage: async (params: any) => {
return await request.get({ url: `/system/tenant-balance/page`, params })
},
// 查询下级租户余额分页
getSubordinateaMountPage: async (params: any) => {
return await request.get({ url: `/system/tenant-balance/get-self-subordinate-amount-page`, params })
},
//查看当前余额
getselfamount: async () => {
return await request.get({ url: `/system/tenant-balance/get-self-amount` })
},
// 查询租户余额详情
getTenantBalance: async (id: number) => {
return await request.get({ url: `/system/tenant-balance/get?id=` + id })
},
// 新增租户余额
createTenantBalance: async (data: TenantBalanceVO) => {
return await request.post({ url: `/system/tenant-balance/create`, data })
},
// 修改租户余额
updateTenantBalance: async (data: TenantBalanceVO) => {
return await request.put({ url: `/system/tenant-balance/update`, data })
},
// 删除租户余额
deleteTenantBalance: async (id: number) => {
return await request.delete({ url: `/system/tenant-balance/delete?id=` + id })
},
// 转账租户余额
tenantTransfer: async (data: TenantBalanceVO) => {
return await request.post({ url: `/system/tenant-balance/transfer`, data })
},
addAmount: async (data: TenantBalanceVO) => {
return await request.post({ url: `/system/tenant-balance/addAmount`, data })
},
// 导出租户余额 Excel
exportTenantBalance: async (params) => {
return await request.download({ url: `/system/tenant-balance/export-excel`, params })
},
// ==================== 子表(租户余额) ====================
// 获得租户余额分页
getTenantPointsPage: async (params) => {
return await request.get({ url: `/system/tenant-points/transaction-history/page`, params })
},
// 新增租户余额
createTenantPoints: async (data) => {
return await request.post({ url: `/system/tenant-balance/tenant-points/create`, data })
},
// 修改租户余额
updateTenantPoints: async (data) => {
return await request.put({ url: `/system/tenant-balance/tenant-points/update`, data })
},
// 删除租户余额
deleteTenantPoints: async (id: number) => {
return await request.delete({ url: `/system/tenant-balance/tenant-points/delete?id=` + id })
},
// 获得租户余额
getTenantPoints: async (id: number) => {
return await request.get({ url: `/system/tenant-balance/tenant-points/get?id=` + id })
}
}

View File

@@ -0,0 +1,48 @@
import request from '@/config/axios'
// 租户积分记录 VO
export interface TenantPointsVO {
id: number // 主键
points: number // 本次变动点数,正加负减
balance: number // 变动后余额快照(冗余)
type: string // 变动类型,如 RECHARGE, CONSUME, TRANSFER_OUT, TRANSFER_IN
remark: string // 备注
orderId: number // 订单 Id/业务单号
bizNo: string // 业务流水号(转账、订单等唯一标识)
operatorId: number // 操作人 Id
targetTenantId: number // 目标租户 Id转账使用
createdAt: Date // 创建时间
}
// 租户积分记录 API
export const TenantPointsApi = {
// 查询租户积分记录分页
getTenantPointsPage: async (params: any) => {
return await request.get({ url: `/system/tenant-points/page`, params })
},
// 查询租户积分记录详情
getTenantPoints: async (id: number) => {
return await request.get({ url: `/system/tenant-points/get?id=` + id })
},
// 新增租户积分记录
createTenantPoints: async (data: TenantPointsVO) => {
return await request.post({ url: `/system/tenant-points/create`, data })
},
// 修改租户积分记录
updateTenantPoints: async (data: TenantPointsVO) => {
return await request.put({ url: `/system/tenant-points/update`, data })
},
// 删除租户积分记录
deleteTenantPoints: async (id: number) => {
return await request.delete({ url: `/system/tenant-points/delete?id=` + id })
},
// 导出租户积分记录 Excel
exportTenantPoints: async (params) => {
return await request.download({ url: `/system/tenant-points/export-excel`, params })
}
}

View File

@@ -20,6 +20,7 @@ export interface UserVO {
tenantId: number
aiChat: number
aiReplay: number
webAi: number
}
// 查询用户管理列表

View File

@@ -491,6 +491,7 @@ export default {
aiOperation: 'Ai Operation',
sortType: 'Sort Type',
sortName: 'Sort Name',
isDelete: 'Is Delete',
placeIsAssigned: 'Please select assignment status',
placeaiOperation: 'Please select Ai operation',
createTime: 'Created Time',

View File

@@ -489,6 +489,7 @@ export default {
aiOperation: '是否AI介入',
sortType: '排序类型',
sortName: '排序名字',
isDelete: '是否删除',
placeIsAssigned: '请选择是否已经分配给员工',
placeaiOperation: '请筛选是否AI介入',
createTime: '创建时间',

View File

@@ -255,5 +255,7 @@ export enum DICT_TYPE {
SORT_STATE = 'sort_state', // 升序降序
SORT_TYPE = 'sort_type', // 排序类型
FLAG_TYPE = 'flag_type', // 旗帜
HOSTS_KIND = 'hosts_type' // 直播类型
HOSTS_KIND = 'hosts_type', // 直播类型
PAY_TYPE = 'pay_type', // 支付类型
}

View File

@@ -74,7 +74,7 @@
<el-card shadow="never" class="mt-8px">
<!-- <div style="display: flex; width: 50%;"
v-if="wsCache.get('roleRouters').find(item => item.id === 1)?.children.find(item => item.id === 100)">
v-if="wsCache.get('roleRouters').find(item => item.id === 5019)?.children.find(item => item.id === 5020)">
<el-select v-model="allocationUser" :placeholder="t('newHosts.placeAllocationUser')" clearable
@change="getUserAccessSource(allocationUser)">
@@ -93,7 +93,7 @@
</el-card>
</el-col> -->
<el-col
v-if="wsCache.get('roleRouters').find(item => item.id === 1)?.children.find(item => item.id === 100)"
v-if="wsCache.get('roleRouters').find(item => item.id === 5019)?.children.find(item => item.id === 5020)"
:xl="20" :lg="20" :md="24" :sm="24" :xs="24">
<el-card shadow="hover" class="mb-8px">
<el-select style="width: 200px;" v-model="days" @change="updataDays">
@@ -106,7 +106,7 @@
</el-skeleton>
</el-card>
</el-col>
<el-col v-else :xl="14" :lg="14" :md="24" :sm="24" :xs="24">
<el-col v-if="tenantLevel != 1" :xl="14" :lg="14" :md="24" :sm="24" :xs="24">
<el-card shadow="hover" class="mb-8px">
<div>当日建联主播数量</div>
{{ HostsOperationNum }}
@@ -121,7 +121,7 @@
<el-skeleton :loading="loading" animated>
<el-row :gutter="20" justify="space-between">
<el-col
v-if="wsCache.get('roleRouters').find(item => item.id === 1)?.children.find(item => item.id === 100)"
v-if="wsCache.get('roleRouters').find(item => item.id === 5041)?.children.find(item => item.id === 5042)"
:xl="20" :lg="20" :md="24" :sm="24" :xs="24">
<el-card shadow="hover" class="mb-8px">
<el-select style="width: 200px;" v-model="daysDsec" @change="updataDaysDsec">
@@ -136,7 +136,7 @@
</el-card>
</el-col>
<el-col v-else :xl="14" :lg="14" :md="24" :sm="24" :xs="24">
<el-col v-if="tenantLevel != 1" :xl="14" :lg="14" :md="24" :sm="24" :xs="24">
<el-card shadow="hover" class="mb-8px">
<div>当日建联大哥数量</div>
@@ -217,7 +217,7 @@ import { getSimpleUserListPage, getSimpleUserList } from '@/api/system/user'
import { EmployeeHostsApi } from '@/api/server/employeehosts'
import { ref, reactive, onMounted, onActivated } from 'vue'
import WxMusic from '../mp/components/wx-music'
import * as TenantApi from '@/api/system/tenant'
let HostsOperationNum = ref(0)//当日建联主播数量
@@ -226,19 +226,28 @@ let HostsOperationNumDsec = ref(0)//当日建联大哥数量
let days = ref(1)//当日
let daysDsec = ref(1)//当日
let tenantLevel = ref()//租户等级
//初始
onMounted(async () => {
await TenantApi.getSelfTenantLevel().then(res => {
tenantLevel.value = res.tenantLevel
console.log(res.tenantLevel)
})
if (tenantLevel.value == 1) {
loading.value = false
return
}
console.log(1321231)
await getAllApi()
await getAllocationList()
if (wsCache.get('roleRouters').find(item => item.id === 1)?.children.find(item => item.id === 100)) {
//用户id == 1 且
if (wsCache.get('roleRouters').find(item => item.id === 5019)?.children.find(item => item.id === 5020)) {
await fetchAllHostsCount(1)
await fetchAllHostsCountDesc(1)
} else {
await fetchDailyHostsCount()
// await fetchDailyHostsCount()
await fetchDailyHostsCountDesc()
}
@@ -246,13 +255,23 @@ onMounted(async () => {
// 每次页面“再次显示”时都会触发(前提:该路由组件被 keep-alive 缓存)
onActivated(async () => {
await fetchDailyHostsCount()
console.log(wsCache.get('user'))
console.log(tenantLevel.value)
await TenantApi.getSelfTenantLevel().then(res => {
tenantLevel.value = res.tenantLevel
console.log(res.tenantLevel)
})
if (tenantLevel.value == 1) {
loading.value = false
return
}
await fetchDailyHostsCount() //爬主播建联率
await fetchDailyHostsCountDesc()
await fetchDailyHostsCountDesc()//爬主播建联率 指定员工
await fetchAllHostsCount(1)
await fetchAllHostsCount(1) //爬大哥建联率
await fetchAllHostsCountDesc(1)
await fetchAllHostsCountDesc(1) //爬大哥建联率 指定员工
})
// 天数切换
@@ -644,7 +663,7 @@ const getAllApi = async () => {
getProject(),
getNotice(),
getShortcut(),
getUserAccessSource(null),
wsCache.get('roleRouters').find(item => item.id === 5019)?.children.find(item => item.id === 5020) ? getUserAccessSource(null) : null,
getWeeklyUserActivity()
])
@@ -719,7 +738,7 @@ async function fetchDailyHostsCountDesc() {
// 如果柱状图依赖这个值,顺便刷新一次图表数据
}
// ✅ 抽成函数:每天所有员工数量(用于多人柱状图)
// ✅ 爬大哥建联率
async function fetchAllHostsCount(val) {
// 以“传入的用户 id”为准展示顺序也按这里来
const ids = allocationUserList.value.map(item => item.value)

View File

@@ -125,6 +125,10 @@
<el-button @click="dialogAllocation = true">
<Icon icon="ep:refresh" class="mr-5px" /> {{ t('newHosts.allocation') }}
</el-button>
<el-button type="danger" plain :disabled="checkedIds.length === 0" @click="handleDeleteBatch"
v-hasPermi="['server:new-hosts:delete']">
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
@@ -179,13 +183,17 @@
</ContentWrap>
<!-- 分配弹窗 -->
<el-dialog v-model="dialogAllocation" :title="t('newHosts.allocationUser')">
<!-- <div style="padding: 0px 0px 30px 0px ;">
<el-alert title="分配成功数量可能会小于选择数量同id主播无法被重复分配" type="warning" />
</div> -->
<div style="display: flex;">
<el-select v-if="wsCache.get('roleRouters').find(item => item.id === 1).children.find(item => item.id === 103)"
v-model="allocationDept" :placeholder="t('newHosts.placeAllocationDept')" clearable
@change="getAllocationUserList">
<el-option v-for="(dept, index) in allocationdeptList" :key="index" :label="dept.name" :value="dept.id" />
</el-select>
<el-select v-model="allocationUser" :placeholder="t('newHosts.placeAllocationUser')" clearable>
<el-option v-for="(user, index) in allocationUserList" :key="index" :label="user.label" :value="user.value" />
</el-select>
<el-select v-model="allocationUser" :placeholder="t('newHosts.placeAllocationUser')" clearable>
<el-option v-for="(user, index) in allocationUserList" :key="index" :label="user.label" :value="user.value" />
</el-select>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogAllocation = false">{{ t('newHosts.cancel') }}</el-button>
@@ -204,7 +212,8 @@ import { getIntDictOptions, DICT_TYPE, getStrDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { BigBrotherApi, BigBrotherVO } from '@/api/server/bigbrother'
import { getAllocation, getSimpleUserList } from '@/api/system/user'
import * as DeptApi from '@/api/system/dept'
import { getAllocation, getSimpleUserList, getSimpleUserListPage } from '@/api/system/user'
import { useCache } from '@/hooks/web/useCache'
import BigBrotherForm from './BigBrotherForm.vue'
import { useDictStore } from '@/store/modules/dict' // 如果你项目里有字典 store
@@ -268,12 +277,19 @@ const exportLoading = ref(false) // 导出的加载中
let dialogAllocation = ref(false)//分配弹窗
let selectBigList = ref([]) //选中的主播列表
let allocationUser = ref('') //选中的分配用户
let allocationDept = ref('') //选中的分配部门
let allocationUserList = ref([
{
label: '分配用户1',
value: 1
}
]) //选中的分配用户
let allocationdeptList = ref([
{
name: '分配用户1',
id: 1
}
]) //选中的分配部门
/** 查询列表 */
const getList = async () => {
loading.value = true
@@ -287,7 +303,17 @@ const getList = async () => {
loading.value = false
}
}
/** 查询部门 */
const getAlldeptList = async () => {
loading.value = true
try {
const data = await DeptApi.getDeptPage(queryParams)
allocationdeptList.value = data
// console.log('=========================================', allocationdeptList)
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
@@ -361,9 +387,22 @@ const getAllocationList = async () => {
loading.value = false
}
}
const getAllocationUserList = async () => {
const data = await getSimpleUserListPage({ pageSize: 1000, pageNo: 1, deptId: allocationDept.value })
allocationUserList.value = []
console.log("----------------------------------------------------", data.list)
data.list.forEach((item) => {
allocationUserList.value.push({
label: item.nickname,
value: item.id
})
})
}
//分配按钮操作
const handleSelectionChange = (val) => {
checkedIds.value = val.map((row) => row.id)
selectBigList.value = val
console.log(selectBigList.value)
}
@@ -442,9 +481,26 @@ function openHtml(item, id) {
window.open(`https://www.tiktok.com/@${id}`)
}
/** 批量删除按钮操作 */
const checkedIds = ref<number[]>([])
const handleDeleteBatch = async () => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起批量删除
await BigBrotherApi.deleteBigBrotherList(checkedIds.value)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch { }
}
/** 初始化 **/
onMounted(() => {
getList()
getAllocationList();
if (wsCache.get('roleRouters').find(item => item.id === 1).children.find(item => item.id === 103)) {
getAlldeptList();
}
})
</script>

View File

@@ -111,6 +111,13 @@
<el-button @click="exportAi(2)">
<Icon icon="ep:copy-document" class="mr-5px" /> 批量复制主播id
</el-button>
<el-button v-if="!isMobile" :disabled="checkedIds.length === 0" type="danger" @click="handleDeleteList">
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
<el-button v-else type="danger" @click="handleDeletePageList">
<Icon icon="ep:delete" class="mr-5px" /> 删除本页
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
@@ -260,6 +267,12 @@ let allocationUserList = ref([
value: 1
}
]) //选中的分配用户
/** 批量删除按钮操作 */
const checkedIds = ref<number[]>([])
//本页数据
const checkedPageIds = ref<number[]>([])
/** 查询列表 */
const getList = async () => {
loading.value = true
@@ -267,8 +280,11 @@ const getList = async () => {
queryParams.createTimeStart = queryParams.createTime?.[0]
queryParams.createTimeEnd = queryParams.createTime?.[1]
const data = await EmployeeBigBrotherApi.getEmployeeBigBrotherPage(queryParams)
list.value = data.list
total.value = data.total
checkedPageIds.value = data.list.map((row) => row.id)
console.log(checkedPageIds.value)
} finally {
loading.value = false
}
@@ -279,7 +295,31 @@ const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 删除按钮操作 */
const handleDeleteList = async () => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await EmployeeBigBrotherApi.deleteEmployeeBigBrotherList(checkedIds.value)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch { }
}
/** 删除本页按钮操作 */
const handleDeletePageList = async () => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await EmployeeBigBrotherApi.deleteEmployeeBigBrotherList(checkedPageIds.value)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch { }
}
// 格式化时间戳
function formatTimestamp(milliseconds) {
const date = new Date(milliseconds); // 直接使用毫秒级时间戳
@@ -360,7 +400,7 @@ const handleExport = async () => {
//分配按钮操作
const handleSelectionChange = (val) => {
checkedIds.value = val.map((row) => row.id)
selectBigList.value = val
console.log(selectBigList.value)
}

View File

@@ -134,7 +134,7 @@
<!-- <el-button type="success" plain @click="handleExport" v-hasPermi="['server:new-hosts:export']">
<Icon icon="ep:download" class="mr-5px" /> {{ t('newHosts.export') }}
</el-button> -->
<el-button v-if="!isMobile" type="danger" @click="handleDeleteList">
<el-button v-if="!isMobile" :disabled="checkedIds.length === 0" type="danger" @click="handleDeleteList">
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
<el-button v-else type="danger" @click="handleDeletePageList">

View File

@@ -110,6 +110,13 @@
:label="t(dict.label)" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item :label="t('newHosts.isDelete')" prop="isDelete">
<el-select v-model="queryParams.isDelete" :placeholder="t('newHosts.isDelete')" clearable class="!w-240px">
<el-option label="全部" :value="3" />
<el-option label="否" :value="0" />
<el-option label="是" :value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> {{ $t('employee.search') }}
@@ -339,6 +346,7 @@ const queryParams = reactive({
createTime: [],
userId: undefined,
operationStatus: undefined,
isDelete: 0,
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中

View File

@@ -173,7 +173,7 @@
<Icon icon="ep:copy-document" class="mr-5px" /> {{ $t('employee.exportAi') }}
</el-button>
<el-button type="danger" plain :disabled="checkedIds.length === 0" @click="handleDeleteBatch"
v-hasPermi="['system:post:delete']">
v-hasPermi="['server:new-hosts:delete']">
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
</el-form-item>
@@ -534,7 +534,6 @@ const handleSelectionChange = (val) => {
checkedIds.value = val.map((row) => row.id)
selectHostList.value = val
console.log(selectHostList.value)
console.log(checkedIds.value)
}
//分配确认

View File

@@ -1,46 +1,64 @@
<template>
<Dialog v-model="dialogVisible" :title="dialogTitle" width="50%">
<el-form ref="formRef" v-loading="formLoading" :model="formData" :rules="formRules" label-width="120px">
<!-- ========== 1. 基础信息 ========== -->
<el-divider>基础信息</el-divider>
<el-form-item label="租户名" prop="name">
<el-input v-model="formData.name" placeholder="请输入租户名" />
</el-form-item>
<el-form-item label="租户套餐" prop="packageId">
<el-select v-model="formData.packageId" clearable placeholder="请选择租户套餐">
<el-option v-for="item in packageList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<el-form-item v-if="tenantLevel <= 1" label="租户类型" prop="tenantType">
<el-radio-group v-model="formData.tenantType">
<el-radio value="代理">代理</el-radio>
<el-radio value="用户">用户</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="联系人" prop="contactName">
<el-input v-model="formData.contactName" placeholder="请输入联系人" />
</el-form-item>
<el-form-item label="联系手机" prop="contactMobile">
<el-input v-model="formData.contactMobile" placeholder="请输入联系手机" />
</el-form-item>
<el-form-item v-if="formData.id === undefined" label="用户名称" prop="username">
<el-input v-model="formData.username" placeholder="请输入用户名称" />
</el-form-item>
<el-form-item v-if="formData.id === undefined" label="用户密码" prop="password">
<el-input v-model="formData.password" placeholder="请输入用户密码" show-password type="password" />
</el-form-item>
<el-form-item label="账号额度" prop="accountCount">
<el-input-number v-model="formData.accountCount" :min="0" controls-position="right" placeholder="请输入账号额度" />
</el-form-item>
<el-form-item label="AI过期时间" prop="aiExpireTime">
<el-form-item v-if="tenantLevel == 0" label="AI过期时间" prop="aiExpireTime">
<el-date-picker v-model="formData.aiExpireTime" clearable placeholder="请选择AI过期时间" type="date"
value-format="x" />
</el-form-item>
<el-form-item label="爬大哥过期时间" prop="brotherExpireTime">
<el-form-item v-if="tenantLevel == 0" label="爬大哥过期时间" prop="brotherExpireTime">
<el-date-picker v-model="formData.brotherExpireTime" clearable placeholder="请选择爬大哥过期时间" type="date"
value-format="x" />
</el-form-item>
<el-form-item label="爬虫后台过期时间" prop="expireTime">
<el-form-item v-if="tenantLevel == 0" label="爬虫后台过期时间" prop="expireTime">
<el-date-picker v-model="formData.expireTime" clearable placeholder="请选择过期时间" type="date" value-format="x" />
</el-form-item>
<el-form-item v-else label="爬虫后台过期时间" prop="expireTime">
<el-date-picker v-model="formData.expireTime" clearable placeholder="请选择过期时间" disabled type="date"
value-format="x" />
</el-form-item>
<el-form-item label="绑定域名" prop="website">
<el-input v-model="formData.website" placeholder="请输入绑定域名" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" placeholder="请输入备注" />
</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">
@@ -48,14 +66,14 @@
</el-radio>
</el-radio-group>
</el-form-item>
<!-- 选择用户 -->
<el-form-item v-if="formType !== 'create'" label="选择用户" prop="userId">
<el-select v-model="userData.id" @change="changeUser" clearable placeholder="请选择租户">
<el-select v-model="userData.id" @change="changeUser" clearable placeholder="请选择租户用户">
<el-option v-for="item in userList" :key="item.id" :label="item.username" :value="item.id" />
</el-select>
</el-form-item>
<!-- 爬虫不受限制直接选 -->
<!-- 爬虫不受限制 -->
<el-form-item v-if="userData.id" label="爬虫" prop="status">
<el-radio-group v-model="userData.crawl">
<el-radio :value="1">开启</el-radio>
@@ -63,7 +81,7 @@
</el-radio-group>
</el-form-item>
<!-- 爬大哥必须先选爬大哥过期时间 -->
<!-- 爬大哥依赖爬大哥过期时间 -->
<el-form-item v-if="userData.id" label="爬大哥" prop="status">
<el-radio-group v-model="userData.bigBrother" :disabled="!formData.brotherExpireTime">
<el-radio :value="1">开启</el-radio>
@@ -71,7 +89,7 @@
</el-radio-group>
</el-form-item>
<!-- ai自动化必须先选AI过期时间 -->
<!-- ai自动化依赖 AI 过期时间 -->
<el-form-item v-if="userData.id" label="ai自动化" prop="status">
<el-radio-group v-model="userData.aiChat" :disabled="!formData.aiExpireTime">
<el-radio :value="1">开启</el-radio>
@@ -79,70 +97,137 @@
</el-radio-group>
</el-form-item>
<!-- ai回复必须先选AI过期时间 -->
<!-- ai回复依赖 AI 过期时间 -->
<el-form-item v-if="userData.id" label="ai回复" prop="status">
<el-radio-group v-model="userData.aiReplay" :disabled="!formData.aiExpireTime">
<el-radio :value="1">开启</el-radio>
<el-radio :value="0">关闭</el-radio>
</el-radio-group>
</el-form-item>
<!--WEB回复依赖 AI 过期时间 -->
<el-form-item v-if="userData.id" label="WEB客户端" prop="status">
<el-radio-group v-model="userData.webAi" :disabled="!formData.aiExpireTime">
<el-radio :value="1">开启</el-radio>
<el-radio :value="0">关闭</el-radio>
</el-radio-group>
</el-form-item>
<!-- ========== 2. 套餐 & 权限 ========== -->
<el-divider>套餐 & 权限</el-divider>
<el-form-item label="租户套餐" prop="packageId">
<!-- 顶层租户或无 parentId走原来的下拉选择 -->
<el-select v-if="(formData.parentId == 1 || formData.parentId == null) && tenantLevel == 0"
v-model="formData.packageId" clearable placeholder="请选择租户套餐">
<el-option v-for="item in packageList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<!-- 代理租户用功能勾选 + 套餐单选 -->
<div v-else style="display: block;">
<!-- 功能勾选1=爬大哥2=爬主播3=AI套餐 -->
<el-checkbox-group v-model="selectedFeatures">
<el-checkbox :label="1">爬主播</el-checkbox>
<el-checkbox :label="2">爬大哥</el-checkbox>
<el-checkbox :label="3">AI套餐</el-checkbox>
<el-checkbox :label="9">代理</el-checkbox>
</el-checkbox-group>
<!-- 按天数过滤30 / 180 / 360 -->
<div style="margin-top: 8px;">
<el-radio-group v-model="selectedDays">
<el-radio-button :label="30">30 </el-radio-button>
<el-radio-button :label="180">180 </el-radio-button>
<el-radio-button :label="365">365 </el-radio-button>
</el-radio-group>
</div>
<!-- 根据勾选结果筛选套餐用单选框选择一个套餐 -->
<div class="package-radio-wrapper">
<el-radio-group v-model="formData.packageId">
<el-radio-button v-for="item in filteredAgencyPackages" :key="item.id" :label="item.id"
class="package-radio">
{{ item.name }}
</el-radio-button>
</el-radio-group>
<div v-if="filteredAgencyPackages.length === 0" class="package-empty-tip">
暂无匹配套餐请调整上方功能勾选
</div>
</div>
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button :disabled="formLoading" type="primary" @click="submitForm">
</el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { ref, reactive, watch, computed } from 'vue'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import * as TenantApi from '@/api/system/tenant'
import { getUserByTenant, updateRoleUser } from '@/api/system/user'
import { CommonStatusEnum } from '@/utils/constants'
import * as TenantPackageApi from '@/api/system/tenantPackage'
import * as userApi from '@/api/system/user'
import { watch } from 'vue' // ⭐ 确保引入 watch
import { useUserStore } from '@/store/modules/user'
defineOptions({ name: 'SystemTenantForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const tenantLevel = ref(3)
TenantApi.getSelfTenantLevel().then(res => {
tenantLevel.value = res.tenantLevel
console.log(tenantLevel.value)
})
const { t } = useI18n()
const message = useMessage()
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formLoading = ref(false)
const formType = ref<'create' | 'update'>('create')
const userStore = useUserStore()
const formData = ref({
id: 0,
name: undefined,
packageId: undefined,
contactName: undefined,
contactMobile: undefined,
accountCount: undefined,
expireTime: undefined,
website: undefined,
name: undefined as string | undefined,
packageId: undefined as number | undefined,
contactName: undefined as string | undefined,
contactMobile: undefined as string | undefined,
accountCount: undefined as number | undefined,
expireTime: Date.now() as number | undefined,
website: undefined as string | undefined,
status: CommonStatusEnum.ENABLE,
// 新增专属
username: undefined,
password: undefined,
remark: undefined,
aiExpireTime: undefined,
brotherExpireTime: undefined,
username: undefined as string | undefined,
password: undefined as string | undefined,
remark: undefined as string | undefined,
aiExpireTime: undefined as number | undefined,
brotherExpireTime: undefined as number | undefined,
tenantType: undefined as number | undefined,
parentId: undefined as number | undefined,
})
const userData = ref({} as userApi.UserVO)
let userList = ref([{
id: 0,
aiChat: 0,
bigBrother: 0,
crawl: 0
}])
const userList = ref<userApi.UserVO[]>([
{
id: 0,
aiChat: 0,
bigBrother: 0,
crawl: 0
} as any
])
const formRules = reactive({
name: [{ required: true, message: '租户名不能为空', trigger: 'blur' }],
packageId: [{ required: true, message: '租户套餐不能为空', trigger: 'blur' }],
contactName: [{ required: true, message: '联系人不能为空', trigger: 'blur' }],
// contactMobile: [
// { required: true, message: '手机号不能为空', trigger: 'blur' },
// { pattern: /^1[3-9]\d{9}$/, message: '请输入有效的 11 位手机号', trigger: ['blur', 'change'] }
// ],
status: [{ required: true, message: '租户状态不能为空', trigger: 'blur' }],
accountCount: [{ required: true, message: '账号额度不能为空', trigger: 'blur' }],
expireTime: [{ required: true, message: '过期时间不能为空', trigger: 'blur' }],
@@ -153,46 +238,110 @@ const formRules = reactive({
],
password: [{ required: true, message: '用户密码不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
const packageList = ref([] as TenantPackageApi.TenantPackageVO[]) // 租户套餐
const formRef = ref()
const packageList = ref([] as TenantPackageApi.TenantPackageVO[])
const packageAgencyList = ref([] as TenantPackageApi.TenantPackageVO[])
// 代理套餐功能勾选1=爬大哥2=爬主播3=AI套餐
const selectedFeatures = ref<number[]>([])
// 按套餐天数筛选30 / 180 / 360不选则不过滤
const selectedDays = ref<number | null>(null)
// 二级代理套餐的 ID
const SECOND_AGENT_PACKAGE_ID = 10001
// 根据 selectedFeatures + packageAgencyList 计算可选套餐
const filteredAgencyPackages = computed(() => {
// 1. 根据创建/编辑确定基础套餐列表
let list: any[] = []
if (formType.value === 'create') {
list = packageList.value || []
} else {
list = packageAgencyList.value || []
}
if (!list.length) return []
const selected = selectedFeatures.value
// 2. 先按功能勾选过滤(保持你原来的逻辑)
let result: any[] = []
if (!selected.length) {
// 一个功能都没勾选时,直接返回全部套餐
result = list
} else {
// 有勾选时才进行过滤
result = list.filter((item: any) => {
const typeStr = String(item.packageType ?? '')
// 要求选中的功能全部包含在 packageType 里
// 例如勾选 [1,2] -> 匹配 '12'、'123' 等
return selected.every(flag => typeStr.includes(String(flag)))
})
}
// 3. 按天数过滤item.days === 30 / 180 / 360
if (selectedDays.value != null) {
result = result.filter((item: any) => item.days === selectedDays.value)
}
// 4. 再根据租户类型进行限制
const tenantType = formData.value.tenantType
if (tenantType === '代理') {
// 只能选二级代理套餐10001
result = result.filter(item => item.id === SECOND_AGENT_PACKAGE_ID)
} else if (tenantType === '租户') {
// 租户不能选二级代理套餐
result = result.filter(item => item.id !== SECOND_AGENT_PACKAGE_ID)
}
return result
})
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
const open = async (type: 'create' | 'update', id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
// 1. 租户基础信息
formData.value = await TenantApi.getTenant(id)
getUserByTenant(id).then(res => {
console.log(res)
userList.value = res
})
// 2. 拉取租户用户列表
const res = await getUserByTenant(id)
userList.value = res || []
// 3. 默认选中第一个用户(有的话)
if (userList.value.length > 0) {
userData.value = userList.value[0]
; (userData.value as any).tenantId = formData.value.id
}
} finally {
formLoading.value = false
}
}
// 加载套餐列表
// 套餐列表
packageList.value = await TenantPackageApi.getTenantPackageList()
if (userStore.getUser.id === 1) {
packageAgencyList.value = await TenantPackageApi.getTenantPackageListagency()
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
defineExpose({ open })
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const emit = defineEmits(['success'])
const submitForm = async () => {
// 校验表单
if (!formRef) return
if (!formRef.value) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as TenantApi.TenantVO
if (formType.value === 'create') {
//下次更改内容,创建账号 给一个权限 爬大哥和AI
await TenantApi.createTenant(data)
message.success(t('common.createSuccess'))
} else {
@@ -201,7 +350,6 @@ const submitForm = async () => {
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
@@ -211,36 +359,48 @@ const submitForm = async () => {
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
id: undefined as any,
name: undefined,
packageId: undefined,
contactName: undefined,
contactMobile: undefined,
accountCount: undefined,
expireTime: undefined,
expireTime: Date.now(),
website: undefined,
status: CommonStatusEnum.ENABLE,
username: undefined,
password: undefined,
remark: undefined,
aiExpireTime: undefined, // ⭐ 加上
brotherExpireTime: undefined, // ⭐ 加上
aiExpireTime: undefined,
brotherExpireTime: undefined
}
userData.value = {} as userApi.UserVO
formRef.value?.resetFields()
userData.value = {} as any
userList.value = [
{
id: 0,
aiChat: 0,
bigBrother: 0,
crawl: 0
} as any
]
// ⭐ 每次打开弹窗时都清空功能选择
selectedFeatures.value = []
formRef.value?.resetFields?.()
}
function changeUser(val) {
userList.value.forEach(item => {
/** 切换用户 */
function changeUser(val: number) {
userList.value.forEach((item) => {
if (item.id === val) {
userData.value = item
userData.value.tenantId = formData.value.id
; (userData.value as any).tenantId = formData.value.id
}
})
}
// AI 过期时间变化:没有时间时,禁止并关闭 ai 功能
/** 联动AI 过期时间 -> AI 权限 */
watch(
() => formData.value.aiExpireTime,
(val) => {
@@ -251,7 +411,7 @@ watch(
}
)
// 爬大哥 过期时间变化:没有时间时,禁止并关闭 bigBrother
/** 联动:爬大哥过期时间 -> bigBrother 权限 */
watch(
() => formData.value.brotherExpireTime,
(val) => {
@@ -261,3 +421,35 @@ watch(
}
)
</script>
<style scoped>
.package-radio-wrapper {
margin-top: 10px;
}
/* 让按钮自动换行,并留点间隔 */
.package-radio-wrapper :deep(.el-radio-group) {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
/* 美化每个 radio-button 的外观(胶囊 + 内边距) */
.package-radio-wrapper :deep(.el-radio-button__inner) {
border-radius: 999px;
padding: 4px 10px;
border: 1px solid #dcdfe6;
height: 30px;
}
/* 选中状态可以稍微加粗/加边框Element Plus 自带选中样式,这里可选 */
.package-radio-wrapper :deep(.is-active .el-radio-button__inner) {
border-color: #409eff;
}
/* 空数据提示 */
.package-empty-tip {
color: #999;
font-size: 12px;
margin-top: 5px;
}
</style>

View File

@@ -0,0 +1,104 @@
<template>
<Dialog :title="'租户续费'" v-model="dialogVisible">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" v-loading="formLoading">
<!-- id选中租户的 id只读展示 -->
<el-form-item label="租户 ID" prop="id">
<el-input v-model="formData.id" disabled />
</el-form-item>
<!-- packageId套餐选择框 -->
<el-form-item label="续费套餐" prop="packageId">
<el-select v-model="formData.packageId" placeholder="请选择套餐" class="w-240px">
<el-option v-for="pkg in packageList" :key="pkg.id" :label="pkg.name" :value="pkg.id" />
</el-select>
</el-form-item>
<!-- remark备注 -->
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" type="textarea" placeholder="请输入备注" :rows="3" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" :loading="formLoading" @click="submitForm">
</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { useMessage } from '@/hooks/web/useMessage'
import * as TenantApi from '@/api/system/tenant'
import * as TenantPackageApi from '@/api/system/tenantPackage'
const message = useMessage()
const emit = defineEmits(['success'])
const dialogVisible = ref(false)
const formRef = ref()
const formLoading = ref(false)
// 表单数据id / packageId / remark
const formData = reactive<{
id: number | null
packageId: number | null
remark: string
}>({
id: null,
packageId: null,
remark: ''
})
const formRules = {
packageId: [{ required: true, message: '请选择续费套餐', trigger: 'change' }],
// 如果备注必填就放开下面这一条
// remark: [{ required: true, message: '请输入备注', trigger: 'blur' }]
}
// 套餐列表
const packageList = ref<TenantPackageApi.TenantPackageVO[]>([])
// 对外暴露的 open 方法:传入租户 id
const open = async (id: number) => {
// 重置表单
formData.id = id
formData.packageId = null
formData.remark = ''
dialogVisible.value = true
// 需要时再加载套餐列表
if (!packageList.value.length) {
packageList.value = await TenantPackageApi.getTenantPackageList()
}
}
defineExpose({ open })
// 提交
const submitForm = () => {
formRef.value?.validate(async (valid: boolean) => {
if (!valid) return
formLoading.value = true
try {
// 🔴 根据你原来续费的接口改这里的调用
// 假设后端有一个 TenantApi.renewTenant
await TenantApi.renewal({
id: formData.id,
packageId: formData.packageId,
remark: formData.remark
})
message.success('续费成功')
dialogVisible.value = false
emit('success') // 通知父组件刷新列表
} catch (e) {
console.error(e)
} finally {
formLoading.value = false
}
})
}
</script>

View File

@@ -73,24 +73,25 @@
</el-form>
</ContentWrap>
<!-- 列表 -->
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" @selection-change="handleRowCheckboxChange">
<el-table v-loading="loading" :data="list" row-key="id" lazy :load="loadChildren"
:tree-props="{ children: 'children', hasChildren: 'hasChildren', checkStrictly: false }"
@selection-change="handleRowCheckboxChange" :row-class-name="rowClassName">
<el-table-column type="selection" width="55" />
<el-table-column label="租户编号" align="center" prop="id" />
<el-table-column label="租户编号" prop="id" width="100" />
<el-table-column label="租户名" align="center" prop="name" />
<el-table-column label="租户类型" align="center" prop="tenantType" />
<el-table-column label="租户套餐" align="center" prop="packageId">
<template #default="scope">
<el-tag v-if="scope.row.packageId === 0" type="danger">系统租户</el-tag>
<template v-else v-for="item in packageList">
<el-tag type="success" :key="item.id" v-if="item.id === scope.row.packageId">
{{ item.name }}
</el-tag>
</template>
<el-tag :type="scope.row.packageId === 0 ? 'danger' : 'success'">
{{ getPackageName(scope.row.packageId) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="联系人" align="center" prop="contactName" />
<el-table-column label="联系手机" align="center" prop="contactMobile" />
<el-table-column label="联系手机" align="center" prop="contactMobile" />1
<el-table-column label="账号额度" align="center" prop="accountCount">
<template #default="scope">
<el-tag>{{ scope.row.accountCount }}</el-tag>
@@ -98,7 +99,7 @@
</el-table-column>
<el-table-column label="AI时间" align="center" prop="aiExpireTime" width="180" :formatter="dateFormatter" />
<el-table-column label="爬大哥时间" align="center" prop="brotherExpireTime" width="180" :formatter="dateFormatter" />
<el-table-column label="爬虫后台过期时间" align="center" prop="expireTime" width="180" :formatter="dateFormatter" />
<el-table-column label="后台管理过期时间" align="center" prop="expireTime" width="180" :formatter="dateFormatter" />
<el-table-column label="绑定域名" align="center" prop="website" width="180" />
<el-table-column label="租户状态" align="center" prop="status">
<template #default="scope">
@@ -107,12 +108,16 @@
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="创建时间" align="center" prop="createTime" width="180" :formatter="dateFormatter" />
<el-table-column label="操作" align="center" min-width="110" fixed="right">
<el-table-column label="操作" align="center" min-width="160" fixed="right">
<template #default="scope">
<el-button link type="primary" @click="openForm('update', scope.row.id)"
v-hasPermi="['system:tenant:update']">
编辑
</el-button>
<el-button v-if="userStore.getUser.id != 1" link type="warning" @click="openRenewForm(scope.row.id)"
v-hasPermi="['system:tenant:renewal']">
续费
</el-button>
<el-button link type="danger" @click="handleDelete(scope.row.id)" v-hasPermi="['system:tenant:delete']">
删除
</el-button>
@@ -124,8 +129,10 @@
@pagination="getList" />
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<TenantForm ref="formRef" @success="getList" />
<TenantRenewForm ref="renewFormRef" @success="getList" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
@@ -134,9 +141,12 @@ import download from '@/utils/download'
import * as TenantApi from '@/api/system/tenant'
import * as TenantPackageApi from '@/api/system/tenantPackage'
import TenantForm from './TenantForm.vue'
import { useUserStore } from '@/store/modules/user'
import { toRaw } from 'vue' // 🔴 新增
import TenantRenewForm from './TenantRenewForm.vue'
defineOptions({ name: 'SystemTenant' })
const userStore = useUserStore()
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
@@ -150,6 +160,7 @@ const queryParams = reactive({
contactName: undefined,
contactMobile: undefined,
status: undefined,
tenantType: undefined,
remark: undefined,
createTime: [],
aiExpireTime: [],
@@ -157,21 +168,39 @@ const queryParams = reactive({
expireTime: [],
})
const queryFormRef = ref() // 搜索的表单
const renewFormRef = ref() // 续费弹窗
const openRenewForm = (id: number) => {
renewFormRef.value.open(id)
}
const exportLoading = ref(false) // 导出的加载中
const packageList = ref([] as TenantPackageApi.TenantPackageVO[]) //租户套餐列表
const packageAgencyList = ref([] as TenantPackageApi.TenantPackageVO[]) //租户套餐列表
/** 查询列表 */
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await TenantApi.getTenantPage(queryParams)
list.value = data.list
let data: any = ''
if (userStore.getUser.id === 1) {
data = await TenantApi.getTenantPage(queryParams)
} else {
data = await TenantApi.getTenantPageSelf(queryParams)
}
// 给顶级节点加 level = 1
list.value = (data.list || []).map((item: any) => ({
...item,
level: 1
}))
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
@@ -186,7 +215,7 @@ const resetQuery = () => {
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
const openForm = (type: 'create' | 'update' | 'renew', id?: number) => {
formRef.value.open(type, id)
}
@@ -235,11 +264,96 @@ const handleExport = async () => {
exportLoading.value = false
}
}
const getPackageName = (packageId: number) => {
if (userStore.getUser.id === 0) return '系统租户'
// 先从 packageList 找
const pkg = packageList.value.find((p) => p.id === packageId)
if (pkg) return pkg.name
// packageList 未找到,并且 packageId !== 1 时,再从 packageAgencyList 找
if (packageId !== 1) {
const agency = packageAgencyList.value.find((a) => a.id === packageId)
if (agency) return agency.name
}
return '未知套餐'
}
// 懒加载子租户(树形表格用)
const loadChildren = async (
row: TenantApi.TenantVO & { level?: number },
treeNode: unknown,
resolve: (data: TenantApi.TenantVO[]) => void
) => {
try {
const baseParams = toRaw(queryParams) as any
const params = {
...baseParams,
pageNo: 1,
pageSize: 999,
id: row.id
}
let data: any
data = await TenantApi.getTenantChildren(params)
const parentLevel = row.level ?? 1
// 子节点 level = 父节点 level + 1
const children = (data || []).map((item: any) => ({
...item,
level: parentLevel + 1
}))
resolve(children)
} catch (e) {
console.error('loadChildren error', e)
resolve([])
}
}
// 行样式 class
const rowClassName = ({ row }: { row: any }) => {
if (row.level === 2) return 'row-level-2'
if (row.level >= 3) return 'row-level-3'
return ''
}
/** 初始化 **/
onMounted(async () => {
await getList()
// 获取租户套餐列表
packageList.value = await TenantPackageApi.getTenantPackageList()
if (userStore.getUser.id === 1) {
packageAgencyList.value = await TenantPackageApi.getTenantPackageListagency()
}
})
</script>
<style scoped lang="scss">
/* 二级行背景色 */
:deep(.row-level-2) {
background-color: #fdf6ec;
/* 浅橙色,你可以自己换 */
}
/* 三级及以下行背景色 */
:deep(.row-level-3) {
background-color: #dedeff;
/* 浅灰色 */
}
/* 可选:加个左边彩色竖条,更醒目 */
:deep(.row-level-2 td:first-child),
:deep(.row-level-3 td:first-child) {
border-left: 4px solid #e6a23c;
/* 二级竖条 */
}
:deep(.row-level-3 td:first-child) {
border-left-color: #3636e0;
/* 三级竖条 */
}
</style>

View File

@@ -1,12 +1,6 @@
<template>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="80px"
>
<el-form ref="formRef" v-loading="formLoading" :model="formData" :rules="formRules" label-width="80px">
<el-form-item label="套餐名" prop="name">
<el-input v-model="formData.name" placeholder="请输入套餐名" />
</el-form-item>
@@ -14,39 +8,19 @@
<el-card class="w-full h-400px !overflow-y-scroll" shadow="never">
<template #header>
全选/全不选:
<el-switch
v-model="treeNodeAll"
active-text=""
inactive-text=""
inline-prompt
@change="handleCheckedTreeNodeAll"
/>
<el-switch v-model="treeNodeAll" active-text="是" inactive-text="否" inline-prompt
@change="handleCheckedTreeNodeAll" />
全部展开/折叠:
<el-switch
v-model="menuExpand"
active-text="展开"
inactive-text="折叠"
inline-prompt
@change="handleCheckedTreeExpand"
/>
<el-switch v-model="menuExpand" active-text="展开" inactive-text="折叠" inline-prompt
@change="handleCheckedTreeExpand" />
</template>
<el-tree
ref="treeRef"
:data="menuOptions"
:props="defaultProps"
empty-text="加载中请稍候"
node-key="id"
show-checkbox
/>
<el-tree ref="treeRef" :data="menuOptions" :props="defaultProps" empty-text="加载中请稍候" node-key="id"
show-checkbox />
</el-card>
</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"
>
<el-radio v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" :key="dict.value" :value="dict.value">
{{ dict.label }}
</el-radio>
</el-radio-group>

View File

@@ -0,0 +1,249 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" v-loading="formLoading">
<el-form-item label="套餐名" prop="name">
<el-input v-model="formData.name" placeholder="请输入套餐名" />
</el-form-item>
<el-form-item label="租户状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio :value="0">正常</el-radio>
<el-radio :value="1">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" placeholder="请输入备注" />
</el-form-item>
<!-- 改成树形菜单选择 -->
<el-form-item label="菜单权限" prop="menuIds">
<el-card class="w-full h-400px !overflow-y-scroll" shadow="never">
<template #header>
全选/全不选
<el-switch v-model="treeNodeAll" active-text="是" inactive-text="否" inline-prompt
@change="handleCheckedTreeNodeAll" />
&nbsp;全部展开/折叠
<el-switch v-model="menuExpand" active-text="展开" inactive-text="折叠" inline-prompt
@change="handleCheckedTreeExpand" />
</template>
<el-tree ref="treeRef" :data="menuOptions" :props="defaultProps" empty-text="加载中请稍候" node-key="id"
show-checkbox />
</el-card>
</el-form-item>
<el-form-item label="套餐天数" prop="days">
<el-input v-model="formData.days" placeholder="请输入套餐天数" />
</el-form-item>
<el-form-item label="套餐价格" prop="price">
<el-input v-model="formData.price" placeholder="请输入套餐价格" />
</el-form-item>
<!-- 爬大哥必须先选爬大哥过期时间 -->
<el-form-item label="爬大哥">
<el-radio-group v-model="formData.brotherClient">
<el-radio :value="1">开启</el-radio>
<el-radio :value="0">关闭</el-radio>
</el-radio-group>
</el-form-item>
<!-- ai自动化必须先选AI过期时间 -->
<el-form-item label="爬主播">
<el-radio-group v-model="formData.hostslClient">
<el-radio :value="1">开启</el-radio>
<el-radio :value="0">关闭</el-radio>
</el-radio-group>
</el-form-item>
<!-- ai回复必须先选AI过期时间 -->
<el-form-item label="AI客户端">
<el-radio-group v-model="formData.aiClient">
<el-radio :value="1">开启</el-radio>
<el-radio :value="0">关闭</el-radio>
</el-radio-group>
</el-form-item>
<!-- ai回复必须先选AI过期时间 -->
<el-form-item label="AI回复">
<el-radio-group v-model="formData.aiReplay">
<el-radio :value="1">开启</el-radio>
<el-radio :value="0">关闭</el-radio>
</el-radio-group>
</el-form-item>
<!-- WEB回复必须先选AI过期时间 -->
<el-form-item label="WEB客户端">
<el-radio-group v-model="formData.webAi">
<el-radio :value="1">开启</el-radio>
<el-radio :value="0">关闭</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="套餐类型" prop="packageType">
<el-input v-model="formData.packageType" placeholder="请输入套餐类型" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { TenantAgencyPackageApi, TenantAgencyPackageVO } from '@/api/system/tenantagencypackage'
import * as MenuApi from '@/api/system/menu'
import { defaultProps, handleTree } from '@/utils/tree'
import { ElTree } from 'element-plus'
/** 代理租户套餐 表单 */
defineOptions({ name: 'TenantAgencyPackageForm' })
const { t } = useI18n()
const message = useMessage()
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formLoading = ref(false)
const formType = ref<'create' | 'update' | string>('')
const formData = ref({
id: undefined as number | undefined,
name: undefined as string | undefined,
status: 0 as 0 | 1 | undefined, // 默认正常
remark: undefined as string | undefined,
menuIds: [] as number[], // ✅ 改成数组
days: undefined as number | undefined,
price: undefined as number | undefined,
hostslClient: undefined as number | undefined,
brotherClient: undefined as number | undefined,
aiClient: undefined as number | undefined,
aiReplay: undefined as number | undefined,
webAi: undefined as number | undefined,
packageType: undefined as string | undefined
})
const formRules = reactive({
name: [{ required: true, message: '套餐名不能为空', trigger: 'blur' }],
status: [{ required: true, message: '租户状态不能为空', trigger: 'change' }],
// menuIds: [{ required: true, message: '关联的菜单编号不能为空', trigger: 'change' }], // ✅ 校验菜单
days: [{ required: true, message: '套餐天数不能为空', trigger: 'blur' }],
price: [{ required: true, message: '套餐价格不能为空', trigger: 'blur' }]
})
const formRef = ref()
// ✅ 菜单树相关
const menuOptions = ref<any[]>([])
const menuExpand = ref(false)
const treeRef = ref<InstanceType<typeof ElTree>>()
const treeNodeAll = ref(false)
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
await resetForm()
// 先加载菜单树数据(一定要先有数据,后 setChecked
formLoading.value = true
try {
const menus = await MenuApi.getSimpleMenusList()
menuOptions.value = handleTree(menus)
} finally {
formLoading.value = false
}
// 编辑:回显数据并勾选
if (id) {
formLoading.value = true
try {
const data = await TenantAgencyPackageApi.getTenantAgencyPackage(id)
formData.value = {
...formData.value,
...data,
menuIds: Array.isArray(data.menuIds) ? data.menuIds : []
}
// 勾选已有关联
formData.value.menuIds.forEach((mid: number) => {
treeRef.value?.setChecked(mid, true, false)
})
} finally {
formLoading.value = false
}
}
}
defineExpose({ open })
/** 提交表单 */
const emit = defineEmits(['success'])
const submitForm = async () => {
// 1. 先做表单校验
await formRef.value.validate()
// 2. 从树组件拿选中的菜单 id包含半选
const checked = (treeRef.value?.getCheckedKeys(false) || []) as number[]
const half = (treeRef.value?.getHalfCheckedKeys() || []) as number[]
const menuIds = Array.from(new Set([...checked, ...half])) // 去重一下
// 3. 把 menuIds 塞回 formData或者直接合并到 data 里)
formData.value.menuIds = menuIds
formLoading.value = true
try {
// 4. 组装真正发给后端的参数
const data = {
...(formData.value as any),
menuIds // ✅ 确保有这个字段
} as TenantAgencyPackageVO
if (formType.value === 'create') {
await TenantAgencyPackageApi.createTenantAgencyPackage(data)
message.success(t('common.createSuccess'))
} else {
await TenantAgencyPackageApi.updateTenantAgencyPackage(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = async () => {
treeNodeAll.value = false
menuExpand.value = false
formData.value = {
id: undefined,
name: undefined,
status: 0,
remark: undefined,
menuIds: [], // ✅ 清空
days: undefined,
price: undefined,
hostslClient: undefined,
brotherClient: undefined,
aiClient: undefined,
aiReplay: undefined,
packageType: undefined
}
treeRef.value?.setCheckedNodes([])
formRef.value?.resetFields()
}
/** 全选/全不选 */
const handleCheckedTreeNodeAll = () => {
treeRef.value?.setCheckedNodes(treeNodeAll.value ? menuOptions.value : [])
}
/** 展开/折叠全部 */
const handleCheckedTreeExpand = () => {
const nodes = (treeRef.value as any)?.store?.nodesMap
if (!nodes) return
for (const key in nodes) {
if (Object.prototype.hasOwnProperty.call(nodes, key)) {
nodes[key].expanded = menuExpand.value
}
}
}
</script>

View File

@@ -0,0 +1,201 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true">
<el-form-item label="套餐名" prop="name">
<el-input v-model="queryParams.name" placeholder="请输入套餐名" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="租户状态 " prop="status">
<el-select v-model="queryParams.status" placeholder="请选择租户状态 " clearable class="!w-240px">
<el-option label="请选择字典生成" value="" />
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="queryParams.remark" placeholder="请输入备注" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker v-model="queryParams.createTime" value-format="YYYY-MM-DD HH:mm:ss" type="daterange"
start-placeholder="开始日期" end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" class="!w-220px" />
</el-form-item>
<el-form-item label="套餐天数" prop="days">
<el-input v-model="queryParams.days" placeholder="请输入套餐天数" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="套餐价格" prop="price">
<el-input v-model="queryParams.price" placeholder="请输入套餐价格" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="爬主播客户端 " prop="hostslClient">
<el-input v-model="queryParams.hostslClient" placeholder="请输入爬主播客户端 " clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="爬大哥客户端 " prop="brotherClient">
<el-input v-model="queryParams.brotherClient" placeholder="请输入爬大哥客户端 " clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="AI 客户端 " prop="aiClient">
<el-input v-model="queryParams.aiClient" placeholder="请输入AI 客户端 " clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="套餐类型" prop="packageType">
<el-select v-model="queryParams.packageType" placeholder="请选择套餐类型" clearable class="!w-240px">
<el-option label="请选择字典生成" value="" />
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> 搜索
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" /> 重置
</el-button>
<el-button type="primary" plain @click="openForm('create')"
v-hasPermi="['system:tenant-agency-package:create']">
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button type="success" plain @click="handleExport" :loading="exportLoading"
v-hasPermi="['system:tenant-agency-package:export']">
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="套餐编号" align="center" prop="id" />
<el-table-column label="套餐名" align="center" prop="name" width="200px" />
<!-- <el-table-column label="租户状态 " align="center" prop="status" /> -->
<!-- <el-table-column label="关联的菜单编号" align="center" prop="menuIds" /> -->
<el-table-column label="创建时间" align="center" prop="createTime" :formatter="dateFormatter" width="180px" />
<el-table-column label="套餐天数" align="center" prop="days" />
<el-table-column label="套餐价格" align="center" prop="price" />
<!-- <el-table-column label="爬主播客户端 " align="center" prop="hostslClient" />
<el-table-column label="爬大哥客户端 " align="center" prop="brotherClient" />
<el-table-column label="AI 客户端 " align="center" prop="aiClient" /> -->
<el-table-column label="套餐类型" align="center" prop="packageType" />
<el-table-column label="备注" align="center" prop="remark" min-width="50px" />
<el-table-column label="操作" align="center" min-width="120px">
<template #default="scope">
<el-button link type="primary" @click="openForm('update', scope.row.id)"
v-hasPermi="['system:tenant-agency-package:update']">
编辑
</el-button>
<el-button link type="danger" @click="handleDelete(scope.row.id)"
v-hasPermi="['system:tenant-agency-package:delete']">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="getList" />
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<TenantAgencyPackageForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { TenantAgencyPackageApi, TenantAgencyPackageVO } from '@/api/system/tenantagencypackage'
import TenantAgencyPackageForm from './TenantAgencyPackageForm.vue'
/** 代理租户套餐 列表 */
defineOptions({ name: 'TenantAgencyPackage' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中
const list = ref<TenantAgencyPackageVO[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
status: undefined,
remark: undefined,
menuIds: undefined,
createTime: [],
days: undefined,
price: undefined,
hostslClient: undefined,
brotherClient: undefined,
aiClient: undefined,
packageType: undefined
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await TenantAgencyPackageApi.getTenantAgencyPackagePage(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 TenantAgencyPackageApi.deleteTenantAgencyPackage(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch { }
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
const data = await TenantAgencyPackageApi.exportTenantAgencyPackage(queryParams)
download.excel(data, '代理租户套餐.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

View File

@@ -0,0 +1,169 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form v-if="formType === 'update'" ref="formRef" :model="formData" :rules="formRules" label-width="100px"
v-loading="formLoading">
<el-form-item label="充值用户" prop="id">
<el-input v-model="formData.id" placeholder="请输入充值用户" />
</el-form-item>
<el-form-item v-if="formType === 'update'" label="充值金额" prop="amount">
<!-- 改为 el-input-number仅允许数值输入且最小 0 -->
<el-input-number v-model.number="formData.amount" :min="0" controls-position="right" placeholder="请输入充值金额" />
</el-form-item>
<!-- 只有 transfer 类型时显示 -->
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" placeholder="请输入备注" />
</el-form-item>
</el-form>
<el-form v-if="formType === 'transfer'" ref="formRef" :model="formData" :rules="formRules" label-width="100px"
v-loading="formLoading">
<el-form-item label="转账用户" prop="targetTenantId">
<el-input v-model="formData.targetTenantId" placeholder="请输入转账用户" />
</el-form-item>
<el-form-item v-if="formType === 'transfer'" label="转账金额" prop="transferAmount">
<!-- 改为 el-input-number仅允许数值输入且最小 0 -->
<el-input-number v-model.number="formData.transferAmount" :min="0" controls-position="right"
placeholder="请输入转账金额" />
</el-form-item>
<el-form-item v-if="formType === 'transfer'" label="登录密码" prop="password">
<el-input v-model="formData.password" type="password" show-password placeholder="请输入登录密码" />
</el-form-item>
<!-- 只有 transfer 类型时显示 -->
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading">
</el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { computed, ref, reactive } from 'vue'
import { TenantBalanceApi, TenantBalanceVO } from '@/api/system/tenantbalance'
/** 租户余额 表单 */
defineOptions({ name: 'TenantBalanceForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改transfer - 转账等
const formData = ref({
tenantId: undefined as number | undefined,
id: undefined as number | undefined,
amount: undefined as number | undefined,
transferAmount: undefined as number | undefined,
targetTenantId: undefined as number | undefined,
remark: undefined as string | undefined, // 变动表述
password: undefined as string | undefined // 登录密码
})
/** 校验规则transfer 时才要求 remark 必填 */
const formRules = computed(() => {
const baseRules: any = {
id: [{ required: true, message: '用户不能为空', trigger: 'blur' }],
amount: [{ required: true, message: '金额不能为空', trigger: 'blur' }]
}
if (formType.value === 'transfer') {
baseRules.password = [
{ required: true, message: '登录密码不能为空', trigger: 'blur' }
]
baseRules.transferAmount = [
{ required: true, message: '转账金额不能为空', trigger: 'blur' }
]
}
return baseRules
})
const formRef = ref() // 表单 Ref
/** 打开弹窗 */
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 data = await TenantBalanceApi.getTenantBalance(id)
// 保证有 remark 和 password 字段(接口没有的话就保持 undefined
formData.value = {
tenantId: data.tenantId,
id: data.id,
amount: data.amount,
targetTenantId: data.id,
remark: undefined,
password: undefined,
transferAmount: undefined
}
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
console.log(formData.value)
// 校验表单
await formRef.value.validate()
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as TenantBalanceVO
if (formType.value === 'create') {
await TenantBalanceApi.createTenantBalance(data)
message.success(t('common.createSuccess'))
} else if (formType.value === 'transfer') {
await TenantBalanceApi.tenantTransfer(data)
message.success(t('common.updateSuccess'))
} else {
await TenantBalanceApi.addAmount(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
tenantId: undefined,
id: undefined,
amount: undefined,
transferAmount: undefined,
remark: undefined,
password: undefined
}
formRef.value?.resetFields()
}
</script>

View File

@@ -0,0 +1,132 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" v-loading="formLoading">
<el-form-item label="本次变动点数,正加负减" prop="points">
<el-input v-model="formData.points" placeholder="请输入本次变动点数,正加负减" />
</el-form-item>
<el-form-item label="变动后余额快照(冗余)" prop="balance">
<el-input v-model="formData.balance" placeholder="请输入变动后余额快照(冗余)" />
</el-form-item>
<el-form-item label="变动类型,如 RECHARGE, CONSUME, TRANSFER_OUT, TRANSFER_IN" prop="type">
<el-select v-model="formData.type" placeholder="请选择变动类型,如 RECHARGE, CONSUME, TRANSFER_OUT, TRANSFER_IN">
<el-option label="请选择字典生成" value="" />
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<Editor v-model="formData.remark" height="150px" />
</el-form-item>
<el-form-item label="订单 Id/业务单号" prop="orderId">
<el-input v-model="formData.orderId" placeholder="请输入订单 Id/业务单号" />
</el-form-item>
<el-form-item label="业务流水号(转账、订单等唯一标识)" prop="bizNo">
<el-input v-model="formData.bizNo" placeholder="请输入业务流水号(转账、订单等唯一标识)" />
</el-form-item>
<el-form-item label="操作人 Id" prop="operatorId">
<el-input v-model="formData.operatorId" placeholder="请输入操作人 Id" />
</el-form-item>
<el-form-item label="目标租户 Id转账使用" prop="targetTenantId">
<el-input v-model="formData.targetTenantId" placeholder="请输入目标租户 Id转账使用" />
</el-form-item>
<el-form-item label="创建时间" prop="createdAt">
<el-date-picker v-model="formData.createdAt" type="date" value-format="x" placeholder="选择创建时间" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { TenantBalanceApi } from '@/api/system/tenantbalance'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref({
id: undefined,
points: undefined,
balance: undefined,
type: undefined,
remark: undefined,
orderId: undefined,
bizNo: undefined,
operatorId: undefined,
targetTenantId: undefined,
createdAt: undefined,
remark: undefined
})
const formRules = reactive({
points: [{ required: true, message: '本次变动点数,正加负减不能为空', trigger: 'blur' }],
createdAt: [{ required: true, message: '创建时间不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
/** 打开弹窗 */
const open = async (type: string, id?: number, tenantId: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
formData.value.tenantId = tenantId
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await TenantBalanceApi.getTenantPoints(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
await formRef.value.validate()
// 提交请求
formLoading.value = true
try {
const data = formData.value
if (formType.value === 'create') {
await TenantBalanceApi.createTenantPoints(data)
message.success(t('common.createSuccess'))
} else {
await TenantBalanceApi.updateTenantPoints(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
points: undefined,
balance: undefined,
type: undefined,
remark: undefined,
orderId: undefined,
bizNo: undefined,
operatorId: undefined,
targetTenantId: undefined,
createdAt: undefined,
}
formRef.value?.resetFields()
}
</script>

View File

@@ -0,0 +1,112 @@
<template>
<!-- 列表 -->
<ContentWrap>
<!-- <el-button type="primary" plain @click="openForm('create')" v-hasPermi="['system:tenant-balance:create']">
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button> -->
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<!-- <el-table-column label="主键" align="center" prop="id" /> -->
<el-table-column label="金额" align="center" prop="points" />
<el-table-column label="变动后余额" align="center" prop="balance" />
<el-table-column label="类型" align="center" prop="type" />
<el-table-column label="描述" align="center" prop="description" />
<el-table-column label="订单 Id/业务单号" align="center" prop="orderId" />
<el-table-column label="业务流水号" align="center" prop="bizNo" />
<el-table-column label="操作人 Id" align="center" prop="operatorId" />
<el-table-column label="目标租户Id" align="center" prop="targetTenantId" />
<el-table-column label="创建时间" align="center" prop="createdAt" :formatter="dateFormatter" width="180px" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button link type="primary" @click="openForm('update', scope.row.id)"
v-hasPermi="['system:tenant-balance:update']">
编辑
</el-button>
<el-button link type="danger" @click="handleDelete(scope.row.id)"
v-hasPermi="['system:tenant-balance:delete']">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="getList" />
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<TenantPointsForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { dateFormatter } from '@/utils/formatTime'
import { TenantBalanceApi } from '@/api/system/tenantbalance'
import TenantPointsForm from './TenantPointsForm.vue'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const props = defineProps<{
tenantId?: number // 租户 Id主表的关联字段
}>()
const loading = ref(false) // 列表的加载中
const list = ref([]) // 列表的数据
const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
tenantId: undefined as unknown
})
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.tenantId,
(val: number) => {
if (!val) {
return
}
queryParams.tenantId = val
handleQuery()
},
{ immediate: true, deep: true }
)
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await TenantBalanceApi.getTenantPointsPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
if (!props.tenantId) {
message.error('请选择一个租户余额')
return
}
formRef.value.open(type, id, props.tenantId)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await TenantBalanceApi.deleteTenantPoints(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch { }
}
</script>

View File

@@ -0,0 +1,220 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
<el-form-item label="积分余额" prop="balance">
<el-input v-model="queryParams.balance" placeholder="请输入当前积分余额" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="租户id" prop="id">
<el-input v-model="queryParams.id" placeholder="请输入租户id" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="租户名" prop="tenantName">
<el-input v-model="queryParams.tenantName" placeholder="请输入租户id" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="更新时间" prop="updatedAt">
<el-date-picker v-model="queryParams.updatedAt" value-format="YYYY-MM-DD" type="date" placeholder="选择更新时间"
clearable class="!w-240px" />
</el-form-item>
<el-form-item style="width: 100%;">
<div class="center-align">
<div>
<el-button @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> 搜索
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" /> 重置
</el-button>
<el-button type="primary" plain @click="openForm('create')" v-hasPermi="['system:tenant-balance:create']">
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button type="success" plain @click="handleExport" :loading="exportLoading"
v-hasPermi="['system:tenant-balance:export']">
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</div>
<div v-if="userStore.getUser.id !== 1" class="balance-text">余额{{ mount.balance }} 测试号{{
mount.testAccountNum }}</div>
</div>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" highlight-current-row
@current-change="handleCurrentChange">
<el-table-column label="当前积分余额" align="center" prop="balance" />
<el-table-column label="租户名" align="center" prop="tenantName" />
<el-table-column label="租户id" align="center" prop="id" />
<el-table-column label="更新时间" align="center" prop="updatedAt" :formatter="dateFormatter" width="180px" />
<el-table-column label="描述" align="center" prop="remark" />
<el-table-column label="操作" align="center" min-width="120px">
<template #default="scope">
<el-button link type="primary" @click="openForm('update', scope.row.id)"
v-hasPermi="['system:tenant-balance:update']">
充值
</el-button>
<el-button v-if="userStore.getUser.id !== 1" link type="primary" @click="openForm('transfer', scope.row.id)"
v-hasPermi="['system:tenant-balance:transfer']">
转账
</el-button>
<el-button link type="danger" @click="handleDelete(scope.row.id)"
v-hasPermi="['system:tenant-balance:delete']">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="getList" />
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<TenantBalanceForm ref="formRef" @success="getList" />
<!-- 子表的列表 -->
<ContentWrap>
<el-tabs model-value="tenantPoints">
<el-tab-pane label="变动记录" name="tenantPoints">
<TenantPointsList :tenant-id="currentRow.id" />
</el-tab-pane>
</el-tabs>
</ContentWrap>
</template>
<script setup lang="ts">
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { TenantBalanceApi, TenantBalanceVO } from '@/api/system/tenantbalance'
import TenantBalanceForm from './TenantBalanceForm.vue'
import TenantPointsList from './components/TenantPointsList.vue'
import { checkPermi } from '@/utils/permission'
import { useUserStore } from '@/store/modules/user'
const userStore = ref(useUserStore())
/** 租户余额 列表 */
defineOptions({ name: 'TenantBalance' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中
const list = ref<TenantBalanceVO[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
balance: undefined,
version: undefined,
updatedAt: undefined,
updatedAt: []
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
const mount = ref({})
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
let data = {}
console.log(checkPermi(['system:tenant-balance:self-subordinate']))
if (userStore.value.getUser.id !== 1) {
getMount()
data = await TenantBalanceApi.getSubordinateaMountPage(queryParams)
} else {
data = await TenantBalanceApi.getTenantBalancePage(queryParams)
}
// if (checkPermi(['system:tenant-balance:self-subordinate'])) {
// } else {
// }
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 查询列表 */
const getMount = async () => {
const data = await TenantBalanceApi.getselfamount()
mount.value = data
}
/** 搜索按钮操作 */
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 TenantBalanceApi.deleteTenantBalance(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch { }
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
const data = await TenantBalanceApi.exportTenantBalance(queryParams)
download.excel(data, '租户余额.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 选中行操作 */
const currentRow = ref({}) // 选中行
const handleCurrentChange = (row) => {
currentRow.value = row
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>
<style scoped>
.center-align {
display: flex;
align-items: center;
width: 100%;
}
.balance-text {
margin-left: auto;
/* 把余额顶到最右边 */
font-weight: 500;
}
</style>

View File

@@ -0,0 +1,129 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" v-loading="formLoading">
<el-form-item label="本次变动点数,正加负减" prop="points">
<el-input v-model="formData.points" placeholder="请输入本次变动点数,正加负减" />
</el-form-item>
<el-form-item label="变动后余额快照(冗余)" prop="balance">
<el-input v-model="formData.balance" placeholder="请输入变动后余额快照(冗余)" />
</el-form-item>
<el-form-item label="变动类型,如 RECHARGE, CONSUME, TRANSFER_OUT, TRANSFER_IN" prop="type">
<el-select v-model="formData.type" placeholder="请选择变动类型,如 RECHARGE, CONSUME, TRANSFER_OUT, TRANSFER_IN">
<el-option label="请选择字典生成" value="" />
</el-select>
</el-form-item>
<el-form-item label="备注" prop="description">
<Editor v-model="formData.remark" height="150px" />
</el-form-item>
<el-form-item label="订单 Id/业务单号" prop="orderId">
<el-input v-model="formData.orderId" placeholder="请输入订单 Id/业务单号" />
</el-form-item>
<el-form-item label="业务流水号(转账、订单等唯一标识)" prop="bizNo">
<el-input v-model="formData.bizNo" placeholder="请输入业务流水号(转账、订单等唯一标识)" />
</el-form-item>
<el-form-item label="操作人 Id" prop="operatorId">
<el-input v-model="formData.operatorId" placeholder="请输入操作人 Id" />
</el-form-item>
<el-form-item label="目标租户 Id转账使用" prop="targetTenantId">
<el-input v-model="formData.targetTenantId" placeholder="请输入目标租户 Id转账使用" />
</el-form-item>
<el-form-item label="创建时间" prop="createdAt">
<el-date-picker v-model="formData.createdAt" type="date" value-format="x" placeholder="选择创建时间" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { TenantPointsApi, TenantPointsVO } from '@/api/system/tenantpoints'
/** 租户积分记录 表单 */
defineOptions({ name: 'TenantPointsForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref({
id: undefined,
points: undefined,
balance: undefined,
type: undefined,
remark: undefined,
orderId: undefined,
bizNo: undefined,
operatorId: undefined,
targetTenantId: undefined,
createdAt: undefined
})
const formRules = reactive({
points: [{ required: true, message: '本次变动点数,正加负减不能为空', trigger: 'blur' }],
createdAt: [{ required: true, message: '创建时间不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
/** 打开弹窗 */
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 {
formData.value = await TenantPointsApi.getTenantPoints(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
await formRef.value.validate()
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as TenantPointsVO
if (formType.value === 'create') {
await TenantPointsApi.createTenantPoints(data)
message.success(t('common.createSuccess'))
} else {
await TenantPointsApi.updateTenantPoints(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
points: undefined,
balance: undefined,
type: undefined,
description: undefined,
orderId: undefined,
bizNo: undefined,
operatorId: undefined,
targetTenantId: undefined,
createdAt: undefined
}
formRef.value?.resetFields()
}
</script>

View File

@@ -0,0 +1,198 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="120px">
<!-- <el-form-item label="金额" prop="points">
<el-input v-model="queryParams.points" placeholder="请输入金额(正加负减)" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="变动后余额" prop="balance">
<el-input v-model="queryParams.balance" placeholder="请输入变动后余额(冗余)" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item> -->
<el-form-item label="类型" prop="type">
<el-select v-model="queryParams.type" placeholder="请选择类型" clearable class="!w-240px">
<el-option v-for="(dict, index) in getIntDictOptions(DICT_TYPE.PAY_TYPE)" :key="index" :label="dict.label"
:value="dict.label" />
</el-select>
</el-form-item>
<el-form-item label="订单 Id" prop="orderId">
<el-input v-model="queryParams.orderId" placeholder="请输入订单 Id / 业务单号" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="业务流水号" prop="bizNo">
<el-input v-model="queryParams.bizNo" placeholder="请输入业务流水号(转账、订单等唯一标识)" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="操作人 Id" prop="operatorId">
<el-input v-model="queryParams.operatorId" placeholder="请输入操作人 Id" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="目标租户 Id" prop="targetTenantId">
<el-input v-model="queryParams.targetTenantId" placeholder="请输入目标租户 Id转账使用" clearable
@keyup.enter="handleQuery" class="!w-240px" />
</el-form-item>
<el-form-item label="创建时间" prop="createdAt">
<el-date-picker v-model="queryParams.createdAt" value-format="YYYY-MM-DD HH:mm:ss" type="daterange"
start-placeholder="开始日期" end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" class="!w-240px" />
</el-form-item>
<!-- <el-form-item label="描述/备注" prop="remark">
<el-input v-model="queryParams.remark" placeholder="请输入描述或备注" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item> -->
<el-form-item>
<el-button @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> 搜索
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" /> 重置
</el-button>
<!-- <el-button type="primary" plain @click="openForm('create')" v-hasPermi="['system:tenant-points:create']">
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button> -->
<el-button type="success" plain @click="handleExport" :loading="exportLoading"
v-hasPermi="['system:tenant-points:export']">
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<!-- 列头按你给的短名称展示prop 对应后端字段 -->
<el-table-column label="变动" align="center" prop="points" />
<el-table-column label="变动后余额" align="center" prop="balance" />
<el-table-column label="测试号余额" align="center" prop="testAccountNum" />
<el-table-column label="类型" align="center" prop="type" />
<el-table-column label="描述" align="center" prop="description" />
<el-table-column label="订单 Id/业务单号" align="center" prop="orderId" />
<el-table-column label="业务流水号" align="center" prop="bizNo" />
<el-table-column label="操作人 Id" align="center" prop="operatorId" />
<el-table-column label="目标租户Id" align="center" prop="targetTenantId" />
<el-table-column label="创建时间" align="center" prop="createdAt" :formatter="dateFormatter" width="180px" />
<el-table-column label="备注" align="center" prop="remark" />
<!-- <el-table-column label="操作" align="center" min-width="120px">
<template #default="scope">
<el-button link type="primary" @click="openForm('update', scope.row.id)"
v-hasPermi="['system:tenant-points:update']">
编辑
</el-button>
<el-button link type="danger" @click="handleDelete(scope.row.id)"
v-hasPermi="['system:tenant-points:delete']">
删除
</el-button>
</template>
</el-table-column> -->
</el-table>
<!-- 分页 -->
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="getList" />
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<TenantPointsForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { TenantPointsApi, TenantPointsVO } from '@/api/system/tenantpoints'
import TenantPointsForm from './TenantPointsForm.vue'
/** 租户积分记录 列表 */
defineOptions({ name: 'TenantPoints' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中
const list = ref<TenantPointsVO[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
points: undefined,
balance: undefined,
type: undefined,
remark: undefined, // 兼容后端的 remark 字段
description: undefined, // 如果后端有 description可用于搜索过滤
orderId: undefined,
bizNo: undefined,
operatorId: undefined,
targetTenantId: undefined,
createdAt: undefined,
createdAtArr: [] // 保留原来的数组字段名兼容用(如果你用范围)
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await TenantPointsApi.getTenantPointsPage(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 TenantPointsApi.deleteTenantPoints(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch { }
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
const data = await TenantPointsApi.exportTenantPoints(queryParams)
download.excel(data, '租户积分记录.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>