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

This commit is contained in:
luo
2026-09-18 10:31:14 +08:00
parent ac541b17af
commit 5e7f5eaba4
19 changed files with 463 additions and 128 deletions

View File

@@ -22,6 +22,9 @@ export class AssetWalletService extends BaseService {
static saveAutoRenewalConfig(
data: UpdateAssetAutoRenewalConfigRequest
): Promise<BaseResponse<AssetAutoRenewalConfig>> {
return this.put<BaseResponse<AssetAutoRenewalConfig>>('/api/admin/asset-auto-renewal-config', data)
return this.put<BaseResponse<AssetAutoRenewalConfig>>(
'/api/admin/asset-auto-renewal-config',
data
)
}
}

View File

@@ -38,9 +38,7 @@ export const POLLING_PRIORITY_STATUS_OPTIONS: Array<{
{ label: '失败', value: 'failed' }
]
export const getPollingPriorityStatusName = (
status?: PollingPriorityItemStatus | string | null
) =>
export const getPollingPriorityStatusName = (status?: PollingPriorityItemStatus | string | null) =>
POLLING_PRIORITY_STATUS_OPTIONS.find((item) => item.value === status)?.label ||
String(status || '-')

View File

@@ -104,12 +104,35 @@ class StorageCompatibilityManager {
return location.href.includes(RoutesAlias.Login)
}
/**
* 检查当前是否处于免登录页面
*/
private isOnPublicPage(): boolean {
if (this.isOnLoginPage()) return true
const route = router.currentRoute.value
if (route?.meta?.noLogin) return true
// 兜底:初始导航可能尚未完成,按地址判断免登录路由
const hashPath = location.hash.replace(/^#/, '').split('?')[0]
const pathName = location.pathname.split('?')[0]
return router.getRoutes().some((record) => {
if (!record.meta?.noLogin) return false
return (
hashPath === record.path ||
hashPath.startsWith(`${record.path}/`) ||
pathName === record.path ||
pathName.startsWith(`${record.path}/`)
)
})
}
/**
* 验证存储数据完整性
*/
validateStorageData(): boolean {
// 如果在登录页面,跳过验证
if (this.isOnLoginPage()) {
// 如果在登录页或免登录页,跳过验证
if (this.isOnPublicPage()) {
return true
}

View File

@@ -64,27 +64,21 @@
<ElFormItem label="店铺名称" prop="shop_name">
<ElInput
v-model="form.shop_name"
:disabled="lockedShopName"
placeholder="请输入店铺名称"
maxlength="50"
clearable
/>
</ElFormItem>
<ElFormItem label="店铺编号" prop="shop_code">
<ElFormItem label="用户名" prop="username">
<ElInput
v-model="form.shop_code"
:disabled="lockedShopCode"
placeholder="请输入店铺编号"
v-model="form.username"
placeholder="请输入登录用户名"
maxlength="50"
clearable
/>
</ElFormItem>
</div>
<ElFormItem label="用户名" prop="username">
<ElInput v-model="form.username" placeholder="请输入登录用户名" maxlength="50" clearable />
</ElFormItem>
<div class="form-grid">
<ElFormItem label="联系人" prop="contact_name">
<ElInput v-model="form.contact_name" placeholder="选填" maxlength="50" clearable />
@@ -102,7 +96,12 @@
</div>
<ElFormItem label="详细地址" prop="address">
<ElInput v-model="form.address" placeholder="选填,请输入详细地址" maxlength="200" clearable />
<ElInput
v-model="form.address"
placeholder="选填,请输入详细地址"
maxlength="200"
clearable
/>
</ElFormItem>
<ElButton
@@ -120,7 +119,7 @@
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, reactive, ref } from 'vue'
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import type { CascaderOption, CascaderValue, FormInstance, FormRules } from 'element-plus'
@@ -140,7 +139,6 @@
code: string
password: string
shop_name: string
shop_code: string
username: string
contact_name: string
province: string
@@ -155,8 +153,7 @@
phone: '',
code: '',
password: '',
shop_name: (route.query.shop_name as string) || '',
shop_code: (route.query.shop_code as string) || '',
shop_name: '',
username: '',
contact_name: '',
province: '',
@@ -185,13 +182,13 @@
{ min: 8, max: 32, message: '密码长度为 8-32 位', trigger: 'blur' }
],
shop_name: [{ required: true, message: '请输入店铺名称', trigger: 'blur' }],
shop_code: [{ required: true, message: '请输入店铺编号', trigger: 'blur' }],
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }]
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, message: '用户名长度必须大于 2 个字符', trigger: 'blur' }
]
}
const lockedDistributionCode = computed(() => !!route.query.distribution_code)
const lockedShopName = computed(() => !!route.query.shop_name)
const lockedShopCode = computed(() => !!route.query.shop_code)
const canSendCode = computed(() => countdown.value <= 0 && !sending.value)
@@ -258,6 +255,16 @@
form.district = parts[2] || ''
}
const generateShopCode = (): string => {
const now = new Date()
const pad = (value: number) => String(value).padStart(2, '0')
const datePart = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}${pad(
now.getHours()
)}${pad(now.getMinutes())}${pad(now.getSeconds())}`
const randomPart = Math.floor(100 + Math.random() * 900)
return `S${datePart}${randomPart}`
}
const handleSubmit = () => {
if (!formRef.value) return
formRef.value.validate(async (valid) => {
@@ -270,7 +277,7 @@
code: form.code.trim(),
password: form.password,
shop_name: form.shop_name.trim(),
shop_code: form.shop_code.trim(),
shop_code: generateShopCode(),
username: form.username.trim()
}
if (form.contact_name.trim()) params.contact_name = form.contact_name.trim()
@@ -297,17 +304,28 @@
submitted.value = false
formRef.value?.resetFields()
form.distribution_code = (route.query.distribution_code as string) || form.distribution_code
form.shop_name = (route.query.shop_name as string) || form.shop_name
form.shop_code = (route.query.shop_code as string) || form.shop_code
form.code = ''
}
onMounted(() => {
document.documentElement.classList.add('allow-touch-scroll')
document.body.classList.add('allow-touch-scroll')
})
onBeforeUnmount(() => {
if (timer) clearInterval(timer)
document.documentElement.classList.remove('allow-touch-scroll')
document.body.classList.remove('allow-touch-scroll')
})
</script>
<style scoped lang="scss">
:global(html.allow-touch-scroll),
:global(body.allow-touch-scroll) {
touch-action: pan-y;
overflow-y: auto;
}
.agent-registration-page {
display: flex;
align-items: flex-start;
@@ -422,4 +440,4 @@
}
}
}
</style>
</style>

View File

@@ -1374,7 +1374,7 @@
{
prop: 'status_name',
label: '状态',
width: 100,
width: 120,
formatter: (row: WithdrawalQualificationItem) => {
const tagType = row.status === 0 ? 'warning' : row.status === 1 ? 'success' : 'danger'
return h(ElTag, { type: tagType }, () => row.status_name || String(row.status))
@@ -1389,7 +1389,8 @@
{
prop: 'attachments',
label: '附件',
minWidth: 200,
minWidth: 240,
showOverflowTooltip: true,
formatter: (row: WithdrawalQualificationItem) => qualificationAttachmentLabels(row)
},
{

View File

@@ -124,7 +124,7 @@
accept="image/*"
content-type="image/jpeg"
@uploading-change="voucherUploading = $event"
@change="createFormRef?.validateField('payment_voucher_key')"
@change="handleVoucherChange"
/>
</ElFormItem>
<ElFormItem
@@ -473,6 +473,7 @@
const paymentMethodOptionsLoading = ref(false)
const otherVoucherUploading = ref(false)
const ocrLoading = ref(false)
const recognizedVoucherKey = ref('')
// 搜索表单初始值
const initialSearchState: AgentRechargeQueryParams = {
@@ -1177,6 +1178,21 @@
// 已上传的支付凭证对象键(OCR 识别取第一个)
const offlineVoucherKeys = computed(() => toVoucherKeyList(createForm.payment_voucher_key))
// 凭证变更:校验字段,并对首次上传自动识别一次(已有流水号则跳过)
const handleVoucherChange = (keys: string[] | string) => {
createFormRef.value?.validateField('payment_voucher_key')
const list = toVoucherKeyList(keys)
const firstKey = list[0] || ''
if (!firstKey) {
recognizedVoucherKey.value = ''
return
}
if (firstKey === recognizedVoucherKey.value) return
recognizedVoucherKey.value = firstKey
if (createForm.external_transaction_no?.trim()) return
void handleRecognizeVoucher()
}
// 识别付款凭证中的交易流水号:只做预填,失败不阻断人工填写
const handleRecognizeVoucher = async () => {
if (ocrLoading.value) return

View File

@@ -97,15 +97,6 @@
/>
</ElFormItem>
<ElFormItem label="外部交易流水号" prop="external_transaction_no">
<ElInput
v-model="form.external_transaction_no"
maxlength="128"
show-word-limit
placeholder="请输入经人工核对的外部交易流水号"
/>
</ElFormItem>
<ElFormItem label="付款凭证" prop="payment_voucher_keys">
<VoucherUpload
ref="uploadRef"
@@ -113,10 +104,30 @@
voucher-name="付款凭证"
:max-count="5"
@uploading-change="voucherUploading = $event"
@change="formRef?.validateField('payment_voucher_keys')"
@change="handleVoucherChange"
/>
</ElFormItem>
<ElFormItem label="外部交易流水号" prop="external_transaction_no">
<div class="external-transaction-row">
<ElInput
v-model="form.external_transaction_no"
maxlength="128"
show-word-limit
placeholder="请输入经人工核对的外部交易流水号,或点击右侧识别凭证预填"
:disabled="ocrLoading"
/>
<ElButton
:loading="ocrLoading"
:disabled="!voucherKeys.length"
@click="handleRecognizeVoucher"
>
识别凭证
</ElButton>
</div>
<div class="external-transaction-tip"> 识别结果仅供参考,请对照凭证核对后再提交。 </div>
</ElFormItem>
<ElFormItem label="备注" prop="remark">
<ElInput
v-model="form.remark"
@@ -160,7 +171,7 @@
import { computed, reactive, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { EmployeeCollectionService } from '@/api/modules'
import { AgentRechargeService, EmployeeCollectionService } from '@/api/modules'
import type {
EmployeeCollectionAllocation,
EmployeeCollectionAllocationRequest,
@@ -209,6 +220,8 @@
const billsLoading = ref(false)
const submitting = ref(false)
const voucherUploading = ref(false)
const ocrLoading = ref(false)
const recognizedVoucherKey = ref('')
const paymentMethods = ref<EmployeeCollectionPaymentMethod[]>([])
const candidateBills = ref<BillOption[]>([])
const selectedBillIds = ref<number[]>([])
@@ -230,6 +243,7 @@
const dialogTitle = computed(() => (isResubmit.value ? '修改并重新提交核销申请' : '创建核销申请'))
const enabledPaymentMethods = computed(() => paymentMethods.value.filter((item) => item.enabled))
const enabledPaymentMethodCount = computed(() => enabledPaymentMethods.value.length)
const voucherKeys = computed(() => toVoucherKeyList(form.payment_voucher_keys))
const paidAmountFen = computed(() => yuanToFen(form.paid_amount) || 0)
const totalAmountFen = computed(() =>
@@ -376,6 +390,7 @@
form.acting_reason = ''
selectedBillIds.value = []
Object.keys(amountMap).forEach((key) => delete amountMap[Number(key)])
recognizedVoucherKey.value = ''
uploadRef.value?.clearFiles()
}
@@ -457,6 +472,58 @@
return payload
}
const recognizeVoucher = async (keysOverride?: string[]): Promise<void> => {
if (ocrLoading.value) return
const keys = keysOverride?.length ? keysOverride : voucherKeys.value
if (!keys.length) {
ElMessage.warning('请先上传付款凭证')
return
}
ocrLoading.value = true
try {
const res = await AgentRechargeService.recognizePaymentVoucher({
payment_voucher_key: keys[0]
})
if (res.code !== 0) {
ElMessage.warning(res.msg || '识别失败,请手动填写交易流水号')
return
}
const transactionNo = res.data?.external_transaction_no
if (!transactionNo) {
ElMessage.warning('未识别到交易流水号,请手动填写')
return
}
form.external_transaction_no = transactionNo
formRef.value?.validateField('external_transaction_no')
ElMessage.success('已识别交易流水号,请对照凭证核对')
} catch (error) {
console.error('识别付款凭证失败:', error)
ElMessage.warning('识别失败,请手动填写交易流水号')
} finally {
ocrLoading.value = false
}
}
const handleRecognizeVoucher = () => {
void recognizeVoucher()
}
const handleVoucherChange = (keys: string[] | string) => {
formRef.value?.validateField('payment_voucher_keys')
const list = toVoucherKeyList(keys)
const firstKey = list[0] || ''
if (!firstKey) {
recognizedVoucherKey.value = ''
return
}
if (firstKey === recognizedVoucherKey.value) return
recognizedVoucherKey.value = firstKey
if (form.external_transaction_no.trim()) return
void recognizeVoucher(list)
}
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate()
@@ -571,4 +638,17 @@
gap: 12px;
justify-content: flex-end;
}
.external-transaction-row {
display: flex;
gap: 8px;
width: 100%;
}
.external-transaction-tip {
margin-top: 4px;
font-size: 12px;
line-height: 18px;
color: var(--el-text-color-secondary);
}
</style>

View File

@@ -537,7 +537,7 @@
{
prop: 'method',
label: '退款方式',
width: 130,
width: 160,
formatter: (row: Refund) => row.method_name || '-'
},
{

View File

@@ -48,10 +48,10 @@
<el-dialog
v-model="createDialogVisible"
title="人工优先入队"
width="520px"
width="30%"
:close-on-click-modal="false"
>
<el-form ref="createFormRef" :model="createForm" :rules="createRules" label-width="100px">
<el-form ref="createFormRef" :model="createForm" :rules="createRules">
<el-form-item label="卡ICCID" prop="card_id">
<el-select
v-model="createForm.card_id"
@@ -93,8 +93,12 @@
<!-- 入队结果 -->
<el-dialog v-model="resultDialogVisible" title="入队结果" width="620px">
<div class="result-summary">
<el-tag type="success" class="result-item">新建 {{ createResult?.created_count ?? 0 }} 条</el-tag>
<el-tag type="warning" class="result-item">合并 {{ createResult?.merged_count ?? 0 }} 条</el-tag>
<el-tag type="success" class="result-item"
>新建 {{ createResult?.created_count ?? 0 }} 条</el-tag
>
<el-tag type="warning" class="result-item"
>合并 {{ createResult?.merged_count ?? 0 }} 条</el-tag
>
<span class="result-item">
任务类型:{{ (createResult?.task_type_names || []).join('、') || '-' }}
</span>
@@ -116,45 +120,49 @@
>
<div v-loading="detailLoading">
<el-descriptions v-if="detailData" :column="2" border>
<el-descriptions-item label="ID">{{ detailData.id }}</el-descriptions-item>
<el-descriptions-item label="卡ICCID">{{ detailData.iccid }}</el-descriptions-item>
<el-descriptions-item label="任务类型">
{{ detailData.task_type_name }}
</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="getStatusTagType(detailData.status)">{{
detailData.status_name
}}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="触发类型">
{{ detailData.trigger_type_name }}
</el-descriptions-item>
<el-descriptions-item label="触发次数">{{ detailData.trigger_count }}</el-descriptions-item>
<el-descriptions-item label="尝试次数">{{ detailData.attempt_count }}</el-descriptions-item>
<el-descriptions-item label="执行结果">
{{ detailData.result_name }}
</el-descriptions-item>
<el-descriptions-item label="失败原因" :span="2">
{{ detailData.failure_reason || '-' }}
</el-descriptions-item>
<el-descriptions-item label="人工操作者">
{{ detailData.manual_operator_name || '-' }}
</el-descriptions-item>
<el-descriptions-item label="入队原因">
{{ detailData.manual_reason || '-' }}
</el-descriptions-item>
<el-descriptions-item label="认领时间">
{{ formatDateTime(detailData.claimed_at) }}
</el-descriptions-item>
<el-descriptions-item label="最后触发时间">
{{ formatDateTime(detailData.last_triggered_at) }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ formatDateTime(detailData.created_at) }}
</el-descriptions-item>
<el-descriptions-item label="更新时间">
{{ formatDateTime(detailData.updated_at) }}
</el-descriptions-item>
<el-descriptions-item label="ID">{{ detailData.id }}</el-descriptions-item>
<el-descriptions-item label="卡ICCID">{{ detailData.iccid }}</el-descriptions-item>
<el-descriptions-item label="任务类型">
{{ detailData.task_type_name }}
</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="getStatusTagType(detailData.status)">{{
detailData.status_name
}}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="触发类型">
{{ detailData.trigger_type_name }}
</el-descriptions-item>
<el-descriptions-item label="触发次数">{{
detailData.trigger_count
}}</el-descriptions-item>
<el-descriptions-item label="尝试次数">{{
detailData.attempt_count
}}</el-descriptions-item>
<el-descriptions-item label="执行结果">
{{ detailData.result_name }}
</el-descriptions-item>
<el-descriptions-item label="失败原因" :span="2">
{{ detailData.failure_reason || '-' }}
</el-descriptions-item>
<el-descriptions-item label="人工操作者">
{{ detailData.manual_operator_name || '-' }}
</el-descriptions-item>
<el-descriptions-item label="入队原因">
{{ detailData.manual_reason || '-' }}
</el-descriptions-item>
<el-descriptions-item label="认领时间">
{{ formatDateTime(detailData.claimed_at) }}
</el-descriptions-item>
<el-descriptions-item label="最后触发时间">
{{ formatDateTime(detailData.last_triggered_at) }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ formatDateTime(detailData.created_at) }}
</el-descriptions-item>
<el-descriptions-item label="更新时间">
{{ formatDateTime(detailData.updated_at) }}
</el-descriptions-item>
</el-descriptions>
</div>
</el-dialog>
@@ -295,10 +303,8 @@
label: '执行结果',
width: 110,
formatter: (row: PollingPriorityItem) =>
h(
ElTag,
{ type: getResultTagType(row.result) },
() => getPollingPriorityResultName(row.result)
h(ElTag, { type: getResultTagType(row.result) }, () =>
getPollingPriorityResultName(row.result)
)
},
{

View File

@@ -34,11 +34,7 @@
</ElRadioGroup>
</ElFormItem>
<ElFormItem
v-if="form.scope === 'specified'"
label="指定主套餐"
prop="package_ids"
>
<ElFormItem v-if="form.scope === 'specified'" label="指定主套餐" prop="package_ids">
<ElSelect
v-model="form.package_ids"
multiple
@@ -70,6 +66,8 @@
</ElForm>
<div class="config-meta">
<span>总开关:{{ config?.enabled_name || '-' }}</span>
<span>适用范围:{{ config?.scope_name || '-' }}</span>
<span>配置版本:v{{ config?.config_version ?? '-' }}</span>
<span>最近保存:{{ config?.updater || '-' }}</span>
<span>{{ config?.updated_at ? formatDateTime(config.updated_at) : '-' }}</span>

View File

@@ -89,7 +89,7 @@
const code = shopData?.distribution_code
if (!shopData || !code) return ''
const base = `${window.location.origin}${window.location.pathname}`
return `${base}#/agent-registration?distribution_code=${encodeURIComponent(code)}&shop_name=${encodeURIComponent(shopData.shop_name)}&shop_code=${encodeURIComponent(shopData.shop_code)}`
return `${base}#/agent-registration?distribution_code=${encodeURIComponent(code)}`
})
const formatBusinessOwner = (value: ShopResponse) => {