feat: 支付商户
This commit is contained in:
@@ -0,0 +1,717 @@
|
||||
<template>
|
||||
<div class="merchant-management">
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchItems"
|
||||
:show-expand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="loadMerchants"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton v-if="canManage" type="primary" :icon="Plus" @click="showCreateDrawer">
|
||||
新增支付商户
|
||||
</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="merchants"
|
||||
:currentPage="pagination.page"
|
||||
:pageSize="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:actions="getActions"
|
||||
:actionsWidth="180"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<ElDrawer
|
||||
v-model="formDrawerVisible"
|
||||
:title="formMode === 'create' ? '新增支付商户' : '编辑支付商户'"
|
||||
size="680px"
|
||||
destroy-on-close
|
||||
@closed="clearSensitiveForm"
|
||||
>
|
||||
<ElAlert
|
||||
title="支付凭证仅用于本次写入,页面不会回显、缓存或记录凭证内容。"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
|
||||
<ElForm ref="formRef" :model="form" :rules="formRules" label-width="120px">
|
||||
<ElFormItem label="商户名称" prop="name">
|
||||
<ElInput v-model="form.name" maxlength="100" placeholder="请输入商户名称" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="支付方式" prop="payment_method">
|
||||
<ElSelect
|
||||
v-model="form.payment_method"
|
||||
:disabled="formMode === 'edit'"
|
||||
style="width: 100%"
|
||||
@change="handlePaymentMethodChange"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in PAYMENT_METHOD_OPTIONS"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="服务商类型" prop="provider_type">
|
||||
<ElSelect
|
||||
v-model="form.provider_type"
|
||||
:disabled="formMode === 'edit'"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in providerOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="商户标识" prop="merchant_identity">
|
||||
<ElInput
|
||||
v-model="form.merchant_identity"
|
||||
:disabled="formMode === 'edit'"
|
||||
maxlength="128"
|
||||
placeholder="请输入商户号或应用标识"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="启停状态">
|
||||
<ElSwitch v-model="form.enabled" active-text="启用" inactive-text="停用" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="备注">
|
||||
<ElInput
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="formMode === 'edit'" label="凭证状态">
|
||||
<ElTag :type="credentialConfigured ? 'success' : 'info'">
|
||||
{{ credentialConfigured ? '已配置' : '未配置' }}
|
||||
</ElTag>
|
||||
<span class="credential-version">版本:{{ form.credential_version || '-' }}</span>
|
||||
<ElButton type="primary" link @click="startCredentialReplacement">更换凭证</ElButton>
|
||||
</ElFormItem>
|
||||
<template v-if="showCredentialEditor">
|
||||
<ElDivider content-position="left">写入支付凭证</ElDivider>
|
||||
<ElAlert
|
||||
title="凭证值以密码方式输入,提交后立即清空。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
<div class="credential-list">
|
||||
<div
|
||||
v-for="(entry, index) in form.credentials"
|
||||
:key="entry.localId"
|
||||
class="credential-row"
|
||||
>
|
||||
<ElInput
|
||||
v-model="entry.key"
|
||||
placeholder="凭证字段名"
|
||||
maxlength="100"
|
||||
aria-label="凭证字段名"
|
||||
/>
|
||||
<ElInput
|
||||
v-model="entry.value"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="凭证值(不可回显)"
|
||||
autocomplete="new-password"
|
||||
aria-label="凭证值"
|
||||
/>
|
||||
<ElButton
|
||||
type="danger"
|
||||
link
|
||||
:icon="Delete"
|
||||
aria-label="删除凭证字段"
|
||||
@click="removeCredential(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ElButton type="primary" plain :icon="Plus" @click="addCredential">添加凭证字段</ElButton>
|
||||
<div v-if="credentialError" class="field-error" role="alert">
|
||||
{{ credentialError }}
|
||||
</div>
|
||||
</template>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="formDrawerVisible = false">取消</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:loading="submitLoading"
|
||||
:disabled="!canManage"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDrawer>
|
||||
|
||||
<ElDrawer v-model="detailDrawerVisible" title="支付商户详情" size="560px">
|
||||
<ElDescriptions v-if="detail" :column="1" border>
|
||||
<ElDescriptionsItem label="商户名称">{{ detail.name }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="支付方式">{{
|
||||
getPaymentMethodLabel(detail.payment_method)
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="服务商类型">{{
|
||||
getPaymentProviderLabel(detail.provider_type)
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="商户标识">{{ detail.merchant_identity }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="启停状态">{{
|
||||
detail.enabled ? '启用' : '停用'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="支付凭证">{{
|
||||
Number(detail.credential_version) > 0 ? '已配置' : '未配置'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="凭证版本">{{
|
||||
detail.credential_version || '-'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="备注">{{ detail.remark || '-' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="更新时间">{{
|
||||
formatDateTime(detail.updated_at)
|
||||
}}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</ElDrawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||
import { Delete, Plus } from '@element-plus/icons-vue'
|
||||
import { ElButton, ElMessage, ElMessageBox, ElTag } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { PaymentMerchantPoolsService } from '@/api/modules'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { usePermission } from '@/composables/usePermission'
|
||||
import type { SearchFormItem } from '@/types/component'
|
||||
import {
|
||||
PAYMENT_METHOD_OPTIONS,
|
||||
PAYMENT_PROVIDER_OPTIONS,
|
||||
getPaymentMethodLabel,
|
||||
getPaymentProviderLabel,
|
||||
type PaymentCredentialEntry,
|
||||
type PaymentCredentials,
|
||||
type PaymentMerchant,
|
||||
type PaymentMerchantMethod,
|
||||
type PaymentMerchantPageResult,
|
||||
type PaymentMerchantProviderType,
|
||||
type PaymentMerchantQueryParams
|
||||
} from '@/types/api/paymentMerchantPools'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
|
||||
defineOptions({ name: 'PaymentMerchantManagement' })
|
||||
|
||||
type FilterVo = string | number | undefined | null | unknown[]
|
||||
|
||||
const { isPlatformAccount } = usePermission()
|
||||
const canManage = computed(() => isPlatformAccount.value)
|
||||
|
||||
// 列表查询
|
||||
const searchForm = reactive<Record<string, FilterVo>>({
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
name: '',
|
||||
payment_method: null,
|
||||
enabled: null
|
||||
})
|
||||
|
||||
const merchants = ref<PaymentMerchant[]>([])
|
||||
const pagination = reactive({ page: 1, page_size: 10, total: 0 })
|
||||
const loading = ref(false)
|
||||
|
||||
const searchItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '商户名称',
|
||||
prop: 'name',
|
||||
type: 'input',
|
||||
config: { clearable: true, placeholder: '请输入商户名称' }
|
||||
},
|
||||
{
|
||||
label: '支付方式',
|
||||
prop: 'payment_method',
|
||||
type: 'select',
|
||||
options: PAYMENT_METHOD_OPTIONS.map((item) => ({
|
||||
label: item.label,
|
||||
value: item.value
|
||||
})),
|
||||
config: { clearable: true }
|
||||
},
|
||||
{
|
||||
label: '启停状态',
|
||||
prop: 'enabled',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '启用', value: true },
|
||||
{ label: '停用', value: false }
|
||||
],
|
||||
config: { clearable: true }
|
||||
}
|
||||
]
|
||||
|
||||
const columnOptions = [
|
||||
{ label: '商户名称', prop: 'name', minWidth: 180 },
|
||||
{
|
||||
label: '支付方式',
|
||||
prop: 'payment_method',
|
||||
width: 110,
|
||||
formatter: (row: PaymentMerchant) => getPaymentMethodLabel(row.payment_method)
|
||||
},
|
||||
{
|
||||
label: '服务商类型',
|
||||
prop: 'provider_type',
|
||||
width: 140,
|
||||
formatter: (row: PaymentMerchant) => getPaymentProviderLabel(row.provider_type)
|
||||
},
|
||||
{ label: '商户标识', prop: 'merchant_identity', minWidth: 180 },
|
||||
{
|
||||
label: '启停状态',
|
||||
prop: 'enabled',
|
||||
width: 110,
|
||||
formatter: (row: PaymentMerchant) =>
|
||||
h(
|
||||
ElTag,
|
||||
{ type: row.enabled ? 'success' : 'info' },
|
||||
{ default: () => (row.enabled ? '启用' : '停用') }
|
||||
)
|
||||
},
|
||||
{
|
||||
label: '凭证状态',
|
||||
prop: 'credential_version',
|
||||
width: 110,
|
||||
formatter: (row: PaymentMerchant) =>
|
||||
h(
|
||||
ElTag,
|
||||
{ type: Number(row.credential_version) > 0 ? 'success' : 'info' },
|
||||
{ default: () => (Number(row.credential_version) > 0 ? '已配置' : '未配置') }
|
||||
)
|
||||
},
|
||||
{
|
||||
label: '更新时间',
|
||||
prop: 'updated_at',
|
||||
width: 170,
|
||||
formatter: (row: PaymentMerchant) => formatDateTime(row.updated_at)
|
||||
},
|
||||
{ label: '备注', prop: 'remark', minWidth: 160 }
|
||||
]
|
||||
|
||||
const { columnChecks, columns } = useCheckedColumns(() => columnOptions)
|
||||
|
||||
// 列表加载
|
||||
const loadMerchants = async () => {
|
||||
if (!canManage.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const params: PaymentMerchantQueryParams = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.page_size
|
||||
}
|
||||
const name = searchForm.name as string | undefined
|
||||
if (name) params.name = name
|
||||
if (searchForm.payment_method)
|
||||
params.payment_method = searchForm.payment_method as PaymentMerchantMethod
|
||||
if (typeof searchForm.enabled === 'boolean') params.enabled = searchForm.enabled
|
||||
|
||||
const res = await PaymentMerchantPoolsService.getPaymentMerchants(params)
|
||||
if (res.code === 0) {
|
||||
const data = (res.data as PaymentMerchantPageResult) || {
|
||||
items: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
size: 10
|
||||
}
|
||||
merchants.value = data.items || []
|
||||
pagination.total = data.total || 0
|
||||
pagination.page = data.page || pagination.page
|
||||
pagination.page_size = data.size || pagination.page_size
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载支付商户失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
loadMerchants()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.name = ''
|
||||
searchForm.payment_method = null
|
||||
searchForm.enabled = null
|
||||
pagination.page = 1
|
||||
loadMerchants()
|
||||
}
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.page_size = size
|
||||
pagination.page = 1
|
||||
loadMerchants()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page
|
||||
loadMerchants()
|
||||
}
|
||||
|
||||
// 表单
|
||||
type FormMode = 'create' | 'edit'
|
||||
|
||||
const formDrawerVisible = ref(false)
|
||||
const formMode = ref<FormMode>('create')
|
||||
const submitLoading = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
const showCredentialEditor = ref(false)
|
||||
const credentialError = ref('')
|
||||
|
||||
const initialFormState = () => ({
|
||||
id: 0,
|
||||
name: '',
|
||||
payment_method: 'wechat' as PaymentMerchantMethod,
|
||||
provider_type: 'wechat' as PaymentMerchantProviderType,
|
||||
merchant_identity: '',
|
||||
enabled: true,
|
||||
remark: '',
|
||||
credential_version: 0,
|
||||
credentials: [] as PaymentCredentialEntry[]
|
||||
})
|
||||
|
||||
const form = reactive(initialFormState())
|
||||
|
||||
const providerOptions = computed(() =>
|
||||
PAYMENT_PROVIDER_OPTIONS.filter((option) => option.paymentMethod === form.payment_method)
|
||||
)
|
||||
|
||||
const credentialConfigured = computed(() => Number(form.credential_version) > 0)
|
||||
|
||||
const formRules = reactive<FormRules>({
|
||||
name: [
|
||||
{ required: true, message: '请输入商户名称', trigger: 'blur' },
|
||||
{ max: 100, message: '商户名称不超过 100 个字符', trigger: 'blur' }
|
||||
],
|
||||
payment_method: [{ required: true, message: '请选择支付方式', trigger: 'change' }],
|
||||
provider_type: [{ required: true, message: '请选择服务商类型', trigger: 'change' }],
|
||||
merchant_identity: [
|
||||
{ required: true, message: '请输入商户标识', trigger: 'blur' },
|
||||
{ max: 128, message: '商户标识不超过 128 个字符', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
const generateLocalId = () =>
|
||||
`cred-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
|
||||
|
||||
const resetSensitiveForm = () => {
|
||||
form.credentials = []
|
||||
showCredentialEditor.value = false
|
||||
credentialError.value = ''
|
||||
}
|
||||
|
||||
const clearSensitiveForm = () => {
|
||||
resetSensitiveForm()
|
||||
form.id = 0
|
||||
form.credential_version = 0
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
const buildCredentialPayload = (): PaymentCredentials | null => {
|
||||
const credentials: Record<string, string> = {}
|
||||
for (const entry of form.credentials) {
|
||||
const key = entry.key.trim()
|
||||
if (!key) {
|
||||
credentialError.value = '请填写凭证字段名'
|
||||
return null
|
||||
}
|
||||
if (!entry.value) {
|
||||
credentialError.value = `请填写凭证字段 ${key} 的值`
|
||||
return null
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(credentials, key)) {
|
||||
credentialError.value = `凭证字段 ${key} 重复`
|
||||
return null
|
||||
}
|
||||
credentials[key] = entry.value
|
||||
}
|
||||
if (form.credentials.length === 0) {
|
||||
credentialError.value = '请至少添加一个凭证字段'
|
||||
return null
|
||||
}
|
||||
credentialError.value = ''
|
||||
return credentials
|
||||
}
|
||||
|
||||
const showCreateDrawer = () => {
|
||||
if (!canManage.value) return
|
||||
Object.assign(form, initialFormState())
|
||||
formDrawerVisible.value = true
|
||||
formMode.value = 'create'
|
||||
showCredentialEditor.value = true
|
||||
addCredential()
|
||||
}
|
||||
|
||||
const startCredentialReplacement = () => {
|
||||
if (!canManage.value) return
|
||||
form.credentials = []
|
||||
showCredentialEditor.value = true
|
||||
addCredential()
|
||||
}
|
||||
|
||||
const addCredential = () => {
|
||||
form.credentials.push({ localId: generateLocalId(), key: '', value: '' })
|
||||
}
|
||||
|
||||
const removeCredential = (index: number) => {
|
||||
form.credentials.splice(index, 1)
|
||||
}
|
||||
|
||||
const handlePaymentMethodChange = (value: PaymentMerchantMethod) => {
|
||||
form.payment_method = value
|
||||
const firstMatch = PAYMENT_PROVIDER_OPTIONS.find((option) => option.paymentMethod === value)
|
||||
if (firstMatch) {
|
||||
form.provider_type = firstMatch.value
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!canManage.value) return
|
||||
if (!formRef.value) return
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
if (formMode.value === 'create' || showCredentialEditor.value) {
|
||||
const credentials = buildCredentialPayload()
|
||||
if (!credentials) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
if (formMode.value === 'create') {
|
||||
await PaymentMerchantPoolsService.createPaymentMerchant({
|
||||
name: form.name.trim(),
|
||||
payment_method: form.payment_method,
|
||||
provider_type: form.provider_type,
|
||||
merchant_identity: form.merchant_identity.trim(),
|
||||
credentials,
|
||||
enabled: form.enabled,
|
||||
remark: form.remark?.trim() || ''
|
||||
})
|
||||
ElMessage.success('支付商户创建成功')
|
||||
} else {
|
||||
await PaymentMerchantPoolsService.updatePaymentMerchant(form.id, {
|
||||
name: form.name.trim(),
|
||||
enabled: form.enabled,
|
||||
remark: form.remark?.trim() || '',
|
||||
credentials
|
||||
})
|
||||
ElMessage.success('支付凭证更新成功')
|
||||
}
|
||||
formDrawerVisible.value = false
|
||||
await loadMerchants()
|
||||
} catch (error) {
|
||||
console.error('提交支付商户失败:', error)
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
await PaymentMerchantPoolsService.updatePaymentMerchant(form.id, {
|
||||
name: form.name.trim(),
|
||||
enabled: form.enabled,
|
||||
remark: form.remark?.trim() || ''
|
||||
})
|
||||
ElMessage.success('支付商户已更新')
|
||||
formDrawerVisible.value = false
|
||||
await loadMerchants()
|
||||
} catch (error) {
|
||||
console.error('更新支付商户失败:', error)
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 详情 / 启停 / 删除
|
||||
const detailDrawerVisible = ref(false)
|
||||
const detail = ref<PaymentMerchant | null>(null)
|
||||
const tableRef = ref()
|
||||
|
||||
const showDetail = async (id: number) => {
|
||||
try {
|
||||
const res = await PaymentMerchantPoolsService.getPaymentMerchantById(id)
|
||||
if (res.code === 0) {
|
||||
detail.value = res.data
|
||||
detailDrawerVisible.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载支付商户详情失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const openEditDrawer = async (id: number) => {
|
||||
if (!canManage.value) return
|
||||
try {
|
||||
const res = await PaymentMerchantPoolsService.getPaymentMerchantById(id)
|
||||
if (res.code !== 0) return
|
||||
const merchant = res.data
|
||||
Object.assign(form, initialFormState(), {
|
||||
id: merchant.id,
|
||||
name: merchant.name,
|
||||
payment_method: merchant.payment_method,
|
||||
provider_type: merchant.provider_type,
|
||||
merchant_identity: merchant.merchant_identity,
|
||||
enabled: merchant.enabled,
|
||||
remark: merchant.remark || '',
|
||||
credential_version: merchant.credential_version
|
||||
})
|
||||
formMode.value = 'edit'
|
||||
showCredentialEditor.value = false
|
||||
formDrawerVisible.value = true
|
||||
} catch (error) {
|
||||
console.error('加载支付商户详情失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleEnabled = async (merchant: PaymentMerchant) => {
|
||||
if (!canManage.value) return
|
||||
const target = !merchant.enabled
|
||||
const action = target ? '启用' : '停用'
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认${action}商户 “${merchant.name}”?`, '操作确认', {
|
||||
type: 'warning'
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await PaymentMerchantPoolsService.updatePaymentMerchant(merchant.id, {
|
||||
enabled: target
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage.success(`已${action}`)
|
||||
await loadMerchants()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${action}支付商户失败:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDeleteMerchant = async (merchant: PaymentMerchant) => {
|
||||
if (!canManage.value) return
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认删除支付商户 “${merchant.name}”?该操作不可恢复。`,
|
||||
'删除确认',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await PaymentMerchantPoolsService.deletePaymentMerchant(merchant.id)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('已删除')
|
||||
if (pagination.total > 1 && merchants.value.length === 1 && pagination.page > 1) {
|
||||
pagination.page -= 1
|
||||
}
|
||||
await loadMerchants()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除支付商户失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const getActions = (row: PaymentMerchant) => [
|
||||
{
|
||||
label: '详情',
|
||||
type: 'primary' as const,
|
||||
handler: () => showDetail(row.id)
|
||||
},
|
||||
{
|
||||
label: '编辑',
|
||||
type: 'primary' as const,
|
||||
handler: () => openEditDrawer(row.id),
|
||||
permission: canManage.value ? '' : 'hidden'
|
||||
},
|
||||
{
|
||||
label: row.enabled ? '停用' : '启用',
|
||||
type: 'primary' as const,
|
||||
handler: () => toggleEnabled(row),
|
||||
permission: canManage.value ? '' : 'hidden'
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
type: 'danger' as const,
|
||||
handler: () => confirmDeleteMerchant(row),
|
||||
permission: canManage.value ? '' : 'hidden'
|
||||
}
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
if (canManage.value) {
|
||||
loadMerchants()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
resetSensitiveForm()
|
||||
merchants.value = []
|
||||
detail.value = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.merchant-management {
|
||||
.credential-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.credential-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr auto;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.credential-version {
|
||||
margin: 0 12px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.field-error {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,871 @@
|
||||
<template>
|
||||
<div class="pool-management">
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchItems"
|
||||
:show-expand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="loadPools"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton v-if="canManage" type="primary" :icon="Plus" @click="showCreateDrawer">
|
||||
新增商户池
|
||||
</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="pools"
|
||||
:currentPage="pagination.page"
|
||||
:pageSize="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:actions="getActions"
|
||||
:actionsWidth="220"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<ElDrawer
|
||||
v-model="formDrawerVisible"
|
||||
:title="formMode === 'create' ? '新增商户池' : '编辑商户池'"
|
||||
size="780px"
|
||||
destroy-on-close
|
||||
@closed="resetForm"
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="formRules" label-width="120px">
|
||||
<ElFormItem label="商户池名称" prop="name">
|
||||
<ElInput v-model="form.name" maxlength="100" placeholder="请输入商户池名称" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="支付方式" prop="payment_method">
|
||||
<ElSelect
|
||||
v-model="form.payment_method"
|
||||
:disabled="formMode === 'edit'"
|
||||
style="width: 100%"
|
||||
@change="handlePaymentMethodChange"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in PAYMENT_METHOD_OPTIONS"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="成员商户" prop="member_ids">
|
||||
<VueDraggable
|
||||
v-if="orderedMemberIds.length"
|
||||
v-model="orderedMemberIds"
|
||||
:animation="150"
|
||||
handle=".drag-handle"
|
||||
class="pool-member-list"
|
||||
>
|
||||
<div v-for="memberId in orderedMemberIds" :key="memberId" class="pool-member-row">
|
||||
<ElIcon class="drag-handle"><Rank /></ElIcon>
|
||||
<span class="member-name">{{ getMemberName(memberId) }}</span>
|
||||
<ElButton type="danger" link :icon="Delete" @click="removeMember(memberId)" />
|
||||
</div>
|
||||
</VueDraggable>
|
||||
<div v-else class="pool-member-empty">尚未选择成员,请从下方选择</div>
|
||||
<ElDivider />
|
||||
<ElSelect
|
||||
v-model="pendingMemberId"
|
||||
filterable
|
||||
placeholder="选择同支付方式的商户"
|
||||
style="width: 100%"
|
||||
:disabled="!form.payment_method"
|
||||
@change="appendMember"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in availableMerchantOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
:disabled="form.member_ids.includes(item.id)"
|
||||
/>
|
||||
</ElSelect>
|
||||
<div v-if="memberError" class="field-error" role="alert">{{ memberError }}</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="轮询策略" prop="strategy">
|
||||
<ElSelect v-model="form.strategy" style="width: 100%">
|
||||
<ElOption
|
||||
v-for="item in PAYMENT_POOL_STRATEGY_OPTIONS"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="form.strategy === 'amount' || form.strategy === 'count'"
|
||||
label="统计周期"
|
||||
prop="statistic_cycle"
|
||||
>
|
||||
<ElSelect v-model="form.statistic_cycle" style="width: 100%">
|
||||
<ElOption
|
||||
v-for="item in PAYMENT_STATISTIC_CYCLE_OPTIONS"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="form.strategy === 'amount'"
|
||||
label="金额阈值(元)"
|
||||
prop="threshold_amount_yuan"
|
||||
>
|
||||
<ElInputNumber
|
||||
v-model="form.threshold_amount_yuan"
|
||||
:min="0.01"
|
||||
:precision="2"
|
||||
:step="100"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-else-if="form.strategy === 'count'" label="笔数阈值" prop="threshold_count">
|
||||
<ElInputNumber
|
||||
v-model="form.threshold_count"
|
||||
:min="1"
|
||||
:precision="0"
|
||||
:step="1"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<template v-else-if="form.strategy === 'time'">
|
||||
<ElFormItem label="时间单位" prop="time_period_unit">
|
||||
<ElSelect v-model="form.time_period_unit" style="width: 100%">
|
||||
<ElOption
|
||||
v-for="item in PAYMENT_TIME_PERIOD_UNIT_OPTIONS"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="时间长度" prop="time_period_value">
|
||||
<ElInputNumber
|
||||
v-model="form.time_period_value"
|
||||
:min="1"
|
||||
:precision="0"
|
||||
:step="1"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="时间起点" prop="time_period_started_at">
|
||||
<ElDatePicker
|
||||
v-model="form.time_period_started_at"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="请选择时间起点"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
<ElFormItem label="启停状态">
|
||||
<ElSwitch v-model="form.enabled" active-text="启用" inactive-text="停用" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="备注">
|
||||
<ElInput
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="formDrawerVisible = false">取消</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:loading="submitLoading"
|
||||
:disabled="!canManage"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDrawer>
|
||||
|
||||
<ElDrawer v-model="detailDrawerVisible" title="商户池详情" size="640px">
|
||||
<ElDescriptions v-if="detail" :column="1" border>
|
||||
<ElDescriptionsItem label="商户池名称">{{ detail.name }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="支付方式">{{
|
||||
getPaymentMethodLabel(detail.payment_method)
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="成员数量">{{ detail.member_ids.length }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="轮询策略">{{
|
||||
getPaymentPoolStrategyLabel(detail.strategy)
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem
|
||||
v-if="detail.strategy === 'amount' || detail.strategy === 'count'"
|
||||
label="统计周期"
|
||||
>
|
||||
{{ getPaymentStatisticCycleLabel(detail.statistic_cycle) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem v-if="detail.strategy === 'amount'" label="金额阈值">
|
||||
{{ (Number(detail.threshold_amount) / 100).toFixed(2) }} 元
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem v-if="detail.strategy === 'count'" label="笔数阈值">
|
||||
{{ detail.threshold_count }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem v-if="detail.strategy === 'time'" label="时间周期">
|
||||
{{ detail.time_period_value }}
|
||||
{{ getPaymentTimePeriodUnitLabel(detail.time_period_unit) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="启停状态">
|
||||
{{ detail.enabled ? '启用' : '停用' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="路由世代">v{{ detail.routing_epoch }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="备注">{{ detail.remark || '-' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="更新时间">{{
|
||||
formatDateTime(detail.updated_at)
|
||||
}}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</ElDrawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
|
||||
import { Delete, Plus, Rank } from '@element-plus/icons-vue'
|
||||
import { VueDraggable } from 'vue-draggable-plus'
|
||||
import { ElButton, ElMessage, ElMessageBox, ElTag } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { PaymentMerchantPoolsService } from '@/api/modules'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { usePermission } from '@/composables/usePermission'
|
||||
import type { SearchFormItem } from '@/types/component'
|
||||
import {
|
||||
PAYMENT_METHOD_OPTIONS,
|
||||
PAYMENT_POOL_STRATEGY_OPTIONS,
|
||||
PAYMENT_STATISTIC_CYCLE_OPTIONS,
|
||||
PAYMENT_TIME_PERIOD_UNIT_OPTIONS,
|
||||
getPaymentMethodLabel,
|
||||
getPaymentPoolStrategyLabel,
|
||||
getPaymentStatisticCycleLabel,
|
||||
getPaymentTimePeriodUnitLabel,
|
||||
type PaymentMerchantMethod,
|
||||
type PaymentMerchantPool,
|
||||
type PaymentMerchantPoolPageResult,
|
||||
type PaymentMerchantPoolPayload,
|
||||
type PaymentMerchantPoolMemberOption,
|
||||
type PaymentPoolStrategy
|
||||
} from '@/types/api/paymentMerchantPools'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
|
||||
defineOptions({ name: 'PaymentMerchantPoolManagement' })
|
||||
|
||||
type FilterVo = string | number | undefined | null | unknown[]
|
||||
|
||||
const { isPlatformAccount } = usePermission()
|
||||
const canManage = computed(() => isPlatformAccount.value)
|
||||
|
||||
const searchForm = reactive<Record<string, FilterVo>>({
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
payment_method: null
|
||||
})
|
||||
|
||||
const pools = ref<PaymentMerchantPool[]>([])
|
||||
const pagination = reactive({ page: 1, page_size: 10, total: 0 })
|
||||
const loading = ref(false)
|
||||
|
||||
const searchItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '支付方式',
|
||||
prop: 'payment_method',
|
||||
type: 'select',
|
||||
options: PAYMENT_METHOD_OPTIONS.map((item) => ({
|
||||
label: item.label,
|
||||
value: item.value
|
||||
})),
|
||||
config: { clearable: true }
|
||||
}
|
||||
]
|
||||
|
||||
const columnOptions = [
|
||||
{ label: '商户池名称', prop: 'name', minWidth: 180 },
|
||||
{
|
||||
label: '支付方式',
|
||||
prop: 'payment_method',
|
||||
width: 110,
|
||||
formatter: (row: PaymentMerchantPool) => getPaymentMethodLabel(row.payment_method)
|
||||
},
|
||||
{
|
||||
label: '成员数量',
|
||||
prop: 'member_ids',
|
||||
width: 110,
|
||||
formatter: (row: PaymentMerchantPool) => `${row.member_ids.length} 个`
|
||||
},
|
||||
{
|
||||
label: '启停状态',
|
||||
prop: 'enabled',
|
||||
width: 110,
|
||||
formatter: (row: PaymentMerchantPool) =>
|
||||
h(
|
||||
ElTag,
|
||||
{ type: row.enabled ? 'success' : 'info' },
|
||||
{ default: () => (row.enabled ? '启用' : '停用') }
|
||||
)
|
||||
},
|
||||
{
|
||||
label: '轮询策略',
|
||||
prop: 'strategy',
|
||||
width: 130,
|
||||
formatter: (row: PaymentMerchantPool) => getPaymentPoolStrategyLabel(row.strategy)
|
||||
},
|
||||
{
|
||||
label: '统计周期',
|
||||
prop: 'statistic_cycle',
|
||||
width: 110,
|
||||
formatter: (row: PaymentMerchantPool) =>
|
||||
row.strategy === 'time' ? '-' : getPaymentStatisticCycleLabel(row.statistic_cycle)
|
||||
},
|
||||
{
|
||||
label: '阈值',
|
||||
prop: 'threshold_summary',
|
||||
width: 150,
|
||||
formatter: (row: PaymentMerchantPool) => describeThreshold(row)
|
||||
},
|
||||
{
|
||||
label: '更新时间',
|
||||
prop: 'updated_at',
|
||||
width: 170,
|
||||
formatter: (row: PaymentMerchantPool) => formatDateTime(row.updated_at)
|
||||
}
|
||||
]
|
||||
|
||||
const { columnChecks, columns } = useCheckedColumns(() => columnOptions)
|
||||
|
||||
const describeThreshold = (row: PaymentMerchantPool): string => {
|
||||
if (row.strategy === 'amount') {
|
||||
return `${(Number(row.threshold_amount) / 100).toFixed(2)} 元`
|
||||
}
|
||||
if (row.strategy === 'count') {
|
||||
return `${row.threshold_count} 笔`
|
||||
}
|
||||
return `${row.time_period_value} ${getPaymentTimePeriodUnitLabel(row.time_period_unit)}`
|
||||
}
|
||||
|
||||
const loadPools = async () => {
|
||||
if (!canManage.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.page_size,
|
||||
payment_method: searchForm.payment_method as PaymentMerchantMethod | undefined
|
||||
}
|
||||
|
||||
const res = await PaymentMerchantPoolsService.getPaymentMerchantPools(params)
|
||||
if (res.code === 0) {
|
||||
const data = (res.data as PaymentMerchantPoolPageResult) || {
|
||||
items: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
size: 10
|
||||
}
|
||||
pools.value = data.items || []
|
||||
pagination.total = data.total || 0
|
||||
pagination.page = data.page || pagination.page
|
||||
pagination.page_size = data.size || pagination.page_size
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载商户池失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
loadPools()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.payment_method = null
|
||||
pagination.page = 1
|
||||
loadPools()
|
||||
}
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.page_size = size
|
||||
pagination.page = 1
|
||||
loadPools()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page
|
||||
loadPools()
|
||||
}
|
||||
|
||||
// 候选成员
|
||||
const availableMerchants = ref<PaymentMerchantPoolMemberOption[]>([])
|
||||
|
||||
const loadAvailableMerchants = async (paymentMethod: PaymentMerchantMethod) => {
|
||||
try {
|
||||
const res = await PaymentMerchantPoolsService.getPaymentMerchants({
|
||||
page: 1,
|
||||
page_size: 100,
|
||||
payment_method: paymentMethod
|
||||
})
|
||||
if (res.code === 0) {
|
||||
const items = res.data?.items || []
|
||||
availableMerchants.value = items.map((item) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
payment_method: item.payment_method,
|
||||
enabled: item.enabled
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载候选商户失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const availableMerchantOptions = computed(() =>
|
||||
availableMerchants.value.filter((m) => m.payment_method === form.payment_method)
|
||||
)
|
||||
|
||||
const getMemberName = (id: number) =>
|
||||
availableMerchants.value.find((m) => m.id === id)?.name || `#${id}`
|
||||
|
||||
// 表单
|
||||
type FormMode = 'create' | 'edit'
|
||||
|
||||
const formDrawerVisible = ref(false)
|
||||
const formMode = ref<FormMode>('create')
|
||||
const submitLoading = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
const memberError = ref('')
|
||||
const pendingMemberId = ref<number | undefined>(undefined)
|
||||
|
||||
const initialFormState = () => ({
|
||||
id: 0,
|
||||
name: '',
|
||||
payment_method: 'wechat' as PaymentMerchantMethod,
|
||||
member_ids: [] as number[],
|
||||
enabled: true,
|
||||
strategy: 'amount' as PaymentPoolStrategy,
|
||||
statistic_cycle: 'round' as PaymentMerchantPoolPayload['statistic_cycle'],
|
||||
threshold_amount_yuan: 0,
|
||||
threshold_count: 1,
|
||||
time_period_unit: 'hour' as PaymentMerchantPoolPayload['time_period_unit'],
|
||||
time_period_value: 1,
|
||||
time_period_started_at: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const form = reactive(initialFormState())
|
||||
|
||||
const orderedMemberIds = ref<number[]>([])
|
||||
watch(
|
||||
() => form.member_ids,
|
||||
(val) => {
|
||||
orderedMemberIds.value = [...val]
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
watch(orderedMemberIds, (val) => {
|
||||
form.member_ids = [...val]
|
||||
})
|
||||
|
||||
const formRules = reactive<FormRules>({
|
||||
name: [
|
||||
{ required: true, message: '请输入商户池名称', trigger: 'blur' },
|
||||
{ max: 100, message: '商户池名称不超过 100 个字符', trigger: 'blur' }
|
||||
],
|
||||
payment_method: [{ required: true, message: '请选择支付方式', trigger: 'change' }],
|
||||
member_ids: [
|
||||
{
|
||||
validator: (_rule, value: number[], callback) => {
|
||||
if (!value || value.length === 0) {
|
||||
callback(new Error('请至少选择一个成员'))
|
||||
return
|
||||
}
|
||||
const unique = new Set(value)
|
||||
if (unique.size !== value.length) {
|
||||
callback(new Error('成员不可重复'))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
strategy: [{ required: true, message: '请选择轮询策略', trigger: 'change' }],
|
||||
statistic_cycle: [
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if ((form.strategy === 'amount' || form.strategy === 'count') && !value) {
|
||||
callback(new Error('请选择统计周期'))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
threshold_amount_yuan: [
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (form.strategy !== 'amount') {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
const num = Number(value)
|
||||
if (!Number.isFinite(num) || num <= 0) {
|
||||
callback(new Error('金额阈值必须大于 0'))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
threshold_count: [
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (form.strategy !== 'count') {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
const num = Number(value)
|
||||
if (!Number.isFinite(num) || !Number.isInteger(num) || num <= 0) {
|
||||
callback(new Error('笔数阈值必须为正整数'))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
time_period_unit: [
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (form.strategy !== 'time') {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
if (!value) {
|
||||
callback(new Error('请选择时间单位'))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
time_period_value: [
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (form.strategy !== 'time') {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
const num = Number(value)
|
||||
if (!Number.isFinite(num) || !Number.isInteger(num) || num <= 0) {
|
||||
callback(new Error('时间长度必须为正整数'))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
time_period_started_at: [
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (form.strategy !== 'time') {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
if (!value) {
|
||||
callback(new Error('请选择时间起点'))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const handlePaymentMethodChange = (value: PaymentMerchantMethod) => {
|
||||
form.payment_method = value
|
||||
form.member_ids = []
|
||||
orderedMemberIds.value = []
|
||||
availableMerchants.value = []
|
||||
memberError.value = ''
|
||||
loadAvailableMerchants(value)
|
||||
}
|
||||
|
||||
const appendMember = (id?: number | string) => {
|
||||
const memberId = Number(id)
|
||||
if (!memberId) {
|
||||
pendingMemberId.value = undefined
|
||||
return
|
||||
}
|
||||
if (form.member_ids.includes(memberId)) {
|
||||
memberError.value = '成员不可重复'
|
||||
} else {
|
||||
form.member_ids = [...form.member_ids, memberId]
|
||||
memberError.value = ''
|
||||
}
|
||||
pendingMemberId.value = undefined
|
||||
}
|
||||
|
||||
const removeMember = (id: number) => {
|
||||
form.member_ids = form.member_ids.filter((memberId) => memberId !== id)
|
||||
}
|
||||
|
||||
const buildPayload = (): PaymentMerchantPoolPayload | null => {
|
||||
if (!form.payment_method) {
|
||||
memberError.value = '请选择支付方式'
|
||||
return null
|
||||
}
|
||||
if (form.member_ids.length === 0) {
|
||||
memberError.value = '请至少选择一个成员'
|
||||
return null
|
||||
}
|
||||
if (new Set(form.member_ids).size !== form.member_ids.length) {
|
||||
memberError.value = '成员不可重复'
|
||||
return null
|
||||
}
|
||||
memberError.value = ''
|
||||
|
||||
const payload: PaymentMerchantPoolPayload = {
|
||||
name: form.name.trim(),
|
||||
payment_method: form.payment_method,
|
||||
member_ids: [...form.member_ids],
|
||||
enabled: form.enabled,
|
||||
strategy: form.strategy,
|
||||
remark: form.remark?.trim() || ''
|
||||
}
|
||||
|
||||
if (form.strategy === 'amount') {
|
||||
if (!form.statistic_cycle) return null
|
||||
const yuan = Number(form.threshold_amount_yuan)
|
||||
if (!Number.isFinite(yuan) || yuan <= 0) return null
|
||||
payload.statistic_cycle = form.statistic_cycle
|
||||
payload.threshold_amount = Math.round(yuan * 100)
|
||||
} else if (form.strategy === 'count') {
|
||||
if (!form.statistic_cycle) return null
|
||||
const count = Number(form.threshold_count)
|
||||
if (!Number.isInteger(count) || count <= 0) return null
|
||||
payload.statistic_cycle = form.statistic_cycle
|
||||
payload.threshold_count = count
|
||||
} else {
|
||||
if (!form.time_period_unit) return null
|
||||
const value = Number(form.time_period_value)
|
||||
if (!Number.isInteger(value) || value <= 0) return null
|
||||
payload.time_period_unit = form.time_period_unit
|
||||
payload.time_period_value = value
|
||||
if (form.time_period_started_at) {
|
||||
payload.time_period_started_at = form.time_period_started_at
|
||||
}
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
const showCreateDrawer = async () => {
|
||||
if (!canManage.value) return
|
||||
Object.assign(form, initialFormState())
|
||||
orderedMemberIds.value = []
|
||||
formDrawerVisible.value = true
|
||||
formMode.value = 'create'
|
||||
await loadAvailableMerchants(form.payment_method)
|
||||
}
|
||||
|
||||
const openEditDrawer = async (pool: PaymentMerchantPool) => {
|
||||
if (!canManage.value) return
|
||||
await loadAvailableMerchants(pool.payment_method)
|
||||
Object.assign(form, initialFormState(), {
|
||||
id: pool.id,
|
||||
name: pool.name,
|
||||
payment_method: pool.payment_method,
|
||||
member_ids: [...pool.member_ids],
|
||||
enabled: pool.enabled,
|
||||
strategy: pool.strategy,
|
||||
statistic_cycle: pool.statistic_cycle,
|
||||
threshold_amount_yuan: Number(pool.threshold_amount) / 100,
|
||||
threshold_count: pool.threshold_count,
|
||||
time_period_unit: pool.time_period_unit,
|
||||
time_period_value: pool.time_period_value,
|
||||
time_period_started_at: pool.time_period_started_at || '',
|
||||
remark: pool.remark || ''
|
||||
})
|
||||
orderedMemberIds.value = [...pool.member_ids]
|
||||
formDrawerVisible.value = true
|
||||
formMode.value = 'edit'
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(form, initialFormState())
|
||||
orderedMemberIds.value = []
|
||||
memberError.value = ''
|
||||
pendingMemberId.value = undefined
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!canManage.value) return
|
||||
if (!formRef.value) return
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
const payload = buildPayload()
|
||||
if (!payload) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
if (formMode.value === 'create') {
|
||||
await PaymentMerchantPoolsService.createPaymentMerchantPool(payload)
|
||||
ElMessage.success('商户池创建成功')
|
||||
} else {
|
||||
await PaymentMerchantPoolsService.updatePaymentMerchantPool(form.id, payload)
|
||||
ElMessage.success('商户池已更新')
|
||||
}
|
||||
formDrawerVisible.value = false
|
||||
await loadPools()
|
||||
} catch (error) {
|
||||
console.error('提交商户池失败:', error)
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 详情
|
||||
const detailDrawerVisible = ref(false)
|
||||
const detail = ref<PaymentMerchantPool | null>(null)
|
||||
const tableRef = ref()
|
||||
|
||||
const showDetail = async (id: number) => {
|
||||
try {
|
||||
const res = await PaymentMerchantPoolsService.getPaymentMerchantPoolById(id)
|
||||
if (res.code === 0) {
|
||||
detail.value = res.data
|
||||
detailDrawerVisible.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载商户池详情失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleEnabled = async (pool: PaymentMerchantPool) => {
|
||||
if (!canManage.value) return
|
||||
const target = !pool.enabled
|
||||
const action = target ? '启用' : '停用'
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认${action}商户池 “${pool.name}”?`, '操作确认', {
|
||||
type: 'warning'
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = target
|
||||
? await PaymentMerchantPoolsService.enablePaymentMerchantPool(pool.id)
|
||||
: await PaymentMerchantPoolsService.disablePaymentMerchantPool(pool.id)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success(`已${action}`)
|
||||
await loadPools()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${action}商户池失败:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
const getActions = (row: PaymentMerchantPool) => [
|
||||
{
|
||||
label: '详情',
|
||||
type: 'primary' as const,
|
||||
handler: () => showDetail(row.id)
|
||||
},
|
||||
{
|
||||
label: '编辑',
|
||||
type: 'primary' as const,
|
||||
handler: () => openEditDrawer(row),
|
||||
permission: canManage.value ? '' : 'hidden'
|
||||
},
|
||||
{
|
||||
label: row.enabled ? '停用' : '启用',
|
||||
type: 'primary' as const,
|
||||
handler: () => toggleEnabled(row),
|
||||
permission: canManage.value ? '' : 'hidden'
|
||||
}
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
if (canManage.value) {
|
||||
loadPools()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
pools.value = []
|
||||
detail.value = null
|
||||
availableMerchants.value = []
|
||||
orderedMemberIds.value = []
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pool-management {
|
||||
.pool-member-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pool-member-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background-color: rgba(var(--art-gray-200-rgb), 0.6);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.pool-member-row :deep(.drag-handle) {
|
||||
color: var(--el-text-color-secondary);
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.pool-member-empty {
|
||||
padding: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
text-align: center;
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.member-name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.field-error {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,234 @@
|
||||
<template>
|
||||
<div class="wechat-authorization-management">
|
||||
<ElCard v-if="canManage" shadow="never" class="art-table-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">微信授权配置</span>
|
||||
<ElTag :type="form.enabled ? 'success' : 'info'">
|
||||
{{ form.enabled ? '已启用' : '已停用' }}
|
||||
</ElTag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ElAlert
|
||||
v-if="!form.enabled"
|
||||
title="微信授权已停用。停用状态下不会影响现有客户端的支付,但新的授权流程将被拦截。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
|
||||
<ElForm ref="formRef" :model="form" :rules="formRules" label-width="140px">
|
||||
<ElFormItem label="启用微信授权">
|
||||
<ElSwitch v-model="form.enabled" active-text="启用" inactive-text="停用" />
|
||||
</ElFormItem>
|
||||
<ElDivider content-position="left">小程序授权</ElDivider>
|
||||
<ElFormItem label="小程序 AppID" prop="miniapp_app_id">
|
||||
<ElInput v-model="form.miniapp_app_id" maxlength="64" placeholder="请输入小程序 AppID" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="小程序 AppSecret">
|
||||
<ElInput
|
||||
v-model="sensitive.miniapp_app_secret"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="如需更换请输入新的 AppSecret,留空表示保持原值"
|
||||
autocomplete="new-password"
|
||||
aria-label="小程序 AppSecret"
|
||||
/>
|
||||
<div class="form-tip">仅在显式更换时填写,提交后立即清空输入。</div>
|
||||
</ElFormItem>
|
||||
<ElDivider content-position="left">公众号授权</ElDivider>
|
||||
<ElFormItem label="公众号 AppID" prop="oa_app_id">
|
||||
<ElInput v-model="form.oa_app_id" maxlength="64" placeholder="请输入公众号 AppID" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="公众号 AppSecret">
|
||||
<ElInput
|
||||
v-model="sensitive.oa_app_secret"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="如需更换请输入新的 AppSecret,留空表示保持原值"
|
||||
autocomplete="new-password"
|
||||
aria-label="公众号 AppSecret"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="公众号 Token">
|
||||
<ElInput
|
||||
v-model="sensitive.oa_token"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="如需更换请输入新的 Token"
|
||||
autocomplete="new-password"
|
||||
aria-label="公众号 Token"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="公众号 AES Key">
|
||||
<ElInput
|
||||
v-model="sensitive.oa_aes_key"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="如需更换请输入新的 EncodingAESKey"
|
||||
autocomplete="new-password"
|
||||
aria-label="公众号 AES Key"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="OAuth 回调地址" prop="oa_oauth_redirect_url">
|
||||
<ElInput
|
||||
v-model="form.oa_oauth_redirect_url"
|
||||
maxlength="512"
|
||||
placeholder="请输入 OAuth 回调地址"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem>
|
||||
<ElButton type="primary" :loading="submitLoading" @click="handleSubmit"> 保存 </ElButton>
|
||||
<ElButton @click="handleCancel">重置</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</ElCard>
|
||||
<ElEmpty v-else description="无访问权限" :image-size="120" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { PaymentMerchantPoolsService } from '@/api/modules'
|
||||
import { usePermission } from '@/composables/usePermission'
|
||||
import type {
|
||||
UpdateWechatAuthorizationRequest,
|
||||
WechatAuthorizationConfig
|
||||
} from '@/types/api/paymentMerchantPools'
|
||||
|
||||
defineOptions({ name: 'WechatAuthorizationManagement' })
|
||||
|
||||
const { isPlatformAccount } = usePermission()
|
||||
const canManage = computed(() => isPlatformAccount.value)
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const submitLoading = ref(false)
|
||||
|
||||
const initialFormState = (): WechatAuthorizationConfig => ({
|
||||
enabled: false,
|
||||
miniapp_app_id: '',
|
||||
oa_app_id: '',
|
||||
oa_oauth_redirect_url: ''
|
||||
})
|
||||
|
||||
const form = reactive<WechatAuthorizationConfig>(initialFormState())
|
||||
|
||||
const initialSensitive = () => ({
|
||||
miniapp_app_secret: '',
|
||||
oa_app_secret: '',
|
||||
oa_token: '',
|
||||
oa_aes_key: ''
|
||||
})
|
||||
|
||||
const sensitive = reactive(initialSensitive())
|
||||
|
||||
const clearSensitive = () => {
|
||||
sensitive.miniapp_app_secret = ''
|
||||
sensitive.oa_app_secret = ''
|
||||
sensitive.oa_token = ''
|
||||
sensitive.oa_aes_key = ''
|
||||
}
|
||||
|
||||
const formRules = reactive<FormRules>({
|
||||
miniapp_app_id: [{ max: 64, message: 'AppID 长度不超过 64 个字符', trigger: 'blur' }],
|
||||
oa_app_id: [{ max: 64, message: 'AppID 长度不超过 64 个字符', trigger: 'blur' }],
|
||||
oa_oauth_redirect_url: [{ max: 512, message: '回调地址长度不超过 512 个字符', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const loadConfig = async () => {
|
||||
if (!canManage.value) return
|
||||
try {
|
||||
const res = await PaymentMerchantPoolsService.getWechatAuthorization()
|
||||
if (res.code === 0) {
|
||||
Object.assign(form, initialFormState(), res.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载微信授权配置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!canManage.value) return
|
||||
if (!formRef.value) return
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const payload: UpdateWechatAuthorizationRequest = {
|
||||
enabled: form.enabled,
|
||||
miniapp_app_id: form.miniapp_app_id?.trim() || '',
|
||||
oa_app_id: form.oa_app_id?.trim() || '',
|
||||
oa_oauth_redirect_url: form.oa_oauth_redirect_url?.trim() || ''
|
||||
}
|
||||
|
||||
if (sensitive.miniapp_app_secret) {
|
||||
payload.miniapp_app_secret = sensitive.miniapp_app_secret
|
||||
}
|
||||
if (sensitive.oa_app_secret) {
|
||||
payload.oa_app_secret = sensitive.oa_app_secret
|
||||
}
|
||||
if (sensitive.oa_token) {
|
||||
payload.oa_token = sensitive.oa_token
|
||||
}
|
||||
if (sensitive.oa_aes_key) {
|
||||
payload.oa_aes_key = sensitive.oa_aes_key
|
||||
}
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const res = await PaymentMerchantPoolsService.updateWechatAuthorization(payload)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('微信授权配置已保存')
|
||||
Object.assign(form, initialFormState(), res.data)
|
||||
clearSensitive()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存微信授权配置失败:', error)
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = async () => {
|
||||
await loadConfig()
|
||||
clearSensitive()
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (canManage.value) {
|
||||
loadConfig()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearSensitive()
|
||||
Object.assign(form, initialFormState())
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wechat-authorization-management {
|
||||
.card-header {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
56
src/views/settings/payment-merchant-pools/index.vue
Normal file
56
src/views/settings/payment-merchant-pools/index.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="payment-merchant-pools-page" id="table-full-screen">
|
||||
<ElCard v-if="canAccess" shadow="never" class="art-table-card">
|
||||
<ElTabs v-model="activeTab" type="card" class="payment-merchant-pools-tabs">
|
||||
<ElTabPane :label="merchantsLabel" name="merchants">
|
||||
<MerchantManagement />
|
||||
</ElTabPane>
|
||||
<ElTabPane :label="poolsLabel" name="pools">
|
||||
<PoolManagement />
|
||||
</ElTabPane>
|
||||
<ElTabPane :label="wechatAuthLabel" name="wechat-auth">
|
||||
<WechatAuthorizationManagement />
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
</ElCard>
|
||||
<ElEmpty v-else description="无访问权限" :image-size="120" />
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import MerchantManagement from './components/MerchantManagement.vue'
|
||||
import PoolManagement from './components/PoolManagement.vue'
|
||||
import WechatAuthorizationManagement from './components/WechatAuthorizationManagement.vue'
|
||||
import { usePermission } from '@/composables/usePermission'
|
||||
|
||||
defineOptions({ name: 'PaymentMerchantPools' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const { isPlatformAccount } = usePermission()
|
||||
const canAccess = computed(() => isPlatformAccount.value)
|
||||
|
||||
const activeTab = ref<'merchants' | 'pools' | 'wechat-auth'>('merchants')
|
||||
|
||||
const merchantsLabel = computed(() => t('menus.settings.paymentMerchantPoolsTabMerchants'))
|
||||
const poolsLabel = computed(() => t('menus.settings.paymentMerchantPoolsTabPools'))
|
||||
const wechatAuthLabel = computed(() => t('menus.settings.paymentMerchantPoolsTabWechatAuth'))
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.payment-merchant-pools-page {
|
||||
height: 100%;
|
||||
|
||||
:deep(.payment-merchant-pools-tabs) {
|
||||
.el-tabs__header {
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
.el-tabs__nav-wrap::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user