update permission and product
This commit is contained in:
@@ -1,36 +1,36 @@
|
||||
<template>
|
||||
<div class="customer-list">
|
||||
<t-cell-group v-if="customerList.length">
|
||||
<t-cell
|
||||
v-entrance-transition="index * 30"
|
||||
v-for="(item, index) in customerList"
|
||||
:key="item.id || item.code || index"
|
||||
:note="item.code || ''"
|
||||
hover
|
||||
@click="handleSelect(item)"
|
||||
>
|
||||
<template #title>
|
||||
<div class="cell-title">
|
||||
<span v-text="item.name || item.code || '未命名客户'" />
|
||||
<gender-icon :value="item.gender" />
|
||||
</div>
|
||||
</template>
|
||||
<template #description>
|
||||
<telephone-link :value="item.telephone" placeholder="未填写电话" />
|
||||
</template>
|
||||
<template #leftIcon>
|
||||
<div class="left-icon">
|
||||
<span v-if="selectable" class="check" :class="{selected: isSelected(item)}">
|
||||
<t-icon v-if="isSelected(item)" name="check" />
|
||||
</span>
|
||||
<t-image v-if="getCustomerPhotoURL(item, 64)" :src="getCustomerPhotoURL(item, 64)" :alt="item.name" fit="cover" class="avatar" />
|
||||
<div v-else class="avatar fallback" v-text="(item.name || item.code || '客').slice(0, 1)" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="removable" #rightIcon>
|
||||
<t-button theme="danger" size="extra-small" @click.stop="handleRemove(item)">移除</t-button>
|
||||
</template>
|
||||
</t-cell>
|
||||
<t-cell
|
||||
v-entrance-transition="index * 30"
|
||||
v-for="(item, index) in customerList"
|
||||
:key="item.id || item.code || index"
|
||||
:note="item.code || ''"
|
||||
hover
|
||||
@click="handleSelect(item)"
|
||||
>
|
||||
<template #title>
|
||||
<div class="cell-title">
|
||||
<span v-text="item.name || item.code || '未命名客户'" />
|
||||
<gender-icon :value="item.gender" />
|
||||
</div>
|
||||
</template>
|
||||
<template #description>
|
||||
<telephone-link :value="item.telephone" placeholder="未填写电话" />
|
||||
</template>
|
||||
<template #leftIcon>
|
||||
<div class="left-icon">
|
||||
<span v-if="selectable" class="check" :class="{selected: isSelected(item)}">
|
||||
<t-icon v-if="isSelected(item)" name="check" />
|
||||
</span>
|
||||
<t-image v-if="getCustomerPhotoURL(item, 64)" :src="getCustomerPhotoURL(item, 64)" :alt="item.name" fit="cover" class="avatar" />
|
||||
<div v-else class="avatar fallback" v-text="(item.name || item.code || '客').slice(0, 1)" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="removable" #rightIcon>
|
||||
<t-button theme="danger" size="extra-small" @click.stop="handleRemove(item)">移除</t-button>
|
||||
</template>
|
||||
</t-cell>
|
||||
</t-cell-group>
|
||||
<div v-else class="empty-block" v-text="emptyText" />
|
||||
</div>
|
||||
@@ -81,6 +81,7 @@ function handleRemove(customer: Customer): void {
|
||||
|
||||
<style scoped lang="less">
|
||||
.customer-list {
|
||||
|
||||
.cell-title {
|
||||
gap: .5rem;
|
||||
display: flex;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="event-selector">
|
||||
<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
|
||||
v-for="(item, index) in props.eventList"
|
||||
v-for="(item, index) in selectableEventList"
|
||||
:key="item.id || item.name"
|
||||
class="item-wrapper"
|
||||
v-entrance-transition="index * 30"
|
||||
@@ -38,6 +38,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { EventItem } from "@/types/EventItem";
|
||||
import { isRetiredEvent } from "@/utils/event";
|
||||
|
||||
interface Props {
|
||||
eventList: EventItem[];
|
||||
@@ -58,6 +59,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
|
||||
const modelValue = defineModel<string[]>({ required: true });
|
||||
const defaultColor = "#2f88ff";
|
||||
const selectableEventList = computed(() => props.eventList.filter(item => !isRetiredEvent(item)));
|
||||
|
||||
function normalizeEventId(item: EventItem): string {
|
||||
if (item.id === undefined || item.id === null) {
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<section class="package-purchase-form">
|
||||
<div v-if="props.packageConfig" class="config-summary">
|
||||
<div class="header">
|
||||
<div class="text-bold" v-text="props.packageConfig.name || '未命名套餐'" />
|
||||
<div class="price">
|
||||
<span class="label gray">标准价:</span>
|
||||
<span class="value red text-bold" v-text="formatPackageAmount(props.packageConfig.standardAmount)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="description" v-text="props.packageConfig.description" />
|
||||
</div>
|
||||
<div v-else class="center-tips" v-text="'当前购买事件未返回绑定套餐,无法完成购买。'" />
|
||||
<t-form-item label="成交价(元)" label-width="6rem" required-mark>
|
||||
<t-input
|
||||
v-model.trim="saleAmount"
|
||||
type="number"
|
||||
align="right"
|
||||
borderless
|
||||
:readonly="!props.canPriceOverride"
|
||||
placeholder="请输入成交价"
|
||||
/>
|
||||
</t-form-item>
|
||||
<t-form-item label-width="0">
|
||||
<div class="items">
|
||||
<div class="title">
|
||||
<strong>购买权益明细</strong>
|
||||
<span v-if="!props.canPriceOverride">当前账号无定制权限,明细不可修改</span>
|
||||
</div>
|
||||
<t-form-item
|
||||
v-for="(item, index) in itemList"
|
||||
:key="`${index}-${item.productId}-${item.productUnitId}`"
|
||||
class="item"
|
||||
:label="getItemLabel(item)"
|
||||
>
|
||||
<t-input
|
||||
:model-value="item.totalQuantity"
|
||||
type="number"
|
||||
:readonly="!props.canPriceOverride"
|
||||
placeholder="总权益数量"
|
||||
borderless
|
||||
align="right"
|
||||
@update:model-value="updateItemQuantity(index, $event)"
|
||||
>
|
||||
<template #suffix-icon>
|
||||
<span class="unit" v-text="getItemUnit(item)" />
|
||||
</template>
|
||||
</t-input>
|
||||
</t-form-item>
|
||||
<div v-if="!itemList.length" class="center-tips" v-text="'当前套餐暂无权益明细'" />
|
||||
</div>
|
||||
</t-form-item>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { GaoPackageConfig, PackagePurchaseItem } from "@/types/Package";
|
||||
import { formatPackageAmount } from "@/utils/package";
|
||||
|
||||
defineOptions({ name: "PackagePurchaseForm" });
|
||||
|
||||
interface Props {
|
||||
packageConfig?: GaoPackageConfig;
|
||||
canPriceOverride: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const saleAmount = defineModel<string>("saleAmount", { required: true });
|
||||
const itemList = defineModel<PackagePurchaseItem[]>("itemList", { required: true });
|
||||
|
||||
function getItemLabel(item: PackagePurchaseItem): string {
|
||||
const configItem = props.packageConfig?.itemList?.find(config => config.productId === item.productId && config.productUnitId === item.productUnitId);
|
||||
return configItem ? `${configItem.productNameSnapshot || "未命名产品"} · ${configItem.unitSnapshot || "未设置单位"}` : "产品权益";
|
||||
}
|
||||
|
||||
function getItemUnit(item: PackagePurchaseItem): string {
|
||||
return props.packageConfig?.itemList?.find(config => config.productId === item.productId && config.productUnitId === item.productUnitId)?.unitSnapshot || "";
|
||||
}
|
||||
|
||||
function updateItemQuantity(index: number, value: string | number): void {
|
||||
itemList.value = itemList.value.map((item, itemIndex) => itemIndex === index ? {
|
||||
...item,
|
||||
totalQuantity: String(value)
|
||||
} : item);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.package-purchase-form {
|
||||
gap: .75rem;
|
||||
display: grid;
|
||||
margin-top: 1rem;
|
||||
|
||||
.config-summary {
|
||||
gap: .25rem;
|
||||
margin: 0 1rem;
|
||||
padding: .5rem .75rem;
|
||||
display: flex;
|
||||
background: var(--td-error-color-1);
|
||||
border-radius: .75rem;
|
||||
flex-direction: column;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.price {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
.label {
|
||||
font-size: .75rem;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 1.75rem;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.items {
|
||||
gap: .5rem;
|
||||
display: grid;
|
||||
|
||||
.title {
|
||||
font-size: .8125rem;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.item {
|
||||
border-radius: 1rem;
|
||||
box-shadow: .25rem .25rem 1rem rgba(0, 0, 0, .08);
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,49 +1,69 @@
|
||||
<template>
|
||||
<div class="product-selector">
|
||||
<t-form-item :label="props.label" :required-mark="props.requiredMark" arrow @click="open">
|
||||
<span class="picker-text" v-text="selectedText || props.placeholder" />
|
||||
</t-form-item>
|
||||
<t-popup v-model="visible" placement="bottom" :style="{height: '80vh'}">
|
||||
<div class="popup">
|
||||
<div class="header">
|
||||
<t-button variant="text" size="small" @click="visible = false">取消</t-button>
|
||||
<strong>选择产品单位</strong>
|
||||
<t-button theme="primary" variant="text" size="small" @click="confirm">确定</t-button>
|
||||
</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" />
|
||||
<span v-if="unit.salePrice !== undefined && unit.salePrice !== null" class="unit-price" v-text="formatProductAmount(unit.salePrice)" />
|
||||
</t-button>
|
||||
</div>
|
||||
</template>
|
||||
</t-cell>
|
||||
</t-cell-group>
|
||||
<div v-else class="empty" v-text="loading ? '正在加载可售产品' : '暂无可售产品'" />
|
||||
</div>
|
||||
<t-form-item
|
||||
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" />
|
||||
</t-form-item>
|
||||
<bottom-popup
|
||||
v-model="visible"
|
||||
title="选择产品"
|
||||
height="80vh"
|
||||
right-button-text=""
|
||||
@left-button-click="visible = false"
|
||||
>
|
||||
<div class="popup">
|
||||
<div class="search-wrap">
|
||||
<t-input
|
||||
v-model="keyword"
|
||||
class="search"
|
||||
borderless
|
||||
placeholder="搜索产品名称或编码"
|
||||
/>
|
||||
</div>
|
||||
</t-popup>
|
||||
</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>
|
||||
</t-cell>
|
||||
</t-cell-group>
|
||||
<div v-else class="empty" v-text="loading ? '正在加载可售产品' : '暂无可售产品'" />
|
||||
</div>
|
||||
</div>
|
||||
</bottom-popup>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BottomPopup } from "timi-tdesign-mobile";
|
||||
import ProductAPI from "@/api/ProductAPI";
|
||||
import type { Product, ProductUnit } from "@/types/Product";
|
||||
import type { Product } from "@/types/Product";
|
||||
import { getErrorMessage } from "@/utils/error";
|
||||
import { Message } from "@/utils/message";
|
||||
import { formatProductAmount, getProductBaseUnitName } from "@/utils/product";
|
||||
import { getProductPhotoURL } from "@/utils/product";
|
||||
|
||||
interface Props {
|
||||
modelValue: { productId: string; productUnitId: string } | undefined;
|
||||
@@ -52,29 +72,27 @@ interface Props {
|
||||
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 visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const keyword = ref("");
|
||||
const productList = ref<Product[]>([]);
|
||||
const draft = ref<Props["modelValue"]>();
|
||||
const selectedText = computed(() => {
|
||||
if (!props.modelValue) {
|
||||
return "";
|
||||
}
|
||||
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 && unit ? `${product.name} · ${unit.name}` : "已选择产品单位";
|
||||
return product?.name || "";
|
||||
});
|
||||
const filteredProductList = computed(() => {
|
||||
const text = keyword.value.toLocaleLowerCase();
|
||||
return productList.value.filter(item => !text || `${item.name}${item.code}`.toLocaleLowerCase().includes(text));
|
||||
});
|
||||
const visibleProductList = computed(() => filteredProductList.value);
|
||||
|
||||
async function open(): Promise<void> {
|
||||
visible.value = true;
|
||||
draft.value = props.modelValue ? { ...props.modelValue } : undefined;
|
||||
if (productList.value.length) {
|
||||
return;
|
||||
}
|
||||
@@ -88,22 +106,15 @@ async function open(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function getSaleableUnits(product: Product): ProductUnit[] {
|
||||
return (product.unitList || []).filter(item => item.allowSale !== false && item.id);
|
||||
}
|
||||
function isSelected(product: Product, unit: ProductUnit): boolean {
|
||||
return draft.value?.productId === product.id && draft.value?.productUnitId === unit.id;
|
||||
}
|
||||
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;
|
||||
function handleProductSelect(product: Product): void {
|
||||
if (!product.id) {
|
||||
return;
|
||||
}
|
||||
emit("update:modelValue", {
|
||||
productId: product.id,
|
||||
productUnitId: product.id === props.modelValue?.productId ? props.modelValue?.productUnitId || "" : ""
|
||||
});
|
||||
visible.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -112,55 +123,65 @@ function confirm(): void {
|
||||
.picker-text {
|
||||
color: var(--td-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.popup {
|
||||
height: 100%;
|
||||
padding: 1rem 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.popup {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
padding: 1rem 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
padding: 0 1rem .75rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.search-wrap {
|
||||
z-index: 1;
|
||||
padding: 1rem 0 .5rem 0;
|
||||
box-shadow: 0 4px 4px rgba(0, 0, 0, .05);
|
||||
|
||||
.search {
|
||||
margin: 0 1rem .75rem;
|
||||
--td-input-vertical-padding: 8px 14px;
|
||||
|
||||
flex: 0;
|
||||
margin: 0 1rem;
|
||||
border: 1px solid var(--td-component-border);
|
||||
border-radius: 1rem;
|
||||
border-radius: .875rem;
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
.items {
|
||||
flex: 1;
|
||||
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 {
|
||||
gap: .35rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
|
||||
.t-button {
|
||||
gap: .125rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.unit-price {
|
||||
font-size: .6875rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: var(--td-text-color-secondary);
|
||||
padding: 2rem 1rem;
|
||||
text-align: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: var(--td-text-color-secondary);
|
||||
padding: 2rem 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.avatar .t-image__img) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: inherit;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user