refactor event
This commit is contained in:
+46
-97
@@ -1,36 +1,10 @@
|
|||||||
import { axios, type Page, type PageResult } from "timi-web";
|
import { axios, type Page, type PageResult } from "timi-web";
|
||||||
import type { Customer, CustomerDailyStatView, CustomerGenderStatView, CustomerInviteUpdateReq, CustomerSearchReq, CustomerTrendStatReq, QuickRegisterReq } from "@/types/Customer";
|
import type { Customer, CustomerDailyStatView, CustomerGenderStatView, CustomerTrendStatReq } from "@/types/Customer";
|
||||||
import type { EventRecord } from "@/types/EventRecord";
|
|
||||||
|
|
||||||
const BASE_URI = "/gao/customer";
|
const BASE_URI = "/gao/customer";
|
||||||
|
|
||||||
async function list(req: Page<Customer>): Promise<PageResult<Customer>> {
|
async function list(page: Page<Customer>): Promise<PageResult<Customer>> {
|
||||||
return await axios.post(`${BASE_URI}/list`, req);
|
return await axios.post(`${BASE_URI}/list`, page);
|
||||||
}
|
|
||||||
|
|
||||||
async function search(req: CustomerSearchReq): Promise<Customer[]> {
|
|
||||||
const [codeMatched, nameMatched] = await Promise.all([
|
|
||||||
req.keyword ? nullableDetailByCode(req.keyword) : undefined,
|
|
||||||
req.keyword ? nullableDetailByName(req.keyword) : undefined
|
|
||||||
]);
|
|
||||||
return [codeMatched, nameMatched].filter((item, index, list): item is Customer => !!item && index === list.findIndex(current => current?.id === item.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function searchPage(req: Page<CustomerSearchReq>): Promise<PageResult<Customer>> {
|
|
||||||
return await axios.post(`${BASE_URI}/list`, {
|
|
||||||
...req,
|
|
||||||
equalsExample: buildCustomerListExample(req.equalsExample)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function exportExcel(req: CustomerSearchReq): Promise<Blob> {
|
|
||||||
return await axios.post(`${BASE_URI}/export`, {
|
|
||||||
index: 0,
|
|
||||||
size: 100000,
|
|
||||||
equalsExample: buildCustomerListExample(req)
|
|
||||||
}, {
|
|
||||||
responseType: "blob"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function detail(id: string): Promise<Customer> {
|
async function detail(id: string): Promise<Customer> {
|
||||||
@@ -39,52 +13,12 @@ async function detail(id: string): Promise<Customer> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function nullableDetailByCode(code: string): Promise<Customer | null> {
|
async function create(customer: Customer): Promise<string> {
|
||||||
return await axios.post(`${BASE_URI}/find/code`, undefined, {
|
return await axios.post(`${BASE_URI}/create`, customer);
|
||||||
params: { code }
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function nullableDetailByName(name: string): Promise<Customer | null> {
|
async function update(page: Customer): Promise<void> {
|
||||||
return await axios.post(`${BASE_URI}/find/name`, undefined, {
|
return await axios.post(`${BASE_URI}/update`, page);
|
||||||
params: { name }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function invitedList(introducerCustomerId: string): Promise<Customer[]> {
|
|
||||||
return await axios.post(`${BASE_URI}/invited/list`, undefined, {
|
|
||||||
params: { introducerCustomerId }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function create(req: Customer): Promise<Customer> {
|
|
||||||
const id = await axios.post(`${BASE_URI}/create`, req) as unknown as string;
|
|
||||||
return {
|
|
||||||
...req,
|
|
||||||
id
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function quickRegister(req: QuickRegisterReq): Promise<EventRecord> {
|
|
||||||
const customer = await nullableDetailByCode(req.customerCode);
|
|
||||||
if (!customer?.id) {
|
|
||||||
throw new Error("客户不存在,无法登记");
|
|
||||||
}
|
|
||||||
const record: EventRecord = {
|
|
||||||
customerId: customer.id,
|
|
||||||
eventId: req.eventId,
|
|
||||||
remark: req.remark,
|
|
||||||
attachmentIdList: req.attachmentIdList
|
|
||||||
};
|
|
||||||
await axios.post("/gao/event/record/create", {
|
|
||||||
...record,
|
|
||||||
eventId: req.eventId
|
|
||||||
});
|
|
||||||
return record;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function update(req: Customer): Promise<void> {
|
|
||||||
return await axios.post(`${BASE_URI}/update`, req);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function remove(id: string): Promise<void> {
|
async function remove(id: string): Promise<void> {
|
||||||
@@ -93,15 +27,41 @@ async function remove(id: string): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateInvite(req: CustomerInviteUpdateReq): Promise<void> {
|
async function findById(id: string): Promise<Customer | undefined> {
|
||||||
if (!req.introducerCustomerId) {
|
return await axios.post(`${BASE_URI}/find/id`, null, {
|
||||||
await deleteInvite(req.customerId);
|
params: { id }
|
||||||
return;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function findByCode(code: string): Promise<Customer | undefined> {
|
||||||
|
return await axios.post(`${BASE_URI}/find/code`, null, {
|
||||||
|
params: { code }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findByName(name: string): Promise<Customer | undefined> {
|
||||||
|
return await axios.post(`${BASE_URI}/find/name`, null, {
|
||||||
|
params: { name }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportXSSF(page: Page<Customer>): Promise<Blob> {
|
||||||
|
return await axios.post(`${BASE_URI}/export`, page, {
|
||||||
|
responseType: "blob"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listInvited(introducerCustomerId: string): Promise<Customer[]> {
|
||||||
|
return await axios.post(`${BASE_URI}/invited/list`, undefined, {
|
||||||
|
params: { introducerCustomerId }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveInvite(introducerCustomerId: string, customerId: string): Promise<void> {
|
||||||
return await axios.post(`${BASE_URI}/invite/save`, undefined, {
|
return await axios.post(`${BASE_URI}/invite/save`, undefined, {
|
||||||
params: {
|
params: {
|
||||||
customerId: req.customerId,
|
introducerCustomerId,
|
||||||
introducerCustomerId: req.introducerCustomerId
|
customerId
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -120,29 +80,18 @@ async function genderStat(): Promise<CustomerGenderStatView[]> {
|
|||||||
return await axios.post(`${BASE_URI}/state/gender`);
|
return await axios.post(`${BASE_URI}/state/gender`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCustomerListExample(req?: CustomerSearchReq): Partial<Customer> {
|
|
||||||
if (!req?.introducerCustomerId) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
introducerCustomerId: req.introducerCustomerId
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
list,
|
list,
|
||||||
search,
|
|
||||||
searchPage,
|
|
||||||
exportExcel,
|
|
||||||
detail,
|
detail,
|
||||||
nullableDetailByCode,
|
|
||||||
nullableDetailByName,
|
|
||||||
invitedList,
|
|
||||||
create,
|
create,
|
||||||
quickRegister,
|
|
||||||
update,
|
update,
|
||||||
remove,
|
remove,
|
||||||
updateInvite,
|
findById,
|
||||||
|
findByCode,
|
||||||
|
findByName,
|
||||||
|
exportXSSF,
|
||||||
|
listInvited,
|
||||||
|
saveInvite,
|
||||||
deleteInvite,
|
deleteInvite,
|
||||||
dailyStat,
|
dailyStat,
|
||||||
genderStat
|
genderStat
|
||||||
|
|||||||
+6
-7
@@ -7,7 +7,7 @@ async function list(req: Page<EventItem>): Promise<PageResult<EventItem>> {
|
|||||||
return await axios.post(`${BASE_URI}/list`, req);
|
return await axios.post(`${BASE_URI}/list`, req);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function enabledList(): Promise<EventItem[]> {
|
async function listValid(): Promise<EventItem[]> {
|
||||||
return await axios.post(`${BASE_URI}/list/valid`);
|
return await axios.post(`${BASE_URI}/list/valid`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,12 +17,12 @@ async function detail(id: string): Promise<EventItem> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function create(req: EventItem): Promise<void> {
|
async function create(event: EventItem): Promise<void> {
|
||||||
return await axios.post(`${BASE_URI}/create`, req);
|
return await axios.post(`${BASE_URI}/create`, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function update(req: EventItem): Promise<void> {
|
async function update(event: EventItem): Promise<void> {
|
||||||
return await axios.post(`${BASE_URI}/update`, req);
|
return await axios.post(`${BASE_URI}/update`, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sort(idList: string[]): Promise<void> {
|
async function sort(idList: string[]): Promise<void> {
|
||||||
@@ -37,11 +37,10 @@ async function remove(id: string): Promise<void> {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
list,
|
list,
|
||||||
enabledList,
|
listValid,
|
||||||
detail,
|
detail,
|
||||||
create,
|
create,
|
||||||
update,
|
update,
|
||||||
sort,
|
sort,
|
||||||
remove
|
remove
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+27
-117
@@ -1,92 +1,33 @@
|
|||||||
import { axios, type Page, type PageResult } from "timi-web";
|
import { axios, type PageResult } from "timi-web";
|
||||||
import type {
|
import type { CustomerEventRankView, EventRecord, EventRecordPage, EventRecordRankPage } from "@/types/EventRecord";
|
||||||
CustomerEventRankResponse,
|
import type { Customer } from "@/types/Customer.ts";
|
||||||
CustomerEventRankView,
|
|
||||||
EventRecord,
|
|
||||||
EventRecordCalendarView,
|
|
||||||
EventRecordCreateReq,
|
|
||||||
EventRecordDailyStatView,
|
|
||||||
EventRecordListItemView,
|
|
||||||
EventRecordPage,
|
|
||||||
EventRecordPageReq,
|
|
||||||
EventRecordTrendStatReq
|
|
||||||
} from "@/types/EventRecord";
|
|
||||||
|
|
||||||
const BASE_URI = "/gao/event/record";
|
const BASE_URI = "/gao/event/record";
|
||||||
|
|
||||||
async function list(req: Page<EventRecord>): Promise<PageResult<EventRecord>> {
|
async function list(page: EventRecordPage): Promise<PageResult<EventRecord>> {
|
||||||
return await axios.post(`${BASE_URI}/list`, buildRecordPage(req));
|
return await axios.post(`${BASE_URI}/list`, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function detail(id: string): Promise<EventRecord> {
|
async function detail(id: string): Promise<EventRecord> {
|
||||||
return await axios.post(`${BASE_URI}/detail`, undefined, {
|
return await axios.post(`${BASE_URI}/detail`, null, {
|
||||||
params: { id }
|
params: { id }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function rangeList(req: EventRecordPage): Promise<EventRecord[]> {
|
async function create(record: EventRecord): Promise<void> {
|
||||||
const result: PageResult<EventRecord> = await axios.post(`${BASE_URI}/list`, buildRecordPage({
|
|
||||||
index: 0,
|
|
||||||
size: 1000,
|
|
||||||
equalsExample: req
|
|
||||||
}));
|
|
||||||
return result.list;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function calendarRangeList(req: EventRecordPage): Promise<EventRecordCalendarView[]> {
|
|
||||||
const result = await rangeList(req);
|
|
||||||
return result.map(item => ({
|
|
||||||
id: item.id,
|
|
||||||
customerId: item.customerId,
|
|
||||||
customerCode: item.customer?.code || "",
|
|
||||||
customerName: item.customer?.name || "",
|
|
||||||
eventId: item.eventId,
|
|
||||||
registeredAt: item.registeredAt
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function rangePage(req: Page<EventRecordPage>): Promise<PageResult<EventRecordListItemView>> {
|
|
||||||
const result: PageResult<EventRecord> = await axios.post(`${BASE_URI}/list`, buildRecordPage(req));
|
|
||||||
return {
|
|
||||||
total: result.total,
|
|
||||||
list: result.list.map(item => ({
|
|
||||||
id: item.id,
|
|
||||||
customerId: item.customerId,
|
|
||||||
customerName: item.customer?.name || "",
|
|
||||||
customerPhotoAttachmentId: item.customer?.attachmentList?.[0]?.id,
|
|
||||||
eventId: item.eventId,
|
|
||||||
eventName: item.event?.name || "",
|
|
||||||
registeredAt: item.registeredAt
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function rangeExport(req: EventRecordPage): Promise<Blob> {
|
|
||||||
return await axios.post(`${BASE_URI}/export`, buildRecordPage({
|
|
||||||
index: 0,
|
|
||||||
size: 100000,
|
|
||||||
equalsExample: req
|
|
||||||
}), {
|
|
||||||
responseType: "blob"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function create(req: EventRecordCreateReq): Promise<EventRecord[]> {
|
|
||||||
const recordList = req.customerIdList.map(customerId => ({
|
|
||||||
customerId,
|
|
||||||
eventId: req.eventId,
|
|
||||||
remark: req.remark,
|
|
||||||
registeredAt: req.registeredAt,
|
|
||||||
attachmentIdList: req.attachmentIdList
|
|
||||||
}));
|
|
||||||
for (const record of recordList) {
|
|
||||||
await axios.post(`${BASE_URI}/create`, record);
|
await axios.post(`${BASE_URI}/create`, record);
|
||||||
}
|
|
||||||
return recordList;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function update(req: EventRecord): Promise<void> {
|
async function createBatch(recordList: EventRecord[]): Promise<void> {
|
||||||
await axios.post(`${BASE_URI}/update`, req);
|
await axios.post(`${BASE_URI}/create/batch`, recordList);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createWithCustomer(customer: Customer): Promise<void> {
|
||||||
|
await axios.post(`${BASE_URI}/create/customer`, customer);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function update(record: EventRecord): Promise<void> {
|
||||||
|
await axios.post(`${BASE_URI}/update`, record);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function remove(id: string): Promise<void> {
|
async function remove(id: string): Promise<void> {
|
||||||
@@ -95,56 +36,25 @@ async function remove(id: string): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function customerRegistrationRank(req?: Partial<EventRecordPage>): Promise<CustomerEventRankView[]> {
|
async function eventRank(page: EventRecordRankPage): Promise<CustomerEventRankView> {
|
||||||
const result: PageResult<CustomerEventRankResponse> = await axios.post(`${BASE_URI}/rank`, buildRecordPage({
|
return await axios.post(`${BASE_URI}/rank`, page);
|
||||||
index: 0,
|
|
||||||
size: 100,
|
|
||||||
equalsExample: req
|
|
||||||
}));
|
|
||||||
return result.list.map(item => ({
|
|
||||||
rank: item.rank,
|
|
||||||
customerId: item.customerId,
|
|
||||||
customerCode: item.customer?.code || "",
|
|
||||||
customerName: item.customer?.name || "",
|
|
||||||
customerPhotoAttachmentId: item.customer?.attachmentList?.[0]?.id,
|
|
||||||
totalEventDays: item.totalEventDays,
|
|
||||||
totalEventCount: item.totalEventCount,
|
|
||||||
recentContinuousEventDays: item.recentContinuousEventDays,
|
|
||||||
latestRegisteredAt: item.latestRegisteredAt
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function allCustomerEventDailyStat(req: EventRecordTrendStatReq): Promise<EventRecordDailyStatView[]> {
|
async function exportXSSF(page: EventRecordPage): Promise<Blob> {
|
||||||
void req;
|
return await axios.post(`${BASE_URI}/export`, page, {
|
||||||
return [];
|
responseType: "blob"
|
||||||
}
|
});
|
||||||
|
|
||||||
function buildRecordPage(req: Page<EventRecord | EventRecordPage> | Page<Partial<EventRecordPage>>): EventRecordPageReq {
|
|
||||||
const equalsExample = req.equalsExample || {};
|
|
||||||
const eventIdList = "eventIdList" in equalsExample ? equalsExample.eventIdList : undefined;
|
|
||||||
const singleEventId = "eventId" in equalsExample ? equalsExample.eventId : undefined;
|
|
||||||
return {
|
|
||||||
index: req.index,
|
|
||||||
size: req.size,
|
|
||||||
customerId: equalsExample.customerId,
|
|
||||||
eventIdList: eventIdList || (singleEventId ? [singleEventId] : undefined),
|
|
||||||
keyword: "keyword" in equalsExample ? equalsExample.keyword : undefined,
|
|
||||||
beginAt: "beginAt" in equalsExample ? equalsExample.beginAt : undefined,
|
|
||||||
endAt: "endAt" in equalsExample ? equalsExample.endAt : undefined
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
list,
|
list,
|
||||||
detail,
|
detail,
|
||||||
rangeList,
|
exportXSSF,
|
||||||
calendarRangeList,
|
|
||||||
rangePage,
|
|
||||||
rangeExport,
|
|
||||||
create,
|
create,
|
||||||
|
createBatch,
|
||||||
|
createWithCustomer,
|
||||||
update,
|
update,
|
||||||
remove,
|
remove,
|
||||||
customerRegistrationRank,
|
eventRank
|
||||||
allCustomerEventDailyStat
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import type { ManagedUser } from "@/types/ManagedUser";
|
|||||||
|
|
||||||
const BASE_URI = "/gao/user";
|
const BASE_URI = "/gao/user";
|
||||||
|
|
||||||
async function list(req: Page<ManagedUser>): Promise<PageResult<ManagedUser>> {
|
async function list(page: Page<ManagedUser>): Promise<PageResult<ManagedUser>> {
|
||||||
return await axios.post(`${BASE_URI}/list`, req);
|
return await axios.post(`${BASE_URI}/list`, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function detail(id: string): Promise<ManagedUser> {
|
async function detail(id: string): Promise<ManagedUser> {
|
||||||
@@ -13,12 +13,12 @@ async function detail(id: string): Promise<ManagedUser> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function create(req: ManagedUser): Promise<ManagedUser> {
|
async function create(user: ManagedUser): Promise<ManagedUser> {
|
||||||
return await axios.post(`${BASE_URI}/create`, req);
|
return await axios.post(`${BASE_URI}/create`, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function update(req: ManagedUser): Promise<ManagedUser> {
|
async function update(user: ManagedUser): Promise<ManagedUser> {
|
||||||
return await axios.post(`${BASE_URI}/update`, req);
|
return await axios.post(`${BASE_URI}/update`, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function remove(id: string): Promise<void> {
|
async function remove(id: string): Promise<void> {
|
||||||
@@ -39,4 +39,3 @@ export default {
|
|||||||
remove,
|
remove,
|
||||||
roleList
|
roleList
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -109,11 +109,13 @@ async function loadCustomers(): Promise<void> {
|
|||||||
searchToken = token;
|
searchToken = token;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const result = await CustomerAPI.searchPage({
|
const result = await CustomerAPI.list({
|
||||||
index: pageIndex.value,
|
index: pageIndex.value,
|
||||||
size: pageSize,
|
size: pageSize,
|
||||||
equalsExample: {
|
likesExample: {
|
||||||
keyword: keyword.value || undefined
|
code: keyword.value || undefined,
|
||||||
|
name: keyword.value || undefined,
|
||||||
|
telephone: keyword.value || undefined
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (token !== searchToken) {
|
if (token !== searchToken) {
|
||||||
@@ -253,7 +255,7 @@ onUnmounted(() => {
|
|||||||
.search-wrapper {
|
.search-wrapper {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
padding: 1rem 0 .5rem 0;
|
padding: 1rem 0 .5rem 0;
|
||||||
box-shadow: var(--td-shadow-1);
|
box-shadow: 0 4px 4px rgba(0, 0, 0, .05);
|
||||||
|
|
||||||
.search {
|
.search {
|
||||||
--td-input-vertical-padding: 8px;
|
--td-input-vertical-padding: 8px;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
:class="{ clickable: !!href }"
|
:class="{ clickable: !!href }"
|
||||||
@click="handleClick"
|
@click="handleClick"
|
||||||
>
|
>
|
||||||
|
<t-icon v-if="value" name="call" />
|
||||||
<span v-text="displayText" />
|
<span v-text="displayText" />
|
||||||
</component>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -50,12 +50,12 @@
|
|||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="客户照片">
|
<t-form-item label="客户照片">
|
||||||
<photo-field
|
<photo-field
|
||||||
v-model="supplementPhoto"
|
v-model="customerPhoto"
|
||||||
alt-text="客户照片"
|
alt-text="客户照片"
|
||||||
:loading="supplementPhotoUploading"
|
:loading="customerPhotoUploading"
|
||||||
:preview="false"
|
:preview="false"
|
||||||
@add="selectSupplementPhoto"
|
@add="customerPhotoInputRef?.click"
|
||||||
@clear="clearSupplementPhoto"
|
@clear="customerPhotoTempFileId = ''"
|
||||||
/>
|
/>
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label-width="0">
|
<t-form-item label-width="0">
|
||||||
@@ -76,18 +76,18 @@
|
|||||||
<div class="photo-part">
|
<div class="photo-part">
|
||||||
<photo-scroller
|
<photo-scroller
|
||||||
title="登记事件照片"
|
title="登记事件照片"
|
||||||
v-model="photoList"
|
v-model="recordPhotoList"
|
||||||
:loading="recordPhotoUploading"
|
:loading="recordPhotoUploading"
|
||||||
@add="selectRecordPhoto"
|
@add="recordPhotoInputRef?.click"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</t-form>
|
</t-form>
|
||||||
<input
|
<input
|
||||||
ref="supplementPhotoInputRef"
|
ref="customerPhotoInputRef"
|
||||||
class="file-input"
|
class="file-input"
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
@change="handleSupplementPhotoSelected"
|
@change="handleCustomerPhotoSelected"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
ref="recordPhotoInputRef"
|
ref="recordPhotoInputRef"
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { AttachmentAPI } from "timi-web";
|
import { AttachmentAPI, Toolkit } from "timi-web";
|
||||||
import type { TdInputProps } from "tdesign-mobile-vue";
|
import type { TdInputProps } from "tdesign-mobile-vue";
|
||||||
import { Toast } from "tdesign-mobile-vue";
|
import { Toast } from "tdesign-mobile-vue";
|
||||||
import CustomerAPI from "@/api/CustomerAPI";
|
import CustomerAPI from "@/api/CustomerAPI";
|
||||||
@@ -123,6 +123,7 @@ import { getCustomerPhotoURL } from "@/utils/customer";
|
|||||||
import { permissionCodes } from "@/utils/permission";
|
import { permissionCodes } from "@/utils/permission";
|
||||||
import { compressUploadImage, compressUploadImageList } from "@/utils/imageCompression";
|
import { compressUploadImage, compressUploadImageList } from "@/utils/imageCompression";
|
||||||
import EventSelector from "@/components/EventSelector.vue";
|
import EventSelector from "@/components/EventSelector.vue";
|
||||||
|
import EventRecordAPI from "@/api/EventRecordAPI.ts";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "QuickEntryPage"
|
name: "QuickEntryPage"
|
||||||
@@ -130,21 +131,25 @@ defineOptions({
|
|||||||
|
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const supplementPhoto = ref("");
|
|
||||||
const supplementPhotoTempFileId = ref("");
|
|
||||||
const supplementPhotoInputRef = ref<HTMLInputElement>();
|
|
||||||
const recordPhotoInputRef = ref<HTMLInputElement>();
|
|
||||||
const recordPhotoTempFileIdList = ref<string[]>([]);
|
|
||||||
const supplementPhotoUploading = ref(false);
|
|
||||||
const recordPhotoUploading = ref(false);
|
|
||||||
const eventList = ref<EventItem[]>([]);
|
const eventList = ref<EventItem[]>([]);
|
||||||
|
|
||||||
const selectedEventIdList = ref<string[]>([]);
|
const selectedEventIdList = ref<string[]>([]);
|
||||||
|
|
||||||
const note = ref("");
|
const note = ref("");
|
||||||
const photoList = ref<string[]>([]);
|
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
const canSubmit = computed(() => !!getInputCode() && !!customerName.value.trim());
|
const canSubmit = computed(() => !!inputCode.value && !!customerName.value.trim() && !customerPhotoUploading.value && !recordPhotoUploading.value);
|
||||||
|
|
||||||
let latestDecodedText = "";
|
let latestDecodedText = "";
|
||||||
|
|
||||||
|
const inputCode = computed(() => currentCode.value || scanCode.value || "");
|
||||||
|
|
||||||
|
type QuickPhoto = {
|
||||||
|
tempFileId: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
// ---------- 扫码 ----------
|
// ---------- 扫码 ----------
|
||||||
const scanCode = ref("");
|
const scanCode = ref("");
|
||||||
const currentCode = ref("");
|
const currentCode = ref("");
|
||||||
@@ -152,7 +157,6 @@ const scanResult = ref("");
|
|||||||
const scanStatus = ref<TdInputProps["status"]>("warning");
|
const scanStatus = ref<TdInputProps["status"]>("warning");
|
||||||
const scannerVisible = ref(false);
|
const scannerVisible = ref(false);
|
||||||
const scannerTips = ref("");
|
const scannerTips = ref("");
|
||||||
const checkedCode = ref("");
|
|
||||||
const boundCodeCustomer = ref<Customer>();
|
const boundCodeCustomer = ref<Customer>();
|
||||||
|
|
||||||
async function toggleScanner(): Promise<void> {
|
async function toggleScanner(): Promise<void> {
|
||||||
@@ -195,7 +199,13 @@ const customerTips = ref("");
|
|||||||
const customerStatus = ref<TdInputProps["status"]>("warning");
|
const customerStatus = ref<TdInputProps["status"]>("warning");
|
||||||
const selectorVisible = ref(false);
|
const selectorVisible = ref(false);
|
||||||
const selectedCustomerId = ref("");
|
const selectedCustomerId = ref("");
|
||||||
|
const checkedCode = ref("");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检验新客户编码
|
||||||
|
*
|
||||||
|
* @param code
|
||||||
|
*/
|
||||||
async function ensureNewCustomerCode(code: string): Promise<boolean> {
|
async function ensureNewCustomerCode(code: string): Promise<boolean> {
|
||||||
await checkCustomerCode(code);
|
await checkCustomerCode(code);
|
||||||
applyBoundCustomerIfNeeded();
|
applyBoundCustomerIfNeeded();
|
||||||
@@ -203,18 +213,34 @@ async function ensureNewCustomerCode(code: string): Promise<boolean> {
|
|||||||
return !isCodeBoundToOtherCustomer();
|
return !isCodeBoundToOtherCustomer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检验编码
|
||||||
|
async function ensureCodeChecked(): Promise<void> {
|
||||||
|
const code = inputCode.value;
|
||||||
|
if (!code || checkedCode.value === code) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 检验编码客户
|
||||||
|
await checkCustomerCode(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择客户
|
||||||
|
*
|
||||||
|
* @param id 客户 ID
|
||||||
|
*/
|
||||||
async function handleCustomerChange(id: string): Promise<void> {
|
async function handleCustomerChange(id: string): Promise<void> {
|
||||||
const customer = await CustomerAPI.detail(id);
|
const customer = await CustomerAPI.detail(id);
|
||||||
currentCustomer.value = customer;
|
currentCustomer.value = customer;
|
||||||
selectedCustomerId.value = customer.id || "";
|
selectedCustomerId.value = customer.id || "";
|
||||||
customerName.value = customer.name || "";
|
customerName.value = customer.name || "";
|
||||||
syncSupplementPhotoFromCustomer(customer);
|
syncCustomerPhoto(customer);
|
||||||
customerTips.value = "";
|
customerTips.value = "";
|
||||||
if (!getInputCode() && customer.code) {
|
if (!inputCode.value && customer.code) {
|
||||||
|
// 补充编码
|
||||||
fillCodeFromCustomer(customer);
|
fillCodeFromCustomer(customer);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (getInputCode()) {
|
if (inputCode.value) {
|
||||||
await ensureCodeChecked();
|
await ensureCodeChecked();
|
||||||
}
|
}
|
||||||
refreshInputTips();
|
refreshInputTips();
|
||||||
@@ -229,7 +255,7 @@ async function applyCustomerName(): Promise<void> {
|
|||||||
refreshInputTips();
|
refreshInputTips();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const matched = await findCustomerByName(normalizedName);
|
const matched = await CustomerAPI.findByName(normalizedName) || undefined;
|
||||||
if (!matched) {
|
if (!matched) {
|
||||||
currentCustomer.value = undefined;
|
currentCustomer.value = undefined;
|
||||||
selectedCustomerId.value = "";
|
selectedCustomerId.value = "";
|
||||||
@@ -240,30 +266,30 @@ async function applyCustomerName(): Promise<void> {
|
|||||||
currentCustomer.value = matched;
|
currentCustomer.value = matched;
|
||||||
selectedCustomerId.value = matched.id || "";
|
selectedCustomerId.value = matched.id || "";
|
||||||
customerName.value = matched.name || normalizedName;
|
customerName.value = matched.name || normalizedName;
|
||||||
syncSupplementPhotoFromCustomer(matched);
|
syncCustomerPhoto(matched);
|
||||||
customerTips.value = "已匹配客户";
|
customerTips.value = "已匹配客户";
|
||||||
if (!getInputCode() && matched.code) {
|
if (!inputCode.value && matched.code) {
|
||||||
fillCodeFromCustomer(matched);
|
fillCodeFromCustomer(matched);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (getInputCode()) {
|
if (inputCode.value) {
|
||||||
await ensureCodeChecked();
|
await ensureCodeChecked();
|
||||||
}
|
}
|
||||||
refreshInputTips();
|
refreshInputTips();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findCustomerByName(name: string): Promise<Customer | undefined> {
|
/**
|
||||||
const list = await CustomerAPI.search({ keyword: name });
|
* 补充客户编码
|
||||||
return list.find(item => item.name?.trim() === name);
|
*
|
||||||
}
|
* @param customer 客户
|
||||||
|
*/
|
||||||
function fillCodeFromCustomer(customer: Customer): void {
|
function fillCodeFromCustomer(customer: Customer): void {
|
||||||
const code = customer.code || "";
|
const code = customer.code || "";
|
||||||
if (!code) {
|
if (!code) {
|
||||||
|
// 未绑定编码
|
||||||
refreshInputTips();
|
refreshInputTips();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
scanCode.value = code;
|
scanCode.value = code;
|
||||||
currentCode.value = code;
|
currentCode.value = code;
|
||||||
checkedCode.value = code;
|
checkedCode.value = code;
|
||||||
@@ -271,15 +297,11 @@ function fillCodeFromCustomer(customer: Customer): void {
|
|||||||
refreshInputTips();
|
refreshInputTips();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureCodeChecked(): Promise<void> {
|
/**
|
||||||
const code = getInputCode();
|
* 检验编码
|
||||||
if (!code || checkedCode.value === code) {
|
*
|
||||||
return;
|
* @param code 编码
|
||||||
}
|
*/
|
||||||
|
|
||||||
await checkCustomerCode(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkCustomerCode(code: string): Promise<void> {
|
async function checkCustomerCode(code: string): Promise<void> {
|
||||||
const normalizedCode = code.trim();
|
const normalizedCode = code.trim();
|
||||||
if (!normalizedCode) {
|
if (!normalizedCode) {
|
||||||
@@ -288,9 +310,8 @@ async function checkCustomerCode(code: string): Promise<void> {
|
|||||||
refreshInputTips();
|
refreshInputTips();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
checkedCode.value = normalizedCode;
|
checkedCode.value = normalizedCode;
|
||||||
boundCodeCustomer.value = await CustomerAPI.nullableDetailByCode(normalizedCode) || undefined;
|
boundCodeCustomer.value = await CustomerAPI.findByCode(normalizedCode) || undefined;
|
||||||
scannerVisible.value = false;
|
scannerVisible.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,41 +324,36 @@ function applyBoundCustomerIfNeeded(): void {
|
|||||||
currentCustomer.value = boundCustomer;
|
currentCustomer.value = boundCustomer;
|
||||||
selectedCustomerId.value = boundCustomer.id;
|
selectedCustomerId.value = boundCustomer.id;
|
||||||
customerName.value = boundCustomer.name || "";
|
customerName.value = boundCustomer.name || "";
|
||||||
syncSupplementPhotoFromCustomer(boundCustomer);
|
syncCustomerPhoto(boundCustomer);
|
||||||
customerTips.value = "";
|
customerTips.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 刷新提示
|
||||||
function refreshInputTips(): void {
|
function refreshInputTips(): void {
|
||||||
scanStatus.value = "warning";
|
scanStatus.value = "warning";
|
||||||
customerStatus.value = "warning";
|
customerStatus.value = "warning";
|
||||||
|
const code = inputCode.value;
|
||||||
const code = getInputCode();
|
|
||||||
if (!code) {
|
if (!code) {
|
||||||
scanResult.value = "";
|
scanResult.value = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (checkedCode.value !== code) {
|
if (checkedCode.value !== code) {
|
||||||
scanResult.value = "";
|
scanResult.value = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCodeBoundToOtherCustomer()) {
|
if (isCodeBoundToOtherCustomer()) {
|
||||||
scanResult.value = `编码已绑定客户 ${getCustomerName(boundCodeCustomer.value)},请使用新的编码`;
|
scanResult.value = `编码已绑定客户 ${getCustomerName(boundCodeCustomer.value)},请使用新的编码`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (boundCodeCustomer.value?.id) {
|
if (boundCodeCustomer.value?.id) {
|
||||||
scanResult.value = "";
|
scanResult.value = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentCustomer.value?.code && currentCustomer.value.code !== code) {
|
if (currentCustomer.value?.code && currentCustomer.value.code !== code) {
|
||||||
scanResult.value = `客户已有编码 ${currentCustomer.value.code},将会替换新的编码`;
|
scanResult.value = `客户已有编码 ${currentCustomer.value.code},将会替换新的编码`;
|
||||||
scanStatus.value = "error";
|
scanStatus.value = "error";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
scanResult.value = currentCustomer.value?.id ? "提交将会使用此编码" : "编码可用";
|
scanResult.value = currentCustomer.value?.id ? "提交将会使用此编码" : "编码可用";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,58 +362,59 @@ function isCodeBoundToOtherCustomer(): boolean {
|
|||||||
if (!boundCustomerId) {
|
if (!boundCustomerId) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return currentCustomer.value?.id !== boundCustomerId;
|
return currentCustomer.value?.id !== boundCustomerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInputCode(): string {
|
|
||||||
return (currentCode.value || scanCode.value).trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCustomerName(customer?: Customer): string {
|
function getCustomerName(customer?: Customer): string {
|
||||||
return customer?.name || customer?.code || "未命名客户";
|
return customer?.name || customer?.code || "未命名客户";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 照片 ----------
|
// ---------- 客户照片 ----------
|
||||||
|
const customerPhoto = ref("");
|
||||||
|
const customerPhotoTempFileId = ref("");
|
||||||
|
const customerPhotoInputRef = ref<HTMLInputElement>();
|
||||||
|
const customerPhotoUploading = ref(false);
|
||||||
|
|
||||||
function selectSupplementPhoto(): void {
|
function syncCustomerPhoto(customer?: Customer): void {
|
||||||
supplementPhotoInputRef.value?.click();
|
customerPhotoTempFileId.value = "";
|
||||||
|
customerPhoto.value = getCustomerPhotoURL(customer);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearSupplementPhoto(): void {
|
async function handleCustomerPhotoSelected(event: Event): Promise<void> {
|
||||||
supplementPhotoTempFileId.value = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncSupplementPhotoFromCustomer(customer?: Customer): void {
|
|
||||||
supplementPhotoTempFileId.value = "";
|
|
||||||
supplementPhoto.value = getCustomerPhotoURL(customer);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSupplementPhotoSelected(event: Event): Promise<void> {
|
|
||||||
const input = event.target as HTMLInputElement;
|
const input = event.target as HTMLInputElement;
|
||||||
const file = input.files?.[0];
|
const file = input.files?.[0];
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
supplementPhotoUploading.value = true;
|
customerPhotoUploading.value = true;
|
||||||
try {
|
try {
|
||||||
const compressedFile = await compressUploadImage(file);
|
const compressedFile = await compressUploadImage(file);
|
||||||
const [tempFile] = await AttachmentAPI.uploadTemp(compressedFile);
|
const [tempFile] = await AttachmentAPI.upload(compressedFile);
|
||||||
if (!tempFile?.id) {
|
if (!tempFile?.id) {
|
||||||
showToast("客户照片上传失败", "warning");
|
showToast("客户照片上传失败", "warning");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
supplementPhotoTempFileId.value = tempFile.id;
|
customerPhotoTempFileId.value = tempFile.id;
|
||||||
supplementPhoto.value = AttachmentAPI.getTempReadURL(tempFile.id);
|
customerPhoto.value = AttachmentAPI.getTempReadURL(tempFile.id);
|
||||||
} finally {
|
} finally {
|
||||||
supplementPhotoUploading.value = false;
|
customerPhotoUploading.value = false;
|
||||||
input.value = "";
|
input.value = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectRecordPhoto(): void {
|
// ---------- 记录照片 ----------
|
||||||
recordPhotoInputRef.value?.click();
|
const recordPhotoInputRef = ref<HTMLInputElement>();
|
||||||
}
|
const recordPhotoUploading = ref(false);
|
||||||
|
const recordPhotoItemList = ref<QuickPhoto[]>([]);
|
||||||
|
const recordPhotoList = computed<string[]>({
|
||||||
|
get() {
|
||||||
|
return recordPhotoItemList.value.map(item => item.url);
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
const urlSet = new Set(value);
|
||||||
|
recordPhotoItemList.value = recordPhotoItemList.value.filter(item => urlSet.has(item.url));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
async function handleRecordPhotoSelected(event: Event): Promise<void> {
|
async function handleRecordPhotoSelected(event: Event): Promise<void> {
|
||||||
const input = event.target as HTMLInputElement;
|
const input = event.target as HTMLInputElement;
|
||||||
@@ -408,13 +425,16 @@ async function handleRecordPhotoSelected(event: Event): Promise<void> {
|
|||||||
recordPhotoUploading.value = true;
|
recordPhotoUploading.value = true;
|
||||||
try {
|
try {
|
||||||
const compressedFileList = await compressUploadImageList(fileList);
|
const compressedFileList = await compressUploadImageList(fileList);
|
||||||
const tempFileList = await AttachmentAPI.uploadTemp(compressedFileList);
|
const tempFileList = await AttachmentAPI.upload(compressedFileList);
|
||||||
for (const item of tempFileList) {
|
for (let i = 0; i < tempFileList.length; i++) {
|
||||||
if (!item.id) {
|
const tempFile = tempFileList[i];
|
||||||
|
if (!tempFile.id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
recordPhotoTempFileIdList.value.push(item.id);
|
recordPhotoItemList.value.push({
|
||||||
photoList.value.push(AttachmentAPI.getTempReadURL(item.id));
|
tempFileId: tempFile.id,
|
||||||
|
url: AttachmentAPI.getTempReadURL(tempFile.id)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
recordPhotoUploading.value = false;
|
recordPhotoUploading.value = false;
|
||||||
@@ -429,28 +449,37 @@ async function saveRecord(): Promise<void> {
|
|||||||
if (!customerCode) {
|
if (!customerCode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
try {
|
try {
|
||||||
|
await ensureCustomerMatched();
|
||||||
if (!await ensureNewCustomerCode(customerCode)) {
|
if (!await ensureNewCustomerCode(customerCode)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await applyCustomerName();
|
const customer: Customer = {
|
||||||
await saveCustomerProfile(customerCode);
|
id: currentCustomer.value?.id,
|
||||||
if (selectedEventIdList.value.length) {
|
code: customerCode,
|
||||||
for (const eventId of selectedEventIdList.value) {
|
name: customerName.value.trim(),
|
||||||
await CustomerAPI.quickRegister({
|
eventRecordList: selectedEventIdList.value.map(eventId => ({
|
||||||
customerCode,
|
|
||||||
eventId,
|
eventId,
|
||||||
remark: note.value || "前台快速登记",
|
remark: note.value || "快速补录登记"
|
||||||
attachmentIdList: recordPhotoTempFileIdList.value
|
}))
|
||||||
});
|
};
|
||||||
|
// 客户照片
|
||||||
|
if (customerPhotoTempFileId.value) {
|
||||||
|
customer.attachmentList = [{
|
||||||
|
tempFileId: customerPhotoTempFileId.value,
|
||||||
|
attachType: "PHOTO"
|
||||||
|
}];
|
||||||
}
|
}
|
||||||
|
// 记录照片
|
||||||
|
if (Toolkit.isNotEmpty(customer.eventRecordList)) {
|
||||||
|
customer.eventRecordList![0].attachmentList = recordPhotoItemList.value.map(item => ({
|
||||||
|
tempFileId: item.tempFileId,
|
||||||
|
attachType: "PHOTO"
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
note.value = "";
|
await EventRecordAPI.createWithCustomer(customer);
|
||||||
customerName.value = "";
|
resetFormAfterSubmit();
|
||||||
photoList.value = [];
|
|
||||||
recordPhotoTempFileIdList.value = [];
|
|
||||||
await settingStore.playRegisterSuccessSound();
|
await settingStore.playRegisterSuccessSound();
|
||||||
showToast("登记已提交");
|
showToast("登记已提交");
|
||||||
await router.replace("/customers");
|
await router.replace("/customers");
|
||||||
@@ -459,24 +488,26 @@ async function saveRecord(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveCustomerProfile(customerCode: string): Promise<void> {
|
async function ensureCustomerMatched(): Promise<void> {
|
||||||
const normalizedName = customerName.value.trim();
|
|
||||||
const attachmentIdList = supplementPhotoTempFileId.value ? [supplementPhotoTempFileId.value] : undefined;
|
|
||||||
if (currentCustomer.value?.id) {
|
if (currentCustomer.value?.id) {
|
||||||
await CustomerAPI.update({
|
|
||||||
...currentCustomer.value,
|
|
||||||
code: customerCode,
|
|
||||||
name: normalizedName,
|
|
||||||
attachmentIdList
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentCustomer.value = await CustomerAPI.create({
|
await applyCustomerName();
|
||||||
code: customerCode,
|
}
|
||||||
name: normalizedName,
|
|
||||||
attachmentIdList
|
function resetFormAfterSubmit(): void {
|
||||||
});
|
note.value = "";
|
||||||
|
scanCode.value = "";
|
||||||
|
currentCode.value = "";
|
||||||
|
customerName.value = "";
|
||||||
|
selectedCustomerId.value = "";
|
||||||
|
currentCustomer.value = undefined;
|
||||||
|
boundCodeCustomer.value = undefined;
|
||||||
|
customerPhoto.value = "";
|
||||||
|
customerPhotoTempFileId.value = "";
|
||||||
|
recordPhotoItemList.value = [];
|
||||||
|
refreshInputTips();
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateForm(): string {
|
function validateForm(): string {
|
||||||
@@ -489,7 +520,6 @@ function validateForm(): string {
|
|||||||
showToast("请填写客户姓名", "warning");
|
showToast("请填写客户姓名", "warning");
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
return customerCode;
|
return customerCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -504,7 +534,7 @@ function showToast(message: string, theme: "success" | "warning" = "success"): v
|
|||||||
// ---------- 初始化 ----------
|
// ---------- 初始化 ----------
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
eventList.value = await EventAPI.enabledList();
|
eventList.value = await EventAPI.listValid();
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(scanCode, (value) => {
|
watch(scanCode, (value) => {
|
||||||
|
|||||||
@@ -25,18 +25,18 @@
|
|||||||
<t-cell
|
<t-cell
|
||||||
v-for="item in recentRecordList"
|
v-for="item in recentRecordList"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:title="getRecordEventName(item)"
|
:title="item.event?.name || '未知事件'"
|
||||||
:description="formatTime(item.registeredAt)"
|
:description="Time.toPassedDateTime(item.registeredAt)"
|
||||||
:note="item.remark || '无备注'"
|
:note="item.remark || '无备注'"
|
||||||
arrow
|
arrow
|
||||||
hover
|
hover
|
||||||
@click="goRecordDetail(item.id || '')"
|
@click="toRecordDetail(item.id || '')"
|
||||||
>
|
>
|
||||||
<template #leftIcon>
|
<template #leftIcon>
|
||||||
<t-image
|
<t-image
|
||||||
v-if="getRecordPhotoURL(item)"
|
v-if="getEventRecordPhotoURL(item)"
|
||||||
:src="getRecordPhotoURL(item)"
|
:src="getEventRecordPhotoURL(item)"
|
||||||
:alt="getRecordEventName(item)"
|
:alt="item.event?.name || '未知事件'"
|
||||||
fit="cover"
|
fit="cover"
|
||||||
class="record-photo"
|
class="record-photo"
|
||||||
/>
|
/>
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
theme="primary"
|
theme="primary"
|
||||||
:disabled="!canSubmitRegister"
|
:disabled="!canSubmitRegister"
|
||||||
:loading="submitting"
|
:loading="submitting"
|
||||||
@click="submitRegister"
|
@click="doSubmit"
|
||||||
>登记</t-button>
|
>登记</t-button>
|
||||||
</div>
|
</div>
|
||||||
<t-popup v-model="eventSelectorVisible" placement="bottom">
|
<t-popup v-model="eventSelectorVisible" placement="bottom">
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
:key="item.id"
|
:key="item.id"
|
||||||
:title="item.name"
|
:title="item.name"
|
||||||
:note="item.id === selectedEventId ? '当前使用' : ''"
|
:note="item.id === selectedEventId ? '当前使用' : ''"
|
||||||
@click="selectEvent(item.id || '')"
|
@click="selectEvent(item.id)"
|
||||||
/>
|
/>
|
||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
<div v-else class="empty-block">暂无可用登记事件</div>
|
<div v-else class="empty-block">暂无可用登记事件</div>
|
||||||
@@ -81,72 +81,116 @@ import type { EventItem } from "@/types/EventItem";
|
|||||||
import type { EventRecord } from "@/types/EventRecord";
|
import type { EventRecord } from "@/types/EventRecord";
|
||||||
import { permissionCodes } from "@/utils/permission";
|
import { permissionCodes } from "@/utils/permission";
|
||||||
import { getEventRecordPhotoURL } from "@/utils/eventRecord";
|
import { getEventRecordPhotoURL } from "@/utils/eventRecord";
|
||||||
import { Time } from "timi-web";
|
import { Storage, Time } from "timi-web";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "QuickSignInPage"
|
name: "QuickSignInPage"
|
||||||
});
|
});
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const EventItemStorageKey = "quick-scan-registration-event-id";
|
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
const scanCode = ref("");
|
|
||||||
const scanStatus = ref("正在启动扫码预览。");
|
|
||||||
const scannerSessionKey = ref(0);
|
|
||||||
const matchedCustomer = ref<Customer>();
|
|
||||||
const recentRecordList = ref<EventRecord[]>([]);
|
|
||||||
const eventList = ref<EventItem[]>([]);
|
|
||||||
const selectedEventId = ref("");
|
|
||||||
const eventSelectorVisible = ref(false);
|
|
||||||
const submitting = ref(false);
|
|
||||||
let latestDecodedText = "";
|
|
||||||
|
|
||||||
|
// ---------- 登记事件 ----------
|
||||||
|
|
||||||
|
const EventItemStorageKey = "quick-scan-event-id";
|
||||||
|
const eventSelectorVisible = ref(false);
|
||||||
|
const selectedEventId = ref("");
|
||||||
|
const eventList = ref<EventItem[]>([]);
|
||||||
const selectedEventName = computed(() => eventList.value.find(item => item.id === selectedEventId.value)?.name || "请选择登记事件");
|
const selectedEventName = computed(() => eventList.value.find(item => item.id === selectedEventId.value)?.name || "请选择登记事件");
|
||||||
const canSubmitRegister = computed(() => !!matchedCustomer.value && !!selectedEventId.value);
|
|
||||||
|
async function loadActions(): Promise<void> {
|
||||||
|
eventList.value = await EventAPI.listValid();
|
||||||
|
const storedEventId = Storage.getString(EventItemStorageKey);
|
||||||
|
const storedAction = eventList.value.find(item => item.id === storedEventId);
|
||||||
|
selectedEventId.value = storedAction?.id || eventList.value[0]?.id || "";
|
||||||
|
if (selectedEventId.value) {
|
||||||
|
Storage.setString(EventItemStorageKey, selectedEventId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectEvent(eventId?: string): void {
|
||||||
|
if (!eventId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedEventId.value = eventId;
|
||||||
|
Storage.setString(EventItemStorageKey, selectedEventId);
|
||||||
|
eventSelectorVisible.value = false;
|
||||||
|
Toast.success("登记事件已切换");
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadActions);
|
||||||
|
|
||||||
// ---------- 扫码 ----------
|
// ---------- 扫码 ----------
|
||||||
|
|
||||||
|
const scanCode = ref("");
|
||||||
|
const scanStatus = ref("正在启动扫码预览");
|
||||||
|
const scannerSessionKey = ref(0);
|
||||||
|
const matchedCustomer = ref<Customer>();
|
||||||
|
let latestDecodedText = "";
|
||||||
|
|
||||||
async function handleDecodedText(rawText: string): Promise<void> {
|
async function handleDecodedText(rawText: string): Promise<void> {
|
||||||
const normalizedText = rawText.trim();
|
const normalizedText = rawText.trim();
|
||||||
if (!normalizedText || normalizedText === latestDecodedText) {
|
if (!normalizedText || normalizedText === latestDecodedText) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
latestDecodedText = normalizedText;
|
latestDecodedText = normalizedText;
|
||||||
scanCode.value = normalizedText;
|
scanCode.value = normalizedText;
|
||||||
await settingStore.playScanSuccessSound();
|
await settingStore.playScanSuccessSound();
|
||||||
await loadCustomer(normalizedText);
|
await loadCustomer(normalizedText);
|
||||||
if (matchedCustomer.value) {
|
if (matchedCustomer.value) {
|
||||||
scanStatus.value = `已识别 ${normalizedText},可直接登记。`;
|
scanStatus.value = `已识别 ${normalizedText},可直接登记`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
scanStatus.value = `已识别 ${normalizedText},但未匹配客户档案`;
|
||||||
scanStatus.value = `已识别 ${normalizedText},但未匹配客户档案。`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleScanStatusChange(value: string): void {
|
function handleScanStatusChange(value: string): void {
|
||||||
scanStatus.value = value;
|
scanStatus.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 提交 ----------
|
// ---------- 读取客户 ----------
|
||||||
|
|
||||||
async function submitRegister(): Promise<void> {
|
const recentRecordList = ref<EventRecord[]>([]);
|
||||||
if (!validateForm()) {
|
|
||||||
|
async function loadCustomer(code: string): Promise<void> {
|
||||||
|
const customer = await CustomerAPI.findByCode(code);
|
||||||
|
if (!customer) {
|
||||||
|
matchedCustomer.value = undefined;
|
||||||
|
recentRecordList.value = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
matchedCustomer.value = customer;
|
||||||
|
recentRecordList.value = (await EventRecordAPI.list({
|
||||||
|
index: 0,
|
||||||
|
size: 5,
|
||||||
|
customerId: matchedCustomer.value.id!
|
||||||
|
})).list;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toRecordDetail(id: string): Promise<void> {
|
||||||
|
if (!id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await router.push(`/management/registration-event-records/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 表单 ----------
|
||||||
|
|
||||||
|
const submitting = ref(false);
|
||||||
|
const canSubmitRegister = computed(() => !!matchedCustomer.value && !!selectedEventId.value);
|
||||||
|
|
||||||
|
async function doSubmit(): Promise<void> {
|
||||||
const customer = matchedCustomer.value;
|
const customer = matchedCustomer.value;
|
||||||
if (!customer) {
|
if (!customer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
try {
|
try {
|
||||||
await CustomerAPI.quickRegister({
|
await EventRecordAPI.create({
|
||||||
customerCode: customer.code || scanCode.value,
|
customerId: customer.id,
|
||||||
eventId: selectedEventId.value,
|
eventId: selectedEventId.value,
|
||||||
remark: "客户快速扫码登记"
|
remark: "客户快速扫码登记"
|
||||||
});
|
})
|
||||||
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;
|
||||||
@@ -158,127 +202,6 @@ async function submitRegister(): Promise<void> {
|
|||||||
submitting.value = false;
|
submitting.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateForm(): boolean {
|
|
||||||
if (!selectedEventId.value) {
|
|
||||||
showToast("请先选择登记事件", "warning");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!matchedCustomer.value) {
|
|
||||||
scanStatus.value = "当前编码未匹配客户,无法登记。";
|
|
||||||
showToast("请先扫码匹配客户", "warning");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadCustomer(code: string): Promise<void> {
|
|
||||||
const customer = await CustomerAPI.nullableDetailByCode(code);
|
|
||||||
if (!customer) {
|
|
||||||
matchedCustomer.value = undefined;
|
|
||||||
recentRecordList.value = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
matchedCustomer.value = customer;
|
|
||||||
await loadRecentRecordList();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadRecentRecordList(): Promise<void> {
|
|
||||||
const customerId = matchedCustomer.value?.id;
|
|
||||||
if (!customerId) {
|
|
||||||
recentRecordList.value = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
recentRecordList.value = (await EventRecordAPI.rangeList({
|
|
||||||
customerId,
|
|
||||||
beginAt: 0,
|
|
||||||
endAt: Time.toTimestamp(new Date())
|
|
||||||
}))
|
|
||||||
.sort((left, right) => (right.registeredAt || 0) - (left.registeredAt || 0))
|
|
||||||
.slice(0, 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function goRecordDetail(id: string): Promise<void> {
|
|
||||||
if (!id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await router.push(`/management/registration-event-records/${id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRecordEventName(record: EventRecord): string {
|
|
||||||
return record.event?.name || "未知事件";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRecordPhotoURL(record: EventRecord): string {
|
|
||||||
return getEventRecordPhotoURL(record);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTime(value?: number): string {
|
|
||||||
if (!value) {
|
|
||||||
return "-";
|
|
||||||
}
|
|
||||||
|
|
||||||
const date = Time.toDateObject(value);
|
|
||||||
if (!date) {
|
|
||||||
return "-";
|
|
||||||
}
|
|
||||||
return Time.formatDateTime(date);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- 登记事件 ----------
|
|
||||||
|
|
||||||
async function loadActions(): Promise<void> {
|
|
||||||
eventList.value = await EventAPI.enabledList();
|
|
||||||
const storedEventId = loadStoredEventId();
|
|
||||||
const storedAction = eventList.value.find(item => item.id === storedEventId);
|
|
||||||
selectedEventId.value = storedAction?.id || eventList.value[0]?.id || "";
|
|
||||||
saveStoredEventId(selectedEventId.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectEvent(eventId: string | number | Array<string | number>): void {
|
|
||||||
if (Array.isArray(eventId)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextEventId = String(eventId);
|
|
||||||
if (!nextEventId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedEventId.value = nextEventId;
|
|
||||||
saveStoredEventId(nextEventId);
|
|
||||||
eventSelectorVisible.value = false;
|
|
||||||
showToast("登记事件已切换");
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadStoredEventId(): string {
|
|
||||||
if (typeof window === "undefined") {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
return window.localStorage.getItem(EventItemStorageKey) || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveStoredEventId(eventId: string): void {
|
|
||||||
if (typeof window === "undefined" || !eventId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.localStorage.setItem(EventItemStorageKey, eventId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showToast(message: string, theme: "success" | "warning" = "success"): void {
|
|
||||||
Toast({
|
|
||||||
theme,
|
|
||||||
direction: "row",
|
|
||||||
message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(loadActions);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
v-if="selectedCustomer"
|
v-if="selectedCustomer"
|
||||||
theme="primary"
|
theme="primary"
|
||||||
@click="clearCustomerSelection"
|
@click="clearCustomerSelection"
|
||||||
>排行</t-button>
|
>清除</t-button>
|
||||||
<t-button class="customer" block theme="primary" variant="outline" @click="selectorVisible = true">
|
<t-button class="customer" block theme="primary" variant="outline" @click="selectorVisible = true">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<t-icon name="user-search" />
|
<t-icon name="user-search" />
|
||||||
@@ -30,24 +30,6 @@
|
|||||||
@status-change="handleScanStatusChange"
|
@status-change="handleScanStatusChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!selectedCustomer" class="line filter">
|
|
||||||
<t-dropdown-menu class="menu">
|
|
||||||
<t-dropdown-item
|
|
||||||
v-model="rankDateFilterType"
|
|
||||||
:label="rankDateFilterLabel"
|
|
||||||
:options="rankDateOptions"
|
|
||||||
@change="selectRankDateFilter"
|
|
||||||
/>
|
|
||||||
<t-dropdown-item
|
|
||||||
v-model="rankEventIdList"
|
|
||||||
:label="rankEventFilterLabel"
|
|
||||||
:options="rankEventOptions"
|
|
||||||
options-columns="2"
|
|
||||||
multiple
|
|
||||||
@change="reloadCustomerRegistrationRank"
|
|
||||||
/>
|
|
||||||
</t-dropdown-menu>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<section v-if="selectedCustomer" class="summary">
|
<section v-if="selectedCustomer" class="summary">
|
||||||
@@ -74,110 +56,33 @@
|
|||||||
<h2 v-text="item.title" />
|
<h2 v-text="item.title" />
|
||||||
<div :ref="element => setChartRef(item.id, element)" class="chart" />
|
<div :ref="element => setChartRef(item.id, element)" class="chart" />
|
||||||
</section>
|
</section>
|
||||||
<pull-down-refresh
|
|
||||||
v-if="!selectedCustomer"
|
|
||||||
v-model="rankRefreshing"
|
|
||||||
class="rank-refresh"
|
|
||||||
:refresh-timeout="30000"
|
|
||||||
@refresh="refreshCustomerRegistrationRank"
|
|
||||||
@scrolltolower="loadNextRankPage"
|
|
||||||
>
|
|
||||||
<div class="rank-content">
|
|
||||||
<div class="rank-summary" v-text="rankSummaryText"></div>
|
|
||||||
<t-cell-group
|
|
||||||
v-if="customerRegistrationRankList.length"
|
|
||||||
class="rank-list"
|
|
||||||
theme="card"
|
|
||||||
>
|
|
||||||
<t-cell
|
|
||||||
v-for="item in visibleCustomerRegistrationRankList"
|
|
||||||
:key="item.customerId"
|
|
||||||
class="item"
|
|
||||||
arrow
|
|
||||||
hover
|
|
||||||
@click="selectRankCustomer(item.customerId || '')"
|
|
||||||
>
|
|
||||||
<template #leftIcon>
|
|
||||||
<span class="left">
|
|
||||||
<t-image
|
|
||||||
v-if="getRankCustomerPhotoURL(item)"
|
|
||||||
:src="getRankCustomerPhotoURL(item)"
|
|
||||||
:alt="item.customerName || '客户头像'"
|
|
||||||
fit="cover"
|
|
||||||
class="photo"
|
|
||||||
/>
|
|
||||||
<span v-else class="photo fallback" v-text="getRankCustomerInitial(item)" />
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<template #title>
|
|
||||||
<t-tag theme="primary" :content="`No.${item.rank}`" />
|
|
||||||
<span class="word-space" v-text="item.customerName || '未知客户'" />
|
|
||||||
</template>
|
|
||||||
<template #description>
|
|
||||||
<div>
|
|
||||||
<span v-text="`累计登记 ${item.totalEventCount || 0} 次,`" />
|
|
||||||
<span v-text="`累计登记 ${item.totalEventDays || 0} 天`" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span v-text="`最近 ${item.recentContinuousEventDays || 0} 天连续登记`" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</t-cell>
|
|
||||||
</t-cell-group>
|
|
||||||
<div v-else class="empty-block" v-text="rankLoading ? '正在加载排行榜' : '暂无登记排行数据'" />
|
|
||||||
<div v-if="1 < rankPageCount" class="rank-load-state" v-text="rankCanLoadMore ? '上拉加载更多' : '已加载全部'" />
|
|
||||||
</div>
|
|
||||||
</pull-down-refresh>
|
|
||||||
<customer-selector
|
<customer-selector
|
||||||
v-model="selectedCustomerId"
|
v-model="selectedCustomerId"
|
||||||
v-model:visible="selectorVisible"
|
v-model:visible="selectorVisible"
|
||||||
@change="handleCustomerSelected"
|
@change="handleCustomerSelected"
|
||||||
/>
|
/>
|
||||||
<t-popup v-model="rankStartPickerVisible" placement="bottom">
|
|
||||||
<t-date-time-picker
|
|
||||||
v-model="rankDateRange[0]"
|
|
||||||
:default-value="rankDateRange[0]"
|
|
||||||
format="YYYY-MM-DD HH:mm"
|
|
||||||
:mode="['date', 'minute']"
|
|
||||||
title="开始时间"
|
|
||||||
@confirm="confirmRankStartTime"
|
|
||||||
/>
|
|
||||||
</t-popup>
|
|
||||||
<t-popup v-model="rankEndPickerVisible" placement="bottom">
|
|
||||||
<t-date-time-picker
|
|
||||||
v-model="rankDateRange[1]"
|
|
||||||
:default-value="rankDateRange[1]"
|
|
||||||
format="YYYY-MM-DD HH:mm"
|
|
||||||
:mode="['date', 'minute']"
|
|
||||||
title="结束时间"
|
|
||||||
@confirm="confirmRankEndTime"
|
|
||||||
/>
|
|
||||||
</t-popup>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type {ECharts, EChartsOption} from "echarts";
|
import type {ECharts, EChartsOption} from "echarts";
|
||||||
import {PullDownRefresh, type DropdownValue} from "tdesign-mobile-vue";
|
|
||||||
import {BarChart, LineChart} from "echarts/charts";
|
import {BarChart, LineChart} from "echarts/charts";
|
||||||
import {GridComponent, LegendComponent, TooltipComponent} from "echarts/components";
|
import {GridComponent, LegendComponent, TooltipComponent} from "echarts/components";
|
||||||
import {init, use} from "echarts/core";
|
import {init, use} from "echarts/core";
|
||||||
import {CanvasRenderer} from "echarts/renderers";
|
import {CanvasRenderer} from "echarts/renderers";
|
||||||
import {AttachmentAPI, Time} from "timi-web";
|
import {Time} from "timi-web";
|
||||||
import CustomerAPI from "@/api/CustomerAPI";
|
import CustomerAPI from "@/api/CustomerAPI";
|
||||||
import EventAPI from "@/api/EventAPI";
|
import EventAPI from "@/api/EventAPI";
|
||||||
import EventRecordAPI from "@/api/EventRecordAPI";
|
import EventRecordAPI from "@/api/EventRecordAPI";
|
||||||
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 {CustomerEventRankView, EventRecord, EventRecordPage} from "@/types/EventRecord";
|
import type {EventRecord} from "@/types/EventRecord";
|
||||||
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 EventRecordCalendar from "@/components/EventRecordCalendar.vue";
|
import EventRecordCalendar from "@/components/EventRecordCalendar.vue";
|
||||||
import {useSettingStore} from "@/store/settingStore";
|
import {useSettingStore} from "@/store/settingStore";
|
||||||
import {permissionCodes} from "@/utils/permission";
|
import {permissionCodes} from "@/utils/permission";
|
||||||
|
|
||||||
type RankDateFilterType = "all" | "today" | "thisMonth" | "custom";
|
|
||||||
|
|
||||||
interface SummaryItem {
|
interface SummaryItem {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
@@ -207,12 +112,6 @@ const router = useRouter();
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const chartRefs = new Map<string, HTMLElement>();
|
const chartRefs = new Map<string, HTMLElement>();
|
||||||
const chartInstances = new Map<string, ECharts>();
|
const chartInstances = new Map<string, ECharts>();
|
||||||
const rankDateOptions: Array<{ label: string; value: RankDateFilterType }> = [
|
|
||||||
{label: "全部", value: "all"},
|
|
||||||
{label: "今天", value: "today"},
|
|
||||||
{label: "本月", value: "thisMonth"},
|
|
||||||
{label: "自定义范围", value: "custom"}
|
|
||||||
];
|
|
||||||
const routeCustomerId = computed(() => String(route.params.id || ""));
|
const routeCustomerId = computed(() => String(route.params.id || ""));
|
||||||
const routeCustomerCode = computed(() => String(route.query.customerCode || ""));
|
const routeCustomerCode = computed(() => String(route.query.customerCode || ""));
|
||||||
const customerCode = ref(routeCustomerCode.value);
|
const customerCode = ref(routeCustomerCode.value);
|
||||||
@@ -226,19 +125,6 @@ const selectedEventIdList = ref<string[]>([]);
|
|||||||
const monthCursor = ref(getMonthStart(new Date()));
|
const monthCursor = ref(getMonthStart(new Date()));
|
||||||
const monthRecords = ref<EventRecord[]>([]);
|
const monthRecords = ref<EventRecord[]>([]);
|
||||||
const calendarLoading = ref(false);
|
const calendarLoading = ref(false);
|
||||||
const customerRegistrationRankList = ref<CustomerEventRankView[]>([]);
|
|
||||||
const rankPageSize = 10;
|
|
||||||
const rankPageIndex = ref(1);
|
|
||||||
const rankDateFilterType = ref<RankDateFilterType>("all");
|
|
||||||
const rankDateRange = ref(buildRankPresetRange("all"));
|
|
||||||
const rankEventIdList = ref<string[]>([]);
|
|
||||||
const rankLoading = ref(false);
|
|
||||||
const rankRefreshing = ref(false);
|
|
||||||
const rankStartPickerVisible = ref(false);
|
|
||||||
const rankEndPickerVisible = ref(false);
|
|
||||||
const rankCache = new Map<string, CustomerEventRankView[]>();
|
|
||||||
const rankPendingRequestMap = new Map<string, Promise<CustomerEventRankView[]>>();
|
|
||||||
let rankRequestToken = 0;
|
|
||||||
let latestDecodedText = "";
|
let latestDecodedText = "";
|
||||||
|
|
||||||
const eventNameMap = computed(() => {
|
const eventNameMap = computed(() => {
|
||||||
@@ -302,33 +188,6 @@ const chartList = computed<ChartProfile[]>(() => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
const rankDateFilterLabel = computed(() => rankDateOptions.find(item => item.value === rankDateFilterType.value)?.label || "全部");
|
|
||||||
const rankEventOptions = computed(() => eventList.value.map(item => ({
|
|
||||||
label: item.name || "未命名事件",
|
|
||||||
value: item.id || ""
|
|
||||||
})).filter(item => item.value));
|
|
||||||
const rankEventFilterLabel = computed(() => {
|
|
||||||
if (!rankEventIdList.value.length || rankEventIdList.value.length === eventList.value.length) {
|
|
||||||
return "全部事件";
|
|
||||||
}
|
|
||||||
|
|
||||||
return eventList.value
|
|
||||||
.filter(item => item.id && rankEventIdList.value.includes(item.id))
|
|
||||||
.map(item => item.name || "未命名事件")
|
|
||||||
.join("、");
|
|
||||||
});
|
|
||||||
const rankSummaryText = computed(() => {
|
|
||||||
if (rankDateFilterType.value === "custom") {
|
|
||||||
return `${rankDateRange.value[0] || "-"} ~ ${rankDateRange.value[1] || "-"}|${rankEventFilterLabel.value}`;
|
|
||||||
}
|
|
||||||
return `范围:${rankDateFilterLabel.value}|${rankEventFilterLabel.value}`;
|
|
||||||
});
|
|
||||||
const visibleCustomerRegistrationRankList = computed(() => {
|
|
||||||
return customerRegistrationRankList.value.slice(0, rankPageIndex.value * rankPageSize);
|
|
||||||
});
|
|
||||||
const rankPageCount = computed(() => Math.max(1, Math.ceil(customerRegistrationRankList.value.length / rankPageSize)));
|
|
||||||
const rankCanLoadMore = computed(() => rankPageIndex.value < rankPageCount.value);
|
|
||||||
|
|
||||||
watch(chartList, async () => {
|
watch(chartList, async () => {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
renderCharts();
|
renderCharts();
|
||||||
@@ -356,7 +215,6 @@ watch([selectedCustomer, monthCursor], async () => {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadActions();
|
await loadActions();
|
||||||
await loadCustomerRegistrationRank();
|
|
||||||
if (routeCustomerId.value) {
|
if (routeCustomerId.value) {
|
||||||
await loadCustomerById(routeCustomerId.value);
|
await loadCustomerById(routeCustomerId.value);
|
||||||
} else if (customerCode.value) {
|
} else if (customerCode.value) {
|
||||||
@@ -409,9 +267,8 @@ function resizeCharts(): void {
|
|||||||
// ---------- 数据 ----------
|
// ---------- 数据 ----------
|
||||||
|
|
||||||
async function loadActions(): Promise<void> {
|
async function loadActions(): Promise<void> {
|
||||||
eventList.value = await EventAPI.enabledList();
|
eventList.value = await EventAPI.listValid();
|
||||||
selectedEventIdList.value = eventList.value.map(item => item.id).filter((item): item is string => Boolean(item));
|
selectedEventIdList.value = eventList.value.map(item => item.id).filter((item): item is string => Boolean(item));
|
||||||
rankEventIdList.value = selectedEventIdList.value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadCustomer(code: string): Promise<void> {
|
async function loadCustomer(code: string): Promise<void> {
|
||||||
@@ -423,7 +280,7 @@ async function loadCustomer(code: string): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const customer = await CustomerAPI.nullableDetailByCode(normalizedCode);
|
const customer = await CustomerAPI.findByCode(normalizedCode);
|
||||||
if (!customer) {
|
if (!customer) {
|
||||||
selectedCustomer.value = undefined;
|
selectedCustomer.value = undefined;
|
||||||
selectedCustomerId.value = "";
|
selectedCustomerId.value = "";
|
||||||
@@ -470,10 +327,6 @@ async function clearCustomerSelection(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectRankCustomer(id: string): Promise<void> {
|
|
||||||
await loadCustomerById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadMonthRecords(): Promise<void> {
|
async function loadMonthRecords(): Promise<void> {
|
||||||
const customerId = selectedCustomer.value?.id;
|
const customerId = selectedCustomer.value?.id;
|
||||||
if (!customerId) {
|
if (!customerId) {
|
||||||
@@ -485,113 +338,18 @@ async function loadMonthRecords(): Promise<void> {
|
|||||||
const endDate = getDateEnd(addDays(beginDate, 41));
|
const endDate = getDateEnd(addDays(beginDate, 41));
|
||||||
calendarLoading.value = true;
|
calendarLoading.value = true;
|
||||||
try {
|
try {
|
||||||
monthRecords.value = await EventRecordAPI.rangeList({
|
monthRecords.value = (await EventRecordAPI.list({
|
||||||
|
index: 0,
|
||||||
|
size: Number.MAX_SAFE_INTEGER,
|
||||||
customerId,
|
customerId,
|
||||||
beginAt: Time.toTimestamp(beginDate),
|
beginAt: Time.toTimestamp(beginDate),
|
||||||
endAt: Time.toTimestamp(endDate)
|
endAt: Time.toTimestamp(endDate)
|
||||||
});
|
})).list;
|
||||||
} finally {
|
} finally {
|
||||||
calendarLoading.value = false;
|
calendarLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadCustomerRegistrationRank(force = false): Promise<void> {
|
|
||||||
const cacheKey = buildRankCacheKey();
|
|
||||||
if (!force) {
|
|
||||||
const cachedList = rankCache.get(cacheKey);
|
|
||||||
if (cachedList) {
|
|
||||||
customerRegistrationRankList.value = cachedList;
|
|
||||||
rankPageIndex.value = 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rankCache.delete(cacheKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = rankRequestToken + 1;
|
|
||||||
rankRequestToken = token;
|
|
||||||
rankPageIndex.value = 1;
|
|
||||||
rankLoading.value = true;
|
|
||||||
let request = force ? undefined : rankPendingRequestMap.get(cacheKey);
|
|
||||||
if (!request) {
|
|
||||||
request = EventRecordAPI.customerRegistrationRank(buildRankStatReq());
|
|
||||||
rankPendingRequestMap.set(cacheKey, request);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const result = await request;
|
|
||||||
if (rankPendingRequestMap.get(cacheKey) === request) {
|
|
||||||
rankCache.set(cacheKey, result);
|
|
||||||
}
|
|
||||||
if (token !== rankRequestToken) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
customerRegistrationRankList.value = result;
|
|
||||||
rankPageIndex.value = 1;
|
|
||||||
} finally {
|
|
||||||
if (rankPendingRequestMap.get(cacheKey) === request) {
|
|
||||||
rankPendingRequestMap.delete(cacheKey);
|
|
||||||
}
|
|
||||||
if (token === rankRequestToken) {
|
|
||||||
rankLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function reloadCustomerRegistrationRank(): Promise<void> {
|
|
||||||
await loadCustomerRegistrationRank();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshCustomerRegistrationRank(): Promise<void> {
|
|
||||||
rankRefreshing.value = true;
|
|
||||||
try {
|
|
||||||
await loadCustomerRegistrationRank(true);
|
|
||||||
} finally {
|
|
||||||
rankRefreshing.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadNextRankPage(): void {
|
|
||||||
if (!rankCanLoadMore.value || rankLoading.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
rankPageIndex.value += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function selectRankDateFilter(value: DropdownValue): Promise<void> {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
rankDateFilterType.value = String(value) as RankDateFilterType;
|
|
||||||
if (rankDateFilterType.value !== "custom") {
|
|
||||||
rankDateRange.value = buildRankPresetRange(rankDateFilterType.value);
|
|
||||||
await loadCustomerRegistrationRank();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!rankDateRange.value[0] || !rankDateRange.value[1]) {
|
|
||||||
rankDateRange.value = buildRankPresetRange("today");
|
|
||||||
}
|
|
||||||
rankStartPickerVisible.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmRankStartTime(): void {
|
|
||||||
rankStartPickerVisible.value = false;
|
|
||||||
rankEndPickerVisible.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function confirmRankEndTime(): Promise<void> {
|
|
||||||
rankEndPickerVisible.value = false;
|
|
||||||
normalizeRankDateRange();
|
|
||||||
await loadCustomerRegistrationRank();
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeRankDateRange(): void {
|
|
||||||
if (rankDateRange.value[0] && rankDateRange.value[1] && rankDateRange.value[1] < rankDateRange.value[0]) {
|
|
||||||
rankDateRange.value = [rankDateRange.value[1], rankDateRange.value[0]];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- 扫码 ----------
|
// ---------- 扫码 ----------
|
||||||
|
|
||||||
async function toggleScanner(): Promise<void> {
|
async function toggleScanner(): Promise<void> {
|
||||||
@@ -632,17 +390,6 @@ function getRecordEventName(record?: EventRecord): string {
|
|||||||
return record.event?.name || (record.eventId ? eventNameMap.value.get(record.eventId) : "") || "未知事件";
|
return record.event?.name || (record.eventId ? eventNameMap.value.get(record.eventId) : "") || "未知事件";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRankCustomerPhotoURL(item: CustomerEventRankView): string {
|
|
||||||
if (!item.customerPhotoAttachmentId) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return AttachmentAPI.getReadURL(item.customerPhotoAttachmentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRankCustomerInitial(item: CustomerEventRankView): string {
|
|
||||||
return (item.customerName || "客").slice(0, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- 图表 ----------
|
// ---------- 图表 ----------
|
||||||
|
|
||||||
function buildBaseOption(): EChartsOption {
|
function buildBaseOption(): EChartsOption {
|
||||||
@@ -795,56 +542,6 @@ function getDateEnd(date: Date): Date {
|
|||||||
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999);
|
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildRankStatReq(): Partial<EventRecordPage> | undefined {
|
|
||||||
const selectedAllEvent = !rankEventIdList.value.length || rankEventIdList.value.length === eventList.value.length;
|
|
||||||
if (rankDateFilterType.value === "all") {
|
|
||||||
return selectedAllEvent ? undefined : {
|
|
||||||
eventIdList: rankEventIdList.value
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
beginAt: Time.toTimestamp(parseRankDateTime(rankDateRange.value[0], false)),
|
|
||||||
endAt: Time.toTimestamp(parseRankDateTime(rankDateRange.value[1], true)),
|
|
||||||
eventIdList: selectedAllEvent ? undefined : rankEventIdList.value
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildRankCacheKey(): string {
|
|
||||||
const req = buildRankStatReq();
|
|
||||||
return JSON.stringify({
|
|
||||||
beginAt: req?.beginAt,
|
|
||||||
endAt: req?.endAt,
|
|
||||||
eventIdList: req?.eventIdList?.slice().sort()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildRankPresetRange(type: Exclude<RankDateFilterType, "custom">): [string, string] {
|
|
||||||
if (type === "all") {
|
|
||||||
return ["", ""];
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
if (type === "thisMonth") {
|
|
||||||
const monthStart = getMonthStart(now);
|
|
||||||
return [`${Time.formatDate(monthStart)} 00:00`, `${Time.formatDate(getDateEnd(now))} 23:59`];
|
|
||||||
}
|
|
||||||
|
|
||||||
const dayStart = new Date(now);
|
|
||||||
dayStart.setHours(0, 0, 0, 0);
|
|
||||||
return [`${Time.formatDate(dayStart)} 00:00`, `${Time.formatDate(getDateEnd(now))} 23:59`];
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseRankDateTime(value: string, isEnd: boolean): Date {
|
|
||||||
if (!value) {
|
|
||||||
return isEnd ? new Date() : new Date(0);
|
|
||||||
}
|
|
||||||
const [dateText, timeText = isEnd ? "23:59" : "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, isEnd ? 59 : 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
@@ -891,33 +588,6 @@ function parseRankDateTime(value: string, isEnd: boolean): Date {
|
|||||||
&.scanner-wrapper {
|
&.scanner-wrapper {
|
||||||
margin: 1rem .5rem;
|
margin: 1rem .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.filter {
|
|
||||||
|
|
||||||
.menu {
|
|
||||||
--td-spacer-1: 6px;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
margin-top: -1rem;
|
|
||||||
padding-top: .8rem;
|
|
||||||
background: #fff;
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.t-dropdown-menu__item) {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.t-dropdown-menu__title) {
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -971,63 +641,6 @@ function parseRankDateTime(value: string, isEnd: boolean): Date {
|
|||||||
margin: 0 1rem;
|
margin: 0 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rank-summary {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rank-content {
|
|
||||||
gap: 1rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rank-list {
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
.item {
|
|
||||||
:deep(.t-cell__title) {
|
|
||||||
flex: 1 1 0;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.t-cell__title-text),
|
|
||||||
:deep(.t-cell__description) {
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left {
|
|
||||||
gap: .5rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.photo {
|
|
||||||
width: 4.3rem;
|
|
||||||
height: 4.3rem;
|
|
||||||
border-radius: .5rem;
|
|
||||||
|
|
||||||
&.fallback {
|
|
||||||
color: #2563eb;
|
|
||||||
display: flex;
|
|
||||||
font-size: 1rem;
|
|
||||||
background: #dbeafe;
|
|
||||||
align-items: center;
|
|
||||||
font-weight: 700;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.rank-load-state {
|
|
||||||
padding: .25rem 0;
|
|
||||||
color: #52607a;
|
|
||||||
font-size: .875rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-section {
|
.chart-section {
|
||||||
h2 {
|
h2 {
|
||||||
margin-bottom: .875rem;
|
margin-bottom: .875rem;
|
||||||
@@ -1039,28 +652,12 @@ function parseRankDateTime(value: string, isEnd: boolean): Date {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-block {
|
|
||||||
padding: 1rem;
|
|
||||||
color: #52607a;
|
|
||||||
margin: 0 1rem;
|
|
||||||
text-align: center;
|
|
||||||
background: #f7faff;
|
|
||||||
border-radius: .75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(html.theme-dark) {
|
:global(html.theme-dark) {
|
||||||
.customer-data-charts-page {
|
.customer-data-charts-page {
|
||||||
.selector {
|
.selector {
|
||||||
background: #10161f;
|
background: #10161f;
|
||||||
|
|
||||||
.filter-line {
|
|
||||||
.filter-menu {
|
|
||||||
background: #172033;
|
|
||||||
border-color: rgba(148, 163, 184, .16);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary {
|
.summary {
|
||||||
@@ -1079,8 +676,7 @@ function parseRankDateTime(value: string, isEnd: boolean): Date {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.calendar-section,
|
.calendar-section,
|
||||||
.chart-section,
|
.chart-section {
|
||||||
.rank-section {
|
|
||||||
background: rgba(15, 23, 42, .88);
|
background: rgba(15, 23, 42, .88);
|
||||||
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
|
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
|
||||||
|
|
||||||
@@ -1089,50 +685,6 @@ function parseRankDateTime(value: string, isEnd: boolean): Date {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.rank-section {
|
|
||||||
.rank-header {
|
|
||||||
p {
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rank-filter {
|
|
||||||
border-color: rgba(148, 163, 184, .16);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.rank-no {
|
|
||||||
color: #bfdbfe;
|
|
||||||
background: rgba(37, 99, 235, .28);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rank-photo {
|
|
||||||
&.fallback {
|
|
||||||
color: #bfdbfe;
|
|
||||||
background: rgba(37, 99, 235, .28);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.rank-stat {
|
|
||||||
strong {
|
|
||||||
color: #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
small {
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-block {
|
|
||||||
color: #94a3b8;
|
|
||||||
background: rgba(30, 41, 59, .9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rank-load-state {
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
class="customer"
|
class="customer"
|
||||||
:customer="customer"
|
:customer="customer"
|
||||||
/>
|
/>
|
||||||
<t-cell-group v-if="customer" theme="card">
|
<t-cell-group v-if="customer" class="info" theme="card">
|
||||||
<t-cell title="出生日期">
|
<t-cell title="出生日期">
|
||||||
<span
|
<span
|
||||||
v-if="isLunarBirthdate"
|
v-if="isLunarBirthdate"
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<t-cell title="录入时间" :note="createdAtText" />
|
<t-cell title="录入时间" :note="createdAtText" />
|
||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
<t-cell-group v-if="customer && canReadStat" theme="card">
|
<t-cell-group v-if="customer && canReadStat" theme="card">
|
||||||
<t-cell title="客户数据图表" arrow @click="goCustomerDataCharts">
|
<t-cell title="客户数据图表" arrow @click="toCustomerDataCharts">
|
||||||
<template #leftIcon>
|
<template #leftIcon>
|
||||||
<t-icon name="chart" size="1.5rem" />
|
<t-icon name="chart" size="1.5rem" />
|
||||||
</template>
|
</template>
|
||||||
@@ -36,18 +36,16 @@
|
|||||||
<body-area-selector v-model="selectedAreas" />
|
<body-area-selector v-model="selectedAreas" />
|
||||||
</div>
|
</div>
|
||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
<t-cell-group v-if="inviter" :title="`由 ${inviter.name} 邀请`" class="invite" theme="card">
|
<t-cell-group v-if="inviter" :title="`由 ${inviter.name} 邀请`" class="invite-list" theme="card">
|
||||||
<customer-profile-card
|
<customer-profile-card :customer="inviter" @click="toCustomerDetail(inviter)"/>
|
||||||
class="profile-card"
|
|
||||||
:customer="inviter"
|
|
||||||
@click="goCustomerDetail(inviter)"
|
|
||||||
/>
|
|
||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
<div class="invite">
|
<div class="invite-list">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="title">邀请客户</div>
|
<div class="title">邀请客户</div>
|
||||||
<div v-can="permissionCodes.customer.update" class="actions">
|
<div v-can="permissionCodes.customer.update" class="actions">
|
||||||
<t-button
|
<t-button
|
||||||
|
theme="primary"
|
||||||
|
variant="outline"
|
||||||
size="extra-small"
|
size="extra-small"
|
||||||
content="选择添加"
|
content="选择添加"
|
||||||
@click="selectorVisible = true"
|
@click="selectorVisible = true"
|
||||||
@@ -67,15 +65,12 @@
|
|||||||
:key="item.id || item.code || index"
|
:key="item.id || item.code || index"
|
||||||
>
|
>
|
||||||
<t-cell
|
<t-cell
|
||||||
|
class="cell"
|
||||||
|
:title="item.name || item.code || '-'"
|
||||||
arrow
|
arrow
|
||||||
hover
|
hover
|
||||||
@click="goCustomerDetail(item)"
|
@click="toCustomerDetail(item)"
|
||||||
>
|
>
|
||||||
<template #title>
|
|
||||||
<div class="title">
|
|
||||||
<span v-text="item.name || item.code" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #description>
|
<template #description>
|
||||||
<telephone-link :value="item.telephone" placeholder="未填写电话" />
|
<telephone-link :value="item.telephone" placeholder="未填写电话" />
|
||||||
</template>
|
</template>
|
||||||
@@ -103,7 +98,7 @@
|
|||||||
class="delete bg-white"
|
class="delete bg-white"
|
||||||
theme="danger"
|
theme="danger"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
:loading="removing"
|
:loading="isRemoving"
|
||||||
@click="confirmRemoveCustomer"
|
@click="confirmRemoveCustomer"
|
||||||
>
|
>
|
||||||
删除客户
|
删除客户
|
||||||
@@ -178,7 +173,7 @@ const selectorVisible = ref(false);
|
|||||||
const scanPopupVisible = ref(false);
|
const scanPopupVisible = ref(false);
|
||||||
const pendingInvitee = ref<Customer>();
|
const pendingInvitee = ref<Customer>();
|
||||||
const scanStatus = ref("点击扫码开始识别。");
|
const scanStatus = ref("点击扫码开始识别。");
|
||||||
const removing = ref(false);
|
const isRemoving = ref(false);
|
||||||
const showSolarBirthdate = ref(false);
|
const showSolarBirthdate = ref(false);
|
||||||
let latestDecodedText = "";
|
let latestDecodedText = "";
|
||||||
|
|
||||||
@@ -188,7 +183,7 @@ const creatorName = computed(() => {
|
|||||||
const creatorUser = customer.value?.creatorUser;
|
const creatorUser = customer.value?.creatorUser;
|
||||||
return creatorUser?.nick || creatorUser?.name || customer.value?.creatorUserId || "无";
|
return creatorUser?.nick || creatorUser?.name || customer.value?.creatorUserId || "无";
|
||||||
});
|
});
|
||||||
const createdAtText = computed(() => formatTime(customer.value?.createdAt));
|
const createdAtText = computed(() => customer.value?.createdAt ? Time.toPassedDateTime(customer.value.createdAt) : "无");
|
||||||
const isLunarBirthdate = computed(() => customer.value?.birthdateCalendar === "LUNAR" && !!customer.value?.birthdate);
|
const isLunarBirthdate = computed(() => customer.value?.birthdateCalendar === "LUNAR" && !!customer.value?.birthdate);
|
||||||
const solarBirthdateText = computed(() => formatSolarBirthdate(customer.value));
|
const solarBirthdateText = computed(() => formatSolarBirthdate(customer.value));
|
||||||
const birthdateText = computed(() => showSolarBirthdate.value && isLunarBirthdate.value ? solarBirthdateText.value : formatBirthdate(customer.value));
|
const birthdateText = computed(() => showSolarBirthdate.value && isLunarBirthdate.value ? solarBirthdateText.value : formatBirthdate(customer.value));
|
||||||
@@ -219,7 +214,7 @@ async function loadCustomer(): Promise<void> {
|
|||||||
customer.value = undefined;
|
customer.value = undefined;
|
||||||
invitees.value = [];
|
invitees.value = [];
|
||||||
console.warn("客户详情加载失败", err);
|
console.warn("客户详情加载失败", err);
|
||||||
showToast("客户详情加载失败", "warning");
|
Toast.warning("客户详情加载失败");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,7 +225,7 @@ async function loadInvitees(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
invitees.value = await CustomerAPI.invitedList(customerId);
|
invitees.value = await CustomerAPI.listInvited(customerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeInvitee(item: Customer): Promise<void> {
|
async function removeInvitee(item: Customer): Promise<void> {
|
||||||
@@ -240,7 +235,7 @@ async function removeInvitee(item: Customer): Promise<void> {
|
|||||||
|
|
||||||
await CustomerAPI.deleteInvite(item.id);
|
await CustomerAPI.deleteInvite(item.id);
|
||||||
await loadCustomer();
|
await loadCustomer();
|
||||||
showToast("已删除邀请关系");
|
Toast.success("已删除邀请关系");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function confirmRemoveCustomer(): Promise<void> {
|
async function confirmRemoveCustomer(): Promise<void> {
|
||||||
@@ -267,13 +262,13 @@ async function confirmRemoveCustomer(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function removeCustomer(id: string): Promise<void> {
|
async function removeCustomer(id: string): Promise<void> {
|
||||||
removing.value = true;
|
isRemoving.value = true;
|
||||||
try {
|
try {
|
||||||
await CustomerAPI.remove(id);
|
await CustomerAPI.remove(id);
|
||||||
showToast("客户已删除");
|
Toast.success("客户已删除");
|
||||||
await router.replace("/management/customers");
|
await router.replace("/management/customers");
|
||||||
} finally {
|
} finally {
|
||||||
removing.value = false;
|
isRemoving.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,25 +279,6 @@ async function toEdit(): Promise<void> {
|
|||||||
await router.push(`/management/customers/${customer.value.id}/edit`);
|
await router.push(`/management/customers/${customer.value.id}/edit`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showToast(message: string, theme: "success" | "warning" = "success"): void {
|
|
||||||
Toast({
|
|
||||||
theme,
|
|
||||||
direction: "row",
|
|
||||||
message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTime(value?: number): string {
|
|
||||||
if (!value) {
|
|
||||||
return "无";
|
|
||||||
}
|
|
||||||
const date = Time.toDateObject(value);
|
|
||||||
if (!date) {
|
|
||||||
return "无";
|
|
||||||
}
|
|
||||||
return Time.formatDateTime(date);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatBirthdate(value?: Customer): string {
|
function formatBirthdate(value?: Customer): string {
|
||||||
if (!value?.birthdate) {
|
if (!value?.birthdate) {
|
||||||
return "无";
|
return "无";
|
||||||
@@ -345,20 +321,17 @@ async function addInvitee(invitee: Customer): Promise<boolean> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (invitee.id === customer.value.id) {
|
if (invitee.id === customer.value.id) {
|
||||||
showToast("不能把客户本人添加为邀请客户", "warning");
|
Toast.warning("不能把客户本人添加为邀请客户");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (isCustomerAncestor(invitee)) {
|
if (isCustomerAncestor(invitee)) {
|
||||||
showToast("不能把上级邀请人添加为邀请客户", "warning");
|
Toast.warning("不能把上级邀请人添加为邀请客户");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
await CustomerAPI.updateInvite({
|
await CustomerAPI.saveInvite(customer.value.id, invitee.id);
|
||||||
customerId: invitee.id,
|
|
||||||
introducerCustomerId: customer.value.id
|
|
||||||
});
|
|
||||||
await loadCustomer();
|
await loadCustomer();
|
||||||
showToast(`已添加 ${invitee.name || invitee.code}`);
|
Toast.success(`已添加 ${invitee.name || invitee.code}`);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,7 +376,7 @@ function handleScanStatusChange(value: string): void {
|
|||||||
|
|
||||||
async function confirmScannedInvitee(): Promise<void> {
|
async function confirmScannedInvitee(): Promise<void> {
|
||||||
if (!pendingInvitee.value) {
|
if (!pendingInvitee.value) {
|
||||||
showToast("请先扫码识别客户", "warning");
|
Toast.warning("请先扫码识别客户");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,17 +386,17 @@ async function confirmScannedInvitee(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadCustomerByCode(code: string): Promise<Customer | undefined> {
|
async function loadCustomerByCode(code: string): Promise<Customer | undefined> {
|
||||||
return await CustomerAPI.nullableDetailByCode(code) || undefined;
|
return await CustomerAPI.findByCode(code) || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function goCustomerDetail(target: Customer): Promise<void> {
|
async function toCustomerDetail(target: Customer): Promise<void> {
|
||||||
if (!target.id) {
|
if (!target.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await router.push(`/management/customers/${target.id}`);
|
await router.push(`/management/customers/${target.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function goCustomerDataCharts(): Promise<void> {
|
async function toCustomerDataCharts(): Promise<void> {
|
||||||
if (!customer.value?.id) {
|
if (!customer.value?.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -451,9 +424,7 @@ watch(() => route.params.id, async () => {
|
|||||||
margin: 0 1rem;
|
margin: 0 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.t-cell__title) {
|
.info {
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.t-cell__title-text) {
|
:deep(.t-cell__title-text) {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -465,6 +436,7 @@ watch(() => route.params.id, async () => {
|
|||||||
white-space: normal;
|
white-space: normal;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.conditioning-part {
|
.conditioning-part {
|
||||||
gap: .5rem;
|
gap: .5rem;
|
||||||
@@ -473,59 +445,7 @@ watch(() => route.params.id, async () => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invite {
|
.invite-list {
|
||||||
|
|
||||||
.inviter-profile {
|
|
||||||
gap: .5rem;
|
|
||||||
display: flex;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: .75rem;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.profile-card {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
|
|
||||||
.title {
|
|
||||||
gap: .5rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
|
|
||||||
:deep(.t-cell:after) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
width: 3rem;
|
|
||||||
height: 3rem;
|
|
||||||
border-radius: .875rem;
|
|
||||||
|
|
||||||
&.fallback {
|
|
||||||
color: #2563eb;
|
|
||||||
display: flex;
|
|
||||||
font-size: 1.125rem;
|
|
||||||
background: #dbeafe;
|
|
||||||
align-items: center;
|
|
||||||
font-weight: 600;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.remove {
|
|
||||||
width: fit-content;
|
|
||||||
padding: 0 1rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -542,11 +462,48 @@ watch(() => route.params.id, async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
gap: 1rem;
|
gap: .875rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
|
||||||
|
:deep(.t-cell:after) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell {
|
||||||
|
padding: 6px 10px 6px 6px;
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 3.25rem;
|
||||||
|
height: 3.25rem;
|
||||||
|
border-radius: .875rem;
|
||||||
|
|
||||||
|
&.fallback {
|
||||||
|
color: #2563eb;
|
||||||
|
display: flex;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
background: #dbeafe;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: 600;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove {
|
||||||
|
width: fit-content;
|
||||||
|
padding: 0 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
color: #52607a;
|
color: #52607a;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
@@ -627,4 +584,3 @@ watch(() => route.params.id, async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<t-form class="form" label-align="left" label-width="6rem">
|
<t-form class="form" label-align="left" label-width="6rem">
|
||||||
<t-form-item label="客户编码">
|
<t-form-item label="客户编码">
|
||||||
<t-input
|
<t-input
|
||||||
v-model.trim="formModel.code"
|
v-model.trim="formData.code"
|
||||||
placeholder="客户编码"
|
placeholder="客户编码"
|
||||||
borderless
|
borderless
|
||||||
>
|
>
|
||||||
@@ -29,50 +29,50 @@
|
|||||||
</div>
|
</div>
|
||||||
<t-form-item label="姓名" required-mark>
|
<t-form-item label="姓名" required-mark>
|
||||||
<t-input
|
<t-input
|
||||||
v-model.trim="formModel.name"
|
v-model.trim="formData.name"
|
||||||
placeholder="姓名"
|
placeholder="姓名"
|
||||||
borderless
|
borderless
|
||||||
/>
|
/>
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="出生日期">
|
<t-form-item label="出生日期">
|
||||||
<birthdate-picker
|
<birthdate-picker
|
||||||
v-model="formModel.birthdate"
|
v-model="formData.birthdate"
|
||||||
v-model:calendar="formModel.birthdateCalendar"
|
v-model:calendar="formData.birthdateCalendar"
|
||||||
/>
|
/>
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="年龄">
|
<t-form-item label="年龄">
|
||||||
<t-input
|
<t-input
|
||||||
v-model.trim="formModel.age"
|
v-model.trim="formData.age"
|
||||||
type="number"
|
type="number"
|
||||||
:readonly="!!formModel.birthdate"
|
:readonly="!!formData.birthdate"
|
||||||
:placeholder="formModel.birthdate ? '根据出生日期自动计算' : '年龄'"
|
:placeholder="formData.birthdate ? '根据出生日期自动计算' : '年龄'"
|
||||||
borderless
|
borderless
|
||||||
/>
|
/>
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="照片">
|
<t-form-item label="照片">
|
||||||
<photo-field
|
<photo-field
|
||||||
v-model="formModel.photoURL"
|
v-model="photoURL"
|
||||||
alt-text="客户照片"
|
alt-text="客户照片"
|
||||||
:loading="photoUploading"
|
:loading="isPhotoUploading"
|
||||||
:preview="false"
|
:preview="false"
|
||||||
@add="selectPhoto"
|
@add="photoInputRef?.click"
|
||||||
@clear="resetPhotoState"
|
@clear="photoTempFileId = ''"
|
||||||
/>
|
/>
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="性别">
|
<t-form-item label="性别">
|
||||||
<t-radio-group class="sex" v-model="formModel.gender" variant="default-filled">
|
<t-radio-group class="sex" v-model="formData.gender" variant="default-filled">
|
||||||
<t-radio :block="false" borderless value="MALE">男</t-radio>
|
<t-radio :block="false" borderless value="MALE">男</t-radio>
|
||||||
<t-radio :block="false" borderless value="FEMALE">女</t-radio>
|
<t-radio :block="false" borderless value="FEMALE">女</t-radio>
|
||||||
<t-radio :block="false" borderless value="">未知</t-radio>
|
<t-radio :block="false" borderless value="">未知</t-radio>
|
||||||
</t-radio-group>
|
</t-radio-group>
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="手机号">
|
<t-form-item label="手机号">
|
||||||
<t-input v-model.trim="formModel.telephone" type="tel" placeholder="手机号" borderless />
|
<t-input v-model.trim="formData.telephone" type="tel" placeholder="手机号" borderless />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="介绍人">
|
<t-form-item label="介绍人">
|
||||||
<div class="referrer">
|
<div class="referrer">
|
||||||
<t-input
|
<t-input
|
||||||
v-model.trim="formModel.referrer"
|
:value="formData.introducerCustomer?.name"
|
||||||
readonly
|
readonly
|
||||||
placeholder="选择或扫码"
|
placeholder="选择或扫码"
|
||||||
borderless
|
borderless
|
||||||
@@ -84,17 +84,17 @@
|
|||||||
size="extra-small"
|
size="extra-small"
|
||||||
@click="selectorVisible = true"
|
@click="selectorVisible = true"
|
||||||
>选择</t-button>
|
>选择</t-button>
|
||||||
<t-button theme="primary" size="extra-small" @click="toggleScanner('referrer')">
|
<t-button theme="primary" size="extra-small" @click="toggleScanner('introducer')">
|
||||||
<span v-text="scannerVisible && scannerTarget === 'referrer' ? '关闭扫码' : '扫码'" />
|
<span v-text="scannerVisible && scannerTarget === 'introducer' ? '关闭扫码' : '扫码'" />
|
||||||
</t-button>
|
</t-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<div v-if="scannerVisible && scannerTarget === 'referrer'" class="code-scanner-wrapper">
|
<div v-if="scannerVisible && scannerTarget === 'introducer'" class="code-scanner-wrapper">
|
||||||
<code-scanner
|
<code-scanner
|
||||||
visible
|
visible
|
||||||
:status-text="scanStatus"
|
:status-text="scanStatus"
|
||||||
active-text="正在识别介绍人编码。"
|
active-text="正在识别介绍人编码"
|
||||||
@decode="handleDecodedText"
|
@decode="handleDecodedText"
|
||||||
@status-change="handleScanStatusChange"
|
@status-change="handleScanStatusChange"
|
||||||
/>
|
/>
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
<t-form-item label="住址">
|
<t-form-item label="住址">
|
||||||
<t-textarea
|
<t-textarea
|
||||||
class="textarea"
|
class="textarea"
|
||||||
v-model.trim="formModel.address"
|
v-model.trim="formData.address"
|
||||||
:autosize="{ minRows: 3, maxRows: 5 }"
|
:autosize="{ minRows: 3, maxRows: 5 }"
|
||||||
placeholder="住址"
|
placeholder="住址"
|
||||||
/>
|
/>
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
<t-form-item label="病症">
|
<t-form-item label="病症">
|
||||||
<t-textarea
|
<t-textarea
|
||||||
class="textarea"
|
class="textarea"
|
||||||
v-model.trim="formModel.disease"
|
v-model.trim="formData.disease"
|
||||||
:autosize="{ minRows: 3, maxRows: 5 }"
|
:autosize="{ minRows: 3, maxRows: 5 }"
|
||||||
placeholder="病症"
|
placeholder="病症"
|
||||||
/>
|
/>
|
||||||
@@ -122,7 +122,7 @@
|
|||||||
<t-form-item label="备注">
|
<t-form-item label="备注">
|
||||||
<t-textarea
|
<t-textarea
|
||||||
class="textarea"
|
class="textarea"
|
||||||
v-model.trim="formModel.note"
|
v-model.trim="formData.remark"
|
||||||
:autosize="{ minRows: 3, maxRows: 5 }"
|
:autosize="{ minRows: 3, maxRows: 5 }"
|
||||||
placeholder="客户备注"
|
placeholder="客户备注"
|
||||||
/>
|
/>
|
||||||
@@ -139,8 +139,8 @@
|
|||||||
theme="primary"
|
theme="primary"
|
||||||
class="save"
|
class="save"
|
||||||
:disabled="!canSubmit"
|
:disabled="!canSubmit"
|
||||||
:loading="saving"
|
:loading="isSubmitting"
|
||||||
@click="saveCustomer"
|
@click="doSubmit"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<t-icon name="check" />
|
<t-icon name="check" />
|
||||||
@@ -148,9 +148,9 @@
|
|||||||
保存
|
保存
|
||||||
</t-button>
|
</t-button>
|
||||||
<customer-selector
|
<customer-selector
|
||||||
v-model="referrerCustomerId"
|
v-model="introducerCustomerId"
|
||||||
v-model:visible="selectorVisible"
|
v-model:visible="selectorVisible"
|
||||||
@change="handleReferrerChange"
|
@change="handleIntroducerChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -164,8 +164,8 @@ import CodeScanner from "@/components/CodeScanner.vue";
|
|||||||
import CustomerSelector from "@/components/CustomerSelector.vue";
|
import CustomerSelector from "@/components/CustomerSelector.vue";
|
||||||
import PhotoField from "@/components/PhotoField.vue";
|
import PhotoField from "@/components/PhotoField.vue";
|
||||||
import { useSettingStore } from "@/store/settingStore";
|
import { useSettingStore } from "@/store/settingStore";
|
||||||
import type { BodyAreaValue, Customer, CustomerBirthdateCalendar, CustomerGender } from "@/types/Customer";
|
import type { BodyAreaValue, Customer } from "@/types/Customer";
|
||||||
import { createBodyAreaValue, getCustomerPhotoURL, normalizeBodyAreaValue } from "@/utils/customer";
|
import { createBodyAreaValue, normalizeBodyAreaValue } from "@/utils/customer";
|
||||||
import { permissionCodes } from "@/utils/permission";
|
import { permissionCodes } from "@/utils/permission";
|
||||||
import { compressUploadImage } from "@/utils/imageCompression";
|
import { compressUploadImage } from "@/utils/imageCompression";
|
||||||
|
|
||||||
@@ -173,126 +173,27 @@ defineOptions({
|
|||||||
name: "CustomerEdit"
|
name: "CustomerEdit"
|
||||||
});
|
});
|
||||||
|
|
||||||
type ScanTarget = "code" | "referrer";
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
const isCreateMode = computed(() => route.name === "CustomerCreate");
|
const isCreateMode = computed(() => route.name === "CustomerCreate");
|
||||||
const savePermissionCode = computed(() => isCreateMode.value ? permissionCodes.customer.create : permissionCodes.customer.update);
|
const savePermissionCode = computed(() => isCreateMode.value ? permissionCodes.customer.create : permissionCodes.customer.update);
|
||||||
const selectedAreas = ref<BodyAreaValue>(createBodyAreaValue());
|
|
||||||
const selectorVisible = ref(false);
|
|
||||||
const referrerCustomerId = ref("");
|
|
||||||
const photoInputRef = ref<HTMLInputElement>();
|
|
||||||
const photoTempFileId = ref("");
|
|
||||||
const photoUploading = ref(false);
|
|
||||||
const scannerVisible = ref(false);
|
|
||||||
const scannerTarget = ref<ScanTarget>("code");
|
|
||||||
const scanStatus = ref("点击扫码开始识别。");
|
|
||||||
const currentCustomer = ref<Customer>();
|
|
||||||
const saving = ref(false);
|
|
||||||
const canSubmit = computed(() => !!formModel.name.trim());
|
|
||||||
let latestDecodedText = "";
|
let latestDecodedText = "";
|
||||||
|
|
||||||
const formModel = reactive({
|
|
||||||
code: "",
|
|
||||||
name: "",
|
|
||||||
gender: "" as CustomerGender | "",
|
|
||||||
photoURL: "",
|
|
||||||
telephone: "",
|
|
||||||
age: "",
|
|
||||||
birthdate: "",
|
|
||||||
birthdateCalendar: "SOLAR" as CustomerBirthdateCalendar,
|
|
||||||
address: "",
|
|
||||||
referrer: "",
|
|
||||||
disease: "",
|
|
||||||
inviterCustomerId: "",
|
|
||||||
note: ""
|
|
||||||
});
|
|
||||||
|
|
||||||
function resetFormModel(): void {
|
|
||||||
Object.assign(formModel, {
|
|
||||||
code: "",
|
|
||||||
name: "",
|
|
||||||
gender: "",
|
|
||||||
photoURL: "",
|
|
||||||
telephone: "",
|
|
||||||
age: "",
|
|
||||||
birthdate: "",
|
|
||||||
birthdateCalendar: "SOLAR",
|
|
||||||
address: "",
|
|
||||||
referrer: "",
|
|
||||||
disease: "",
|
|
||||||
inviterCustomerId: "",
|
|
||||||
note: ""
|
|
||||||
});
|
|
||||||
currentCustomer.value = undefined;
|
|
||||||
selectedAreas.value = createBodyAreaValue();
|
|
||||||
referrerCustomerId.value = "";
|
|
||||||
photoTempFileId.value = "";
|
|
||||||
scannerVisible.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fillCreateInviter(): Promise<void> {
|
|
||||||
const inviterCode = String(route.query.inviterCode || "");
|
|
||||||
if (!inviterCode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const inviter = await findCustomer(inviterCode);
|
|
||||||
if (!inviter) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
formModel.referrer = inviter.name || inviter.code || "";
|
|
||||||
formModel.inviterCustomerId = inviter.id || "";
|
|
||||||
referrerCustomerId.value = inviter.id || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fillForm(): Promise<void> {
|
|
||||||
resetFormModel();
|
|
||||||
if (isCreateMode.value) {
|
|
||||||
await fillCreateInviter();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = String(route.params.id || "");
|
|
||||||
if (!id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const customer = await CustomerAPI.detail(id);
|
|
||||||
currentCustomer.value = customer;
|
|
||||||
Object.assign(formModel, {
|
|
||||||
code: customer.code || "",
|
|
||||||
name: customer.name || "",
|
|
||||||
gender: customer.gender || "",
|
|
||||||
photoURL: getCustomerPhotoURL(customer),
|
|
||||||
telephone: customer.telephone || "",
|
|
||||||
age: customer.age === undefined || customer.age === null ? "" : String(customer.age),
|
|
||||||
birthdate: customer.birthdate || "",
|
|
||||||
birthdateCalendar: customer.birthdateCalendar || "SOLAR",
|
|
||||||
address: customer.address || "",
|
|
||||||
referrer: customer.introducerCustomer?.name || "",
|
|
||||||
disease: customer.disease || "",
|
|
||||||
inviterCustomerId: customer.introducerCustomerId || "",
|
|
||||||
note: customer.remark || ""
|
|
||||||
});
|
|
||||||
selectedAreas.value = normalizeBodyAreaValue(customer.conditioningPart);
|
|
||||||
referrerCustomerId.value = customer.introducerCustomer?.id || "";
|
|
||||||
} catch (err) {
|
|
||||||
console.warn("客户详情加载失败", err);
|
|
||||||
showToast("客户详情加载失败", "warning");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- 扫码 ----------
|
// ---------- 扫码 ----------
|
||||||
|
|
||||||
|
type ScanTarget = "code" | "introducer";
|
||||||
|
const scannerVisible = ref(false);
|
||||||
|
const scannerTarget = ref<ScanTarget>("code");
|
||||||
|
const scanStatus = ref("点击扫码开始识别");
|
||||||
|
const introducerCustomerId = ref("");
|
||||||
|
|
||||||
async function toggleScanner(target: ScanTarget): Promise<void> {
|
async function toggleScanner(target: ScanTarget): Promise<void> {
|
||||||
if (scannerVisible.value && scannerTarget.value === target) {
|
if (scannerVisible.value && scannerTarget.value === target) {
|
||||||
scannerVisible.value = false;
|
scannerVisible.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
scannerTarget.value = target;
|
scannerTarget.value = target;
|
||||||
scannerVisible.value = true;
|
scannerVisible.value = true;
|
||||||
latestDecodedText = "";
|
latestDecodedText = "";
|
||||||
@@ -303,192 +204,92 @@ async function handleDecodedText(rawText: string): Promise<void> {
|
|||||||
if (!normalizedText || normalizedText === latestDecodedText) {
|
if (!normalizedText || normalizedText === latestDecodedText) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
latestDecodedText = normalizedText;
|
latestDecodedText = normalizedText;
|
||||||
await settingStore.playScanSuccessSound();
|
await settingStore.playScanSuccessSound();
|
||||||
if (scannerTarget.value === "code") {
|
if (scannerTarget.value === "code") {
|
||||||
formModel.code = normalizedText;
|
// 选择客户扫码
|
||||||
scanStatus.value = `已识别客户编码 ${normalizedText}。`;
|
formData.code = normalizedText;
|
||||||
|
scanStatus.value = `已识别客户编码 ${normalizedText}`;
|
||||||
scannerVisible.value = false;
|
scannerVisible.value = false;
|
||||||
return;
|
return;
|
||||||
|
} else {
|
||||||
|
// 选择介绍人扫码
|
||||||
|
await applyIntroducer(normalizedText);
|
||||||
}
|
}
|
||||||
|
|
||||||
await applyReferrer(normalizedText);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleScanStatusChange(value: string): void {
|
function handleScanStatusChange(value: string): void {
|
||||||
scanStatus.value = value;
|
scanStatus.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 照片 ----------
|
|
||||||
|
|
||||||
function selectPhoto(): void {
|
|
||||||
photoInputRef.value?.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetPhotoState(): void {
|
|
||||||
photoTempFileId.value = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handlePhotoSelected(event: Event): Promise<void> {
|
|
||||||
const input = event.target as HTMLInputElement;
|
|
||||||
const file = input.files?.[0];
|
|
||||||
if (!file) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
photoUploading.value = true;
|
|
||||||
try {
|
|
||||||
const compressedFile = await compressUploadImage(file);
|
|
||||||
const [tempFile] = await AttachmentAPI.uploadTemp(compressedFile);
|
|
||||||
if (!tempFile?.id) {
|
|
||||||
showToast("客户照片上传失败", "warning");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
photoTempFileId.value = tempFile.id;
|
|
||||||
formModel.photoURL = AttachmentAPI.getTempReadURL(tempFile.id);
|
|
||||||
} finally {
|
|
||||||
photoUploading.value = false;
|
|
||||||
input.value = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- 介绍人 ----------
|
// ---------- 介绍人 ----------
|
||||||
|
|
||||||
async function findCustomer(rawValue: string): Promise<Customer | undefined> {
|
const selectorVisible = ref(false);
|
||||||
const normalizedText = rawValue.trim();
|
|
||||||
if (!normalizedText) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const customer = await CustomerAPI.nullableDetailByCode(normalizedText);
|
async function applyIntroducer(code: string): Promise<void> {
|
||||||
if (customer) {
|
if (!code) {
|
||||||
return customer;
|
formData.introducerCustomerId = "";
|
||||||
}
|
introducerCustomerId.value = "";
|
||||||
|
|
||||||
const list = await CustomerAPI.search({ keyword: normalizedText });
|
|
||||||
return list[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function applyReferrer(rawValue: string): Promise<void> {
|
|
||||||
const normalizedText = rawValue.trim();
|
|
||||||
if (!normalizedText) {
|
|
||||||
formModel.referrer = "";
|
|
||||||
formModel.inviterCustomerId = "";
|
|
||||||
referrerCustomerId.value = "";
|
|
||||||
scannerVisible.value = false;
|
scannerVisible.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const matched = await CustomerAPI.findByCode(code);
|
||||||
const matched = await findCustomer(normalizedText);
|
|
||||||
if (!matched?.id) {
|
if (!matched?.id) {
|
||||||
formModel.referrer = "";
|
formData.introducerCustomerId = "";
|
||||||
formModel.inviterCustomerId = "";
|
introducerCustomerId.value = "";
|
||||||
referrerCustomerId.value = "";
|
|
||||||
scanStatus.value = `未匹配到介绍人 ${normalizedText},请重新选择或扫码。`;
|
|
||||||
scannerVisible.value = false;
|
scannerVisible.value = false;
|
||||||
showToast("介绍人不存在,请选择或扫码已有客户", "warning");
|
scannerVisible.value = false;
|
||||||
|
Toast.warning(`未匹配到 ${code} 编码介绍人,请选择或扫码已有客户`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
formData.introducerCustomerId = matched.id;
|
||||||
formModel.referrer = matched.name || matched.code || "";
|
formData.introducerCustomer = matched;
|
||||||
formModel.inviterCustomerId = matched.id;
|
introducerCustomerId.value = matched.id;
|
||||||
referrerCustomerId.value = matched.id;
|
|
||||||
scanStatus.value = `已匹配介绍人 ${formModel.referrer}。`;
|
|
||||||
scannerVisible.value = false;
|
scannerVisible.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleReferrerChange(id: string): Promise<void> {
|
async function handleIntroducerChange(id: string): Promise<void> {
|
||||||
const customer = await CustomerAPI.detail(id);
|
const customer = await CustomerAPI.findById(id);
|
||||||
formModel.referrer = customer.name || customer.code || "";
|
introducerCustomerId.value = customer!.id!;
|
||||||
formModel.inviterCustomerId = customer.id || "";
|
formData.introducerCustomer = customer;
|
||||||
referrerCustomerId.value = customer.id || "";
|
|
||||||
scanStatus.value = `已选择介绍人 ${formModel.referrer}。`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 保存 ----------
|
// ---------- 表单 ----------
|
||||||
|
|
||||||
async function saveCustomer(): Promise<void> {
|
const photoInputRef = ref<HTMLInputElement>();
|
||||||
if (!validateForm()) {
|
const photoTempFileId = ref("");
|
||||||
|
const isPhotoUploading = ref(false);
|
||||||
|
const photoURL = ref();
|
||||||
|
const selectedAreas = ref<BodyAreaValue>(createBodyAreaValue());
|
||||||
|
const formData = reactive<Customer>({
|
||||||
|
});
|
||||||
|
|
||||||
|
const canSubmit = computed(() => formData.name?.trim());
|
||||||
|
|
||||||
|
async function loadDetail() {
|
||||||
|
const id = String(route.params.id || "");
|
||||||
|
if (!id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const customerCode = formModel.code.trim();
|
|
||||||
const payload: Customer = {
|
|
||||||
...(currentCustomer.value || {}),
|
|
||||||
code: customerCode || null,
|
|
||||||
name: formModel.name,
|
|
||||||
gender: formModel.gender || undefined,
|
|
||||||
age: Number(formModel.age) || undefined,
|
|
||||||
birthdate: formModel.birthdate || null,
|
|
||||||
birthdateCalendar: formModel.birthdate ? formModel.birthdateCalendar : undefined,
|
|
||||||
telephone: formModel.telephone,
|
|
||||||
address: formModel.address,
|
|
||||||
remark: formModel.note,
|
|
||||||
disease: formModel.disease,
|
|
||||||
conditioningPart: selectedAreas.value,
|
|
||||||
introducerCustomerId: formModel.inviterCustomerId || null,
|
|
||||||
attachmentIdList: photoTempFileId.value ? [photoTempFileId.value] : undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
saving.value = true;
|
|
||||||
try {
|
try {
|
||||||
let savedCustomer = currentCustomer.value;
|
const customer = await CustomerAPI.detail(id);
|
||||||
if (isCreateMode.value) {
|
Object.assign(formData, customer);
|
||||||
savedCustomer = await CustomerAPI.create(payload);
|
selectedAreas.value = normalizeBodyAreaValue(customer.conditioningPart);
|
||||||
} else {
|
introducerCustomerId.value = customer.introducerCustomer?.id || "";
|
||||||
await CustomerAPI.update(payload);
|
} catch (err) {
|
||||||
}
|
console.warn("客户详情加载失败", err);
|
||||||
showToast("客户已保存");
|
Toast.error("客户详情加载失败");
|
||||||
const redirect = String(route.query.redirect || "");
|
|
||||||
if (redirect) {
|
|
||||||
await router.replace(redirect);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (savedCustomer?.id) {
|
|
||||||
await router.replace(`/management/customers/${savedCustomer.id}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await router.replace("/management/customers");
|
|
||||||
} finally {
|
|
||||||
saving.value = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onMounted(loadDetail);
|
||||||
|
watch(() => route.fullPath, loadDetail);
|
||||||
|
|
||||||
function validateForm(): boolean {
|
// ---------- 年龄 ----------
|
||||||
if (!formModel.name.trim()) {
|
|
||||||
showToast("请填写客户姓名", "warning");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
watch(() => formData.birthdate, (value) => {
|
||||||
}
|
|
||||||
|
|
||||||
function showToast(message: string, theme: "success" | "warning" = "success"): void {
|
|
||||||
Toast({
|
|
||||||
theme,
|
|
||||||
direction: "row",
|
|
||||||
message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(() => formModel.referrer, (value) => {
|
|
||||||
if (!value.trim()) {
|
|
||||||
formModel.inviterCustomerId = "";
|
|
||||||
referrerCustomerId.value = "";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(() => formModel.birthdate, (value) => {
|
|
||||||
if (value) {
|
if (value) {
|
||||||
formModel.age = String(calculateAge(value));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function calculateAge(value: string): number {
|
|
||||||
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
||||||
if (!match) {
|
if (match) {
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const birthYear = Number(match[1]);
|
const birthYear = Number(match[1]);
|
||||||
const birthMonth = Number(match[2]);
|
const birthMonth = Number(match[2]);
|
||||||
const birthDay = Number(match[3]);
|
const birthDay = Number(match[3]);
|
||||||
@@ -497,16 +298,63 @@ function calculateAge(value: string): number {
|
|||||||
if (today.getMonth() + 1 < birthMonth || (today.getMonth() + 1 === birthMonth && today.getDate() < birthDay)) {
|
if (today.getMonth() + 1 < birthMonth || (today.getMonth() + 1 === birthMonth && today.getDate() < birthDay)) {
|
||||||
age--;
|
age--;
|
||||||
}
|
}
|
||||||
return Math.max(age, 0);
|
formData.age = Math.max(age, 0);
|
||||||
|
} else {
|
||||||
|
formData.age = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------- 照片 ----------
|
||||||
|
|
||||||
|
async function handlePhotoSelected(event: Event): Promise<void> {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
const file = input.files?.[0];
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isPhotoUploading.value = true;
|
||||||
|
try {
|
||||||
|
const compressedFile = await compressUploadImage(file);
|
||||||
|
const [tempFile] = await AttachmentAPI.upload(compressedFile);
|
||||||
|
photoTempFileId.value = tempFile.id;
|
||||||
|
photoURL.value = AttachmentAPI.getTempReadURL(tempFile.id);
|
||||||
|
} finally {
|
||||||
|
isPhotoUploading.value = false;
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
// ---------- 保存 ----------
|
||||||
await fillForm();
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(() => route.fullPath, async () => {
|
const isSubmitting = ref(false);
|
||||||
await fillForm();
|
|
||||||
});
|
async function doSubmit(): Promise<void> {
|
||||||
|
isSubmitting.value = true;
|
||||||
|
try {
|
||||||
|
formData.conditioningPart = selectedAreas.value;
|
||||||
|
formData.attachmentList = photoTempFileId.value ? [{
|
||||||
|
tempFileId: photoTempFileId.value
|
||||||
|
}] : undefined;
|
||||||
|
|
||||||
|
if (isCreateMode.value) {
|
||||||
|
const id = await CustomerAPI.create(formData);
|
||||||
|
Toast.success("创建成功");
|
||||||
|
await router.replace(`/management/customers/${id}`);
|
||||||
|
} else {
|
||||||
|
await CustomerAPI.update(formData);
|
||||||
|
Toast.success("保存成功");
|
||||||
|
const redirect = String(route.query.redirect || "");
|
||||||
|
if (redirect) {
|
||||||
|
await router.replace(redirect);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await router.replace("/management/customers");
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="pageRef" v-can="permissionCodes.customer.read" class="customer-list">
|
<div v-can="permissionCodes.customer.read" class="customer-list">
|
||||||
<div class="search-bar">
|
<div class="search-bar">
|
||||||
<t-input
|
<t-input
|
||||||
v-model.trim="keyword"
|
v-model.trim="keyword"
|
||||||
@@ -11,14 +11,21 @@
|
|||||||
</div>
|
</div>
|
||||||
<section class="total">
|
<section class="total">
|
||||||
<span class="gray">客户总数:</span>
|
<span class="gray">客户总数:</span>
|
||||||
<span class="bold" v-text="String(totalCount)" />
|
<span class="bold" v-text="pageResult.total" />
|
||||||
</section>
|
</section>
|
||||||
|
<t-pull-down-refresh
|
||||||
|
v-model="isRefreshing"
|
||||||
|
class="list-refresh"
|
||||||
|
:refresh-timeout="30000"
|
||||||
|
@refresh="refresh"
|
||||||
|
@scrolltolower="fetch"
|
||||||
|
>
|
||||||
<section class="items">
|
<section class="items">
|
||||||
<div
|
<div
|
||||||
class="item"
|
class="item"
|
||||||
v-for="(item, index) in customerList"
|
v-for="(item, index) in pageResult.list"
|
||||||
:key="item.id || item.code || index"
|
:key="item.id || item.code || index"
|
||||||
@click="goDetail(item)"
|
@click="router.push(`/management/customers/${item.id}`)"
|
||||||
>
|
>
|
||||||
<t-image v-if="getCustomerPhotoURL(item)" :src="getCustomerPhotoURL(item)" :alt="item.name" fit="cover" class="avatar" />
|
<t-image v-if="getCustomerPhotoURL(item)" :src="getCustomerPhotoURL(item)" :alt="item.name" fit="cover" class="avatar" />
|
||||||
<div v-else class="avatar fallback" v-text="(item.name || item.code || '客').slice(0, 1)" />
|
<div v-else class="avatar fallback" v-text="(item.name || item.code || '客').slice(0, 1)" />
|
||||||
@@ -46,18 +53,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!customerList.length" class="empty-block" v-text="loading ? '正在加载客户' : '暂无客户数据'" />
|
<div v-if="!pageResult.list.length" class="empty-block" v-text="isLoading ? '正在加载客户' : '暂无客户数据'" />
|
||||||
<div v-if="customerList.length" class="load-state" v-text="loading ? '正在加载更多' : noMore ? '已经到底了' : '上拉加载更多'" />
|
<div v-if="pageResult.list.length" class="load-state" v-text="isLoading ? '正在加载更多' : isNoMore ? '已经到底了' : '上拉加载更多'" />
|
||||||
</section>
|
</section>
|
||||||
|
</t-pull-down-refresh>
|
||||||
<div class="bottom-actions">
|
<div class="bottom-actions">
|
||||||
<t-button
|
<t-button
|
||||||
v-can="permissionCodes.customer.export"
|
v-can="permissionCodes.customer.export"
|
||||||
theme="primary"
|
theme="primary"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="export bg-white"
|
class="export bg-white"
|
||||||
:loading="exporting"
|
:loading="isExporting"
|
||||||
:disabled="exporting"
|
:disabled="isExporting"
|
||||||
@click="exportCustomers"
|
@click="doExport"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<t-icon name="download" />
|
<t-icon name="download" />
|
||||||
@@ -81,10 +89,10 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Toast } from "tdesign-mobile-vue";
|
import { Toast } from "tdesign-mobile-vue";
|
||||||
import { Scroller, Time, Toolkit } from "timi-web";
|
import { type Page, type PageResult, Time, Toolkit } from "timi-web";
|
||||||
import CustomerAPI from "@/api/CustomerAPI";
|
import CustomerAPI from "@/api/CustomerAPI";
|
||||||
import GenderIcon from "@/components/GenderIcon.vue";
|
import GenderIcon from "@/components/GenderIcon.vue";
|
||||||
import type { Customer, CustomerSearchReq } from "@/types/Customer";
|
import type { Customer } from "@/types/Customer";
|
||||||
import { getCustomerPhotoURL } from "@/utils/customer";
|
import { getCustomerPhotoURL } from "@/utils/customer";
|
||||||
import { permissionCodes } from "@/utils/permission";
|
import { permissionCodes } from "@/utils/permission";
|
||||||
|
|
||||||
@@ -92,113 +100,69 @@ defineOptions({
|
|||||||
name: "CustomerList"
|
name: "CustomerList"
|
||||||
});
|
});
|
||||||
|
|
||||||
const scrollListenerName = "CustomerList";
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pageSize = 20;
|
const isRefreshing = ref(false);
|
||||||
const keyword = ref("");
|
const isExporting = ref(false);
|
||||||
const loading = ref(false);
|
|
||||||
const exporting = ref(false);
|
|
||||||
const noMore = ref(false);
|
|
||||||
const pageIndex = ref(0);
|
|
||||||
const totalCount = ref(0);
|
|
||||||
const customerList = ref<Customer[]>([]);
|
|
||||||
const pageRef = ref<HTMLElement>();
|
|
||||||
let loadToken = 0;
|
|
||||||
let keywordTimer: number | undefined;
|
|
||||||
|
|
||||||
function getPageScrollElement(): HTMLElement | null {
|
// ---------- 分页 ----------
|
||||||
return pageRef.value?.closest(".pages") || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function reloadCustomers(): Promise<void> {
|
const keyword = ref();
|
||||||
pageIndex.value = 0;
|
const isLoading = ref(false);
|
||||||
totalCount.value = 0;
|
const isNoMore = ref(false);
|
||||||
customerList.value = [];
|
const pageResult = reactive<PageResult<Customer>>({
|
||||||
noMore.value = false;
|
total: 0,
|
||||||
await loadCustomers();
|
list: []
|
||||||
}
|
});
|
||||||
|
const page = reactive<Page<Customer>>({
|
||||||
|
index: 0,
|
||||||
|
size: 15
|
||||||
|
});
|
||||||
|
|
||||||
async function loadCustomers(): Promise<void> {
|
const fetch = Toolkit.debounce(async () => {
|
||||||
if (loading.value || noMore.value) {
|
const result = await CustomerAPI.list({
|
||||||
return;
|
...page,
|
||||||
|
likesExample: {
|
||||||
|
code: keyword.value,
|
||||||
|
name: keyword.value,
|
||||||
|
telephone: keyword.value,
|
||||||
}
|
}
|
||||||
const token = loadToken + 1;
|
|
||||||
loadToken = token;
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
const result = await CustomerAPI.searchPage({
|
|
||||||
index: pageIndex.value,
|
|
||||||
size: pageSize,
|
|
||||||
equalsExample: buildSearchReq()
|
|
||||||
});
|
});
|
||||||
if (token !== loadToken) {
|
pageResult.total = result.total;
|
||||||
return;
|
pageResult.list = pageResult.list.concat(result.list);
|
||||||
}
|
page.index += 1;
|
||||||
customerList.value = customerList.value.concat(result.list || []);
|
isNoMore.value = (result.list || []).length < page.size || pageResult.total <= pageResult.list.length;
|
||||||
totalCount.value = result.total || 0;
|
});
|
||||||
pageIndex.value += 1;
|
|
||||||
noMore.value = customerList.value.length >= totalCount.value || (result.list || []).length < pageSize;
|
async function refresh(): Promise<void> {
|
||||||
|
isRefreshing.value = true;
|
||||||
|
try {
|
||||||
|
page.index = 0;
|
||||||
|
page.size = 15;
|
||||||
|
pageResult.total = 0;
|
||||||
|
pageResult.list = [];
|
||||||
|
fetch();
|
||||||
} finally {
|
} finally {
|
||||||
if (token === loadToken) {
|
isRefreshing.value = false;
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exportCustomers(): Promise<void> {
|
onMounted(fetch);
|
||||||
if (exporting.value) {
|
|
||||||
|
// ---------- 导出 ----------
|
||||||
|
|
||||||
|
async function doExport(): Promise<void> {
|
||||||
|
if (isExporting.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
exporting.value = true;
|
isExporting.value = true;
|
||||||
try {
|
try {
|
||||||
const blob = await CustomerAPI.exportExcel(buildSearchReq());
|
const blob = await CustomerAPI.exportXSSF(page);
|
||||||
Toolkit.downloadBlob(blob, `客户列表-${Time.formatDate(new Date())}.xlsx`);
|
Toolkit.downloadBlob(blob, `客户列表-${Time.formatDate(new Date())}.xlsx`);
|
||||||
showToast("导出已开始");
|
Toast.success("导出已开始");
|
||||||
} finally {
|
} finally {
|
||||||
exporting.value = false;
|
isExporting.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function goDetail(customer: Customer): Promise<void> {
|
|
||||||
if (!customer.id) {
|
|
||||||
showToast("客户 ID 缺失,无法打开详情", "warning");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await router.push(`/management/customers/${customer.id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildSearchReq(): CustomerSearchReq {
|
|
||||||
return {
|
|
||||||
keyword: keyword.value || undefined
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function showToast(message: string, theme: "success" | "warning" = "success"): void {
|
|
||||||
Toast({
|
|
||||||
message,
|
|
||||||
theme
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(keyword, async () => {
|
|
||||||
window.clearTimeout(keywordTimer);
|
|
||||||
keywordTimer = window.setTimeout(async () => {
|
|
||||||
await reloadCustomers();
|
|
||||||
}, 300);
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await reloadCustomers();
|
|
||||||
Scroller.addBottomListener(scrollListenerName, loadCustomers, {
|
|
||||||
canLoad: () => !loading.value && !noMore.value,
|
|
||||||
getElement: getPageScrollElement
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.clearTimeout(keywordTimer);
|
|
||||||
Scroller.removeListener(scrollListenerName);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
|
|||||||
@@ -95,8 +95,8 @@
|
|||||||
class="delete bg-white"
|
class="delete bg-white"
|
||||||
theme="danger"
|
theme="danger"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
:loading="removing"
|
:loading="isRemoving"
|
||||||
:disabled="saving || removing"
|
:disabled="isSaving || isRemoving"
|
||||||
@click="confirmRemoveEventItem"
|
@click="confirmRemoveEventItem"
|
||||||
>
|
>
|
||||||
删除事件
|
删除事件
|
||||||
@@ -105,8 +105,8 @@
|
|||||||
v-can="savePermissionCode"
|
v-can="savePermissionCode"
|
||||||
class="save"
|
class="save"
|
||||||
theme="primary"
|
theme="primary"
|
||||||
:loading="saving"
|
:loading="isSaving"
|
||||||
:disabled="removing"
|
:disabled="isRemoving"
|
||||||
@click="saveEventItem"
|
@click="saveEventItem"
|
||||||
>
|
>
|
||||||
保存
|
保存
|
||||||
@@ -201,6 +201,7 @@ defineOptions({
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const routeId = computed(() => String(route.params.id || ""));
|
const routeId = computed(() => String(route.params.id || ""));
|
||||||
|
|
||||||
const isCreateMode = computed(() => !routeId.value);
|
const isCreateMode = computed(() => !routeId.value);
|
||||||
const savePermissionCode = computed(() => isCreateMode.value ? permissionCodes.event.create : permissionCodes.event.update);
|
const savePermissionCode = computed(() => isCreateMode.value ? permissionCodes.event.create : permissionCodes.event.update);
|
||||||
const deletePermissionCode = permissionCodes.event.delete;
|
const deletePermissionCode = permissionCodes.event.delete;
|
||||||
@@ -208,44 +209,12 @@ const pageAccess = computed(() => ({
|
|||||||
value: [permissionCodes.event.read, permissionCodes.user.read, savePermissionCode.value],
|
value: [permissionCodes.event.read, permissionCodes.user.read, savePermissionCode.value],
|
||||||
mode: "and" as const
|
mode: "and" as const
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// ---------- 基础表单 ----------
|
||||||
|
|
||||||
const roleOptions = ref<Role[]>([]);
|
const roleOptions = ref<Role[]>([]);
|
||||||
const colorPickerVisible = ref(false);
|
const isSaving = ref(false);
|
||||||
const colorPickerRenderKey = ref(0);
|
const isRemoving = ref(false);
|
||||||
const colorDraft = ref("#2f88ff");
|
|
||||||
const beginAtPickerVisible = ref(false);
|
|
||||||
const endAtPickerVisible = ref(false);
|
|
||||||
const beginAtDraftText = ref("");
|
|
||||||
const endAtDraftText = ref("");
|
|
||||||
const saving = ref(false);
|
|
||||||
const removing = ref(false);
|
|
||||||
const colorSwatchList = [
|
|
||||||
"#2F88FF",
|
|
||||||
"#0EA5E9",
|
|
||||||
"#10B981",
|
|
||||||
"#22C55E",
|
|
||||||
"#F59E0B",
|
|
||||||
"#F97316",
|
|
||||||
"#EF4444",
|
|
||||||
"#EC4899",
|
|
||||||
"#8B5CF6",
|
|
||||||
"#64748B"
|
|
||||||
];
|
|
||||||
const limitOptions: LimitOption[] = [
|
|
||||||
{ label: "不限", value: "NONE" },
|
|
||||||
{ label: "上午和下午各一次", value: "DAILY_HALF_DAY" },
|
|
||||||
{ label: "间隔小时内一次", value: "INTERVAL_HOUR" },
|
|
||||||
{ label: "每个自然日一次", value: "NATURAL_DAY" },
|
|
||||||
{ label: "每个自然月一次", value: "NATURAL_MONTH" },
|
|
||||||
{ label: "事件总次数", value: "TOTAL_LIMIT" },
|
|
||||||
{ label: "单客户总次数", value: "CUSTOMER_TOTAL_LIMIT" }
|
|
||||||
];
|
|
||||||
const statusOptions: Array<{ label: string; value: EventStatus }> = [
|
|
||||||
{ label: "草稿", value: "DRAFT" },
|
|
||||||
{ label: "可登记", value: "ACTIVE" },
|
|
||||||
{ label: "不可登记", value: "INACTIVE" }
|
|
||||||
];
|
|
||||||
const limitPickerColumns = [limitOptions];
|
|
||||||
const statusPickerColumns = [statusOptions];
|
|
||||||
const formModel = reactive({
|
const formModel = reactive({
|
||||||
id: "",
|
id: "",
|
||||||
name: "",
|
name: "",
|
||||||
@@ -259,10 +228,57 @@ const formModel = reactive({
|
|||||||
endAtText: "",
|
endAtText: "",
|
||||||
roleCodeList: [] as string[]
|
roleCodeList: [] as string[]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------- 颜色 ----------
|
||||||
|
|
||||||
|
const colorPickerVisible = ref(false);
|
||||||
|
const colorPickerRenderKey = ref(0);
|
||||||
|
const colorDraft = ref("#2f88ff");
|
||||||
|
const colorSwatchList = [
|
||||||
|
"#2F88FF",
|
||||||
|
"#0EA5E9",
|
||||||
|
"#10B981",
|
||||||
|
"#22C55E",
|
||||||
|
"#F59E0B",
|
||||||
|
"#F97316",
|
||||||
|
"#EF4444",
|
||||||
|
"#EC4899",
|
||||||
|
"#8B5CF6",
|
||||||
|
"#64748B"
|
||||||
|
];
|
||||||
|
|
||||||
|
// ---------- 次数限制 ----------
|
||||||
|
|
||||||
|
const limitOptions: LimitOption[] = [
|
||||||
|
{ label: "不限", value: "NONE" },
|
||||||
|
{ label: "上午和下午各一次", value: "DAILY_HALF_DAY" },
|
||||||
|
{ label: "间隔小时内一次", value: "INTERVAL_HOUR" },
|
||||||
|
{ label: "每个自然日一次", value: "NATURAL_DAY" },
|
||||||
|
{ label: "每个自然月一次", value: "NATURAL_MONTH" },
|
||||||
|
{ label: "事件总次数", value: "TOTAL_LIMIT" },
|
||||||
|
{ label: "单客户总次数", value: "CUSTOMER_TOTAL_LIMIT" }
|
||||||
|
];
|
||||||
|
const limitPickerColumns = [limitOptions];
|
||||||
const limitPickerVisible = ref(false);
|
const limitPickerVisible = ref(false);
|
||||||
const limitPickerDraft = ref<string[]>([formModel.limitType]);
|
const limitPickerDraft = ref<string[]>([formModel.limitType]);
|
||||||
|
|
||||||
|
// ---------- 状态 ----------
|
||||||
|
|
||||||
|
const statusOptions: Array<{ label: string; value: EventStatus }> = [
|
||||||
|
{ label: "草稿", value: "DRAFT" },
|
||||||
|
{ label: "可登记", value: "ACTIVE" },
|
||||||
|
{ label: "不可登记", value: "INACTIVE" }
|
||||||
|
];
|
||||||
|
const statusPickerColumns = [statusOptions];
|
||||||
const statusPickerVisible = ref(false);
|
const statusPickerVisible = ref(false);
|
||||||
const statusPickerDraft = ref<EventStatus[]>([formModel.status]);
|
const statusPickerDraft = ref<EventStatus[]>([formModel.status]);
|
||||||
|
|
||||||
|
// ---------- 生效时间 ----------
|
||||||
|
|
||||||
|
const beginAtPickerVisible = ref(false);
|
||||||
|
const endAtPickerVisible = ref(false);
|
||||||
|
const beginAtDraftText = ref("");
|
||||||
|
const endAtDraftText = ref("");
|
||||||
const minimumDateTimeText = "1970-01-01 00:00";
|
const minimumDateTimeText = "1970-01-01 00:00";
|
||||||
const nowDateTimeText = computed(() => Time.formatDateTime(new Date()));
|
const nowDateTimeText = computed(() => Time.formatDateTime(new Date()));
|
||||||
const endAtMinimumDateTimeText = computed(() => formModel.beginAtText || minimumDateTimeText);
|
const endAtMinimumDateTimeText = computed(() => formModel.beginAtText || minimumDateTimeText);
|
||||||
@@ -281,6 +297,8 @@ const limitValuePlaceholder = computed(() => {
|
|||||||
return "请输入总次数上限";
|
return "请输入总次数上限";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------- 颜色 ----------
|
||||||
|
|
||||||
function openColorPicker(): void {
|
function openColorPicker(): void {
|
||||||
colorDraft.value = formModel.color;
|
colorDraft.value = formModel.color;
|
||||||
colorPickerVisible.value = true;
|
colorPickerVisible.value = true;
|
||||||
@@ -304,6 +322,8 @@ function updateColorDraftFromPalette(context: { color: ColorObject }): void {
|
|||||||
colorDraft.value = context.color.hex8;
|
colorDraft.value = context.color.hex8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 选项文案 ----------
|
||||||
|
|
||||||
function getLimitText(value: EventLimitType): string {
|
function getLimitText(value: EventLimitType): string {
|
||||||
return limitOptions.find(option => option.value === value)?.label || "不限";
|
return limitOptions.find(option => option.value === value)?.label || "不限";
|
||||||
}
|
}
|
||||||
@@ -312,6 +332,8 @@ function getStatusText(value: EventStatus): string {
|
|||||||
return statusOptions.find(option => option.value === value)?.label || "可登记";
|
return statusOptions.find(option => option.value === value)?.label || "可登记";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 次数限制 ----------
|
||||||
|
|
||||||
function openLimitPicker(): void {
|
function openLimitPicker(): void {
|
||||||
limitPickerDraft.value = [formModel.limitType];
|
limitPickerDraft.value = [formModel.limitType];
|
||||||
limitPickerVisible.value = true;
|
limitPickerVisible.value = true;
|
||||||
@@ -325,6 +347,8 @@ function confirmLimitPicker(value: Array<string | number>): void {
|
|||||||
limitPickerVisible.value = false;
|
limitPickerVisible.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 状态 ----------
|
||||||
|
|
||||||
function openStatusPicker(): void {
|
function openStatusPicker(): void {
|
||||||
statusPickerDraft.value = [formModel.status];
|
statusPickerDraft.value = [formModel.status];
|
||||||
statusPickerVisible.value = true;
|
statusPickerVisible.value = true;
|
||||||
@@ -351,6 +375,8 @@ function resolveLimitValue(): number | undefined {
|
|||||||
return 0 < value ? Math.floor(value) : undefined;
|
return 0 < value ? Math.floor(value) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 表单转换 ----------
|
||||||
|
|
||||||
function resolveTimestamp(valueText: string): number | undefined {
|
function resolveTimestamp(valueText: string): number | undefined {
|
||||||
if (!valueText.trim()) {
|
if (!valueText.trim()) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -363,41 +389,27 @@ function resolveTimestamp(valueText: string): number | undefined {
|
|||||||
|
|
||||||
function validateForm(): boolean {
|
function validateForm(): boolean {
|
||||||
if (!formModel.name.trim()) {
|
if (!formModel.name.trim()) {
|
||||||
showToast("请输入登记事件名称", "warning");
|
Toast.warning("请输入登记事件名称");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!formModel.roleCodeList.length) {
|
if (!formModel.roleCodeList.length) {
|
||||||
showToast("请至少选择一个可登记角色", "warning");
|
Toast.warning("请至少选择一个可登记角色");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (isValueLimitType(formModel.limitType) && !resolveLimitValue()) {
|
if (isValueLimitType(formModel.limitType) && !resolveLimitValue()) {
|
||||||
showToast("请输入有效的约束数值", "warning");
|
Toast.warning("请输入有效的约束数值");
|
||||||
return false;
|
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) {
|
||||||
showToast("终止时间不能早于起始时间", "warning");
|
Toast.warning("终止时间不能早于起始时间");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildPayload(): EventItem {
|
// ---------- 生效时间 ----------
|
||||||
return {
|
|
||||||
id: formModel.id || undefined,
|
|
||||||
name: formModel.name.trim(),
|
|
||||||
description: formModel.description,
|
|
||||||
color: formModel.color,
|
|
||||||
status: formModel.status,
|
|
||||||
requirePhoto: formModel.requirePhoto,
|
|
||||||
limitType: formModel.limitType,
|
|
||||||
limitValue: resolveLimitValue(),
|
|
||||||
beginAt: resolveTimestamp(formModel.beginAtText),
|
|
||||||
endAt: resolveTimestamp(formModel.endAtText),
|
|
||||||
roleCodeList: [...formModel.roleCodeList]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function openBeginAtPicker(): void {
|
function openBeginAtPicker(): void {
|
||||||
beginAtDraftText.value = formModel.beginAtText || nowDateTimeText.value;
|
beginAtDraftText.value = formModel.beginAtText || nowDateTimeText.value;
|
||||||
@@ -431,34 +443,48 @@ function clearEndAt(): void {
|
|||||||
endAtPickerVisible.value = false;
|
endAtPickerVisible.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 保存 ----------
|
||||||
|
|
||||||
async function saveEventItem(): Promise<void> {
|
async function saveEventItem(): Promise<void> {
|
||||||
if (!validateForm()) {
|
if (!validateForm()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
isSaving.value = true;
|
||||||
saving.value = true;
|
|
||||||
try {
|
try {
|
||||||
const payload = buildPayload();
|
const payload: EventItem = {
|
||||||
|
id: formModel.id || undefined,
|
||||||
|
name: formModel.name.trim(),
|
||||||
|
description: formModel.description,
|
||||||
|
color: formModel.color,
|
||||||
|
status: formModel.status,
|
||||||
|
requirePhoto: formModel.requirePhoto,
|
||||||
|
limitType: formModel.limitType,
|
||||||
|
limitValue: resolveLimitValue(),
|
||||||
|
beginAt: resolveTimestamp(formModel.beginAtText),
|
||||||
|
endAt: resolveTimestamp(formModel.endAtText),
|
||||||
|
roleCodeList: [...formModel.roleCodeList]
|
||||||
|
};
|
||||||
if (isCreateMode.value) {
|
if (isCreateMode.value) {
|
||||||
await EventAPI.create(payload);
|
await EventAPI.create(payload);
|
||||||
} else {
|
} else {
|
||||||
await EventAPI.update(payload);
|
await EventAPI.update(payload);
|
||||||
}
|
}
|
||||||
showToast("登记事件已保存");
|
Toast.success("登记事件已保存");
|
||||||
await router.replace("/management/registration-events");
|
await router.replace("/management/registration-events");
|
||||||
} catch {
|
} catch {
|
||||||
showToast("登记事件保存失败", "error");
|
Toast.error("登记事件保存失败");
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false;
|
isSaving.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 删除 ----------
|
||||||
|
|
||||||
async function confirmRemoveEventItem(): Promise<void> {
|
async function confirmRemoveEventItem(): Promise<void> {
|
||||||
const id = formModel.id || routeId.value;
|
const id = formModel.id || routeId.value;
|
||||||
if (!id || removing.value) {
|
if (!id || isRemoving.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventName = formModel.name.trim() || "未命名事件";
|
const eventName = formModel.name.trim() || "未命名事件";
|
||||||
const dialog = DialogPlugin.confirm({
|
const dialog = DialogPlugin.confirm({
|
||||||
title: "删除登记事件",
|
title: "删除登记事件",
|
||||||
@@ -476,36 +502,28 @@ async function confirmRemoveEventItem(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function removeEventItem(id: string): Promise<void> {
|
async function removeEventItem(id: string): Promise<void> {
|
||||||
if (removing.value) {
|
if (isRemoving.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
isRemoving.value = true;
|
||||||
removing.value = true;
|
|
||||||
try {
|
try {
|
||||||
await EventAPI.remove(id);
|
await EventAPI.remove(id);
|
||||||
showToast("登记事件已删除");
|
Toast.success("登记事件已删除");
|
||||||
await router.replace("/management/registration-events");
|
await router.replace("/management/registration-events");
|
||||||
} catch {
|
} catch {
|
||||||
showToast("登记事件删除失败", "error");
|
Toast.error("登记事件删除失败");
|
||||||
} finally {
|
} finally {
|
||||||
removing.value = false;
|
isRemoving.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showToast(message: string, theme: "success" | "error" | "warning" = "success"): void {
|
// ---------- 页面数据 ----------
|
||||||
Toast({
|
|
||||||
theme,
|
|
||||||
direction: "row",
|
|
||||||
message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fillForm(): Promise<void> {
|
async function fillForm(): Promise<void> {
|
||||||
roleOptions.value = await ManagedUserAPI.roleList();
|
roleOptions.value = await ManagedUserAPI.roleList();
|
||||||
if (isCreateMode.value) {
|
if (isCreateMode.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const current = await EventAPI.detail(routeId.value);
|
const current = await EventAPI.detail(routeId.value);
|
||||||
Object.assign(formModel, {
|
Object.assign(formModel, {
|
||||||
id: current.id || "",
|
id: current.id || "",
|
||||||
@@ -516,8 +534,8 @@ async function fillForm(): Promise<void> {
|
|||||||
requirePhoto: current.requirePhoto ?? false,
|
requirePhoto: current.requirePhoto ?? false,
|
||||||
limitType: current.limitType || "NONE",
|
limitType: current.limitType || "NONE",
|
||||||
limitValueText: current.limitValue ? String(current.limitValue) : "",
|
limitValueText: current.limitValue ? String(current.limitValue) : "",
|
||||||
beginAtText: current.beginAt ? Time.formatDateTime(new Date(current.beginAt)) : "",
|
beginAtText: current.beginAt ? Time.toDateTime(current.beginAt) : "",
|
||||||
endAtText: current.endAt ? Time.formatDateTime(new Date(current.endAt)) : "",
|
endAtText: current.endAt ? Time.toDateTime(current.endAt) : "",
|
||||||
roleCodeList: [...(current.roleCodeList || [])]
|
roleCodeList: [...(current.roleCodeList || [])]
|
||||||
});
|
});
|
||||||
colorDraft.value = formModel.color;
|
colorDraft.value = formModel.color;
|
||||||
@@ -527,7 +545,7 @@ onMounted(async () => {
|
|||||||
try {
|
try {
|
||||||
await fillForm();
|
await fillForm();
|
||||||
} catch {
|
} catch {
|
||||||
showToast("登记事件加载失败", "error");
|
Toast.error("登记事件加载失败");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -696,4 +714,3 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-can="{ value: [permissionCodes.event.read, permissionCodes.user.read], mode: 'and' }" class="registration-event-list">
|
<div v-can="{ value: [permissionCodes.event.read, permissionCodes.user.read], mode: 'and' }" class="registration-event-list">
|
||||||
|
<t-pull-down-refresh
|
||||||
|
v-model="isRefreshing"
|
||||||
|
:refresh-timeout="30000"
|
||||||
|
@refresh="refresh"
|
||||||
|
@scrolltolower="fetch"
|
||||||
|
>
|
||||||
<t-cell-group theme="card">
|
<t-cell-group theme="card">
|
||||||
<t-cell
|
<t-cell
|
||||||
class="item"
|
class="item"
|
||||||
v-for="item in eventList"
|
v-for="item in pageResult.list"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:arrow="canUpdateEventItem"
|
:arrow="canUpdateEventItem"
|
||||||
:hover="canUpdateEventItem"
|
:hover="canUpdateEventItem"
|
||||||
@click="handleEventClick(item)"
|
@click="toEventItem(item)"
|
||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
@@ -40,8 +46,10 @@
|
|||||||
</template>
|
</template>
|
||||||
</t-cell>
|
</t-cell>
|
||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
<div v-if="!eventList.length" class="empty-block">暂无登记事件</div>
|
<div v-if="!pageResult.list.length" class="empty-block" v-text="isLoading ? '正在加载登记事件' : '暂无登记事件'" />
|
||||||
<t-button v-can="permissionCodes.event.create" class="create" theme="primary" @click="createEventItem">
|
<div v-if="pageResult.list.length" class="load-state" v-text="isNoMore ? '已经到底了' : '上拉加载更多'" />
|
||||||
|
</t-pull-down-refresh>
|
||||||
|
<t-button v-can="permissionCodes.event.create" class="create" theme="primary" @click="router.push('/management/registration-events/create')">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<t-icon name="add" />
|
<t-icon name="add" />
|
||||||
</template>
|
</template>
|
||||||
@@ -52,11 +60,11 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Toast } from "tdesign-mobile-vue";
|
import { Toast } from "tdesign-mobile-vue";
|
||||||
import { useUserStore, type Role } from "timi-web";
|
import { type Page, type PageResult, type Role, useUserStore } from "timi-web";
|
||||||
import EventAPI from "@/api/EventAPI";
|
import EventAPI from "@/api/EventAPI";
|
||||||
import ManagedUserAPI from "@/api/ManagedUserAPI";
|
import ManagedUserAPI from "@/api/ManagedUserAPI";
|
||||||
import { useNavBarStore } from "@/store/navBarStore";
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
import type { EventItem, EventStatus, EventLimitType } from "@/types/EventItem";
|
import type { EventItem, EventLimitType, EventStatus } from "@/types/EventItem";
|
||||||
import { permissionCodes } from "@/utils/permission";
|
import { permissionCodes } from "@/utils/permission";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@@ -66,9 +74,22 @@ defineOptions({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const navBarStore = useNavBarStore();
|
const navBarStore = useNavBarStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
// ---------- 列表数据 ----------
|
||||||
|
|
||||||
const defaultColor = "#2f88ff";
|
const defaultColor = "#2f88ff";
|
||||||
const eventList = ref<EventItem[]>([]);
|
|
||||||
const roleOptions = ref<Role[]>([]);
|
const roleOptions = ref<Role[]>([]);
|
||||||
|
const isRefreshing = ref(false);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const isNoMore = ref(false);
|
||||||
|
const page = reactive<Page<EventItem>>({
|
||||||
|
index: 0,
|
||||||
|
size: 15
|
||||||
|
});
|
||||||
|
const pageResult = reactive<PageResult<EventItem>>({
|
||||||
|
total: 0,
|
||||||
|
list: []
|
||||||
|
});
|
||||||
const canUpdateEventItem = computed(() => userStore.hasPermissions(permissionCodes.event.update));
|
const canUpdateEventItem = computed(() => userStore.hasPermissions(permissionCodes.event.update));
|
||||||
|
|
||||||
type StatusTag = {
|
type StatusTag = {
|
||||||
@@ -85,30 +106,18 @@ const activeStatusTag: StatusTag = { text: "可登记", theme: "primary" };
|
|||||||
const notEffectiveStatusTag: StatusTag = { text: "未生效", theme: "default" };
|
const notEffectiveStatusTag: StatusTag = { text: "未生效", theme: "default" };
|
||||||
const expiredStatusTag: StatusTag = { text: "已过期", theme: "danger" };
|
const expiredStatusTag: StatusTag = { text: "已过期", theme: "danger" };
|
||||||
|
|
||||||
function createEventItem(): void {
|
function toEventItem(item: EventItem): void {
|
||||||
router.push("/management/registration-events/create");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function goSort(): Promise<void> {
|
|
||||||
await router.push("/management/registration-events/sort");
|
|
||||||
}
|
|
||||||
|
|
||||||
function editEventItem(item: EventItem): void {
|
|
||||||
if (!item.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
router.push(`/management/registration-events/${item.id}/edit`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleEventClick(item: EventItem): void {
|
|
||||||
if (!canUpdateEventItem.value) {
|
if (!canUpdateEventItem.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!item.id) {
|
||||||
editEventItem(item);
|
return;
|
||||||
|
}
|
||||||
|
router.push(`/management/registration-events/${item.id}/edit`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 展示文案 ----------
|
||||||
|
|
||||||
function getStatusTag(item: EventItem): StatusTag {
|
function getStatusTag(item: EventItem): StatusTag {
|
||||||
const status = item.status || "ACTIVE";
|
const status = item.status || "ACTIVE";
|
||||||
const fixedStatusTag = fixedStatusTagMap[status];
|
const fixedStatusTag = fixedStatusTagMap[status];
|
||||||
@@ -143,31 +152,60 @@ function getRoleName(code: string): string {
|
|||||||
return role?.name || code;
|
return role?.name || code;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 页面数据 ----------
|
||||||
|
|
||||||
|
async function fetch(): Promise<void> {
|
||||||
|
if (isLoading.value || isNoMore.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
const result = await EventAPI.list(page);
|
||||||
|
pageResult.total = result.total;
|
||||||
|
pageResult.list = pageResult.list.concat(result.list || []);
|
||||||
|
page.index += 1;
|
||||||
|
isNoMore.value = (result.list || []).length < page.size || pageResult.total <= pageResult.list.length;
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh(): Promise<void> {
|
||||||
|
isRefreshing.value = true;
|
||||||
|
try {
|
||||||
|
page.index = 0;
|
||||||
|
pageResult.total = 0;
|
||||||
|
pageResult.list = [];
|
||||||
|
isNoMore.value = false;
|
||||||
|
await fetch();
|
||||||
|
} finally {
|
||||||
|
isRefreshing.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadData(): Promise<void> {
|
async function loadData(): Promise<void> {
|
||||||
const [eventResult, roles] = await Promise.all([
|
const [eventResult, roles] = await Promise.all([
|
||||||
EventAPI.list({
|
EventAPI.list(page),
|
||||||
index: 0,
|
|
||||||
size: 100
|
|
||||||
}),
|
|
||||||
ManagedUserAPI.roleList()
|
ManagedUserAPI.roleList()
|
||||||
]);
|
]);
|
||||||
roleOptions.value = roles || [];
|
roleOptions.value = roles || [];
|
||||||
eventList.value = eventResult.list || [];
|
pageResult.total = eventResult.total;
|
||||||
|
pageResult.list = eventResult.list || [];
|
||||||
|
page.index = 1;
|
||||||
|
isNoMore.value = pageResult.list.length >= pageResult.total || pageResult.list.length < page.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
navBarStore.clearRight();
|
navBarStore.clearRight();
|
||||||
if (canUpdateEventItem.value) {
|
if (canUpdateEventItem.value) {
|
||||||
navBarStore.setRightText("排序", "var(--td-brand-color)", goSort);
|
navBarStore.setRightText("排序", "var(--td-brand-color)", () => {
|
||||||
|
router.push("/management/registration-events/sort");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await loadData();
|
await loadData();
|
||||||
} catch {
|
} catch {
|
||||||
Toast({
|
Toast.error("登记事件加载失败");
|
||||||
theme: "error",
|
|
||||||
direction: "row",
|
|
||||||
message: "登记事件加载失败"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -245,4 +283,3 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<customer-profile-card v-if="record?.customer" class="customer-profile" :customer="record.customer" />
|
<customer-profile-card v-if="record?.customer" class="customer-profile" :customer="record.customer" />
|
||||||
<t-cell-group v-if="record" title="登记信息" theme="card">
|
<t-cell-group v-if="record" title="登记信息" theme="card">
|
||||||
<t-cell title="登记事件" :note="record.event?.name || '未知事件'" />
|
<t-cell title="登记事件" :note="record.event?.name || '未知事件'" />
|
||||||
<t-cell title="登记时间" :note="formatTime(record.registeredAt)" />
|
<t-cell title="登记时间" :note="record.registeredAt ? Time.toPassedDateTime(record.registeredAt) : '-'" />
|
||||||
<t-cell title="登记人" :note="Text.display(record.operator?.nick || record.operator?.name)" />
|
<t-cell title="登记人" :note="Text.display(record.operator?.nick || record.operator?.name)" />
|
||||||
<t-cell title="客户编码" :note="Text.display(record.customer?.code)" />
|
<t-cell title="客户编码" :note="Text.display(record.customer?.code)" />
|
||||||
<t-cell title="备注" :note="Text.display(record.remark)" />
|
<t-cell title="备注" :note="Text.display(record.remark)" />
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
title="登记照片"
|
title="登记照片"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
<t-empty v-else :description="loading ? '正在加载登记事件记录' : '登记事件记录不存在'" />
|
<t-empty v-else :description="isLoading ? '正在加载登记事件记录' : '登记事件记录不存在'" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -38,8 +38,11 @@ const route = useRoute();
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const navBarStore = useNavBarStore();
|
const navBarStore = useNavBarStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
// ---------- 记录数据 ----------
|
||||||
|
|
||||||
const record = ref<EventRecord>();
|
const record = ref<EventRecord>();
|
||||||
const loading = ref(false);
|
const isLoading = ref(false);
|
||||||
|
|
||||||
const canUpdateRecord = computed(() => userStore.hasPermissions(permissionCodes.eventRecord.update));
|
const canUpdateRecord = computed(() => userStore.hasPermissions(permissionCodes.eventRecord.update));
|
||||||
const recordPhotoList = computed(() => {
|
const recordPhotoList = computed(() => {
|
||||||
@@ -47,6 +50,8 @@ const recordPhotoList = computed(() => {
|
|||||||
return url ? [url] : [];
|
return url ? [url] : [];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------- 页面数据 ----------
|
||||||
|
|
||||||
async function loadRecord(): Promise<void> {
|
async function loadRecord(): Promise<void> {
|
||||||
const recordId = String(route.params.id || "");
|
const recordId = String(route.params.id || "");
|
||||||
if (!recordId) {
|
if (!recordId) {
|
||||||
@@ -54,18 +59,20 @@ async function loadRecord(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
record.value = await EventRecordAPI.detail(recordId);
|
record.value = await EventRecordAPI.detail(recordId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
record.value = undefined;
|
record.value = undefined;
|
||||||
console.warn("登记事件记录加载失败", err);
|
console.warn("登记事件记录加载失败", err);
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function goEdit(): Promise<void> {
|
// ---------- 导航 ----------
|
||||||
|
|
||||||
|
async function toEdit(): Promise<void> {
|
||||||
const recordId = String(route.params.id || "");
|
const recordId = String(route.params.id || "");
|
||||||
if (!recordId) {
|
if (!recordId) {
|
||||||
return;
|
return;
|
||||||
@@ -73,18 +80,6 @@ async function goEdit(): Promise<void> {
|
|||||||
await router.push(`/management/registration-event-records/${recordId}/edit`);
|
await router.push(`/management/registration-event-records/${recordId}/edit`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTime(value?: number): string {
|
|
||||||
if (!value) {
|
|
||||||
return "-";
|
|
||||||
}
|
|
||||||
|
|
||||||
const date = Time.toDateObject(value);
|
|
||||||
if (!date) {
|
|
||||||
return "-";
|
|
||||||
}
|
|
||||||
return Time.formatDateTime(date);
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(() => route.params.id, async () => {
|
watch(() => route.params.id, async () => {
|
||||||
await loadRecord();
|
await loadRecord();
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
@@ -92,7 +87,7 @@ watch(() => route.params.id, async () => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
navBarStore.clearRight();
|
navBarStore.clearRight();
|
||||||
if (canUpdateRecord.value) {
|
if (canUpdateRecord.value) {
|
||||||
navBarStore.setRightText("编辑", "var(--td-brand-color)", goEdit);
|
navBarStore.setRightText("编辑", "var(--td-brand-color)", toEdit);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
</t-form-item>
|
</t-form-item>
|
||||||
|
|
||||||
<div class="photo-part">
|
<div class="photo-part">
|
||||||
<photo-scroller v-model="photoList" :loading="photoUploading" @add="capturePhoto" />
|
<photo-scroller v-model="photoList" :loading="isPhotoUploading" @add="capturePhoto" />
|
||||||
</div>
|
</div>
|
||||||
</t-form>
|
</t-form>
|
||||||
<t-cell-group
|
<t-cell-group
|
||||||
@@ -91,10 +91,10 @@
|
|||||||
v-for="item in recentRecordList"
|
v-for="item in recentRecordList"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:title="getRecordEventName(item)"
|
:title="getRecordEventName(item)"
|
||||||
:description="formatTime(item.registeredAt)"
|
:description="item.registeredAt ? Time.toPassedDateTime(item.registeredAt) : '-'"
|
||||||
arrow
|
arrow
|
||||||
hover
|
hover
|
||||||
@click="goRecordDetail(item.id || '')"
|
@click="toRecordDetail(item.id || '')"
|
||||||
>
|
>
|
||||||
<template #leftIcon>
|
<template #leftIcon>
|
||||||
<t-image v-if="getRecordPhotoURL(item)" :src="getRecordPhotoURL(item)" alt="事件照片" fit="cover" class="record-photo" />
|
<t-image v-if="getRecordPhotoURL(item)" :src="getRecordPhotoURL(item)" alt="事件照片" fit="cover" class="record-photo" />
|
||||||
@@ -132,7 +132,7 @@
|
|||||||
multiple
|
multiple
|
||||||
@change="handleRecordPhotoSelected"
|
@change="handleRecordPhotoSelected"
|
||||||
>
|
>
|
||||||
<t-button theme="primary" class="save" :disabled="!canSubmit" :loading="saving" @click="submitRecord">
|
<t-button theme="primary" class="save" :disabled="!canSubmit" :loading="isSaving" @click="submitRecord">
|
||||||
<span v-text="saveButtonText" />
|
<span v-text="saveButtonText" />
|
||||||
</t-button>
|
</t-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -177,28 +177,6 @@ defineOptions({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
const customerCode = ref("");
|
|
||||||
const scanCode = ref(customerCode.value);
|
|
||||||
const selectorVisible = ref(false);
|
|
||||||
const selectedCustomerListVisible = ref(false);
|
|
||||||
const selectedCustomerIdList = ref<string[]>([]);
|
|
||||||
const selectedCustomerList = ref<Customer[]>([]);
|
|
||||||
const eventList = ref<EventItem[]>([]);
|
|
||||||
const selectedEventIdList = ref<string[]>([]);
|
|
||||||
const recentRecordList = ref<EventRecord[]>([]);
|
|
||||||
const minimumDateTimeText = "1970-01-01 00:00";
|
|
||||||
const registeredAtText = ref("");
|
|
||||||
const registeredAtDraftText = ref("");
|
|
||||||
const registeredAtPickerVisible = ref(false);
|
|
||||||
const note = ref("");
|
|
||||||
const recordPhotoInputRef = ref<HTMLInputElement>();
|
|
||||||
const photoItemList = ref<EditablePhoto[]>([]);
|
|
||||||
const photoUploading = ref(false);
|
|
||||||
const scannerVisible = ref(false);
|
|
||||||
const scanStatus = ref("点击扫码开始识别。");
|
|
||||||
const saving = ref(false);
|
|
||||||
const editingRecord = ref<EventRecord>();
|
|
||||||
let latestDecodedText = "";
|
|
||||||
|
|
||||||
type EditablePhoto = {
|
type EditablePhoto = {
|
||||||
attachment?: Partial<Attachment>;
|
attachment?: Partial<Attachment>;
|
||||||
@@ -206,13 +184,23 @@ type EditablePhoto = {
|
|||||||
url: string;
|
url: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ---------- 路由权限 ----------
|
||||||
|
|
||||||
const routeId = computed(() => String(route.params.id || ""));
|
const routeId = computed(() => String(route.params.id || ""));
|
||||||
const isEditMode = computed(() => !!routeId.value);
|
const isEditMode = computed(() => !!routeId.value);
|
||||||
const pageAccess = computed(() => isEditMode.value ? permissionCodes.eventRecord.update : permissionCodes.eventRecord.create);
|
const pageAccess = computed(() => isEditMode.value ? permissionCodes.eventRecord.update : permissionCodes.eventRecord.create);
|
||||||
const saveButtonText = computed(() => isEditMode.value ? "保存记录" : "提交");
|
const saveButtonText = computed(() => isEditMode.value ? "保存记录" : "提交");
|
||||||
|
|
||||||
|
// ---------- 客户 ----------
|
||||||
|
|
||||||
|
const customerCode = ref("");
|
||||||
|
const selectorVisible = ref(false);
|
||||||
|
const selectedCustomerListVisible = ref(false);
|
||||||
|
const selectedCustomerIdList = ref<string[]>([]);
|
||||||
|
const selectedCustomerList = ref<Customer[]>([]);
|
||||||
|
|
||||||
const selectedCustomer = computed(() => selectedCustomerList.value[0]);
|
const selectedCustomer = computed(() => selectedCustomerList.value[0]);
|
||||||
const showSelectedCustomerProfile = computed(() => selectedCustomerList.value.length === 1 && !!selectedCustomer.value?.id);
|
const showSelectedCustomerProfile = computed(() => selectedCustomerList.value.length === 1 && !!selectedCustomer.value?.id);
|
||||||
const canSubmit = computed(() => selectedCustomerList.value.some(item => !!item.id) && 0 < selectedEventIdList.value.length);
|
|
||||||
const customerDisplayText = computed(() => {
|
const customerDisplayText = computed(() => {
|
||||||
if (1 < selectedCustomerList.value.length) {
|
if (1 < selectedCustomerList.value.length) {
|
||||||
return `已选择 ${selectedCustomerList.value.length} 位客户`;
|
return `已选择 ${selectedCustomerList.value.length} 位客户`;
|
||||||
@@ -220,67 +208,115 @@ const customerDisplayText = computed(() => {
|
|||||||
|
|
||||||
return selectedCustomer.value?.name || selectedCustomer.value?.code || customerCode.value;
|
return selectedCustomer.value?.name || selectedCustomer.value?.code || customerCode.value;
|
||||||
});
|
});
|
||||||
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 eventNameMap = computed(() => {
|
|
||||||
const map = new Map<string, string>();
|
|
||||||
for (const item of eventList.value) {
|
|
||||||
if (item.id) {
|
|
||||||
map.set(item.id, item.name || "未命名事件");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---------- 页面数据 ----------
|
async function handleCustomerChange(value: string | string[]): Promise<void> {
|
||||||
|
selectedCustomerIdList.value = Array.isArray(value) ? value : [value];
|
||||||
async function loadPageData(): Promise<void> {
|
await loadSelectedCustomerList();
|
||||||
eventList.value = await EventAPI.enabledList();
|
|
||||||
if (isEditMode.value) {
|
|
||||||
await loadRecordForEdit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedEventIdList.value = eventList.value.slice(0, 1).map(item => item.id).filter((item): item is string => Boolean(item));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadRecordForEdit(): Promise<void> {
|
|
||||||
const record = await EventRecordAPI.detail(routeId.value);
|
|
||||||
editingRecord.value = record;
|
|
||||||
appendRecordEvent(record);
|
|
||||||
selectedCustomerList.value = record.customer ? [record.customer] : [];
|
|
||||||
customerCode.value = record.customer?.code || record.customerId || "";
|
|
||||||
selectedCustomerIdList.value = record.customer?.id ? [record.customer.id] : [];
|
|
||||||
selectedEventIdList.value = record.eventId ? [record.eventId] : [];
|
|
||||||
registeredAtText.value = record.registeredAt ? Time.formatDateTime(new Date(record.registeredAt)) : "";
|
|
||||||
note.value = record.remark || "";
|
|
||||||
photoItemList.value = (record.attachmentList || [])
|
|
||||||
.filter(item => item.attachType === "PHOTO" || !item.attachType)
|
|
||||||
.filter(item => !!item.id)
|
|
||||||
.map(item => ({
|
|
||||||
attachment: item,
|
|
||||||
url: AttachmentAPI.getReadURL(item.id || "")
|
|
||||||
}));
|
|
||||||
await loadRecentRecordList();
|
await loadRecentRecordList();
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendRecordEvent(record: EventRecord): void {
|
async function loadSelectedCustomerList(): Promise<void> {
|
||||||
if (!record.event?.id || eventList.value.some(item => item.id === record.event?.id)) {
|
const customerList = await Promise.all(selectedCustomerIdList.value.map(id => CustomerAPI.detail(id)));
|
||||||
|
selectedCustomerList.value = customerList.filter(item => !!item.id);
|
||||||
|
const firstCustomer = selectedCustomer.value;
|
||||||
|
customerCode.value = firstCustomer?.code || "";
|
||||||
|
scanCode.value = firstCustomer?.code || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSelectedCustomerList(): void {
|
||||||
|
selectedCustomerListVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openCustomerSelector(): Promise<void> {
|
||||||
|
selectedCustomerListVisible.value = false;
|
||||||
|
await nextTick();
|
||||||
|
selectorVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeSelectedCustomer(customer: Customer): Promise<void> {
|
||||||
|
if (!customer.id || isEditMode.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedCustomerIdList.value = selectedCustomerIdList.value.filter(id => id !== customer.id);
|
||||||
|
selectedCustomerList.value = selectedCustomerList.value.filter(item => item.id !== customer.id);
|
||||||
|
await loadRecentRecordList();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toRecordDetail(id: string): Promise<void> {
|
||||||
|
if (!id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await router.push(`/management/registration-event-records/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSelectedCustomer(): Promise<void> {
|
||||||
|
const code = customerCode.value.trim();
|
||||||
|
if (!code) {
|
||||||
|
selectedCustomerList.value = [];
|
||||||
|
selectedCustomerIdList.value = [];
|
||||||
|
recentRecordList.value = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
eventList.value = [record.event, ...eventList.value];
|
const customer = await CustomerAPI.findByCode(code);
|
||||||
|
if (!customer) {
|
||||||
|
selectedCustomerList.value = [];
|
||||||
|
selectedCustomerIdList.value = [];
|
||||||
|
recentRecordList.value = [];
|
||||||
|
scanStatus.value = `客户 ${code} 不存在,请先建档。`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (customer.id) {
|
||||||
|
selectedCustomerIdList.value = isEditMode.value
|
||||||
|
? [customer.id]
|
||||||
|
: [...new Set([...selectedCustomerIdList.value, customer.id])];
|
||||||
|
selectedCustomerList.value = isEditMode.value
|
||||||
|
? [customer]
|
||||||
|
: [...selectedCustomerList.value.filter(item => item.id !== customer.id), customer];
|
||||||
|
}
|
||||||
|
await loadRecentRecordList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 最近记录 ----------
|
||||||
|
|
||||||
|
const recentRecordList = ref<EventRecord[]>([]);
|
||||||
|
|
||||||
|
async function loadRecentRecordList(): Promise<void> {
|
||||||
|
const customerId = selectedCustomer.value?.id;
|
||||||
|
if (!customerId) {
|
||||||
|
recentRecordList.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const endAt = Time.toTimestamp(new Date());
|
||||||
|
const result = await EventRecordAPI.list({
|
||||||
|
index: 0,
|
||||||
|
size: 5,
|
||||||
|
customerId,
|
||||||
|
beginAt: 0,
|
||||||
|
endAt
|
||||||
|
});
|
||||||
|
recentRecordList.value = (result.list || [])
|
||||||
|
.sort((left, right) => (right.registeredAt || 0) - (left.registeredAt || 0))
|
||||||
|
.slice(0, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRecordEventName(record: EventRecord): string {
|
||||||
|
return record.event?.name || (record.eventId ? eventNameMap.value.get(record.eventId) : "") || "未知事件";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRecordPhotoURL(record: EventRecord): string {
|
||||||
|
return getEventRecordPhotoURL(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 扫码 ----------
|
// ---------- 扫码 ----------
|
||||||
|
|
||||||
|
const scanCode = ref(customerCode.value);
|
||||||
|
const scannerVisible = ref(false);
|
||||||
|
const scanStatus = ref("点击扫码开始识别。");
|
||||||
|
let latestDecodedText = "";
|
||||||
|
|
||||||
async function toggleScanner(): Promise<void> {
|
async function toggleScanner(): Promise<void> {
|
||||||
scannerVisible.value = !scannerVisible.value;
|
scannerVisible.value = !scannerVisible.value;
|
||||||
if (scannerVisible.value) {
|
if (scannerVisible.value) {
|
||||||
@@ -316,104 +352,45 @@ function handleScanStatusChange(value: string): void {
|
|||||||
scanStatus.value = value;
|
scanStatus.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 客户 ----------
|
// ---------- 事件 ----------
|
||||||
|
|
||||||
async function handleCustomerChange(value: string | string[]): Promise<void> {
|
const eventList = ref<EventItem[]>([]);
|
||||||
selectedCustomerIdList.value = Array.isArray(value) ? value : [value];
|
const selectedEventIdList = ref<string[]>([]);
|
||||||
await loadSelectedCustomerList();
|
|
||||||
await loadRecentRecordList();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadSelectedCustomerList(): Promise<void> {
|
const eventNameMap = computed(() => {
|
||||||
const customerList = await Promise.all(selectedCustomerIdList.value.map(id => CustomerAPI.detail(id)));
|
const map = new Map<string, string>();
|
||||||
selectedCustomerList.value = customerList.filter(item => !!item.id);
|
for (const item of eventList.value) {
|
||||||
const firstCustomer = selectedCustomer.value;
|
if (item.id) {
|
||||||
customerCode.value = firstCustomer?.code || "";
|
map.set(item.id, item.name || "未命名事件");
|
||||||
scanCode.value = firstCustomer?.code || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function openSelectedCustomerList(): void {
|
|
||||||
selectedCustomerListVisible.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openCustomerSelector(): Promise<void> {
|
|
||||||
selectedCustomerListVisible.value = false;
|
|
||||||
await nextTick();
|
|
||||||
selectorVisible.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeSelectedCustomer(customer: Customer): Promise<void> {
|
|
||||||
if (!customer.id || isEditMode.value) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
selectedCustomerIdList.value = selectedCustomerIdList.value.filter(id => id !== customer.id);
|
|
||||||
selectedCustomerList.value = selectedCustomerList.value.filter(item => item.id !== customer.id);
|
|
||||||
await loadRecentRecordList();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function goRecordDetail(id: string): Promise<void> {
|
|
||||||
if (!id) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
await router.push(`/management/registration-event-records/${id}`);
|
return map;
|
||||||
}
|
});
|
||||||
|
|
||||||
async function loadSelectedCustomer(): Promise<void> {
|
function appendRecordEvent(record: EventRecord): void {
|
||||||
const code = customerCode.value.trim();
|
if (!record.event?.id || eventList.value.some(item => item.id === record.event?.id)) {
|
||||||
if (!code) {
|
|
||||||
selectedCustomerList.value = [];
|
|
||||||
selectedCustomerIdList.value = [];
|
|
||||||
recentRecordList.value = [];
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const customer = await CustomerAPI.nullableDetailByCode(code);
|
eventList.value = [record.event, ...eventList.value];
|
||||||
if (!customer) {
|
|
||||||
selectedCustomerList.value = [];
|
|
||||||
selectedCustomerIdList.value = [];
|
|
||||||
recentRecordList.value = [];
|
|
||||||
scanStatus.value = `客户 ${code} 不存在,请先建档。`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (customer.id) {
|
|
||||||
selectedCustomerIdList.value = isEditMode.value
|
|
||||||
? [customer.id]
|
|
||||||
: [...new Set([...selectedCustomerIdList.value, customer.id])];
|
|
||||||
selectedCustomerList.value = isEditMode.value
|
|
||||||
? [customer]
|
|
||||||
: [...selectedCustomerList.value.filter(item => item.id !== customer.id), customer];
|
|
||||||
}
|
|
||||||
await loadRecentRecordList();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadRecentRecordList(): Promise<void> {
|
|
||||||
const customerId = selectedCustomer.value?.id;
|
|
||||||
if (!customerId) {
|
|
||||||
recentRecordList.value = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const endAt = Time.toTimestamp(new Date());
|
|
||||||
recentRecordList.value = (await EventRecordAPI.rangeList({
|
|
||||||
customerId,
|
|
||||||
beginAt: 0,
|
|
||||||
endAt
|
|
||||||
}))
|
|
||||||
.sort((left, right) => (right.registeredAt || 0) - (left.registeredAt || 0))
|
|
||||||
.slice(0, 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRecordEventName(record: EventRecord): string {
|
|
||||||
return record.event?.name || (record.eventId ? eventNameMap.value.get(record.eventId) : "") || "未知事件";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRecordPhotoURL(record: EventRecord): string {
|
|
||||||
return getEventRecordPhotoURL(record);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 照片 ----------
|
// ---------- 照片 ----------
|
||||||
|
|
||||||
|
const recordPhotoInputRef = ref<HTMLInputElement>();
|
||||||
|
const photoItemList = ref<EditablePhoto[]>([]);
|
||||||
|
const isPhotoUploading = ref(false);
|
||||||
|
|
||||||
|
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 capturePhoto(): void {
|
function capturePhoto(): void {
|
||||||
recordPhotoInputRef.value?.click();
|
recordPhotoInputRef.value?.click();
|
||||||
}
|
}
|
||||||
@@ -424,10 +401,10 @@ async function handleRecordPhotoSelected(event: Event): Promise<void> {
|
|||||||
if (!fileList.length) {
|
if (!fileList.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
photoUploading.value = true;
|
isPhotoUploading.value = true;
|
||||||
try {
|
try {
|
||||||
const compressedFileList = await compressUploadImageList(fileList);
|
const compressedFileList = await compressUploadImageList(fileList);
|
||||||
const tempFileList = await AttachmentAPI.uploadTemp(compressedFileList);
|
const tempFileList = await AttachmentAPI.upload(compressedFileList);
|
||||||
for (const item of tempFileList) {
|
for (const item of tempFileList) {
|
||||||
if (!item.id) {
|
if (!item.id) {
|
||||||
continue;
|
continue;
|
||||||
@@ -438,12 +415,17 @@ async function handleRecordPhotoSelected(event: Event): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
photoUploading.value = false;
|
isPhotoUploading.value = false;
|
||||||
input.value = "";
|
input.value = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 提交 ----------
|
// ---------- 登记时间 ----------
|
||||||
|
|
||||||
|
const minimumDateTimeText = "1970-01-01 00:00";
|
||||||
|
const registeredAtText = ref("");
|
||||||
|
const registeredAtDraftText = ref("");
|
||||||
|
const registeredAtPickerVisible = ref(false);
|
||||||
|
|
||||||
function openRegisteredAtPicker(): void {
|
function openRegisteredAtPicker(): void {
|
||||||
registeredAtDraftText.value = registeredAtText.value || Time.formatDateTime(new Date());
|
registeredAtDraftText.value = registeredAtText.value || Time.formatDateTime(new Date());
|
||||||
@@ -460,12 +442,35 @@ function resetRegisteredAt(): void {
|
|||||||
registeredAtDraftText.value = "";
|
registeredAtDraftText.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildRegisteredAt(): number {
|
||||||
|
return Time.toTimestamp(parseDateTime(registeredAtText.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 editingRecord = ref<EventRecord>();
|
||||||
|
|
||||||
|
const canSubmit = computed(() => selectedCustomerList.value.some(item => !!item.id) && 0 < selectedEventIdList.value.length);
|
||||||
|
|
||||||
async function submitRecord(): Promise<void> {
|
async function submitRecord(): Promise<void> {
|
||||||
if (!validateForm()) {
|
if (!validateForm()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
saving.value = true;
|
isSaving.value = true;
|
||||||
try {
|
try {
|
||||||
if (isEditMode.value) {
|
if (isEditMode.value) {
|
||||||
await updateRecord();
|
await updateRecord();
|
||||||
@@ -479,7 +484,10 @@ async function submitRecord(): Promise<void> {
|
|||||||
customerIdList,
|
customerIdList,
|
||||||
remark: note.value || "客户现场登记",
|
remark: note.value || "客户现场登记",
|
||||||
registeredAt: buildRegisteredAt(),
|
registeredAt: buildRegisteredAt(),
|
||||||
attachmentIdList: buildTempFileIdList()
|
attachmentList: buildTempFileIdList().map(tempFileId => ({
|
||||||
|
tempFileId,
|
||||||
|
attachType: "PHOTO"
|
||||||
|
}))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
selectedCustomerList.value = [];
|
selectedCustomerList.value = [];
|
||||||
@@ -488,20 +496,20 @@ async function submitRecord(): Promise<void> {
|
|||||||
photoItemList.value = [];
|
photoItemList.value = [];
|
||||||
await loadRecentRecordList();
|
await loadRecentRecordList();
|
||||||
await settingStore.playRegisterSuccessSound();
|
await settingStore.playRegisterSuccessSound();
|
||||||
showToast("登记已提交");
|
Toast.success("登记已提交");
|
||||||
await router.replace("/customers");
|
await router.replace("/customers");
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false;
|
isSaving.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateForm(): boolean {
|
function validateForm(): boolean {
|
||||||
if (!selectedCustomerList.value.some(item => !!item.id)) {
|
if (!selectedCustomerList.value.some(item => !!item.id)) {
|
||||||
showToast("请先选择或扫码客户", "warning");
|
Toast.warning("请先选择或扫码客户");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!selectedEventIdList.value.length) {
|
if (!selectedEventIdList.value.length) {
|
||||||
showToast("请至少选择一个登记事件", "warning");
|
Toast.warning("请至少选择一个登记事件");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,7 +519,7 @@ function validateForm(): boolean {
|
|||||||
async function updateRecord(): Promise<void> {
|
async function updateRecord(): Promise<void> {
|
||||||
const submitCustomer = selectedCustomer.value;
|
const submitCustomer = selectedCustomer.value;
|
||||||
if (!editingRecord.value?.id || !submitCustomer?.id) {
|
if (!editingRecord.value?.id || !submitCustomer?.id) {
|
||||||
showToast("登记事件记录不存在", "warning");
|
Toast.warning("登记事件记录不存在");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,7 +531,7 @@ async function updateRecord(): Promise<void> {
|
|||||||
registeredAt: buildRegisteredAt(),
|
registeredAt: buildRegisteredAt(),
|
||||||
attachmentList: buildPhotoAttachmentList()
|
attachmentList: buildPhotoAttachmentList()
|
||||||
});
|
});
|
||||||
showToast("记录已保存");
|
Toast.success("记录已保存");
|
||||||
await router.replace(`/management/registration-event-records/${editingRecord.value.id}`);
|
await router.replace(`/management/registration-event-records/${editingRecord.value.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,39 +552,36 @@ function buildPhotoAttachmentList(): Array<Partial<Attachment>> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildRegisteredAt(): number {
|
// ---------- 页面数据 ----------
|
||||||
return Time.toTimestamp(parseDateTime(registeredAtText.value));
|
|
||||||
}
|
|
||||||
|
|
||||||
function showToast(message: string, theme: "success" | "warning" = "success"): void {
|
async function loadPageData(): Promise<void> {
|
||||||
Toast({
|
eventList.value = await EventAPI.listValid();
|
||||||
theme,
|
if (isEditMode.value) {
|
||||||
direction: "row",
|
await loadRecordForEdit();
|
||||||
message
|
return;
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTime(value?: number): string {
|
|
||||||
if (!value) {
|
|
||||||
return "-";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const date = Time.toDateObject(value);
|
selectedEventIdList.value = eventList.value.slice(0, 1).map(item => item.id).filter((item): item is string => Boolean(item));
|
||||||
if (!date) {
|
|
||||||
return "-";
|
|
||||||
}
|
|
||||||
return Time.formatDateTime(date);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseDateTime(value: string): Date {
|
async function loadRecordForEdit(): Promise<void> {
|
||||||
if (!value) {
|
const record = await EventRecordAPI.detail(routeId.value);
|
||||||
return new Date();
|
editingRecord.value = record;
|
||||||
}
|
appendRecordEvent(record);
|
||||||
|
selectedCustomerList.value = record.customer ? [record.customer] : [];
|
||||||
const [dateText, timeText = "00:00"] = value.split(" ");
|
customerCode.value = record.customer?.code || record.customerId || "";
|
||||||
const [year, month, day] = dateText.split("-").map(Number);
|
selectedCustomerIdList.value = record.customer?.id ? [record.customer.id] : [];
|
||||||
const [hour, minute] = timeText.split(":").map(Number);
|
selectedEventIdList.value = record.eventId ? [record.eventId] : [];
|
||||||
return new Date(year, month - 1, day, hour || 0, minute || 0, 0, 0);
|
registeredAtText.value = record.registeredAt ? Time.formatDateTime(new Date(record.registeredAt)) : "";
|
||||||
|
note.value = record.remark || "";
|
||||||
|
photoItemList.value = (record.attachmentList || [])
|
||||||
|
.filter(item => item.attachType === "PHOTO" || !item.attachType)
|
||||||
|
.filter(item => !!item.id)
|
||||||
|
.map(item => ({
|
||||||
|
attachment: item,
|
||||||
|
url: AttachmentAPI.getReadURL(item.id || "")
|
||||||
|
}));
|
||||||
|
await loadRecentRecordList();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(loadPageData);
|
onMounted(loadPageData);
|
||||||
@@ -728,4 +733,3 @@ watch(customerCode, async (value) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="pageRef" v-can="permissionCodes.eventRecord.read" class="registration-record-list">
|
<div v-can="permissionCodes.eventRecord.read" class="registration-record-list">
|
||||||
<div class="search-bar">
|
<div class="search-bar">
|
||||||
<t-input
|
<t-input
|
||||||
v-model.trim="keyword"
|
v-model.trim="keyword"
|
||||||
@@ -43,13 +43,19 @@
|
|||||||
<div v-text="summaryBehaviorText" />
|
<div v-text="summaryBehaviorText" />
|
||||||
<div v-text="summaryTotalText" />
|
<div v-text="summaryTotalText" />
|
||||||
</div>
|
</div>
|
||||||
|
<t-pull-down-refresh
|
||||||
|
v-model="isRefreshing"
|
||||||
|
:refresh-timeout="30000"
|
||||||
|
@refresh="reloadRecords"
|
||||||
|
@scrolltolower="loadRecords"
|
||||||
|
>
|
||||||
<t-cell-group
|
<t-cell-group
|
||||||
v-if="recordList.length"
|
v-if="pageResult.list.length"
|
||||||
class="record-list"
|
class="record-list"
|
||||||
theme="card"
|
theme="card"
|
||||||
>
|
>
|
||||||
<t-swipe-cell
|
<t-swipe-cell
|
||||||
v-for="item in recordList"
|
v-for="item in pageResult.list"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="item"
|
class="item"
|
||||||
>
|
>
|
||||||
@@ -57,10 +63,10 @@
|
|||||||
class="cell"
|
class="cell"
|
||||||
:title="getRecordEventName(item)"
|
:title="getRecordEventName(item)"
|
||||||
:description="getRecordCustomerName(item)"
|
:description="getRecordCustomerName(item)"
|
||||||
:note="formatTime(item.registeredAt)"
|
:note="item.registeredAt ? Time.toPassedDateTime(item.registeredAt) : '-'"
|
||||||
arrow
|
arrow
|
||||||
hover
|
hover
|
||||||
@click="goDetail(item.id || '')"
|
@click="toDetail(item.id || '')"
|
||||||
>
|
>
|
||||||
<template #leftIcon>
|
<template #leftIcon>
|
||||||
<t-image
|
<t-image
|
||||||
@@ -77,7 +83,7 @@
|
|||||||
<t-button
|
<t-button
|
||||||
class="delete"
|
class="delete"
|
||||||
theme="danger"
|
theme="danger"
|
||||||
:loading="deletingId === item.id"
|
:loading="isDeleting && deletingId === item.id"
|
||||||
@click.stop="confirmRemoveRecord(item)"
|
@click.stop="confirmRemoveRecord(item)"
|
||||||
>
|
>
|
||||||
删除
|
删除
|
||||||
@@ -85,14 +91,15 @@
|
|||||||
</template>
|
</template>
|
||||||
</t-swipe-cell>
|
</t-swipe-cell>
|
||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
<div v-else class="empty-block" v-text="loading ? '正在加载登记事件记录' : '暂无登记事件记录'" />
|
<div v-else class="empty-block" v-text="isLoading ? '正在加载登记事件记录' : '暂无登记事件记录'" />
|
||||||
<div v-if="recordList.length" class="load-state" v-text="loadStateText" />
|
<div v-if="pageResult.list.length" class="load-state" v-text="loadStateText" />
|
||||||
|
</t-pull-down-refresh>
|
||||||
</div>
|
</div>
|
||||||
<t-button
|
<t-button
|
||||||
v-can="permissionCodes.eventRecord.export"
|
v-can="permissionCodes.eventRecord.export"
|
||||||
class="export"
|
class="export"
|
||||||
theme="primary"
|
theme="primary"
|
||||||
:loading="exporting"
|
:loading="isExporting"
|
||||||
@click="exportExcel"
|
@click="exportExcel"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
@@ -124,7 +131,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { DialogPlugin, Toast, type DropdownValue } from "tdesign-mobile-vue";
|
import { DialogPlugin, Toast, type DropdownValue } from "tdesign-mobile-vue";
|
||||||
import { AttachmentAPI, Scroller, Time, Toolkit, useUserStore } from "timi-web";
|
import { AttachmentAPI, Time, Toolkit, type PageResult, useUserStore } from "timi-web";
|
||||||
import EventAPI from "@/api/EventAPI";
|
import EventAPI from "@/api/EventAPI";
|
||||||
import EventRecordAPI from "@/api/EventRecordAPI";
|
import EventRecordAPI from "@/api/EventRecordAPI";
|
||||||
import { useNavBarStore } from "@/store/navBarStore";
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
@@ -139,7 +146,6 @@ defineOptions({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const pageSize = 20;
|
const pageSize = 20;
|
||||||
const scrollListenerName = "EventRecordList";
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const navBarStore = useNavBarStore();
|
const navBarStore = useNavBarStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
@@ -161,14 +167,9 @@ const actionFilter = ref<string[]>([]);
|
|||||||
const eventList = ref<EventItem[]>([]);
|
const eventList = ref<EventItem[]>([]);
|
||||||
const startVisible = ref(false);
|
const startVisible = ref(false);
|
||||||
const endVisible = ref(false);
|
const endVisible = ref(false);
|
||||||
const pageRef = ref<HTMLElement>();
|
|
||||||
let keywordTimer: number | undefined;
|
let keywordTimer: number | undefined;
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
|
|
||||||
function getPageScrollElement(): HTMLElement | null {
|
|
||||||
return pageRef.value?.closest(".pages") || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const canCreateRecord = computed(() => userStore.hasPermissions(permissionCodes.eventRecord.create));
|
const canCreateRecord = computed(() => userStore.hasPermissions(permissionCodes.eventRecord.create));
|
||||||
const isCustomRange = computed(() => dateFilterType.value === "custom");
|
const isCustomRange = computed(() => dateFilterType.value === "custom");
|
||||||
const dateFilterLabel = computed(() => dateOptions.find(item => item.value === dateFilterType.value)?.label || "今天");
|
const dateFilterLabel = computed(() => dateOptions.find(item => item.value === dateFilterType.value)?.label || "今天");
|
||||||
@@ -229,63 +230,75 @@ watch(dateRange, async (value) => {
|
|||||||
|
|
||||||
// ---------- 列表分页 ----------
|
// ---------- 列表分页 ----------
|
||||||
|
|
||||||
const pageIndex = ref(0);
|
const isRefreshing = ref(false);
|
||||||
const totalCount = ref(0);
|
const isLoading = ref(false);
|
||||||
const recordList = ref<EventRecordListItemView[]>([]);
|
const isNoMore = ref(false);
|
||||||
const loading = ref(false);
|
const page = reactive<EventRecordPage>({
|
||||||
const noMore = ref(false);
|
index: 0,
|
||||||
|
size: pageSize
|
||||||
|
});
|
||||||
|
const pageResult = reactive<PageResult<EventRecordListItemView>>({
|
||||||
|
total: 0,
|
||||||
|
list: []
|
||||||
|
});
|
||||||
let loadToken = 0;
|
let loadToken = 0;
|
||||||
|
|
||||||
const summaryTotalText = computed(() => `共 ${totalCount.value} 条数据,已加载 ${recordList.value.length} 条`);
|
const summaryTotalText = computed(() => `共 ${pageResult.total} 条数据,已加载 ${pageResult.list.length} 条`);
|
||||||
const loadStateText = computed(() => {
|
const loadStateText = computed(() => {
|
||||||
if (loading.value) {
|
if (isLoading.value) {
|
||||||
return "正在加载更多";
|
return "正在加载更多";
|
||||||
}
|
}
|
||||||
return noMore.value ? "没有更多记录" : "上拉加载更多";
|
return isNoMore.value ? "没有更多记录" : "上拉加载更多";
|
||||||
});
|
});
|
||||||
|
|
||||||
async function reloadRecords(): Promise<void> {
|
async function reloadRecords(): Promise<void> {
|
||||||
pageIndex.value = 0;
|
isRefreshing.value = true;
|
||||||
totalCount.value = 0;
|
try {
|
||||||
recordList.value = [];
|
page.index = 0;
|
||||||
noMore.value = false;
|
pageResult.total = 0;
|
||||||
|
pageResult.list = [];
|
||||||
|
isNoMore.value = false;
|
||||||
await loadRecords();
|
await loadRecords();
|
||||||
|
} finally {
|
||||||
|
isRefreshing.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadRecords(): Promise<void> {
|
async function loadRecords(): Promise<void> {
|
||||||
if (loading.value || noMore.value) {
|
if (isLoading.value || isNoMore.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const token = loadToken + 1;
|
const token = loadToken + 1;
|
||||||
loadToken = token;
|
loadToken = token;
|
||||||
loading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const result = await EventRecordAPI.rangePage({
|
const result = await EventRecordAPI.list({
|
||||||
index: pageIndex.value,
|
index: page.index,
|
||||||
size: pageSize,
|
size: page.size,
|
||||||
equalsExample: buildRangeReq()
|
...buildRangeReq()
|
||||||
});
|
});
|
||||||
if (token !== loadToken) {
|
if (token !== loadToken) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
recordList.value = recordList.value.concat(result.list || []);
|
pageResult.list = pageResult.list.concat(result.list || []);
|
||||||
totalCount.value = result.total || 0;
|
pageResult.total = result.total || 0;
|
||||||
pageIndex.value += 1;
|
page.index += 1;
|
||||||
noMore.value = recordList.value.length >= totalCount.value || (result.list || []).length < pageSize;
|
isNoMore.value = pageResult.list.length >= pageResult.total || (result.list || []).length < page.size;
|
||||||
} finally {
|
} finally {
|
||||||
if (token === loadToken) {
|
if (token === loadToken) {
|
||||||
loading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 操作 ----------
|
// ---------- 操作 ----------
|
||||||
|
|
||||||
const exporting = ref(false);
|
const isExporting = ref(false);
|
||||||
|
const isDeleting = ref(false);
|
||||||
const deletingId = ref("");
|
const deletingId = ref("");
|
||||||
|
|
||||||
async function loadActions(): Promise<void> {
|
async function loadActions(): Promise<void> {
|
||||||
eventList.value = await EventAPI.enabledList();
|
eventList.value = await EventAPI.listValid();
|
||||||
actionFilter.value = eventList.value.map(item => item.id).filter((item): item is string => Boolean(item));
|
actionFilter.value = eventList.value.map(item => item.id).filter((item): item is string => Boolean(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,27 +315,31 @@ async function selectDateFilter(type: DropdownValue): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function exportExcel(): Promise<void> {
|
async function exportExcel(): Promise<void> {
|
||||||
if (exporting.value) {
|
if (isExporting.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
exporting.value = true;
|
isExporting.value = true;
|
||||||
try {
|
try {
|
||||||
const blob = await EventRecordAPI.rangeExport(buildRangeReq());
|
const blob = await EventRecordAPI.exportXSSF({
|
||||||
|
index: 0,
|
||||||
|
size: pageSize,
|
||||||
|
...buildRangeReq()
|
||||||
|
});
|
||||||
Toolkit.downloadBlob(blob, `登记事件记录-${Time.formatDate(new Date())}.xlsx`);
|
Toolkit.downloadBlob(blob, `登记事件记录-${Time.formatDate(new Date())}.xlsx`);
|
||||||
showToast("导出已开始");
|
Toast.success("导出已开始");
|
||||||
} finally {
|
} finally {
|
||||||
exporting.value = false;
|
isExporting.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function goDetail(id: string): Promise<void> {
|
async function toDetail(id: string): Promise<void> {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await router.push(`/management/registration-event-records/${id}`);
|
await router.push(`/management/registration-event-records/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function goCreate(): Promise<void> {
|
async function toCreate(): Promise<void> {
|
||||||
await router.push("/management/registration-event-records/create");
|
await router.push("/management/registration-event-records/create");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,17 +365,19 @@ async function confirmRemoveRecord(record: EventRecordListItemView): Promise<voi
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function removeRecord(id: string): Promise<void> {
|
async function removeRecord(id: string): Promise<void> {
|
||||||
if (deletingId.value) {
|
if (isDeleting.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isDeleting.value = true;
|
||||||
deletingId.value = id;
|
deletingId.value = id;
|
||||||
try {
|
try {
|
||||||
await EventRecordAPI.remove(id);
|
await EventRecordAPI.remove(id);
|
||||||
recordList.value = recordList.value.filter(item => item.id !== id);
|
pageResult.list = pageResult.list.filter(item => item.id !== id);
|
||||||
totalCount.value = Math.max(0, totalCount.value - 1);
|
pageResult.total = Math.max(0, pageResult.total - 1);
|
||||||
showToast("登记记录已删除");
|
Toast.success("登记记录已删除");
|
||||||
} finally {
|
} finally {
|
||||||
|
isDeleting.value = false;
|
||||||
deletingId.value = "";
|
deletingId.value = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -384,19 +403,7 @@ function getRecordCustomerInitial(record: EventRecordListItemView): string {
|
|||||||
return (record.customerName || "客").slice(0, 1);
|
return (record.customerName || "客").slice(0, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTime(value?: number): string {
|
function buildRangeReq(): Omit<EventRecordPage, "index" | "size"> {
|
||||||
if (!value) {
|
|
||||||
return "-";
|
|
||||||
}
|
|
||||||
|
|
||||||
const date = Time.toDateObject(value);
|
|
||||||
if (!date) {
|
|
||||||
return "-";
|
|
||||||
}
|
|
||||||
return Time.formatDateTime(date);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildRangeReq(): EventRecordPage {
|
|
||||||
const [beginAt, endAt] = buildUnixRange();
|
const [beginAt, endAt] = buildUnixRange();
|
||||||
const selectedAllEvent = !actionFilter.value.length || actionFilter.value.length === eventList.value.length;
|
const selectedAllEvent = !actionFilter.value.length || actionFilter.value.length === eventList.value.length;
|
||||||
return {
|
return {
|
||||||
@@ -457,31 +464,19 @@ function parseDateTime(value: string, isEnd: boolean): Date {
|
|||||||
return new Date(year, month - 1, day, hour || 0, minute || 0, isEnd ? 59 : 0, 0);
|
return new Date(year, month - 1, day, hour || 0, minute || 0, isEnd ? 59 : 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showToast(message: string, theme: "success" | "warning" = "success"): void {
|
|
||||||
Toast({
|
|
||||||
message,
|
|
||||||
theme
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
navBarStore.clearRight();
|
navBarStore.clearRight();
|
||||||
if (canCreateRecord.value) {
|
if (canCreateRecord.value) {
|
||||||
navBarStore.setRightText("添加", "var(--td-brand-color)", goCreate);
|
navBarStore.setRightText("添加", "var(--td-brand-color)", toCreate);
|
||||||
}
|
}
|
||||||
await loadActions();
|
await loadActions();
|
||||||
initialized = true;
|
initialized = true;
|
||||||
await reloadRecords();
|
await reloadRecords();
|
||||||
Scroller.addBottomListener(scrollListenerName, loadRecords, {
|
|
||||||
canLoad: () => !loading.value && !noMore.value,
|
|
||||||
getElement: getPageScrollElement
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
navBarStore.clearRight();
|
navBarStore.clearRight();
|
||||||
window.clearTimeout(keywordTimer);
|
window.clearTimeout(keywordTimer);
|
||||||
Scroller.removeListener(scrollListenerName);
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -648,4 +643,3 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -39,13 +39,13 @@
|
|||||||
</template>
|
</template>
|
||||||
</t-cell>
|
</t-cell>
|
||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
<div v-else class="empty-block" v-text="loading ? '正在加载登记事件' : '暂无登记事件'" />
|
<div v-else class="empty-block" v-text="isLoading ? '正在加载登记事件' : '暂无登记事件'" />
|
||||||
</div>
|
</div>
|
||||||
<t-button
|
<t-button
|
||||||
class="save"
|
class="save"
|
||||||
theme="primary"
|
theme="primary"
|
||||||
:loading="saving"
|
:loading="isSaving"
|
||||||
:disabled="!sortDirty || saving || loading"
|
:disabled="!sortDirty || isSaving || isLoading"
|
||||||
@click="saveSort"
|
@click="saveSort"
|
||||||
>
|
>
|
||||||
保存排序
|
保存排序
|
||||||
@@ -65,12 +65,18 @@ defineOptions({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const navBarStore = useNavBarStore();
|
const navBarStore = useNavBarStore();
|
||||||
|
|
||||||
|
// ---------- 列表数据 ----------
|
||||||
|
|
||||||
const defaultColor = "#2f88ff";
|
const defaultColor = "#2f88ff";
|
||||||
const eventList = ref<EventItem[]>([]);
|
const eventList = ref<EventItem[]>([]);
|
||||||
const sortListRef = ref<HTMLElement>();
|
const sortListRef = ref<HTMLElement>();
|
||||||
const loading = ref(false);
|
const isLoading = ref(false);
|
||||||
const saving = ref(false);
|
const isSaving = ref(false);
|
||||||
const sortDirty = ref(false);
|
const sortDirty = ref(false);
|
||||||
|
|
||||||
|
// ---------- 拖拽状态 ----------
|
||||||
|
|
||||||
const draggingIndex = ref(-1);
|
const draggingIndex = ref(-1);
|
||||||
let pointerId: number | undefined;
|
let pointerId: number | undefined;
|
||||||
let pointerStartY = 0;
|
let pointerStartY = 0;
|
||||||
@@ -78,6 +84,8 @@ let pointerMoved = false;
|
|||||||
let pointerTarget: HTMLElement | undefined;
|
let pointerTarget: HTMLElement | undefined;
|
||||||
let dragInputType: "pointer" | "touch" | undefined;
|
let dragInputType: "pointer" | "touch" | undefined;
|
||||||
|
|
||||||
|
// ---------- 展示文案 ----------
|
||||||
|
|
||||||
function getStatusText(status?: EventStatus): string {
|
function getStatusText(status?: EventStatus): string {
|
||||||
if (status === "DRAFT") {
|
if (status === "DRAFT") {
|
||||||
return "草稿";
|
return "草稿";
|
||||||
@@ -98,6 +106,8 @@ function getStatusTheme(status?: EventStatus): TagProps["theme"] {
|
|||||||
return "primary";
|
return "primary";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 拖拽排序 ----------
|
||||||
|
|
||||||
function resolveDropIndex(clientY: number): number {
|
function resolveDropIndex(clientY: number): number {
|
||||||
const elements = sortListRef.value?.querySelectorAll<HTMLElement>(".item");
|
const elements = sortListRef.value?.querySelectorAll<HTMLElement>(".item");
|
||||||
if (!elements?.length) {
|
if (!elements?.length) {
|
||||||
@@ -152,7 +162,7 @@ function finishDragState(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startDrag(event: PointerEvent, index: number): void {
|
function startDrag(event: PointerEvent, index: number): void {
|
||||||
if (saving.value || loading.value || dragInputType || !event.isPrimary || event.button !== 0) {
|
if (isSaving.value || isLoading.value || dragInputType || !event.isPrimary || event.button !== 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +194,7 @@ function finishDrag(event: PointerEvent): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startTouchDrag(event: TouchEvent, index: number): void {
|
function startTouchDrag(event: TouchEvent, index: number): void {
|
||||||
if (saving.value || loading.value || dragInputType || event.touches.length !== 1) {
|
if (isSaving.value || isLoading.value || dragInputType || event.touches.length !== 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,12 +226,14 @@ function finishTouchDrag(event: TouchEvent): void {
|
|||||||
finishDragState();
|
finishDragState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 保存 ----------
|
||||||
|
|
||||||
async function saveSort(): Promise<void> {
|
async function saveSort(): Promise<void> {
|
||||||
if (!sortDirty.value || saving.value) {
|
if (!sortDirty.value || isSaving.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
saving.value = true;
|
isSaving.value = true;
|
||||||
try {
|
try {
|
||||||
const idList = eventList.value
|
const idList = eventList.value
|
||||||
.map(item => item.id)
|
.map(item => item.id)
|
||||||
@@ -231,24 +243,18 @@ async function saveSort(): Promise<void> {
|
|||||||
item.sort = index;
|
item.sort = index;
|
||||||
});
|
});
|
||||||
sortDirty.value = false;
|
sortDirty.value = false;
|
||||||
showToast("登记事件排序已保存");
|
Toast.success("登记事件排序已保存");
|
||||||
} catch {
|
} catch {
|
||||||
showToast("登记事件排序保存失败", "error");
|
Toast.error("登记事件排序保存失败");
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false;
|
isSaving.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showToast(message: string, theme: "success" | "error" = "success"): void {
|
// ---------- 页面数据 ----------
|
||||||
Toast({
|
|
||||||
theme,
|
|
||||||
direction: "row",
|
|
||||||
message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadData(): Promise<void> {
|
async function loadData(): Promise<void> {
|
||||||
loading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const result = await EventAPI.list({
|
const result = await EventAPI.list({
|
||||||
index: 0,
|
index: 0,
|
||||||
@@ -257,7 +263,7 @@ async function loadData(): Promise<void> {
|
|||||||
eventList.value = result.list || [];
|
eventList.value = result.list || [];
|
||||||
sortDirty.value = false;
|
sortDirty.value = false;
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,7 +272,7 @@ onMounted(async () => {
|
|||||||
try {
|
try {
|
||||||
await loadData();
|
await loadData();
|
||||||
} catch {
|
} catch {
|
||||||
showToast("登记事件加载失败", "error");
|
Toast.error("登记事件加载失败");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -351,4 +357,3 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
v-model:selected-event-id-list="selectedEventIdList"
|
v-model:selected-event-id-list="selectedEventIdList"
|
||||||
:event-list="eventList"
|
:event-list="eventList"
|
||||||
empty-text="当前月份暂无登记事件记录"
|
empty-text="当前月份暂无登记事件记录"
|
||||||
:loading="calendarLoading"
|
:loading="isCalendarLoading"
|
||||||
:records="monthRecords"
|
:records="monthRecords"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -35,7 +35,6 @@ import { init, use } from "echarts/core";
|
|||||||
import { CanvasRenderer } from "echarts/renderers";
|
import { CanvasRenderer } from "echarts/renderers";
|
||||||
import CustomerAPI from "@/api/CustomerAPI";
|
import CustomerAPI from "@/api/CustomerAPI";
|
||||||
import EventAPI from "@/api/EventAPI";
|
import EventAPI from "@/api/EventAPI";
|
||||||
import EventRecordAPI from "@/api/EventRecordAPI";
|
|
||||||
import EventRecordCalendar from "@/components/EventRecordCalendar.vue";
|
import EventRecordCalendar from "@/components/EventRecordCalendar.vue";
|
||||||
import type { CustomerDailyStatView, CustomerGenderStatView } from "@/types/Customer";
|
import type { CustomerDailyStatView, CustomerGenderStatView } from "@/types/Customer";
|
||||||
import type { EventItem } from "@/types/EventItem";
|
import type { EventItem } from "@/types/EventItem";
|
||||||
@@ -69,14 +68,17 @@ use([
|
|||||||
|
|
||||||
const chartRefs = new Map<string, HTMLElement>();
|
const chartRefs = new Map<string, HTMLElement>();
|
||||||
const chartInstances = new Map<string, ECharts>();
|
const chartInstances = new Map<string, ECharts>();
|
||||||
|
|
||||||
|
// ---------- 数据源 ----------
|
||||||
|
|
||||||
const eventList = ref<EventItem[]>([]);
|
const eventList = ref<EventItem[]>([]);
|
||||||
const selectedEventIdList = ref<string[]>([]);
|
const selectedEventIdList = ref<string[]>([]);
|
||||||
const customerDailyStatList = ref<CustomerDailyStatView[]>([]);
|
const customerDailyStatList = ref<CustomerDailyStatView[]>([]);
|
||||||
const customerGenderStatList = ref<CustomerGenderStatView[]>([]);
|
const customerGenderStatList = ref<CustomerGenderStatView[]>([]);
|
||||||
const recordDailyStatMap = ref<Record<string, EventRecordDailyStatView[]>>({});
|
const recordDailyStatMap = ref<Record<string, EventRecordDailyStatView[]>>({});
|
||||||
const monthRecords = ref<EventRecord[]>([]);
|
const monthRecords = ref<EventRecord[]>([]);
|
||||||
const loading = ref(false);
|
const isLoading = ref(false);
|
||||||
const calendarLoading = ref(false);
|
const isCalendarLoading = ref(false);
|
||||||
const monthCursor = ref(getMonthStart(new Date()));
|
const monthCursor = ref(getMonthStart(new Date()));
|
||||||
const genderColorMap = {
|
const genderColorMap = {
|
||||||
MALE: "#2563eb",
|
MALE: "#2563eb",
|
||||||
@@ -91,6 +93,8 @@ const timestampRange = {
|
|||||||
endAt: Time.toTimestamp(endDate)
|
endAt: Time.toTimestamp(endDate)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ---------- 摘要 ----------
|
||||||
|
|
||||||
const summaryList = computed<SummaryItem[]>(() => [
|
const summaryList = computed<SummaryItem[]>(() => [
|
||||||
{
|
{
|
||||||
label: "当前客户",
|
label: "当前客户",
|
||||||
@@ -106,6 +110,8 @@ const summaryList = computed<SummaryItem[]>(() => [
|
|||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// ---------- 图表配置 ----------
|
||||||
|
|
||||||
const chartList = computed<ChartProfile[]>(() => [
|
const chartList = computed<ChartProfile[]>(() => [
|
||||||
{
|
{
|
||||||
id: "customer-growth",
|
id: "customer-growth",
|
||||||
@@ -134,6 +140,8 @@ watch(chartList, async () => {
|
|||||||
renderCharts();
|
renderCharts();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------- 日历 ----------
|
||||||
|
|
||||||
watch(monthCursor, async () => {
|
watch(monthCursor, async () => {
|
||||||
await loadMonthRecords();
|
await loadMonthRecords();
|
||||||
});
|
});
|
||||||
@@ -152,6 +160,8 @@ onBeforeUnmount(() => {
|
|||||||
chartInstances.clear();
|
chartInstances.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------- 图表实例 ----------
|
||||||
|
|
||||||
function setChartRef(id: string, element: Element | ComponentPublicInstance | null): void {
|
function setChartRef(id: string, element: Element | ComponentPublicInstance | null): void {
|
||||||
if (element instanceof HTMLElement) {
|
if (element instanceof HTMLElement) {
|
||||||
chartRefs.set(id, element);
|
chartRefs.set(id, element);
|
||||||
@@ -185,10 +195,12 @@ function resizeCharts(): void {
|
|||||||
chartInstances.forEach(item => item.resize());
|
chartInstances.forEach(item => item.resize());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 页面数据 ----------
|
||||||
|
|
||||||
async function loadChartsData(): Promise<void> {
|
async function loadChartsData(): Promise<void> {
|
||||||
loading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
eventList.value = await EventAPI.enabledList();
|
eventList.value = await EventAPI.listValid();
|
||||||
const valideventList = eventList.value.filter((item): item is EventItem & { id: string } => Boolean(item.id));
|
const valideventList = eventList.value.filter((item): item is EventItem & { id: string } => Boolean(item.id));
|
||||||
selectedEventIdList.value = valideventList.map(item => item.id);
|
selectedEventIdList.value = valideventList.map(item => item.id);
|
||||||
const [customerStatList, genderStatList, recordDailyStatList] = await Promise.all([
|
const [customerStatList, genderStatList, recordDailyStatList] = await Promise.all([
|
||||||
@@ -199,36 +211,35 @@ async function loadChartsData(): Promise<void> {
|
|||||||
CustomerAPI.genderStat(),
|
CustomerAPI.genderStat(),
|
||||||
Promise.all(valideventList.map(async item => ({
|
Promise.all(valideventList.map(async item => ({
|
||||||
eventId: item.id,
|
eventId: item.id,
|
||||||
stats: await EventRecordAPI.allCustomerEventDailyStat({
|
stats: []
|
||||||
eventId: item.id,
|
|
||||||
beginAt: timestampRange.beginAt,
|
|
||||||
endAt: timestampRange.endAt
|
|
||||||
})
|
|
||||||
})))
|
})))
|
||||||
]);
|
]);
|
||||||
customerDailyStatList.value = customerStatList;
|
customerDailyStatList.value = customerStatList;
|
||||||
customerGenderStatList.value = genderStatList;
|
customerGenderStatList.value = genderStatList;
|
||||||
recordDailyStatMap.value = Object.fromEntries(recordDailyStatList.map(item => [item.eventId, item.stats]));
|
recordDailyStatMap.value = Object.fromEntries(recordDailyStatList.map(item => [item.eventId, item.stats]));
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadMonthRecords(): Promise<void> {
|
async function loadMonthRecords(): Promise<void> {
|
||||||
const begin = getCalendarBeginDate(monthCursor.value);
|
const begin = getCalendarBeginDate(monthCursor.value);
|
||||||
const end = getDateEnd(addDays(begin, 41));
|
const end = getDateEnd(addDays(begin, 41));
|
||||||
calendarLoading.value = true;
|
isCalendarLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const calendarRecordList = await EventRecordAPI.calendarRangeList({
|
await Promise.resolve({
|
||||||
beginAt: Time.toTimestamp(begin),
|
beginAt: Time.toTimestamp(begin),
|
||||||
endAt: Time.toTimestamp(end)
|
endAt: Time.toTimestamp(end)
|
||||||
});
|
});
|
||||||
|
const calendarRecordList: EventRecordCalendarView[] = [];
|
||||||
monthRecords.value = calendarRecordList.map(buildCalendarRecord);
|
monthRecords.value = calendarRecordList.map(buildCalendarRecord);
|
||||||
} finally {
|
} finally {
|
||||||
calendarLoading.value = false;
|
isCalendarLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 图表构建 ----------
|
||||||
|
|
||||||
function buildBaseOption(): EChartsOption {
|
function buildBaseOption(): EChartsOption {
|
||||||
return {
|
return {
|
||||||
color: ["#2563eb", "#0f766e", "#d97706", "#db2777", "#7c3aed"],
|
color: ["#2563eb", "#0f766e", "#d97706", "#db2777", "#7c3aed"],
|
||||||
@@ -381,6 +392,8 @@ function buildGenderStatOption(): EChartsOption {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 统计计算 ----------
|
||||||
|
|
||||||
function getLatestTotalCustomerCount(): number {
|
function getLatestTotalCustomerCount(): number {
|
||||||
const latest = customerDailyStatList.value[customerDailyStatList.value.length - 1];
|
const latest = customerDailyStatList.value[customerDailyStatList.value.length - 1];
|
||||||
return latest?.totalCustomerCount || 0;
|
return latest?.totalCustomerCount || 0;
|
||||||
@@ -416,6 +429,8 @@ function getGenderCount(genderCode: "MALE" | "FEMALE" | "UNKNOWN"): number {
|
|||||||
return customerGenderStatList.value.find(item => item.genderCode === genderCode)?.count || 0;
|
return customerGenderStatList.value.find(item => item.genderCode === genderCode)?.count || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 日历转换 ----------
|
||||||
|
|
||||||
function buildCalendarRecord(record: EventRecordCalendarView): EventRecord {
|
function buildCalendarRecord(record: EventRecordCalendarView): EventRecord {
|
||||||
return {
|
return {
|
||||||
id: record.id,
|
id: record.id,
|
||||||
@@ -430,6 +445,8 @@ function buildCalendarRecord(record: EventRecordCalendarView): EventRecord {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 日期工具 ----------
|
||||||
|
|
||||||
function getDateStart(date: Date): Date {
|
function getDateStart(date: Date): Date {
|
||||||
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0);
|
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0);
|
||||||
}
|
}
|
||||||
@@ -547,4 +564,3 @@ function buildDateTextList(begin: Date, end: Date): string[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ defineOptions({
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
// ---------- 表单 ----------
|
||||||
|
|
||||||
const isSaving = ref(false);
|
const isSaving = ref(false);
|
||||||
const formModel = reactive({
|
const formModel = reactive({
|
||||||
oldPassword: "",
|
oldPassword: "",
|
||||||
@@ -32,21 +35,15 @@ const formModel = reactive({
|
|||||||
confirmPassword: ""
|
confirmPassword: ""
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------- 保存 ----------
|
||||||
|
|
||||||
async function savePassword(): Promise<void> {
|
async function savePassword(): Promise<void> {
|
||||||
if (!formModel.oldPassword || !formModel.newPassword) {
|
if (!formModel.oldPassword || !formModel.newPassword) {
|
||||||
Toast({
|
Toast.warning("请填写旧密码和新密码");
|
||||||
theme: "warning",
|
|
||||||
direction: "row",
|
|
||||||
message: "请填写旧密码和新密码"
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (formModel.newPassword !== formModel.confirmPassword) {
|
if (formModel.newPassword !== formModel.confirmPassword) {
|
||||||
Toast({
|
Toast.warning("两次输入的新密码不一致");
|
||||||
theme: "warning",
|
|
||||||
direction: "row",
|
|
||||||
message: "两次输入的新密码不一致"
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
isSaving.value = true;
|
isSaving.value = true;
|
||||||
@@ -56,11 +53,7 @@ async function savePassword(): Promise<void> {
|
|||||||
newValue: formModel.newPassword
|
newValue: formModel.newPassword
|
||||||
});
|
});
|
||||||
userStore.clearLoginUser();
|
userStore.clearLoginUser();
|
||||||
Toast({
|
Toast.success("密码已更新,请重新登录");
|
||||||
theme: "success",
|
|
||||||
direction: "row",
|
|
||||||
message: "密码已更新,请重新登录"
|
|
||||||
});
|
|
||||||
await router.replace("/login");
|
await router.replace("/login");
|
||||||
} finally {
|
} finally {
|
||||||
isSaving.value = false;
|
isSaving.value = false;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
add-text="更换"
|
add-text="更换"
|
||||||
alt-text="用户头像"
|
alt-text="用户头像"
|
||||||
size="compact"
|
size="compact"
|
||||||
:loading="avatarUploading"
|
:loading="isAvatarUploading"
|
||||||
:preview="false"
|
:preview="false"
|
||||||
:show-remove="false"
|
:show-remove="false"
|
||||||
@add="changeAvatar"
|
@add="changeAvatar"
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
@change="handleAvatarSelected"
|
@change="handleAvatarSelected"
|
||||||
>
|
>
|
||||||
<t-cell-group theme="card">
|
<t-cell-group theme="card">
|
||||||
<t-cell title="修改密码" arrow hover @click="goPassword" />
|
<t-cell title="修改密码" arrow hover @click="toPassword" />
|
||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
<t-button class="save" theme="primary" :loading="isSaving" @click="saveProfile">保存</t-button>
|
<t-button class="save" theme="primary" :loading="isSaving" @click="saveProfile">保存</t-button>
|
||||||
<t-button class="logout bg-white" theme="danger" variant="outline" @click="confirmLogout">退出登录</t-button>
|
<t-button class="logout bg-white" theme="danger" variant="outline" @click="confirmLogout">退出登录</t-button>
|
||||||
@@ -65,9 +65,15 @@ defineOptions({
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
// ---------- 头像 ----------
|
||||||
|
|
||||||
const avatarInputRef = ref<HTMLInputElement>();
|
const avatarInputRef = ref<HTMLInputElement>();
|
||||||
const avatarTempFileId = ref("");
|
const avatarTempFileId = ref("");
|
||||||
const avatarUploading = ref(false);
|
const isAvatarUploading = ref(false);
|
||||||
|
|
||||||
|
// ---------- 表单 ----------
|
||||||
|
|
||||||
const isSaving = ref(false);
|
const isSaving = ref(false);
|
||||||
|
|
||||||
const formModel = reactive({
|
const formModel = reactive({
|
||||||
@@ -85,6 +91,8 @@ const formModel = reactive({
|
|||||||
lastLoginAt: ""
|
lastLoginAt: ""
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------- 页面数据 ----------
|
||||||
|
|
||||||
function fillForm(): void {
|
function fillForm(): void {
|
||||||
const user = userStore.loginUser.user;
|
const user = userStore.loginUser.user;
|
||||||
Object.assign(formModel, {
|
Object.assign(formModel, {
|
||||||
@@ -103,6 +111,8 @@ function fillForm(): void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 头像 ----------
|
||||||
|
|
||||||
function changeAvatar(): void {
|
function changeAvatar(): void {
|
||||||
avatarInputRef.value?.click();
|
avatarInputRef.value?.click();
|
||||||
}
|
}
|
||||||
@@ -113,37 +123,33 @@ async function handleAvatarSelected(event: Event): Promise<void> {
|
|||||||
if (!file) {
|
if (!file) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
avatarUploading.value = true;
|
isAvatarUploading.value = true;
|
||||||
try {
|
try {
|
||||||
const compressedFile = await compressUploadImage(file);
|
const compressedFile = await compressUploadImage(file);
|
||||||
const [tempFile] = await AttachmentAPI.uploadTemp(compressedFile);
|
const [tempFile] = await AttachmentAPI.upload(compressedFile);
|
||||||
if (!tempFile?.id) {
|
if (!tempFile?.id) {
|
||||||
Toast({
|
Toast.error("头像上传失败");
|
||||||
theme: "error",
|
|
||||||
direction: "row",
|
|
||||||
message: "头像上传失败"
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
avatarTempFileId.value = tempFile.id;
|
avatarTempFileId.value = tempFile.id;
|
||||||
formModel.avatar = AttachmentAPI.getTempReadURL(tempFile.id);
|
formModel.avatar = AttachmentAPI.getTempReadURL(tempFile.id);
|
||||||
} finally {
|
} finally {
|
||||||
avatarUploading.value = false;
|
isAvatarUploading.value = false;
|
||||||
input.value = "";
|
input.value = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function goPassword(): Promise<void> {
|
// ---------- 导航 ----------
|
||||||
|
|
||||||
|
async function toPassword(): Promise<void> {
|
||||||
await router.push("/management/profile/password");
|
await router.push("/management/profile/password");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 保存 ----------
|
||||||
|
|
||||||
async function saveProfile(): Promise<void> {
|
async function saveProfile(): Promise<void> {
|
||||||
if (!formModel.name) {
|
if (!formModel.name) {
|
||||||
Toast({
|
Toast.warning("请填写用户名");
|
||||||
theme: "warning",
|
|
||||||
direction: "row",
|
|
||||||
message: "请填写用户名"
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
isSaving.value = true;
|
isSaving.value = true;
|
||||||
@@ -156,17 +162,15 @@ async function saveProfile(): Promise<void> {
|
|||||||
tempFileIdList: avatarTempFileId.value ? [avatarTempFileId.value] : undefined
|
tempFileIdList: avatarTempFileId.value ? [avatarTempFileId.value] : undefined
|
||||||
});
|
});
|
||||||
await userStore.updateLoginUser(result);
|
await userStore.updateLoginUser(result);
|
||||||
Toast({
|
Toast.success("资料已保存");
|
||||||
theme: "success",
|
|
||||||
direction: "row",
|
|
||||||
message: "资料已保存"
|
|
||||||
});
|
|
||||||
await router.back();
|
await router.back();
|
||||||
} finally {
|
} finally {
|
||||||
isSaving.value = false;
|
isSaving.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 退出登录 ----------
|
||||||
|
|
||||||
async function confirmLogout(): Promise<void> {
|
async function confirmLogout(): Promise<void> {
|
||||||
const dialog = DialogPlugin.confirm({
|
const dialog = DialogPlugin.confirm({
|
||||||
title: "退出登录",
|
title: "退出登录",
|
||||||
@@ -188,6 +192,8 @@ async function logout(): Promise<void> {
|
|||||||
await router.replace("/login");
|
await router.replace("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 格式化 ----------
|
||||||
|
|
||||||
function sexToText(sex?: number): "男" | "女" | "未知" {
|
function sexToText(sex?: number): "男" | "女" | "未知" {
|
||||||
if (sex === 1) {
|
if (sex === 1) {
|
||||||
return "男";
|
return "男";
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ defineOptions({
|
|||||||
|
|
||||||
const globalUIStore = useGlobalUIStore();
|
const globalUIStore = useGlobalUIStore();
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
|
|
||||||
|
// ---------- 主题 ----------
|
||||||
|
|
||||||
const themeOptions: Array<{ label: string; value: ThemeMode }> = [
|
const themeOptions: Array<{ label: string; value: ThemeMode }> = [
|
||||||
{ label: "浅色", value: "light" },
|
{ label: "浅色", value: "light" },
|
||||||
{ label: "深色", value: "dark" },
|
{ label: "深色", value: "dark" },
|
||||||
@@ -63,6 +66,8 @@ function handleThemeChange(value: string): void {
|
|||||||
globalUIStore.setThemeMode(value as ThemeMode);
|
globalUIStore.setThemeMode(value as ThemeMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 声音 ----------
|
||||||
|
|
||||||
async function handleScanSuccessChange(value: boolean): Promise<void> {
|
async function handleScanSuccessChange(value: boolean): Promise<void> {
|
||||||
settingStore.setSound({
|
settingStore.setSound({
|
||||||
scanSuccess: value
|
scanSuccess: value
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ defineOptions({
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const permissionStore = usePermissionSettingsStore();
|
const permissionStore = usePermissionSettingsStore();
|
||||||
|
|
||||||
|
// ---------- 路由权限 ----------
|
||||||
|
|
||||||
const routeId = computed(() => String(route.params.id || ""));
|
const routeId = computed(() => String(route.params.id || ""));
|
||||||
const isCreateMode = computed(() => !routeId.value);
|
const isCreateMode = computed(() => !routeId.value);
|
||||||
const savePermissionCode = computed(() => isCreateMode.value ? corePermissionCodes.permission.create : corePermissionCodes.permission.update);
|
const savePermissionCode = computed(() => isCreateMode.value ? corePermissionCodes.permission.create : corePermissionCodes.permission.update);
|
||||||
@@ -37,6 +40,9 @@ const pageAccess = computed(() => ({
|
|||||||
value: [corePermissionCodes.role.read, corePermissionCodes.permission.read, savePermissionCode.value],
|
value: [corePermissionCodes.role.read, corePermissionCodes.permission.read, savePermissionCode.value],
|
||||||
mode: "and" as const
|
mode: "and" as const
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// ---------- 表单 ----------
|
||||||
|
|
||||||
const formModel = reactive({
|
const formModel = reactive({
|
||||||
id: "",
|
id: "",
|
||||||
moduleCode: moduleCode,
|
moduleCode: moduleCode,
|
||||||
@@ -45,6 +51,8 @@ const formModel = reactive({
|
|||||||
description: ""
|
description: ""
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------- 页面数据 ----------
|
||||||
|
|
||||||
async function fillForm(): Promise<void> {
|
async function fillForm(): Promise<void> {
|
||||||
await permissionStore.loadAll();
|
await permissionStore.loadAll();
|
||||||
const current = await permissionStore.loadPermissionDetail(routeId.value);
|
const current = await permissionStore.loadPermissionDetail(routeId.value);
|
||||||
@@ -57,6 +65,8 @@ async function fillForm(): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 保存 ----------
|
||||||
|
|
||||||
async function savePermission(): Promise<void> {
|
async function savePermission(): Promise<void> {
|
||||||
const permission = await permissionStore.upsertPermission({
|
const permission = await permissionStore.upsertPermission({
|
||||||
id: formModel.id,
|
id: formModel.id,
|
||||||
@@ -65,11 +75,7 @@ async function savePermission(): Promise<void> {
|
|||||||
name: formModel.name,
|
name: formModel.name,
|
||||||
description: formModel.description
|
description: formModel.description
|
||||||
}, isCreateMode.value);
|
}, isCreateMode.value);
|
||||||
Toast({
|
Toast.success("权限已保存");
|
||||||
theme: "success",
|
|
||||||
direction: "row",
|
|
||||||
message: "权限已保存"
|
|
||||||
});
|
|
||||||
await router.push(isCreateMode.value ? "/management/permissions/items" : `/management/permissions/items/${permission?.id || formModel.id}`);
|
await router.push(isCreateMode.value ? "/management/permissions/items" : `/management/permissions/items/${permission?.id || formModel.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-role="permissionManagementRoleAccess" v-can="{ value: [corePermissionCodes.role.read, corePermissionCodes.permission.read], mode: 'and' }" class="permission-list">
|
<div v-role="permissionManagementRoleAccess" v-can="{ value: [corePermissionCodes.role.read, corePermissionCodes.permission.read], mode: 'and' }" class="permission-list">
|
||||||
|
<t-pull-down-refresh
|
||||||
|
v-model="isRefreshing"
|
||||||
|
:refresh-timeout="30000"
|
||||||
|
@refresh="refresh"
|
||||||
|
>
|
||||||
<t-cell-group v-if="sortedPermissions.length" theme="card">
|
<t-cell-group v-if="sortedPermissions.length" theme="card">
|
||||||
<t-swipe-cell
|
<t-swipe-cell
|
||||||
v-for="item in sortedPermissions"
|
v-for="item in sortedPermissions"
|
||||||
@@ -19,7 +24,8 @@
|
|||||||
</t-swipe-cell>
|
</t-swipe-cell>
|
||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
<t-empty v-else-if="!permissionStore.loading" description="暂无权限" />
|
<t-empty v-else-if="!permissionStore.loading" description="暂无权限" />
|
||||||
<t-button v-can="corePermissionCodes.permission.create" class="add" theme="primary" type="button" @click="goCreate">
|
</t-pull-down-refresh>
|
||||||
|
<t-button v-can="corePermissionCodes.permission.create" class="add" theme="primary" type="button" @click="router.push('/management/permissions/items/create')">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<t-icon name="add" />
|
<t-icon name="add" />
|
||||||
</template>
|
</template>
|
||||||
@@ -39,16 +45,27 @@ defineOptions({
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const permissionStore = usePermissionSettingsStore();
|
const permissionStore = usePermissionSettingsStore();
|
||||||
const sortedPermissions = computed<Permission[]>(() => [...permissionStore.permissionList].sort((left, right) => left.code.localeCompare(right.code)));
|
const isRefreshing = ref(false);
|
||||||
|
|
||||||
async function goCreate(): Promise<void> {
|
// ---------- 列表数据 ----------
|
||||||
await router.push("/management/permissions/items/create");
|
|
||||||
}
|
const sortedPermissions = computed<Permission[]>(() => [...permissionStore.permissionList].sort((left, right) => left.code.localeCompare(right.code)));
|
||||||
|
|
||||||
async function removePermission(id: string): Promise<void> {
|
async function removePermission(id: string): Promise<void> {
|
||||||
await permissionStore.removePermission(id);
|
await permissionStore.removePermission(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 页面数据 ----------
|
||||||
|
|
||||||
|
async function refresh(): Promise<void> {
|
||||||
|
isRefreshing.value = true;
|
||||||
|
try {
|
||||||
|
await permissionStore.loadAll(true);
|
||||||
|
} finally {
|
||||||
|
isRefreshing.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await permissionStore.loadAll();
|
await permissionStore.loadAll();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,22 +4,22 @@
|
|||||||
class="permission-settings"
|
class="permission-settings"
|
||||||
>
|
>
|
||||||
<t-cell-group title="权限配置" theme="card">
|
<t-cell-group title="权限配置" theme="card">
|
||||||
<t-cell v-can="corePermissionCodes.role.read" title="角色管理" :note="`${permissionStore.roleList.length} 个角色`" arrow hover @click="go('/management/permissions/roles')">
|
<t-cell v-can="corePermissionCodes.role.read" title="角色管理" :note="`${permissionStore.roleList.length} 个角色`" arrow hover @click="router.push('/management/permissions/roles')">
|
||||||
<template #leftIcon>
|
<template #leftIcon>
|
||||||
<t-icon name="user-setting" size="1.75rem" />
|
<t-icon name="user-setting" size="1.75rem" />
|
||||||
</template>
|
</template>
|
||||||
</t-cell>
|
</t-cell>
|
||||||
<t-cell v-can="corePermissionCodes.permission.read" title="权限管理" :note="`${permissionStore.permissionList.length} 个权限`" arrow hover @click="go('/management/permissions/items')">
|
<t-cell v-can="corePermissionCodes.permission.read" title="权限管理" :note="`${permissionStore.permissionList.length} 个权限`" arrow hover @click="router.push('/management/permissions/items')">
|
||||||
<template #leftIcon>
|
<template #leftIcon>
|
||||||
<t-icon name="secured" size="1.75rem" />
|
<t-icon name="secured" size="1.75rem" />
|
||||||
</template>
|
</template>
|
||||||
</t-cell>
|
</t-cell>
|
||||||
<t-cell v-can="rolePermissionAssignAccess" title="角色权限授权" note="为角色分配可用权限" arrow hover @click="go('/management/permissions/assign')">
|
<t-cell v-can="rolePermissionAssignAccess" title="角色权限授权" note="为角色分配可用权限" arrow hover @click="router.push('/management/permissions/assign')">
|
||||||
<template #leftIcon>
|
<template #leftIcon>
|
||||||
<t-icon name="user-checked" size="1.75rem" />
|
<t-icon name="user-checked" size="1.75rem" />
|
||||||
</template>
|
</template>
|
||||||
</t-cell>
|
</t-cell>
|
||||||
<t-cell v-can="corePermissionCodes.userRole.read" title="用户角色授权" note="为用户分配业务角色" arrow hover @click="go('/management/permissions/user-roles')">
|
<t-cell v-can="corePermissionCodes.userRole.read" title="用户角色授权" note="为用户分配业务角色" arrow hover @click="router.push('/management/permissions/user-roles')">
|
||||||
<template #leftIcon>
|
<template #leftIcon>
|
||||||
<t-icon name="usergroup" size="1.75rem" />
|
<t-icon name="usergroup" size="1.75rem" />
|
||||||
</template>
|
</template>
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
:note="`${item.permissionIdList.length} 项权限`"
|
:note="`${item.permissionIdList.length} 项权限`"
|
||||||
arrow
|
arrow
|
||||||
hover
|
hover
|
||||||
@click="goAssign(item.id)"
|
@click="toAssign(item.id)"
|
||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="title" v-text="item.name" :style="{ paddingLeft: `${item.level * 1.25}rem` }" />
|
<div class="title" v-text="item.name" :style="{ paddingLeft: `${item.level * 1.25}rem` }" />
|
||||||
@@ -59,6 +59,9 @@ defineOptions({
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const permissionStore = usePermissionSettingsStore();
|
const permissionStore = usePermissionSettingsStore();
|
||||||
|
|
||||||
|
// ---------- 权限入口 ----------
|
||||||
|
|
||||||
const rolePermissionAssignAccess = {
|
const rolePermissionAssignAccess = {
|
||||||
value: [
|
value: [
|
||||||
corePermissionCodes.role.read,
|
corePermissionCodes.role.read,
|
||||||
@@ -69,11 +72,7 @@ const rolePermissionAssignAccess = {
|
|||||||
mode: "and"
|
mode: "and"
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
async function go(path: string): Promise<void> {
|
async function toAssign(roleId: string): Promise<void> {
|
||||||
await router.push(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function goAssign(roleId: string): Promise<void> {
|
|
||||||
await router.push({
|
await router.push({
|
||||||
path: "/management/permissions/assign",
|
path: "/management/permissions/assign",
|
||||||
query: {
|
query: {
|
||||||
@@ -82,6 +81,8 @@ async function goAssign(roleId: string): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 页面数据 ----------
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await permissionStore.loadAll();
|
await permissionStore.loadAll();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ defineOptions({
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const permissionStore = usePermissionSettingsStore();
|
const permissionStore = usePermissionSettingsStore();
|
||||||
|
|
||||||
|
// ---------- 路由权限 ----------
|
||||||
|
|
||||||
const routeId = computed(() => String(route.params.id || ""));
|
const routeId = computed(() => String(route.params.id || ""));
|
||||||
const isCreateMode = computed(() => !routeId.value);
|
const isCreateMode = computed(() => !routeId.value);
|
||||||
const savePermissionCode = computed(() => isCreateMode.value ? corePermissionCodes.role.create : corePermissionCodes.role.update);
|
const savePermissionCode = computed(() => isCreateMode.value ? corePermissionCodes.role.create : corePermissionCodes.role.update);
|
||||||
@@ -55,6 +58,9 @@ const pageAccess = computed(() => ({
|
|||||||
value: [corePermissionCodes.role.read, corePermissionCodes.permission.read, savePermissionCode.value],
|
value: [corePermissionCodes.role.read, corePermissionCodes.permission.read, savePermissionCode.value],
|
||||||
mode: "and" as const
|
mode: "and" as const
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// ---------- 表单 ----------
|
||||||
|
|
||||||
const formModel = reactive({
|
const formModel = reactive({
|
||||||
id: "",
|
id: "",
|
||||||
moduleCode: moduleCode,
|
moduleCode: moduleCode,
|
||||||
@@ -63,6 +69,9 @@ const formModel = reactive({
|
|||||||
description: "",
|
description: "",
|
||||||
childRoleIdList: [] as string[]
|
childRoleIdList: [] as string[]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------- 子角色 ----------
|
||||||
|
|
||||||
const childOptions = computed(() => permissionStore.roleList.filter(item => item.id !== formModel.id && !hasChildRole(item.id, formModel.id)));
|
const childOptions = computed(() => permissionStore.roleList.filter(item => item.id !== formModel.id && !hasChildRole(item.id, formModel.id)));
|
||||||
|
|
||||||
function hasChildRole(roleId: string, targetRoleId: string, visitedRoleIds = new Set<string>()): boolean {
|
function hasChildRole(roleId: string, targetRoleId: string, visitedRoleIds = new Set<string>()): boolean {
|
||||||
@@ -79,6 +88,8 @@ function hasChildRole(roleId: string, targetRoleId: string, visitedRoleIds = new
|
|||||||
return current.childRoleIdList.some(childRoleId => childRoleId === targetRoleId || hasChildRole(childRoleId, targetRoleId, visitedRoleIds));
|
return current.childRoleIdList.some(childRoleId => childRoleId === targetRoleId || hasChildRole(childRoleId, targetRoleId, visitedRoleIds));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 页面数据 ----------
|
||||||
|
|
||||||
async function fillForm(): Promise<void> {
|
async function fillForm(): Promise<void> {
|
||||||
await permissionStore.loadAll();
|
await permissionStore.loadAll();
|
||||||
const current = await permissionStore.loadRoleDetail(routeId.value);
|
const current = await permissionStore.loadRoleDetail(routeId.value);
|
||||||
@@ -92,6 +103,8 @@ async function fillForm(): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 保存 ----------
|
||||||
|
|
||||||
async function saveRole(): Promise<void> {
|
async function saveRole(): Promise<void> {
|
||||||
const role = await permissionStore.upsertRole({
|
const role = await permissionStore.upsertRole({
|
||||||
id: formModel.id,
|
id: formModel.id,
|
||||||
@@ -102,11 +115,7 @@ async function saveRole(): Promise<void> {
|
|||||||
permissionIdList: permissionStore.getRoleById(formModel.id)?.permissionIdList || [],
|
permissionIdList: permissionStore.getRoleById(formModel.id)?.permissionIdList || [],
|
||||||
childRoleIdList: [...formModel.childRoleIdList]
|
childRoleIdList: [...formModel.childRoleIdList]
|
||||||
}, isCreateMode.value);
|
}, isCreateMode.value);
|
||||||
Toast({
|
Toast.success("角色已保存");
|
||||||
theme: "success",
|
|
||||||
direction: "row",
|
|
||||||
message: "角色已保存"
|
|
||||||
});
|
|
||||||
await router.push(isCreateMode.value ? "/management/permissions/roles" : `/management/permissions/roles/${role?.id || formModel.id}`);
|
await router.push(isCreateMode.value ? "/management/permissions/roles" : `/management/permissions/roles/${role?.id || formModel.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-role="permissionManagementRoleAccess" v-can="{ value: [corePermissionCodes.role.read, corePermissionCodes.permission.read], mode: 'and' }" class="role-list">
|
<div v-role="permissionManagementRoleAccess" v-can="{ value: [corePermissionCodes.role.read, corePermissionCodes.permission.read], mode: 'and' }" class="role-list">
|
||||||
|
<t-pull-down-refresh
|
||||||
|
v-model="isRefreshing"
|
||||||
|
:refresh-timeout="30000"
|
||||||
|
@refresh="refresh"
|
||||||
|
>
|
||||||
<t-cell-group v-if="sortedRoles.length" theme="card">
|
<t-cell-group v-if="sortedRoles.length" theme="card">
|
||||||
<t-swipe-cell
|
<t-swipe-cell
|
||||||
v-for="item in sortedRoles"
|
v-for="item in sortedRoles"
|
||||||
@@ -11,7 +16,7 @@
|
|||||||
:note="item.code"
|
:note="item.code"
|
||||||
arrow
|
arrow
|
||||||
hover
|
hover
|
||||||
@click="goEdit(item.id)"
|
@click="router.push(`/management/permissions/roles/${item.id}`)"
|
||||||
/>
|
/>
|
||||||
<template #right>
|
<template #right>
|
||||||
<t-button v-can="corePermissionCodes.role.delete" class="delete" theme="danger" @click="removeRole(item.id)">删除</t-button>
|
<t-button v-can="corePermissionCodes.role.delete" class="delete" theme="danger" @click="removeRole(item.id)">删除</t-button>
|
||||||
@@ -19,7 +24,8 @@
|
|||||||
</t-swipe-cell>
|
</t-swipe-cell>
|
||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
<t-empty v-else-if="!permissionStore.loading" description="暂无角色" />
|
<t-empty v-else-if="!permissionStore.loading" description="暂无角色" />
|
||||||
<t-button v-can="corePermissionCodes.role.create" class="add" theme="primary" type="button" @click="goCreate">
|
</t-pull-down-refresh>
|
||||||
|
<t-button v-can="corePermissionCodes.role.create" class="add" theme="primary" type="button" @click="router.push('/management/permissions/roles/create')">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<t-icon name="add" />
|
<t-icon name="add" />
|
||||||
</template>
|
</template>
|
||||||
@@ -39,20 +45,27 @@ defineOptions({
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const permissionStore = usePermissionSettingsStore();
|
const permissionStore = usePermissionSettingsStore();
|
||||||
|
const isRefreshing = ref(false);
|
||||||
|
|
||||||
|
// ---------- 列表数据 ----------
|
||||||
|
|
||||||
const sortedRoles = computed<Role[]>(() => [...permissionStore.roleList].sort((left, right) => (left.name || "").localeCompare(right.name || "")));
|
const sortedRoles = computed<Role[]>(() => [...permissionStore.roleList].sort((left, right) => (left.name || "").localeCompare(right.name || "")));
|
||||||
|
|
||||||
async function goCreate(): Promise<void> {
|
|
||||||
await router.push("/management/permissions/roles/create");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function goEdit(id: string): Promise<void> {
|
|
||||||
await router.push(`/management/permissions/roles/${id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeRole(id: string): Promise<void> {
|
async function removeRole(id: string): Promise<void> {
|
||||||
await permissionStore.removeRole(id);
|
await permissionStore.removeRole(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 页面数据 ----------
|
||||||
|
|
||||||
|
async function refresh(): Promise<void> {
|
||||||
|
isRefreshing.value = true;
|
||||||
|
try {
|
||||||
|
await permissionStore.loadAll(true);
|
||||||
|
} finally {
|
||||||
|
isRefreshing.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await permissionStore.loadAll();
|
await permissionStore.loadAll();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ defineOptions({
|
|||||||
const permissionStore = usePermissionSettingsStore();
|
const permissionStore = usePermissionSettingsStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
|
// ---------- 角色选择 ----------
|
||||||
|
|
||||||
const activeRoleId = ref(String(route.query.roleId || ""));
|
const activeRoleId = ref(String(route.query.roleId || ""));
|
||||||
const checkedPermissionIds = ref<string[]>([]);
|
const checkedPermissionIds = ref<string[]>([]);
|
||||||
const showChildRolePermissions = ref(false);
|
const showChildRolePermissions = ref(false);
|
||||||
@@ -74,6 +77,9 @@ const roleOptions = computed<DropdownOption[]>(() => permissionStore.roleList
|
|||||||
value: item.id,
|
value: item.id,
|
||||||
disabled: false
|
disabled: false
|
||||||
})));
|
})));
|
||||||
|
|
||||||
|
// ---------- 权限状态 ----------
|
||||||
|
|
||||||
const checkedPermissionIdSet = computed(() => new Set(checkedPermissionIds.value));
|
const checkedPermissionIdSet = computed(() => new Set(checkedPermissionIds.value));
|
||||||
const childRolePermissionIdSet = computed(() => {
|
const childRolePermissionIdSet = computed(() => {
|
||||||
if (!activeRole.value) {
|
if (!activeRole.value) {
|
||||||
@@ -82,6 +88,8 @@ const childRolePermissionIdSet = computed(() => {
|
|||||||
return collectChildRolePermissionIds(activeRole.value.childRoleIdList, new Set<string>());
|
return collectChildRolePermissionIds(activeRole.value.childRoleIdList, new Set<string>());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------- 角色切换 ----------
|
||||||
|
|
||||||
async function handleRoleChange(value: DropdownValue): Promise<void> {
|
async function handleRoleChange(value: DropdownValue): Promise<void> {
|
||||||
activeRoleId.value = Array.isArray(value) ? String(value[0] || "") : String(value);
|
activeRoleId.value = Array.isArray(value) ? String(value[0] || "") : String(value);
|
||||||
await loadActiveRolePermissions();
|
await loadActiveRolePermissions();
|
||||||
@@ -104,15 +112,15 @@ async function loadActiveRolePermissions(): Promise<void> {
|
|||||||
checkedPermissionIds.value = [...role.permissionIdList];
|
checkedPermissionIds.value = [...role.permissionIdList];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 保存 ----------
|
||||||
|
|
||||||
async function savePermissionList(): Promise<void> {
|
async function savePermissionList(): Promise<void> {
|
||||||
await permissionStore.setRolePermissionList(activeRoleId.value, checkedPermissionIds.value);
|
await permissionStore.setRolePermissionList(activeRoleId.value, checkedPermissionIds.value);
|
||||||
Toast({
|
Toast.success("授权已保存");
|
||||||
theme: "success",
|
|
||||||
direction: "row",
|
|
||||||
message: "授权已保存"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 权限勾选 ----------
|
||||||
|
|
||||||
function isPermissionChecked(permissionId: string): boolean {
|
function isPermissionChecked(permissionId: string): boolean {
|
||||||
return checkedPermissionIdSet.value.has(permissionId);
|
return checkedPermissionIdSet.value.has(permissionId);
|
||||||
}
|
}
|
||||||
@@ -140,6 +148,8 @@ function setPermissionChecked(permissionId: string, value: boolean): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 子角色继承 ----------
|
||||||
|
|
||||||
function collectChildRolePermissionIds(roleIdList: string[], visitedRoleIdSet: Set<string>): Set<string> {
|
function collectChildRolePermissionIds(roleIdList: string[], visitedRoleIdSet: Set<string>): Set<string> {
|
||||||
const permissionIdSet = new Set<string>();
|
const permissionIdSet = new Set<string>();
|
||||||
for (const roleId of roleIdList) {
|
for (const roleId of roleIdList) {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
note="为用户授权业务角色"
|
note="为用户授权业务角色"
|
||||||
arrow
|
arrow
|
||||||
hover
|
hover
|
||||||
@click="goRoleAssign"
|
@click="toRoleAssign"
|
||||||
>
|
>
|
||||||
<template #leftIcon>
|
<template #leftIcon>
|
||||||
<t-icon name="usergroup" size="1.75rem" />
|
<t-icon name="usergroup" size="1.75rem" />
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
<div v-if="!scopeList.length" class="empty-card">未配置</div>
|
<div v-if="!scopeList.length" class="empty-card">未配置</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<t-button v-can="permissionCodes.user.update" class="edit" theme="primary" @click="goEdit">编辑用户</t-button>
|
<t-button v-can="permissionCodes.user.update" class="edit" theme="primary" @click="toEdit">编辑用户</t-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -77,6 +77,9 @@ defineOptions({
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const permissionStore = usePermissionSettingsStore();
|
const permissionStore = usePermissionSettingsStore();
|
||||||
|
|
||||||
|
// ---------- 用户数据 ----------
|
||||||
|
|
||||||
const managedUser = ref<ManagedUser>();
|
const managedUser = ref<ManagedUser>();
|
||||||
const eventList = ref<EventItem[]>([]);
|
const eventList = ref<EventItem[]>([]);
|
||||||
|
|
||||||
@@ -105,7 +108,9 @@ const scopeList = computed(() => {
|
|||||||
.map(item => item.name);
|
.map(item => item.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
async function goEdit(): Promise<void> {
|
// ---------- 导航 ----------
|
||||||
|
|
||||||
|
async function toEdit(): Promise<void> {
|
||||||
if (!currentUser.value) {
|
if (!currentUser.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -113,7 +118,7 @@ async function goEdit(): Promise<void> {
|
|||||||
await router.push(`/management/users/${currentUser.value.id}/edit`);
|
await router.push(`/management/users/${currentUser.value.id}/edit`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function goRoleAssign(): Promise<void> {
|
async function toRoleAssign(): Promise<void> {
|
||||||
if (!currentUser.value) {
|
if (!currentUser.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -123,6 +128,8 @@ async function goRoleAssign(): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 头像 ----------
|
||||||
|
|
||||||
function buildFallbackAvatar(name: string): string {
|
function buildFallbackAvatar(name: string): string {
|
||||||
const text = encodeURIComponent((name.trim() || "用户").slice(0, 2));
|
const text = encodeURIComponent((name.trim() || "用户").slice(0, 2));
|
||||||
return [
|
return [
|
||||||
@@ -144,6 +151,8 @@ function isUser(user?: Partial<User>): user is User {
|
|||||||
return typeof user?.name === "string";
|
return typeof user?.name === "string";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 事件范围 ----------
|
||||||
|
|
||||||
function canEventItem(action: EventItem, item: ManagedUser): boolean {
|
function canEventItem(action: EventItem, item: ManagedUser): boolean {
|
||||||
const userRoleCodeSet = buildEffectiveRoleCodeSet(item, permissionStore.roleList);
|
const userRoleCodeSet = buildEffectiveRoleCodeSet(item, permissionStore.roleList);
|
||||||
const roleCodeList = action.roleCodeList || [];
|
const roleCodeList = action.roleCodeList || [];
|
||||||
@@ -153,11 +162,13 @@ function canEventItem(action: EventItem, item: ManagedUser): boolean {
|
|||||||
return roleCodeList.some(code => userRoleCodeSet.has(code));
|
return roleCodeList.some(code => userRoleCodeSet.has(code));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 页面数据 ----------
|
||||||
|
|
||||||
async function loadData(): Promise<void> {
|
async function loadData(): Promise<void> {
|
||||||
const routeId = String(route.params.id || "");
|
const routeId = String(route.params.id || "");
|
||||||
const [detail, actions] = await Promise.all([
|
const [detail, actions] = await Promise.all([
|
||||||
ManagedUserAPI.detail(routeId),
|
ManagedUserAPI.detail(routeId),
|
||||||
EventAPI.enabledList(),
|
EventAPI.listValid(),
|
||||||
permissionStore.loadAll()
|
permissionStore.loadAll()
|
||||||
]);
|
]);
|
||||||
managedUser.value = detail;
|
managedUser.value = detail;
|
||||||
@@ -168,11 +179,7 @@ onMounted(async () => {
|
|||||||
try {
|
try {
|
||||||
await loadData();
|
await loadData();
|
||||||
} catch {
|
} catch {
|
||||||
Toast({
|
Toast.error("用户详情加载失败");
|
||||||
theme: "error",
|
|
||||||
direction: "row",
|
|
||||||
message: "用户详情加载失败"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -242,4 +249,3 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ defineOptions({
|
|||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
// ---------- 表单 ----------
|
||||||
|
|
||||||
const currentManagedUser = ref<ManagedUser>();
|
const currentManagedUser = ref<ManagedUser>();
|
||||||
const isSaving = ref(false);
|
const isSaving = ref(false);
|
||||||
const formModel = reactive({
|
const formModel = reactive({
|
||||||
@@ -39,6 +42,8 @@ const formModel = reactive({
|
|||||||
const isCreateMode = computed(() => !String(route.params.id || ""));
|
const isCreateMode = computed(() => !String(route.params.id || ""));
|
||||||
const savePermissionCode = computed(() => isCreateMode.value ? permissionCodes.user.create : permissionCodes.user.update);
|
const savePermissionCode = computed(() => isCreateMode.value ? permissionCodes.user.create : permissionCodes.user.update);
|
||||||
|
|
||||||
|
// ---------- 页面数据 ----------
|
||||||
|
|
||||||
async function fillForm(): Promise<void> {
|
async function fillForm(): Promise<void> {
|
||||||
const routeId = String(route.params.id || "");
|
const routeId = String(route.params.id || "");
|
||||||
if (routeId) {
|
if (routeId) {
|
||||||
@@ -63,27 +68,16 @@ async function fillForm(): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 保存 ----------
|
||||||
|
|
||||||
async function saveUser(): Promise<void> {
|
async function saveUser(): Promise<void> {
|
||||||
if (!formModel.name) {
|
if (!validateForm()) {
|
||||||
Toast({
|
|
||||||
theme: "warning",
|
|
||||||
direction: "row",
|
|
||||||
message: "请填写姓名"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isCreateMode.value && !formModel.password) {
|
|
||||||
Toast({
|
|
||||||
theme: "warning",
|
|
||||||
direction: "row",
|
|
||||||
message: "请填写密码"
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
isSaving.value = true;
|
isSaving.value = true;
|
||||||
try {
|
try {
|
||||||
const payload: ManagedUser = {
|
const formData: ManagedUser = {
|
||||||
id: formModel.id || undefined,
|
id: formModel.id || undefined,
|
||||||
userId: formModel.userId || undefined,
|
userId: formModel.userId || undefined,
|
||||||
enabled: currentManagedUser.value?.enabled ?? true,
|
enabled: currentManagedUser.value?.enabled ?? true,
|
||||||
@@ -96,27 +90,31 @@ async function saveUser(): Promise<void> {
|
|||||||
password: isCreateMode.value ? formModel.password : undefined
|
password: isCreateMode.value ? formModel.password : undefined
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const saved = isCreateMode.value ? await ManagedUserAPI.create(payload) : await ManagedUserAPI.update(payload);
|
const saved = isCreateMode.value ? await ManagedUserAPI.create(formData) : await ManagedUserAPI.update(formData);
|
||||||
Toast({
|
Toast.success("用户已保存");
|
||||||
theme: "success",
|
|
||||||
direction: "row",
|
|
||||||
message: "用户已保存"
|
|
||||||
});
|
|
||||||
await router.push(`/management/users/${saved.id}`);
|
await router.push(`/management/users/${saved.id}`);
|
||||||
} finally {
|
} finally {
|
||||||
isSaving.value = false;
|
isSaving.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateForm(): boolean {
|
||||||
|
if (!formModel.name) {
|
||||||
|
Toast.warning("请填写姓名");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isCreateMode.value && !formModel.password) {
|
||||||
|
Toast.warning("请填写密码");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
await fillForm();
|
await fillForm();
|
||||||
} catch {
|
} catch {
|
||||||
Toast({
|
Toast.error("用户数据加载失败");
|
||||||
theme: "error",
|
|
||||||
direction: "row",
|
|
||||||
message: "用户数据加载失败"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-can="permissionCodes.user.read" class="user-list">
|
<div v-can="permissionCodes.user.read" class="user-list">
|
||||||
<div v-for="item in userList" :key="item.id" class="item" @click="goDetail(item)">
|
<t-pull-down-refresh
|
||||||
|
v-model="isRefreshing"
|
||||||
|
:refresh-timeout="30000"
|
||||||
|
@refresh="refresh"
|
||||||
|
@scrolltolower="fetch"
|
||||||
|
>
|
||||||
|
<div v-for="item in pageResult.list" :key="item.id" class="item" @click="toDetail(item)">
|
||||||
<t-image :src="buildUserAvatar(item)" :alt="getUserName(item)" fit="cover" class="avatar" />
|
<t-image :src="buildUserAvatar(item)" :alt="getUserName(item)" fit="cover" class="avatar" />
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
@@ -10,7 +16,10 @@
|
|||||||
<span class="events gray" v-text="`可登记事件:${getActionScope(item)}`" />
|
<span class="events gray" v-text="`可登记事件:${getActionScope(item)}`" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<t-button v-can="permissionCodes.user.create" class="add" theme="primary" type="button" @click="goCreate">
|
<div v-if="!pageResult.list.length" class="empty-block" v-text="isLoading ? '正在加载用户' : '暂无用户'" />
|
||||||
|
<div v-if="pageResult.list.length" class="load-state" v-text="isNoMore ? '已经到底了' : '上拉加载更多'" />
|
||||||
|
</t-pull-down-refresh>
|
||||||
|
<t-button v-can="permissionCodes.user.create" class="add" theme="primary" type="button" @click="router.push('/management/users/create')">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<t-icon name="add" />
|
<t-icon name="add" />
|
||||||
</template>
|
</template>
|
||||||
@@ -21,7 +30,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Toast } from "tdesign-mobile-vue";
|
import { Toast } from "tdesign-mobile-vue";
|
||||||
import { UserAPI, type User } from "timi-web";
|
import { type Page, type PageResult, UserAPI, type User } from "timi-web";
|
||||||
import EventAPI from "@/api/EventAPI";
|
import EventAPI from "@/api/EventAPI";
|
||||||
import ManagedUserAPI from "@/api/ManagedUserAPI";
|
import ManagedUserAPI from "@/api/ManagedUserAPI";
|
||||||
import type { ManagedUser } from "@/types/ManagedUser";
|
import type { ManagedUser } from "@/types/ManagedUser";
|
||||||
@@ -36,8 +45,23 @@ defineOptions({
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const permissionStore = usePermissionSettingsStore();
|
const permissionStore = usePermissionSettingsStore();
|
||||||
const userList = ref<ManagedUser[]>([]);
|
|
||||||
|
// ---------- 列表数据 ----------
|
||||||
|
|
||||||
const eventList = ref<EventItem[]>([]);
|
const eventList = ref<EventItem[]>([]);
|
||||||
|
const isRefreshing = ref(false);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const isNoMore = ref(false);
|
||||||
|
const page = reactive<Page<ManagedUser>>({
|
||||||
|
index: 0,
|
||||||
|
size: 20
|
||||||
|
});
|
||||||
|
const pageResult = reactive<PageResult<ManagedUser>>({
|
||||||
|
total: 0,
|
||||||
|
list: []
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------- 展示文案 ----------
|
||||||
|
|
||||||
function getUserName(item: ManagedUser): string {
|
function getUserName(item: ManagedUser): string {
|
||||||
return item.user?.nick || item.user?.name || "未命名用户";
|
return item.user?.nick || item.user?.name || "未命名用户";
|
||||||
@@ -54,6 +78,8 @@ function getActionScope(item: ManagedUser): string {
|
|||||||
return nameList.join("、") || "未配置";
|
return nameList.join("、") || "未配置";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 事件范围 ----------
|
||||||
|
|
||||||
function canEventItem(action: EventItem, item: ManagedUser): boolean {
|
function canEventItem(action: EventItem, item: ManagedUser): boolean {
|
||||||
const userRoleCodeSet = buildEffectiveRoleCodeSet(item, permissionStore.roleList);
|
const userRoleCodeSet = buildEffectiveRoleCodeSet(item, permissionStore.roleList);
|
||||||
const roleCodeList = action.roleCodeList || [];
|
const roleCodeList = action.roleCodeList || [];
|
||||||
@@ -63,6 +89,8 @@ function canEventItem(action: EventItem, item: ManagedUser): boolean {
|
|||||||
return roleCodeList.some(code => userRoleCodeSet.has(code));
|
return roleCodeList.some(code => userRoleCodeSet.has(code));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 头像 ----------
|
||||||
|
|
||||||
function buildUserAvatar(item: ManagedUser): string {
|
function buildUserAvatar(item: ManagedUser): string {
|
||||||
return isUser(item.user) ? UserAPI.getAvatarURL(item.user) || buildFallbackAvatar(getUserName(item)) : buildFallbackAvatar(getUserName(item));
|
return isUser(item.user) ? UserAPI.getAvatarURL(item.user) || buildFallbackAvatar(getUserName(item)) : buildFallbackAvatar(getUserName(item));
|
||||||
}
|
}
|
||||||
@@ -83,24 +111,54 @@ function buildFallbackAvatar(name: string): string {
|
|||||||
].join("");
|
].join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 页面数据 ----------
|
||||||
|
|
||||||
async function loadData(): Promise<void> {
|
async function loadData(): Promise<void> {
|
||||||
const [userResult, actions] = await Promise.all([
|
const [userResult, actions] = await Promise.all([
|
||||||
ManagedUserAPI.list({
|
ManagedUserAPI.list(page),
|
||||||
index: 0,
|
EventAPI.listValid(),
|
||||||
size: 100
|
|
||||||
}),
|
|
||||||
EventAPI.enabledList(),
|
|
||||||
permissionStore.loadAll()
|
permissionStore.loadAll()
|
||||||
]);
|
]);
|
||||||
userList.value = userResult.list || [];
|
pageResult.total = userResult.total;
|
||||||
|
pageResult.list = userResult.list || [];
|
||||||
|
page.index = 1;
|
||||||
|
isNoMore.value = pageResult.list.length >= pageResult.total || pageResult.list.length < page.size;
|
||||||
eventList.value = actions || [];
|
eventList.value = actions || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function goCreate(): Promise<void> {
|
async function refresh(): Promise<void> {
|
||||||
await router.push("/management/users/create");
|
isRefreshing.value = true;
|
||||||
|
try {
|
||||||
|
page.index = 0;
|
||||||
|
pageResult.total = 0;
|
||||||
|
pageResult.list = [];
|
||||||
|
isNoMore.value = false;
|
||||||
|
await fetch();
|
||||||
|
} finally {
|
||||||
|
isRefreshing.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function goDetail(item: ManagedUser): Promise<void> {
|
async function fetch(): Promise<void> {
|
||||||
|
if (isLoading.value || isNoMore.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
const result = await ManagedUserAPI.list(page);
|
||||||
|
pageResult.total = result.total;
|
||||||
|
pageResult.list = pageResult.list.concat(result.list || []);
|
||||||
|
page.index += 1;
|
||||||
|
isNoMore.value = pageResult.list.length >= pageResult.total || (result.list || []).length < page.size;
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 导航 ----------
|
||||||
|
|
||||||
|
async function toDetail(item: ManagedUser): Promise<void> {
|
||||||
if (!item.id) {
|
if (!item.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -112,11 +170,7 @@ onMounted(async () => {
|
|||||||
try {
|
try {
|
||||||
await loadData();
|
await loadData();
|
||||||
} catch {
|
} catch {
|
||||||
Toast({
|
Toast.error("用户列表加载失败");
|
||||||
theme: "error",
|
|
||||||
direction: "row",
|
|
||||||
message: "用户列表加载失败"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -193,4 +247,3 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -26,12 +26,12 @@
|
|||||||
</template>
|
</template>
|
||||||
</t-cell>
|
</t-cell>
|
||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
<t-empty v-else-if="!loading" description="暂无可授权角色" />
|
<t-empty v-else-if="!isLoading" description="暂无可授权角色" />
|
||||||
<t-button
|
<t-button
|
||||||
v-can="userRoleWriteAccess"
|
v-can="userRoleWriteAccess"
|
||||||
class="save"
|
class="save"
|
||||||
theme="primary"
|
theme="primary"
|
||||||
:loading="saving"
|
:loading="isSaving"
|
||||||
@click="saveRoleList"
|
@click="saveRoleList"
|
||||||
>
|
>
|
||||||
保存授权
|
保存授权
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { DropdownOption, DropdownValue } from "tdesign-mobile-vue";
|
import type { DropdownOption, DropdownValue } from "tdesign-mobile-vue";
|
||||||
import { Toast } from "tdesign-mobile-vue";
|
import { Toast } from "tdesign-mobile-vue";
|
||||||
import { OrderType, RoleAPI, type ModuleCode, type Role } from "timi-web";
|
import { RoleAPI, type ModuleCode, type Role } from "timi-web";
|
||||||
import ManagedUserAPI from "@/api/ManagedUserAPI";
|
import ManagedUserAPI from "@/api/ManagedUserAPI";
|
||||||
import type { ManagedUser } from "@/types/ManagedUser";
|
import type { ManagedUser } from "@/types/ManagedUser";
|
||||||
import { corePermissionCodes } from "@/utils/permission";
|
import { corePermissionCodes } from "@/utils/permission";
|
||||||
@@ -54,15 +54,20 @@ defineOptions({
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const moduleCode: ModuleCode = "GAO";
|
const moduleCode: ModuleCode = "GAO";
|
||||||
const pageSize = 1000;
|
const pageSize = 1000;
|
||||||
|
|
||||||
|
// ---------- 用户选择 ----------
|
||||||
|
|
||||||
const userList = ref<ManagedUser[]>([]);
|
const userList = ref<ManagedUser[]>([]);
|
||||||
const roleList = ref<Role[]>([]);
|
const roleList = ref<Role[]>([]);
|
||||||
const routeManagedUserId = computed(() => String(route.params.id || route.query.managedUserId || ""));
|
const routeManagedUserId = computed(() => String(route.params.id || route.query.managedUserId || ""));
|
||||||
const activeManagedUserId = ref(routeManagedUserId.value);
|
const activeManagedUserId = ref(routeManagedUserId.value);
|
||||||
const checkedRoleIds = ref<string[]>([]);
|
const checkedRoleIds = ref<string[]>([]);
|
||||||
const loading = ref(false);
|
const isLoading = ref(false);
|
||||||
const saving = ref(false);
|
const isSaving = ref(false);
|
||||||
let roleSyncSeq = 0;
|
let roleSyncSeq = 0;
|
||||||
|
|
||||||
|
// ---------- 权限 ----------
|
||||||
|
|
||||||
const userRoleWriteAccess = {
|
const userRoleWriteAccess = {
|
||||||
value: [
|
value: [
|
||||||
corePermissionCodes.userRole.create,
|
corePermissionCodes.userRole.create,
|
||||||
@@ -70,6 +75,9 @@ const userRoleWriteAccess = {
|
|||||||
],
|
],
|
||||||
mode: "and"
|
mode: "and"
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
// ---------- 用户状态 ----------
|
||||||
|
|
||||||
const activeUser = computed(() => userList.value.find(item => item.id === activeManagedUserId.value));
|
const activeUser = computed(() => userList.value.find(item => item.id === activeManagedUserId.value));
|
||||||
const activeUserLabel = computed(() => activeUser.value ? `当前用户:${getUserName(activeUser.value)}` : "选择用户");
|
const activeUserLabel = computed(() => activeUser.value ? `当前用户:${getUserName(activeUser.value)}` : "选择用户");
|
||||||
const userOptions = computed<DropdownOption[]>(() => userList.value
|
const userOptions = computed<DropdownOption[]>(() => userList.value
|
||||||
@@ -80,13 +88,17 @@ const userOptions = computed<DropdownOption[]>(() => userList.value
|
|||||||
disabled: false
|
disabled: false
|
||||||
})));
|
})));
|
||||||
|
|
||||||
|
// ---------- 用户切换 ----------
|
||||||
|
|
||||||
async function handleUserChange(value: DropdownValue): Promise<void> {
|
async function handleUserChange(value: DropdownValue): Promise<void> {
|
||||||
activeManagedUserId.value = Array.isArray(value) ? String(value[0] || "") : String(value);
|
activeManagedUserId.value = Array.isArray(value) ? String(value[0] || "") : String(value);
|
||||||
await loadActiveUserRoles();
|
await loadActiveUserRoles();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 页面数据 ----------
|
||||||
|
|
||||||
async function loadData(): Promise<void> {
|
async function loadData(): Promise<void> {
|
||||||
loading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const [userResult, roles] = await Promise.all([
|
const [userResult, roles] = await Promise.all([
|
||||||
ManagedUserAPI.list({
|
ManagedUserAPI.list({
|
||||||
@@ -102,10 +114,12 @@ async function loadData(): Promise<void> {
|
|||||||
}
|
}
|
||||||
await loadActiveUserRoles();
|
await loadActiveUserRoles();
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 角色同步 ----------
|
||||||
|
|
||||||
async function loadActiveUserRoles(): Promise<void> {
|
async function loadActiveUserRoles(): Promise<void> {
|
||||||
const managedUserId = activeManagedUserId.value;
|
const managedUserId = activeManagedUserId.value;
|
||||||
const managedUser = activeUser.value;
|
const managedUser = activeUser.value;
|
||||||
@@ -124,38 +138,36 @@ async function loadActiveUserRoles(): Promise<void> {
|
|||||||
checkedRoleIds.value = roles.map(getRoleId).filter((item): item is string => !!item);
|
checkedRoleIds.value = roles.map(getRoleId).filter((item): item is string => !!item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 保存 ----------
|
||||||
|
|
||||||
async function saveRoleList(): Promise<void> {
|
async function saveRoleList(): Promise<void> {
|
||||||
const current = activeUser.value;
|
const current = activeUser.value;
|
||||||
if (!current?.id || !current.userId) {
|
if (!current?.id || !current.userId) {
|
||||||
Toast({
|
Toast.warning("请选择用户");
|
||||||
theme: "warning",
|
|
||||||
direction: "row",
|
|
||||||
message: "请选择用户"
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
saving.value = true;
|
isSaving.value = true;
|
||||||
try {
|
try {
|
||||||
await RoleAPI.authorize({
|
await RoleAPI.authorize({
|
||||||
userId: current.userId,
|
userId: current.userId,
|
||||||
moduleCode: moduleCode,
|
moduleCode: moduleCode,
|
||||||
roleIdList: [...checkedRoleIds.value]
|
roleIdList: [...checkedRoleIds.value]
|
||||||
});
|
});
|
||||||
Toast({
|
Toast.success("用户角色已保存");
|
||||||
theme: "success",
|
|
||||||
direction: "row",
|
|
||||||
message: "用户角色已保存"
|
|
||||||
});
|
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false;
|
isSaving.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 展示文案 ----------
|
||||||
|
|
||||||
function getUserName(item: ManagedUser): string {
|
function getUserName(item: ManagedUser): string {
|
||||||
return item.user?.nick || item.user?.name || item.user?.phoneNo || "未命名用户";
|
return item.user?.nick || item.user?.name || item.user?.phoneNo || "未命名用户";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 角色列表 ----------
|
||||||
|
|
||||||
async function loadRoleList(): Promise<Role[]> {
|
async function loadRoleList(): Promise<Role[]> {
|
||||||
const result = await RoleAPI.list({
|
const result = await RoleAPI.list({
|
||||||
index: 0,
|
index: 0,
|
||||||
@@ -164,12 +176,14 @@ async function loadRoleList(): Promise<Role[]> {
|
|||||||
moduleCode: moduleCode
|
moduleCode: moduleCode
|
||||||
},
|
},
|
||||||
orderMap: {
|
orderMap: {
|
||||||
code: OrderType.ASC
|
code: "ASC"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return result.list || [];
|
return result.list || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 角色勾选 ----------
|
||||||
|
|
||||||
function isRoleChecked(roleId: string): boolean {
|
function isRoleChecked(roleId: string): boolean {
|
||||||
return checkedRoleIds.value.includes(roleId);
|
return checkedRoleIds.value.includes(roleId);
|
||||||
}
|
}
|
||||||
@@ -198,11 +212,7 @@ onMounted(async () => {
|
|||||||
try {
|
try {
|
||||||
await loadData();
|
await loadData();
|
||||||
} catch {
|
} catch {
|
||||||
Toast({
|
Toast.error("用户角色加载失败");
|
||||||
theme: "error",
|
|
||||||
direction: "row",
|
|
||||||
message: "用户角色加载失败"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import {
|
import {
|
||||||
OrderType,
|
|
||||||
PermissionAPI,
|
PermissionAPI,
|
||||||
RoleAPI,
|
RoleAPI,
|
||||||
type ModuleCode,
|
type ModuleCode,
|
||||||
@@ -105,7 +104,7 @@ export const usePermissionSettingsStore = defineStore("permission-settings", ()
|
|||||||
moduleCode: moduleCode
|
moduleCode: moduleCode
|
||||||
},
|
},
|
||||||
orderMap: {
|
orderMap: {
|
||||||
code: OrderType.ASC
|
code: "ASC"
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
PermissionAPI.list({
|
PermissionAPI.list({
|
||||||
@@ -115,7 +114,7 @@ export const usePermissionSettingsStore = defineStore("permission-settings", ()
|
|||||||
moduleCode: moduleCode
|
moduleCode: moduleCode
|
||||||
},
|
},
|
||||||
orderMap: {
|
orderMap: {
|
||||||
code: OrderType.ASC
|
code: "ASC"
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|||||||
+8
-18
@@ -1,4 +1,5 @@
|
|||||||
import type { Attachment, Model, User } from "timi-web";
|
import type { Attachment, Model, User } from "timi-web";
|
||||||
|
import type { EventRecord } from "@/types/EventRecord.ts";
|
||||||
|
|
||||||
export type CustomerGender = "MALE" | "FEMALE";
|
export type CustomerGender = "MALE" | "FEMALE";
|
||||||
export type CustomerBirthdateCalendar = "SOLAR" | "LUNAR";
|
export type CustomerBirthdateCalendar = "SOLAR" | "LUNAR";
|
||||||
@@ -8,25 +9,26 @@ export type BodyAreaValue = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type Customer = {
|
export type Customer = {
|
||||||
code?: string | null;
|
code?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
gender?: CustomerGender;
|
gender?: CustomerGender;
|
||||||
age?: number;
|
age?: number;
|
||||||
birthdate?: string | null;
|
birthdate?: string;
|
||||||
birthdateCalendar?: CustomerBirthdateCalendar;
|
birthdateCalendar?: CustomerBirthdateCalendar;
|
||||||
telephone?: string;
|
telephone?: string;
|
||||||
address?: string;
|
address?: string;
|
||||||
remark?: string;
|
remark?: string;
|
||||||
disease?: string;
|
disease?: string;
|
||||||
|
invitedCount?: number;
|
||||||
conditioningPart?: BodyAreaValue;
|
conditioningPart?: BodyAreaValue;
|
||||||
introducerCustomerId?: string | null;
|
introducerCustomerId?: string;
|
||||||
creatorUserId?: string;
|
creatorUserId?: string;
|
||||||
lastSignInAt?: number;
|
|
||||||
introducerCustomer?: Customer;
|
introducerCustomer?: Customer;
|
||||||
creatorUser?: Partial<User>;
|
creatorUser?: Partial<User>;
|
||||||
invitedCount?: number;
|
eventRecord?: EventRecord;
|
||||||
|
eventRecordList?: EventRecord[];
|
||||||
attachmentList?: Attachment[];
|
attachmentList?: Attachment[];
|
||||||
attachmentIdList?: string[];
|
|
||||||
} & Model;
|
} & Model;
|
||||||
|
|
||||||
export type CustomerSearchReq = {
|
export type CustomerSearchReq = {
|
||||||
@@ -49,15 +51,3 @@ export type CustomerGenderStatView = {
|
|||||||
genderCode?: CustomerGender | "UNKNOWN";
|
genderCode?: CustomerGender | "UNKNOWN";
|
||||||
count?: number;
|
count?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CustomerInviteUpdateReq = {
|
|
||||||
customerId: string;
|
|
||||||
introducerCustomerId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QuickRegisterReq = {
|
|
||||||
customerCode: string;
|
|
||||||
eventId: string;
|
|
||||||
remark?: string;
|
|
||||||
attachmentIdList?: string[];
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import type { Attachment, Model, User } from "timi-web";
|
import type { Attachment, BasePage, Model, User } from "timi-web";
|
||||||
import type { Customer } from "@/types/Customer";
|
import type { Customer } from "@/types/Customer";
|
||||||
import type { EventItem } from "@/types/EventItem";
|
import type { EventItem } from "@/types/EventItem";
|
||||||
|
|
||||||
export type EventRecord = {
|
export type EventRecord = {
|
||||||
customerId?: string;
|
customerId?: string;
|
||||||
|
customerIdList?: string[];
|
||||||
eventId?: string;
|
eventId?: string;
|
||||||
operatorUserId?: string;
|
operatorUserId?: string;
|
||||||
remark?: string;
|
remark?: string;
|
||||||
registeredAt?: number;
|
registeredAt?: number;
|
||||||
|
|
||||||
customer?: Customer;
|
customer?: Customer;
|
||||||
event?: EventItem;
|
event?: EventItem;
|
||||||
operator?: Partial<User>;
|
operator?: Partial<User>;
|
||||||
attachmentList?: Array<Partial<Attachment>>;
|
attachmentList?: Attachment[];
|
||||||
attachmentIdList?: string[];
|
|
||||||
} & Model;
|
} & Model;
|
||||||
|
|
||||||
export type EventRecordListItemView = {
|
export type EventRecordListItemView = {
|
||||||
@@ -26,59 +27,21 @@ export type EventRecordListItemView = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type EventRecordPage = {
|
export type EventRecordPage = {
|
||||||
customerId?: string;
|
|
||||||
eventId?: string;
|
|
||||||
eventIdList?: string[];
|
|
||||||
keyword?: string;
|
|
||||||
beginAt: number;
|
|
||||||
endAt: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type EventRecordPageReq = {
|
|
||||||
index: number;
|
|
||||||
size: number;
|
|
||||||
customerId?: string;
|
customerId?: string;
|
||||||
eventIdList?: string[];
|
eventIdList?: string[];
|
||||||
keyword?: string;
|
keyword?: string;
|
||||||
beginAt?: number;
|
beginAt?: number;
|
||||||
endAt?: number;
|
endAt?: number;
|
||||||
};
|
} & BasePage;
|
||||||
|
|
||||||
export type EventRecordStatReq = {
|
export type EventRecordRankPage = {
|
||||||
customerId?: string;
|
customerId?: string;
|
||||||
|
eventIdList?: string[];
|
||||||
beginAt?: number;
|
beginAt?: number;
|
||||||
endAt?: number;
|
endAt?: number;
|
||||||
};
|
} & BasePage;
|
||||||
|
|
||||||
export type EventRecordTrendStatReq = {
|
|
||||||
customerId?: string;
|
|
||||||
eventId: string;
|
|
||||||
beginAt: number;
|
|
||||||
endAt: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CustomerEventStatView = {
|
|
||||||
customerId?: string;
|
|
||||||
customerName?: string;
|
|
||||||
eventId?: string;
|
|
||||||
eventName?: string;
|
|
||||||
count?: number;
|
|
||||||
latestRegisteredAt?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CustomerEventRankView = {
|
export type CustomerEventRankView = {
|
||||||
rank?: number;
|
|
||||||
customerId?: string;
|
|
||||||
customerCode?: string;
|
|
||||||
customerName?: string;
|
|
||||||
customerPhotoAttachmentId?: string;
|
|
||||||
totalEventDays?: number;
|
|
||||||
totalEventCount?: number;
|
|
||||||
recentContinuousEventDays?: number;
|
|
||||||
latestRegisteredAt?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CustomerEventRankResponse = {
|
|
||||||
rank?: number;
|
rank?: number;
|
||||||
customerId?: string;
|
customerId?: string;
|
||||||
customer?: Customer;
|
customer?: Customer;
|
||||||
@@ -102,12 +65,3 @@ export type EventRecordCalendarView = {
|
|||||||
eventId?: string;
|
eventId?: string;
|
||||||
registeredAt?: number;
|
registeredAt?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EventRecordCreateReq = {
|
|
||||||
customerIdList: string[];
|
|
||||||
eventId: string;
|
|
||||||
remark?: string;
|
|
||||||
registeredAt?: number;
|
|
||||||
attachmentIdList?: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ import { AttachmentAPI } from "timi-web";
|
|||||||
import type { BodyAreaValue, Customer } from "@/types/Customer";
|
import type { BodyAreaValue, Customer } from "@/types/Customer";
|
||||||
|
|
||||||
export function getCustomerPhotoURL(customer?: Customer): string {
|
export function getCustomerPhotoURL(customer?: Customer): string {
|
||||||
const attachment = customer?.attachmentList?.find(item => item.attachType === "PHOTO") || customer?.attachmentList?.[0];
|
const attachment = customer?.attachmentList?.find(item => item.attachType === "PHOTO");
|
||||||
if (!attachment?.id) {
|
if (!attachment?.id) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
return AttachmentAPI.getReadURL(attachment.id);
|
return AttachmentAPI.getReadURL(attachment.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,5 @@ export function getEventRecordPhotoURL(record?: EventRecord): string {
|
|||||||
if (!attachment?.id) {
|
if (!attachment?.id) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
return AttachmentAPI.getReadURL(attachment.id);
|
return AttachmentAPI.getReadURL(attachment.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user