add user invite
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<div class="poster-page">
|
||||
<section class="poster-wrap">
|
||||
<img v-if="posterImage" class="poster" :src="posterImage" alt="邀请注册海报" />
|
||||
<div v-else class="empty" v-text="message" />
|
||||
</section>
|
||||
<div v-if="posterImage" class="actions">
|
||||
<t-button class="bg-white" variant="outline" theme="primary" block @click="copyLink">复制链接</t-button>
|
||||
<t-button theme="primary" block :loading="isDownloading" @click="savePoster">保存海报</t-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Toast } from "tdesign-mobile-vue";
|
||||
import { createInvitePosterImage, downloadInvitePoster } from "@/utils/qrCode";
|
||||
|
||||
defineOptions({ name: "UserInvitePoster" });
|
||||
|
||||
const route = useRoute();
|
||||
const posterImage = ref("");
|
||||
const isDownloading = ref(false);
|
||||
const message = ref("邀请海报参数无效");
|
||||
const code = String(route.query.code || "");
|
||||
const expireAt = Number(route.query.expireAt || 0);
|
||||
const storeName = String(route.query.storeName || "");
|
||||
const roleName = String(route.query.roleName || "");
|
||||
const registerLink = `${window.location.origin}/gao/invite/register?code=${encodeURIComponent(code)}`;
|
||||
const posterOptions = computed(() => ({
|
||||
code: registerLink,
|
||||
storeName,
|
||||
roleName,
|
||||
expireAt
|
||||
}));
|
||||
|
||||
function createPoster(): void {
|
||||
if (!/^\w{10}$/.test(code) || !expireAt) {
|
||||
return;
|
||||
}
|
||||
posterImage.value = createInvitePosterImage(posterOptions.value);
|
||||
}
|
||||
|
||||
async function savePoster(): Promise<void> {
|
||||
if (isDownloading.value) {
|
||||
return;
|
||||
}
|
||||
isDownloading.value = true;
|
||||
try {
|
||||
await downloadInvitePoster(posterOptions.value, "GAO-邀请注册海报.png");
|
||||
Toast.success("海报已保存");
|
||||
} catch {
|
||||
Toast.error("海报保存失败");
|
||||
} finally {
|
||||
isDownloading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function copyLink(): Promise<void> {
|
||||
try {
|
||||
await navigator.clipboard.writeText(registerLink);
|
||||
Toast.success("注册链接已复制");
|
||||
} catch {
|
||||
Toast.error("复制失败,请检查浏览器权限");
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
try {
|
||||
createPoster();
|
||||
} catch {
|
||||
message.value = "海报生成失败,请重新生成";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.poster-page {
|
||||
gap: 1rem;
|
||||
min-height: 100vh;
|
||||
padding: calc(var(--app-nav-height) + 1rem) 1rem 6rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
.poster-wrap {
|
||||
width: min(100%, 28rem);
|
||||
padding: .5rem;
|
||||
background: #fff;
|
||||
box-shadow: 0 .75rem 2rem rgba(15, 118, 110, .2);
|
||||
border-radius: 1.25rem;
|
||||
|
||||
.poster {
|
||||
width: 100%;
|
||||
display: block;
|
||||
border-radius: .875rem;
|
||||
}
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 5rem 1rem;
|
||||
color: var(--td-text-color-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.actions {
|
||||
gap: .75rem;
|
||||
left: 1rem;
|
||||
right: 1rem;
|
||||
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
|
||||
display: grid;
|
||||
position: fixed;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
:global(html.theme-dark) .poster-page {
|
||||
.poster-wrap {
|
||||
background: #0f172a;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+22
-10
@@ -1,6 +1,10 @@
|
||||
<template>
|
||||
<div v-can="permissionCodes.user.read" class="user-list">
|
||||
<div class="store-menu">
|
||||
<div
|
||||
v-can="permissionCodes.user.read"
|
||||
class="user-list"
|
||||
:class="{ 'has-store-filter': isGlobalManager }"
|
||||
>
|
||||
<div v-if="isGlobalManager" class="store-menu">
|
||||
<t-dropdown-menu v-if="isGlobalManager">
|
||||
<t-dropdown-item
|
||||
v-model="selectedStoreId"
|
||||
@@ -39,13 +43,12 @@
|
||||
<div v-if="!pageResult.list.length" class="load-state" 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="toCreate">
|
||||
<template #icon>
|
||||
<t-icon name="add" />
|
||||
</template>
|
||||
新增
|
||||
</t-button>
|
||||
<div class="actions">
|
||||
<t-button v-can="permissionCodes.user.create" theme="primary" type="button" @click="inviteVisible = true">邀请注册</t-button>
|
||||
<t-button v-can="permissionCodes.user.create" theme="primary" type="button" @click="toCreate">新增用户</t-button>
|
||||
</div>
|
||||
</div>
|
||||
<user-invite v-model:visible="inviteVisible" :store-id="selectedStoreId === allStoreFilterValue ? undefined : selectedStoreId" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -60,6 +63,7 @@ import { usePermissionSettingsStore } from "@/store/permissionSettingsStore";
|
||||
import { buildFallbackAvatar } from "@/utils/avatar";
|
||||
import { buildEffectiveRoleCodeSet, getManagedUserRoleList, getRoleDisplayName } from "@/utils/role";
|
||||
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
||||
import UserInvite from "@/pages/user/component/UserInvite.vue";
|
||||
|
||||
defineOptions({
|
||||
name: "UserList"
|
||||
@@ -69,6 +73,7 @@ const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const currentStoreStore = useCurrentStoreStore();
|
||||
const permissionStore = usePermissionSettingsStore();
|
||||
const inviteVisible = ref(false);
|
||||
|
||||
const isGlobalManager = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
|
||||
|
||||
@@ -242,10 +247,14 @@ onMounted(async () => {
|
||||
|
||||
<style scoped lang="less">
|
||||
.user-list {
|
||||
padding: calc(var(--app-nav-height) + 4rem) 0 6rem 0;
|
||||
padding: calc(var(--app-nav-height) + 1rem) 0 6rem 0;
|
||||
display: grid;
|
||||
gap: .75rem;
|
||||
|
||||
&.has-store-filter {
|
||||
padding-top: calc(var(--app-nav-height) + 4rem);
|
||||
}
|
||||
|
||||
.store-menu {
|
||||
top: var(--app-nav-height);
|
||||
left: 0;
|
||||
@@ -273,11 +282,14 @@ onMounted(async () => {
|
||||
}
|
||||
}
|
||||
|
||||
.add {
|
||||
.actions {
|
||||
left: 1rem;
|
||||
right: 1rem;
|
||||
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
|
||||
position: fixed;
|
||||
gap: .75rem;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
<template>
|
||||
<t-dialog
|
||||
v-model:visible="popupVisible"
|
||||
:cancel-btn="null"
|
||||
confirm-btn="生成邀请海报"
|
||||
:close-on-overlay-click="!pickerVisible"
|
||||
:z-index="10000"
|
||||
destroy-on-close
|
||||
@confirm="generateInvite"
|
||||
>
|
||||
<section class="invite-popup">
|
||||
<header class="header">
|
||||
<h2>配置邀请信息</h2>
|
||||
<button class="close" type="button" @click="popupVisible = false">×</button>
|
||||
</header>
|
||||
<t-form class="form" label-align="left">
|
||||
<t-form-item
|
||||
v-if="isGlobalManager"
|
||||
label="目标门店"
|
||||
arrow
|
||||
@click="openPicker('store')"
|
||||
>
|
||||
<span v-text="selectedStoreName" />
|
||||
</t-form-item>
|
||||
<t-form-item
|
||||
label="目标角色"
|
||||
arrow
|
||||
@click="openPicker('role')"
|
||||
>
|
||||
<span v-text="selectedRoleName" />
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
</section>
|
||||
</t-dialog>
|
||||
<t-popup v-model="pickerVisible" placement="bottom" :z-index="11000">
|
||||
<t-picker
|
||||
v-model="pickerValue"
|
||||
:columns="pickerColumns"
|
||||
:title="pickerType === 'role' ? '选择角色' : '选择门店'"
|
||||
@cancel="pickerVisible = false"
|
||||
@confirm="confirmPicker"
|
||||
/>
|
||||
</t-popup>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Toast } from "tdesign-mobile-vue";
|
||||
import ManagedUserAPI from "@/api/ManagedUserAPI";
|
||||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||
import { gaoRoleCodes } from "@/utils/permission";
|
||||
import { RoleAPI, useUserStore } from "timi-web";
|
||||
|
||||
defineOptions({ name: "UserInvite" });
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
storeId?: string;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
"update:visible": [value: boolean];
|
||||
}>();
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const currentStoreStore = useCurrentStoreStore();
|
||||
const isGlobalManager = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
|
||||
const popupVisible = computed({
|
||||
get: () => props.visible,
|
||||
set: value => emit("update:visible", value)
|
||||
});
|
||||
const formModel = reactive({ roleId: "", storeId: "" });
|
||||
const roleList = ref<Array<{ id?: string; name?: string; code?: string }>>([]);
|
||||
const isGenerating = ref(false);
|
||||
const isLoaded = ref(false);
|
||||
const roleOptions = computed(() => roleList.value.filter(item => item.id).map(item => ({ label: item.name || item.code || "未命名角色", value: item.id as string })));
|
||||
const storeOptions = computed(() => currentStoreStore.storeList.filter(item => item.id).map(item => ({ label: item.name || item.code || "未命名门店", value: item.id as string })));
|
||||
const selectedRoleName = computed(() => roleOptions.value.find(item => item.value === formModel.roleId)?.label || "请选择角色");
|
||||
const selectedStoreName = computed(() => storeOptions.value.find(item => item.value === formModel.storeId)?.label || "请选择门店");
|
||||
const pickerVisible = ref(false);
|
||||
const pickerType = ref<"role" | "store">("role");
|
||||
const pickerValue = ref<Array<string | number>>([]);
|
||||
const pickerColumns = computed(() => [pickerType.value === "role" ? roleOptions.value : storeOptions.value]);
|
||||
|
||||
async function loadData(): Promise<void> {
|
||||
if (isLoaded.value) {
|
||||
return;
|
||||
}
|
||||
const roleRequest = RoleAPI.manageableList("GAO");
|
||||
if (isGlobalManager.value) {
|
||||
await currentStoreStore.loadStoreList();
|
||||
formModel.storeId = currentStoreStore.storeList.some(item => item.id === props.storeId)
|
||||
? props.storeId || ""
|
||||
: currentStoreStore.currentStoreId;
|
||||
}
|
||||
roleList.value = await roleRequest;
|
||||
if (!isGlobalManager.value) {
|
||||
const store = await currentStoreStore.loadLoginStore();
|
||||
formModel.storeId = store?.id || "";
|
||||
}
|
||||
isLoaded.value = true;
|
||||
}
|
||||
|
||||
function openPicker(type: "role" | "store"): void {
|
||||
pickerType.value = type;
|
||||
pickerValue.value = [type === "role" ? formModel.roleId : formModel.storeId];
|
||||
pickerVisible.value = true;
|
||||
}
|
||||
|
||||
function confirmPicker(): void {
|
||||
const value = String(pickerValue.value[0] || "");
|
||||
if (pickerType.value === "role") {
|
||||
formModel.roleId = value;
|
||||
} else {
|
||||
formModel.storeId = value;
|
||||
}
|
||||
pickerVisible.value = false;
|
||||
}
|
||||
|
||||
async function generateInvite(): Promise<void> {
|
||||
if (!formModel.roleId || (isGlobalManager.value && !formModel.storeId)) {
|
||||
Toast.warning(isGlobalManager.value ? "请选择角色和门店" : "请选择角色");
|
||||
return;
|
||||
}
|
||||
isGenerating.value = true;
|
||||
try {
|
||||
const result = await ManagedUserAPI.invite(isGlobalManager.value ? { roleId: formModel.roleId, storeId: formModel.storeId } : { roleId: formModel.roleId });
|
||||
popupVisible.value = false;
|
||||
await router.push({
|
||||
name: "UserInvitePoster",
|
||||
query: {
|
||||
code: result.code,
|
||||
expireAt: String(result.expireAt),
|
||||
storeName: isGlobalManager.value ? selectedStoreName.value : currentStoreStore.currentStoreName,
|
||||
roleName: selectedRoleName.value
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
isGenerating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.visible, async visible => {
|
||||
if (!visible) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await loadData();
|
||||
} catch {
|
||||
Toast.error("邀请配置加载失败");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.invite-popup {
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
color: #0f172a;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.close {
|
||||
border: 0;
|
||||
padding: 0 .25rem;
|
||||
color: #64748b;
|
||||
background: transparent;
|
||||
font-size: 1.75rem;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.form {
|
||||
margin: 1rem 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
:global(html.theme-dark) .invite-popup {
|
||||
background: #0f172a;
|
||||
|
||||
.header h2 {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user