Files
YschcmsFront/src/views/cms/LinkManage.vue
T
chenxing e494a2d2d8 feat(B4): 友链分组改下拉选已有+允许新建(Select allow-create filterable)
- options=数据聚合的既有分组;仍落 groupName 字符串,后端零改动
- 拼错旧组名不再产生新脏分组;vitest 25/25 绿+build 成功
2026-09-08 15:14:08 +08:00

252 lines
8.0 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 { computed, onMounted, reactive, ref, watch } from "vue";
import { Message } from "view-ui-plus";
import Utils from "@/utils/index";
import MyConfirm from "@/components/MyConfirm";
import { linkApi, uploadUrl } from "@/api/module/cms";
import { useCmsStore } from "@/store";
import { LinkVo } from "@/types/cms";
const store = useCmsStore();
const rows = ref<LinkVo[]>([]);
const loading = ref(false);
const tableHeight = Utils.useTableHeight({ isOperat: true });
const columns = [
{ title: "分组", key: "groupName", width: 140 },
{
title: "Logo",
key: "logoUrl",
width: 90,
align: "center",
render: (h: any, p: any) =>
p.row.logoUrl
? h("img", { src: p.row.logoUrl, style: { maxHeight: "36px", maxWidth: "72px" } })
: h("span", { style: { color: "#999" } }, "-"),
},
{ title: "名称", key: "name", minWidth: 200, tooltip: true },
{ title: "链接地址", key: "linkUrl", minWidth: 260, tooltip: true },
{ title: "排序", key: "sortNo", width: 80, align: "center" },
{
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,
groupName: "default",
name: "",
logoUrl: "",
linkUrl: "",
sortNo: 0,
status: 1,
});
const rules = {
name: [{ required: true, message: "链接名称不得为空", trigger: "blur" }],
linkUrl: [{ required: true, message: "链接地址不得为空", trigger: "blur" }],
};
/** 分组 Tab(从数据聚合) */
const groups = computed(() => {
const set = new Set<string>();
rows.value.forEach(r => set.add(r.groupName));
return Array.from(set);
});
const activeGroup = ref<string>("");
const visibleRows = computed(() =>
activeGroup.value ? rows.value.filter(r => r.groupName === activeGroup.value) : rows.value
);
async function run() {
if (!store.siteId) return;
loading.value = true;
try {
rows.value = (await linkApi.list(store.siteId).send()) || [];
} finally {
loading.value = false;
}
}
function openCreate() {
Object.assign(form, {
id: undefined,
groupName: activeGroup.value || "default",
name: "",
logoUrl: "",
linkUrl: "",
sortNo: 0,
status: 1,
});
modalVisible.value = true;
}
function openEdit(row: LinkVo) {
Object.assign(form, {
id: row.id,
groupName: row.groupName,
name: row.name,
logoUrl: row.logoUrl || "",
linkUrl: row.linkUrl,
sortNo: row.sortNo,
status: row.status,
});
modalVisible.value = true;
}
function logoUploaded(res: any) {
if (res.code === 0 && res.data) {
form.logoUrl = res.data.url;
} else {
Message.error(res.msg || "Logo 上传失败");
}
}
async function save() {
const valid = await formRef.value?.validate();
if (!valid) return;
saving.value = true;
try {
await linkApi
.save({
id: form.id,
siteId: store.siteId!,
groupName: form.groupName || "default",
name: form.name,
logoUrl: form.logoUrl || undefined,
linkUrl: form.linkUrl,
sortNo: form.sortNo,
status: form.status,
})
.send();
Message.success("保存成功!");
modalVisible.value = false;
run();
} finally {
saving.value = false;
}
}
function doDelete(row: LinkVo) {
MyConfirm({
title: "删除友链",
msg: `确定删除「${row.name}」吗?`,
async onOk() {
await linkApi.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>
<Tabs v-if="groups.length" class="fr" :value="activeGroup" @on-click="(g: string) => (activeGroup = g)">
<TabPane v-for="g in groups" :key="g" :name="g" :label="`${g}(${rows.filter(r => r.groupName === g).length})`" />
</Tabs>
</div>
<Table
:columns="columns"
:data="visibleRows"
:height="tableHeight"
:loading="loading"
no-data-text="暂无友链"
/>
<Modal v-model="modalVisible" :title="form.id ? '编辑友链' : '新增友链'" :mask-closable="false" width="520px">
<Form ref="formRef" :model="form" :rules="rules" :label-width="100">
<FormItem label="分组">
<!-- B4:下拉选已有分组 + 允许新建(仍落 groupName 字符串,后端零改动);杜绝拼错旧组名产生脏分组 -->
<Select
v-model="form.groupName"
filterable
allow-create
:maxlength="64"
placeholder="选择已有分组,或输入新名称回车新建"
style="width: 320px"
>
<Option v-for="g in groups" :key="g" :value="g">{{ g }}</Option>
</Select>
</FormItem>
<FormItem label="名称" prop="name">
<Input v-model="form.name" :maxlength="128" />
</FormItem>
<FormItem label="Logo(可选)">
<Upload
:action="uploadUrl() + '?siteId=' + store.siteId + '&scene=link'"
with-credentials
:show-upload-list="false"
:format="['jpg', 'jpeg', 'png', 'gif', 'webp']"
:on-success="logoUploaded"
>
<Button icon="ios-cloud-upload-outline">上传 Logo</Button>
</Upload>
<img v-if="form.logoUrl" :src="form.logoUrl" class="img-preview" />
</FormItem>
<FormItem label="链接地址" prop="linkUrl">
<Input v-model="form.linkUrl" :maxlength="500" placeholder="http(s):// 开头" />
</FormItem>
<FormItem label="排序">
<InputNumber v-model="form.sortNo" :min="0" />
</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">
.img-preview {
display: block;
max-width: 200px;
max-height: 64px;
margin-top: 8px;
border: 1px solid #e5e6eb;
}
</style>