remove in store list

This commit is contained in:
Timi
2026-08-29 00:49:22 +08:00
parent 147808adda
commit 9ec4e1fe35
3 changed files with 281 additions and 123 deletions
+195
View File
@@ -0,0 +1,195 @@
<template>
<t-dialog
class="remove-dialog"
v-model:visible="visible"
title="删除门店"
>
<div class="content">
<div class="warning">删除后将同步处理以下数据:</div>
<ul class="items">
<li>删除该门店下的所有客户</li>
<li>删除客户的登记记录及相关附件</li>
<li>删除积分规则和积分账户</li>
<li>删除登记事件及事件角色关联</li>
<li>删除门店营业日数据</li>
<li>删除门店用户关系</li>
<li>删除门店自有标签</li>
</ul>
<div class="warning">特别说明:</div>
<ul class="items">
<li>已产生的积分流水不会删除,将保留积分撤销记录</li>
<li>关联的系统用户账号不会删除</li>
<li>删除后无法恢复,请谨慎操作</li>
</ul>
<div class="hint">
请输入门店名称
<span class="name red" v-text="storeName" />
以确认删除
</div>
<t-input
class="confirm-input"
v-model.trim="confirmName"
size="small"
borderless
clearable
placeholder="请输入门店名称"
/>
</div>
<template #cancelBtn>
<t-button
theme="default"
content="取消"
block
@click="close"
/>
</template>
<template #confirmBtn>
<t-button
theme="danger"
:content="confirmButtonText"
block
:loading="loading"
:disabled="!canConfirm || loading || 0 < countdown"
@click="confirm"
/>
</template>
</t-dialog>
</template>
<script setup lang="ts">
import type { Store } from "@/types/Store";
defineOptions({
name: "StoreRemoveDialog"
});
interface Props {
visible?: boolean;
store?: Store;
loading?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
loading: false
});
const emit = defineEmits<{
"update:visible": [visible: boolean];
confirm: [store: Store];
}>();
const COUNTDOWN_SECONDS = 30;
const confirmName = ref("");
const countdown = ref(COUNTDOWN_SECONDS);
let countdownTimer: number | undefined;
const visible = computed({
get: () => props.visible,
set: (value: boolean) => emit("update:visible", value)
});
const storeName = computed(() => props.store?.name || "");
const canConfirm = computed(() => !!storeName.value && confirmName.value === storeName.value);
const confirmButtonText = computed(() => countdown.value ? `删除(${countdown.value} 秒)` : "删除");
watch(() => props.visible, () => {
confirmName.value = "";
resetCountdown();
}, { immediate: true });
onUnmounted(clearCountdown);
function resetCountdown(): void {
clearCountdown();
countdown.value = COUNTDOWN_SECONDS;
if (!props.visible) {
return;
}
countdownTimer = window.setInterval(() => {
if (countdown.value < 2) {
countdown.value = 0;
clearCountdown();
return;
}
countdown.value -= 1;
}, 1000);
}
function clearCountdown(): void {
if (countdownTimer === undefined) {
return;
}
window.clearInterval(countdownTimer);
countdownTimer = undefined;
}
function close(): void {
visible.value = false;
}
function confirm(): void {
if (!props.store?.id || !canConfirm.value || props.loading || 0 < countdown.value) {
return;
}
emit("confirm", props.store);
}
</script>
<style scoped lang="less">
.remove-dialog {
:deep(.t-dialog__footer) {
gap: 1rem;
}
.content {
text-align: left;
.warning {
color: #b42318;
font-weight: 700;
}
.items {
margin: .25rem 0 1rem;
max-height: 5rem;
display: flex;
overflow-y: auto;
line-height: 1.2;
padding-left: 1.25rem;
flex-direction: column;
}
.name {
font-weight: 700;
}
.countdown-hint {
color: var(--td-text-color-secondary);
margin-top: .5rem;
font-size: .875rem;
}
.confirm-input {
--td-input-vertical-padding: 10px;
border: 1px solid var(--td-component-border);
margin-top: .5rem;
border-radius: .75rem;
}
}
}
:global(html.theme-dark) {
.remove-dialog {
.warning {
color: #fda29b;
}
.confirm-input {
background: rgba(15, 23, 42, .72);
border-color: rgba(148, 163, 184, .16);
}
}
}
</style>
+12 -118
View File
@@ -45,69 +45,22 @@
@click="confirmRemoveStore" @click="confirmRemoveStore"
>删除</t-button> >删除</t-button>
</div> </div>
<t-dialog <store-remove-dialog
class="remove-dialog"
v-model:visible="removeDialogVisible" v-model:visible="removeDialogVisible"
title="删除门店" :store="formData"
> :loading="isRemoving"
<div class="content"> @confirm="removeConfirmedStore"
<div class="warning">删除后将同步处理以下数据:</div> />
<ul class="items">
<li>删除该门店下的所有客户</li>
<li>删除客户的登记记录及相关附件</li>
<li>删除积分规则和积分账户</li>
<li>删除登记事件及事件角色关联</li>
<li>删除门店营业日数据</li>
<li>删除门店用户关系</li>
<li>删除门店自有标签</li>
</ul>
<div class="warning">特别说明:</div>
<ul class="items">
<li>已产生的积分流水不会删除,将保留积分撤销记录</li>
<li>关联的系统用户账号不会删除</li>
<li>删除后无法恢复,请谨慎操作</li>
</ul>
<div class="hint">
请输入门店名称
<span class="name red" v-text="removeStoreName" />
以确认删除
</div>
<t-input
class="confirm-input"
v-model.trim="removeConfirmName"
size="small"
borderless
clearable
placeholder="请输入门店名称"
/>
</div>
<template #cancelBtn>
<t-button
theme="default"
content="取消"
block
@click="closeRemoveDialog"
/>
</template>
<template #confirmBtn>
<t-button
theme="danger"
content="删除"
block
:disabled="!canConfirmRemove"
@click="removeConfirmedStore"
/>
</template>
</t-dialog>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useUserStore } from "timi-web-api"; import { useUserStore } from "timi-web-api";
import StoreAPI from "@/api/StoreAPI"; import StoreAPI from "@/api/StoreAPI";
import StoreRemoveDialog from "@/components/StoreRemoveDialog.vue";
import { useCurrentStoreStore } from "@/store/currentStoreStore"; import { useCurrentStoreStore } from "@/store/currentStoreStore";
import { useNavBarStore } from "@/store/navBarStore"; import { useNavBarStore } from "@/store/navBarStore";
import type { StoreStatus } from "@/types/Store"; import type { Store, StoreStatus } from "@/types/Store";
import { gaoRoleCodes, permissionCodes } from "@/utils/permission"; import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
import { Message } from "@/utils/message.ts"; import { Message } from "@/utils/message.ts";
@@ -133,7 +86,6 @@ const savePermissionCode = computed(() => isCreateMode.value ? permissionCodes.s
const isSaving = ref(false); const isSaving = ref(false);
const isRemoving = ref(false); const isRemoving = ref(false);
const removeDialogVisible = ref(false); const removeDialogVisible = ref(false);
const removeConfirmName = ref("");
const formData = reactive({ const formData = reactive({
id: "", id: "",
@@ -146,9 +98,6 @@ const formData = reactive({
autoClosedDay: true autoClosedDay: true
}); });
const removeStoreName = computed(() => formData.name);
const canConfirmRemove = computed(() => !!removeStoreName.value && removeConfirmName.value === removeStoreName.value);
async function doSave(): Promise<void> { async function doSave(): Promise<void> {
if (!validateForm()) { if (!validateForm()) {
return; return;
@@ -215,31 +164,29 @@ async function confirmRemoveStore(): Promise<void> {
if (!formData.id || isRemoving.value) { if (!formData.id || isRemoving.value) {
return; return;
} }
removeConfirmName.value = "";
removeDialogVisible.value = true; removeDialogVisible.value = true;
} }
function closeRemoveDialog(): void { function closeRemoveDialog(): void {
removeDialogVisible.value = false; removeDialogVisible.value = false;
removeConfirmName.value = "";
} }
async function removeConfirmedStore(): Promise<void> { async function removeConfirmedStore(store: Store): Promise<void> {
if (!formData.id || !canConfirmRemove.value || isRemoving.value) { if (!store.id || isRemoving.value) {
return; return;
} }
closeRemoveDialog(); closeRemoveDialog();
await removeStore(); await removeStore(store.id);
} }
async function removeStore(): Promise<void> { async function removeStore(id: string): Promise<void> {
if (isRemoving.value) { if (isRemoving.value) {
return; return;
} }
isRemoving.value = true; isRemoving.value = true;
try { try {
await StoreAPI.remove(formData.id); await StoreAPI.remove(id);
Message.success("门店已删除"); Message.success("门店已删除");
await router.replace("/management/stores"); await router.replace("/management/stores");
} finally { } finally {
@@ -277,57 +224,4 @@ async function removeStore(): Promise<void> {
} }
} }
.remove-dialog {
:deep(.t-dialog__footer) {
gap: 1rem;
}
.content {
text-align: left;
.warning {
color: #b42318;
font-weight: 700;
}
.items {
margin: .25rem 0 1rem;
display: flex;
max-height: 5rem;
overflow-y: auto;
line-height: 1.2;
padding-left: 1.25rem;
flex-direction: column;
}
.name {
font-weight: 700;
}
.confirm-input {
--td-input-vertical-padding: 10px;
border: 1px solid var(--td-component-border);
margin-top: .5rem;
border-radius: .75rem;
}
}
}
:global(html.theme-dark) {
.store-edit {
.remove-dialog {
.warning {
color: #fda29b;
}
.confirm-input {
background: rgba(15, 23, 42, .72);
border-color: rgba(148, 163, 184, .16);
}
}
}
}
</style> </style>
+74 -5
View File
@@ -8,11 +8,14 @@
@scrolltolower="fetch" @scrolltolower="fetch"
> >
<t-cell-group theme="card"> <t-cell-group theme="card">
<t-swipe-cell
v-entrance-transition="index * 30"
v-for="(item, index) in pageResult.list"
:key="item.id"
class="item"
>
<t-cell <t-cell
v-entrance-transition="index * 30" class="cell"
v-for="(item, index) in pageResult.list"
:key="item.id"
class="item"
arrow arrow
@click="toEdit(item)" @click="toEdit(item)"
> >
@@ -29,11 +32,28 @@
</div> </div>
</template> </template>
</t-cell> </t-cell>
<template v-if="canDeleteStore" #right>
<t-button
class="delete"
theme="danger"
:loading="isRemoving && removingStoreId === item.id"
@click.stop="openRemoveDialog(item)"
>
删除
</t-button>
</template>
</t-swipe-cell>
</t-cell-group> </t-cell-group>
<div v-if="!pageResult.list.length" class="empty-block" v-text="isLoading ? '正在加载门店' : '暂无门店'" /> <div v-if="!pageResult.list.length" class="empty-block" 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>
</div> </div>
<store-remove-dialog
v-model:visible="removeDialogVisible"
:store="removeTarget"
:loading="isRemoving"
@confirm="removeStore"
/>
<nav-bar-action-menu v-if="canShowActionMenu" :actions="actionList" /> <nav-bar-action-menu v-if="canShowActionMenu" :actions="actionList" />
</template> </template>
@@ -42,10 +62,11 @@ import { Message } from "@/utils/message";
import { Time, WebToolkit } from "timi-web"; import { Time, WebToolkit } from "timi-web";
import { type Page, type PageResult, useUserStore } from "timi-web-api"; import { type Page, type PageResult, useUserStore } from "timi-web-api";
import StoreAPI from "@/api/StoreAPI"; import StoreAPI from "@/api/StoreAPI";
import StoreRemoveDialog from "@/components/StoreRemoveDialog.vue";
import { useNavBarStore } from "@/store/navBarStore"; import { useNavBarStore } from "@/store/navBarStore";
import type { Store } from "@/types/Store"; import type { Store } from "@/types/Store";
import type { NavBarAction } from "@/types/NavBarAction"; import type { NavBarAction } from "@/types/NavBarAction";
import { permissionCodes } from "@/utils/permission"; import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
import { getStoreStatusText, getStoreStatusTheme } from "@/utils/store"; import { getStoreStatusText, getStoreStatusTheme } from "@/utils/store";
defineOptions({ defineOptions({
@@ -55,11 +76,17 @@ defineOptions({
const router = useRouter(); const router = useRouter();
const navBarStore = useNavBarStore(); const navBarStore = useNavBarStore();
const userStore = useUserStore(); const userStore = useUserStore();
const isStoreManagerScope = computed(() => userStore.hasRoles(gaoRoleCodes.storeManager) && !userStore.hasRoles(gaoRoleCodes.globalManager));
const canCreateStore = computed(() => userStore.hasPermissions(permissionCodes.store.create)); const canCreateStore = computed(() => userStore.hasPermissions(permissionCodes.store.create));
const canSortStore = computed(() => userStore.hasPermissions(permissionCodes.store.update)); const canSortStore = computed(() => userStore.hasPermissions(permissionCodes.store.update));
const canDeleteStore = computed(() => !isStoreManagerScope.value && userStore.hasPermissions(permissionCodes.store.delete));
const canExportStore = computed(() => userStore.hasPermissions(permissionCodes.store.export)); const canExportStore = computed(() => userStore.hasPermissions(permissionCodes.store.export));
const canShowActionMenu = computed(() => canCreateStore.value || canSortStore.value || canExportStore.value); const canShowActionMenu = computed(() => canCreateStore.value || canSortStore.value || canExportStore.value);
const isExporting = ref(false); const isExporting = ref(false);
const isRemoving = ref(false);
const removeDialogVisible = ref(false);
const removeTarget = ref<Store>();
const removingStoreId = ref("");
const actionList = computed<NavBarAction[]>(() => { const actionList = computed<NavBarAction[]>(() => {
const list: NavBarAction[] = []; const list: NavBarAction[] = [];
@@ -174,6 +201,38 @@ async function toSort(): Promise<void> {
await router.push("/management/stores/sort"); await router.push("/management/stores/sort");
} }
// ---------- 删除门店 ----------
function openRemoveDialog(store: Store): void {
if (!store.id || isRemoving.value) {
return;
}
removeTarget.value = store;
removeDialogVisible.value = true;
}
async function removeStore(store: Store): Promise<void> {
const storeId = store.id;
if (!storeId || isRemoving.value) {
return;
}
isRemoving.value = true;
removingStoreId.value = storeId;
removeDialogVisible.value = false;
removeTarget.value = undefined;
try {
await StoreAPI.remove(storeId);
pageResult.list = pageResult.list.filter(item => item.id !== storeId);
pageResult.total = Math.max(0, pageResult.total - 1);
isNoMore.value = pageResult.list.length >= pageResult.total;
Message.success("门店已删除");
} finally {
isRemoving.value = false;
removingStoreId.value = "";
}
}
onMounted(async () => { onMounted(async () => {
navBarStore.clearRight(); navBarStore.clearRight();
navBarStore.setCustomRightVisible(canShowActionMenu.value); navBarStore.setCustomRightVisible(canShowActionMenu.value);
@@ -210,6 +269,12 @@ onUnmounted(() => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.delete {
height: 100%;
padding: 0 1rem;
border-radius: 0;
}
} }
} }
@@ -220,6 +285,10 @@ onUnmounted(() => {
.item { .item {
background: #1e293b; background: #1e293b;
.cell {
background: #1e293b;
}
.name { .name {
color: #e2e8f0; color: #e2e8f0;
} }