修改bug
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m54s

This commit is contained in:
sexygoat
2026-04-09 13:48:11 +08:00
parent cc9d2a85a2
commit cb3ba06106
26 changed files with 987 additions and 1984 deletions

View File

@@ -3,6 +3,7 @@
*/
import { BaseService } from '../BaseService'
import request from '@/utils/http'
import type {
PlatformRole,
RoleQueryParams,
@@ -93,12 +94,16 @@ export class RoleService extends BaseService {
}
/**
* 移除角色的单个权限
* DELETE /api/admin/roles/{role_id}/permissions/{perm_id}
* 移除角色的权限(支持批量)
* DELETE /api/admin/roles/{role_id}/permissions
* @param roleId 角色ID
* @param permId 权限ID
* @param permIds 权限ID列表
*/
static removePermission(roleId: number, permId: number): Promise<BaseResponse> {
return this.delete<BaseResponse>(`/api/admin/roles/${roleId}/permissions/${permId}`)
static removePermissions(roleId: number, permIds: number[]): Promise<BaseResponse> {
// 直接使用 request.del传递 data 到 Body
return request.del<BaseResponse>({
url: `/api/admin/roles/${roleId}/permissions`,
data: { perm_ids: permIds }
})
}
}

View File

@@ -24,6 +24,34 @@ export class ShopService extends BaseService {
return this.getPage<ShopResponse>('/api/admin/shops', params)
}
/**
* 店铺级联查询(树结构)
* GET /api/admin/shops/cascade
* @param params 查询参数
*/
static getShopsCascade(params?: {
shop_name?: string
parent_id?: number
}): Promise<
BaseResponse<
Array<{
id: number
shop_name: string
has_children: boolean
}>
>
> {
return this.get<
BaseResponse<
Array<{
id: number
shop_name: string
has_children: boolean
}>
>
>('/api/admin/shops/cascade', params)
}
/**
* 创建店铺
* POST /api/admin/shops

View File

@@ -79,26 +79,26 @@
<!-- </div>-->
<!--</div>-->
<!-- 语言 -->
<div class="btn-box" v-if="showLanguage">
<el-dropdown @command="changeLanguage" popper-class="langDropDownStyle">
<div class="btn language-btn">
<i class="iconfont-sys">&#xe611;</i>
</div>
<template #dropdown>
<el-dropdown-menu>
<div v-for="item in languageOptions" :key="item.value" class="lang-btn-item">
<el-dropdown-item
:command="item.value"
:class="{ 'is-selected': locale === item.value }"
>
<span class="menu-txt">{{ item.label }}</span>
<i v-if="locale === item.value" class="iconfont-sys">&#xe621;</i>
</el-dropdown-item>
</div>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<!--<div class="btn-box" v-if="showLanguage">-->
<!-- <el-dropdown @command="changeLanguage" popper-class="langDropDownStyle">-->
<!-- <div class="btn language-btn">-->
<!-- <i class="iconfont-sys">&#xe611;</i>-->
<!-- </div>-->
<!-- <template #dropdown>-->
<!-- <el-dropdown-menu>-->
<!-- <div v-for="item in languageOptions" :key="item.value" class="lang-btn-item">-->
<!-- <el-dropdown-item-->
<!-- :command="item.value"-->
<!-- :class="{ 'is-selected': locale === item.value }"-->
<!-- >-->
<!-- <span class="menu-txt">{{ item.label }}</span>-->
<!-- <i v-if="locale === item.value" class="iconfont-sys">&#xe621;</i>-->
<!-- </el-dropdown-item>-->
<!-- </div>-->
<!-- </el-dropdown-menu>-->
<!-- </template>-->
<!-- </el-dropdown>-->
<!--</div>-->
<!-- 设置 -->
<div class="btn-box" @click="openSetting">
<el-popover :visible="showSettingGuide" placement="bottom-start" :width="190" :offset="0">
@@ -162,7 +162,7 @@
<i class="menu-icon iconfont-sys">&#xe817;</i>
<span class="menu-txt">{{ $t('topBar.user.lockScreen') }}</span>
</li>
<div class="line"></div>
<div class="line" />
<div class="logout-btn" @click="loginOut">
{{ $t('topBar.user.logout') }}
</div>
@@ -180,7 +180,7 @@
</template>
<script setup lang="ts">
import { LanguageEnum, MenuTypeEnum, MenuWidth } from '@/enums/appEnum'
import { MenuTypeEnum, MenuWidth } from '@/enums/appEnum'
import { useSettingStore } from '@/store/modules/setting'
import { useUserStore } from '@/store/modules/user'
import { useFullscreen } from '@vueuse/core'
@@ -190,7 +190,6 @@
import { mittBus } from '@/utils/sys'
import { useMenuStore } from '@/store/modules/menu'
import AppConfig from '@/config'
import { languageOptions } from '@/locales'
const isWindows = navigator.userAgent.includes('Windows')
const { locale } = useI18n()
@@ -201,7 +200,6 @@
const {
showMenuButton,
showRefreshButton,
showLanguage,
menuOpen,
showCrumbs,
systemThemeColor,
@@ -225,7 +223,6 @@
const isTopLeftMenu = computed(() => menuType.value === MenuTypeEnum.TOP_LEFT)
import { useCommon } from '@/composables/useCommon'
import { WEB_LINKS } from '@/utils/constants'
import { themeAnimation } from '@/utils/theme/animation'
const { t } = useI18n()
@@ -322,13 +319,6 @@
locale.value = language.value
}
const changeLanguage = (lang: LanguageEnum) => {
if (locale.value === lang) return
locale.value = lang
userStore.setLanguage(lang)
reload(50)
}
const openSetting = () => {
mittBus.emit('openSetting')
@@ -358,10 +348,6 @@
}
}
const visibleNotice = () => {
showNotice.value = !showNotice.value
}
const openChat = () => {
mittBus.emit('openChat')
}

View File

@@ -404,16 +404,9 @@
"accountManagement": {
"title": "Account Management",
"account": "Account List",
"platformAccount": "Platform Account",
"shopAccount": "Shop Account",
"customer": "Customer Management",
"customerRole": "Customer Role",
"agent": "Agent Management",
"customerAccount": "Customer Account",
"enterpriseCustomer": "Enterprise Customer",
"enterpriseCustomerAccounts": "Enterprise Customer Accounts",
"enterpriseCards": "Enterprise Card Management",
"customerCommission": "Customer Commission"
"enterpriseCards": "Enterprise Card Management"
},
"orderManagement": {
"title": "Order Management",
@@ -503,463 +496,5 @@
"zebra": "Zebra",
"border": "Border",
"headerBackground": "Header BG"
},
"platformAccount": {
"title": "Platform Account Management",
"searchForm": {
"accountName": "Account Name",
"accountNamePlaceholder": "Please enter account name",
"username": "Username",
"usernamePlaceholder": "Please enter username",
"status": "Status",
"statusPlaceholder": "Please select status"
},
"table": {
"id": "Account ID",
"accountName": "Account Name",
"username": "Username",
"phone": "Phone",
"email": "Email",
"roles": "Roles",
"status": "Status",
"createTime": "Create Time",
"lastLoginTime": "Last Login",
"actions": "Actions"
},
"status": {
"enabled": "Enabled",
"disabled": "Disabled"
},
"buttons": {
"add": "Add Account",
"edit": "Edit",
"delete": "Delete",
"view": "View Details",
"changePassword": "Change Password",
"assignRoles": "Assign Roles"
},
"dialog": {
"add": "Add Platform Account",
"edit": "Edit Platform Account",
"detail": "Account Details",
"changePassword": "Change Password",
"assignRoles": "Assign Roles"
},
"form": {
"accountName": "Account Name",
"accountNamePlaceholder": "Please enter account name",
"username": "Username",
"usernamePlaceholder": "Please enter username (login name)",
"password": "Password",
"passwordPlaceholder": "Please enter password",
"confirmPassword": "Confirm Password",
"confirmPasswordPlaceholder": "Please enter password again",
"phone": "Phone",
"phonePlaceholder": "Please enter phone number",
"email": "Email",
"emailPlaceholder": "Please enter email",
"status": "Status",
"remark": "Remark",
"remarkPlaceholder": "Please enter remark",
"newPassword": "New Password",
"newPasswordPlaceholder": "Please enter new password",
"confirmNewPassword": "Confirm New Password",
"confirmNewPasswordPlaceholder": "Please enter new password again",
"currentRoles": "Current Roles",
"availableRoles": "Available Roles",
"basicInfo": "Basic Information",
"roleInfo": "Role Information",
"statusInfo": "Status Information",
"remarkInfo": "Remark Information",
"createTime": "Create Time",
"updateTime": "Update Time",
"lastLoginTime": "Last Login Time"
},
"validation": {
"accountNameRequired": "Please enter account name",
"usernameRequired": "Please enter username",
"usernameExists": "Username already exists",
"passwordRequired": "Please enter password",
"passwordStrength": "Password must be at least 8 characters with letters and numbers",
"confirmPasswordRequired": "Please enter password again",
"passwordNotMatch": "Passwords do not match",
"phoneRequired": "Please enter phone number",
"phonePattern": "Please enter valid phone number",
"emailPattern": "Please enter valid email",
"newPasswordRequired": "Please enter new password",
"atLeastOneRole": "Account must have at least one role"
},
"messages": {
"createSuccess": "Account created successfully",
"updateSuccess": "Account updated successfully",
"deleteSuccess": "Account deleted successfully",
"passwordChangeSuccess": "Password changed successfully",
"roleAssignSuccess": "Roles assigned successfully",
"statusChangeSuccess": "Status changed successfully",
"deleteConfirm": "Are you sure to delete account {name}? This action cannot be undone",
"cannotDeleteSelf": "Cannot delete current login account",
"cannotDisableSelf": "Cannot disable current login account",
"noData": "No account data, click add button to create account",
"passwordHint": "Please use change password function to modify password",
"disabledLoginHint": "Account has been disabled, please contact administrator"
}
},
"commission": {
"menu": {
"management": "Commission Management",
"withdrawal": "Withdrawal Approval",
"withdrawalSettings": "Withdrawal Settings",
"myCommission": "My Commission",
"agentCommission": "Agent Commission Management"
},
"table": {
"withdrawalNo": "Withdrawal No.",
"applicant": "Applicant",
"applicantAccount": "Applicant Account",
"withdrawalAmount": "Withdrawal Amount",
"fee": "Fee",
"actualAmount": "Actual Amount",
"withdrawalMethod": "Withdrawal Method",
"accountInfo": "Account Info",
"accountName": "Account Name",
"accountNumber": "Account Number",
"bankName": "Bank Name",
"status": "Status",
"rejectReason": "Reject Reason",
"applyTime": "Apply Time",
"approveTime": "Approve Time",
"transferTime": "Transfer Time",
"actions": "Actions",
"commissionNo": "Commission No.",
"commissionType": "Commission Type",
"commissionAmount": "Commission Amount",
"orderNo": "Order No.",
"sourceUser": "Source User",
"beneficiary": "Beneficiary",
"settleTime": "Settle Time",
"withdrawTime": "Withdraw Time",
"remark": "Remark"
},
"status": {
"pending": "Pending",
"approved": "Approved",
"rejected": "Rejected",
"completed": "Completed",
"frozen": "Frozen",
"unfreezing": "Unfreezing",
"settled": "Settled",
"invalid": "Invalid"
},
"type": {
"oneTime": "One-time Commission",
"longTerm": "Long-term Commission"
},
"method": {
"alipay": "Alipay",
"wechat": "WeChat",
"bank": "Bank Card"
},
"form": {
"withdrawalAmount": "Withdrawal Amount",
"withdrawalAmountPlaceholder": "Please enter withdrawal amount",
"withdrawalMethod": "Withdrawal Method",
"withdrawalMethodPlaceholder": "Please select withdrawal method",
"accountName": "Account Name",
"accountNamePlaceholder": "Please enter account name",
"accountNumber": "Account Number",
"accountNumberPlaceholder": "Please enter account number",
"bankName": "Bank Name",
"bankNamePlaceholder": "Please enter bank name",
"alipayAccount": "Alipay Account",
"alipayAccountPlaceholder": "Please enter Alipay account",
"wechatAccount": "WeChat Account",
"wechatAccountPlaceholder": "Please enter WeChat account",
"rejectReason": "Reject Reason",
"rejectReasonPlaceholder": "Please enter reject reason",
"remark": "Remark",
"remarkPlaceholder": "Please enter remark",
"minWithdrawal": "Min Withdrawal",
"minWithdrawalPlaceholder": "Please enter min withdrawal amount",
"maxWithdrawal": "Max Withdrawal",
"maxWithdrawalPlaceholder": "Please enter max withdrawal amount",
"feeRate": "Fee Rate",
"feeRatePlaceholder": "Please enter fee rate (%)",
"availableBalance": "Available Balance",
"totalCommission": "Total Commission",
"frozenAmount": "Frozen Amount",
"withdrawnAmount": "Withdrawn Amount"
},
"buttons": {
"approve": "Approve",
"reject": "Reject",
"applyWithdrawal": "Apply Withdrawal",
"viewDetail": "View Details",
"export": "Export",
"batchApprove": "Batch Approve",
"batchReject": "Batch Reject"
},
"messages": {
"approveSuccess": "Approved successfully",
"rejectSuccess": "Rejected successfully",
"withdrawalSuccess": "Withdrawal application submitted successfully",
"updateSuccess": "Updated successfully",
"deleteSuccess": "Deleted successfully",
"approveConfirm": "Are you sure to approve this withdrawal?",
"rejectConfirm": "Are you sure to reject this withdrawal?",
"batchApproveConfirm": "Are you sure to approve {count} withdrawals?",
"batchRejectConfirm": "Are you sure to reject {count} withdrawals?",
"insufficientBalance": "Insufficient balance",
"belowMinAmount": "Amount below minimum withdrawal",
"exceedMaxAmount": "Amount exceeds maximum withdrawal",
"noData": "No data"
},
"validation": {
"withdrawalAmountRequired": "Please enter withdrawal amount",
"withdrawalAmountInvalid": "Withdrawal amount must be greater than 0",
"withdrawalMethodRequired": "Please select withdrawal method",
"accountNameRequired": "Please enter account name",
"accountNumberRequired": "Please enter account number",
"bankNameRequired": "Please enter bank name",
"rejectReasonRequired": "Please enter reject reason",
"remarkRequired": "Please enter remark",
"minWithdrawalRequired": "Please enter min withdrawal amount",
"maxWithdrawalRequired": "Please enter max withdrawal amount",
"feeRateRequired": "Please enter fee rate",
"feeRateInvalid": "Fee rate must be between 0-100"
},
"searchForm": {
"withdrawalNo": "Withdrawal No.",
"withdrawalNoPlaceholder": "Please enter withdrawal no.",
"applicant": "Applicant",
"applicantPlaceholder": "Please enter applicant",
"shopName": "Shop Name",
"shopNamePlaceholder": "Please enter shop name",
"status": "Status",
"statusPlaceholder": "Please select status",
"withdrawalMethod": "Withdrawal Method",
"withdrawalMethodPlaceholder": "Please select withdrawal method",
"dateRange": "Date Range",
"dateRangePlaceholder": ["Start Date", "End Date"],
"commissionType": "Commission Type",
"commissionTypePlaceholder": "Please select commission type"
},
"dialog": {
"approve": "Approve Withdrawal",
"reject": "Reject Withdrawal",
"detail": "Withdrawal Details",
"applyWithdrawal": "Apply Withdrawal",
"withdrawalSettings": "Withdrawal Settings"
},
"summary": {
"title": "Commission Summary",
"totalCommission": "Total Commission",
"availableBalance": "Available Balance",
"frozenAmount": "Frozen Amount",
"withdrawnAmount": "Withdrawn Amount",
"pendingAmount": "Pending Amount",
"todayCommission": "Today's Commission"
}
},
"seriesBinding": {
"buttons": {
"batchSetSeries": "Batch Set Series",
"clearBinding": "Clear Binding"
},
"dialog": {
"title": "Batch Set Series Binding",
"titleCard": "Batch Set Card Series Binding",
"titleDevice": "Batch Set Device Series Binding"
},
"form": {
"seriesAllocation": "Series Allocation",
"seriesAllocationPlaceholder": "Please select series allocation",
"clearBindingOption": "Clear Association",
"selectedCount": "{count} items selected"
},
"messages": {
"noSelection": "Please select items to set",
"setSuccess": "Series binding set successfully",
"setFailed": "Failed to set series binding",
"partialSuccess": "Partial success: {success} succeeded, {fail} failed",
"confirmSet": "Are you sure to set series binding for {count} selected items?",
"confirmClear": "Are you sure to clear series binding for {count} selected items?"
},
"result": {
"title": "Set Result",
"successCount": "Success Count",
"failCount": "Fail Count",
"failedItems": "Failed Items",
"iccid": "ICCID",
"deviceId": "Device ID",
"deviceNo": "Device No.",
"reason": "Reason"
}
},
"orderManagement": {
"title": "Order Management",
"orderList": "Order List",
"orderDetail": "Order Details",
"orderItems": "Order Items",
"createOrder": "Create Order",
"searchForm": {
"orderNo": "Order No.",
"orderNoPlaceholder": "Please enter order number",
"paymentStatus": "Payment Status",
"paymentStatusPlaceholder": "Please select payment status",
"orderType": "Order Type",
"orderTypePlaceholder": "Please select order type",
"dateRange": "Created Time",
"startDate": "Start Time",
"endDate": "End Time"
},
"table": {
"id": "Order ID",
"orderNo": "Order No.",
"orderType": "Order Type",
"buyerId": "Buyer ID",
"buyerType": "Buyer Type",
"paymentStatus": "Payment Status",
"paymentMethod": "Payment Method",
"totalAmount": "Total Amount",
"paidAt": "Paid At",
"commissionStatus": "Commission Status",
"createdAt": "Created At",
"updatedAt": "Updated At",
"operation": "Actions"
},
"orderType": {
"singleCard": "Single Card Purchase",
"device": "Device Purchase"
},
"buyerType": {
"personal": "Personal Customer",
"agent": "Agent"
},
"paymentStatus": {
"pending": "Pending",
"paid": "Paid",
"cancelled": "Cancelled",
"refunded": "Refunded"
},
"paymentMethod": {
"wallet": "Wallet Payment",
"wechat": "WeChat Payment",
"alipay": "Alipay Payment",
"offline": "Offline Payment"
},
"commissionStatus": {
"notApplicable": "Not Applicable",
"pending": "Pending",
"settled": "Settled"
},
"actions": {
"viewDetail": "View Details",
"cancel": "Cancel",
"confirm": "Confirm",
"submit": "Submit",
"close": "Close"
},
"createForm": {
"packageIds": "Select Packages",
"packageIdsPlaceholder": "Please select packages",
"iotCardId": "IoT Card ID",
"iotCardIdPlaceholder": "Please enter IoT card ID",
"deviceId": "Device ID",
"deviceIdPlaceholder": "Please enter device ID"
},
"items": {
"packageName": "Package Name",
"quantity": "Quantity",
"unitPrice": "Unit Price",
"amount": "Subtotal"
},
"messages": {
"createSuccess": "Order created successfully",
"createFailed": "Failed to create order",
"cancelSuccess": "Order cancelled successfully",
"cancelFailed": "Failed to cancel order",
"cancelConfirm": "Cancel Order Confirmation",
"cancelConfirmText": "Are you sure to cancel this order? This action cannot be undone",
"cannotCancelPaid": "Cannot cancel a paid order",
"cannotCancelCancelled": "Order is already cancelled",
"cannotCancelRefunded": "Cannot cancel a refunded order",
"loadFailed": "Failed to load order data",
"noData": "No order data available"
},
"validation": {
"orderTypeRequired": "Please select order type",
"packageIdsRequired": "Please select at least one package",
"iotCardRequired": "Please select IoT card",
"deviceRequired": "Please select device",
"packagesMaxLimit": "Can select up to 10 packages"
}
},
"enterpriseDevices": {
"title": "Enterprise Device List",
"searchForm": {
"enterpriseId": "Enterprise ID",
"enterpriseIdPlaceholder": "Please enter enterprise ID",
"deviceNo": "Device No.",
"deviceNoPlaceholder": "Please enter device number"
},
"table": {
"deviceId": "Device ID",
"deviceNo": "Device No.",
"deviceName": "Device Name",
"deviceModel": "Device Model",
"cardCount": "Card Count",
"authorizedAt": "Authorized At",
"operation": "Actions"
},
"buttons": {
"allocateDevices": "Allocate Devices",
"recallDevices": "Recall Authorization",
"refresh": "Refresh"
},
"dialog": {
"allocateTitle": "Allocate Devices to Enterprise",
"recallTitle": "Recall Device Authorization",
"resultTitle": "Operation Result"
},
"form": {
"deviceNos": "Device Numbers",
"deviceNosPlaceholder": "Enter device numbers, separated by newlines or commas",
"deviceNosHint": "One device number per line or comma-separated, maximum 100 devices",
"remark": "Remark",
"remarkPlaceholder": "Enter allocation remark (optional)",
"selectedDevices": "Selected Devices",
"selectedCount": "{count} devices selected"
},
"result": {
"successCount": "Success Count",
"failCount": "Fail Count",
"authorizedDevices": "Authorized Devices",
"failedItems": "Failed Items",
"deviceNo": "Device No.",
"deviceId": "Device ID",
"cardCount": "Card Count",
"reason": "Failure Reason"
},
"messages": {
"allocateSuccess": "Device allocation successful",
"allocateFailed": "Device allocation failed",
"allocatePartialSuccess": "Partial success: {success} succeeded, {fail} failed",
"recallSuccess": "Authorization recall successful",
"recallFailed": "Authorization recall failed",
"recallPartialSuccess": "Partial recall success: {success} succeeded, {fail} failed",
"recallConfirm": "Recall Authorization Confirmation",
"recallConfirmText": "Are you sure to recall authorization for {count} selected devices?",
"noSelection": "Please select devices to recall first",
"deviceNosRequired": "Please enter device numbers",
"deviceNosEmpty": "Device number list cannot be empty",
"deviceNosMaxLimit": "Cannot exceed 100 device numbers",
"invalidDeviceNos": "Invalid device number format exists",
"loadFailed": "Failed to load device list",
"noData": "No device data available"
},
"validation": {
"deviceNosRequired": "Please enter device number list",
"deviceNosMaxLength": "Cannot exceed 100 device numbers"
}
}
}

View File

@@ -103,13 +103,13 @@
"subTitle": "美观实用的界面,经过视觉优化,确保卓越的用户体验"
},
"title": "欢迎回来",
"subTitle": "输入您的号和密码登录",
"subTitle": "输入您的手机号和密码登录",
"roles": {
"super": "超级管理员",
"admin": "管理员",
"user": "普通用户"
},
"placeholder": ["请输入号", "请输入密码", "请拖动滑块完成验证"],
"placeholder": ["请输入手机号", "请输入密码", "请拖动滑块完成验证"],
"sliderText": "按住滑块拖动",
"sliderSuccessText": "验证成功",
"rememberPwd": "记住密码",
@@ -122,7 +122,7 @@
"message": "欢迎回来"
},
"validation": {
"usernameLength": "用户名长度为 3-20 个字符",
"usernameLength": "用户名长度为 2-20 个字符",
"usernamePattern": "用户名只能包含字母、数字和下划线",
"passwordLength": "密码长度为 6-20 个字符",
"strongPasswordLength": "密码长度为 8-20 个字符",
@@ -354,16 +354,9 @@
"accountManagement": {
"title": "账号管理",
"account": "账号列表",
"platformAccount": "平台账号",
"shopAccount": "代理账号",
"customer": "客户管理",
"customerRole": "客户角色",
"agent": "代理商管理",
"customerAccount": "客户账号",
"enterpriseCustomer": "企业客户",
"enterpriseCustomerAccounts": "关联账号列表",
"enterpriseCards": "企业卡管理",
"customerCommission": "客户账号佣金"
"enterpriseCards": "企业卡管理"
},
"shopManagement": {
"title": "店铺管理",
@@ -371,23 +364,15 @@
},
"assetManagement": {
"title": "资产管理",
"cardSearch": "IoT卡查询",
"deviceSearch": "设备查询",
"assetInformation": "资产信息",
"singleCard": "单卡信息",
"standaloneCardList": "IoT卡管理",
"iotCardDetail": "IoT卡详情",
"iotCardTask": "IoT卡任务",
"deviceTask": "设备任务",
"taskDetail": "任务详情",
"devices": "设备管理",
"deviceDetail": "设备详情",
"assetAssign": "分配记录",
"assetAssignDetail": "资产分配详情",
"allocationRecordDetail": "分配记录详情",
"cardReplacementRequest": "换卡申请",
"authorizationRecords": "授权记录",
"authorizationDetail": "授权记录详情",
"authorizationRecordDetail": "授权记录详情",
"enterpriseDevices": "企业设备列表",
"recordsManagement": "记录管理",
@@ -438,463 +423,5 @@
"zebra": "斑马纹",
"border": "边框",
"headerBackground": "表头背景"
},
"platformAccount": {
"title": "平台账号管理",
"searchForm": {
"accountName": "账号名称",
"accountNamePlaceholder": "请输入账号名称",
"username": "用户名",
"usernamePlaceholder": "请输入用户名",
"status": "状态",
"statusPlaceholder": "请选择状态"
},
"table": {
"id": "账号ID",
"accountName": "账号名称",
"username": "用户名",
"phone": "手机号",
"email": "邮箱",
"roles": "角色",
"status": "状态",
"createTime": "创建时间",
"lastLoginTime": "最后登录时间",
"actions": "操作"
},
"status": {
"enabled": "启用",
"disabled": "禁用"
},
"buttons": {
"add": "新增账号",
"edit": "编辑",
"delete": "删除",
"view": "查看详情",
"changePassword": "修改密码",
"assignRoles": "分配角色"
},
"dialog": {
"add": "新增平台账号",
"edit": "编辑平台账号",
"detail": "账号详情",
"changePassword": "修改密码",
"assignRoles": "分配角色"
},
"form": {
"accountName": "账号名称",
"accountNamePlaceholder": "请输入账号名称",
"username": "用户名",
"usernamePlaceholder": "请输入用户名(登录名)",
"password": "密码",
"passwordPlaceholder": "请输入密码",
"confirmPassword": "确认密码",
"confirmPasswordPlaceholder": "请再次输入密码",
"phone": "手机号",
"phonePlaceholder": "请输入手机号",
"email": "邮箱",
"emailPlaceholder": "请输入邮箱",
"status": "状态",
"remark": "备注",
"remarkPlaceholder": "请输入备注信息",
"newPassword": "新密码",
"newPasswordPlaceholder": "请输入新密码",
"confirmNewPassword": "确认新密码",
"confirmNewPasswordPlaceholder": "请再次输入新密码",
"currentRoles": "当前角色",
"availableRoles": "可分配角色",
"basicInfo": "基本信息",
"roleInfo": "角色信息",
"statusInfo": "状态信息",
"remarkInfo": "备注信息",
"createTime": "创建时间",
"updateTime": "更新时间",
"lastLoginTime": "最后登录时间"
},
"validation": {
"accountNameRequired": "请输入账号名称",
"usernameRequired": "请输入用户名",
"usernameExists": "用户名已存在",
"passwordRequired": "请输入密码",
"passwordStrength": "密码强度不足至少8位且包含字母、数字",
"confirmPasswordRequired": "请再次输入密码",
"passwordNotMatch": "两次输入的密码不一致",
"phoneRequired": "请输入手机号",
"phonePattern": "请输入正确的手机号格式",
"emailPattern": "请输入正确的邮箱格式",
"newPasswordRequired": "请输入新密码",
"atLeastOneRole": "账号至少需要保留一个角色"
},
"messages": {
"createSuccess": "账号创建成功",
"updateSuccess": "账号更新成功",
"deleteSuccess": "账号删除成功",
"passwordChangeSuccess": "密码修改成功",
"roleAssignSuccess": "角色分配成功",
"statusChangeSuccess": "状态切换成功",
"deleteConfirm": "确定要删除账号 {name} 吗?此操作不可撤销",
"cannotDeleteSelf": "不能删除当前登录账号",
"cannotDisableSelf": "不能禁用当前登录账号",
"noData": "暂无账号数据,点击新增按钮创建账号",
"passwordHint": "如需修改密码请使用修改密码功能",
"disabledLoginHint": "账号已被禁用,请联系管理员"
}
},
"commission": {
"menu": {
"management": "佣金管理",
"withdrawal": "提现审批",
"withdrawalSettings": "提现配置",
"myCommission": "我的佣金",
"agentCommission": "代理商佣金"
},
"table": {
"withdrawalNo": "提现单号",
"applicant": "申请人",
"applicantAccount": "申请账号",
"withdrawalAmount": "提现金额",
"fee": "手续费",
"actualAmount": "实际到账",
"withdrawalMethod": "提现方式",
"accountInfo": "收款账户",
"accountName": "账户名称",
"accountNumber": "账号",
"bankName": "银行名称",
"status": "状态",
"rejectReason": "拒绝原因",
"applyTime": "申请时间",
"approveTime": "审批时间",
"transferTime": "到账时间",
"actions": "操作",
"commissionNo": "佣金单号",
"commissionType": "佣金类型",
"commissionAmount": "佣金金额",
"orderNo": "关联订单号",
"sourceUser": "来源用户",
"beneficiary": "受益人",
"settleTime": "结算时间",
"withdrawTime": "提现时间",
"remark": "备注"
},
"status": {
"pending": "待审核",
"approved": "已通过",
"rejected": "已拒绝",
"completed": "已到账",
"frozen": "已冻结",
"unfreezing": "解冻中",
"settled": "已发放",
"invalid": "已失效"
},
"type": {
"oneTime": "一次性佣金",
"longTerm": "长期佣金"
},
"method": {
"alipay": "支付宝",
"wechat": "微信",
"bank": "银行卡"
},
"form": {
"withdrawalAmount": "提现金额",
"withdrawalAmountPlaceholder": "请输入提现金额",
"withdrawalMethod": "提现方式",
"withdrawalMethodPlaceholder": "请选择提现方式",
"accountName": "账户名称",
"accountNamePlaceholder": "请输入账户名称",
"accountNumber": "账号",
"accountNumberPlaceholder": "请输入账号",
"bankName": "银行名称",
"bankNamePlaceholder": "请输入银行名称",
"alipayAccount": "支付宝账号",
"alipayAccountPlaceholder": "请输入支付宝账号",
"wechatAccount": "微信账号",
"wechatAccountPlaceholder": "请输入微信账号",
"rejectReason": "拒绝原因",
"rejectReasonPlaceholder": "请输入拒绝原因",
"remark": "备注",
"remarkPlaceholder": "请输入备注信息",
"minWithdrawal": "最低提现金额",
"minWithdrawalPlaceholder": "请输入最低提现金额",
"maxWithdrawal": "最高提现金额",
"maxWithdrawalPlaceholder": "请输入最高提现金额",
"feeRate": "手续费率",
"feeRatePlaceholder": "请输入手续费率(%",
"availableBalance": "可提现余额",
"totalCommission": "累计佣金",
"frozenAmount": "冻结金额",
"withdrawnAmount": "已提现金额"
},
"buttons": {
"approve": "审批通过",
"reject": "拒绝",
"applyWithdrawal": "提交提现申请",
"viewDetail": "查看详情",
"export": "导出",
"batchApprove": "批量通过",
"batchReject": "批量拒绝"
},
"messages": {
"approveSuccess": "审批通过成功",
"rejectSuccess": "拒绝成功",
"withdrawalSuccess": "提现申请提交成功",
"updateSuccess": "更新成功",
"deleteSuccess": "删除成功",
"approveConfirm": "确定要通过该提现申请吗?",
"rejectConfirm": "确定要拒绝该提现申请吗?",
"batchApproveConfirm": "确定要批量通过选中的 {count} 条提现申请吗?",
"batchRejectConfirm": "确定要批量拒绝选中的 {count} 条提现申请吗?",
"insufficientBalance": "可提现余额不足",
"belowMinAmount": "提现金额不能低于最低提现金额",
"exceedMaxAmount": "提现金额不能超过最高提现金额",
"noData": "暂无数据"
},
"validation": {
"withdrawalAmountRequired": "请输入提现金额",
"withdrawalAmountInvalid": "提现金额必须大于0",
"withdrawalMethodRequired": "请选择提现方式",
"accountNameRequired": "请输入账户名称",
"accountNumberRequired": "请输入账号",
"bankNameRequired": "请输入银行名称",
"rejectReasonRequired": "请输入拒绝原因",
"remarkRequired": "请输入备注",
"minWithdrawalRequired": "请输入最低提现金额",
"maxWithdrawalRequired": "请输入最高提现金额",
"feeRateRequired": "请输入手续费率",
"feeRateInvalid": "手续费率必须在0-100之间"
},
"searchForm": {
"withdrawalNo": "提现单号",
"withdrawalNoPlaceholder": "请输入提现单号",
"applicant": "申请人",
"applicantPlaceholder": "请输入申请人",
"shopName": "店铺名称",
"shopNamePlaceholder": "请输入店铺名称",
"status": "状态",
"statusPlaceholder": "请选择状态",
"withdrawalMethod": "提现方式",
"withdrawalMethodPlaceholder": "请选择提现方式",
"dateRange": "申请时间",
"dateRangePlaceholder": ["开始日期", "结束日期"],
"commissionType": "佣金类型",
"commissionTypePlaceholder": "请选择佣金类型"
},
"dialog": {
"approve": "审批提现申请",
"reject": "拒绝提现申请",
"detail": "提现详情",
"applyWithdrawal": "提交提现申请",
"withdrawalSettings": "提现配置"
},
"summary": {
"title": "佣金统计",
"totalCommission": "累计佣金",
"availableBalance": "可提现余额",
"frozenAmount": "冻结金额",
"withdrawnAmount": "已提现金额",
"pendingAmount": "待审核金额",
"todayCommission": "今日佣金"
}
},
"seriesBinding": {
"buttons": {
"batchSetSeries": "批量设置套餐系列",
"clearBinding": "清除绑定"
},
"dialog": {
"title": "批量设置套餐系列绑定",
"titleCard": "批量设置卡的套餐系列绑定",
"titleDevice": "批量设置设备的套餐系列绑定"
},
"form": {
"seriesAllocation": "套餐系列分配",
"seriesAllocationPlaceholder": "请选择套餐系列分配",
"clearBindingOption": "清除关联",
"selectedCount": "已选择 {count} 项"
},
"messages": {
"noSelection": "请先选择要设置的项",
"setSuccess": "套餐系列绑定设置成功",
"setFailed": "套餐系列绑定设置失败",
"partialSuccess": "部分设置成功:成功 {success} 项,失败 {fail} 项",
"confirmSet": "确定要为选中的 {count} 项设置套餐系列绑定吗?",
"confirmClear": "确定要清除选中的 {count} 项的套餐系列绑定吗?"
},
"result": {
"title": "设置结果",
"successCount": "成功数量",
"failCount": "失败数量",
"failedItems": "失败项详情",
"iccid": "ICCID",
"deviceId": "设备ID",
"deviceNo": "设备号",
"reason": "失败原因"
}
},
"orderManagement": {
"title": "订单管理",
"orderList": "订单列表",
"orderDetail": "订单详情",
"orderItems": "订单明细",
"createOrder": "创建订单",
"searchForm": {
"orderNo": "订单号",
"orderNoPlaceholder": "请输入订单号",
"paymentStatus": "支付状态",
"paymentStatusPlaceholder": "请选择支付状态",
"orderType": "订单类型",
"orderTypePlaceholder": "请选择订单类型",
"dateRange": "创建时间",
"startDate": "开始时间",
"endDate": "结束时间"
},
"table": {
"id": "订单ID",
"orderNo": "订单号",
"orderType": "订单类型",
"buyerId": "买家ID",
"buyerType": "买家类型",
"paymentStatus": "支付状态",
"paymentMethod": "支付方式",
"totalAmount": "订单金额",
"paidAt": "支付时间",
"commissionStatus": "佣金状态",
"createdAt": "创建时间",
"updatedAt": "更新时间",
"operation": "操作"
},
"orderType": {
"singleCard": "单卡购买",
"device": "设备购买"
},
"buyerType": {
"personal": "个人客户",
"agent": "代理商"
},
"paymentStatus": {
"pending": "待支付",
"paid": "已支付",
"cancelled": "已取消",
"refunded": "已退款"
},
"paymentMethod": {
"wallet": "钱包支付",
"wechat": "微信支付",
"alipay": "支付宝支付",
"offline": "线下支付"
},
"commissionStatus": {
"notApplicable": "不适用",
"pending": "待结算",
"settled": "已结算"
},
"actions": {
"viewDetail": "查看详情",
"cancel": "取消",
"confirm": "确定",
"submit": "提交",
"close": "关闭"
},
"createForm": {
"packageIds": "选择套餐",
"packageIdsPlaceholder": "请选择套餐",
"iotCardId": "ICCID",
"iotCardIdPlaceholder": "请选择ICCID卡",
"deviceId": "设备号",
"deviceIdPlaceholder": "请选择设备号"
},
"items": {
"packageName": "套餐名称",
"quantity": "数量",
"unitPrice": "单价",
"amount": "小计"
},
"messages": {
"createSuccess": "订单创建成功",
"createFailed": "订单创建失败",
"cancelSuccess": "订单取消成功",
"cancelFailed": "订单取消失败",
"cancelConfirm": "取消订单确认",
"cancelConfirmText": "确定要取消该订单吗?取消后不可恢复",
"cannotCancelPaid": "已支付的订单无法取消",
"cannotCancelCancelled": "订单已取消",
"cannotCancelRefunded": "已退款的订单无法取消",
"loadFailed": "加载订单数据失败",
"noData": "暂无订单数据"
},
"validation": {
"orderTypeRequired": "请选择订单类型",
"packageIdsRequired": "请至少选择一个套餐",
"iotCardRequired": "请选择IoT卡",
"deviceRequired": "请选择设备",
"packagesMaxLimit": "最多只能选择10个套餐"
}
},
"enterpriseDevices": {
"title": "企业设备列表",
"searchForm": {
"enterpriseId": "企业ID",
"enterpriseIdPlaceholder": "请输入企业ID",
"deviceNo": "设备号",
"deviceNoPlaceholder": "请输入设备号"
},
"table": {
"deviceId": "设备ID",
"deviceNo": "设备号",
"deviceName": "设备名称",
"deviceModel": "设备型号",
"cardCount": "绑定卡数量",
"authorizedAt": "授权时间",
"operation": "操作"
},
"buttons": {
"allocateDevices": "授权设备",
"recallDevices": "撤销授权",
"refresh": "刷新"
},
"dialog": {
"allocateTitle": "授权设备给企业",
"recallTitle": "撤销设备授权",
"resultTitle": "操作结果"
},
"form": {
"deviceNos": "设备号列表",
"deviceNosPlaceholder": "请输入设备号,支持换行或逗号分隔",
"deviceNosHint": "每行一个设备号或使用逗号分隔最多100个",
"remark": "备注",
"remarkPlaceholder": "请输入授权备注(可选)",
"selectedDevices": "已选择设备",
"selectedCount": "已选择 {count} 个设备"
},
"result": {
"successCount": "成功数量",
"failCount": "失败数量",
"authorizedDevices": "已授权设备",
"failedItems": "失败项",
"deviceNo": "设备号",
"deviceId": "设备ID",
"cardCount": "绑定卡数",
"reason": "失败原因"
},
"messages": {
"allocateSuccess": "设备授权成功",
"allocateFailed": "设备授权失败",
"allocatePartialSuccess": "部分设备授权成功:成功 {success} 个,失败 {fail} 个",
"recallSuccess": "撤销授权成功",
"recallFailed": "撤销授权失败",
"recallPartialSuccess": "部分设备撤销成功:成功 {success} 个,失败 {fail} 个",
"recallConfirm": "撤销授权确认",
"recallConfirmText": "确定要撤销选中的 {count} 个设备的授权吗?",
"noSelection": "请先选择要撤销的设备",
"deviceNosRequired": "请输入设备号",
"deviceNosEmpty": "设备号列表不能为空",
"deviceNosMaxLimit": "设备号数量不能超过100个",
"invalidDeviceNos": "存在无效的设备号格式",
"loadFailed": "加载设备列表失败",
"noData": "暂无设备数据"
},
"validation": {
"deviceNosRequired": "请输入设备号列表",
"deviceNosMaxLength": "设备号数量不能超过100个"
}
}
}

View File

@@ -312,7 +312,8 @@ export interface StandaloneCardQueryParams extends PaginationParams {
shop_id?: number // 分销商ID
iccid?: string // ICCID(模糊查询)
msisdn?: string // 卡接入号(模糊查询)
virtual_no?: string // 虚拟号(模糊查询)
virtual_no?: string // 虚拟号(模糊查询)
device_virtual_no?: string // 绑定设备虚拟号(模糊查询)
batch_no?: string // 批次号
package_id?: number // 套餐ID
is_distributed?: boolean // 是否已分销
@@ -328,7 +329,8 @@ export interface StandaloneIotCard {
iccid: string // ICCID
imsi?: string // IMSI (可选)
msisdn?: string // 卡接入号 (可选)
virtual_no?: string // 虚拟号(可空)
virtual_no?: string // 虚拟号(可空)
device_virtual_no?: string // 绑定设备虚拟号(可空)
carrier_id: number // 运营商ID
carrier_type: string // 运营商类型 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电)
carrier_name: string // 运营商名称

View File

@@ -94,6 +94,7 @@ export interface CreateOrderRequest {
identifier?: string // 资产标识符ICCID 或 VirtualNo
package_ids: number[] // 套餐ID列表
payment_method: OrderPaymentMethod // 支付方式
payment_voucher_key?: string // 线下支付凭证(仅 offline 必填)
}
// 创建订单响应 (返回订单详情)

View File

@@ -205,6 +205,8 @@ export interface SeriesSelectOption {
id: number
series_name: string
series_code: string
enable_one_time_commission?: boolean // 是否启用一次性佣金
one_time_commission_config?: SeriesOneTimeCommissionConfig // 一次性佣金配置
}
// ==================== 代理系列授权 (新接口) ====================

View File

@@ -13,6 +13,7 @@ export interface ShopResponse {
shop_code: string // 店铺编号
level: number // 店铺层级 (1-7级)
parent_id: number | null // 上级店铺ID
parent_shop_name?: string // 上级店铺名称
province: string // 省份
city: string // 城市
district: string // 区县
@@ -26,6 +27,7 @@ export interface ShopResponse {
export interface ShopQueryParams extends PaginationParams {
shop_name?: string // 店铺名称模糊查询
shop_code?: string // 店铺编号模糊查询
parent_shop_name?: string // 上级店铺名称模糊查询
parent_id?: number | null // 上级店铺ID
level?: number | null // 店铺层级 (1-7级)
status?: number | null // 状态 (0:禁用, 1:启用)

View File

@@ -87,6 +87,7 @@ declare module 'vue' {
ElButton: typeof import('element-plus/es')['ElButton']
ElCalendar: typeof import('element-plus/es')['ElCalendar']
ElCard: typeof import('element-plus/es')['ElCard']
ElCascader: typeof import('element-plus/es')['ElCascader']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
ElCol: typeof import('element-plus/es')['ElCol']
@@ -104,6 +105,7 @@ declare module 'vue' {
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElImage: typeof import('element-plus/es')['ElImage']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElMenu: typeof import('element-plus/es')['ElMenu']

View File

@@ -14,7 +14,7 @@ export const usernameRules = (t: (key: string) => string): FormItemRule[] => [
trigger: 'blur'
},
{
min: 3,
min: 2,
max: 20,
message: t('login.validation.usernameLength'),
trigger: 'blur'

View File

@@ -104,20 +104,15 @@
<!-- 只有非代理账号才显示归属店铺选择 -->
<ElCol :span="12" v-if="!isAgentAccount">
<ElFormItem label="归属店铺" prop="owner_shop_id">
<ElTreeSelect
<ElCascader
v-model="form.owner_shop_id"
:data="shopTreeData"
:options="shopCascadeOptions"
:props="shopCascadeProps"
placeholder="请选择归属店铺"
filterable
clearable
check-strictly
:render-after-expand="false"
:props="{
label: 'shop_name',
value: 'id',
children: 'children'
}"
style="width: 100%"
@change="handleShopChange"
/>
</ElFormItem>
</ElCol>
@@ -240,6 +235,37 @@
const currentEnterpriseId = ref<number>(0)
const shopTreeData = ref<ShopResponse[]>([])
// 店铺级联选择相关
const shopCascadeOptions = ref<any[]>([])
const shopCascadeProps = {
lazy: true,
checkStrictly: true,
lazyLoad: async (node: any, resolve: any) => {
const { level, value } = node
const parentId = level === 0 ? undefined : value
try {
const res = await ShopService.getShopsCascade({ parent_id: parentId })
if (res.code === 0) {
const nodes = (res.data || []).map((item: any) => ({
value: item.id,
label: item.shop_name,
leaf: !item.has_children
}))
resolve(nodes)
} else {
resolve([])
}
} catch (error) {
console.error('加载店铺级联数据失败:', error)
resolve([])
}
},
value: 'value',
label: 'label',
children: 'children'
}
// 搜索表单初始值
const initialSearchState = {
enterprise_name: '',
@@ -510,19 +536,17 @@
}
})
// 加载店铺列表(获取所有店铺构建树形结构)
// 加载店铺列表 - 改用级联查询
const loadShopList = async () => {
shopLoading.value = true
try {
const params: any = {
page: 1,
page_size: 9999 // 获取所有数据用于构建树形结构
}
const res = await ShopService.getShops(params)
const res = await ShopService.getShopsCascade({ parent_id: undefined })
if (res.code === 0) {
const items = res.data.items || []
// 构建树形数据
shopTreeData.value = buildTreeData(items)
shopCascadeOptions.value = (res.data || []).map((item: any) => ({
value: item.id,
label: item.shop_name,
leaf: !item.has_children
}))
}
} catch (error) {
console.error('获取店铺列表失败:', error)
@@ -531,6 +555,15 @@
}
}
// 处理店铺选择变化
const handleShopChange = (value: any) => {
if (Array.isArray(value) && value.length > 0) {
form.owner_shop_id = value[value.length - 1]
} else {
form.owner_shop_id = null
}
}
// 构建树形数据
const buildTreeData = (items: ShopResponse[]) => {
const map = new Map<number, ShopResponse & { children?: ShopResponse[] }>()

File diff suppressed because it is too large Load Diff

View File

@@ -76,7 +76,7 @@
<ElFormItem label="已选设备数">
<span style="font-weight: bold; color: #409eff">{{ selectedDevices.length }}</span>
</ElFormItem>
<ElFormItem label="目标店铺" prop="target_shop_id">
<ElFormItem label="店铺" prop="target_shop_id">
<ElTreeSelect
v-model="allocateForm.target_shop_id"
:data="shopTreeData"

View File

@@ -5,7 +5,7 @@
<ElCard shadow="never" style="margin-bottom: 16px">
<template #header>
<div class="card-header">
<span>{{ $t('enterpriseDevices.title') }}</span>
<span>企业设备列表</span>
<ElButton @click="goBack">{{ $t('common.cancel') }}</ElButton>
</div>
</template>
@@ -42,7 +42,7 @@
@click="showAllocateDialog"
v-permission="'enterprise_device:allocate'"
>
{{ $t('enterpriseDevices.buttons.allocateDevices') }}
授权设备
</ElButton>
<ElButton
type="warning"
@@ -50,7 +50,7 @@
@click="showRecallDialog"
v-permission="'enterprise_device:recall'"
>
{{ $t('enterpriseDevices.buttons.recallDevices') }}
撤销授权
</ElButton>
</template>
</ArtTableHeader>
@@ -78,7 +78,7 @@
<!-- 授权设备对话框 -->
<ElDialog
v-model="allocateDialogVisible"
:title="$t('enterpriseDevices.dialog.allocateTitle')"
title="授权设备给企业"
width="75%"
@close="handleAllocateDialogClose"
>
@@ -137,19 +137,13 @@
<!-- 撤销授权对话框 -->
<ElDialog
v-model="recallDialogVisible"
:title="$t('enterpriseDevices.dialog.recallTitle')"
title="撤销设备授权"
width="600px"
@close="handleRecallDialogClose"
>
<ElForm ref="recallFormRef" :model="recallForm" :rules="recallRules">
<ElFormItem :label="$t('enterpriseDevices.form.selectedDevices')">
<div>
{{
$t('enterpriseDevices.form.selectedCount', {
count: selectedDevices.length
})
}}
</div>
<ElFormItem label="已选择设备">
<div>已选择 {{ selectedDevices.length }} 个设备</div>
</ElFormItem>
</ElForm>
<template #footer>
@@ -165,14 +159,14 @@
<!-- 结果对话框 -->
<ElDialog
v-model="resultDialogVisible"
:title="$t('enterpriseDevices.dialog.resultTitle')"
title="操作结果"
width="700px"
>
<ElDescriptions :column="2" border>
<ElDescriptionsItem :label="$t('enterpriseDevices.result.successCount')">
<ElDescriptionsItem label="成功数量">
<ElTag type="success">{{ operationResult.success_count }}</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem :label="$t('enterpriseDevices.result.failCount')">
<ElDescriptionsItem label="失败数量">
<ElTag type="danger">{{ operationResult.fail_count }}</ElTag>
</ElDescriptionsItem>
</ElDescriptions>
@@ -182,16 +176,14 @@
v-if="operationResult.failed_items && operationResult.failed_items.length > 0"
style="margin-top: 20px"
>
<ElDivider content-position="left">{{
$t('enterpriseDevices.result.failedItems')
}}</ElDivider>
<ElDivider content-position="left">失败项</ElDivider>
<ElTable :data="operationResult.failed_items" border max-height="300">
<ElTableColumn
prop="virtual_no"
:label="$t('enterpriseDevices.result.deviceNo')"
label="设备号"
width="180"
/>
<ElTableColumn prop="reason" :label="$t('enterpriseDevices.result.reason')" />
<ElTableColumn prop="reason" label="失败原因" />
</ElTable>
</div>
@@ -202,21 +194,19 @@
"
style="margin-top: 20px"
>
<ElDivider content-position="left">{{
$t('enterpriseDevices.result.authorizedDevices')
}}</ElDivider>
<ElDivider content-position="left">已授权设备</ElDivider>
<ElTable :data="operationResult.authorized_devices" border max-height="200">
<ElTableColumn
prop="virtual_no"
:label="$t('enterpriseDevices.result.deviceNo')"
label="设备号"
width="180"
/>
<ElTableColumn
prop="device_id"
:label="$t('enterpriseDevices.result.deviceId')"
label="设备ID"
width="100"
/>
<ElTableColumn :label="$t('enterpriseDevices.result.cardCount')">
<ElTableColumn label="绑定卡数">
<template #default="{ row }">
{{ row.card_count || 0 }}
</template>
@@ -259,7 +249,7 @@
defineOptions({ name: 'EnterpriseDevices' })
const { t } = useI18n()
const { t } = useI18n() // 仅保留用于 common.confirm, common.cancel
const route = useRoute()
const router = useRouter()
const loading = ref(false)
@@ -326,12 +316,12 @@
// 搜索表单配置
const searchFormItems: SearchFormItem[] = [
{
label: t('enterpriseDevices.searchForm.deviceNo'),
label: '设备号',
prop: 'virtual_no',
type: 'input',
config: {
clearable: true,
placeholder: t('enterpriseDevices.searchForm.deviceNoPlaceholder')
placeholder: '请输入设备号'
}
}
]
@@ -375,12 +365,12 @@
// 列配置
const columnOptions = [
{ label: t('enterpriseDevices.table.deviceId'), prop: 'device_id' },
{ label: t('enterpriseDevices.table.deviceNo'), prop: 'virtual_no' },
{ label: t('enterpriseDevices.table.deviceName'), prop: 'device_name' },
{ label: t('enterpriseDevices.table.deviceModel'), prop: 'device_model' },
{ label: t('enterpriseDevices.table.cardCount'), prop: 'card_count' },
{ label: t('enterpriseDevices.table.authorizedAt'), prop: 'authorized_at' }
{ label: '设备ID', prop: 'device_id' },
{ label: '设备号', prop: 'virtual_no' },
{ label: '设备名称', prop: 'device_name' },
{ label: '设备型号', prop: 'device_model' },
{ label: '绑定卡数量', prop: 'card_count' },
{ label: '授权时间', prop: 'authorized_at' }
]
const deviceList = ref<EnterpriseDeviceItem[]>([])
@@ -421,32 +411,32 @@
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'device_id',
label: t('enterpriseDevices.table.deviceId'),
label: '设备ID',
width: 100
},
{
prop: 'virtual_no',
label: t('enterpriseDevices.table.deviceNo'),
label: '设备号',
minWidth: 150
},
{
prop: 'device_name',
label: t('enterpriseDevices.table.deviceName'),
label: '设备名称',
minWidth: 150
},
{
prop: 'device_model',
label: t('enterpriseDevices.table.deviceModel'),
label: '设备型号',
width: 120
},
{
prop: 'card_count',
label: t('enterpriseDevices.table.cardCount'),
label: '绑定卡数量',
width: 100
},
{
prop: 'authorized_at',
label: t('enterpriseDevices.table.authorizedAt'),
label: '授权时间',
width: 180,
formatter: (row: EnterpriseDeviceItem) =>
row.authorized_at ? formatDateTime(row.authorized_at) : '-'
@@ -499,7 +489,7 @@
getEnterpriseInfo()
getTableData()
} else {
ElMessage.error(t('enterpriseDevices.messages.loadFailed'))
ElMessage.error('加载设备列表失败')
goBack()
}
})
@@ -686,16 +676,13 @@
// 显示成功消息
if (res.data.success_count > 0 && res.data.fail_count === 0) {
ElMessage.success(t('enterpriseDevices.messages.allocateSuccess'))
ElMessage.success('设备授权成功')
} else if (res.data.success_count > 0 && res.data.fail_count > 0) {
ElMessage.warning(
t('enterpriseDevices.messages.allocatePartialSuccess', {
success: res.data.success_count,
fail: res.data.fail_count
})
`部分设备授权成功:成功 ${res.data.success_count} 个,失败 ${res.data.fail_count} 个`
)
} else {
ElMessage.error(t('enterpriseDevices.messages.allocateFailed'))
ElMessage.error('设备授权失败')
}
getTableData()
@@ -715,7 +702,7 @@
// 显示撤销授权对话框
const showRecallDialog = () => {
if (selectedDevices.value.length === 0) {
ElMessage.warning(t('enterpriseDevices.messages.noSelection'))
ElMessage.warning('请先选择要撤销的设备')
return
}
recallDialogVisible.value = true
@@ -729,10 +716,8 @@
if (!recallFormRef.value) return
ElMessageBox.confirm(
t('enterpriseDevices.messages.recallConfirmText', {
count: selectedDevices.value.length
}),
t('enterpriseDevices.messages.recallConfirm'),
`确定要撤销选中的 ${selectedDevices.value.length} 个设备的授权吗?`,
'撤销授权确认',
{
confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel'),
@@ -753,16 +738,13 @@
// 显示成功消息
if (res.data.success_count > 0 && res.data.fail_count === 0) {
ElMessage.success(t('enterpriseDevices.messages.recallSuccess'))
ElMessage.success('撤销授权成功')
} else if (res.data.success_count > 0 && res.data.fail_count > 0) {
ElMessage.warning(
t('enterpriseDevices.messages.recallPartialSuccess', {
success: res.data.success_count,
fail: res.data.fail_count
})
`部分设备撤销成功:成功 ${res.data.success_count} 个,失败 ${res.data.fail_count} 个`
)
} else {
ElMessage.error(t('enterpriseDevices.messages.recallFailed'))
ElMessage.error('撤销授权失败')
}
// 清空选择
@@ -774,7 +756,7 @@
}
} catch (error) {
console.error(error)
ElMessage.error(t('enterpriseDevices.messages.recallFailed'))
ElMessage.error('撤销授权失败')
} finally {
recallLoading.value = false
}

View File

@@ -93,7 +93,7 @@
:rules="allocateRules"
label-width="120px"
>
<ElFormItem label="目标店铺" prop="to_shop_id">
<ElFormItem label="店铺" prop="to_shop_id">
<ElSelect
v-model="allocateForm.to_shop_id"
placeholder="请选择或搜索目标店铺"
@@ -669,6 +669,7 @@
iccid: string
msisdn: string
virtual_no: string
device_virtual_no: string
is_distributed: undefined | number
[key: string]: any
} = {
@@ -677,6 +678,7 @@
iccid: '',
msisdn: '',
virtual_no: '',
device_virtual_no: '',
is_distributed: undefined
}
@@ -827,12 +829,21 @@
}
},
{
label: '虚拟号',
label: '虚拟号',
prop: 'virtual_no',
type: 'input',
config: {
clearable: true,
placeholder: '请输入虚拟号'
placeholder: '请输入虚拟号'
}
},
{
label: '绑定设备虚拟号',
prop: 'device_virtual_no',
type: 'input',
config: {
clearable: true,
placeholder: '请输入绑定设备虚拟号'
}
},
{
@@ -854,7 +865,8 @@
const columnOptions = [
{ label: 'ICCID', prop: 'iccid' },
{ label: '卡接入号', prop: 'msisdn' },
{ label: '虚拟号', prop: 'virtual_no' },
{ label: '虚拟号', prop: 'virtual_no' },
{ label: '绑定设备虚拟号', prop: 'device_virtual_no' },
{ label: '卡业务类型', prop: 'card_category' },
{ label: '运营商', prop: 'carrier_name' },
{ label: '店铺名称', prop: 'shop_name' },
@@ -987,9 +999,15 @@
},
{
prop: 'virtual_no',
label: '虚拟号',
label: '虚拟号',
width: 160,
formatter: (row: StandaloneIotCard) => row.virtual_no
formatter: (row: StandaloneIotCard) => row.virtual_no || '-'
},
{
prop: 'device_virtual_no',
label: '绑定设备虚拟号',
width: 160,
formatter: (row: StandaloneIotCard) => row.device_virtual_no || '-'
},
{
prop: 'card_category',

View File

@@ -52,7 +52,7 @@
</div>
<!-- 导入对话框 -->
<ElDialog v-model="importDialogVisible" title="批量导入设备" width="700px" align-center>
<ElDialog v-model="importDialogVisible" title="批量导入设备" width="40%" align-center>
<ElAlert type="info" :closable="false" style="margin-bottom: 20px">
<template #title>
<div style="line-height: 1.8">
@@ -62,7 +62,7 @@
<p>3. 列格式请设置为文本格式避免长数字被转为科学计数法</p>
<p
>4.
必填列virtual_no设备device_name设备名称device_model设备型号device_type设备类型imei设备IMEI号</p
必填列virtual_no虚拟device_name设备名称device_model设备型号device_type设备类型imei设备IMEI号</p
>
<p
>5. 可选列manufacturer制造商max_sim_slots最大插槽数默认4iccid_1 ~

View File

@@ -10,22 +10,22 @@
</i>
</div>
<ElDropdown @command="changeLanguage" popper-class="langDropDownStyle">
<div class="btn language-btn">
<i class="iconfont-sys icon-language">&#xe611;</i>
</div>
<template #dropdown>
<ElDropdownMenu>
<div v-for="lang in languageOptions" :key="lang.value" class="lang-btn-item">
<ElDropdownItem
:command="lang.value"
:class="{ 'is-selected': locale === lang.value }"
>
<span class="menu-txt">{{ lang.label }}</span>
<i v-if="locale === lang.value" class="iconfont-sys icon-check">&#xe621;</i>
</ElDropdownItem>
</div>
</ElDropdownMenu>
</template>
<!--<div class="btn language-btn">-->
<!-- <i class="iconfont-sys icon-language">&#xe611;</i>-->
<!--</div>-->
<!--<template #dropdown>-->
<!-- <ElDropdownMenu>-->
<!-- <div v-for="lang in languageOptions" :key="lang.value" class="lang-btn-item">-->
<!-- <ElDropdownItem-->
<!-- :command="lang.value"-->
<!-- :class="{ 'is-selected': locale === lang.value }"-->
<!-- >-->
<!-- <span class="menu-txt">{{ lang.label }}</span>-->
<!-- <i v-if="locale === lang.value" class="iconfont-sys icon-check">&#xe621;</i>-->
<!-- </ElDropdownItem>-->
<!-- </div>-->
<!-- </ElDropdownMenu>-->
<!--</template>-->
</ElDropdown>
</div>
<div class="header">
@@ -97,9 +97,6 @@
<script setup lang="ts">
import AppConfig from '@/config'
import { RoutesAlias } from '@/router/routesAlias'
import { getCssVar } from '@/utils/ui'
import { languageOptions } from '@/locales'
import { LanguageEnum, SystemThemeEnum } from '@/enums/appEnum'
import { useI18n } from 'vue-i18n'
import { useSettingStore } from '@/store/modules/setting'

View File

@@ -40,24 +40,24 @@
<!-- 拒绝提现对话框 -->
<ElDialog
v-model="rejectDialogVisible"
:title="$t('commission.dialog.reject')"
title="拒绝提现申请"
width="500px"
>
<ElForm ref="rejectFormRef" :model="rejectForm" :rules="rejectRules" label-width="100px">
<ElFormItem :label="$t('commission.form.rejectReason')" prop="reject_reason">
<ElFormItem label="拒绝原因" prop="reject_reason">
<ElInput
v-model="rejectForm.reject_reason"
type="textarea"
:rows="4"
:placeholder="$t('commission.form.rejectReasonPlaceholder')"
placeholder="请输入拒绝原因"
/>
</ElFormItem>
<ElFormItem :label="$t('commission.form.remark')" prop="remark">
<ElFormItem label="备注" prop="remark">
<ElInput
v-model="rejectForm.remark"
type="textarea"
:rows="3"
:placeholder="$t('commission.form.remarkPlaceholder')"
placeholder="请输入备注信息"
/>
</ElFormItem>
</ElForm>
@@ -94,7 +94,7 @@
defineOptions({ name: 'WithdrawalApproval' })
const { t } = useI18n()
const { t } = useI18n() // 仅保留用于 common.tips, common.confirm, common.cancel
const rejectDialogVisible = ref(false)
const loading = ref(false)
@@ -116,50 +116,50 @@
// 提现状态选项
const withdrawalStatusOptions = [
{ label: t('commission.status.pending'), value: 1 },
{ label: t('commission.status.approved'), value: 2 },
{ label: t('commission.status.rejected'), value: 3 },
{ label: t('commission.status.completed'), value: 4 }
{ label: '待审核', value: 1 },
{ label: '已通过', value: 2 },
{ label: '已拒绝', value: 3 },
{ label: '已到账', value: 4 }
]
// 搜索表单配置
const searchFormItems: SearchFormItem[] = [
{
label: t('commission.searchForm.status'),
label: '状态',
prop: 'status',
type: 'select',
options: withdrawalStatusOptions,
config: {
clearable: true,
placeholder: t('commission.searchForm.statusPlaceholder')
placeholder: '请选择状态'
}
},
{
label: t('commission.searchForm.withdrawalNo'),
label: '提现单号',
prop: 'withdrawal_no',
type: 'input',
config: {
clearable: true,
placeholder: t('commission.searchForm.withdrawalNoPlaceholder')
placeholder: '请输入提现单号'
}
},
{
label: t('commission.searchForm.shopName'),
label: '店铺名称',
prop: 'shop_name',
type: 'input',
config: {
clearable: true,
placeholder: t('commission.searchForm.shopNamePlaceholder')
placeholder: '请输入店铺名称'
}
},
{
label: t('commission.searchForm.dateRange'),
label: '申请时间',
prop: 'dateRange',
type: 'daterange',
config: {
clearable: true,
startPlaceholder: t('commission.searchForm.dateRangePlaceholder.0'),
endPlaceholder: t('commission.searchForm.dateRangePlaceholder.1'),
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
onChange: (val: [string, string] | null) => {
if (val && val.length === 2) {
@@ -183,27 +183,27 @@
// 列配置
const columnOptions = [
{ label: t('commission.table.withdrawalNo'), prop: 'withdrawal_no' },
{ label: t('commission.table.shopName'), prop: 'shop_name' },
{ label: t('commission.table.applicant'), prop: 'applicant_name' },
{ label: t('commission.table.withdrawalAmount'), prop: 'amount' },
{ label: t('commission.table.fee'), prop: 'fee' },
{ label: t('commission.table.actualAmount'), prop: 'actual_amount' },
{ label: t('commission.table.withdrawalMethod'), prop: 'withdrawal_method' },
{ label: t('commission.table.status'), prop: 'status' },
{ label: t('commission.table.applyTime'), prop: 'created_at' },
{ label: t('commission.table.approveTime'), prop: 'processed_at' },
{ label: t('commission.table.actions'), prop: 'operation' }
{ label: '提现单号', prop: 'withdrawal_no' },
{ label: '店铺名称', prop: 'shop_name' },
{ label: '申请人', prop: 'applicant_name' },
{ label: '提现金额', prop: 'amount' },
{ label: '手续费', prop: 'fee' },
{ label: '实际到账', prop: 'actual_amount' },
{ label: '提现方式', prop: 'withdrawal_method' },
{ label: '状态', prop: 'status' },
{ label: '申请时间', prop: 'created_at' },
{ label: '审批时间', prop: 'processed_at' },
{ label: '操作', prop: 'operation' }
]
const rejectFormRef = ref<FormInstance>()
const rejectRules = reactive<FormRules>({
reject_reason: [
{ required: true, message: t('commission.validation.rejectReasonRequired'), trigger: 'blur' }
{ required: true, message: '请输入拒绝原因', trigger: 'blur' }
],
remark: [
{ required: true, message: t('commission.validation.remarkRequired'), trigger: 'blur' }
{ required: true, message: '请输入备注', trigger: 'blur' }
]
})
@@ -218,43 +218,43 @@
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'withdrawal_no',
label: t('commission.table.withdrawalNo'),
label: '提现单号',
minWidth: 180
},
{
prop: 'shop_name',
label: t('commission.table.shopName'),
label: '店铺名称',
minWidth: 150
},
{
prop: 'applicant_name',
label: t('commission.table.applicant'),
label: '申请人',
width: 120
},
{
prop: 'amount',
label: t('commission.table.withdrawalAmount'),
label: '提现金额',
width: 120,
align: 'right',
formatter: (row: WithdrawalRequestItem) => formatMoney(row.amount)
},
{
prop: 'fee',
label: t('commission.table.fee'),
label: '手续费',
width: 100,
align: 'right',
formatter: (row: WithdrawalRequestItem) => formatMoney(row.fee)
},
{
prop: 'actual_amount',
label: t('commission.table.actualAmount'),
label: '实际到账',
width: 120,
align: 'right',
formatter: (row: WithdrawalRequestItem) => formatMoney(row.actual_amount)
},
{
prop: 'withdrawal_method',
label: t('commission.table.withdrawalMethod'),
label: '提现方式',
width: 120,
formatter: (row: WithdrawalRequestItem) => {
const method = WithdrawalMethodMap[row.withdrawal_method as WithdrawalMethod]
@@ -263,7 +263,7 @@
},
{
prop: 'status',
label: t('commission.table.status'),
label: '状态',
width: 100,
formatter: (row: WithdrawalRequestItem) => {
const statusInfo = WithdrawalStatusMap[row.status as keyof typeof WithdrawalStatusMap]
@@ -272,19 +272,19 @@
},
{
prop: 'created_at',
label: t('commission.table.applyTime'),
label: '申请时间',
width: 180,
formatter: (row: WithdrawalRequestItem) => formatDateTime(row.created_at)
},
{
prop: 'processed_at',
label: t('commission.table.approveTime'),
label: '审批时间',
width: 180,
formatter: (row: WithdrawalRequestItem) => formatDateTime(row.processed_at)
},
{
prop: 'operation',
label: t('commission.table.actions'),
label: '操作',
width: 150,
fixed: 'right',
formatter: (row: WithdrawalRequestItem) => {
@@ -292,12 +292,12 @@
if (row.status === 1) {
return h('div', { style: 'display: flex; gap: 8px;' }, [
h(ArtButtonTable, {
text: t('commission.buttons.approve'),
text: '审批通过',
iconColor: '#67C23A',
onClick: () => handleApprove(row)
}),
h(ArtButtonTable, {
text: t('commission.buttons.reject'),
text: '拒绝',
iconColor: '#F56C6C',
onClick: () => showRejectDialog(row)
})
@@ -368,7 +368,7 @@
// 审批通过
const handleApprove = (row: WithdrawalRequestItem) => {
ElMessageBox.confirm(t('commission.messages.approveConfirm'), t('common.tips'), {
ElMessageBox.confirm('确定要通过该提现申请吗?', t('common.tips'), {
confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel'),
type: 'warning'
@@ -376,7 +376,7 @@
.then(async () => {
try {
await CommissionService.approveWithdrawal(row.id)
ElMessage.success(t('commission.messages.approveSuccess'))
ElMessage.success('审批通过成功')
getTableData()
} catch (error) {
console.error(error)
@@ -407,7 +407,7 @@
reject_reason: rejectForm.reject_reason,
remark: rejectForm.remark
})
ElMessage.success(t('commission.messages.rejectSuccess'))
ElMessage.success('拒绝成功')
rejectDialogVisible.value = false
rejectFormRef.value?.resetFields()
getTableData()

View File

@@ -18,7 +18,12 @@
@refresh="handleRefresh"
>
<template #left>
<ElButton type="primary" @click="showCreateDialog" v-if="hasAuth('agent_recharge:create')">创建充值订单</ElButton>
<ElButton
type="primary"
@click="showCreateDialog"
v-if="hasAuth('agent_recharge:create')"
>创建充值订单</ElButton
>
</template>
</ArtTableHeader>
@@ -80,21 +85,16 @@
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="目标店铺" prop="shop_id">
<ElTreeSelect
<ElFormItem label="店铺" prop="shop_id">
<ElCascader
v-model="createForm.shop_id"
:data="shopTreeData"
:options="shopCascadeOptions"
:props="shopCascadeProps"
placeholder="请选择店铺"
filterable
clearable
check-strictly
:render-after-expand="false"
:props="{
label: 'shop_name',
value: 'id',
children: 'children'
}"
style="width: 100%"
@change="handleShopChange"
/>
</ElFormItem>
</ElForm>
@@ -154,7 +154,7 @@
import { h } from 'vue'
import { useRouter } from 'vue-router'
import { AgentRechargeService, ShopService } from '@/api/modules'
import { ElMessage, ElTag, ElTreeSelect, ElButton } from 'element-plus'
import { ElMessage, ElTag, ElTreeSelect, ElButton, ElCascader } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import type {
AgentRecharge,
@@ -201,6 +201,37 @@
const shopOptions = ref<any[]>([])
const shopTreeData = ref<ShopResponse[]>([])
// 店铺级联选择相关
const shopCascadeOptions = ref<any[]>([])
const shopCascadeProps = {
lazy: true,
checkStrictly: true,
lazyLoad: async (node: any, resolve: any) => {
const { level, value } = node
const parentId = level === 0 ? undefined : value
try {
const res = await ShopService.getShopsCascade({ parent_id: parentId })
if (res.code === 0) {
const nodes = (res.data || []).map((item: any) => ({
value: item.id,
label: item.shop_name,
leaf: !item.has_children
}))
resolve(nodes)
} else {
resolve([])
}
} catch (error) {
console.error('加载店铺级联数据失败:', error)
resolve([])
}
},
value: 'value',
label: 'label',
children: 'children'
}
// 搜索表单配置
const searchFormItems: SearchFormItem[] = [
{
@@ -427,26 +458,38 @@
return tree
}
// 加载店铺列表
// 加载店铺列表 - 改用级联查询
const loadShops = async () => {
try {
const params: any = {
page: 1,
page_size: 9999 // 获取所有数据用于构建树形结构
// 加载平铺列表用于搜索(保留原有逻辑)
const res1 = await ShopService.getShops({ page: 1, page_size: 9999 })
if (res1.code === 0) {
shopOptions.value = res1.data.items || []
}
const res = await ShopService.getShops(params)
if (res.code === 0) {
const items = res.data.items || []
// 保留平铺列表用于搜索
shopOptions.value = items
// 构建树形数据用于创建对话框
shopTreeData.value = buildTreeData(items)
// 加载顶级店铺用于级联选择
const res2 = await ShopService.getShopsCascade({ parent_id: undefined })
if (res2.code === 0) {
shopCascadeOptions.value = (res2.data || []).map((item: any) => ({
value: item.id,
label: item.shop_name,
leaf: !item.has_children
}))
}
} catch (error) {
console.error('Load shops failed:', error)
}
}
// 处理店铺选择变化
const handleShopChange = (value: any) => {
if (Array.isArray(value) && value.length > 0) {
createForm.shop_id = value[value.length - 1]
} else {
createForm.shop_id = null
}
}
// 获取充值订单列表
const getTableData = async () => {
loading.value = true
@@ -590,12 +633,24 @@
})
}
// 查看详情
const handleViewDetail = (row: AgentRecharge) => {
router.push({
name: 'AgentRechargeDetailRoute',
params: { id: row.id }
})
}
// 获取操作按钮
const getActions = (row: AgentRecharge) => {
const actions: any[] = []
// 待支付且线下转账的订单可以确认支付
if (row.status === 1 && row.payment_method === 'offline' && hasAuth('agent_recharge:confirm_payment')) {
if (
row.status === 1 &&
row.payment_method === 'offline' &&
hasAuth('agent_recharge:confirm_payment')
) {
actions.push({
label: '确认支付',
handler: () => handleShowConfirmPay(row),

View File

@@ -51,7 +51,6 @@
import { useRoute, useRouter } from 'vue-router'
import { ElCard, ElButton, ElIcon, ElMessage, ElTable, ElTableColumn, ElTag } from 'element-plus'
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
import { useI18n } from 'vue-i18n'
import DetailPage from '@/components/common/DetailPage.vue'
import type { DetailSection } from '@/components/common/DetailPage.vue'
import { OrderService } from '@/api/modules'
@@ -60,7 +59,6 @@
defineOptions({ name: 'OrderDetail' })
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
@@ -74,24 +72,20 @@
// 获取订单类型文本
const getOrderTypeText = (type: string): string => {
return type === 'single_card'
? t('orderManagement.orderType.singleCard')
: t('orderManagement.orderType.device')
return type === 'single_card' ? '单卡购买' : '设备购买'
}
// 获取买家类型文本
const getBuyerTypeText = (type: string): string => {
return type === 'personal'
? t('orderManagement.buyerType.personal')
: t('orderManagement.buyerType.agent')
return type === 'personal' ? '个人客户' : '代理商'
}
// 获取支付方式文本
const getPaymentMethodText = (method: string): string => {
const methodMap: Record<string, string> = {
wallet: t('orderManagement.paymentMethod.wallet'),
wechat: t('orderManagement.paymentMethod.wechat'),
alipay: t('orderManagement.paymentMethod.alipay')
wallet: '钱包支付',
wechat: '微信支付',
alipay: '支付宝支付'
}
return methodMap[method] || method
}
@@ -99,9 +93,9 @@
// 获取佣金状态文本
const getCommissionStatusText = (status: number): string => {
const statusMap: Record<number, string> = {
0: t('orderManagement.commissionStatus.notApplicable'),
1: t('orderManagement.commissionStatus.pending'),
2: t('orderManagement.commissionStatus.settled')
0: '不适用',
1: '待结算',
2: '已结算'
}
return statusMap[status] || '-'
}
@@ -122,17 +116,17 @@
{
title: '基本信息',
fields: [
{ label: t('orderManagement.table.orderNo'), prop: 'order_no' },
{ label: '订单号', prop: 'order_no' },
{
label: t('orderManagement.table.orderType'),
label: '订单类型',
formatter: (_, data) => getOrderTypeText(data.order_type)
},
{
label: t('orderManagement.table.paymentStatus'),
label: '支付状态',
formatter: (_, data) => data.payment_status_text || '-'
},
{
label: t('orderManagement.table.totalAmount'),
label: '订单金额',
prop: 'total_amount',
formatter: (value) => formatCurrency(value)
},
@@ -148,7 +142,7 @@
formatter: (value) => value || '-'
},
{
label: t('orderManagement.table.buyerType'),
label: '买家类型',
formatter: (_, data) => (data.buyer_type ? getBuyerTypeText(data.buyer_type) : '-')
},
{
@@ -198,7 +192,7 @@
formatter: (value) => (value ? formatDateTime(value) : '-')
},
{
label: t('orderManagement.table.commissionStatus'),
label: '佣金状态',
formatter: (_, data) => getCommissionStatusText(data.commission_status)
},
{
@@ -206,12 +200,12 @@
prop: 'commission_config_version'
},
{
label: t('orderManagement.table.createdAt'),
label: '创建时间',
prop: 'created_at',
formatter: (value) => formatDateTime(value)
},
{
label: t('orderManagement.table.updatedAt'),
label: '更新时间',
prop: 'updated_at',
formatter: (value) => formatDateTime(value)
}

View File

@@ -176,6 +176,26 @@
</template>
</div>
</ElFormItem>
<ElFormItem
v-if="createForm.payment_method === 'offline'"
label="支付凭证"
prop="payment_voucher_key"
>
<ElUpload
ref="uploadRef"
:auto-upload="false"
:on-change="handleVoucherFileChange"
:on-remove="handleRemoveVoucher"
accept="image/*"
list-type="picture-card"
:limit="1"
>
<el-icon><Plus /></el-icon>
<template #tip>
<div class="el-upload__tip">支持 jpgpng 格式文件大小不超过 5MB</div>
</template>
</ElUpload>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
@@ -265,9 +285,10 @@
<script setup lang="ts">
import { h } from 'vue'
import { useRouter } from 'vue-router'
import { OrderService, CardService, DeviceService, PackageManageService } from '@/api/modules'
import { OrderService, CardService, DeviceService, PackageManageService, StorageService } from '@/api/modules'
import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import type { FormInstance, FormRules, UploadInstance, UploadFile } from 'element-plus'
import type {
Order,
OrderQueryParams,
@@ -408,29 +429,104 @@
]
const createFormRef = ref<FormInstance>()
const uploadRef = ref<UploadInstance>()
const createRules = reactive<FormRules>({
order_type: [
{
required: true,
message: '请选择订单类型',
trigger: 'change'
// 处理支付凭证文件选择
const handleVoucherFileChange = async (uploadFile: UploadFile) => {
const file = uploadFile.raw
if (!file) return
// 验证文件类型
const isImage = file.type.startsWith('image/')
if (!isImage) {
ElMessage.error('只能上传图片文件!')
uploadRef.value?.clearFiles()
return
}
// 验证文件大小
const isLt5M = file.size / 1024 / 1024 < 5
if (!isLt5M) {
ElMessage.error('图片大小不能超过 5MB!')
uploadRef.value?.clearFiles()
return
}
// 开始上传
try {
ElMessage.info('正在上传支付凭证...')
// 1. 获取上传 URL
const uploadUrlRes = await StorageService.getUploadUrl({
file_name: file.name,
content_type: file.type,
purpose: 'attachment'
})
if (uploadUrlRes.code !== 0) {
ElMessage.error(uploadUrlRes.msg || '获取上传地址失败')
uploadRef.value?.clearFiles()
return
}
],
package_ids: [
{
required: true,
message: '请选择套餐',
trigger: 'change'
}
],
payment_method: [
{
required: true,
message: '请选择支付方式',
trigger: 'change'
}
]
const { upload_url, file_key } = uploadUrlRes.data
// 2. 上传文件到对象存储
await StorageService.uploadFile(upload_url, file, file.type)
// 3. 保存 file_key
createForm.payment_voucher_key = file_key
ElMessage.success('上传成功')
} catch (error: any) {
console.error('上传失败:', error)
ElMessage.error(error.message || '上传失败,请重试')
createForm.payment_voucher_key = undefined
uploadRef.value?.clearFiles()
}
}
// 删除支付凭证
const handleRemoveVoucher = () => {
createForm.payment_voucher_key = undefined
}
const createRules = computed<FormRules>(() => {
const baseRules: FormRules = {
order_type: [
{
required: true,
message: '请选择订单类型',
trigger: 'change'
}
],
package_ids: [
{
required: true,
message: '请选择套餐',
trigger: 'change'
}
],
payment_method: [
{
required: true,
message: '请选择支付方式',
trigger: 'change'
}
]
}
// 线下支付时,支付凭证为必填
if (createForm.payment_method === 'offline') {
baseRules.payment_voucher_key = [
{
required: true,
message: '请上传支付凭证',
trigger: 'change'
}
]
}
return baseRules
})
const createForm = reactive<CreateOrderRequest>({
@@ -438,7 +534,8 @@
package_ids: [],
iot_card_id: null,
device_id: null,
payment_method: 'wallet' // 默认使用钱包支付
payment_method: 'wallet', // 默认使用钱包支付
payment_voucher_key: undefined // 线下支付凭证
})
const orderList = ref<Order[]>([])
@@ -884,6 +981,10 @@
createForm.iot_card_id = null
createForm.device_id = null
createForm.payment_method = 'wallet'
createForm.payment_voucher_key = undefined
// 清空上传文件列表
uploadRef.value?.clearFiles()
// 清空套餐、IoT卡和设备搜索结果
packageOptions.value = []
@@ -964,6 +1065,11 @@
payment_method: createForm.payment_method
}
// 线下支付时,添加支付凭证
if (createForm.payment_method === 'offline' && createForm.payment_voucher_key) {
data.payment_voucher_key = createForm.payment_voucher_key
}
await OrderService.createOrder(data)
// 根据支付方式显示不同的成功消息

View File

@@ -690,15 +690,17 @@
label: '虚流量比例',
width: 120,
formatter: (row: PackageResponse) => {
// 如果启用虚流量且流量大于0计算比
// 如果启用虚流量且流量大于0计算虚量百分
const virtualData = row.virtual_data_mb ?? 0
const realData = row.real_data_mb ?? 0
if (row.enable_virtual_data && virtualData > 0) {
const ratio = realData / virtualData
return ratio.toFixed(2)
if (row.enable_virtual_data && realData > 0 && virtualData > 0) {
// 虚量百分比 = (1 - 虚流量/真流量) * 100%
// 例如真流量100G虚流量70G则虚量百分比 = (1 - 70/100) * 100% = 30%
const ratio = (1 - virtualData / realData) * 100
return `${ratio.toFixed(2)}%`
}
// 否则返回 1.0
return '1.00'
// 否则返回 0%
return '0%'
}
},
{

View File

@@ -174,13 +174,18 @@
<ElFormItem label="成本价(元)" prop="cost_price_yuan">
<ElInputNumber
v-model="packageForm.cost_price_yuan"
:min="0"
:min="packageForm.original_cost_price || 0"
:max="packageForm.original_cost_price ? packageForm.original_cost_price * 1.5 : undefined"
:precision="2"
:step="0.01"
:controls="false"
style="width: 100%"
placeholder="请输入成本价"
/>
<div v-if="packageForm.original_cost_price" class="form-tip">
原价: ¥{{ packageForm.original_cost_price.toFixed(2) }},
最高: ¥{{ (packageForm.original_cost_price * 1.5).toFixed(2) }} (50%溢价)
</div>
</ElFormItem>
</ElForm>
@@ -245,17 +250,16 @@
<ElRow :gutter="20" v-if="form.series_id">
<ElCol :span="24">
<ElFormItem label="选择店铺" prop="shop_id">
<ElTreeSelect
<ElCascader
v-model="form.shop_id"
:data="shopTreeData"
:props="{ label: 'shop_name', value: 'id', children: 'children' }"
placeholder="请选择店铺"
:options="shopCascadeOptions"
:props="shopCascadeProps"
placeholder="请选择或搜索店铺"
style="width: 100%"
filterable
clearable
:loading="shopLoading"
check-strictly
:render-after-expand="false"
@change="handleShopChange"
/>
</ElFormItem>
</ElCol>
@@ -308,6 +312,7 @@
<ElInputNumber
v-model="pkg.cost_price"
:min="pkg.original_cost_price || 0"
:max="pkg.original_cost_price ? pkg.original_cost_price * 1.5 : undefined"
:precision="2"
:step="0.01"
:controls="false"
@@ -315,7 +320,7 @@
style="width: 150px"
/>
<span v-if="pkg.original_cost_price" class="min-cost-hint">
(成本: ¥{{ pkg.original_cost_price.toFixed(2) }})
(: ¥{{ pkg.original_cost_price.toFixed(2) }}, 最高: ¥{{ (pkg.original_cost_price * 1.5).toFixed(2) }})
</span>
</div>
<ElButton type="danger" size="small" @click="removePackage(index)"
@@ -324,7 +329,7 @@
</div>
</div>
<div class="form-tip"
>设置每个套餐的成本价单位不能低于套餐原始成本</div
>设置每个套餐的成本价单位不能低于原价不能高于原价的50%</div
>
</ElFormItem>
</template>
@@ -333,12 +338,13 @@
<!-- 第二步佣金和强制充值配置 -->
<div v-show="currentStep === 1">
<!-- 一次性佣金配置 -->
<div class="form-section-title">
<span class="title-text">一次性佣金配置</span>
</div>
<template v-if="form.enable_one_time_commission">
<div class="form-section-title">
<span class="title-text">一次性佣金配置</span>
</div>
<!-- 佣金类型和金额 - 2列布局 -->
<ElRow :gutter="20">
<!-- 佣金类型和金额 - 2列布局 -->
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="佣金类型">
<div class="commission-type-display">
@@ -432,6 +438,7 @@
</div>
</ElFormItem>
</template>
</template>
<!-- 强制充值配置 -->
<div class="form-section-title">
@@ -497,12 +504,13 @@
</div>
<!-- 一次性佣金配置 -->
<div class="form-section-title">
<span class="title-text">一次性佣金配置</span>
</div>
<template v-if="form.enable_one_time_commission">
<div class="form-section-title">
<span class="title-text">一次性佣金配置</span>
</div>
<!-- 佣金类型和金额 - 2列布局 -->
<ElRow :gutter="20" v-if="form.series_id">
<!-- 佣金类型和金额 - 2列布局 -->
<ElRow :gutter="20" v-if="form.series_id">
<ElCol :span="12">
<ElFormItem label="佣金类型">
<div class="commission-type-display">
@@ -596,6 +604,7 @@
</div>
</ElFormItem>
</template>
</template>
<!-- 强制充值配置 -->
<div class="form-section-title">
@@ -808,6 +817,7 @@
package_name?: string
package_code?: string
cost_price_yuan: number
original_cost_price?: number // 原始成本价(元)
}>({
cost_price_yuan: 0
})
@@ -819,7 +829,22 @@
],
cost_price_yuan: [
{ required: true, message: '请输入成本价', trigger: 'blur' },
{ type: 'number', min: 0, message: '成本价不能小于0', trigger: 'blur' }
{
validator: (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
callback(new Error('请输入成本价'))
} else if (value < 0) {
callback(new Error('成本价不能小于0'))
} else if (packageForm.value.original_cost_price && value < packageForm.value.original_cost_price) {
callback(new Error(`成本价不能低于原价 ¥${packageForm.value.original_cost_price.toFixed(2)}`))
} else if (packageForm.value.original_cost_price && value > packageForm.value.original_cost_price * 1.5) {
callback(new Error(`成本价不能高于原价的50%溢价(最高:¥${(packageForm.value.original_cost_price * 1.5).toFixed(2)}`))
} else {
callback()
}
},
trigger: 'blur'
}
]
}))
@@ -832,6 +857,37 @@
const searchShopOptions = ref<ShopResponse[]>([])
const searchAllocatorShopOptions = ref<ShopResponse[]>([])
// 店铺级联选择相关
const shopCascadeOptions = ref<any[]>([])
const shopCascadeProps = {
lazy: true,
checkStrictly: true, // 允许选择任意一级
lazyLoad: async (node: any, resolve: any) => {
const { level, value } = node
const parentId = level === 0 ? undefined : value
try {
const res = await ShopService.getShopsCascade({ parent_id: parentId })
if (res.code === 0) {
const nodes = (res.data || []).map((item: any) => ({
value: item.id,
label: item.shop_name,
leaf: !item.has_children
}))
resolve(nodes)
} else {
resolve([])
}
} catch (error) {
console.error('加载店铺级联数据失败:', error)
resolve([])
}
},
value: 'value',
label: 'label',
children: 'children'
}
// 搜索表单初始值
const initialSearchState = {
shop_id: undefined as number | undefined,
@@ -944,8 +1000,9 @@
series_name: '',
shop_name: '',
allocator_shop_name: '',
enable_one_time_commission: false, // 是否启用一次性佣金(从系列继承)
commission_type: 'fixed' as 'fixed' | 'tiered', // 佣金类型
one_time_commission_amount: 0, // 固定佣金金额(元)
one_time_commission_amount: undefined, // 固定佣金金额(元)
series_max_commission_amount: 0, // 系列最大佣金金额(元)
commission_tiers: [] as Array<{
operator?: '>=' | '>' | '<=' | '<' // 比较运算符
@@ -968,59 +1025,62 @@
shop_id: [{ required: true, message: '请选择店铺', trigger: 'change' }]
}
// 根据佣金类型添加验证规则
if (form.commission_type === 'fixed') {
baseRules.one_time_commission_amount = [
{ required: true, message: '请输入固定佣金金额', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
callback(new Error('请输入固定佣金金额'))
} else if (value < 0) {
callback(new Error('佣金金额不能小于0'))
} else if (
form.series_max_commission_amount > 0 &&
value > form.series_max_commission_amount
) {
callback(
new Error(
`佣金金额不能超过该系列最大值 ¥${form.series_max_commission_amount.toFixed(2)}`
)
)
} else {
callback()
}
},
trigger: 'blur'
}
]
} else if (form.commission_type === 'tiered') {
baseRules.commission_tiers = [
{
validator: (rule, value, callback) => {
if (!value || value.length === 0) {
callback(new Error('请至少添加一个梯度配置'))
return
}
// 验证每个梯度的佣金金额不超过最大值
for (let i = 0; i < value.length; i++) {
const tier = value[i]
if (tier.max_amount && tier.amount > tier.max_amount) {
// 只有启用一次性佣金时才添加佣金验证规则
if (form.enable_one_time_commission) {
// 根据佣金类型添加验证规则
if (form.commission_type === 'fixed') {
baseRules.one_time_commission_amount = [
{ required: true, message: '请输入固定佣金金额', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
callback(new Error('请输入固定佣金金额'))
} else if (value < 0) {
callback(new Error('佣金金额不能小于0'))
} else if (
form.series_max_commission_amount > 0 &&
value > form.series_max_commission_amount
) {
callback(
new Error(
`${i + 1}个梯度的佣金金额¥${tier.amount.toFixed(2)}不能超过系列配置的最大值¥${tier.max_amount.toFixed(2)}`
`佣金金额不能超过系列最大值 ¥${form.series_max_commission_amount.toFixed(2)}`
)
)
} else {
callback()
}
},
trigger: 'blur'
}
]
} else if (form.commission_type === 'tiered') {
baseRules.commission_tiers = [
{
validator: (rule, value, callback) => {
if (!value || value.length === 0) {
callback(new Error('请至少添加一个梯度配置'))
return
}
}
callback()
},
trigger: 'change'
}
]
// 验证每个梯度的佣金金额不超过最大值
for (let i = 0; i < value.length; i++) {
const tier = value[i]
if (tier.max_amount && tier.amount > tier.max_amount) {
callback(
new Error(
`${i + 1}个梯度的佣金金额¥${tier.amount.toFixed(2)}不能超过系列配置的最大值¥${tier.max_amount.toFixed(2)}`
)
)
return
}
}
callback()
},
trigger: 'change'
}
]
}
}
// 如果启用了强制充值,添加验证规则
@@ -1207,9 +1267,14 @@
return tree
}
// 店铺树节点过滤方法
const filterShopNode = (value: string, data: any) => {
if (!value) return true
return data.shop_name.toLowerCase().includes(value.toLowerCase())
}
onMounted(() => {
loadSeriesOptions()
loadShopOptions()
loadSearchSeriesOptions()
loadSearchShopOptions()
loadSearchAllocatorShopOptions()
@@ -1322,6 +1387,9 @@
// 设置系列名称
form.series_name = selectedSeries.series_name
// 获取系列是否启用一次性佣金
form.enable_one_time_commission = selectedSeries.enable_one_time_commission || false
// 从系列配置中获取佣金类型
const commissionConfig = selectedSeries.one_time_commission_config
if (commissionConfig && commissionConfig.commission_type) {
@@ -1329,14 +1397,14 @@
// 根据佣金类型初始化对应字段
if (commissionConfig.commission_type === 'fixed') {
form.one_time_commission_amount = 0
form.one_time_commission_amount = undefined
// 设置系列最大佣金金额(从分转换为元)
form.series_max_commission_amount = commissionConfig.commission_amount
? commissionConfig.commission_amount / 100
: 0
form.commission_tiers = [{ threshold: 0, amount: 0 }]
} else if (commissionConfig.commission_type === 'tiered') {
form.one_time_commission_amount = 0
form.one_time_commission_amount = undefined
form.series_max_commission_amount = 0
// 梯度配置从系列继承,包含完整字段,佣金金额也默认继承
if (commissionConfig.tiers && commissionConfig.tiers.length > 0) {
@@ -1362,7 +1430,7 @@
} else {
// 默认固定佣金
form.commission_type = 'fixed'
form.one_time_commission_amount = 0
form.one_time_commission_amount = undefined
form.series_max_commission_amount = 0
form.series_force_recharge_amount = 0
}
@@ -1374,7 +1442,7 @@
// 未选择系列,重置佣金类型
form.series_name = ''
form.commission_type = 'fixed'
form.one_time_commission_amount = 0
form.one_time_commission_amount = undefined
form.series_max_commission_amount = 0
form.series_force_recharge_amount = 0
form.commission_tiers = []
@@ -1394,6 +1462,22 @@
}
)
// 监听套餐选择,自动设置原始成本价和成本价
watch(
() => packageForm.value.package_id,
(packageId) => {
if (packageDialogType.value === 'add' && packageId) {
// 从 availablePackages 中查找选中的套餐
const selectedPackage = availablePackages.value.find((p) => p.id === packageId)
if (selectedPackage) {
const originalCostPrice = selectedPackage.cost_price ? selectedPackage.cost_price / 100 : 0
packageForm.value.original_cost_price = originalCostPrice
packageForm.value.cost_price_yuan = originalCostPrice
}
}
}
)
// 加载系列选项(用于新增对话框,默认加载10条
const loadSeriesOptions = async (seriesName?: string) => {
seriesLoading.value = true
@@ -1417,26 +1501,6 @@
}
}
// 加载店铺选项(用于新增对话框,加载所有店铺并构建树形结构)
const loadShopOptions = async () => {
shopLoading.value = true
try {
// 加载所有店铺,不分页
const res = await ShopService.getShops({
page: 1,
page_size: 10000 // 使用较大的值获取所有店铺
})
if (res.code === 0) {
shopOptions.value = res.data.items
// 构建树形结构数据
shopTreeData.value = buildTreeData(shopOptions.value)
}
} catch (error) {
console.error('加载店铺选项失败:', error)
} finally {
shopLoading.value = false
}
}
// 加载搜索栏系列选项(默认加载10条)
const loadSearchSeriesOptions = async () => {
@@ -1487,6 +1551,35 @@
}
}
// 加载店铺列表(用于新增对话框) - 改用级联查询
const loadShopListForDialog = async () => {
shopLoading.value = true
try {
const res = await ShopService.getShopsCascade({ parent_id: undefined })
if (res.code === 0) {
shopCascadeOptions.value = (res.data || []).map((item: any) => ({
value: item.id,
label: item.shop_name,
leaf: !item.has_children
}))
}
} catch (error) {
console.error('加载店铺列表失败:', error)
} finally {
shopLoading.value = false
}
}
// 处理店铺选择变化
const handleShopChange = (value: any) => {
// 级联选择器返回的是数组,取最后一个值作为 shop_id
if (Array.isArray(value) && value.length > 0) {
form.shop_id = value[value.length - 1]
} else {
form.shop_id = undefined
}
}
// 搜索系列(用于搜索栏)
const handleSearchSeries = async (query: string) => {
if (!query) {
@@ -1608,6 +1701,9 @@
dialogVisible.value = true
dialogType.value = type
// 每次打开对话框时重新加载店铺列表,确保获取最新的店铺数据
await loadShopListForDialog()
if (type === 'edit' && row) {
// 编辑模式:先调用详情接口获取完整数据
loading.value = true
@@ -1628,9 +1724,13 @@
// 获取系列配置信息
const selectedSeries = seriesOptions.value.find((s) => s.id === detail.series_id)
// 设置是否启用一次性佣金
form.enable_one_time_commission = selectedSeries?.enable_one_time_commission || false
// 设置佣金配置
if (detail.commission_type === 'fixed') {
form.one_time_commission_amount = detail.one_time_commission_amount / 100
const amount = detail.one_time_commission_amount / 100
form.one_time_commission_amount = amount > 0 ? amount : undefined
// 设置系列最大佣金金额
if (selectedSeries?.one_time_commission_config?.commission_amount) {
form.series_max_commission_amount =
@@ -1711,8 +1811,9 @@
form.series_name = ''
form.shop_name = ''
form.allocator_shop_name = ''
form.enable_one_time_commission = false
form.commission_type = 'fixed'
form.one_time_commission_amount = 0
form.one_time_commission_amount = undefined
form.series_max_commission_amount = 0
form.series_force_recharge_amount = 0
form.commission_tiers = []
@@ -1742,8 +1843,9 @@
form.series_name = ''
form.shop_name = ''
form.allocator_shop_name = ''
form.enable_one_time_commission = false
form.commission_type = 'fixed'
form.one_time_commission_amount = 0
form.one_time_commission_amount = undefined
form.series_max_commission_amount = 0
form.series_force_recharge_amount = 0
form.commission_tiers = []
@@ -1803,17 +1905,20 @@
// 构建请求数据
const data: any = {}
// 根据佣金类型携带不同参数
if (form.commission_type === 'fixed') {
// 固定模式:携带 one_time_commission_amount将元转换为分
data.one_time_commission_amount = Math.round(form.one_time_commission_amount * 100)
} else if (form.commission_type === 'tiered') {
// 梯度模式:携带 commission_tiers将元转换为分
// 注意:请求时不传 operatoroperator 是响应中从 PackageSeries 合并过来的
data.commission_tiers = form.commission_tiers.map((tier: CommissionTier) => ({
threshold: tier.threshold,
amount: Math.round(tier.amount * 100) // 将元转换为分
}))
// 只有启用一次性佣金时才传递佣金相关参数
if (form.enable_one_time_commission) {
// 根据佣金类型携带不同参数
if (form.commission_type === 'fixed') {
// 固定模式:携带 one_time_commission_amount将元转换为分
data.one_time_commission_amount = Math.round((form.one_time_commission_amount || 0) * 100)
} else if (form.commission_type === 'tiered') {
// 梯度模式:携带 commission_tiers(将元转换为分)
// 注意:请求时不传 operatoroperator 是响应中从 PackageSeries 合并过来的
data.commission_tiers = form.commission_tiers.map((tier: CommissionTier) => ({
threshold: tier.threshold,
amount: Math.round(tier.amount * 100) // 将元转换为分
}))
}
}
// 强制充值配置(无论开关状态都要传递)
@@ -1828,9 +1933,30 @@
// 可选:套餐配置(将元转换为分)
if (form.packages.length > 0) {
// 验证成本价是否在允许范围内
for (const pkg of form.packages) {
const costPrice = pkg.cost_price ?? 0
const originalCostPrice = pkg.original_cost_price ?? 0
if (originalCostPrice > 0 && costPrice > originalCostPrice * 1.5) {
ElMessage.error(
`套餐 ${getPackageName(pkg.package_id)} 的成本价不能高于原价的50%溢价(最高:¥${(originalCostPrice * 1.5).toFixed(2)}`
)
submitLoading.value = false
return
}
if (originalCostPrice > 0 && costPrice < originalCostPrice) {
ElMessage.error(
`套餐 ${getPackageName(pkg.package_id)} 的成本价不能低于原价(¥${originalCostPrice.toFixed(2)}`
)
submitLoading.value = false
return
}
}
data.packages = form.packages.map((pkg: any) => ({
package_id: pkg.package_id,
cost_price: Math.round(pkg.cost_price * 100) // 将元转换为分
cost_price: Math.round((pkg.cost_price ?? 0) * 100) // 将元转换为分
}))
}
@@ -1912,11 +2038,17 @@
// 显示编辑套餐对话框
const showEditPackageDialog = (row: GrantPackageInfo) => {
packageDialogType.value = 'edit'
// 从 packageOptions 中查找原始成本价
const pkgOption = packageOptions.value.find((p) => p.id === row.package_id)
const originalCostPrice = pkgOption?.cost_price ? pkgOption.cost_price / 100 : 0
packageForm.value = {
package_id: row.package_id,
package_name: row.package_name,
package_code: row.package_code,
cost_price_yuan: row.cost_price / 100
cost_price_yuan: row.cost_price / 100,
original_cost_price: originalCostPrice
}
packageDialogVisible.value = true

View File

@@ -5,6 +5,7 @@
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
:show-expand="false"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
@@ -31,9 +32,6 @@
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
:default-expand-all="false"
:pagination="false"
:actions="getActions"
:actionsWidth="160"
@selection-change="handleSelectionChange"
@@ -76,20 +74,15 @@
<ElRow :gutter="20" v-if="dialogType === 'add'">
<ElCol :span="12">
<ElFormItem label="上级店铺" prop="parent_id">
<ElTreeSelect
<ElCascader
v-model="formData.parent_id"
:data="parentShopTreeData"
:options="parentShopCascadeOptions"
:props="cascadeProps"
placeholder="一级店铺可不选"
filterable
clearable
check-strictly
:render-after-expand="false"
:props="{
label: 'shop_name',
value: 'id',
children: 'children'
}"
style="width: 100%"
@change="handleParentShopChange"
/>
</ElFormItem>
</ElCol>
@@ -287,7 +280,6 @@
ElSwitch,
ElSelect,
ElOption,
ElTreeSelect,
ElCascader
} from 'element-plus'
import { ref, reactive, computed, nextTick, onMounted } from 'vue'
@@ -312,12 +304,41 @@
const dialogVisible = ref(false)
const loading = ref(false)
const submitLoading = ref(false)
const parentShopLoading = ref(false)
const parentShopList = ref<ShopResponse[]>([])
const parentShopTreeData = ref<ShopResponse[]>([])
const defaultRoleLoading = ref(false)
const defaultRoleList = ref<any[]>([])
// 级联选择相关
const parentShopCascadeOptions = ref<any[]>([])
const cascadeProps = {
lazy: true,
checkStrictly: true, // 允许选择任意一级
lazyLoad: async (node: any, resolve: any) => {
const { level, value } = node
// level 0 表示根节点,不传 parent_idlevel > 0 传 parent_id
const parentId = level === 0 ? undefined : value
try {
const res = await ShopService.getShopsCascade({ parent_id: parentId })
if (res.code === 0) {
const nodes = (res.data || []).map((item: any) => ({
value: item.id,
label: item.shop_name,
leaf: !item.has_children
}))
resolve(nodes)
} else {
resolve([])
}
} catch (error) {
console.error('加载店铺级联数据失败:', error)
resolve([])
}
},
value: 'value',
label: 'label',
children: 'children'
}
// 默认角色管理相关状态
const defaultRolesDialogVisible = ref(false)
const defaultRolesLoading = ref(false)
@@ -330,6 +351,7 @@
const initialSearchState = {
shop_name: '',
shop_code: '',
parent_shop_name: '',
parent_id: undefined as number | undefined,
level: undefined as number | undefined,
status: undefined as number | undefined
@@ -386,24 +408,6 @@
placeholder: '请输入店铺编号'
}
},
{
label: '上级店铺',
prop: 'parent_id',
type: 'tree-select',
config: {
data: parentShopTreeData.value,
clearable: true,
filterable: true,
checkStrictly: true,
renderAfterExpand: false,
props: {
label: 'shop_name',
value: 'id',
children: 'children'
},
placeholder: '请选择上级店铺'
}
},
{
label: '状态',
prop: 'status',
@@ -420,6 +424,7 @@
const columnOptions = [
{ label: '店铺名称', prop: 'shop_name' },
{ label: '店铺编号', prop: 'shop_code' },
{ label: '上级店铺', prop: 'parent_shop_name' },
{ label: '所在地区', prop: 'region' },
{ label: '联系人', prop: 'contact_name' },
{ label: '联系电话', prop: 'contact_phone' },
@@ -458,9 +463,6 @@
formData.init_phone = ''
formData.default_role_id = undefined
} else {
// 新增模式下重新获取上级店铺列表
await loadParentShopList()
formData.id = 0
formData.shop_name = ''
formData.shop_code = ''
@@ -499,8 +501,8 @@
await ShopService.deleteShop(row.id)
ElMessage.success('删除成功')
await getShopList()
// 删除成功后也更新上级店铺列表
await loadParentShopList()
// 删除成功后也重新加载顶级店铺
await loadTopLevelShops()
} catch (error) {
console.error(error)
}
@@ -524,6 +526,13 @@
minWidth: 160,
showOverflowTooltip: true
},
{
prop: 'parent_shop_name',
label: '上级店铺',
width: 150,
showOverflowTooltip: true,
formatter: (row: ShopResponse) => row.parent_shop_name || '-'
},
{
prop: 'region',
label: '所在地区',
@@ -621,7 +630,7 @@
id: 0,
shop_name: '',
shop_code: '',
parent_id: undefined as number | undefined,
parent_id: undefined as number | number[] | undefined, // 支持级联选择器的数组格式
region: [] as string[], // 省市区级联数据
province: '',
city: '',
@@ -682,54 +691,55 @@
}
}
onMounted(() => {
getShopList()
loadParentShopList()
searchDefaultRoles('') // 加载初始默认角色列表
})
// 加载上级店铺列表(用于新增对话框和搜索栏,获取所有店铺构建树形结构)
const loadParentShopList = async (shopName?: string) => {
parentShopLoading.value = true
try {
const params: any = {
page: 1,
page_size: 9999 // 获取所有数据用于构建树形结构
}
if (shopName) {
params.shop_name = shopName
}
const res = await ShopService.getShops(params)
if (res.code === 0) {
const items = res.data.items || []
parentShopList.value = items
// 构建树形数据
parentShopTreeData.value = buildTreeData(items)
}
} catch (error) {
console.error('获取上级店铺列表失败:', error)
} finally {
parentShopLoading.value = false
// 处理级联选择变化
const handleParentShopChange = (value: any) => {
// 级联选择器返回的是数组,取最后一个值作为 parent_id
if (Array.isArray(value) && value.length > 0) {
formData.parent_id = value[value.length - 1]
} else {
formData.parent_id = undefined
}
}
// 加载顶级店铺数据(用于级联选择器初始化)
const loadTopLevelShops = async () => {
try {
const res = await ShopService.getShopsCascade({ parent_id: undefined })
if (res.code === 0) {
parentShopCascadeOptions.value = (res.data || []).map((item: any) => ({
value: item.id,
label: item.shop_name,
leaf: !item.has_children
}))
}
} catch (error) {
console.error('加载顶级店铺失败:', error)
}
}
onMounted(() => {
getShopList()
loadTopLevelShops() // 加载顶级店铺用于级联选择
searchDefaultRoles('') // 加载初始默认角色列表
})
// 获取店铺列表
const getShopList = async () => {
loading.value = true
try {
const params = {
page: 1,
page_size: 9999, // 获取所有数据用于构建树形结构
page: pagination.currentPage,
page_size: pagination.pageSize,
shop_name: searchForm.shop_name || undefined,
shop_code: searchForm.shop_code || undefined,
parent_shop_name: searchForm.parent_shop_name || undefined,
parent_id: searchForm.parent_id,
level: searchForm.level,
status: searchForm.status
}
const res = await ShopService.getShops(params)
if (res.code === 0) {
const items = res.data.items || []
tableData.value = buildTreeData(items)
tableData.value = res.data.items || []
pagination.total = res.data.total || 0
}
} catch (error) {
@@ -739,33 +749,6 @@
}
}
// 构建树形数据
const buildTreeData = (items: ShopResponse[]) => {
const map = new Map<number, ShopResponse & { children?: ShopResponse[] }>()
const tree: ShopResponse[] = []
// 先将所有项放入 map
items.forEach((item) => {
map.set(item.id, { ...item, children: [] })
})
// 构建树形结构
items.forEach((item) => {
const node = map.get(item.id)!
if (item.parent_id && map.has(item.parent_id)) {
// 有父节点,添加到父节点的 children 中
const parent = map.get(item.parent_id)!
if (!parent.children) parent.children = []
parent.children.push(node)
} else {
// 没有父节点或父节点不存在,作为根节点
tree.push(node)
}
})
return tree
}
const handleRefresh = () => {
getShopList()
}
@@ -825,8 +808,12 @@
default_role_id: formData.default_role_id
}
// 可选字段
if (formData.parent_id) data.parent_id = formData.parent_id
// 可选字段 - parent_id 可能是数组(级联选择器)或数字
if (formData.parent_id) {
data.parent_id = Array.isArray(formData.parent_id)
? formData.parent_id[formData.parent_id.length - 1]
: formData.parent_id
}
if (formData.province) data.province = formData.province
if (formData.city) data.city = formData.city
if (formData.district) data.district = formData.district
@@ -856,8 +843,8 @@
dialogVisible.value = false
await getShopList()
// 提交成功后也更新上级店铺列表
await loadParentShopList()
// 提交成功后也重新加载顶级店铺
await loadTopLevelShops()
} catch (error) {
console.error(error)
} finally {

View File

@@ -830,19 +830,8 @@
}
}
// 检查节点是否有子节点(子菜单或按钮)
const hasChildren = (data: any): boolean => {
return data.children && data.children.length > 0
}
// 移除单个权限
const removeSinglePermission = async (data: any) => {
// 检查是否有子菜单或按钮
if (hasChildren(data)) {
ElMessage.warning('该权限下还有子菜单或按钮,请先移除子项后再移除此权限')
return
}
try {
// 保存右侧树的展开节点
const expandedKeys = rightTreeRef.value?.store?.nodesMap
@@ -851,10 +840,16 @@
.map((key) => Number(key))
: []
await RoleService.removePermission(currentRoleId.value, data.id)
// 获取要移除的所有权限ID包括当前节点和所有子节点
const idsToRemove = [data.id, ...getAllChildrenIds(data.id)]
// 更新已选权限列表
selectedPermissions.value = selectedPermissions.value.filter((id) => id !== data.id)
// 调用API批量移除权限传递所有需要移除的权限ID
await RoleService.removePermissions(currentRoleId.value, idsToRemove)
// 更新已选权限列表(移除当前节点及其所有子节点)
selectedPermissions.value = selectedPermissions.value.filter(
(id) => !idsToRemove.includes(id)
)
// 重新构建左右两侧树
const fullTreeData = buildTreeData(originalPermissionTree.value)