feat(member): change MemberId to Guid strongly typed id
- Convert MemberId from long to Guid strongly typed ID - Update all Member commands to use record pattern with MemberId - Update all Member endpoints to use record pattern with MemberId - Update entity configurations to use GuidVersion7ValueGenerator - Add implicit conversion operators for MemberId Migration: ChangeMemberIdToGuid
This commit is contained in:
parent
38db4e2e73
commit
bda792c89d
@ -1,5 +1,6 @@
|
||||
export * from './log';
|
||||
export * from './oauth';
|
||||
export * from './points-rule';
|
||||
export * from './role';
|
||||
export * from './tenant';
|
||||
export * from './user';
|
||||
|
||||
53
apps/web-ele/src/api/fengling/points-rule.ts
Normal file
53
apps/web-ele/src/api/fengling/points-rule.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import type { FenglingApi } from './typings';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace PointsRuleApi {
|
||||
const apiPrefix = '/api/v1/points-rules';
|
||||
|
||||
export async function getRulesList() {
|
||||
return requestClient.get<FenglingApi.PointsRule.PointsRuleDto[]>(apiPrefix);
|
||||
}
|
||||
|
||||
export async function getRuleById(id: string) {
|
||||
return requestClient.get<FenglingApi.PointsRule.PointsRuleDto>(`${apiPrefix}/${id}`);
|
||||
}
|
||||
|
||||
export async function createRule(data: FenglingApi.PointsRule.CreatePointsRuleRequest) {
|
||||
return requestClient.post<FenglingApi.PointsRule.CreatePointsRuleResponse, FenglingApi.PointsRule.CreatePointsRuleRequest>(apiPrefix, data);
|
||||
}
|
||||
|
||||
export async function updateRule(id: string, data: Partial<FenglingApi.PointsRule.CreatePointsRuleRequest>) {
|
||||
return requestClient.put<void, Partial<FenglingApi.PointsRule.CreatePointsRuleRequest>>(`${apiPrefix}/${id}`, data);
|
||||
}
|
||||
|
||||
export async function deleteRule(id: string) {
|
||||
return requestClient.delete<void>(`${apiPrefix}/${id}`);
|
||||
}
|
||||
|
||||
export async function toggleRuleStatus(id: string, isActive: boolean) {
|
||||
return requestClient.patch<void>(`${apiPrefix}/${id}/status`, { isActive });
|
||||
}
|
||||
|
||||
export async function calculatePoints(data: FenglingApi.PointsRule.CalculatePointsRequest) {
|
||||
return requestClient.post<FenglingApi.PointsRule.PointsCalculationResultDto, FenglingApi.PointsRule.CalculatePointsRequest>(`${apiPrefix}/calculate`, data);
|
||||
}
|
||||
|
||||
export async function batchEnable(ids: string[]) {
|
||||
return requestClient.post<{ success: number; failed: number }, { ids: string[] }>(`${apiPrefix}/batch/enable`, { ids });
|
||||
}
|
||||
|
||||
export async function batchDisable(ids: string[]) {
|
||||
return requestClient.post<{ success: number; failed: number }, { ids: string[] }>(`${apiPrefix}/batch/disable`, { ids });
|
||||
}
|
||||
|
||||
export async function batchDelete(ids: string[]) {
|
||||
return requestClient.post<{ success: number; failed: number }, { ids: string[] }>(`${apiPrefix}/batch/delete`, { ids });
|
||||
}
|
||||
|
||||
export async function batchExport(ids: string[]) {
|
||||
return requestClient.post<Blob, { ids: string[] }>(`${apiPrefix}/batch/export`, { ids }, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -186,6 +186,92 @@ export namespace FenglingApi {
|
||||
}
|
||||
}
|
||||
|
||||
export namespace PointsRule {
|
||||
export enum RuleType {
|
||||
FixedValue = 1,
|
||||
PriceWeighted = 2,
|
||||
}
|
||||
|
||||
export enum CalculationMode {
|
||||
Synchronous = 1,
|
||||
Asynchronous = 2,
|
||||
}
|
||||
|
||||
export enum DimensionType {
|
||||
Product = 1,
|
||||
Dealer = 2,
|
||||
Distributor = 3,
|
||||
Store = 4,
|
||||
}
|
||||
|
||||
export interface PointsRuleConditionDto {
|
||||
id: string
|
||||
ruleId: string
|
||||
dimensionType: DimensionType
|
||||
dimensionValue: string
|
||||
operator?: string
|
||||
}
|
||||
|
||||
export interface PointsRuleDto {
|
||||
id: string
|
||||
name: string
|
||||
code: string
|
||||
ruleType: RuleType
|
||||
basePoints: number
|
||||
weightFactor?: number
|
||||
validityDays: number
|
||||
priority: number
|
||||
calculationMode: CalculationMode
|
||||
isActive: boolean
|
||||
effectiveFrom: string
|
||||
effectiveTo?: string
|
||||
conditions: PointsRuleConditionDto[]
|
||||
}
|
||||
|
||||
export interface CreateConditionRequest {
|
||||
dimensionType: DimensionType
|
||||
dimensionValue: string
|
||||
operator?: string
|
||||
}
|
||||
|
||||
export interface CreatePointsRuleRequest {
|
||||
name: string
|
||||
code: string
|
||||
ruleType: RuleType
|
||||
basePoints: number
|
||||
weightFactor?: number
|
||||
validityDays: number
|
||||
priority: number
|
||||
calculationMode: CalculationMode
|
||||
conditions: CreateConditionRequest[]
|
||||
}
|
||||
|
||||
export interface CreatePointsRuleResponse {
|
||||
id: string
|
||||
name: string
|
||||
code: string
|
||||
}
|
||||
|
||||
export interface PointsCalculationResultDto {
|
||||
success: boolean
|
||||
points: number
|
||||
expireAt: string
|
||||
appliedRuleId?: string
|
||||
message?: string
|
||||
matchedDimensions: string[]
|
||||
}
|
||||
|
||||
export interface CalculatePointsRequest {
|
||||
productId: string
|
||||
productName: string
|
||||
productPrice?: string
|
||||
dealerId: string
|
||||
distributorId: string
|
||||
storeId: string
|
||||
codeId: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
items: T[]
|
||||
totalCount: number
|
||||
|
||||
@ -65,6 +65,15 @@ const routes: RouteRecordRaw[] = [
|
||||
title: 'Logs',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'PointsRules',
|
||||
path: '/fengling/points-rules',
|
||||
component: () => import('#/views/fengling/points-rules/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:coins',
|
||||
title: 'Points Rules',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
581
apps/web-ele/src/views/fengling/points-rules/index.vue
Normal file
581
apps/web-ele/src/views/fengling/points-rules/index.vue
Normal file
@ -0,0 +1,581 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
|
||||
import {
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElSelect,
|
||||
ElOption,
|
||||
ElTag,
|
||||
ElMessage,
|
||||
ElPopconfirm,
|
||||
ElSwitch,
|
||||
ElInputNumber,
|
||||
ElDatePicker,
|
||||
ElRow,
|
||||
ElCol,
|
||||
ElIcon,
|
||||
ElDropdown,
|
||||
ElDropdownMenu,
|
||||
ElDropdownItem,
|
||||
ElDrawer,
|
||||
ElCard,
|
||||
ElTimeline,
|
||||
ElTimelineItem,
|
||||
} from 'element-plus';
|
||||
|
||||
import { Plus, Delete, RefreshRight, Operation, Download, View, Coins, Close } from '@element-plus/icons-vue';
|
||||
|
||||
import { PointsRuleApi, type FenglingApi } from '#/api/fengling';
|
||||
|
||||
const loading = ref(false);
|
||||
const tableData = ref<FenglingApi.PointsRule.PointsRuleDto[]>([]);
|
||||
const selectedRows = ref<FenglingApi.PointsRule.PointsRuleDto[]>([]);
|
||||
const dialogVisible = ref(false);
|
||||
const dialogTitle = ref('Create Points Rule');
|
||||
const formData = ref<Partial<FenglingApi.PointsRule.CreatePointsRuleRequest>>({});
|
||||
const searchKeyword = ref('');
|
||||
|
||||
const testPanelVisible = ref(false);
|
||||
const testResult = ref<FenglingApi.PointsRule.PointsCalculationResultDto | null>(null);
|
||||
const testData = ref({
|
||||
productId: '',
|
||||
productName: '',
|
||||
productPrice: '',
|
||||
dealerId: '',
|
||||
distributorId: '',
|
||||
storeId: '',
|
||||
codeId: '',
|
||||
});
|
||||
const testHistory = ref<Array<{
|
||||
time: string;
|
||||
productName: string;
|
||||
points: number;
|
||||
success: boolean;
|
||||
}>>([]);
|
||||
|
||||
const ruleTypeOptions = [
|
||||
{ label: 'Fixed Value', value: FenglingApi.PointsRule.RuleType.FixedValue },
|
||||
{ label: 'Price Weighted', value: FenglingApi.PointsRule.RuleType.PriceWeighted },
|
||||
];
|
||||
|
||||
const calculationModeOptions = [
|
||||
{ label: 'Synchronous', value: FenglingApi.PointsRule.CalculationMode.Synchronous },
|
||||
{ label: 'Asynchronous', value: FenglingApi.PointsRule.CalculationMode.Asynchronous },
|
||||
];
|
||||
|
||||
const dimensionTypeOptions = [
|
||||
{ label: 'Product', value: FenglingApi.PointsRule.DimensionType.Product },
|
||||
{ label: 'Dealer', value: FenglingApi.PointsRule.DimensionType.Dealer },
|
||||
{ label: 'Distributor', value: FenglingApi.PointsRule.DimensionType.Distributor },
|
||||
{ label: 'Store', value: FenglingApi.PointsRule.DimensionType.Store },
|
||||
];
|
||||
|
||||
const hasSelection = computed(() => selectedRows.value.length > 0);
|
||||
|
||||
const loadRules = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
tableData.value = await PointsRuleApi.getRulesList();
|
||||
} catch (error) {
|
||||
ElMessage.error('Failed to load points rules');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectionChange = (selection: FenglingApi.PointsRule.PointsRuleDto[]) => {
|
||||
selectedRows.value = selection;
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
dialogTitle.value = 'Create Points Rule';
|
||||
formData.value = {
|
||||
basePoints: 10,
|
||||
validityDays: 365,
|
||||
priority: 0,
|
||||
calculationMode: FenglingApi.PointsRule.CalculationMode.Synchronous,
|
||||
ruleType: FenglingApi.PointsRule.RuleType.FixedValue,
|
||||
conditions: [],
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleEdit = (row: FenglingApi.PointsRule.PointsRuleDto) => {
|
||||
dialogTitle.value = 'Edit Points Rule';
|
||||
formData.value = {
|
||||
name: row.name,
|
||||
code: row.code,
|
||||
ruleType: row.ruleType,
|
||||
basePoints: row.basePoints,
|
||||
weightFactor: row.weightFactor,
|
||||
validityDays: row.validityDays,
|
||||
priority: row.priority,
|
||||
calculationMode: row.calculationMode,
|
||||
conditions: row.conditions.map(c => ({
|
||||
dimensionType: c.dimensionType,
|
||||
dimensionValue: c.dimensionValue,
|
||||
operator: c.operator,
|
||||
})),
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
await PointsRuleApi.deleteRule(id);
|
||||
ElMessage.success('Points rule deleted successfully');
|
||||
loadRules();
|
||||
} catch (error) {
|
||||
ElMessage.error('Failed to delete points rule');
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleStatus = async (id: string, isActive: boolean) => {
|
||||
try {
|
||||
await PointsRuleApi.toggleRuleStatus(id, isActive);
|
||||
ElMessage.success('Points rule status updated successfully');
|
||||
loadRules();
|
||||
} catch (error) {
|
||||
ElMessage.error('Failed to update points rule status');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
if (dialogTitle.value === 'Create Points Rule') {
|
||||
await PointsRuleApi.createRule(formData.value as FenglingApi.PointsRule.CreatePointsRuleRequest);
|
||||
ElMessage.success('Points rule created successfully');
|
||||
} else {
|
||||
const editedRow = tableData.value.find(r => r.name === formData.value.name);
|
||||
if (editedRow) {
|
||||
await PointsRuleApi.updateRule(editedRow.id, formData.value);
|
||||
ElMessage.success('Points rule updated successfully');
|
||||
}
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
loadRules();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || 'Failed to save points rule');
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchEnable = async () => {
|
||||
try {
|
||||
const ids = selectedRows.value.map(r => r.id);
|
||||
const result = await PointsRuleApi.batchEnable(ids);
|
||||
ElMessage.success(`Batch enable completed: ${result.data.success} succeeded, ${result.data.failed} failed`);
|
||||
selectedRows.value = [];
|
||||
loadRules();
|
||||
} catch (error) {
|
||||
ElMessage.error('Failed to batch enable rules');
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchDisable = async () => {
|
||||
try {
|
||||
const ids = selectedRows.value.map(r => r.id);
|
||||
const result = await PointsRuleApi.batchDisable(ids);
|
||||
ElMessage.success(`Batch disable completed: ${result.data.success} succeeded, ${result.data.failed} failed`);
|
||||
selectedRows.value = [];
|
||||
loadRules();
|
||||
} catch (error) {
|
||||
ElMessage.error('Failed to batch disable rules');
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchDelete = async () => {
|
||||
try {
|
||||
const ids = selectedRows.value.map(r => r.id);
|
||||
const result = await PointsRuleApi.batchDelete(ids);
|
||||
ElMessage.success(`Batch delete completed: ${result.data.success} succeeded, ${result.data.failed} failed`);
|
||||
selectedRows.value = [];
|
||||
loadRules();
|
||||
} catch (error) {
|
||||
ElMessage.error('Failed to batch delete rules');
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchExport = async () => {
|
||||
try {
|
||||
const ids = selectedRows.value.map(r => r.id);
|
||||
const blob = await PointsRuleApi.batchExport(ids);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `points-rules-${new Date().getTime()}.xlsx`;
|
||||
link.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
ElMessage.success('Export completed successfully');
|
||||
} catch (error) {
|
||||
ElMessage.error('Failed to export rules');
|
||||
}
|
||||
};
|
||||
|
||||
const openTestPanel = () => {
|
||||
testPanelVisible.value = true;
|
||||
};
|
||||
|
||||
const handleCalculatePoints = async () => {
|
||||
try {
|
||||
testResult.value = await PointsRuleApi.calculatePoints(testData.value as FenglingApi.PointsRule.CalculatePointsRequest);
|
||||
|
||||
testHistory.value.unshift({
|
||||
time: new Date().toLocaleTimeString(),
|
||||
productName: testData.value.productName,
|
||||
points: testResult.value.points,
|
||||
success: testResult.value.success,
|
||||
});
|
||||
|
||||
if (testHistory.value.length > 10) {
|
||||
testHistory.value = testHistory.value.slice(0, 10);
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('Failed to calculate points');
|
||||
}
|
||||
};
|
||||
|
||||
const addCondition = () => {
|
||||
if (!formData.value.conditions) {
|
||||
formData.value.conditions = [];
|
||||
}
|
||||
formData.value.conditions.push({
|
||||
dimensionType: FenglingApi.PointsRule.DimensionType.Product,
|
||||
dimensionValue: '*',
|
||||
operator: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
const removeCondition = (index: number) => {
|
||||
formData.value.conditions?.splice(index, 1);
|
||||
};
|
||||
|
||||
const getRuleTypeLabel = (type: FenglingApi.PointsRule.RuleType) => {
|
||||
const option = ruleTypeOptions.find(o => o.value === type);
|
||||
return option?.label || type;
|
||||
};
|
||||
|
||||
const getCalculationModeLabel = (mode: FenglingApi.PointsRule.CalculationMode) => {
|
||||
const option = calculationModeOptions.find(o => o.value === mode);
|
||||
return option?.label || mode;
|
||||
};
|
||||
|
||||
const getDimensionTypeLabel = (type: FenglingApi.PointsRule.DimensionType) => {
|
||||
const option = dimensionTypeOptions.find(o => o.value === type);
|
||||
return option?.label || type;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadRules();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-5">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div class="flex gap-2">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="Search by code or name"
|
||||
clearable
|
||||
style="width: 250px"
|
||||
@clear="loadRules"
|
||||
/>
|
||||
<el-button type="primary" @click="loadRules">
|
||||
<el-icon><View /></el-icon>
|
||||
Search
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<el-dropdown :disabled="!hasSelection">
|
||||
<el-button type="warning" :disabled="!hasSelection">
|
||||
Batch Operation
|
||||
<el-icon class="ml-1"><Operation /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="handleBatchEnable">
|
||||
<el-icon><View /></el-icon>
|
||||
Enable
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="handleBatchDisable">
|
||||
<el-icon><Close /></el-icon>
|
||||
Disable
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="handleBatchDelete" divided>
|
||||
<el-icon><Delete /></el-icon>
|
||||
Delete
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="handleBatchExport">
|
||||
<el-icon><Download /></el-icon>
|
||||
Export
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-button type="primary" @click="handleCreate">
|
||||
<el-icon><Plus /></el-icon>
|
||||
Create Rule
|
||||
</el-button>
|
||||
<el-button @click="loadRules">
|
||||
<el-icon><RefreshRight /></el-icon>
|
||||
Refresh
|
||||
</el-button>
|
||||
<el-button @click="openTestPanel">
|
||||
<el-icon><Coins /></el-icon>
|
||||
Test Calculation
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
border
|
||||
stripe
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="id" label="ID" width="220" fixed="left" />
|
||||
<el-table-column prop="code" label="Code" width="150" />
|
||||
<el-table-column prop="name" label="Name" width="200" />
|
||||
<el-table-column label="Rule Type" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-tag>{{ getRuleTypeLabel(row.ruleType) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="basePoints" label="Base Points" width="100" />
|
||||
<el-table-column prop="weightFactor" label="Weight Factor" width="120" />
|
||||
<el-table-column prop="validityDays" label="Validity Days" width="120" />
|
||||
<el-table-column prop="priority" label="Priority" width="80" sortable />
|
||||
<el-table-column label="Calculation Mode" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-tag>{{ getCalculationModeLabel(row.calculationMode) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Status" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-model="row.isActive"
|
||||
@change="handleToggleStatus(row.id, $event)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Conditions" width="300">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.conditions.length === 0" class="text-gray-400 text-sm">No conditions</div>
|
||||
<div v-else class="flex flex-wrap gap-1">
|
||||
<el-tag v-for="cond in row.conditions" :key="cond.id" size="small" type="info">
|
||||
{{ getDimensionTypeLabel(cond.dimensionType) }}: {{ cond.dimensionValue }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="effectiveFrom" label="Effective From" width="180" />
|
||||
<el-table-column prop="effectiveTo" label="Effective To" width="180" />
|
||||
<el-table-column label="Actions" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="handleEdit(row)">Edit</el-button>
|
||||
<el-popconfirm title="Are you sure to delete this rule?" @confirm="handleDelete(row.id)">
|
||||
<template #reference>
|
||||
<el-button size="small" type="danger">Delete</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="800px">
|
||||
<el-form :model="formData" label-width="150px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="Code" required>
|
||||
<el-input v-model="formData.code" placeholder="Rule code" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="Name" required>
|
||||
<el-input v-model="formData.name" placeholder="Rule name" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="Rule Type" required>
|
||||
<el-select v-model="formData.ruleType" placeholder="Select rule type">
|
||||
<el-option
|
||||
v-for="option in ruleTypeOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="Calculation Mode" required>
|
||||
<el-select v-model="formData.calculationMode" placeholder="Select mode">
|
||||
<el-option
|
||||
v-for="option in calculationModeOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="Base Points" required>
|
||||
<el-input-number v-model="formData.basePoints" :min="0" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="Weight Factor">
|
||||
<el-input-number v-model="formData.weightFactor" :min="0" :precision="4" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="Validity Days" required>
|
||||
<el-input-number v-model="formData.validityDays" :min="1" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="Priority" required>
|
||||
<el-input-number v-model="formData.priority" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="Conditions">
|
||||
<el-button size="small" type="primary" @click="addCondition">
|
||||
<el-icon><Plus /></el-icon>
|
||||
Add Condition
|
||||
</el-button>
|
||||
<div v-if="formData.conditions && formData.conditions.length > 0" class="mt-3 space-y-2">
|
||||
<el-row
|
||||
v-for="(condition, index) in formData.conditions"
|
||||
:key="index"
|
||||
:gutter="10"
|
||||
align="middle"
|
||||
>
|
||||
<el-col :span="6">
|
||||
<el-select v-model="condition.dimensionType" placeholder="Dimension">
|
||||
<el-option
|
||||
v-for="option in dimensionTypeOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col :span="14">
|
||||
<el-input v-model="condition.dimensionValue" placeholder="Value (use * for wildcard)" />
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-button type="danger" size="small" @click="removeCondition(index)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">Cancel</el-button>
|
||||
<el-button type="primary" @click="handleSave">Save</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-drawer v-model="testPanelVisible" title="Points Calculation Test" size="500px">
|
||||
<el-card class="mb-4">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Test Data</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-form label-width="120px">
|
||||
<div class="mb-4 font-semibold">Product Information</div>
|
||||
<el-form-item label="Product ID" required>
|
||||
<el-input v-model="testData.productId" placeholder="Product ID" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Product Name" required>
|
||||
<el-input v-model="testData.productName" placeholder="Product Name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Product Price">
|
||||
<el-input v-model="testData.productPrice" placeholder="Product Price" />
|
||||
</el-form-item>
|
||||
|
||||
<div class="mb-4 font-semibold">Channel Information</div>
|
||||
<el-form-item label="Dealer ID" required>
|
||||
<el-input v-model="testData.dealerId" placeholder="Dealer ID" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Distributor ID" required>
|
||||
<el-input v-model="testData.distributorId" placeholder="Distributor ID" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Store ID" required>
|
||||
<el-input v-model="testData.storeId" placeholder="Store ID" />
|
||||
</el-form-item>
|
||||
|
||||
<div class="mb-4 font-semibold">Code Information</div>
|
||||
<el-form-item label="Code ID" required>
|
||||
<el-input v-model="testData.codeId" placeholder="Code ID" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button type="primary" @click="handleCalculatePoints" style="width: 100%">
|
||||
<el-icon><Coins /></el-icon>
|
||||
Calculate Points
|
||||
</el-button>
|
||||
</el-card>
|
||||
|
||||
<el-card v-if="testResult" class="mb-4">
|
||||
<template #header>
|
||||
<span>Calculation Result</span>
|
||||
</template>
|
||||
<div class="space-y-2">
|
||||
<div>
|
||||
<strong>Success:</strong>
|
||||
<el-tag :type="testResult.success ? 'success' : 'danger'">
|
||||
{{ testResult.success ? 'Yes' : 'No' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div><strong>Points:</strong> {{ testResult.points }}</div>
|
||||
<div><strong>Expire At:</strong> {{ testResult.expireAt }}</div>
|
||||
<div><strong>Message:</strong> {{ testResult.message }}</div>
|
||||
<div><strong>Applied Rule ID:</strong> {{ testResult.appliedRuleId }}</div>
|
||||
<div>
|
||||
<strong>Matched Dimensions:</strong>
|
||||
<div class="mt-2">
|
||||
<el-tag v-for="dim in testResult.matchedDimensions" :key="dim" class="mr-1 mb-1">
|
||||
{{ dim }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card v-if="testHistory.length > 0">
|
||||
<template #header>
|
||||
<span>Test History</span>
|
||||
</template>
|
||||
<el-timeline>
|
||||
<el-timeline-item
|
||||
v-for="(item, index) in testHistory"
|
||||
:key="index"
|
||||
:type="item.success ? 'success' : 'danger'"
|
||||
:timestamp="item.time"
|
||||
>
|
||||
{{ item.productName }} - {{ item.points }} points
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</el-card>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
Loading…
Reference in New Issue
Block a user