admin-panel: add shared modal/header/icon/delete-handler components
New src/components/common/: - BaseFormModal.vue — the standard shell every converted add/edit modal will use: header=title, footer=buttons, body=form. Replaces the ad-hoc 60%/70%/80% <a-modal> widths used across the codebase with 4 named sizes. - ListPageHeader.vue — the add button that goes above every list page, gated on the same rid the create route used (modelled on the one place this already existed, ListUser.vue's add-user button). New src/components/icons.ts: the legacy-Feather-name -> ant-icon map, extracted out of TooltipBtn.vue so the sidebar can resolve the same names through the same map instead of a second copy. New src/composables/useDeleteHandler.ts: the delete-then-refetch-then-toast handler that was copy-pasted verbatim in 6 list views (city/cityList.vue, user/ListUser.vue, apps/category/List.vue, finical/list.vue, apps/product/ProductList.vue, manage/Const.vue). Preserves each site's exact success-message wording and message.success/info choice, and ProductList's irregular POST-based delete endpoint. Verified: pnpm build passes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
f0de478940
commit
02d92fc465
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<!--
|
||||
Standard shell for every "add/edit" modal in the panel: header = title,
|
||||
footer = action buttons, body = the form (passed in via the default slot).
|
||||
The 16 forms converted from full pages to modals all use this instead of
|
||||
hand-rolling <a-modal> + <a-card> + a trailing button, which used to be
|
||||
duplicated 16 times with inconsistent widths (60%/70%/80%/default).
|
||||
-->
|
||||
<a-modal
|
||||
:open="open"
|
||||
:title="id ? editTitle : addTitle"
|
||||
:width="WIDTH[width]"
|
||||
:confirm-loading="submitting"
|
||||
:ok-text="okText"
|
||||
cancel-text="انصراف"
|
||||
:mask-closable="false"
|
||||
@update:open="onUpdateOpen"
|
||||
@ok="$emit('submit')"
|
||||
@cancel="onCancel"
|
||||
>
|
||||
<a-spin :spinning="loading">
|
||||
<a-form layout="vertical">
|
||||
<slot />
|
||||
</a-form>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
open: boolean;
|
||||
/** Presence of `id` switches the modal into edit mode (title only — the
|
||||
* form body decides its own create/edit behaviour from the same prop). */
|
||||
id?: string;
|
||||
addTitle: string;
|
||||
editTitle: string;
|
||||
/** Drives the inner a-spin, e.g. while loading the record being edited. */
|
||||
loading?: boolean;
|
||||
/** Drives the OK button's loading state while a submit is in flight. */
|
||||
submitting?: boolean;
|
||||
/** 520 / 720 / 900 / 1140 — replaces the old ad-hoc 60%/70%/80% widths. */
|
||||
width?: 'sm' | 'md' | 'lg' | 'xl';
|
||||
okText?: string;
|
||||
}>(),
|
||||
{
|
||||
id: undefined,
|
||||
loading: false,
|
||||
submitting: false,
|
||||
width: 'md',
|
||||
okText: 'ثبت اطلاعات',
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:open', value: boolean): void;
|
||||
(e: 'submit'): void;
|
||||
(e: 'cancel'): void;
|
||||
}>();
|
||||
|
||||
const WIDTH: Record<'sm' | 'md' | 'lg' | 'xl', number> = {
|
||||
sm: 520,
|
||||
md: 720,
|
||||
lg: 900,
|
||||
xl: 1140,
|
||||
};
|
||||
|
||||
function onUpdateOpen(value: boolean) {
|
||||
emit('update:open', value);
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
emit('update:open', false);
|
||||
emit('cancel');
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<!--
|
||||
Standard header for every list page: the section title plus, when the
|
||||
caller has one, an "add" button ABOVE the list — same rid the create
|
||||
route used to gate on, so permissions are not re-derived by hand.
|
||||
Modelled on the one place this already existed (ListUser.vue's add-user
|
||||
button) and generalised so every list page looks the same.
|
||||
-->
|
||||
<div class="list-page-header">
|
||||
<a-button v-if="!rid || hasPermission(rid)" type="primary" @click="$emit('add')">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
{{ addLabel }}
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import { useUsersStore } from '../../stores/users';
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
/** Permission id gating the button; omit if the action has no rid of its own. */
|
||||
rid?: string;
|
||||
addLabel?: string;
|
||||
}>(),
|
||||
{ rid: undefined, addLabel: 'افزودن' },
|
||||
);
|
||||
|
||||
defineEmits<{ (e: 'add'): void }>();
|
||||
|
||||
const users = useUsersStore();
|
||||
const hasPermission = (id: string) => Boolean(users.hasPermission(id));
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.list-page-header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,81 @@
|
||||
// Shared legacy-Feather-name -> ant-design-vue icon map.
|
||||
//
|
||||
// Originally lived only inside TooltipBtn.vue (row-action icons). Extracted
|
||||
// here so the sidebar (navMenuItems.ts icon: values) can resolve the SAME
|
||||
// names through the SAME map instead of duplicating it — one source of
|
||||
// truth for "legacy Feather icon name" -> "ant component" across the app.
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
BarChartOutlined,
|
||||
BellOutlined,
|
||||
BlockOutlined,
|
||||
BookOutlined,
|
||||
CameraOutlined,
|
||||
CheckOutlined,
|
||||
CloseOutlined,
|
||||
CreditCardOutlined,
|
||||
DeleteOutlined,
|
||||
DollarOutlined,
|
||||
EditOutlined,
|
||||
EyeOutlined,
|
||||
FileOutlined,
|
||||
FileTextOutlined,
|
||||
GiftOutlined,
|
||||
HomeOutlined,
|
||||
InboxOutlined,
|
||||
MailOutlined,
|
||||
MessageOutlined,
|
||||
PercentageOutlined,
|
||||
PlusOutlined,
|
||||
SettingOutlined,
|
||||
ShoppingOutlined,
|
||||
StopOutlined,
|
||||
TeamOutlined,
|
||||
UnorderedListOutlined,
|
||||
UserAddOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
|
||||
/** legacy Feather name -> ant component. */
|
||||
export const ICONS: Record<string, unknown> = {
|
||||
// row-action icons (TooltipBtn.vue's original set)
|
||||
ListIcon: UnorderedListOutlined,
|
||||
FileIcon: FileOutlined,
|
||||
TrashIcon: DeleteOutlined,
|
||||
EditIcon: EditOutlined,
|
||||
CheckIcon: CheckOutlined,
|
||||
CameraIcon: CameraOutlined,
|
||||
EyeIcon: EyeOutlined,
|
||||
PlusIcon: PlusOutlined,
|
||||
XIcon: CloseOutlined,
|
||||
FileTextIcon: FileTextOutlined,
|
||||
|
||||
// sidebar icons (navMenuItems.ts), added when the sidebar started
|
||||
// rendering them for the first time
|
||||
HomeIcon: HomeOutlined,
|
||||
PackageIcon: InboxOutlined,
|
||||
UserIcon: UserOutlined,
|
||||
UsersIcon: TeamOutlined,
|
||||
UserPlusIcon: UserAddOutlined,
|
||||
ShoppingBagIcon: ShoppingOutlined,
|
||||
BoxIcon: InboxOutlined,
|
||||
GridIcon: AppstoreOutlined,
|
||||
LayersIcon: BlockOutlined,
|
||||
CreditCardIcon: CreditCardOutlined,
|
||||
DollarSignIcon: DollarOutlined,
|
||||
PercentIcon: PercentageOutlined,
|
||||
GiftIcon: GiftOutlined,
|
||||
SlashIcon: StopOutlined,
|
||||
BookIcon: BookOutlined,
|
||||
MessageSquareIcon: MessageOutlined,
|
||||
MailIcon: MailOutlined,
|
||||
BellIcon: BellOutlined,
|
||||
BarChartIcon: BarChartOutlined,
|
||||
SettingsIcon: SettingOutlined,
|
||||
InboxIcon: InboxOutlined,
|
||||
};
|
||||
|
||||
export function resolveIcon(name: string | undefined): unknown | undefined {
|
||||
if (!name) return undefined;
|
||||
return ICONS[name];
|
||||
}
|
||||
@@ -5,8 +5,9 @@
|
||||
it confirms first, without it fires immediately.
|
||||
|
||||
Legacy rendered <feather-icon icon="ListIcon">. vue-feather has no Vue 3
|
||||
build, so the feather NAME is mapped to an ant-design icon here. Ported views
|
||||
keep writing icon="ListIcon" unchanged.
|
||||
build, so the feather NAME is mapped to an ant-design icon via the shared
|
||||
map in components/icons.ts (also used by the sidebar). Ported views keep
|
||||
writing icon="ListIcon" unchanged.
|
||||
-->
|
||||
<div>
|
||||
<a-tooltip :title="title">
|
||||
@@ -33,18 +34,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import {
|
||||
CameraOutlined,
|
||||
CheckOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
EyeOutlined,
|
||||
FileOutlined,
|
||||
FileTextOutlined,
|
||||
PlusOutlined,
|
||||
UnorderedListOutlined,
|
||||
CloseOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { UnorderedListOutlined } from '@ant-design/icons-vue';
|
||||
import { resolveIcon } from '../icons';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -58,21 +49,7 @@ const props = withDefaults(
|
||||
|
||||
const emit = defineEmits<{ (e: 'click'): void }>();
|
||||
|
||||
/** feather name -> ant component */
|
||||
const ICONS: Record<string, unknown> = {
|
||||
ListIcon: UnorderedListOutlined,
|
||||
FileIcon: FileOutlined,
|
||||
TrashIcon: DeleteOutlined,
|
||||
EditIcon: EditOutlined,
|
||||
CheckIcon: CheckOutlined,
|
||||
CameraIcon: CameraOutlined,
|
||||
EyeIcon: EyeOutlined,
|
||||
PlusIcon: PlusOutlined,
|
||||
XIcon: CloseOutlined,
|
||||
FileTextIcon: FileTextOutlined,
|
||||
};
|
||||
|
||||
const iconComponent = computed(() => ICONS[props.icon ?? ''] ?? UnorderedListOutlined);
|
||||
const iconComponent = computed(() => resolveIcon(props.icon) ?? UnorderedListOutlined);
|
||||
|
||||
function onClick() {
|
||||
emit('click');
|
||||
|
||||
Reference in New Issue
Block a user