|
|
@@ -103,6 +103,18 @@
|
|
|
|
@status-change="handleScanStatusChange"
|
|
|
|
@status-change="handleScanStatusChange"
|
|
|
|
/>
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<t-form-item label="调理部位" arrow @click="openBodyAreaPicker">
|
|
|
|
|
|
|
|
<div class="picker-text" v-text="selectedAreaText" />
|
|
|
|
|
|
|
|
</t-form-item>
|
|
|
|
|
|
|
|
</t-form>
|
|
|
|
|
|
|
|
<customer-tag-card
|
|
|
|
|
|
|
|
v-if="canReadTag"
|
|
|
|
|
|
|
|
class="tag-card"
|
|
|
|
|
|
|
|
:editable="canEditTag"
|
|
|
|
|
|
|
|
:tag-list="customerTagList"
|
|
|
|
|
|
|
|
@edit="openTagSelector"
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
<t-form class="form" label-align="left" label-width="3rem">
|
|
|
|
<t-form-item label="住址">
|
|
|
|
<t-form-item label="住址">
|
|
|
|
<t-textarea
|
|
|
|
<t-textarea
|
|
|
|
class="textarea"
|
|
|
|
class="textarea"
|
|
|
@@ -119,9 +131,6 @@
|
|
|
|
placeholder="病症"
|
|
|
|
placeholder="病症"
|
|
|
|
/>
|
|
|
|
/>
|
|
|
|
</t-form-item>
|
|
|
|
</t-form-item>
|
|
|
|
<t-form-item label="调理部位" arrow @click="openBodyAreaPicker">
|
|
|
|
|
|
|
|
<div class="picker-text" v-text="selectedAreaText" />
|
|
|
|
|
|
|
|
</t-form-item>
|
|
|
|
|
|
|
|
<t-form-item label="备注">
|
|
|
|
<t-form-item label="备注">
|
|
|
|
<t-textarea
|
|
|
|
<t-textarea
|
|
|
|
class="textarea"
|
|
|
|
class="textarea"
|
|
|
@@ -155,6 +164,12 @@
|
|
|
|
v-model:visible="selectorVisible"
|
|
|
|
v-model:visible="selectorVisible"
|
|
|
|
@change="handleIntroducerChange"
|
|
|
|
@change="handleIntroducerChange"
|
|
|
|
/>
|
|
|
|
/>
|
|
|
|
|
|
|
|
<tag-selector
|
|
|
|
|
|
|
|
v-if="canEditTag"
|
|
|
|
|
|
|
|
v-model="selectedTagIdList"
|
|
|
|
|
|
|
|
v-model:visible="tagSelectorVisible"
|
|
|
|
|
|
|
|
@change="handleTagSelected"
|
|
|
|
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<t-popup v-model="storePickerVisible" placement="bottom">
|
|
|
|
<t-popup v-model="storePickerVisible" placement="bottom">
|
|
|
|
<t-picker
|
|
|
|
<t-picker
|
|
|
@@ -180,16 +195,20 @@
|
|
|
|
import { type Attachment, AttachmentAPI, useUserStore } from "timi-web";
|
|
|
|
import { type Attachment, AttachmentAPI, useUserStore } from "timi-web";
|
|
|
|
import { Toast } from "tdesign-mobile-vue";
|
|
|
|
import { Toast } from "tdesign-mobile-vue";
|
|
|
|
import CustomerAPI from "@/api/CustomerAPI";
|
|
|
|
import CustomerAPI from "@/api/CustomerAPI";
|
|
|
|
|
|
|
|
import TagAPI from "@/api/TagAPI";
|
|
|
|
import BodyAreaSelector from "@/components/BodyAreaSelector.vue";
|
|
|
|
import BodyAreaSelector from "@/components/BodyAreaSelector.vue";
|
|
|
|
import CodeScanner from "@/components/CodeScanner.vue";
|
|
|
|
import CodeScanner from "@/components/CodeScanner.vue";
|
|
|
|
import CustomerSelector from "@/components/CustomerSelector.vue";
|
|
|
|
import CustomerSelector from "@/components/CustomerSelector.vue";
|
|
|
|
import PhotoScroller from "@/components/PhotoScroller.vue";
|
|
|
|
import PhotoScroller from "@/components/PhotoScroller.vue";
|
|
|
|
|
|
|
|
import TagSelector from "@/components/TagSelector.vue";
|
|
|
|
|
|
|
|
import CustomerTagCard from "@/pages/customer/component/CustomerTagCard.vue";
|
|
|
|
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
|
|
|
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
|
|
|
import { useSettingStore } from "@/store/settingStore";
|
|
|
|
import { useSettingStore } from "@/store/settingStore";
|
|
|
|
import type { BodyAreaValue, Customer, CustomerAttachmentType, EditableCustomerPhoto } from "@/types/Customer";
|
|
|
|
import type { BodyAreaValue, Customer, CustomerAttachmentType, EditableCustomerPhoto } from "@/types/Customer";
|
|
|
|
|
|
|
|
import type { Tag } from "@/types/Tag";
|
|
|
|
import { bodyAreaLabelMap } from "@/utils/bodyArea";
|
|
|
|
import { bodyAreaLabelMap } from "@/utils/bodyArea";
|
|
|
|
import { normalizeBodyAreaValue } from "@/utils/customer";
|
|
|
|
import { normalizeBodyAreaValue } from "@/utils/customer";
|
|
|
|
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
|
|
|
import { corePermissionCodes, gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
|
|
|
import { compressUploadImage } from "@/utils/imageCompression";
|
|
|
|
import { compressUploadImage } from "@/utils/imageCompression";
|
|
|
|
|
|
|
|
|
|
|
|
type PickerValue = string | number;
|
|
|
|
type PickerValue = string | number;
|
|
|
@@ -285,39 +304,44 @@ async function handleIntroducerChange(id: string): Promise<void> {
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- 表单 ----------
|
|
|
|
// ---------- 表单 ----------
|
|
|
|
|
|
|
|
|
|
|
|
const photoInputRef = ref<HTMLInputElement>();
|
|
|
|
const formData = reactive<Customer>({
|
|
|
|
const photoAddActions = [
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
icon: "user-avatar",
|
|
|
|
|
|
|
|
text: "拍照片",
|
|
|
|
|
|
|
|
emit: "add-photo",
|
|
|
|
|
|
|
|
theme: "primary"
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
icon: "cardmembership",
|
|
|
|
|
|
|
|
text: "拍积分卡",
|
|
|
|
|
|
|
|
emit: "add-point-card",
|
|
|
|
|
|
|
|
theme: "primary"
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
icon: "file-attachment",
|
|
|
|
|
|
|
|
text: "其他文件",
|
|
|
|
|
|
|
|
emit: "add-other",
|
|
|
|
|
|
|
|
theme: "default"
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
] as const;
|
|
|
|
|
|
|
|
const photoPickerTarget = ref<CustomerAttachmentType>("PHOTO");
|
|
|
|
|
|
|
|
const isPhotoUploading = ref(false);
|
|
|
|
|
|
|
|
const photoItemList = ref<EditableCustomerPhoto[]>([]);
|
|
|
|
|
|
|
|
const photoList = computed<string[]>({
|
|
|
|
|
|
|
|
get() {
|
|
|
|
|
|
|
|
return photoItemList.value.map(item => item.url);
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
set(value) {
|
|
|
|
|
|
|
|
const urlSet = new Set(value);
|
|
|
|
|
|
|
|
photoItemList.value = photoItemList.value.filter(item => urlSet.has(item.url));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- 标签 ----------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const canReadTag = computed(() => userStore.hasPermissions(corePermissionCodes.tag.read));
|
|
|
|
|
|
|
|
const canApplyTag = computed(() => userStore.hasPermissions(corePermissionCodes.tag.apply));
|
|
|
|
|
|
|
|
const canEditTag = computed(() => canReadTag.value && canApplyTag.value);
|
|
|
|
|
|
|
|
const customerTagList = ref<Tag[]>([]);
|
|
|
|
|
|
|
|
const selectedTagIdList = ref<string[]>([]);
|
|
|
|
|
|
|
|
const tagSelectorVisible = ref(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function loadCustomerTags(customerId: string): Promise<void> {
|
|
|
|
|
|
|
|
if (!customerId || !canReadTag.value) {
|
|
|
|
|
|
|
|
customerTagList.value = [];
|
|
|
|
|
|
|
|
selectedTagIdList.value = [];
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
customerTagList.value = await TagAPI.listByBizType("GAO_CUSTOMER", customerId);
|
|
|
|
|
|
|
|
selectedTagIdList.value = customerTagList.value.map(item => item.id || "").filter(Boolean);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function openTagSelector(): void {
|
|
|
|
|
|
|
|
if (!canEditTag.value) {
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
selectedTagIdList.value = customerTagList.value.map(item => item.id || "").filter(Boolean);
|
|
|
|
|
|
|
|
tagSelectorVisible.value = true;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function handleTagSelected(tagList: Tag[]): void {
|
|
|
|
|
|
|
|
customerTagList.value = tagList;
|
|
|
|
|
|
|
|
selectedTagIdList.value = tagList.map(item => item.id || "").filter(Boolean);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- 调理部位 ----------
|
|
|
|
|
|
|
|
|
|
|
|
const selectedAreas = ref<BodyAreaValue>({
|
|
|
|
const selectedAreas = ref<BodyAreaValue>({
|
|
|
|
selectedCodeList: []
|
|
|
|
selectedCodeList: []
|
|
|
|
});
|
|
|
|
});
|
|
|
@@ -325,9 +349,6 @@ const bodyAreaPickerVisible = ref(false);
|
|
|
|
const bodyAreaDraft = ref<BodyAreaValue>({
|
|
|
|
const bodyAreaDraft = ref<BodyAreaValue>({
|
|
|
|
selectedCodeList: []
|
|
|
|
selectedCodeList: []
|
|
|
|
});
|
|
|
|
});
|
|
|
|
const formData = reactive<Customer>({
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const selectedAreaText = computed(() => {
|
|
|
|
const selectedAreaText = computed(() => {
|
|
|
|
const selectedLabelList = selectedAreas.value.selectedCodeList.map(item => bodyAreaLabelMap.get(item) || item);
|
|
|
|
const selectedLabelList = selectedAreas.value.selectedCodeList.map(item => bodyAreaLabelMap.get(item) || item);
|
|
|
|
return selectedLabelList.length ? `${selectedLabelList.join("、")}` : "选择调理部位";
|
|
|
|
return selectedLabelList.length ? `${selectedLabelList.join("、")}` : "选择调理部位";
|
|
|
@@ -412,11 +433,14 @@ async function loadDetail() {
|
|
|
|
await prepareStoreScope();
|
|
|
|
await prepareStoreScope();
|
|
|
|
const id = String(route.params.id || "");
|
|
|
|
const id = String(route.params.id || "");
|
|
|
|
if (!id) {
|
|
|
|
if (!id) {
|
|
|
|
|
|
|
|
customerTagList.value = [];
|
|
|
|
|
|
|
|
selectedTagIdList.value = [];
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const customer = await CustomerAPI.detail(id);
|
|
|
|
const customer = await CustomerAPI.detail(id);
|
|
|
|
Object.assign(formData, customer);
|
|
|
|
Object.assign(formData, customer);
|
|
|
|
|
|
|
|
await loadCustomerTags(id);
|
|
|
|
formData.storeId = customer.storeId || formData.storeId;
|
|
|
|
formData.storeId = customer.storeId || formData.storeId;
|
|
|
|
photoItemList.value = (customer.attachmentList || [])
|
|
|
|
photoItemList.value = (customer.attachmentList || [])
|
|
|
|
.filter(item => !!item.id)
|
|
|
|
.filter(item => !!item.id)
|
|
|
@@ -458,6 +482,40 @@ watch(() => formData.birthdate, (value) => {
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- 照片 ----------
|
|
|
|
// ---------- 照片 ----------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const photoInputRef = ref<HTMLInputElement>();
|
|
|
|
|
|
|
|
const photoAddActions = [
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
icon: "user-avatar",
|
|
|
|
|
|
|
|
text: "拍照片",
|
|
|
|
|
|
|
|
emit: "add-photo",
|
|
|
|
|
|
|
|
theme: "primary"
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
icon: "cardmembership",
|
|
|
|
|
|
|
|
text: "拍积分卡",
|
|
|
|
|
|
|
|
emit: "add-point-card",
|
|
|
|
|
|
|
|
theme: "primary"
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
icon: "file-attachment",
|
|
|
|
|
|
|
|
text: "其他文件",
|
|
|
|
|
|
|
|
emit: "add-other",
|
|
|
|
|
|
|
|
theme: "default"
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
] as const;
|
|
|
|
|
|
|
|
const photoPickerTarget = ref<CustomerAttachmentType>("PHOTO");
|
|
|
|
|
|
|
|
const isPhotoUploading = ref(false);
|
|
|
|
|
|
|
|
const photoItemList = ref<EditableCustomerPhoto[]>([]);
|
|
|
|
|
|
|
|
const photoList = computed<string[]>({
|
|
|
|
|
|
|
|
get() {
|
|
|
|
|
|
|
|
return photoItemList.value.map(item => item.url);
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
set(value) {
|
|
|
|
|
|
|
|
const urlSet = new Set(value);
|
|
|
|
|
|
|
|
photoItemList.value = photoItemList.value.filter(item => urlSet.has(item.url));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeAttachmentType(attachType?: string): CustomerAttachmentType {
|
|
|
|
function normalizeAttachmentType(attachType?: string): CustomerAttachmentType {
|
|
|
|
if (attachType === "POINT_CARD" || attachType === "OTHER") {
|
|
|
|
if (attachType === "POINT_CARD" || attachType === "OTHER") {
|
|
|
|
return attachType;
|
|
|
|
return attachType;
|
|
|
@@ -505,6 +563,14 @@ async function doSubmit(): Promise<void> {
|
|
|
|
payload.conditioningPart = selectedAreas.value;
|
|
|
|
payload.conditioningPart = selectedAreas.value;
|
|
|
|
payload.introducerCustomerId = introducerCustomerId.value || "";
|
|
|
|
payload.introducerCustomerId = introducerCustomerId.value || "";
|
|
|
|
payload.attachmentList = buildAttachmentList();
|
|
|
|
payload.attachmentList = buildAttachmentList();
|
|
|
|
|
|
|
|
delete payload.tagApply;
|
|
|
|
|
|
|
|
if (canEditTag.value) {
|
|
|
|
|
|
|
|
payload.tagApply = {
|
|
|
|
|
|
|
|
bizType: "GAO_CUSTOMER",
|
|
|
|
|
|
|
|
bizId: payload.id || "",
|
|
|
|
|
|
|
|
tagIdList: [...selectedTagIdList.value]
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
}
|
|
|
|
delete payload.introducerCustomer;
|
|
|
|
delete payload.introducerCustomer;
|
|
|
|
|
|
|
|
|
|
|
|
if (isCreateMode.value) {
|
|
|
|
if (isCreateMode.value) {
|
|
|
@@ -546,7 +612,10 @@ function buildAttachmentList(): Attachment[] {
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="less">
|
|
|
|
<style scoped lang="less">
|
|
|
|
.customer-edit {
|
|
|
|
.customer-edit {
|
|
|
|
|
|
|
|
gap: 1rem;
|
|
|
|
padding: calc(var(--app-nav-height) + 1rem) 0 4rem 0;
|
|
|
|
padding: calc(var(--app-nav-height) + 1rem) 0 4rem 0;
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
|
|
|
|
|
|
.form {
|
|
|
|
.form {
|
|
|
|
padding: .5rem 0;
|
|
|
|
padding: .5rem 0;
|
|
|
|