fix: update some files
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m44s

This commit is contained in:
luo
2026-08-18 17:36:41 +08:00
parent 93f67967c5
commit d9d07422a9
38 changed files with 1140 additions and 728 deletions

View File

@@ -88,4 +88,14 @@ export class AgentRechargeService extends BaseService {
): Promise<BaseResponse<void>> {
return this.post<BaseResponse<void>>(`/api/admin/agent-recharges/${id}/reject`, data)
}
/**
* 补发历史线下代理充值审批
* @param id 充值记录ID
*/
static triggerApproval(id: number): Promise<BaseResponse<AgentRecharge>> {
return this.post<BaseResponse<AgentRecharge>>(
`/api/admin/agent-recharges/${id}/trigger-approval`
)
}
}

View File

@@ -45,4 +45,12 @@ export class RefundService extends BaseService {
static resubmitRefund(id: number, data: ResubmitRefundRequest): Promise<BaseResponse<void>> {
return this.post<BaseResponse<void>>(`/api/admin/refunds/${id}/resubmit`, data)
}
/**
* 补发历史退款审批
* @param id 退款申请ID
*/
static triggerApproval(id: number): Promise<BaseResponse<Refund>> {
return this.post<BaseResponse<Refund>>(`/api/admin/refunds/${id}/trigger-approval`)
}
}

View File

