update permission and product
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
import { axios } from "timi-web-api";
|
import { axios } from "timi-web-api";
|
||||||
import type { Page, PageResult } from "timi-web-api";
|
import type { Page, PageResult } from "timi-web-api";
|
||||||
import type { EventItem } from "@/types/EventItem";
|
|
||||||
import type { Store, StoreBusinessDay } from "@/types/Store";
|
import type { Store, StoreBusinessDay } from "@/types/Store";
|
||||||
|
|
||||||
const STORE_BASE_URI = "/gao/store";
|
const STORE_BASE_URI = "/gao/store";
|
||||||
@@ -16,12 +15,6 @@ async function detail(id: string): Promise<Store> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function directProductSaleEventList(storeId: string): Promise<EventItem[]> {
|
|
||||||
return await axios.post(`${STORE_BASE_URI}/direct-product-sale-event/list`, undefined, {
|
|
||||||
params: { storeId }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loginStore(): Promise<Store | undefined> {
|
async function loginStore(): Promise<Store | undefined> {
|
||||||
return await axios.post(`${STORE_BASE_URI}/user`);
|
return await axios.post(`${STORE_BASE_URI}/user`);
|
||||||
}
|
}
|
||||||
@@ -87,7 +80,6 @@ async function removeBusinessDay(id: string): Promise<void> {
|
|||||||
export default {
|
export default {
|
||||||
list,
|
list,
|
||||||
detail,
|
detail,
|
||||||
directProductSaleEventList,
|
|
||||||
loginStore,
|
loginStore,
|
||||||
create,
|
create,
|
||||||
update,
|
update,
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ function handleRemove(customer: Customer): void {
|
|||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.customer-list {
|
.customer-list {
|
||||||
|
|
||||||
.cell-title {
|
.cell-title {
|
||||||
gap: .5rem;
|
gap: .5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="event-selector">
|
<div class="event-selector">
|
||||||
<p v-if="props.label" class="t-form__label" :class="{required: props.requiredMark}" v-text="props.label" />
|
<p v-if="props.label" class="t-form__label" :class="{required: props.requiredMark}" v-text="props.label" />
|
||||||
<div v-if="props.eventList.length" class="group" role="group">
|
<div v-if="selectableEventList.length" class="group" role="group">
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in props.eventList"
|
v-for="(item, index) in selectableEventList"
|
||||||
:key="item.id || item.name"
|
:key="item.id || item.name"
|
||||||
class="item-wrapper"
|
class="item-wrapper"
|
||||||
v-entrance-transition="index * 30"
|
v-entrance-transition="index * 30"
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { EventItem } from "@/types/EventItem";
|
import type { EventItem } from "@/types/EventItem";
|
||||||
|
import { isRetiredEvent } from "@/utils/event";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
eventList: EventItem[];
|
eventList: EventItem[];
|
||||||
@@ -58,6 +59,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
|
|
||||||
const modelValue = defineModel<string[]>({ required: true });
|
const modelValue = defineModel<string[]>({ required: true });
|
||||||
const defaultColor = "#2f88ff";
|
const defaultColor = "#2f88ff";
|
||||||
|
const selectableEventList = computed(() => props.eventList.filter(item => !isRetiredEvent(item)));
|
||||||
|
|
||||||
function normalizeEventId(item: EventItem): string {
|
function normalizeEventId(item: EventItem): string {
|
||||||
if (item.id === undefined || item.id === null) {
|
if (item.id === undefined || item.id === null) {
|
||||||
|
|||||||
@@ -1,49 +1,69 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="product-selector">
|
<t-form-item
|
||||||
<t-form-item :label="props.label" :required-mark="props.requiredMark" arrow @click="open">
|
class="product-selector"
|
||||||
|
:label="props.label"
|
||||||
|
:required-mark="props.requiredMark"
|
||||||
|
content-align="right"
|
||||||
|
arrow
|
||||||
|
@click="open"
|
||||||
|
>
|
||||||
<span class="picker-text" v-text="selectedText || props.placeholder" />
|
<span class="picker-text" v-text="selectedText || props.placeholder" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-popup v-model="visible" placement="bottom" :style="{height: '80vh'}">
|
<bottom-popup
|
||||||
<div class="popup">
|
v-model="visible"
|
||||||
<div class="header">
|
title="选择产品"
|
||||||
<t-button variant="text" size="small" @click="visible = false">取消</t-button>
|
height="80vh"
|
||||||
<strong>选择产品单位</strong>
|
right-button-text=""
|
||||||
<t-button theme="primary" variant="text" size="small" @click="confirm">确定</t-button>
|
@left-button-click="visible = false"
|
||||||
</div>
|
|
||||||
<t-input v-model.trim="keyword" class="search" borderless clearable placeholder="搜索产品名称或编码" />
|
|
||||||
<div class="list">
|
|
||||||
<t-cell-group v-if="filteredProductList.length" theme="card">
|
|
||||||
<t-cell v-for="product in filteredProductList" :key="product.id" class="product" :title="product.name" :description="`${product.code || '-'} · ${getProductBaseUnitName(product) || '-'}`">
|
|
||||||
<template #rightIcon>
|
|
||||||
<div class="units">
|
|
||||||
<t-button
|
|
||||||
v-for="unit in getSaleableUnits(product)"
|
|
||||||
:key="unit.id"
|
|
||||||
:theme="isSelected(product, unit) ? 'primary' : 'default'"
|
|
||||||
:variant="isSelected(product, unit) ? 'base' : 'outline'"
|
|
||||||
size="small"
|
|
||||||
@click.stop="select(product, unit)"
|
|
||||||
>
|
>
|
||||||
<span class="unit-name" v-text="unit.name" />
|
<div class="popup">
|
||||||
<span v-if="unit.salePrice !== undefined && unit.salePrice !== null" class="unit-price" v-text="formatProductAmount(unit.salePrice)" />
|
<div class="search-wrap">
|
||||||
</t-button>
|
<t-input
|
||||||
|
v-model="keyword"
|
||||||
|
class="search"
|
||||||
|
borderless
|
||||||
|
placeholder="搜索产品名称或编码"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="items">
|
||||||
|
<t-cell-group v-if="visibleProductList.length" theme="card">
|
||||||
|
<t-cell
|
||||||
|
v-for="product in visibleProductList"
|
||||||
|
:key="product.id"
|
||||||
|
class="item"
|
||||||
|
:title="product.name"
|
||||||
|
:description="`${product.code || '-'}`"
|
||||||
|
@click="handleProductSelect(product)"
|
||||||
|
>
|
||||||
|
<template #leftIcon>
|
||||||
|
<t-image
|
||||||
|
v-if="getProductPhotoURL(product)"
|
||||||
|
:src="getProductPhotoURL(product)"
|
||||||
|
:alt="product.name || '产品图片'"
|
||||||
|
fit="cover"
|
||||||
|
class="avatar"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="avatar fallback"
|
||||||
|
v-text="(product.name || product.code || '产').slice(0, 1)"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</t-cell>
|
</t-cell>
|
||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
<div v-else class="empty" v-text="loading ? '正在加载可售产品' : '暂无可售产品'" />
|
<div v-else class="empty" v-text="loading ? '正在加载可售产品' : '暂无可售产品'" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</t-popup>
|
</bottom-popup>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { BottomPopup } from "timi-tdesign-mobile";
|
||||||
import ProductAPI from "@/api/ProductAPI";
|
import ProductAPI from "@/api/ProductAPI";
|
||||||
import type { Product, ProductUnit } from "@/types/Product";
|
import type { Product } from "@/types/Product";
|
||||||
import { getErrorMessage } from "@/utils/error";
|
import { getErrorMessage } from "@/utils/error";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
import { formatProductAmount, getProductBaseUnitName } from "@/utils/product";
|
import { getProductPhotoURL } from "@/utils/product";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: { productId: string; productUnitId: string } | undefined;
|
modelValue: { productId: string; productUnitId: string } | undefined;
|
||||||
@@ -52,29 +72,27 @@ interface Props {
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), { label: "产品", requiredMark: false, placeholder: "请选择产品单位" });
|
const props = withDefaults(defineProps<Props>(), { label: "产品", requiredMark: false, placeholder: "请选择产品单位", selectionMode: "product-unit" });
|
||||||
const emit = defineEmits<{ "update:modelValue": [value: Props["modelValue"]] }>();
|
const emit = defineEmits<{ "update:modelValue": [value: Props["modelValue"]] }>();
|
||||||
const visible = ref(false);
|
const visible = ref(false);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const keyword = ref("");
|
const keyword = ref("");
|
||||||
const productList = ref<Product[]>([]);
|
const productList = ref<Product[]>([]);
|
||||||
const draft = ref<Props["modelValue"]>();
|
|
||||||
const selectedText = computed(() => {
|
const selectedText = computed(() => {
|
||||||
if (!props.modelValue) {
|
if (!props.modelValue) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
const product = productList.value.find(item => item.id === props.modelValue?.productId);
|
const product = productList.value.find(item => item.id === props.modelValue?.productId);
|
||||||
const unit = product ? product.unitList?.find(item => item.id === props.modelValue?.productUnitId) : undefined;
|
return product?.name || "";
|
||||||
return product && unit ? `${product.name} · ${unit.name}` : "已选择产品单位";
|
|
||||||
});
|
});
|
||||||
const filteredProductList = computed(() => {
|
const filteredProductList = computed(() => {
|
||||||
const text = keyword.value.toLocaleLowerCase();
|
const text = keyword.value.toLocaleLowerCase();
|
||||||
return productList.value.filter(item => !text || `${item.name}${item.code}`.toLocaleLowerCase().includes(text));
|
return productList.value.filter(item => !text || `${item.name}${item.code}`.toLocaleLowerCase().includes(text));
|
||||||
});
|
});
|
||||||
|
const visibleProductList = computed(() => filteredProductList.value);
|
||||||
|
|
||||||
async function open(): Promise<void> {
|
async function open(): Promise<void> {
|
||||||
visible.value = true;
|
visible.value = true;
|
||||||
draft.value = props.modelValue ? { ...props.modelValue } : undefined;
|
|
||||||
if (productList.value.length) {
|
if (productList.value.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -88,23 +106,16 @@ async function open(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSaleableUnits(product: Product): ProductUnit[] {
|
function handleProductSelect(product: Product): void {
|
||||||
return (product.unitList || []).filter(item => item.allowSale !== false && item.id);
|
if (!product.id) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
function isSelected(product: Product, unit: ProductUnit): boolean {
|
emit("update:modelValue", {
|
||||||
return draft.value?.productId === product.id && draft.value?.productUnitId === unit.id;
|
productId: product.id,
|
||||||
}
|
productUnitId: product.id === props.modelValue?.productId ? props.modelValue?.productUnitId || "" : ""
|
||||||
function select(product: Product, unit: ProductUnit): void {
|
});
|
||||||
if (product.id && unit.id) {
|
|
||||||
draft.value = { productId: product.id, productUnitId: unit.id };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function confirm(): void {
|
|
||||||
if (draft.value) {
|
|
||||||
emit("update:modelValue", draft.value);
|
|
||||||
visible.value = false;
|
visible.value = false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
@@ -112,46 +123,50 @@ function confirm(): void {
|
|||||||
.picker-text {
|
.picker-text {
|
||||||
color: var(--td-text-color-primary);
|
color: var(--td-text-color-primary);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.popup {
|
.popup {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
padding: 1rem 0;
|
padding: 1rem 0;
|
||||||
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
.header {
|
.search-wrap {
|
||||||
display: flex;
|
z-index: 1;
|
||||||
padding: 0 1rem .75rem;
|
padding: 1rem 0 .5rem 0;
|
||||||
align-items: center;
|
box-shadow: 0 4px 4px rgba(0, 0, 0, .05);
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search {
|
.search {
|
||||||
margin: 0 1rem .75rem;
|
--td-input-vertical-padding: 8px 14px;
|
||||||
|
|
||||||
|
flex: 0;
|
||||||
|
margin: 0 1rem;
|
||||||
border: 1px solid var(--td-component-border);
|
border: 1px solid var(--td-component-border);
|
||||||
border-radius: 1rem;
|
border-radius: .875rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.list {
|
.items {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-bottom: env(safe-area-inset-bottom, 0);
|
||||||
|
|
||||||
|
.item {
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 3.5rem;
|
||||||
|
height: 3.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product {
|
|
||||||
.units {
|
.units {
|
||||||
gap: .35rem;
|
gap: .35rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|
||||||
.t-button {
|
|
||||||
gap: .125rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.unit-price {
|
|
||||||
font-size: .6875rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,5 +177,11 @@ function confirm(): void {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.avatar .t-image__img) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: inherit;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -25,17 +25,9 @@
|
|||||||
<t-form-item v-if="isGlobalManager" label="门店" :arrow="isCreateMode" @click="openStorePicker">
|
<t-form-item v-if="isGlobalManager" label="门店" :arrow="isCreateMode" @click="openStorePicker">
|
||||||
<div class="picker-text" v-text="selectedStoreName" />
|
<div class="picker-text" v-text="selectedStoreName" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="业务类型" :arrow="isCreateMode" @click="isCreateMode && openBizTypePicker()">
|
<t-form-item label="业务类型" :arrow="false">
|
||||||
<div class="picker-text" v-text="getBizTypeText(formModel.bizType)" />
|
<div class="picker-text" v-text="getBizTypeText(formModel.bizType)" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item v-if="formModel.bizType === 'PACKAGE_PURCHASE'" label="绑定套餐" required-mark arrow @click="openPackageConfigPicker">
|
|
||||||
<div class="picker-text" v-text="selectedPackageConfig?.name || '请选择套餐配置'" />
|
|
||||||
</t-form-item>
|
|
||||||
<t-form-item
|
|
||||||
label-width="0"
|
|
||||||
v-if="formModel.bizType === 'PACKAGE_PURCHASE'"
|
|
||||||
class="form-tips bg-white"
|
|
||||||
>套餐购买事件必须绑定一个套餐配置。登记该事件即视为购买</t-form-item>
|
|
||||||
<t-form-item label="说明">
|
<t-form-item label="说明">
|
||||||
<t-textarea
|
<t-textarea
|
||||||
class="description"
|
class="description"
|
||||||
@@ -188,24 +180,6 @@
|
|||||||
@confirm="confirmStorePicker"
|
@confirm="confirmStorePicker"
|
||||||
/>
|
/>
|
||||||
</t-popup>
|
</t-popup>
|
||||||
<t-popup v-model="bizTypePickerVisible" placement="bottom">
|
|
||||||
<t-picker
|
|
||||||
v-model="bizTypePickerDraft"
|
|
||||||
:columns="bizTypePickerColumns"
|
|
||||||
title="业务类型"
|
|
||||||
@cancel="bizTypePickerVisible = false"
|
|
||||||
@confirm="confirmBizTypePicker"
|
|
||||||
/>
|
|
||||||
</t-popup>
|
|
||||||
<t-popup v-model="packageConfigPickerVisible" placement="bottom">
|
|
||||||
<t-picker
|
|
||||||
v-model="packageConfigPickerDraft"
|
|
||||||
:columns="packageConfigPickerColumns"
|
|
||||||
title="绑定套餐"
|
|
||||||
@cancel="packageConfigPickerVisible = false"
|
|
||||||
@confirm="confirmPackageConfigPicker"
|
|
||||||
/>
|
|
||||||
</t-popup>
|
|
||||||
<t-popup v-model="statusPickerVisible" placement="bottom">
|
<t-popup v-model="statusPickerVisible" placement="bottom">
|
||||||
<t-picker
|
<t-picker
|
||||||
v-model="statusPickerDraft"
|
v-model="statusPickerDraft"
|
||||||
@@ -268,16 +242,13 @@ import { Time } from "timi-web";
|
|||||||
import { type ModuleCode, type Role, RoleAPI, useUserStore } from "timi-web-api";
|
import { type ModuleCode, type Role, RoleAPI, useUserStore } from "timi-web-api";
|
||||||
import type { ColorObject } from "tdesign-mobile-vue";
|
import type { ColorObject } from "tdesign-mobile-vue";
|
||||||
import EventAPI from "@/api/EventAPI";
|
import EventAPI from "@/api/EventAPI";
|
||||||
import PackageAPI from "@/api/PackageAPI";
|
|
||||||
import EventOptionEditor from "@/pages/event/component/EventOptionEditor.vue";
|
import EventOptionEditor from "@/pages/event/component/EventOptionEditor.vue";
|
||||||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||||
import { useNavBarStore } from "@/store/navBarStore";
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
import type { EventBizType, EventItem, EventLimitType, EventOptionGroup, EventStatus, EventValueType, EventValueUnit } from "@/types/EventItem";
|
import type { EventBizType, EventItem, EventLimitType, EventOptionGroup, EventStatus, EventValueType, EventValueUnit } from "@/types/EventItem";
|
||||||
import type { GaoPackageConfig } from "@/types/Package";
|
|
||||||
import { getErrorMessage } from "@/utils/error";
|
import { getErrorMessage } from "@/utils/error";
|
||||||
import { normalizeEventOptionGroupList, prepareEventOptionGroupList, validateEventOptionGroupList } from "@/utils/eventOption";
|
import { normalizeEventOptionGroupList, prepareEventOptionGroupList, validateEventOptionGroupList } from "@/utils/eventOption";
|
||||||
import { getEventBizTypeText } from "@/utils/event";
|
import { getEventBizTypeText } from "@/utils/event";
|
||||||
import { getPackageConfigStatusText } from "@/utils/package";
|
|
||||||
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
|
|
||||||
@@ -317,7 +288,6 @@ const formModel = reactive({
|
|||||||
id: "",
|
id: "",
|
||||||
storeId: "",
|
storeId: "",
|
||||||
bizType: "GENERAL" as EventBizType,
|
bizType: "GENERAL" as EventBizType,
|
||||||
packageConfigId: "",
|
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
color: "#0f766e",
|
color: "#0f766e",
|
||||||
@@ -337,91 +307,10 @@ const isPackagePurchase = computed(() => formModel.bizType === "PACKAGE_PURCHASE
|
|||||||
|
|
||||||
// ---------- 业务类型 ----------
|
// ---------- 业务类型 ----------
|
||||||
|
|
||||||
const bizTypeOptions: Array<{ label: string; value: EventBizType }> = [
|
|
||||||
{ label: "普通登记", value: "GENERAL" },
|
|
||||||
{ label: "套餐购买", value: "PACKAGE_PURCHASE" }
|
|
||||||
];
|
|
||||||
const bizTypePickerVisible = ref(false);
|
|
||||||
const bizTypePickerDraft = ref<PickerValue[]>([formModel.bizType]);
|
|
||||||
const bizTypePickerColumns = [bizTypeOptions];
|
|
||||||
|
|
||||||
function getBizTypeText(value: EventBizType): string {
|
function getBizTypeText(value: EventBizType): string {
|
||||||
return getEventBizTypeText(value);
|
return getEventBizTypeText(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openBizTypePicker(): void {
|
|
||||||
if (!isCreateMode.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
bizTypePickerDraft.value = [formModel.bizType];
|
|
||||||
bizTypePickerVisible.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function confirmBizTypePicker(value: Array<string | number>): Promise<void> {
|
|
||||||
const bizType = value[0];
|
|
||||||
if (bizType !== "GENERAL" && bizType !== "PACKAGE_PURCHASE") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
formModel.bizType = bizType;
|
|
||||||
if (bizType === "PACKAGE_PURCHASE") {
|
|
||||||
formModel.requireVerification = false;
|
|
||||||
await loadPackageConfigList();
|
|
||||||
} else {
|
|
||||||
formModel.packageConfigId = "";
|
|
||||||
}
|
|
||||||
bizTypePickerVisible.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- 套餐绑定 ----------
|
|
||||||
|
|
||||||
const packageConfigList = ref<GaoPackageConfig[]>([]);
|
|
||||||
const packageConfigPickerVisible = ref(false);
|
|
||||||
const packageConfigPickerDraft = ref<PickerValue[]>([]);
|
|
||||||
const packageConfigPickerColumns = computed(() => [[...packageConfigList.value].map(item => ({
|
|
||||||
label: `${item.name || "未命名套餐"}(${getPackageConfigStatusText(item.status)})`,
|
|
||||||
value: item.id || ""
|
|
||||||
}))]);
|
|
||||||
const selectedPackageConfig = computed(() => packageConfigList.value.find(item => item.id === formModel.packageConfigId));
|
|
||||||
|
|
||||||
async function loadPackageConfigList(): Promise<void> {
|
|
||||||
if (formModel.bizType !== "PACKAGE_PURCHASE" || !formModel.storeId) {
|
|
||||||
packageConfigList.value = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const result = await PackageAPI.configList({
|
|
||||||
storeId: formModel.storeId,
|
|
||||||
index: 0,
|
|
||||||
size: 1000
|
|
||||||
});
|
|
||||||
packageConfigList.value = result.list || [];
|
|
||||||
} catch (error) {
|
|
||||||
packageConfigList.value = [];
|
|
||||||
Message.error(getErrorMessage(error, "套餐配置加载失败"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openPackageConfigPicker(): void {
|
|
||||||
if (formModel.bizType !== "PACKAGE_PURCHASE") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!packageConfigList.value.length) {
|
|
||||||
Message.warning("当前门店没有可绑定的套餐配置");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
packageConfigPickerDraft.value = [formModel.packageConfigId];
|
|
||||||
packageConfigPickerVisible.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmPackageConfigPicker(value: Array<string | number>): void {
|
|
||||||
const packageConfigId = String(value[0] || "");
|
|
||||||
if (!packageConfigList.value.some(item => item.id === packageConfigId)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
formModel.packageConfigId = packageConfigId;
|
|
||||||
packageConfigPickerVisible.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- 门店选择 ----------
|
// ---------- 门店选择 ----------
|
||||||
|
|
||||||
const storePickerVisible = ref(false);
|
const storePickerVisible = ref(false);
|
||||||
@@ -459,9 +348,6 @@ async function confirmStorePicker(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
formModel.storeId = storeId;
|
formModel.storeId = storeId;
|
||||||
if (formModel.bizType === "PACKAGE_PURCHASE") {
|
|
||||||
await loadPackageConfigList();
|
|
||||||
}
|
|
||||||
storePickerVisible.value = false;
|
storePickerVisible.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -725,10 +611,6 @@ function validateForm(): boolean {
|
|||||||
Message.warning("请至少选择一个可登记角色");
|
Message.warning("请至少选择一个可登记角色");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (formModel.bizType === "PACKAGE_PURCHASE" && (!formModel.packageConfigId || !selectedPackageConfig.value)) {
|
|
||||||
Message.warning("请选择绑定套餐配置");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const optionError = validateEventOptionGroupList(optionGroupList.value);
|
const optionError = validateEventOptionGroupList(optionGroupList.value);
|
||||||
if (optionError) {
|
if (optionError) {
|
||||||
Message.warning(optionError);
|
Message.warning(optionError);
|
||||||
@@ -738,10 +620,6 @@ function validateForm(): boolean {
|
|||||||
Message.warning("请输入有效的约束数值");
|
Message.warning("请输入有效的约束数值");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (formModel.bizType === "PACKAGE_PURCHASE" && formModel.requireVerification) {
|
|
||||||
Message.warning("套餐购买事件不能要求核销");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const beginAt = resolveTimestamp(formModel.beginAtText);
|
const beginAt = resolveTimestamp(formModel.beginAtText);
|
||||||
const endAt = resolveTimestamp(formModel.endAtText);
|
const endAt = resolveTimestamp(formModel.endAtText);
|
||||||
if (beginAt !== undefined && endAt !== undefined && endAt < beginAt) {
|
if (beginAt !== undefined && endAt !== undefined && endAt < beginAt) {
|
||||||
@@ -815,7 +693,6 @@ async function saveEventItem(): Promise<void> {
|
|||||||
id: formModel.id || undefined,
|
id: formModel.id || undefined,
|
||||||
storeId: formModel.storeId,
|
storeId: formModel.storeId,
|
||||||
bizType: formModel.bizType,
|
bizType: formModel.bizType,
|
||||||
packageConfigId: formModel.bizType === "PACKAGE_PURCHASE" ? formModel.packageConfigId : undefined,
|
|
||||||
name: formModel.name.trim(),
|
name: formModel.name.trim(),
|
||||||
description: formModel.description,
|
description: formModel.description,
|
||||||
color: formModel.color,
|
color: formModel.color,
|
||||||
@@ -875,11 +752,15 @@ async function fillForm(): Promise<void> {
|
|||||||
await router.replace({ name: "EventDetail", params: { id: routeId.value } });
|
await router.replace({ name: "EventDetail", params: { id: routeId.value } });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (current.bizType === "PACKAGE_PURCHASE" || current.bizType === "LEGACY_CONSUMPTION") {
|
||||||
|
Message.warning("退役事件仅支持历史查看,不能编辑");
|
||||||
|
await router.replace({ name: "EventDetail", params: { id: routeId.value } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
Object.assign(formModel, {
|
Object.assign(formModel, {
|
||||||
id: current.id || "",
|
id: current.id || "",
|
||||||
storeId: current.storeId || formModel.storeId,
|
storeId: current.storeId || formModel.storeId,
|
||||||
bizType: current.bizType || "GENERAL",
|
bizType: current.bizType || "GENERAL",
|
||||||
packageConfigId: current.packageConfigId || "",
|
|
||||||
name: current.name || "",
|
name: current.name || "",
|
||||||
description: current.description || "",
|
description: current.description || "",
|
||||||
color: current.color || "#2f88ff",
|
color: current.color || "#2f88ff",
|
||||||
@@ -894,12 +775,6 @@ async function fillForm(): Promise<void> {
|
|||||||
endAtText: current.endAt ? Time.toDateTime(current.endAt) : "",
|
endAtText: current.endAt ? Time.toDateTime(current.endAt) : "",
|
||||||
roleCodeList: [...(current.roleCodeList || [])]
|
roleCodeList: [...(current.roleCodeList || [])]
|
||||||
});
|
});
|
||||||
if (formModel.bizType === "PACKAGE_PURCHASE") {
|
|
||||||
await loadPackageConfigList();
|
|
||||||
if (current.packageConfig && !packageConfigList.value.some(item => item.id === current.packageConfig?.id)) {
|
|
||||||
packageConfigList.value.unshift(current.packageConfig);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
optionGroupList.value = normalizeEventOptionGroupList(current.optionGroupList);
|
optionGroupList.value = normalizeEventOptionGroupList(current.optionGroupList);
|
||||||
formModel.limitValueText = resolveLimitValueText(current.limitValue);
|
formModel.limitValueText = resolveLimitValueText(current.limitValue);
|
||||||
colorDraft.value = formModel.color;
|
colorDraft.value = formModel.color;
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
<t-tag v-if="item.requirePhoto" theme="warning" variant="outline">需拍照</t-tag>
|
<t-tag v-if="item.requirePhoto" theme="warning" variant="outline">需拍照</t-tag>
|
||||||
<t-tag v-if="item.requireVerification" theme="warning" variant="outline">需核销</t-tag>
|
<t-tag v-if="item.requireVerification" theme="warning" variant="outline">需核销</t-tag>
|
||||||
<t-tag v-if="item.bizType === 'PACKAGE_PURCHASE'" theme="primary" variant="outline">套餐购买</t-tag>
|
<t-tag v-if="item.bizType === 'PACKAGE_PURCHASE'" theme="primary" variant="outline">套餐购买</t-tag>
|
||||||
|
<t-tag v-if="item.bizType === 'LEGACY_CONSUMPTION'" theme="default" variant="outline">历史消费</t-tag>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #description>
|
<template #description>
|
||||||
@@ -91,7 +92,7 @@ import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
|||||||
import { useNavBarStore } from "@/store/navBarStore";
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
import type { EventItem, EventLimitType, EventStatus } from "@/types/EventItem";
|
import type { EventItem, EventLimitType, EventStatus } from "@/types/EventItem";
|
||||||
import type { NavBarAction } from "@/types/NavBarAction";
|
import type { NavBarAction } from "@/types/NavBarAction";
|
||||||
import { getEventBizTypeText } from "@/utils/event";
|
import { getEventBizTypeText, isRetiredEvent } from "@/utils/event";
|
||||||
import { getEventValueUnitText } from "@/utils/eventValue";
|
import { getEventValueUnitText } from "@/utils/eventValue";
|
||||||
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
||||||
|
|
||||||
@@ -156,7 +157,7 @@ const pageResult = reactive<PageResult<EventItem>>({
|
|||||||
total: 0,
|
total: 0,
|
||||||
list: []
|
list: []
|
||||||
});
|
});
|
||||||
const visibleEventList = computed(() => pageResult.list);
|
const visibleEventList = computed(() => pageResult.list.filter(item => !isRetiredEvent(item)));
|
||||||
const emptyText = computed(() => {
|
const emptyText = computed(() => {
|
||||||
if (isLoading.value) {
|
if (isLoading.value) {
|
||||||
return "正在加载登记事件";
|
return "正在加载登记事件";
|
||||||
|
|||||||
@@ -50,19 +50,18 @@
|
|||||||
class="event-selector"
|
class="event-selector"
|
||||||
:event-list="eventList"
|
:event-list="eventList"
|
||||||
:disabled="isEditMode"
|
:disabled="isEditMode"
|
||||||
:selection-mode="isPackagePurchaseSelected ? 'single' : 'multiple'"
|
selection-mode="multiple"
|
||||||
required-mark
|
required-mark
|
||||||
@update:model-value="handleEventSelectionChange"
|
@update:model-value="handleEventSelectionChange"
|
||||||
/>
|
/>
|
||||||
<event-option-selector
|
<event-option-selector
|
||||||
v-if="!isPackagePurchaseSelected"
|
|
||||||
class="option-selector"
|
class="option-selector"
|
||||||
v-model="selectedOptionSelectionMap"
|
v-model="selectedOptionSelectionMap"
|
||||||
:event-list="eventList"
|
:event-list="eventList"
|
||||||
:selected-event-id-list="selectedEventIdList"
|
:selected-event-id-list="selectedEventIdList"
|
||||||
:disabled="isSaving"
|
:disabled="isSaving"
|
||||||
/>
|
/>
|
||||||
<div v-if="!isEditMode && !isPackagePurchaseSelected && selectedNumberEventList.length" class="event-value-list">
|
<div v-if="!isEditMode && selectedNumberEventList.length" class="event-value-list">
|
||||||
<t-form-item
|
<t-form-item
|
||||||
v-for="item in selectedNumberEventList"
|
v-for="item in selectedNumberEventList"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
@@ -79,25 +78,6 @@
|
|||||||
/>
|
/>
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
</div>
|
</div>
|
||||||
<package-purchase-form
|
|
||||||
v-if="isPackagePurchaseSelected"
|
|
||||||
v-model:item-list="packagePurchaseItemList"
|
|
||||||
v-model:sale-amount="saleAmountText"
|
|
||||||
:can-price-override="canPriceOverride"
|
|
||||||
:package-config="selectedPackageConfig"
|
|
||||||
/>
|
|
||||||
<t-form-item v-if="showSourceValueInput" label="积分动作">
|
|
||||||
<span class="picker-text" v-text="getPointActionText(sourceValueAction)" />
|
|
||||||
</t-form-item>
|
|
||||||
<t-form-item v-if="showSourceValueInput" label="积分数量" required-mark>
|
|
||||||
<t-input
|
|
||||||
v-model.trim="pointValueText"
|
|
||||||
type="number"
|
|
||||||
align="right"
|
|
||||||
borderless
|
|
||||||
:placeholder="sourceValuePlaceholder"
|
|
||||||
/>
|
|
||||||
</t-form-item>
|
|
||||||
<t-form-item v-if="!isEditMode" label="登记时间" content-align="right">
|
<t-form-item v-if="!isEditMode" label="登记时间" content-align="right">
|
||||||
<div class="time-actions">
|
<div class="time-actions">
|
||||||
<t-button
|
<t-button
|
||||||
@@ -120,7 +100,7 @@
|
|||||||
<t-form-item v-else label="登记时间" content-align="right">
|
<t-form-item v-else label="登记时间" content-align="right">
|
||||||
<span class="readonly-time" v-text="registeredAtText || '此刻'" />
|
<span class="readonly-time" v-text="registeredAtText || '此刻'" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item :label="isPackagePurchaseSelected ? '购买备注' : '登记备注'">
|
<t-form-item label="登记备注">
|
||||||
<t-textarea
|
<t-textarea
|
||||||
v-model.trim="note"
|
v-model.trim="note"
|
||||||
class="textarea"
|
class="textarea"
|
||||||
@@ -166,7 +146,7 @@
|
|||||||
v-if="!isEditMode"
|
v-if="!isEditMode"
|
||||||
v-model="selectedCustomerIdList"
|
v-model="selectedCustomerIdList"
|
||||||
v-model:visible="selectorVisible"
|
v-model:visible="selectorVisible"
|
||||||
:selection-mode="isEditMode || isPackagePurchaseSelected ? 'single' : 'multiple'"
|
:selection-mode="isEditMode ? 'single' : 'multiple'"
|
||||||
@change="handleCustomerChange"
|
@change="handleCustomerChange"
|
||||||
/>
|
/>
|
||||||
<t-popup v-model="selectedCustomerListVisible" placement="bottom">
|
<t-popup v-model="selectedCustomerListVisible" placement="bottom">
|
||||||
@@ -222,21 +202,17 @@ import CustomerSelector from "@/components/CustomerSelector.vue";
|
|||||||
import EventOptionSelector from "@/components/EventOptionSelector.vue";
|
import EventOptionSelector from "@/components/EventOptionSelector.vue";
|
||||||
import PhotoScroller from "@/components/PhotoScroller.vue";
|
import PhotoScroller from "@/components/PhotoScroller.vue";
|
||||||
import EventSelector from "@/components/EventSelector.vue";
|
import EventSelector from "@/components/EventSelector.vue";
|
||||||
import PackagePurchaseForm from "@/pages/event/component/PackagePurchaseForm.vue";
|
|
||||||
import { useEventRecordPhoto } from "@/pages/event/component/useEventRecordPhoto";
|
import { useEventRecordPhoto } from "@/pages/event/component/useEventRecordPhoto";
|
||||||
import { usePackagePurchaseRecord } from "@/pages/event/component/usePackagePurchaseRecord";
|
|
||||||
import { useNavBarStore } from "@/store/navBarStore";
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
import { useSettingStore } from "@/store/settingStore";
|
import { useSettingStore } from "@/store/settingStore";
|
||||||
import type { Customer } from "@/types/Customer";
|
import type { Customer } from "@/types/Customer";
|
||||||
import type { EventItem, EventOptionSelectionMap } from "@/types/EventItem";
|
import type { EventItem, EventOptionSelectionMap } from "@/types/EventItem";
|
||||||
import type { EventRecord } from "@/types/EventRecord";
|
import type { EventRecord } from "@/types/EventRecord";
|
||||||
import type { PointAction } from "@/types/Point";
|
import { isRetiredEvent } from "@/utils/event";
|
||||||
import { isPackagePurchaseEvent } from "@/utils/event";
|
|
||||||
import { buildEventOptionSelectionMap, resolveSelectedOptionGroupList, validateEventOptionSelection } from "@/utils/eventOption";
|
import { buildEventOptionSelectionMap, resolveSelectedOptionGroupList, validateEventOptionSelection } from "@/utils/eventOption";
|
||||||
import { getEventRecordPhotoURL, hasPhotoAttachment } from "@/utils/eventRecord";
|
import { getEventRecordPhotoURL, hasPhotoAttachment } from "@/utils/eventRecord";
|
||||||
import { getEventNumberValuePlaceholder, resolveEventNumberValue } from "@/utils/eventValue";
|
import { getEventNumberValuePlaceholder, resolveEventNumberValue } from "@/utils/eventValue";
|
||||||
import { permissionCodes } from "@/utils/permission";
|
import { permissionCodes } from "@/utils/permission";
|
||||||
import { loadSourceValueConfig, type SourceValueConfig } from "@/utils/point";
|
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "EventRecordEdit"
|
name: "EventRecordEdit"
|
||||||
@@ -274,9 +250,6 @@ const customerDisplayText = computed(() => {
|
|||||||
|
|
||||||
async function handleCustomerChange(value: string | string[]): Promise<void> {
|
async function handleCustomerChange(value: string | string[]): Promise<void> {
|
||||||
selectedCustomerIdList.value = Array.isArray(value) ? value : [value];
|
selectedCustomerIdList.value = Array.isArray(value) ? value : [value];
|
||||||
if (isPackagePurchaseSelected.value) {
|
|
||||||
resetPackageRequestNo();
|
|
||||||
}
|
|
||||||
await loadSelectedCustomerList();
|
await loadSelectedCustomerList();
|
||||||
await loadRecentRecordList();
|
await loadRecentRecordList();
|
||||||
}
|
}
|
||||||
@@ -334,10 +307,10 @@ async function loadSelectedCustomer(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (customer.id) {
|
if (customer.id) {
|
||||||
selectedCustomerIdList.value = isEditMode.value || isPackagePurchaseSelected.value
|
selectedCustomerIdList.value = isEditMode.value
|
||||||
? [customer.id]
|
? [customer.id]
|
||||||
: [...new Set([...selectedCustomerIdList.value, customer.id])];
|
: [...new Set([...selectedCustomerIdList.value, customer.id])];
|
||||||
selectedCustomerList.value = isEditMode.value || isPackagePurchaseSelected.value
|
selectedCustomerList.value = isEditMode.value
|
||||||
? [customer]
|
? [customer]
|
||||||
: [...selectedCustomerList.value.filter(item => item.id !== customer.id), customer];
|
: [...selectedCustomerList.value.filter(item => item.id !== customer.id), customer];
|
||||||
}
|
}
|
||||||
@@ -449,6 +422,10 @@ function appendRecordEvent(record: EventRecord): void {
|
|||||||
eventList.value = [record.event, ...eventList.value];
|
eventList.value = [record.event, ...eventList.value];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleEventSelectionChange(eventIdList: string[]): void {
|
||||||
|
selectedEventIdList.value = eventIdList;
|
||||||
|
}
|
||||||
|
|
||||||
function resolveRecordOptionGroupList(eventId: string) {
|
function resolveRecordOptionGroupList(eventId: string) {
|
||||||
const event = eventList.value.find(item => item.id === eventId);
|
const event = eventList.value.find(item => item.id === eventId);
|
||||||
return event ? resolveSelectedOptionGroupList(event, selectedOptionSelectionMap) : [];
|
return event ? resolveSelectedOptionGroupList(event, selectedOptionSelectionMap) : [];
|
||||||
@@ -494,52 +471,6 @@ function validateNumberValueList(): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 积分规则联动 ----------
|
|
||||||
|
|
||||||
const pointValueText = ref("");
|
|
||||||
const sourceValueConfig = ref<SourceValueConfig>();
|
|
||||||
const sourceValueAction = ref<PointAction>("EARN");
|
|
||||||
let sourceValueLoadToken = 0;
|
|
||||||
|
|
||||||
const showSourceValueInput = computed(() => (
|
|
||||||
!isEditMode.value
|
|
||||||
&& !isPackagePurchaseSelected.value
|
|
||||||
&& userStore.hasPermissions(permissionCodes.point.sourceValue)
|
|
||||||
&& !!sourceValueConfig.value
|
|
||||||
));
|
|
||||||
const sourceValuePlaceholder = computed(() => {
|
|
||||||
const range = sourceValueConfig.value?.range;
|
|
||||||
return range ? `请输入 ${range[0]}-${range[1]} 的数量` : "请输入数量";
|
|
||||||
});
|
|
||||||
|
|
||||||
async function loadSourceValueRule(): Promise<void> {
|
|
||||||
const token = sourceValueLoadToken + 1;
|
|
||||||
sourceValueLoadToken = token;
|
|
||||||
sourceValueConfig.value = undefined;
|
|
||||||
if (isEditMode.value || isPackagePurchaseSelected.value || !userStore.hasPermissions(permissionCodes.point.sourceValue) || !selectedEventIdList.value.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
sourceValueConfig.value = await loadSourceValueConfig(selectedEventIdList.value);
|
|
||||||
sourceValueAction.value = sourceValueConfig.value?.action || "EARN";
|
|
||||||
if (token !== sourceValueLoadToken) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
sourceValueConfig.value = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveSourceValue(): number | undefined {
|
|
||||||
const value = Number(pointValueText.value);
|
|
||||||
return Number.isSafeInteger(value) && 0 < value ? sourceValueAction.value === "CONSUME" ? -value : value : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPointActionText(action: PointAction): string {
|
|
||||||
return action === "CONSUME" ? "消耗积分" : "发放积分";
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- 照片 ----------
|
// ---------- 照片 ----------
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -594,29 +525,6 @@ function parseDateTime(value: string): Date {
|
|||||||
// ---------- 提交 ----------
|
// ---------- 提交 ----------
|
||||||
|
|
||||||
const note = ref("");
|
const note = ref("");
|
||||||
const {
|
|
||||||
selectedPackageEvent,
|
|
||||||
isPackagePurchaseSelected,
|
|
||||||
selectedPackageConfig,
|
|
||||||
canPriceOverride,
|
|
||||||
saleAmountText,
|
|
||||||
packagePurchaseItemList,
|
|
||||||
handleEventSelectionChange,
|
|
||||||
applySelectedPackageEvent,
|
|
||||||
resetPackagePurchaseForm,
|
|
||||||
resetPackageRequestNo,
|
|
||||||
submitPackagePurchase,
|
|
||||||
validatePackagePurchaseForm
|
|
||||||
} = usePackagePurchaseRecord({
|
|
||||||
eventList,
|
|
||||||
selectedEventIdList,
|
|
||||||
selectedCustomerIdList,
|
|
||||||
selectedCustomerList,
|
|
||||||
note,
|
|
||||||
hasPermission: permissionCode => userStore.hasPermissions(permissionCode),
|
|
||||||
buildRegisteredAt,
|
|
||||||
buildTempFileIdList
|
|
||||||
});
|
|
||||||
const isSaving = ref(false);
|
const isSaving = ref(false);
|
||||||
const editingRecord = ref<EventRecord>();
|
const editingRecord = ref<EventRecord>();
|
||||||
|
|
||||||
@@ -639,21 +547,14 @@ async function submitRecord(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const packageResult = isPackagePurchaseSelected.value ? await submitPackagePurchase() : undefined;
|
const success = await submitEventRecords();
|
||||||
const success = isPackagePurchaseSelected.value
|
|
||||||
? !!packageResult?.id
|
|
||||||
: await submitEventRecords();
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
resetCreateForm();
|
resetCreateForm();
|
||||||
await loadRecentRecordList();
|
await loadRecentRecordList();
|
||||||
await settingStore.playRegisterSuccessSound();
|
await settingStore.playRegisterSuccessSound();
|
||||||
Message.success(isPackagePurchaseSelected.value ? "套餐购买已提交" : "登记已提交");
|
Message.success("登记已提交");
|
||||||
if (packageResult?.id) {
|
|
||||||
await router.replace(`/management/customer-packages/${packageResult.id}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await router.replace("/customers");
|
await router.replace("/customers");
|
||||||
} finally {
|
} finally {
|
||||||
isSaving.value = false;
|
isSaving.value = false;
|
||||||
@@ -668,7 +569,6 @@ async function submitEventRecords(): Promise<boolean> {
|
|||||||
selectedOptionGroupList: resolveRecordOptionGroupList(eventId),
|
selectedOptionGroupList: resolveRecordOptionGroupList(eventId),
|
||||||
remark: note.value || "客户现场登记",
|
remark: note.value || "客户现场登记",
|
||||||
registeredAt: buildRegisteredAt(),
|
registeredAt: buildRegisteredAt(),
|
||||||
pointValue: showSourceValueInput.value ? resolveSourceValue() : undefined,
|
|
||||||
numberValue: resolveNumberValue(eventId),
|
numberValue: resolveNumberValue(eventId),
|
||||||
attachmentList: buildTempFileIdList().map(tempFileId => ({
|
attachmentList: buildTempFileIdList().map(tempFileId => ({
|
||||||
tempFileId,
|
tempFileId,
|
||||||
@@ -682,7 +582,6 @@ function resetCreateForm(): void {
|
|||||||
selectedCustomerList.value = [];
|
selectedCustomerList.value = [];
|
||||||
selectedCustomerIdList.value = [];
|
selectedCustomerIdList.value = [];
|
||||||
note.value = "";
|
note.value = "";
|
||||||
pointValueText.value = "";
|
|
||||||
photoItemList.value = [];
|
photoItemList.value = [];
|
||||||
for (const eventId of Object.keys(numberValueTextMap)) {
|
for (const eventId of Object.keys(numberValueTextMap)) {
|
||||||
delete numberValueTextMap[eventId];
|
delete numberValueTextMap[eventId];
|
||||||
@@ -690,11 +589,6 @@ function resetCreateForm(): void {
|
|||||||
for (const eventId of Object.keys(selectedOptionSelectionMap)) {
|
for (const eventId of Object.keys(selectedOptionSelectionMap)) {
|
||||||
delete selectedOptionSelectionMap[eventId];
|
delete selectedOptionSelectionMap[eventId];
|
||||||
}
|
}
|
||||||
if (selectedPackageEvent.value) {
|
|
||||||
applySelectedPackageEvent(selectedPackageEvent.value);
|
|
||||||
} else {
|
|
||||||
resetPackagePurchaseForm();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateForm(): boolean {
|
function validateForm(): boolean {
|
||||||
@@ -706,9 +600,6 @@ function validateForm(): boolean {
|
|||||||
Message.warning("请至少选择一个登记事件");
|
Message.warning("请至少选择一个登记事件");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (isPackagePurchaseSelected.value) {
|
|
||||||
return validatePackagePurchaseForm() && validateRequiredPhotoList();
|
|
||||||
}
|
|
||||||
for (const eventId of selectedEventIdList.value) {
|
for (const eventId of selectedEventIdList.value) {
|
||||||
const event = eventList.value.find(item => item.id === eventId);
|
const event = eventList.value.find(item => item.id === eventId);
|
||||||
if (!event) {
|
if (!event) {
|
||||||
@@ -726,14 +617,6 @@ function validateForm(): boolean {
|
|||||||
if (!isEditMode.value && !validateNumberValueList()) {
|
if (!isEditMode.value && !validateNumberValueList()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (showSourceValueInput.value) {
|
|
||||||
const pointValue = resolveSourceValue();
|
|
||||||
const range = sourceValueConfig.value?.range;
|
|
||||||
if (!pointValue || !range || Math.abs(pointValue) < range[0] || range[1] < Math.abs(pointValue)) {
|
|
||||||
Message.warning(`积分数量必须是 ${range?.[0] || 1}-${range?.[1] || 0} 的正整数`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -767,20 +650,21 @@ async function updateRecord(): Promise<void> {
|
|||||||
// ---------- 页面数据 ----------
|
// ---------- 页面数据 ----------
|
||||||
|
|
||||||
async function loadPageData(): Promise<void> {
|
async function loadPageData(): Promise<void> {
|
||||||
eventList.value = await EventAPI.listValid();
|
// 套餐购买不再走事件登记,退役消费事件也不可登记
|
||||||
|
eventList.value = (await EventAPI.listValid()).filter(item => !isRetiredEvent(item));
|
||||||
if (isEditMode.value) {
|
if (isEditMode.value) {
|
||||||
await loadRecordForEdit();
|
await loadRecordForEdit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultEvent = eventList.value.find(item => !isPackagePurchaseEvent(item));
|
const defaultEvent = eventList.value[0];
|
||||||
selectedEventIdList.value = defaultEvent?.id ? [defaultEvent.id] : [];
|
selectedEventIdList.value = defaultEvent?.id ? [defaultEvent.id] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadRecordForEdit(): Promise<void> {
|
async function loadRecordForEdit(): Promise<void> {
|
||||||
const record = await EventRecordAPI.detail(routeId.value);
|
const record = await EventRecordAPI.detail(routeId.value);
|
||||||
if (isPackagePurchaseEvent(record.event)) {
|
if (isRetiredEvent(record.event)) {
|
||||||
Message.warning("套餐购买记录不能通过普通登记页编辑,请在客户套餐详情中查看");
|
Message.warning("退役事件记录仅支持历史查看,不能编辑");
|
||||||
await router.replace({ name: "EventRecordDetail", params: { id: routeId.value } });
|
await router.replace({ name: "EventRecordDetail", params: { id: routeId.value } });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -798,7 +682,6 @@ async function loadRecordForEdit(): Promise<void> {
|
|||||||
}
|
}
|
||||||
registeredAtText.value = record.registeredAt ? Time.formatDateTime(new Date(record.registeredAt)) : "";
|
registeredAtText.value = record.registeredAt ? Time.formatDateTime(new Date(record.registeredAt)) : "";
|
||||||
note.value = record.remark || "";
|
note.value = record.remark || "";
|
||||||
pointValueText.value = "";
|
|
||||||
setExistingPhotoList(record.attachmentList);
|
setExistingPhotoList(record.attachmentList);
|
||||||
await loadRecentRecordList();
|
await loadRecentRecordList();
|
||||||
}
|
}
|
||||||
@@ -816,10 +699,6 @@ onUnmounted(() => {
|
|||||||
navBarStore.clearRight();
|
navBarStore.clearRight();
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(selectedEventIdList, async () => {
|
|
||||||
await loadSourceValueRule();
|
|
||||||
}, { deep: true });
|
|
||||||
|
|
||||||
watch(customerCode, async (value) => {
|
watch(customerCode, async (value) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ import { useNavBarStore } from "@/store/navBarStore";
|
|||||||
import type { EventItem, EventStatus } from "@/types/EventItem";
|
import type { EventItem, EventStatus } from "@/types/EventItem";
|
||||||
import type { EventOptionStat, EventRecordPage, EventRecordPageResult, VerificationStatus } from "@/types/EventRecord";
|
import type { EventOptionStat, EventRecordPage, EventRecordPageResult, VerificationStatus } from "@/types/EventRecord";
|
||||||
import type { NavBarAction } from "@/types/NavBarAction";
|
import type { NavBarAction } from "@/types/NavBarAction";
|
||||||
import { isPackagePurchaseEvent } from "@/utils/event";
|
import { isRetiredEvent } from "@/utils/event";
|
||||||
import { formatEventNumberValue } from "@/utils/eventValue";
|
import { formatEventNumberValue } from "@/utils/eventValue";
|
||||||
import { permissionCodes } from "@/utils/permission";
|
import { permissionCodes } from "@/utils/permission";
|
||||||
import { Message } from "@/utils/message.ts";
|
import { Message } from "@/utils/message.ts";
|
||||||
@@ -403,7 +403,7 @@ async function loadActions(): Promise<void> {
|
|||||||
const result = canCreateEvent.value
|
const result = canCreateEvent.value
|
||||||
? await EventAPI.listHistory()
|
? await EventAPI.listHistory()
|
||||||
: await EventAPI.list({ index: 0, size: 1000 });
|
: await EventAPI.list({ index: 0, size: 1000 });
|
||||||
eventList.value = (result.list || []).filter(item => !isPackagePurchaseEvent(item));
|
eventList.value = (result.list || []).filter(item => !isRetiredEvent(item));
|
||||||
const eventIdList = eventList.value.map(item => item.id).filter((item): item is string => Boolean(item));
|
const eventIdList = eventList.value.map(item => item.id).filter((item): item is string => Boolean(item));
|
||||||
const eventIdSet = new Set(eventIdList);
|
const eventIdSet = new Set(eventIdList);
|
||||||
const selectedEventIdList = parseEventIdList(route.query.eventIds).filter(eventId => eventIdSet.has(eventId));
|
const selectedEventIdList = parseEventIdList(route.query.eventIds).filter(eventId => eventIdSet.has(eventId));
|
||||||
|
|||||||
@@ -75,25 +75,6 @@
|
|||||||
@add="capturePhoto"
|
@add="capturePhoto"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<t-form
|
|
||||||
v-if="showSourceValueInput"
|
|
||||||
class="input-form"
|
|
||||||
label-align="left"
|
|
||||||
label-width="6rem"
|
|
||||||
>
|
|
||||||
<t-form-item label="积分动作" content-align="right">
|
|
||||||
<span class="form-text" v-text="sourceValueAction === 'CONSUME' ? '消耗积分' : '发放积分'" />
|
|
||||||
</t-form-item>
|
|
||||||
<t-form-item label="积分数量" required-mark>
|
|
||||||
<t-input
|
|
||||||
v-model.trim="pointValueText"
|
|
||||||
type="number"
|
|
||||||
align="right"
|
|
||||||
borderless
|
|
||||||
:placeholder="sourceValuePlaceholder"
|
|
||||||
/>
|
|
||||||
</t-form-item>
|
|
||||||
</t-form>
|
|
||||||
<t-cell-group
|
<t-cell-group
|
||||||
v-if="recentRecordList.length"
|
v-if="recentRecordList.length"
|
||||||
title="最近登记记录"
|
title="最近登记记录"
|
||||||
@@ -145,16 +126,14 @@ import type { Customer } from "@/types/Customer";
|
|||||||
import type { EventItem, EventOptionSelectionMap } from "@/types/EventItem";
|
import type { EventItem, EventOptionSelectionMap } from "@/types/EventItem";
|
||||||
import type { EventRecord } from "@/types/EventRecord";
|
import type { EventRecord } from "@/types/EventRecord";
|
||||||
import type { PhotoItem } from "@/types/Photo";
|
import type { PhotoItem } from "@/types/Photo";
|
||||||
import type { PointAction } from "@/types/Point";
|
|
||||||
import { getEventNumberValuePlaceholder, resolveEventNumberValue } from "@/utils/eventValue";
|
import { getEventNumberValuePlaceholder, resolveEventNumberValue } from "@/utils/eventValue";
|
||||||
import { isPackagePurchaseEvent } from "@/utils/event";
|
import { isRetiredEvent } from "@/utils/event";
|
||||||
import { resolveSelectedOptionGroupList, validateEventOptionSelection } from "@/utils/eventOption";
|
import { resolveSelectedOptionGroupList, validateEventOptionSelection } from "@/utils/eventOption";
|
||||||
import { permissionCodes } from "@/utils/permission";
|
import { permissionCodes } from "@/utils/permission";
|
||||||
import { getEventRecordPhotoURL, hasPhotoAttachment } from "@/utils/eventRecord";
|
import { getEventRecordPhotoURL, hasPhotoAttachment } from "@/utils/eventRecord";
|
||||||
import { compressUploadImageList } from "@/utils/imageCompression";
|
import { compressUploadImageList } from "@/utils/imageCompression";
|
||||||
import { Storage, Time } from "timi-web";
|
import { Storage, Time } from "timi-web";
|
||||||
import { AttachmentAPI, useUserStore, type Attachment } from "timi-web-api";
|
import { AttachmentAPI, type Attachment } from "timi-web-api";
|
||||||
import { loadSourceValueConfig, type SourceValueConfig } from "@/utils/point";
|
|
||||||
import EventSelector from "@/components/EventSelector.vue";
|
import EventSelector from "@/components/EventSelector.vue";
|
||||||
import { Space } from "timi-tdesign-mobile";
|
import { Space } from "timi-tdesign-mobile";
|
||||||
|
|
||||||
@@ -163,7 +142,6 @@ defineOptions({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const userStore = useUserStore();
|
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
|
|
||||||
// ---------- 登记事件 ----------
|
// ---------- 登记事件 ----------
|
||||||
@@ -179,7 +157,6 @@ const selectedEventList = computed(() => selectedEventIdList.value
|
|||||||
.filter((item): item is EventItem => !!item));
|
.filter((item): item is EventItem => !!item));
|
||||||
const selectedNumberEventList = computed(() => selectedEventList.value.filter(item => item.valueType === "NUMBER"));
|
const selectedNumberEventList = computed(() => selectedEventList.value.filter(item => item.valueType === "NUMBER"));
|
||||||
const selectedRequiredPhotoEventList = computed(() => selectedEventList.value.filter(item => item.requirePhoto === true));
|
const selectedRequiredPhotoEventList = computed(() => selectedEventList.value.filter(item => item.requirePhoto === true));
|
||||||
let sourceValueRequestNo = 0;
|
|
||||||
|
|
||||||
function normalizeEventId(item?: EventItem): string {
|
function normalizeEventId(item?: EventItem): string {
|
||||||
if (!item || item.id === undefined || item.id === null) {
|
if (!item || item.id === undefined || item.id === null) {
|
||||||
@@ -195,7 +172,7 @@ async function loadActions(): Promise<void> {
|
|||||||
isEventLoading.value = true;
|
isEventLoading.value = true;
|
||||||
eventLoadFailed.value = false;
|
eventLoadFailed.value = false;
|
||||||
try {
|
try {
|
||||||
eventList.value = (await EventAPI.listValid()).filter(item => !isPackagePurchaseEvent(item));
|
eventList.value = (await EventAPI.listValid()).filter(item => !isRetiredEvent(item));
|
||||||
selectedEventIdList.value = buildInitialSelectedEventIdList();
|
selectedEventIdList.value = buildInitialSelectedEventIdList();
|
||||||
syncStoredEventId();
|
syncStoredEventId();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -203,12 +180,10 @@ async function loadActions(): Promise<void> {
|
|||||||
eventLoadFailed.value = true;
|
eventLoadFailed.value = true;
|
||||||
eventList.value = [];
|
eventList.value = [];
|
||||||
selectedEventIdList.value = [];
|
selectedEventIdList.value = [];
|
||||||
sourceValueConfig.value = undefined;
|
|
||||||
Message.error("登记事件加载失败");
|
Message.error("登记事件加载失败");
|
||||||
} finally {
|
} finally {
|
||||||
isEventLoading.value = false;
|
isEventLoading.value = false;
|
||||||
}
|
}
|
||||||
await loadSourceValueRule();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildInitialSelectedEventIdList(): string[] {
|
function buildInitialSelectedEventIdList(): string[] {
|
||||||
@@ -231,7 +206,6 @@ function normalizeSelectedEventIdList(eventIdList: string[]): string[] {
|
|||||||
async function handleEventSelectionChange(eventIdList: string[]): Promise<void> {
|
async function handleEventSelectionChange(eventIdList: string[]): Promise<void> {
|
||||||
selectedEventIdList.value = normalizeSelectedEventIdList(eventIdList);
|
selectedEventIdList.value = normalizeSelectedEventIdList(eventIdList);
|
||||||
syncStoredEventId();
|
syncStoredEventId();
|
||||||
await loadSourceValueRule();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveRecordOptionGroupList(eventId: string) {
|
function resolveRecordOptionGroupList(eventId: string) {
|
||||||
@@ -320,45 +294,8 @@ async function toRecordDetail(id: string): Promise<void> {
|
|||||||
// ---------- 表单 ----------
|
// ---------- 表单 ----------
|
||||||
|
|
||||||
const submitting = ref(false);
|
const submitting = ref(false);
|
||||||
const pointValueText = ref("");
|
|
||||||
const numberValueTextMap = reactive<Record<string, string>>({});
|
const numberValueTextMap = reactive<Record<string, string>>({});
|
||||||
const sourceValueConfig = ref<SourceValueConfig>();
|
|
||||||
const sourceValueAction = ref<PointAction>("EARN");
|
|
||||||
const canSubmitRegister = computed(() => !!matchedCustomer.value && !!selectedEventIdList.value.length);
|
const canSubmitRegister = computed(() => !!matchedCustomer.value && !!selectedEventIdList.value.length);
|
||||||
const showSourceValueInput = computed(() => (
|
|
||||||
userStore.hasPermissions(permissionCodes.point.sourceValue)
|
|
||||||
&& !!sourceValueConfig.value
|
|
||||||
));
|
|
||||||
const sourceValuePlaceholder = computed(() => {
|
|
||||||
const range = sourceValueConfig.value?.range;
|
|
||||||
return range ? `请输入 ${range[0]}-${range[1]} 的数量` : "请输入数量";
|
|
||||||
});
|
|
||||||
|
|
||||||
async function loadSourceValueRule(): Promise<void> {
|
|
||||||
const requestNo = ++sourceValueRequestNo;
|
|
||||||
if (!selectedEventIdList.value.length || !userStore.hasPermissions(permissionCodes.point.sourceValue)) {
|
|
||||||
sourceValueConfig.value = undefined;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const config = await loadSourceValueConfig(selectedEventIdList.value);
|
|
||||||
if (requestNo !== sourceValueRequestNo) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sourceValueConfig.value = config;
|
|
||||||
sourceValueAction.value = sourceValueConfig.value?.action || "EARN";
|
|
||||||
} catch {
|
|
||||||
if (requestNo !== sourceValueRequestNo) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sourceValueConfig.value = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveSourceValue(): number | undefined {
|
|
||||||
const value = Number(pointValueText.value);
|
|
||||||
return Number.isSafeInteger(value) && 0 < value ? sourceValueAction.value === "CONSUME" ? -value : value : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNumberValuePlaceholder(event: EventItem): string {
|
function getNumberValuePlaceholder(event: EventItem): string {
|
||||||
return getEventNumberValuePlaceholder(event);
|
return getEventNumberValuePlaceholder(event);
|
||||||
@@ -484,19 +421,12 @@ async function doSubmit(): Promise<void> {
|
|||||||
if (!validateNumberValueList()) {
|
if (!validateNumberValueList()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const pointValue = resolveSourceValue();
|
|
||||||
const range = sourceValueConfig.value?.range;
|
|
||||||
if (showSourceValueInput.value && (!pointValue || !range || Math.abs(pointValue) < range[0] || range[1] < Math.abs(pointValue))) {
|
|
||||||
Message.warning(`积分数量必须是 ${range?.[0] || 1}-${range?.[1] || 0} 的正整数`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
try {
|
try {
|
||||||
await EventRecordAPI.createBatch(selectedEventIdList.value.map(eventId => ({
|
await EventRecordAPI.createBatch(selectedEventIdList.value.map(eventId => ({
|
||||||
eventId,
|
eventId,
|
||||||
customerId: customer.id,
|
customerId: customer.id,
|
||||||
selectedOptionGroupList: resolveRecordOptionGroupList(eventId),
|
selectedOptionGroupList: resolveRecordOptionGroupList(eventId),
|
||||||
pointValue: showSourceValueInput.value ? pointValue : undefined,
|
|
||||||
numberValue: resolveNumberValue(eventId),
|
numberValue: resolveNumberValue(eventId),
|
||||||
remark: "客户快速扫码登记",
|
remark: "客户快速扫码登记",
|
||||||
attachmentList: buildPhotoAttachmentList()
|
attachmentList: buildPhotoAttachmentList()
|
||||||
@@ -504,7 +434,6 @@ async function doSubmit(): Promise<void> {
|
|||||||
scanStatus.value = `登记成功:${customer.name || customer.code || scanCode.value}。`;
|
scanStatus.value = `登记成功:${customer.name || customer.code || scanCode.value}。`;
|
||||||
scanCode.value = "";
|
scanCode.value = "";
|
||||||
matchedCustomer.value = undefined;
|
matchedCustomer.value = undefined;
|
||||||
pointValueText.value = "";
|
|
||||||
recordPhotoItemList.value = [];
|
recordPhotoItemList.value = [];
|
||||||
for (const eventId of Object.keys(numberValueTextMap)) {
|
for (const eventId of Object.keys(numberValueTextMap)) {
|
||||||
delete numberValueTextMap[eventId];
|
delete numberValueTextMap[eventId];
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
</t-cell>
|
</t-cell>
|
||||||
<t-cell v-if="customerPackage.salesOrderId && canReadOrder" title="销售订单" :note="order?.orderNo || customerPackage.salesOrderId" arrow @click="toOrder" />
|
<t-cell v-if="customerPackage.salesOrderId && canReadOrder" title="销售订单" :note="order?.orderNo || customerPackage.salesOrderId" arrow @click="toOrder" />
|
||||||
<t-cell v-if="canReadPackageConfig && customerPackage.packageConfigId" title="套餐方案详情" arrow @click="toPackageConfig" />
|
<t-cell v-if="canReadPackageConfig && customerPackage.packageConfigId" title="套餐方案详情" arrow @click="toPackageConfig" />
|
||||||
<t-cell v-if="canReadEventRecord && customerPackage.purchaseEventRecord?.id" title="购买登记记录" arrow @click="toEventRecord" />
|
|
||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
<t-cell-group v-if="order" title="订单资金" theme="card" v-entrance-transition="140">
|
<t-cell-group v-if="order" title="订单资金" theme="card" v-entrance-transition="140">
|
||||||
<t-cell title="成交金额" :note="formatPackageAmount(order.saleAmount)" />
|
<t-cell title="成交金额" :note="formatPackageAmount(order.saleAmount)" />
|
||||||
@@ -95,7 +94,6 @@ const canCreateFulfillment = computed(() => userStore.hasPermissions(permissionC
|
|||||||
const canReadOrder = computed(() => userStore.hasPermissions(permissionCodes.salesOrder.read));
|
const canReadOrder = computed(() => userStore.hasPermissions(permissionCodes.salesOrder.read));
|
||||||
const canReadFund = computed(() => userStore.hasPermissions(permissionCodes.fundRecord.read));
|
const canReadFund = computed(() => userStore.hasPermissions(permissionCodes.fundRecord.read));
|
||||||
const canReadPackageConfig = computed(() => userStore.hasPermissions(permissionCodes.packageConfig.read));
|
const canReadPackageConfig = computed(() => userStore.hasPermissions(permissionCodes.packageConfig.read));
|
||||||
const canReadEventRecord = computed(() => userStore.hasPermissions(permissionCodes.eventRecord.read));
|
|
||||||
const hasAvailableItem = computed(() => customerPackage.value?.itemList?.some(item => 0 < Number(item.availableQuantity)) ?? false);
|
const hasAvailableItem = computed(() => customerPackage.value?.itemList?.some(item => 0 < Number(item.availableQuantity)) ?? false);
|
||||||
|
|
||||||
async function loadData(): Promise<void> {
|
async function loadData(): Promise<void> {
|
||||||
@@ -117,9 +115,7 @@ async function loadData(): Promise<void> {
|
|||||||
function getStatusTheme(status?: string): "success" | "warning" | "danger" | "default" { return status === "ACTIVE" ? "success" : status === "CANCELLED" ? "danger" : "default"; }
|
function getStatusTheme(status?: string): "success" | "warning" | "danger" | "default" { return status === "ACTIVE" ? "success" : status === "CANCELLED" ? "danger" : "default"; }
|
||||||
function getFulfillmentSummary(item: OrderFulfillment): string { return `${item.deliveredAt ? Time.toDateTime(item.deliveredAt) : "时间未知"} · ${(item.itemList || []).length} 项`; }
|
function getFulfillmentSummary(item: OrderFulfillment): string { return `${item.deliveredAt ? Time.toDateTime(item.deliveredAt) : "时间未知"} · ${(item.itemList || []).length} 项`; }
|
||||||
async function toOrder(): Promise<void> { if (order.value?.id) await router.push(`/management/sales-orders/${order.value.id}`); }
|
async function toOrder(): Promise<void> { if (order.value?.id) await router.push(`/management/sales-orders/${order.value.id}`); }
|
||||||
async function toPackageConfig(): Promise<void> { if (customerPackage.value?.packageConfigId) await router.push(`/management/package-configs/${customerPackage.value.packageConfigId}`); }
|
async function toPackageConfig(): Promise<void> { if (customerPackage.value?.packageConfigId) await router.push(`/management/package-configs/${customerPackage.value.packageConfigId}`); }async function toFundRecords(): Promise<void> { if (order.value?.id) await router.push({ path: "/management/fund-records", query: { salesOrderId: order.value.id } }); }
|
||||||
async function toEventRecord(): Promise<void> { if (customerPackage.value?.purchaseEventRecord?.id) await router.push(`/management/registration-event-records/${customerPackage.value.purchaseEventRecord.id}`); }
|
|
||||||
async function toFundRecords(): Promise<void> { if (order.value?.id) await router.push({ path: "/management/fund-records", query: { salesOrderId: order.value.id } }); }
|
|
||||||
async function toFulfillment(id?: string): Promise<void> { if (id) await router.push(`/management/order-fulfillments/${id}`); }
|
async function toFulfillment(id?: string): Promise<void> { if (id) await router.push(`/management/order-fulfillments/${id}`); }
|
||||||
async function toCreateFulfillment(): Promise<void> {
|
async function toCreateFulfillment(): Promise<void> {
|
||||||
if (order.value?.id && customerPackage.value?.id) {
|
if (order.value?.id && customerPackage.value?.id) {
|
||||||
|
|||||||
@@ -22,6 +22,13 @@
|
|||||||
/>
|
/>
|
||||||
<div v-if="!config.itemList?.length" class="empty" v-text="'暂无产品权益'" />
|
<div v-if="!config.itemList?.length" class="empty" v-text="'暂无产品权益'" />
|
||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
|
<t-button
|
||||||
|
v-if="canPurchase"
|
||||||
|
class="cell-button"
|
||||||
|
theme="primary"
|
||||||
|
content="购买套餐"
|
||||||
|
@click="toPurchase"
|
||||||
|
/>
|
||||||
<t-button
|
<t-button
|
||||||
v-if="canDelete"
|
v-if="canDelete"
|
||||||
class="cell-button"
|
class="cell-button"
|
||||||
@@ -58,6 +65,11 @@ const config = ref<GaoPackageConfig>();
|
|||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
const canUpdate = computed(() => !!config.value && userStore.hasPermissions(permissionCodes.packageConfig.update));
|
const canUpdate = computed(() => !!config.value && userStore.hasPermissions(permissionCodes.packageConfig.update));
|
||||||
const canDelete = computed(() => !!config.value && userStore.hasPermissions(permissionCodes.packageConfig.delete));
|
const canDelete = computed(() => !!config.value && userStore.hasPermissions(permissionCodes.packageConfig.delete));
|
||||||
|
const canPurchase = computed(() =>
|
||||||
|
!!config.value
|
||||||
|
&& config.value.status === "ACTIVE"
|
||||||
|
&& userStore.hasPermissions(permissionCodes.customerPackage.create)
|
||||||
|
);
|
||||||
|
|
||||||
function getStatusTheme(status?: PackageConfigStatus): "success" | "warning" | "default" {
|
function getStatusTheme(status?: PackageConfigStatus): "success" | "warning" | "default" {
|
||||||
return status === "ACTIVE" ? "success" : status === "INACTIVE" ? "warning" : "default";
|
return status === "ACTIVE" ? "success" : status === "INACTIVE" ? "warning" : "default";
|
||||||
@@ -89,6 +101,12 @@ async function toEdit(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function toPurchase(): Promise<void> {
|
||||||
|
if (config.value?.id && canPurchase.value) {
|
||||||
|
await router.push(`/management/package-configs/${config.value.id}/purchase`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function removeConfig(): void {
|
function removeConfig(): void {
|
||||||
if (!config.value?.id || !canDelete.value) {
|
if (!config.value?.id || !canDelete.value) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -0,0 +1,439 @@
|
|||||||
|
<template>
|
||||||
|
<div v-can="permissionCodes.customerPackage.create" class="package-purchase">
|
||||||
|
<t-form class="form" label-align="left" v-entrance-transition>
|
||||||
|
<t-form-item label="客户" required-mark>
|
||||||
|
<t-input
|
||||||
|
:model-value="customerDisplayText"
|
||||||
|
class="customer"
|
||||||
|
readonly
|
||||||
|
borderless
|
||||||
|
placeholder="选择或扫码客户"
|
||||||
|
@click="openSelectedCustomerList()"
|
||||||
|
>
|
||||||
|
<template #suffix-icon>
|
||||||
|
<div class="actions">
|
||||||
|
<t-button
|
||||||
|
theme="primary"
|
||||||
|
variant="outline"
|
||||||
|
size="extra-small"
|
||||||
|
@click.stop="selectorVisible = true"
|
||||||
|
>选择</t-button>
|
||||||
|
<t-button
|
||||||
|
theme="primary"
|
||||||
|
size="extra-small"
|
||||||
|
@click.stop="toggleScanner"
|
||||||
|
>
|
||||||
|
<span v-text="scannerVisible ? '关闭扫码' : '扫码'" />
|
||||||
|
</t-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</t-input>
|
||||||
|
</t-form-item>
|
||||||
|
<div v-if="scannerVisible" class="code-scanner-wrapper">
|
||||||
|
<code-scanner
|
||||||
|
visible
|
||||||
|
:status-text="scanStatus"
|
||||||
|
active-text="正在识别客户编码,请将条码或二维码移入取景框。"
|
||||||
|
@decode="handleDecodedText"
|
||||||
|
@status-change="handleScanStatusChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<customer-profile-card
|
||||||
|
v-if="selectedCustomer"
|
||||||
|
class="customer-profile"
|
||||||
|
:customer="selectedCustomer"
|
||||||
|
v-entrance-transition
|
||||||
|
/>
|
||||||
|
<div v-if="config && !isSaleable" class="saleable-tip red">套餐当前不可购买,请检查套餐状态和有效期</div>
|
||||||
|
<package-purchase-form
|
||||||
|
:package-config="config"
|
||||||
|
:can-price-override="canPriceOverride"
|
||||||
|
v-model:item-list="packagePurchaseItemList"
|
||||||
|
v-model:sale-amount="saleAmountText"
|
||||||
|
/>
|
||||||
|
<t-form-item label="购买时间" content-align="right">
|
||||||
|
<div class="time-actions">
|
||||||
|
<t-button
|
||||||
|
theme="primary"
|
||||||
|
variant="outline"
|
||||||
|
size="extra-small"
|
||||||
|
:content="purchasedAtText || '此刻'"
|
||||||
|
@click="openPurchasedAtPicker"
|
||||||
|
/>
|
||||||
|
<t-button
|
||||||
|
v-if="purchasedAtText"
|
||||||
|
theme="primary"
|
||||||
|
variant="text"
|
||||||
|
size="extra-small"
|
||||||
|
content="此刻"
|
||||||
|
@click="resetPurchasedAt"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="购买备注">
|
||||||
|
<t-textarea
|
||||||
|
v-model.trim="note"
|
||||||
|
class="textarea"
|
||||||
|
:autosize="{ minRows: 3, maxRows: 5 }"
|
||||||
|
placeholder="购买说明"
|
||||||
|
/>
|
||||||
|
</t-form-item>
|
||||||
|
</t-form>
|
||||||
|
<customer-selector
|
||||||
|
v-model="selectedCustomerId"
|
||||||
|
v-model:visible="selectorVisible"
|
||||||
|
selection-mode="single"
|
||||||
|
@change="handleCustomerChange"
|
||||||
|
/>
|
||||||
|
<t-popup v-model="selectedCustomerListVisible" placement="bottom">
|
||||||
|
<div class="selected-customer-popup bg-white">
|
||||||
|
<div class="popup-header">
|
||||||
|
<t-button variant="text" size="small" @click="selectedCustomerListVisible = false">关闭</t-button>
|
||||||
|
<h3>已选客户</h3>
|
||||||
|
<t-button theme="primary" variant="text" size="small" @click="openCustomerSelector">选择</t-button>
|
||||||
|
</div>
|
||||||
|
<customer-list
|
||||||
|
class="list"
|
||||||
|
:customer-list="selectedCustomerList"
|
||||||
|
removable
|
||||||
|
empty-text="暂未选择客户"
|
||||||
|
@remove="removeSelectedCustomer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</t-popup>
|
||||||
|
</div>
|
||||||
|
<t-popup v-model="purchasedAtPickerVisible" placement="bottom">
|
||||||
|
<t-date-time-picker
|
||||||
|
v-if="purchasedAtPickerVisible"
|
||||||
|
v-model="purchasedAtDraftText"
|
||||||
|
:start="minimumDateTimeText"
|
||||||
|
:mode="['date', 'minute']"
|
||||||
|
format="YYYY-MM-DD HH:mm"
|
||||||
|
title="购买时间"
|
||||||
|
@cancel="purchasedAtPickerVisible = false"
|
||||||
|
@confirm="confirmPurchasedAt"
|
||||||
|
/>
|
||||||
|
</t-popup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Time } from "timi-web";
|
||||||
|
import { useUserStore } from "timi-web-api";
|
||||||
|
import CustomerAPI from "@/api/CustomerAPI";
|
||||||
|
import PackageAPI from "@/api/PackageAPI";
|
||||||
|
import CodeScanner from "@/components/CodeScanner.vue";
|
||||||
|
import CustomerList from "@/components/CustomerList.vue";
|
||||||
|
import CustomerProfileCard from "@/components/CustomerProfileCard.vue";
|
||||||
|
import CustomerSelector from "@/components/CustomerSelector.vue";
|
||||||
|
import PackagePurchaseForm from "@/components/PackagePurchaseForm.vue";
|
||||||
|
import { usePackagePurchase } from "@/pages/package/usePackagePurchase";
|
||||||
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
|
import { useSettingStore } from "@/store/settingStore";
|
||||||
|
import type { Customer } from "@/types/Customer";
|
||||||
|
import type { GaoPackageConfig } from "@/types/Package";
|
||||||
|
import { getErrorMessage } from "@/utils/error";
|
||||||
|
import { Message } from "@/utils/message";
|
||||||
|
import { permissionCodes } from "@/utils/permission";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "PackagePurchase"
|
||||||
|
});
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const navBarStore = useNavBarStore();
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
// ---------- 套餐配置 ----------
|
||||||
|
|
||||||
|
const configId = computed(() => String(route.params.id || ""));
|
||||||
|
const config = ref<GaoPackageConfig>();
|
||||||
|
|
||||||
|
// ---------- 客户 ----------
|
||||||
|
|
||||||
|
const customerCode = ref("");
|
||||||
|
const selectorVisible = ref(false);
|
||||||
|
const selectedCustomerListVisible = ref(false);
|
||||||
|
const selectedCustomerId = ref("");
|
||||||
|
const selectedCustomerList = ref<Customer[]>([]);
|
||||||
|
const selectedCustomer = computed(() => selectedCustomerList.value[0]);
|
||||||
|
const customerDisplayText = computed(() => selectedCustomer.value?.name || selectedCustomer.value?.code || customerCode.value);
|
||||||
|
|
||||||
|
async function handleCustomerChange(value: string | string[]): Promise<void> {
|
||||||
|
selectedCustomerId.value = Array.isArray(value) ? value[0] || "" : value;
|
||||||
|
resetRequestNo();
|
||||||
|
await loadSelectedCustomer();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSelectedCustomer(): Promise<void> {
|
||||||
|
if (!selectedCustomerId.value) {
|
||||||
|
selectedCustomerList.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const customer = await CustomerAPI.detail(selectedCustomerId.value);
|
||||||
|
selectedCustomerList.value = customer?.id ? [customer] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSelectedCustomerList(): void {
|
||||||
|
selectedCustomerListVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openCustomerSelector(): Promise<void> {
|
||||||
|
selectedCustomerListVisible.value = false;
|
||||||
|
await nextTick();
|
||||||
|
selectorVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeSelectedCustomer(): Promise<void> {
|
||||||
|
selectedCustomerId.value = "";
|
||||||
|
selectedCustomerList.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 扫码 ----------
|
||||||
|
|
||||||
|
const scannerVisible = ref(false);
|
||||||
|
const scanStatus = ref("点击扫码开始识别。");
|
||||||
|
let latestDecodedText = "";
|
||||||
|
|
||||||
|
async function toggleScanner(): Promise<void> {
|
||||||
|
scannerVisible.value = !scannerVisible.value;
|
||||||
|
if (scannerVisible.value) {
|
||||||
|
latestDecodedText = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDecodedText(rawText: string): Promise<void> {
|
||||||
|
const normalizedText = rawText.trim();
|
||||||
|
if (!normalizedText || normalizedText === latestDecodedText) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
latestDecodedText = normalizedText;
|
||||||
|
await settingStore.playScanSuccessSound();
|
||||||
|
try {
|
||||||
|
const customer = await CustomerAPI.findByCode(normalizedText);
|
||||||
|
if (!customer?.id) {
|
||||||
|
scanStatus.value = `客户 ${normalizedText} 不存在,请先建档。`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedCustomerId.value = customer.id;
|
||||||
|
await loadSelectedCustomer();
|
||||||
|
resetRequestNo();
|
||||||
|
scanStatus.value = `已识别 ${customer.code || normalizedText},已选中 ${customer.name || "未命名客户"}。`;
|
||||||
|
scannerVisible.value = false;
|
||||||
|
} catch {
|
||||||
|
scanStatus.value = "客户查询失败,请重试。";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleScanStatusChange(value: string): void {
|
||||||
|
scanStatus.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 购买时间 ----------
|
||||||
|
|
||||||
|
const minimumDateTimeText = "1970-01-01 00:00";
|
||||||
|
const purchasedAtText = ref("");
|
||||||
|
const purchasedAtDraftText = ref("");
|
||||||
|
const purchasedAtPickerVisible = ref(false);
|
||||||
|
|
||||||
|
function openPurchasedAtPicker(): void {
|
||||||
|
purchasedAtDraftText.value = purchasedAtText.value || Time.formatDateTime(new Date());
|
||||||
|
purchasedAtPickerVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmPurchasedAt(value?: string | number): void {
|
||||||
|
purchasedAtText.value = String(value || purchasedAtDraftText.value);
|
||||||
|
purchasedAtPickerVisible.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetPurchasedAt(): void {
|
||||||
|
purchasedAtText.value = "";
|
||||||
|
purchasedAtDraftText.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPurchasedAt(): number {
|
||||||
|
return Time.toTimestamp(parseDateTime(purchasedAtText.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDateTime(value: string): Date {
|
||||||
|
if (!value) {
|
||||||
|
return new Date();
|
||||||
|
}
|
||||||
|
const [dateText, timeText = "00:00"] = value.split(" ");
|
||||||
|
const [year, month, day] = dateText.split("-").map(Number);
|
||||||
|
const [hour, minute] = timeText.split(":").map(Number);
|
||||||
|
return new Date(year, month - 1, day, hour || 0, minute || 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 购买 ----------
|
||||||
|
|
||||||
|
const note = ref("");
|
||||||
|
const isSaving = ref(false);
|
||||||
|
const {
|
||||||
|
isSaleable,
|
||||||
|
canPriceOverride,
|
||||||
|
saleAmountText,
|
||||||
|
packagePurchaseItemList,
|
||||||
|
applyConfig,
|
||||||
|
resetRequestNo,
|
||||||
|
submitPurchase,
|
||||||
|
validatePurchaseForm
|
||||||
|
} = usePackagePurchase({
|
||||||
|
config,
|
||||||
|
selectedCustomerList,
|
||||||
|
note,
|
||||||
|
hasPermission: permissionCode => userStore.hasPermissions(permissionCode),
|
||||||
|
buildRegisteredAt: buildPurchasedAt
|
||||||
|
});
|
||||||
|
|
||||||
|
async function submitRecord(): Promise<void> {
|
||||||
|
if (isSaving.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!validatePurchaseForm()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSaving.value = true;
|
||||||
|
try {
|
||||||
|
const result = await submitPurchase();
|
||||||
|
if (!result?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await settingStore.playRegisterSuccessSound();
|
||||||
|
Message.success("套餐购买已提交");
|
||||||
|
await router.replace(`/management/customer-packages/${result.id}`);
|
||||||
|
} catch (error) {
|
||||||
|
Message.error(getErrorMessage(error, "套餐购买提交失败"));
|
||||||
|
} finally {
|
||||||
|
isSaving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 页面数据 ----------
|
||||||
|
|
||||||
|
async function loadPageData(): Promise<void> {
|
||||||
|
if (!configId.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
config.value = await PackageAPI.configDetail(configId.value);
|
||||||
|
applyConfig(config.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
navBarStore.clearRight();
|
||||||
|
if (userStore.hasPermissions(permissionCodes.customerPackage.create)) {
|
||||||
|
navBarStore.setRightText("提交", "var(--td-brand-color)", submitRecord);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await loadPageData();
|
||||||
|
} catch (error) {
|
||||||
|
Message.error(getErrorMessage(error, "套餐配置加载失败"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => navBarStore.clearRight());
|
||||||
|
|
||||||
|
watch(config, value => {
|
||||||
|
if (value) {
|
||||||
|
applyConfig(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.package-purchase {
|
||||||
|
gap: 1rem;
|
||||||
|
display: flex;
|
||||||
|
padding: calc(var(--app-nav-height) + 1rem) 0 1rem 0;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.form {
|
||||||
|
padding: .5rem 0;
|
||||||
|
background: rgba(255, 255, 255, .92);
|
||||||
|
box-shadow: var(--td-shadow-1);
|
||||||
|
border-radius: 1.4rem;
|
||||||
|
|
||||||
|
.customer .actions {
|
||||||
|
gap: .5rem;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-scanner-wrapper,
|
||||||
|
.photo-part {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-profile {
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saleable-tip {
|
||||||
|
padding: 0 1rem .5rem;
|
||||||
|
font-size: .8125rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-actions {
|
||||||
|
gap: .5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-customer-popup {
|
||||||
|
height: 70vh;
|
||||||
|
overflow: auto;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-radius: 1.25rem 1.25rem 0 0;
|
||||||
|
|
||||||
|
.popup-header {
|
||||||
|
padding: 0 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html.theme-dark) {
|
||||||
|
.package-purchase {
|
||||||
|
:deep(.form-item .t-form__label) {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
background: rgba(15, 23, 42, .88);
|
||||||
|
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.textarea .t-textarea__inner) {
|
||||||
|
color: #e2e8f0;
|
||||||
|
background: rgba(15, 23, 42, .72);
|
||||||
|
border-color: rgba(148, 163, 184, .16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.saleable-tip {
|
||||||
|
color: #fbbf24;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+36
-75
@@ -1,69 +1,44 @@
|
|||||||
import { computed, ref, type Ref } from "vue";
|
import { computed, ref, type Ref } from "vue";
|
||||||
import PackageAPI from "@/api/PackageAPI";
|
import PackageAPI from "@/api/PackageAPI";
|
||||||
import type { Customer } from "@/types/Customer";
|
import type { Customer } from "@/types/Customer";
|
||||||
import type { EventItem } from "@/types/EventItem";
|
import type { GaoCustomerPackage, GaoPackageConfig, PackagePurchaseItem } from "@/types/Package";
|
||||||
import type { GaoCustomerPackage, PackagePurchaseItem } from "@/types/Package";
|
|
||||||
import { isPackagePurchaseEvent } from "@/utils/event";
|
|
||||||
import { createPackageRequestNo, formatPackageAmountInput, normalizePackageQuantity, parsePackageAmount } from "@/utils/package";
|
import { createPackageRequestNo, formatPackageAmountInput, normalizePackageQuantity, parsePackageAmount } from "@/utils/package";
|
||||||
import { permissionCodes } from "@/utils/permission";
|
import { permissionCodes } from "@/utils/permission";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
|
|
||||||
interface UsePackagePurchaseRecordOptions {
|
interface UsePackagePurchaseOptions {
|
||||||
eventList: Ref<EventItem[]>;
|
config: Ref<GaoPackageConfig | undefined>;
|
||||||
selectedEventIdList: Ref<string[]>;
|
|
||||||
selectedCustomerIdList: Ref<string[]>;
|
|
||||||
selectedCustomerList: Ref<Customer[]>;
|
selectedCustomerList: Ref<Customer[]>;
|
||||||
note: Ref<string>;
|
note: Ref<string>;
|
||||||
hasPermission: (permissionCode: string) => boolean;
|
hasPermission: (permissionCode: string) => boolean;
|
||||||
buildRegisteredAt: () => number;
|
buildRegisteredAt: () => number;
|
||||||
buildTempFileIdList: () => string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePackagePurchaseRecord(options: UsePackagePurchaseRecordOptions) {
|
/** 套餐配置直接购买流程,购买请求只依赖套餐配置 ID,不再关联购买事件。 */
|
||||||
const selectedPackageEvent = computed(() => options.selectedEventIdList.value
|
export function usePackagePurchase(options: UsePackagePurchaseOptions) {
|
||||||
.map(eventId => options.eventList.value.find(item => item.id === eventId))
|
const canPriceOverride = computed(() => options.hasPermission(permissionCodes.customerPackage.priceOverride));
|
||||||
.find((item): item is EventItem => !!item && isPackagePurchaseEvent(item)));
|
const saleAmountText = ref("");
|
||||||
const isPackagePurchaseSelected = computed(() => !!selectedPackageEvent.value);
|
const packagePurchaseItemList = ref<PackagePurchaseItem[]>([]);
|
||||||
const selectedPackageConfig = computed(() => selectedPackageEvent.value?.packageConfig);
|
const packageRequestNo = ref("");
|
||||||
const isSelectedPackageConfigSaleable = computed(() => {
|
|
||||||
const config = selectedPackageConfig.value;
|
/** 套餐配置当前是否处于可购买状态。 */
|
||||||
|
function isConfigSaleable(config?: GaoPackageConfig): boolean {
|
||||||
if (!config || config.status !== "ACTIVE") {
|
if (!config || config.status !== "ACTIVE") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
return (config.beginAt === undefined || config.beginAt === null || config.beginAt <= now)
|
return (config.beginAt === undefined || config.beginAt === null || config.beginAt <= now)
|
||||||
&& (config.endAt === undefined || config.endAt === null || now <= config.endAt);
|
&& (config.endAt === undefined || config.endAt === null || now <= config.endAt);
|
||||||
});
|
|
||||||
const canPriceOverride = computed(() => options.hasPermission(permissionCodes.customerPackage.priceOverride));
|
|
||||||
const saleAmountText = ref("");
|
|
||||||
const packagePurchaseItemList = ref<PackagePurchaseItem[]>([]);
|
|
||||||
const packageRequestNo = ref("");
|
|
||||||
|
|
||||||
function handleEventSelectionChange(eventIdList: string[]): void {
|
|
||||||
const packageEvent = eventIdList
|
|
||||||
.map(eventId => options.eventList.value.find(item => item.id === eventId))
|
|
||||||
.find(item => isPackagePurchaseEvent(item));
|
|
||||||
if (packageEvent?.id) {
|
|
||||||
if (!options.hasPermission(permissionCodes.customerPackage.create)) {
|
|
||||||
Message.warning("当前账号没有套餐购买权限");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
options.selectedEventIdList.value = [packageEvent.id];
|
|
||||||
keepSingleCustomer();
|
|
||||||
applySelectedPackageEvent(packageEvent);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
options.selectedEventIdList.value = eventIdList;
|
|
||||||
resetPackagePurchaseForm();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function applySelectedPackageEvent(event: EventItem): void {
|
const isSaleable = computed(() => isConfigSaleable(options.config.value));
|
||||||
const config = event.packageConfig;
|
|
||||||
|
/** 按套餐配置初始化成交价与权益明细。 */
|
||||||
|
function applyConfig(config?: GaoPackageConfig): void {
|
||||||
if (!config) {
|
if (!config) {
|
||||||
resetPackagePurchaseForm();
|
resetPurchaseForm();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
saleAmountText.value = formatPackageAmountInput(config.standardAmount);
|
saleAmountText.value = formatPackageAmountInput(config.standardAmount);
|
||||||
packagePurchaseItemList.value = (config.itemList || []).map((item, index) => ({
|
packagePurchaseItemList.value = (config.itemList || []).map((item, index) => ({
|
||||||
productId: item.productId,
|
productId: item.productId,
|
||||||
@@ -74,30 +49,30 @@ export function usePackagePurchaseRecord(options: UsePackagePurchaseRecordOption
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetPackagePurchaseForm(): void {
|
function resetPurchaseForm(): void {
|
||||||
saleAmountText.value = "";
|
saleAmountText.value = "";
|
||||||
packagePurchaseItemList.value = [];
|
packagePurchaseItemList.value = [];
|
||||||
packageRequestNo.value = "";
|
packageRequestNo.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetPackageRequestNo(): void {
|
function resetRequestNo(): void {
|
||||||
packageRequestNo.value = "";
|
packageRequestNo.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitPackagePurchase(): Promise<GaoCustomerPackage | undefined> {
|
async function submitPurchase(): Promise<GaoCustomerPackage | undefined> {
|
||||||
const eventId = selectedPackageEvent.value?.id;
|
const config = options.config.value;
|
||||||
const customerId = options.selectedCustomerList.value[0]?.id;
|
const customerId = options.selectedCustomerList.value[0]?.id;
|
||||||
const saleAmount = parsePackageAmount(saleAmountText.value);
|
const saleAmount = parsePackageAmount(saleAmountText.value);
|
||||||
if (!eventId || !customerId || saleAmount === undefined) {
|
if (!config?.id || !customerId || saleAmount === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!packageRequestNo.value) {
|
if (!packageRequestNo.value) {
|
||||||
packageRequestNo.value = createPackageRequestNo();
|
packageRequestNo.value = createPackageRequestNo();
|
||||||
}
|
}
|
||||||
const result = await PackageAPI.purchase({
|
return await PackageAPI.purchase({
|
||||||
requestNo: packageRequestNo.value,
|
requestNo: packageRequestNo.value,
|
||||||
eventId,
|
packageConfigId: config.id,
|
||||||
customerId,
|
customerId,
|
||||||
saleAmount,
|
saleAmount,
|
||||||
purchasedAt: options.buildRegisteredAt(),
|
purchasedAt: options.buildRegisteredAt(),
|
||||||
@@ -108,30 +83,25 @@ export function usePackagePurchaseRecord(options: UsePackagePurchaseRecordOption
|
|||||||
sort: item.sort ?? index,
|
sort: item.sort ?? index,
|
||||||
remark: item.remark ?? null
|
remark: item.remark ?? null
|
||||||
})),
|
})),
|
||||||
attachmentList: options.buildTempFileIdList().map(tempFileId => ({
|
|
||||||
tempFileId,
|
|
||||||
attachType: "PHOTO"
|
|
||||||
})),
|
|
||||||
remark: options.note.value.trim() || null
|
remark: options.note.value.trim() || null
|
||||||
});
|
});
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function validatePackagePurchaseForm(): boolean {
|
function validatePurchaseForm(): boolean {
|
||||||
if (!options.hasPermission(permissionCodes.customerPackage.create)) {
|
if (!options.hasPermission(permissionCodes.customerPackage.create)) {
|
||||||
Message.warning("当前账号没有套餐购买权限");
|
Message.warning("当前账号没有套餐购买权限");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (options.selectedCustomerList.value.length < 1 || 1 < options.selectedCustomerList.value.length || !options.selectedCustomerList.value[0]?.id) {
|
if (options.selectedCustomerList.value.length !== 1 || !options.selectedCustomerList.value[0]?.id) {
|
||||||
Message.warning("套餐购买只能选择一个客户");
|
Message.warning("套餐购买只能选择一个客户");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!selectedPackageConfig.value) {
|
if (!options.config.value?.id) {
|
||||||
Message.warning("当前购买事件未绑定有效套餐配置");
|
Message.warning("套餐配置不存在,无法购买");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!isSelectedPackageConfigSaleable.value) {
|
if (!isSaleable.value) {
|
||||||
Message.warning("事件绑定的套餐当前不可购买,请检查套餐状态和有效期");
|
Message.warning("套餐当前不可购买,请检查套餐状态和有效期");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (parsePackageAmount(saleAmountText.value) === undefined) {
|
if (parsePackageAmount(saleAmountText.value) === undefined) {
|
||||||
@@ -152,24 +122,15 @@ export function usePackagePurchaseRecord(options: UsePackagePurchaseRecordOption
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function keepSingleCustomer(): void {
|
|
||||||
const customer = options.selectedCustomerList.value[0];
|
|
||||||
options.selectedCustomerList.value = customer ? [customer] : [];
|
|
||||||
options.selectedCustomerIdList.value = customer?.id ? [customer.id] : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selectedPackageEvent,
|
isSaleable,
|
||||||
isPackagePurchaseSelected,
|
|
||||||
selectedPackageConfig,
|
|
||||||
canPriceOverride,
|
canPriceOverride,
|
||||||
saleAmountText,
|
saleAmountText,
|
||||||
packagePurchaseItemList,
|
packagePurchaseItemList,
|
||||||
handleEventSelectionChange,
|
applyConfig,
|
||||||
applySelectedPackageEvent,
|
resetPurchaseForm,
|
||||||
resetPackagePurchaseForm,
|
resetRequestNo,
|
||||||
resetPackageRequestNo,
|
submitPurchase,
|
||||||
submitPackagePurchase,
|
validatePurchaseForm
|
||||||
validatePackagePurchaseForm
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div v-can="pageAccess" class="permission-edit">
|
|
||||||
<t-form class="form" label-align="left">
|
|
||||||
<t-form-item label="名称">
|
|
||||||
<t-input v-model.trim="formModel.name" placeholder="权限名称" borderless />
|
|
||||||
</t-form-item>
|
|
||||||
<t-form-item label="代码">
|
|
||||||
<t-input v-model.trim="formModel.code" placeholder="如 customer:read" borderless />
|
|
||||||
</t-form-item>
|
|
||||||
<t-form-item label="模块">
|
|
||||||
<t-input v-model.trim="formModel.moduleCode" placeholder="模块代码" borderless />
|
|
||||||
</t-form-item>
|
|
||||||
<t-form-item label="说明">
|
|
||||||
<t-textarea v-model.trim="formModel.description" class="textarea" placeholder="权限说明" autosize borderless />
|
|
||||||
</t-form-item>
|
|
||||||
</t-form>
|
|
||||||
<t-button v-can="savePermissionCode" class="save" theme="primary" @click="savePermission">保存</t-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { Message } from "@/utils/message";
|
|
||||||
import { moduleCode, usePermissionSettingsStore } from "@/store/permissionSettingsStore";
|
|
||||||
import { corePermissionCodes } from "@/utils/permission";
|
|
||||||
|
|
||||||
defineOptions({
|
|
||||||
name: "PermissionEdit"
|
|
||||||
});
|
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
const router = useRouter();
|
|
||||||
const permissionStore = usePermissionSettingsStore();
|
|
||||||
|
|
||||||
// ---------- 路由权限 ----------
|
|
||||||
|
|
||||||
const routeId = computed(() => String(route.params.id || ""));
|
|
||||||
const isCreateMode = computed(() => !routeId.value);
|
|
||||||
const savePermissionCode = computed(() => isCreateMode.value ? corePermissionCodes.permission.create : corePermissionCodes.permission.update);
|
|
||||||
const pageAccess = computed(() => ({
|
|
||||||
value: [corePermissionCodes.role.read, corePermissionCodes.permission.read, savePermissionCode.value],
|
|
||||||
mode: "and" as const
|
|
||||||
}));
|
|
||||||
|
|
||||||
// ---------- 表单 ----------
|
|
||||||
|
|
||||||
const formModel = reactive({
|
|
||||||
id: "",
|
|
||||||
moduleCode: moduleCode,
|
|
||||||
code: "",
|
|
||||||
name: "",
|
|
||||||
description: ""
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---------- 页面数据 ----------
|
|
||||||
|
|
||||||
async function fillForm(): Promise<void> {
|
|
||||||
await permissionStore.loadAll();
|
|
||||||
const current = await permissionStore.loadPermissionDetail(routeId.value);
|
|
||||||
Object.assign(formModel, {
|
|
||||||
id: current.id,
|
|
||||||
moduleCode: current.moduleCode || moduleCode,
|
|
||||||
code: current.code,
|
|
||||||
name: current.name,
|
|
||||||
description: current.description
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- 保存 ----------
|
|
||||||
|
|
||||||
async function savePermission(): Promise<void> {
|
|
||||||
const permission = await permissionStore.upsertPermission({
|
|
||||||
id: formModel.id,
|
|
||||||
moduleCode: moduleCode,
|
|
||||||
code: formModel.code,
|
|
||||||
name: formModel.name,
|
|
||||||
description: formModel.description
|
|
||||||
}, isCreateMode.value);
|
|
||||||
Message.success("权限已保存");
|
|
||||||
await router.push(isCreateMode.value ? "/management/permissions/items" : `/management/permissions/items/${permission?.id || formModel.id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await fillForm();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.permission-edit {
|
|
||||||
padding: calc(var(--app-nav-height) + 1rem) 0 5rem 0;
|
|
||||||
|
|
||||||
.form {
|
|
||||||
border-radius: .75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.textarea {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.save {
|
|
||||||
left: 1rem;
|
|
||||||
right: 1rem;
|
|
||||||
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
|
|
||||||
position: fixed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
@@ -67,7 +67,10 @@
|
|||||||
@click="canAssignRole(item) && toAssign(item.id)"
|
@click="canAssignRole(item) && toAssign(item.id)"
|
||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="title" v-text="item.name" :style="{ paddingLeft: `${item.level * 1.25}rem` }" />
|
<div class="title" :style="{ paddingLeft: `${item.level * 1.25}rem` }">
|
||||||
|
<span v-text="item.name" />
|
||||||
|
<span class="scope" v-text="getRoleScopeLabel(item)" />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</t-cell>
|
</t-cell>
|
||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
@@ -88,6 +91,8 @@ const router = useRouter();
|
|||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const permissionStore = usePermissionSettingsStore();
|
const permissionStore = usePermissionSettingsStore();
|
||||||
const authorizationRoleList = ref<Role[]>([]);
|
const authorizationRoleList = ref<Role[]>([]);
|
||||||
|
const directRoleIdSet = ref(new Set<string>());
|
||||||
|
const manageableRoleIdSet = ref(new Set<string>());
|
||||||
const isSystemAdmin = computed(() => userStore.hasRoles(coreRoleCodes.admin));
|
const isSystemAdmin = computed(() => userStore.hasRoles(coreRoleCodes.admin));
|
||||||
const roleCount = computed(() => authorizationRoleList.value.length);
|
const roleCount = computed(() => authorizationRoleList.value.length);
|
||||||
const permissionCount = computed(() => permissionStore.permissionList.length);
|
const permissionCount = computed(() => permissionStore.permissionList.length);
|
||||||
@@ -141,6 +146,15 @@ function canAssignRole(role: Pick<Role, "protectedRole">): boolean {
|
|||||||
return isSystemAdmin.value || role.protectedRole !== true;
|
return isSystemAdmin.value || role.protectedRole !== true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRoleScopeLabel(role: Pick<Role, "id">): string {
|
||||||
|
const direct = directRoleIdSet.value.has(role.id);
|
||||||
|
const manageable = manageableRoleIdSet.value.has(role.id);
|
||||||
|
if (direct && manageable) {
|
||||||
|
return "直属且可管理";
|
||||||
|
}
|
||||||
|
return direct ? "直属角色" : "可管理角色";
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- 页面数据 ----------
|
// ---------- 页面数据 ----------
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -151,8 +165,17 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await permissionStore.loadAll();
|
await permissionStore.loadAll();
|
||||||
const roleList = await permissionStore.loadManageableRoles();
|
const [manageableRoleList, currentRoleList] = await Promise.all([
|
||||||
authorizationRoleList.value = roleList;
|
permissionStore.loadManageableRoles(),
|
||||||
|
permissionStore.loadCurrentRoleDetails()
|
||||||
|
]);
|
||||||
|
const roleMap = new Map<string, Role>();
|
||||||
|
for (const role of [...currentRoleList, ...manageableRoleList]) {
|
||||||
|
roleMap.set(role.id, role);
|
||||||
|
}
|
||||||
|
authorizationRoleList.value = Array.from(roleMap.values());
|
||||||
|
directRoleIdSet.value = new Set(currentRoleList.map(item => item.id).filter((id): id is string => Boolean(id)));
|
||||||
|
manageableRoleIdSet.value = new Set(manageableRoleList.map(item => item.id).filter((id): id is string => Boolean(id)));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -179,5 +202,16 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
gap: .5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.scope {
|
||||||
|
color: var(--td-text-color-placeholder);
|
||||||
|
font-size: .75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<t-input v-model.trim="formModel.name" placeholder="角色名称" borderless :disabled="!canEditRoleInfo" />
|
<t-input v-model.trim="formModel.name" placeholder="角色名称" borderless :disabled="!canEditRoleInfo" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="代码" :help="isBuiltinRole ? '内建角色代码不可修改' : ''">
|
<t-form-item label="代码" :help="isBuiltinRole ? '内建角色代码不可修改' : ''">
|
||||||
<t-input v-model.trim="formModel.code" placeholder="如 front-desk" borderless :disabled="!canEditRoleCode" />
|
<t-input v-model.trim="formModel.code" placeholder="如 ROLE_CODE" borderless :disabled="!canEditRoleCode" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="模块">
|
<t-form-item label="模块">
|
||||||
<t-input v-model.trim="formModel.moduleCode" placeholder="模块代码" borderless disabled />
|
<t-input v-model.trim="formModel.moduleCode" placeholder="模块代码" borderless disabled />
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
<div class="picker-text" v-text="selectedChildRoleName" />
|
<div class="picker-text" v-text="selectedChildRoleName" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
</t-form>
|
</t-form>
|
||||||
<t-cell-group v-entrance-transition="70" v-if="!isCreateMode" title="权限授权" theme="card">
|
<t-cell-group v-entrance-transition="70" v-if="!isCreateMode && canManageRole" title="权限授权" theme="card">
|
||||||
<t-cell
|
<t-cell
|
||||||
v-can="rolePermissionReadAccess"
|
v-can="rolePermissionReadAccess"
|
||||||
title="直接授权权限 P"
|
title="直接授权权限 P"
|
||||||
@@ -65,7 +65,6 @@
|
|||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
</card-container>
|
</card-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<t-popup v-model="parentRolePickerVisible" placement="bottom">
|
<t-popup v-model="parentRolePickerVisible" placement="bottom">
|
||||||
<t-picker
|
<t-picker
|
||||||
v-model="parentRolePickerDraft"
|
v-model="parentRolePickerDraft"
|
||||||
@@ -170,7 +169,12 @@ const isSystemAdmin = computed(() => userStore.hasRoles(coreRoleCodes.admin));
|
|||||||
const isBuiltinRole = computed(() => currentRole.value?.builtin === true);
|
const isBuiltinRole = computed(() => currentRole.value?.builtin === true);
|
||||||
const isProtectedRole = computed(() => !isSystemAdmin.value && currentRole.value?.protectedRole === true);
|
const isProtectedRole = computed(() => !isSystemAdmin.value && currentRole.value?.protectedRole === true);
|
||||||
const canManageRole = computed(() => isCreateMode.value || Boolean(currentRole.value && candidateRoleList.value.some(item => item.id === formModel.id)));
|
const canManageRole = computed(() => isCreateMode.value || Boolean(currentRole.value && candidateRoleList.value.some(item => item.id === formModel.id)));
|
||||||
const canEditRoleInfo = computed(() => userStore.hasPermissions(savePermissionCode.value) && (isCreateMode.value || (
|
const isCurrentUserDirectRole = computed(() => {
|
||||||
|
const current = currentRole.value;
|
||||||
|
return Boolean(current?.moduleCode && current.code && userStore.hasRoles(`${current.moduleCode}:${current.code}`));
|
||||||
|
});
|
||||||
|
const hasSavePermission = computed(() => userStore.hasPermissions(savePermissionCode.value));
|
||||||
|
const canEditRoleInfo = computed(() => hasSavePermission.value && (isCreateMode.value || (
|
||||||
canManageRole.value
|
canManageRole.value
|
||||||
&& !isProtectedRole.value
|
&& !isProtectedRole.value
|
||||||
)));
|
)));
|
||||||
@@ -181,11 +185,10 @@ const canUpdateRoleRelation = computed(() => userStore.hasPermissions([
|
|||||||
corePermissionCodes.rolePermissionInherit.delete
|
corePermissionCodes.rolePermissionInherit.delete
|
||||||
], "and"));
|
], "and"));
|
||||||
const canEditRoleRelation = computed(() => !isCreateMode.value && canUpdateRoleRelation.value && (
|
const canEditRoleRelation = computed(() => !isCreateMode.value && canUpdateRoleRelation.value && (
|
||||||
canManageRole.value
|
(canManageRole.value || isCurrentUserDirectRole.value)
|
||||||
&& !isProtectedRole.value
|
&& !isProtectedRole.value
|
||||||
));
|
));
|
||||||
const canSaveBaseInfo = computed(() => canEditRoleInfo.value);
|
const canSaveBaseInfo = computed(() => canEditRoleInfo.value);
|
||||||
const canShowSaveAction = computed(() => canSaveBaseInfo.value && userStore.hasPermissions(savePermissionCode.value));
|
|
||||||
type PermissionSource = "direct" | "inherited";
|
type PermissionSource = "direct" | "inherited";
|
||||||
type PermissionWithSource = Permission & {
|
type PermissionWithSource = Permission & {
|
||||||
source: PermissionSource;
|
source: PermissionSource;
|
||||||
@@ -308,14 +311,14 @@ function isManagementChildRole(roleId: string, parentRoleId: string, visitedRole
|
|||||||
// ---------- 页面跳转 ----------
|
// ---------- 页面跳转 ----------
|
||||||
|
|
||||||
async function toRolePermissionPage(): Promise<void> {
|
async function toRolePermissionPage(): Promise<void> {
|
||||||
if (!formModel.id) {
|
if (!formModel.id || !canManageRole.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await router.push(`/management/permissions/roles/${formModel.id}/assign/p`);
|
await router.push(`/management/permissions/roles/${formModel.id}/assign/p`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toRoleDelegationPage(): Promise<void> {
|
async function toRoleDelegationPage(): Promise<void> {
|
||||||
if (!formModel.id) {
|
if (!formModel.id || !canManageRole.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await router.push(`/management/permissions/roles/${formModel.id}/assign/d`);
|
await router.push(`/management/permissions/roles/${formModel.id}/assign/d`);
|
||||||
@@ -327,7 +330,7 @@ async function fillForm(): Promise<void> {
|
|||||||
await permissionStore.loadAll();
|
await permissionStore.loadAll();
|
||||||
await loadAuthorizationScope();
|
await loadAuthorizationScope();
|
||||||
if (isCreateMode.value) {
|
if (isCreateMode.value) {
|
||||||
parentRoleOptions.value = candidateRoleList.value;
|
parentRoleOptions.value = await permissionStore.loadCreateParentGrantableRoles();
|
||||||
formModel.parentRoleId = parentRoleOptions.value[0]?.id || "";
|
formModel.parentRoleId = parentRoleOptions.value[0]?.id || "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -342,16 +345,6 @@ async function reloadCurrentRole(): Promise<void> {
|
|||||||
.filter(item => item.id !== current.id && isManagementChildRole(item.id, current.id) && !hasChildRole(item.id, current.id))
|
.filter(item => item.id !== current.id && isManagementChildRole(item.id, current.id) && !hasChildRole(item.id, current.id))
|
||||||
.map(item => item.id));
|
.map(item => item.id));
|
||||||
const childRoleIdList = current.childRoleIdList.filter(id => childOptionIdSet.has(id));
|
const childRoleIdList = current.childRoleIdList.filter(id => childOptionIdSet.has(id));
|
||||||
parentRoleOptions.value = await permissionStore.loadParentGrantableRoles(current.id);
|
|
||||||
if (current.parentRole?.id && !parentRoleOptions.value.some(item => item.id === current.parentRoleId)) {
|
|
||||||
parentRoleOptions.value.unshift({
|
|
||||||
...current.parentRole,
|
|
||||||
id: current.parentRole.id,
|
|
||||||
permissionIdList: current.parentRole.permissionIdList || current.parentRole.permissionList?.map(item => item.id) || [],
|
|
||||||
childRoleIdList: current.parentRole.childRoleIdList || current.parentRole.childRoleList?.map(item => item.id).filter((id): id is string => Boolean(id)) || [],
|
|
||||||
delegationPermissionIdList: current.parentRole.delegationPermissionIdList || current.parentRole.delegationPermissionList?.map(item => item.id) || []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Object.assign(formModel, {
|
Object.assign(formModel, {
|
||||||
id: current.id,
|
id: current.id,
|
||||||
moduleCode: current.moduleCode || moduleCode,
|
moduleCode: current.moduleCode || moduleCode,
|
||||||
@@ -361,6 +354,18 @@ async function reloadCurrentRole(): Promise<void> {
|
|||||||
parentRoleId: current.parentRoleId || "",
|
parentRoleId: current.parentRoleId || "",
|
||||||
childRoleIdList
|
childRoleIdList
|
||||||
});
|
});
|
||||||
|
parentRoleOptions.value = canEditParentRole.value
|
||||||
|
? await permissionStore.loadParentGrantableRoles(current.id)
|
||||||
|
: [];
|
||||||
|
if (current.parentRole?.id && !parentRoleOptions.value.some(item => item.id === current.parentRoleId)) {
|
||||||
|
parentRoleOptions.value.unshift({
|
||||||
|
...current.parentRole,
|
||||||
|
id: current.parentRole.id,
|
||||||
|
permissionIdList: current.parentRole.permissionIdList || current.parentRole.permissionList?.map(item => item.id) || [],
|
||||||
|
childRoleIdList: current.parentRole.childRoleIdList || current.parentRole.childRoleList?.map(item => item.id).filter((id): id is string => Boolean(id)) || [],
|
||||||
|
delegationPermissionIdList: current.parentRole.delegationPermissionIdList || current.parentRole.delegationPermissionList?.map(item => item.id) || []
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadAuthorizationScope(): Promise<void> {
|
async function loadAuthorizationScope(): Promise<void> {
|
||||||
@@ -453,16 +458,22 @@ async function getCurrentStoreId(): Promise<string> {
|
|||||||
return storeId;
|
return storeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(fillForm);
|
|
||||||
|
|
||||||
function updateNavBar(): void {
|
function updateNavBar(): void {
|
||||||
navBarStore.clearRight();
|
navBarStore.clearRight();
|
||||||
if (canShowSaveAction.value) {
|
if (!hasSavePermission.value) {
|
||||||
navBarStore.setRightText("保存", "var(--td-brand-color)", saveBaseInfo);
|
return;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(canShowSaveAction, updateNavBar, { immediate: true });
|
navBarStore.setRightText("保存", "var(--td-brand-color)", saveBaseInfo);
|
||||||
|
navBarStore.setRightDisabled(!canSaveBaseInfo.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch([hasSavePermission, canSaveBaseInfo], updateNavBar, { immediate: true });
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await fillForm();
|
||||||
|
updateNavBar();
|
||||||
|
});
|
||||||
|
|
||||||
onUnmounted(() => navBarStore.clearRight());
|
onUnmounted(() => navBarStore.clearRight());
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
>
|
>
|
||||||
<t-cell
|
<t-cell
|
||||||
:title="item.name"
|
:title="item.name"
|
||||||
:note="item.code"
|
:note="`${item.code} · ${getRoleScopeLabel(item)}`"
|
||||||
arrow
|
arrow
|
||||||
hover
|
hover
|
||||||
@click="router.push(`/management/permissions/roles/${item.id}`)"
|
@click="router.push(`/management/permissions/roles/${item.id}`)"
|
||||||
@@ -53,6 +53,7 @@ const permissionStore = usePermissionSettingsStore();
|
|||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const isRefreshing = ref(false);
|
const isRefreshing = ref(false);
|
||||||
const deletableRoleIdSet = ref(new Set<string>());
|
const deletableRoleIdSet = ref(new Set<string>());
|
||||||
|
const directRoleIdSet = ref(new Set<string>());
|
||||||
|
|
||||||
// ---------- 顶部导航 ----------
|
// ---------- 顶部导航 ----------
|
||||||
|
|
||||||
@@ -93,6 +94,15 @@ function canDeleteRole(role: Role): boolean {
|
|||||||
return role.builtin !== true && role.protectedRole !== true && deletableRoleIdSet.value.has(role.id);
|
return role.builtin !== true && role.protectedRole !== true && deletableRoleIdSet.value.has(role.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRoleScopeLabel(role: Role): string {
|
||||||
|
const direct = directRoleIdSet.value.has(role.id);
|
||||||
|
const manageable = deletableRoleIdSet.value.has(role.id);
|
||||||
|
if (direct && manageable) {
|
||||||
|
return "直属且可管理";
|
||||||
|
}
|
||||||
|
return direct ? "直属角色" : "可管理角色";
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- 页面数据 ----------
|
// ---------- 页面数据 ----------
|
||||||
|
|
||||||
async function refresh(): Promise<void> {
|
async function refresh(): Promise<void> {
|
||||||
@@ -155,10 +165,19 @@ async function refreshAuthorizationScope(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const roleList = await permissionStore.loadManageableRoles();
|
const [manageableRoleList, currentRoleList] = await Promise.all([
|
||||||
displayRoleList.value = roleList;
|
permissionStore.loadManageableRoles(),
|
||||||
deletableRoleIdSet.value = new Set(roleList.map(item => item.id).filter((id): id is string => Boolean(id)));
|
permissionStore.loadCurrentRoleDetails()
|
||||||
|
]);
|
||||||
|
const roleMap = new Map<string, Role>();
|
||||||
|
for (const role of [...currentRoleList, ...manageableRoleList]) {
|
||||||
|
roleMap.set(role.id, role);
|
||||||
|
}
|
||||||
|
displayRoleList.value = Array.from(roleMap.values());
|
||||||
|
directRoleIdSet.value = new Set(currentRoleList.map(item => item.id).filter((id): id is string => Boolean(id)));
|
||||||
|
deletableRoleIdSet.value = new Set(manageableRoleList.map(item => item.id).filter((id): id is string => Boolean(id)));
|
||||||
} catch {
|
} catch {
|
||||||
|
directRoleIdSet.value = new Set<string>();
|
||||||
deletableRoleIdSet.value = new Set<string>();
|
deletableRoleIdSet.value = new Set<string>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<t-cell-group
|
<t-cell-group
|
||||||
v-if="activeRole && grantablePermissionList.length"
|
v-if="canLoadActiveRolePermissions && activeRole && grantablePermissionList.length"
|
||||||
:title="showChildRolePermissions ? '继承权限表示通过继承子级角色间接获得的权限' : ''"
|
:title="showChildRolePermissions ? '继承权限表示通过继承子级角色间接获得的权限' : ''"
|
||||||
class="list"
|
class="list"
|
||||||
theme="card"
|
theme="card"
|
||||||
@@ -41,7 +41,10 @@
|
|||||||
</template>
|
</template>
|
||||||
</t-cell>
|
</t-cell>
|
||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
<t-empty v-else-if="!permissionStore.loading" description="暂无可配置的直接权限 P" />
|
<t-empty
|
||||||
|
v-else-if="!permissionStore.loading"
|
||||||
|
:description="canLoadActiveRolePermissions ? '暂无可配置的直接权限 P' : '当前角色不在可管理范围内,不能配置直接授权权限 P'"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -80,6 +83,8 @@ const isSystemAdmin = computed(() => userStore.hasRoles(coreRoleCodes.admin));
|
|||||||
const canEditActiveRole = computed(() => Boolean(activeRole.value)
|
const canEditActiveRole = computed(() => Boolean(activeRole.value)
|
||||||
&& candidateRoleList.value.some(item => item.id === activeRoleId.value)
|
&& candidateRoleList.value.some(item => item.id === activeRoleId.value)
|
||||||
&& (isSystemAdmin.value || activeRole.value?.protectedRole !== true));
|
&& (isSystemAdmin.value || activeRole.value?.protectedRole !== true));
|
||||||
|
const canLoadActiveRolePermissions = computed(() => Boolean(activeRole.value)
|
||||||
|
&& candidateRoleList.value.some(item => item.id === activeRoleId.value));
|
||||||
const roleOptions = computed<DropdownOption[]>(() => candidateRoleList.value
|
const roleOptions = computed<DropdownOption[]>(() => candidateRoleList.value
|
||||||
.filter(item => !!item.id)
|
.filter(item => !!item.id)
|
||||||
.map(item => ({
|
.map(item => ({
|
||||||
@@ -110,6 +115,10 @@ async function loadActiveRolePermissions(): Promise<void> {
|
|||||||
clearPermissionState();
|
clearPermissionState();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!candidateRoleList.value.some(item => item.id === roleId)) {
|
||||||
|
clearPermissionState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const currentSeq = permissionSyncSeq + 1;
|
const currentSeq = permissionSyncSeq + 1;
|
||||||
permissionSyncSeq = currentSeq;
|
permissionSyncSeq = currentSeq;
|
||||||
@@ -254,6 +263,21 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.t-cell__title) {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.t-cell__description) {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.t-cell__right) {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-left: .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.list {
|
.list {
|
||||||
|
|
||||||
:deep(.t-cell) {
|
:deep(.t-cell) {
|
||||||
|
|||||||
@@ -62,10 +62,8 @@
|
|||||||
<t-cell-group title="命中规则" theme="card" v-entrance-transition="350">
|
<t-cell-group title="命中规则" theme="card" v-entrance-transition="350">
|
||||||
<template v-if="hitRule">
|
<template v-if="hitRule">
|
||||||
<t-cell title="规则名称" :note="hitRule.name || '未命名规则'" />
|
<t-cell title="规则名称" :note="hitRule.name || '未命名规则'" />
|
||||||
<t-cell title="触发类型" :note="getTriggerTypeText(hitRule.triggerType)" />
|
<t-cell title="规则模式" :note="hitRule.ruleMode === 'GROOVY' ? '脚本规则' : '简单规则'" />
|
||||||
<t-cell title="积分模式" :note="getPointModeText(hitRule.pointMode)" />
|
|
||||||
<t-cell title="规则积分" :note="getRulePointText(hitRule)" />
|
<t-cell title="规则积分" :note="getRulePointText(hitRule)" />
|
||||||
<t-cell title="匹配条件" :note="getRuleConditionText(hitRule)" />
|
|
||||||
<t-cell title="规则状态" :note="getRuleStatusText(hitRule.status)" />
|
<t-cell title="规则状态" :note="getRuleStatusText(hitRule.status)" />
|
||||||
<t-cell title="规则优先级" :note="getPointText(hitRule.priority)" />
|
<t-cell title="规则优先级" :note="getPointText(hitRule.priority)" />
|
||||||
<t-cell title="命中后停止匹配" :note="hitRule.exclusive ? '是' : '否'" />
|
<t-cell title="命中后停止匹配" :note="hitRule.exclusive ? '是' : '否'" />
|
||||||
@@ -90,7 +88,7 @@ import CustomerProfileCard from "@/components/CustomerProfileCard.vue";
|
|||||||
import type { Customer } from "@/types/Customer";
|
import type { Customer } from "@/types/Customer";
|
||||||
import type { EventItem } from "@/types/EventItem";
|
import type { EventItem } from "@/types/EventItem";
|
||||||
import type { EventRecord } from "@/types/EventRecord";
|
import type { EventRecord } from "@/types/EventRecord";
|
||||||
import type { PointLedger, PointRule, PointRuleMode, PointRuleStatus, PointRuleTriggerType } from "@/types/Point";
|
import type { PointLedger, PointRule, PointRuleStatus } from "@/types/Point";
|
||||||
import { formatEventNumberValue, getEventValueUnitText } from "@/utils/eventValue";
|
import { formatEventNumberValue, getEventValueUnitText } from "@/utils/eventValue";
|
||||||
import { permissionCodes } from "@/utils/permission";
|
import { permissionCodes } from "@/utils/permission";
|
||||||
|
|
||||||
@@ -115,7 +113,6 @@ const canReadRule = computed(() => userStore.hasPermissions(permissionCodes.poin
|
|||||||
const ledger = ref<PointLedger>();
|
const ledger = ref<PointLedger>();
|
||||||
const customer = ref<Customer>();
|
const customer = ref<Customer>();
|
||||||
const triggerEvent = ref<EventItem>();
|
const triggerEvent = ref<EventItem>();
|
||||||
const sourceEvent = ref<EventItem>();
|
|
||||||
const eventRecord = ref<EventRecord>();
|
const eventRecord = ref<EventRecord>();
|
||||||
const hitRule = ref<PointRule>();
|
const hitRule = ref<PointRule>();
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
@@ -126,10 +123,7 @@ const hasEarnedPointValue = computed(() => eventRecord.value?.earnedPointValue !
|
|||||||
const isDeletedEventRevoke = computed(() => isDeletedEventRevokeLedger(ledger.value));
|
const isDeletedEventRevoke = computed(() => isDeletedEventRevokeLedger(ledger.value));
|
||||||
|
|
||||||
function isDeletedEventRevokeLedger(value?: PointLedger): boolean {
|
function isDeletedEventRevokeLedger(value?: PointLedger): boolean {
|
||||||
const idempotencyKey = value?.idempotencyKey || "";
|
return value?.changeType === "REVOKE" && value?.triggerSourceType === "EVENT_RECORD" && !value.triggerSourceId;
|
||||||
return value?.changeType === "REVOKE"
|
|
||||||
&& idempotencyKey.startsWith("GAO_EVENT_RECORD:")
|
|
||||||
&& idempotencyKey.endsWith(":REVOKE");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getQueryText(key: string): string {
|
function getQueryText(key: string): string {
|
||||||
@@ -171,7 +165,6 @@ async function loadPageData(): Promise<void> {
|
|||||||
ledger.value = parseLedgerSnapshot();
|
ledger.value = parseLedgerSnapshot();
|
||||||
customer.value = undefined;
|
customer.value = undefined;
|
||||||
triggerEvent.value = undefined;
|
triggerEvent.value = undefined;
|
||||||
sourceEvent.value = undefined;
|
|
||||||
eventRecord.value = undefined;
|
eventRecord.value = undefined;
|
||||||
hitRule.value = undefined;
|
hitRule.value = undefined;
|
||||||
if (!ledger.value) {
|
if (!ledger.value) {
|
||||||
@@ -180,11 +173,12 @@ async function loadPageData(): Promise<void> {
|
|||||||
|
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
if (ledger.value.eventRecordId && canReadEventRecord.value && !isDeletedEventRevoke.value) {
|
if (ledger.value.triggerSourceType === "EVENT_RECORD" && ledger.value.triggerSourceId && canReadEventRecord.value && !isDeletedEventRevoke.value) {
|
||||||
eventRecord.value = await loadOptional(() => EventRecordAPI.detail(ledger.value!.eventRecordId!), "积分流水关联事件记录加载失败");
|
eventRecord.value = await loadOptional(() => EventRecordAPI.detail(ledger.value!.triggerSourceId!), "积分流水关联事件记录加载失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventId = eventRecord.value?.eventId || ledger.value.eventId;
|
const eventId = eventRecord.value?.eventId
|
||||||
|
|| (ledger.value.triggerSourceType === "EVENT_RECORD" ? ledger.value.triggerSourceId : "");
|
||||||
const eventResult = eventRecord.value?.event
|
const eventResult = eventRecord.value?.event
|
||||||
? Promise.resolve(eventRecord.value.event)
|
? Promise.resolve(eventRecord.value.event)
|
||||||
: eventId && canReadEvent.value
|
: eventId && canReadEvent.value
|
||||||
@@ -201,11 +195,6 @@ async function loadPageData(): Promise<void> {
|
|||||||
if (eventRecord.value?.customer) {
|
if (eventRecord.value?.customer) {
|
||||||
customer.value = eventRecord.value.customer;
|
customer.value = eventRecord.value.customer;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceEventId = hitRule.value?.conditionJson?.sourceEventId;
|
|
||||||
if (sourceEventId && sourceEventId !== eventId && canReadEvent.value) {
|
|
||||||
sourceEvent.value = await loadOptional(() => EventAPI.detail(sourceEventId), "积分规则来源事件加载失败");
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
@@ -255,27 +244,9 @@ function getEventNumberValueText(): string {
|
|||||||
return formatEventNumberValue(eventRecord.value?.event || triggerEvent.value, eventRecord.value?.numberValue);
|
return formatEventNumberValue(eventRecord.value?.event || triggerEvent.value, eventRecord.value?.numberValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTriggerTypeText(triggerType?: PointRuleTriggerType): string {
|
|
||||||
return ({
|
|
||||||
EVERY_TIME: "每次登记",
|
|
||||||
FIRST_TIME: "首次登记",
|
|
||||||
AFTER_COUNT_EVERY_TIME: "达到阈值后每次登记",
|
|
||||||
SOURCE_VALUE: "登记时自定义积分",
|
|
||||||
CUSTOMER_EVENT_VALUE_TIER: "按来源事件累计分档"
|
|
||||||
} as Record<string, string>)[triggerType || ""] || "未知触发类型";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPointModeText(pointMode?: PointRuleMode): string {
|
|
||||||
return ({
|
|
||||||
FIXED: "固定积分",
|
|
||||||
SOURCE_VALUE: "使用登记时填写的积分",
|
|
||||||
TIER: "分档积分"
|
|
||||||
} as Record<string, string>)[pointMode || ""] || "未知积分模式";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRulePointText(rule: PointRule): string {
|
function getRulePointText(rule: PointRule): string {
|
||||||
if (rule.pointMode === "TIER") {
|
if (rule.ruleMode === "GROOVY") {
|
||||||
return rule.conditionJson?.tiers?.length ? `${rule.conditionJson.tiers.length} 档` : "分档积分";
|
return "由脚本决定";
|
||||||
}
|
}
|
||||||
if (rule.pointValue === undefined || rule.pointValue === null) {
|
if (rule.pointValue === undefined || rule.pointValue === null) {
|
||||||
return "-";
|
return "-";
|
||||||
@@ -283,42 +254,6 @@ function getRulePointText(rule: PointRule): string {
|
|||||||
return `${0 < rule.pointValue ? "发放" : "消耗"} ${Math.abs(rule.pointValue)} 分`;
|
return `${0 < rule.pointValue ? "发放" : "消耗"} ${Math.abs(rule.pointValue)} 分`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRuleConditionText(rule: PointRule): string {
|
|
||||||
const condition = rule.conditionJson;
|
|
||||||
if (!condition) {
|
|
||||||
return "无额外条件";
|
|
||||||
}
|
|
||||||
|
|
||||||
const textList: string[] = [];
|
|
||||||
if (condition.min !== undefined || condition.max !== undefined) {
|
|
||||||
textList.push(`数值范围 ${condition.min ?? "不限"}~${condition.max ?? "不限"}`);
|
|
||||||
}
|
|
||||||
if (condition.threshold !== undefined) {
|
|
||||||
textList.push(`累计达到 ${condition.threshold} 次`);
|
|
||||||
}
|
|
||||||
if (condition.includeCurrent !== undefined) {
|
|
||||||
textList.push(condition.includeCurrent ? "包含本次登记" : "不含本次登记");
|
|
||||||
}
|
|
||||||
if (condition.window) {
|
|
||||||
textList.push(`累计窗口 ${condition.window.amount} ${condition.window.unit === "YEAR" ? "年" : condition.window.unit}`);
|
|
||||||
}
|
|
||||||
if (condition.sourceEventId) {
|
|
||||||
textList.push(`来源事件:${sourceEvent.value?.name || sourceEvent.value?.id || condition.sourceEventId}`);
|
|
||||||
}
|
|
||||||
if (condition.tiers?.length) {
|
|
||||||
const tierSourceEvent = sourceEvent.value || (condition.sourceEventId === triggerEvent.value?.id ? triggerEvent.value : undefined);
|
|
||||||
textList.push(condition.tiers.map(tier => {
|
|
||||||
const minText = formatEventNumberValue(tierSourceEvent, tier.min);
|
|
||||||
const maxText = tier.max === undefined || tier.max === null ? "以上" : formatEventNumberValue(tierSourceEvent, tier.max);
|
|
||||||
const pointText = tier.pointValue === 0
|
|
||||||
? "不产生积分流水"
|
|
||||||
: `${0 < tier.pointValue ? "发放" : "消耗"} ${Math.abs(tier.pointValue)} 分`;
|
|
||||||
return `${minText}~${maxText}:${pointText}`;
|
|
||||||
}).join(","));
|
|
||||||
}
|
|
||||||
return textList.join(";") || "无额外条件";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRuleStatusText(status?: PointRuleStatus): string {
|
function getRuleStatusText(status?: PointRuleStatus): string {
|
||||||
return ({
|
return ({
|
||||||
DRAFT: "草稿",
|
DRAFT: "草稿",
|
||||||
|
|||||||
@@ -418,8 +418,11 @@ async function toDetail(item: PointLedger): Promise<void> {
|
|||||||
balanceBefore: item.balanceBefore,
|
balanceBefore: item.balanceBefore,
|
||||||
balanceAfter: item.balanceAfter,
|
balanceAfter: item.balanceAfter,
|
||||||
ruleId: item.ruleId,
|
ruleId: item.ruleId,
|
||||||
eventId: item.eventId,
|
triggerSourceType: item.triggerSourceType,
|
||||||
eventRecordId: item.eventRecordId,
|
triggerSourceId: item.triggerSourceId,
|
||||||
|
salesOrderId: item.salesOrderId,
|
||||||
|
groovyScriptVersionId: item.groovyScriptVersionId,
|
||||||
|
ruleSnapshot: item.ruleSnapshot,
|
||||||
sourcePayload: item.sourcePayload,
|
sourcePayload: item.sourcePayload,
|
||||||
idempotencyKey: item.idempotencyKey,
|
idempotencyKey: item.idempotencyKey,
|
||||||
remark: item.remark,
|
remark: item.remark,
|
||||||
|
|||||||
@@ -15,12 +15,14 @@
|
|||||||
<t-cell-group v-if="rule" title="基本信息" theme="card" v-entrance-transition="70">
|
<t-cell-group v-if="rule" title="基本信息" theme="card" v-entrance-transition="70">
|
||||||
<t-cell title="所属门店" :note="storeName" />
|
<t-cell title="所属门店" :note="storeName" />
|
||||||
<t-cell
|
<t-cell
|
||||||
title="登记事件"
|
v-if="rule.triggerSourceType === 'EVENT_RECORD'"
|
||||||
|
title="触发事件"
|
||||||
:note="event?.name || '未绑定事件'"
|
:note="event?.name || '未绑定事件'"
|
||||||
:arrow="canReadEvent && !!event?.id"
|
:arrow="canReadEvent && !!event?.id"
|
||||||
:hover="canReadEvent && !!event?.id"
|
:hover="canReadEvent && !!event?.id"
|
||||||
@click="toEventDetail"
|
@click="toEventDetail"
|
||||||
/>
|
/>
|
||||||
|
<t-cell v-else-if="rule.triggerSourceType === 'SALES_ORDER_PAID'" title="触发来源" note="订单首次全额实收" />
|
||||||
<t-cell title="规则名称" :note="rule.name || '未命名规则'" />
|
<t-cell title="规则名称" :note="rule.name || '未命名规则'" />
|
||||||
<t-cell title="规则状态">
|
<t-cell title="规则状态">
|
||||||
<template #note>
|
<template #note>
|
||||||
@@ -30,26 +32,9 @@
|
|||||||
<t-cell title="规则备注" :note="Text.display(rule.remark)" />
|
<t-cell title="规则备注" :note="Text.display(rule.remark)" />
|
||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
<t-cell-group v-if="rule" title="积分配置" theme="card" v-entrance-transition="140">
|
<t-cell-group v-if="rule" title="积分配置" theme="card" v-entrance-transition="140">
|
||||||
<t-cell title="触发类型" :note="getTriggerText(rule.triggerType)" />
|
<t-cell title="规则模式" :note="rule.ruleMode === 'GROOVY' ? '脚本规则' : '简单规则'" />
|
||||||
<t-cell title="积分值模式" :note="getModeText(rule.pointMode)" />
|
<t-cell v-if="rule.ruleMode !== 'GROOVY'" title="固定积分" :note="formatPointValue(rule.pointValue)" />
|
||||||
<t-cell v-if="rule.pointMode === 'FIXED'" title="固定积分" :note="formatPointValue(rule.pointValue)" />
|
<t-cell v-else title="执行脚本" :note="rule.groovyScriptId || '未绑定脚本'" />
|
||||||
<t-cell v-if="rule.pointMode === 'SOURCE_VALUE'" title="自定义积分范围" :note="sourceValueRangeText" />
|
|
||||||
<t-cell v-if="rule.triggerType === 'AFTER_COUNT_EVERY_TIME'" title="触发阈值" :note="getThresholdText(rule)" />
|
|
||||||
<t-cell v-if="rule.triggerType === 'FIRST_TIME'" title="匹配范围" note="每个客户的首次登记" />
|
|
||||||
<t-cell v-if="rule.conditionJson?.window" title="累计窗口" :note="getWindowText(rule)" />
|
|
||||||
<t-cell v-if="rule.conditionJson?.sourceEventId" title="来源事件">
|
|
||||||
<template #note>
|
|
||||||
<span v-text="sourceEvent?.name || rule.conditionJson.sourceEventId" />
|
|
||||||
</template>
|
|
||||||
</t-cell>
|
|
||||||
</t-cell-group>
|
|
||||||
<t-cell-group
|
|
||||||
v-if="tierTextList.length"
|
|
||||||
title="积分分档"
|
|
||||||
theme="card"
|
|
||||||
v-entrance-transition="210"
|
|
||||||
>
|
|
||||||
<t-cell v-for="(text, index) in tierTextList" :key="index" :title="`第 ${index + 1} 档`" :note="text" />
|
|
||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
<t-cell-group v-if="rule" title="其他配置" theme="card" v-entrance-transition="280">
|
<t-cell-group v-if="rule" title="其他配置" theme="card" v-entrance-transition="280">
|
||||||
<t-cell title="规则优先级" :note="formatNumber(rule.priority)" />
|
<t-cell title="规则优先级" :note="formatNumber(rule.priority)" />
|
||||||
@@ -82,8 +67,7 @@ import PointAPI from "@/api/PointAPI";
|
|||||||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||||
import { useNavBarStore } from "@/store/navBarStore";
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
import type { EventItem } from "@/types/EventItem";
|
import type { EventItem } from "@/types/EventItem";
|
||||||
import type { PointRule, PointRuleStatus, PointRuleMode, PointRuleTriggerType } from "@/types/Point";
|
import type { PointRule, PointRuleStatus } from "@/types/Point";
|
||||||
import { formatEventNumberValue } from "@/utils/eventValue";
|
|
||||||
import { getErrorMessage } from "@/utils/error";
|
import { getErrorMessage } from "@/utils/error";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
||||||
@@ -118,31 +102,16 @@ const storeName = computed(() => {
|
|||||||
|
|
||||||
const rule = ref<PointRule>();
|
const rule = ref<PointRule>();
|
||||||
const event = ref<EventItem>();
|
const event = ref<EventItem>();
|
||||||
const sourceEvent = ref<EventItem>();
|
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
const isDeleting = ref(false);
|
const isDeleting = ref(false);
|
||||||
|
|
||||||
const condition = computed(() => rule.value?.conditionJson);
|
|
||||||
const isConsumeRule = computed(() => (rule.value?.pointValue ?? 0) < 0);
|
const isConsumeRule = computed(() => (rule.value?.pointValue ?? 0) < 0);
|
||||||
const rulePointText = computed(() => {
|
const rulePointText = computed(() => {
|
||||||
if (rule.value?.pointMode === "TIER") {
|
if (rule.value?.ruleMode === "GROOVY") {
|
||||||
return condition.value?.tiers?.length ? `${condition.value.tiers.length} 档积分` : "分档积分";
|
return "脚本规则";
|
||||||
}
|
|
||||||
if (rule.value?.pointMode === "SOURCE_VALUE") {
|
|
||||||
return "按登记时填写的积分变化";
|
|
||||||
}
|
}
|
||||||
return formatPointValue(rule.value?.pointValue);
|
return formatPointValue(rule.value?.pointValue);
|
||||||
});
|
});
|
||||||
const sourceValueRangeText = computed(() => {
|
|
||||||
const minText = condition.value?.min === undefined ? "不限" : formatPointValue(condition.value.min);
|
|
||||||
const maxText = condition.value?.max === undefined ? "不限" : formatPointValue(condition.value.max);
|
|
||||||
return `最小值:${minText};最大值:${maxText}`;
|
|
||||||
});
|
|
||||||
const tierTextList = computed(() => (condition.value?.tiers || []).map(tier => {
|
|
||||||
const minText = formatEventNumberValue(sourceEvent.value, tier.min);
|
|
||||||
const maxText = tier.max === undefined || tier.max === null ? "以上" : formatEventNumberValue(sourceEvent.value, tier.max);
|
|
||||||
return `${minText}~${maxText}:${formatPointValue(tier.pointValue)}`;
|
|
||||||
}));
|
|
||||||
|
|
||||||
async function loadStoreScope(): Promise<void> {
|
async function loadStoreScope(): Promise<void> {
|
||||||
if (isGlobalManager.value) {
|
if (isGlobalManager.value) {
|
||||||
@@ -166,13 +135,11 @@ async function loadRule(): Promise<void> {
|
|||||||
if (!ruleId) {
|
if (!ruleId) {
|
||||||
rule.value = undefined;
|
rule.value = undefined;
|
||||||
event.value = undefined;
|
event.value = undefined;
|
||||||
sourceEvent.value = undefined;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
rule.value = undefined;
|
rule.value = undefined;
|
||||||
event.value = undefined;
|
event.value = undefined;
|
||||||
sourceEvent.value = undefined;
|
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
await loadStoreScope();
|
await loadStoreScope();
|
||||||
@@ -181,20 +148,13 @@ async function loadRule(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventId = rule.value.eventId || "";
|
const triggerRefId = rule.value.triggerRefId || "";
|
||||||
const sourceEventId = rule.value.conditionJson?.sourceEventId || "";
|
event.value = triggerRefId && rule.value.triggerSourceType === "EVENT_RECORD"
|
||||||
const [eventResult, sourceEventResult] = await Promise.all([
|
? await loadOptionalEvent(triggerRefId, "积分规则触发事件加载失败")
|
||||||
eventId ? loadOptionalEvent(eventId, "积分规则登记事件加载失败") : Promise.resolve(undefined),
|
: undefined;
|
||||||
sourceEventId && sourceEventId !== eventId
|
|
||||||
? loadOptionalEvent(sourceEventId, "积分规则来源事件加载失败")
|
|
||||||
: Promise.resolve(undefined)
|
|
||||||
]);
|
|
||||||
event.value = eventResult;
|
|
||||||
sourceEvent.value = sourceEventResult || (sourceEventId === eventId ? eventResult : undefined);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
rule.value = undefined;
|
rule.value = undefined;
|
||||||
event.value = undefined;
|
event.value = undefined;
|
||||||
sourceEvent.value = undefined;
|
|
||||||
console.warn("积分规则详情加载失败", error);
|
console.warn("积分规则详情加载失败", error);
|
||||||
Message.error("积分规则加载失败");
|
Message.error("积分规则加载失败");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -226,24 +186,6 @@ function getStatusTheme(status?: PointRuleStatus): "default" | "success" | "warn
|
|||||||
return "default";
|
return "default";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTriggerText(triggerType?: PointRuleTriggerType): string {
|
|
||||||
return ({
|
|
||||||
EVERY_TIME: "每次登记",
|
|
||||||
FIRST_TIME: "首次登记",
|
|
||||||
AFTER_COUNT_EVERY_TIME: "达到阈值后每次登记",
|
|
||||||
SOURCE_VALUE: "登记时自定义积分",
|
|
||||||
CUSTOMER_EVENT_VALUE_TIER: "按来源事件累计分档"
|
|
||||||
} as Record<string, string>)[triggerType || ""] || "未知触发类型";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getModeText(pointMode?: PointRuleMode): string {
|
|
||||||
return ({
|
|
||||||
FIXED: "固定积分",
|
|
||||||
SOURCE_VALUE: "使用登记时填写的积分",
|
|
||||||
TIER: "分档积分"
|
|
||||||
} as Record<string, string>)[pointMode || ""] || "未知积分模式";
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatPointValue(value?: number | null): string {
|
function formatPointValue(value?: number | null): string {
|
||||||
if (value === undefined || value === null) {
|
if (value === undefined || value === null) {
|
||||||
return "未配置";
|
return "未配置";
|
||||||
@@ -254,19 +196,6 @@ function formatPointValue(value?: number | null): string {
|
|||||||
return `${0 < value ? "发放" : "消耗"} ${Math.abs(value)} 分`;
|
return `${0 < value ? "发放" : "消耗"} ${Math.abs(value)} 分`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getThresholdText(value: PointRule): string {
|
|
||||||
const threshold = value.conditionJson?.threshold;
|
|
||||||
return threshold === undefined || threshold === null ? "未配置" : `${threshold} 次`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWindowText(value: PointRule): string {
|
|
||||||
const window = value.conditionJson?.window;
|
|
||||||
if (!window) {
|
|
||||||
return "未配置";
|
|
||||||
}
|
|
||||||
return `${window.amount} ${window.unit === "YEAR" ? "年" : window.unit}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatNumber(value?: number): string {
|
function formatNumber(value?: number): string {
|
||||||
return value === undefined || value === null ? "未配置" : String(value);
|
return value === undefined || value === null ? "未配置" : String(value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,13 @@
|
|||||||
<t-form-item v-if="isGlobalManager" label="门店" required-mark :arrow="isCreateMode" @click="openStorePicker">
|
<t-form-item v-if="isGlobalManager" label="门店" required-mark :arrow="isCreateMode" @click="openStorePicker">
|
||||||
<span class="picker-text" v-text="selectedStoreName" />
|
<span class="picker-text" v-text="selectedStoreName" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="登记事件" required-mark arrow @click="openEventPicker">
|
<t-form-item label="触发来源" required-mark arrow @click="openTriggerSourcePicker">
|
||||||
|
<span class="picker-text" v-text="getTriggerSourceText(formModel.triggerSourceType)" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item v-if="formModel.triggerSourceType === 'EVENT_RECORD'" label="触发事件" required-mark arrow @click="openEventPicker">
|
||||||
<span class="picker-text" v-text="selectedEventName" />
|
<span class="picker-text" v-text="selectedEventName" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
|
<t-form-item v-else label-width="0" class="form-tips">订单首次全额实收时触发,同一订单每条规则只生效一次</t-form-item>
|
||||||
<t-form-item label="规则名称" required-mark>
|
<t-form-item label="规则名称" required-mark>
|
||||||
<t-input v-model.trim="formModel.name" align="right" borderless placeholder="例如:每次登记加 10 分" />
|
<t-input v-model.trim="formModel.name" align="right" borderless placeholder="例如:每次登记加 10 分" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
@@ -19,16 +23,10 @@
|
|||||||
</t-form-item>
|
</t-form-item>
|
||||||
</card-container>
|
</card-container>
|
||||||
<card-container v-entrance-transition="70">
|
<card-container v-entrance-transition="70">
|
||||||
<t-form-item label="触发类型" arrow @click="openTriggerPicker">
|
<t-form-item label="积分动作" arrow @click="openPointActionPicker">
|
||||||
<span class="picker-text" v-text="getTriggerText(formModel.triggerType)" />
|
|
||||||
</t-form-item>
|
|
||||||
<t-form-item label="积分值模式" :arrow="formModel.triggerType !== 'CUSTOMER_EVENT_VALUE_TIER'" @click="openModePicker">
|
|
||||||
<span class="picker-text" v-text="getModeText(formModel.pointMode)" />
|
|
||||||
</t-form-item>
|
|
||||||
<t-form-item v-if="formModel.pointMode === 'FIXED'" label="积分动作" arrow @click="openPointActionPicker">
|
|
||||||
<span class="picker-text" v-text="getPointActionText(formModel.pointAction)" />
|
<span class="picker-text" v-text="getPointActionText(formModel.pointAction)" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item v-if="formModel.pointMode === 'FIXED'" label="积分数量" required-mark>
|
<t-form-item label="积分数量" required-mark>
|
||||||
<t-input
|
<t-input
|
||||||
v-model.trim="formModel.pointValueText"
|
v-model.trim="formModel.pointValueText"
|
||||||
type="number"
|
type="number"
|
||||||
@@ -37,36 +35,6 @@
|
|||||||
placeholder="请输入数量"
|
placeholder="请输入数量"
|
||||||
/>
|
/>
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item v-if="formModel.triggerType === 'AFTER_COUNT_EVERY_TIME'" label="触发阈值" required-mark>
|
|
||||||
<t-input
|
|
||||||
v-model.trim="formModel.thresholdText"
|
|
||||||
type="number"
|
|
||||||
align="right"
|
|
||||||
borderless
|
|
||||||
placeholder="累计登记达到几次"
|
|
||||||
/>
|
|
||||||
</t-form-item>
|
|
||||||
<div v-if="formModel.pointMode === 'SOURCE_VALUE'" class="source-condition">
|
|
||||||
<div class="source-title">自定义积分范围</div>
|
|
||||||
<t-form-item label="积分动作" arrow @click="openPointActionPicker">
|
|
||||||
<span class="picker-text" v-text="getPointActionText(formModel.pointAction)" />
|
|
||||||
</t-form-item>
|
|
||||||
<div class="source-inputs">
|
|
||||||
<t-input v-model.trim="formModel.minText" type="number" align="right" borderless placeholder="最小值" />
|
|
||||||
<span>至</span>
|
|
||||||
<t-input v-model.trim="formModel.maxText" type="number" align="right" borderless placeholder="最大值" />
|
|
||||||
</div>
|
|
||||||
<span class="tips">登记时输入数量,系统会按选定动作执行积分变化</span>
|
|
||||||
</div>
|
|
||||||
<template v-if="formModel.triggerType === 'CUSTOMER_EVENT_VALUE_TIER'">
|
|
||||||
<t-form-item label="来源事件" required-mark arrow @click="openSourceEventPicker">
|
|
||||||
<span class="picker-text" v-text="selectedSourceEventName" />
|
|
||||||
</t-form-item>
|
|
||||||
<t-form-item label="累计窗口(年)" label-width="8rem">
|
|
||||||
<t-input v-model.trim="formModel.windowAmountText" type="number" align="right" borderless />
|
|
||||||
</t-form-item>
|
|
||||||
<point-rule-tier-editor v-model="formModel.tiers" :value-unit="selectedSourceValueUnit" />
|
|
||||||
</template>
|
|
||||||
</card-container>
|
</card-container>
|
||||||
<card-container v-entrance-transition="140">
|
<card-container v-entrance-transition="140">
|
||||||
<t-form-item label="生效开始" arrow @click="openBeginAtPicker">
|
<t-form-item label="生效开始" arrow @click="openBeginAtPicker">
|
||||||
@@ -102,18 +70,18 @@
|
|||||||
<t-picker
|
<t-picker
|
||||||
v-model="eventPickerDraft"
|
v-model="eventPickerDraft"
|
||||||
:columns="eventPickerColumns"
|
:columns="eventPickerColumns"
|
||||||
title="选择登记事件"
|
title="选择触发事件"
|
||||||
@cancel="eventPickerVisible = false"
|
@cancel="eventPickerVisible = false"
|
||||||
@confirm="confirmEventPicker"
|
@confirm="confirmEventPicker"
|
||||||
/>
|
/>
|
||||||
</t-popup>
|
</t-popup>
|
||||||
<t-popup v-model="sourceEventPickerVisible" placement="bottom">
|
<t-popup v-model="triggerSourcePickerVisible" placement="bottom">
|
||||||
<t-picker
|
<t-picker
|
||||||
v-model="sourceEventPickerDraft"
|
v-model="triggerSourcePickerDraft"
|
||||||
:columns="sourceEventPickerColumns"
|
:columns="triggerSourcePickerColumns"
|
||||||
title="选择来源事件"
|
title="触发来源"
|
||||||
@cancel="sourceEventPickerVisible = false"
|
@cancel="triggerSourcePickerVisible = false"
|
||||||
@confirm="confirmSourceEventPicker"
|
@confirm="confirmTriggerSourcePicker"
|
||||||
/>
|
/>
|
||||||
</t-popup>
|
</t-popup>
|
||||||
<t-popup v-model="statusPickerVisible" placement="bottom">
|
<t-popup v-model="statusPickerVisible" placement="bottom">
|
||||||
@@ -125,24 +93,6 @@
|
|||||||
@confirm="confirmStatusPicker"
|
@confirm="confirmStatusPicker"
|
||||||
/>
|
/>
|
||||||
</t-popup>
|
</t-popup>
|
||||||
<t-popup v-model="triggerPickerVisible" placement="bottom">
|
|
||||||
<t-picker
|
|
||||||
v-model="triggerPickerDraft"
|
|
||||||
:columns="triggerPickerColumns"
|
|
||||||
title="触发类型"
|
|
||||||
@cancel="triggerPickerVisible = false"
|
|
||||||
@confirm="confirmTriggerPicker"
|
|
||||||
/>
|
|
||||||
</t-popup>
|
|
||||||
<t-popup v-model="modePickerVisible" placement="bottom">
|
|
||||||
<t-picker
|
|
||||||
v-model="modePickerDraft"
|
|
||||||
:columns="modePickerColumns"
|
|
||||||
title="积分值模式"
|
|
||||||
@cancel="modePickerVisible = false"
|
|
||||||
@confirm="confirmModePicker"
|
|
||||||
/>
|
|
||||||
</t-popup>
|
|
||||||
<t-popup v-model="pointActionPickerVisible" placement="bottom">
|
<t-popup v-model="pointActionPickerVisible" placement="bottom">
|
||||||
<t-picker
|
<t-picker
|
||||||
v-model="pointActionPickerDraft"
|
v-model="pointActionPickerDraft"
|
||||||
@@ -181,29 +131,17 @@ import { Time } from "timi-web";
|
|||||||
import { useUserStore } from "timi-web-api";
|
import { useUserStore } from "timi-web-api";
|
||||||
import EventAPI from "@/api/EventAPI";
|
import EventAPI from "@/api/EventAPI";
|
||||||
import PointAPI from "@/api/PointAPI";
|
import PointAPI from "@/api/PointAPI";
|
||||||
import PointRuleTierEditor from "@/pages/point/component/PointRuleTierEditor.vue";
|
|
||||||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||||
import { useNavBarStore } from "@/store/navBarStore";
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
import type { EventItem } from "@/types/EventItem";
|
import type { EventItem } from "@/types/EventItem";
|
||||||
import type {
|
import type {
|
||||||
PointAction,
|
PointAction,
|
||||||
PointRule,
|
PointRule,
|
||||||
PointRuleMode,
|
|
||||||
PointRuleStatus,
|
PointRuleStatus,
|
||||||
PointRuleTier,
|
PointRuleTriggerSourceType
|
||||||
PointRuleTierForm,
|
|
||||||
PointRuleTriggerType
|
|
||||||
} from "@/types/Point";
|
} from "@/types/Point";
|
||||||
import { getEventValueUnitText } from "@/utils/eventValue";
|
|
||||||
import { getErrorMessage } from "@/utils/error";
|
import { getErrorMessage } from "@/utils/error";
|
||||||
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
||||||
import {
|
|
||||||
formatTierBoundaryValue,
|
|
||||||
getTierMinValue,
|
|
||||||
isLastTier,
|
|
||||||
resolveNonNegativeInteger,
|
|
||||||
resolveTierBoundaryValue
|
|
||||||
} from "@/utils/pointRule";
|
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
|
|
||||||
type PickerValue = string | number;
|
type PickerValue = string | number;
|
||||||
@@ -232,19 +170,12 @@ const isSaving = ref(false);
|
|||||||
const formModel = reactive({
|
const formModel = reactive({
|
||||||
id: "",
|
id: "",
|
||||||
storeId: "",
|
storeId: "",
|
||||||
eventId: "",
|
triggerSourceType: "EVENT_RECORD" as PointRuleTriggerSourceType,
|
||||||
|
triggerRefId: "",
|
||||||
name: "",
|
name: "",
|
||||||
status: "DRAFT" as PointRuleStatus,
|
status: "DRAFT" as PointRuleStatus,
|
||||||
triggerType: "EVERY_TIME" as PointRuleTriggerType,
|
|
||||||
pointMode: "FIXED" as PointRuleMode,
|
|
||||||
pointAction: "EARN" as PointAction,
|
pointAction: "EARN" as PointAction,
|
||||||
pointValueText: "",
|
pointValueText: "",
|
||||||
thresholdText: "",
|
|
||||||
minText: "",
|
|
||||||
maxText: "",
|
|
||||||
sourceEventId: "",
|
|
||||||
windowAmountText: "1",
|
|
||||||
tiers: [] as PointRuleTierForm[],
|
|
||||||
beginAtText: "",
|
beginAtText: "",
|
||||||
endAtText: "",
|
endAtText: "",
|
||||||
exclusive: false,
|
exclusive: false,
|
||||||
@@ -255,7 +186,7 @@ const formModel = reactive({
|
|||||||
|
|
||||||
const eventList = ref<EventItem[]>([]);
|
const eventList = ref<EventItem[]>([]);
|
||||||
const selectedStoreName = computed(() => currentStoreStore.storeList.find(item => item.id === formModel.storeId)?.name || "请选择门店");
|
const selectedStoreName = computed(() => currentStoreStore.storeList.find(item => item.id === formModel.storeId)?.name || "请选择门店");
|
||||||
const selectedEventName = computed(() => eventList.value.find(item => item.id === formModel.eventId)?.name || "请选择登记事件");
|
const selectedEventName = computed(() => eventList.value.find(item => item.id === formModel.triggerRefId)?.name || "请选择触发事件");
|
||||||
const storePickerVisible = ref(false);
|
const storePickerVisible = ref(false);
|
||||||
const storePickerDraft = ref<PickerValue[]>([]);
|
const storePickerDraft = ref<PickerValue[]>([]);
|
||||||
const storePickerColumns = computed(() => [
|
const storePickerColumns = computed(() => [
|
||||||
@@ -270,36 +201,6 @@ const eventPickerColumns = computed(() => [
|
|||||||
.map(item => ({ label: item.name || "未命名事件", value: item.id || "" }))
|
.map(item => ({ label: item.name || "未命名事件", value: item.id || "" }))
|
||||||
.filter(item => item.value)
|
.filter(item => item.value)
|
||||||
]);
|
]);
|
||||||
const sourceEventPickerVisible = ref(false);
|
|
||||||
const sourceEventPickerDraft = ref<PickerValue[]>([]);
|
|
||||||
const sourceEventList = computed(() => eventList.value.filter(item => item.valueType === "NUMBER" && item.id));
|
|
||||||
const selectedSourceEvent = computed(() => sourceEventList.value.find(item => item.id === formModel.sourceEventId));
|
|
||||||
const selectedSourceValueUnit = computed(() => selectedSourceEvent.value?.valueUnit || null);
|
|
||||||
const sourceEventPickerColumns = computed(() => [
|
|
||||||
sourceEventList.value
|
|
||||||
.map(item => ({ label: `${item.name || "未命名事件"}(${getEventValueUnitText(item.valueUnit)})`, value: item.id || "" }))
|
|
||||||
.filter(item => item.value)
|
|
||||||
]);
|
|
||||||
const selectedSourceEventName = computed(() => selectedSourceEvent.value
|
|
||||||
? `${selectedSourceEvent.value.name || "未命名事件"}(${getEventValueUnitText(selectedSourceEvent.value.valueUnit)})`
|
|
||||||
: "请选择来源事件");
|
|
||||||
|
|
||||||
let tierKey = 0;
|
|
||||||
|
|
||||||
function createTierForm(tier?: PointRuleTier, isLast = false, valueUnit?: EventItem["valueUnit"]): PointRuleTierForm {
|
|
||||||
const pointValue = tier?.pointValue;
|
|
||||||
return {
|
|
||||||
key: tierKey++,
|
|
||||||
minText: tier?.min === undefined || tier.min === null ? "0" : formatTierBoundaryValue(tier.min, valueUnit),
|
|
||||||
maxText: !isLast && tier?.max !== undefined && tier.max !== null ? formatTierBoundaryValue(tier.max, valueUnit) : "",
|
|
||||||
pointAction: pointValue !== undefined && pointValue < 0 ? "CONSUME" : "EARN",
|
|
||||||
pointValueText: pointValue === undefined ? "0" : String(Math.abs(pointValue))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formModel.tiers.length) {
|
|
||||||
formModel.tiers.push(createTierForm(undefined, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
function openStorePicker(): void {
|
function openStorePicker(): void {
|
||||||
if (!isCreateMode.value || !storePickerColumns.value[0]?.length) {
|
if (!isCreateMode.value || !storePickerColumns.value[0]?.length) {
|
||||||
@@ -311,14 +212,14 @@ function openStorePicker(): void {
|
|||||||
|
|
||||||
async function confirmStorePicker(value: Array<string | number>): Promise<void> {
|
async function confirmStorePicker(value: Array<string | number>): Promise<void> {
|
||||||
const storeId = String(value[0] || "");
|
const storeId = String(value[0] || "");
|
||||||
if (storeId) {
|
if (storeId && storeId !== formModel.storeId) {
|
||||||
formModel.storeId = storeId;
|
formModel.storeId = storeId;
|
||||||
formModel.eventId = "";
|
formModel.triggerRefId = "";
|
||||||
try {
|
try {
|
||||||
await loadEventList();
|
await loadEventList();
|
||||||
} catch {
|
} catch {
|
||||||
eventList.value = [];
|
eventList.value = [];
|
||||||
Message.error("登记事件加载失败");
|
Message.error("触发事件加载失败");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
storePickerVisible.value = false;
|
storePickerVisible.value = false;
|
||||||
@@ -326,47 +227,21 @@ async function confirmStorePicker(value: Array<string | number>): Promise<void>
|
|||||||
|
|
||||||
function openEventPicker(): void {
|
function openEventPicker(): void {
|
||||||
if (!eventPickerColumns.value[0]?.length) {
|
if (!eventPickerColumns.value[0]?.length) {
|
||||||
Message.warning("暂无可选登记事件");
|
Message.warning("暂无可选触发事件");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
eventPickerDraft.value = [formModel.eventId];
|
eventPickerDraft.value = [formModel.triggerRefId];
|
||||||
eventPickerVisible.value = true;
|
eventPickerVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmEventPicker(value: Array<string | number>): void {
|
function confirmEventPicker(value: Array<string | number>): void {
|
||||||
const eventId = String(value[0] || "");
|
const eventId = String(value[0] || "");
|
||||||
if (eventId) {
|
if (eventId) {
|
||||||
formModel.eventId = eventId;
|
formModel.triggerRefId = eventId;
|
||||||
}
|
}
|
||||||
eventPickerVisible.value = false;
|
eventPickerVisible.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openSourceEventPicker(): void {
|
|
||||||
if (formModel.triggerType !== "CUSTOMER_EVENT_VALUE_TIER") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!sourceEventPickerColumns.value[0]?.length) {
|
|
||||||
Message.warning("暂无可用的 NUMBER 来源事件");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sourceEventPickerDraft.value = [formModel.sourceEventId];
|
|
||||||
sourceEventPickerVisible.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmSourceEventPicker(value: Array<string | number>): void {
|
|
||||||
const sourceEventId = String(value[0] || "");
|
|
||||||
if (sourceEventId) {
|
|
||||||
const previousSourceEvent = sourceEventList.value.find(item => item.id === formModel.sourceEventId);
|
|
||||||
const nextSourceEvent = sourceEventList.value.find(item => item.id === sourceEventId);
|
|
||||||
if (formModel.sourceEventId && previousSourceEvent?.valueUnit !== nextSourceEvent?.valueUnit) {
|
|
||||||
formModel.tiers = [createTierForm(undefined, true, nextSourceEvent?.valueUnit)];
|
|
||||||
Message.warning("来源事件单位已变化,原分档已清空,请重新配置");
|
|
||||||
}
|
|
||||||
formModel.sourceEventId = sourceEventId;
|
|
||||||
}
|
|
||||||
sourceEventPickerVisible.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function prepareStoreScope(): Promise<void> {
|
async function prepareStoreScope(): Promise<void> {
|
||||||
if (isGlobalManager.value) {
|
if (isGlobalManager.value) {
|
||||||
await currentStoreStore.loadStoreList();
|
await currentStoreStore.loadStoreList();
|
||||||
@@ -383,12 +258,10 @@ async function prepareStoreScope(): Promise<void> {
|
|||||||
async function loadEventList(): Promise<void> {
|
async function loadEventList(): Promise<void> {
|
||||||
const options = isGlobalManager.value ? { storeId: formModel.storeId } : undefined;
|
const options = isGlobalManager.value ? { storeId: formModel.storeId } : undefined;
|
||||||
const result = await EventAPI.list({ index: 0, size: 1000 }, options);
|
const result = await EventAPI.list({ index: 0, size: 1000 }, options);
|
||||||
eventList.value = result.list || [];
|
// 退役消费事件不能作为触发对象
|
||||||
const requiredEventIdList = [formModel.eventId, formModel.sourceEventId].filter((id, index, list) => id && list.indexOf(id) === index);
|
eventList.value = (result.list || []).filter(item => item.bizType !== "LEGACY_CONSUMPTION");
|
||||||
for (const eventId of requiredEventIdList) {
|
if (formModel.triggerRefId && !eventList.value.some(item => item.id === formModel.triggerRefId)) {
|
||||||
if (!eventList.value.some(item => item.id === eventId)) {
|
eventList.value.push(await EventAPI.detail(formModel.triggerRefId));
|
||||||
eventList.value.push(await EventAPI.detail(eventId));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,31 +272,16 @@ const statusOptions: Array<{ label: string; value: PointRuleStatus }> = [
|
|||||||
{ label: "启用", value: "ACTIVE" },
|
{ label: "启用", value: "ACTIVE" },
|
||||||
{ label: "停用", value: "INACTIVE" }
|
{ label: "停用", value: "INACTIVE" }
|
||||||
];
|
];
|
||||||
const triggerOptions: Array<{ label: string; value: PointRuleTriggerType }> = [
|
const triggerSourceOptions: Array<{ label: string; value: PointRuleTriggerSourceType }> = [
|
||||||
{ label: "每次登记", value: "EVERY_TIME" },
|
{ label: "事件登记", value: "EVENT_RECORD" },
|
||||||
{ label: "首次登记", value: "FIRST_TIME" },
|
{ label: "订单全额实收", value: "SALES_ORDER_PAID" }
|
||||||
{ label: "达到阈值后每次登记", value: "AFTER_COUNT_EVERY_TIME" },
|
|
||||||
{ label: "登记时自定义积分", value: "SOURCE_VALUE" },
|
|
||||||
{ label: "按来源事件累计分档", value: "CUSTOMER_EVENT_VALUE_TIER" }
|
|
||||||
];
|
];
|
||||||
const modeOptions = computed<Array<{ label: string; value: PointRuleMode }>>(() => {
|
|
||||||
if (formModel.triggerType === "CUSTOMER_EVENT_VALUE_TIER") {
|
|
||||||
return [{ label: "分档积分", value: "TIER" }];
|
|
||||||
}
|
|
||||||
if (formModel.triggerType === "SOURCE_VALUE") {
|
|
||||||
return [{ label: "使用登记时填写的积分", value: "SOURCE_VALUE" }];
|
|
||||||
}
|
|
||||||
return [{ label: "固定积分", value: "FIXED" }];
|
|
||||||
});
|
|
||||||
const statusPickerVisible = ref(false);
|
const statusPickerVisible = ref(false);
|
||||||
const statusPickerDraft = ref<PickerValue[]>([]);
|
const statusPickerDraft = ref<PickerValue[]>([]);
|
||||||
const statusPickerColumns = [statusOptions];
|
const statusPickerColumns = [statusOptions];
|
||||||
const triggerPickerVisible = ref(false);
|
const triggerSourcePickerVisible = ref(false);
|
||||||
const triggerPickerDraft = ref<PickerValue[]>([]);
|
const triggerSourcePickerDraft = ref<PickerValue[]>([]);
|
||||||
const triggerPickerColumns = [triggerOptions];
|
const triggerSourcePickerColumns = [triggerSourceOptions];
|
||||||
const modePickerVisible = ref(false);
|
|
||||||
const modePickerDraft = ref<PickerValue[]>([]);
|
|
||||||
const modePickerColumns = computed(() => [modeOptions.value]);
|
|
||||||
const pointActionOptions: Array<{ label: string; value: PointAction }> = [
|
const pointActionOptions: Array<{ label: string; value: PointAction }> = [
|
||||||
{ label: "发放积分", value: "EARN" },
|
{ label: "发放积分", value: "EARN" },
|
||||||
{ label: "消耗积分", value: "CONSUME" }
|
{ label: "消耗积分", value: "CONSUME" }
|
||||||
@@ -445,48 +303,23 @@ function confirmStatusPicker(value: Array<string | number>): void {
|
|||||||
statusPickerVisible.value = false;
|
statusPickerVisible.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openTriggerPicker(): void {
|
function openTriggerSourcePicker(): void {
|
||||||
triggerPickerDraft.value = [formModel.triggerType];
|
triggerSourcePickerDraft.value = [formModel.triggerSourceType];
|
||||||
triggerPickerVisible.value = true;
|
triggerSourcePickerVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmTriggerPicker(value: Array<string | number>): void {
|
function confirmTriggerSourcePicker(value: Array<string | number>): void {
|
||||||
const triggerType = value[0];
|
const triggerSourceType = value[0];
|
||||||
if (typeof triggerType === "string" && triggerOptions.some(item => item.value === triggerType)) {
|
if (typeof triggerSourceType === "string" && triggerSourceOptions.some(item => item.value === triggerSourceType)) {
|
||||||
formModel.triggerType = triggerType as PointRuleTriggerType;
|
formModel.triggerSourceType = triggerSourceType as PointRuleTriggerSourceType;
|
||||||
formModel.pointMode = triggerType === "SOURCE_VALUE" ? "SOURCE_VALUE" : triggerType === "CUSTOMER_EVENT_VALUE_TIER" ? "TIER" : "FIXED";
|
if (formModel.triggerSourceType === "SALES_ORDER_PAID") {
|
||||||
|
formModel.triggerRefId = "";
|
||||||
}
|
}
|
||||||
triggerPickerVisible.value = false;
|
|
||||||
}
|
}
|
||||||
|
triggerSourcePickerVisible.value = false;
|
||||||
function openModePicker(): void {
|
|
||||||
if (formModel.triggerType === "CUSTOMER_EVENT_VALUE_TIER") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
modePickerDraft.value = [formModel.pointMode];
|
|
||||||
modePickerVisible.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmModePicker(value: Array<string | number>): void {
|
|
||||||
const pointMode = value[0];
|
|
||||||
if (pointMode === "FIXED" || pointMode === "SOURCE_VALUE" || pointMode === "TIER") {
|
|
||||||
if (!modeOptions.value.some(item => item.value === pointMode)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
formModel.pointMode = pointMode;
|
|
||||||
formModel.triggerType = pointMode === "SOURCE_VALUE"
|
|
||||||
? "SOURCE_VALUE"
|
|
||||||
: pointMode === "TIER"
|
|
||||||
? "CUSTOMER_EVENT_VALUE_TIER"
|
|
||||||
: formModel.triggerType === "SOURCE_VALUE" || formModel.triggerType === "CUSTOMER_EVENT_VALUE_TIER" ? "EVERY_TIME" : formModel.triggerType;
|
|
||||||
}
|
|
||||||
modePickerVisible.value = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openPointActionPicker(): void {
|
function openPointActionPicker(): void {
|
||||||
if (formModel.pointMode === "TIER") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
pointActionPickerDraft.value = [formModel.pointAction];
|
pointActionPickerDraft.value = [formModel.pointAction];
|
||||||
pointActionPickerVisible.value = true;
|
pointActionPickerVisible.value = true;
|
||||||
}
|
}
|
||||||
@@ -503,12 +336,8 @@ function getStatusText(status: PointRuleStatus): string {
|
|||||||
return statusOptions.find(item => item.value === status)?.label || "草稿";
|
return statusOptions.find(item => item.value === status)?.label || "草稿";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTriggerText(triggerType: PointRuleTriggerType): string {
|
function getTriggerSourceText(triggerSourceType: PointRuleTriggerSourceType): string {
|
||||||
return triggerOptions.find(item => item.value === triggerType)?.label || "每次登记";
|
return triggerSourceOptions.find(item => item.value === triggerSourceType)?.label || "事件登记";
|
||||||
}
|
|
||||||
|
|
||||||
function getModeText(pointMode: PointRuleMode): string {
|
|
||||||
return modeOptions.value.find(item => item.value === pointMode)?.label || "固定积分";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPointActionText(action: PointAction): string {
|
function getPointActionText(action: PointAction): string {
|
||||||
@@ -561,9 +390,9 @@ function resolvePositiveInteger(value: string): number | undefined {
|
|||||||
return Number.isSafeInteger(numberValue) && 0 < numberValue ? numberValue : undefined;
|
return Number.isSafeInteger(numberValue) && 0 < numberValue ? numberValue : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveSignedPointValue(value: string, action: PointAction): number | undefined {
|
function resolvePointValue(): number | undefined {
|
||||||
const pointValue = resolvePositiveInteger(value);
|
const pointValue = resolvePositiveInteger(formModel.pointValueText);
|
||||||
return pointValue === undefined ? undefined : action === "CONSUME" ? -pointValue : pointValue;
|
return pointValue === undefined ? undefined : formModel.pointAction === "CONSUME" ? -pointValue : pointValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveTimestamp(value: string): number | undefined {
|
function resolveTimestamp(value: string): number | undefined {
|
||||||
@@ -574,121 +403,23 @@ function resolveTimestamp(value: string): number | undefined {
|
|||||||
return Number.isFinite(timestamp) ? timestamp : undefined;
|
return Number.isFinite(timestamp) ? timestamp : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCondition(): PointRule["conditionJson"] {
|
|
||||||
if (formModel.triggerType === "CUSTOMER_EVENT_VALUE_TIER") {
|
|
||||||
const valueUnit = selectedSourceValueUnit.value;
|
|
||||||
return {
|
|
||||||
sourceEventId: formModel.sourceEventId,
|
|
||||||
sourceValueField: "numberValue",
|
|
||||||
window: {
|
|
||||||
type: "ROLLING",
|
|
||||||
amount: resolvePositiveInteger(formModel.windowAmountText) || 1,
|
|
||||||
unit: "YEAR"
|
|
||||||
},
|
|
||||||
tiers: formModel.tiers.map((tier, index) => {
|
|
||||||
const min = getTierMinValue(formModel.tiers, index, valueUnit);
|
|
||||||
const result: PointRuleTier = {
|
|
||||||
min: min ?? 0,
|
|
||||||
pointValue: (tier.pointAction === "CONSUME" ? -1 : 1) * (resolveNonNegativeInteger(tier.pointValueText) || 0)
|
|
||||||
};
|
|
||||||
if (!isLastTier(formModel.tiers, index)) {
|
|
||||||
result.max = resolveTierBoundaryValue(tier.maxText, valueUnit);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
})
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (formModel.pointMode === "SOURCE_VALUE") {
|
|
||||||
return {
|
|
||||||
min: resolveSignedPointValue(formModel.minText, formModel.pointAction),
|
|
||||||
max: resolveSignedPointValue(formModel.maxText, formModel.pointAction)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (formModel.triggerType === "FIRST_TIME") {
|
|
||||||
return { scope: "CUSTOMER_EVENT" };
|
|
||||||
}
|
|
||||||
if (formModel.triggerType === "AFTER_COUNT_EVERY_TIME") {
|
|
||||||
return {
|
|
||||||
threshold: resolvePositiveInteger(formModel.thresholdText),
|
|
||||||
includeCurrent: true,
|
|
||||||
scope: "CUSTOMER_EVENT"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateForm(): boolean {
|
function validateForm(): boolean {
|
||||||
if (!formModel.storeId) {
|
if (!formModel.storeId) {
|
||||||
Message.warning("请选择门店");
|
Message.warning("请选择门店");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!formModel.eventId) {
|
|
||||||
Message.warning("请选择登记事件");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!formModel.name.trim()) {
|
if (!formModel.name.trim()) {
|
||||||
Message.warning("请输入规则名称");
|
Message.warning("请输入规则名称");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (formModel.triggerType === "CUSTOMER_EVENT_VALUE_TIER") {
|
if (formModel.triggerSourceType === "EVENT_RECORD" && !formModel.triggerRefId) {
|
||||||
if (formModel.pointMode !== "TIER") {
|
Message.warning("请选择触发事件");
|
||||||
Message.warning("分档规则必须使用分档积分模式");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!selectedSourceEvent.value) {
|
if (!resolvePositiveInteger(formModel.pointValueText)) {
|
||||||
Message.warning("请选择 NUMBER 类型的来源事件");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (selectedSourceValueUnit.value !== "CENT" && selectedSourceValueUnit.value !== "COUNT") {
|
|
||||||
Message.warning("来源事件必须配置数值单位");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!resolvePositiveInteger(formModel.windowAmountText)) {
|
|
||||||
Message.warning("累计窗口必须是正整数");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!formModel.tiers.length || !(formModel.tiers.length < 100)) {
|
|
||||||
Message.warning("分档数量必须是 1-100 档");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (let i = 0; i < formModel.tiers.length; i++) {
|
|
||||||
const tier = formModel.tiers[i];
|
|
||||||
const min = getTierMinValue(formModel.tiers, i, selectedSourceValueUnit.value);
|
|
||||||
const pointValue = resolveNonNegativeInteger(tier.pointValueText);
|
|
||||||
if (min === undefined) {
|
|
||||||
Message.warning(`第 ${i + 1} 档的上一档最大值无效`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (pointValue === undefined) {
|
|
||||||
Message.warning(`第 ${i + 1} 档积分数量必须是大于等于 0 的整数`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!isLastTier(formModel.tiers, i)) {
|
|
||||||
const max = resolveTierBoundaryValue(tier.maxText, selectedSourceValueUnit.value);
|
|
||||||
if (max === undefined || max < min) {
|
|
||||||
Message.warning(selectedSourceValueUnit.value === "CENT"
|
|
||||||
? `第 ${i + 1} 档最大值必须是不小于最小值且最多两位小数`
|
|
||||||
: `第 ${i + 1} 档最大值必须是不小于最小值的整数`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (formModel.pointMode === "FIXED" && !resolvePositiveInteger(formModel.pointValueText)) {
|
|
||||||
Message.warning("积分数量必须是正整数");
|
Message.warning("积分数量必须是正整数");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (formModel.triggerType === "AFTER_COUNT_EVERY_TIME" && !resolvePositiveInteger(formModel.thresholdText)) {
|
|
||||||
Message.warning("请输入有效的触发阈值");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (formModel.pointMode === "SOURCE_VALUE") {
|
|
||||||
const min = resolvePositiveInteger(formModel.minText);
|
|
||||||
const max = resolvePositiveInteger(formModel.maxText);
|
|
||||||
if (!min || !max || max < min) {
|
|
||||||
Message.warning("积分数量范围必须是正整数,且最大值不能小于最小值");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const beginAt = resolveTimestamp(formModel.beginAtText);
|
const beginAt = resolveTimestamp(formModel.beginAtText);
|
||||||
const endAt = resolveTimestamp(formModel.endAtText);
|
const endAt = resolveTimestamp(formModel.endAtText);
|
||||||
if (beginAt !== undefined && endAt !== undefined && endAt < beginAt) {
|
if (beginAt !== undefined && endAt !== undefined && endAt < beginAt) {
|
||||||
@@ -707,13 +438,14 @@ async function saveRule(): Promise<void> {
|
|||||||
const payload: PointRule = {
|
const payload: PointRule = {
|
||||||
id: formModel.id || undefined,
|
id: formModel.id || undefined,
|
||||||
storeId: formModel.storeId,
|
storeId: formModel.storeId,
|
||||||
eventId: formModel.eventId,
|
ruleMode: "SIMPLE",
|
||||||
|
triggerSourceType: formModel.triggerSourceType,
|
||||||
|
triggerRefType: formModel.triggerSourceType === "EVENT_RECORD" ? "EVENT" : undefined,
|
||||||
|
triggerRefId: formModel.triggerSourceType === "EVENT_RECORD" ? formModel.triggerRefId : undefined,
|
||||||
name: formModel.name.trim(),
|
name: formModel.name.trim(),
|
||||||
status: formModel.status,
|
status: formModel.status,
|
||||||
triggerType: formModel.triggerType,
|
pointValue: resolvePointValue() ?? null,
|
||||||
pointMode: formModel.pointMode,
|
groovyScriptId: undefined,
|
||||||
pointValue: formModel.pointMode === "FIXED" ? resolveSignedPointValue(formModel.pointValueText, formModel.pointAction) : null,
|
|
||||||
conditionJson: buildCondition(),
|
|
||||||
beginAt: resolveTimestamp(formModel.beginAtText),
|
beginAt: resolveTimestamp(formModel.beginAtText),
|
||||||
endAt: resolveTimestamp(formModel.endAtText),
|
endAt: resolveTimestamp(formModel.endAtText),
|
||||||
exclusive: formModel.exclusive,
|
exclusive: formModel.exclusive,
|
||||||
@@ -742,37 +474,19 @@ async function fillForm(): Promise<void> {
|
|||||||
Object.assign(formModel, {
|
Object.assign(formModel, {
|
||||||
id: current.id || "",
|
id: current.id || "",
|
||||||
storeId: current.storeId || formModel.storeId,
|
storeId: current.storeId || formModel.storeId,
|
||||||
eventId: current.eventId || "",
|
triggerSourceType: current.triggerSourceType || "EVENT_RECORD",
|
||||||
|
triggerRefId: current.triggerRefId || "",
|
||||||
name: current.name || "",
|
name: current.name || "",
|
||||||
status: current.status === "ACTIVE" ? "ACTIVE" : current.status === "INACTIVE" ? "INACTIVE" : "DRAFT",
|
status: current.status === "ACTIVE" ? "ACTIVE" : current.status === "INACTIVE" ? "INACTIVE" : "DRAFT",
|
||||||
triggerType: current.triggerType || "EVERY_TIME",
|
pointAction: current.pointValue !== undefined && current.pointValue !== null && current.pointValue < 0 ? "CONSUME" : "EARN",
|
||||||
pointMode: current.pointMode || "FIXED",
|
|
||||||
pointAction: current.pointValue !== undefined && current.pointValue !== null
|
|
||||||
? current.pointValue < 0 ? "CONSUME" : "EARN"
|
|
||||||
: current.conditionJson?.min !== undefined && current.conditionJson.min < 0 ? "CONSUME" : "EARN",
|
|
||||||
pointValueText: current.pointValue === undefined || current.pointValue === null ? "" : String(Math.abs(current.pointValue)),
|
pointValueText: current.pointValue === undefined || current.pointValue === null ? "" : String(Math.abs(current.pointValue)),
|
||||||
thresholdText: current.conditionJson?.threshold === undefined ? "" : String(current.conditionJson.threshold),
|
|
||||||
minText: current.conditionJson?.min === undefined ? "" : String(Math.abs(current.conditionJson.min)),
|
|
||||||
maxText: current.conditionJson?.max === undefined ? "" : String(Math.abs(current.conditionJson.max)),
|
|
||||||
sourceEventId: current.conditionJson?.sourceEventId || "",
|
|
||||||
windowAmountText: current.conditionJson?.window?.amount === undefined ? "1" : String(current.conditionJson.window.amount),
|
|
||||||
tiers: [],
|
|
||||||
beginAtText: current.beginAt ? Time.toDateTime(current.beginAt) : "",
|
beginAtText: current.beginAt ? Time.toDateTime(current.beginAt) : "",
|
||||||
endAtText: current.endAt ? Time.toDateTime(current.endAt) : "",
|
endAtText: current.endAt ? Time.toDateTime(current.endAt) : "",
|
||||||
exclusive: current.exclusive ?? false,
|
exclusive: current.exclusive ?? false,
|
||||||
remark: current.remark || ""
|
remark: current.remark || ""
|
||||||
});
|
});
|
||||||
await loadEventList();
|
|
||||||
const valueUnit = selectedSourceValueUnit.value;
|
|
||||||
formModel.tiers = current.conditionJson?.tiers?.length
|
|
||||||
? current.conditionJson.tiers.map((tier, index, list) => createTierForm(tier, index === list.length - 1, valueUnit))
|
|
||||||
: [createTierForm(undefined, true, valueUnit)];
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
await loadEventList();
|
await loadEventList();
|
||||||
if (!formModel.tiers.length) {
|
|
||||||
formModel.tiers.push(createTierForm(undefined, true, selectedSourceValueUnit.value));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -821,54 +535,10 @@ onUnmounted(() => navBarStore.clearRight());
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.source-condition {
|
.form-tips {
|
||||||
padding: .75rem 1rem 1rem;
|
padding: 0 1rem;
|
||||||
|
font-size: .8125rem;
|
||||||
.source-title {
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.source-inputs {
|
|
||||||
gap: .5rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: .5rem;
|
|
||||||
|
|
||||||
.t-input {
|
|
||||||
flex: 1;
|
|
||||||
border: 1px solid var(--td-component-border);
|
|
||||||
padding: .25rem .5rem;
|
|
||||||
border-radius: .5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tier-condition {
|
|
||||||
padding: .75rem 1rem 1rem;
|
|
||||||
|
|
||||||
.window-row {
|
|
||||||
gap: .5rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: .5rem 0;
|
|
||||||
|
|
||||||
.t-input {
|
|
||||||
flex: 1;
|
|
||||||
border: 1px solid var(--td-component-border);
|
|
||||||
padding: .25rem .5rem;
|
|
||||||
border-radius: .5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.window-label {
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tips {
|
|
||||||
color: #52607a;
|
color: #52607a;
|
||||||
font-size: .75rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -880,15 +550,9 @@ onUnmounted(() => navBarStore.clearRight());
|
|||||||
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
|
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
|
||||||
|
|
||||||
.picker-text,
|
.picker-text,
|
||||||
.source-condition p {
|
.form-tips {
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.source-inputs .t-input {
|
|
||||||
background: rgba(15, 23, 42, .72);
|
|
||||||
border-color: rgba(148, 163, 184, .16);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,8 +45,8 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #description>
|
<template #description>
|
||||||
<div class="description">
|
<div class="description">
|
||||||
<div v-text="`登记事件:${item.eventId ? eventNameMap.get(item.eventId) : '未绑定事件'}`"></div>
|
<div v-text="`触发来源:${getTriggerSourceText(item)}`"></div>
|
||||||
<div v-text="`触发类型:${getTriggerText(item)}`"></div>
|
<div v-text="`触发对象:${getTriggerRefText(item)}`"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</t-cell>
|
</t-cell>
|
||||||
@@ -220,20 +220,20 @@ async function loadEventList(): Promise<void> {
|
|||||||
|
|
||||||
// ---------- 展示与导航 ----------
|
// ---------- 展示与导航 ----------
|
||||||
|
|
||||||
function getTriggerText(rule: PointRule): string {
|
function getTriggerSourceText(rule: PointRule): string {
|
||||||
const map: Record<string, string> = {
|
return rule.triggerSourceType === "SALES_ORDER_PAID" ? "订单全额实收" : "事件登记";
|
||||||
EVERY_TIME: "每次登记",
|
}
|
||||||
FIRST_TIME: "首次登记",
|
|
||||||
AFTER_COUNT_EVERY_TIME: `达到 ${rule.conditionJson?.threshold ?? 0} 次后每次登记`,
|
function getTriggerRefText(rule: PointRule): string {
|
||||||
SOURCE_VALUE: "登记时自定义积分",
|
if (rule.triggerSourceType === "SALES_ORDER_PAID") {
|
||||||
CUSTOMER_EVENT_VALUE_TIER: `按 ${rule.conditionJson?.window?.amount ?? 1} 年累计分档`
|
return "订单";
|
||||||
};
|
}
|
||||||
return map[rule.triggerType || ""] || "未知触发类型";
|
return rule.triggerRefId ? eventNameMap.value.get(rule.triggerRefId) || "未知事件" : "未绑定事件";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPointText(rule: PointRule): string {
|
function getPointText(rule: PointRule): string {
|
||||||
if (rule.pointMode === "TIER") {
|
if (rule.ruleMode === "GROOVY") {
|
||||||
return "分档";
|
return "脚本";
|
||||||
}
|
}
|
||||||
if (rule.pointValue === undefined || rule.pointValue === null) {
|
if (rule.pointValue === undefined || rule.pointValue === null) {
|
||||||
return "?";
|
return "?";
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
<template #description>
|
<template #description>
|
||||||
<div class="description">
|
<div class="description">
|
||||||
<span v-text="'积分值:' + getPointText(item)" />
|
<span v-text="'积分值:' + getPointText(item)" />
|
||||||
<span v-text="'登记事件:' + (eventNameMap.get(item.eventId || '') || '未绑定事件')" />
|
<span v-text="'触发对象:' + (item.triggerRefId ? eventNameMap.get(item.triggerRefId) || '未知事件' : '任意订单')" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #note>
|
<template #note>
|
||||||
@@ -66,11 +66,11 @@ const sortDirty = ref(false);
|
|||||||
const eventNameMap = computed(() => new Map(eventList.value.filter(item => item.id).map(item => [item.id as string, item.name])));
|
const eventNameMap = computed(() => new Map(eventList.value.filter(item => item.id).map(item => [item.id as string, item.name])));
|
||||||
|
|
||||||
function getPointText(rule: PointRule): string {
|
function getPointText(rule: PointRule): string {
|
||||||
if (rule.pointMode === "TIER") {
|
if (rule.ruleMode === "GROOVY") {
|
||||||
return "分档";
|
return "脚本";
|
||||||
}
|
}
|
||||||
if (rule.pointValue === undefined || rule.pointValue === null) {
|
if (rule.pointValue === undefined || rule.pointValue === null) {
|
||||||
return "按登记值";
|
return "未配置";
|
||||||
}
|
}
|
||||||
return (0 < rule.pointValue ? "+" : "") + rule.pointValue;
|
return (0 < rule.pointValue ? "+" : "") + rule.pointValue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,343 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="tier-editor">
|
|
||||||
<div class="title">
|
|
||||||
<div v-text="`累计数值分档(${valueUnitText})`" />
|
|
||||||
<div class="tips gray" v-text="`首档起点可自定义,末档不设上限;分档单位为${valueUnitText},积分为 0 不产生流水`" />
|
|
||||||
</div>
|
|
||||||
<div class="list">
|
|
||||||
<div
|
|
||||||
v-for="(tier, index) in tiers"
|
|
||||||
:key="tier.key"
|
|
||||||
class="card"
|
|
||||||
@click="openTierEditor(index)"
|
|
||||||
>
|
|
||||||
<div class="summary">
|
|
||||||
<span class="index" v-text="`第 ${index + 1} 档`" />
|
|
||||||
<strong v-text="getTierRangeText(index)" />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="point text-bold"
|
|
||||||
:class="{ 'consume': tier.pointAction === 'CONSUME' }"
|
|
||||||
v-text="getTierPointText(tier)"
|
|
||||||
/>
|
|
||||||
<t-button
|
|
||||||
v-if="1 < tiers.length"
|
|
||||||
theme="danger"
|
|
||||||
variant="text"
|
|
||||||
shape="square"
|
|
||||||
@click.stop="removeTier(index)"
|
|
||||||
>
|
|
||||||
<t-icon name="close" />
|
|
||||||
</t-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<t-button class="add-tier" variant="outline" size="small" content="增加分档" @click="addTier" />
|
|
||||||
</div>
|
|
||||||
<bottom-popup
|
|
||||||
v-model="tierEditorVisible"
|
|
||||||
:title="tierEditorTitle"
|
|
||||||
@left-button-click="closeTierEditor"
|
|
||||||
@right-button-click="confirmTierEditor"
|
|
||||||
>
|
|
||||||
<div class="tier-editor-popup">
|
|
||||||
<t-form v-if="tierEditorIndex >= 0" class="form" label-align="left" label-width="10rem">
|
|
||||||
<t-form-item v-if="tierEditorIndex < 1" :label="`最小值(${valueUnitText})`" required-mark>
|
|
||||||
<t-input
|
|
||||||
v-model.trim="tierEditorDraft.minText"
|
|
||||||
type="number"
|
|
||||||
:step="valueUnit === 'CENT' ? '0.01' : '1'"
|
|
||||||
align="right"
|
|
||||||
borderless
|
|
||||||
:placeholder="valueUnit === 'CENT' ? '最多两位小数' : '请输入非负整数'"
|
|
||||||
/>
|
|
||||||
</t-form-item>
|
|
||||||
<t-form-item v-else :label="`最小值(${valueUnitText})`" content-align="right">
|
|
||||||
<span class="gray" v-text="getTierMinText(tiers, tierEditorIndex, valueUnit)" />
|
|
||||||
</t-form-item>
|
|
||||||
<t-form-item
|
|
||||||
v-if="!isLastTier(tiers, tierEditorIndex) || tierEditorAdding"
|
|
||||||
:label="`${tierEditorAdding ? '当前档最大值' : '最大值'}(${valueUnitText})`"
|
|
||||||
required-mark
|
|
||||||
>
|
|
||||||
<t-input
|
|
||||||
v-model.trim="tierEditorDraft.maxText"
|
|
||||||
type="number"
|
|
||||||
:step="valueUnit === 'CENT' ? '0.01' : '1'"
|
|
||||||
align="right"
|
|
||||||
borderless
|
|
||||||
:placeholder="valueUnit === 'CENT' ? '最多两位小数' : '请输入整数'"
|
|
||||||
/>
|
|
||||||
</t-form-item>
|
|
||||||
<t-form-item label="积分动作" content-align="right">
|
|
||||||
<t-radio-group v-model="tierEditorDraft.pointAction" variant="default-filled">
|
|
||||||
<t-radio :block="false" borderless value="EARN">发放</t-radio>
|
|
||||||
<t-radio :block="false" borderless value="CONSUME">消耗</t-radio>
|
|
||||||
</t-radio-group>
|
|
||||||
</t-form-item>
|
|
||||||
<t-form-item label="积分数量" required-mark>
|
|
||||||
<t-input
|
|
||||||
v-model.trim="tierEditorDraft.pointValueText"
|
|
||||||
type="number"
|
|
||||||
align="right"
|
|
||||||
borderless
|
|
||||||
placeholder="请输入数量,可填写 0"
|
|
||||||
/>
|
|
||||||
</t-form-item>
|
|
||||||
<div class="tips">积分为 0 时规则仍会命中,但系统不会创建积分流水。</div>
|
|
||||||
</t-form>
|
|
||||||
</div>
|
|
||||||
</bottom-popup>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { BottomPopup } from "timi-tdesign-mobile";
|
|
||||||
import type { EventValueUnit } from "@/types/EventItem";
|
|
||||||
import type { PointAction, PointRuleTierForm } from "@/types/Point";
|
|
||||||
import { formatTierBoundaryValue, getTierMinText, getTierMinValue, isLastTier, resolveNonNegativeInteger, resolveTierBoundaryValue } from "@/utils/pointRule";
|
|
||||||
import { Message } from "@/utils/message";
|
|
||||||
|
|
||||||
defineOptions({
|
|
||||||
name: "PointRuleTierEditor"
|
|
||||||
});
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
valueUnit?: EventValueUnit | null;
|
|
||||||
}>();
|
|
||||||
const tiers = defineModel<PointRuleTierForm[]>({ required: true });
|
|
||||||
const tierEditorVisible = ref(false);
|
|
||||||
const tierEditorIndex = ref(-1);
|
|
||||||
const tierEditorAdding = ref(false);
|
|
||||||
const tierEditorDraft = reactive({
|
|
||||||
minText: "",
|
|
||||||
maxText: "",
|
|
||||||
pointAction: "EARN" as PointAction,
|
|
||||||
pointValueText: ""
|
|
||||||
});
|
|
||||||
const tierEditorTitle = computed(() => {
|
|
||||||
if (tierEditorIndex.value < 0) {
|
|
||||||
return "编辑分档";
|
|
||||||
}
|
|
||||||
return tierEditorAdding.value ? `编辑第 ${tierEditorIndex.value + 1} 档并新增下一档` : `编辑第 ${tierEditorIndex.value + 1} 档`;
|
|
||||||
});
|
|
||||||
const valueUnit = computed(() => props.valueUnit);
|
|
||||||
const valueUnitText = computed(() => valueUnit.value === "CENT" ? "元" : valueUnit.value === "COUNT" ? "次" : "数值");
|
|
||||||
|
|
||||||
function getTierRangeText(index: number): string {
|
|
||||||
const tier = tiers.value[index];
|
|
||||||
if (!tier) {
|
|
||||||
return "待填写";
|
|
||||||
}
|
|
||||||
const minText = getTierMinText(tiers.value, index, valueUnit.value);
|
|
||||||
if (isLastTier(tiers.value, index)) {
|
|
||||||
return `${minText} 及以上`;
|
|
||||||
}
|
|
||||||
const max = resolveTierBoundaryValue(tier.maxText, valueUnit.value);
|
|
||||||
return max === undefined ? `${minText} - 待填写` : `${minText}-${formatTierBoundaryValue(max, valueUnit.value)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTierPointText(tier: PointRuleTierForm): string {
|
|
||||||
const pointValue = resolveNonNegativeInteger(tier.pointValueText);
|
|
||||||
if (pointValue === undefined) {
|
|
||||||
return "待填写";
|
|
||||||
}
|
|
||||||
return `${tier.pointAction === "CONSUME" ? "-" : "+"}${pointValue} 分`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function openTierEditor(index: number, adding = false): void {
|
|
||||||
const tier = tiers.value[index];
|
|
||||||
if (!tier) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
tierEditorIndex.value = index;
|
|
||||||
tierEditorAdding.value = adding;
|
|
||||||
tierEditorDraft.minText = index < 1 ? tier.minText : "";
|
|
||||||
tierEditorDraft.maxText = adding ? "" : tier.maxText;
|
|
||||||
tierEditorDraft.pointAction = tier.pointAction === "CONSUME" ? "CONSUME" : "EARN";
|
|
||||||
tierEditorDraft.pointValueText = tier.pointValueText;
|
|
||||||
tierEditorVisible.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeTierEditor(): void {
|
|
||||||
tierEditorAdding.value = false;
|
|
||||||
tierEditorVisible.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmTierEditor(): void {
|
|
||||||
const index = tierEditorIndex.value;
|
|
||||||
const tier = tiers.value[index];
|
|
||||||
if (!tier) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const min = index < 1
|
|
||||||
? resolveTierBoundaryValue(tierEditorDraft.minText, valueUnit.value)
|
|
||||||
: getTierMinValue(tiers.value, index, valueUnit.value);
|
|
||||||
const pointValue = resolveNonNegativeInteger(tierEditorDraft.pointValueText);
|
|
||||||
if (min === undefined) {
|
|
||||||
Message.warning(index < 1
|
|
||||||
? valueUnit.value === "CENT" ? "首档最小值必须是非负且最多两位小数" : "首档最小值必须是非负整数"
|
|
||||||
: "请先修正上一档的最大值");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (pointValue === undefined) {
|
|
||||||
Message.warning("积分数量必须是大于等于 0 的整数,允许填写 0");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const shouldAddTier = tierEditorAdding.value && isLastTier(tiers.value, index);
|
|
||||||
if (!isLastTier(tiers.value, index) || shouldAddTier) {
|
|
||||||
const max = resolveTierBoundaryValue(tierEditorDraft.maxText, valueUnit.value);
|
|
||||||
if (max === undefined || max < min) {
|
|
||||||
Message.warning(valueUnit.value === "CENT" ? "当前档最大值必须是不小于最小值且最多两位小数" : "当前档最大值必须是不小于最小值的整数");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
tier.maxText = formatTierBoundaryValue(max, valueUnit.value);
|
|
||||||
} else {
|
|
||||||
tier.maxText = "";
|
|
||||||
}
|
|
||||||
if (index < 1) {
|
|
||||||
tier.minText = formatTierBoundaryValue(min, valueUnit.value);
|
|
||||||
}
|
|
||||||
tier.pointValueText = String(pointValue);
|
|
||||||
tier.pointAction = tierEditorDraft.pointAction;
|
|
||||||
if (shouldAddTier) {
|
|
||||||
tiers.value.push(createTierForm());
|
|
||||||
}
|
|
||||||
closeTierEditor();
|
|
||||||
}
|
|
||||||
|
|
||||||
function addTier(): void {
|
|
||||||
if (!(tiers.value.length < 100)) {
|
|
||||||
Message.warning("最多配置 100 档");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const lastTierIndex = tiers.value.length - 1;
|
|
||||||
if (!(lastTierIndex < 0)) {
|
|
||||||
openTierEditor(lastTierIndex, true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
tiers.value.push(createTierForm());
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeTier(index: number): void {
|
|
||||||
if (!(1 < tiers.value.length)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
tiers.value.splice(index, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createTierForm(): PointRuleTierForm {
|
|
||||||
const key = tiers.value.reduce((max, tier) => Math.max(max, tier.key), -1) + 1;
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
minText: "0",
|
|
||||||
maxText: "",
|
|
||||||
pointAction: "EARN",
|
|
||||||
pointValueText: "0"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.tier-editor {
|
|
||||||
padding: 1rem;
|
|
||||||
|
|
||||||
.tips {
|
|
||||||
font-size: .75rem;
|
|
||||||
margin-top: .25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list {
|
|
||||||
gap: .5rem;
|
|
||||||
display: grid;
|
|
||||||
margin-top: .5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: .5rem .75rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
background: var(--td-bg-color-container);
|
|
||||||
border: 1px solid var(--td-component-border);
|
|
||||||
border-radius: .75rem;
|
|
||||||
|
|
||||||
.summary {
|
|
||||||
gap: .25rem;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
strong {
|
|
||||||
font-size: .9375rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.index {
|
|
||||||
color: var(--td-brand-color);
|
|
||||||
font-size: .75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.point {
|
|
||||||
gap: .25rem;
|
|
||||||
color: var(--td-brand-color);
|
|
||||||
display: flex;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
align-items: baseline;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
&.consume {
|
|
||||||
color: var(--td-error-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-tier {
|
|
||||||
width: 100%;
|
|
||||||
margin-top: .75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tier-editor-popup {
|
|
||||||
|
|
||||||
.form {
|
|
||||||
padding: .5rem 0 1rem 0;
|
|
||||||
|
|
||||||
.tips {
|
|
||||||
color: #52607a;
|
|
||||||
padding: 0 1rem;
|
|
||||||
font-size: .75rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
margin-top: .5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(html.theme-dark) {
|
|
||||||
.tier-editor {
|
|
||||||
.tips,
|
|
||||||
.points span {
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background: rgba(15, 23, 42, .72);
|
|
||||||
border-color: rgba(148, 163, 184, .16);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tier-editor-popup {
|
|
||||||
background: #0f172a;
|
|
||||||
|
|
||||||
:deep(.t-input) {
|
|
||||||
background: rgba(15, 23, 42, .72);
|
|
||||||
border-color: rgba(148, 163, 184, .16);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form {
|
|
||||||
.picker-text,
|
|
||||||
.tips {
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="avatar placeholder"
|
class="avatar fallback"
|
||||||
v-text="(item.name || '产').slice(0, 1)"
|
v-text="(item.name || '产').slice(0, 1)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="avatar placeholder"
|
class="avatar fallback"
|
||||||
v-text="(item.product?.name || '产').slice(0, 1)"
|
v-text="(item.product?.name || '产').slice(0, 1)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -92,7 +92,7 @@ const isNoMore = ref(false);
|
|||||||
let keywordTimer: number | undefined;
|
let keywordTimer: number | undefined;
|
||||||
let requestToken = 0;
|
let requestToken = 0;
|
||||||
|
|
||||||
const emptyText = computed(() => isLoading.value ? "正在加载库存" : keyword.value ? "未找到匹配库存" : "暂无库存记录");
|
const emptyText = computed(() => isLoading.value ? "正在加载库存" : keyword.value ? "未找到匹配产品" : "暂无启用库存管理的产品");
|
||||||
|
|
||||||
async function fetch(reset = false): Promise<void> {
|
async function fetch(reset = false): Promise<void> {
|
||||||
if (!reset && (isLoading.value || isNoMore.value)) {
|
if (!reset && (isLoading.value || isNoMore.value)) {
|
||||||
|
|||||||
@@ -204,10 +204,25 @@ function openManualRevoke(): void {
|
|||||||
|
|
||||||
async function openRelatedRecord(id?: string): Promise<void> {
|
async function openRelatedRecord(id?: string): Promise<void> {
|
||||||
if (id) {
|
if (id) {
|
||||||
await router.push({ name: "ProductStockRecordDetail", params: { id } });
|
await router.push({
|
||||||
|
name: "ProductStockRecordDetail",
|
||||||
|
params: { id },
|
||||||
|
query: { transitionDepth: String(getCurrentRouteDepth() + 1) }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCurrentRouteDepth(): number {
|
||||||
|
const transitionDepth = Array.isArray(route.query.transitionDepth)
|
||||||
|
? route.query.transitionDepth[0]
|
||||||
|
: route.query.transitionDepth;
|
||||||
|
const depth = Number(transitionDepth);
|
||||||
|
if (transitionDepth && Number.isInteger(depth) && 0 <= depth && depth <= 20) {
|
||||||
|
return depth;
|
||||||
|
}
|
||||||
|
return Number(route.meta.depth ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
async function submitManualRevoke(): Promise<void> {
|
async function submitManualRevoke(): Promise<void> {
|
||||||
if (!canManualRevoke.value || !record.value?.id || isOperating.value) {
|
if (!canManualRevoke.value || !record.value?.id || isOperating.value) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -123,10 +123,25 @@ function getDeltaClass(value?: number | string): string {
|
|||||||
|
|
||||||
async function toDetail(record: StockRecord): Promise<void> {
|
async function toDetail(record: StockRecord): Promise<void> {
|
||||||
if (record.id) {
|
if (record.id) {
|
||||||
await router.push({ name: "ProductStockRecordDetail", params: { id: record.id } });
|
await router.push({
|
||||||
|
name: "ProductStockRecordDetail",
|
||||||
|
params: { id: record.id },
|
||||||
|
query: { transitionDepth: String(getCurrentRouteDepth() + 1) }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCurrentRouteDepth(): number {
|
||||||
|
const transitionDepth = Array.isArray(route.query.transitionDepth)
|
||||||
|
? route.query.transitionDepth[0]
|
||||||
|
: route.query.transitionDepth;
|
||||||
|
const depth = Number(transitionDepth);
|
||||||
|
if (transitionDepth && Number.isInteger(depth) && 0 <= depth && depth <= 20) {
|
||||||
|
return depth;
|
||||||
|
}
|
||||||
|
return Number(route.meta.depth ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
async function loadMore(force = false): Promise<void> {
|
async function loadMore(force = false): Promise<void> {
|
||||||
if (!force && (loading.value || noMore.value)) {
|
if (!force && (loading.value || noMore.value)) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="avatar placeholder"
|
class="avatar fallback"
|
||||||
v-text="(props.product.name || props.product.code || '产').slice(0, 1)"
|
v-text="(props.product.name || props.product.code || '产').slice(0, 1)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-can="permissionCodes.salesOrder.cancel" class="sales-order-operation">
|
<div v-can="permissionCodes.salesOrder.cancel" class="sales-order-operation">
|
||||||
<t-cell-group v-if="order" title="销售订单" theme="card"><t-cell title="订单号" :note="order.orderNo || order.id" /><t-cell title="当前状态" :note="order.status || '未知'" /></t-cell-group>
|
<t-cell-group v-if="order" title="销售订单" theme="card"><t-cell title="订单号" :note="order.orderNo || order.id" /><t-cell title="当前状态" :note="order.status || '未知'" /></t-cell-group>
|
||||||
|
<div v-if="order" class="revoke-tip">取消订单会立即撤销本单有效消费;受影响的订单积分和签到消费条件积分将由系统自动追加撤销流水。退款仍需按资金流程处理。</div>
|
||||||
<t-form v-if="order" class="form" label-align="left" label-width="6rem">
|
<t-form v-if="order" class="form" label-align="left" label-width="6rem">
|
||||||
<t-form-item label="取消原因" required-mark><t-textarea v-model.trim="reason" borderless autosize maxlength="500" placeholder="请输入取消原因" /></t-form-item>
|
<t-form-item label="取消原因" required-mark><t-textarea v-model.trim="reason" borderless autosize maxlength="500" placeholder="请输入取消原因" /></t-form-item>
|
||||||
</t-form>
|
</t-form>
|
||||||
@@ -48,6 +49,7 @@ onUnmounted(() => navBarStore.clearRight());
|
|||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.sales-order-operation { gap: 1rem; padding: calc(var(--app-nav-height) + 1rem) 1rem 2rem; display: flex; flex-direction: column;
|
.sales-order-operation { gap: 1rem; padding: calc(var(--app-nav-height) + 1rem) 1rem 2rem; display: flex; flex-direction: column;
|
||||||
|
.revoke-tip { padding: .75rem; color: var(--td-warning-color); font-size: .8125rem; line-height: 1.5; background: var(--td-warning-color-1); border-radius: .75rem; }
|
||||||
.form { padding: 0 .5rem; background: var(--td-bg-color-container); box-shadow: var(--td-shadow-1); border-radius: 1rem; overflow: hidden; }
|
.form { padding: 0 .5rem; background: var(--td-bg-color-container); box-shadow: var(--td-shadow-1); border-radius: 1rem; overflow: hidden; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,140 +1,201 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-can="permissionCodes.salesOrder.create" class="sales-order-create">
|
<div v-can="permissionCodes.salesOrder.create" class="sales-order-create">
|
||||||
<header class="intro">
|
<t-form class="form" label-align="left" label-width="5.25rem" v-entrance-transition>
|
||||||
<span class="eyebrow">销售订单</span>
|
<t-form-item label="客户" required-mark>
|
||||||
<h1>创建直接产品订单</h1>
|
<t-input
|
||||||
<p>选择客户并添加本次购买的产品,提交后即可继续处理收款。</p>
|
:model-value="customerDisplayText"
|
||||||
</header>
|
class="customer"
|
||||||
|
readonly
|
||||||
<section class="section customer-section">
|
borderless
|
||||||
<div class="section-head">
|
placeholder="选择或扫码客户"
|
||||||
<div>
|
@click="customerSelectorVisible = true"
|
||||||
<span class="step" v-text="'01 / 客户'" />
|
>
|
||||||
<h2>选择客户</h2>
|
<template #suffix-icon>
|
||||||
<p>订单将归档到所选客户名下</p>
|
<div class="actions">
|
||||||
|
<t-button
|
||||||
|
theme="primary"
|
||||||
|
variant="outline"
|
||||||
|
size="extra-small"
|
||||||
|
@click.stop="customerSelectorVisible = true"
|
||||||
|
>选择</t-button>
|
||||||
|
<t-button theme="primary" size="extra-small" @click.stop="toggleScanner">
|
||||||
|
<span v-text="scannerVisible ? '关闭扫码' : '扫码'" />
|
||||||
|
</t-button>
|
||||||
</div>
|
</div>
|
||||||
<span class="required" v-text="'必填'" />
|
</template>
|
||||||
|
</t-input>
|
||||||
|
</t-form-item>
|
||||||
|
<div v-if="scannerVisible" class="code-scanner-wrapper">
|
||||||
|
<code-scanner
|
||||||
|
visible
|
||||||
|
:status-text="scanStatus"
|
||||||
|
active-text="正在识别客户编码,请将条码或二维码移入取景框。"
|
||||||
|
@decode="handleDecodedText"
|
||||||
|
@status-change="handleScanStatusChange"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button class="customer-picker" type="button" @click="customerSelectorVisible = true">
|
<customer-profile-card
|
||||||
<span class="customer-avatar" :class="{selected: !!customer}" v-text="customer?.name?.slice(0, 1) || '客'" />
|
v-if="customer"
|
||||||
<span class="customer-copy">
|
class="customer-profile"
|
||||||
<strong v-if="customer" v-text="customer.name || customer.code" />
|
:customer="customer"
|
||||||
<strong v-else class="placeholder">请选择客户</strong>
|
v-entrance-transition
|
||||||
<span v-if="customer?.telephone" v-text="customer.telephone" />
|
/>
|
||||||
<span v-else class="hint" v-text="customer ? customer.code : '支持按姓名、编码或电话搜索'" />
|
<t-form-item label-width="0">
|
||||||
</span>
|
<div class="product-part">
|
||||||
<t-icon name="chevron-right" size="1.25rem" />
|
<div class="header">
|
||||||
</button>
|
<strong>购买产品</strong>
|
||||||
</section>
|
<span v-text="`${items.length} 项`" />
|
||||||
|
|
||||||
<section class="section product-section">
|
|
||||||
<div class="section-head">
|
|
||||||
<div>
|
|
||||||
<span class="step" v-text="'02 / 产品'" />
|
|
||||||
<h2>添加产品</h2>
|
|
||||||
<p>同一产品单位不能重复,数量最多三位小数</p>
|
|
||||||
</div>
|
|
||||||
<span class="item-count" v-text="`${items.length} 项`" />
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tips">同一产品单位不能重复,数量最多三位小数</div>
|
||||||
<div class="items">
|
<div class="items">
|
||||||
<article v-for="(item, index) in items" :key="item.clientKey" class="order-item">
|
<div v-for="(item, index) in items" :key="item.clientKey" class="item">
|
||||||
<div class="item-head">
|
<div class="header">
|
||||||
<span class="item-number" v-text="String(index + 1).padStart(2, '0')" />
|
<span class="no" v-text="String(index + 1).padStart(2, '0')" />
|
||||||
<div class="item-title">
|
|
||||||
<span class="item-label" v-text="`产品 ${index + 1}`" />
|
|
||||||
<strong v-text="item.productName || '未选择产品'" />
|
<strong v-text="item.productName || '未选择产品'" />
|
||||||
|
<t-button theme="danger" variant="text" size="extra-small" @click="removeItem(index)">删除</t-button>
|
||||||
</div>
|
</div>
|
||||||
<t-button theme="danger" variant="text" size="small" @click="removeItem(index)">删除</t-button>
|
|
||||||
</div>
|
|
||||||
<t-form class="item-form" label-align="left" label-width="5.25rem">
|
|
||||||
<product-selector
|
<product-selector
|
||||||
:model-value="item.productId && item.productUnitId ? {productId: item.productId, productUnitId: item.productUnitId} : undefined"
|
:model-value="item.productId ? {productId: item.productId, productUnitId: item.productUnitId || ''} : undefined"
|
||||||
label="产品单位"
|
label="产品"
|
||||||
placeholder="请选择产品及单位"
|
placeholder="请选择产品"
|
||||||
|
selection-mode="product"
|
||||||
required-mark
|
required-mark
|
||||||
@update:model-value="applyProduct(index, $event)"
|
@update:model-value="applyProduct(index, $event)"
|
||||||
/>
|
/>
|
||||||
<div class="number-fields">
|
<t-form-item v-if="item.productId" label="产品单位" required-mark>
|
||||||
<t-form-item label="数量" required-mark>
|
<div v-if="getSaleableUnits(item).length" class="unit-list">
|
||||||
<t-input v-model.trim="item.quantity" type="number" align="right" borderless placeholder="请输入数量" />
|
<t-button
|
||||||
|
v-for="unit in getSaleableUnits(item)"
|
||||||
|
:key="unit.id"
|
||||||
|
:theme="item.productUnitId === unit.id ? 'primary' : 'default'"
|
||||||
|
:variant="item.productUnitId === unit.id ? 'base' : 'outline'"
|
||||||
|
size="extra-small"
|
||||||
|
:content="unit.name"
|
||||||
|
@click="selectProductUnit(index, unit)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span v-else class="unit-empty" v-text="'暂无可售单位'" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item v-if="canPriceOverride" label="成交单价">
|
<t-form-item v-if="canPriceOverride" label="成交单价">
|
||||||
<t-input v-model.trim="item.unitPriceText" type="number" align="right" borderless placeholder="产品价" />
|
<t-input
|
||||||
|
v-model.trim="item.unitPriceText"
|
||||||
|
type="number"
|
||||||
|
align="right"
|
||||||
|
borderless
|
||||||
|
:placeholder="getItemUnitPricePlaceholder(item)"
|
||||||
|
/>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="数量" content-align="right">
|
||||||
|
<t-stepper
|
||||||
|
v-model="item.quantity"
|
||||||
|
size="large"
|
||||||
|
:max="maxQuantity"
|
||||||
|
:min="1"
|
||||||
|
:step="1"
|
||||||
|
:integer="false"
|
||||||
|
theme="outline"
|
||||||
|
/>
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
</div>
|
|
||||||
<t-form-item label="产品备注">
|
<t-form-item label="产品备注">
|
||||||
<t-input v-model.trim="item.remark" borderless placeholder="选填" />
|
<t-input v-model.trim="item.remark" borderless placeholder="选填" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
</t-form>
|
<div class="price">
|
||||||
</article>
|
<strong class="coin red">¥</strong>
|
||||||
|
<strong class="value red" v-text="getItemSubtotalText(item)" />
|
||||||
</div>
|
</div>
|
||||||
<button class="add-item" type="button" @click="addItem">
|
</div>
|
||||||
|
</div>
|
||||||
|
<t-button
|
||||||
|
class="add"
|
||||||
|
type="button"
|
||||||
|
theme="light"
|
||||||
|
variant="dashed"
|
||||||
|
block
|
||||||
|
@click="addItem"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
<t-icon name="add" size="1.125rem" />
|
<t-icon name="add" size="1.125rem" />
|
||||||
|
</template>
|
||||||
<span>继续添加产品</span>
|
<span>继续添加产品</span>
|
||||||
</button>
|
</t-button>
|
||||||
</section>
|
</div>
|
||||||
|
</t-form-item>
|
||||||
<section class="section setting-section">
|
<t-form-item label="下单时间" content-align="right">
|
||||||
<div class="section-head">
|
<div class="time-actions">
|
||||||
<div>
|
<t-button
|
||||||
<span class="step" v-text="'03 / 设置'" />
|
theme="primary"
|
||||||
<h2>订单设置</h2>
|
variant="outline"
|
||||||
<p>时间不填则默认使用提交时刻</p>
|
size="extra-small"
|
||||||
|
:content="purchasedAtText || '此刻'"
|
||||||
|
@click="openTimePicker"
|
||||||
|
/>
|
||||||
|
<t-button
|
||||||
|
v-if="purchasedAtText"
|
||||||
|
theme="primary"
|
||||||
|
variant="text"
|
||||||
|
size="extra-small"
|
||||||
|
content="此刻"
|
||||||
|
@click="resetPurchasedAt"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="订单备注">
|
||||||
|
<t-textarea v-model.trim="remark" class="textarea" borderless autosize maxlength="500" placeholder="记录本单需要注意的内容" />
|
||||||
|
</t-form-item>
|
||||||
|
<div class="photo-part">
|
||||||
|
<package-photo-field v-model="photoItems" title="订单照片(选填)" />
|
||||||
|
</div>
|
||||||
|
</t-form>
|
||||||
|
<div class="submit-wrap glass-white bg-white">
|
||||||
|
<div class="total">
|
||||||
|
<span>订单总价</span>
|
||||||
|
<div class="price">
|
||||||
|
<strong class="red coin">¥</strong>
|
||||||
|
<strong class="red value" v-text="totalAmountText" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="setting-row" type="button" @click="pickerVisible = true">
|
<t-button
|
||||||
<span class="setting-icon"><t-icon name="time" size="1.25rem" /></span>
|
class="submit"
|
||||||
<span class="setting-copy">
|
theme="danger"
|
||||||
<strong>下单时间</strong>
|
:loading="isSaving"
|
||||||
<span v-text="purchasedAtText || '此刻'" />
|
:disabled="isSaving"
|
||||||
</span>
|
@click="save"
|
||||||
<span class="setting-action" v-text="purchasedAtText ? '已设置' : '修改'" />
|
>提交订单</t-button>
|
||||||
<t-icon name="chevron-right" size="1.125rem" />
|
|
||||||
</button>
|
|
||||||
<div class="remark-field">
|
|
||||||
<div class="remark-head">
|
|
||||||
<strong>订单备注</strong>
|
|
||||||
<span v-text="'选填'" />
|
|
||||||
</div>
|
|
||||||
<t-textarea v-model.trim="remark" borderless autosize maxlength="500" placeholder="记录本单需要注意的内容" />
|
|
||||||
</div>
|
|
||||||
<package-photo-field v-model="photoItems" title="购买照片(消费事件要求时必填)" />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="submit-bar">
|
|
||||||
<span>确认信息无误后提交订单</span>
|
|
||||||
<t-button theme="primary" block :loading="isSaving" :disabled="isSaving" @click="save">提交订单</t-button>
|
|
||||||
</div>
|
</div>
|
||||||
<customer-selector v-model="customerIdList" v-model:visible="customerSelectorVisible" selection-mode="single" @change="loadCustomer" />
|
<customer-selector v-model="customerIdList" v-model:visible="customerSelectorVisible" selection-mode="single" @change="loadCustomer" />
|
||||||
<t-popup v-model="pickerVisible" placement="bottom">
|
<t-popup v-model="pickerVisible" placement="bottom">
|
||||||
<t-date-time-picker v-model="draftTime" format="YYYY-MM-DD HH:mm" :mode="['date', 'minute']" title="下单时间" @cancel="pickerVisible = false" @confirm="confirmTime" />
|
<t-date-time-picker v-if="pickerVisible" v-model="draftTime" format="YYYY-MM-DD HH:mm" :mode="['date', 'minute']" title="下单时间" @cancel="pickerVisible = false" @confirm="confirmTime" />
|
||||||
</t-popup>
|
</t-popup>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useUserStore, type Attachment } from "timi-web-api";
|
import { type Attachment, useUserStore } from "timi-web-api";
|
||||||
|
import { Time } from "timi-web";
|
||||||
import CustomerAPI from "@/api/CustomerAPI";
|
import CustomerAPI from "@/api/CustomerAPI";
|
||||||
import ProductAPI from "@/api/ProductAPI";
|
import ProductAPI from "@/api/ProductAPI";
|
||||||
import SalesOrderAPI from "@/api/SalesOrderAPI";
|
import SalesOrderAPI from "@/api/SalesOrderAPI";
|
||||||
|
import CodeScanner from "@/components/CodeScanner.vue";
|
||||||
|
import CustomerProfileCard from "@/components/CustomerProfileCard.vue";
|
||||||
import CustomerSelector from "@/components/CustomerSelector.vue";
|
import CustomerSelector from "@/components/CustomerSelector.vue";
|
||||||
import PackagePhotoField from "@/components/PackagePhotoField.vue";
|
import PackagePhotoField from "@/components/PackagePhotoField.vue";
|
||||||
import type { Customer } from "@/types/Customer";
|
import type { Customer } from "@/types/Customer";
|
||||||
import type { Product } from "@/types/Product";
|
import type { Product, ProductUnit } from "@/types/Product";
|
||||||
import type { PhotoItem } from "@/types/Photo";
|
import type { PhotoItem } from "@/types/Photo";
|
||||||
import type { DirectProductOrderItemRequest } from "@/types/SalesOrder";
|
import type { DirectProductOrderItemRequest } from "@/types/SalesOrder";
|
||||||
import { createPackageRequestNo, normalizePackageQuantity, parsePackageAmount } from "@/utils/package";
|
import { createPackageRequestNo, normalizePackageQuantity, parsePackageAmount } from "@/utils/package";
|
||||||
|
import { formatProductAmount } from "@/utils/product";
|
||||||
import { getErrorMessage } from "@/utils/error";
|
import { getErrorMessage } from "@/utils/error";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
import { permissionCodes } from "@/utils/permission";
|
import { permissionCodes } from "@/utils/permission";
|
||||||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
|
||||||
import { useNavBarStore } from "@/store/navBarStore";
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
|
import { useSettingStore } from "@/store/settingStore";
|
||||||
|
|
||||||
type FormItem = Partial<DirectProductOrderItemRequest> & { clientKey: string; productName: string; unitPriceText: string };
|
type FormItem = Partial<DirectProductOrderItemRequest> & { clientKey: string; productName: string; unitPriceText: string };
|
||||||
defineOptions({ name: "SalesOrderCreate" });
|
defineOptions({ name: "SalesOrderCreate" });
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const navBarStore = useNavBarStore();
|
const navBarStore = useNavBarStore();
|
||||||
const currentStoreStore = useCurrentStoreStore();
|
const settingStore = useSettingStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const canPriceOverride = computed(() => userStore.hasPermissions(permissionCodes.salesOrder.priceOverride));
|
const canPriceOverride = computed(() => userStore.hasPermissions(permissionCodes.salesOrder.priceOverride));
|
||||||
const customerIdList = ref<string[]>([]);
|
const customerIdList = ref<string[]>([]);
|
||||||
@@ -147,21 +208,158 @@ const draftTime = ref("");
|
|||||||
const pickerVisible = ref(false);
|
const pickerVisible = ref(false);
|
||||||
const isSaving = ref(false);
|
const isSaving = ref(false);
|
||||||
const photoItems = ref<PhotoItem[]>([]);
|
const photoItems = ref<PhotoItem[]>([]);
|
||||||
|
const maxQuantity = Number.MAX_SAFE_INTEGER;
|
||||||
const productCache = new Map<string, Product>();
|
const productCache = new Map<string, Product>();
|
||||||
const requestNo = ref("");
|
const requestNo = ref("");
|
||||||
function createItem(): FormItem { return { clientKey: window.crypto.randomUUID(), quantity: "", productName: "", unitPriceText: "", remark: "" }; }
|
const customerDisplayText = computed(() => customer.value?.name || customer.value?.code || "选择客户");
|
||||||
function addItem(): void { items.value.push(createItem()); }
|
const customerCode = ref("");
|
||||||
function removeItem(index: number): void { if (1 < items.value.length) items.value.splice(index, 1); else Message.warning("订单至少需要一项产品"); }
|
const scanCode = ref("");
|
||||||
|
const scannerVisible = ref(false);
|
||||||
|
const scanStatus = ref("点击扫码开始识别。");
|
||||||
|
let latestDecodedText = "";
|
||||||
|
const totalAmount = computed(() => {
|
||||||
|
let amount = 0;
|
||||||
|
for (const item of items.value) {
|
||||||
|
const itemAmount = calculateItemAmount(item);
|
||||||
|
if (itemAmount === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
amount += itemAmount;
|
||||||
|
}
|
||||||
|
return amount;
|
||||||
|
});
|
||||||
|
const totalAmountText = computed(() => totalAmount.value === undefined ? "0.00" : formatProductAmount(totalAmount.value));
|
||||||
|
|
||||||
|
function createItem(): FormItem {
|
||||||
|
return {
|
||||||
|
clientKey: window.crypto.randomUUID(),
|
||||||
|
quantity: "1",
|
||||||
|
productName: "",
|
||||||
|
unitPriceText: "",
|
||||||
|
remark: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function addItem(): void {
|
||||||
|
items.value.push(createItem());
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeItem(index: number): void {
|
||||||
|
if (1 < items.value.length) {
|
||||||
|
items.value.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
Message.warning("订单至少需要一项产品");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function applyProduct(index: number, value?: { productId: string; productUnitId: string }): Promise<void> {
|
async function applyProduct(index: number, value?: { productId: string; productUnitId: string }): Promise<void> {
|
||||||
if (!value) return;
|
if (!value) return;
|
||||||
const product = productCache.get(value.productId) || await ProductAPI.detail(value.productId);
|
const product = productCache.get(value.productId) || await ProductAPI.detail(value.productId);
|
||||||
productCache.set(value.productId, product);
|
productCache.set(value.productId, product);
|
||||||
items.value[index].productId = value.productId;
|
const item = items.value[index];
|
||||||
items.value[index].productUnitId = value.productUnitId;
|
const selectionChanged = item.productId !== value.productId || item.productUnitId !== value.productUnitId;
|
||||||
items.value[index].productName = `${product.name} · ${product.unitList?.find(unit => unit.id === value.productUnitId)?.name || "单位"}`;
|
item.productId = value.productId;
|
||||||
|
item.productUnitId = value.productUnitId;
|
||||||
|
item.productName = product.name;
|
||||||
|
if (selectionChanged) {
|
||||||
|
item.unitPriceText = "";
|
||||||
}
|
}
|
||||||
async function loadCustomer(value: string | string[]): Promise<void> { const id = Array.isArray(value) ? value[0] : value; customer.value = id ? await CustomerAPI.detail(id) : undefined; }
|
}
|
||||||
function confirmTime(value: string | number): void { purchasedAtText.value = String(value); pickerVisible.value = false; }
|
function getSaleableUnits(item: FormItem): ProductUnit[] {
|
||||||
|
return (productCache.get(item.productId || "")?.unitList || []).filter(unit => unit.allowSale !== false && unit.id);
|
||||||
|
}
|
||||||
|
function selectProductUnit(index: number, unit: ProductUnit): void {
|
||||||
|
const item = items.value[index];
|
||||||
|
const product = productCache.get(item.productId || "");
|
||||||
|
if (!product?.id || !unit.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (item.productUnitId !== unit.id) {
|
||||||
|
item.unitPriceText = "";
|
||||||
|
}
|
||||||
|
item.productUnitId = unit.id;
|
||||||
|
item.productName = product.name;
|
||||||
|
}
|
||||||
|
function getItemUnitPrice(item: FormItem): number | undefined {
|
||||||
|
if (canPriceOverride.value && item.unitPriceText.trim()) {
|
||||||
|
return parsePackageAmount(item.unitPriceText);
|
||||||
|
}
|
||||||
|
const salePrice = productCache.get(item.productId || "")?.unitList?.find(unit => unit.id === item.productUnitId)?.salePrice;
|
||||||
|
return salePrice === null ? undefined : salePrice;
|
||||||
|
}
|
||||||
|
function getItemUnitPricePlaceholder(item: FormItem): string {
|
||||||
|
const salePrice = productCache.get(item.productId || "")?.unitList?.find(unit => unit.id === item.productUnitId)?.salePrice;
|
||||||
|
return salePrice === undefined || salePrice === null ? "产品价" : `产品价:${formatProductAmount(salePrice)}`;
|
||||||
|
}
|
||||||
|
function calculateItemAmount(item: FormItem): number | undefined {
|
||||||
|
const quantity = normalizePackageQuantity(item.quantity ?? "");
|
||||||
|
const unitPrice = getItemUnitPrice(item);
|
||||||
|
if (!quantity || quantity === "0" || unitPrice === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const [integerPart, fractionPart = ""] = quantity.split(".");
|
||||||
|
const quantityScale = 10n ** BigInt(fractionPart.length);
|
||||||
|
const quantityValue = BigInt(`${integerPart}${fractionPart}`);
|
||||||
|
const amount = BigInt(unitPrice) * quantityValue;
|
||||||
|
if (amount % quantityScale !== 0n) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const result = Number(amount / quantityScale);
|
||||||
|
return Number.isSafeInteger(result) ? result : undefined;
|
||||||
|
}
|
||||||
|
function getItemSubtotalText(item: FormItem): string {
|
||||||
|
const amount = calculateItemAmount(item);
|
||||||
|
return amount === undefined ? "0.00" : formatProductAmount(amount);
|
||||||
|
}
|
||||||
|
async function loadCustomer(value: string | string[]): Promise<void> {
|
||||||
|
const id = Array.isArray(value) ? value[0] : value;
|
||||||
|
customer.value = id ? await CustomerAPI.detail(id) : undefined;
|
||||||
|
customerCode.value = customer.value?.code || "";
|
||||||
|
scanCode.value = customerCode.value;
|
||||||
|
}
|
||||||
|
function toggleScanner(): void {
|
||||||
|
scannerVisible.value = !scannerVisible.value;
|
||||||
|
if (scannerVisible.value) {
|
||||||
|
latestDecodedText = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function handleDecodedText(rawText: string): Promise<void> {
|
||||||
|
const normalizedText = rawText.trim();
|
||||||
|
if (!normalizedText || normalizedText === latestDecodedText) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
latestDecodedText = normalizedText;
|
||||||
|
scanCode.value = normalizedText;
|
||||||
|
await settingStore.playScanSuccessSound();
|
||||||
|
await applyScanCode();
|
||||||
|
}
|
||||||
|
async function applyScanCode(): Promise<void> {
|
||||||
|
customerCode.value = scanCode.value.trim();
|
||||||
|
scanStatus.value = `已识别 ${customerCode.value},正在加载客户档案。`;
|
||||||
|
if (!customerCode.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await CustomerAPI.findByCode(customerCode.value);
|
||||||
|
if (!result?.id) {
|
||||||
|
customer.value = undefined;
|
||||||
|
customerIdList.value = [];
|
||||||
|
scanStatus.value = `客户 ${customerCode.value} 不存在,请先建档。`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
customer.value = result;
|
||||||
|
customerIdList.value = [result.id];
|
||||||
|
scanStatus.value = `已识别 ${result.code || customerCode.value},已自动选中 ${result.name || "未命名客户"}。`;
|
||||||
|
scannerVisible.value = false;
|
||||||
|
}
|
||||||
|
function handleScanStatusChange(value: string): void {
|
||||||
|
scanStatus.value = value;
|
||||||
|
}
|
||||||
|
function openTimePicker(): void { draftTime.value = purchasedAtText.value || Time.formatDateTime(new Date()); pickerVisible.value = true; }
|
||||||
|
function confirmTime(value?: string | number): void { purchasedAtText.value = String(value || draftTime.value); pickerVisible.value = false; }
|
||||||
|
function resetPurchasedAt(): void { purchasedAtText.value = ""; draftTime.value = ""; }
|
||||||
function resolveTimestamp(value: string): number | undefined {
|
function resolveTimestamp(value: string): number | undefined {
|
||||||
if (!value) return undefined;
|
if (!value) return undefined;
|
||||||
const timestamp = new Date(value.replace(" ", "T")).getTime();
|
const timestamp = new Date(value.replace(" ", "T")).getTime();
|
||||||
@@ -209,19 +407,14 @@ async function save(): Promise<void> {
|
|||||||
await router.replace(`/management/sales-orders/${result.id}`);
|
await router.replace(`/management/sales-orders/${result.id}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = getErrorMessage(error, "销售订单创建失败,重试会复用同一请求号");
|
const message = getErrorMessage(error, "销售订单创建失败,重试会复用同一请求号");
|
||||||
if (message.includes("直接产品消费事件")) {
|
|
||||||
Message.warning("请先在门店配置中选择直接产品消费事件");
|
|
||||||
const storeId = currentStoreStore.currentStoreId;
|
|
||||||
if (storeId) {
|
|
||||||
await router.replace(`/management/stores/${storeId}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Message.error(message);
|
Message.error(message);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
finally { isSaving.value = false; }
|
finally { isSaving.value = false; }
|
||||||
}
|
}
|
||||||
onMounted(() => { addItem(); navBarStore.setRightText("提交订单", "var(--td-brand-color)", save); });
|
onMounted(() => {
|
||||||
|
addItem();
|
||||||
|
navBarStore.clearRight();
|
||||||
|
});
|
||||||
onUnmounted(() => navBarStore.clearRight());
|
onUnmounted(() => navBarStore.clearRight());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -229,172 +422,82 @@ onUnmounted(() => navBarStore.clearRight());
|
|||||||
.sales-order-create {
|
.sales-order-create {
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: calc(var(--app-nav-height) + 1rem) 1rem calc(7.5rem + env(safe-area-inset-bottom, 0));
|
padding: calc(var(--app-nav-height) + 1rem) 0 calc(6.5rem + env(safe-area-inset-bottom, 0));
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
.intro {
|
.form {
|
||||||
padding: .25rem .25rem .5rem;
|
margin: 0 1rem;
|
||||||
|
padding: .5rem 0;
|
||||||
.eyebrow {
|
background: rgba(255, 255, 255, .92);
|
||||||
color: var(--td-brand-color);
|
|
||||||
font-size: .6875rem;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: .12em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin: .5rem 0 .35rem;
|
|
||||||
color: var(--td-text-color-primary);
|
|
||||||
font-size: 1.5rem;
|
|
||||||
line-height: 1.25;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--td-text-color-secondary);
|
|
||||||
font-size: .8125rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
|
||||||
padding: 1rem;
|
|
||||||
background: var(--td-bg-color-container);
|
|
||||||
box-shadow: var(--td-shadow-1);
|
box-shadow: var(--td-shadow-1);
|
||||||
border-radius: 1rem;
|
border-radius: 1.4rem;
|
||||||
|
|
||||||
|
.customer {
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
gap: .5rem;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-head {
|
.code-scanner-wrapper,
|
||||||
gap: .75rem;
|
.photo-part {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-profile {
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-actions {
|
||||||
|
gap: .5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-part {
|
||||||
|
|
||||||
|
> .header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
.step {
|
|
||||||
color: var(--td-brand-color);
|
|
||||||
display: block;
|
|
||||||
font-size: .6875rem;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: .08em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: .3rem 0 .25rem;
|
|
||||||
font-size: 1.0625rem;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--td-text-color-secondary);
|
|
||||||
font-size: .75rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.required,
|
|
||||||
.item-count {
|
|
||||||
color: var(--td-brand-color);
|
|
||||||
padding: .25rem .5rem;
|
|
||||||
background: var(--td-brand-color-1);
|
|
||||||
border-radius: .75rem;
|
|
||||||
font-size: .6875rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-picker,
|
|
||||||
.setting-row {
|
|
||||||
width: 100%;
|
|
||||||
border: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
text-align: left;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-picker {
|
|
||||||
gap: .75rem;
|
|
||||||
margin-top: 1rem;
|
|
||||||
padding: .75rem;
|
|
||||||
border: 1px solid var(--td-component-border);
|
|
||||||
border-radius: .875rem;
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background: var(--td-bg-color-secondarycontainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-avatar {
|
|
||||||
width: 2.75rem;
|
|
||||||
height: 2.75rem;
|
|
||||||
color: var(--td-brand-color);
|
|
||||||
flex: none;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: var(--td-brand-color-1);
|
|
||||||
border-radius: .875rem;
|
|
||||||
font-size: 1.125rem;
|
|
||||||
font-weight: 700;
|
|
||||||
|
|
||||||
&.selected {
|
|
||||||
color: var(--td-text-color-anti);
|
|
||||||
background: var(--td-brand-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-copy {
|
|
||||||
gap: .2rem;
|
|
||||||
min-width: 0;
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
strong,
|
|
||||||
span {
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
strong {
|
strong {
|
||||||
color: var(--td-text-color-primary);
|
|
||||||
font-size: .9375rem;
|
font-size: .9375rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
color: var(--td-text-color-secondary);
|
color: var(--td-brand-color);
|
||||||
font-size: .75rem;
|
font-size: .75rem;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.placeholder,
|
.tips {
|
||||||
.hint {
|
margin-top: .25rem;
|
||||||
color: var(--td-text-color-placeholder);
|
color: var(--td-text-color-secondary);
|
||||||
}
|
font-size: .75rem;
|
||||||
}
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.items {
|
.items {
|
||||||
gap: .75rem;
|
gap: .75rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-top: 1rem;
|
margin-top: .75rem;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
|
||||||
|
|
||||||
.order-item {
|
.item {
|
||||||
padding: .875rem;
|
border: 1px dashed var(--td-border-level-1-color);
|
||||||
background: var(--td-bg-color-secondarycontainer);
|
padding: .5rem;
|
||||||
border: 1px solid var(--td-border-level-1-color);
|
|
||||||
border-radius: .875rem;
|
border-radius: .875rem;
|
||||||
}
|
|
||||||
|
|
||||||
.item-head {
|
.header {
|
||||||
gap: .625rem;
|
gap: .625rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: .5rem;
|
margin-bottom: .375rem;
|
||||||
}
|
|
||||||
|
|
||||||
.item-number {
|
.no {
|
||||||
width: 1.75rem;
|
width: 1.75rem;
|
||||||
height: 1.75rem;
|
height: 1.75rem;
|
||||||
color: var(--td-brand-color);
|
color: var(--td-brand-color);
|
||||||
@@ -408,19 +511,9 @@ onUnmounted(() => navBarStore.clearRight());
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-title {
|
|
||||||
gap: .1rem;
|
|
||||||
min-width: 0;
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.item-label {
|
|
||||||
color: var(--td-text-color-secondary);
|
|
||||||
font-size: .6875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
strong {
|
strong {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--td-text-color-primary);
|
color: var(--td-text-color-primary);
|
||||||
font-size: .875rem;
|
font-size: .875rem;
|
||||||
@@ -429,167 +522,112 @@ onUnmounted(() => navBarStore.clearRight());
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-form {
|
.unit-list {
|
||||||
:deep(.t-form__item) {
|
gap: .5rem;
|
||||||
min-height: 2.75rem;
|
|
||||||
padding: .125rem 0;
|
|
||||||
border-bottom: 1px solid var(--td-border-level-1-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.t-form__item:last-child) {
|
|
||||||
border-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.t-form__label) {
|
|
||||||
color: var(--td-text-color-secondary);
|
|
||||||
font-size: .8125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.t-input) {
|
|
||||||
padding-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.t-form__controls) {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.number-fields {
|
|
||||||
gap: .75rem;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
|
|
||||||
:deep(.t-form__item) {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-item {
|
|
||||||
gap: .375rem;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unit-empty {
|
||||||
|
color: var(--td-text-color-secondary);
|
||||||
|
font-size: .75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.price {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: end;
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add {
|
||||||
|
--td-button-border-radius: 1rem;
|
||||||
|
|
||||||
|
gap: .375rem;
|
||||||
|
color: var(--td-brand-color);
|
||||||
margin-top: .75rem;
|
margin-top: .75rem;
|
||||||
padding: .75rem;
|
}
|
||||||
color: var(--td-brand-color);
|
}
|
||||||
border: 1px dashed var(--td-brand-color-4);
|
|
||||||
|
|
||||||
|
.textarea {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-part {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-wrap {
|
||||||
|
gap: 1rem;
|
||||||
|
left: 2rem;
|
||||||
|
right: 2rem;
|
||||||
|
border: 1px solid rgba(0, 0, 0, .05);
|
||||||
|
bottom: calc(env(safe-area-inset-bottom, 0) + 1rem);
|
||||||
|
z-index: 12;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
padding: .5rem 1rem calc(.5rem + env(safe-area-inset-bottom, 0)) 1rem;
|
||||||
|
position: fixed;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
border-radius: 1.75rem;
|
||||||
background: var(--td-brand-color-1);
|
backdrop-filter: blur(10px);
|
||||||
border-radius: .75rem;
|
justify-content: space-between;
|
||||||
font-size: .8125rem;
|
|
||||||
font-weight: 600;
|
|
||||||
|
|
||||||
&:active {
|
.total {
|
||||||
background: var(--td-brand-color-2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-row {
|
|
||||||
gap: .75rem;
|
|
||||||
margin-top: 1rem;
|
|
||||||
padding: .75rem 0;
|
|
||||||
border-top: 1px solid var(--td-border-level-1-color);
|
|
||||||
border-bottom: 1px solid var(--td-border-level-1-color);
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background: var(--td-bg-color-secondarycontainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-icon {
|
|
||||||
width: 2.25rem;
|
|
||||||
height: 2.25rem;
|
|
||||||
color: var(--td-brand-color);
|
|
||||||
flex: none;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: var(--td-brand-color-1);
|
|
||||||
border-radius: .75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-copy {
|
|
||||||
gap: .2rem;
|
|
||||||
min-width: 0;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
strong {
|
|
||||||
color: var(--td-text-color-primary);
|
|
||||||
font-size: .875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
color: var(--td-text-color-secondary);
|
|
||||||
font-size: .75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-action {
|
|
||||||
color: var(--td-brand-color);
|
|
||||||
font-size: .75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.remark-field {
|
|
||||||
padding-top: 1rem;
|
|
||||||
|
|
||||||
.remark-head {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
strong {
|
|
||||||
color: var(--td-text-color-primary);
|
|
||||||
font-size: .875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
color: var(--td-text-color-placeholder);
|
|
||||||
font-size: .75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.t-textarea) {
|
|
||||||
margin-top: .5rem;
|
|
||||||
padding: .75rem;
|
|
||||||
background: var(--td-bg-color-secondarycontainer);
|
|
||||||
border-radius: .75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-bar {
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 12;
|
|
||||||
gap: .75rem;
|
|
||||||
padding: .625rem 1rem calc(.625rem + env(safe-area-inset-bottom, 0));
|
|
||||||
position: fixed;
|
|
||||||
box-shadow: 0 -.25rem 1rem rgba(15, 23, 42, .08);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
background: var(--td-bg-color-container);
|
|
||||||
backdrop-filter: blur(1rem);
|
|
||||||
|
|
||||||
span {
|
span {
|
||||||
color: var(--td-text-color-secondary);
|
color: var(--td-text-color-secondary);
|
||||||
font-size: .6875rem;
|
font-size: .6875rem;
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.t-button) {
|
.price {
|
||||||
width: 9rem;
|
display: flex;
|
||||||
flex: none;
|
align-items: baseline;
|
||||||
border-radius: .75rem;
|
|
||||||
|
.value {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit {
|
||||||
|
--td-button-border-radius: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(html.theme-dark) {
|
:global(html.theme-dark) {
|
||||||
.sales-order-create {
|
.sales-order-create {
|
||||||
|
.form {
|
||||||
|
background: rgba(15, 23, 42, .88);
|
||||||
|
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
|
||||||
|
|
||||||
|
.order-item {
|
||||||
|
background: rgba(15, 23, 42, .72);
|
||||||
|
border-color: rgba(148, 163, 184, .16);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.textarea .t-textarea__inner) {
|
||||||
|
color: #e2e8f0;
|
||||||
|
background: rgba(15, 23, 42, .72);
|
||||||
|
border-color: rgba(148, 163, 184, .16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.submit-bar {
|
.submit-bar {
|
||||||
box-shadow: 0 -.25rem 1rem rgba(0, 0, 0, .2);
|
box-shadow: 0 -.25rem 1rem rgba(0, 0, 0, .2);
|
||||||
|
background: rgba(15, 23, 42, .92);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,45 @@
|
|||||||
<div class="filters">
|
<div class="filters">
|
||||||
<t-input v-model.trim="keyword" class="keyword" type="search" borderless clearable placeholder="搜索订单号、客户" />
|
<t-input v-model.trim="keyword" class="keyword" type="search" borderless clearable placeholder="搜索订单号、客户" />
|
||||||
<t-dropdown-menu class="menus">
|
<t-dropdown-menu class="menus">
|
||||||
<t-dropdown-item v-model="orderType" label="订单类型" :options="orderTypeOptions" @change="selectOrderType" />
|
<t-dropdown-item
|
||||||
<t-dropdown-item v-model="status" label="订单状态" :options="statusOptions" @change="selectStatus" />
|
v-model="orderType"
|
||||||
<t-dropdown-item v-model="paymentStatus" label="收款状态" :options="paymentOptions" @change="selectPaymentStatus" />
|
label="订单类型"
|
||||||
|
:options="orderTypeOptions"
|
||||||
|
@change="selectOrderType"
|
||||||
|
/>
|
||||||
|
<t-dropdown-item
|
||||||
|
v-model="status"
|
||||||
|
label="订单状态"
|
||||||
|
:options="statusOptions"
|
||||||
|
@change="selectStatus"
|
||||||
|
/>
|
||||||
|
<t-dropdown-item
|
||||||
|
v-model="paymentStatus"
|
||||||
|
label="收款状态"
|
||||||
|
:options="paymentOptions"
|
||||||
|
@change="selectPaymentStatus"
|
||||||
|
/>
|
||||||
</t-dropdown-menu>
|
</t-dropdown-menu>
|
||||||
</div>
|
</div>
|
||||||
<t-pull-down-refresh v-model="refreshing" :refresh-timeout="30000" @refresh="refresh" @scrolltolower="loadMore">
|
<t-pull-down-refresh
|
||||||
<div v-if="viewMode" class="view-tip" v-text="viewMode === 'pending-receive' ? '当前显示未收清订单' : '当前显示待处理退款订单'" />
|
v-model="refreshing"
|
||||||
|
:refresh-timeout="30000"
|
||||||
|
@refresh="refresh"
|
||||||
|
@scrolltolower="loadMore"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="viewMode"
|
||||||
|
class="view-tip"
|
||||||
|
v-text="viewMode === 'pending-receive' ? '当前显示未收清订单' : '当前显示待处理退款订单'"
|
||||||
|
/>
|
||||||
<t-cell-group v-if="displayList.length" theme="card">
|
<t-cell-group v-if="displayList.length" theme="card">
|
||||||
<t-cell v-for="(item, index) in displayList" :key="item.id" arrow hover v-entrance-transition="index * 30" @click="toDetail(item)">
|
<t-cell
|
||||||
|
v-for="(item, index) in displayList"
|
||||||
|
:key="item.id"
|
||||||
|
arrow
|
||||||
|
v-entrance-transition="index * 30"
|
||||||
|
@click="toDetail(item)"
|
||||||
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<strong v-text="item.orderNo || item.id || '未命名订单'" />
|
<strong v-text="item.orderNo || item.id || '未命名订单'" />
|
||||||
@@ -28,7 +58,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</t-cell>
|
</t-cell>
|
||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
<div v-else class="empty" v-text="loading ? '正在加载销售订单' : '暂无销售订单'" />
|
<div v-else class="center-tips" v-text="loading ? '正在加载销售订单' : '暂无销售订单'" />
|
||||||
<div class="center-tips" v-text="loading ? '正在加载' : noMore ? '已经到底了' : '上拉加载更多'" />
|
<div class="center-tips" v-text="loading ? '正在加载' : noMore ? '已经到底了' : '上拉加载更多'" />
|
||||||
</t-pull-down-refresh>
|
</t-pull-down-refresh>
|
||||||
</div>
|
</div>
|
||||||
@@ -116,18 +146,83 @@ onUnmounted(() => { navBarStore.clearRight(); search.cancel(); });
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.sales-order-list { padding: var(--app-nav-height) 0 1rem;
|
.sales-order-list {
|
||||||
.filters { top: var(--app-nav-height); z-index: 10; padding: .75rem 1rem .5rem; position: sticky; background: var(--td-bg-color-page); box-shadow: var(--td-shadow-1);
|
padding: var(--app-nav-height) 0 1rem;
|
||||||
.keyword { border: 1px solid var(--td-component-border); background: var(--td-bg-color-container); border-radius: 1rem; }
|
|
||||||
.menus { margin-top: .5rem; border-radius: .875rem;
|
.filters {
|
||||||
:deep(.t-dropdown-menu__item) { min-width: 0; }
|
top: var(--app-nav-height);
|
||||||
:deep(.t-dropdown-menu__title) { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
padding: .75rem 1rem;
|
||||||
|
position: sticky;
|
||||||
|
background: #F5F5F8;
|
||||||
|
box-shadow: var(--td-shadow-1);
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.keyword {
|
||||||
|
--td-input-vertical-padding: 8px 14px;
|
||||||
|
|
||||||
|
border: 1px solid var(--td-component-border);
|
||||||
|
z-index: 1;
|
||||||
|
background: var(--td-bg-color-container);
|
||||||
|
border-radius: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menus {
|
||||||
|
--td-spacer-1: 6px;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: -1rem;
|
||||||
|
padding-top: .8rem;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid rgba(148, 163, 184, .18);
|
||||||
|
border-radius: 0 0 1rem 1rem;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.t-dropdown-menu__item) {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.t-dropdown-menu__title) {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.title, .description { gap: .5rem; display: flex; align-items: center; }
|
}
|
||||||
.description { color: var(--td-text-color-secondary); flex-wrap: wrap; }
|
|
||||||
.amount { color: var(--td-brand-color); font-weight: 700; }
|
.title, .description {
|
||||||
.view-tip { color: var(--td-text-color-secondary); padding: .75rem 1rem 0; font-size: .8125rem; }
|
gap: .5rem;
|
||||||
.empty { color: var(--td-text-color-secondary); padding: 3rem 1rem; text-align: center; }
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
color: var(--td-text-color-secondary);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount {
|
||||||
|
color: var(--td-brand-color);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-tip {
|
||||||
|
color: var(--td-text-color-secondary);
|
||||||
|
padding: .75rem 1rem 0;
|
||||||
|
font-size: .8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
color: var(--td-text-color-secondary);
|
||||||
|
padding: 3rem 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<t-cell title="剩余待收款" :note="formatPackageAmount(props.order.unpaidAmount)" />
|
<t-cell title="剩余待收款" :note="formatPackageAmount(props.order.unpaidAmount)" />
|
||||||
<t-cell title="有效退款" :note="formatPackageAmount(props.order.refundedAmount)" />
|
<t-cell title="有效退款" :note="formatPackageAmount(props.order.refundedAmount)" />
|
||||||
<t-cell title="收款状态" :note="getSalesPaymentStatusText(props.order.paymentStatus)" />
|
<t-cell title="收款状态" :note="getSalesPaymentStatusText(props.order.paymentStatus)" />
|
||||||
|
<t-cell v-if="props.order.paymentStatus === 'PAID'" title="全额实收时间" :note="getPaidAtText(props.order.paidAt)" />
|
||||||
<t-cell title="退款状态" :note="getSalesRefundStatusText(props.order.refundStatus)" />
|
<t-cell title="退款状态" :note="getSalesRefundStatusText(props.order.refundStatus)" />
|
||||||
<t-cell v-if="props.canReadFund" title="查看全部资金流水" arrow @click="emit('fund')" />
|
<t-cell v-if="props.canReadFund" title="查看全部资金流水" arrow @click="emit('fund')" />
|
||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
@@ -35,9 +36,14 @@
|
|||||||
import type { SalesOrder } from "@/types/SalesOrder";
|
import type { SalesOrder } from "@/types/SalesOrder";
|
||||||
import { formatPackageAmount } from "@/utils/package";
|
import { formatPackageAmount } from "@/utils/package";
|
||||||
import { getFundOperationText, getFundStatusText, getSalesPaymentStatusText, getSalesRefundStatusText, getStatusTheme } from "@/utils/salesOrder";
|
import { getFundOperationText, getFundStatusText, getSalesPaymentStatusText, getSalesRefundStatusText, getStatusTheme } from "@/utils/salesOrder";
|
||||||
|
import { Time } from "timi-web";
|
||||||
|
|
||||||
const props = defineProps<{ order: SalesOrder; canReadFund: boolean }>();
|
const props = defineProps<{ order: SalesOrder; canReadFund: boolean }>();
|
||||||
const emit = defineEmits<{ fund: []; record: [id: string] }>();
|
const emit = defineEmits<{ fund: []; record: [id: string] }>();
|
||||||
|
|
||||||
|
function getPaidAtText(paidAt?: number | null): string {
|
||||||
|
return paidAt ? Time.toDateTime(paidAt) : "待后端回填";
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
|
|||||||
@@ -35,13 +35,6 @@
|
|||||||
<t-switch v-model="formData.autoClosedDay" />
|
<t-switch v-model="formData.autoClosedDay" />
|
||||||
</template>
|
</template>
|
||||||
</t-cell>
|
</t-cell>
|
||||||
<t-cell
|
|
||||||
title="直接产品消费事件"
|
|
||||||
:note="directProductSaleEventName"
|
|
||||||
description="直接产品订单使用该事件记录消费金额并触发积分"
|
|
||||||
arrow
|
|
||||||
@click="openDirectProductSaleEventPicker"
|
|
||||||
/>
|
|
||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
<div v-if="!isCreateMode && !isStoreManagerScope" class="actions" v-entrance-transition="140">
|
<div v-if="!isCreateMode && !isStoreManagerScope" class="actions" v-entrance-transition="140">
|
||||||
<t-button
|
<t-button
|
||||||
@@ -59,21 +52,11 @@
|
|||||||
@confirm="removeConfirmedStore"
|
@confirm="removeConfirmedStore"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<t-popup v-model="directProductSaleEventPickerVisible" placement="bottom">
|
|
||||||
<t-picker
|
|
||||||
v-model="directProductSaleEventPickerDraft"
|
|
||||||
:columns="directProductSaleEventPickerColumns"
|
|
||||||
title="选择直接产品消费事件"
|
|
||||||
@cancel="directProductSaleEventPickerVisible = false"
|
|
||||||
@confirm="confirmDirectProductSaleEvent"
|
|
||||||
/>
|
|
||||||
</t-popup>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useUserStore } from "timi-web-api";
|
import { useUserStore } from "timi-web-api";
|
||||||
import StoreAPI from "@/api/StoreAPI";
|
import StoreAPI from "@/api/StoreAPI";
|
||||||
import type { EventItem } from "@/types/EventItem";
|
|
||||||
import StoreRemoveDialog from "@/components/StoreRemoveDialog.vue";
|
import StoreRemoveDialog from "@/components/StoreRemoveDialog.vue";
|
||||||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||||
import { useNavBarStore } from "@/store/navBarStore";
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
@@ -85,8 +68,6 @@ defineOptions({
|
|||||||
name: "StoreEdit"
|
name: "StoreEdit"
|
||||||
});
|
});
|
||||||
|
|
||||||
type PickerValue = string | number;
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const navBarStore = useNavBarStore();
|
const navBarStore = useNavBarStore();
|
||||||
@@ -114,75 +95,9 @@ const formData = reactive({
|
|||||||
telephone: "",
|
telephone: "",
|
||||||
address: "",
|
address: "",
|
||||||
remark: "",
|
remark: "",
|
||||||
autoClosedDay: true,
|
autoClosedDay: true
|
||||||
directProductSaleEventId: ""
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------- 直接产品消费事件 ----------
|
|
||||||
|
|
||||||
const directProductSaleEventList = ref<EventItem[]>([]);
|
|
||||||
const directProductSaleEventLoaded = ref(false);
|
|
||||||
const directProductSaleEventPickerVisible = ref(false);
|
|
||||||
const directProductSaleEventPickerDraft = ref<PickerValue[]>([]);
|
|
||||||
const directProductSaleEventPickerColumns = computed(() => [
|
|
||||||
directProductSaleEventList.value
|
|
||||||
.map(item => ({
|
|
||||||
label: `${item.name || "未命名事件"}${item.requirePhoto ? "(需照片)" : ""}`,
|
|
||||||
value: item.id || ""
|
|
||||||
}))
|
|
||||||
.filter(item => item.value)
|
|
||||||
]);
|
|
||||||
const selectedDirectProductSaleEvent = computed(() => directProductSaleEventList.value.find(item => item.id === formData.directProductSaleEventId));
|
|
||||||
const directProductSaleEventName = computed(() => {
|
|
||||||
if (selectedDirectProductSaleEvent.value) {
|
|
||||||
return selectedDirectProductSaleEvent.value.name || "未命名事件";
|
|
||||||
}
|
|
||||||
if (formData.directProductSaleEventId && !directProductSaleEventLoaded.value) {
|
|
||||||
return "已配置,点击查看";
|
|
||||||
}
|
|
||||||
return formData.directProductSaleEventId ? "当前配置事件不可用" : "未配置,直接产品订单不可提交";
|
|
||||||
});
|
|
||||||
|
|
||||||
async function loadDirectProductSaleEvents(): Promise<boolean> {
|
|
||||||
if (!formData.id) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
directProductSaleEventList.value = await StoreAPI.directProductSaleEventList(formData.id);
|
|
||||||
directProductSaleEventLoaded.value = true;
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
directProductSaleEventList.value = [];
|
|
||||||
directProductSaleEventLoaded.value = false;
|
|
||||||
Message.error("直接产品消费事件加载失败");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openDirectProductSaleEventPicker(): Promise<void> {
|
|
||||||
if (!formData.id) {
|
|
||||||
Message.warning("请先保存门店,再配置直接产品消费事件");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!await loadDirectProductSaleEvents()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!directProductSaleEventPickerColumns.value[0]?.length) {
|
|
||||||
Message.warning("暂无可选事件,请先创建并启用 GENERAL / NUMBER / CENT 消费事件");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
directProductSaleEventPickerDraft.value = [formData.directProductSaleEventId];
|
|
||||||
directProductSaleEventPickerVisible.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmDirectProductSaleEvent(value: Array<string | number>): void {
|
|
||||||
const eventId = String(value[0] || "");
|
|
||||||
if (eventId) {
|
|
||||||
formData.directProductSaleEventId = eventId;
|
|
||||||
}
|
|
||||||
directProductSaleEventPickerVisible.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function doSave(): Promise<void> {
|
async function doSave(): Promise<void> {
|
||||||
if (!validateForm()) {
|
if (!validateForm()) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -93,7 +93,6 @@ const menuGroupList = computed<MenuGroup[]>(() => [
|
|||||||
title: "套餐管理",
|
title: "套餐管理",
|
||||||
items: [
|
items: [
|
||||||
{ icon: "layers", path: "/management/package-configs", title: "套餐配置", visible: userStore.hasPermissions(permissionCodes.packageConfig.read) },
|
{ icon: "layers", path: "/management/package-configs", title: "套餐配置", visible: userStore.hasPermissions(permissionCodes.packageConfig.read) },
|
||||||
{ icon: "calendar", path: "/management/registration-events", title: "套餐购买事件绑定", visible: userStore.hasPermissions([permissionCodes.event.read, permissionCodes.packageConfig.read], "and") },
|
|
||||||
{ icon: "usergroup", path: "/management/customer-packages", title: "客户套餐", visible: userStore.hasPermissions(permissionCodes.customerPackage.read) }
|
{ icon: "usergroup", path: "/management/customer-packages", title: "客户套餐", visible: userStore.hasPermissions(permissionCodes.customerPackage.read) }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { RouteRecordRaw } from "vue-router";
|
|||||||
import PackageConfigList from "@/pages/package/PackageConfigList.vue";
|
import PackageConfigList from "@/pages/package/PackageConfigList.vue";
|
||||||
import PackageConfigEdit from "@/pages/package/PackageConfigEdit.vue";
|
import PackageConfigEdit from "@/pages/package/PackageConfigEdit.vue";
|
||||||
import PackageConfigDetail from "@/pages/package/PackageConfigDetail.vue";
|
import PackageConfigDetail from "@/pages/package/PackageConfigDetail.vue";
|
||||||
|
import PackagePurchase from "@/pages/package/PackagePurchase.vue";
|
||||||
import CustomerPackageList from "@/pages/package/CustomerPackageList.vue";
|
import CustomerPackageList from "@/pages/package/CustomerPackageList.vue";
|
||||||
import CustomerPackageDetail from "@/pages/package/CustomerPackageDetail.vue";
|
import CustomerPackageDetail from "@/pages/package/CustomerPackageDetail.vue";
|
||||||
import FundRecordList from "@/pages/package/FundRecordList.vue";
|
import FundRecordList from "@/pages/package/FundRecordList.vue";
|
||||||
@@ -17,6 +18,7 @@ export default [
|
|||||||
{ path: "/management/package-configs/create", name: "PackageConfigCreate", component: PackageConfigEdit, meta: pageMeta("新增套餐配置", 4) },
|
{ path: "/management/package-configs/create", name: "PackageConfigCreate", component: PackageConfigEdit, meta: pageMeta("新增套餐配置", 4) },
|
||||||
{ path: "/management/package-configs/:id", name: "PackageConfigDetail", component: PackageConfigDetail, meta: pageMeta("套餐配置详情", 4) },
|
{ path: "/management/package-configs/:id", name: "PackageConfigDetail", component: PackageConfigDetail, meta: pageMeta("套餐配置详情", 4) },
|
||||||
{ path: "/management/package-configs/:id/edit", name: "PackageConfigEdit", component: PackageConfigEdit, meta: pageMeta("编辑套餐配置", 5) },
|
{ path: "/management/package-configs/:id/edit", name: "PackageConfigEdit", component: PackageConfigEdit, meta: pageMeta("编辑套餐配置", 5) },
|
||||||
|
{ path: "/management/package-configs/:id/purchase", name: "PackagePurchase", component: PackagePurchase, meta: pageMeta("购买套餐", 5) },
|
||||||
{ path: "/management/customer-packages", name: "CustomerPackageList", component: CustomerPackageList, meta: pageMeta("客户套餐") },
|
{ path: "/management/customer-packages", name: "CustomerPackageList", component: CustomerPackageList, meta: pageMeta("客户套餐") },
|
||||||
{ path: "/management/customer-packages/:id", name: "CustomerPackageDetail", component: CustomerPackageDetail, meta: pageMeta("客户套餐详情", 4) },
|
{ path: "/management/customer-packages/:id", name: "CustomerPackageDetail", component: CustomerPackageDetail, meta: pageMeta("客户套餐详情", 4) },
|
||||||
{ path: "/management/fund-records", name: "FundRecordList", component: FundRecordList, meta: pageMeta("资金流水") },
|
{ path: "/management/fund-records", name: "FundRecordList", component: FundRecordList, meta: pageMeta("资金流水") },
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import PermissionSettings from "@/pages/permission/PermissionSettings.vue";
|
|||||||
import RoleList from "@/pages/permission/RoleList.vue";
|
import RoleList from "@/pages/permission/RoleList.vue";
|
||||||
import RoleEdit from "@/pages/permission/RoleEdit.vue";
|
import RoleEdit from "@/pages/permission/RoleEdit.vue";
|
||||||
import PermissionList from "@/pages/permission/PermissionList.vue";
|
import PermissionList from "@/pages/permission/PermissionList.vue";
|
||||||
import PermissionEdit from "@/pages/permission/PermissionEdit.vue";
|
|
||||||
import RolePermissionAssign from "@/pages/permission/RolePermissionAssign.vue";
|
import RolePermissionAssign from "@/pages/permission/RolePermissionAssign.vue";
|
||||||
import RoleDelegationPermissionAssign from "@/pages/permission/RoleDelegationPermissionAssign.vue";
|
import RoleDelegationPermissionAssign from "@/pages/permission/RoleDelegationPermissionAssign.vue";
|
||||||
import UserRoleAssign from "@/pages/user/UserRoleAssign.vue";
|
import UserRoleAssign from "@/pages/user/UserRoleAssign.vue";
|
||||||
@@ -107,34 +106,6 @@ const permissionRoutes: RouteRecordRaw[] = [
|
|||||||
darkBodyBackground: "#10161f"
|
darkBodyBackground: "#10161f"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "/management/permissions/items/create",
|
|
||||||
name: "PermissionCreate",
|
|
||||||
component: PermissionEdit,
|
|
||||||
meta: {
|
|
||||||
depth: 5,
|
|
||||||
navBarVisible: true,
|
|
||||||
navBarCanBack: true,
|
|
||||||
navBarTitle: "新增权限",
|
|
||||||
tabBarVisible: false,
|
|
||||||
bodyBackground: "#F5F5F8",
|
|
||||||
darkBodyBackground: "#10161f"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/management/permissions/items/:id",
|
|
||||||
name: "PermissionEdit",
|
|
||||||
component: PermissionEdit,
|
|
||||||
meta: {
|
|
||||||
depth: 5,
|
|
||||||
navBarVisible: true,
|
|
||||||
navBarCanBack: true,
|
|
||||||
navBarTitle: "编辑权限",
|
|
||||||
tabBarVisible: false,
|
|
||||||
bodyBackground: "#F5F5F8",
|
|
||||||
darkBodyBackground: "#10161f"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/management/permissions/assign",
|
path: "/management/permissions/assign",
|
||||||
redirect: "/management/permissions/assign/p"
|
redirect: "/management/permissions/assign/p"
|
||||||
|
|||||||
@@ -167,6 +167,14 @@ export const usePermissionSettingsStore = defineStore("permission-settings", ()
|
|||||||
return (await RoleAPI.parentGrantableList(roleId)).map(normalizeRole);
|
return (await RoleAPI.parentGrantableList(roleId)).map(normalizeRole);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadCreateParentGrantableRoles(targetModuleCode: ModuleCode = moduleCode): Promise<Role[]> {
|
||||||
|
return (await RoleAPI.createParentGrantableList(targetModuleCode)).map(normalizeRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCurrentRoleDetails(targetModuleCode: ModuleCode = moduleCode): Promise<Role[]> {
|
||||||
|
return (await RoleAPI.currentDetailList(targetModuleCode)).map(normalizeRole);
|
||||||
|
}
|
||||||
|
|
||||||
async function loadPermissionDetail(id: string): Promise<Permission> {
|
async function loadPermissionDetail(id: string): Promise<Permission> {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return createEmptyPermission();
|
return createEmptyPermission();
|
||||||
@@ -389,6 +397,8 @@ export const usePermissionSettingsStore = defineStore("permission-settings", ()
|
|||||||
loadDelegationPermissions,
|
loadDelegationPermissions,
|
||||||
loadGrantableDelegationPermissions,
|
loadGrantableDelegationPermissions,
|
||||||
loadParentGrantableRoles,
|
loadParentGrantableRoles,
|
||||||
|
loadCreateParentGrantableRoles,
|
||||||
|
loadCurrentRoleDetails,
|
||||||
loadRoleDetail,
|
loadRoleDetail,
|
||||||
loadPermissionDetail,
|
loadPermissionDetail,
|
||||||
getRoleById,
|
getRoleById,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { Model } from "timi-web-api";
|
|||||||
import type { GaoPackageConfig } from "@/types/Package";
|
import type { GaoPackageConfig } from "@/types/Package";
|
||||||
|
|
||||||
export type EventStatus = "DRAFT" | "ACTIVE" | "INACTIVE" | "DELETED";
|
export type EventStatus = "DRAFT" | "ACTIVE" | "INACTIVE" | "DELETED";
|
||||||
export type EventBizType = "GENERAL" | "PACKAGE_PURCHASE";
|
export type EventBizType = "GENERAL" | "PACKAGE_PURCHASE" | "LEGACY_CONSUMPTION";
|
||||||
|
|
||||||
export type EventValueType = "NONE" | "NUMBER";
|
export type EventValueType = "NONE" | "NUMBER";
|
||||||
export type EventValueUnit = "CENT" | "COUNT";
|
export type EventValueUnit = "CENT" | "COUNT";
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { Attachment, Model, PageResult, User } from "timi-web-api";
|
import type { Attachment, Model, PageResult, User } from "timi-web-api";
|
||||||
import type { Customer } from "@/types/Customer";
|
import type { Customer } from "@/types/Customer";
|
||||||
import type { EventRecord } from "@/types/EventRecord";
|
|
||||||
import type { OrderFulfillment } from "@/types/Fulfillment";
|
import type { OrderFulfillment } from "@/types/Fulfillment";
|
||||||
|
|
||||||
export type PackageConfigStatus = "DRAFT" | "ACTIVE" | "INACTIVE";
|
export type PackageConfigStatus = "DRAFT" | "ACTIVE" | "INACTIVE";
|
||||||
@@ -64,7 +63,6 @@ export type GaoCustomerPackage = {
|
|||||||
remark?: string | null;
|
remark?: string | null;
|
||||||
customer?: Customer;
|
customer?: Customer;
|
||||||
cancelOperator?: Partial<User>;
|
cancelOperator?: Partial<User>;
|
||||||
purchaseEventRecord?: EventRecord;
|
|
||||||
itemList?: CustomerPackageItem[];
|
itemList?: CustomerPackageItem[];
|
||||||
fulfillmentList?: OrderFulfillment[];
|
fulfillmentList?: OrderFulfillment[];
|
||||||
} & Model;
|
} & Model;
|
||||||
@@ -83,7 +81,7 @@ export type PackageAttachment = Partial<Attachment> & { tempFileId?: string; att
|
|||||||
|
|
||||||
export type PackagePurchaseRequest = {
|
export type PackagePurchaseRequest = {
|
||||||
requestNo: string;
|
requestNo: string;
|
||||||
eventId: string;
|
packageConfigId: string;
|
||||||
customerId: string;
|
customerId: string;
|
||||||
saleAmount: number;
|
saleAmount: number;
|
||||||
purchasedAt?: number;
|
purchasedAt?: number;
|
||||||
|
|||||||
+21
-38
@@ -2,41 +2,20 @@ import type { BasePage, Model } from "timi-web-api";
|
|||||||
import type { Customer } from "@/types/Customer";
|
import type { Customer } from "@/types/Customer";
|
||||||
|
|
||||||
export type PointRuleStatus = "DRAFT" | "ACTIVE" | "INACTIVE" | "DELETED";
|
export type PointRuleStatus = "DRAFT" | "ACTIVE" | "INACTIVE" | "DELETED";
|
||||||
export type PointRuleTriggerType = "EVERY_TIME" | "FIRST_TIME" | "AFTER_COUNT_EVERY_TIME" | "SOURCE_VALUE" | "CUSTOMER_EVENT_VALUE_TIER";
|
|
||||||
export type PointRuleMode = "FIXED" | "SOURCE_VALUE" | "TIER";
|
/** 规则模式:简单规则固定积分值;脚本规则由 Groovy 脚本决定 */
|
||||||
|
export type PointRuleMode = "SIMPLE" | "GROOVY";
|
||||||
|
|
||||||
|
/** 直接触发来源类型:事件登记 / 订单首次全额实收 */
|
||||||
|
export type PointRuleTriggerSourceType = "EVENT_RECORD" | "SALES_ORDER_PAID";
|
||||||
|
|
||||||
|
/** 触发对象筛选类型 */
|
||||||
|
export type PointRuleTriggerRefType = "EVENT";
|
||||||
|
|
||||||
export type PointLedgerChangeType = "EARN" | "CONSUME" | "REVOKE" | "ADJUST";
|
export type PointLedgerChangeType = "EARN" | "CONSUME" | "REVOKE" | "ADJUST";
|
||||||
export type PointLedgerDirection = "INCREASE" | "DECREASE";
|
export type PointLedgerDirection = "INCREASE" | "DECREASE";
|
||||||
export type PointAction = Extract<PointLedgerChangeType, "EARN" | "CONSUME">;
|
export type PointAction = Extract<PointLedgerChangeType, "EARN" | "CONSUME">;
|
||||||
|
export type PointLedgerTriggerSourceType = PointRuleTriggerSourceType;
|
||||||
export type PointRuleCondition = {
|
|
||||||
min?: number;
|
|
||||||
max?: number;
|
|
||||||
threshold?: number;
|
|
||||||
includeCurrent?: boolean;
|
|
||||||
scope?: "CUSTOMER_EVENT";
|
|
||||||
sourceEventId?: string;
|
|
||||||
sourceValueField?: "numberValue";
|
|
||||||
window?: {
|
|
||||||
type: "ROLLING";
|
|
||||||
amount: number;
|
|
||||||
unit: "YEAR";
|
|
||||||
};
|
|
||||||
tiers?: PointRuleTier[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PointRuleTier = {
|
|
||||||
min: number;
|
|
||||||
max?: number | null;
|
|
||||||
pointValue: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PointRuleTierForm = {
|
|
||||||
key: number;
|
|
||||||
minText: string;
|
|
||||||
maxText: string;
|
|
||||||
pointAction: PointAction;
|
|
||||||
pointValueText: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PointAccount = {
|
export type PointAccount = {
|
||||||
storeId?: string;
|
storeId?: string;
|
||||||
@@ -65,8 +44,11 @@ export type PointLedger = {
|
|||||||
balanceBefore?: number;
|
balanceBefore?: number;
|
||||||
balanceAfter?: number;
|
balanceAfter?: number;
|
||||||
ruleId?: string;
|
ruleId?: string;
|
||||||
eventId?: string;
|
triggerSourceType?: PointLedgerTriggerSourceType;
|
||||||
eventRecordId?: string;
|
triggerSourceId?: string;
|
||||||
|
salesOrderId?: string;
|
||||||
|
groovyScriptVersionId?: string;
|
||||||
|
ruleSnapshot?: Record<string, unknown> | null;
|
||||||
sourcePayload?: Record<string, unknown>;
|
sourcePayload?: Record<string, unknown>;
|
||||||
idempotencyKey?: string;
|
idempotencyKey?: string;
|
||||||
remark?: string;
|
remark?: string;
|
||||||
@@ -86,13 +68,14 @@ export type PointLedgerPage = {
|
|||||||
|
|
||||||
export type PointRule = {
|
export type PointRule = {
|
||||||
storeId?: string;
|
storeId?: string;
|
||||||
eventId?: string;
|
ruleMode?: PointRuleMode;
|
||||||
|
triggerSourceType?: PointRuleTriggerSourceType;
|
||||||
|
triggerRefType?: PointRuleTriggerRefType;
|
||||||
|
triggerRefId?: string;
|
||||||
name: string;
|
name: string;
|
||||||
status?: PointRuleStatus;
|
status?: PointRuleStatus;
|
||||||
triggerType?: PointRuleTriggerType;
|
|
||||||
pointMode?: PointRuleMode;
|
|
||||||
pointValue?: number | null;
|
pointValue?: number | null;
|
||||||
conditionJson?: PointRuleCondition;
|
groovyScriptId?: string;
|
||||||
beginAt?: number;
|
beginAt?: number;
|
||||||
endAt?: number;
|
endAt?: number;
|
||||||
priority?: number;
|
priority?: number;
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export type SalesOrder = {
|
|||||||
sourceEventRecordId?: string;
|
sourceEventRecordId?: string;
|
||||||
requestNo?: string;
|
requestNo?: string;
|
||||||
purchasedAt?: number;
|
purchasedAt?: number;
|
||||||
|
paidAt?: number | null;
|
||||||
operatorUserId?: string;
|
operatorUserId?: string;
|
||||||
cancelledAt?: number;
|
cancelledAt?: number;
|
||||||
cancelOperatorUserId?: string;
|
cancelOperatorUserId?: string;
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export type Store = {
|
|||||||
address?: string;
|
address?: string;
|
||||||
remark?: string;
|
remark?: string;
|
||||||
autoClosedDay?: boolean;
|
autoClosedDay?: boolean;
|
||||||
directProductSaleEventId?: string;
|
|
||||||
sort?: number;
|
sort?: number;
|
||||||
} & Model;
|
} & Model;
|
||||||
|
|
||||||
|
|||||||
+17
-1
@@ -2,10 +2,26 @@ import type { EventBizType, EventItem } from "@/types/EventItem";
|
|||||||
|
|
||||||
/** 返回事件业务类型的前端文案。 */
|
/** 返回事件业务类型的前端文案。 */
|
||||||
export function getEventBizTypeText(bizType?: EventBizType): string {
|
export function getEventBizTypeText(bizType?: EventBizType): string {
|
||||||
return bizType === "PACKAGE_PURCHASE" ? "套餐购买" : "普通登记";
|
if (bizType === "PACKAGE_PURCHASE") {
|
||||||
|
return "套餐购买";
|
||||||
|
}
|
||||||
|
if (bizType === "LEGACY_CONSUMPTION") {
|
||||||
|
return "历史消费(退役)";
|
||||||
|
}
|
||||||
|
return "普通登记";
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 判断事件是否为套餐购买事件。 */
|
/** 判断事件是否为套餐购买事件。 */
|
||||||
export function isPackagePurchaseEvent(event?: Pick<EventItem, "bizType">): boolean {
|
export function isPackagePurchaseEvent(event?: Pick<EventItem, "bizType">): boolean {
|
||||||
return event?.bizType === "PACKAGE_PURCHASE";
|
return event?.bizType === "PACKAGE_PURCHASE";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 判断事件是否为历史消费退役事件,此类事件不可再登记或作为规则触发对象。 */
|
||||||
|
export function isLegacyConsumptionEvent(event?: Pick<EventItem, "bizType">): boolean {
|
||||||
|
return event?.bizType === "LEGACY_CONSUMPTION";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断事件是否已退役,退役事件只能通过历史详情查看。 */
|
||||||
|
export function isRetiredEvent(event?: Pick<EventItem, "bizType">): boolean {
|
||||||
|
return isPackagePurchaseEvent(event) || isLegacyConsumptionEvent(event);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,107 +0,0 @@
|
|||||||
import PointAPI from "@/api/PointAPI";
|
|
||||||
import type { PointAction, PointRule } from "@/types/Point";
|
|
||||||
|
|
||||||
export type SourceValueConfig = {
|
|
||||||
range: [number, number];
|
|
||||||
action: PointAction;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询一组事件可用的自定义积分范围,并返回所有规则范围的交集。
|
|
||||||
*
|
|
||||||
* @param eventIdList 事件 ID 列表
|
|
||||||
* @returns 可用范围;没有完整有效的 SOURCE_VALUE 规则时返回 undefined
|
|
||||||
*/
|
|
||||||
export async function loadSourceValueRange(eventIdList: string[]): Promise<[number, number] | undefined> {
|
|
||||||
return (await loadSourceValueConfig(eventIdList))?.range;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询登记时自定义积分的数量范围和积分动作。
|
|
||||||
*
|
|
||||||
* @param eventIdList 事件 ID 列表
|
|
||||||
* @returns 所有事件规则的共同配置
|
|
||||||
*/
|
|
||||||
export async function loadSourceValueConfig(eventIdList: string[]): Promise<SourceValueConfig | undefined> {
|
|
||||||
if (!eventIdList.length) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const resultList = await Promise.all(eventIdList.map(eventId => PointAPI.ruleList({
|
|
||||||
index: 0,
|
|
||||||
size: 1000,
|
|
||||||
equalsExample: {
|
|
||||||
eventId,
|
|
||||||
status: "ACTIVE"
|
|
||||||
}
|
|
||||||
})));
|
|
||||||
const ruleList = resultList.flatMap(result => result.list || []);
|
|
||||||
if (!ruleList.length) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rangeList = ruleList
|
|
||||||
.map(resolvePointValueRuleConfig)
|
|
||||||
.filter((range): range is SourceValueConfig => !!range);
|
|
||||||
const action = rangeList[0]?.action;
|
|
||||||
if (!action) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if (rangeList.some(range => range.action !== action)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const min = Math.max(...rangeList.map(range => range.range[0]), 1);
|
|
||||||
const max = Math.min(...rangeList.map(range => range.range[1]));
|
|
||||||
return min <= max ? { range: [min, max], action } : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析积分规则动作。
|
|
||||||
*
|
|
||||||
* 固定积分优先按 pointValue 正负判定;登记时自定义积分按 conditionJson.min 正负判定。
|
|
||||||
*
|
|
||||||
* @param rule 积分规则
|
|
||||||
* @returns 积分动作;缺少有效积分值时返回 undefined
|
|
||||||
*/
|
|
||||||
export function resolvePointRuleAction(rule: PointRule): PointAction | undefined {
|
|
||||||
if (rule.pointValue !== undefined && rule.pointValue !== null && rule.pointValue !== 0) {
|
|
||||||
return rule.pointValue < 0 ? "CONSUME" : "EARN";
|
|
||||||
}
|
|
||||||
const min = rule.conditionJson?.min;
|
|
||||||
if (min === undefined || min === 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return min < 0 ? "CONSUME" : "EARN";
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolvePointValueRuleConfig(rule: PointRule): SourceValueConfig | undefined {
|
|
||||||
if (rule.status !== "ACTIVE" || rule.triggerType !== "SOURCE_VALUE" || rule.pointMode !== "SOURCE_VALUE") {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return resolveSourceValueRuleConfig(rule);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveSourceValueRuleConfig(rule: PointRule): SourceValueConfig | undefined {
|
|
||||||
const min = rule.conditionJson?.min;
|
|
||||||
const max = rule.conditionJson?.max;
|
|
||||||
const action = resolvePointRuleAction(rule);
|
|
||||||
if (
|
|
||||||
min === undefined
|
|
||||||
|| max === undefined
|
|
||||||
|| action === undefined
|
|
||||||
|| !Number.isSafeInteger(min)
|
|
||||||
|| !Number.isSafeInteger(max)
|
|
||||||
|| min === 0
|
|
||||||
|| max === 0
|
|
||||||
|| (action === "CONSUME") !== (min < 0)
|
|
||||||
|| (action === "CONSUME") !== (max < 0)
|
|
||||||
) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const boundaryList = [Math.abs(min), Math.abs(max)];
|
|
||||||
return {
|
|
||||||
action,
|
|
||||||
range: [Math.min(...boundaryList), Math.max(...boundaryList)]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import type { EventValueUnit } from "@/types/EventItem";
|
|
||||||
import type { PointRuleTierForm } from "@/types/Point";
|
|
||||||
import { formatCentValue, resolveCentValue } from "@/utils/eventValue";
|
|
||||||
|
|
||||||
export function resolveNonNegativeInteger(value: string): number | undefined {
|
|
||||||
if (!value.trim()) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const numberValue = Number(value);
|
|
||||||
return Number.isSafeInteger(numberValue) && !(numberValue < 0) ? numberValue : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 按来源事件的存储单位解析分档边界。
|
|
||||||
* CENT 的表单值使用元,解析结果使用分;COUNT 直接使用整数次数。
|
|
||||||
*/
|
|
||||||
export function resolveTierBoundaryValue(value: string, valueUnit?: EventValueUnit | null): number | undefined {
|
|
||||||
return valueUnit === "CENT" ? resolveCentValue(value.trim()) : resolveNonNegativeInteger(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 按来源事件的展示单位格式化分档边界。
|
|
||||||
*/
|
|
||||||
export function formatTierBoundaryValue(value: number, valueUnit?: EventValueUnit | null): string {
|
|
||||||
return valueUnit === "CENT" ? formatCentValue(value) : String(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTierMinValue(tierList: PointRuleTierForm[], index: number, valueUnit?: EventValueUnit | null): number | undefined {
|
|
||||||
if (index < 1) {
|
|
||||||
return resolveTierBoundaryValue(tierList[0]?.minText || "", valueUnit);
|
|
||||||
}
|
|
||||||
const previousMax = resolveTierBoundaryValue(tierList[index - 1]?.maxText || "", valueUnit);
|
|
||||||
if (previousMax === undefined || !Number.isSafeInteger(previousMax + 1)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return previousMax + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTierMinText(tierList: PointRuleTierForm[], index: number, valueUnit?: EventValueUnit | null): string {
|
|
||||||
const min = getTierMinValue(tierList, index, valueUnit);
|
|
||||||
return min === undefined ? "待填写" : formatTierBoundaryValue(min, valueUnit);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isLastTier(tierList: PointRuleTierForm[], index: number): boolean {
|
|
||||||
return index === tierList.length - 1;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user