update
This commit is contained in:
@@ -8,3 +8,4 @@ front/node_modules/
|
||||
front/dist/
|
||||
certs/
|
||||
*.pem
|
||||
front/tsconfig.tsbuildinfo
|
||||
@@ -0,0 +1,43 @@
|
||||
import { request } from './http';
|
||||
|
||||
export type AdSlot = {
|
||||
id: number;
|
||||
temple_id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
description: string | null;
|
||||
sort_order: number;
|
||||
is_active: boolean;
|
||||
};
|
||||
|
||||
export type AdSlotPayload = {
|
||||
name: string;
|
||||
code: string;
|
||||
description?: string | null;
|
||||
sort_order?: number;
|
||||
is_active?: boolean;
|
||||
};
|
||||
|
||||
export function listAdSlots() {
|
||||
return request<AdSlot[]>('/ad-slots');
|
||||
}
|
||||
|
||||
export function createAdSlot(payload: AdSlotPayload) {
|
||||
return request<AdSlot>('/ad-slots', {
|
||||
method: 'POST',
|
||||
json: payload,
|
||||
});
|
||||
}
|
||||
|
||||
export function updateAdSlot(id: number, payload: Partial<AdSlotPayload>) {
|
||||
return request<AdSlot>(`/ad-slots/${id}`, {
|
||||
method: 'PATCH',
|
||||
json: payload,
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteAdSlot(id: number) {
|
||||
return request<void>(`/ad-slots/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { request } from './http';
|
||||
|
||||
export type AdTargetType = 'app' | 'link';
|
||||
|
||||
export type Ad = {
|
||||
id: number;
|
||||
temple_id: number;
|
||||
slot_id: number;
|
||||
slot_name: string | null;
|
||||
slot_code: string | null;
|
||||
title: string;
|
||||
image_url: string;
|
||||
target_type: AdTargetType;
|
||||
target_url: string | null;
|
||||
product_id: number | null;
|
||||
product_name: string | null;
|
||||
sort_order: number;
|
||||
is_active: boolean;
|
||||
};
|
||||
|
||||
export type AdPayload = {
|
||||
slot_id: number;
|
||||
title: string;
|
||||
image_url: string;
|
||||
target_type: AdTargetType;
|
||||
target_url?: string | null;
|
||||
product_id?: number | null;
|
||||
sort_order?: number;
|
||||
is_active?: boolean;
|
||||
};
|
||||
|
||||
export function listAds() {
|
||||
return request<Ad[]>('/ads');
|
||||
}
|
||||
|
||||
export function createAd(payload: AdPayload) {
|
||||
return request<Ad>('/ads', {
|
||||
method: 'POST',
|
||||
json: payload,
|
||||
});
|
||||
}
|
||||
|
||||
export function updateAd(id: number, payload: Partial<AdPayload>) {
|
||||
return request<Ad>(`/ads/${id}`, {
|
||||
method: 'PATCH',
|
||||
json: payload,
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteAd(id: number) {
|
||||
return request<void>(`/ads/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { request } from './http';
|
||||
|
||||
export type FeatureIconTargetType = 'app' | 'link';
|
||||
|
||||
export type FeatureIcon = {
|
||||
id: number;
|
||||
temple_id: number;
|
||||
title: string;
|
||||
icon_url: string;
|
||||
target_type: FeatureIconTargetType;
|
||||
target_url: string | null;
|
||||
product_id: number | null;
|
||||
product_name: string | null;
|
||||
sort_order: number;
|
||||
is_active: boolean;
|
||||
};
|
||||
|
||||
export type FeatureIconPayload = {
|
||||
title: string;
|
||||
icon_url: string;
|
||||
target_type: FeatureIconTargetType;
|
||||
target_url?: string | null;
|
||||
product_id?: number | null;
|
||||
sort_order?: number;
|
||||
is_active?: boolean;
|
||||
};
|
||||
|
||||
export function listFeatureIcons() {
|
||||
return request<FeatureIcon[]>('/feature-icons');
|
||||
}
|
||||
|
||||
export function listSourceFeatureIcons(sourceTempleId: number) {
|
||||
return request<FeatureIcon[]>(`/feature-icons/source/${sourceTempleId}`);
|
||||
}
|
||||
|
||||
export function createFeatureIcon(payload: FeatureIconPayload) {
|
||||
return request<FeatureIcon>('/feature-icons', {
|
||||
method: 'POST',
|
||||
json: payload,
|
||||
});
|
||||
}
|
||||
|
||||
export function updateFeatureIcon(id: number, payload: Partial<FeatureIconPayload>) {
|
||||
return request<FeatureIcon>(`/feature-icons/${id}`, {
|
||||
method: 'PATCH',
|
||||
json: payload,
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteFeatureIcon(id: number) {
|
||||
return request<void>(`/feature-icons/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
export function copyFeatureIcons(sourceTempleId: number, iconIds: number[]) {
|
||||
return request<FeatureIcon[]>('/feature-icons/copy', {
|
||||
method: 'POST',
|
||||
json: {
|
||||
source_temple_id: sourceTempleId,
|
||||
icon_ids: iconIds,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
<script setup lang="ts">
|
||||
import { DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons-vue';
|
||||
import Modal from 'ant-design-vue/es/modal';
|
||||
import message from 'ant-design-vue/es/message';
|
||||
import type { Rule } from 'ant-design-vue/es/form';
|
||||
import type { ColumnsType } from 'ant-design-vue/es/table';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import {
|
||||
createAdSlot,
|
||||
deleteAdSlot,
|
||||
listAdSlots,
|
||||
updateAdSlot,
|
||||
type AdSlot,
|
||||
type AdSlotPayload,
|
||||
} from '@/api/adSlots';
|
||||
|
||||
type AdSlotFormState = {
|
||||
id?: number;
|
||||
name: string;
|
||||
code: string;
|
||||
description: string;
|
||||
sort_order: number;
|
||||
is_active: boolean;
|
||||
};
|
||||
|
||||
const adSlots = ref<AdSlot[]>([]);
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const modalOpen = ref(false);
|
||||
const formRef = ref();
|
||||
|
||||
const formState = reactive<AdSlotFormState>({
|
||||
name: '',
|
||||
code: '',
|
||||
description: '',
|
||||
sort_order: 0,
|
||||
is_active: true,
|
||||
});
|
||||
|
||||
const isEditing = computed(() => formState.id !== undefined);
|
||||
const modalTitle = computed(() => (isEditing.value ? '编辑广告位' : '新建广告位'));
|
||||
|
||||
const columns: ColumnsType<AdSlot> = [
|
||||
{ title: '广告位名称', dataIndex: 'name', width: 180 },
|
||||
{ title: '广告位编码', dataIndex: 'code', width: 160 },
|
||||
{ title: '说明', dataIndex: 'description' },
|
||||
{ title: '排序', dataIndex: 'sort_order', width: 90 },
|
||||
{ title: '是否启用', dataIndex: 'is_active', width: 100 },
|
||||
{ title: '操作', key: 'actions', width: 140, fixed: 'right' },
|
||||
];
|
||||
|
||||
const rules: Record<string, Rule[]> = {
|
||||
name: [{ required: true, message: '请输入广告位名称', trigger: 'blur' }],
|
||||
code: [
|
||||
{ required: true, message: '请输入广告位编码', trigger: 'blur' },
|
||||
{ pattern: /^[a-zA-Z0-9_-]+$/, message: '编码只支持字母、数字、下划线和短横线', trigger: 'blur' },
|
||||
],
|
||||
};
|
||||
|
||||
function resetForm() {
|
||||
formState.id = undefined;
|
||||
formState.name = '';
|
||||
formState.code = '';
|
||||
formState.description = '';
|
||||
formState.sort_order = 0;
|
||||
formState.is_active = true;
|
||||
formRef.value?.clearValidate?.();
|
||||
}
|
||||
|
||||
async function fetchAdSlots() {
|
||||
loading.value = true;
|
||||
try {
|
||||
adSlots.value = await listAdSlots();
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '加载广告位失败,请先选择寺院');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateModal() {
|
||||
resetForm();
|
||||
modalOpen.value = true;
|
||||
}
|
||||
|
||||
function openEditModal(adSlot: AdSlot) {
|
||||
formState.id = adSlot.id;
|
||||
formState.name = adSlot.name;
|
||||
formState.code = adSlot.code;
|
||||
formState.description = adSlot.description || '';
|
||||
formState.sort_order = adSlot.sort_order;
|
||||
formState.is_active = adSlot.is_active;
|
||||
modalOpen.value = true;
|
||||
}
|
||||
|
||||
function buildPayload(): AdSlotPayload {
|
||||
return {
|
||||
code: formState.code.trim(),
|
||||
description: formState.description.trim() || null,
|
||||
is_active: formState.is_active,
|
||||
name: formState.name,
|
||||
sort_order: formState.sort_order,
|
||||
};
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
await formRef.value?.validate?.();
|
||||
saving.value = true;
|
||||
try {
|
||||
if (isEditing.value && formState.id !== undefined) {
|
||||
await updateAdSlot(formState.id, buildPayload());
|
||||
message.success('广告位已更新');
|
||||
} else {
|
||||
await createAdSlot(buildPayload());
|
||||
message.success('广告位已创建');
|
||||
}
|
||||
modalOpen.value = false;
|
||||
resetForm();
|
||||
await fetchAdSlots();
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '保存广告位失败');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDelete(adSlot: AdSlot) {
|
||||
Modal.confirm({
|
||||
title: '删除广告位',
|
||||
content: `确认删除广告位 ${adSlot.name}?`,
|
||||
okText: '删除',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
async onOk() {
|
||||
await deleteAdSlot(adSlot.id);
|
||||
message.success('广告位已删除');
|
||||
await fetchAdSlots();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(fetchAdSlots);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="workspace">
|
||||
<div class="page-heading">
|
||||
<h1>广告位管理</h1>
|
||||
<a-space>
|
||||
<a-button :loading="loading" @click="fetchAdSlots">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
刷新
|
||||
</a-button>
|
||||
<a-button type="primary" @click="openCreateModal">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新建广告位
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="adSlots"
|
||||
:loading="loading"
|
||||
:pagination="{ pageSize: 8, showSizeChanger: false }"
|
||||
bordered
|
||||
row-key="id"
|
||||
size="middle"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'description'">
|
||||
{{ record.description || '-' }}
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'is_active'">
|
||||
<a-tag :color="record.is_active ? 'green' : 'default'">
|
||||
{{ record.is_active ? '启用' : '停用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'actions'">
|
||||
<a-space>
|
||||
<a-tooltip title="编辑">
|
||||
<a-button type="text" @click="openEditModal(record)">
|
||||
<template #icon><EditOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="删除">
|
||||
<a-button type="text" danger @click="confirmDelete(record)">
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</section>
|
||||
|
||||
<a-modal
|
||||
v-model:open="modalOpen"
|
||||
:confirm-loading="saving"
|
||||
:title="modalTitle"
|
||||
ok-text="保存"
|
||||
cancel-text="取消"
|
||||
@ok="submitForm"
|
||||
>
|
||||
<a-form ref="formRef" :model="formState" :rules="rules" layout="vertical">
|
||||
<a-form-item label="广告位名称" name="name">
|
||||
<a-input v-model:value="formState.name" placeholder="首页广告" />
|
||||
</a-form-item>
|
||||
<a-form-item label="广告位编码" name="code">
|
||||
<a-input v-model:value="formState.code" placeholder="home_banner" />
|
||||
</a-form-item>
|
||||
<a-form-item label="说明" name="description">
|
||||
<a-textarea v-model:value="formState.description" :rows="3" placeholder="用于首页顶部广告展示" />
|
||||
</a-form-item>
|
||||
<a-form-item label="排序" name="sort_order">
|
||||
<a-input-number v-model:value="formState.sort_order" :min="0" class="full-width-control" />
|
||||
</a-form-item>
|
||||
<a-form-item label="是否启用" name="is_active">
|
||||
<a-switch v-model:checked="formState.is_active" checked-children="启用" un-checked-children="停用" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
@@ -0,0 +1,428 @@
|
||||
<script setup lang="ts">
|
||||
import { DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons-vue';
|
||||
import Modal from 'ant-design-vue/es/modal';
|
||||
import message from 'ant-design-vue/es/message';
|
||||
import type { Rule } from 'ant-design-vue/es/form';
|
||||
import type { ColumnsType } from 'ant-design-vue/es/table';
|
||||
import type { UploadFile } from 'ant-design-vue/es/upload/interface';
|
||||
import type { UploadRequestOption } from 'ant-design-vue/es/vc-upload/interface';
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||
|
||||
import {
|
||||
createAd,
|
||||
deleteAd,
|
||||
listAds,
|
||||
updateAd,
|
||||
type Ad,
|
||||
type AdPayload,
|
||||
type AdTargetType,
|
||||
} from '@/api/ads';
|
||||
import { listAdSlots, type AdSlot } from '@/api/adSlots';
|
||||
import { listProducts, type Product } from '@/api/products';
|
||||
import { uploadImage, type UploadResponse } from '@/api/uploads';
|
||||
|
||||
type AdFormState = {
|
||||
id?: number;
|
||||
slot_id?: number;
|
||||
title: string;
|
||||
image_url: string;
|
||||
target_type: AdTargetType;
|
||||
target_url: string;
|
||||
product_id?: number;
|
||||
sort_order: number;
|
||||
is_active: boolean;
|
||||
};
|
||||
|
||||
const ads = ref<Ad[]>([]);
|
||||
const adSlots = ref<AdSlot[]>([]);
|
||||
const products = ref<Product[]>([]);
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const modalOpen = ref(false);
|
||||
const formRef = ref();
|
||||
const imageFileList = ref<UploadFile[]>([]);
|
||||
|
||||
const formState = reactive<AdFormState>({
|
||||
slot_id: undefined,
|
||||
title: '',
|
||||
image_url: '',
|
||||
target_type: 'link',
|
||||
target_url: '',
|
||||
product_id: undefined,
|
||||
sort_order: 0,
|
||||
is_active: true,
|
||||
});
|
||||
|
||||
const isEditing = computed(() => formState.id !== undefined);
|
||||
const modalTitle = computed(() => (isEditing.value ? '编辑广告' : '新建广告'));
|
||||
|
||||
const slotOptions = computed(() =>
|
||||
adSlots.value.map((slot) => ({
|
||||
label: `${slot.name}(${slot.code})`,
|
||||
value: slot.id,
|
||||
})),
|
||||
);
|
||||
|
||||
const productOptions = computed(() =>
|
||||
products.value.map((product) => ({
|
||||
label: product.alias ? `${product.name}(${product.alias})` : product.name,
|
||||
value: product.id,
|
||||
})),
|
||||
);
|
||||
|
||||
const columns: ColumnsType<Ad> = [
|
||||
{ title: '广告图', dataIndex: 'image_url', width: 150 },
|
||||
{ title: '广告名称', dataIndex: 'title', width: 180 },
|
||||
{ title: '广告位', dataIndex: 'slot_name', width: 180 },
|
||||
{ title: '跳转目标', key: 'target', width: 260 },
|
||||
{ title: '类型', dataIndex: 'target_type', width: 100 },
|
||||
{ title: '是否启用', dataIndex: 'is_active', width: 100 },
|
||||
{ title: '排序', dataIndex: 'sort_order', width: 90 },
|
||||
{ title: '操作', key: 'actions', width: 140, fixed: 'right' },
|
||||
];
|
||||
|
||||
const rules: Record<string, Rule[]> = {
|
||||
slot_id: [{ required: true, message: '请选择广告位', trigger: 'change' }],
|
||||
title: [{ required: true, message: '请输入广告名称', trigger: 'blur' }],
|
||||
image_url: [{ required: true, message: '请上传广告图', trigger: 'change' }],
|
||||
};
|
||||
|
||||
function resetForm() {
|
||||
formState.id = undefined;
|
||||
formState.slot_id = undefined;
|
||||
formState.title = '';
|
||||
formState.image_url = '';
|
||||
formState.target_type = 'link';
|
||||
formState.target_url = '';
|
||||
formState.product_id = undefined;
|
||||
formState.sort_order = 0;
|
||||
formState.is_active = true;
|
||||
imageFileList.value = [];
|
||||
formRef.value?.clearValidate?.();
|
||||
}
|
||||
|
||||
function getFileName(url: string) {
|
||||
try {
|
||||
return new URL(url).pathname.split('/').filter(Boolean).pop() || 'ad';
|
||||
} catch {
|
||||
return url.split('/').filter(Boolean).pop() || 'ad';
|
||||
}
|
||||
}
|
||||
|
||||
function createUploadFile(url: string): UploadFile {
|
||||
return {
|
||||
uid: `ad-${url}`,
|
||||
name: getFileName(url),
|
||||
status: 'done',
|
||||
url,
|
||||
};
|
||||
}
|
||||
|
||||
function syncImageFileList() {
|
||||
imageFileList.value = formState.image_url ? [createUploadFile(formState.image_url)] : [];
|
||||
}
|
||||
|
||||
function getRequestFile(option: UploadRequestOption) {
|
||||
return option.file instanceof Blob ? option.file : null;
|
||||
}
|
||||
|
||||
async function handleImageUpload(option: UploadRequestOption) {
|
||||
const file = getRequestFile(option);
|
||||
if (!file) {
|
||||
message.error('请选择图片文件');
|
||||
option.onError?.(new Error('请选择图片文件'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await uploadImage(file, 'ads');
|
||||
const response = result as UploadResponse;
|
||||
formState.image_url = response.url;
|
||||
imageFileList.value = [
|
||||
{
|
||||
uid: String((option.file as Blob & { uid?: string }).uid || Date.now()),
|
||||
name: getFileName(response.url),
|
||||
status: 'done',
|
||||
url: response.url,
|
||||
},
|
||||
];
|
||||
option.onSuccess?.(result);
|
||||
} catch (error) {
|
||||
const uploadError = error instanceof Error ? error : new Error('上传失败');
|
||||
message.error(uploadError.message);
|
||||
option.onError?.(uploadError);
|
||||
}
|
||||
}
|
||||
|
||||
function removeImage() {
|
||||
formState.image_url = '';
|
||||
imageFileList.value = [];
|
||||
return true;
|
||||
}
|
||||
|
||||
function getTargetText(ad: Ad) {
|
||||
if (ad.target_type === 'app') {
|
||||
return ad.product_name || '未选择商品';
|
||||
}
|
||||
return ad.target_url || '-';
|
||||
}
|
||||
|
||||
async function fetchAds() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const [adRows, slotRows, productRows] = await Promise.all([listAds(), listAdSlots(), listProducts()]);
|
||||
ads.value = adRows;
|
||||
adSlots.value = slotRows;
|
||||
products.value = productRows;
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '加载广告失败,请先选择寺院');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateModal() {
|
||||
resetForm();
|
||||
if (slotOptions.value.length > 0) {
|
||||
formState.slot_id = slotOptions.value[0].value;
|
||||
}
|
||||
modalOpen.value = true;
|
||||
}
|
||||
|
||||
function openEditModal(ad: Ad) {
|
||||
formState.id = ad.id;
|
||||
formState.slot_id = ad.slot_id;
|
||||
formState.title = ad.title;
|
||||
formState.image_url = ad.image_url;
|
||||
formState.target_type = ad.target_type;
|
||||
formState.target_url = ad.target_url || '';
|
||||
formState.product_id = ad.product_id || undefined;
|
||||
formState.sort_order = ad.sort_order;
|
||||
formState.is_active = ad.is_active;
|
||||
syncImageFileList();
|
||||
modalOpen.value = true;
|
||||
}
|
||||
|
||||
function buildPayload(): AdPayload {
|
||||
const payload: AdPayload = {
|
||||
image_url: formState.image_url,
|
||||
is_active: formState.is_active,
|
||||
slot_id: Number(formState.slot_id),
|
||||
sort_order: formState.sort_order,
|
||||
target_type: formState.target_type,
|
||||
title: formState.title,
|
||||
};
|
||||
if (formState.target_type === 'link') {
|
||||
payload.target_url = formState.target_url.trim();
|
||||
payload.product_id = null;
|
||||
} else {
|
||||
payload.target_url = null;
|
||||
payload.product_id = formState.product_id || null;
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
await formRef.value?.validate?.();
|
||||
if (!formState.slot_id) {
|
||||
message.error('请选择广告位');
|
||||
return;
|
||||
}
|
||||
if (formState.target_type === 'link' && !formState.target_url.trim()) {
|
||||
message.error('请输入链接地址');
|
||||
return;
|
||||
}
|
||||
if (formState.target_type === 'app' && !formState.product_id) {
|
||||
message.error('请选择商品');
|
||||
return;
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
if (isEditing.value && formState.id !== undefined) {
|
||||
await updateAd(formState.id, buildPayload());
|
||||
message.success('广告已更新');
|
||||
} else {
|
||||
await createAd(buildPayload());
|
||||
message.success('广告已创建');
|
||||
}
|
||||
modalOpen.value = false;
|
||||
resetForm();
|
||||
await fetchAds();
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '保存广告失败');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDelete(ad: Ad) {
|
||||
Modal.confirm({
|
||||
title: '删除广告',
|
||||
content: `确认删除广告 ${ad.title}?`,
|
||||
okText: '删除',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
async onOk() {
|
||||
await deleteAd(ad.id);
|
||||
message.success('广告已删除');
|
||||
await fetchAds();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => formState.target_type,
|
||||
(targetType) => {
|
||||
if (targetType === 'link') {
|
||||
formState.product_id = undefined;
|
||||
} else {
|
||||
formState.target_url = '';
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(fetchAds);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="workspace">
|
||||
<div class="page-heading">
|
||||
<h1>广告管理</h1>
|
||||
<a-space>
|
||||
<a-button :loading="loading" @click="fetchAds">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
刷新
|
||||
</a-button>
|
||||
<a-button type="primary" @click="openCreateModal">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新建广告
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="ads"
|
||||
:loading="loading"
|
||||
:pagination="{ pageSize: 8, showSizeChanger: false }"
|
||||
:scroll="{ x: 1180 }"
|
||||
bordered
|
||||
row-key="id"
|
||||
size="middle"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'image_url'">
|
||||
<a-image :src="record.image_url" :width="96" :height="48" class="ad-preview" />
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'slot_name'">
|
||||
{{ record.slot_name || '-' }}
|
||||
</template>
|
||||
<template v-if="column.key === 'target'">
|
||||
{{ getTargetText(record) }}
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'target_type'">
|
||||
<a-tag :color="record.target_type === 'link' ? 'blue' : 'purple'">
|
||||
{{ record.target_type === 'link' ? '链接' : '应用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'is_active'">
|
||||
<a-tag :color="record.is_active ? 'green' : 'default'">
|
||||
{{ record.is_active ? '启用' : '停用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'actions'">
|
||||
<a-space>
|
||||
<a-tooltip title="编辑">
|
||||
<a-button type="text" @click="openEditModal(record)">
|
||||
<template #icon><EditOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="删除">
|
||||
<a-button type="text" danger @click="confirmDelete(record)">
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</section>
|
||||
|
||||
<a-modal
|
||||
v-model:open="modalOpen"
|
||||
:confirm-loading="saving"
|
||||
:title="modalTitle"
|
||||
width="720px"
|
||||
ok-text="保存"
|
||||
cancel-text="取消"
|
||||
@ok="submitForm"
|
||||
>
|
||||
<a-form ref="formRef" :model="formState" :rules="rules" layout="vertical">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="广告位" name="slot_id">
|
||||
<a-select v-model:value="formState.slot_id" :options="slotOptions" placeholder="请选择广告位" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="广告名称" name="title">
|
||||
<a-input v-model:value="formState.title" placeholder="首页头图广告" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="广告图片" name="image_url">
|
||||
<a-upload
|
||||
v-model:file-list="imageFileList"
|
||||
accept="image/*"
|
||||
list-type="picture-card"
|
||||
:custom-request="handleImageUpload"
|
||||
:max-count="1"
|
||||
@remove="removeImage"
|
||||
>
|
||||
<div v-if="imageFileList.length < 1">
|
||||
<PlusOutlined />
|
||||
<div class="upload-text">上传图片</div>
|
||||
</div>
|
||||
</a-upload>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="类型" name="target_type">
|
||||
<a-segmented
|
||||
v-model:value="formState.target_type"
|
||||
:options="[
|
||||
{ label: '链接', value: 'link' },
|
||||
{ label: '应用', value: 'app' },
|
||||
]"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="排序" name="sort_order">
|
||||
<a-input-number v-model:value="formState.sort_order" :min="0" class="full-width-control" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="是否启用" name="is_active">
|
||||
<a-switch v-model:checked="formState.is_active" checked-children="启用" un-checked-children="停用" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item v-if="formState.target_type === 'link'" label="链接地址" name="target_url">
|
||||
<a-input v-model:value="formState.target_url" placeholder="https://example.com 或 /pages/detail" />
|
||||
</a-form-item>
|
||||
<a-form-item v-else label="选择商品" name="product_id">
|
||||
<a-select
|
||||
v-model:value="formState.product_id"
|
||||
:options="productOptions"
|
||||
placeholder="请选择当前寺院商品"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
@@ -0,0 +1,528 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
CopyOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
PlusOutlined,
|
||||
ReloadOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import Modal from 'ant-design-vue/es/modal';
|
||||
import message from 'ant-design-vue/es/message';
|
||||
import type { Rule } from 'ant-design-vue/es/form';
|
||||
import type { ColumnsType } from 'ant-design-vue/es/table';
|
||||
import type { UploadFile } from 'ant-design-vue/es/upload/interface';
|
||||
import type { UploadRequestOption } from 'ant-design-vue/es/vc-upload/interface';
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||
|
||||
import {
|
||||
copyFeatureIcons,
|
||||
createFeatureIcon,
|
||||
deleteFeatureIcon,
|
||||
listFeatureIcons,
|
||||
listSourceFeatureIcons,
|
||||
updateFeatureIcon,
|
||||
type FeatureIcon,
|
||||
type FeatureIconPayload,
|
||||
type FeatureIconTargetType,
|
||||
} from '@/api/featureIcons';
|
||||
import { CURRENT_TEMPLE_ID_KEY, DEFAULT_TEMPLE_ID } from '@/api/http';
|
||||
import { listProducts, type Product } from '@/api/products';
|
||||
import { listTemples, type Temple } from '@/api/temples';
|
||||
import { uploadImage, type UploadResponse } from '@/api/uploads';
|
||||
|
||||
type IconFormState = {
|
||||
id?: number;
|
||||
title: string;
|
||||
icon_url: string;
|
||||
target_type: FeatureIconTargetType;
|
||||
target_url: string;
|
||||
product_id?: number;
|
||||
sort_order: number;
|
||||
is_active: boolean;
|
||||
};
|
||||
|
||||
const icons = ref<FeatureIcon[]>([]);
|
||||
const products = ref<Product[]>([]);
|
||||
const temples = ref<Temple[]>([]);
|
||||
const sourceIcons = ref<FeatureIcon[]>([]);
|
||||
const selectedSourceIconIds = ref<number[]>([]);
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const syncLoading = ref(false);
|
||||
const sourceLoading = ref(false);
|
||||
const modalOpen = ref(false);
|
||||
const syncModalOpen = ref(false);
|
||||
const formRef = ref();
|
||||
const iconFileList = ref<UploadFile[]>([]);
|
||||
const currentTempleId = computed(() => Number(localStorage.getItem(CURRENT_TEMPLE_ID_KEY) || DEFAULT_TEMPLE_ID));
|
||||
|
||||
const formState = reactive<IconFormState>({
|
||||
title: '',
|
||||
icon_url: '',
|
||||
target_type: 'link',
|
||||
target_url: '',
|
||||
product_id: undefined,
|
||||
sort_order: 0,
|
||||
is_active: true,
|
||||
});
|
||||
|
||||
const syncState = reactive({
|
||||
source_temple_id: undefined as number | undefined,
|
||||
});
|
||||
|
||||
const isEditing = computed(() => formState.id !== undefined);
|
||||
const modalTitle = computed(() => (isEditing.value ? '编辑图标' : '添加图标'));
|
||||
|
||||
const productOptions = computed(() =>
|
||||
products.value.map((product) => ({
|
||||
label: product.alias ? `${product.name}(${product.alias})` : product.name,
|
||||
value: product.id,
|
||||
})),
|
||||
);
|
||||
|
||||
const templeOptions = computed(() =>
|
||||
temples.value
|
||||
.filter((temple) => temple.id !== currentTempleId.value)
|
||||
.map((temple) => ({
|
||||
label: temple.name,
|
||||
value: temple.id,
|
||||
})),
|
||||
);
|
||||
|
||||
const columns: ColumnsType<FeatureIcon> = [
|
||||
{ title: '图标预览', dataIndex: 'icon_url', width: 110 },
|
||||
{ title: '图标列表', dataIndex: 'title', width: 160 },
|
||||
{ title: '跳转地址', key: 'target', width: 240 },
|
||||
{ title: '类型', dataIndex: 'target_type', width: 100 },
|
||||
{ title: '是否启用', dataIndex: 'is_active', width: 100 },
|
||||
{ title: '排序', dataIndex: 'sort_order', width: 90 },
|
||||
{ title: '操作', key: 'actions', width: 140, fixed: 'right' },
|
||||
];
|
||||
|
||||
const sourceColumns: ColumnsType<FeatureIcon> = [
|
||||
{ title: '图标', dataIndex: 'icon_url', width: 90 },
|
||||
{ title: '名称', dataIndex: 'title' },
|
||||
{ title: '类型', dataIndex: 'target_type', width: 100 },
|
||||
{ title: '状态', dataIndex: 'is_active', width: 100 },
|
||||
];
|
||||
|
||||
const sourceRowSelection = computed(() => ({
|
||||
selectedRowKeys: selectedSourceIconIds.value,
|
||||
onChange: (keys: (string | number)[]) => {
|
||||
selectedSourceIconIds.value = keys.map(Number);
|
||||
},
|
||||
}));
|
||||
|
||||
const rules: Record<string, Rule[]> = {
|
||||
title: [{ required: true, message: '请输入图标名称', trigger: 'blur' }],
|
||||
icon_url: [{ required: true, message: '请上传图标', trigger: 'change' }],
|
||||
target_type: [{ required: true, message: '请选择类型', trigger: 'change' }],
|
||||
};
|
||||
|
||||
function resetForm() {
|
||||
formState.id = undefined;
|
||||
formState.title = '';
|
||||
formState.icon_url = '';
|
||||
formState.target_type = 'link';
|
||||
formState.target_url = '';
|
||||
formState.product_id = undefined;
|
||||
formState.sort_order = 0;
|
||||
formState.is_active = true;
|
||||
iconFileList.value = [];
|
||||
formRef.value?.clearValidate?.();
|
||||
}
|
||||
|
||||
function getFileName(url: string) {
|
||||
try {
|
||||
return new URL(url).pathname.split('/').filter(Boolean).pop() || 'icon';
|
||||
} catch {
|
||||
return url.split('/').filter(Boolean).pop() || 'icon';
|
||||
}
|
||||
}
|
||||
|
||||
function createUploadFile(url: string): UploadFile {
|
||||
return {
|
||||
uid: `icon-${url}`,
|
||||
name: getFileName(url),
|
||||
status: 'done',
|
||||
url,
|
||||
};
|
||||
}
|
||||
|
||||
function syncIconFileList() {
|
||||
iconFileList.value = formState.icon_url ? [createUploadFile(formState.icon_url)] : [];
|
||||
}
|
||||
|
||||
function getRequestFile(option: UploadRequestOption) {
|
||||
return option.file instanceof Blob ? option.file : null;
|
||||
}
|
||||
|
||||
async function handleIconUpload(option: UploadRequestOption) {
|
||||
const file = getRequestFile(option);
|
||||
if (!file) {
|
||||
message.error('请选择图片文件');
|
||||
option.onError?.(new Error('请选择图片文件'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await uploadImage(file, 'feature-icons');
|
||||
formState.icon_url = result.url;
|
||||
const response = result as UploadResponse;
|
||||
iconFileList.value = [
|
||||
{
|
||||
uid: String((option.file as Blob & { uid?: string }).uid || Date.now()),
|
||||
name: getFileName(response.url),
|
||||
status: 'done',
|
||||
url: response.url,
|
||||
},
|
||||
];
|
||||
option.onSuccess?.(result);
|
||||
} catch (error) {
|
||||
const uploadError = error instanceof Error ? error : new Error('上传失败');
|
||||
message.error(uploadError.message);
|
||||
option.onError?.(uploadError);
|
||||
}
|
||||
}
|
||||
|
||||
function removeIconImage() {
|
||||
formState.icon_url = '';
|
||||
iconFileList.value = [];
|
||||
return true;
|
||||
}
|
||||
|
||||
function getTargetText(icon: FeatureIcon) {
|
||||
if (icon.target_type === 'app') {
|
||||
return icon.product_name || '未选择商品';
|
||||
}
|
||||
return icon.target_url || '-';
|
||||
}
|
||||
|
||||
async function fetchBaseOptions() {
|
||||
const [productRows, templeRows] = await Promise.all([listProducts(), listTemples()]);
|
||||
products.value = productRows;
|
||||
temples.value = templeRows;
|
||||
}
|
||||
|
||||
async function fetchIcons() {
|
||||
loading.value = true;
|
||||
try {
|
||||
icons.value = await listFeatureIcons();
|
||||
await fetchBaseOptions();
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '加载图标失败,请先选择寺院');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateModal() {
|
||||
resetForm();
|
||||
modalOpen.value = true;
|
||||
}
|
||||
|
||||
function openEditModal(icon: FeatureIcon) {
|
||||
formState.id = icon.id;
|
||||
formState.title = icon.title;
|
||||
formState.icon_url = icon.icon_url;
|
||||
formState.target_type = icon.target_type;
|
||||
formState.target_url = icon.target_url || '';
|
||||
formState.product_id = icon.product_id || undefined;
|
||||
formState.sort_order = icon.sort_order;
|
||||
formState.is_active = icon.is_active;
|
||||
syncIconFileList();
|
||||
modalOpen.value = true;
|
||||
}
|
||||
|
||||
function buildPayload(): FeatureIconPayload {
|
||||
const payload: FeatureIconPayload = {
|
||||
icon_url: formState.icon_url,
|
||||
is_active: formState.is_active,
|
||||
sort_order: formState.sort_order,
|
||||
target_type: formState.target_type,
|
||||
title: formState.title,
|
||||
};
|
||||
if (formState.target_type === 'link') {
|
||||
payload.target_url = formState.target_url.trim();
|
||||
payload.product_id = null;
|
||||
} else {
|
||||
payload.target_url = null;
|
||||
payload.product_id = formState.product_id || null;
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
await formRef.value?.validate?.();
|
||||
if (formState.target_type === 'link' && !formState.target_url.trim()) {
|
||||
message.error('请输入链接地址');
|
||||
return;
|
||||
}
|
||||
if (formState.target_type === 'app' && !formState.product_id) {
|
||||
message.error('请选择商品');
|
||||
return;
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
if (isEditing.value && formState.id !== undefined) {
|
||||
await updateFeatureIcon(formState.id, buildPayload());
|
||||
message.success('图标已更新');
|
||||
} else {
|
||||
await createFeatureIcon(buildPayload());
|
||||
message.success('图标已添加');
|
||||
}
|
||||
modalOpen.value = false;
|
||||
resetForm();
|
||||
await fetchIcons();
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '保存图标失败');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDelete(icon: FeatureIcon) {
|
||||
Modal.confirm({
|
||||
title: '删除图标',
|
||||
content: `确认删除图标 ${icon.title}?`,
|
||||
okText: '删除',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
async onOk() {
|
||||
await deleteFeatureIcon(icon.id);
|
||||
message.success('图标已删除');
|
||||
await fetchIcons();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function openSyncModal() {
|
||||
syncModalOpen.value = true;
|
||||
selectedSourceIconIds.value = [];
|
||||
sourceIcons.value = [];
|
||||
await fetchBaseOptions();
|
||||
if (!syncState.source_temple_id && templeOptions.value.length > 0) {
|
||||
syncState.source_temple_id = templeOptions.value[0].value;
|
||||
}
|
||||
await fetchSourceIcons();
|
||||
}
|
||||
|
||||
async function fetchSourceIcons() {
|
||||
if (!syncState.source_temple_id) {
|
||||
sourceIcons.value = [];
|
||||
return;
|
||||
}
|
||||
sourceLoading.value = true;
|
||||
try {
|
||||
sourceIcons.value = await listSourceFeatureIcons(syncState.source_temple_id);
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '加载来源图标失败');
|
||||
} finally {
|
||||
sourceLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitSync() {
|
||||
if (!syncState.source_temple_id) {
|
||||
message.error('请选择来源寺院');
|
||||
return;
|
||||
}
|
||||
if (selectedSourceIconIds.value.length === 0) {
|
||||
message.error('请选择要同步的图标');
|
||||
return;
|
||||
}
|
||||
syncLoading.value = true;
|
||||
try {
|
||||
await copyFeatureIcons(syncState.source_temple_id, selectedSourceIconIds.value);
|
||||
message.success('图标已同步到当前寺院');
|
||||
syncModalOpen.value = false;
|
||||
selectedSourceIconIds.value = [];
|
||||
await fetchIcons();
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '同步图标失败');
|
||||
} finally {
|
||||
syncLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => formState.target_type,
|
||||
(targetType) => {
|
||||
if (targetType === 'link') {
|
||||
formState.product_id = undefined;
|
||||
} else {
|
||||
formState.target_url = '';
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(fetchIcons);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="workspace">
|
||||
<div class="page-heading">
|
||||
<h1>图标管理</h1>
|
||||
<a-space>
|
||||
<a-button :loading="loading" @click="fetchIcons">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
刷新
|
||||
</a-button>
|
||||
<a-button @click="openSyncModal">
|
||||
<template #icon><CopyOutlined /></template>
|
||||
同步其他寺院图标
|
||||
</a-button>
|
||||
<a-button type="primary" @click="openCreateModal">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
添加图标
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="icons"
|
||||
:loading="loading"
|
||||
:pagination="{ pageSize: 8, showSizeChanger: false }"
|
||||
:scroll="{ x: 940 }"
|
||||
bordered
|
||||
row-key="id"
|
||||
size="middle"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'icon_url'">
|
||||
<a-image :src="record.icon_url" :width="48" :height="48" class="square-preview" />
|
||||
</template>
|
||||
<template v-if="column.key === 'target'">
|
||||
{{ getTargetText(record) }}
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'target_type'">
|
||||
<a-tag :color="record.target_type === 'link' ? 'blue' : 'purple'">
|
||||
{{ record.target_type === 'link' ? '链接' : '应用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'is_active'">
|
||||
<a-tag :color="record.is_active ? 'green' : 'default'">
|
||||
{{ record.is_active ? '启用' : '停用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'actions'">
|
||||
<a-space>
|
||||
<a-tooltip title="编辑">
|
||||
<a-button type="text" @click="openEditModal(record)">
|
||||
<template #icon><EditOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="删除">
|
||||
<a-button type="text" danger @click="confirmDelete(record)">
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</section>
|
||||
|
||||
<a-modal
|
||||
v-model:open="modalOpen"
|
||||
:confirm-loading="saving"
|
||||
:title="modalTitle"
|
||||
ok-text="保存"
|
||||
cancel-text="取消"
|
||||
@ok="submitForm"
|
||||
>
|
||||
<a-form ref="formRef" :model="formState" :rules="rules" layout="vertical">
|
||||
<a-form-item label="图标名称" name="title">
|
||||
<a-input v-model:value="formState.title" placeholder="功德商城" />
|
||||
</a-form-item>
|
||||
<a-form-item label="图标" name="icon_url" extra="建议图标宽高 120px * 120px">
|
||||
<a-upload
|
||||
v-model:file-list="iconFileList"
|
||||
accept="image/*"
|
||||
list-type="picture-card"
|
||||
:custom-request="handleIconUpload"
|
||||
:max-count="1"
|
||||
@remove="removeIconImage"
|
||||
>
|
||||
<div v-if="iconFileList.length < 1">
|
||||
<PlusOutlined />
|
||||
<div class="upload-text">上传图标</div>
|
||||
</div>
|
||||
</a-upload>
|
||||
</a-form-item>
|
||||
<a-form-item label="类型" name="target_type">
|
||||
<a-segmented
|
||||
v-model:value="formState.target_type"
|
||||
:options="[
|
||||
{ label: '链接', value: 'link' },
|
||||
{ label: '应用', value: 'app' },
|
||||
]"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="formState.target_type === 'link'" label="链接地址" name="target_url">
|
||||
<a-input v-model:value="formState.target_url" placeholder="https://example.com 或 /pages/detail" />
|
||||
</a-form-item>
|
||||
<a-form-item v-else label="选择商品" name="product_id">
|
||||
<a-select
|
||||
v-model:value="formState.product_id"
|
||||
:options="productOptions"
|
||||
placeholder="请选择当前寺院商品"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="排序" name="sort_order">
|
||||
<a-input-number v-model:value="formState.sort_order" :min="0" class="full-width-control" />
|
||||
</a-form-item>
|
||||
<a-form-item label="是否启用" name="is_active">
|
||||
<a-switch v-model:checked="formState.is_active" checked-children="启用" un-checked-children="停用" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<a-modal
|
||||
v-model:open="syncModalOpen"
|
||||
:confirm-loading="syncLoading"
|
||||
title="同步其他寺院图标"
|
||||
width="760px"
|
||||
ok-text="复制到当前寺院"
|
||||
cancel-text="取消"
|
||||
@ok="submitSync"
|
||||
>
|
||||
<a-space direction="vertical" class="full-width-control" size="middle">
|
||||
<a-select
|
||||
v-model:value="syncState.source_temple_id"
|
||||
:options="templeOptions"
|
||||
class="full-width-control"
|
||||
placeholder="选择来源寺院"
|
||||
@change="fetchSourceIcons"
|
||||
/>
|
||||
<a-table
|
||||
:columns="sourceColumns"
|
||||
:data-source="sourceIcons"
|
||||
:loading="sourceLoading"
|
||||
:pagination="{ pageSize: 5, showSizeChanger: false }"
|
||||
:row-selection="sourceRowSelection"
|
||||
bordered
|
||||
row-key="id"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'icon_url'">
|
||||
<a-image :src="record.icon_url" :width="40" :height="40" class="square-preview" />
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'target_type'">
|
||||
{{ record.target_type === 'link' ? '链接' : '应用' }}
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'is_active'">
|
||||
<a-tag :color="record.is_active ? 'green' : 'default'">
|
||||
{{ record.is_active ? '启用' : '停用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-space>
|
||||
</a-modal>
|
||||
</template>
|
||||
Reference in New Issue
Block a user