Merge branch 'main' of http://1.117.67.95:3000/ZhangRongHong/tyapi-frontend
This commit is contained in:
5288
pnpm-lock.yaml
generated
Normal file
5288
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,9 +3,10 @@ import { announcementApi } from './announcement.js'
|
||||
import { articleApi } from './article.js'
|
||||
import { balanceAlertApi } from './balanceAlertApi.js'
|
||||
import { adminInvoiceApi, invoiceApi } from './invoice.js'
|
||||
import { queryWhitelistApi } from './queryWhitelist.js'
|
||||
|
||||
// 直接导出发票API、文章API、公告API和余额预警API
|
||||
export { adminInvoiceApi, announcementApi, articleApi, balanceAlertApi, invoiceApi }
|
||||
export { adminInvoiceApi, announcementApi, articleApi, balanceAlertApi, invoiceApi, queryWhitelistApi }
|
||||
|
||||
// 用户相关接口 - 严格按照后端路由定义
|
||||
export const userApi = {
|
||||
@@ -403,5 +404,6 @@ export default {
|
||||
whiteList: whiteListApi,
|
||||
api: apiApi,
|
||||
invoice: invoiceApi,
|
||||
adminInvoice: adminInvoiceApi
|
||||
adminInvoice: adminInvoiceApi,
|
||||
queryWhitelist: queryWhitelistApi
|
||||
}
|
||||
|
||||
13
src/api/queryWhitelist.js
Normal file
13
src/api/queryWhitelist.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export const queryWhitelistApi = {
|
||||
getEntries: (params) => request.get('/admin/query-whitelist/entries', { params }),
|
||||
getEntry: (id) => request.get(`/admin/query-whitelist/entries/${id}`),
|
||||
createEntry: (data) => request.post('/admin/query-whitelist/entries', data),
|
||||
updateEntry: (id, data) => request.put(`/admin/query-whitelist/entries/${id}`, data),
|
||||
updateEntryStatus: (id, status) => request.patch(`/admin/query-whitelist/entries/${id}/status`, { status }),
|
||||
deleteEntry: (id) => request.delete(`/admin/query-whitelist/entries/${id}`),
|
||||
importLegacy: () => request.post('/admin/query-whitelist/entries/import-legacy')
|
||||
}
|
||||
|
||||
export default queryWhitelistApi
|
||||
@@ -12,6 +12,12 @@
|
||||
/>
|
||||
|
||||
<div class="header-title">
|
||||
<img
|
||||
v-if="logo"
|
||||
class="header-logo"
|
||||
:src="logo"
|
||||
alt="天远数据 Logo"
|
||||
/>
|
||||
<h1 class="title-text">{{ title }}</h1>
|
||||
<el-tag v-if="badge" :type="badgeType" size="small" class="badge-tag">
|
||||
{{ badge }}
|
||||
@@ -98,6 +104,10 @@ defineProps({
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
logo: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
badge: {
|
||||
type: String,
|
||||
default: ''
|
||||
@@ -190,7 +200,14 @@ const handleUserCommand = async (command) => {
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
flex-shrink: 0;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
|
||||
@@ -168,6 +168,7 @@ export const getUserAccessibleMenuItems = (userType = 'user', _isCertified = fal
|
||||
children: [
|
||||
{ name: '系统统计', path: '/admin/statistics', icon: ChartBar },
|
||||
{ name: '企业审核', path: '/admin/certification-reviews', icon: ShieldCheck },
|
||||
{ name: '查询白名单', path: '/admin/query-whitelist', icon: ShieldCheck },
|
||||
{ name: '产品管理', path: '/admin/products', icon: Cube },
|
||||
{ name: '用户管理', path: '/admin/users', icon: Users },
|
||||
{ name: '分类管理', path: '/admin/categories', icon: Tag },
|
||||
|
||||
20
src/constants/queryWhitelist.js
Normal file
20
src/constants/queryWhitelist.js
Normal file
@@ -0,0 +1,20 @@
|
||||
// 司法/特殊名单等预设接口编码,与后端 LegacyHardcodedAPICodes 保持一致,后续可在此累加
|
||||
export const JUDICIAL_API_CODES = [
|
||||
// flxg — 司法
|
||||
'FLXG0V4B',
|
||||
'FLXG2E8F',
|
||||
'FLXG3A9B',
|
||||
'FLXG5A3B',
|
||||
'FLXG7E8F',
|
||||
'FLXG9C1D',
|
||||
'FLXGCA3D',
|
||||
'FLXGDEA8',
|
||||
'FLXGDEA9',
|
||||
'FLXGHB4F',
|
||||
'FLXGK5D2',
|
||||
// jrzq — 特殊名单
|
||||
'JRZQV7MD',
|
||||
'JRZQ8A2D',
|
||||
// qygl — 企业司法失信
|
||||
'QYGL2S0W'
|
||||
]
|
||||
@@ -9,6 +9,7 @@
|
||||
<el-header class="el-header">
|
||||
<AppHeader
|
||||
:title="headerTitle"
|
||||
:logo="headerLogo"
|
||||
:badge="headerBadge"
|
||||
:badge-type="headerBadgeType"
|
||||
:theme="headerTheme"
|
||||
@@ -52,6 +53,7 @@ import FloatingCustomerService from '@/components/common/FloatingCustomerService
|
||||
import AppHeader from '@/components/layout/AppHeader.vue'
|
||||
import AppSidebar from '@/components/layout/AppSidebar.vue'
|
||||
import NotificationPanel from '@/components/layout/NotificationPanel.vue'
|
||||
import headerLogo from '@/assets/logo.png'
|
||||
import { isSubPortal } from '@/constants/portal'
|
||||
import { getCurrentPageCertificationConfig, getUserAccessibleMenuItems } from '@/constants/menu'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
@@ -71,6 +73,7 @@ const headerTitle = computed(() => {
|
||||
return isAdmin.value ? '天远数据控制台' : '天远数据控制台'
|
||||
})
|
||||
|
||||
|
||||
const headerBadge = computed(() => {
|
||||
return isAdmin.value ? '管理员模式' : null
|
||||
})
|
||||
|
||||
@@ -69,9 +69,9 @@
|
||||
<el-table-column label="操作" width="240" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openDetail(row.id)">查看详情</el-button>
|
||||
<template v-if="canShowApproveReject(row)">
|
||||
<el-button link type="success" @click="handleApprove(row)">通过</el-button>
|
||||
<el-button link type="danger" @click="handleReject(row)">拒绝</el-button>
|
||||
<template v-if="canShowApprove(row) || canShowReject(row)">
|
||||
<el-button v-if="canShowApprove(row)" link type="success" @click="handleApprove(row)">通过</el-button>
|
||||
<el-button v-if="canShowReject(row)" link type="danger" @click="handleReject(row)">拒绝</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -92,9 +92,9 @@
|
||||
<div class="mobile-card-info"><span>统一代码:</span>{{ row.unified_social_code || '-' }}</div>
|
||||
<div class="mobile-card-actions">
|
||||
<el-button type="primary" plain size="small" @click="openDetail(row.id)">查看详情</el-button>
|
||||
<template v-if="canShowApproveReject(row)">
|
||||
<el-button type="success" plain size="small" @click="handleApprove(row)">通过</el-button>
|
||||
<el-button type="danger" plain size="small" @click="handleReject(row)">拒绝</el-button>
|
||||
<template v-if="canShowApprove(row) || canShowReject(row)">
|
||||
<el-button v-if="canShowApprove(row)" type="success" plain size="small" @click="handleApprove(row)">通过</el-button>
|
||||
<el-button v-if="canShowReject(row)" type="danger" plain size="small" @click="handleReject(row)">拒绝</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</article>
|
||||
@@ -125,9 +125,9 @@
|
||||
<template #header>
|
||||
<span class="drawer-title">企业信息详情</span>
|
||||
<div class="drawer-actions">
|
||||
<template v-if="detail && canShowApproveReject(detail)">
|
||||
<el-button type="success" size="small" @click="approveFromDrawer">通过</el-button>
|
||||
<el-button type="danger" size="small" @click="rejectFromDrawer">拒绝</el-button>
|
||||
<template v-if="detail && (canShowApprove(detail) || canShowReject(detail))">
|
||||
<el-button v-if="canShowApprove(detail)" type="success" size="small" @click="approveFromDrawer">通过</el-button>
|
||||
<el-button v-if="canShowReject(detail)" type="danger" size="small" @click="rejectFromDrawer">拒绝</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -255,7 +255,7 @@
|
||||
|
||||
<script setup>
|
||||
import { certificationApi } from '@/api/index.js'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
@@ -333,12 +333,21 @@ function statusTagType(row) {
|
||||
* 是否展示通过/拒绝:须当前认证为待审核,且本条提交记录为「校验通过」(verified)。
|
||||
* 历史失败记录 (failed) 与认证状态无关字段共用同一 certification_status,否则会误显按钮。
|
||||
*/
|
||||
function canShowApproveReject(row) {
|
||||
function canShowApprove(row) {
|
||||
if (!row) return false
|
||||
if (row.certification_status !== 'info_pending_review') return false
|
||||
return row.status === 'verified'
|
||||
}
|
||||
|
||||
function canShowReject(row) {
|
||||
if (!row) return false
|
||||
const rejectableStatus =
|
||||
row.certification_status === 'info_pending_review' ||
|
||||
row.certification_status === 'info_submitted'
|
||||
if (!rejectableStatus) return false
|
||||
return row.status === 'verified'
|
||||
}
|
||||
|
||||
// 判断是否可作为图片展示(含七牛云等无扩展名的 CDN URL)
|
||||
function isImageUrl(url) {
|
||||
if (!url || typeof url !== 'string') return false
|
||||
@@ -440,7 +449,27 @@ async function confirmApprove() {
|
||||
pendingUserId.value = ''
|
||||
loadList()
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || '操作失败')
|
||||
const msg = e?.message || '操作失败'
|
||||
if (msg.includes('生成企业认证链接失败')) {
|
||||
const esignError = msg.replace(/^生成企业认证链接失败:\s*/, '')
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`企业认证链接生成失败:${esignError}\n\n请根据 e签宝 返回的错误信息填写拒绝原因并点击「拒绝」,以便用户修正后重新提交。`,
|
||||
'无法通过审核',
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: '去填写拒绝原因',
|
||||
cancelButtonText: '知道了'
|
||||
}
|
||||
)
|
||||
rejectRemark.value = esignError
|
||||
rejectDialogVisible.value = true
|
||||
} catch {
|
||||
// 用户点击「知道了」
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(msg)
|
||||
}
|
||||
} finally {
|
||||
actionLoading.value = false
|
||||
}
|
||||
|
||||
@@ -132,6 +132,13 @@
|
||||
<span class="card-label">成本价</span>
|
||||
<span class="card-value text-gray-600">¥{{ formatPrice(product.cost_price) }}</span>
|
||||
</div>
|
||||
<div class="card-row">
|
||||
<span class="card-label">备注</span>
|
||||
<el-tag v-if="product.remark" type="warning" size="small" class="max-w-[60%] truncate">
|
||||
{{ product.remark }}
|
||||
</el-tag>
|
||||
<span v-else class="card-value text-gray-400">-</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="action-buttons">
|
||||
@@ -208,6 +215,13 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.remark" type="warning" size="small">{{ row.remark }}</el-tag>
|
||||
<span v-else class="text-gray-400">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="is_enabled" label="启用状态" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.is_enabled ? 'success' : 'danger'" size="small">
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="isEdit ? '编辑查询白名单' : '新增查询白名单'"
|
||||
width="640px"
|
||||
destroy-on-close
|
||||
@closed="handleClosed"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
|
||||
<el-form-item label="生效范围" prop="scope">
|
||||
<el-radio-group v-model="form.scope" @change="handleScopeChange">
|
||||
<el-radio value="global">全局(所有用户)</el-radio>
|
||||
<el-radio value="user">指定用户</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="form.scope === 'user'" label="用户" prop="user_id">
|
||||
<el-select
|
||||
v-model="form.user_id"
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
placeholder="搜索手机号或企业名"
|
||||
:remote-method="searchUsers"
|
||||
:loading="userLoading"
|
||||
class="w-full"
|
||||
>
|
||||
<el-option
|
||||
v-for="user in userOptions"
|
||||
:key="user.id"
|
||||
:label="formatUserLabel(user)"
|
||||
:value="user.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="姓名" prop="name">
|
||||
<el-input v-model="form.name" placeholder="输入真实姓名;填 * 表示不校验姓名" />
|
||||
<div class="form-tip">
|
||||
必填。填 <code>*</code> 表示<strong>通配姓名</strong>:只要请求里的身份证号命中本条规则,无论传入什么姓名都会拦截;填具体姓名(如「张三」)则身份证与姓名须<strong>同时一致</strong>才命中
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="身份证号" prop="id_card">
|
||||
<el-input
|
||||
v-model="form.id_card"
|
||||
placeholder="18位身份证号(必填)"
|
||||
maxlength="18"
|
||||
:disabled="isEdit"
|
||||
/>
|
||||
<div class="form-tip">必填,18 位。命中判断以身份证号为准;编辑时不可修改,如需变更请删除后重新添加</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="生效接口" prop="api_codes">
|
||||
<div class="api-codes-field">
|
||||
<el-select
|
||||
v-model="form.api_codes"
|
||||
multiple
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
placeholder="选择或输入 API 编码"
|
||||
class="w-full"
|
||||
@change="handleApiCodesChange"
|
||||
>
|
||||
<el-option label="全部接口 (*)" value="*" />
|
||||
<el-option
|
||||
v-for="product in productOptions"
|
||||
:key="product.code"
|
||||
:label="`${product.code} - ${product.name}`"
|
||||
:value="product.code"
|
||||
/>
|
||||
</el-select>
|
||||
<div class="api-codes-actions">
|
||||
<el-button link type="primary" size="small" @click="selectJudicialApis">
|
||||
一键选司法接口
|
||||
</el-button>
|
||||
<span class="api-codes-actions-tip">({{ JUDICIAL_API_CODES.length }} 个,可继续手动追加)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-tip">选择「全部接口」时仅对入参<strong>必填身份证</strong>的 API 生效,企业类等无身份证入参的接口不受影响</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="可选备注" maxlength="500" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { productAdminApi, queryWhitelistApi, userApi } from '@/api'
|
||||
import { JUDICIAL_API_CODES } from '@/constants/queryWhitelist.js'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: Boolean,
|
||||
entry: { type: Object, default: null }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'success'])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const isEdit = computed(() => !!props.entry?.id)
|
||||
const formRef = ref(null)
|
||||
const submitting = ref(false)
|
||||
const userLoading = ref(false)
|
||||
const userOptions = ref([])
|
||||
const productOptions = ref([])
|
||||
|
||||
const defaultForm = () => ({
|
||||
scope: 'global',
|
||||
user_id: '*',
|
||||
name: '*',
|
||||
id_card: '',
|
||||
api_codes: ['*'],
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const form = reactive(defaultForm())
|
||||
|
||||
const rules = {
|
||||
scope: [{ required: true, message: '请选择生效范围', trigger: 'change' }],
|
||||
user_id: [{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (form.scope === 'user' && !value) {
|
||||
callback(new Error('请选择用户'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'change'
|
||||
}],
|
||||
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
||||
id_card: [{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (isEdit.value) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
if (!value || value.length !== 18) {
|
||||
callback(new Error('请输入18位身份证号'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}],
|
||||
api_codes: [{ required: true, type: 'array', min: 1, message: '请选择至少一个接口', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const formatUserLabel = (user) => {
|
||||
const phone = user.phone || '-'
|
||||
const company = user.enterprise_info?.company_name
|
||||
return company ? `${phone}(${company})` : phone
|
||||
}
|
||||
|
||||
const handleScopeChange = (scope) => {
|
||||
form.user_id = scope === 'global' ? '*' : ''
|
||||
}
|
||||
|
||||
const handleApiCodesChange = (codes) => {
|
||||
if (codes.includes('*') && codes.length > 1) {
|
||||
form.api_codes = ['*']
|
||||
}
|
||||
}
|
||||
|
||||
const selectJudicialApis = () => {
|
||||
const current = form.api_codes.filter((code) => code !== '*')
|
||||
const before = new Set(current)
|
||||
form.api_codes = [...new Set([...current, ...JUDICIAL_API_CODES])]
|
||||
const added = form.api_codes.length - before.size
|
||||
if (added > 0) {
|
||||
ElMessage.success(`已添加 ${added} 个司法接口,可继续手动追加`)
|
||||
} else {
|
||||
ElMessage.info('司法接口已全部选中')
|
||||
}
|
||||
}
|
||||
|
||||
const searchUsers = async (query) => {
|
||||
if (!query) return
|
||||
userLoading.value = true
|
||||
try {
|
||||
const res = await userApi.getUserList({ phone: query, page: 1, page_size: 20 })
|
||||
userOptions.value = res.data?.items || []
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
userLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadProducts = async () => {
|
||||
try {
|
||||
const res = await productAdminApi.getAvailableProducts({ page: 1, page_size: 500 })
|
||||
productOptions.value = res.data?.items || res.data || []
|
||||
} catch (e) {
|
||||
console.error('加载产品列表失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
const fillForm = (entry) => {
|
||||
Object.assign(form, defaultForm())
|
||||
if (!entry) return
|
||||
form.scope = entry.is_global ? 'global' : 'user'
|
||||
form.user_id = entry.user_id
|
||||
form.name = entry.name
|
||||
form.api_codes = [...(entry.api_codes || ['*'])]
|
||||
form.remark = entry.remark || ''
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, (open) => {
|
||||
if (open) {
|
||||
fillForm(props.entry)
|
||||
loadProducts()
|
||||
if (props.entry && !props.entry.is_global) {
|
||||
userOptions.value = [{ id: props.entry.user_id, phone: props.entry.user_id }]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await formRef.value?.validate()
|
||||
submitting.value = true
|
||||
try {
|
||||
const payload = {
|
||||
user_id: form.scope === 'global' ? '*' : form.user_id,
|
||||
name: form.name.trim(),
|
||||
api_codes: form.api_codes,
|
||||
remark: form.remark
|
||||
}
|
||||
if (!isEdit.value) {
|
||||
payload.id_card = form.id_card.trim()
|
||||
await queryWhitelistApi.createEntry(payload)
|
||||
ElMessage.success('创建成功')
|
||||
} else {
|
||||
await queryWhitelistApi.updateEntry(props.entry.id, {
|
||||
name: payload.name,
|
||||
api_codes: payload.api_codes,
|
||||
remark: payload.remark
|
||||
})
|
||||
ElMessage.success('更新成功')
|
||||
}
|
||||
visible.value = false
|
||||
emit('success')
|
||||
} catch (e) {
|
||||
ElMessage.error(e.message || '操作失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleClosed = () => {
|
||||
formRef.value?.resetFields()
|
||||
Object.assign(form, defaultForm())
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-tip {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.form-tip code {
|
||||
background: #f3f4f6;
|
||||
padding: 0 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.api-codes-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.api-codes-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.api-codes-actions-tip {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
</style>
|
||||
319
src/pages/admin/query-whitelist/index.vue
Normal file
319
src/pages/admin/query-whitelist/index.vue
Normal file
@@ -0,0 +1,319 @@
|
||||
<template>
|
||||
<ListPageLayout
|
||||
title="查询白名单"
|
||||
subtitle="配置命中后返回「查询为空」的规则;「全部接口」仅作用于入参必填身份证的 API"
|
||||
>
|
||||
<template #actions>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<el-button :loading="importing" @click="handleImportLegacy">
|
||||
导入历史硬编码
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleCreate">
|
||||
<PlusIcon class="w-4 h-4 mr-1" />
|
||||
新增规则
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #filters>
|
||||
<FilterSection>
|
||||
<FilterItem label="生效范围">
|
||||
<el-select v-model="filters.scope" clearable placeholder="全部" class="w-full" @change="handleFilterChange">
|
||||
<el-option label="全局规则" value="global" />
|
||||
<el-option label="指定用户" value="user" />
|
||||
</el-select>
|
||||
</FilterItem>
|
||||
|
||||
<FilterItem label="用户ID">
|
||||
<el-input v-model="filters.user_id" placeholder="用户 UUID 或 *" clearable class="w-full" />
|
||||
</FilterItem>
|
||||
|
||||
<FilterItem label="状态">
|
||||
<el-select v-model="filters.status" clearable placeholder="全部" class="w-full" @change="handleFilterChange">
|
||||
<el-option label="启用" value="enabled" />
|
||||
<el-option label="禁用" value="disabled" />
|
||||
</el-select>
|
||||
</FilterItem>
|
||||
|
||||
<FilterItem label="API编码">
|
||||
<el-input v-model="filters.api_code" placeholder="如 FLXG0V4B" clearable class="w-full" />
|
||||
</FilterItem>
|
||||
|
||||
<FilterItem label="关键词">
|
||||
<el-input
|
||||
v-model="filters.keyword"
|
||||
placeholder="姓名 / 备注 / 脱敏身份证"
|
||||
clearable
|
||||
class="w-full"
|
||||
@input="handleSearch"
|
||||
/>
|
||||
</FilterItem>
|
||||
|
||||
<template #stats>
|
||||
共 {{ total }} 条规则
|
||||
</template>
|
||||
|
||||
<template #buttons>
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
<el-button type="primary" @click="loadEntries">查询</el-button>
|
||||
</template>
|
||||
</FilterSection>
|
||||
</template>
|
||||
|
||||
<template #table>
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||||
<el-table v-loading="loading" :data="entries" stripe class="w-full">
|
||||
<el-table-column label="生效范围" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.is_global ? 'warning' : 'primary'" size="small">
|
||||
{{ row.is_global ? '全局' : '指定用户' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="user_id" label="用户ID" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span class="font-mono text-xs">{{ row.is_global ? '*' : row.user_id }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="name" label="姓名" width="100">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.name === '*' ? '任意(*)' : row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="id_card_masked" label="身份证" width="180">
|
||||
<template #default="{ row }">
|
||||
<span class="font-mono">{{ row.id_card_masked }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="生效接口" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.api_codes?.includes('*')">
|
||||
<el-tag size="small" type="info">全部接口</el-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-tag v-for="code in row.api_codes" :key="code" size="small" class="mr-1 mb-1">{{ code }}</el-tag>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="status" label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
:model-value="row.status === 'enabled'"
|
||||
active-text="启用"
|
||||
inactive-text="禁用"
|
||||
inline-prompt
|
||||
@change="(val) => handleToggleStatus(row, val)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
|
||||
|
||||
<el-table-column prop="operation_ip" label="操作IP" width="130" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span class="font-mono text-xs">{{ row.operation_ip || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="created_at" label="创建时间" width="170">
|
||||
<template #default="{ row }">{{ formatDate(row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #pagination>
|
||||
<el-pagination
|
||||
v-if="total > 0"
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #extra>
|
||||
<QueryWhitelistFormDialog
|
||||
v-model="showFormDialog"
|
||||
:entry="currentEntry"
|
||||
@success="loadEntries"
|
||||
/>
|
||||
</template>
|
||||
</ListPageLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { queryWhitelistApi } from '@/api/queryWhitelist.js'
|
||||
import FilterItem from '@/components/common/FilterItem.vue'
|
||||
import FilterSection from '@/components/common/FilterSection.vue'
|
||||
import ListPageLayout from '@/components/common/ListPageLayout.vue'
|
||||
import { PlusIcon } from '@heroicons/vue/24/outline'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import QueryWhitelistFormDialog from './components/QueryWhitelistFormDialog.vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const importing = ref(false)
|
||||
const entries = ref([])
|
||||
const total = ref(0)
|
||||
const showFormDialog = ref(false)
|
||||
const currentEntry = ref(null)
|
||||
|
||||
const filters = reactive({
|
||||
scope: '',
|
||||
user_id: '',
|
||||
status: '',
|
||||
api_code: '',
|
||||
keyword: ''
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
})
|
||||
|
||||
let searchTimer = null
|
||||
|
||||
const buildParams = () => {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.pageSize
|
||||
}
|
||||
if (filters.status) params.status = filters.status
|
||||
if (filters.api_code) params.api_code = filters.api_code
|
||||
if (filters.keyword) params.keyword = filters.keyword
|
||||
if (filters.scope === 'global') {
|
||||
params.user_id = '*'
|
||||
} else if (filters.scope === 'user') {
|
||||
params.user_id = filters.user_id || undefined
|
||||
if (!filters.user_id) {
|
||||
// 指定用户但未填 ID 时,排除全局需在服务端支持;这里用非 * 过滤需填 user_id
|
||||
}
|
||||
} else if (filters.user_id) {
|
||||
params.user_id = filters.user_id
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
const loadEntries = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await queryWhitelistApi.getEntries(buildParams())
|
||||
entries.value = res.data?.items || []
|
||||
total.value = res.data?.total || 0
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
ElMessage.error('加载查询白名单失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleFilterChange = () => {
|
||||
pagination.page = 1
|
||||
loadEntries()
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
if (searchTimer) clearTimeout(searchTimer)
|
||||
searchTimer = setTimeout(() => {
|
||||
pagination.page = 1
|
||||
loadEntries()
|
||||
}, 400)
|
||||
}
|
||||
|
||||
const resetFilters = () => {
|
||||
Object.assign(filters, { scope: '', user_id: '', status: '', api_code: '', keyword: '' })
|
||||
pagination.page = 1
|
||||
loadEntries()
|
||||
}
|
||||
|
||||
const handleSizeChange = (size) => {
|
||||
pagination.pageSize = size
|
||||
pagination.page = 1
|
||||
loadEntries()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page) => {
|
||||
pagination.page = page
|
||||
loadEntries()
|
||||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
currentEntry.value = null
|
||||
showFormDialog.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row) => {
|
||||
currentEntry.value = { ...row }
|
||||
showFormDialog.value = true
|
||||
}
|
||||
|
||||
const handleToggleStatus = async (row, enabled) => {
|
||||
const status = enabled ? 'enabled' : 'disabled'
|
||||
try {
|
||||
await queryWhitelistApi.updateEntryStatus(row.id, status)
|
||||
row.status = status
|
||||
ElMessage.success(enabled ? '已启用' : '已禁用')
|
||||
} catch (e) {
|
||||
ElMessage.error(e.message || '状态更新失败')
|
||||
loadEntries()
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定删除该规则?(${row.id_card_masked} / ${row.name === '*' ? '任意姓名' : row.name})`,
|
||||
'确认删除',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
await queryWhitelistApi.deleteEntry(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
loadEntries()
|
||||
} catch (e) {
|
||||
if (e !== 'cancel') ElMessage.error(e.message || '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleImportLegacy = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'将导入原 processor 中 17 条硬编码身份证为「全局规则」(user_id=*,name=*)。生效接口为原硬编码所在的司法、特殊名单等 API 编码,并非全部接口。已存在的会自动跳过。',
|
||||
'导入历史硬编码',
|
||||
{ type: 'info', confirmButtonText: '开始导入' }
|
||||
)
|
||||
importing.value = true
|
||||
const res = await queryWhitelistApi.importLegacy()
|
||||
const { imported, skipped, total: t } = res.data || {}
|
||||
ElMessage.success(`导入完成:新增 ${imported} 条,跳过 ${skipped} 条,共 ${t} 条`)
|
||||
loadEntries()
|
||||
} catch (e) {
|
||||
if (e !== 'cancel') ElMessage.error(e.message || '导入失败')
|
||||
} finally {
|
||||
importing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return '-'
|
||||
return new Date(dateString).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
onMounted(loadEntries)
|
||||
</script>
|
||||
@@ -14,6 +14,18 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-alert
|
||||
v-if="showRejectionAlert"
|
||||
type="error"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="rejection-alert"
|
||||
title="上次审核未通过"
|
||||
>
|
||||
<p class="rejection-message">{{ failureMessage }}</p>
|
||||
<p class="rejection-hint">请根据以上原因修改企业信息后重新提交</p>
|
||||
</el-alert>
|
||||
|
||||
<div class="enterprise-form">
|
||||
<el-form
|
||||
ref="enterpriseFormRef"
|
||||
@@ -383,9 +395,21 @@ const props = defineProps({
|
||||
authorizedRepPhone: '',
|
||||
authorizedRepIDImageURLs: []
|
||||
})
|
||||
},
|
||||
certificationStatus: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
failureMessage: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const showRejectionAlert = computed(
|
||||
() => props.certificationStatus === 'info_rejected' && !!props.failureMessage?.trim()
|
||||
)
|
||||
|
||||
const emit = defineEmits(['submit'])
|
||||
|
||||
const userStore = useUserStore()
|
||||
@@ -939,6 +963,22 @@ onUnmounted(() => {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rejection-alert {
|
||||
margin: 0 24px 16px;
|
||||
}
|
||||
|
||||
.rejection-message {
|
||||
margin: 0 0 4px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.rejection-hint {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
/* 卡片头部 */
|
||||
.card-header {
|
||||
display: flex;
|
||||
|
||||
@@ -65,6 +65,8 @@
|
||||
<EnterpriseInfo
|
||||
v-if="currentStep === 'enterprise_info'"
|
||||
:form-data="enterpriseForm"
|
||||
:certification-status="certificationData?.status"
|
||||
:failure-message="certificationData?.failure_message"
|
||||
@submit="handleEnterpriseSubmit"
|
||||
/>
|
||||
|
||||
|
||||
@@ -429,6 +429,7 @@
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
class="qr-code-dialog"
|
||||
@close="handleQrCodeDialogClose"
|
||||
>
|
||||
<div class="qr-code-container">
|
||||
<div class="qr-code-wrapper">
|
||||
@@ -437,6 +438,7 @@
|
||||
<p class="qr-code-tip">请使用微信扫描上方二维码完成支付</p>
|
||||
<p class="qr-code-amount">支付金额:¥{{ formatPrice(wechatForm.amount) }}</p>
|
||||
<p v-if="isCheckingPayment" class="qr-code-checking">正在确认支付状态,请稍候...</p>
|
||||
<p class="qr-code-tip">关闭窗口后将继续确认支付结果,请勿重复支付</p>
|
||||
<el-button type="primary" @click="closeQrCodeDialog" class="close-qr-btn">关闭</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
@@ -476,6 +478,11 @@ const showQrCodeDialog = ref(false)
|
||||
const qrCodeCanvas = ref(null)
|
||||
const currentWechatOrderNo = ref(null)
|
||||
const isCheckingPayment = ref(false)
|
||||
const wechatPaymentResolved = ref(false)
|
||||
const skipQrCloseRedirect = ref(false)
|
||||
const wechatDialogPollCount = ref(0)
|
||||
const WECHAT_POLL_INTERVAL_MS = 5000
|
||||
const WECHAT_DIALOG_MAX_POLL_COUNT = 12 // 弹窗内最多轮询12次(约1分钟),之后跳转处理页
|
||||
let wechatOrderPollTimer = null
|
||||
|
||||
// 钱包信息
|
||||
@@ -993,26 +1000,78 @@ const showQrCode = async (codeUrl) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭二维码弹窗
|
||||
// 关闭二维码弹窗(用户主动关闭,跳转处理页继续轮询)
|
||||
const closeQrCodeDialog = () => {
|
||||
stopWechatOrderPolling()
|
||||
showQrCodeDialog.value = false
|
||||
}
|
||||
|
||||
const handleQrCodeDialogClose = () => {
|
||||
if (wechatPaymentResolved.value) {
|
||||
wechatPaymentResolved.value = false
|
||||
stopWechatOrderPolling()
|
||||
currentWechatOrderNo.value = null
|
||||
isCheckingPayment.value = false
|
||||
wechatDialogPollCount.value = 0
|
||||
return
|
||||
}
|
||||
|
||||
if (skipQrCloseRedirect.value) {
|
||||
skipQrCloseRedirect.value = false
|
||||
isCheckingPayment.value = false
|
||||
wechatDialogPollCount.value = 0
|
||||
return
|
||||
}
|
||||
|
||||
const orderNo = currentWechatOrderNo.value
|
||||
const orderAmount = wechatForm.amount
|
||||
stopWechatOrderPolling()
|
||||
currentWechatOrderNo.value = null
|
||||
isCheckingPayment.value = false
|
||||
wechatDialogPollCount.value = 0
|
||||
|
||||
if (orderNo) {
|
||||
router.push({
|
||||
path: '/finance/wallet/processing',
|
||||
query: {
|
||||
out_trade_no: orderNo,
|
||||
amount: orderAmount,
|
||||
payment_type: 'wechat',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const redirectToWechatProcessingPage = () => {
|
||||
const orderNo = currentWechatOrderNo.value
|
||||
const orderAmount = wechatForm.amount
|
||||
if (!orderNo) return
|
||||
|
||||
stopWechatOrderPolling()
|
||||
skipQrCloseRedirect.value = true
|
||||
currentWechatOrderNo.value = null
|
||||
wechatDialogPollCount.value = 0
|
||||
showQrCodeDialog.value = false
|
||||
isCheckingPayment.value = false
|
||||
router.push({
|
||||
path: '/finance/wallet/processing',
|
||||
query: {
|
||||
out_trade_no: orderNo,
|
||||
amount: orderAmount,
|
||||
payment_type: 'wechat',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 开始轮询微信订单状态
|
||||
const startWechatOrderPolling = () => {
|
||||
// 清除之前的定时器
|
||||
stopWechatOrderPolling()
|
||||
|
||||
// 立即检查一次
|
||||
wechatDialogPollCount.value = 0
|
||||
checkWechatOrderStatus()
|
||||
|
||||
// 每3秒轮询一次
|
||||
wechatOrderPollTimer = setInterval(() => {
|
||||
wechatDialogPollCount.value++
|
||||
checkWechatOrderStatus()
|
||||
}, 3000)
|
||||
}, WECHAT_POLL_INTERVAL_MS)
|
||||
}
|
||||
|
||||
// 停止轮询
|
||||
@@ -1029,47 +1088,49 @@ const checkWechatOrderStatus = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
if (wechatDialogPollCount.value >= WECHAT_DIALOG_MAX_POLL_COUNT) {
|
||||
redirectToWechatProcessingPage()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
isCheckingPayment.value = true
|
||||
const response = await callProtectedAPI(financeApi.getWechatOrderStatus, {
|
||||
const response = await financeApi.getWechatOrderStatus({
|
||||
out_trade_no: currentWechatOrderNo.value
|
||||
})
|
||||
|
||||
if (!response || !response.data) {
|
||||
if (!response?.data) {
|
||||
isCheckingPayment.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const orderStatus = response.data.status
|
||||
|
||||
// 订单状态:pending, success, failed, closed
|
||||
if (orderStatus === 'success') {
|
||||
// 支付成功
|
||||
wechatPaymentResolved.value = true
|
||||
stopWechatOrderPolling()
|
||||
isCheckingPayment.value = false
|
||||
closeQrCodeDialog()
|
||||
showQrCodeDialog.value = false
|
||||
ElMessage.success('充值成功!')
|
||||
|
||||
// 刷新钱包余额
|
||||
await loadWalletInfo()
|
||||
|
||||
// 重置表单
|
||||
wechatForm.amount = ''
|
||||
selectedPresetAmount.value = null
|
||||
currentWechatOrderNo.value = null
|
||||
wechatDialogPollCount.value = 0
|
||||
} else if (orderStatus === 'failed' || orderStatus === 'closed') {
|
||||
// 支付失败或关闭
|
||||
wechatPaymentResolved.value = true
|
||||
stopWechatOrderPolling()
|
||||
isCheckingPayment.value = false
|
||||
showQrCodeDialog.value = false
|
||||
ElMessage.error('支付失败,请重新支付')
|
||||
closeQrCodeDialog()
|
||||
currentWechatOrderNo.value = null
|
||||
wechatDialogPollCount.value = 0
|
||||
} else {
|
||||
// pending 状态继续轮询
|
||||
isCheckingPayment.value = false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('查询微信订单状态失败:', error)
|
||||
isCheckingPayment.value = false
|
||||
// 不显示错误,继续轮询
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
<span class="font-medium">{{ orderInfo.out_trade_no }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500">支付宝交易号:</span>
|
||||
<span class="text-gray-500">{{ tradeNoLabel }}:</span>
|
||||
<span class="font-medium">{{ orderInfo.trade_no || '暂无' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
@@ -245,6 +245,57 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 轮询超时(支付可能仍在处理中) -->
|
||||
<div v-else-if="orderStatus === 'timeout'" class="text-center">
|
||||
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-yellow-100">
|
||||
<svg
|
||||
class="h-6 w-6 text-yellow-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="mt-6 text-3xl font-extrabold text-gray-900">仍在确认支付结果</h2>
|
||||
<p class="mt-2 text-sm text-gray-600">
|
||||
若您已完成支付,余额通常会在几分钟内到账,请稍后刷新钱包查看。
|
||||
</p>
|
||||
|
||||
<div class="mt-8 bg-white shadow rounded-lg p-6">
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500">订单号:</span>
|
||||
<span class="font-medium">{{ orderInfo.out_trade_no }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500">金额:</span>
|
||||
<span class="font-medium text-green-600">¥{{ orderInfo.amount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 space-y-3">
|
||||
<button
|
||||
@click="goToWallet"
|
||||
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
返回钱包查看余额
|
||||
</button>
|
||||
<button
|
||||
@click="contactService"
|
||||
class="w-full flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
联系客服
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 其他状态 -->
|
||||
<div v-else class="text-center">
|
||||
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-yellow-100">
|
||||
@@ -291,12 +342,16 @@ export default {
|
||||
const orderStatus = ref('processing')
|
||||
const isProcessing = ref(true)
|
||||
const pollCount = ref(0)
|
||||
const maxPollCount = ref(30) // 最多轮询30次
|
||||
const maxPollCount = ref(36) // 最多轮询36次(约3分钟)
|
||||
const pollIntervalMs = 5000 // 每5秒查询一次,避免过于频繁
|
||||
const pollInterval = ref(null)
|
||||
|
||||
// 获取URL参数
|
||||
const outTradeNo = route.query.out_trade_no
|
||||
const amount = route.query.amount
|
||||
const paymentType = route.query.payment_type === 'wechat' ? 'wechat' : 'alipay'
|
||||
const isWechatPay = computed(() => paymentType === 'wechat')
|
||||
const tradeNoLabel = computed(() => (isWechatPay.value ? '微信交易号' : '支付宝交易号'))
|
||||
|
||||
// 初始化订单信息
|
||||
orderInfo.value = {
|
||||
@@ -304,6 +359,15 @@ export default {
|
||||
amount: amount,
|
||||
}
|
||||
|
||||
const normalizeOrderData = (data) => {
|
||||
if (!data) return data
|
||||
return {
|
||||
...data,
|
||||
amount: data.amount ?? amount,
|
||||
trade_no: data.trade_no || data.transaction_id || null,
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timeStr) => {
|
||||
if (!timeStr) return '暂无'
|
||||
@@ -313,41 +377,45 @@ export default {
|
||||
// 查询订单状态
|
||||
const queryOrderStatus = async () => {
|
||||
try {
|
||||
const response = await financeApi.getAlipayOrderStatus({ out_trade_no: outTradeNo })
|
||||
orderInfo.value = response.data
|
||||
// 根据状态更新页面
|
||||
const response = isWechatPay.value
|
||||
? await financeApi.getWechatOrderStatus({ out_trade_no: outTradeNo })
|
||||
: await financeApi.getAlipayOrderStatus({ out_trade_no: outTradeNo })
|
||||
|
||||
if (!response?.data) return
|
||||
|
||||
orderInfo.value = normalizeOrderData(response.data)
|
||||
|
||||
if (response.data.status === 'success') {
|
||||
orderStatus.value = 'success'
|
||||
isProcessing.value = false
|
||||
stopPolling()
|
||||
} else if (response.data.status === 'failed') {
|
||||
} else if (response.data.status === 'failed' || response.data.status === 'closed') {
|
||||
orderStatus.value = 'failed'
|
||||
isProcessing.value = false
|
||||
stopPolling()
|
||||
} else if (response.data.status === 'pending') {
|
||||
// 继续轮询,轮询次数在setInterval中已经增加
|
||||
if (pollCount.value >= maxPollCount.value) {
|
||||
// 超过最大轮询次数,停止轮询
|
||||
} else if (pollCount.value >= maxPollCount.value) {
|
||||
orderStatus.value = 'timeout'
|
||||
isProcessing.value = false
|
||||
stopPolling()
|
||||
// 可以显示超时提示
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('查询订单状态失败:', error)
|
||||
// 查询失败时继续轮询,不要停止
|
||||
if (pollCount.value >= maxPollCount.value) {
|
||||
orderStatus.value = 'timeout'
|
||||
isProcessing.value = false
|
||||
stopPolling()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 开始轮询
|
||||
const startPolling = () => {
|
||||
// 立即查询一次
|
||||
queryOrderStatus()
|
||||
|
||||
// 每3秒查询一次
|
||||
pollInterval.value = setInterval(() => {
|
||||
pollCount.value++ // 增加轮询次数
|
||||
pollCount.value++
|
||||
queryOrderStatus()
|
||||
}, 3000)
|
||||
}, pollIntervalMs)
|
||||
}
|
||||
|
||||
// 停止轮询
|
||||
@@ -368,10 +436,13 @@ export default {
|
||||
}
|
||||
|
||||
const retryPayment = () => {
|
||||
// 重新创建支付订单
|
||||
router.push({
|
||||
path: '/finance/wallet',
|
||||
query: { retry: 'true', amount: amount },
|
||||
query: {
|
||||
retry: 'true',
|
||||
amount: amount,
|
||||
method: isWechatPay.value ? 'wechat' : 'alipay',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -404,6 +475,7 @@ export default {
|
||||
isProcessing,
|
||||
pollCount,
|
||||
maxPollCount,
|
||||
tradeNoLabel,
|
||||
formatTime,
|
||||
goToWallet,
|
||||
goToRechargeRecords,
|
||||
|
||||
@@ -1271,6 +1271,65 @@ const createReportPaymentOrder = async (paymentType) => {
|
||||
}
|
||||
}
|
||||
|
||||
const WECHAT_REPORT_POLL_INTERVAL_MS = 5000
|
||||
const WECHAT_REPORT_MAX_POLL_COUNT = 36
|
||||
|
||||
const stopWechatPaymentPolling = () => {
|
||||
if (wechatOrderPollTimer) {
|
||||
clearInterval(wechatOrderPollTimer)
|
||||
wechatOrderPollTimer = null
|
||||
}
|
||||
isCheckingWechatPayment.value = false
|
||||
}
|
||||
|
||||
const startWechatPaymentPolling = (orderId) => {
|
||||
if (!orderId) return
|
||||
|
||||
stopWechatPaymentPolling()
|
||||
|
||||
let pollCount = 0
|
||||
|
||||
const checkOnce = async () => {
|
||||
pollCount++
|
||||
if (pollCount > WECHAT_REPORT_MAX_POLL_COUNT) {
|
||||
stopWechatPaymentPolling()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
isCheckingWechatPayment.value = true
|
||||
const response = await productApi.checkComponentReportPaymentStatus(orderId)
|
||||
const responseData = response.data?.data || response.data
|
||||
const { payment_status, can_download } = responseData || {}
|
||||
|
||||
if (payment_status === 'success' && can_download) {
|
||||
stopWechatPaymentPolling()
|
||||
ElMessageBox.close()
|
||||
canDownloadReport.value = true
|
||||
reportPaymentStatus.value = 'success'
|
||||
ElMessage.success('支付成功,正在下载示例报告...')
|
||||
await downloadComponentReport()
|
||||
} else if (payment_status === 'failed') {
|
||||
stopWechatPaymentPolling()
|
||||
ElMessageBox.close()
|
||||
ElMessage.error('支付失败,请重试')
|
||||
} else if (payment_status === 'cancelled') {
|
||||
stopWechatPaymentPolling()
|
||||
ElMessageBox.close()
|
||||
ElMessage.info('支付已取消')
|
||||
} else {
|
||||
isCheckingWechatPayment.value = false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[startWechatPaymentPolling] 检查支付状态失败:', error)
|
||||
isCheckingWechatPayment.value = false
|
||||
}
|
||||
}
|
||||
|
||||
checkOnce()
|
||||
wechatOrderPollTimer = setInterval(checkOnce, WECHAT_REPORT_POLL_INTERVAL_MS)
|
||||
}
|
||||
|
||||
// 显示微信支付二维码
|
||||
const showWechatPaymentQRCode = (codeUrl, orderId) => {
|
||||
// 验证二维码URL是否有效
|
||||
@@ -1343,6 +1402,7 @@ const showWechatPaymentQRCode = (codeUrl, orderId) => {
|
||||
closeOnClickModal: false,
|
||||
closeOnPressEscape: false,
|
||||
beforeClose: (action, instance, done) => {
|
||||
stopWechatPaymentPolling()
|
||||
if (action === 'confirm') {
|
||||
// 用户点击了"我已支付"按钮,开始检查支付状态
|
||||
startPaymentStatusCheck(orderId)
|
||||
@@ -1353,7 +1413,7 @@ const showWechatPaymentQRCode = (codeUrl, orderId) => {
|
||||
done()
|
||||
}
|
||||
}).catch(() => {
|
||||
// 对话框被关闭(通过取消按钮或其他方式)
|
||||
stopWechatPaymentPolling()
|
||||
})
|
||||
|
||||
// 开始轮询微信支付状态
|
||||
|
||||
@@ -349,6 +349,12 @@ const routes = [
|
||||
name: 'AdminCertificationReviews',
|
||||
component: () => import('@/pages/admin/certification-reviews/index.vue'),
|
||||
meta: { title: '企业审核' }
|
||||
},
|
||||
{
|
||||
path: 'query-whitelist',
|
||||
name: 'AdminQueryWhitelist',
|
||||
component: () => import('@/pages/admin/query-whitelist/index.vue'),
|
||||
meta: { title: '查询白名单' }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -121,9 +121,16 @@ export default defineConfig({
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
server: {
|
||||
proxy: {
|
||||
// // 本地开发时将 /api/v1 的请求代理到 8080 端口
|
||||
'/api/v1': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
// 保持路径不变
|
||||
rewrite: path => path,
|
||||
},
|
||||
// '/api/v1': {
|
||||
// target: 'http://localhost:8080',
|
||||
// changeOrigin: true,
|
||||
|
||||
Reference in New Issue
Block a user