fix: some
All checks were successful
构建并部署前端到测试环境 / deploy (push) Successful in 55s

This commit is contained in:
luo
2026-10-14 17:08:32 +08:00
parent 16effb69f3
commit 0d77a7d55b
23 changed files with 1113 additions and 89 deletions

View File

@@ -3,6 +3,7 @@
*/
import { BaseService } from '../BaseService'
import { getNewLoginPasswordError } from '@/utils/validation/loginPassword'
import type {
AgentDistributionRegistrationPageResult,
AgentDistributionRegistrationQueryParams,
@@ -33,7 +34,7 @@ export interface AgentDistributionRegistrationParams {
distribution_code: string // 分销码
phone: string // 手机号
code: string // 短信验证码
password: string // 密码
password: string // 8~12 位,大小写字母、数字、半角特殊符号至少三类,无空格和中文
shop_name: string // 店铺名称
shop_code: string // 店铺编号
username: string // 用户名
@@ -71,10 +72,12 @@ export class AgentDistributionService extends BaseService {
static registerAgent(
params: AgentDistributionRegistrationParams
): Promise<BaseResponse<AgentDistributionRegistrationResponse>> {
const error = getNewLoginPasswordError(params.password)
if (error) return Promise.reject(new Error(error))
return this.post<BaseResponse<AgentDistributionRegistrationResponse>>(
'/api/c/v1/agent-distribution-registrations',
params,
{ requestOptions: { withToken: false } }
{ requestOptions: { withToken: false, errorMessageMode: 'none' } }
)
}

View File

@@ -1,6 +1,7 @@
<template>
<ElDialog
v-model="dialogVisible"
append-to-body
:title="t('passwordChange.title')"
width="min(420px, calc(100vw - 32px))"
:close-on-click-modal="false"

View File

@@ -53,12 +53,18 @@
</div>
<!-- 列设置 -->
<ElPopover placement="bottom" trigger="click">
<ElPopover
v-model:visible="columnSettingsOpen"
placement="bottom"
trigger="click"
@after-enter="startColumnDrag"
@before-leave="stopColumnDrag"
>
<template #reference>
<div class="btn"><i class="iconfont-sys" style="font-size: 17px">&#xe620;</i> </div>
</template>
<div>
<VueDraggable v-model="columns">
<div ref="columnSettingsRef">
<div v-for="item in columns" :key="item.prop || item.type" class="column-option">
<div class="drag-icon">
<i class="iconfont-sys">&#xe648;</i>
@@ -67,7 +73,7 @@
item.label || (item.type === 'selection' ? t('table.selection') : '')
}}</ElCheckbox>
</div>
</VueDraggable>
</div>
</div>
</ElPopover>
<!-- 其他设置 -->
@@ -98,8 +104,10 @@
import { TableSizeEnum } from '@/enums/formEnum'
import { useTableStore } from '@/store/modules/table'
import { ElPopover, ElCheckbox } from 'element-plus'
import { VueDraggable } from 'vue-draggable-plus'
import { useDraggable } from 'vue-draggable-plus'
import { useI18n } from 'vue-i18n'
import { onBeforeUnmount, onDeactivated, onMounted, onUnmounted, ref } from 'vue'
import { storeToRefs } from 'pinia'
const { t } = useI18n()
@@ -128,6 +136,27 @@
const columns = defineModel<ColumnOption[]>('columns', { required: true })
const columnSettingsRef = ref<HTMLElement | null>(null)
const columnSettingsOpen = ref(false)
// Popover 内容和路由生命周期不同,禁止拖拽库在挂载时用空 ref 自动初始化。
const columnDraggable = useDraggable(columnSettingsRef, columns, { immediate: false })
const startColumnDrag = () => {
const element = columnSettingsRef.value
if (!columnSettingsOpen.value || !(element instanceof HTMLElement) || !element.isConnected) {
return
}
columnDraggable.start(element)
}
const stopColumnDrag = () => {
columnSettingsOpen.value = false
columnDraggable.destroy()
}
onBeforeUnmount(stopColumnDrag)
onDeactivated(stopColumnDrag)
// 定义视图模式 model
const currentView = defineModel<'table' | 'descriptions'>('currentView', { default: 'table' })

View File

@@ -27,7 +27,7 @@
"newPasswordRequired": "Please enter a new password",
"confirmPasswordRequired": "Please enter the new password again",
"passwordLength": "Password must be 8-12 characters",
"passwordRule": "8-12 characters with uppercase, lowercase and digits; no special characters",
"passwordRule": "8-12 characters; at least three of uppercase letters, lowercase letters, digits and special characters. No spaces or Chinese characters.",
"passwordMismatch": "The new passwords do not match",
"submit": "Change password",
"success": "Password changed successfully",

View File

@@ -28,7 +28,7 @@
"newPasswordRequired": "请输入新密码",
"confirmPasswordRequired": "请再次输入新密码",
"passwordLength": "密码长度需为8-12位",
"passwordRule": "8~12位,必须包含大写字母、小写字母和数字,不允许特殊符号",
"passwordRule": "8~12位,大写字母、小写字母、数字、特殊符号至少包含三类,不允许空格和中文",
"passwordMismatch": "两次输入的新密码不一致",
"submit": "确认修改",
"success": "密码修改成功",

Binary file not shown.

View File

@@ -124,7 +124,7 @@ export interface AgentQueryParams extends PaginationParams {
// 创建账号参数(匹配后端 ModelCreateAccountRequest)
export interface CreatePlatformAccountParams {
username: string // 用户名 (3-50字符)
password: string // 8~12位,必须包含大写、小写和数字,禁止特殊符号
password: string // 8~12位,大写、小写、数字、特殊符号至少三类,无空格或中文
phone: string // 手机号 (11位)
user_type: number // 用户类型 (1:超级管理员, 2:平台用户, 3:代理账号, 4:企业账号)
enterprise_id?: number | null // 关联企业ID(企业账号必填)

View File

@@ -32,6 +32,7 @@ export interface LoginData {
export interface UserInfo {
id: number // 用户ID
username: string // 用户名
name?: string | null // me 接口返回的员工姓名
phone: string // 手机号
user_type: number // 用户类型 (1:超级管理员, 2:平台用户, 3:代理账号, 4:企业账号)
user_type_name: string // 用户类型名称

View File

@@ -92,16 +92,26 @@ declare module 'vue' {
CreateRefundDialog: typeof import('./../components/business/CreateRefundDialog.vue')['default']
CustomerAccountDialog: typeof import('./../components/business/CustomerAccountDialog.vue')['default']
DetailPage: typeof import('./../components/common/DetailPage.vue')['default']
ElAlert: typeof import('element-plus/es')['ElAlert']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard']
ElCascader: typeof import('element-plus/es')['ElCascader']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
ElCol: typeof import('element-plus/es')['ElCol']
ElCollapse: typeof import('element-plus/es')['ElCollapse']
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
ElEmpty: typeof import('element-plus/es')['ElEmpty']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
@@ -114,16 +124,22 @@ declare module 'vue' {
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElResult: typeof import('element-plus/es')['ElResult']
ElRow: typeof import('element-plus/es')['ElRow']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTag: typeof import('element-plus/es')['ElTag']
ElTimeline: typeof import('element-plus/es')['ElTimeline']
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
ElUpload: typeof import('element-plus/es')['ElUpload']
ElWatermark: typeof import('element-plus/es')['ElWatermark']
ExportTaskCreateDialog: typeof import('./../components/business/ExportTaskCreateDialog.vue')['default']

View File

@@ -9,6 +9,7 @@ import { LocationQueryRaw } from 'vue-router'
export interface UserInfo {
id: number // 用户ID
username: string // 用户名
name?: string | null // me 接口返回的员工姓名
phone: string // 手机号
user_type: number // 用户类型 (1:超级管理员, 2:平台用户, 3:代理账号, 4:企业账号)
user_type_name: string // 用户类型名称

View File

@@ -0,0 +1,24 @@
import { normalizeApiError } from './apiError'
const asRecord = (value: unknown): Record<string, unknown> | undefined =>
value !== null && typeof value === 'object' ? (value as Record<string, unknown>) : undefined
// 仅对公开代理注册隐藏企微场景/模板细节,后台管理和其他接口仍展示原错误。
export const getAgentRegistrationFailureMessage = (value: unknown): string => {
const root = asRecord(value)
const payload =
asRecord(asRecord(root?.response)?.data) ??
(root?.code !== undefined ? root : (asRecord(root?.data) ?? root))
if (
payload?.code === 1050 ||
payload?.code === 1001 ||
payload?.code === '1050' ||
payload?.code === '1001'
) {
return '注册失败, 请联系管理员'
}
for (const message of [payload?.msg, payload?.message]) {
if (typeof message === 'string' && message.trim()) return message
}
return normalizeApiError(value).message
}

View File

@@ -0,0 +1,34 @@
import type { EmployeeCollectionDebtorSnapshot } from '@/types/api/employeeCollection'
interface ResponsibleBill {
debtor_account_id?: number | null
debtor_snapshot?: EmployeeCollectionDebtorSnapshot | null
}
interface CurrentEmployee {
id?: number
name?: string | null
user_type?: number
}
const normalizedName = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')
export const isEmployeeCollectionBillSelf = (
bill: ResponsibleBill | undefined,
employee: CurrentEmployee
): boolean => {
const employeeName = normalizedName(employee.name)
const debtorName = normalizedName(bill?.debtor_snapshot?.account_name)
// 用户确认:账单负责员工与 me.user.name 同名时,本人办理,不发送代办原因。
if (employeeName && debtorName && employeeName === debtorName) return true
const debtorId = Number(bill?.debtor_snapshot?.account_id ?? bill?.debtor_account_id)
const employeeId = Number(employee.id)
return debtorId > 0 && employeeId > 0 && debtorId === employeeId
}
export const requiresEmployeeCollectionActingReason = (
bills: Array<ResponsibleBill | undefined>,
employee: CurrentEmployee
): boolean =>
Number(employee.user_type) === 1 &&
(bills.length === 0 || bills.some((bill) => !isEmployeeCollectionBillSelf(bill, employee)))

View File

@@ -1,14 +1,22 @@
import type { FormItemRule } from 'element-plus'
export const NEW_LOGIN_PASSWORD_HINT = '8~12位,必须包含大写字母、小写字母和数字,不允许特殊符号'
export const NEW_LOGIN_PASSWORD_HINT =
'8~12位,大写字母、小写字母、数字、特殊符号至少包含三类,不允许空格和中文'
export const getNewLoginPasswordError = (value: unknown): string | undefined => {
if (typeof value !== 'string' || value.length < 8 || value.length > 12) {
return '密码长度必须为8~12位'
}
if (/[^A-Za-z0-9]/.test(value)) return '密码只允许英文字母和数字,不允许特殊符号、空格或中文'
if (!/[A-Z]/.test(value) || !/[a-z]/.test(value) || !/[0-9]/.test(value)) {
return '密码必须同时包含大写字母、小写字母和数字'
// 允许所有可打印的半角符号,不将空白、中文、emoji 或控制字符计作特殊符号。
if (/[^\x21-\x7E]/.test(value)) return '密码只允许英文字母、数字和半角特殊符号,不允许空格或中文'
const categoryCount = [
/[A-Z]/.test(value),
/[a-z]/.test(value),
/[0-9]/.test(value),
/[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]/.test(value)
].filter(Boolean).length
if (categoryCount < 3) {
return '密码须包含大写字母、小写字母、数字、特殊符号中的至少三类'
}
return undefined
}

View File

@@ -47,17 +47,25 @@
</div>
</ElFormItem>
<ElFormItem label="密码" prop="password">
<ElFormItem label="密码" prop="password" class="password-form-item">
<ElInput
v-model="form.password"
type="password"
show-password
placeholder="请输入密码(8-32 位)"
maxlength="32"
placeholder="请输入密码(8~12位)"
maxlength="12"
autocomplete="new-password"
aria-describedby="registration-password-rule"
/>
<div class="strength-bar" :class="strengthClass">
<p id="registration-password-rule" class="password-hint">
{{ NEW_LOGIN_PASSWORD_HINT }}
</p>
<div class="strength-bar" :class="strengthClass" aria-hidden="true">
<i v-for="n in 3" :key="n" :class="{ active: passwordStrength >= n }" />
</div>
<span v-if="form.password" class="password-status" aria-live="polite">
{{ passwordStrength === 3 ? '密码符合规则' : '密码尚未符合规则' }}
</span>
</ElFormItem>
<div class="form-grid">
@@ -129,6 +137,12 @@
type AgentDistributionRegistrationParams
} from '@/api/modules/agentDistribution'
import { regionData } from '@/utils/constants/regionData'
import { getAgentRegistrationFailureMessage } from '@/utils/business/agentRegistrationError'
import {
createNewLoginPasswordRule,
getNewLoginPasswordError,
NEW_LOGIN_PASSWORD_HINT
} from '@/utils/validation/loginPassword'
const route = useRoute()
@@ -180,7 +194,7 @@
code: [{ required: true, message: '请输入短信验证码', trigger: 'blur' }],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 8, max: 32, message: '密码长度为 8-32 位', trigger: 'blur' }
createNewLoginPasswordRule()
],
shop_name: [{ required: true, message: '请输入店铺名称', trigger: 'blur' }],
username: [
@@ -234,14 +248,7 @@
const passwordStrength = computed(() => {
const pwd = form.password
if (!pwd) return 0
let score = 0
if (pwd.length >= 8) score += 1
if (/[a-z]/.test(pwd) && /[A-Z]/.test(pwd)) score += 1
if (/\d/.test(pwd)) score += 1
if (/[^A-Za-z0-9]/.test(pwd)) score += 1
if (score >= 4) return 3
if (score >= 2) return 2
return 1
return getNewLoginPasswordError(pwd) ? 1 : 3
})
const strengthClass = computed(() => {
@@ -267,9 +274,9 @@
}
const handleSubmit = () => {
if (!formRef.value) return
if (!formRef.value || submitting.value) return
formRef.value.validate(async (valid) => {
if (!valid) return
if (!valid || submitting.value) return
submitting.value = true
try {
const params: AgentDistributionRegistrationParams = {
@@ -291,10 +298,10 @@
if (res.code === 0) {
submitted.value = true
} else {
ElMessage.error('分销码不可用')
ElMessage.error(getAgentRegistrationFailureMessage(res))
}
} catch {
ElMessage.error('分销码不可用')
} catch (error) {
ElMessage.error(getAgentRegistrationFailureMessage(error))
} finally {
submitting.value = false
}
@@ -315,19 +322,19 @@
<style scoped lang="scss">
.agent-registration-page {
box-sizing: border-box;
display: flex;
align-items: flex-start;
justify-content: center;
min-height: 100vh;
padding: 48px 16px 64px;
box-sizing: border-box;
background: linear-gradient(160deg, #eef3fb 0%, #f7f9fd 40%, #eef3fb 100%);
.register-card {
box-sizing: border-box;
width: 100%;
max-width: 520px;
padding: 36px 36px 28px;
box-sizing: border-box;
background: #fff;
border-radius: 12px;
box-shadow: 0 8px 30px rgb(93 135 255 / 12%);
@@ -375,13 +382,14 @@
.strength-bar {
display: flex;
gap: 6px;
width: 100%;
margin-top: 8px;
i {
flex: 1;
height: 4px;
border-radius: 2px;
background: var(--el-border-color-lighter);
border-radius: 2px;
transition: background-color 0.2s ease;
&.active {
@@ -398,6 +406,21 @@
}
}
.password-hint,
.password-status {
width: 100%;
margin: 8px 0 0;
font-size: 13px;
line-height: 1.5;
color: var(--el-text-color-regular);
}
.password-form-item :deep(.el-form-item__error) {
position: static;
width: 100%;
line-height: 1.5;
}
.region-cascader {
width: 100%;
}
@@ -411,7 +434,7 @@
padding: 8px 0;
}
@media (max-width: 768px) {
@media (width <= 768px) {
padding: 24px 12px 40px;
.register-card {
@@ -429,7 +452,7 @@
}
}
@media (max-width: 600px) {
@media (width <= 600px) {
:global(.agent-registration-cascader-popper) {
width: calc(100vw - 24px) !important;
max-width: calc(100vw - 24px) !important;
@@ -441,10 +464,10 @@
}
:global(.agent-registration-cascader-popper .el-cascader-menu) {
box-sizing: border-box;
flex: 1 1 0;
width: 0;
min-width: 0 !important;
box-sizing: border-box;
}
:global(.agent-registration-cascader-popper .el-cascader-node__label) {

View File

@@ -315,6 +315,7 @@
import { fenToYuan, yuanToFen } from '@/utils/business/format'
import { getErrorMessage, toVoucherKeyList } from '@/utils/business'
import { useUserStore } from '@/store/modules/user'
import { requiresEmployeeCollectionActingReason } from '@/utils/business/employeeCollectionActing'
import VoucherUpload from '@/components/business/VoucherUpload.vue'
import {
BILL_SOURCE_OPTIONS,
@@ -404,28 +405,34 @@
const dialogTitle = computed(() => (isResubmit.value ? '修改并重新提交核销申请' : '创建核销申请'))
const voucherKeys = computed(() => toVoucherKeyList(form.payment_voucher_keys))
const currentAccountId = computed(() => userStore.info?.id)
const findBillOption = (billId: number): BillOption | undefined =>
selectedBillOptions.value[billId] || candidateBills.value.find((bill) => bill.id === billId)
const findBillOption = (billId: number): BillOption | undefined => {
const selected = selectedBillOptions.value[billId]
const candidate = candidateBills.value.find((bill) => bill.id === billId)
if (!selected) return candidate
return {
...selected,
debtor_account_id: candidate?.debtor_account_id ?? selected.debtor_account_id,
debtor_snapshot: candidate?.debtor_snapshot ?? selected.debtor_snapshot
}
}
const getBillAssetIdentifier = (bill: BillOption): string => {
const source = candidateBills.value.find((item) => item.id === bill.id) || bill
return source.customer_snapshot?.asset_identifier || source.asset_identifier || '-'
}
const getBillDebtorAccountId = (billId: number): number | null => {
const option = findBillOption(billId)
return option?.debtor_account_id ?? option?.debtor_snapshot?.account_id ?? null
}
const isActing = computed(() =>
requiresEmployeeCollectionActingReason(
selectedBillIds.value.map(findBillOption),
userStore.info ?? {}
)
)
const isActing = computed(() => {
if (!userStore.isSuperAdmin) return false
if (!selectedBillIds.value.length) return true
return selectedBillIds.value.some((billId) => {
const debtorAccountId = getBillDebtorAccountId(billId)
return debtorAccountId === null || debtorAccountId !== currentAccountId.value
})
watch(isActing, (acting) => {
if (!acting) {
form.acting_reason = ''
formRef.value?.clearValidate('acting_reason')
}
})
const displayBills = computed<BillOption[]>(() => {
@@ -703,8 +710,22 @@
form.acting_reason = application.acting_reason || ''
const allocations = await fetchApplicationAllocations(application.id)
allocations.forEach((allocation) => {
const allocationBills = await Promise.all(
allocations.map(async (allocation) => {
const candidate = candidateBills.value.find((bill) => bill.id === allocation.bill_id)
if (candidate) return candidate
try {
const res = await EmployeeCollectionService.getBillById(allocation.bill_id)
return res.code === 0 && res.data?.bill ? toBillOption(res.data.bill) : undefined
} catch {
// 无法取得责任员工信息时保持原保守代办规则,不猜测为本人。
return undefined
}
})
)
allocations.forEach((allocation, index) => {
const option: BillOption = {
...allocationBills[index],
id: allocation.bill_id,
source_no: allocation.bill_source_no,
remaining_amount: Math.max(
@@ -846,9 +867,9 @@
external_transaction_no: form.external_transaction_no.trim(),
payment_voucher_keys: toVoucherKeyList(form.payment_voucher_keys),
remark: form.remark.trim() || undefined,
allocations,
acting_reason: isActing.value ? form.acting_reason.trim() : undefined
allocations
}
if (isActing.value) payload.acting_reason = form.acting_reason.trim()
submitting.value = true
try {