Files
YschcmsFront/src/views/cms/BannerManage.vue
T
chenxing bcd639cfc2 feat(B2): 附件管理改"未使用"语义+用途列;banner/link 上传带 scene
- AttachmentManage: orphanCount→unusedCount(零残留),用途列(文章/轮播/友链/站点配置/未使用)
- BannerManage/LinkManage 上传 action 传 scene;ArticleList 删除提示文案同步
- types AttachmentVo 增 refType;vitest 24/24+build 绿
2026-09-08 15:14:07 +08:00

279 lines
9.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { onMounted, reactive, ref, watch } from "vue";
import { Message } from "view-ui-plus";
import Utils from "@/utils/index";
import MyConfirm from "@/components/MyConfirm";
import { bannerApi, uploadUrl } from "@/api/module/cms";
import { useCmsStore } from "@/store";
import { BannerVo } from "@/types/cms";
const store = useCmsStore();
const rows = ref<BannerVo[]>([]);
const loading = ref(false);
const tableHeight = Utils.useTableHeight({ isSearch: true, isOperat: true });
const linkTypeDict = [
{ value: 1, label: "文章" },
{ value: 2, label: "栏目" },
{ value: 3, label: "外链" },
];
const linkTypeText = (v: number) => linkTypeDict.find(i => i.value === v)?.label || "-";
const columns = [
{
title: "图片",
key: "imageUrl",
width: 220,
render: (h: any, p: any) =>
h("img", { src: p.row.imageUrl, style: { maxHeight: "60px", maxWidth: "200px" } }),
},
{ title: "标题", key: "title", minWidth: 200, tooltip: true },
{
title: "跳转",
key: "linkType",
width: 110,
align: "center",
render: (h: any, p: any) => h("span", linkTypeText(p.row.linkType)),
},
{ title: "跳转目标", key: "linkValue", width: 180, tooltip: true },
{ title: "排序", key: "sortNo", width: 80, align: "center" },
{
title: "上线时间",
key: "startTime",
width: 160,
render: (h: any, p: any) => h("span", p.row.startTime || "立即"),
},
{
title: "下线时间",
key: "endTime",
width: 160,
render: (h: any, p: any) => h("span", p.row.endTime || "不限"),
},
{
title: "状态",
key: "status",
width: 90,
align: "center",
render: (h: any, p: any) => h(Tag, { color: p.row.status === 1 ? "success" : "default" }, () => (p.row.status === 1 ? "启用" : "停用")),
},
{
title: "操作",
key: "action",
width: 120,
fixed: "right",
align: "center",
render: (h: any, p: any) => [
Utils.operatingHandle(h, "编辑", "icon-edit", () => openEdit(p.row)),
Utils.operatingHandle(h, "删除", "icon-ashbin", () => doDelete(p.row)),
],
},
];
const modalVisible = ref(false);
const saving = ref(false);
const formRef = ref();
const form = reactive({
id: undefined as number | undefined,
title: "",
imageUrl: "",
linkType: 3,
linkValue: "",
sortNo: 0,
startTime: "" as string | "",
endTime: "" as string | "",
status: 1,
});
const rules = {
title: [{ required: true, message: "标题不得为空", trigger: "blur" }],
linkType: [{ required: true, type: "number", message: "请选择跳转类型", trigger: "change" }],
};
async function run() {
if (!store.siteId) return;
loading.value = true;
try {
rows.value = (await bannerApi.list(store.siteId, "home").send()) || [];
} finally {
loading.value = false;
}
}
function openCreate() {
Object.assign(form, {
id: undefined,
title: "",
imageUrl: "",
linkType: 3,
linkValue: "",
sortNo: 0,
startTime: "",
endTime: "",
status: 1,
});
modalVisible.value = true;
}
function openEdit(row: BannerVo) {
Object.assign(form, {
id: row.id,
title: row.title,
imageUrl: row.imageUrl,
linkType: row.linkType,
linkValue: row.linkValue || "",
sortNo: row.sortNo,
startTime: row.startTime || "",
endTime: row.endTime || "",
status: row.status,
});
modalVisible.value = true;
}
function bannerUploaded(res: any) {
if (res.code === 0 && res.data) {
form.imageUrl = res.data.url;
} else {
Message.error(res.msg || "图片上传失败");
}
}
async function save() {
const valid = await formRef.value?.validate();
if (!valid) return;
if (!form.imageUrl) {
Message.warning("请上传轮播图片");
return;
}
saving.value = true;
try {
await bannerApi
.save({
id: form.id,
siteId: store.siteId!,
position: "home",
title: form.title,
imageUrl: form.imageUrl,
linkType: form.linkType,
linkValue: form.linkValue || undefined,
sortNo: form.sortNo,
startTime: form.startTime || undefined,
endTime: form.endTime || undefined,
status: form.status,
})
.send();
Message.success("保存成功!");
modalVisible.value = false;
run();
} finally {
saving.value = false;
}
}
function doDelete(row: BannerVo) {
MyConfirm({
title: "删除轮播",
msg: `确定删除「${row.title}」吗?`,
async onOk() {
await bannerApi.delete({ id: row.id!, siteId: store.siteId! }).send();
Message.success("操作成功!");
run();
},
});
}
onMounted(run);
// 站点上下文切换即时重载(docs/09 §3.3)
watch(() => store.siteId, () => run());
</script>
<template>
<div class="page-wrapper">
<div class="main-container">
<div class="fix">
<Button class="fl" type="primary" size="large" @click="openCreate">
<Icon type="ios-add" />
新增轮播
</Button>
<span class="size-tip fr">轮播图设计尺寸建议 1920×560(皮肤包约定)</span>
</div>
<Table :columns="columns" :data="rows" :height="tableHeight" :loading="loading" no-data-text="暂无轮播" />
<Modal v-model="modalVisible" :title="form.id ? '编辑轮播' : '新增轮播'" :mask-closable="false" width="560px">
<Form ref="formRef" :model="form" :rules="rules" :label-width="100">
<FormItem label="标题" prop="title">
<Input v-model="form.title" :maxlength="200" placeholder="alt 文案/无图兜底" />
</FormItem>
<FormItem label="轮播图片" required>
<Upload
:action="uploadUrl() + '?siteId=' + store.siteId + '&scene=banner'"
with-credentials
:show-upload-list="false"
:format="['jpg', 'jpeg', 'png', 'gif', 'webp']"
:on-success="bannerUploaded"
>
<Button icon="ios-cloud-upload-outline">上传图片</Button>
</Upload>
<img v-if="form.imageUrl" :src="form.imageUrl" class="img-preview" />
</FormItem>
<FormItem label="跳转类型" prop="linkType">
<RadioGroup v-model="form.linkType">
<Radio v-for="t in linkTypeDict" :key="t.value" :label="t.value">{{ t.label }}</Radio>
</RadioGroup>
</FormItem>
<FormItem
v-if="form.linkType === 3"
label="外链地址"
prop="linkValue"
:rules="[{ required: true, message: '外链地址不得为空', trigger: 'blur' }]"
>
<Input v-model="form.linkValue" :maxlength="500" placeholder="http(s):// 开头" />
</FormItem>
<FormItem
v-else
label="跳转目标"
prop="linkValue"
:rules="[{ required: true, message: '请填写目标 ID', trigger: 'blur' }]"
>
<Input v-model="form.linkValue" :maxlength="64" :placeholder="form.linkType === 1 ? '文章 ID' : '栏目 ID'" />
</FormItem>
<FormItem label="排序">
<InputNumber v-model="form.sortNo" :min="0" />
</FormItem>
<FormItem label="上线时间">
<DatePicker type="datetime" v-model="form.startTime" format="yyyy-MM-dd HH:mm:ss" placeholder="空=立即上线" />
</FormItem>
<FormItem label="下线时间">
<DatePicker type="datetime" v-model="form.endTime" format="yyyy-MM-dd HH:mm:ss" placeholder="空=不自动下线" />
</FormItem>
<FormItem label="状态">
<RadioGroup v-model="form.status">
<Radio :label="1">启用</Radio>
<Radio :label="0">停用</Radio>
</RadioGroup>
</FormItem>
</Form>
<template #footer>
<Button @click="modalVisible = false">取消</Button>
<Button type="primary" :loading="saving" @click="save">保存</Button>
</template>
</Modal>
</div>
</div>
</template>
<style scoped lang="scss">
.size-tip {
color: #999;
line-height: 40px;
}
.img-preview {
display: block;
max-width: 320px;
max-height: 120px;
margin-top: 8px;
border: 1px solid #e5e6eb;
}
</style>