feat: 顾问新客邀请链接与岗位职级字典
注册经 ?advisor= 绑定所属顾问(Account);用户抽屉展示邀请链接;岗位/职级走字典;去掉组织类型岗位。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
bcfddf79f3
commit
a703d1efdd
@ -144,7 +144,9 @@ export const CustomerService = {
|
||||
assertBusinessOk(r.data)
|
||||
},
|
||||
|
||||
async create(data: Partial<CustomerDto> & { password?: string; inviteCode?: string }): Promise<void> {
|
||||
async create(
|
||||
data: Partial<CustomerDto> & { password?: string; inviteCode?: string; advisorCode?: string },
|
||||
): Promise<void> {
|
||||
const body = {
|
||||
customerCode: data.customerCode || undefined,
|
||||
customerName: data.customerName,
|
||||
@ -165,6 +167,7 @@ export const CustomerService = {
|
||||
memberLevelId: data.memberLevelId,
|
||||
priceTierKey: data.priceTierKey,
|
||||
inviteCode: data.inviteCode?.trim() || undefined,
|
||||
advisorCode: data.advisorCode?.trim() || undefined,
|
||||
}
|
||||
const r = await axiosInstance.post(omsUrl('/customer/create'), body)
|
||||
assertBusinessOk(r.data)
|
||||
|
||||
@ -49,6 +49,8 @@ export interface WeChatCustomerRegisterInput {
|
||||
password: string;
|
||||
/** 邀请码(来自 ?ref=,注册时绑定引荐人) */
|
||||
inviteCode?: string;
|
||||
/** 顾问邀请(来自 ?advisor=,值为员工账号,注册时绑定所属顾问) */
|
||||
advisorCode?: string;
|
||||
wechat: WeChatProfileInput;
|
||||
}
|
||||
|
||||
|
||||
@ -145,10 +145,21 @@
|
||||
field="employeePosition"
|
||||
v-bind="itemColProps"
|
||||
>
|
||||
<a-input
|
||||
<a-select
|
||||
v-model="model.position"
|
||||
allow-clear
|
||||
allow-search
|
||||
:loading="positionDictLoading"
|
||||
:placeholder="t('system.employee.form.positionPlaceholder')"
|
||||
/>
|
||||
>
|
||||
<a-option
|
||||
v-for="item in positionOptions"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
@ -157,10 +168,21 @@
|
||||
field="employeeJobLevel"
|
||||
v-bind="itemColProps"
|
||||
>
|
||||
<a-input
|
||||
<a-select
|
||||
v-model="model.jobLevel"
|
||||
allow-clear
|
||||
allow-search
|
||||
:loading="jobLevelDictLoading"
|
||||
:placeholder="t('system.employee.form.jobLevelPlaceholder')"
|
||||
/>
|
||||
>
|
||||
<a-option
|
||||
v-for="item in jobLevelOptions"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
@ -298,10 +320,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { getDictListAll } from '@/api/system/dict'
|
||||
import type { EmployeeFormState } from '@/types/employee-profile'
|
||||
|
||||
const EMPLOYEE_POSITION_DICT_TYPE = 'employee_position'
|
||||
const EMPLOYEE_JOB_LEVEL_DICT_TYPE = 'employee_job_level'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = withDefaults(
|
||||
@ -317,6 +343,64 @@ const props = withDefaults(
|
||||
|
||||
const model = defineModel<EmployeeFormState>({ required: true })
|
||||
|
||||
type DictOption = { label: string; value: string }
|
||||
|
||||
const positionDictLoading = ref(false)
|
||||
const positionDictRows = ref<DictOption[]>([])
|
||||
const jobLevelDictLoading = ref(false)
|
||||
const jobLevelDictRows = ref<DictOption[]>([])
|
||||
|
||||
function withCurrentOption(rows: DictOption[], currentRaw: string | undefined | null): DictOption[] {
|
||||
const list = [...rows]
|
||||
const current = String(currentRaw || '').trim()
|
||||
if (current && !list.some((x) => x.value === current)) {
|
||||
list.unshift({ label: current, value: current })
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
const positionOptions = computed(() => withCurrentOption(positionDictRows.value, model.value?.position))
|
||||
const jobLevelOptions = computed(() => withCurrentOption(jobLevelDictRows.value, model.value?.jobLevel))
|
||||
|
||||
function mapDictRows(rows: Awaited<ReturnType<typeof getDictListAll>>): DictOption[] {
|
||||
return (rows || [])
|
||||
.slice()
|
||||
.sort((a, b) => (a.dictSort ?? 0) - (b.dictSort ?? 0))
|
||||
.map((row) => {
|
||||
const name = String(row.dictName || '').trim()
|
||||
const code = String(row.dictCode || '').trim()
|
||||
return { label: name || code, value: name || code }
|
||||
})
|
||||
.filter((x) => x.value)
|
||||
}
|
||||
|
||||
async function loadPositionDict() {
|
||||
positionDictLoading.value = true
|
||||
try {
|
||||
positionDictRows.value = mapDictRows(await getDictListAll({ dictType: EMPLOYEE_POSITION_DICT_TYPE }))
|
||||
} catch {
|
||||
positionDictRows.value = []
|
||||
} finally {
|
||||
positionDictLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadJobLevelDict() {
|
||||
jobLevelDictLoading.value = true
|
||||
try {
|
||||
jobLevelDictRows.value = mapDictRows(await getDictListAll({ dictType: EMPLOYEE_JOB_LEVEL_DICT_TYPE }))
|
||||
} catch {
|
||||
jobLevelDictRows.value = []
|
||||
} finally {
|
||||
jobLevelDictLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void loadPositionDict()
|
||||
void loadJobLevelDict()
|
||||
})
|
||||
|
||||
const itemColProps = computed(() =>
|
||||
props.dense
|
||||
? {}
|
||||
|
||||
@ -140,6 +140,23 @@
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-if="isEdit && advisorInviteUrl" :span="24">
|
||||
<a-form-item :label="t('system.user.form.advisorInviteLink')">
|
||||
<div class="ue-invite-row">
|
||||
<a-input
|
||||
class="ue-invite-input"
|
||||
:model-value="advisorInviteUrl"
|
||||
readonly
|
||||
/>
|
||||
<a-button type="primary" class="ue-invite-copy" @click="copyAdvisorInviteLink">
|
||||
{{ t('system.user.actions.copyInviteLink') }}
|
||||
</a-button>
|
||||
</div>
|
||||
<template #extra>
|
||||
<span class="ue-invite-hint">{{ t('system.user.form.advisorInviteHint') }}</span>
|
||||
</template>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</section>
|
||||
|
||||
@ -237,11 +254,13 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { IconClose, IconPlus } from '@arco-design/web-vue/es/icon'
|
||||
import EmployeeProfileFields from '@/components/EmployeeProfileFields/index.vue'
|
||||
import type { EmployeeFormState } from '@/types/employee-profile'
|
||||
import type { UserFormExtState } from '@/types/rbac-user-profile'
|
||||
import { resolveAvatarUrl } from '@/utils/avatar'
|
||||
import { buildAdvisorInviteRegisterUrl } from '@/utils/inviteLink'
|
||||
import { $baseMessage } from '@/utils'
|
||||
import { readFileAsDataUrl } from '@/utils/image-data-url'
|
||||
|
||||
@ -324,6 +343,21 @@ const employmentStatusText = computed(() => {
|
||||
return map[props.employeeFormState.employmentStatus] || ''
|
||||
})
|
||||
|
||||
const advisorInviteUrl = computed(() =>
|
||||
buildAdvisorInviteRegisterUrl(String(props.formState.account || '')),
|
||||
)
|
||||
|
||||
async function copyAdvisorInviteLink() {
|
||||
const url = advisorInviteUrl.value
|
||||
if (!url) return
|
||||
try {
|
||||
await navigator.clipboard.writeText(url)
|
||||
Message.success(t('system.user.message.copyInviteSuccess'))
|
||||
} catch {
|
||||
Message.error(t('system.user.message.copyInviteFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
function onVisibleChange(val: boolean) {
|
||||
emit('update:visible', val)
|
||||
}
|
||||
@ -374,22 +408,28 @@ defineExpose({
|
||||
|
||||
<style scoped lang="less">
|
||||
.ue-drawer {
|
||||
--ue-ink: #1a2e38;
|
||||
--ue-muted: rgba(26, 46, 56, 0.68);
|
||||
--ue-soft: rgba(26, 46, 56, 0.48);
|
||||
--ue-radius: 16px;
|
||||
--ue-edge: var(--app-acrylic-border);
|
||||
--ue-panel-bg: var(--app-acrylic-bg);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
margin: -16px;
|
||||
background: var(--app-page-bg, #f0f3f4);
|
||||
}
|
||||
|
||||
.ue-drawer__header {
|
||||
flex-shrink: 0;
|
||||
padding: 20px 24px 16px;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
color-mix(in srgb, var(--app-primary-color) 12%, var(--app-card-bg)) 0%,
|
||||
var(--app-card-bg) 58%
|
||||
);
|
||||
border-bottom: 1px solid var(--color-border-2, var(--color-border-2));
|
||||
padding: 18px 20px 14px;
|
||||
border-bottom: 1px solid var(--ue-edge);
|
||||
background: var(--ue-panel-bg);
|
||||
backdrop-filter: var(--app-acrylic-blur);
|
||||
-webkit-backdrop-filter: var(--app-acrylic-blur);
|
||||
}
|
||||
|
||||
.ue-drawer__title-row {
|
||||
@ -411,15 +451,15 @@ defineExpose({
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
line-height: 1.35;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1, #1d2129);
|
||||
font-weight: 700;
|
||||
color: var(--ue-ink);
|
||||
}
|
||||
|
||||
.ue-drawer__desc {
|
||||
margin: 10px 0 0;
|
||||
margin: 8px 0 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
color: var(--color-text-2, #4e5969);
|
||||
color: var(--ue-muted);
|
||||
}
|
||||
|
||||
.ue-drawer__close {
|
||||
@ -428,15 +468,16 @@ defineExpose({
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
color: var(--color-text-2, #4e5969);
|
||||
border: 1px solid var(--ue-edge);
|
||||
border-radius: 12px;
|
||||
background: var(--ue-panel-bg);
|
||||
color: var(--ue-muted);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, color 0.2s ease;
|
||||
transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: color-mix(in srgb, var(--app-primary-color) 12%, var(--app-card-bg));
|
||||
border-color: color-mix(in srgb, var(--app-primary-color) 22%, var(--ue-edge));
|
||||
background: var(--app-acrylic-hover-bg);
|
||||
color: var(--app-primary-color);
|
||||
}
|
||||
}
|
||||
@ -445,29 +486,22 @@ defineExpose({
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 16px 24px 8px;
|
||||
padding: 16px 16px 10px;
|
||||
}
|
||||
|
||||
.ue-preview {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 14px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 12px;
|
||||
background:
|
||||
radial-gradient(
|
||||
120% 140% at 0% 0%,
|
||||
color-mix(in srgb, #fff 24%, transparent),
|
||||
transparent 55%
|
||||
),
|
||||
linear-gradient(
|
||||
135deg,
|
||||
color-mix(in srgb, var(--app-primary-color) 92%, #102020) 0%,
|
||||
color-mix(in srgb, var(--app-primary-color) 62%, #1c3333) 100%
|
||||
);
|
||||
color: #fff;
|
||||
box-shadow: 0 10px 28px color-mix(in srgb, var(--app-primary-color) 24%, transparent);
|
||||
border-radius: var(--ue-radius);
|
||||
border: 1px solid var(--ue-edge);
|
||||
background: var(--ue-panel-bg);
|
||||
backdrop-filter: var(--app-acrylic-blur);
|
||||
-webkit-backdrop-filter: var(--app-acrylic-blur);
|
||||
box-shadow: none;
|
||||
color: var(--ue-ink);
|
||||
}
|
||||
|
||||
.ue-preview__avatar {
|
||||
@ -478,10 +512,12 @@ defineExpose({
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
flex-shrink: 0;
|
||||
border: 1px solid var(--ue-edge);
|
||||
background: color-mix(in srgb, var(--app-primary-color) 12%, #fff);
|
||||
color: var(--app-primary-color);
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
@ -493,14 +529,15 @@ defineExpose({
|
||||
.ue-preview__name {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
font-weight: 650;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
color: var(--ue-ink);
|
||||
}
|
||||
|
||||
.ue-preview__account {
|
||||
margin: 4px 0 0;
|
||||
font-size: 12px;
|
||||
opacity: 0.78;
|
||||
color: var(--ue-soft);
|
||||
}
|
||||
|
||||
.ue-preview__tags {
|
||||
@ -515,19 +552,26 @@ defineExpose({
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
border: 1px solid var(--ue-edge);
|
||||
background: color-mix(in srgb, #fff 55%, transparent);
|
||||
color: var(--ue-muted);
|
||||
|
||||
&--accent {
|
||||
background: color-mix(in srgb, #fff 22%, transparent);
|
||||
border-color: color-mix(in srgb, var(--app-primary-color) 22%, var(--ue-edge));
|
||||
background: color-mix(in srgb, var(--app-primary-color) 10%, #fff);
|
||||
color: var(--app-primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.ue-section {
|
||||
margin-bottom: 16px;
|
||||
padding: 14px 14px 4px;
|
||||
border-radius: 10px;
|
||||
background: var(--color-fill-2);
|
||||
border: 1px solid var(--color-border-2, var(--color-border-2));
|
||||
margin-bottom: 14px;
|
||||
padding: 14px 14px 6px;
|
||||
border-radius: var(--ue-radius);
|
||||
border: 1px solid var(--ue-edge);
|
||||
background: var(--ue-panel-bg);
|
||||
backdrop-filter: var(--app-acrylic-blur);
|
||||
-webkit-backdrop-filter: var(--app-acrylic-blur);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.ue-section__head {
|
||||
@ -541,27 +585,28 @@ defineExpose({
|
||||
flex-shrink: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
border-radius: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--app-primary-color);
|
||||
background: color-mix(in srgb, var(--app-primary-color) 14%, var(--app-card-bg));
|
||||
border: 1px solid color-mix(in srgb, var(--app-primary-color) 18%, var(--ue-edge));
|
||||
background: color-mix(in srgb, var(--app-primary-color) 10%, #fff);
|
||||
}
|
||||
|
||||
.ue-section__title {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1, #1d2129);
|
||||
font-weight: 700;
|
||||
color: var(--ue-ink);
|
||||
}
|
||||
|
||||
.ue-section__hint {
|
||||
margin: 2px 0 0;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3, #86909c);
|
||||
color: var(--ue-soft);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@ -573,7 +618,7 @@ defineExpose({
|
||||
:deep(.arco-form-item-label) {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1, #1d2129);
|
||||
color: var(--ue-ink);
|
||||
}
|
||||
|
||||
:deep(.arco-input-wrapper),
|
||||
@ -582,7 +627,9 @@ defineExpose({
|
||||
:deep(.arco-textarea-wrapper),
|
||||
:deep(.arco-input-number),
|
||||
:deep(.arco-picker) {
|
||||
border-radius: 6px;
|
||||
border-radius: 12px;
|
||||
border-color: color-mix(in srgb, var(--ue-edge) 70%, #d0d5dd);
|
||||
background: color-mix(in srgb, #fff 72%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
@ -596,14 +643,39 @@ defineExpose({
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.ue-invite-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ue-invite-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ue-invite-copy {
|
||||
flex-shrink: 0;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.ue-invite-hint {
|
||||
color: var(--ue-soft);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.ue-drawer__footer {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 14px 24px 18px;
|
||||
border-top: 1px solid var(--color-border-2, var(--color-border-2));
|
||||
background: var(--app-card-bg);
|
||||
padding: 14px 16px 16px;
|
||||
border-top: 1px solid var(--ue-edge);
|
||||
background: var(--ue-panel-bg);
|
||||
backdrop-filter: var(--app-acrylic-blur);
|
||||
-webkit-backdrop-filter: var(--app-acrylic-blur);
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -617,6 +689,11 @@ defineExpose({
|
||||
padding: 16px;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--app-page-bg, #f0f3f4);
|
||||
}
|
||||
|
||||
.arco-drawer {
|
||||
background: var(--app-page-bg, #f0f3f4);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -2763,6 +2763,12 @@ export default {
|
||||
'system.user.form.avatarPlaceholder': 'Enter avatar URL',
|
||||
'system.user.form.roles': 'Roles',
|
||||
'system.user.form.rolesPlaceholder': 'Select roles',
|
||||
'system.user.form.advisorInviteLink': 'New customer invite link',
|
||||
'system.user.form.advisorInviteHint':
|
||||
'Customers who register via this link are auto-bound to this account as their advisor',
|
||||
'system.user.actions.copyInviteLink': 'Copy link',
|
||||
'system.user.message.copyInviteSuccess': 'Invite link copied',
|
||||
'system.user.message.copyInviteFailed': 'Copy failed; please select the link manually',
|
||||
'system.user.form.phone': 'Phone',
|
||||
'system.user.form.phonePlaceholder': 'Enter phone number',
|
||||
'system.user.form.employeeNo': 'Employee No.',
|
||||
@ -2799,9 +2805,9 @@ export default {
|
||||
'system.employee.form.department': 'Department',
|
||||
'system.employee.form.departmentPlaceholder': 'Enter department',
|
||||
'system.employee.form.position': 'Position',
|
||||
'system.employee.form.positionPlaceholder': 'Enter position',
|
||||
'system.employee.form.positionPlaceholder': 'Select position',
|
||||
'system.employee.form.jobLevel': 'Job Level',
|
||||
'system.employee.form.jobLevelPlaceholder': 'Enter job level',
|
||||
'system.employee.form.jobLevelPlaceholder': 'Select job level',
|
||||
'system.employee.form.employmentType': 'Employment Type',
|
||||
'system.employee.form.workLocation': 'Work Location',
|
||||
'system.employee.form.workLocationPlaceholder': 'Enter work location',
|
||||
|
||||
@ -2687,6 +2687,11 @@ export default {
|
||||
'system.user.form.avatarPlaceholder': '请输入头像URL',
|
||||
'system.user.form.roles': '角色',
|
||||
'system.user.form.rolesPlaceholder': '请选择角色',
|
||||
'system.user.form.advisorInviteLink': '新客邀请链接',
|
||||
'system.user.form.advisorInviteHint': '客户通过此链接注册后,自动绑定当前账号为所属顾问',
|
||||
'system.user.actions.copyInviteLink': '复制链接',
|
||||
'system.user.message.copyInviteSuccess': '邀请链接已复制',
|
||||
'system.user.message.copyInviteFailed': '复制失败,请手动选择链接',
|
||||
'system.user.form.phone': '电话',
|
||||
'system.user.form.phonePlaceholder': '请输入电话',
|
||||
'system.user.form.employeeNo': '工号',
|
||||
@ -2721,9 +2726,9 @@ export default {
|
||||
'system.employee.form.department': '部门',
|
||||
'system.employee.form.departmentPlaceholder': '请输入部门',
|
||||
'system.employee.form.position': '岗位',
|
||||
'system.employee.form.positionPlaceholder': '请输入岗位',
|
||||
'system.employee.form.positionPlaceholder': '请选择岗位',
|
||||
'system.employee.form.jobLevel': '职级',
|
||||
'system.employee.form.jobLevelPlaceholder': '请输入职级',
|
||||
'system.employee.form.jobLevelPlaceholder': '请选择职级',
|
||||
'system.employee.form.employmentType': '用工类型',
|
||||
'system.employee.form.workLocation': '工作地点',
|
||||
'system.employee.form.workLocationPlaceholder': '请输入工作地点',
|
||||
|
||||
99
src/utils/advisorInviteStorage.ts
Normal file
99
src/utils/advisorInviteStorage.ts
Normal file
@ -0,0 +1,99 @@
|
||||
/** 顾问邀请归因暂存:7 天 last-touch(与客户邀请码并列,参数名为 advisor) */
|
||||
|
||||
const ADVISOR_INVITE_STORAGE_KEY = 'oms_advisor_invite_v1'
|
||||
const ADVISOR_INVITE_TTL_MS = 7 * 24 * 60 * 60 * 1000
|
||||
|
||||
type AdvisorInvitePayload = {
|
||||
code: string
|
||||
ts: number
|
||||
}
|
||||
|
||||
function safeParse(raw: string | null): AdvisorInvitePayload | null {
|
||||
if (!raw) return null
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as AdvisorInvitePayload
|
||||
if (!parsed?.code || typeof parsed.ts !== 'number') return null
|
||||
return { code: String(parsed.code).trim(), ts: parsed.ts }
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function isExpired(ts: number, now = Date.now()): boolean {
|
||||
return now - ts > ADVISOR_INVITE_TTL_MS
|
||||
}
|
||||
|
||||
export function stashAdvisorInvite(advisorCode?: string | null): void {
|
||||
const code = String(advisorCode ?? '').trim()
|
||||
if (!code) return
|
||||
const payload: AdvisorInvitePayload = { code, ts: Date.now() }
|
||||
const raw = JSON.stringify(payload)
|
||||
try {
|
||||
localStorage.setItem(ADVISOR_INVITE_STORAGE_KEY, raw)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
try {
|
||||
sessionStorage.setItem(ADVISOR_INVITE_STORAGE_KEY, raw)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
export function readStashedAdvisorInvite(): string {
|
||||
const now = Date.now()
|
||||
const fromSession = safeParse(
|
||||
(() => {
|
||||
try {
|
||||
return sessionStorage.getItem(ADVISOR_INVITE_STORAGE_KEY)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})(),
|
||||
)
|
||||
if (fromSession && !isExpired(fromSession.ts, now)) {
|
||||
return fromSession.code
|
||||
}
|
||||
|
||||
const fromLocal = safeParse(
|
||||
(() => {
|
||||
try {
|
||||
return localStorage.getItem(ADVISOR_INVITE_STORAGE_KEY)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})(),
|
||||
)
|
||||
if (fromLocal && !isExpired(fromLocal.ts, now)) {
|
||||
stashAdvisorInvite(fromLocal.code)
|
||||
return fromLocal.code
|
||||
}
|
||||
|
||||
if (fromLocal || fromSession) {
|
||||
clearStashedAdvisorInvite()
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export function clearStashedAdvisorInvite(): void {
|
||||
try {
|
||||
localStorage.removeItem(ADVISOR_INVITE_STORAGE_KEY)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
try {
|
||||
sessionStorage.removeItem(ADVISOR_INVITE_STORAGE_KEY)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
/** 解析顾问邀请:query.advisor → 7 天暂存 */
|
||||
export function resolveAdvisorInvite(query: Record<string, unknown>): string {
|
||||
const fromQuery = String(query.advisor ?? '').trim()
|
||||
if (fromQuery) {
|
||||
stashAdvisorInvite(fromQuery)
|
||||
return fromQuery
|
||||
}
|
||||
return readStashedAdvisorInvite()
|
||||
}
|
||||
@ -6,6 +6,16 @@ export function buildInviteRegisterUrl(inviteCode: string, origin = window.locat
|
||||
return `${origin}/wechat/register?ref=${encodeURIComponent(code)}`;
|
||||
}
|
||||
|
||||
/** 顾问新客邀请落地页:携带 ?advisor=员工账号,注册后绑定所属顾问 */
|
||||
export function buildAdvisorInviteRegisterUrl(
|
||||
advisorAccount: string,
|
||||
origin = window.location.origin,
|
||||
): string {
|
||||
const account = advisorAccount.trim();
|
||||
if (!account) return '';
|
||||
return `${origin}/wechat/register?advisor=${encodeURIComponent(account)}`;
|
||||
}
|
||||
|
||||
/** 复制/分享用话术 */
|
||||
export function buildInviteShareText(inviteCode: string, inviteUrl: string): string {
|
||||
const code = inviteCode.trim();
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { OrganizationTreeDto } from '@/api/rbac'
|
||||
import type { CustomLeftMenuTreeNode } from '@/components/CustomLeftMenu/types'
|
||||
|
||||
/** 岗位为末级,不可再挂下级;公司/部门/小组均可新增子组织 */
|
||||
/** 公司/部门/小组均可新增子组织(历史 type=4「岗位」节点视为末级,兼容旧数据) */
|
||||
export function canOrganizationHaveChildren(type?: number | null): boolean {
|
||||
return type !== 4
|
||||
}
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
import type { OrganizationTreeDto } from '@/api/rbac';
|
||||
|
||||
/** 组织类型 → 编码前缀 */
|
||||
/** 组织类型 → 编码前缀(1 公司 / 2 部门 / 3 小组;岗位走员工档案字典,不作为组织类型) */
|
||||
export const ORGANIZATION_TYPE_CODES: Record<number, string> = {
|
||||
1: 'GS', // 公司
|
||||
2: 'BM', // 部门
|
||||
3: 'XZ', // 小组
|
||||
4: 'GW', // 岗位
|
||||
};
|
||||
|
||||
export function getOrganizationTypeCode(type?: number | null): string {
|
||||
|
||||
@ -81,6 +81,11 @@ import type { FieldRule, FormInstance } from '@arco-design/web-vue'
|
||||
import { CustomerService } from '@/api/customer'
|
||||
import { validateInviteCode } from '@/api/marketing/portal'
|
||||
import { isLetterAndDigitPassword } from '@/utils/customerPassword'
|
||||
import {
|
||||
clearStashedAdvisorInvite,
|
||||
resolveAdvisorInvite,
|
||||
stashAdvisorInvite,
|
||||
} from '@/utils/advisorInviteStorage'
|
||||
import { stashInviteRef } from '@/utils/inviteRefStorage'
|
||||
import { isTimeoutError, withTimeout } from '@/utils/withTimeout'
|
||||
|
||||
@ -94,6 +99,7 @@ const router = useRouter()
|
||||
const formRef = ref<FormInstance>()
|
||||
const submitting = ref(false)
|
||||
const inviteCode = ref('')
|
||||
const advisorCode = ref('')
|
||||
const referrerInfo = ref<Record<string, unknown> | null>(null)
|
||||
const referrerValidated = ref<boolean | null>(null)
|
||||
const referrerLoading = ref(false)
|
||||
@ -189,9 +195,11 @@ async function handleSubmit() {
|
||||
...(inviteCode.value && referrerValidated.value
|
||||
? ({ inviteCode: inviteCode.value } as Record<string, string>)
|
||||
: {}),
|
||||
...(advisorCode.value ? { advisorCode: advisorCode.value } : {}),
|
||||
})
|
||||
const { clearStashedInviteRef } = await import('@/utils/inviteRefStorage')
|
||||
clearStashedInviteRef()
|
||||
clearStashedAdvisorInvite()
|
||||
Message.success(t('marketing.register.success'))
|
||||
router.replace({ name: 'login', query: inviteCode.value ? {} : undefined })
|
||||
} catch (e: any) {
|
||||
@ -208,6 +216,11 @@ function goLogin() {
|
||||
onMounted(() => {
|
||||
const ref = String(route.query.ref ?? route.query.invite ?? '').trim()
|
||||
if (ref) void loadReferrer(ref)
|
||||
const advisor = resolveAdvisorInvite(route.query as Record<string, unknown>)
|
||||
if (advisor) {
|
||||
advisorCode.value = advisor
|
||||
stashAdvisorInvite(advisor)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@ -102,6 +102,8 @@ export const OMS_DICT_TYPE_REMARKS: Record<string, string> = {
|
||||
secret_level: '密级',
|
||||
data_scope: '数据范围',
|
||||
org_scope: '组织范围',
|
||||
employee_position: '员工岗位',
|
||||
employee_job_level: '员工职级',
|
||||
}
|
||||
|
||||
/** 按预置种子中的 dictType 生成类型备注行(含排序) */
|
||||
@ -469,6 +471,32 @@ export const OMS_DICT_BATCH_SEED: DictSeedRow[] = [
|
||||
['1', '仅本级'],
|
||||
['2', '本级及下级'],
|
||||
]),
|
||||
...pack('employee_position', [
|
||||
['generalManager', '总经理'],
|
||||
['deputyGeneralManager', '副总经理'],
|
||||
['logisticsAdvisor', '物流顾问'],
|
||||
['csManager', '客服主管'],
|
||||
['csSpecialist', '客服专员'],
|
||||
['opsManager', '运营经理'],
|
||||
['opsSpecialist', '运营专员'],
|
||||
['financeManager', '财务经理'],
|
||||
['financeSpecialist', '财务专员'],
|
||||
['warehouseManager', '仓储主管'],
|
||||
['warehouseClerk', '仓管员'],
|
||||
['customsSpecialist', '关务专员'],
|
||||
['productManager', '产品经理'],
|
||||
['techEngineer', '技术工程师'],
|
||||
['hrAdmin', '人事行政'],
|
||||
['marketingSpecialist', '市场专员'],
|
||||
['dispatcher', '调度员'],
|
||||
]),
|
||||
...pack('employee_job_level', [
|
||||
['1', '一级'],
|
||||
['2', '二级'],
|
||||
['3', '三级'],
|
||||
['4', '四级'],
|
||||
['5', '五级'],
|
||||
]),
|
||||
]
|
||||
|
||||
/** 与库表唯一约束一致:dictType + dictCode */
|
||||
|
||||
@ -174,7 +174,6 @@
|
||||
<a-option :value="1">{{ t('system.organization.form.type.company') }}</a-option>
|
||||
<a-option :value="2">{{ t('system.organization.form.type.department') }}</a-option>
|
||||
<a-option :value="3">{{ t('system.organization.form.type.group') }}</a-option>
|
||||
<a-option :value="4">{{ t('system.organization.form.type.position') }}</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('system.organization.form.leader')" field="leaderId">
|
||||
@ -410,7 +409,6 @@ function getTypeName(type?: number) {
|
||||
1: t('system.organization.form.type.company'),
|
||||
2: t('system.organization.form.type.department'),
|
||||
3: t('system.organization.form.type.group'),
|
||||
4: t('system.organization.form.type.position'),
|
||||
}
|
||||
return names[type || 0] || t('system.organization.type.unknown')
|
||||
}
|
||||
@ -434,7 +432,8 @@ function handleOrgSearch() {
|
||||
}
|
||||
|
||||
function customCellRenderer(_row: number, col: number, _value: any, rowData: UserDto): string | null {
|
||||
const dataColIndex = col - 1
|
||||
// HandsontablePro 列序:操作 → 序号 → 数据;showActionColumn 时 dataColStart=2
|
||||
const dataColIndex = col - 2
|
||||
if (dataColIndex < 0 || dataColIndex >= hotColumns.value.length) return null
|
||||
const key = hotColumns.value[dataColIndex].key
|
||||
|
||||
|
||||
@ -177,7 +177,8 @@ const hotColumns = computed(() => [
|
||||
])
|
||||
|
||||
function customCellRenderer(_row: number, col: number, _value: any, rowData: UserDto): string | null {
|
||||
const dataColIndex = col - 1
|
||||
// HandsontablePro 列序:操作 → 序号 → 数据;showActionColumn 时 dataColStart=2
|
||||
const dataColIndex = col - 2
|
||||
if (dataColIndex < 0 || dataColIndex >= hotColumns.value.length) return null
|
||||
const key = hotColumns.value[dataColIndex].key
|
||||
|
||||
|
||||
@ -345,8 +345,11 @@ function isCustomerPortalUser(u: any, excludedUserIds?: Set<string>): boolean {
|
||||
}
|
||||
|
||||
function mapUserToSelectOption(u: any): SelectOption | null {
|
||||
const label = getUserDisplayName(u)
|
||||
return label ? { value: label, label } : null
|
||||
const account = String(u?.account ?? u?.Account ?? '').trim()
|
||||
if (!account) return null
|
||||
const name = getUserDisplayName(u)
|
||||
const label = name && name !== account ? `${name}(${account})` : account
|
||||
return { value: account, label }
|
||||
}
|
||||
|
||||
function getUserDisplayName(u: any): string {
|
||||
@ -362,14 +365,43 @@ function getUserDisplayName(u: any): string {
|
||||
)
|
||||
}
|
||||
|
||||
/** 表单展示/提交用姓名;若历史数据存的是用户 id,则反查姓名 */
|
||||
function resolveStaffNameForForm(value: string | number | undefined): string {
|
||||
/** 表单 value 存员工 Account;兼容历史姓名 / id */
|
||||
function resolveStaffAccountForForm(value: string | number | undefined): string {
|
||||
if (value == null || value === '') return ''
|
||||
const str = String(value)
|
||||
const byName = userOptions.value.find((o) => o.label === str || String(o.value) === str)
|
||||
if (byName) return String(byName.value)
|
||||
const user = userList.value.find((u) => String(u?.id ?? u?.Id) === str)
|
||||
if (user) return getUserDisplayName(user)
|
||||
const str = String(value).trim()
|
||||
const byAccount = userOptions.value.find((o) => String(o.value) === str)
|
||||
if (byAccount) return String(byAccount.value)
|
||||
const byLabel = userOptions.value.find((o) => o.label === str || String(o.label).includes(`(${str})`))
|
||||
if (byLabel) return String(byLabel.value)
|
||||
const user = userList.value.find(
|
||||
(u) =>
|
||||
String(u?.account ?? u?.Account ?? '') === str ||
|
||||
String(u?.id ?? u?.Id ?? '') === str ||
|
||||
getUserDisplayName(u) === str,
|
||||
)
|
||||
if (user) {
|
||||
const account = String(user?.account ?? user?.Account ?? '').trim()
|
||||
if (account) return account
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
function formatStaffDisplay(value: string | number | undefined): string {
|
||||
if (value == null || value === '') return '--'
|
||||
const str = String(value).trim()
|
||||
const opt = userOptions.value.find((o) => String(o.value) === str)
|
||||
if (opt?.label) return String(opt.label)
|
||||
const user = userList.value.find(
|
||||
(u) =>
|
||||
String(u?.account ?? u?.Account ?? '') === str ||
|
||||
getUserDisplayName(u) === str,
|
||||
)
|
||||
if (user) {
|
||||
const account = String(user?.account ?? user?.Account ?? '').trim()
|
||||
const name = getUserDisplayName(user)
|
||||
if (account && name && name !== account) return `${name}(${account})`
|
||||
return name || account || str
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
@ -615,6 +647,12 @@ function customCellRenderer(row: number, col: number, value: any, rowData: any):
|
||||
if (val === undefined || val === null) return '--'
|
||||
return Number(val).toLocaleString()
|
||||
}
|
||||
if (colKey === 'salesPerson') {
|
||||
return formatStaffDisplay(rowData?.salesPerson ?? rowData?.salesman)
|
||||
}
|
||||
if (colKey === 'customerService') {
|
||||
return formatStaffDisplay(rowData?.customerService)
|
||||
}
|
||||
if (colKey === 'createTime') return rowData?.createTime ?? rowData?.createdAt ?? '--'
|
||||
if (colKey === 'updateTime') return rowData?.updateTime ?? '--'
|
||||
if (colKey === 'lastOnlineTime') return rowData?.lastOnlineTime ?? rowData?.lastLoginTime ?? '--'
|
||||
@ -802,8 +840,8 @@ function handleEdit(record: CustomerRow) {
|
||||
memberLevelId: record.memberLevelId ?? undefined,
|
||||
priceTierKey: record.priceTierKey || '普通客户',
|
||||
remark: record.remark,
|
||||
salesPerson: resolveStaffNameForForm(record.salesPerson ?? record.salesman),
|
||||
customerService: resolveStaffNameForForm(record.customerService),
|
||||
salesPerson: resolveStaffAccountForForm(record.salesPerson ?? record.salesman),
|
||||
customerService: resolveStaffAccountForForm(record.customerService),
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
@ -884,14 +922,14 @@ async function handleModalOk(): Promise<boolean> {
|
||||
}
|
||||
modalLoading.value = true
|
||||
try {
|
||||
const salesPersonName = resolveStaffNameForForm(formState.salesPerson)
|
||||
const customerServiceName = resolveStaffNameForForm(formState.customerService)
|
||||
const salesPersonAccount = resolveStaffAccountForForm(formState.salesPerson)
|
||||
const customerServiceAccount = resolveStaffAccountForForm(formState.customerService)
|
||||
|
||||
if (isEdit.value && currentId.value != null && currentId.value !== '') {
|
||||
await CustomerService.update(currentId.value, {
|
||||
...formState,
|
||||
salesPerson: salesPersonName,
|
||||
customerService: customerServiceName,
|
||||
salesPerson: salesPersonAccount,
|
||||
customerService: customerServiceAccount,
|
||||
})
|
||||
Message.success(t('user.customer.message.updateSuccess'))
|
||||
} else {
|
||||
@ -899,8 +937,8 @@ async function handleModalOk(): Promise<boolean> {
|
||||
...formState,
|
||||
customerCode: undefined,
|
||||
password: formState.password,
|
||||
salesPerson: salesPersonName,
|
||||
customerService: customerServiceName,
|
||||
salesPerson: salesPersonAccount,
|
||||
customerService: customerServiceAccount,
|
||||
})
|
||||
Message.success(t('user.customer.message.createSuccess'))
|
||||
currentPage.value = 1
|
||||
|
||||
@ -211,6 +211,11 @@ import {
|
||||
resolveWeChatInviteRef,
|
||||
stashWeChatInviteRef,
|
||||
} from '@/utils/wechatOAuth';
|
||||
import {
|
||||
clearStashedAdvisorInvite,
|
||||
resolveAdvisorInvite,
|
||||
stashAdvisorInvite,
|
||||
} from '@/utils/advisorInviteStorage';
|
||||
import { buildInviteRegisterUrl } from '@/utils/inviteLink';
|
||||
import { isTimeoutError, withTimeout } from '@/utils/withTimeout';
|
||||
import { isWeChatBrowser, hideWeChatBrowserNav } from '@/utils/device';
|
||||
@ -295,6 +300,7 @@ const loadingHint = ref('');
|
||||
const authModalVisible = ref(false);
|
||||
const wxUserInfo = ref<WeChatOAuthUserInfo | null>(null);
|
||||
const inviteCode = ref('');
|
||||
const advisorCode = ref('');
|
||||
const referrerValidated = ref<boolean | null>(null);
|
||||
const referrerInfo = ref<{ name: string; code: string } | null>(null);
|
||||
let loadingSlowTimer: number | undefined;
|
||||
@ -417,6 +423,7 @@ async function handleSubmit() {
|
||||
customerName: formState.customerName.trim(),
|
||||
password: formState.password,
|
||||
...(canBindInvite ? { inviteCode: inviteCode.value } : {}),
|
||||
...(advisorCode.value ? { advisorCode: advisorCode.value } : {}),
|
||||
wechat: {
|
||||
openid: wxUserInfo.value.openid,
|
||||
nickname: wxUserInfo.value.nickname,
|
||||
@ -425,6 +432,7 @@ async function handleSubmit() {
|
||||
},
|
||||
});
|
||||
clearStashedWeChatInviteRef();
|
||||
clearStashedAdvisorInvite();
|
||||
Message.success(t('wechat.register.success'));
|
||||
const account = String(result?.customerCode || '').trim();
|
||||
const loggedIn = account
|
||||
@ -454,6 +462,11 @@ onMounted(() => {
|
||||
if (refCode) {
|
||||
void loadReferrer(refCode);
|
||||
}
|
||||
const advisor = resolveAdvisorInvite(route.query as Record<string, unknown>);
|
||||
if (advisor) {
|
||||
advisorCode.value = advisor;
|
||||
stashAdvisorInvite(advisor);
|
||||
}
|
||||
|
||||
const code = route.query.code;
|
||||
if (typeof code !== 'string' || !code) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user