This commit is contained in:
@@ -149,6 +149,7 @@
|
|||||||
| 申请详情 | `/finance/employee-collection/applications/detail/:id` | `applicationDetail` | `employee_collection:application_detail` |
|
| 申请详情 | `/finance/employee-collection/applications/detail/:id` | `applicationDetail` | `employee_collection:application_detail` |
|
||||||
| 修改并重新提交 | — | `applicationUpdate` | `employee_collection:application_update` |
|
| 修改并重新提交 | — | `applicationUpdate` | `employee_collection:application_update` |
|
||||||
| 查看凭证 | — | `applicationViewVoucher` | `employee_collection:application_view_voucher` |
|
| 查看凭证 | — | `applicationViewVoucher` | `employee_collection:application_view_voucher` |
|
||||||
|
| 恢复审批 | — | `applicationRecoverApproval` | `employee_collection:application_recover_approval` |
|
||||||
|
|
||||||
## 财务管理:收款方式管理
|
## 财务管理:收款方式管理
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Change: 对齐业务用户组成员增量管理接口
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
《产品迭代8月份/修改.md》已将业务用户组成员维护从“整体替换”调整为增量增加/迁移和按组移除。当前前端仍调用旧 PUT/全局 DELETE 接口,无法展示候选成员归属和操作提示。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 对齐成员查询、成员候选、增量增加/迁移、按组移除接口。
|
||||||
|
- 成员管理弹窗展示当前成员及成员关系信息。
|
||||||
|
- 候选账号展示 `add`、`already_member`、`move` 操作提示。
|
||||||
|
- 停用组允许查询和移除,但新增/迁移由后端拒绝并提示。
|
||||||
|
- 用户组列表/详情使用实时成员数。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Affected specs: `business-user-group-management`
|
||||||
|
- Affected code: business user group types/API/page
|
||||||
|
- Breaking API alignment: 移除前端对旧整体替换 PUT 与全局清空 DELETE 的调用。
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Incremental Business User Group Membership
|
||||||
|
|
||||||
|
The frontend SHALL use the member query, candidate query, incremental POST add/move, and scoped DELETE remove contracts from the modification document.
|
||||||
|
|
||||||
|
#### Scenario: Add or move members
|
||||||
|
|
||||||
|
- **WHEN** the user selects candidate accounts and confirms adding members
|
||||||
|
- **THEN** the frontend MUST POST the account IDs to `/api/admin/business-user-groups/{id}/members`
|
||||||
|
- **AND** MUST refresh current members and member counts
|
||||||
|
|
||||||
|
#### Scenario: Remove current members
|
||||||
|
|
||||||
|
- **WHEN** the user selects members currently belonging to the group and confirms removal
|
||||||
|
- **THEN** the frontend MUST DELETE `/api/admin/business-user-groups/{id}/members` with the account IDs
|
||||||
|
- **AND** MUST refresh current members
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
## 1. Contract
|
||||||
|
|
||||||
|
- [ ] 1.1 更新成员、候选和成员数类型。
|
||||||
|
- [ ] 1.2 更新 `BusinessUserGroupService` 方法与 HTTP 方法/路径。
|
||||||
|
|
||||||
|
## 2. UI
|
||||||
|
|
||||||
|
- [ ] 2.1 展示当前成员列表和成员状态。
|
||||||
|
- [ ] 2.2 使用候选接口展示归属筛选和操作提示。
|
||||||
|
- [ ] 2.3 将“整体替换”改为增量添加/迁移,将移除限定为当前组成员。
|
||||||
|
|
||||||
|
## 3. Verification
|
||||||
|
|
||||||
|
- [ ] 3.1 运行类型检查、lint、编码检查、构建和 OpenSpec 校验。
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
核销申请的审批提交状态与业务状态独立维护。恢复操作只能复用最新审批尝试和原审批实例,不能创建新的申请或审批尝试。
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
- 资格以响应中的 `recoverable` 为准,前端不根据状态字段自行推导。
|
||||||
|
- 列表提供快速入口,详情提供主要操作入口;两处共用同一恢复权限编码。
|
||||||
|
- 恢复接口只传申请 ID,结果由后端返回并触发重新加载。
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Change: 补齐核销申请恢复审批
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
《产品迭代8月份/修改.md》新增核销申请恢复原审批接口和恢复资格字段。当前前端类型、列表和详情尚未提供恢复入口,提交失败或结果未知的核销申请无法由运营人员处理。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 扩展核销申请响应类型,支持 `recoverable`、提交状态、失败摘要和最近恢复时间。
|
||||||
|
- 新增 `EmployeeCollectionService.recoverApplicationApproval(id)`。
|
||||||
|
- 核销申请列表和详情在 `recoverable = true` 且具备权限时显示“恢复审批”。
|
||||||
|
- 恢复成功或冲突后刷新详情/列表,并展示后端错误消息。
|
||||||
|
- 新增权限 `employee_collection:application_recover_approval` 并写入权限文档。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Affected specs: `employee-collection`
|
||||||
|
- Affected code:
|
||||||
|
- `src/types/api/employeeCollection.ts`
|
||||||
|
- `src/api/modules/employeeCollection.ts`
|
||||||
|
- `src/config/constants/augustIteration.ts`
|
||||||
|
- `src/views/finance/employee-collection/applications/index.vue`
|
||||||
|
- `src/views/finance/employee-collection/applications/detail.vue`
|
||||||
|
- `docs/权限.md`
|
||||||
|
- Out of scope: 后端接口、数据库和企业微信审批引擎实现。
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Recover Employee Collection Approval
|
||||||
|
|
||||||
|
The frontend SHALL expose recovery for an employee collection application only when the backend returns `recoverable = true` and the user has the recovery permission.
|
||||||
|
|
||||||
|
#### Scenario: Recover approval from application detail
|
||||||
|
|
||||||
|
- **GIVEN** a核销申请返回 `recoverable = true`
|
||||||
|
- **WHEN** the user confirms “恢复审批”
|
||||||
|
- **THEN** the frontend MUST call `POST /api/admin/employee-collection-applications/{id}/recover-approval`
|
||||||
|
- **AND** MUST reload the application data after success or conflict
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
## 1. Implementation
|
||||||
|
|
||||||
|
- [x] 1.1 扩展核销申请类型与恢复接口响应。
|
||||||
|
- [x] 1.2 增加恢复审批 API service 和唯一权限编码。
|
||||||
|
- [x] 1.3 列表增加提交状态字段和恢复审批操作。
|
||||||
|
- [x] 1.4 详情增加提交状态字段和恢复审批操作。
|
||||||
|
- [x] 1.5 更新 `docs/权限.md`。
|
||||||
|
|
||||||
|
## 2. Verification
|
||||||
|
|
||||||
|
- [x] 2.1 运行类型检查、lint、编码检查和构建。
|
||||||
|
- [x] 2.2 运行 OpenSpec 严格校验。
|
||||||
@@ -6,6 +6,9 @@ import { BaseService } from '../BaseService'
|
|||||||
import type {
|
import type {
|
||||||
BusinessUserGroupItem,
|
BusinessUserGroupItem,
|
||||||
BusinessUserGroupMembersResult,
|
BusinessUserGroupMembersResult,
|
||||||
|
BusinessUserGroupMembersPageResult,
|
||||||
|
BusinessUserGroupMembersQueryParams,
|
||||||
|
BusinessUserGroupMemberCandidatesQueryParams,
|
||||||
BusinessUserGroupQueryParams,
|
BusinessUserGroupQueryParams,
|
||||||
CreateBusinessUserGroupParams,
|
CreateBusinessUserGroupParams,
|
||||||
DeleteBusinessUserGroupParams,
|
DeleteBusinessUserGroupParams,
|
||||||
@@ -67,25 +70,47 @@ export class BusinessUserGroupService extends BaseService {
|
|||||||
* 批量设置平台用户业务用户组归属(整体替换)
|
* 批量设置平台用户业务用户组归属(整体替换)
|
||||||
* PUT /api/admin/business-user-groups/{id}/members
|
* PUT /api/admin/business-user-groups/{id}/members
|
||||||
*/
|
*/
|
||||||
static setGroupMembers(
|
static addGroupMembers(
|
||||||
id: number,
|
id: number,
|
||||||
data: SetBusinessUserGroupMembersParams
|
data: SetBusinessUserGroupMembersParams
|
||||||
): Promise<BaseResponse<BusinessUserGroupMembersResult>> {
|
): Promise<BaseResponse<BusinessUserGroupMembersResult>> {
|
||||||
return this.put<BaseResponse<BusinessUserGroupMembersResult>>(
|
return this.post<BaseResponse<BusinessUserGroupMembersResult>>(
|
||||||
`/api/admin/business-user-groups/${id}/members`,
|
`/api/admin/business-user-groups/${id}/members`,
|
||||||
data
|
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 带 body)
|
||||||
* DELETE /api/admin/business-user-groups/members
|
* DELETE /api/admin/business-user-groups/members
|
||||||
*/
|
*/
|
||||||
static clearGroupMembers(
|
static removeGroupMembers(
|
||||||
|
id: number,
|
||||||
data: SetBusinessUserGroupMembersParams
|
data: SetBusinessUserGroupMembersParams
|
||||||
): Promise<BaseResponse<BusinessUserGroupMembersResult>> {
|
): Promise<BaseResponse<BusinessUserGroupMembersResult>> {
|
||||||
return this.delete<BaseResponse<BusinessUserGroupMembersResult>>(
|
return this.delete<BaseResponse<BusinessUserGroupMembersResult>>(
|
||||||
'/api/admin/business-user-groups/members',
|
`/api/admin/business-user-groups/${id}/members`,
|
||||||
undefined,
|
undefined,
|
||||||
{ data }
|
{ data }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ import type {
|
|||||||
EmployeeCollectionPaymentMethodListResponse,
|
EmployeeCollectionPaymentMethodListResponse,
|
||||||
EmployeeCollectionPaymentMethodQueryParams,
|
EmployeeCollectionPaymentMethodQueryParams,
|
||||||
EmployeeCollectionPaymentMethodResponse,
|
EmployeeCollectionPaymentMethodResponse,
|
||||||
UpdateEmployeeCollectionPaymentMethodRequest
|
UpdateEmployeeCollectionPaymentMethodRequest,
|
||||||
|
BaseResponse,
|
||||||
|
RecoverEmployeeCollectionApplicationResponse
|
||||||
} from '@/types/api'
|
} from '@/types/api'
|
||||||
|
|
||||||
const PAYMENT_METHODS_BASE_URL = '/api/admin/employee-collection-payment-methods'
|
const PAYMENT_METHODS_BASE_URL = '/api/admin/employee-collection-payment-methods'
|
||||||
@@ -146,4 +148,13 @@ export class EmployeeCollectionService extends BaseService {
|
|||||||
data
|
data
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 恢复核销申请原审批提交 */
|
||||||
|
static recoverApplicationApproval(
|
||||||
|
id: number
|
||||||
|
): Promise<BaseResponse<RecoverEmployeeCollectionApplicationResponse>> {
|
||||||
|
return this.post<BaseResponse<RecoverEmployeeCollectionApplicationResponse>>(
|
||||||
|
`${APPLICATIONS_BASE_URL}/${id}/recover-approval`
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,20 @@
|
|||||||
</ElSelect>
|
</ElSelect>
|
||||||
<ElInput v-else :model-value="props.initialOrderNo" disabled style="width: 100%" />
|
<ElInput v-else :model-value="props.initialOrderNo" disabled style="width: 100%" />
|
||||||
</ElFormItem>
|
</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">
|
<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">
|
<ElRadio v-for="opt in RefundMethodOptions" :key="opt.value" :label="opt.value">
|
||||||
{{ opt.label }}
|
{{ opt.label }}
|
||||||
</ElRadio>
|
</ElRadio>
|
||||||
@@ -84,7 +96,7 @@
|
|||||||
@change="formRef?.validateField('refund_voucher_key')"
|
@change="formRef?.validateField('refund_voucher_key')"
|
||||||
/>
|
/>
|
||||||
<div v-if="formData.method === RefundMethod.CUSTOMER_ACCOUNT" class="voucher-tip">
|
<div v-if="formData.method === RefundMethod.CUSTOMER_ACCOUNT" class="voucher-tip">
|
||||||
客户收款信息方式下须同时提供客户收款信息与至少 1 个退款凭证
|
线下退款方式下须同时提供收款信息与至少 1 个退款凭证
|
||||||
</div>
|
</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
@@ -112,7 +124,6 @@
|
|||||||
import { RefundMethod, type CreateRefundRequest, type Order } from '@/types/api'
|
import { RefundMethod, type CreateRefundRequest, type Order } from '@/types/api'
|
||||||
import { RefundMethodOptions } from '@/config/constants/refund'
|
import { RefundMethodOptions } from '@/config/constants/refund'
|
||||||
import { formatMoney, yuanToFen } from '@/utils/business/format'
|
import { formatMoney, yuanToFen } from '@/utils/business/format'
|
||||||
import { getErrorMessage } from '@/utils/business'
|
|
||||||
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -222,6 +233,35 @@
|
|||||||
return formatMoney(selectedOrder?.actual_paid_amount ?? 0, false)
|
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) => {
|
const searchOrders = async (query: string) => {
|
||||||
orderSearchLoading.value = true
|
orderSearchLoading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -238,6 +278,11 @@
|
|||||||
orderSearchOptions.value = res.data.items || []
|
orderSearchOptions.value = res.data.items || []
|
||||||
if (hasInitialOrder.value && props.initialOrderId) {
|
if (hasInitialOrder.value && props.initialOrderId) {
|
||||||
formData.order_id = 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) {
|
if (hasInitialOrder.value && props.initialOrderNo) {
|
||||||
const matchedOrder = orderSearchOptions.value.find(
|
const matchedOrder = orderSearchOptions.value.find(
|
||||||
@@ -246,6 +291,7 @@
|
|||||||
if (matchedOrder) {
|
if (matchedOrder) {
|
||||||
formData.order_id = matchedOrder.id
|
formData.order_id = matchedOrder.id
|
||||||
formData.actual_received_amount = matchedOrder.actual_paid_amount ?? 0
|
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)
|
const selectedOrder = orderSearchOptions.value.find((o) => o.id === orderId)
|
||||||
if (selectedOrder) {
|
if (selectedOrder) {
|
||||||
formData.actual_received_amount = selectedOrder.actual_paid_amount ?? 0
|
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',
|
applicationDetail: 'employee_collection:application_detail',
|
||||||
applicationUpdate: 'employee_collection:application_update',
|
applicationUpdate: 'employee_collection:application_update',
|
||||||
applicationViewVoucher: 'employee_collection:application_view_voucher',
|
applicationViewVoucher: 'employee_collection:application_view_voucher',
|
||||||
|
applicationRecoverApproval: 'employee_collection:application_recover_approval',
|
||||||
paymentMethodPage: 'employee_collection:payment_method_view',
|
paymentMethodPage: 'employee_collection:payment_method_view',
|
||||||
paymentMethodCreate: 'employee_collection:payment_method_create',
|
paymentMethodCreate: 'employee_collection:payment_method_create',
|
||||||
paymentMethodEdit: 'employee_collection:payment_method_edit',
|
paymentMethodEdit: 'employee_collection:payment_method_edit',
|
||||||
|
|||||||
@@ -20,17 +20,15 @@ export const RefundStatusNameMap: Record<RefundStatus, string> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 退款状态标签类型映射 */
|
/** 退款状态标签类型映射 */
|
||||||
export const RefundStatusTypeMap: Record<
|
export const RefundStatusTypeMap: Record<RefundStatus, 'warning' | 'success' | 'danger' | 'info'> =
|
||||||
RefundStatus,
|
{
|
||||||
'warning' | 'success' | 'danger' | 'info'
|
[RefundStatus.PENDING]: 'warning',
|
||||||
> = {
|
[RefundStatus.APPROVED]: 'success',
|
||||||
[RefundStatus.PENDING]: 'warning',
|
[RefundStatus.REJECTED]: 'danger',
|
||||||
[RefundStatus.APPROVED]: 'success',
|
[RefundStatus.RETURNED]: 'info',
|
||||||
[RefundStatus.REJECTED]: 'danger',
|
[RefundStatus.REFUNDING]: 'warning',
|
||||||
[RefundStatus.RETURNED]: 'info',
|
[RefundStatus.REFUND_FAILED]: 'danger'
|
||||||
[RefundStatus.REFUNDING]: 'warning',
|
}
|
||||||
[RefundStatus.REFUND_FAILED]: 'danger'
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 退款方式名称映射 */
|
/** 退款方式名称映射 */
|
||||||
export const RefundMethodNameMap: Record<RefundMethod, string> = {
|
export const RefundMethodNameMap: Record<RefundMethod, string> = {
|
||||||
@@ -62,7 +60,7 @@ export const RefundFailureReasonNameMap: Record<RefundFailureReason, string> = {
|
|||||||
/** 退款方式选项(表单用) */
|
/** 退款方式选项(表单用) */
|
||||||
export const RefundMethodOptions = [
|
export const RefundMethodOptions = [
|
||||||
{ label: '原路退回', value: RefundMethod.ORIGINAL_ROUTE },
|
{ label: '原路退回', value: RefundMethod.ORIGINAL_ROUTE },
|
||||||
{ label: '客户收款信息', value: RefundMethod.CUSTOMER_ACCOUNT },
|
{ label: '线下退款', value: RefundMethod.CUSTOMER_ACCOUNT },
|
||||||
{ label: '资产钱包', value: RefundMethod.ASSET_WALLET },
|
{ label: '资产钱包', value: RefundMethod.ASSET_WALLET },
|
||||||
{ label: '代理钱包', value: RefundMethod.AGENT_WALLET }
|
{ label: '代理钱包', value: RefundMethod.AGENT_WALLET }
|
||||||
]
|
]
|
||||||
@@ -86,4 +84,4 @@ export const canResubmitRefund = (item: {
|
|||||||
export const canManualApproveRefund = (item: {
|
export const canManualApproveRefund = (item: {
|
||||||
status?: RefundStatus
|
status?: RefundStatus
|
||||||
approval_instance_id?: number | null
|
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 // 备注
|
remark: string // 备注
|
||||||
created_at: string // 创建时间
|
created_at: string // 创建时间
|
||||||
updated_at: string // 更新时间
|
updated_at: string // 更新时间
|
||||||
|
member_count?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 业务用户组列表查询参数 */
|
/** 业务用户组列表查询参数 */
|
||||||
@@ -67,8 +68,49 @@ export interface SetBusinessUserGroupMembersParams {
|
|||||||
account_ids: number[] // 平台用户账号ID列表,至少一个;每个账号必须是启用平台用户
|
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 {
|
export interface BusinessUserGroupMembersResult {
|
||||||
group_id: number // 目标业务用户组ID;清空操作为 0
|
group_id: number // 目标业务用户组ID;清空操作为 0
|
||||||
account_ids: number[] | null // 本次成功维护归属的平台用户账号ID列表
|
account_ids: number[] | null // 本次成功维护归属的平台用户账号ID列表
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -229,6 +229,12 @@ export interface EmployeeCollectionApplication {
|
|||||||
decided_at?: string | null
|
decided_at?: string | null
|
||||||
latest_approval_instance_id?: number | null
|
latest_approval_instance_id?: number | null
|
||||||
latest_attempt_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
|
created_at?: string | null
|
||||||
updated_at?: string | null
|
updated_at?: string | null
|
||||||
}
|
}
|
||||||
@@ -279,6 +285,17 @@ export interface EmployeeCollectionApplicationSubmitResult {
|
|||||||
attempt?: EmployeeCollectionApplicationAttempt | null
|
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<
|
export type EmployeeCollectionPaymentMethodListResponse = BaseResponse<
|
||||||
|
|||||||
@@ -9,6 +9,17 @@
|
|||||||
返回
|
返回
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<h2 class="detail-title">核销申请详情</h2>
|
<h2 class="detail-title">核销申请详情</h2>
|
||||||
|
<ElButton
|
||||||
|
v-if="
|
||||||
|
application?.recoverable &&
|
||||||
|
hasAuth(AUGUST_PERMISSIONS.employeeCollection.applicationRecoverApproval)
|
||||||
|
"
|
||||||
|
type="warning"
|
||||||
|
:loading="recovering"
|
||||||
|
@click="recoverApproval"
|
||||||
|
>
|
||||||
|
恢复审批
|
||||||
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DetailPage v-if="application" :sections="detailSections" :data="application" />
|
<DetailPage v-if="application" :sections="detailSections" :data="application" />
|
||||||
@@ -112,6 +123,7 @@
|
|||||||
ElCard,
|
ElCard,
|
||||||
ElIcon,
|
ElIcon,
|
||||||
ElMessage,
|
ElMessage,
|
||||||
|
ElMessageBox,
|
||||||
ElTable,
|
ElTable,
|
||||||
ElTableColumn,
|
ElTableColumn,
|
||||||
ElTimeline,
|
ElTimeline,
|
||||||
@@ -146,6 +158,7 @@
|
|||||||
const { hasAuth } = useAuth()
|
const { hasAuth } = useAuth()
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const recovering = ref(false)
|
||||||
const detail = ref<EmployeeCollectionApplicationDetailData | null>(null)
|
const detail = ref<EmployeeCollectionApplicationDetailData | null>(null)
|
||||||
|
|
||||||
const application = computed<EmployeeCollectionApplication | null>(
|
const application = computed<EmployeeCollectionApplication | null>(
|
||||||
@@ -186,6 +199,20 @@
|
|||||||
prop: 'status',
|
prop: 'status',
|
||||||
formatter: (value, data) => data.status_name || getApplicationStatusLabel(value)
|
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',
|
label: '审批实例ID',
|
||||||
prop: 'latest_approval_instance_id',
|
prop: 'latest_approval_instance_id',
|
||||||
@@ -241,6 +268,38 @@
|
|||||||
router.push({ path: RoutesAlias.EmployeeCollectionApplications })
|
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())
|
onMounted(() => void loadDetail())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h, onMounted, reactive, ref } from 'vue'
|
import { h, onMounted, reactive, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
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 { EmployeeCollectionService } from '@/api/modules'
|
||||||
import type {
|
import type {
|
||||||
EmployeeCollectionApplication,
|
EmployeeCollectionApplication,
|
||||||
@@ -141,6 +141,8 @@
|
|||||||
{ label: '收款方式', prop: 'payment_method_name' },
|
{ label: '收款方式', prop: 'payment_method_name' },
|
||||||
{ label: '付款金额', prop: 'paid_amount' },
|
{ label: '付款金额', prop: 'paid_amount' },
|
||||||
{ label: '状态', prop: 'status' },
|
{ label: '状态', prop: 'status' },
|
||||||
|
{ label: '提交状态', prop: 'approval_submission_name' },
|
||||||
|
{ label: '可恢复', prop: 'recoverable' },
|
||||||
{ label: '提交时间', prop: 'created_at' }
|
{ label: '提交时间', prop: 'created_at' }
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -184,6 +186,21 @@
|
|||||||
() => row.status_name || getApplicationStatusLabel(row.status)
|
() => 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',
|
prop: 'created_at',
|
||||||
label: '提交时间',
|
label: '提交时间',
|
||||||
@@ -271,6 +288,30 @@
|
|||||||
getTableData()
|
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 getActions = (row: EmployeeCollectionApplication) => {
|
||||||
const actions: any[] = []
|
const actions: any[] = []
|
||||||
if (
|
if (
|
||||||
@@ -279,6 +320,16 @@
|
|||||||
) {
|
) {
|
||||||
actions.push({ label: '修改并重新提交', handler: () => handleResubmit(row), type: 'primary' })
|
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
|
return actions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
v-model:filter="searchForm"
|
v-model:filter="searchForm"
|
||||||
:items="searchFormItems"
|
:items="searchFormItems"
|
||||||
show-expand
|
show-expand
|
||||||
label-width="100px"
|
|
||||||
@reset="handleReset"
|
@reset="handleReset"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -64,7 +64,13 @@
|
|||||||
placeholder="1-64 字符,创建后不可修改"
|
placeholder="1-64 字符,创建后不可修改"
|
||||||
maxlength="64"
|
maxlength="64"
|
||||||
show-word-limit
|
show-word-limit
|
||||||
/>
|
>
|
||||||
|
<template #append>
|
||||||
|
<ElButton v-if="dialogType === 'add'" @click="handleGenerateGroupCode">
|
||||||
|
生成编码
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</ElInput>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="名称" prop="name">
|
<ElFormItem label="名称" prop="name">
|
||||||
<ElInput v-model="form.name" placeholder="请输入名称" maxlength="100" show-word-limit />
|
<ElInput v-model="form.name" placeholder="请输入名称" maxlength="100" show-word-limit />
|
||||||
@@ -103,105 +109,106 @@
|
|||||||
</ElDialog>
|
</ElDialog>
|
||||||
|
|
||||||
<!-- 成员管理弹窗 -->
|
<!-- 成员管理弹窗 -->
|
||||||
<ElDialog
|
<ElDrawer
|
||||||
v-model="memberDialogVisible"
|
v-model="memberDialogVisible"
|
||||||
:title="`成员管理 - ${currentGroup?.name || ''}`"
|
:title="`成员管理 - ${currentGroup?.name || ''} - ${currentGroup?.enabled ? '启用' : '已停用'}`"
|
||||||
width="680px"
|
direction="rtl"
|
||||||
|
size="65%"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
>
|
>
|
||||||
<div v-if="currentGroup" class="member-group-info">
|
<div class="member-transfer-layout">
|
||||||
<ElTag :type="currentGroup.enabled ? 'success' : 'danger'" size="small">
|
<div class="member-panel">
|
||||||
{{ currentGroup.enabled ? '启用' : '已停用' }}
|
<div class="member-panel-header">
|
||||||
</ElTag>
|
<div class="member-panel-title">候选账号</div>
|
||||||
<span class="member-group-code">{{ currentGroup.code }}</span>
|
<ElButton v-if="candidateHasMore" link type="primary" @click="loadMoreCandidates">
|
||||||
<span v-if="!currentGroup.enabled" class="member-group-tip">
|
查看更多
|
||||||
停用组保留现有成员,不能作为批量目标
|
</ElButton>
|
||||||
</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>
|
</div>
|
||||||
</ElOption>
|
<ElInput
|
||||||
</ElSelect>
|
v-model="candidateKeyword"
|
||||||
<div class="member-actions">
|
placeholder="搜索用户名或手机号"
|
||||||
<ElButton type="primary" :loading="settingMembers" @click="handleSetMembers">
|
clearable
|
||||||
保存成员
|
@input="searchMemberCandidates"
|
||||||
</ElButton>
|
/>
|
||||||
</div>
|
<ElTable
|
||||||
|
:data="memberCandidates"
|
||||||
<ElDivider content-position="left">移出成员(回未分组)</ElDivider>
|
border
|
||||||
<ElAlert type="info" :closable="false" show-icon class="member-alert">
|
stripe
|
||||||
选择要移出用户组的平台账号,清空后回未分组。
|
size="small"
|
||||||
</ElAlert>
|
height="300"
|
||||||
<ElSelect
|
v-loading="memberCandidatesLoading"
|
||||||
v-model="clearAccountIds"
|
@selection-change="candidateSelection = $event"
|
||||||
multiple
|
>
|
||||||
filterable
|
<ElTableColumn v-if="memberCandidates.length" type="selection" width="42" />
|
||||||
remote
|
<ElTableColumn prop="username" label="用户名" min-width="120" />
|
||||||
:remote-method="searchMemberCandidates"
|
<ElTableColumn prop="phone" label="手机号" min-width="110" />
|
||||||
:loading="memberCandidatesLoading"
|
<ElTableColumn label="操作提示" min-width="130" show-overflow-tooltip>
|
||||||
placeholder="搜索并选择要移出的平台账号"
|
<template #default="scope">
|
||||||
style="width: 100%; margin-top: 12px"
|
<ElTag size="small" type="info">
|
||||||
>
|
{{ getMemberOperationLabel(scope.row.action || scope.row.operation) }}
|
||||||
<ElOption
|
</ElTag>
|
||||||
v-for="candidate in memberCandidates"
|
</template>
|
||||||
:key="candidate.id"
|
</ElTableColumn>
|
||||||
:label="candidate.username"
|
</ElTable>
|
||||||
:value="candidate.id"
|
<ElButton type="primary" :loading="settingMembers" @click="handleSetMembers">
|
||||||
>
|
添加/迁移所选账号
|
||||||
<div class="member-option">
|
</ElButton>
|
||||||
<span>{{ candidate.username }}</span>
|
</div>
|
||||||
<span class="member-option-phone">{{ candidate.phone_summary }}</span>
|
<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>
|
</div>
|
||||||
</ElOption>
|
<ElInput v-model="currentMemberKeyword" placeholder="搜索当前成员" clearable />
|
||||||
</ElSelect>
|
<ElTable
|
||||||
<div class="member-actions">
|
:data="filteredCurrentMembers"
|
||||||
<ElButton type="danger" :loading="clearingMembers" @click="handleClearMembers">
|
border
|
||||||
移出所选账号
|
stripe
|
||||||
</ElButton>
|
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>
|
</div>
|
||||||
</ElDialog>
|
</ElDrawer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, h, onMounted, reactive, ref } from 'vue'
|
import { computed, h, onMounted, reactive, ref } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { BusinessUserGroupService, ShopService } from '@/api/modules'
|
import { BusinessUserGroupService } from '@/api/modules'
|
||||||
import type {
|
import type {
|
||||||
BusinessUserGroupBusinessLine,
|
BusinessUserGroupBusinessLine,
|
||||||
BusinessUserGroupItem,
|
BusinessUserGroupItem,
|
||||||
|
BusinessUserGroupMember,
|
||||||
CreateBusinessUserGroupParams,
|
CreateBusinessUserGroupParams,
|
||||||
UpdateBusinessUserGroupParams,
|
UpdateBusinessUserGroupParams,
|
||||||
ShopBusinessOwnerCandidate
|
BusinessUserGroupMemberCandidate
|
||||||
} from '@/types/api'
|
} from '@/types/api'
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
import type { SearchFormItem } from '@/types'
|
import type { SearchFormItem } from '@/types'
|
||||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||||
import { useAuth } from '@/composables/useAuth'
|
import { useAuth } from '@/composables/useAuth'
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
|
import { generateShopCode } from '@/utils/codeGenerator'
|
||||||
import {
|
import {
|
||||||
BUSINESS_USER_GROUP_BUSINESS_LINE_MAP,
|
BUSINESS_USER_GROUP_BUSINESS_LINE_MAP,
|
||||||
BUSINESS_USER_GROUP_BUSINESS_LINE_OPTIONS,
|
BUSINESS_USER_GROUP_BUSINESS_LINE_OPTIONS,
|
||||||
@@ -436,6 +443,12 @@
|
|||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleGenerateGroupCode = () => {
|
||||||
|
form.code = generateShopCode()
|
||||||
|
formRef.value?.clearValidate('code')
|
||||||
|
ElMessage.success('编码生成成功')
|
||||||
|
}
|
||||||
|
|
||||||
const openEditDialog = (row: BusinessUserGroupItem) => {
|
const openEditDialog = (row: BusinessUserGroupItem) => {
|
||||||
dialogType.value = 'edit'
|
dialogType.value = 'edit'
|
||||||
currentGroup.value = row
|
currentGroup.value = row
|
||||||
@@ -512,22 +525,34 @@
|
|||||||
|
|
||||||
// ==================== 成员管理 ====================
|
// ==================== 成员管理 ====================
|
||||||
const memberDialogVisible = ref(false)
|
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 memberCandidatesLoading = ref(false)
|
||||||
const memberAccountIds = ref<number[]>([])
|
const memberCandidatesPage = ref(1)
|
||||||
const clearAccountIds = ref<number[]>([])
|
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 settingMembers = ref(false)
|
||||||
const clearingMembers = 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
|
memberCandidatesLoading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await ShopService.getBusinessOwnerCandidates({
|
const res = await BusinessUserGroupService.getMemberCandidates(currentGroup.value.id, {
|
||||||
page: 1,
|
page: memberCandidatesPage.value,
|
||||||
page_size: 50,
|
page_size: 20,
|
||||||
keyword: keyword?.trim() || undefined
|
keyword: keyword.trim() || undefined,
|
||||||
|
scope: 'all'
|
||||||
})
|
})
|
||||||
memberCandidates.value = res.data.items || []
|
memberCandidates.value = res.data.items || []
|
||||||
|
memberCandidatesTotal.value = res.data.total || 0
|
||||||
} catch {
|
} catch {
|
||||||
memberCandidates.value = []
|
memberCandidates.value = []
|
||||||
} finally {
|
} 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) => {
|
const openMemberDialog = (row: BusinessUserGroupItem) => {
|
||||||
currentGroup.value = row
|
currentGroup.value = row
|
||||||
memberAccountIds.value = []
|
candidateSelection.value = []
|
||||||
clearAccountIds.value = []
|
clearSelection.value = []
|
||||||
|
currentMemberKeyword.value = ''
|
||||||
memberCandidates.value = []
|
memberCandidates.value = []
|
||||||
|
candidateKeyword.value = ''
|
||||||
memberDialogVisible.value = true
|
memberDialogVisible.value = true
|
||||||
|
void loadCurrentMembers(row.id)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
searchMemberCandidates()
|
searchMemberCandidates()
|
||||||
}, 0)
|
}, 0)
|
||||||
@@ -548,14 +649,15 @@
|
|||||||
|
|
||||||
const handleSetMembers = async () => {
|
const handleSetMembers = async () => {
|
||||||
if (!currentGroup.value) return
|
if (!currentGroup.value) return
|
||||||
if (!memberAccountIds.value.length) {
|
const accountIds = candidateSelection.value.map((member) => member.account_id)
|
||||||
|
if (!accountIds.length) {
|
||||||
ElMessage.warning('请至少选择一个平台账号')
|
ElMessage.warning('请至少选择一个平台账号')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
settingMembers.value = true
|
settingMembers.value = true
|
||||||
try {
|
try {
|
||||||
const res = await BusinessUserGroupService.setGroupMembers(currentGroup.value.id, {
|
const res = await BusinessUserGroupService.addGroupMembers(currentGroup.value.id, {
|
||||||
account_ids: memberAccountIds.value
|
account_ids: accountIds
|
||||||
})
|
})
|
||||||
ElMessage.success(`已维护 ${res.data?.account_ids?.length ?? 0} 个账号归属`)
|
ElMessage.success(`已维护 ${res.data?.account_ids?.length ?? 0} 个账号归属`)
|
||||||
memberDialogVisible.value = false
|
memberDialogVisible.value = false
|
||||||
@@ -566,14 +668,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleClearMembers = async () => {
|
const handleClearMembers = async () => {
|
||||||
if (!clearAccountIds.value.length) {
|
const accountIds = clearSelection.value.map((member) => member.account_id)
|
||||||
|
if (!accountIds.length) {
|
||||||
ElMessage.warning('请至少选择一个平台账号')
|
ElMessage.warning('请至少选择一个平台账号')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
clearingMembers.value = true
|
clearingMembers.value = true
|
||||||
try {
|
try {
|
||||||
const res = await BusinessUserGroupService.clearGroupMembers({
|
const res = await BusinessUserGroupService.removeGroupMembers(currentGroup.value?.id || 0, {
|
||||||
account_ids: clearAccountIds.value
|
account_ids: accountIds
|
||||||
})
|
})
|
||||||
ElMessage.success(`已清空 ${res.data?.account_ids?.length ?? 0} 个账号归属`)
|
ElMessage.success(`已清空 ${res.data?.account_ids?.length ?? 0} 个账号归属`)
|
||||||
memberDialogVisible.value = false
|
memberDialogVisible.value = false
|
||||||
@@ -604,6 +707,12 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--el-text-color-secondary);
|
color: var(--el-text-color-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.member-layout-tip {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-alert {
|
.member-alert {
|
||||||
@@ -616,6 +725,28 @@
|
|||||||
margin-top: 12px;
|
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 {
|
.member-option {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
Reference in New Issue
Block a user