- AttachmentManage: orphanCount→unusedCount(零残留),用途列(文章/轮播/友链/站点配置/未使用) - BannerManage/LinkManage 上传 action 传 scene;ArticleList 删除提示文案同步 - types AttachmentVo 增 refType;vitest 24/24+build 绿
279 lines
9.1 KiB
Vue
279 lines
9.1 KiB
Vue
<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>
|