update user detail

This commit is contained in:
Timi
2026-08-07 16:02:29 +08:00
parent 1b1121f052
commit 28b789eb86
13 changed files with 421 additions and 147 deletions
+25 -1
View File
@@ -4,7 +4,26 @@ import { withCurrentStoreId, withCurrentStorePage } from "@/utils/store";
const BASE_URI = "/gao/user"; const BASE_URI = "/gao/user";
async function list(page: Page<ManagedUser>): Promise<PageResult<ManagedUser>> { type ListOptions = {
allStores?: boolean;
storeId?: string;
};
async function list(page: Page<ManagedUser>, options?: ListOptions): Promise<PageResult<ManagedUser>> {
if (options?.allStores) {
return await axios.post(`${BASE_URI}/list`, page);
}
if (options?.storeId) {
return await axios.post(`${BASE_URI}/list`, {
...page,
equalsExample: {
...(page.equalsExample || {}),
storeId: options.storeId
}
});
}
return await axios.post(`${BASE_URI}/list`, withCurrentStorePage(page)); return await axios.post(`${BASE_URI}/list`, withCurrentStorePage(page));
} }
@@ -22,6 +41,10 @@ async function update(user: ManagedUser): Promise<ManagedUser> {
return await axios.post(`${BASE_URI}/update`, withCurrentStoreId(user)); return await axios.post(`${BASE_URI}/update`, withCurrentStoreId(user));
} }
async function transferStore(user: ManagedUser): Promise<ManagedUser> {
return await axios.post(`${BASE_URI}/transfer/store`, user);
}
async function remove(id: string): Promise<void> { async function remove(id: string): Promise<void> {
return await axios.post(`${BASE_URI}/delete`, undefined, { return await axios.post(`${BASE_URI}/delete`, undefined, {
params: { id } params: { id }
@@ -37,6 +60,7 @@ export default {
detail, detail,
create, create,
update, update,
transferStore,
remove, remove,
roleList roleList
}; };
+5
View File
@@ -14,6 +14,10 @@ async function detail(id: string): Promise<Store> {
}); });
} }
async function loginStore(): Promise<Store> {
return await axios.post(`${STORE_BASE_URI}/user`);
}
async function create(store: Store): Promise<string> { async function create(store: Store): Promise<string> {
return await axios.post(`${STORE_BASE_URI}/create`, store); return await axios.post(`${STORE_BASE_URI}/create`, store);
} }
@@ -75,6 +79,7 @@ async function removeBusinessDay(id: string): Promise<void> {
export default { export default {
list, list,
detail, detail,
loginStore,
create, create,
update, update,
sort, sort,
+2 -13
View File
@@ -52,6 +52,7 @@
import { DialogPlugin, Toast } from "tdesign-mobile-vue"; import { DialogPlugin, Toast } from "tdesign-mobile-vue";
import { AttachmentAPI, UserAPI, useUserStore } from "timi-web"; import { AttachmentAPI, UserAPI, useUserStore } from "timi-web";
import PhotoField from "@/components/PhotoField.vue"; import PhotoField from "@/components/PhotoField.vue";
import { buildFallbackAvatar } from "@/utils/avatar";
import { compressUploadImage } from "@/utils/imageCompression"; import { compressUploadImage } from "@/utils/imageCompression";
type UserAvatarSource = NonNullable<Parameters<typeof UserAPI.getAvatarURL>[0]> & { type UserAvatarSource = NonNullable<Parameters<typeof UserAPI.getAvatarURL>[0]> & {
@@ -223,21 +224,9 @@ function textToSex(text: string): number {
return 0; return 0;
} }
function buildFallbackAvatar(name: string): string {
const text = encodeURIComponent((name.trim() || "用户").slice(0, 2));
return [
"data:image/svg+xml;charset=UTF-8,",
"%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120' viewBox='0 0 120 120'%3E",
"%3Crect width='120' height='120' rx='24' fill='%230f766e'/%3E",
"%3Ctext x='50%25' y='54%25' fill='white' font-size='36' font-family='Arial,sans-serif' text-anchor='middle' dominant-baseline='middle'%3E",
text,
"%3C/text%3E%3C/svg%3E"
].join("");
}
function buildUserAvatar(user?: UserAvatarSource): string { function buildUserAvatar(user?: UserAvatarSource): string {
const name = user?.nick || user?.name || "用户"; const name = user?.nick || user?.name || "用户";
return UserAPI.getAvatarURL(user) || buildFallbackAvatar(name); return UserAPI.getAvatarURL(user) || buildFallbackAvatar(name, "#0f766e");
} }
onMounted(() => { onMounted(() => {
+21 -29
View File
@@ -1,28 +1,34 @@
<template> <template>
<div v-can="permissionCodes.user.read" class="user-edit"> <div
<t-cell-group v-if="currentUser" theme="card"> v-if="managedUser"
v-can="permissionCodes.user.read"
class="user-edit"
>
<t-cell-group v-if="managedUser.user" theme="card">
<t-cell <t-cell
:title="currentUser.name" :title="managedUser.user.name"
:description="currentUser.role" :description="getRoleDisplayName(managedUser.user.roleList?.[0], permissionStore.roleList)"
> >
<template #leftIcon> <template #leftIcon>
<t-image <t-image
class="avatar" class="avatar"
:src="currentUser.avatar || buildFallbackAvatar(currentUser.name)" :src="buildUserAvatar(managedUser.user)"
:alt="currentUser.name" :alt="managedUser.user.name"
fit="cover" fit="cover"
/> />
</template> </template>
</t-cell> </t-cell>
</t-cell-group> </t-cell-group>
<t-cell-group v-if="currentUser" theme="card"> <t-cell-group v-if="managedUser.user" theme="card">
<t-cell title="状态" :note="managedUser.enabled ? '启用' : '禁用'" />
<t-cell title="备注" :note="managedUser.remark" />
<t-cell title="手机号"> <t-cell title="手机号">
<template #description> <template #note>
<telephone-link :value="currentUser.mobile" placeholder="未填写" /> <telephone-link :value="managedUser.user.phoneNo" placeholder="未填写" />
</template> </template>
</t-cell> </t-cell>
<t-cell title="上次登录时间" :description="currentUser.lastLoginAt || '暂无记录'" /> <t-cell title="上次登录时间" :note="Time.toPassedDateTime(managedUser.user.lastLoginAt)" />
</t-cell-group> </t-cell-group>
<t-cell-group v-if="currentUser" theme="card"> <t-cell-group v-if="currentUser" theme="card">
@@ -61,12 +67,13 @@
<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 { Time, type User, UserAPI } 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";
import type { EventItem } from "@/types/EventItem"; import type { EventItem } from "@/types/EventItem";
import { usePermissionSettingsStore } from "@/store/permissionSettingsStore"; import { usePermissionSettingsStore } from "@/store/permissionSettingsStore";
import { buildFallbackAvatar } from "@/utils/avatar";
import { buildEffectiveRoleCodeSet, getRoleDisplayName } from "@/utils/role"; import { buildEffectiveRoleCodeSet, getRoleDisplayName } from "@/utils/role";
import { permissionCodes } from "@/utils/permission"; import { permissionCodes } from "@/utils/permission";
@@ -95,7 +102,7 @@ const currentUser = computed(() => {
role: getRoleDisplayName(item.roleList?.[0], permissionStore.roleList), role: getRoleDisplayName(item.roleList?.[0], permissionStore.roleList),
avatar: buildUserAvatar(user), avatar: buildUserAvatar(user),
mobile: user?.phoneNo || "", mobile: user?.phoneNo || "",
lastLoginAt: user?.lastLoginAt ? new Date(user.lastLoginAt).toLocaleString() : "" lastLoginAt: user?.lastLoginAt
}; };
}); });
const scopeList = computed(() => { const scopeList = computed(() => {
@@ -114,7 +121,6 @@ async function toEdit(): Promise<void> {
if (!currentUser.value) { if (!currentUser.value) {
return; return;
} }
await router.push(`/management/users/${currentUser.value.id}/edit`); await router.push(`/management/users/${currentUser.value.id}/edit`);
} }
@@ -122,26 +128,12 @@ async function toRoleAssign(): Promise<void> {
if (!currentUser.value) { if (!currentUser.value) {
return; return;
} }
await router.push({ await router.push({
path: `/management/users/${currentUser.value.id}/roles` path: `/management/users/${currentUser.value.id}/roles`,
query: managedUser.value?.storeId ? { storeId: managedUser.value.storeId } : undefined
}); });
} }
// ---------- 头像 ----------
function buildFallbackAvatar(name: string): string {
const text = encodeURIComponent((name.trim() || "用户").slice(0, 2));
return [
"data:image/svg+xml;charset=UTF-8,",
"%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120' viewBox='0 0 120 120'%3E",
"%3Crect width='120' height='120' rx='24' fill='%232563eb'/%3E",
"%3Ctext x='50%25' y='54%25' fill='white' font-size='36' font-family='Arial,sans-serif' text-anchor='middle' dominant-baseline='middle'%3E",
text,
"%3C/text%3E%3C/svg%3E"
].join("");
}
function buildUserAvatar(user?: Partial<User>): string { function buildUserAvatar(user?: Partial<User>): string {
const name = user?.nick || user?.name || "用户"; const name = user?.nick || user?.name || "用户";
return isUser(user) ? UserAPI.getAvatarURL(user) || buildFallbackAvatar(name) : buildFallbackAvatar(name); return isUser(user) ? UserAPI.getAvatarURL(user) || buildFallbackAvatar(name) : buildFallbackAvatar(name);
+101 -3
View File
@@ -10,16 +10,32 @@
<t-form-item v-if="isCreateMode" label="密码"> <t-form-item v-if="isCreateMode" label="密码">
<t-input v-model.trim="formModel.password" type="password" placeholder="密码" borderless /> <t-input v-model.trim="formModel.password" type="password" placeholder="密码" borderless />
</t-form-item> </t-form-item>
<t-form-item v-if="isGlobalManager" label="门店" arrow @click="openStorePicker">
<div class="picker-text" v-text="selectedStoreName" />
</t-form-item>
</t-form> </t-form>
<t-button class="save" theme="primary" :loading="isSaving" @click="saveUser">保存</t-button> <t-button class="save" theme="primary" :loading="isSaving" @click="saveUser">保存</t-button>
</div> </div>
<t-popup v-model="storePickerVisible" placement="bottom">
<t-picker
v-model="storePickerDraft"
:columns="storePickerColumns"
title="选择门店"
@cancel="storePickerVisible = false"
@confirm="confirmStorePicker"
/>
</t-popup>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Toast } from "tdesign-mobile-vue"; import { Toast } from "tdesign-mobile-vue";
import { useUserStore } from "timi-web";
import ManagedUserAPI from "@/api/ManagedUserAPI"; import ManagedUserAPI from "@/api/ManagedUserAPI";
import { useCurrentStoreStore } from "@/store/currentStoreStore";
import type { ManagedUser } from "@/types/ManagedUser"; import type { ManagedUser } from "@/types/ManagedUser";
import { permissionCodes } from "@/utils/permission"; import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
type PickerValue = string | number;
defineOptions({ defineOptions({
name: "UserEdit" name: "UserEdit"
@@ -27,6 +43,10 @@ defineOptions({
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const userStore = useUserStore();
const currentStoreStore = useCurrentStoreStore();
const isGlobalManager = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
// ---------- 表单 ---------- // ---------- 表单 ----------
@@ -35,6 +55,7 @@ const isSaving = ref(false);
const formModel = reactive({ const formModel = reactive({
id: "", id: "",
userId: "", userId: "",
storeId: "",
name: "", name: "",
mobile: "", mobile: "",
password: "" password: ""
@@ -42,9 +63,59 @@ 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);
// ---------- 门店选择 ----------
const storePickerVisible = ref(false);
const storePickerDraft = ref<PickerValue[]>([]);
const storePickerColumns = computed(() => [
currentStoreStore.storeList.map(item => ({
label: item.name || item.code || "未命名门店",
value: item.id || ""
})).filter(item => item.value)
]);
const selectedStoreName = computed(() => {
const store = currentStoreStore.storeList.find(item => item.id === formModel.storeId);
return store?.name || store?.code || "请选择门店";
});
function openStorePicker(): void {
if (!currentStoreStore.storeList.length) {
Toast.warning("暂无可选门店");
return;
}
storePickerDraft.value = [formModel.storeId];
storePickerVisible.value = true;
}
function confirmStorePicker(): void {
const storeId = String(storePickerDraft.value[0] || "");
if (!storeId) {
return;
}
formModel.storeId = storeId;
storePickerVisible.value = false;
}
// ---------- 页面数据 ---------- // ---------- 页面数据 ----------
async function prepareStoreScope(): Promise<void> {
if (isGlobalManager.value) {
await currentStoreStore.loadStoreList();
const queryStoreId = String(route.query.storeId || "");
formModel.storeId = currentStoreStore.storeList.some(item => item.id === queryStoreId)
? queryStoreId
: currentStoreStore.currentStoreId;
return;
}
const store = await currentStoreStore.loadLoginStore();
formModel.storeId = store.id || "";
}
async function fillForm(): Promise<void> { async function fillForm(): Promise<void> {
await prepareStoreScope();
const routeId = String(route.params.id || ""); const routeId = String(route.params.id || "");
if (routeId) { if (routeId) {
const current = await ManagedUserAPI.detail(routeId); const current = await ManagedUserAPI.detail(routeId);
@@ -52,6 +123,7 @@ async function fillForm(): Promise<void> {
Object.assign(formModel, { Object.assign(formModel, {
id: current.id, id: current.id,
userId: current.userId || "", userId: current.userId || "",
storeId: current.storeId || formModel.storeId,
name: current.user?.nick || current.user?.name || "", name: current.user?.nick || current.user?.name || "",
mobile: current.user?.phoneNo || "", mobile: current.user?.phoneNo || "",
password: "" password: ""
@@ -62,6 +134,7 @@ async function fillForm(): Promise<void> {
Object.assign(formModel, { Object.assign(formModel, {
id: "", id: "",
userId: "", userId: "",
storeId: formModel.storeId,
name: "", name: "",
mobile: "", mobile: "",
password: "" password: ""
@@ -80,6 +153,7 @@ async function saveUser(): Promise<void> {
const formData: ManagedUser = { const formData: ManagedUser = {
id: formModel.id || undefined, id: formModel.id || undefined,
userId: formModel.userId || undefined, userId: formModel.userId || undefined,
storeId: formModel.storeId || undefined,
enabled: currentManagedUser.value?.enabled ?? true, enabled: currentManagedUser.value?.enabled ?? true,
remark: currentManagedUser.value?.remark || "", remark: currentManagedUser.value?.remark || "",
user: { user: {
@@ -90,9 +164,25 @@ async function saveUser(): Promise<void> {
password: isCreateMode.value ? formModel.password : undefined password: isCreateMode.value ? formModel.password : undefined
} }
}; };
const saved = isCreateMode.value ? await ManagedUserAPI.create(formData) : await ManagedUserAPI.update(formData); let saved: ManagedUser;
if (isCreateMode.value) {
saved = await ManagedUserAPI.create(formData);
} else {
saved = await ManagedUserAPI.update(formData);
if (isGlobalManager.value && currentManagedUser.value?.storeId !== formModel.storeId) {
await ManagedUserAPI.transferStore({
id: formModel.id || undefined,
storeId: formModel.storeId || undefined
});
}
}
Toast.success("用户已保存"); Toast.success("用户已保存");
await router.push(`/management/users/${saved.id}`); const detailPath = `/management/users/${saved.id || formModel.id}`;
if (isCreateMode.value) {
await router.replace(detailPath);
} else {
await router.push(detailPath);
}
} finally { } finally {
isSaving.value = false; isSaving.value = false;
} }
@@ -103,6 +193,10 @@ function validateForm(): boolean {
Toast.warning("请填写姓名"); Toast.warning("请填写姓名");
return false; return false;
} }
if (!formModel.storeId) {
Toast.warning("请选择门店");
return false;
}
if (isCreateMode.value && !formModel.password) { if (isCreateMode.value && !formModel.password) {
Toast.warning("请填写密码"); Toast.warning("请填写密码");
return false; return false;
@@ -127,6 +221,10 @@ onMounted(async () => {
border-radius: 1rem; border-radius: 1rem;
} }
.picker-text {
color: var(--td-text-color-primary);
}
.save { .save {
left: 1rem; left: 1rem;
right: 1rem; right: 1rem;
+109 -59
View File
@@ -1,25 +1,45 @@
<template> <template>
<div v-can="permissionCodes.user.read" class="user-list"> <div v-can="permissionCodes.user.read" class="user-list">
<div class="store-menu">
<t-dropdown-menu v-if="isGlobalManager">
<t-dropdown-item
v-model="selectedStoreId"
:label="storeFilterLabel"
:options="storeOptions"
@change="selectStoreFilter"
/>
</t-dropdown-menu>
</div>
<t-pull-down-refresh <t-pull-down-refresh
v-model="isRefreshing" v-model="isRefreshing"
:refresh-timeout="30000" :refresh-timeout="30000"
@refresh="refresh" @refresh="refresh"
@scrolltolower="fetch" @scrolltolower="fetch"
> >
<div v-for="item in pageResult.list" :key="item.id" class="item" @click="toDetail(item)"> <t-cell-group theme="card">
<t-image :src="buildUserAvatar(item)" :alt="getUserName(item)" fit="cover" class="avatar" /> <t-cell
<div class="content"> class="item"
<div class="header"> v-for="item in pageResult.list"
<span class="name" v-text="getUserName(item)" /> :key="item.id"
<t-tag class="meta" size="small" theme="success" variant="outline" :content="getRoleName(item)" /> :description="`可登记事件:${getActionScope(item)}`"
</div> arrow
<span class="events gray" v-text="`可登记事件:${getActionScope(item)}`" /> @click="toDetail(item)"
</div> >
</div> <template #left-icon>
<div v-if="!pageResult.list.length" class="empty-block" v-text="isLoading ? '正在加载用户' : '暂无用户'" /> <t-image :src="buildUserAvatar(item)" :alt="getUserName(item)" fit="cover" class="avatar" />
</template>
<template #title>
<div class="header">
<span class="name" v-text="getUserName(item)" />
<t-tag class="meta" size="small" theme="success" variant="outline" :content="getRoleName(item)" />
</div>
</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 ? '已经到底了' : '上拉加载更多'" /> <div v-if="pageResult.list.length" class="load-state" v-text="isNoMore ? '已经到底了' : '上拉加载更多'" />
</t-pull-down-refresh> </t-pull-down-refresh>
<t-button v-can="permissionCodes.user.create" class="add" theme="primary" type="button" @click="router.push('/management/users/create')"> <t-button v-can="permissionCodes.user.create" class="add" theme="primary" type="button" @click="toCreate">
<template #icon> <template #icon>
<t-icon name="add" /> <t-icon name="add" />
</template> </template>
@@ -29,23 +49,53 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Toast } from "tdesign-mobile-vue"; import { Toast, type DropdownValue } from "tdesign-mobile-vue";
import { type Page, type PageResult, UserAPI, type User } from "timi-web"; import { type Page, type PageResult, UserAPI, type User, 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 { useCurrentStoreStore } from "@/store/currentStoreStore";
import type { ManagedUser } from "@/types/ManagedUser"; import type { ManagedUser } from "@/types/ManagedUser";
import type { EventItem } from "@/types/EventItem"; import type { EventItem } from "@/types/EventItem";
import { usePermissionSettingsStore } from "@/store/permissionSettingsStore"; import { usePermissionSettingsStore } from "@/store/permissionSettingsStore";
import { buildFallbackAvatar } from "@/utils/avatar";
import { buildEffectiveRoleCodeSet, getRoleDisplayName } from "@/utils/role"; import { buildEffectiveRoleCodeSet, getRoleDisplayName } from "@/utils/role";
import { permissionCodes } from "@/utils/permission"; import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
defineOptions({ defineOptions({
name: "UserList" name: "UserList"
}); });
const router = useRouter(); const router = useRouter();
const userStore = useUserStore();
const currentStoreStore = useCurrentStoreStore();
const permissionStore = usePermissionSettingsStore(); const permissionStore = usePermissionSettingsStore();
const isGlobalManager = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
// ---------- 门店筛选 ----------
const allStoreFilterValue = "__ALL_STORES__";
const selectedStoreId = ref(allStoreFilterValue);
const loginStoreId = ref("");
const storeOptions = computed(() => [
{ label: "全部门店", value: allStoreFilterValue },
...currentStoreStore.storeList
.map(item => ({
label: item.name || item.code || "未命名门店",
value: item.id || ""
}))
.filter(item => item.value)
]);
const storeFilterLabel = computed(() => storeOptions.value.find(item => item.value === selectedStoreId.value)?.label || "全部门店");
const userListOptions = computed(() => {
if (isGlobalManager.value) {
return selectedStoreId.value === allStoreFilterValue
? { allStores: true }
: { storeId: selectedStoreId.value };
}
return { storeId: loginStoreId.value };
});
// ---------- 列表数据 ---------- // ---------- 列表数据 ----------
const eventList = ref<EventItem[]>([]); const eventList = ref<EventItem[]>([]);
@@ -99,23 +149,22 @@ function isUser(user?: Partial<User>): user is User {
return typeof user?.name === "string"; return typeof user?.name === "string";
} }
function buildFallbackAvatar(name: string): string {
const text = encodeURIComponent((name.trim() || "用户").slice(0, 2));
return [
"data:image/svg+xml;charset=UTF-8,",
"%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120' viewBox='0 0 120 120'%3E",
"%3Crect width='120' height='120' rx='24' fill='%232563eb'/%3E",
"%3Ctext x='50%25' y='54%25' fill='white' font-size='36' font-family='Arial,sans-serif' text-anchor='middle' dominant-baseline='middle'%3E",
text,
"%3C/text%3E%3C/svg%3E"
].join("");
}
// ---------- 页面数据 ---------- // ---------- 页面数据 ----------
async function loadStoreScope(): Promise<void> {
if (isGlobalManager.value) {
await currentStoreStore.loadStoreList();
return;
}
const store = await currentStoreStore.loadLoginStore();
loginStoreId.value = store.id || "";
}
async function loadData(): Promise<void> { async function loadData(): Promise<void> {
await loadStoreScope();
const [userResult, actions] = await Promise.all([ const [userResult, actions] = await Promise.all([
ManagedUserAPI.list(page), ManagedUserAPI.list(page, userListOptions.value),
EventAPI.listValid(), EventAPI.listValid(),
permissionStore.loadAll() permissionStore.loadAll()
]); ]);
@@ -146,7 +195,7 @@ async function fetch(): Promise<void> {
isLoading.value = true; isLoading.value = true;
try { try {
const result = await ManagedUserAPI.list(page); const result = await ManagedUserAPI.list(page, userListOptions.value);
pageResult.total = result.total; pageResult.total = result.total;
pageResult.list = pageResult.list.concat(result.list || []); pageResult.list = pageResult.list.concat(result.list || []);
page.index += 1; page.index += 1;
@@ -156,8 +205,24 @@ async function fetch(): Promise<void> {
} }
} }
async function selectStoreFilter(value: DropdownValue): Promise<void> {
if (Array.isArray(value)) {
return;
}
selectedStoreId.value = String(value);
await refresh();
}
// ---------- 导航 ---------- // ---------- 导航 ----------
async function toCreate(): Promise<void> {
await router.push({
path: "/management/users/create",
query: selectedStoreId.value === allStoreFilterValue ? undefined : { storeId: selectedStoreId.value }
});
}
async function toDetail(item: ManagedUser): Promise<void> { async function toDetail(item: ManagedUser): Promise<void> {
if (!item.id) { if (!item.id) {
return; return;
@@ -177,49 +242,34 @@ onMounted(async () => {
<style scoped lang="less"> <style scoped lang="less">
.user-list { .user-list {
padding: calc(var(--app-nav-height) + 1rem) 1rem 6rem 1rem; padding: calc(var(--app-nav-height) + 4rem) 0 6rem 0;
display: grid; display: grid;
gap: .75rem; gap: .75rem;
.store-menu {
top: var(--app-nav-height);
left: 0;
right: 0;
z-index: 20;
position: fixed;
background: #fff;
}
.item { .item {
gap: .5rem; padding: 10px;
display: flex;
padding: .5rem;
align-items: center;
justify-content: space-between;
border: 1px solid var(--td-border-level-1-color);
background: #f7faff;
border-radius: 1rem;
.avatar { .avatar {
width: 3.5rem; width: 3.5rem;
height: 3.5rem; height: 3.5rem;
flex: none;
border-radius: .5rem; border-radius: .5rem;
} }
.content { .header {
gap: 1rem;
flex: 1; flex: 1;
gap: .5rem;
display: flex; display: flex;
min-width: 0; align-items: center;
flex-direction: column; justify-content: space-between;
.header {
gap: .5rem;
display: flex;
align-items: center;
justify-content: space-between;
}
.name {
font-size: 1rem;
font-weight: 700;
}
.events {
font-size: .875rem;
}
} }
} }
+81 -19
View File
@@ -2,6 +2,13 @@
<div v-can="corePermissionCodes.userRole.read" class="user-role-assign"> <div v-can="corePermissionCodes.userRole.read" class="user-role-assign">
<div class="user-menu"> <div class="user-menu">
<t-dropdown-menu> <t-dropdown-menu>
<t-dropdown-item
v-if="isGlobalManager"
v-model="selectedStoreId"
:label="storeFilterLabel"
:options="storeOptions"
@change="handleStoreChange"
/>
<t-dropdown-item <t-dropdown-item
:label="activeUserLabel" :label="activeUserLabel"
:options="userOptions" :options="userOptions"
@@ -42,10 +49,15 @@
<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 { RoleAPI, type ModuleCode, type Role } from "timi-web"; import { RoleAPI, type ModuleCode, type Role, useUserStore } from "timi-web";
import ManagedUserAPI from "@/api/ManagedUserAPI"; import ManagedUserAPI from "@/api/ManagedUserAPI";
import { useCurrentStoreStore } from "@/store/currentStoreStore";
import type { ManagedUser } from "@/types/ManagedUser"; import type { ManagedUser } from "@/types/ManagedUser";
import { corePermissionCodes } from "@/utils/permission"; import { corePermissionCodes, gaoRoleCodes } from "@/utils/permission";
type ManagedRole = Role & {
id: string;
};
defineOptions({ defineOptions({
name: "UserRoleAssign" name: "UserRoleAssign"
@@ -54,18 +66,43 @@ defineOptions({
const route = useRoute(); const route = useRoute();
const moduleCode: ModuleCode = "GAO"; const moduleCode: ModuleCode = "GAO";
const pageSize = 1000; const pageSize = 1000;
const allStoreFilterValue = "__ALL_STORES__";
// ---------- 用户选择 ---------- // ---------- 用户选择 ----------
const userStore = useUserStore();
const currentStoreStore = useCurrentStoreStore();
const userList = ref<ManagedUser[]>([]); const userList = ref<ManagedUser[]>([]);
const roleList = ref<Role[]>([]); const roleList = ref<ManagedRole[]>([]);
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 selectedStoreId = ref(allStoreFilterValue);
const loginStoreId = ref("");
const checkedRoleIds = ref<string[]>([]); const checkedRoleIds = ref<string[]>([]);
const isLoading = ref(false); const isLoading = ref(false);
const isSaving = ref(false); const isSaving = ref(false);
let roleSyncSeq = 0; let roleSyncSeq = 0;
const isGlobalManager = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
const storeOptions = computed(() => [
{ label: "全部门店", value: allStoreFilterValue },
...currentStoreStore.storeList
.map(item => ({
label: item.name || item.code || "未命名门店",
value: item.id || ""
}))
.filter(item => item.value)
]);
const storeFilterLabel = computed(() => storeOptions.value.find(item => item.value === selectedStoreId.value)?.label || "全部门店");
const userListOptions = computed(() => {
if (isGlobalManager.value) {
return selectedStoreId.value === allStoreFilterValue
? { allStores: true }
: { storeId: selectedStoreId.value };
}
return { storeId: loginStoreId.value };
});
// ---------- 权限 ---------- // ---------- 权限 ----------
const userRoleWriteAccess = { const userRoleWriteAccess = {
@@ -95,29 +132,54 @@ async function handleUserChange(value: DropdownValue): Promise<void> {
await loadActiveUserRoles(); await loadActiveUserRoles();
} }
async function handleStoreChange(value: DropdownValue): Promise<void> {
if (Array.isArray(value)) {
return;
}
selectedStoreId.value = String(value);
await loadUserList();
}
// ---------- 页面数据 ---------- // ---------- 页面数据 ----------
async function loadStoreScope(): Promise<void> {
if (isGlobalManager.value) {
await currentStoreStore.loadStoreList();
const queryStoreId = String(route.query.storeId || "");
selectedStoreId.value = currentStoreStore.storeList.some(item => item.id === queryStoreId)
? queryStoreId
: allStoreFilterValue;
return;
}
const store = await currentStoreStore.loadLoginStore();
loginStoreId.value = store.id || "";
}
async function loadData(): Promise<void> { async function loadData(): Promise<void> {
isLoading.value = true; isLoading.value = true;
try { try {
const [userResult, roles] = await Promise.all([ await loadStoreScope();
ManagedUserAPI.list({ roleList.value = await loadRoleList();
index: 0, await loadUserList();
size: 100
}),
loadRoleList()
]);
userList.value = userResult.list || [];
roleList.value = roles || [];
if (!activeManagedUserId.value) {
activeManagedUserId.value = userList.value[0]?.id || "";
}
await loadActiveUserRoles();
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
} }
async function loadUserList(): Promise<void> {
const result = await ManagedUserAPI.list({
index: 0,
size: 100
}, userListOptions.value);
userList.value = result.list || [];
if (!userList.value.some(item => item.id === activeManagedUserId.value)) {
activeManagedUserId.value = userList.value[0]?.id || "";
}
await loadActiveUserRoles();
}
// ---------- 角色同步 ---------- // ---------- 角色同步 ----------
async function loadActiveUserRoles(): Promise<void> { async function loadActiveUserRoles(): Promise<void> {
@@ -168,7 +230,7 @@ function getUserName(item: ManagedUser): string {
// ---------- 角色列表 ---------- // ---------- 角色列表 ----------
async function loadRoleList(): Promise<Role[]> { async function loadRoleList(): Promise<ManagedRole[]> {
const result = await RoleAPI.list({ const result = await RoleAPI.list({
index: 0, index: 0,
size: pageSize, size: pageSize,
@@ -179,7 +241,7 @@ async function loadRoleList(): Promise<Role[]> {
code: "ASC" code: "ASC"
} }
}); });
return result.list || []; return (result.list || []).filter((role): role is ManagedRole => Boolean(role.id));
} }
// ---------- 角色勾选 ---------- // ---------- 角色勾选 ----------
@@ -190,7 +252,7 @@ function isRoleChecked(roleId: string): boolean {
function getRoleId(role: Role | string): string { function getRoleId(role: Role | string): string {
if (typeof role !== "string") { if (typeof role !== "string") {
return role.id; return role.id || "";
} }
const fullRole = roleList.value.find(item => item.code === role || `${item.moduleCode}:${item.code}` === role); const fullRole = roleList.value.find(item => item.code === role || `${item.moduleCode}:${item.code}` === role);
+9 -17
View File
@@ -21,9 +21,8 @@
/> />
</template> </template>
</t-cell> </t-cell>
</t-cell-group>
<t-cell-group v-if="canSelectStore" class="store-card" theme="card">
<t-cell <t-cell
v-if="canSelectStore"
class="cell" class="cell"
title="当前门店" title="当前门店"
:note="currentStoreStore.currentStoreName" :note="currentStoreStore.currentStoreName"
@@ -72,7 +71,8 @@
import { UserAPI, useUserStore } from "timi-web"; import { UserAPI, useUserStore } from "timi-web";
import { Toast } from "tdesign-mobile-vue"; import { Toast } from "tdesign-mobile-vue";
import { useCurrentStoreStore } from "@/store/currentStoreStore"; import { useCurrentStoreStore } from "@/store/currentStoreStore";
import { corePermissionCodes, coreRoleCodes, permissionCodes } from "@/utils/permission"; import { buildFallbackAvatar } from "@/utils/avatar";
import { corePermissionCodes, coreRoleCodes, gaoRoleCodes, permissionCodes } from "@/utils/permission";
type UserAvatarSource = Parameters<typeof UserAPI.getAvatarURL>[0]; type UserAvatarSource = Parameters<typeof UserAPI.getAvatarURL>[0];
type PickerValue = string | number; type PickerValue = string | number;
@@ -105,12 +105,14 @@ const currentUserDesc = computed(() => currentUser.value?.phoneNo || currentUser
const currentUserAvatar = computed(() => buildUserAvatar(currentUser.value)); const currentUserAvatar = computed(() => buildUserAvatar(currentUser.value));
const canSelectStore = computed(() => userStore.hasPermissions(permissionCodes.store.read)); const canSelectStore = computed(() => userStore.hasPermissions(permissionCodes.store.read));
const canLoadLoginStore = computed(() => userStore.hasRoles([gaoRoleCodes.storeManager, gaoRoleCodes.employee], "or"));
const canManagePermission = computed(() => ( const canManagePermission = computed(() => (
userStore.hasRoles(coreRoleCodes.admin) || userStore.hasRoles(coreRoleCodes.userRoleAdmin) userStore.hasRoles(coreRoleCodes.admin) || userStore.hasRoles(coreRoleCodes.userRoleAdmin)
) && userStore.hasPermissions([ ) && userStore.hasPermissions([
corePermissionCodes.role.read, corePermissionCodes.role.read,
corePermissionCodes.permission.read corePermissionCodes.permission.read
], "and")); ], "and"));
const canManageUser = computed(() => userStore.hasRoles([gaoRoleCodes.globalManager, gaoRoleCodes.storeManager], "or"));
const visibleMenuGroupList = computed<MenuGroup[]>(() => menuGroupList.value const visibleMenuGroupList = computed<MenuGroup[]>(() => menuGroupList.value
.map(group => ({ .map(group => ({
...group, ...group,
@@ -211,7 +213,7 @@ const menuGroupList = computed<MenuGroup[]>(() => [
icon: "root-list", icon: "root-list",
path: "/management/users", path: "/management/users",
title: "用户管理", title: "用户管理",
visible: userStore.hasPermissions(permissionCodes.user.read) visible: canManageUser.value && userStore.hasPermissions(permissionCodes.user.read)
}, },
{ {
icon: "qrcode", icon: "qrcode",
@@ -223,20 +225,8 @@ const menuGroupList = computed<MenuGroup[]>(() => [
} }
]); ]);
function buildFallbackAvatar(name: string): string {
const text = encodeURIComponent((name.trim() || "用户").slice(0, 2));
return [
"data:image/svg+xml;charset=UTF-8,",
"%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120' viewBox='0 0 120 120'%3E",
"%3Crect width='120' height='120' rx='24' fill='%230f766e'/%3E",
"%3Ctext x='50%25' y='54%25' fill='white' font-size='36' font-family='Arial,sans-serif' text-anchor='middle' dominant-baseline='middle'%3E",
text,
"%3C/text%3E%3C/svg%3E"
].join("");
}
function buildUserAvatar(user?: UserAvatarSource): string { function buildUserAvatar(user?: UserAvatarSource): string {
return UserAPI.getAvatarURL(user) || buildFallbackAvatar(currentUserName.value); return UserAPI.getAvatarURL(user) || buildFallbackAvatar(currentUserName.value, "#0f766e");
} }
async function go(path: string): Promise<void> { async function go(path: string): Promise<void> {
@@ -246,6 +236,8 @@ async function go(path: string): Promise<void> {
onMounted(async () => { onMounted(async () => {
if (canSelectStore.value) { if (canSelectStore.value) {
await currentStoreStore.loadStoreList(); await currentStoreStore.loadStoreList();
} else if (canLoadLoginStore.value) {
await currentStoreStore.loadLoginStore();
} }
}); });
</script> </script>
+38 -1
View File
@@ -1,12 +1,18 @@
import { Toast } from "tdesign-mobile-vue"; import { Toast } from "tdesign-mobile-vue";
import { useUserStore } from "timi-web";
import StoreAPI from "@/api/StoreAPI"; import StoreAPI from "@/api/StoreAPI";
import type { Store } from "@/types/Store"; import type { Store } from "@/types/Store";
import { getCurrentStoreId, readStoredCurrentStoreId, setCurrentStoreId } from "@/utils/store"; import { getCurrentStoreId, readStoredCurrentStoreId, setCurrentStoreId } from "@/utils/store";
export const useCurrentStoreStore = defineStore("currentStore", () => { export const useCurrentStoreStore = defineStore("currentStore", () => {
const userStore = useUserStore();
const storeList = ref<Store[]>([]); const storeList = ref<Store[]>([]);
const currentStoreId = ref(readStoredCurrentStoreId()); const currentStoreId = ref(readStoredCurrentStoreId());
const loginStore = ref<Store>();
const loginStoreUserId = ref("");
const isLoading = ref(false); const isLoading = ref(false);
let loginStoreRequest: Promise<Store> | undefined;
let loginStoreRequestUserId = "";
const currentStore = computed(() => storeList.value.find(item => item.id === currentStoreId.value)); const currentStore = computed(() => storeList.value.find(item => item.id === currentStoreId.value));
const currentStoreName = computed(() => currentStore.value?.name || "默认门店"); const currentStoreName = computed(() => currentStore.value?.name || "默认门店");
@@ -38,6 +44,36 @@ export const useCurrentStoreStore = defineStore("currentStore", () => {
} }
} }
async function loadLoginStore(): Promise<Store> {
const loginUserId = userStore.loginUser.user?.id || userStore.loginUser.id || "";
if (loginStore.value?.id && loginStoreUserId.value === loginUserId) {
return loginStore.value;
}
if (!loginStoreRequest || loginStoreRequestUserId !== loginUserId) {
loginStoreRequestUserId = loginUserId;
loginStoreRequest = loadLoginStoreRequest(loginUserId);
}
const request = loginStoreRequest;
try {
return await request;
} finally {
if (loginStoreRequest === request) {
loginStoreRequest = undefined;
}
}
}
async function loadLoginStoreRequest(loginUserId: string): Promise<Store> {
const result = await StoreAPI.loginStore();
loginStore.value = result;
loginStoreUserId.value = loginUserId;
if (result.id) {
selectStore(result.id);
}
return result;
}
function selectStore(storeId: string): void { function selectStore(storeId: string): void {
if (!storeId) { if (!storeId) {
return; return;
@@ -64,10 +100,11 @@ export const useCurrentStoreStore = defineStore("currentStore", () => {
storeList, storeList,
currentStoreId, currentStoreId,
currentStore, currentStore,
loginStore,
currentStoreName, currentStoreName,
isLoading, isLoading,
loadStoreList, loadStoreList,
loadLoginStore,
selectStore selectStore
}; };
}); });
+4 -2
View File
@@ -13,7 +13,8 @@ export const moduleCode: ModuleCode = "GAO";
const pageSize = 1000; const pageSize = 1000;
export type Permission = UserPermission; export type Permission = UserPermission;
export type Role = UserRole & { export type Role = Omit<UserRole, "id" | "permissionIdList" | "childRoleIdList"> & {
id: string;
permissionIdList: string[]; permissionIdList: string[];
childRoleIdList: string[]; childRoleIdList: string[];
}; };
@@ -40,10 +41,11 @@ export type PermissionDraft = {
function normalizeRole(role: UserRole): Role { function normalizeRole(role: UserRole): Role {
return { return {
...role, ...role,
id: role.id || "",
moduleCode: role.moduleCode || moduleCode, moduleCode: role.moduleCode || moduleCode,
description: role.description || "", description: role.description || "",
permissionIdList: role.permissionIdList || role.permissionList?.map(item => item.id) || [], permissionIdList: role.permissionIdList || role.permissionList?.map(item => item.id) || [],
childRoleIdList: role.childRoleIdList || role.childRoleList?.map(item => item.id) || [] childRoleIdList: role.childRoleIdList || role.childRoleList?.map(item => item.id).filter((id): id is string => Boolean(id)) || []
}; };
} }
+18
View File
@@ -0,0 +1,18 @@
/**
* 构建用户备用头像。
*
* @param name 头像中显示的用户名
* @param backgroundColor 头像背景色
*/
export function buildFallbackAvatar(name: string, backgroundColor = "#2563eb"): string {
const text = encodeURIComponent((name.trim() || "用户").slice(0, 2));
const fill = encodeURIComponent(backgroundColor);
return [
"data:image/svg+xml;charset=UTF-8,",
"%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120' viewBox='0 0 120 120'%3E",
`%3Crect width='120' height='120' rx='24' fill='${fill}'/%3E`,
"%3Ctext x='50%25' y='54%25' fill='white' font-size='36' font-family='Arial,sans-serif' text-anchor='middle' dominant-baseline='middle'%3E",
text,
"%3C/text%3E%3C/svg%3E"
].join("");
}
+6
View File
@@ -75,6 +75,12 @@ export const coreRoleCodes = {
userRoleAdmin: "CORE:USER_ROLE_ADMIN" userRoleAdmin: "CORE:USER_ROLE_ADMIN"
} as const; } as const;
export const gaoRoleCodes = {
globalManager: "GAO:GLOBAL_MANAGER",
storeManager: "GAO:STORE_MANAGER",
employee: "GAO:EMPLOYEE"
} as const;
export const permissionManagementRoleAccess = { export const permissionManagementRoleAccess = {
value: [ value: [
coreRoleCodes.admin, coreRoleCodes.admin,
+2 -3
View File
@@ -2,7 +2,6 @@ import type { StoreBusinessDayOpenStatus, StoreStatus } from "@/types/Store";
import { permissionCodes } from "@/utils/permission"; import { permissionCodes } from "@/utils/permission";
import { type Page, useUserStore } from "timi-web"; import { type Page, useUserStore } from "timi-web";
export const defaultStoreId = "00000000-0000-0000-0000-000000000001";
export const currentStoreIdStorageKey = "gao-customer-h5:current-store-id"; export const currentStoreIdStorageKey = "gao-customer-h5:current-store-id";
type LoginUserWithStore = { type LoginUserWithStore = {
@@ -21,9 +20,9 @@ type LoginUserWithStore = {
export function getCurrentStoreId(): string { export function getCurrentStoreId(): string {
const userStore = useUserStore(); const userStore = useUserStore();
if (userStore.hasPermissions(permissionCodes.store.read)) { if (userStore.hasPermissions(permissionCodes.store.read)) {
return readStoredCurrentStoreId() || getLoginUserStoreId() || defaultStoreId; return readStoredCurrentStoreId() || getLoginUserStoreId();
} }
return getLoginUserStoreId() || defaultStoreId; return getLoginUserStoreId();
} }
export function setCurrentStoreId(storeId: string): void { export function setCurrentStoreId(storeId: string): void {