refactor file tree
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="managedUser"
|
||||
v-can="permissionCodes.user.read"
|
||||
class="user-edit"
|
||||
>
|
||||
<t-cell-group v-if="managedUser.user" theme="card">
|
||||
<t-cell
|
||||
:title="managedUser.user.name"
|
||||
:description="getRoleDisplayName(getManagedUserRoleList(managedUser)[0], permissionStore.roleList)"
|
||||
>
|
||||
<template #leftIcon>
|
||||
<t-image
|
||||
class="avatar"
|
||||
:src="buildUserAvatar(managedUser.user)"
|
||||
:alt="managedUser.user.name"
|
||||
fit="cover"
|
||||
/>
|
||||
</template>
|
||||
</t-cell>
|
||||
</t-cell-group>
|
||||
|
||||
<t-cell-group v-if="managedUser.user" theme="card">
|
||||
<t-cell title="状态" :note="managedUser.enabled ? '启用' : '禁用'" />
|
||||
<t-cell title="备注" :note="managedUser.remark" />
|
||||
<t-cell title="手机号">
|
||||
<template #note>
|
||||
<telephone-link :value="managedUser.user.phoneNo" placeholder="未填写" />
|
||||
</template>
|
||||
</t-cell>
|
||||
<t-cell title="上次登录时间" :note="Time.toPassedDateTime(managedUser.user.lastLoginAt)" />
|
||||
</t-cell-group>
|
||||
|
||||
<t-cell-group v-if="currentUser" theme="card">
|
||||
<t-cell
|
||||
v-can="permissionCodes.user.update"
|
||||
title="配置角色"
|
||||
note="为用户授权业务角色"
|
||||
arrow
|
||||
hover
|
||||
@click="toRoleAssign"
|
||||
>
|
||||
<template #leftIcon>
|
||||
<t-icon name="usergroup" size="1.75rem" />
|
||||
</template>
|
||||
</t-cell>
|
||||
</t-cell-group>
|
||||
|
||||
<section v-if="currentUser" class="registration-actions bg-white">
|
||||
<header>
|
||||
<h4 class="title">可登记事件</h4>
|
||||
</header>
|
||||
<div class="actions">
|
||||
<t-tag
|
||||
v-for="item in scopeList"
|
||||
:key="item"
|
||||
variant="light-outline"
|
||||
theme="success"
|
||||
:content="item"
|
||||
/>
|
||||
<div v-if="!scopeList.length" class="empty-card">未配置</div>
|
||||
</div>
|
||||
</section>
|
||||
<t-button v-can="permissionCodes.user.update" class="edit" theme="primary" @click="toEdit">编辑用户</t-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Toast } from "tdesign-mobile-vue";
|
||||
import { Time, type User, UserAPI } from "timi-web";
|
||||
import EventAPI from "@/api/EventAPI";
|
||||
import ManagedUserAPI from "@/api/ManagedUserAPI";
|
||||
import type { ManagedUser } from "@/types/ManagedUser";
|
||||
import type { EventItem } from "@/types/EventItem";
|
||||
import { usePermissionSettingsStore } from "@/store/permissionSettingsStore";
|
||||
import { buildFallbackAvatar } from "@/utils/avatar";
|
||||
import { buildEffectiveRoleCodeSet, getManagedUserRoleList, getRoleDisplayName } from "@/utils/role";
|
||||
import { permissionCodes } from "@/utils/permission";
|
||||
|
||||
defineOptions({
|
||||
name: "UserDetail"
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const permissionStore = usePermissionSettingsStore();
|
||||
|
||||
// ---------- 用户数据 ----------
|
||||
|
||||
const managedUser = ref<ManagedUser>();
|
||||
const eventList = ref<EventItem[]>([]);
|
||||
|
||||
const currentUser = computed(() => {
|
||||
const item = managedUser.value;
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
const user = item.user;
|
||||
return {
|
||||
id: item.id || "",
|
||||
name: user?.nick || user?.name || "未命名用户",
|
||||
role: getRoleDisplayName(getManagedUserRoleList(item)[0], permissionStore.roleList),
|
||||
avatar: buildUserAvatar(user),
|
||||
mobile: user?.phoneNo || "",
|
||||
lastLoginAt: user?.lastLoginAt
|
||||
};
|
||||
});
|
||||
const scopeList = computed(() => {
|
||||
const item = managedUser.value;
|
||||
if (!item) {
|
||||
return [];
|
||||
}
|
||||
return eventList.value
|
||||
.filter(action => canEventItem(action, item))
|
||||
.map(item => item.name);
|
||||
});
|
||||
|
||||
// ---------- 导航 ----------
|
||||
|
||||
async function toEdit(): Promise<void> {
|
||||
if (!currentUser.value) {
|
||||
return;
|
||||
}
|
||||
await router.push(`/management/users/${currentUser.value.id}/edit`);
|
||||
}
|
||||
|
||||
async function toRoleAssign(): Promise<void> {
|
||||
if (!currentUser.value) {
|
||||
return;
|
||||
}
|
||||
await router.push({
|
||||
path: `/management/users/${currentUser.value.id}/roles`,
|
||||
query: managedUser.value?.storeId ? { storeId: managedUser.value.storeId } : undefined
|
||||
});
|
||||
}
|
||||
|
||||
function buildUserAvatar(user?: Partial<User>): string {
|
||||
const name = user?.nick || user?.name || "用户";
|
||||
return isUser(user) ? UserAPI.getAvatarURL(user) || buildFallbackAvatar(name) : buildFallbackAvatar(name);
|
||||
}
|
||||
|
||||
function isUser(user?: Partial<User>): user is User {
|
||||
return typeof user?.name === "string";
|
||||
}
|
||||
|
||||
// ---------- 事件范围 ----------
|
||||
|
||||
function canEventItem(action: EventItem, item: ManagedUser): boolean {
|
||||
const userRoleCodeSet = buildEffectiveRoleCodeSet(item);
|
||||
const roleCodeList = action.roleCodeList || [];
|
||||
if (!roleCodeList.length) {
|
||||
return false;
|
||||
}
|
||||
return roleCodeList.some(code => userRoleCodeSet.has(code));
|
||||
}
|
||||
|
||||
// ---------- 页面数据 ----------
|
||||
|
||||
async function loadData(): Promise<void> {
|
||||
const routeId = String(route.params.id || "");
|
||||
const [detail, actions] = await Promise.all([
|
||||
ManagedUserAPI.detail(routeId),
|
||||
EventAPI.listValid(),
|
||||
permissionStore.loadAll()
|
||||
]);
|
||||
managedUser.value = detail;
|
||||
eventList.value = actions || [];
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await loadData();
|
||||
} catch {
|
||||
Toast.error("用户详情加载失败");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.user-edit {
|
||||
gap: 1rem;
|
||||
display: flex;
|
||||
padding: calc(var(--app-nav-height) + 1rem) 0 4rem 0;
|
||||
flex-direction: column;
|
||||
|
||||
.avatar {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
border-radius: .875rem;
|
||||
}
|
||||
|
||||
.registration-actions {
|
||||
gap: .5rem;
|
||||
margin: 0 1rem;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
border-radius: 1rem;
|
||||
flex-direction: column;
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
gap: .5rem;
|
||||
display: flex;
|
||||
|
||||
.empty-card {
|
||||
color: #52607a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit {
|
||||
left: 1rem;
|
||||
right: 1rem;
|
||||
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
|
||||
position: fixed;
|
||||
}
|
||||
}
|
||||
|
||||
:global(html.theme-dark) {
|
||||
.user-edit {
|
||||
.panel,
|
||||
.bottom-bar {
|
||||
background: rgba(15, 23, 42, .92);
|
||||
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
|
||||
}
|
||||
|
||||
p,
|
||||
.info-list li span,
|
||||
.empty-card {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.action {
|
||||
color: #e2e8f0;
|
||||
background: rgba(15, 23, 42, .72);
|
||||
border-color: rgba(148, 163, 184, .16);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,244 @@
|
||||
<template>
|
||||
<div v-can="savePermissionCode" class="user-edit">
|
||||
<t-form class="form" label-align="left">
|
||||
<t-form-item label="姓名">
|
||||
<t-input v-model.trim="formModel.name" placeholder="姓名" borderless />
|
||||
</t-form-item>
|
||||
<t-form-item v-if="isCreateMode" label="手机号">
|
||||
<t-input v-model.trim="formModel.mobile" type="tel" placeholder="手机号" borderless />
|
||||
</t-form-item>
|
||||
<t-form-item v-if="isCreateMode" label="密码">
|
||||
<t-input v-model.trim="formModel.password" type="password" placeholder="密码" borderless />
|
||||
</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-button class="save" theme="primary" :loading="isSaving" @click="saveUser">保存</t-button>
|
||||
</div>
|
||||
<t-popup v-model="storePickerVisible" placement="bottom">
|
||||
<t-picker
|
||||
v-model="storePickerDraft"
|
||||
:columns="storePickerColumns"
|
||||
title="选择门店"
|
||||
@cancel="storePickerVisible = false"
|
||||
@confirm="confirmStorePicker"
|
||||
/>
|
||||
</t-popup>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Toast } from "tdesign-mobile-vue";
|
||||
import { useUserStore } from "timi-web";
|
||||
import ManagedUserAPI from "@/api/ManagedUserAPI";
|
||||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||
import type { ManagedUser } from "@/types/ManagedUser";
|
||||
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
||||
|
||||
type PickerValue = string | number;
|
||||
|
||||
defineOptions({
|
||||
name: "UserEdit"
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const currentStoreStore = useCurrentStoreStore();
|
||||
|
||||
const isGlobalManager = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
|
||||
|
||||
// ---------- 表单 ----------
|
||||
|
||||
const currentManagedUser = ref<ManagedUser>();
|
||||
const isSaving = ref(false);
|
||||
const formModel = reactive({
|
||||
id: "",
|
||||
userId: "",
|
||||
storeId: "",
|
||||
name: "",
|
||||
mobile: "",
|
||||
password: ""
|
||||
});
|
||||
const isCreateMode = computed(() => !String(route.params.id || ""));
|
||||
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> {
|
||||
await prepareStoreScope();
|
||||
const routeId = String(route.params.id || "");
|
||||
if (routeId) {
|
||||
const current = await ManagedUserAPI.detail(routeId);
|
||||
currentManagedUser.value = current;
|
||||
Object.assign(formModel, {
|
||||
id: current.id,
|
||||
userId: current.userId || "",
|
||||
storeId: current.storeId || formModel.storeId,
|
||||
name: current.user?.nick || current.user?.name || "",
|
||||
mobile: current.user?.phoneNo || "",
|
||||
password: ""
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Object.assign(formModel, {
|
||||
id: "",
|
||||
userId: "",
|
||||
storeId: formModel.storeId,
|
||||
name: "",
|
||||
mobile: "",
|
||||
password: ""
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- 保存 ----------
|
||||
|
||||
async function saveUser(): Promise<void> {
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
isSaving.value = true;
|
||||
try {
|
||||
const formData: ManagedUser = {
|
||||
id: formModel.id || undefined,
|
||||
userId: formModel.userId || undefined,
|
||||
storeId: formModel.storeId || undefined,
|
||||
enabled: currentManagedUser.value?.enabled ?? true,
|
||||
remark: currentManagedUser.value?.remark || "",
|
||||
user: {
|
||||
id: formModel.userId || undefined,
|
||||
name: formModel.name,
|
||||
nick: formModel.name,
|
||||
phoneNo: formModel.mobile || undefined,
|
||||
password: isCreateMode.value ? formModel.password : undefined
|
||||
}
|
||||
};
|
||||
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("用户已保存");
|
||||
const detailPath = `/management/users/${saved.id || formModel.id}`;
|
||||
if (isCreateMode.value) {
|
||||
await router.replace(detailPath);
|
||||
} else {
|
||||
await router.push(detailPath);
|
||||
}
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function validateForm(): boolean {
|
||||
if (!formModel.name) {
|
||||
Toast.warning("请填写姓名");
|
||||
return false;
|
||||
}
|
||||
if (!formModel.storeId) {
|
||||
Toast.warning("请选择门店");
|
||||
return false;
|
||||
}
|
||||
if (isCreateMode.value && !formModel.password) {
|
||||
Toast.warning("请填写密码");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await fillForm();
|
||||
} catch {
|
||||
Toast.error("用户数据加载失败");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.user-edit {
|
||||
padding: calc(var(--app-nav-height) + 1rem) 0 4rem 0;
|
||||
|
||||
.form {
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.picker-text {
|
||||
color: var(--td-text-color-primary);
|
||||
}
|
||||
|
||||
.save {
|
||||
left: 1rem;
|
||||
right: 1rem;
|
||||
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
|
||||
position: fixed;
|
||||
}
|
||||
}
|
||||
|
||||
:global(html.theme-dark) {
|
||||
.user-edit {
|
||||
:deep(.form-item .t-form__label) {
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
<template>
|
||||
<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
|
||||
v-model="isRefreshing"
|
||||
:refresh-timeout="30000"
|
||||
@refresh="refresh"
|
||||
@scrolltolower="fetch"
|
||||
>
|
||||
<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" />
|
||||
</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 ? '已经到底了' : '上拉加载更多'" />
|
||||
</t-pull-down-refresh>
|
||||
<t-button v-can="permissionCodes.user.create" class="add" theme="primary" type="button" @click="toCreate">
|
||||
<template #icon>
|
||||
<t-icon name="add" />
|
||||
</template>
|
||||
新增
|
||||
</t-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Toast, type DropdownValue } from "tdesign-mobile-vue";
|
||||
import { type Page, type PageResult, UserAPI, type User, useUserStore } from "timi-web";
|
||||
import EventAPI from "@/api/EventAPI";
|
||||
import ManagedUserAPI from "@/api/ManagedUserAPI";
|
||||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||
import type { ManagedUser } from "@/types/ManagedUser";
|
||||
import type { EventItem } from "@/types/EventItem";
|
||||
import { usePermissionSettingsStore } from "@/store/permissionSettingsStore";
|
||||
import { buildFallbackAvatar } from "@/utils/avatar";
|
||||
import { buildEffectiveRoleCodeSet, getManagedUserRoleList, getRoleDisplayName } from "@/utils/role";
|
||||
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
||||
|
||||
defineOptions({
|
||||
name: "UserList"
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const currentStoreStore = useCurrentStoreStore();
|
||||
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 isRefreshing = ref(false);
|
||||
const isLoading = ref(false);
|
||||
const isNoMore = ref(false);
|
||||
const page = reactive<Page<ManagedUser>>({
|
||||
index: 0,
|
||||
size: 20
|
||||
});
|
||||
const pageResult = reactive<PageResult<ManagedUser>>({
|
||||
total: 0,
|
||||
list: []
|
||||
});
|
||||
|
||||
// ---------- 展示文案 ----------
|
||||
|
||||
function getUserName(item: ManagedUser): string {
|
||||
return item.user?.nick || item.user?.name || "未命名用户";
|
||||
}
|
||||
|
||||
function getRoleName(item: ManagedUser): string {
|
||||
return getRoleDisplayName(getManagedUserRoleList(item)[0], permissionStore.roleList);
|
||||
}
|
||||
|
||||
function getActionScope(item: ManagedUser): string {
|
||||
const nameList = eventList.value
|
||||
.filter(action => canEventItem(action, item))
|
||||
.map(action => action.name);
|
||||
return nameList.join("、") || "未配置";
|
||||
}
|
||||
|
||||
// ---------- 事件范围 ----------
|
||||
|
||||
function canEventItem(action: EventItem, item: ManagedUser): boolean {
|
||||
const userRoleCodeSet = buildEffectiveRoleCodeSet(item);
|
||||
const roleCodeList = action.roleCodeList || [];
|
||||
if (!roleCodeList.length) {
|
||||
return false;
|
||||
}
|
||||
return roleCodeList.some(code => userRoleCodeSet.has(code));
|
||||
}
|
||||
|
||||
// ---------- 头像 ----------
|
||||
|
||||
function buildUserAvatar(item: ManagedUser): string {
|
||||
return isUser(item.user) ? UserAPI.getAvatarURL(item.user) || buildFallbackAvatar(getUserName(item)) : buildFallbackAvatar(getUserName(item));
|
||||
}
|
||||
|
||||
function isUser(user?: Partial<User>): user is User {
|
||||
return typeof user?.name === "string";
|
||||
}
|
||||
|
||||
// ---------- 页面数据 ----------
|
||||
|
||||
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> {
|
||||
await loadStoreScope();
|
||||
const [userResult, actions] = await Promise.all([
|
||||
ManagedUserAPI.list(page, userListOptions.value),
|
||||
EventAPI.listValid(),
|
||||
permissionStore.loadAll()
|
||||
]);
|
||||
pageResult.total = userResult.total;
|
||||
pageResult.list = userResult.list || [];
|
||||
page.index = 1;
|
||||
isNoMore.value = pageResult.list.length >= pageResult.total || pageResult.list.length < page.size;
|
||||
eventList.value = actions || [];
|
||||
}
|
||||
|
||||
async function refresh(): Promise<void> {
|
||||
isRefreshing.value = true;
|
||||
try {
|
||||
page.index = 0;
|
||||
pageResult.total = 0;
|
||||
pageResult.list = [];
|
||||
isNoMore.value = false;
|
||||
await fetch();
|
||||
} finally {
|
||||
isRefreshing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetch(): Promise<void> {
|
||||
if (isLoading.value || isNoMore.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const result = await ManagedUserAPI.list(page, userListOptions.value);
|
||||
pageResult.total = result.total;
|
||||
pageResult.list = pageResult.list.concat(result.list || []);
|
||||
page.index += 1;
|
||||
isNoMore.value = pageResult.list.length >= pageResult.total || (result.list || []).length < page.size;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
if (!item.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
await router.push(`/management/users/${item.id}`);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await loadData();
|
||||
} catch {
|
||||
Toast.error("用户列表加载失败");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.user-list {
|
||||
padding: calc(var(--app-nav-height) + 4rem) 0 6rem 0;
|
||||
display: grid;
|
||||
gap: .75rem;
|
||||
|
||||
.store-menu {
|
||||
top: var(--app-nav-height);
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 20;
|
||||
position: fixed;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.item {
|
||||
padding: 10px;
|
||||
|
||||
.avatar {
|
||||
width: 3.5rem;
|
||||
height: 3.5rem;
|
||||
border-radius: .5rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
flex: 1;
|
||||
gap: .5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.add {
|
||||
left: 1rem;
|
||||
right: 1rem;
|
||||
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
|
||||
position: fixed;
|
||||
}
|
||||
}
|
||||
|
||||
:global(html.theme-dark) {
|
||||
.user-list {
|
||||
.item {
|
||||
background: rgba(30, 41, 59, .9);
|
||||
|
||||
.name {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.meta {
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div class="password-page">
|
||||
<t-form class="form" label-align="left">
|
||||
<t-form-item label="旧密码">
|
||||
<t-input v-model.trim="formModel.oldPassword" type="password" placeholder="旧密码" borderless />
|
||||
</t-form-item>
|
||||
<t-form-item label="新密码">
|
||||
<t-input v-model.trim="formModel.newPassword" type="password" placeholder="新密码" borderless />
|
||||
</t-form-item>
|
||||
<t-form-item label="确认密码">
|
||||
<t-input v-model.trim="formModel.confirmPassword" type="password" placeholder="确认密码" borderless />
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
<t-button class="save" theme="primary" :loading="isSaving" @click="savePassword">保存</t-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Toast } from "tdesign-mobile-vue";
|
||||
import { UserAPI, useUserStore } from "timi-web";
|
||||
|
||||
defineOptions({
|
||||
name: "UserPassword"
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
|
||||
// ---------- 表单 ----------
|
||||
|
||||
const isSaving = ref(false);
|
||||
const formModel = reactive({
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
confirmPassword: ""
|
||||
});
|
||||
|
||||
// ---------- 保存 ----------
|
||||
|
||||
async function savePassword(): Promise<void> {
|
||||
if (!formModel.oldPassword || !formModel.newPassword) {
|
||||
Toast.warning("请填写旧密码和新密码");
|
||||
return;
|
||||
}
|
||||
if (formModel.newPassword !== formModel.confirmPassword) {
|
||||
Toast.warning("两次输入的新密码不一致");
|
||||
return;
|
||||
}
|
||||
isSaving.value = true;
|
||||
try {
|
||||
await UserAPI.updatePassword({
|
||||
oldValue: formModel.oldPassword,
|
||||
newValue: formModel.newPassword
|
||||
});
|
||||
userStore.clearLoginUser();
|
||||
Toast.success("密码已更新,请重新登录");
|
||||
await router.replace("/login");
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.password-page {
|
||||
padding: calc(var(--app-nav-height) + 1rem) 0 4rem 0;
|
||||
|
||||
.form {
|
||||
padding: .5rem 0;
|
||||
background: rgba(255, 255, 255, .92);
|
||||
box-shadow: var(--td-shadow-1);
|
||||
border-radius: 1.4rem;
|
||||
}
|
||||
|
||||
.save {
|
||||
left: 1rem;
|
||||
right: 1rem;
|
||||
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
|
||||
position: fixed;
|
||||
}
|
||||
}
|
||||
|
||||
:global(html.theme-dark) {
|
||||
.password-page {
|
||||
.form {
|
||||
background: rgba(15, 23, 42, .88);
|
||||
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<div class="profile-page">
|
||||
<t-form class="form" label-align="left">
|
||||
<t-form-item label="头像">
|
||||
<photo-field
|
||||
v-model="formModel.avatar"
|
||||
add-text="更换"
|
||||
alt-text="用户头像"
|
||||
size="compact"
|
||||
:loading="isAvatarUploading"
|
||||
:preview="false"
|
||||
:show-remove="false"
|
||||
@add="changeAvatar"
|
||||
/>
|
||||
</t-form-item>
|
||||
<t-form-item label="用户名">
|
||||
<t-input v-model.trim="formModel.name" placeholder="用户名" borderless />
|
||||
</t-form-item>
|
||||
<t-form-item label="昵称">
|
||||
<t-input v-model.trim="formModel.nickname" placeholder="昵称" borderless />
|
||||
</t-form-item>
|
||||
<t-form-item label="性别">
|
||||
<t-radio-group class="sex" v-model="formModel.gender" variant="default-filled">
|
||||
<t-radio :block="false" borderless value="MALE">男</t-radio>
|
||||
<t-radio :block="false" borderless value="FEMALE">女</t-radio>
|
||||
<t-radio :block="false" borderless value="">未知</t-radio>
|
||||
</t-radio-group>
|
||||
</t-form-item>
|
||||
<t-form-item label="手机号">
|
||||
<div class="mobile-value">
|
||||
<div v-if="formModel.mobile" v-text="formModel.mobile" />
|
||||
<span v-else v-text="'未填写'" />
|
||||
</div>
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
<input
|
||||
ref="avatarInputRef"
|
||||
class="avatar-input"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
@change="handleAvatarSelected"
|
||||
>
|
||||
<t-cell-group theme="card">
|
||||
<t-cell title="修改密码" arrow hover @click="toPassword" />
|
||||
</t-cell-group>
|
||||
<t-button class="save" theme="primary" :loading="isSaving" @click="saveProfile">保存</t-button>
|
||||
<t-button class="logout bg-white" theme="danger" variant="outline" @click="confirmLogout">退出登录</t-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DialogPlugin, Toast } from "tdesign-mobile-vue";
|
||||
import { AttachmentAPI, type Gender, UserAPI, useUserStore } from "timi-web";
|
||||
import PhotoField from "@/components/PhotoField.vue";
|
||||
import { buildFallbackAvatar } from "@/utils/avatar";
|
||||
import { compressUploadImage } from "@/utils/imageCompression";
|
||||
|
||||
type UserAvatarSource = NonNullable<Parameters<typeof UserAPI.getAvatarURL>[0]> & {
|
||||
name?: string;
|
||||
nick?: string;
|
||||
};
|
||||
type ProfileGender = Gender | "";
|
||||
defineOptions({
|
||||
name: "UserProfile"
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
|
||||
// ---------- 头像 ----------
|
||||
|
||||
const avatarInputRef = ref<HTMLInputElement>();
|
||||
const avatarTempFileId = ref("");
|
||||
const isAvatarUploading = ref(false);
|
||||
|
||||
// ---------- 表单 ----------
|
||||
|
||||
const isSaving = ref(false);
|
||||
|
||||
const formModel = reactive({
|
||||
id: "",
|
||||
name: "",
|
||||
avatar: "",
|
||||
nickname: "",
|
||||
role: "",
|
||||
gender: "" as ProfileGender,
|
||||
mobile: "",
|
||||
password: "",
|
||||
office: "",
|
||||
scope: "",
|
||||
status: "",
|
||||
lastLoginAt: ""
|
||||
});
|
||||
|
||||
// ---------- 页面数据 ----------
|
||||
|
||||
function fillForm(): void {
|
||||
const user = userStore.loginUser.user;
|
||||
Object.assign(formModel, {
|
||||
id: user?.id || "",
|
||||
name: user?.name || "",
|
||||
avatar: buildUserAvatar(user),
|
||||
nickname: user?.nick || "",
|
||||
role: formatRoleCode(userStore.getRoleList()[0]),
|
||||
gender: user?.gender || "",
|
||||
mobile: user?.phoneNo || "",
|
||||
password: "",
|
||||
office: "",
|
||||
scope: "",
|
||||
status: "",
|
||||
lastLoginAt: user?.lastLoginAt ? new Date(user.lastLoginAt).toLocaleString() : ""
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- 头像 ----------
|
||||
|
||||
function changeAvatar(): void {
|
||||
avatarInputRef.value?.click();
|
||||
}
|
||||
|
||||
async function handleAvatarSelected(event: Event): Promise<void> {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
isAvatarUploading.value = true;
|
||||
try {
|
||||
const compressedFile = await compressUploadImage(file);
|
||||
const [tempFile] = await AttachmentAPI.upload(compressedFile);
|
||||
if (!tempFile?.id) {
|
||||
Toast.error("头像上传失败");
|
||||
return;
|
||||
}
|
||||
avatarTempFileId.value = tempFile.id;
|
||||
formModel.avatar = AttachmentAPI.getTempReadURL(tempFile.id);
|
||||
} finally {
|
||||
isAvatarUploading.value = false;
|
||||
input.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 导航 ----------
|
||||
|
||||
async function toPassword(): Promise<void> {
|
||||
await router.push("/management/profile/password");
|
||||
}
|
||||
|
||||
// ---------- 保存 ----------
|
||||
|
||||
async function saveProfile(): Promise<void> {
|
||||
if (!formModel.name) {
|
||||
Toast.warning("请填写用户名");
|
||||
return;
|
||||
}
|
||||
isSaving.value = true;
|
||||
try {
|
||||
const result = await UserAPI.updateCurrent({
|
||||
id: formModel.id,
|
||||
name: formModel.name,
|
||||
nick: formModel.nickname,
|
||||
gender: formModel.gender,
|
||||
tempFileIdList: avatarTempFileId.value ? [avatarTempFileId.value] : undefined
|
||||
});
|
||||
await userStore.updateLoginUser(result);
|
||||
Toast.success("资料已保存");
|
||||
await router.back();
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 退出登录 ----------
|
||||
|
||||
async function confirmLogout(): Promise<void> {
|
||||
const dialog = DialogPlugin.confirm({
|
||||
title: "退出登录",
|
||||
content: "确定要退出当前账号吗?",
|
||||
cancelBtn: "取消",
|
||||
confirmBtn: "退出",
|
||||
onCancel() {
|
||||
dialog.destroy();
|
||||
},
|
||||
async onConfirm() {
|
||||
dialog.destroy();
|
||||
await logout();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function logout(): Promise<void> {
|
||||
await userStore.logout();
|
||||
await router.replace("/login");
|
||||
}
|
||||
|
||||
// ---------- 格式化 ----------
|
||||
|
||||
function formatRoleCode(roleCode?: string): string {
|
||||
if (!roleCode) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const codePartList = roleCode.split(":");
|
||||
return codePartList[codePartList.length - 1] || roleCode;
|
||||
}
|
||||
|
||||
function buildUserAvatar(user?: UserAvatarSource): string {
|
||||
const name = user?.nick || user?.name || "用户";
|
||||
return UserAPI.getAvatarURL(user) || buildFallbackAvatar(name, "#0f766e");
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fillForm();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.profile-page {
|
||||
gap: 1rem;
|
||||
padding: calc(var(--app-nav-height) + 1rem) 0 6rem 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.form {
|
||||
padding: .5rem 0;
|
||||
background: rgba(255, 255, 255, .92);
|
||||
box-shadow: var(--td-shadow-1);
|
||||
border-radius: 1.4rem;
|
||||
|
||||
.sex {
|
||||
gap: .5rem;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-value {
|
||||
color: var(--td-text-color-primary);
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
.save,
|
||||
.logout {
|
||||
left: 1rem;
|
||||
right: 1rem;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.save {
|
||||
bottom: calc(3.75rem + env(safe-area-inset-bottom, 0));
|
||||
}
|
||||
|
||||
.logout {
|
||||
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
|
||||
}
|
||||
}
|
||||
|
||||
:global(html.theme-dark) {
|
||||
.profile-page {
|
||||
.form {
|
||||
background: rgba(15, 23, 42, .88);
|
||||
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,335 @@
|
||||
<template>
|
||||
<div v-can="corePermissionCodes.userRole.read" class="user-role-assign">
|
||||
<div class="user-menu">
|
||||
<t-dropdown-menu>
|
||||
<t-dropdown-item
|
||||
v-if="isGlobalManager"
|
||||
v-model="selectedStoreId"
|
||||
:label="storeFilterLabel"
|
||||
:options="storeOptions"
|
||||
@change="handleStoreChange"
|
||||
/>
|
||||
<t-dropdown-item
|
||||
:label="activeUserLabel"
|
||||
:options="userOptions"
|
||||
:value="activeManagedUserId"
|
||||
@change="handleUserChange"
|
||||
/>
|
||||
</t-dropdown-menu>
|
||||
</div>
|
||||
<t-cell-group v-if="activeUser && roleList.length" theme="card">
|
||||
<t-cell
|
||||
v-for="item in roleList"
|
||||
:key="item.id"
|
||||
:title="item.name"
|
||||
:description="item.code"
|
||||
>
|
||||
<template #right-icon>
|
||||
<t-switch
|
||||
v-can="userRoleWriteAccess"
|
||||
:model-value="isRoleChecked(item.id)"
|
||||
@change="value => setRoleChecked(item.id, value === true)"
|
||||
/>
|
||||
</template>
|
||||
</t-cell>
|
||||
</t-cell-group>
|
||||
<t-empty v-else-if="!isLoading" description="暂无可授权角色" />
|
||||
<t-button
|
||||
v-can="userRoleWriteAccess"
|
||||
class="save"
|
||||
theme="primary"
|
||||
:loading="isSaving"
|
||||
@click="saveRoleList"
|
||||
>
|
||||
保存授权
|
||||
</t-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { DropdownOption, DropdownValue } from "tdesign-mobile-vue";
|
||||
import { Toast } from "tdesign-mobile-vue";
|
||||
import { RoleAPI, type ModuleCode, type Role as UserRole, useUserStore } from "timi-web";
|
||||
import ManagedUserAPI from "@/api/ManagedUserAPI";
|
||||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||
import type { Role } from "@/store/permissionSettingsStore";
|
||||
import type { ManagedUser } from "@/types/ManagedUser";
|
||||
import { corePermissionCodes, gaoRoleCodes } from "@/utils/permission";
|
||||
|
||||
defineOptions({
|
||||
name: "UserRoleAssign"
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const moduleCode: ModuleCode = "GAO";
|
||||
const allStoreFilterValue = "__ALL_STORES__";
|
||||
|
||||
// ---------- 用户选择 ----------
|
||||
|
||||
const userStore = useUserStore();
|
||||
const currentStoreStore = useCurrentStoreStore();
|
||||
const userList = ref<ManagedUser[]>([]);
|
||||
const roleList = ref<Role[]>([]);
|
||||
const routeManagedUserId = computed(() => String(route.params.id || route.query.managedUserId || ""));
|
||||
const activeManagedUserId = ref(routeManagedUserId.value);
|
||||
const selectedStoreId = ref(allStoreFilterValue);
|
||||
const loginStoreId = ref("");
|
||||
const checkedRoleIds = ref<string[]>([]);
|
||||
const isLoading = ref(false);
|
||||
const isSaving = ref(false);
|
||||
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 = {
|
||||
value: [
|
||||
corePermissionCodes.userRole.create,
|
||||
corePermissionCodes.userRole.delete
|
||||
],
|
||||
mode: "and"
|
||||
} as const;
|
||||
|
||||
// ---------- 用户状态 ----------
|
||||
|
||||
const activeUser = computed(() => userList.value.find(item => item.id === activeManagedUserId.value));
|
||||
const activeUserLabel = computed(() => activeUser.value ? `当前用户:${getUserName(activeUser.value)}` : "选择用户");
|
||||
const userOptions = computed<DropdownOption[]>(() => userList.value
|
||||
.filter(item => !!item.id)
|
||||
.map(item => ({
|
||||
label: getUserName(item),
|
||||
value: item.id as string,
|
||||
disabled: false
|
||||
})));
|
||||
|
||||
// ---------- 用户切换 ----------
|
||||
|
||||
async function handleUserChange(value: DropdownValue): Promise<void> {
|
||||
activeManagedUserId.value = Array.isArray(value) ? String(value[0] || "") : String(value);
|
||||
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> {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
await loadStoreScope();
|
||||
roleList.value = await loadRoleList();
|
||||
await loadUserList();
|
||||
} finally {
|
||||
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> {
|
||||
const managedUserId = activeManagedUserId.value;
|
||||
const managedUser = activeUser.value;
|
||||
if (!managedUserId || !managedUser?.userId) {
|
||||
checkedRoleIds.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSeq = roleSyncSeq + 1;
|
||||
roleSyncSeq = currentSeq;
|
||||
const roles = await RoleAPI.authorizedList(managedUser.userId, moduleCode);
|
||||
if (currentSeq !== roleSyncSeq || activeManagedUserId.value !== managedUserId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const availableRoleIdSet = new Set(roleList.value.map(item => item.id));
|
||||
checkedRoleIds.value = roles
|
||||
.map(getRoleId)
|
||||
.filter((item): item is string => !!item && availableRoleIdSet.has(item));
|
||||
}
|
||||
|
||||
// ---------- 保存 ----------
|
||||
|
||||
async function saveRoleList(): Promise<void> {
|
||||
const current = activeUser.value;
|
||||
if (!current?.id || !current.userId) {
|
||||
Toast.warning("请选择用户");
|
||||
return;
|
||||
}
|
||||
|
||||
isSaving.value = true;
|
||||
try {
|
||||
await RoleAPI.authorize({
|
||||
userId: current.userId,
|
||||
moduleCode: moduleCode,
|
||||
roleIdList: [...checkedRoleIds.value]
|
||||
});
|
||||
await loadActiveUserRoles();
|
||||
Toast.success("用户角色已保存");
|
||||
} catch (error) {
|
||||
checkedRoleIds.value = [];
|
||||
try {
|
||||
await reloadAuthorizationScope();
|
||||
await loadActiveUserRoles();
|
||||
} finally {
|
||||
Toast.error(getErrorMessage(error, "用户角色授权失败"));
|
||||
}
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 展示文案 ----------
|
||||
|
||||
function getUserName(item: ManagedUser): string {
|
||||
return item.user?.nick || item.user?.name || item.user?.phoneNo || "未命名用户";
|
||||
}
|
||||
|
||||
// ---------- 角色列表 ----------
|
||||
|
||||
async function loadRoleList(): Promise<Role[]> {
|
||||
const result = await RoleAPI.grantableList(moduleCode);
|
||||
return (result || [])
|
||||
.filter((role): role is UserRole & { id: string } => Boolean(role.id))
|
||||
.map(role => ({
|
||||
...role,
|
||||
id: role.id,
|
||||
permissionIdList: role.permissionIdList || role.permissionList?.map(item => item.id) || [],
|
||||
childRoleIdList: role.childRoleIdList || role.childRoleList?.map(item => item.id).filter((id): id is string => Boolean(id)) || [],
|
||||
delegationPermissionIdList: role.delegationPermissionIdList || role.delegationPermissionList?.map(item => item.id) || []
|
||||
}));
|
||||
}
|
||||
|
||||
async function reloadAuthorizationScope(): Promise<void> {
|
||||
roleList.value = await loadRoleList();
|
||||
}
|
||||
|
||||
// ---------- 角色勾选 ----------
|
||||
|
||||
function isRoleChecked(roleId: string): boolean {
|
||||
return checkedRoleIds.value.includes(roleId);
|
||||
}
|
||||
|
||||
function getRoleId(role: UserRole | string): string {
|
||||
if (typeof role !== "string") {
|
||||
return role.id || "";
|
||||
}
|
||||
|
||||
const fullRole = roleList.value.find(item => item.code === role || `${item.moduleCode}:${item.code}` === role);
|
||||
return fullRole?.id || "";
|
||||
}
|
||||
|
||||
function getErrorMessage(error: unknown, fallback: string): string {
|
||||
if (error instanceof Error && error.message) {
|
||||
return error.message;
|
||||
}
|
||||
if (typeof error === "string" && error) {
|
||||
return error;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function setRoleChecked(roleId: string, value: boolean): void {
|
||||
if (value && !checkedRoleIds.value.includes(roleId)) {
|
||||
checkedRoleIds.value.push(roleId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
checkedRoleIds.value = checkedRoleIds.value.filter(item => item !== roleId);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await loadData();
|
||||
} catch {
|
||||
Toast.error("用户角色加载失败");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.user-role-assign {
|
||||
gap: 1rem;
|
||||
display: flex;
|
||||
padding: calc(var(--app-nav-height) + 4.5rem) 0 6rem 0;
|
||||
flex-direction: column;
|
||||
|
||||
.user-menu {
|
||||
top: var(--app-nav-height);
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 20;
|
||||
position: fixed;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.save {
|
||||
left: 1rem;
|
||||
right: 1rem;
|
||||
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
|
||||
position: fixed;
|
||||
}
|
||||
}
|
||||
|
||||
:global(html.theme-dark) {
|
||||
.user-role-assign {
|
||||
.user-menu {
|
||||
background: #0f172a;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user