From c81016292671c6d291894d6505201e0e0f92236a Mon Sep 17 00:00:00 2001 From: Timi Date: Sat, 15 Aug 2026 01:33:52 +0800 Subject: [PATCH] add user invite --- src/api/ManagedUserAPI.ts | 47 +++- src/pages/InviteRegister.vue | 281 ++++++++++++++++++++++++ src/pages/user/UserInvitePoster.vue | 121 ++++++++++ src/pages/user/UserList.vue | 32 ++- src/pages/user/component/UserInvite.vue | 189 ++++++++++++++++ src/router/index.ts | 15 +- src/router/routes/userRoutes.ts | 15 ++ src/utils/qrCode.ts | 77 +++++++ 8 files changed, 764 insertions(+), 13 deletions(-) create mode 100644 src/pages/InviteRegister.vue create mode 100644 src/pages/user/UserInvitePoster.vue create mode 100644 src/pages/user/component/UserInvite.vue diff --git a/src/api/ManagedUserAPI.ts b/src/api/ManagedUserAPI.ts index 9445c92..eda220a 100644 --- a/src/api/ManagedUserAPI.ts +++ b/src/api/ManagedUserAPI.ts @@ -1,4 +1,4 @@ -import { axios, type Page, type PageResult, type Role } from "timi-web"; +import { axios, type LoginResponse, type Page, type PageResult, type Role } from "timi-web"; import type { ManagedUser } from "@/types/ManagedUser"; import { withCurrentStoreId, withCurrentStorePage } from "@/utils/store"; @@ -9,6 +9,34 @@ type ListOptions = { storeId?: string; }; +export type InviteRequest = { + storeId?: string; + roleId: string; +}; + +export type InviteResult = { + code: string; + expireAt: number; +}; + +export type InviteValidateResult = { + valid: boolean; + expireAt: number; +}; + +export type InviteRegisterData = { + code: string; + phoneNo: string; + name: string; + password: string; +}; + +export type InviteRegisterRequest = { + captchaId: string; + captcha: string; + data: InviteRegisterData; +}; + async function list(page: Page, options?: ListOptions): Promise> { if (options?.allStores) { return await axios.post(`${BASE_URI}/list`, page); @@ -55,6 +83,18 @@ async function roleList(): Promise { return await axios.post(`${BASE_URI}/role/list`); } +async function invite(request: InviteRequest): Promise { + return await axios.post(`${BASE_URI}/invite`, request); +} + +async function validateInvite(code: string): Promise { + return await axios.post(`${BASE_URI}/invite/validate`, { code }); +} + +async function registerByInvite(request: InviteRegisterRequest): Promise { + return await axios.post(`${BASE_URI}/invite/register`, request); +} + export default { list, detail, @@ -62,5 +102,8 @@ export default { update, transferStore, remove, - roleList + roleList, + invite, + validateInvite, + registerByInvite }; diff --git a/src/pages/InviteRegister.vue b/src/pages/InviteRegister.vue new file mode 100644 index 0000000..fe75805 --- /dev/null +++ b/src/pages/InviteRegister.vue @@ -0,0 +1,281 @@ + + + + + diff --git a/src/pages/user/UserInvitePoster.vue b/src/pages/user/UserInvitePoster.vue new file mode 100644 index 0000000..23a8de8 --- /dev/null +++ b/src/pages/user/UserInvitePoster.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/src/pages/user/UserList.vue b/src/pages/user/UserList.vue index 61cdcec..cdf552d 100644 --- a/src/pages/user/UserList.vue +++ b/src/pages/user/UserList.vue @@ -1,6 +1,10 @@ + + diff --git a/src/router/index.ts b/src/router/index.ts index cf611e0..ca6f37e 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -2,6 +2,7 @@ import { createRouter, createWebHistory, type RouteLocationNormalized } from "vu import RootLayout from "@/layout/RootLayout.vue"; import MainLayout from "@/layout/MainLayout.vue"; import Login from "@/pages/Login.vue"; +import InviteRegister from "@/pages/InviteRegister.vue"; import chartRoutes from "@/router/routes/chartRoutes"; import customerRoutes from "@/router/routes/customerRoutes"; import { managementAccountRoutes, managementSettingRoutes, storeRoutes } from "@/router/routes/managementRoutes"; @@ -60,6 +61,18 @@ const router = createRouter({ darkBodyBackground: "#10161f" } }, + { + path: "gao/invite/register", + name: "InviteRegister", + component: InviteRegister, + meta: { + depth: 1, + navBarVisible: false, + tabBarVisible: false, + bodyBackground: "#F5F5F8", + darkBodyBackground: "#10161f" + } + }, { path: "", component: MainLayout, @@ -86,7 +99,7 @@ const router = createRouter({ router.beforeEach(async (to) => { const userStore = useUserStore(); - if (to.path === "/login") { + if (to.path === "/login" || to.path === "/gao/invite/register") { return userStore.isLogged() ? "/management" : true; } diff --git a/src/router/routes/userRoutes.ts b/src/router/routes/userRoutes.ts index f9f5940..4953f40 100644 --- a/src/router/routes/userRoutes.ts +++ b/src/router/routes/userRoutes.ts @@ -3,8 +3,23 @@ import UserList from "@/pages/user/UserList.vue"; import UserDetail from "@/pages/user/UserDetail.vue"; import UserEdit from "@/pages/user/UserEdit.vue"; import UserRoleAssign from "@/pages/user/UserRoleAssign.vue"; +import UserInvitePoster from "@/pages/user/UserInvitePoster.vue"; const userRoutes: RouteRecordRaw[] = [ + { + path: "/management/users/invite/poster", + name: "UserInvitePoster", + component: UserInvitePoster, + meta: { + depth: 5, + navBarVisible: true, + navBarCanBack: true, + navBarTitle: "邀请注册海报", + tabBarVisible: false, + bodyBackground: "#F0FDFA", + darkBodyBackground: "#10161f" + } + }, { path: "/management/users", name: "UserList", diff --git a/src/utils/qrCode.ts b/src/utils/qrCode.ts index e89ad1a..f6e0210 100644 --- a/src/utils/qrCode.ts +++ b/src/utils/qrCode.ts @@ -1,4 +1,5 @@ import { BarcodeFormat, EncodeHintType, QRCodeWriter } from "@zxing/library"; +import { Time } from "timi-web"; export interface QrCodePage { pageIndex: number; @@ -23,6 +24,13 @@ export interface QrCodeGrid { pageSize: number; } +export interface InvitePosterOptions { + code: string; + storeName: string; + roleName: string; + expireAt: number; +} + const pageWidth = 2480; const pageHeight = 3508; const pagePadding = 80; @@ -77,6 +85,14 @@ export async function downloadQrCodeImages(options: DownloadQrCodeOptions): Prom } } +export async function downloadInvitePoster(options: InvitePosterOptions, fileName: string): Promise { + await downloadCanvas(renderInvitePoster(options), fileName); +} + +export function createInvitePosterImage(options: InvitePosterOptions): string { + return renderInvitePoster(options).toDataURL("image/png"); +} + export async function printQrCodeImages(options: PrintQrCodeOptions): Promise { const printWindow = window.open("", "_blank"); if (!printWindow) { @@ -177,6 +193,67 @@ function renderQrCodePage(codes: string[], maxCodeLength = 0): HTMLCanvasElement return canvas; } +function renderInvitePoster(options: InvitePosterOptions): HTMLCanvasElement { + const canvas = document.createElement("canvas"); + canvas.width = 1200; + canvas.height = 1820; + const context = canvas.getContext("2d"); + if (!context) { + throw new Error("当前浏览器不支持 Canvas"); + } + + const teal = "#0F766E"; + const darkTeal = "#115E59"; + context.fillStyle = "#F0FDFA"; + context.fillRect(0, 0, canvas.width, canvas.height); + context.fillStyle = teal; + context.fillRect(0, 0, canvas.width, 640); + context.fillStyle = "#CCFBF1"; + context.beginPath(); + context.arc(1140, 40, 220, 0, Math.PI * 2); + context.fill(); + context.fillStyle = "#FFF"; + context.textAlign = "left"; + context.textBaseline = "middle"; + context.font = "700 80px Microsoft YaHei, sans-serif"; + context.fillText("邀请您加入", 64, 120); + context.font = "700 110px Microsoft YaHei, sans-serif"; + context.fillText(options.storeName, 64, 280); + context.font = "700 64px Microsoft YaHei, sans-serif"; + context.fillText(`成为:${options.roleName}`, 64, 420); + + context.fillStyle = "#FFFFFF"; + context.shadowColor = "rgba(15, 118, 110, .18)"; + context.shadowBlur = 36; + context.shadowOffsetY = 12; + context.beginPath(); + context.roundRect(120, 520, 960, 960, 40); + context.fill(); + context.shadowColor = "transparent"; + context.shadowBlur = 0; + context.shadowOffsetY = 0; + + const qrSize = 920; + const qrLeft = (canvas.width - qrSize) / 2; + const qrTop = 540; + drawQrCode(context, options.code, qrLeft, qrTop, qrSize); + context.textAlign = "center"; + context.fillStyle = "#64748B"; + context.font = "400 40px Microsoft YaHei, sans-serif"; + context.fillText(`有效期至 ${Time.toDateTime(options.expireAt)}`, 600, 1430); + context.fillStyle = darkTeal; + context.font = "400 52px Microsoft YaHei, sans-serif"; + context.fillText("糕店客户管理系统", 600, 1550); + context.fillStyle = "#64748B"; + context.font = "400 40px Microsoft YaHei, sans-serif"; + context.fillText("客户档案 | 事件登记 | 积分激励 | 门店协同 | AI 分析", 600, 1645); + context.fillStyle = "#94A3B8"; + context.font = "400 40px Microsoft YaHei, sans-serif"; + context.fillText("本邀请仅限本人使用,请勿转发", 600, 1760); + + return canvas; +} + async function waitPrintImagesLoaded(document: Document): Promise { const imageList = Array.from(document.images); await Promise.all(imageList.map(image => {