feat: opportunityAddRuleApi

This commit is contained in:
xinxin.wu
2025-02-20 16:30:30 +08:00
committed by Craftsman
parent 4e23709529
commit d95a90b4ad
15 changed files with 337 additions and 162 deletions

View File

@@ -5,4 +5,12 @@ export enum CompanyTypeEnum {
INTERNAL = 'INTERNAL', // 国际飞书
}
export default {};
// 操作符号
export enum OperatorEnum {
EQ = 'EQ', // 等于
NE = 'NE', // 不等于
GT = 'GT', // 大于
GE = 'GE', // 大于等于
LT = 'LT', // 小于
LE = 'LE', // 小于等于
}

View File

@@ -23,3 +23,50 @@ export interface ModuleSortParams {
end: number;
dragModuleId: string; // 拖拽模块ID
}
export interface ModuleUserScopedItem {
id: string;
scope: string;
name: string;
}
export interface ModuleConditionsItem {
column: string;
operator: string;
value: string;
}
export interface OpportunityBaseInfoItem {
name: string;
enable: boolean;
expireNotice: boolean; // 到期提醒
noticeDays: number; // 提前提醒天数
operator: string; // 操作符
auto: boolean; // 自动回收
}
// 模块商机列表
export interface OpportunityItem extends OpportunityBaseInfoItem {
id: string;
organizationId: string;
ownerId: string; // 管理员ID
scopeId: string; // 范围ID
condition: string; // 回收条件
createUser: string;
updateUser: string;
createTime: number;
updateTime: number;
members: ModuleUserScopedItem[]; // 成员集合
owners: ModuleUserScopedItem[]; // 管理员集合
}
// 模块商机详情
export interface OpportunityDetail extends OpportunityBaseInfoItem {
id?: string;
conditions: ModuleConditionsItem[]; // 规则条件集合
}
export interface OpportunityParams extends OpportunityDetail {
scopeIds: string[];
ownerIds: string[];
}

View File

