update user page style

This commit is contained in:
Timi
2026-08-19 10:39:19 +08:00
parent 0109a7f3aa
commit ff156926f5
8 changed files with 115 additions and 115 deletions
+42 -57
View File
@@ -28,7 +28,11 @@
<telephone-link :value="managedUser.user.phoneNo" placeholder="未填写" />
</template>
</t-cell>
<t-cell title="上次登录时间" :note="Time.toPassedDateTime(managedUser.user.lastLoginAt)" />
<t-cell title="上次登录时间">
<template #note>
<pass-time-label :timestamp="managedUser.user.lastLoginAt" underline />
</template>
</t-cell>
</t-cell-group>
<t-cell-group v-if="currentUser" theme="card">
@@ -46,33 +50,23 @@
</t-cell>
</t-cell-group>
<section v-if="currentUser" class="registration-actions bg-white">
<header>
<h4 class="title">可登记事件</h4>
</header>
<div class="actions">
<t-tag
v-for="item in scopeList"
:key="item"
variant="light-outline"
theme="success"
:content="item"
/>
<div v-if="!scopeList.length" class="empty-card">未配置</div>
</div>
</section>
<t-button v-can="permissionCodes.user.update" class="edit" theme="primary" @click="toEdit">编辑用户</t-button>
<t-cell-group v-if="currentUser" title="可登记事件" theme="card">
<t-cell v-for="item in scopeList" :key="item.id" :title="item.name"/>
<t-cell v-if="!scopeList.length" title="未配置" />
</t-cell-group>
</div>
</template>
<script setup lang="ts">
import { Toast } from "tdesign-mobile-vue";
import { Time, type User, UserAPI } from "timi-web";
import { PassTimeLabel, type User, UserAPI } from "timi-web";
import EventAPI from "@/api/EventAPI";
import ManagedUserAPI from "@/api/ManagedUserAPI";
import type { ManagedUser } from "@/types/ManagedUser";
import type { EventItem } from "@/types/EventItem";
import { usePermissionSettingsStore } from "@/store/permissionSettingsStore";
import { useNavBarStore } from "@/store/navBarStore";
import { useUserStore } from "timi-web";
import { buildFallbackAvatar } from "@/utils/avatar";
import { buildEffectiveRoleCodeSet, getManagedUserRoleList, getRoleDisplayName } from "@/utils/role";
import { permissionCodes } from "@/utils/permission";
@@ -83,6 +77,8 @@ defineOptions({
const route = useRoute();
const router = useRouter();
const navBarStore = useNavBarStore();
const userStore = useUserStore();
const permissionStore = usePermissionSettingsStore();
// ---------- 用户数据 ----------
@@ -110,9 +106,7 @@ const scopeList = computed(() => {
if (!item) {
return [];
}
return eventList.value
.filter(action => canEventItem(action, item))
.map(item => item.name);
return eventList.value.filter(action => canEventItem(action, item));
});
// ---------- 导航 ----------
@@ -146,7 +140,7 @@ function isUser(user?: Partial<User>): user is User {
// ---------- 事件范围 ----------
function canEventItem(action: EventItem, item: ManagedUser): boolean {
const userRoleCodeSet = buildEffectiveRoleCodeSet(item);
const userRoleCodeSet = buildEffectiveRoleCodeSet(item, permissionStore.roleList);
const roleCodeList = action.roleCodeList || [];
if (!roleCodeList.length) {
return false;
@@ -158,22 +152,45 @@ function canEventItem(action: EventItem, item: ManagedUser): boolean {
async function loadData(): Promise<void> {
const routeId = String(route.params.id || "");
const [detail, actions] = await Promise.all([
const [detail] = await Promise.all([
ManagedUserAPI.detail(routeId),
EventAPI.listValid(),
permissionStore.loadAll()
]);
managedUser.value = detail;
eventList.value = actions || [];
if (!userStore.hasPermissions(permissionCodes.event.read)) {
return;
}
try {
const result = await EventAPI.list({ index: 0, size: 1000 }, { storeId: detail.storeId });
eventList.value = (result.list || []).filter(isValidEvent);
} catch {
eventList.value = [];
}
}
function isValidEvent(event: EventItem): boolean {
if (event.status !== "ACTIVE") {
return false;
}
const now = Date.now();
return (event.beginAt === undefined || event.beginAt <= now)
&& (event.endAt === undefined || now <= event.endAt);
}
onMounted(async () => {
try {
await loadData();
if (userStore.hasPermissions(permissionCodes.user.update)) {
navBarStore.setRightText("编辑", "var(--td-brand-color)", toEdit);
}
} catch {
Toast.error("用户详情加载失败");
}
});
onUnmounted(() => navBarStore.clearRight());
</script>
<style scoped lang="less">
@@ -189,34 +206,6 @@ onMounted(async () => {
border-radius: .875rem;
}
.registration-actions {
gap: .5rem;
margin: 0 1rem;
padding: 1rem;
display: flex;
border-radius: 1rem;
flex-direction: column;
.title {
margin: 0;
}
.actions {
gap: .5rem;
display: flex;
.empty-card {
color: #52607a;
}
}
}
.edit {
left: 1rem;
right: 1rem;
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
position: fixed;
}
}
:global(html.theme-dark) {
@@ -229,10 +218,6 @@ onMounted(async () => {
p,
.info-list li span,
.empty-card {
color: #94a3b8;
}
.action {
color: #e2e8f0;
background: rgba(15, 23, 42, .72);
+7 -7
View File
@@ -14,7 +14,6 @@
<div class="picker-text" v-text="selectedStoreName" />
</t-form-item>
</t-form>
<t-button class="save" theme="primary" :loading="isSaving" @click="saveUser">保存</t-button>
</div>
<t-popup v-model="storePickerVisible" placement="bottom">
<t-picker
@@ -32,6 +31,7 @@ import { Toast } from "tdesign-mobile-vue";
import { useUserStore } from "timi-web";
import ManagedUserAPI from "@/api/ManagedUserAPI";
import { useCurrentStoreStore } from "@/store/currentStoreStore";
import { useNavBarStore } from "@/store/navBarStore";
import type { ManagedUser } from "@/types/ManagedUser";
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
@@ -43,6 +43,7 @@ defineOptions({
const route = useRoute();
const router = useRouter();
const navBarStore = useNavBarStore();
const userStore = useUserStore();
const currentStoreStore = useCurrentStoreStore();
@@ -205,12 +206,17 @@ function validateForm(): boolean {
}
onMounted(async () => {
if (userStore.hasPermissions(savePermissionCode.value)) {
navBarStore.setRightText("保存", "var(--td-brand-color)", saveUser);
}
try {
await fillForm();
} catch {
Toast.error("用户数据加载失败");
}
});
onUnmounted(() => navBarStore.clearRight());
</script>
<style scoped lang="less">
@@ -225,12 +231,6 @@ onMounted(async () => {
color: var(--td-text-color-primary);
}
.save {
left: 1rem;
right: 1rem;
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
position: fixed;
}
}
:global(html.theme-dark) {
+2
View File
@@ -25,11 +25,13 @@ 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 inviterName = String(route.query.inviterName || "未命名用户");
const registerLink = `${window.location.origin}/gao/invite/register?code=${encodeURIComponent(code)}`;
const posterOptions = computed(() => ({
code: registerLink,
storeName,
roleName,
inviterName,
expireAt
}));
+6 -27
View File
@@ -25,7 +25,6 @@
class="item"
v-for="item in pageResult.list"
:key="item.id"
:description="`可登记事件:${getActionScope(item)}`"
arrow
@click="toDetail(item)"
>
@@ -38,14 +37,17 @@
<t-tag class="meta" size="small" theme="success" variant="outline" :content="getRoleName(item)" />
</div>
</template>
<template #description>
<telephone-link :value="item.user?.phoneNo" />
</template>
</t-cell>
</t-cell-group>
<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>
<div class="actions">
<t-button v-can="permissionCodes.user.create" theme="light" type="button" @click="toCreate">新增用户</t-button>
<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" />
@@ -54,14 +56,12 @@
<script setup lang="ts">
import { Toast, type DropdownValue } from "tdesign-mobile-vue";
import { type Page, type PageResult, UserAPI, type User, useUserStore } from "timi-web";
import EventAPI from "@/api/EventAPI";
import ManagedUserAPI from "@/api/ManagedUserAPI";
import { useCurrentStoreStore } from "@/store/currentStoreStore";
import type { ManagedUser } from "@/types/ManagedUser";
import type { EventItem } from "@/types/EventItem";
import { usePermissionSettingsStore } from "@/store/permissionSettingsStore";
import { buildFallbackAvatar } from "@/utils/avatar";
import { buildEffectiveRoleCodeSet, getManagedUserRoleList, getRoleDisplayName } from "@/utils/role";
import { getManagedUserRoleList, getRoleDisplayName } from "@/utils/role";
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
import UserInvite from "@/pages/user/component/UserInvite.vue";
@@ -103,7 +103,6 @@ const userListOptions = computed(() => {
// ---------- 列表数据 ----------
const eventList = ref<EventItem[]>([]);
const isRefreshing = ref(false);
const isLoading = ref(false);
const isNoMore = ref(false);
@@ -126,24 +125,6 @@ function getRoleName(item: ManagedUser): string {
return getRoleDisplayName(getManagedUserRoleList(item)[0], permissionStore.roleList);
}
function getActionScope(item: ManagedUser): string {
const nameList = eventList.value
.filter(action => canEventItem(action, item))
.map(action => action.name);
return nameList.join("、") || "未配置";
}
// ---------- 事件范围 ----------
function canEventItem(action: EventItem, item: ManagedUser): boolean {
const userRoleCodeSet = buildEffectiveRoleCodeSet(item);
const roleCodeList = action.roleCodeList || [];
if (!roleCodeList.length) {
return false;
}
return roleCodeList.some(code => userRoleCodeSet.has(code));
}
// ---------- 头像 ----------
function buildUserAvatar(item: ManagedUser): string {
@@ -168,16 +149,14 @@ async function loadStoreScope(): Promise<void> {
async function loadData(): Promise<void> {
await loadStoreScope();
const [userResult, actions] = await Promise.all([
const [userResult] = await Promise.all([
ManagedUserAPI.list(page, userListOptions.value),
EventAPI.listValid(),
permissionStore.loadAll()
]);
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 || [];
}
async function refresh(): Promise<void> {
+10 -15
View File
@@ -34,15 +34,6 @@
</t-cell>
</t-cell-group>
<t-empty v-else-if="!isLoading" description="暂无可授权角色" />
<t-button
v-can="userRoleWriteAccess"
class="save"
theme="primary"
:loading="isSaving"
@click="saveRoleList"
>
保存授权
</t-button>
</div>
</template>
@@ -52,6 +43,7 @@ import { Toast } from "tdesign-mobile-vue";
import { RoleAPI, type ModuleCode, type Role as UserRole, useUserStore } from "timi-web";
import ManagedUserAPI from "@/api/ManagedUserAPI";
import { useCurrentStoreStore } from "@/store/currentStoreStore";
import { useNavBarStore } from "@/store/navBarStore";
import type { Role } from "@/store/permissionSettingsStore";
import type { ManagedUser } from "@/types/ManagedUser";
import { getErrorMessage } from "@/utils/error";
@@ -62,6 +54,7 @@ defineOptions({
});
const route = useRoute();
const navBarStore = useNavBarStore();
const moduleCode: ModuleCode = "GAO";
const allStoreFilterValue = "__ALL_STORES__";
@@ -283,12 +276,20 @@ function setRoleChecked(roleId: string, value: boolean): void {
}
onMounted(async () => {
if (
userStore.hasPermissions(corePermissionCodes.userRole.create)
&& userStore.hasPermissions(corePermissionCodes.userRole.delete)
) {
navBarStore.setRightText("保存", "var(--td-brand-color)", saveRoleList);
}
try {
await loadData();
} catch {
Toast.error("用户角色加载失败");
}
});
onUnmounted(() => navBarStore.clearRight());
</script>
<style scoped lang="less">
@@ -307,12 +308,6 @@ onMounted(async () => {
background: #fff;
}
.save {
left: 1rem;
right: 1rem;
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
position: fixed;
}
}
:global(html.theme-dark) {
+7 -1
View File
@@ -130,7 +130,8 @@ async function generateInvite(): Promise<void> {
code: result.code,
expireAt: String(result.expireAt),
storeName: isGlobalManager.value ? selectedStoreName.value : currentStoreStore.currentStoreName,
roleName: selectedRoleName.value
roleName: selectedRoleName.value,
inviterName: getCurrentUserName()
}
});
} finally {
@@ -138,6 +139,11 @@ async function generateInvite(): Promise<void> {
}
}
function getCurrentUserName(): string {
const user = userStore.loginUser.user;
return user?.nick || user?.name || "未命名用户";
}
watch(() => props.visible, async visible => {
if (!visible) {
return;
+3 -2
View File
@@ -28,6 +28,7 @@ export interface InvitePosterOptions {
code: string;
storeName: string;
roleName: string;
inviterName: string;
expireAt: number;
}
@@ -216,7 +217,7 @@ function renderInvitePoster(options: InvitePosterOptions): HTMLCanvasElement {
context.textAlign = "left";
context.textBaseline = "middle";
context.font = "700 80px Microsoft YaHei, sans-serif";
context.fillText("邀请您加入", 64, 120);
context.fillText(`${options.inviterName} 邀请您加入`, 64, 120);
context.font = "700 110px Microsoft YaHei, sans-serif";
context.fillText(options.storeName, 64, 280);
context.font = "700 64px Microsoft YaHei, sans-serif";
@@ -246,7 +247,7 @@ function renderInvitePoster(options: InvitePosterOptions): HTMLCanvasElement {
context.fillText("糕店客户管理系统", 600, 1550);
context.fillStyle = "#64748B";
context.font = "400 40px Microsoft YaHei, sans-serif";
context.fillText("客户档案 | 事件登记 | 积分激励 | 门店协同", 600, 1645);
context.fillText("客户档案 | 事件登记 | 积分激励 | 门店协同 | AI 分析", 600, 1645);
context.fillStyle = "#94A3B8";
context.font = "400 40px Microsoft YaHei, sans-serif";
context.fillText("本邀请仅限本人使用,请勿转发", 600, 1760);
+38 -6
View File
@@ -34,14 +34,46 @@ export function getRoleDisplayName(role: ManagedUserRole | undefined, roleList:
return typeof role === "string" ? role : role.name || role.code || "未配置角色";
}
export function buildEffectiveRoleCodeSet(user: ManagedUser): Set<string> {
export function buildEffectiveRoleCodeSet(user: ManagedUser, roleList: Role[] = []): Set<string> {
const roleCodeSet = new Set<string>();
for (const role of getManagedUserRoleList(user)) {
if (typeof role !== "string") {
roleCodeSet.add(buildRoleCode(role as Role));
continue;
const visitedRoleIds = new Set<string>();
function addRole(role: ManagedUserRole | Role): void {
const roleId = typeof role === "string"
? roleList.find(item => buildRoleCode(item) === role || item.code === role)?.id
: role.id || roleList.find(item => buildRoleCode(item) === buildRoleCode(role as Role))?.id;
if (roleId && visitedRoleIds.has(roleId)) {
return;
}
roleCodeSet.add(role);
if (roleId) {
visitedRoleIds.add(roleId);
}
if (typeof role === "string") {
roleCodeSet.add(role);
const separatorIndex = role.indexOf(":");
if (-1 < separatorIndex) {
roleCodeSet.add(role.slice(separatorIndex + 1));
}
} else {
roleCodeSet.add(role.code);
roleCodeSet.add(buildRoleCode(role as Role));
}
const fullRole = roleId ? roleList.find(item => item.id === roleId) : undefined;
for (const childRoleId of fullRole?.childRoleIdList || []) {
const childRole = roleList.find(item => item.id === childRoleId);
if (childRole) {
addRole(childRole);
}
}
}
for (const role of getManagedUserRoleList(user)) {
addRole(role);
}
for (const roleCode of user.user?.roleList || []) {
addRole(roleCode);
}
return roleCodeSet;
}