support tag for customer create

This commit is contained in:
Timi
2026-08-20 14:57:50 +08:00
parent 2bab92edaa
commit f83404f0ee
4 changed files with 111 additions and 41 deletions
-1
View File
@@ -103,7 +103,6 @@ function toggle(code: string): void {
align-items: center; align-items: center;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
background: #f8fbff;
border-radius: 1.25rem; border-radius: 1.25rem;
.figure { .figure {
+108 -39
View File
@@ -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;
@@ -66,7 +66,7 @@ function emitEdit(): void {
display: flex; display: flex;
padding: 1rem; padding: 1rem;
overflow: hidden; overflow: hidden;
border-radius: .75rem; border-radius: 1rem;
flex-direction: column; flex-direction: column;
.header { .header {
+2
View File
@@ -1,5 +1,6 @@
import type { Attachment, Model, User } from "timi-web"; import type { Attachment, Model, User } from "timi-web";
import type { EventRecord } from "@/types/EventRecord.ts"; import type { EventRecord } from "@/types/EventRecord.ts";
import type { TagApply } from "@/types/Tag";
export type CustomerGender = "MALE" | "FEMALE"; export type CustomerGender = "MALE" | "FEMALE";
export type CustomerBirthdateCalendar = "SOLAR" | "LUNAR"; export type CustomerBirthdateCalendar = "SOLAR" | "LUNAR";
@@ -30,6 +31,7 @@ export type Customer = {
eventRecord?: EventRecord; eventRecord?: EventRecord;
eventRecordList?: EventRecord[]; eventRecordList?: EventRecord[];
attachmentList?: Attachment[]; attachmentList?: Attachment[];
tagApply?: TagApply;
} & Model; } & Model;
export type CustomerAttachmentType = "PHOTO" | "POINT_CARD" | "OTHER"; export type CustomerAttachmentType = "PHOTO" | "POINT_CARD" | "OTHER";