@@ -10,7 +10,13 @@ import {
toggleModuleNavStatusUrl,
updateOpportunityRuleUrl,
} from '@lib/shared/api/requrls/system/module';
import type { ModuleNavBaseInfoItem, ModuleSortParams } from '@lib/shared/models/system/module';
import type { CommonList, TableQueryParams } from '@lib/shared/models/common';
import type {
ModuleNavBaseInfoItem,
ModuleSortParams,
OpportunityDetail,
OpportunityItem,
} from '@lib/shared/models/system/module';
// 模块首页-导航模块列表
export function getModuleNavConfigList(data: { organizationId: string }) {
@@ -27,18 +33,18 @@ export function toggleModuleNavStatus(id: string) {
return CDR.get({ url: `${toggleModuleNavStatusUrl}/${id}` });
}
// 模块-商机-商机规则列表 TODO 类型
export function getOpportunityList(data: any) {
return CDR.post({ url: getOpportunityListUrl, data });
// 模块-商机-商机规则列表
export function getOpportunityList(data: TableQueryParams) {
return CDR.post<CommonList<OpportunityItem>>({ url: getOpportunityListUrl, data });
}
// 模块-商机-添加商机规则 TODO 类型
export function addOpportunityRule(data: any) {
// 模块-商机-添加商机规则
export function addOpportunityRule(data: OpportunityDetail) {
return CDR.post({ url: addOpportunityRuleUrl, data });
}
// 模块-商机-更新商机规则 TODO 类型
export function updateOpportunityRule(data: any) {
// 模块-商机-更新商机规则
export function updateOpportunityRule(data: OpportunityDetail) {
return CDR.post({ url: updateOpportunityRuleUrl, data });
}

View File

@@ -8,7 +8,7 @@
class="z-[1] w-[34px]"
@click="changeAllOr"
>
{{ form.allOr === 'all' ? 'all' : 'or' }}
{{ form.allOr === 'AND' ? 'all' : 'or' }}
</CrmTag>
</div>
<div class="flex-1">
@@ -171,7 +171,7 @@
});
function changeAllOr() {
form.value.allOr = form.value.allOr === 'all' ? 'or' : 'all';
form.value.allOr = form.value.allOr === 'ALL' ? 'OR' : 'AND';
}
function fieldNotRepeat(value: any[] | string | undefined, index: number, field: string, msg?: string) {

View File

@@ -10,7 +10,12 @@
/>
<div v-else class="flex items-center gap-[8px]">
<slot>{{ value }} </slot>
<CrmIcon class="cursor-pointer text-[var(--text-n4)]" type="iconicon_edit" :size="16" @click="enableEditMode" />
<CrmIcon
class="table-row-edit cursor-pointer text-[var(--text-n4)]"
type="iconicon_edit"
:size="16"
@click="enableEditMode"
/>
</div>
</template>
@@ -55,3 +60,16 @@
isEditing.value = false;
}
</script>
<style lang="less">
.n-data-table {
.table-row-edit {
@apply invisible;
}
.n-data-table-tr:not(.n-data-table-tr--summary):hover {
.table-row-edit {
@apply visible;
}
}
}
</style>

View File

@@ -0,0 +1,12 @@
import { useI18n } from '@/hooks/useI18n';
import { OperatorEnum } from '@lib/shared/enums/commonEnum';
const { t } = useI18n();
export const EQUAL = { value: OperatorEnum.EQ, label: t('common.equal') }; // 等于
export const NOT_EQUAL = { value: OperatorEnum.NE, label: t('common.notEqual') }; // 不等于
export const GT = { value: OperatorEnum.GT, label: t('common.gt') }; // 大于
export const GE = { value: OperatorEnum.GE, label: t('common.ge') }; // 大于等于
export const LT = { value: OperatorEnum.LT, label: t('common.lt') }; // 小于
export const LE = { value: OperatorEnum.LE, label: t('common.le') }; // 小于等于

View File

@@ -238,4 +238,10 @@ export default {
'common.confirmEnableTitle': 'Are you sure to enable {name}?',
'common.connectionSuccess': 'Connection success',
'common.closed': 'Closed',
'common.equal': 'Equal',
'common.notEqual': 'Not equal',
'common.gt': 'Greater than',
'common.ge': 'Greater than or equal to',
'common.lt': 'Less than',
'common.le': 'Less than or equal to',
};

View File

@@ -238,4 +238,10 @@ export default {
'common.confirmEnableTitle': '确认启用 {name} 吗?',
'common.connectionSuccess': '连接成功',
'common.closed': '已关闭',
'common.equal': '等于',
'common.notEqual': '不等于',
'common.gt': '大于',
'common.ge': '大于等于',
'common.lt': '小于',
'common.le': '小于等于',
};

View File

@@ -3,7 +3,7 @@
v-model:show="visible"
:width="800"
:title="t('module.businessManage.businessCloseRule')"
:show-continue="true"
:show-continue="!form.id"
:loading="loading"
@confirm="confirmHandler(false)"
@continue="confirmHandler(true)"
@@ -22,10 +22,10 @@
<n-form-item
require-mark-placement="left"
label-placement="left"
path="ruleName"
path="name"
:label="t('opportunity.ruleName')"
>
<n-input v-model:value="form.ruleName" type="text" :placeholder="t('common.pleaseInput')" />
<n-input v-model:value="form.name" type="text" :placeholder="t('common.pleaseInput')" />
</n-form-item>
</div>
<div class="flex">
@@ -33,10 +33,10 @@
<n-form-item
require-mark-placement="left"
label-placement="left"
path="adminId"
path="ownerIds"
:label="t('opportunity.admin')"
>
<n-select v-model:value="form.adminId" :placeholder="t('common.pleaseSelect')" :options="adminOptions" />
<CrmUserTagSelector v-model:selected-list="form.ownerIds" />
</n-form-item>
</div>
<div class="flex-1">
@@ -46,25 +46,22 @@
path="userId"
:label="t('opportunity.members')"
>
<CrmUserSelect
v-model:value="form.userId"
value-field="id"
label-field="name"
mode="remote"
filterable
:fetch-api="getUserOptions"
/>
<CrmUserTagSelector v-model:selected-list="form.scopeIds" />
</n-form-item>
</div>
</div>
<div class="crm-module-form-title"> {{ t('module.businessManage.businessCloseRule') }}</div>
<!-- 自动关闭 -->
<n-form-item
v-if="!form.id"
require-mark-placement="left"
label-placement="left"
path="autoClose"
path="auto"
:label="t('opportunity.autoClose')"
:show-feedback="false"
>
<n-radio-group v-model:value="form.autoClose" name="radiogroup">
<n-radio-group v-model:value="form.auto" name="radiogroup">
<n-space>
<n-radio key="yes" :value="true">
{{ t('common.yes') }}
@@ -76,21 +73,23 @@
</n-radio-group>
</n-form-item>
<CrmBatchForm
v-if="form.autoRecycle"
v-if="form.auto"
ref="batchFormRef"
class="mt-[16px]"
:models="formItemModel"
:default-list="form.list"
:default-list="form.conditions"
:add-text="t('module.clue.addConditions')"
:validate-when-add="true"
show-all-or
/>
<n-form-item
require-mark-placement="left"
label-placement="left"
class="mt-[16px]"
path="expirationReminder"
path="expireNotice"
:label="t('opportunity.expirationReminder')"
>
<n-radio-group v-model:value="form.expirationReminder" name="radiogroup">
<n-radio-group v-model:value="form.expireNotice" name="radiogroup">
<n-space>
<n-radio key="yes" :value="true">
{{ t('common.yes') }}
@@ -102,54 +101,19 @@
</n-radio-group>
</n-form-item>
<n-form-item
v-if="form.expireNotice"
require-mark-placement="left"
label-placement="left"
path="reminderAdvance"
path="noticeDays"
:label="t('module.reminderAdvance')"
>
<n-input
v-model:value="form.reminderAdvance"
<n-input-number
v-model:value="form.noticeDays"
class="crm-reminder-advance-input"
type="text"
:placeholder="t('common.pleaseInput')"
/>
<div class="flex flex-nowrap"> {{ t('module.reminderDays') }}</div>
</n-form-item>
<div class="crm-module-form-title"> {{ t('opportunity.clueRecoveryRule') }}</div>
<n-form-item
require-mark-placement="left"
label-placement="left"
path="expirationReminder"
:label="t('module.autoRecycle')"
>
<n-radio-group v-model:value="form.autoRecycle" name="radiogroup">
<n-space>
<n-radio key="yes" :value="true">
{{ t('common.yes') }}
</n-radio>
<n-radio key="no" :value="false">
{{ t('common.no') }}
</n-radio>
</n-space>
</n-radio-group>
</n-form-item>
<n-form-item
require-mark-placement="left"
label-placement="left"
path="expirationReminder"
:label="t('module.expirationReminder')"
>
<n-radio-group v-model:value="form.expirationReminder" name="radiogroup">
<n-space>
<n-radio key="yes" :value="true">
{{ t('common.yes') }}
</n-radio>
<n-radio key="no" :value="false">
{{ t('common.no') }}
</n-radio>
</n-space>
</n-radio-group>
</n-form-item>
</n-form>
</CrmDrawer>
</template>
@@ -162,46 +126,98 @@
NForm,
NFormItem,
NInput,
NInputNumber,
NRadio,
NRadioGroup,
NSelect,
NSpace,
SelectOption,
useMessage,
} from 'naive-ui';
import { cloneDeep } from 'lodash-es';
import CrmDrawer from '@/components/pure/crm-drawer/index.vue';
import CrmBatchForm from '@/components/business/crm-batch-form/index.vue';
import type { FormItemModel } from '@/components/business/crm-batch-form/types';
import { FieldTypeEnum } from '@/components/business/crm-form-create/enum';
import CrmUserSelect from '@/components/business/crm-user-select/index.vue';
import { SelectedUsersItem } from '@/components/business/crm-select-user-drawer/type';
import CrmUserTagSelector from '@/components/business/crm-user-tag-selector/index.vue';
import { getUserOptions } from '@/api/modules/system/org';
import { addOpportunityRule, updateOpportunityRule } from '@/api/modules/system/module';
import { EQUAL, GE, GT, LE, LT, NOT_EQUAL } from '@/config/operator';
import { useI18n } from '@/hooks/useI18n';
import { OperatorEnum } from '@lib/shared/enums/commonEnum';
import type { ModuleConditionsItem, OpportunityDetail, OpportunityParams } from '@lib/shared/models/system/module';
const { t } = useI18n();
const Message = useMessage();
export type OpportunityDetailType = {
ownerIds: SelectedUsersItem[];
scopeIds: SelectedUsersItem[];
} & OpportunityDetail;
const props = defineProps<{
rows?: OpportunityDetailType;
}>();
const emit = defineEmits<{
(e: 'loadList'): void;
(e: 'cancel'): void;
}>();
const visible = defineModel<boolean>('visible', {
required: true,
});
const adminOptions = ref([]);
const rules: FormRules = {
ruleName: [{ required: true, message: t('common.notNull', { value: `${t('org.userName')}` }) }],
adminId: [{ required: true, message: t('common.pleaseSelect') }],
userId: [{ required: true, message: t('common.pleaseSelect') }],
reminderAdvance: [{ required: true, message: t('common.pleaseInput') }],
name: [{ required: true, message: t('common.notNull', { value: `${t('org.userName')}` }) }],
ownerIds: [{ required: true, message: t('common.pleaseSelect') }],
scopeIds: [{ required: true, message: t('common.pleaseSelect') }],
noticeDays: [{ required: true, message: t('common.pleaseInput') }],
};
const closeAttrsOptions = ref<SelectOption[]>([
{
value: 'belongDays',
label: t('opportunity.belongDays'),
},
{
value: 'remainingDays',
label: t('module.remainingDays'),
},
{
value: 'opportunityStage',
label: t('opportunity.opportunityStage'),
},
]);
const formItemModel: Ref<FormItemModel[]> = ref([
{
path: 'member',
type: FieldTypeEnum.INPUT,
path: 'column',
type: FieldTypeEnum.SELECT,
rule: [
{
required: true,
message: t('common.pleaseSelect'),
},
],
selectProps: {
options: closeAttrsOptions.value,
},
},
{
path: 'operator',
type: FieldTypeEnum.INPUT,
type: FieldTypeEnum.SELECT,
rule: [
{
required: true,
message: t('common.pleaseSelect'),
},
],
selectProps: {
options: [EQUAL, NOT_EQUAL, GT, GE, LT, LE],
},
},
{
path: 'value',
@@ -209,51 +225,100 @@
},
]);
// TODO 类型
const form = ref({
id: '',
ruleName: '',
adminId: null,
userId: null,
autoClose: true,
expirationReminder: true,
reminderAdvance: '1',
autoRecycle: true,
list: [],
});
const initDefaultItem: ModuleConditionsItem = {
column: 'belongDays',
operator: OperatorEnum.EQ,
value: '',
};
const initRuleForm: OpportunityDetailType = {
name: '',
auto: false,
enable: true,
expireNotice: false,
noticeDays: 0,
conditions: [initDefaultItem],
operator: 'AND',
ownerIds: [],
scopeIds: [],
};
const form = ref<OpportunityDetailType>(cloneDeep(initRuleForm));
function cancelHandler() {
form.value.userId = null;
form.value = cloneDeep(initRuleForm);
visible.value = false;
}
const formRef = ref<FormInst | null>(null);
const loading = ref<boolean>(false);
function confirmHandler(isContinue: boolean) {
formRef.value?.validate(async (error) => {
if (!error) {
try {
loading.value = true;
if (form.value.id) {
Message.success(t('common.updateSuccess'));
} else {
Message.success(t('common.addSuccess'));
}
if (isContinue) {
// TODO
} else {
cancelHandler();
}
} catch (e) {
// eslint-disable-next-line no-console
console.log(e);
} finally {
loading.value = false;
}
const batchFormRef = ref<InstanceType<typeof CrmBatchForm>>();
function userFormValidate(cb: (_isContinue: boolean) => Promise<any>, isContinue: boolean) {
batchFormRef.value?.formValidate(async (batchForm?: Record<string, any>) => {
try {
loading.value = true;
form.value.conditions = batchForm?.list;
form.value.operator = batchForm?.allOr;
await cb(isContinue);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
});
}
async function handleSave(isContinue: boolean) {
try {
loading.value = true;
const { ownerIds, scopeIds } = form.value;
const params: OpportunityParams = {
...form.value,
ownerIds: ownerIds.map((e) => e.id),
scopeIds: scopeIds.map((e) => e.id),
};
if (form.value.id) {
await updateOpportunityRule(params);
Message.success(t('common.updateSuccess'));
} else {
await addOpportunityRule(params);
Message.success(t('common.addSuccess'));
}
if (isContinue) {
form.value = cloneDeep(initRuleForm);
} else {
cancelHandler();
}
emit('loadList');
} catch (e) {
// eslint-disable-next-line no-console
console.log(e);
} finally {
loading.value = false;
}
}
function confirmHandler(isContinue: boolean) {
formRef.value?.validate(async (error) => {
if (!error) {
userFormValidate(handleSave, isContinue);
}
});
}
watch(
() => props.rows,
(val) => {
if (val) {
// TODO 回显待联调
form.value = cloneDeep(val);
}
}
);
</script>
<style scoped lang="less"></style>

View File

@@ -12,4 +12,6 @@ export default {
'opportunity.gotIt': 'Got it',
'opportunity.goMove': 'To transfer',
'opportunity.clueRecoveryRule': 'Clue Recovery Rule',
'opportunity.belongDays': 'Belong Days',
'opportunity.opportunityStage': 'Opportunity stage',
};

View File

@@ -11,4 +11,6 @@ export default {
'opportunity.gotIt': '知道了',
'opportunity.goMove': '去转移',
'opportunity.clueRecoveryRule': '线索回收规则',
'opportunity.belongDays': '归属天数',
'opportunity.opportunityStage': '商机阶段',
};

View File

@@ -9,9 +9,12 @@
>
<div class="business-close-rule">
<div class="h-full bg-[var(--text-n10)] p-[16px]">
<n-button class="mb-[16px] mr-[12px]" type="primary" @click="addRule">
{{ t('module.businessManage.addRules') }}
</n-button>
<div class="mb-[16px] flex items-center justify-between">
<n-button class="mr-[12px]" type="primary" @click="addRule">
{{ t('module.businessManage.addRules') }}
</n-button>
<CrmSearchInput v-model:value="keyword" class="!w-[240px]" @search="searchData" />
</div>
<CrmTable
v-bind="propsRes"
@page-change="propsEvent.pageChange"
@@ -20,7 +23,12 @@
@filter-change="propsEvent.filterChange"
/>
</div>
<AddRuleDrawer v-model:visible="showAddRuleDrawer" />
<AddRuleDrawer
v-model:visible="showAddRuleDrawer"
:rows="ruleRecord"
@load-list="initOpportunityList()"
@cancel="handleCancel"
/>
</div>
</CrmDrawer>
</template>
@@ -31,19 +39,20 @@
import CrmDrawer from '@/components/pure/crm-drawer/index.vue';
import CrmNameTooltip from '@/components/pure/crm-name-tooltip/index.vue';
import CrmSearchInput from '@/components/pure/crm-search-input/index.vue';
import CrmTable from '@/components/pure/crm-table/index.vue';
import type { CrmTableDataItem } from '@/components/pure/crm-table/type';
import { CrmDataTableColumn } from '@/components/pure/crm-table/type';
import useTable from '@/components/pure/crm-table/useTable';
import CrmOperationButton from '@/components/business/crm-operation-button/index.vue';
import AddRuleDrawer from './addRuleDrawer.vue';
import { getOpportunityList } from '@/api/modules/system/module';
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import { characterLimit } from '@/utils';
import type { OpportunityDetailType } from './addRuleDrawer.vue';
import { TableKeyEnum } from '@lib/shared/enums/tableEnum';
import type { CommonList } from '@lib/shared/models/common';
const { openModal } = useModal();
const Message = useMessage();
@@ -54,6 +63,8 @@
required: true,
});
const keyword = ref<string>('');
const groupList = ref([
{
label: t('common.edit'),
@@ -65,7 +76,9 @@
},
]);
// TODO 等待联调
function addOrEditRule(row: any) {}
// 删除规则
function deleteRule(row: any) {
const hasData = false;
@@ -244,38 +257,7 @@
},
];
function initData() {
const data: CommonList<CrmTableDataItem<any>> = {
total: 11,
pageSize: 10,
current: 1,
list: [
{
id: '11',
num: 'string',
title: 'string',
enable: false,
updateTime: null,
createTime: null,
},
{
id: '22',
num: '232324323',
title: '222',
enable: false,
updateTime: null,
createTime: null,
},
],
};
return new Promise<CommonList<CrmTableDataItem<any>>>((resolve) => {
setTimeout(() => {
resolve(data);
}, 200);
});
}
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(initData, {
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(getOpportunityList, {
tableKey: TableKeyEnum.MODULE_OPPORTUNITY_RULE_TABLE,
showSetting: true,
columns,
@@ -283,10 +265,27 @@
});
const showAddRuleDrawer = ref<boolean>(false);
const ruleRecord = ref<OpportunityDetailType>();
function addRule() {
showAddRuleDrawer.value = true;
}
function handleCancel() {
ruleRecord.value = undefined;
}
function initOpportunityList() {
setLoadListParams({
keyword: keyword.value,
});
loadList();
}
function searchData(val: string) {
keyword.value = val;
initOpportunityList();
}
onBeforeMount(() => {
loadList();
});

View File

@@ -13,6 +13,7 @@ export default {
'After closing, members will not find the module in the main navigation, please be careful!',
'module.openModuleTip': 'Confirm to open module {name}?',
'module.openModuleTipContent': 'After opening, the module appears in the main navigation menu',
'module.remainingDays': 'Remaining ownership days',
'module.customer.openSea': 'Public Sea Settings',
'module.customer.capacitySet': 'Customer capacity settings',
'module.clue': 'Clue Pool',

View File

@@ -12,6 +12,7 @@ export default {
'module.closeModuleTipContent': '关闭后,成员在主导航找不到该模块,请谨慎操作!',
'module.openModuleTip': '确认开启模块 {name} 吗',
'module.openModuleTipContent': '模块开启后,模块出现在主导航菜单',
'module.remainingDays': '剩余归属天数',
'module.customer.openSea': '公海设置',
'module.customer.capacitySet': '客户库容设置',
'module.clue': '线索池',

View File

@@ -690,14 +690,14 @@
tooltip: true,
},
},
{
title: t('org.userGroup'),
key: 'userGroup',
width: 100,
ellipsis: {
tooltip: true,
},
},
// {
// title: t('org.userGroup'),
// key: 'userGroup',
// width: 100,
// ellipsis: {
// tooltip: true,
// },
// },
{
title: t('common.createTime'),
key: 'createTime',
@@ -763,6 +763,8 @@
position: row.position || '-',
departmentName: row.departmentName || '-',
workCity: getCityPath(row.workCity) || '-',
phone: row.phone || '-',
email: row.email || '-',
};
}
);