This commit is contained in:
@@ -6,6 +6,9 @@ import { BaseService } from '../BaseService'
|
||||
import type {
|
||||
BusinessUserGroupItem,
|
||||
BusinessUserGroupMembersResult,
|
||||
BusinessUserGroupMembersPageResult,
|
||||
BusinessUserGroupMembersQueryParams,
|
||||
BusinessUserGroupMemberCandidatesQueryParams,
|
||||
BusinessUserGroupQueryParams,
|
||||
CreateBusinessUserGroupParams,
|
||||
DeleteBusinessUserGroupParams,
|
||||
@@ -67,25 +70,47 @@ export class BusinessUserGroupService extends BaseService {
|
||||
* 批量设置平台用户业务用户组归属(整体替换)
|
||||
* PUT /api/admin/business-user-groups/{id}/members
|
||||
*/
|
||||
static setGroupMembers(
|
||||
static addGroupMembers(
|
||||
id: number,
|
||||
data: SetBusinessUserGroupMembersParams
|
||||
): Promise<BaseResponse<BusinessUserGroupMembersResult>> {
|
||||
return this.put<BaseResponse<BusinessUserGroupMembersResult>>(
|
||||
return this.post<BaseResponse<BusinessUserGroupMembersResult>>(
|
||||
`/api/admin/business-user-groups/${id}/members`,
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
static getMemberCandidates(
|
||||
id: number,
|
||||
params?: BusinessUserGroupMemberCandidatesQueryParams
|
||||
): Promise<BaseResponse<BusinessUserGroupMembersPageResult>> {
|
||||
return this.get<BaseResponse<BusinessUserGroupMembersPageResult>>(
|
||||
`/api/admin/business-user-groups/${id}/member-candidates`,
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
/** 查询业务用户组现有成员 */
|
||||
static getGroupMembers(
|
||||
id: number,
|
||||
params?: BusinessUserGroupMembersQueryParams
|
||||
): Promise<BaseResponse<BusinessUserGroupMembersPageResult>> {
|
||||
return this.get<BaseResponse<BusinessUserGroupMembersPageResult>>(
|
||||
`/api/admin/business-user-groups/${id}/members`,
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量清空平台用户业务用户组归属(DELETE 带 body)
|
||||
* DELETE /api/admin/business-user-groups/members
|
||||
*/
|
||||
static clearGroupMembers(
|
||||
static removeGroupMembers(
|
||||
id: number,
|
||||
data: SetBusinessUserGroupMembersParams
|
||||
): Promise<BaseResponse<BusinessUserGroupMembersResult>> {
|
||||
return this.delete<BaseResponse<BusinessUserGroupMembersResult>>(
|
||||
'/api/admin/business-user-groups/members',
|
||||
`/api/admin/business-user-groups/${id}/members`,
|
||||
undefined,
|
||||
{ data }
|
||||
)
|
||||
|
||||
@@ -20,7 +20,9 @@ import type {
|
||||
EmployeeCollectionPaymentMethodListResponse,
|
||||
EmployeeCollectionPaymentMethodQueryParams,
|
||||
EmployeeCollectionPaymentMethodResponse,
|
||||
UpdateEmployeeCollectionPaymentMethodRequest
|
||||
UpdateEmployeeCollectionPaymentMethodRequest,
|
||||
BaseResponse,
|
||||
RecoverEmployeeCollectionApplicationResponse
|
||||
} from '@/types/api'
|
||||
|
||||
const PAYMENT_METHODS_BASE_URL = '/api/admin/employee-collection-payment-methods'
|
||||
@@ -146,4 +148,13 @@ export class EmployeeCollectionService extends BaseService {
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/** 恢复核销申请原审批提交 */
|
||||
static recoverApplicationApproval(
|
||||
id: number
|
||||
): Promise<BaseResponse<RecoverEmployeeCollectionApplicationResponse>> {
|
||||
return this.post<BaseResponse<RecoverEmployeeCollectionApplicationResponse>>(
|
||||
`${APPLICATIONS_BASE_URL}/${id}/recover-approval`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,20 @@
|
||||
</ElSelect>
|
||||
<ElInput v-else :model-value="props.initialOrderNo" disabled style="width: 100%" />
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="selectedOrder" label="支付方式">
|
||||
<ElInput :model-value="paymentMethodLabel" disabled style="width: 100%" />
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="selectedOrder" label="买家类型">
|
||||
<ElInput :model-value="buyerTypeLabel" disabled style="width: 100%" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="退款方式" prop="method">
|
||||
<ElRadioGroup v-model="formData.method">
|
||||
<ElInput
|
||||
v-if="walletRefundFixed"
|
||||
:model-value="refundMethodLabel"
|
||||
disabled
|
||||
style="width: 100%"
|
||||
/>
|
||||
<ElRadioGroup v-else v-model="formData.method">
|
||||
<ElRadio v-for="opt in RefundMethodOptions" :key="opt.value" :label="opt.value">
|
||||
{{ opt.label }}
|
||||
</ElRadio>
|
||||
@@ -84,7 +96,7 @@
|
||||
@change="formRef?.validateField('refund_voucher_key')"
|
||||
/>
|
||||
<div v-if="formData.method === RefundMethod.CUSTOMER_ACCOUNT" class="voucher-tip">
|
||||
客户收款信息方式下须同时提供客户收款信息与至少 1 个退款凭证
|
||||
线下退款方式下须同时提供收款信息与至少 1 个退款凭证
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
@@ -112,7 +124,6 @@
|
||||
import { RefundMethod, type CreateRefundRequest, type Order } from '@/types/api'
|
||||
import { RefundMethodOptions } from '@/config/constants/refund'
|
||||
import { formatMoney, yuanToFen } from '@/utils/business/format'
|
||||
import { getErrorMessage } from '@/utils/business'
|
||||
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
||||
|
||||
interface Props {
|
||||
@@ -222,6 +233,35 @@
|
||||
return formatMoney(selectedOrder?.actual_paid_amount ?? 0, false)
|
||||
})
|
||||
|
||||
const refundMethodLabel = computed(
|
||||
() => RefundMethodOptions.find((option) => option.value === formData.method)?.label || '-'
|
||||
)
|
||||
|
||||
const selectedOrder = computed(() =>
|
||||
orderSearchOptions.value.find((order) => order.id === formData.order_id)
|
||||
)
|
||||
const paymentMethodLabel = computed(() => {
|
||||
const labels = {
|
||||
wallet: '钱包支付',
|
||||
wechat: '微信支付',
|
||||
alipay: '支付宝支付',
|
||||
offline: '线下支付'
|
||||
}
|
||||
return selectedOrder.value ? labels[selectedOrder.value.payment_method] : '-'
|
||||
})
|
||||
const buyerTypeLabel = computed(() =>
|
||||
selectedOrder.value?.buyer_type === 'agent' ? '代理商' : selectedOrder.value ? '个人客户' : '-'
|
||||
)
|
||||
const walletRefundFixed = computed(() => selectedOrder.value?.payment_method === 'wallet')
|
||||
|
||||
const resolveRefundMethod = (order: Order): RefundMethod => {
|
||||
if (order.payment_method === 'wallet') {
|
||||
return order.buyer_type === 'agent' ? RefundMethod.AGENT_WALLET : RefundMethod.ASSET_WALLET
|
||||
}
|
||||
if (order.payment_method === 'offline') return RefundMethod.CUSTOMER_ACCOUNT
|
||||
return RefundMethod.ORIGINAL_ROUTE
|
||||
}
|
||||
|
||||
const searchOrders = async (query: string) => {
|
||||
orderSearchLoading.value = true
|
||||
try {
|
||||
@@ -238,6 +278,11 @@
|
||||
orderSearchOptions.value = res.data.items || []
|
||||
if (hasInitialOrder.value && props.initialOrderId) {
|
||||
formData.order_id = props.initialOrderId
|
||||
const initialOrder = orderSearchOptions.value.find((o) => o.id === props.initialOrderId)
|
||||
if (initialOrder) {
|
||||
formData.actual_received_amount = initialOrder.actual_paid_amount ?? 0
|
||||
formData.method = resolveRefundMethod(initialOrder)
|
||||
}
|
||||
}
|
||||
if (hasInitialOrder.value && props.initialOrderNo) {
|
||||
const matchedOrder = orderSearchOptions.value.find(
|
||||
@@ -246,6 +291,7 @@
|
||||
if (matchedOrder) {
|
||||
formData.order_id = matchedOrder.id
|
||||
formData.actual_received_amount = matchedOrder.actual_paid_amount ?? 0
|
||||
formData.method = resolveRefundMethod(matchedOrder)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -264,6 +310,8 @@
|
||||
const selectedOrder = orderSearchOptions.value.find((o) => o.id === orderId)
|
||||
if (selectedOrder) {
|
||||
formData.actual_received_amount = selectedOrder.actual_paid_amount ?? 0
|
||||
formData.method = resolveRefundMethod(selectedOrder)
|
||||
formRef.value?.validateField('method')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ export const AUGUST_PERMISSIONS = {
|
||||
applicationDetail: 'employee_collection:application_detail',
|
||||
applicationUpdate: 'employee_collection:application_update',
|
||||
applicationViewVoucher: 'employee_collection:application_view_voucher',
|
||||
applicationRecoverApproval: 'employee_collection:application_recover_approval',
|
||||
paymentMethodPage: 'employee_collection:payment_method_view',
|
||||
paymentMethodCreate: 'employee_collection:payment_method_create',
|
||||
paymentMethodEdit: 'employee_collection:payment_method_edit',
|
||||
|
||||
@@ -20,17 +20,15 @@ export const RefundStatusNameMap: Record<RefundStatus, string> = {
|
||||
}
|
||||
|
||||
/** 退款状态标签类型映射 */
|
||||
export const RefundStatusTypeMap: Record<
|
||||
RefundStatus,
|
||||
'warning' | 'success' | 'danger' | 'info'
|
||||
> = {
|
||||
[RefundStatus.PENDING]: 'warning',
|
||||
[RefundStatus.APPROVED]: 'success',
|
||||
[RefundStatus.REJECTED]: 'danger',
|
||||
[RefundStatus.RETURNED]: 'info',
|
||||
[RefundStatus.REFUNDING]: 'warning',
|
||||
[RefundStatus.REFUND_FAILED]: 'danger'
|
||||
}
|
||||
export const RefundStatusTypeMap: Record<RefundStatus, 'warning' | 'success' | 'danger' | 'info'> =
|
||||
{
|
||||
[RefundStatus.PENDING]: 'warning',
|
||||
[RefundStatus.APPROVED]: 'success',
|
||||
[RefundStatus.REJECTED]: 'danger',
|
||||
[RefundStatus.RETURNED]: 'info',
|
||||
[RefundStatus.REFUNDING]: 'warning',
|
||||
[RefundStatus.REFUND_FAILED]: 'danger'
|
||||
}
|
||||
|
||||
/** 退款方式名称映射 */
|
||||
export const RefundMethodNameMap: Record<RefundMethod, string> = {
|
||||
@@ -62,7 +60,7 @@ export const RefundFailureReasonNameMap: Record<RefundFailureReason, string> = {
|
||||
/** 退款方式选项(表单用) */
|
||||
export const RefundMethodOptions = [
|
||||
{ label: '原路退回', value: RefundMethod.ORIGINAL_ROUTE },
|
||||
{ label: '客户收款信息', value: RefundMethod.CUSTOMER_ACCOUNT },
|
||||
{ label: '线下退款', value: RefundMethod.CUSTOMER_ACCOUNT },
|
||||
{ label: '资产钱包', value: RefundMethod.ASSET_WALLET },
|
||||
{ label: '代理钱包', value: RefundMethod.AGENT_WALLET }
|
||||
]
|
||||
@@ -86,4 +84,4 @@ export const canResubmitRefund = (item: {
|
||||
export const canManualApproveRefund = (item: {
|
||||
status?: RefundStatus
|
||||
approval_instance_id?: number | null
|
||||
}): boolean => item.status === RefundStatus.PENDING && !item.approval_instance_id
|
||||
}): boolean => item.status === RefundStatus.PENDING && !item.approval_instance_id
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface BusinessUserGroupItem {
|
||||
remark: string // 备注
|
||||
created_at: string // 创建时间
|
||||
updated_at: string // 更新时间
|
||||
member_count?: number
|
||||
}
|
||||
|
||||
/** 业务用户组列表查询参数 */
|
||||
@@ -67,8 +68,49 @@ export interface SetBusinessUserGroupMembersParams {
|
||||
account_ids: number[] // 平台用户账号ID列表,至少一个;每个账号必须是启用平台用户
|
||||
}
|
||||
|
||||
export interface BusinessUserGroupMembersQueryParams extends PaginationParams {
|
||||
keyword?: string
|
||||
account_status?: 'enabled' | 'disabled' | 'deleted'
|
||||
}
|
||||
|
||||
export type BusinessUserGroupMemberScope = 'all' | 'ungrouped' | 'current' | 'other'
|
||||
|
||||
export interface BusinessUserGroupMemberCandidatesQueryParams extends PaginationParams {
|
||||
keyword?: string
|
||||
scope?: BusinessUserGroupMemberScope
|
||||
}
|
||||
|
||||
export interface BusinessUserGroupMember {
|
||||
account_id: number
|
||||
username: string
|
||||
phone?: string | null
|
||||
enabled?: boolean
|
||||
deleted?: boolean
|
||||
status_name?: string | null
|
||||
member_created_at?: string | null
|
||||
current_group_id?: number | null
|
||||
current_group_code?: string | null
|
||||
current_group_name?: string | null
|
||||
operation?: 'add' | 'already_member' | 'move' | string
|
||||
operation_name?: string | null
|
||||
}
|
||||
|
||||
export interface BusinessUserGroupMemberCandidate extends BusinessUserGroupMember {
|
||||
id?: number
|
||||
phone_summary?: string | null
|
||||
action?: 'add' | 'already_member' | 'move' | string
|
||||
can_add?: boolean
|
||||
}
|
||||
|
||||
export interface BusinessUserGroupMembersPageResult {
|
||||
items: BusinessUserGroupMember[] | null
|
||||
page?: number
|
||||
size?: number
|
||||
total?: number
|
||||
}
|
||||
|
||||
/** 批量设置/清空成员结果 */
|
||||
export interface BusinessUserGroupMembersResult {
|
||||
group_id: number // 目标业务用户组ID;清空操作为 0
|
||||
account_ids: number[] | null // 本次成功维护归属的平台用户账号ID列表
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,6 +229,12 @@ export interface EmployeeCollectionApplication {
|
||||
decided_at?: string | null
|
||||
latest_approval_instance_id?: number | null
|
||||
latest_attempt_id?: number | null
|
||||
approval_failure_summary?: string | null
|
||||
approval_submission_name?: string | null
|
||||
approval_submission_status?: number | null
|
||||
last_recovery_at?: string | null
|
||||
recoverable?: boolean
|
||||
recovery_result?: string | null
|
||||
created_at?: string | null
|
||||
updated_at?: string | null
|
||||
}
|
||||
@@ -279,6 +285,17 @@ export interface EmployeeCollectionApplicationSubmitResult {
|
||||
attempt?: EmployeeCollectionApplicationAttempt | null
|
||||
}
|
||||
|
||||
export interface RecoverEmployeeCollectionApplicationResponse {
|
||||
action?: string
|
||||
application_id?: number
|
||||
approval_instance_id?: number | null
|
||||
last_recovery_at?: string | null
|
||||
recoverable?: boolean
|
||||
result?: string
|
||||
submission_status?: number | null
|
||||
submission_status_name?: string | null
|
||||
}
|
||||
|
||||
// ========== 响应类型 ==========
|
||||
|
||||
export type EmployeeCollectionPaymentMethodListResponse = BaseResponse<
|
||||
|
||||
@@ -9,6 +9,17 @@
|
||||
返回
|
||||
</ElButton>
|
||||
<h2 class="detail-title">核销申请详情</h2>
|
||||
<ElButton
|
||||
v-if="
|
||||
application?.recoverable &&
|
||||
hasAuth(AUGUST_PERMISSIONS.employeeCollection.applicationRecoverApproval)
|
||||
"
|
||||
type="warning"
|
||||
:loading="recovering"
|
||||
@click="recoverApproval"
|
||||
>
|
||||
恢复审批
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<DetailPage v-if="application" :sections="detailSections" :data="application" />
|
||||
@@ -112,6 +123,7 @@
|
||||
ElCard,
|
||||
ElIcon,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTimeline,
|
||||
@@ -146,6 +158,7 @@
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
const loading = ref(false)
|
||||
const recovering = ref(false)
|
||||
const detail = ref<EmployeeCollectionApplicationDetailData | null>(null)
|
||||
|
||||
const application = computed<EmployeeCollectionApplication | null>(
|
||||
@@ -186,6 +199,20 @@
|
||||
prop: 'status',
|
||||
formatter: (value, data) => data.status_name || getApplicationStatusLabel(value)
|
||||
},
|
||||
{
|
||||
label: '审批提交状态',
|
||||
formatter: (_, data) => data.approval_submission_name || '-'
|
||||
},
|
||||
{
|
||||
label: '提交失败摘要',
|
||||
formatter: (_, data) => data.approval_failure_summary || '-',
|
||||
fullWidth: true
|
||||
},
|
||||
{
|
||||
label: '最近恢复时间',
|
||||
formatter: (_, data) =>
|
||||
data.last_recovery_at ? formatDateTime(data.last_recovery_at) : '-'
|
||||
},
|
||||
{
|
||||
label: '审批实例ID',
|
||||
prop: 'latest_approval_instance_id',
|
||||
@@ -241,6 +268,38 @@
|
||||
router.push({ path: RoutesAlias.EmployeeCollectionApplications })
|
||||
}
|
||||
|
||||
const recoverApproval = async () => {
|
||||
if (
|
||||
!application.value?.recoverable ||
|
||||
!hasAuth(AUGUST_PERMISSIONS.employeeCollection.applicationRecoverApproval) ||
|
||||
recovering.value
|
||||
) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要恢复该核销申请的原审批提交吗?', '恢复审批', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消'
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
recovering.value = true
|
||||
try {
|
||||
const res = await EmployeeCollectionService.recoverApplicationApproval(application.value.id)
|
||||
ElMessage[res.code === 0 ? 'success' : 'error'](
|
||||
res.msg || (res.code === 0 ? '恢复审批成功' : '恢复审批失败')
|
||||
)
|
||||
await loadDetail()
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, '恢复审批失败'))
|
||||
await loadDetail()
|
||||
} finally {
|
||||
recovering.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => void loadDetail())
|
||||
</script>
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
<script setup lang="ts">
|
||||
import { h, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElButton, ElMessage, ElTag } from 'element-plus'
|
||||
import { ElButton, ElMessage, ElMessageBox, ElTag } from 'element-plus'
|
||||
import { EmployeeCollectionService } from '@/api/modules'
|
||||
import type {
|
||||
EmployeeCollectionApplication,
|
||||
@@ -141,6 +141,8 @@
|
||||
{ label: '收款方式', prop: 'payment_method_name' },
|
||||
{ label: '付款金额', prop: 'paid_amount' },
|
||||
{ label: '状态', prop: 'status' },
|
||||
{ label: '提交状态', prop: 'approval_submission_name' },
|
||||
{ label: '可恢复', prop: 'recoverable' },
|
||||
{ label: '提交时间', prop: 'created_at' }
|
||||
]
|
||||
|
||||
@@ -184,6 +186,21 @@
|
||||
() => row.status_name || getApplicationStatusLabel(row.status)
|
||||
)
|
||||
},
|
||||
{
|
||||
prop: 'approval_submission_name',
|
||||
label: '提交状态',
|
||||
width: 130,
|
||||
formatter: (row: EmployeeCollectionApplication) => row.approval_submission_name || '-'
|
||||
},
|
||||
{
|
||||
prop: 'recoverable',
|
||||
label: '可恢复',
|
||||
width: 90,
|
||||
formatter: (row: EmployeeCollectionApplication) =>
|
||||
h(ElTag, { type: row.recoverable ? 'warning' : 'info' }, () =>
|
||||
row.recoverable ? '是' : '否'
|
||||
)
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '提交时间',
|
||||
@@ -271,6 +288,30 @@
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleRecoverApproval = async (row: EmployeeCollectionApplication) => {
|
||||
if (
|
||||
!row.recoverable ||
|
||||
!hasAuth(AUGUST_PERMISSIONS.employeeCollection.applicationRecoverApproval)
|
||||
) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要恢复该核销申请的原审批提交吗?', '恢复审批', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消'
|
||||
})
|
||||
const res = await EmployeeCollectionService.recoverApplicationApproval(row.id)
|
||||
if (res.code === 0) ElMessage.success(res.msg || '恢复审批成功')
|
||||
else ElMessage.error(res.msg || '恢复审批失败')
|
||||
await getTableData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel' && error !== 'close') {
|
||||
ElMessage.error(getErrorMessage(error, '恢复审批失败'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getActions = (row: EmployeeCollectionApplication) => {
|
||||
const actions: any[] = []
|
||||
if (
|
||||
@@ -279,6 +320,16 @@
|
||||
) {
|
||||
actions.push({ label: '修改并重新提交', handler: () => handleResubmit(row), type: 'primary' })
|
||||
}
|
||||
if (
|
||||
row.recoverable &&
|
||||
hasAuth(AUGUST_PERMISSIONS.employeeCollection.applicationRecoverApproval)
|
||||
) {
|
||||
actions.push({
|
||||
label: '恢复审批',
|
||||
handler: () => handleRecoverApproval(row),
|
||||
type: 'warning'
|
||||
})
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
show-expand
|
||||
label-width="100px"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
@@ -64,7 +64,13 @@
|
||||
placeholder="1-64 字符,创建后不可修改"
|
||||
maxlength="64"
|
||||
show-word-limit
|
||||
/>
|
||||
>
|
||||
<template #append>
|
||||
<ElButton v-if="dialogType === 'add'" @click="handleGenerateGroupCode">
|
||||
生成编码
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="名称" prop="name">
|
||||
<ElInput v-model="form.name" placeholder="请输入名称" maxlength="100" show-word-limit />
|
||||
@@ -103,105 +109,106 @@
|
||||
</ElDialog>
|
||||
|
||||
<!-- 成员管理弹窗 -->
|
||||
<ElDialog
|
||||
<ElDrawer
|
||||
v-model="memberDialogVisible"
|
||||
:title="`成员管理 - ${currentGroup?.name || ''}`"
|
||||
width="680px"
|
||||
:title="`成员管理 - ${currentGroup?.name || ''} - ${currentGroup?.enabled ? '启用' : '已停用'}`"
|
||||
direction="rtl"
|
||||
size="65%"
|
||||
destroy-on-close
|
||||
>
|
||||
<div v-if="currentGroup" class="member-group-info">
|
||||
<ElTag :type="currentGroup.enabled ? 'success' : 'danger'" size="small">
|
||||
{{ currentGroup.enabled ? '启用' : '已停用' }}
|
||||
</ElTag>
|
||||
<span class="member-group-code">{{ currentGroup.code }}</span>
|
||||
<span v-if="!currentGroup.enabled" class="member-group-tip">
|
||||
停用组保留现有成员,不能作为批量目标
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ElDivider content-position="left">设置成员(整体替换)</ElDivider>
|
||||
<ElAlert type="warning" :closable="false" show-icon class="member-alert">
|
||||
保存后将以所选账号整体替换该组现有成员;账号必须是启用平台用户。
|
||||
</ElAlert>
|
||||
<ElSelect
|
||||
v-model="memberAccountIds"
|
||||
multiple
|
||||
filterable
|
||||
remote
|
||||
:remote-method="searchMemberCandidates"
|
||||
:loading="memberCandidatesLoading"
|
||||
placeholder="搜索并选择平台账号"
|
||||
style="width: 100%; margin-top: 12px"
|
||||
>
|
||||
<ElOption
|
||||
v-for="candidate in memberCandidates"
|
||||
:key="candidate.id"
|
||||
:label="candidate.username"
|
||||
:value="candidate.id"
|
||||
>
|
||||
<div class="member-option">
|
||||
<span>{{ candidate.username }}</span>
|
||||
<span class="member-option-phone">{{ candidate.phone_summary }}</span>
|
||||
<div class="member-transfer-layout">
|
||||
<div class="member-panel">
|
||||
<div class="member-panel-header">
|
||||
<div class="member-panel-title">候选账号</div>
|
||||
<ElButton v-if="candidateHasMore" link type="primary" @click="loadMoreCandidates">
|
||||
查看更多
|
||||
</ElButton>
|
||||
</div>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
<div class="member-actions">
|
||||
<ElButton type="primary" :loading="settingMembers" @click="handleSetMembers">
|
||||
保存成员
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<ElDivider content-position="left">移出成员(回未分组)</ElDivider>
|
||||
<ElAlert type="info" :closable="false" show-icon class="member-alert">
|
||||
选择要移出用户组的平台账号,清空后回未分组。
|
||||
</ElAlert>
|
||||
<ElSelect
|
||||
v-model="clearAccountIds"
|
||||
multiple
|
||||
filterable
|
||||
remote
|
||||
:remote-method="searchMemberCandidates"
|
||||
:loading="memberCandidatesLoading"
|
||||
placeholder="搜索并选择要移出的平台账号"
|
||||
style="width: 100%; margin-top: 12px"
|
||||
>
|
||||
<ElOption
|
||||
v-for="candidate in memberCandidates"
|
||||
:key="candidate.id"
|
||||
:label="candidate.username"
|
||||
:value="candidate.id"
|
||||
>
|
||||
<div class="member-option">
|
||||
<span>{{ candidate.username }}</span>
|
||||
<span class="member-option-phone">{{ candidate.phone_summary }}</span>
|
||||
<ElInput
|
||||
v-model="candidateKeyword"
|
||||
placeholder="搜索用户名或手机号"
|
||||
clearable
|
||||
@input="searchMemberCandidates"
|
||||
/>
|
||||
<ElTable
|
||||
:data="memberCandidates"
|
||||
border
|
||||
stripe
|
||||
size="small"
|
||||
height="300"
|
||||
v-loading="memberCandidatesLoading"
|
||||
@selection-change="candidateSelection = $event"
|
||||
>
|
||||
<ElTableColumn v-if="memberCandidates.length" type="selection" width="42" />
|
||||
<ElTableColumn prop="username" label="用户名" min-width="120" />
|
||||
<ElTableColumn prop="phone" label="手机号" min-width="110" />
|
||||
<ElTableColumn label="操作提示" min-width="130" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<ElTag size="small" type="info">
|
||||
{{ getMemberOperationLabel(scope.row.action || scope.row.operation) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
<ElButton type="primary" :loading="settingMembers" @click="handleSetMembers">
|
||||
添加/迁移所选账号
|
||||
</ElButton>
|
||||
</div>
|
||||
<div class="member-panel">
|
||||
<div class="member-panel-header">
|
||||
<div class="member-panel-title">当前成员</div>
|
||||
<ElButton
|
||||
v-if="currentMembersHasMore"
|
||||
link
|
||||
type="primary"
|
||||
@click="loadMoreCurrentMembers"
|
||||
>
|
||||
查看更多
|
||||
</ElButton>
|
||||
</div>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
<div class="member-actions">
|
||||
<ElButton type="danger" :loading="clearingMembers" @click="handleClearMembers">
|
||||
移出所选账号
|
||||
</ElButton>
|
||||
<ElInput v-model="currentMemberKeyword" placeholder="搜索当前成员" clearable />
|
||||
<ElTable
|
||||
:data="filteredCurrentMembers"
|
||||
border
|
||||
stripe
|
||||
size="small"
|
||||
height="300"
|
||||
v-loading="currentMembersLoading"
|
||||
empty-text="当前业务组暂无成员"
|
||||
@selection-change="clearSelection = $event"
|
||||
>
|
||||
<ElTableColumn v-if="filteredCurrentMembers.length" type="selection" width="42" />
|
||||
<ElTableColumn prop="username" label="用户名" min-width="120" />
|
||||
<ElTableColumn prop="phone" label="手机号" min-width="110" />
|
||||
<ElTableColumn prop="status_name" label="状态" width="90" />
|
||||
</ElTable>
|
||||
<ElButton type="danger" :loading="clearingMembers" @click="handleClearMembers">
|
||||
移出所选成员
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</ElDialog>
|
||||
</ElDrawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { BusinessUserGroupService, ShopService } from '@/api/modules'
|
||||
import { BusinessUserGroupService } from '@/api/modules'
|
||||
import type {
|
||||
BusinessUserGroupBusinessLine,
|
||||
BusinessUserGroupItem,
|
||||
BusinessUserGroupMember,
|
||||
CreateBusinessUserGroupParams,
|
||||
UpdateBusinessUserGroupParams,
|
||||
ShopBusinessOwnerCandidate
|
||||
BusinessUserGroupMemberCandidate
|
||||
} from '@/types/api'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { generateShopCode } from '@/utils/codeGenerator'
|
||||
import {
|
||||
BUSINESS_USER_GROUP_BUSINESS_LINE_MAP,
|
||||
BUSINESS_USER_GROUP_BUSINESS_LINE_OPTIONS,
|
||||
@@ -436,6 +443,12 @@
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleGenerateGroupCode = () => {
|
||||
form.code = generateShopCode()
|
||||
formRef.value?.clearValidate('code')
|
||||
ElMessage.success('编码生成成功')
|
||||
}
|
||||
|
||||
const openEditDialog = (row: BusinessUserGroupItem) => {
|
||||
dialogType.value = 'edit'
|
||||
currentGroup.value = row
|
||||
@@ -512,22 +525,34 @@
|
||||
|
||||
// ==================== 成员管理 ====================
|
||||
const memberDialogVisible = ref(false)
|
||||
const memberCandidates = ref<ShopBusinessOwnerCandidate[]>([])
|
||||
const memberCandidates = ref<BusinessUserGroupMemberCandidate[]>([])
|
||||
const currentMembers = ref<BusinessUserGroupMember[]>([])
|
||||
const currentMembersLoading = ref(false)
|
||||
const memberCandidatesLoading = ref(false)
|
||||
const memberAccountIds = ref<number[]>([])
|
||||
const clearAccountIds = ref<number[]>([])
|
||||
const memberCandidatesPage = ref(1)
|
||||
const memberCandidatesTotal = ref(0)
|
||||
const candidateKeyword = ref('')
|
||||
const currentMembersPage = ref(1)
|
||||
const currentMembersTotal = ref(0)
|
||||
const currentMemberKeyword = ref('')
|
||||
const candidateSelection = ref<BusinessUserGroupMemberCandidate[]>([])
|
||||
const clearSelection = ref<BusinessUserGroupMember[]>([])
|
||||
const settingMembers = ref(false)
|
||||
const clearingMembers = ref(false)
|
||||
|
||||
const searchMemberCandidates = async (keyword?: string) => {
|
||||
const searchMemberCandidates = async (keyword = '') => {
|
||||
if (!currentGroup.value) return
|
||||
memberCandidatesPage.value = 1
|
||||
memberCandidatesLoading.value = true
|
||||
try {
|
||||
const res = await ShopService.getBusinessOwnerCandidates({
|
||||
page: 1,
|
||||
page_size: 50,
|
||||
keyword: keyword?.trim() || undefined
|
||||
const res = await BusinessUserGroupService.getMemberCandidates(currentGroup.value.id, {
|
||||
page: memberCandidatesPage.value,
|
||||
page_size: 20,
|
||||
keyword: keyword.trim() || undefined,
|
||||
scope: 'all'
|
||||
})
|
||||
memberCandidates.value = res.data.items || []
|
||||
memberCandidatesTotal.value = res.data.total || 0
|
||||
} catch {
|
||||
memberCandidates.value = []
|
||||
} finally {
|
||||
@@ -535,12 +560,88 @@
|
||||
}
|
||||
}
|
||||
|
||||
const candidateHasMore = computed(
|
||||
() => memberCandidates.value.length < memberCandidatesTotal.value
|
||||
)
|
||||
|
||||
const loadMoreCandidates = async () => {
|
||||
if (!currentGroup.value || memberCandidatesLoading.value) return
|
||||
memberCandidatesPage.value += 1
|
||||
memberCandidatesLoading.value = true
|
||||
try {
|
||||
const res = await BusinessUserGroupService.getMemberCandidates(currentGroup.value.id, {
|
||||
page: memberCandidatesPage.value,
|
||||
page_size: 20,
|
||||
scope: 'all'
|
||||
})
|
||||
if (res.code === 0) memberCandidates.value.push(...(res.data?.items || []))
|
||||
} finally {
|
||||
memberCandidatesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getMemberOperationLabel = (operation?: string | null) => {
|
||||
if (operation === 'add') return '可新增'
|
||||
if (operation === 'already_member') return '已是当前成员'
|
||||
if (operation === 'move') return '可迁移'
|
||||
return operation || '-'
|
||||
}
|
||||
|
||||
const loadCurrentMembers = async (groupId: number) => {
|
||||
currentMembersLoading.value = true
|
||||
try {
|
||||
const res = await BusinessUserGroupService.getGroupMembers(groupId, {
|
||||
page: 1,
|
||||
page_size: 20
|
||||
})
|
||||
if (res.code === 0) {
|
||||
currentMembers.value = res.data?.items || []
|
||||
currentMembersPage.value = 1
|
||||
currentMembersTotal.value = res.data?.total || 0
|
||||
}
|
||||
} finally {
|
||||
currentMembersLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const currentMembersHasMore = computed(
|
||||
() => currentMembers.value.length < currentMembersTotal.value
|
||||
)
|
||||
|
||||
const filteredCurrentMembers = computed(() => {
|
||||
const keyword = currentMemberKeyword.value.trim().toLowerCase()
|
||||
if (!keyword) return currentMembers.value
|
||||
return currentMembers.value.filter((member) =>
|
||||
[member.username, member.phone]
|
||||
.filter(Boolean)
|
||||
.some((value) => String(value).toLowerCase().includes(keyword))
|
||||
)
|
||||
})
|
||||
|
||||
const loadMoreCurrentMembers = async () => {
|
||||
if (!currentGroup.value || currentMembersLoading.value) return
|
||||
currentMembersPage.value += 1
|
||||
currentMembersLoading.value = true
|
||||
try {
|
||||
const res = await BusinessUserGroupService.getGroupMembers(currentGroup.value.id, {
|
||||
page: currentMembersPage.value,
|
||||
page_size: 20
|
||||
})
|
||||
if (res.code === 0) currentMembers.value.push(...(res.data?.items || []))
|
||||
} finally {
|
||||
currentMembersLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openMemberDialog = (row: BusinessUserGroupItem) => {
|
||||
currentGroup.value = row
|
||||
memberAccountIds.value = []
|
||||
clearAccountIds.value = []
|
||||
candidateSelection.value = []
|
||||
clearSelection.value = []
|
||||
currentMemberKeyword.value = ''
|
||||
memberCandidates.value = []
|
||||
candidateKeyword.value = ''
|
||||
memberDialogVisible.value = true
|
||||
void loadCurrentMembers(row.id)
|
||||
setTimeout(() => {
|
||||
searchMemberCandidates()
|
||||
}, 0)
|
||||
@@ -548,14 +649,15 @@
|
||||
|
||||
const handleSetMembers = async () => {
|
||||
if (!currentGroup.value) return
|
||||
if (!memberAccountIds.value.length) {
|
||||
const accountIds = candidateSelection.value.map((member) => member.account_id)
|
||||
if (!accountIds.length) {
|
||||
ElMessage.warning('请至少选择一个平台账号')
|
||||
return
|
||||
}
|
||||
settingMembers.value = true
|
||||
try {
|
||||
const res = await BusinessUserGroupService.setGroupMembers(currentGroup.value.id, {
|
||||
account_ids: memberAccountIds.value
|
||||
const res = await BusinessUserGroupService.addGroupMembers(currentGroup.value.id, {
|
||||
account_ids: accountIds
|
||||
})
|
||||
ElMessage.success(`已维护 ${res.data?.account_ids?.length ?? 0} 个账号归属`)
|
||||
memberDialogVisible.value = false
|
||||
@@ -566,14 +668,15 @@
|
||||
}
|
||||
|
||||
const handleClearMembers = async () => {
|
||||
if (!clearAccountIds.value.length) {
|
||||
const accountIds = clearSelection.value.map((member) => member.account_id)
|
||||
if (!accountIds.length) {
|
||||
ElMessage.warning('请至少选择一个平台账号')
|
||||
return
|
||||
}
|
||||
clearingMembers.value = true
|
||||
try {
|
||||
const res = await BusinessUserGroupService.clearGroupMembers({
|
||||
account_ids: clearAccountIds.value
|
||||
const res = await BusinessUserGroupService.removeGroupMembers(currentGroup.value?.id || 0, {
|
||||
account_ids: accountIds
|
||||
})
|
||||
ElMessage.success(`已清空 ${res.data?.account_ids?.length ?? 0} 个账号归属`)
|
||||
memberDialogVisible.value = false
|
||||
@@ -604,6 +707,12 @@
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.member-layout-tip {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.member-alert {
|
||||
@@ -616,6 +725,28 @@
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.member-transfer-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.member-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.member-panel :deep(.el-table) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.member-panel-title {
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.member-option {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
Reference in New Issue
Block a user