update user detail
This commit is contained in:
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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-cell
|
||||||
|
class="item"
|
||||||
|
v-for="item in pageResult.list"
|
||||||
|
:key="item.id"
|
||||||
|
:description="`可登记事件:${getActionScope(item)}`"
|
||||||
|
arrow
|
||||||
|
@click="toDetail(item)"
|
||||||
|
>
|
||||||
|
<template #left-icon>
|
||||||
<t-image :src="buildUserAvatar(item)" :alt="getUserName(item)" fit="cover" class="avatar" />
|
<t-image :src="buildUserAvatar(item)" :alt="getUserName(item)" fit="cover" class="avatar" />
|
||||||
<div class="content">
|
</template>
|
||||||
|
<template #title>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<span class="name" v-text="getUserName(item)" />
|
<span class="name" v-text="getUserName(item)" />
|
||||||
<t-tag class="meta" size="small" theme="success" variant="outline" :content="getRoleName(item)" />
|
<t-tag class="meta" size="small" theme="success" variant="outline" :content="getRoleName(item)" />
|
||||||
</div>
|
</div>
|
||||||
<span class="events gray" v-text="`可登记事件:${getActionScope(item)}`" />
|
</template>
|
||||||
</div>
|
</t-cell>
|
||||||
</div>
|
</t-cell-group>
|
||||||
<div v-if="!pageResult.list.length" class="empty-block" v-text="isLoading ? '正在加载用户' : '暂无用户'" />
|
<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,50 +242,35 @@ 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 {
|
|
||||||
gap: 1rem;
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
min-width: 0;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
flex: 1;
|
||||||
gap: .5rem;
|
gap: .5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.events {
|
|
||||||
font-size: .875rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.add {
|
.add {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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)) || []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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("");
|
||||||
|
}
|
||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user