feat: 完善15- 角色默认信用与店铺实际额度管理
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m8s

This commit is contained in:
luo
2026-07-25 11:55:44 +08:00
parent 9289a6e940
commit ee508c3e1e
16 changed files with 457 additions and 100 deletions

View File

@@ -20,7 +20,7 @@ Non-Goals:
- 角色默认信用保存到 `PUT /api/admin/roles/{id}/default-credit`,请求体为 `credit_enabled``credit_limit`
- 店铺实际信用额度保存到 `PUT /api/admin/shops/{id}/credit-limit`,请求体为 `credit_enabled``credit_limit``version`
- 资金概况继续通过 `GET /api/admin/shops/fund-summary` 获取,前端展示 `balance``frozen_balance``credit_enabled``credit_limit``available_balance``is_in_debt``debt_amount``version`
- 资金概况继续通过 `GET /api/admin/shops/fund-summary` 获取,前端展示 `main_balance``main_frozen_balance``cash_available_balance``credit_enabled``credit_limit``available_balance``is_in_debt``debt_amount``version`
- 调整弹框展示修改前后金额预览,但最终可用金额、欠款金额和欠款状态仍以后端刷新后的资金概况为准。
## Risks / Trade-offs

View File

@@ -8,8 +8,8 @@ The agent fund overview page SHALL display shop actual credit and fund summary f
- **GIVEN** 用户进入代理商资金概况页
- **WHEN** `GET /api/admin/shops/fund-summary` 返回资金概况记录
- **THEN** 页面 MUST 展示现金余额 `balance`
- **AND** 页面 MUST 展示冻结金额 `frozen_balance`
- **THEN** 页面 MUST 展示账面余额 `main_balance`
- **AND** 页面 MUST 展示冻结金额 `main_frozen_balance`
- **AND** 页面 MUST 展示实际信用额度 `credit_limit`
- **AND** 页面 MUST 展示可用金额 `available_balance`
- **AND** 页面 MUST 展示欠款金额 `debt_amount`
@@ -74,8 +74,9 @@ The frontend SHALL read actual credit fields from `GET /api/admin/shops/fund-sum
#### Scenario: Read actual credit response fields
- **WHEN** 前端请求 `GET /api/admin/shops/fund-summary`
- **THEN** 前端 MUST read `balance`
- **AND** 前端 MUST read `frozen_balance`
- **THEN** 前端 MUST read `main_balance`
- **AND** 前端 MUST read `main_frozen_balance`
- **AND** 前端 MUST read `cash_available_balance`
- **AND** 前端 MUST read `credit_enabled`
- **AND** 前端 MUST read `credit_limit`
- **AND** 前端 MUST read `available_balance`

View File

@@ -37,6 +37,6 @@
- 角色默认信用保存只调用角色默认信用接口,店铺资金概况只读取后端返回的实际额度,未发现前端同步历史店铺额度的逻辑。
- 店铺创建参数未包含信用额度字段,实际额度初始化由后端按所选客户角色处理。
- 资金概况列表直接展示 `balance``frozen_balance``credit_limit``available_balance``debt_amount``version` 和后端欠款状态字段。
- 资金概况列表直接展示 `main_balance``main_frozen_balance``credit_limit``available_balance``debt_amount``version` 和后端欠款状态字段。
- 关闭信用时角色配置和店铺调额表单均将额度归零,并以 `credit_limit=0` 提交。
- 调额响应或异常命中版本冲突时,页面提示并重新加载资金概况。

View File

@@ -20,8 +20,8 @@
## 4. Verification
- [ ] 4.1 验证创建、编辑、清空业务员和无可选账号状态。
- [ ] 4.2 验证列表业务员展示和按业务员筛选正确。
- [ ] 4.3 验证详情对历史停用业务员仍能展示名称和手机号摘要。
- [x] 4.1 验证创建、编辑、清空业务员和无可选账号状态。
- [x] 4.2 验证列表业务员展示和按业务员筛选正确。
- [x] 4.3 验证详情对历史停用业务员仍能展示名称和手机号摘要。
- [x] 4.4 确认页面不引入分销、佣金或发展层级文案。
- [x] 4.5 运行相关前端校验,并执行 `openspec validate update-shop-salesperson-selection --strict`

View File

@@ -9,6 +9,7 @@ import type {
RoleQueryParams,
PlatformRoleFormData,
UpdateRoleDefaultCreditRequest,
UpdateRoleDefaultCreditResponse,
BaseResponse,
PaginationResponse
} from '@/types/api'
@@ -47,7 +48,7 @@ export class RoleService extends BaseService {
* @param id 角色ID
* @param data 角色数据
*/
static updateRole(id: number, data: PlatformRoleFormData): Promise<BaseResponse> {
static updateRole(id: number, data: Partial<PlatformRoleFormData>): Promise<BaseResponse> {
return this.update(`/api/admin/roles/${id}`, data)
}
@@ -60,8 +61,11 @@ export class RoleService extends BaseService {
static updateRoleDefaultCredit(
id: number,
data: UpdateRoleDefaultCreditRequest
): Promise<BaseResponse> {
return this.put<BaseResponse>(`/api/admin/roles/${id}/default-credit`, data)
): Promise<BaseResponse<UpdateRoleDefaultCreditResponse>> {
return this.put<BaseResponse<UpdateRoleDefaultCreditResponse>>(
`/api/admin/roles/${id}/default-credit`,
data
)
}
/**

View File

@@ -11,6 +11,7 @@ import type {
ShopRolesResponse,
AssignShopRolesRequest,
UpdateShopCreditLimitRequest,
UpdateShopCreditLimitResponse,
BaseResponse,
PaginationResponse,
ShopBusinessOwnerCandidate,
@@ -148,7 +149,10 @@ export class ShopService extends BaseService {
static updateShopCreditLimit(
shopId: number,
data: UpdateShopCreditLimitRequest
): Promise<BaseResponse> {
return this.put<BaseResponse>(`/api/admin/shops/${shopId}/credit-limit`, data)
): Promise<BaseResponse<UpdateShopCreditLimitResponse>> {
return this.put<BaseResponse<UpdateShopCreditLimitResponse>>(
`/api/admin/shops/${shopId}/credit-limit`,
data
)
}
}

View File

@@ -122,6 +122,18 @@ export const asyncRoutes: AppRouteRecord[] = [
roles: ['R_SUPER']
}
},
{
path: 'role/detail/:id',
name: 'RoleDetail',
component: RoutesAlias.RoleDetail,
meta: {
title: '角色详情',
permissions: ['role:detail'],
isHide: true,
keepAlive: false,
roles: ['R_SUPER']
}
},
{
path: 'permission',
name: 'PermissionManagement',
@@ -261,6 +273,17 @@ export const asyncRoutes: AppRouteRecord[] = [
title: 'menus.shopManagement.shopList',
keepAlive: true
}
},
// 店铺详情
{
path: 'detail/:id',
name: 'ShopDetail',
component: RoutesAlias.ShopDetail,
meta: {
title: '店铺详情',
isHide: true,
keepAlive: false
}
}
]
},

View File

@@ -18,6 +18,7 @@ export enum RoutesAlias {
// 系统管理
Role = '/system/role', // 角色
RoleDetail = '/system/role/detail', // 角色详情
Permission = '/system/permission', // 权限管理
CarrierManagement = '/system/carrier-management', // 运营商管理
UserCenter = '/system/user-center', // 用户中心
@@ -33,6 +34,7 @@ export enum RoutesAlias {
// 店铺管理
Shop = '/shop-management/list', // 店铺列表
ShopDetail = '/shop-management/detail', // 店铺详情
// 通用页面(店铺账号-企业账号)
EnterpriseCustomerAccounts = '/common/account-list', // 企业客户账号列表和店铺账号列表共用

View File

@@ -211,10 +211,8 @@ export interface ShopFundSummaryItem {
phone?: string // 主账号手机号
main_balance: number // 预充值余额(分)
main_frozen_balance: number // 预充值冻结余额(分)
balance: number // 现金余额(分)
frozen_balance: number // 现金冻结金额(分)
cash_available: number // 现金可用余额(分)
low_balance_warning: boolean // 现金可用余额不足100元预警
cash_available_balance: number // 现金可用余额(分)
low_balance_warning?: boolean // 现金可用余额不足100元预警
credit_enabled: boolean // 实际信用额度是否启用
credit_limit: number // 实际信用额度(分)
available_balance: number // 可用金额(分),以后端返回为准

View File

@@ -18,6 +18,7 @@ export enum RoleStatus {
// 角色实体(匹配后端 ModelRole
export interface PlatformRole {
id?: number
ID: number
CreatedAt?: string
UpdatedAt?: string
@@ -30,6 +31,19 @@ export interface PlatformRole {
status: RoleStatus // 状态
credit_enabled?: boolean // 新建代理默认信用是否启用(仅客户角色)
credit_limit?: number // 新建代理默认信用额度(分,仅客户角色)
default_credit_enabled?: boolean // 接口返回的新建代理默认信用状态
default_credit_limit?: number // 接口返回的新建代理默认信用额度(分)
default_credit_scope?: string // 接口返回的信用模板生效范围
created_at?: string // 接口返回的创建时间
updated_at?: string // 接口返回的更新时间
}
export interface UpdateRoleDefaultCreditResponse {
affects_existing_wallets: boolean
credit_enabled: boolean
credit_limit: number
role_id: number
scope: string
}
// 角色查询参数

View File

@@ -21,6 +21,7 @@ export interface ShopResponse {
contact_name: string // 联系人姓名
contact_phone: string // 联系人电话
status: number // 状态 (0:禁用, 1:启用)
status_name?: string // 状态名称
business_owner_account_id: number | null // 平台业务员账号ID
business_owner_username: string // 平台业务员账号名
business_owner_phone_summary: string // 平台业务员手机号摘要
@@ -49,6 +50,7 @@ export interface CreateShopParams {
init_username: string // 初始账号用户名(必填)
init_password: string // 初始账号密码(必填)
init_phone: string // 初始账号手机号(必填)
default_role_id: number // 初始账号默认客户角色(必填)
parent_id?: number | null // 上级店铺ID一级店铺可不填
province?: string // 省份
city?: string // 城市
@@ -121,3 +123,15 @@ export interface UpdateShopCreditLimitRequest {
credit_limit: number // 实际信用额度(分)
version: number // 资金概况版本,用于并发控制
}
// 更新店铺实际信用额度响应
export interface UpdateShopCreditLimitResponse {
available_balance: number // 总可用金额(分)
balance: number // 账面余额(分)
credit_enabled: boolean // 是否启用实际信用额度
credit_limit: number // 实际信用额度(分)
frozen_balance: number // 冻结金额(分)
shop_id: number // 店铺 ID
version: number // 更新后的钱包版本
wallet_id: number // 主钱包 ID
}

View File

@@ -618,8 +618,8 @@
return
}
if (value < 0) {
callback(new Error('实际信用额度不能小于0'))
if (value <= 0) {
callback(new Error('实际信用额度必须大于0'))
return
}
@@ -655,12 +655,13 @@
{ label: '店铺名称', prop: 'shop_name' },
{ label: '用户名', prop: 'username' },
{ label: '手机号', prop: 'phone' },
{ label: '现金余额', prop: 'balance' },
{ label: '冻结金额', prop: 'frozen_balance' },
{ label: '账面余额', prop: 'main_balance' },
{ label: '冻结金额', prop: 'main_frozen_balance' },
{ label: '现金可用', prop: 'cash_available_balance' },
{ label: '实际信用额度', prop: 'credit_limit' },
{ label: '可用金额', prop: 'available_balance' },
{ label: '可用金额', prop: 'available_balance' },
{ label: '欠款金额', prop: 'debt_amount' },
{ label: '版本', prop: 'version' },
{ label: '钱包版本', prop: 'version' },
{ label: '余额预警', prop: 'low_balance_warning' },
{ label: '总佣金', prop: 'total_commission' },
{ label: '可提现', prop: 'available_commission' },
@@ -748,22 +749,28 @@
width: 120
},
{
prop: 'balance',
label: '现金余额',
prop: 'main_balance',
label: '账面余额',
minWidth: 130,
formatter: (row: ShopFundSummaryItem) => {
return h(
'span',
{ style: 'color: var(--el-color-primary); font-weight: 500' },
formatMoney(row.balance)
formatMoney(row.main_balance)
)
}
},
{
prop: 'frozen_balance',
prop: 'main_frozen_balance',
label: '冻结金额',
minWidth: 120,
formatter: (row: ShopFundSummaryItem) => formatMoney(row.frozen_balance)
formatter: (row: ShopFundSummaryItem) => formatMoney(row.main_frozen_balance)
},
{
prop: 'cash_available_balance',
label: '现金可用',
minWidth: 120,
formatter: (row: ShopFundSummaryItem) => formatMoney(row.cash_available_balance)
},
{
prop: 'credit_limit',
@@ -783,7 +790,7 @@
},
{
prop: 'available_balance',
label: '可用金额',
label: '可用金额',
minWidth: 130,
formatter: (row: ShopFundSummaryItem) => {
return h(
@@ -806,7 +813,7 @@
},
{
prop: 'version',
label: '版本',
label: '钱包版本',
minWidth: 90,
formatter: (row: ShopFundSummaryItem) => row.version ?? '-'
},
@@ -957,11 +964,13 @@
})
}
if (hasAuth('shop:credit-limit:manage')) {
actions.push({
label: '调整额度',
handler: () => showCreditDialog(row),
type: 'primary'
})
}
return actions
}

View File

@@ -0,0 +1,108 @@
<template>
<div class="shop-detail-page">
<ElCard shadow="never">
<div class="detail-header">
<ElButton @click="handleBack">
<template #icon>
<ElIcon><ArrowLeft /></ElIcon>
</template>
返回
</ElButton>
<h2 class="detail-title">店铺详情</h2>
</div>
<div v-loading="loading">
<ElDescriptions v-if="shop" :column="2" border>
<ElDescriptionsItem label="店铺名称">{{ shop.shop_name || '-' }}</ElDescriptionsItem>
<ElDescriptionsItem label="店铺编号">{{ shop.shop_code || '-' }}</ElDescriptionsItem>
<ElDescriptionsItem label="店铺层级">{{ shop.level }}</ElDescriptionsItem>
<ElDescriptionsItem label="上级店铺">{{ shop.parent_shop_name || '-' }}</ElDescriptionsItem>
<ElDescriptionsItem label="联系人">{{ shop.contact_name || '-' }}</ElDescriptionsItem>
<ElDescriptionsItem label="联系电话">{{ shop.contact_phone || '-' }}</ElDescriptionsItem>
<ElDescriptionsItem label="平台业务员">
{{ formatBusinessOwner(shop) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="业务员通知状态">
{{ shop.business_owner_account_id ? (shop.business_owner_available ? '可用' : '已停用') : '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="省份">{{ shop.province || '-' }}</ElDescriptionsItem>
<ElDescriptionsItem label="城市">{{ shop.city || '-' }}</ElDescriptionsItem>
<ElDescriptionsItem label="区县">{{ shop.district || '-' }}</ElDescriptionsItem>
<ElDescriptionsItem label="详细地址">{{ shop.address || '-' }}</ElDescriptionsItem>
<ElDescriptionsItem label="状态">{{ shop.status_name || shop.status }}</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间">{{ shop.created_at || '-' }}</ElDescriptionsItem>
<ElDescriptionsItem label="更新时间">{{ shop.updated_at || '-' }}</ElDescriptionsItem>
</ElDescriptions>
</div>
</ElCard>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ArrowLeft } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { ShopService } from '@/api/modules'
import type { ShopResponse } from '@/types/api'
defineOptions({ name: 'ShopDetail' })
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const shop = ref<ShopResponse | null>(null)
const formatBusinessOwner = (value: ShopResponse) => {
if (!value.business_owner_username) return '-'
return value.business_owner_phone_summary
? `${value.business_owner_username} (${value.business_owner_phone_summary})`
: value.business_owner_username
}
const handleBack = () => router.back()
const fetchDetail = async () => {
const id = Number(route.params.id)
if (!id) {
ElMessage.error('缺少店铺 ID')
return
}
loading.value = true
try {
const response = await ShopService.getShopDetail(id)
if (response.code !== 0 || !response.data) {
ElMessage.error(response.msg || '获取店铺详情失败')
return
}
shop.value = response.data
} catch (error: any) {
ElMessage.error(error?.message || '获取店铺详情失败')
} finally {
loading.value = false
}
}
onMounted(fetchDetail)
</script>
<style scoped lang="scss">
.shop-detail-page {
padding: 20px;
}
.detail-header {
display: flex;
gap: 16px;
align-items: center;
padding-bottom: 16px;
}
.detail-title {
margin: 0;
font-size: 20px;
font-weight: 600;
color: var(--el-text-color-primary);
}
</style>

View File

@@ -7,6 +7,7 @@
:items="searchFormItems"
show-expand
@reset="handleReset"
label-width="100px"
@search="handleSearch"
></ArtSearchBar>
@@ -203,9 +204,9 @@
>
<ElOption
v-for="role in defaultRoleList"
:key="role.ID"
:key="role.id"
:label="role.role_name"
:value="role.ID"
:value="role.id"
>
<div
style="display: flex; align-items: center; justify-content: space-between"
@@ -238,16 +239,6 @@
</template>
</ElDialog>
<ElDialog v-model="detailDialogVisible" title="店铺详情" width="500px">
<ElDescriptions v-if="detailShop" :column="1" border>
<ElDescriptionsItem label="店铺名称">{{ detailShop.shop_name }}</ElDescriptionsItem>
<ElDescriptionsItem label="店铺编号">{{ detailShop.shop_code }}</ElDescriptionsItem>
<ElDescriptionsItem label="平台业务员">
{{ formatBusinessOwner(detailShop) }}
</ElDescriptionsItem>
</ElDescriptions>
</ElDialog>
<!-- 店铺默认角色管理对话框 -->
<ElDialog
v-model="defaultRolesDialogVisible"
@@ -339,6 +330,7 @@
import { formatDateTime } from '@/utils/business/format'
import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants'
import { regionData } from '@/utils/constants/regionData'
import { RoutesAlias } from '@/router/routesAlias'
defineOptions({ name: 'Shop' })
@@ -354,8 +346,6 @@
const defaultRoleList = ref<any[]>([])
const salespersonLoading = ref(false)
const salespersonOptions = ref<ShopBusinessOwnerCandidate[]>([])
const detailDialogVisible = ref(false)
const detailShop = ref<ShopResponse | null>(null)
// 级联选择相关
const parentShopCascadeOptions = ref<any[]>([])
@@ -445,19 +435,12 @@
: shop.business_owner_username
}
const showDetail = async (row: ShopResponse) => {
try {
const response = await ShopService.getShopDetail(row.id)
if (response.code !== 0 || !response.data) {
ElMessage.error(response.msg || '获取店铺详情失败')
return
const handleNameClick = (row: ShopResponse) => {
if (hasAuth('shop:detail')) {
void router.push(`${RoutesAlias.ShopDetail}/${row.id}`)
} else {
ElMessage.warning('您没有查看详情的权限')
}
detailShop.value = response.data
} catch (error: any) {
ElMessage.error(error?.message || '获取店铺详情失败')
return
}
detailDialogVisible.value = true
}
// 定义表单搜索初始值
@@ -689,7 +672,25 @@
prop: 'shop_name',
label: '店铺名称',
width: 160,
showOverflowTooltip: true
showOverflowTooltip: true,
formatter: (row: ShopResponse) => {
const hasPermission = hasAuth('shop:detail')
return h(
'span',
{
style: hasPermission
? 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;'
: '',
onClick: hasPermission
? (event: MouseEvent) => {
event.stopPropagation()
handleNameClick(row)
}
: undefined
},
row.shop_name
)
}
},
{
prop: 'shop_code',
@@ -768,12 +769,6 @@
const getActions = (row: ShopResponse) => {
const actions: any[] = []
actions.push({
label: '详情',
handler: () => showDetail(row),
type: 'primary'
})
if (hasAuth('shop:look_customer')) {
actions.push({
label: '账号列表',
@@ -862,7 +857,7 @@
try {
const params: any = {
page: 1,
pageSize: 20,
page_size: 20,
role_type: RoleType.CUSTOMER, // 仅客户角色
status: RoleStatus.ENABLED // 仅启用的角色
}
@@ -1129,7 +1124,7 @@
// 将 PlatformRole 转换为与 ShopRoleResponse 兼容的格式
availableRoles.value = (res.data.items || []).map((role) => ({
...role,
role_id: role.ID,
role_id: role.id ?? role.ID,
role_name: role.role_name,
role_desc: role.role_desc,
role_type: role.role_type,

View File

@@ -0,0 +1,111 @@
<template>
<div class="role-detail-page">
<ElCard shadow="never">
<div class="detail-header">
<ElButton @click="handleBack">
<template #icon>
<ElIcon><ArrowLeft /></ElIcon>
</template>
返回
</ElButton>
<h2 class="detail-title">角色详情</h2>
</div>
<div v-loading="loading">
<ElDescriptions v-if="role" :column="2" border>
<ElDescriptionsItem label="角色名称">{{ role.role_name || '-' }}</ElDescriptionsItem>
<ElDescriptionsItem label="角色类型">
{{ role.role_type === 1 ? '平台角色' : '客户角色' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="角色描述">{{ role.role_desc || '-' }}</ElDescriptionsItem>
<ElDescriptionsItem label="状态">
{{ role.status === 1 ? '启用' : '禁用' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="新建代理默认信用">
{{ role.default_credit_enabled ? '启用' : '未启用' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="默认信用额度">
{{ formatCredit(role.default_credit_limit) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="信用生效范围">
{{ role.default_credit_scope || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间">
{{ formatDateTime(role.created_at) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="更新时间">
{{ formatDateTime(role.updated_at) }}
</ElDescriptionsItem>
</ElDescriptions>
</div>
</ElCard>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ArrowLeft } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { RoleService } from '@/api/modules'
import type { PlatformRole } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
defineOptions({ name: 'RoleDetail' })
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const role = ref<PlatformRole | null>(null)
const formatCredit = (value?: number) => {
if (value === undefined || value === null) return '-'
return `${(value / 100).toFixed(2)}`
}
const handleBack = () => router.back()
const fetchDetail = async () => {
const id = Number(route.params.id)
if (!id) {
ElMessage.error('缺少角色 ID')
return
}
loading.value = true
try {
const response = await RoleService.getRole(id)
if (response.code !== 0 || !response.data) {
ElMessage.error(response.msg || '获取角色详情失败')
return
}
role.value = response.data
} catch (error: any) {
ElMessage.error(error?.message || '获取角色详情失败')
} finally {
loading.value = false
}
}
onMounted(fetchDetail)
</script>
<style scoped lang="scss">
.role-detail-page {
padding: 20px;
}
.detail-header {
display: flex;
gap: 16px;
align-items: center;
padding-bottom: 16px;
}
.detail-title {
margin: 0;
font-size: 20px;
font-weight: 600;
color: var(--el-text-color-primary);
}
</style>

View File

@@ -25,7 +25,7 @@
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="ID"
row-key="id"
:loading="loading"
:data="roleList"
:currentPage="pagination.page"
@@ -46,7 +46,7 @@
:title="dialogType === 'add' ? '新增角色' : '编辑角色'"
width="30%"
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
<ElForm ref="formRef" :model="form" :rules="rules" label-width="160px">
<ElFormItem label="角色名称" prop="role_name">
<ElInput v-model="form.role_name" placeholder="请输入角色名称" />
</ElFormItem>
@@ -91,7 +91,7 @@
<ElInputNumber
v-model="form.credit_limit_yuan"
:disabled="!form.credit_enabled"
:min="0"
:min="1"
:precision="2"
:step="100"
controls-position="right"
@@ -250,6 +250,8 @@
import { RoleType } from '@/types/api'
import type { PlatformRole, PermissionTreeNode } from '@/types/api'
import type { SearchFormItem } from '@/types'
import { useRouter } from 'vue-router'
import { RoutesAlias } from '@/router/routesAlias'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { fenToYuan, formatDateTime, yuanToFen } from '@/utils/business/format'
@@ -263,6 +265,7 @@
defineOptions({ name: 'Role' })
const { hasAuth } = useAuth()
const router = useRouter()
const canUpdateRoleStatus = hasAuth('role:update_status')
const dialogVisible = ref(false)
@@ -345,8 +348,10 @@
{ label: '角色名称', prop: 'role_name' },
{ label: '角色描述', prop: 'role_desc' },
{ label: '角色类型', prop: 'role_type' },
{ label: '新建代理默认信用', prop: 'default_credit_enabled' },
{ label: '默认信用额度', prop: 'default_credit_limit' },
...(canUpdateRoleStatus ? [{ label: '状态', prop: 'status' }] : []),
{ label: '创建时间', prop: 'CreatedAt' }
{ label: '创建时间', prop: 'created_at' }
]
const formRef = ref<FormInstance>()
@@ -357,7 +362,23 @@
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
],
role_desc: [{ max: 200, message: '描述不能超过200个字符', trigger: 'blur' }],
role_type: [{ required: true, message: '请选择角色类型', trigger: 'change' }]
role_type: [{ required: true, message: '请选择角色类型', trigger: 'change' }],
credit_limit_yuan: [
{
validator: (_rule, value, callback) => {
if (!isCustomerRoleForm.value || !form.credit_enabled) {
callback()
return
}
if (value === undefined || value === null || value <= 0) {
callback(new Error('默认信用额度必须大于0'))
return
}
callback()
},
trigger: 'blur'
}
]
})
const form = reactive<any>({
@@ -388,12 +409,40 @@
const roleList = ref<PlatformRole[]>([])
const getRoleId = (role: PlatformRole | any) => role.id ?? role.ID
const handleNameClick = (row: PlatformRole) => {
if (hasAuth('role:detail')) {
void router.push(`${RoutesAlias.RoleDetail}/${getRoleId(row)}`)
} else {
ElMessage.warning('您没有查看详情的权限')
}
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'role_name',
label: '角色名称',
width: 180
width: 180,
formatter: (row: PlatformRole) => {
const hasPermission = hasAuth('role:detail')
return h(
'span',
{
style: hasPermission
? 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;'
: '',
onClick: hasPermission
? (event: MouseEvent) => {
event.stopPropagation()
handleNameClick(row)
}
: undefined
},
row.role_name
)
}
},
{
prop: 'role_type',
@@ -405,6 +454,28 @@
)
}
},
{
prop: 'default_credit_enabled',
label: '新建代理默认信用',
width: 150,
formatter: (row: PlatformRole) => {
if (row.role_type !== RoleType.CUSTOMER) return '-'
return h(
ElTag,
{ type: row.default_credit_enabled ? 'success' : 'info' },
() => (row.default_credit_enabled ? '启用' : '未启用')
)
}
},
{
prop: 'default_credit_limit',
label: '默认信用额度',
width: 140,
formatter: (row: PlatformRole) => {
if (row.role_type !== RoleType.CUSTOMER || !row.default_credit_enabled) return '-'
return `${fenToYuan(row.default_credit_limit)}`
}
},
...(canUpdateRoleStatus
? [
{
@@ -431,10 +502,10 @@
label: '角色描述'
},
{
prop: 'CreatedAt',
prop: 'created_at',
label: '创建时间',
width: 180,
formatter: (row: any) => formatDateTime(row.CreatedAt)
formatter: (row: any) => formatDateTime(row.created_at)
},
{
prop: 'actions',
@@ -934,7 +1005,7 @@
// 显示分配权限对话框
const showPermissionDialog = async (row: PlatformRole) => {
currentRoleId.value = row.ID
currentRoleId.value = getRoleId(row)
currentRole.value = {
role_name: row.role_name,
role_type: row.role_type
@@ -1043,7 +1114,7 @@
if (type === 'edit' && row) {
let roleDetail = row
try {
const res = await RoleService.getRole(row.ID)
const res = await RoleService.getRole(getRoleId(row))
if (res.code === 0 && res.data) {
roleDetail = res.data
}
@@ -1051,13 +1122,17 @@
console.error('获取角色详情失败:', error)
}
form.id = roleDetail.ID
form.id = getRoleId(roleDetail)
form.role_name = roleDetail.role_name
form.role_desc = roleDetail.role_desc
form.role_type = roleDetail.role_type
form.status = roleDetail.status
form.credit_enabled = Boolean(roleDetail.credit_enabled)
form.credit_limit_yuan = fenToYuan(roleDetail.credit_limit)
form.credit_enabled = Boolean(
roleDetail.default_credit_enabled ?? roleDetail.credit_enabled
)
form.credit_limit_yuan = fenToYuan(
roleDetail.default_credit_limit ?? roleDetail.credit_limit
)
} else {
form.id = 0
form.role_name = ''
@@ -1080,7 +1155,7 @@
})
.then(async () => {
try {
await RoleService.deleteRole(row.ID)
await RoleService.deleteRole(getRoleId(row))
ElMessage.success('删除成功')
await getTableData()
} catch (error) {
@@ -1118,7 +1193,6 @@
const data = {
role_name: form.role_name,
role_desc: form.role_desc,
role_type: form.role_type,
status: form.status
}
await RoleService.updateRole(form.id, data)
@@ -1146,7 +1220,7 @@
// 先更新UI
row.status = newStatus
try {
await RoleService.updateRoleStatus(row.ID, newStatus as 0 | 1)
await RoleService.updateRoleStatus(getRoleId(row), newStatus as 0 | 1)
ElMessage.success('状态切换成功')
} catch (error) {
// 切换失败,恢复原状态