init project
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
import { AttachmentAPI } from "timi-web";
|
||||
import type { BodyAreaValue, Customer } from "@/types/Customer";
|
||||
|
||||
export function getCustomerPhotoURL(customer?: Customer): string {
|
||||
const attachment = customer?.attachmentList?.find(item => item.attachType === "PHOTO") || customer?.attachmentList?.[0];
|
||||
if (!attachment?.id) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return AttachmentAPI.getReadURL(attachment.id);
|
||||
}
|
||||
|
||||
export function createBodyAreaValue(): BodyAreaValue {
|
||||
return {
|
||||
selectedCodeList: []
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeBodyAreaValue(value: unknown): BodyAreaValue {
|
||||
if (!value || typeof value !== "object") {
|
||||
return createBodyAreaValue();
|
||||
}
|
||||
|
||||
const raw = value as Partial<BodyAreaValue>;
|
||||
if (!Array.isArray(raw.selectedCodeList)) {
|
||||
return createBodyAreaValue();
|
||||
}
|
||||
|
||||
return {
|
||||
selectedCodeList: raw.selectedCodeList.map(item => String(item).trim()).filter(Boolean)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
export const gaoPermissionCodes = {
|
||||
customer: {
|
||||
create: "GAO:CUSTOMER:CREATE",
|
||||
read: "GAO:CUSTOMER:READ",
|
||||
update: "GAO:CUSTOMER:UPDATE",
|
||||
delete: "GAO:CUSTOMER:DELETE"
|
||||
},
|
||||
registrationEvent: {
|
||||
create: "GAO:REGISTRATION_EVENT:CREATE",
|
||||
read: "GAO:REGISTRATION_EVENT:READ",
|
||||
update: "GAO:REGISTRATION_EVENT:UPDATE",
|
||||
delete: "GAO:REGISTRATION_EVENT:DELETE"
|
||||
},
|
||||
registrationEventRecord: {
|
||||
create: "GAO:REGISTRATION_EVENT_RECORD:CREATE",
|
||||
read: "GAO:REGISTRATION_EVENT_RECORD:READ",
|
||||
update: "GAO:REGISTRATION_EVENT_RECORD:UPDATE",
|
||||
delete: "GAO:REGISTRATION_EVENT_RECORD:DELETE"
|
||||
},
|
||||
stat: {
|
||||
read: "GAO:STAT:READ"
|
||||
},
|
||||
user: {
|
||||
create: "GAO:USER:CREATE",
|
||||
read: "GAO:USER:READ",
|
||||
update: "GAO:USER:UPDATE",
|
||||
delete: "GAO:USER:DELETE"
|
||||
},
|
||||
qrCode: {
|
||||
create: "GAO:QR_CODE:CREATE"
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const corePermissionCodes = {
|
||||
role: {
|
||||
create: "CORE:ROLE:CREATE",
|
||||
read: "CORE:ROLE:READ",
|
||||
update: "CORE:ROLE:UPDATE",
|
||||
delete: "CORE:ROLE:DELETE"
|
||||
},
|
||||
permission: {
|
||||
create: "CORE:PERMISSION:CREATE",
|
||||
read: "CORE:PERMISSION:READ",
|
||||
update: "CORE:PERMISSION:UPDATE",
|
||||
delete: "CORE:PERMISSION:DELETE"
|
||||
},
|
||||
rolePermission: {
|
||||
create: "CORE:ROLE_PERMISSION:CREATE",
|
||||
read: "CORE:ROLE_PERMISSION:READ",
|
||||
delete: "CORE:ROLE_PERMISSION:DELETE"
|
||||
},
|
||||
roleRelation: {
|
||||
create: "CORE:ROLE_RELATION:CREATE",
|
||||
read: "CORE:ROLE_RELATION:READ",
|
||||
delete: "CORE:ROLE_RELATION:DELETE"
|
||||
},
|
||||
userRole: {
|
||||
create: "CORE:USER_ROLE:CREATE",
|
||||
read: "CORE:USER_ROLE:READ",
|
||||
delete: "CORE:USER_ROLE:DELETE"
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const coreRoleCodes = {
|
||||
admin: "CORE:ADMIN",
|
||||
userRoleAdmin: "CORE:USER_ROLE_ADMIN"
|
||||
} as const;
|
||||
|
||||
export const permissionManagementRoleAccess = {
|
||||
value: [
|
||||
coreRoleCodes.admin,
|
||||
coreRoleCodes.userRoleAdmin
|
||||
],
|
||||
mode: "or"
|
||||
} as const;
|
||||
|
||||
export type GaoPermissionCodes = typeof gaoPermissionCodes;
|
||||
export type CorePermissionCodes = typeof corePermissionCodes;
|
||||
@@ -0,0 +1,70 @@
|
||||
import type { Role } from "@/store/permissionSettingsStore";
|
||||
import type { GaoUser } from "@/types/GaoUser";
|
||||
|
||||
type GaoUserRole = NonNullable<GaoUser["roleList"]>[number];
|
||||
|
||||
function buildRoleCode(role: Role): string {
|
||||
return role.moduleCode && !role.code.includes(":") ? `${role.moduleCode}:${role.code}` : role.code;
|
||||
}
|
||||
|
||||
function getBareRoleCode(code: string): string {
|
||||
const codePartList = code.split(":");
|
||||
return codePartList[codePartList.length - 1] || code;
|
||||
}
|
||||
|
||||
function findRole(role: GaoUserRole, roleList: Role[]): Role | undefined {
|
||||
if (typeof role === "string") {
|
||||
return roleList.find(roleItem => roleItem.code === role || buildRoleCode(roleItem) === role);
|
||||
}
|
||||
return roleList.find(roleItem => roleItem.id === role.id || roleItem.code === role.code || buildRoleCode(roleItem) === role.code);
|
||||
}
|
||||
|
||||
function appendCompatibleRoleCode(roleCodeSet: Set<string>, code: string): void {
|
||||
if (!code) {
|
||||
return;
|
||||
}
|
||||
|
||||
roleCodeSet.add(code);
|
||||
roleCodeSet.add(getBareRoleCode(code));
|
||||
}
|
||||
|
||||
export function getGaoRoleName(role: GaoUserRole | undefined, roleList: Role[]): string {
|
||||
if (!role) {
|
||||
return "未配置角色";
|
||||
}
|
||||
|
||||
const fullRole = findRole(role, roleList);
|
||||
if (fullRole) {
|
||||
return fullRole.name || fullRole.code || "未配置角色";
|
||||
}
|
||||
|
||||
return typeof role === "string" ? getBareRoleCode(role) : role.name || role.code || "未配置角色";
|
||||
}
|
||||
|
||||
export function buildEffectiveRoleCodeSet(user: GaoUser, roleList: Role[]): Set<string> {
|
||||
const roleCodeSet = new Set<string>();
|
||||
for (const role of user.roleList || []) {
|
||||
const fullRole = findRole(role, roleList);
|
||||
if (fullRole) {
|
||||
appendRoleCode(roleCodeSet, fullRole, roleList, new Set<string>());
|
||||
continue;
|
||||
}
|
||||
appendCompatibleRoleCode(roleCodeSet, typeof role === "string" ? role : role.code);
|
||||
}
|
||||
return roleCodeSet;
|
||||
}
|
||||
|
||||
function appendRoleCode(roleCodeSet: Set<string>, role: Role, roleList: Role[], visitedRoleIds: Set<string>): void {
|
||||
if (!role.id || visitedRoleIds.has(role.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
visitedRoleIds.add(role.id);
|
||||
appendCompatibleRoleCode(roleCodeSet, buildRoleCode(role));
|
||||
for (const childRoleId of role.childRoleIdList) {
|
||||
const childRole = roleList.find(item => item.id === childRoleId);
|
||||
if (childRole) {
|
||||
appendRoleCode(roleCodeSet, childRole, roleList, visitedRoleIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import imageCompression from "browser-image-compression";
|
||||
|
||||
const PHOTO_MAX_SIZE_MB = 300 / 1024;
|
||||
const PHOTO_MAX_WIDTH_OR_HEIGHT = 1920;
|
||||
|
||||
/**
|
||||
* 压缩上传图片,统一限制到 300 KB。
|
||||
*/
|
||||
export async function compressUploadImage(file: File): Promise<File> {
|
||||
return await imageCompression(file, {
|
||||
maxSizeMB: PHOTO_MAX_SIZE_MB,
|
||||
maxWidthOrHeight: PHOTO_MAX_WIDTH_OR_HEIGHT,
|
||||
useWebWorker: true,
|
||||
initialQuality: .86
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量压缩上传图片。
|
||||
*/
|
||||
export async function compressUploadImageList(fileList: File[]): Promise<File[]> {
|
||||
const result: File[] = [];
|
||||
for (const file of fileList) {
|
||||
result.push(await compressUploadImage(file));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
import { BarcodeFormat, EncodeHintType, QRCodeWriter } from "@zxing/library";
|
||||
|
||||
export interface ManagementQrCodePage {
|
||||
pageIndex: number;
|
||||
codes: string[];
|
||||
}
|
||||
|
||||
export interface DownloadManagementQrCodeOptions {
|
||||
codes: string[];
|
||||
filePrefix: string;
|
||||
maxCodeLength?: number;
|
||||
}
|
||||
|
||||
export interface PrintManagementQrCodeOptions {
|
||||
codes: string[];
|
||||
maxCodeLength?: number;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface ManagementQrCodeGrid {
|
||||
rows: number;
|
||||
columns: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
const pageWidth = 2480;
|
||||
const pageHeight = 3508;
|
||||
const pagePadding = 80;
|
||||
const qrSize = 300;
|
||||
const minCellGap = 18;
|
||||
const codeFontSize = 34;
|
||||
const codeTopGap = 3;
|
||||
const gridLineWidth = 2;
|
||||
const codeTextHorizontalPadding = 28;
|
||||
const codeTextWidthRatio = .62;
|
||||
const qrWriter = new QRCodeWriter();
|
||||
const qrEncodeHints = new Map<EncodeHintType, string | number>([
|
||||
[EncodeHintType.ERROR_CORRECTION, "H"],
|
||||
[EncodeHintType.CHARACTER_SET, "UTF-8"],
|
||||
[EncodeHintType.MARGIN, 4]
|
||||
]);
|
||||
|
||||
export function calculateManagementQrCodeGrid(maxCodeLength = 0): ManagementQrCodeGrid {
|
||||
const codeTextMinWidth = Math.ceil(maxCodeLength * codeFontSize * codeTextWidthRatio + codeTextHorizontalPadding);
|
||||
const cellMinWidth = Math.max(qrSize + minCellGap, codeTextMinWidth + minCellGap);
|
||||
const cellMinHeight = qrSize + codeTopGap + codeFontSize + minCellGap;
|
||||
const columns = Math.max(1, Math.floor((pageWidth - pagePadding * 2) / cellMinWidth));
|
||||
const rows = Math.max(1, Math.floor((pageHeight - pagePadding * 2) / cellMinHeight));
|
||||
|
||||
return {
|
||||
rows,
|
||||
columns,
|
||||
pageSize: rows * columns
|
||||
};
|
||||
}
|
||||
|
||||
export function splitManagementQrCodePages(codes: string[], maxCodeLength = 0): ManagementQrCodePage[] {
|
||||
const pages: ManagementQrCodePage[] = [];
|
||||
const pageSize = calculateManagementQrCodeGrid(maxCodeLength).pageSize;
|
||||
|
||||
for (let index = 0; index < codes.length; index += pageSize) {
|
||||
pages.push({
|
||||
pageIndex: pages.length,
|
||||
codes: codes.slice(index, index + pageSize)
|
||||
});
|
||||
}
|
||||
|
||||
return pages;
|
||||
}
|
||||
|
||||
export async function downloadManagementQrCodeImages(options: DownloadManagementQrCodeOptions): Promise<void> {
|
||||
const pages = splitManagementQrCodePages(options.codes, options.maxCodeLength);
|
||||
|
||||
for (const page of pages) {
|
||||
const canvas = renderManagementQrCodePage(page.codes, options.maxCodeLength);
|
||||
await downloadCanvas(canvas, `${options.filePrefix}-${String(page.pageIndex + 1).padStart(2, "0")}.png`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function printManagementQrCodeImages(options: PrintManagementQrCodeOptions): Promise<void> {
|
||||
const printWindow = window.open("", "_blank");
|
||||
if (!printWindow) {
|
||||
throw new Error("浏览器阻止了打印窗口,请允许弹出窗口后重试");
|
||||
}
|
||||
|
||||
const pages = splitManagementQrCodePages(options.codes, options.maxCodeLength);
|
||||
const imageList = pages.map(page => renderManagementQrCodePage(page.codes, options.maxCodeLength).toDataURL("image/png"));
|
||||
const title = options.title || "客户二维码打印";
|
||||
const imageHTML = imageList.map(src => `<section class="page"><img src="${src}" alt="客户二维码" /></section>`).join("");
|
||||
|
||||
printWindow.document.open();
|
||||
printWindow.document.write(`<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>${escapeHTML(title)}</title>
|
||||
<style>
|
||||
@page {
|
||||
size: A4 portrait;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.page {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 210mm;
|
||||
height: 297mm;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
page-break-after: always;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.page:last-child {
|
||||
page-break-after: auto;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 210mm;
|
||||
height: 297mm;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>${imageHTML}</body>
|
||||
</html>`);
|
||||
printWindow.document.close();
|
||||
await waitPrintImagesLoaded(printWindow.document);
|
||||
printWindow.focus();
|
||||
printWindow.print();
|
||||
}
|
||||
|
||||
function renderManagementQrCodePage(codes: string[], maxCodeLength = 0): HTMLCanvasElement {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = pageWidth;
|
||||
canvas.height = pageHeight;
|
||||
const grid = calculateManagementQrCodeGrid(maxCodeLength);
|
||||
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) {
|
||||
throw new Error("当前浏览器不支持 Canvas");
|
||||
}
|
||||
|
||||
context.fillStyle = "#ffffff";
|
||||
context.fillRect(0, 0, pageWidth, pageHeight);
|
||||
context.fillStyle = "#000000";
|
||||
context.font = `500 ${codeFontSize}px Arial, sans-serif`;
|
||||
context.textAlign = "center";
|
||||
context.textBaseline = "top";
|
||||
|
||||
const cellWidth = (pageWidth - pagePadding * 2) / grid.columns;
|
||||
const cellHeight = (pageHeight - pagePadding * 2) / grid.rows;
|
||||
|
||||
drawGridLines(context, grid, cellWidth, cellHeight);
|
||||
|
||||
codes.forEach((code, index) => {
|
||||
const rowIndex = Math.floor(index / grid.columns);
|
||||
const columnIndex = index % grid.columns;
|
||||
const cellLeft = pagePadding + columnIndex * cellWidth;
|
||||
const cellTop = pagePadding + rowIndex * cellHeight;
|
||||
const qrLeft = Math.round(cellLeft + (cellWidth - qrSize) / 2);
|
||||
const qrTop = Math.round(cellTop + (cellHeight - qrSize - codeTopGap - codeFontSize) / 2);
|
||||
|
||||
drawQrCode(context, code, qrLeft, qrTop, qrSize);
|
||||
context.fillStyle = "#000000";
|
||||
context.fillText(code, cellLeft + cellWidth / 2, qrTop + qrSize + codeTopGap);
|
||||
});
|
||||
|
||||
return canvas;
|
||||
}
|
||||
|
||||
async function waitPrintImagesLoaded(document: Document): Promise<void> {
|
||||
const imageList = Array.from(document.images);
|
||||
await Promise.all(imageList.map(image => {
|
||||
if (image.complete) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
image.onload = () => resolve();
|
||||
image.onerror = () => reject(new Error("打印图片加载失败"));
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
function escapeHTML(value: string): string {
|
||||
return value
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll("\"", """);
|
||||
}
|
||||
|
||||
function drawGridLines(context: CanvasRenderingContext2D, grid: ManagementQrCodeGrid, cellWidth: number, cellHeight: number): void {
|
||||
context.strokeStyle = "#d1d5db";
|
||||
context.lineWidth = gridLineWidth;
|
||||
|
||||
for (let rowIndex = 0; rowIndex <= grid.rows; rowIndex++) {
|
||||
const top = Math.round(pagePadding + rowIndex * cellHeight);
|
||||
context.beginPath();
|
||||
context.moveTo(pagePadding, top);
|
||||
context.lineTo(pageWidth - pagePadding, top);
|
||||
context.stroke();
|
||||
}
|
||||
|
||||
for (let columnIndex = 0; columnIndex <= grid.columns; columnIndex++) {
|
||||
const left = Math.round(pagePadding + columnIndex * cellWidth);
|
||||
context.beginPath();
|
||||
context.moveTo(left, pagePadding);
|
||||
context.lineTo(left, pageHeight - pagePadding);
|
||||
context.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
function drawQrCode(context: CanvasRenderingContext2D, code: string, left: number, top: number, qrSize: number): void {
|
||||
const matrix = qrWriter.encode(code, BarcodeFormat.QR_CODE, qrSize, qrSize, qrEncodeHints);
|
||||
const width = matrix.getWidth();
|
||||
const height = matrix.getHeight();
|
||||
const sourceCanvas = document.createElement("canvas");
|
||||
sourceCanvas.width = width;
|
||||
sourceCanvas.height = height;
|
||||
const sourceContext = sourceCanvas.getContext("2d");
|
||||
|
||||
if (!sourceContext) {
|
||||
throw new Error("当前浏览器不支持 Canvas");
|
||||
}
|
||||
|
||||
sourceContext.fillStyle = "#ffffff";
|
||||
sourceContext.fillRect(0, 0, width, height);
|
||||
sourceContext.fillStyle = "#000000";
|
||||
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
if (matrix.get(x, y)) {
|
||||
sourceContext.fillRect(x, y, 1, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.fillStyle = "#ffffff";
|
||||
context.fillRect(left, top, qrSize, qrSize);
|
||||
context.imageSmoothingEnabled = false;
|
||||
context.drawImage(sourceCanvas, left, top, qrSize, qrSize);
|
||||
context.imageSmoothingEnabled = true;
|
||||
}
|
||||
|
||||
async function downloadCanvas(canvas: HTMLCanvasElement, fileName: string): Promise<void> {
|
||||
const blob = await new Promise<Blob>((resolve, reject) => {
|
||||
canvas.toBlob(result => {
|
||||
if (result) {
|
||||
resolve(result);
|
||||
return;
|
||||
}
|
||||
|
||||
reject(new Error("图片生成失败"));
|
||||
}, "image/png");
|
||||
});
|
||||
const link = document.createElement("a");
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
link.href = url;
|
||||
link.download = fileName;
|
||||
link.click();
|
||||
|
||||
await new Promise<void>(resolve => {
|
||||
window.setTimeout(resolve, 80);
|
||||
});
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { AttachmentAPI } from "timi-web";
|
||||
import type { RegistrationEventRecord } from "@/types/RegistrationEventRecord";
|
||||
|
||||
export function getRegistrationEventRecordPhotoURL(record?: RegistrationEventRecord): string {
|
||||
const attachment = record?.attachmentList?.find(item => item.attachType === "PHOTO") || record?.attachmentList?.[0];
|
||||
if (!attachment?.id) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return AttachmentAPI.getReadURL(attachment.id);
|
||||
}
|
||||
Reference in New Issue
Block a user