@@ -12,14 +12,14 @@
:key="tab.value"
type="button"
:class="{ active: activeCategory === tab.value }"
@click="activeCategory = tab.value"
@click="handleCategoryChange(tab.value)"
>
{{ tab.label }}<span v-if="tab.count"> ({{ tab.count }})</span>
</button>
</div>
<div class="content">
<div v-loading="notificationStore.loading" class="scroll">
<div ref="notificationListRef" v-loading="notificationStore.loading" class="scroll">
<button
v-for="item in filteredItems"
:key="item.id"
@@ -43,6 +43,21 @@
<p>暂无通知</p>
</div>
</div>
<div
v-if="notificationStore.notificationTotal > notificationStore.notificationPageSize"
class="pagination"
>
<ElPagination
:current-page="notificationStore.notificationPage"
:page-size="notificationStore.notificationPageSize"
:total="notificationStore.notificationTotal"
:pager-count="5"
background
layout="prev, pager, next"
small
@current-change="handlePageChange"
/>
</div>
</div>
</div>
</div>
@@ -64,6 +79,7 @@
const router = useRouter()
const notificationStore = useNotificationStore()
const activeCategory = ref<'all' | NotificationCategory>('all')
const notificationListRef = ref<HTMLElement | null>(null)
const visible = computed(() => props.modelValue)
const categoryMatches = (item: NotificationItem, category: string) => {
@@ -96,6 +112,24 @@
const close = () => emit('update:modelValue', false)
const loadNotificationPage = async (page: number) => {
try {
await notificationStore.loadNotifications(page)
notificationListRef.value?.scrollTo({ top: 0 })
} catch (error: any) {
ElMessage.error(error?.message || '获取通知失败')
}
}
const handleCategoryChange = (category: 'all' | NotificationCategory) => {
activeCategory.value = category
void loadNotificationPage(1)
}
const handlePageChange = (page: number) => {
void loadNotificationPage(page)
}
const handleMarkAllRead = async () => {
try {
const response = await notificationStore.markAllRead()

View File

@@ -68,6 +68,18 @@
overflow-y: auto;
}
.pagination {
display: flex;
justify-content: center;
padding: 12px 8px;
overflow-x: auto;
border-top: 1px solid var(--art-border-color);
:deep(.el-pagination) {
justify-content: center;
}
}
.notification-item {
display: flex;
gap: 10px;

View File

@@ -1,7 +1,12 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { NotificationService } from '@/api/modules/notification'
import type { NotificationItem, NotificationUnreadSummary } from '@/types/api'
import type {
BaseResponse,
NotificationItem,
NotificationListResponse,
NotificationUnreadSummary
} from '@/types/api'
const emptySummary = (): NotificationUnreadSummary => ({
approval: 0,
@@ -16,8 +21,22 @@ export const useNotificationStore = defineStore('notificationStore', () => {
const displayCount = ref('0')
const summary = ref<NotificationUnreadSummary>(emptySummary())
const recentNotifications = ref<NotificationItem[]>([])
const notificationPage = ref(1)
const notificationPageSize = 10
const notificationTotal = ref(0)
const loading = ref(false)
const applyNotificationList = (
response: BaseResponse<NotificationListResponse>,
fallbackPage: number
) => {
if (response.code !== 0 || !response.data) return
recentNotifications.value = response.data.items
notificationPage.value = response.data.page || fallbackPage
notificationTotal.value = Math.max(0, response.data.total || 0)
}
const refreshUnreadCount = async () => {
const response = await NotificationService.getUnreadCount()
if (response.code === 0 && response.data) {
@@ -27,19 +46,33 @@ export const useNotificationStore = defineStore('notificationStore', () => {
return unreadCount.value
}
const refreshSummary = async () => {
const loadNotifications = async (page = notificationPage.value) => {
const targetPage = Math.max(1, page)
loading.value = true
try {
const response = await NotificationService.getNotifications({
page: targetPage,
page_size: notificationPageSize
})
applyNotificationList(response, targetPage)
return response
} finally {
loading.value = false
}
}
const refreshSummary = async (page = 1) => {
const targetPage = Math.max(1, page)
loading.value = true
try {
const [summaryResponse, listResponse] = await Promise.all([
NotificationService.getUnreadSummary(),
NotificationService.getNotifications({ page: 1, page_size: 10 })
NotificationService.getNotifications({ page: targetPage, page_size: notificationPageSize })
])
if (summaryResponse.code === 0 && summaryResponse.data) {
summary.value = summaryResponse.data
}
if (listResponse.code === 0 && listResponse.data) {
recentNotifications.value = listResponse.data.items.slice(0, 10)
}
applyNotificationList(listResponse, targetPage)
await refreshUnreadCount()
return summary.value
} finally {
@@ -50,7 +83,7 @@ export const useNotificationStore = defineStore('notificationStore', () => {
const markRead = async (id: number) => {
const response = await NotificationService.markRead(id)
if (response.code === 0) {
await refreshSummary()
await refreshSummary(notificationPage.value)
}
return response
}
@@ -58,7 +91,7 @@ export const useNotificationStore = defineStore('notificationStore', () => {
const markAllRead = async (category?: string) => {
const response = await NotificationService.markAllRead(category ? { category } : {})
if (response.code === 0) {
await refreshSummary()
await refreshSummary(notificationPage.value)
}
return response
}
@@ -68,8 +101,12 @@ export const useNotificationStore = defineStore('notificationStore', () => {
displayCount,
summary,
recentNotifications,
notificationPage,
notificationPageSize,
notificationTotal,
loading,
refreshUnreadCount,
loadNotifications,
refreshSummary,
markRead,
markAllRead

View File

@@ -397,11 +397,11 @@
>
<ElDatePicker
v-model="seriesBindingForm.created_at_range"
type="daterange"
type="datetimerange"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DD"
value-format="YYYY-MM-DDTHH:mm:ssZ"
style="width: 100%"
/>
</ElFormItem>
@@ -1537,13 +1537,13 @@
{
label: '起止时间',
prop: 'dateRange',
type: 'date',
type: 'datetimerange',
config: {
type: 'daterange',
type: 'datetimerange',
rangeSeparator: '至',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
valueFormat: 'YYYY-MM-DD'
valueFormat: 'YYYY-MM-DDTHH:mm:ssZ'
}
}
]

View File

@@ -10,30 +10,6 @@
返回
</ElButton>
<h2 class="detail-title">换货单详情</h2>
<ElButton
v-if="exchangeAuditTarget && hasAuth(exchangeAuditPermission)"
type="primary"
plain
@click="openAuditInvestigation(exchangeAuditTarget)"
>
{{ userType === 3 ? '活动记录' : '换货审计' }}
</ElButton>
<ElButton
v-if="oldAssetAuditTarget && hasAuth(AUDIT_PERMISSIONS.exchangeOldAssetEntry)"
type="primary"
plain
@click="openAuditInvestigation(oldAssetAuditTarget)"
>
旧资产审计
</ElButton>
<ElButton
v-if="newAssetAuditTarget && hasAuth(AUDIT_PERMISSIONS.exchangeNewAssetEntry)"
type="primary"
plain
@click="openAuditInvestigation(newAssetAuditTarget)"
>
新资产审计
</ElButton>
</div>
<!-- 详情内容 -->
@@ -59,51 +35,15 @@
import { formatDateTime } from '@/utils/business/format'
import DetailPage from '@/components/common/DetailPage.vue'
import type { DetailSection } from '@/components/common/DetailPage.vue'
import { useAuth } from '@/composables/useAuth'
import { useUserStore } from '@/store/modules/user'
import { AUDIT_PERMISSIONS } from '@/config/constants/audit'
import { resolveAuditResourceTarget } from '@/utils/business/auditNavigation'
import { openAuditInvestigation } from '@/components/business/audit/investigationController'
defineOptions({ name: 'ExchangeDetail' })
const route = useRoute()
const router = useRouter()
const { hasAuth } = useAuth()
const userStore = useUserStore()
const loading = ref(false)
const exchangeDetail = ref<ExchangeResponse | null>(null)
const exchangeId = ref<number>(0)
const userType = computed(() => Number(userStore.info.user_type))
const exchangeAuditPermission = computed(() =>
userType.value === 3 ? AUDIT_PERMISSIONS.agentExchangeActivity : AUDIT_PERMISSIONS.exchangeEntry
)
const exchangeAuditTarget = computed(() => {
if (!exchangeDetail.value || ![1, 2, 3].includes(userType.value)) return null
return resolveAuditResourceTarget({
userType: userType.value,
resourceType: 'exchange_order',
internalId: exchangeDetail.value.id,
businessIdentifier: exchangeDetail.value.exchange_no
})
})
const oldAssetAuditTarget = computed(() => {
if (!exchangeDetail.value || ![1, 2].includes(userType.value)) return null
return resolveAuditResourceTarget({
userType: userType.value,
resourceType: exchangeDetail.value.old_asset_type,
internalId: exchangeDetail.value.old_asset_id
})
})
const newAssetAuditTarget = computed(() => {
if (!exchangeDetail.value || ![1, 2].includes(userType.value)) return null
return resolveAuditResourceTarget({
userType: userType.value,
resourceType: exchangeDetail.value.new_asset_type || '',
internalId: exchangeDetail.value.new_asset_id
})
})
const formatExchangeShopName = (shopName?: string | null, shopId?: number | null) => {
if (shopName) return shopName

View File

@@ -851,15 +851,15 @@
}
},
{
label: '创建时间',
label: '起止时间',
prop: 'created_at_range',
type: 'date',
type: 'datetimerange',
config: {
type: 'daterange',
type: 'datetimerange',
rangeSeparator: '至',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
valueFormat: 'YYYY-MM-DD'
valueFormat: 'YYYY-MM-DDTHH:mm:ssZ'
}
}
])

View File

@@ -80,7 +80,7 @@
total_count: 0,
window_days: 15
})
const searchForm = reactive<ExpiringAssetQueryParams>({
const searchForm = reactive<ExpiringAssetQueryParams & { expires_range: string[] }>({
asset_type: undefined,
keyword: '',
shop_id: undefined,
@@ -88,7 +88,8 @@
days_min: undefined,
days_max: undefined,
expires_from: undefined,
expires_to: undefined
expires_to: undefined,
expires_range: []
})
const pagination = reactive({ currentPage: 1, pageSize: 20, total: 0 })
@@ -166,16 +167,17 @@
}
},
{
label: '预计到期起',
prop: 'expires_from',
type: 'date',
config: { valueFormat: 'YYYY-MM-DD', clearable: true }
},
{
label: '预计到期结束',
prop: 'expires_to',
type: 'date',
config: { valueFormat: 'YYYY-MM-DD', clearable: true }
label: '预计到期起',
prop: 'expires_range',
type: 'datetimerange',
config: {
type: 'datetimerange',
rangeSeparator: '至',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
valueFormat: 'YYYY-MM-DDTHH:mm:ssZ',
clearable: true
}
}
])
@@ -314,8 +316,9 @@
const daysMax = toInteger(searchForm.days_max)
if (daysMin !== undefined) params.days_min = daysMin
if (daysMax !== undefined) params.days_max = daysMax
if (searchForm.expires_from) params.expires_from = searchForm.expires_from
if (searchForm.expires_to) params.expires_to = searchForm.expires_to
const [expiresFrom, expiresTo] = searchForm.expires_range || []
if (expiresFrom) params.expires_from = expiresFrom
if (expiresTo) params.expires_to = expiresTo
const response = await AssetService.getExpiringAssets(params)
if (response.code === 0 && response.data) {
@@ -347,7 +350,8 @@
days_min: undefined,
days_max: undefined,
expires_from: undefined,
expires_to: undefined
expires_to: undefined,
expires_range: []
})
pagination.currentPage = 1
void loadAssets()

View File

@@ -100,15 +100,15 @@
options: () => statusOptions
},
{
label: '创建时间',
label: '起止时间',
prop: 'dateRange',
type: 'date',
type: 'datetimerange',
config: {
type: 'daterange',
type: 'datetimerange',
rangeSeparator: '至',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
valueFormat: 'YYYY-MM-DD'
valueFormat: 'YYYY-MM-DDTHH:mm:ssZ'
}
}
]

View File

@@ -10,22 +10,6 @@
返回
</ElButton>
<h2 class="detail-title">资产分配详情</h2>
<ElButton
v-if="recordAuditTarget && hasAuth(recordAuditPermission)"
type="primary"
plain
@click="openAuditInvestigation(recordAuditTarget)"
>
{{ userType === 3 ? '活动记录' : '分配审计' }}
</ElButton>
<ElButton
v-if="assetAuditTarget && hasAuth(AUDIT_PERMISSIONS.assetAllocationAssetEntry)"
type="primary"
plain
@click="openAuditInvestigation(assetAuditTarget)"
>
资产审计
</ElButton>
</div>
<!-- 详情内容 -->
@@ -41,7 +25,7 @@
</template>
<script setup lang="ts">
import { computed, ref, onMounted } from 'vue'
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElCard, ElButton, ElIcon, ElMessage } from 'element-plus'
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
@@ -50,44 +34,14 @@
import { CardService } from '@/api/modules'
import type { AssetAllocationRecord } from '@/types/api/card'
import { formatDateTime } from '@/utils/business/format'
import { useAuth } from '@/composables/useAuth'
import { useUserStore } from '@/store/modules/user'
import { AUDIT_PERMISSIONS } from '@/config/constants/audit'
import { resolveAuditResourceTarget } from '@/utils/business/auditNavigation'
import { openAuditInvestigation } from '@/components/business/audit/investigationController'
defineOptions({ name: 'AssetAssignDetail' })
const route = useRoute()
const router = useRouter()
const { hasAuth } = useAuth()
const userStore = useUserStore()
const loading = ref(false)
const detailData = ref<AssetAllocationRecord | null>(null)
const userType = computed(() => Number(userStore.info.user_type))
const recordAuditPermission = computed(() =>
userType.value === 3
? AUDIT_PERMISSIONS.agentAssetAllocationActivity
: AUDIT_PERMISSIONS.assetAllocationEntry
)
const recordAuditTarget = computed(() => {
if (!detailData.value || ![1, 2, 3].includes(userType.value)) return null
return resolveAuditResourceTarget({
userType: userType.value,
resourceType: 'asset_allocation_record',
internalId: detailData.value.id,
businessIdentifier: detailData.value.allocation_no
})
})
const assetAuditTarget = computed(() => {
if (!detailData.value || ![1, 2].includes(userType.value)) return null
return resolveAuditResourceTarget({
userType: userType.value,
resourceType: detailData.value.asset_type,
internalId: detailData.value.asset_id
})
})
// 详情页配置
const detailSections: DetailSection[] = [

View File

@@ -172,16 +172,16 @@
}))
},
{
label: '创建时间',
label: '起止时间',
prop: 'dateRange',
type: 'date',
type: 'datetimerange',
config: {
type: 'daterange',
type: 'datetimerange',
clearable: true,
rangeSeparator: '至',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
valueFormat: 'YYYY-MM-DD'
valueFormat: 'YYYY-MM-DDTHH:mm:ssZ'
}
}
]

View File

@@ -200,12 +200,12 @@
{
label: '授权时间',
prop: 'dateRange',
type: 'daterange',
type: 'datetimerange',
config: {
type: 'daterange',
type: 'datetimerange',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
valueFormat: 'YYYY-MM-DD'
valueFormat: 'YYYY-MM-DDTHH:mm:ssZ'
}
}
]

View File

@@ -213,9 +213,9 @@
{
label: '创建时间',
prop: 'dateRange',
type: 'daterange',
type: 'datetimerange',
config: {
type: 'daterange',
type: 'datetimerange',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
valueFormat: 'YYYY-MM-DDTHH:mm:ssZ'

View File

@@ -329,9 +329,9 @@
{
label: '创建时间',
prop: 'dateRange',
type: 'daterange',
type: 'datetimerange',
config: {
type: 'daterange',
type: 'datetimerange',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
valueFormat: 'YYYY-MM-DDTHH:mm:ssZ'

View File

@@ -52,8 +52,15 @@
const loading = ref(false)
const items = ref<AuditEventView[]>([])
const pagination = reactive({ page: 1, pageSize: 20, total: 0 })
function createDefaultDateRange() {
const end = new Date()
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000)
const format = (date: Date) => date.toISOString().replace(/\.\d{3}Z$/, 'Z')
return [format(start), format(end)]
}
const initialSearchState = {
dateRange: [] as string[],
dateRange: createDefaultDateRange(),
category: undefined,
result: undefined,
risk: undefined,
@@ -107,17 +114,25 @@
{ label: '店铺', value: 'shop' },
{ label: '个人客户', value: 'personal_customer' }
]
const handleDateRangeChange = (value: unknown) => {
if (!Array.isArray(value) || value.length !== 2) {
searchForm.dateRange = createDefaultDateRange()
}
}
const searchFormItems: SearchFormItem[] = [
{
label: '起止时间',
prop: 'dateRange',
type: 'date',
type: 'datetimerange',
onChange: ({ val }) => handleDateRangeChange(val),
config: {
type: 'daterange',
type: 'datetimerange',
rangeSeparator: '至',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
valueFormat: 'YYYY-MM-DD'
valueFormat: 'YYYY-MM-DDTHH:mm:ssZ',
clearable: false
}
},
{
@@ -241,18 +256,11 @@
}
])
const toAuditDate = (value?: string, endExclusive = false) => {
if (!value) return undefined
const [year, month, day] = value.split('-').map(Number)
const date = new Date(year, month - 1, day + (endExclusive ? 1 : 0))
return date.toISOString()
}
const buildQuery = (): AuditEventQuery => ({
page: pagination.page,
page_size: pagination.pageSize,
created_from: toAuditDate(searchForm.dateRange?.[0]),
created_to: toAuditDate(searchForm.dateRange?.[1], true),
created_from: searchForm.dateRange?.[0],
created_to: searchForm.dateRange?.[1],
category: searchForm.category,
result: searchForm.result,
risk: searchForm.risk,
@@ -277,7 +285,7 @@
load()
}
const handleReset = () => {
Object.assign(searchForm, { ...initialSearchState, dateRange: [] })
Object.assign(searchForm, { ...initialSearchState, dateRange: createDefaultDateRange() })
pagination.page = 1
load()
}

View File

@@ -12,6 +12,8 @@
value-format="YYYY-MM-DDTHH:mm:ssZ"
start-placeholder="开始时间"
end-placeholder="结束时间"
:clearable="false"
@change="handleDateRangeChange"
/>
</ElFormItem>
</ElCol>
@@ -19,10 +21,10 @@
<ElFormItem>
<ElSelect v-model="query.provider" clearable placeholder="请选择提供方">
<ElOption
v-for="item in overview?.providers || []"
:key="item.code"
:label="item.name"
:value="item.code"
v-for="item in providerOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</ElSelect>
</ElFormItem>
@@ -31,10 +33,10 @@
<ElFormItem>
<ElSelect v-model="query.direction" clearable placeholder="请选择方向">
<ElOption
v-for="item in overview?.directions || []"
:key="item.code"
:label="item.name"
:value="item.code"
v-for="item in directionOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</ElSelect>
</ElFormItem>
@@ -43,10 +45,10 @@
<ElFormItem>
<ElSelect v-model="query.result" clearable placeholder="请选择结果">
<ElOption
v-for="item in overview?.results || []"
:key="item.code"
:label="item.name"
:value="item.code"
v-for="item in resultOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</ElSelect>
</ElFormItem>
@@ -231,9 +233,12 @@
import { AuditService } from '@/api/modules'
import type {
AuditNamedCount,
IntegrationDirection,
IntegrationListItem,
IntegrationOverview,
IntegrationQuery
IntegrationProvider,
IntegrationQuery,
IntegrationResult
} from '@/types/api'
import { auditResultDisplay, integrationCategoryMeta } from '@/utils/business/audit'
import { formatDateTime } from '@/utils/business/format'
@@ -243,11 +248,39 @@
const router = useRouter()
const loading = ref(false)
const headerColumns = ref([])
const dateRange = ref<string[]>([])
const dateRange = ref<string[]>(createDefaultDateRange())
const query = reactive<IntegrationQuery>({ page: 1, page_size: 20 })
const overview = ref<IntegrationOverview>()
const items = ref<IntegrationListItem[]>([])
const total = ref(0)
const providerOptions: Array<{ label: string; value: IntegrationProvider }> = [
{ label: '中国电信', value: 'ctcc' },
{ label: '中国移动', value: 'cmcc' },
{ label: '中国联通', value: 'cucc' },
{ label: '微信支付', value: 'wechat_pay' },
{ label: '支付宝', value: 'alipay' },
{ label: '富友', value: 'fuiou' },
{ label: '企业微信', value: 'wecom' },
{ label: '设备网关', value: 'gateway' }
]
const directionOptions: Array<{ label: string; value: IntegrationDirection }> = [
{ label: '入站', value: 'inbound' },
{ label: '出站', value: 'outbound' }
]
const resultOptions: Array<{ label: string; value: IntegrationResult }> = [
{ label: '待处理', value: 'pending' },
{ label: '成功', value: 'success' },
{ label: '失败', value: 'failed' },
{ label: '结果未知', value: 'unknown' },
{ label: '未找到', value: 'not_found' },
{ label: '无效载荷', value: 'invalid_payload' },
{ label: '冲突', value: 'conflict' },
{ label: '已忽略', value: 'ignored' },
{ label: '已合并', value: 'merged' },
{ label: '已限频', value: 'rate_limited' },
{ label: '已提前完成', value: 'completed' },
{ label: '已取消', value: 'cancelled' }
]
const aggregateChartData = (values: AuditNamedCount[]) => {
const totals = new Map<string, AuditNamedCount>()
values.forEach((item) => {
@@ -261,9 +294,21 @@
const providerChartData = computed(() => aggregateChartData(overview.value?.providers || []))
const directionChartData = computed(() => aggregateChartData(overview.value?.directions || []))
const resultChartData = computed(() => aggregateChartData(overview.value?.results || []))
function createDefaultDateRange() {
const end = new Date()
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000)
const format = (date: Date) => date.toISOString().replace(/\.\d{3}Z$/, 'Z')
return [format(start), format(end)]
}
const handleDateRangeChange = (value: string[] | null) => {
if (!value || value.length !== 2) dateRange.value = createDefaultDateRange()
}
const syncDates = () => {
query.created_from = dateRange.value?.[0] || undefined
query.created_to = dateRange.value?.[1] || undefined
if (dateRange.value.length !== 2) dateRange.value = createDefaultDateRange()
query.created_from = dateRange.value[0]
query.created_to = dateRange.value[1]
}
const loadList = async () => {
syncDates()

View File

@@ -12,6 +12,8 @@
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DDTHH:mm:ssZ"
:clearable="false"
@change="handleDateRangeChange"
/>
</ElFormItem>
</ElCol>
@@ -184,7 +186,7 @@
const { hasAuth } = useAuth()
const loading = ref(false)
const headerColumns = ref([])
const dateRange = ref<string[]>([])
const dateRange = ref<string[]>(createDefaultDateRange())
const overview = ref<AuditRiskOverview>()
const events = ref<AuditEventView[]>([])
const total = ref(0)
@@ -202,9 +204,21 @@
const riskChartData = computed(() => aggregateChartData(overview.value?.risks || []))
const resultChartData = computed(() => aggregateChartData(overview.value?.results || []))
const sourceChartData = computed(() => aggregateChartData(overview.value?.sources || []))
function createDefaultDateRange() {
const end = new Date()
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000)
const format = (date: Date) => date.toISOString().replace(/\.\d{3}Z$/, 'Z')
return [format(start), format(end)]
}
const handleDateRangeChange = (value: string[] | null) => {
if (!value || value.length !== 2) dateRange.value = createDefaultDateRange()
}
const syncDates = () => {
query.created_from = dateRange.value?.[0] || undefined
query.created_to = dateRange.value?.[1] || undefined
if (dateRange.value.length !== 2) dateRange.value = createDefaultDateRange()
query.created_from = dateRange.value[0]
query.created_to = dateRange.value[1]
if (
dateRange.value?.length === 2 &&
new Date(dateRange.value[1]).getTime() - new Date(dateRange.value[0]).getTime() >

View File

@@ -249,11 +249,11 @@
<ElFormItem label="交易日期">
<ElDatePicker
v-model="mainWalletSearchForm.date_range"
type="daterange"
type="datetimerange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
value-format="YYYY-MM-DDTHH:mm:ssZ"
unlink-panels
/>
</ElFormItem>

View File

@@ -400,11 +400,11 @@
{
label: '起止时间',
prop: 'dateRange',
type: 'date',
type: 'datetimerange',
config: {
type: 'daterange',
type: 'datetimerange',
clearable: true,
valueFormat: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DDTHH:mm:ssZ',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期'
},
@@ -479,16 +479,19 @@
{
prop: 'iccid',
label: 'ICCID',
width: 200,
formatter: (row: ShopCommissionRecordItem) => row.iccid || '-'
},
{
prop: 'virtual_no',
label: '虚拟号',
width: 180,
formatter: (row: ShopCommissionRecordItem) => row.virtual_no || '-'
},
{
prop: 'order_no',
label: '订单号',
width: 220,
formatter: (row: ShopCommissionRecordItem) => row.order_no || '-'
},
{
@@ -608,11 +611,11 @@
{
label: '起止时间',
prop: 'dateRange',
type: 'date',
type: 'datetimerange',
config: {
type: 'daterange',
type: 'datetimerange',
clearable: true,
valueFormat: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DDTHH:mm:ssZ',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期'
},
@@ -656,7 +659,7 @@
{
prop: 'withdrawal_no',
label: '提现单号',
minWidth: 160
minWidth: 180
},
{
prop: 'amount',
@@ -700,7 +703,8 @@
{
prop: 'reject_reason',
label: '拒绝原因',
minWidth: 120,
minWidth: 230,
showOverflowTooltip: true,
formatter: (row: WithdrawalRequestItem) => row.reject_reason || '-'
},
{

View File

@@ -165,13 +165,13 @@
{
label: '起止时间',
prop: 'dateRange',
type: 'date',
type: 'datetimerange',
config: {
type: 'daterange',
type: 'datetimerange',
rangeSeparator: '至',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
valueFormat: 'YYYY-MM-DD'
valueFormat: 'YYYY-MM-DDTHH:mm:ssZ'
}
}
]

View File

@@ -12,6 +12,7 @@ interface BuildAgentRechargeActionsOptions {
onViewPaymentVoucher: (row: AgentRecharge) => void
onConfirmPayment: (row: AgentRecharge) => void
onReject: (row: AgentRecharge) => void
onTriggerApproval: (row: AgentRecharge) => void
}
export const buildAgentRechargeActions = (
@@ -24,6 +25,14 @@ export const buildAgentRechargeActions = (
row.approval_source === 'wecom' ||
row.approval_source === 'legacy' ||
(row.approval_instance_id !== undefined && row.approval_instance_id !== null)
const approvalStatusEmpty = row.approval_status === undefined || row.approval_status === null
const canTriggerApproval =
approvalStatusEmpty &&
![
AgentRechargeStatus.COMPLETED,
AgentRechargeStatus.REJECTED,
AgentRechargeStatus.CLOSED
].includes(row.status)
if (
row.payment_method === 'offline' &&
@@ -31,7 +40,7 @@ export const buildAgentRechargeActions = (
options.hasAuth('agent_recharge:view_payment_voucher')
) {
actions.push({
label: '查看支付凭证',
label: '支付凭证',
handler: () => options.onViewPaymentVoucher(row),
type: 'primary'
})
@@ -50,6 +59,14 @@ export const buildAgentRechargeActions = (
})
}
if (canTriggerApproval && options.hasAuth('agent_recharge:trigger_approval')) {
actions.push({
label: '补发审批',
handler: () => options.onTriggerApproval(row),
type: 'primary'
})
}
if (
!hasApprovalRecord &&
row.status === AgentRechargeStatus.PENDING &&

View File

@@ -10,36 +10,6 @@
返回
</ElButton>
<h2 class="detail-title">{{ pageTitle }}</h2>
<ElButton
v-if="detailData && isPlatformUser && hasAuth(AUDIT_PERMISSIONS.agentRechargeEntry)"
type="primary"
plain
@click="openRechargeAudit"
>
审计记录
</ElButton>
<ElButton
v-if="
detailData && isPlatformUser && hasAuth(AUDIT_PERMISSIONS.agentRechargeFinanceEntry)
"
type="primary"
plain
@click="openRechargeFinance"
>
资金链路
</ElButton>
<ElButton
v-if="
detailData?.approval_instance_id &&
isPlatformUser &&
hasAuth(AUDIT_PERMISSIONS.agentRechargeApprovalEntry)
"
type="primary"
plain
@click="openApprovalAudit"
>
审批审计
</ElButton>
</div>
<!-- 详情内容 -->
@@ -71,45 +41,19 @@
import { formatDateTime } from '@/utils/business/format'
import { hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
import { formatRejectionReason } from './agentRechargeDisplay'
import { useAuth } from '@/composables/useAuth'
import { useUserStore } from '@/store/modules/user'
import { AUDIT_PERMISSIONS } from '@/config/constants/audit'
import {
resolveAuditResourceTarget,
resolveFinanceAuditTarget
} from '@/utils/business/auditNavigation'
import { openAuditInvestigation } from '@/components/business/audit/investigationController'
import { formatRejectionReason } from './agentRechargeDisplay'
defineOptions({ name: 'AgentRechargeDetail' })
const route = useRoute()
const router = useRouter()
const { hasAuth } = useAuth()
const userStore = useUserStore()
const isRestrictedCustomerRole = computed(() => [3, 4].includes(Number(userStore.info.user_type)))
const loading = ref(false)
const detailData = ref<AgentRecharge | null>(null)
const paymentVoucherFileKeys = ref<string[]>([])
const isPlatformUser = computed(() => [1, 2].includes(Number(userStore.info.user_type)))
const openRechargeAudit = () => {
const target = resolveAuditResourceTarget({
resourceType: 'agent_recharge',
internalId: detailData.value?.id
})
if (target) openAuditInvestigation(target)
}
const openRechargeFinance = () => {
const target = resolveFinanceAuditTarget('recharge_id', detailData.value?.id)
if (target) openAuditInvestigation(target)
}
const openApprovalAudit = () => {
const target = resolveAuditResourceTarget({
resourceType: 'approval_instance',
internalId: detailData.value?.approval_instance_id
})
if (target) openAuditInvestigation(target)
}
const pageTitle = computed(() => `充值订单详情`)
@@ -171,7 +115,15 @@
{ label: '充值单号', prop: 'recharge_no' },
{ label: '店铺名称', prop: 'shop_name' },
{ label: '充值来源', formatter: (_, data) => getRechargeSourceText(data) },
{ label: '提交人', prop: 'submitter_name', formatter: (value) => value || '-' },
...(!isRestrictedCustomerRole.value
? [
{
label: '提交人',
prop: 'submitter_name',
formatter: (value: string | null | undefined) => value || '-'
}
]
: []),
{
label: '充值金额',
formatter: (_, data) => formatCurrency(data.amount)
@@ -181,15 +133,19 @@
render: (data) =>
h(ElTag, { type: getStatusType(data.status) }, () => data.status_name || '-')
},
{
label: '驳回原因',
prop: 'rejection_reason',
formatter: (value) => formatRejectionReason(value),
fullWidth: true
}
...(!isRestrictedCustomerRole.value
? [
{
label: '驳回原因',
prop: 'rejection_reason',
formatter: (value: string | null | undefined) => formatRejectionReason(value),
fullWidth: true
}
]
: [])
]
},
...(isOfflineRecharge
...(isOfflineRecharge && !isRestrictedCustomerRole.value
? [
{
title: '企微审批信息',
@@ -212,15 +168,19 @@
}
]
: []),
{
title: '业务处理结果',
fields: [
{
label: '处理状态',
formatter: (_, data) => data.processing_status_name || '-'
}
]
},
...(!isRestrictedCustomerRole.value
? [
{
title: '业务处理结果',
fields: [
{
label: '处理状态',
formatter: (_: unknown, data: AgentRecharge) => data.processing_status_name || '-'
}
]
}
]
: []),
{
title: '支付信息',
fields: [
@@ -269,12 +229,16 @@
}
]
: []),
{
label: '运营备注',
prop: 'remark',
formatter: (value) => value || '-',
fullWidth: true
}
...(!isRestrictedCustomerRole.value
? [
{
label: '运营备注',
prop: 'remark',
formatter: (value: string | null | undefined) => value || '-',
fullWidth: true
}
]
: [])
]
},
{

View File

@@ -5,7 +5,7 @@
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
:show-expand="false"
show-expand
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
@@ -300,6 +300,7 @@
import { AgentRechargeService, CommissionService, ShopService } from '@/api/modules'
import {
ElMessage,
ElMessageBox,
ElTag,
ElButton,
ElCascader,
@@ -356,6 +357,18 @@
const isAgentAccount = computed(() => Number(userStore.info.user_type) === 3)
const isPlatformAccount = computed(() => [1, 2].includes(Number(userStore.info.user_type)))
const isRestrictedCustomerRole = computed(() => [3, 4].includes(Number(userStore.info.user_type)))
const hiddenInternalColumnProps = new Set([
'approval_provider',
'submitter_name',
'approval_status',
'current_approver_summary',
'processing_status_name',
'remark',
'rejection_reason'
])
const shouldShowInternalColumn = (prop?: string) =>
!isRestrictedCustomerRole.value || !hiddenInternalColumnProps.has(prop || '')
const canViewRecharge = computed(() => [1, 2, 3].includes(Number(userStore.info.user_type)))
const canCreateRecharge = computed(() => isAgentAccount.value || isPlatformAccount.value)
const createMode = ref<'online' | 'offline'>(isAgentAccount.value ? 'online' : 'offline')
@@ -369,6 +382,7 @@
const voucherUploading = ref(false)
const confirmPayLoading = ref(false)
const rejectLoading = ref(false)
const triggerApprovalLoading = ref(false)
const tableRef = ref()
const createDialogVisible = ref(false)
const exportDialogVisible = ref(false)
@@ -385,7 +399,7 @@
const qrContent = ref('')
const onlineRequestId = ref<string | null>(null)
const paymentStatusTimer = ref<ReturnType<typeof setInterval> | null>(null)
const ONLINE_MIN_RECHARGE_AMOUNT_FEN = 10
const ONLINE_MIN_RECHARGE_AMOUNT_FEN = 10000
const ONLINE_MAX_RECHARGE_AMOUNT_FEN = 100000000
const paymentMethodsBounds = reactive({
min_amount: ONLINE_MIN_RECHARGE_AMOUNT_FEN,
@@ -493,13 +507,13 @@
{
label: '起止时间',
prop: 'dateRange',
type: 'date',
type: 'datetimerange',
config: {
type: 'daterange',
type: 'datetimerange',
rangeSeparator: '至',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
valueFormat: 'YYYY-MM-DD'
valueFormat: 'YYYY-MM-DDTHH:mm:ssZ'
}
}
)
@@ -534,7 +548,7 @@
{ label: '支付时间', prop: 'paid_at' },
{ label: '完成时间', prop: 'completed_at' },
{ label: '更新时间', prop: 'updated_at' }
]
].filter(({ prop }) => shouldShowInternalColumn(prop))
const createFormRef = ref<FormInstance>()
const confirmPayFormRef = ref<FormInstance>()
@@ -694,139 +708,142 @@
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'recharge_no',
label: '充值单号',
minWidth: 240,
formatter: (row: AgentRecharge) => {
return h(
'span',
{
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
onClick: (e: MouseEvent) => {
e.stopPropagation()
handleNameClick(row)
}
},
row.recharge_no
)
const { columnChecks, columns } = useCheckedColumns(() =>
[
{
prop: 'recharge_no',
label: '充值单号',
minWidth: 240,
formatter: (row: AgentRecharge) => {
return h(
'span',
{
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
onClick: (e: MouseEvent) => {
e.stopPropagation()
handleNameClick(row)
}
},
row.recharge_no
)
}
},
{
prop: 'shop_name',
label: '店铺名称',
minWidth: 150,
showOverflowTooltip: true
},
{
prop: 'recharge_source',
label: '充值来源',
width: 140,
formatter: (row: AgentRecharge) => getRechargeSourceText(row)
},
{
prop: 'amount',
label: '充值金额',
width: 120,
formatter: (row: AgentRecharge) => formatCurrency(row.amount)
},
{
prop: 'status',
label: '状态',
width: 100,
formatter: (row: AgentRecharge) => {
return h(ElTag, { type: getStatusType(row.status) }, () => row.status_name || '-')
}
},
{
prop: 'approval_provider',
label: '审批渠道',
width: 110,
formatter: (row: AgentRecharge) => {
if (row.approval_provider === 'wecom' || row.approval_source === 'wecom') return '企微'
if (row.approval_source === 'legacy') return '历史审批'
return row.approval_provider || row.approval_source || '-'
}
},
{
prop: 'submitter_name',
label: '提交人',
width: 120,
showOverflowTooltip: true,
formatter: (row: AgentRecharge) => row.submitter_name || '-'
},
{
prop: 'approval_status',
label: '审批状态',
width: 120,
formatter: (row: AgentRecharge) => getApprovalStatusText(row)
},
{
prop: 'current_approver_summary',
label: '当前审批人摘要',
minWidth: 180,
showOverflowTooltip: true,
formatter: (row: AgentRecharge) => getCurrentApproverSummaryText(row)
},
{
prop: 'processing_status_name',
label: '业务处理状态',
width: 140,
showOverflowTooltip: true,
formatter: (row: AgentRecharge) => getProcessingStatusText(row)
},
{
prop: 'payment_method',
label: '支付方式',
width: 140,
formatter: (row: AgentRecharge) => getPaymentMethodText(row.payment_method)
},
{
prop: 'payment_channel',
label: '支付通道',
width: 120,
formatter: (row: AgentRecharge) => row.payment_channel || '-'
},
{
prop: 'remark',
label: '运营备注',
minWidth: 180,
showOverflowTooltip: true,
formatter: (row: AgentRecharge) => row.remark || '-'
},
{
prop: 'rejection_reason',
label: '驳回原因',
minWidth: 180,
showOverflowTooltip: true,
formatter: (row: AgentRecharge) => formatRejectionReason(row.rejection_reason)
},
{
prop: 'created_at',
label: '创建时间',
width: 180,
formatter: (row: AgentRecharge) => formatDateTime(row.created_at)
},
{
prop: 'paid_at',
label: '支付时间',
width: 180,
formatter: (row: AgentRecharge) => (row.paid_at ? formatDateTime(row.paid_at) : '-')
},
{
prop: 'completed_at',
label: '完成时间',
width: 180,
formatter: (row: AgentRecharge) =>
row.completed_at ? formatDateTime(row.completed_at) : '-'
},
{
prop: 'updated_at',
label: '更新时间',
width: 180,
formatter: (row: AgentRecharge) => formatDateTime(row.updated_at)
}
},
{
prop: 'shop_name',
label: '店铺名称',
minWidth: 150,
showOverflowTooltip: true
},
{
prop: 'recharge_source',
label: '充值来源',
width: 140,
formatter: (row: AgentRecharge) => getRechargeSourceText(row)
},
{
prop: 'amount',
label: '充值金额',
width: 120,
formatter: (row: AgentRecharge) => formatCurrency(row.amount)
},
{
prop: 'status',
label: '状态',
width: 100,
formatter: (row: AgentRecharge) => {
return h(ElTag, { type: getStatusType(row.status) }, () => row.status_name || '-')
}
},
{
prop: 'approval_provider',
label: '审批渠道',
width: 110,
formatter: (row: AgentRecharge) => {
if (row.approval_provider === 'wecom' || row.approval_source === 'wecom') return '企微'
if (row.approval_source === 'legacy') return '历史审批'
return row.approval_provider || row.approval_source || '-'
}
},
{
prop: 'submitter_name',
label: '提交人',
width: 120,
showOverflowTooltip: true,
formatter: (row: AgentRecharge) => row.submitter_name || '-'
},
{
prop: 'approval_status',
label: '审批状态',
width: 120,
formatter: (row: AgentRecharge) => getApprovalStatusText(row)
},
{
prop: 'current_approver_summary',
label: '当前审批人摘要',
minWidth: 180,
showOverflowTooltip: true,
formatter: (row: AgentRecharge) => getCurrentApproverSummaryText(row)
},
{
prop: 'processing_status_name',
label: '业务处理状态',
width: 140,
showOverflowTooltip: true,
formatter: (row: AgentRecharge) => getProcessingStatusText(row)
},
{
prop: 'payment_method',
label: '支付方式',
width: 140,
formatter: (row: AgentRecharge) => getPaymentMethodText(row.payment_method)
},
{
prop: 'payment_channel',
label: '支付通道',
width: 120,
formatter: (row: AgentRecharge) => row.payment_channel || '-'
},
{
prop: 'remark',
label: '运营备注',
minWidth: 180,
showOverflowTooltip: true,
formatter: (row: AgentRecharge) => row.remark || '-'
},
{
prop: 'rejection_reason',
label: '驳回原因',
minWidth: 180,
showOverflowTooltip: true,
formatter: (row: AgentRecharge) => formatRejectionReason(row.rejection_reason)
},
{
prop: 'created_at',
label: '创建时间',
width: 180,
formatter: (row: AgentRecharge) => formatDateTime(row.created_at)
},
{
prop: 'paid_at',
label: '支付时间',
width: 180,
formatter: (row: AgentRecharge) => (row.paid_at ? formatDateTime(row.paid_at) : '-')
},
{
prop: 'completed_at',
label: '完成时间',
width: 180,
formatter: (row: AgentRecharge) => (row.completed_at ? formatDateTime(row.completed_at) : '-')
},
{
prop: 'updated_at',
label: '更新时间',
width: 180,
formatter: (row: AgentRecharge) => formatDateTime(row.updated_at)
}
])
].filter(({ prop }) => shouldShowInternalColumn(prop))
)
onMounted(() => {
getTableData()
@@ -1294,6 +1311,37 @@
})
}
// 补发历史线下代理充值审批
const handleTriggerApproval = (row: AgentRecharge) => {
if (triggerApprovalLoading.value) return
ElMessageBox.confirm(`确定要为充值单号 ${row.recharge_no} 补发企微审批吗?`, '补发审批', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
triggerApprovalLoading.value = true
try {
const res = await AgentRechargeService.triggerApproval(row.id)
if (res.code !== 0) {
ElMessage.error(res.msg || '补发审批失败')
return
}
ElMessage.success('补发审批成功')
await getTableData()
} catch (error) {
console.error(error)
ElMessage.error('补发审批失败')
} finally {
triggerApprovalLoading.value = false
}
})
.catch(() => {
// 用户取消
})
}
// 处理名称点击
const handleNameClick = (row: AgentRecharge) => {
if (hasAuth('agent_recharge:detail_page')) {
@@ -1317,7 +1365,8 @@
hasAuth,
onViewPaymentVoucher: handleViewPaymentVoucher,
onConfirmPayment: handleShowConfirmPay,
onReject: handleShowReject
onReject: handleShowReject,
onTriggerApproval: handleTriggerApproval
})
if (isPlatformAccount.value && hasAuth(AUDIT_PERMISSIONS.agentRechargeEntry)) {
const resourceTarget = resolveAuditResourceTarget({
@@ -1352,6 +1401,13 @@
type: 'primary'
})
}
const paymentVoucherIndex = actions.findIndex((action) => action.label === '支付凭证')
if (paymentVoucherIndex > 0) {
const [paymentVoucherAction] = actions.splice(paymentVoucherIndex, 1)
actions.unshift(paymentVoucherAction)
}
return actions
}

View File

@@ -10,34 +10,6 @@
返回
</ElButton>
<h2 class="detail-title">{{ pageTitle }}</h2>
<ElButton
v-if="refund && isPlatformUser && hasAuth(AUDIT_PERMISSIONS.refundEntry)"
type="primary"
plain
@click="openRefundAudit"
>
审计记录
</ElButton>
<ElButton
v-if="refund && isPlatformUser && hasAuth(AUDIT_PERMISSIONS.refundFinanceEntry)"
type="primary"
plain
@click="openRefundFinance"
>
资金链路
</ElButton>
<ElButton
v-if="
refund?.approval_instance_id &&
isPlatformUser &&
hasAuth(AUDIT_PERMISSIONS.refundApprovalEntry)
"
type="primary"
plain
@click="openApprovalAudit"
>
审批审计
</ElButton>
<ElButton
v-if="refund && canResubmit(refund) && hasAuth('refund:resubmit')"
type="primary"
@@ -148,12 +120,6 @@
import VoucherUpload from '@/components/business/VoucherUpload.vue'
import { useAuth } from '@/composables/useAuth'
import { useUserStore } from '@/store/modules/user'
import { AUDIT_PERMISSIONS } from '@/config/constants/audit'
import {
resolveAuditResourceTarget,
resolveFinanceAuditTarget
} from '@/utils/business/auditNavigation'
import { openAuditInvestigation } from '@/components/business/audit/investigationController'
defineOptions({ name: 'RefundDetail' })
@@ -161,29 +127,11 @@
const router = useRouter()
const { hasAuth } = useAuth()
const userStore = useUserStore()
const isRestrictedCustomerRole = computed(() => [3, 4].includes(Number(userStore.info.user_type)))
const loading = ref(false)
const refund = ref<Refund | null>(null)
const refundVoucherFileKeys = ref<string[]>([])
const isPlatformUser = computed(() => [1, 2].includes(Number(userStore.info.user_type)))
const openRefundAudit = () => {
const target = resolveAuditResourceTarget({
resourceType: 'refund',
internalId: refund.value?.id
})
if (target) openAuditInvestigation(target)
}
const openRefundFinance = () => {
const target = resolveFinanceAuditTarget('refund_id', refund.value?.id)
if (target) openAuditInvestigation(target)
}
const openApprovalAudit = () => {
const target = resolveAuditResourceTarget({
resourceType: 'approval_instance',
internalId: refund.value?.approval_instance_id
})
if (target) openAuditInvestigation(target)
}
const pageTitle = computed(() => `退款详情`)
@@ -291,7 +239,15 @@
{ label: '资产标识符', prop: 'asset_identifier' },
{ label: '资产类型', prop: 'asset_type' },
{ label: '退款状态', prop: 'status_name', formatter: (value) => value || '-' },
{ label: '提交人', prop: 'submitter_name', formatter: (value) => value || '-' },
...(!isRestrictedCustomerRole.value
? [
{
label: '提交人',
prop: 'submitter_name',
formatter: (value: string | null | undefined) => value || '-'
}
]
: []),
{
label: '申请退款金额',
formatter: (_, data) => formatCurrency(data.requested_refund_amount)
@@ -304,24 +260,28 @@
label: '实收金额',
formatter: (_, data) => formatCurrency(data.actual_received_amount)
},
{
label: '退款原因',
prop: 'refund_reason',
formatter: (value) => value || '-',
fullWidth: true
},
{
label: '拒绝原因',
prop: 'reject_reason',
formatter: (value) => value || '-',
fullWidth: true
},
{
label: '备注',
prop: 'remark',
formatter: (value) => value || '-',
fullWidth: true
},
...(!isRestrictedCustomerRole.value
? [
{
label: '退款原因',
prop: 'refund_reason',
formatter: (value: string | null | undefined) => value || '-',
fullWidth: true
},
{
label: '拒绝原因',
prop: 'reject_reason',
formatter: (value: string | null | undefined) => value || '-',
fullWidth: true
},
{
label: '备注',
prop: 'remark',
formatter: (value: string | null | undefined) => value || '-',
fullWidth: true
}
]
: []),
{
label: '退款凭证',
fullWidth: true,
@@ -360,7 +320,14 @@
{
title: '企微审批信息',
fields: [
{ label: '审批渠道', formatter: (_, data) => getApprovalProviderText(data) },
...(!isRestrictedCustomerRole.value
? [
{
label: '审批渠道',
formatter: (_: unknown, data: Refund) => getApprovalProviderText(data)
}
]
: []),
{
label: '审批来源',
formatter: (_, data) => data.approval?.source || data.approval_source || '-'
@@ -370,10 +337,14 @@
label: '审批状态',
formatter: (_, data) => getRefundApprovalStatusText(data)
},
{
label: '当前审批人摘要',
formatter: (_, data) => data.current_approver_summary || '-'
},
...(!isRestrictedCustomerRole.value
? [
{
label: '当前审批人摘要',
formatter: (_: unknown, data: Refund) => data.current_approver_summary || '-'
}
]
: []),
{ label: '模板版本', prop: 'approval.template_version' },
{
label: '申请人',
@@ -417,10 +388,15 @@
{
title: '业务处理结果',
fields: [
{
label: '处理状态',
formatter: (_, data) => data.processing_status_name || data.processing_status || '-'
},
...(!isRestrictedCustomerRole.value
? [
{
label: '处理状态',
formatter: (_: unknown, data: Refund) =>
data.processing_status_name || data.processing_status || '-'
}
]
: []),
{
label: '失败摘要',
formatter: (_, data) => data.processing_failure_summary || data.error_summary || '-',

View File

@@ -143,17 +143,17 @@
</template>
<script setup lang="ts">
import { h } from 'vue'
import { computed, h } from 'vue'
import { useRouter } from 'vue-router'
import { RefundService, ShopService, OrderService } from '@/api/modules'
import { ElMessage, ElTag, ElButton } from 'element-plus'
import { ElMessage, ElMessageBox, ElTag, ElButton } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import type {
Refund,
RefundQueryParams,
import {
RefundStatus,
ResubmitRefundRequest,
RefundAttachment
type Refund,
type RefundQueryParams,
type ResubmitRefundRequest,
type RefundAttachment
} from '@/types/api'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
@@ -183,10 +183,23 @@
const router = useRouter()
const userStore = useUserStore()
const { hasAuth } = useAuth()
const isRestrictedCustomerRole = computed(() => [3, 4].includes(Number(userStore.info.user_type)))
const hiddenInternalColumnProps = new Set([
'approval_provider',
'submitter_name',
'current_approver_summary',
'processing_status_name',
'refund_reason',
'reject_reason',
'remark'
])
const shouldShowInternalColumn = (prop?: string) =>
!isRestrictedCustomerRole.value || !hiddenInternalColumnProps.has(prop || '')
const loading = ref(false)
const resubmitLoading = ref(false)
const resubmitVoucherUploading = ref(false)
const triggerApprovalLoading = ref(false)
const tableRef = ref()
const resubmitUploadRef = ref<InstanceType<typeof VoucherUpload>>()
const createDialogVisible = ref(false)
@@ -303,7 +316,7 @@
{ label: '创建时间', prop: 'created_at' },
{ label: '审批时间', prop: 'processed_at' },
{ label: '更新时间', prop: 'updated_at' }
]
].filter(({ prop }) => shouldShowInternalColumn(prop))
const resubmitFormRef = ref<FormInstance>()
@@ -344,175 +357,177 @@
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'refund_no',
label: '退款单号',
minWidth: 210,
formatter: (row: Refund) => {
return h(
'span',
{
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
onClick: (e: MouseEvent) => {
e.stopPropagation()
handleNameClick(row)
}
},
row.refund_no
)
const { columnChecks, columns } = useCheckedColumns(() =>
[
{
prop: 'refund_no',
label: '退款单号',
minWidth: 210,
formatter: (row: Refund) => {
return h(
'span',
{
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
onClick: (e: MouseEvent) => {
e.stopPropagation()
handleNameClick(row)
}
},
row.refund_no
)
}
},
{
prop: 'shop_name',
label: '店铺名称',
width: 160
},
{
prop: 'order_no',
label: '订单号',
width: 180,
showOverflowTooltip: true
},
{
prop: 'asset_identifier',
label: '资产标识符',
width: 200,
showOverflowTooltip: true
},
{
prop: 'asset_type',
label: '资产类型',
width: 100,
formatter: (row: Refund) =>
row.asset_type === 'device'
? '设备'
: row.asset_type === 'card'
? '单卡'
: row.asset_type === 'iot_card'
? 'IoT卡'
: row.asset_type || '-'
},
{
prop: 'requested_refund_amount',
label: '申请退款金额',
width: 150,
formatter: (row: Refund) => formatCurrency(row.requested_refund_amount)
},
{
prop: 'approved_refund_amount',
label: '实际退款金额',
width: 150,
formatter: (row: Refund) => formatCurrency(row.approved_refund_amount)
},
{
prop: 'actual_received_amount',
label: '实收金额',
width: 120,
formatter: (row: Refund) => formatCurrency(row.actual_received_amount)
},
{
prop: 'status',
label: '状态',
width: 100,
formatter: (row: Refund) => {
return h(ElTag, { type: getStatusType(row.status) }, () => row.status_name || '-')
}
},
{
prop: 'approval_provider',
label: '审批渠道',
width: 110,
formatter: (row: Refund) => {
if (row.approval_provider === 'wecom' || row.approval_source === 'wecom') return '企微'
if (row.approval_source === 'legacy') return '历史审批'
return row.approval_provider || row.approval_source || '-'
}
},
{
prop: 'submitter_name',
label: '提交人',
width: 120,
showOverflowTooltip: true,
formatter: (row: Refund) => row.submitter_name || '-'
},
{
prop: 'approval_status',
label: '审批状态',
width: 120,
formatter: (row: Refund) => row.approval_status_name || '-'
},
{
prop: 'current_approver_summary',
label: '当前审批人摘要',
minWidth: 180,
showOverflowTooltip: true,
formatter: (row: Refund) => getCurrentApproverSummaryText(row)
},
{
prop: 'processing_status_name',
label: '业务处理状态',
width: 140,
showOverflowTooltip: true,
formatter: (row: Refund) => getProcessingStatusText(row)
},
{
prop: 'refund_reason',
label: '退款原因',
minWidth: 200,
formatter: (row: Refund) => row.refund_reason || '-'
},
{
prop: 'reject_reason',
label: '拒绝原因',
minWidth: 200,
formatter: (row: Refund) => row.reject_reason || '-'
},
{
prop: 'remark',
label: '审批备注',
minWidth: 200,
formatter: (row: Refund) => row.remark || '-'
},
{
prop: 'asset_reset',
label: '资产重置',
width: 100,
formatter: (row: Refund) => {
return h(ElTag, { type: row.asset_reset ? 'success' : 'info' }, () =>
row.asset_reset ? '是' : '否'
)
}
},
{
prop: 'commission_deducted',
label: '佣金扣除',
width: 100,
formatter: (row: Refund) => {
return h(ElTag, { type: row.commission_deducted ? 'success' : 'info' }, () =>
row.commission_deducted ? '是' : '否'
)
}
},
{
prop: 'created_at',
label: '创建时间',
width: 180,
formatter: (row: Refund) => formatDateTime(row.created_at)
},
{
prop: 'processed_at',
label: '审批时间',
width: 180,
formatter: (row: Refund) => (row.processed_at ? formatDateTime(row.processed_at) : '-')
},
{
prop: 'updated_at',
label: '更新时间',
width: 180,
formatter: (row: Refund) => formatDateTime(row.updated_at)
}
},
{
prop: 'shop_name',
label: '店铺名称',
width: 160
},
{
prop: 'order_no',
label: '订单号',
width: 180,
showOverflowTooltip: true
},
{
prop: 'asset_identifier',
label: '资产标识符',
width: 200,
showOverflowTooltip: true
},
{
prop: 'asset_type',
label: '资产类型',
width: 100,
formatter: (row: Refund) =>
row.asset_type === 'device'
? '设备'
: row.asset_type === 'card'
? '单卡'
: row.asset_type === 'iot_card'
? 'IoT卡'
: row.asset_type || '-'
},
{
prop: 'requested_refund_amount',
label: '申请退款金额',
width: 150,
formatter: (row: Refund) => formatCurrency(row.requested_refund_amount)
},
{
prop: 'approved_refund_amount',
label: '实际退款金额',
width: 150,
formatter: (row: Refund) => formatCurrency(row.approved_refund_amount)
},
{
prop: 'actual_received_amount',
label: '实收金额',
width: 120,
formatter: (row: Refund) => formatCurrency(row.actual_received_amount)
},
{
prop: 'status',
label: '状态',
width: 100,
formatter: (row: Refund) => {
return h(ElTag, { type: getStatusType(row.status) }, () => row.status_name || '-')
}
},
{
prop: 'approval_provider',
label: '审批渠道',
width: 110,
formatter: (row: Refund) => {
if (row.approval_provider === 'wecom' || row.approval_source === 'wecom') return '企微'
if (row.approval_source === 'legacy') return '历史审批'
return row.approval_provider || row.approval_source || '-'
}
},
{
prop: 'submitter_name',
label: '提交人',
width: 120,
showOverflowTooltip: true,
formatter: (row: Refund) => row.submitter_name || '-'
},
{
prop: 'approval_status',
label: '审批状态',
width: 120,
formatter: (row: Refund) => row.approval_status_name || '-'
},
{
prop: 'current_approver_summary',
label: '当前审批人摘要',
minWidth: 180,
showOverflowTooltip: true,
formatter: (row: Refund) => getCurrentApproverSummaryText(row)
},
{
prop: 'processing_status_name',
label: '业务处理状态',
width: 140,
showOverflowTooltip: true,
formatter: (row: Refund) => getProcessingStatusText(row)
},
{
prop: 'refund_reason',
label: '退款原因',
minWidth: 200,
formatter: (row: Refund) => row.refund_reason || '-'
},
{
prop: 'reject_reason',
label: '拒绝原因',
minWidth: 200,
formatter: (row: Refund) => row.reject_reason || '-'
},
{
prop: 'remark',
label: '审批备注',
minWidth: 200,
formatter: (row: Refund) => row.remark || '-'
},
{
prop: 'asset_reset',
label: '资产重置',
width: 100,
formatter: (row: Refund) => {
return h(ElTag, { type: row.asset_reset ? 'success' : 'info' }, () =>
row.asset_reset ? '是' : '否'
)
}
},
{
prop: 'commission_deducted',
label: '佣金扣除',
width: 100,
formatter: (row: Refund) => {
return h(ElTag, { type: row.commission_deducted ? 'success' : 'info' }, () =>
row.commission_deducted ? '是' : '否'
)
}
},
{
prop: 'created_at',
label: '创建时间',
width: 180,
formatter: (row: Refund) => formatDateTime(row.created_at)
},
{
prop: 'processed_at',
label: '审批时间',
width: 180,
formatter: (row: Refund) => (row.processed_at ? formatDateTime(row.processed_at) : '-')
},
{
prop: 'updated_at',
label: '更新时间',
width: 180,
formatter: (row: Refund) => formatDateTime(row.updated_at)
}
])
].filter(({ prop }) => shouldShowInternalColumn(prop))
)
onMounted(() => {
getTableData()
@@ -735,6 +750,42 @@
)
}
const canTriggerApproval = (row: Refund) => {
const approvalStatusEmpty = row.approval_status === undefined || row.approval_status === null
return row.status === RefundStatus.PENDING && approvalStatusEmpty
}
// 补发历史退款审批
const handleTriggerApproval = (row: Refund) => {
if (triggerApprovalLoading.value) return
ElMessageBox.confirm(`确定要为退款单号 ${row.refund_no} 补发企微审批吗?`, '补发审批', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
triggerApprovalLoading.value = true
try {
const res = await RefundService.triggerApproval(row.id)
if (res.code !== 0) {
ElMessage.error(res.msg || '补发审批失败')
return
}
ElMessage.success('补发审批成功')
await getTableData()
} catch (error) {
console.error(error)
ElMessage.error('补发审批失败')
} finally {
triggerApprovalLoading.value = false
}
})
.catch(() => {
// 用户取消
})
}
// 获取操作按钮
const getActions = (row: Refund) => {
const actions: any[] = []
@@ -743,6 +794,15 @@
[1, 2].includes(userStore.getUserInfo.user_type || 0) &&
hasAuth(AUDIT_PERMISSIONS.refundEntry)
) {
const voucherKeys = getRefundAttachmentKeys(row)
if (voucherKeys.length && hasAuth('refund:view_voucher')) {
actions.push({
label: '退款凭证',
handler: () => handleViewRefundVoucher(row),
type: 'primary'
})
}
const auditTarget = resolveAuditResourceTarget({
userType: userStore.getUserInfo.user_type,
resourceType: 'refund',
@@ -754,6 +814,7 @@
handler: () => openAuditInvestigation(auditTarget),
type: 'primary'
})
if (hasAuth(AUDIT_PERMISSIONS.refundFinanceEntry)) {
const financeTarget = resolveFinanceAuditTarget('refund_id', row.id)
if (financeTarget)
@@ -775,7 +836,18 @@
type: 'primary'
})
}
const voucherKeys = getRefundAttachmentKeys(row)
if (
[1, 2].includes(userStore.getUserInfo.user_type || 0) &&
canTriggerApproval(row) &&
hasAuth('refund:trigger_approval')
) {
actions.push({
label: '补发审批',
handler: () => handleTriggerApproval(row),
type: 'primary'
})
}
if (canResubmit(row) && hasAuth('refund:resubmit')) {
actions.push({
@@ -785,14 +857,6 @@
})
}
if (voucherKeys.length && hasAuth('refund:view_voucher')) {
actions.push({
label: '查看退款凭证',
handler: () => handleViewRefundVoucher(row),
type: 'primary'
})
}
return actions
}

View File

@@ -10,22 +10,6 @@
返回
</ElButton>
<h2 class="detail-title">订单详情</h2>
<ElButton
v-if="detailData && isPlatformUser && hasAuth(AUDIT_PERMISSIONS.orderEntry)"
type="primary"
plain
@click="openOrderAudit"
>
审计记录
</ElButton>
<ElButton
v-if="detailData && isPlatformUser && hasAuth(AUDIT_PERMISSIONS.orderFinanceEntry)"
type="primary"
plain
@click="openOrderFinance"
>
资金链路
</ElButton>
</div>
<!-- 详情内容 -->
@@ -98,12 +82,6 @@
import { formatDateTime } from '@/utils/business/format'
import { hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
import { AUDIT_PERMISSIONS } from '@/config/constants/audit'
import {
resolveAuditResourceTarget,
resolveFinanceAuditTarget
} from '@/utils/business/auditNavigation'
import { openAuditInvestigation } from '@/components/business/audit/investigationController'
defineOptions({ name: 'OrderDetail' })
@@ -115,18 +93,6 @@
const loading = ref(false)
const detailData = ref<Order | null>(null)
const paymentVoucherFileKeys = ref<string[]>([])
const isPlatformUser = computed(() => [1, 2].includes(Number(userStore.info.user_type)))
const openOrderAudit = () => {
const target = resolveAuditResourceTarget({
resourceType: 'order',
internalId: detailData.value?.id
})
if (target) openAuditInvestigation(target)
}
const openOrderFinance = () => {
const target = resolveFinanceAuditTarget('order_id', detailData.value?.id)
if (target) openAuditInvestigation(target)
}
// 格式化货币 - 将分转换为元
const formatCurrency = (amount: number): string => {

View File

@@ -483,15 +483,15 @@
}
},
{
label: '开始至结束',
label: '起止时间',
prop: 'dateRange',
type: 'date',
type: 'datetimerange',
config: {
type: 'daterange',
type: 'datetimerange',
rangeSeparator: '至',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
valueFormat: 'YYYY-MM-DD'
valueFormat: 'YYYY-MM-DDTHH:mm:ssZ'
}
}
]