add point rule sort

This commit is contained in:
Timi
2026-08-19 16:12:12 +08:00
parent 84bc6d137d
commit f4fb604f53
7 changed files with 193 additions and 41 deletions
+5
View File
@@ -78,6 +78,10 @@ async function ruleUpdate(rule: PointRule): Promise<void> {
await axios.post(`${BASE_URI}/rule/update`, withCurrentStoreId(rule));
}
async function ruleSort(idList: string[]): Promise<void> {
await axios.post(`${BASE_URI}/rule/sort`, idList);
}
async function ruleRemove(id: string): Promise<void> {
await axios.post(`${BASE_URI}/rule/delete`, undefined, {
params: { id }
@@ -92,5 +96,6 @@ export default {
ruleDetail,
ruleCreate,
ruleUpdate,
ruleSort,
ruleRemove
};
+1 -15
View File
@@ -44,15 +44,6 @@
</t-cell-group>
<div v-else class="empty-block" v-text="isLoading ? '正在加载登记事件' : '暂无登记事件'" />
</div>
<t-button
class="save"
theme="primary"
:loading="isSaving"
:disabled="!sortDirty || isSaving || isLoading"
@click="saveSort"
>
保存排序
</t-button>
</div>
</template>
@@ -151,6 +142,7 @@ async function loadData(): Promise<void> {
onMounted(async () => {
navBarStore.clearRight();
navBarStore.setRightText("保存", "var(--td-brand-color)", saveSort);
try {
await loadData();
} catch {
@@ -218,12 +210,6 @@ onUnmounted(() => {
text-align: center;
}
.save {
left: 1rem;
right: 1rem;
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
position: fixed;
}
}
:global(html.theme-dark) {
-11
View File
@@ -83,9 +83,6 @@
</t-form-item>
</card-container>
<card-container tips="命中规则并执行积分动作后不再匹配后续规则">
<t-form-item label="优先级" required-mark>
<t-input v-model.trim="formModel.priorityText" type="number" align="right" borderless placeholder="数值越小越优先" />
</t-form-item>
<t-form-item label="独占匹配" content-align="right">
<t-switch size="small" v-model="formModel.exclusive" />
</t-form-item>
@@ -248,7 +245,6 @@ const formModel = reactive({
tiers: [] as PointRuleTierForm[],
beginAtText: "",
endAtText: "",
priorityText: "0",
exclusive: false,
remark: ""
});
@@ -696,11 +692,6 @@ function validateForm(): boolean {
Toast.warning("生效结束时间不能早于开始时间");
return false;
}
const priority = Number(formModel.priorityText);
if (!Number.isSafeInteger(priority) || 0 > priority) {
Toast.warning("优先级必须是大于等于 0 的整数");
return false;
}
return true;
}
@@ -722,7 +713,6 @@ async function saveRule(): Promise<void> {
conditionJson: buildCondition(),
beginAt: resolveTimestamp(formModel.beginAtText),
endAt: resolveTimestamp(formModel.endAtText),
priority: Number(formModel.priorityText),
exclusive: formModel.exclusive,
remark: formModel.remark.trim()
};
@@ -803,7 +793,6 @@ async function fillForm(): Promise<void> {
tiers: [],
beginAtText: current.beginAt ? Time.toDateTime(current.beginAt) : "",
endAtText: current.endAt ? Time.toDateTime(current.endAt) : "",
priorityText: String(current.priority ?? 0),
exclusive: current.exclusive ?? false,
remark: current.remark || ""
});
+10
View File
@@ -73,6 +73,7 @@ import { type DropdownValue, Toast } from "tdesign-mobile-vue";
import EventAPI from "@/api/EventAPI";
import PointAPI from "@/api/PointAPI";
import { useCurrentStoreStore } from "@/store/currentStoreStore";
import { useNavBarStore } from "@/store/navBarStore";
import type { EventItem } from "@/types/EventItem";
import type { PointRule, PointRuleStatus } from "@/types/Point";
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
@@ -85,6 +86,7 @@ defineOptions({
});
const router = useRouter();
const navBarStore = useNavBarStore();
const userStore = useUserStore();
const currentStoreStore = useCurrentStoreStore();
@@ -297,12 +299,20 @@ async function preparePage(): Promise<void> {
}
onMounted(async () => {
navBarStore.clearRight();
if (userStore.hasPermissions(permissionCodes.pointRule.update)) {
navBarStore.setRightText("排序", "var(--td-brand-color)", () => {
router.push("/management/points/rules/sort");
});
}
try {
await preparePage();
} catch {
Toast.error("积分规则页面加载失败");
}
});
onUnmounted(() => navBarStore.clearRight());
</script>
<style scoped lang="less">
+161
View File
@@ -0,0 +1,161 @@
<template>
<div v-can="permissionCodes.pointRule.update" class="point-rule-sort">
<div class="tip" v-text="'按住右侧拖动手柄调整规则顺序,保存后生效;仅显示启用规则'" />
<div class="items">
<t-cell-group v-if="ruleList.length" theme="card">
<vue-draggable-next
v-model="ruleList"
class="draggable"
item-key="id"
handle=".handle"
ghost-class="ghost"
chosen-class="chosen"
drag-class="dragging"
:animation="180"
:disabled="isLoading || isSaving"
@change="sortDirty = true"
>
<t-cell v-for="item in ruleList" :key="item.id" class="cell">
<template #title>
<div class="title" v-text="item.name || '未命名规则'" />
</template>
<template #description>
<div class="description">
<span v-text="'积分值:' + getPointText(item)" />
<span v-text="'登记事件:' + (eventNameMap.get(item.eventId || '') || '未绑定事件')" />
</div>
</template>
<template #note>
<t-tag :theme="item.exclusive ? 'warning' : 'default'" variant="outline" :content="item.exclusive ? '独占匹配' : '继续匹配'" />
</template>
<template #rightIcon>
<t-icon class="handle" role="button" name="move" aria-label="拖动排序" />
</template>
</t-cell>
</vue-draggable-next>
</t-cell-group>
<div v-else class="empty-block" v-text="isLoading ? '正在加载启用规则' : '暂无启用规则'" />
</div>
</div>
</template>
<script setup lang="ts">
import { Toast } from "tdesign-mobile-vue";
import { VueDraggableNext } from "vue-draggable-next";
import EventAPI from "@/api/EventAPI";
import PointAPI from "@/api/PointAPI";
import { useNavBarStore } from "@/store/navBarStore";
import type { EventItem } from "@/types/EventItem";
import type { PointRule } from "@/types/Point";
import { permissionCodes } from "@/utils/permission";
defineOptions({ name: "PointRuleSort" });
const navBarStore = useNavBarStore();
const router = useRouter();
const ruleList = ref<PointRule[]>([]);
const eventList = ref<EventItem[]>([]);
const isLoading = ref(false);
const isSaving = ref(false);
const sortDirty = ref(false);
const eventNameMap = computed(() => new Map(eventList.value.filter(item => item.id).map(item => [item.id as string, item.name])));
function getPointText(rule: PointRule): string {
if (rule.pointMode === "TIER") {
return "分档";
}
if (rule.pointValue === undefined || rule.pointValue === null) {
return "按登记值";
}
return (0 < rule.pointValue ? "+" : "") + rule.pointValue;
}
async function loadData(): Promise<void> {
isLoading.value = true;
try {
const [ruleResult, eventResult] = await Promise.all([
PointAPI.ruleList({ index: 0, size: 1000, equalsExample: { status: "ACTIVE" } }),
EventAPI.list({ index: 0, size: 1000 })
]);
ruleList.value = ruleResult.list || [];
eventList.value = eventResult.list || [];
sortDirty.value = false;
} finally {
isLoading.value = false;
}
}
async function saveSort(): Promise<void> {
if (!sortDirty.value || isSaving.value) {
return;
}
isSaving.value = true;
try {
const idList = ruleList.value.map(item => item.id).filter((id): id is string => Boolean(id));
await PointAPI.ruleSort(idList);
ruleList.value.forEach((item, index) => { item.priority = index; });
sortDirty.value = false;
Toast.success("积分规则排序已保存");
await router.back();
} catch {
Toast.error("积分规则排序保存失败");
} finally {
isSaving.value = false;
}
}
onMounted(async () => {
navBarStore.clearRight();
navBarStore.setRightText("保存", "var(--td-brand-color)", saveSort);
try {
await loadData();
} catch {
Toast.error("启用积分规则加载失败");
}
});
onUnmounted(() => navBarStore.clearRight());
</script>
<style scoped lang="less">
.point-rule-sort {
padding: calc(var(--app-nav-height) + 1rem) 0 6rem;
.tip,
.empty-block {
color: #52607a;
padding: 0 1rem .75rem;
font-size: .8125rem;
}
.empty-block {
padding-top: 2rem;
text-align: center;
}
.draggable {
.cell {
.title { font-weight: 600; }
.description {
display: flex;
gap: .75rem;
font-size: .75rem;
}
.handle { color: #52607a; cursor: grab; touch-action: none; }
}
.ghost { opacity: .4; }
.chosen,
.dragging { z-index: 1; box-shadow: 0 .5rem 1rem rgba(15, 23, 42, .16); }
}
}
:global(html.theme-dark) {
.point-rule-sort {
.tip,
.empty-block,
.handle { color: #94a3b8; }
.draggable .cell { background: rgba(15, 23, 42, .88); }
}
}
</style>
+1 -15
View File
@@ -42,15 +42,6 @@
</t-cell-group>
<div v-else class="empty-block" v-text="isLoading ? '正在加载门店' : '暂无门店'" />
</div>
<t-button
class="save"
theme="primary"
:loading="isSaving"
:disabled="!sortDirty || isSaving || isLoading"
@click="saveSort"
>
保存排序
</t-button>
</div>
</template>
@@ -140,6 +131,7 @@ async function loadData(): Promise<void> {
onMounted(async () => {
navBarStore.clearRight();
navBarStore.setRightText("保存", "var(--td-brand-color)", saveSort);
try {
await loadData();
} catch {
@@ -192,12 +184,6 @@ onUnmounted(() => {
text-align: center;
}
.save {
left: 1rem;
right: 1rem;
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
position: fixed;
}
}
:global(html.theme-dark) {
+15
View File
@@ -5,6 +5,7 @@ import PointLedgerDetail from "@/pages/point/PointLedgerDetail.vue";
import PointLedgerList from "@/pages/point/PointLedgerList.vue";
import PointRuleEdit from "@/pages/point/PointRuleEdit.vue";
import PointRuleList from "@/pages/point/PointRuleList.vue";
import PointRuleSort from "@/pages/point/PointRuleSort.vue";
const pointRoutes: RouteRecordRaw[] = [
{
@@ -21,6 +22,20 @@ const pointRoutes: RouteRecordRaw[] = [
darkBodyBackground: "#10161f"
}
},
{
path: "/management/points/rules/sort",
name: "PointRuleSort",
component: PointRuleSort,
meta: {
depth: 4,
navBarVisible: true,
navBarCanBack: true,
navBarTitle: "积分规则排序",
tabBarVisible: false,
bodyBackground: "#F5F5F8",
darkBodyBackground: "#10161f"
}
},
{
path: "/management/points/ledger",
name: "PointLedgerList",