fix user profile update

This commit is contained in:
Timi
2026-08-07 18:20:29 +08:00
parent 82e717e3c6
commit 16fa5fdd36
10 changed files with 386 additions and 73 deletions
+20 -1
View File
@@ -4,7 +4,26 @@ import { getCurrentStoreId, withCurrentStoreId, withCurrentStorePage } from "@/u
const BASE_URI = "/gao/event"; const BASE_URI = "/gao/event";
async function list(req: Page<EventItem>): Promise<PageResult<EventItem>> { type ListOptions = {
allStores?: boolean;
storeId?: string;
};
async function list(req: Page<EventItem>, options?: ListOptions): Promise<PageResult<EventItem>> {
if (options?.allStores) {
return await axios.post(`${BASE_URI}/list`, req);
}
if (options?.storeId) {
return await axios.post(`${BASE_URI}/list`, {
...req,
equalsExample: {
...(req.equalsExample || {}),
storeId: options.storeId
}
});
}
return await axios.post(`${BASE_URI}/list`, withCurrentStorePage(req)); return await axios.post(`${BASE_URI}/list`, withCurrentStorePage(req));
} }
+1 -1
View File
@@ -1,6 +1,6 @@
<template> <template>
<t-icon <t-icon
v-if="iconName" v-if="value && iconName"
:name="iconName" :name="iconName"
:size="size" :size="size"
:class="['gender-icon', `gender-icon--${tone}`]" :class="['gender-icon', `gender-icon--${tone}`]"
+78 -3
View File
@@ -18,6 +18,9 @@
</template> </template>
</t-input> </t-input>
</t-form-item> </t-form-item>
<t-form-item v-if="isGlobalManager" label="门店" required-mark arrow @click="openStorePicker">
<div class="picker-text" v-text="selectedStoreName" />
</t-form-item>
<div v-if="scannerVisible && scannerTarget === 'code'" class="code-scanner-wrapper"> <div v-if="scannerVisible && scannerTarget === 'code'" class="code-scanner-wrapper">
<code-scanner <code-scanner
visible visible
@@ -153,31 +156,46 @@
@change="handleIntroducerChange" @change="handleIntroducerChange"
/> />
</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 { AttachmentAPI } from "timi-web"; import { AttachmentAPI, useUserStore } from "timi-web";
import { Toast } from "tdesign-mobile-vue"; import { Toast } from "tdesign-mobile-vue";
import CustomerAPI from "@/api/CustomerAPI"; import CustomerAPI from "@/api/CustomerAPI";
import BodyAreaSelector from "@/components/BodyAreaSelector.vue"; import BodyAreaSelector from "@/components/BodyAreaSelector.vue";
import CodeScanner from "@/components/CodeScanner.vue"; import CodeScanner from "@/components/CodeScanner.vue";
import CustomerSelector from "@/components/CustomerSelector.vue"; import CustomerSelector from "@/components/CustomerSelector.vue";
import PhotoField from "@/components/PhotoField.vue"; import PhotoField from "@/components/PhotoField.vue";
import { useCurrentStoreStore } from "@/store/currentStoreStore";
import { useSettingStore } from "@/store/settingStore"; import { useSettingStore } from "@/store/settingStore";
import type { BodyAreaValue, Customer } from "@/types/Customer"; import type { BodyAreaValue, Customer } from "@/types/Customer";
import { createBodyAreaValue, normalizeBodyAreaValue } from "@/utils/customer"; import { createBodyAreaValue, normalizeBodyAreaValue } from "@/utils/customer";
import { permissionCodes } from "@/utils/permission"; import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
import { compressUploadImage } from "@/utils/imageCompression"; import { compressUploadImage } from "@/utils/imageCompression";
type PickerValue = string | number;
defineOptions({ defineOptions({
name: "CustomerEdit" name: "CustomerEdit"
}); });
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const userStore = useUserStore();
const settingStore = useSettingStore(); const settingStore = useSettingStore();
const currentStoreStore = useCurrentStoreStore();
const isCreateMode = computed(() => route.name === "CustomerCreate"); const isCreateMode = computed(() => route.name === "CustomerCreate");
const savePermissionCode = computed(() => isCreateMode.value ? permissionCodes.customer.create : permissionCodes.customer.update); const savePermissionCode = computed(() => isCreateMode.value ? permissionCodes.customer.create : permissionCodes.customer.update);
const isGlobalManager = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
let latestDecodedText = ""; let latestDecodedText = "";
@@ -268,9 +286,60 @@ function openPhotoPicker(): void {
photoInputRef.value?.click(); photoInputRef.value?.click();
} }
const canSubmit = computed(() => formData.name?.trim()); const canSubmit = computed(() => formData.name?.trim() && formData.storeId);
// ---------- 门店选择 ----------
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 === formData.storeId);
return store?.name || store?.code || "请选择门店";
});
function openStorePicker(): void {
if (!currentStoreStore.storeList.length) {
Toast.warning("暂无可选门店");
return;
}
storePickerDraft.value = [formData.storeId || ""];
storePickerVisible.value = true;
}
function confirmStorePicker(): void {
const storeId = String(storePickerDraft.value[0] || "");
if (!storeId) {
return;
}
formData.storeId = storeId;
currentStoreStore.selectStore(storeId);
storePickerVisible.value = false;
}
async function prepareStoreScope(): Promise<void> {
if (isGlobalManager.value) {
await currentStoreStore.loadStoreList();
const queryStoreId = String(route.query.storeId || "");
formData.storeId = currentStoreStore.storeList.some(item => item.id === queryStoreId)
? queryStoreId
: currentStoreStore.currentStoreId;
return;
}
const store = await currentStoreStore.loadLoginStore();
formData.storeId = store?.id || "";
}
async function loadDetail() { async function loadDetail() {
await prepareStoreScope();
const id = String(route.params.id || ""); const id = String(route.params.id || "");
if (!id) { if (!id) {
return; return;
@@ -278,6 +347,7 @@ async function loadDetail() {
try { try {
const customer = await CustomerAPI.detail(id); const customer = await CustomerAPI.detail(id);
Object.assign(formData, customer); Object.assign(formData, customer);
formData.storeId = customer.storeId || formData.storeId;
selectedAreas.value = normalizeBodyAreaValue(customer.conditioningPart); selectedAreas.value = normalizeBodyAreaValue(customer.conditioningPart);
introducerCustomerId.value = customer.introducerCustomer?.id || ""; introducerCustomerId.value = customer.introducerCustomer?.id || "";
} catch (err) { } catch (err) {
@@ -334,6 +404,11 @@ async function handlePhotoSelected(event: Event): Promise<void> {
const isSubmitting = ref(false); const isSubmitting = ref(false);
async function doSubmit(): Promise<void> { async function doSubmit(): Promise<void> {
if (!formData.storeId) {
Toast.warning("请选择门店");
return;
}
isSubmitting.value = true; isSubmitting.value = true;
try { try {
formData.conditioningPart = selectedAreas.value; formData.conditioningPart = selectedAreas.value;
+64 -7
View File
@@ -1,6 +1,14 @@
<template> <template>
<div v-can="permissionCodes.customer.read" class="customer-list"> <div v-can="permissionCodes.customer.read" class="customer-list">
<div class="search-bar"> <div class="filter-bar">
<t-dropdown-menu v-if="isGlobalManager">
<t-dropdown-item
v-model="selectedStoreId"
:label="storeFilterLabel"
:options="storeOptions"
@change="selectStoreFilter"
/>
</t-dropdown-menu>
<t-input <t-input
v-model.trim="keyword" v-model.trim="keyword"
class="text-input" class="text-input"
@@ -70,7 +78,7 @@
v-can="permissionCodes.customer.create" v-can="permissionCodes.customer.create"
theme="primary" theme="primary"
class="create" class="create"
@click="router.push('/management/customers/create')" @click="toCreate"
> >
<template #icon> <template #icon>
<t-icon name="add" /> <t-icon name="add" />
@@ -83,20 +91,47 @@
<script setup lang="ts"> <script setup lang="ts">
import { Toast } from "tdesign-mobile-vue"; import { Toast } from "tdesign-mobile-vue";
import { type Page, type PageResult, Time, Toolkit } from "timi-web"; import { type DropdownValue } from "tdesign-mobile-vue";
import { type Page, type PageResult, Time, Toolkit, useUserStore } from "timi-web";
import CustomerAPI from "@/api/CustomerAPI"; import CustomerAPI from "@/api/CustomerAPI";
import GenderIcon from "@/components/GenderIcon.vue"; import GenderIcon from "@/components/GenderIcon.vue";
import { useCurrentStoreStore } from "@/store/currentStoreStore";
import type { Customer } from "@/types/Customer"; import type { Customer } from "@/types/Customer";
import { getCustomerPhotoURL } from "@/utils/customer"; import { getCustomerPhotoURL } from "@/utils/customer";
import { permissionCodes } from "@/utils/permission"; import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
defineOptions({ defineOptions({
name: "CustomerList" name: "CustomerList"
}); });
const router = useRouter(); const router = useRouter();
const userStore = useUserStore();
const currentStoreStore = useCurrentStoreStore();
const isRefreshing = ref(false); const isRefreshing = ref(false);
const isExporting = ref(false); const isExporting = ref(false);
const isGlobalManager = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
// ---------- 门店筛选 ----------
const selectedStoreId = ref(currentStoreStore.currentStoreId);
const storeOptions = computed(() => 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 || "选择门店");
async function selectStoreFilter(value: DropdownValue): Promise<void> {
if (Array.isArray(value)) {
return;
}
const storeId = String(value);
selectedStoreId.value = storeId;
currentStoreStore.selectStore(storeId);
await refresh();
}
// ---------- 分页 ---------- // ---------- 分页 ----------
@@ -140,7 +175,26 @@ async function refresh(): Promise<void> {
} }
} }
onMounted(fetch); async function loadData(): Promise<void> {
if (isGlobalManager.value) {
await currentStoreStore.loadStoreList();
selectedStoreId.value = currentStoreStore.currentStoreId;
} else {
await currentStoreStore.loadLoginStore();
}
await fetch();
}
onMounted(loadData);
// ---------- 导航 ----------
async function toCreate(): Promise<void> {
await router.push({
path: "/management/customers/create",
query: isGlobalManager.value && selectedStoreId.value ? { storeId: selectedStoreId.value } : undefined
});
}
// ---------- 导出 ---------- // ---------- 导出 ----------
@@ -163,13 +217,16 @@ async function doExport(): Promise<void> {
.customer-list { .customer-list {
padding: calc(var(--app-nav-height)) 0 5.5rem 0; padding: calc(var(--app-nav-height)) 0 5.5rem 0;
.search-bar { .filter-bar {
top: var(--app-nav-height); top: var(--app-nav-height);
left: 0; left: 0;
right: 0; right: 0;
z-index: 10; z-index: 10;
gap: .5rem;
display: flex;
padding: .75rem 1rem; padding: .75rem 1rem;
position: sticky; position: sticky;
flex-direction: column;
background: #F5F5F8; background: #F5F5F8;
box-shadow: var(--td-shadow-1); box-shadow: var(--td-shadow-1);
@@ -234,7 +291,7 @@ async function doExport(): Promise<void> {
:global(html.theme-dark) { :global(html.theme-dark) {
.customer-list { .customer-list {
.search-bar { .filter-bar {
background: #10161f; background: #10161f;
.text-input { .text-input {
+82 -2
View File
@@ -22,6 +22,9 @@
borderless borderless
/> />
</t-form-item> </t-form-item>
<t-form-item v-if="isGlobalManager" label="门店" :arrow="isCreateMode" @click="openStorePicker">
<div class="picker-text" v-text="selectedStoreName" />
</t-form-item>
<t-form-item label="说明"> <t-form-item label="说明">
<t-textarea <t-textarea
class="description" class="description"
@@ -140,6 +143,15 @@
@confirm="confirmLimitPicker" @confirm="confirmLimitPicker"
/> />
</t-popup> </t-popup>
<t-popup v-model="storePickerVisible" placement="bottom">
<t-picker
v-model="storePickerDraft"
:columns="storePickerColumns"
title="选择门店"
@cancel="storePickerVisible = false"
@confirm="confirmStorePicker"
/>
</t-popup>
<t-popup v-model="statusPickerVisible" placement="bottom"> <t-popup v-model="statusPickerVisible" placement="bottom">
<t-picker <t-picker
v-model="statusPickerDraft" v-model="statusPickerDraft"
@@ -183,11 +195,14 @@
import { Time } from "timi-web"; import { Time } from "timi-web";
import { DialogPlugin, Toast } from "tdesign-mobile-vue"; import { DialogPlugin, Toast } from "tdesign-mobile-vue";
import type { ColorObject } from "tdesign-mobile-vue"; import type { ColorObject } from "tdesign-mobile-vue";
import type { Role } from "timi-web"; import { useUserStore, type Role } 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 { EventItem, EventStatus, EventLimitType } from "@/types/EventItem"; import type { EventItem, EventStatus, EventLimitType } from "@/types/EventItem";
import { permissionCodes } from "@/utils/permission"; import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
type PickerValue = string | number;
type LimitOption = { type LimitOption = {
label: string; label: string;
@@ -200,9 +215,12 @@ defineOptions({
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const userStore = useUserStore();
const currentStoreStore = useCurrentStoreStore();
const routeId = computed(() => String(route.params.id || "")); const routeId = computed(() => String(route.params.id || ""));
const isCreateMode = computed(() => !routeId.value); const isCreateMode = computed(() => !routeId.value);
const isGlobalManager = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
const savePermissionCode = computed(() => isCreateMode.value ? permissionCodes.event.create : permissionCodes.event.update); const savePermissionCode = computed(() => isCreateMode.value ? permissionCodes.event.create : permissionCodes.event.update);
const deletePermissionCode = permissionCodes.event.delete; const deletePermissionCode = permissionCodes.event.delete;
const pageAccess = computed(() => ({ const pageAccess = computed(() => ({
@@ -217,6 +235,7 @@ const isSaving = ref(false);
const isRemoving = ref(false); const isRemoving = ref(false);
const formModel = reactive({ const formModel = reactive({
id: "", id: "",
storeId: "",
name: "", name: "",
description: "", description: "",
color: "#0f766e", color: "#0f766e",
@@ -229,6 +248,46 @@ const formModel = reactive({
roleCodeList: [] as string[] roleCodeList: [] as string[]
}); });
// ---------- 门店选择 ----------
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 (!isCreateMode.value) {
return;
}
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;
}
// ---------- 颜色 ---------- // ---------- 颜色 ----------
const colorPickerVisible = ref(false); const colorPickerVisible = ref(false);
@@ -388,6 +447,10 @@ function resolveTimestamp(valueText: string): number | undefined {
} }
function validateForm(): boolean { function validateForm(): boolean {
if (!formModel.storeId) {
Toast.warning("请选择门店");
return false;
}
if (!formModel.name.trim()) { if (!formModel.name.trim()) {
Toast.warning("请输入登记事件名称"); Toast.warning("请输入登记事件名称");
return false; return false;
@@ -453,6 +516,7 @@ async function saveEventItem(): Promise<void> {
try { try {
const payload: EventItem = { const payload: EventItem = {
id: formModel.id || undefined, id: formModel.id || undefined,
storeId: formModel.storeId,
name: formModel.name.trim(), name: formModel.name.trim(),
description: formModel.description, description: formModel.description,
color: formModel.color, color: formModel.color,
@@ -519,7 +583,22 @@ async function removeEventItem(id: string): Promise<void> {
// ---------- 页面数据 ---------- // ---------- 页面数据 ----------
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();
roleOptions.value = await ManagedUserAPI.roleList(); roleOptions.value = await ManagedUserAPI.roleList();
if (isCreateMode.value) { if (isCreateMode.value) {
return; return;
@@ -527,6 +606,7 @@ async function fillForm(): Promise<void> {
const current = await EventAPI.detail(routeId.value); const current = await EventAPI.detail(routeId.value);
Object.assign(formModel, { Object.assign(formModel, {
id: current.id || "", id: current.id || "",
storeId: current.storeId || formModel.storeId,
name: current.name || "", name: current.name || "",
description: current.description || "", description: current.description || "",
color: current.color || "#2f88ff", color: current.color || "#2f88ff",
+102 -9
View File
@@ -1,5 +1,20 @@
<template> <template>
<div v-can="{ value: [permissionCodes.event.read, permissionCodes.user.read], mode: 'and' }" class="registration-event-list"> <div
v-can="{ value: [permissionCodes.event.read, permissionCodes.user.read], mode: 'and' }"
:class="{ 'has-filter': hasFilter }"
class="registration-event-list"
>
<div v-if="hasFilter" class="filter-menu">
<t-dropdown-menu>
<t-dropdown-item
v-if="isGlobalManager"
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"
@@ -9,7 +24,7 @@
<t-cell-group theme="card"> <t-cell-group theme="card">
<t-cell <t-cell
class="item" class="item"
v-for="item in pageResult.list" v-for="item in visibleEventList"
:key="item.id" :key="item.id"
:arrow="canUpdateEventItem" :arrow="canUpdateEventItem"
:hover="canUpdateEventItem" :hover="canUpdateEventItem"
@@ -46,10 +61,10 @@
</template> </template>
</t-cell> </t-cell>
</t-cell-group> </t-cell-group>
<div v-if="!pageResult.list.length" class="empty-block" v-text="isLoading ? '正在加载登记事件' : '暂无登记事件'" /> <div v-if="!visibleEventList.length" class="empty-block" v-text="emptyText" />
<div v-if="pageResult.list.length" class="load-state" v-text="isNoMore ? '已经到底了' : '上拉加载更多'" /> <div v-if="visibleEventList.length" class="load-state" v-text="isNoMore ? '已经到底了' : '上拉加载更多'" />
</t-pull-down-refresh> </t-pull-down-refresh>
<t-button v-can="permissionCodes.event.create" class="create" theme="primary" @click="router.push('/management/registration-events/create')"> <t-button v-can="permissionCodes.event.create" class="create" theme="primary" @click="toCreate">
<template #icon> <template #icon>
<t-icon name="add" /> <t-icon name="add" />
</template> </template>
@@ -59,13 +74,14 @@
</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, type Role, useUserStore } from "timi-web"; import { type Page, type PageResult, type Role, 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 { useNavBarStore } from "@/store/navBarStore"; import { useNavBarStore } from "@/store/navBarStore";
import type { EventItem, EventLimitType, EventStatus } from "@/types/EventItem"; import type { EventItem, EventLimitType, EventStatus } from "@/types/EventItem";
import { permissionCodes } from "@/utils/permission"; import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
defineOptions({ defineOptions({
name: "eventList" name: "eventList"
@@ -74,6 +90,34 @@ defineOptions({
const router = useRouter(); const router = useRouter();
const navBarStore = useNavBarStore(); const navBarStore = useNavBarStore();
const userStore = useUserStore(); const userStore = useUserStore();
const currentStoreStore = useCurrentStoreStore();
const isGlobalManager = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
const hasFilter = computed(() => isGlobalManager.value);
// ---------- 门店筛选 ----------
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 eventListOptions = computed(() => {
if (isGlobalManager.value) {
return selectedStoreId.value === allStoreFilterValue
? { allStores: true }
: { storeId: selectedStoreId.value };
}
return { storeId: loginStoreId.value };
});
// ---------- 列表数据 ---------- // ---------- 列表数据 ----------
@@ -91,6 +135,13 @@ const pageResult = reactive<PageResult<EventItem>>({
list: [] list: []
}); });
const canUpdateEventItem = computed(() => userStore.hasPermissions(permissionCodes.event.update)); const canUpdateEventItem = computed(() => userStore.hasPermissions(permissionCodes.event.update));
const visibleEventList = computed(() => pageResult.list);
const emptyText = computed(() => {
if (isLoading.value) {
return "正在加载登记事件";
}
return "暂无登记事件";
});
type StatusTag = { type StatusTag = {
text: string; text: string;
@@ -154,13 +205,23 @@ function getRoleName(code: string): string {
// ---------- 页面数据 ---------- // ---------- 页面数据 ----------
async function loadStoreScope(): Promise<void> {
if (isGlobalManager.value) {
await currentStoreStore.loadStoreList();
return;
}
const store = await currentStoreStore.loadLoginStore();
loginStoreId.value = store?.id || "";
}
async function fetch(): Promise<void> { async function fetch(): Promise<void> {
if (isLoading.value || isNoMore.value) { if (isLoading.value || isNoMore.value) {
return; return;
} }
isLoading.value = true; isLoading.value = true;
try { try {
const result = await EventAPI.list(page); const result = await EventAPI.list(page, eventListOptions.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;
@@ -184,8 +245,9 @@ async function refresh(): Promise<void> {
} }
async function loadData(): Promise<void> { async function loadData(): Promise<void> {
await loadStoreScope();
const [eventResult, roles] = await Promise.all([ const [eventResult, roles] = await Promise.all([
EventAPI.list(page), EventAPI.list(page, eventListOptions.value),
ManagedUserAPI.roleList() ManagedUserAPI.roleList()
]); ]);
roleOptions.value = roles || []; roleOptions.value = roles || [];
@@ -195,6 +257,24 @@ async function loadData(): Promise<void> {
isNoMore.value = pageResult.list.length >= pageResult.total || pageResult.list.length < page.size; isNoMore.value = pageResult.list.length >= pageResult.total || pageResult.list.length < page.size;
} }
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/registration-events/create",
query: isGlobalManager.value && selectedStoreId.value !== allStoreFilterValue
? { storeId: selectedStoreId.value }
: undefined
});
}
onMounted(async () => { onMounted(async () => {
navBarStore.clearRight(); navBarStore.clearRight();
if (canUpdateEventItem.value) { if (canUpdateEventItem.value) {
@@ -218,6 +298,19 @@ onUnmounted(() => {
.registration-event-list { .registration-event-list {
padding: calc(var(--app-nav-height) + 1rem) 0 6rem 0; padding: calc(var(--app-nav-height) + 1rem) 0 6rem 0;
&.has-filter {
padding-top: calc(var(--app-nav-height) + 4rem);
}
.filter-menu {
top: var(--app-nav-height);
left: 0;
right: 0;
z-index: 20;
position: fixed;
background: var(--td-bg-color-container);
}
.item { .item {
.header { .header {
+9 -29
View File
@@ -21,14 +21,14 @@
</t-form-item> </t-form-item>
<t-form-item label="性别"> <t-form-item label="性别">
<t-radio-group class="sex" v-model="formModel.gender" variant="default-filled"> <t-radio-group class="sex" v-model="formModel.gender" variant="default-filled">
<t-radio :block="false" borderless value="男">男</t-radio> <t-radio :block="false" borderless value="MALE">男</t-radio>
<t-radio :block="false" borderless value="女">女</t-radio> <t-radio :block="false" borderless value="FEMALE">女</t-radio>
<t-radio :block="false" borderless value="未知">未知</t-radio> <t-radio :block="false" borderless value="">未知</t-radio>
</t-radio-group> </t-radio-group>
</t-form-item> </t-form-item>
<t-form-item label="手机号"> <t-form-item label="手机号">
<div class="mobile-value"> <div class="mobile-value">
<telephone-link v-if="formModel.mobile" :value="formModel.mobile" /> <div v-if="formModel.mobile" v-text="formModel.mobile" />
<span v-else v-text="'未填写'" /> <span v-else v-text="'未填写'" />
</div> </div>
</t-form-item> </t-form-item>
@@ -50,7 +50,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { DialogPlugin, Toast } from "tdesign-mobile-vue"; import { DialogPlugin, Toast } from "tdesign-mobile-vue";
import { AttachmentAPI, UserAPI, useUserStore } from "timi-web"; import { AttachmentAPI, type Gender, UserAPI, useUserStore } from "timi-web";
import PhotoField from "@/components/PhotoField.vue"; import PhotoField from "@/components/PhotoField.vue";
import { buildFallbackAvatar } from "@/utils/avatar"; import { buildFallbackAvatar } from "@/utils/avatar";
import { compressUploadImage } from "@/utils/imageCompression"; import { compressUploadImage } from "@/utils/imageCompression";
@@ -59,7 +59,7 @@ type UserAvatarSource = NonNullable<Parameters<typeof UserAPI.getAvatarURL>[0]>
name?: string; name?: string;
nick?: string; nick?: string;
}; };
type ProfileGender = Gender | "";
defineOptions({ defineOptions({
name: "ManagementProfile" name: "ManagementProfile"
}); });
@@ -83,7 +83,7 @@ const formModel = reactive({
avatar: "", avatar: "",
nickname: "", nickname: "",
role: "", role: "",
gender: "未知", gender: "" as ProfileGender,
mobile: "", mobile: "",
password: "", password: "",
office: "", office: "",
@@ -102,7 +102,7 @@ function fillForm(): void {
avatar: buildUserAvatar(user), avatar: buildUserAvatar(user),
nickname: user?.nick || "", nickname: user?.nick || "",
role: formatRoleCode(userStore.getRoleList()[0]), role: formatRoleCode(userStore.getRoleList()[0]),
gender: sexToText(user?.sex), gender: user?.gender || "",
mobile: user?.phoneNo || "", mobile: user?.phoneNo || "",
password: "", password: "",
office: "", office: "",
@@ -159,7 +159,7 @@ async function saveProfile(): Promise<void> {
id: formModel.id, id: formModel.id,
name: formModel.name, name: formModel.name,
nick: formModel.nickname, nick: formModel.nickname,
sex: textToSex(formModel.gender), gender: formModel.gender,
tempFileIdList: avatarTempFileId.value ? [avatarTempFileId.value] : undefined tempFileIdList: avatarTempFileId.value ? [avatarTempFileId.value] : undefined
}); });
await userStore.updateLoginUser(result); await userStore.updateLoginUser(result);
@@ -195,16 +195,6 @@ async function logout(): Promise<void> {
// ---------- 格式化 ---------- // ---------- 格式化 ----------
function sexToText(sex?: number): "男" | "女" | "未知" {
if (sex === 1) {
return "男";
}
if (sex === 2) {
return "女";
}
return "未知";
}
function formatRoleCode(roleCode?: string): string { function formatRoleCode(roleCode?: string): string {
if (!roleCode) { if (!roleCode) {
return ""; return "";
@@ -214,16 +204,6 @@ function formatRoleCode(roleCode?: string): string {
return codePartList[codePartList.length - 1] || roleCode; return codePartList[codePartList.length - 1] || roleCode;
} }
function textToSex(text: string): number {
if (text === "男") {
return 1;
}
if (text === "女") {
return 2;
}
return 0;
}
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, "#0f766e"); return UserAPI.getAvatarURL(user) || buildFallbackAvatar(name, "#0f766e");
+24 -18
View File
@@ -2,32 +2,37 @@
<div class="management-tab"> <div class="management-tab">
<t-cell-group class="user-card" theme="card"> <t-cell-group class="user-card" theme="card">
<t-cell <t-cell
class="cell" v-if="currentUser"
:title="currentUserName" class="info"
arrow arrow
hover hover
@click="go('/management/profile')" @click="go('/management/profile')"
> >
<template #description> <template #description>
<telephone-link v-if="currentUser?.phoneNo" :value="currentUser.phoneNo" /> <div v-if="currentUser?.phoneNo" v-text="currentUser.phoneNo" />
<span v-else v-text="currentUserDesc" /> </template>
<template #title>
<div class="header">
<div class="name" v-text="currentUser.name" />
<gender-icon :value="currentUser.gender" />
</div>
</template> </template>
<template #leftIcon> <template #leftIcon>
<t-image <t-image
class="avatar" class="avatar"
:src="currentUserAvatar" :src="currentUserAvatar"
:alt="currentUserName" :alt="currentUser.name"
fit="cover" fit="cover"
/> />
</template> </template>
</t-cell> </t-cell>
<t-cell <t-cell
v-if="canSelectStore" v-if="canViewStore"
class="cell" class="cell"
title="当前门店" title="当前门店"
:note="currentStoreStore.currentStoreName" :note="currentStoreStore.currentStoreName"
arrow :arrow="canSelectStore"
hover :hover="canSelectStore"
@click="openStorePicker" @click="openStorePicker"
> >
<template #leftIcon> <template #leftIcon>
@@ -99,13 +104,10 @@ const currentStoreStore = useCurrentStoreStore();
const currentUser = computed(() => userStore.loginUser.user); const currentUser = computed(() => userStore.loginUser.user);
const currentUserName = computed(() => currentUser.value?.nick || currentUser.value?.name || "未登录用户");
const currentUserDesc = computed(() => currentUser.value?.phoneNo || currentUser.value?.email || "暂无联系方式");
const currentUserAvatar = computed(() => buildUserAvatar(currentUser.value)); const currentUserAvatar = computed(() => buildUserAvatar(currentUser.value));
const canSelectStore = computed(() => userStore.hasPermissions(permissionCodes.store.read)); const canSelectStore = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
const canLoadLoginStore = computed(() => userStore.hasRoles([gaoRoleCodes.storeManager, gaoRoleCodes.employee], "or")); const canLoadLoginStore = computed(() => userStore.hasRoles([gaoRoleCodes.storeManager, gaoRoleCodes.employee], "or"));
const canViewStore = computed(() => canSelectStore.value || canLoadLoginStore.value);
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([
@@ -132,6 +134,10 @@ const storePickerColumns = computed(() => [
]); ]);
function openStorePicker(): void { function openStorePicker(): void {
if (!canSelectStore.value) {
return;
}
if (!currentStoreStore.storeList.length) { if (!currentStoreStore.storeList.length) {
Toast.warning("暂无可选门店"); Toast.warning("暂无可选门店");
return; return;
@@ -160,7 +166,7 @@ const menuGroupList = computed<MenuGroup[]>(() => [
icon: "shop", icon: "shop",
path: "/management/stores", path: "/management/stores",
title: "门店管理", title: "门店管理",
visible: userStore.hasPermissions(permissionCodes.store.read) visible: canSelectStore.value && userStore.hasPermissions(permissionCodes.store.read)
}, },
{ {
icon: "usergroup", icon: "usergroup",
@@ -226,7 +232,7 @@ const menuGroupList = computed<MenuGroup[]>(() => [
]); ]);
function buildUserAvatar(user?: UserAvatarSource): string { function buildUserAvatar(user?: UserAvatarSource): string {
return UserAPI.getAvatarURL(user) || buildFallbackAvatar(currentUserName.value, "#0f766e"); return UserAPI.getAvatarURL(user) || buildFallbackAvatar(currentUser.value?.name, "#0f766e");
} }
async function go(path: string): Promise<void> { async function go(path: string): Promise<void> {
@@ -251,7 +257,7 @@ onMounted(async () => {
.user-card { .user-card {
.cell { .info {
padding: 10px; padding: 10px;
.avatar { .avatar {
@@ -260,9 +266,10 @@ onMounted(async () => {
border-radius: .875rem; border-radius: .875rem;
} }
.title { .header {
gap: .5rem; gap: .5rem;
display: flex; display: flex;
align-items: center;
} }
} }
} }
@@ -299,4 +306,3 @@ onMounted(async () => {
} }
} }
</style> </style>
+4 -1
View File
@@ -15,7 +15,10 @@ export const useCurrentStoreStore = defineStore("currentStore", () => {
let loginStoreRequest: Promise<Store | undefined> | undefined; let loginStoreRequest: Promise<Store | undefined> | undefined;
let loginStoreRequestUserId = ""; let loginStoreRequestUserId = "";
const currentStore = computed(() => storeList.value.find(item => item.id === currentStoreId.value)); const currentStore = computed(() => (
storeList.value.find(item => item.id === currentStoreId.value)
|| (loginStore.value?.id === currentStoreId.value ? loginStore.value : undefined)
));
const currentStoreName = computed(() => currentStore.value?.name || "默认门店"); const currentStoreName = computed(() => currentStore.value?.name || "默认门店");
async function loadStoreList(): Promise<void> { async function loadStoreList(): Promise<void> {
+2 -2
View File
@@ -4,8 +4,8 @@
* @param name 头像中显示的用户名 * @param name 头像中显示的用户名
* @param backgroundColor 头像背景色 * @param backgroundColor 头像背景色
*/ */
export function buildFallbackAvatar(name: string, backgroundColor = "#2563eb"): string { export function buildFallbackAvatar(name?: string, backgroundColor = "#2563eb"): string {
const text = encodeURIComponent((name.trim() || "用户").slice(0, 2)); const text = encodeURIComponent((name?.trim() || "用户").slice(0, 2));
const fill = encodeURIComponent(backgroundColor); const fill = encodeURIComponent(backgroundColor);
return [ return [
"data:image/svg+xml;charset=UTF-8,", "data:image/svg+xml;charset=UTF-8,",