feat(cms): docs/16 站点设置整页化+首页布局编辑器- SiteSetting.vue整页7分区(基本信息/皮肤与外观/首页布局/基础展示/页脚信息/SEO与域名/扩展参数)锚点导航sticky+IntersectionObserver+分区卡片三段式每分区独立保存(configJson单一事实源+submitSection统一提交);创建模式仅①②可编辑其余锁定,创建成功原地replace到site/setting/:id;SiteWizard三步向导退役删除(门户预览收编portalPreviewUrl);SiteManage瘦身表格+行内编辑跳整页(adminPathOf子站带站点段);ImageAssetField图片双通道(上传+路径粘贴折叠);SiteConfigForm支持groups/showExtras/siteId+asset字段接ImageAssetField+三旧键表单不再展示(ABSORBED_LEGACY_KEYS静默保留);homeLayout三件套(homeLayout.ts模型parse/serialize/validate/genFromCategories一键生成+blockTypes.ts注册表showIf条件字段+HomeLayoutEditor.vue序列编辑增删显隐上移下移);routes site/new+site/setting/:id+legacy redirect;CategoryManage home_style提示改口指向首页布局;新增siteSetting.spec+homeLayout.spec,重写siteManage.spec,117用例全绿
build / build (push) Successful in 2m19s

This commit is contained in:
chenxing
2026-09-10 15:09:10 +08:00
parent 7c925cd4f2
commit 34733ec280
16 changed files with 2399 additions and 840 deletions
+152
View File
@@ -0,0 +1,152 @@
<script setup lang="ts">
import { computed, ref } from "vue";
import { Message } from "view-ui-plus";
import { assetUrl, uploadUrl } from "@/api/module/cms";
/**
* 图片类配置字段控件(docs/16 D7):上传 + 手动粘贴路径双通道。
* v-model 绑定资产路径(/ 开头相对路径或 http(s) URL);上传成功自动回填(服务端返回相对 url)。
* 缩略预览 + 清空;格式校验由外层 FormItem 规则(ASSET_RE)承担,控件只负责取值。
*/
const props = defineProps<{
modelValue: string;
/** 上传归属站点(附件场景引用;缺省不携带) */
siteId?: number;
placeholder?: string;
}>();
const emit = defineEmits<{ (e: "update:modelValue", v: string): void }>();
const manualOpen = ref(false);
const manualPath = ref("");
const uploading = ref(false);
const previewSrc = computed(() => (props.modelValue ? assetUrl(props.modelValue) : ""));
const action = computed(() => {
const base = uploadUrl();
return props.siteId ? `${base}?siteId=${props.siteId}` : base;
});
function onUploaded(res: any) {
uploading.value = false;
if (res && res.code === 0 && res.data && res.data.url) {
emit("update:modelValue", res.data.url);
manualOpen.value = false;
} else {
Message.error(res?.msg || "图片上传失败");
}
}
function onUploadError() {
uploading.value = false;
Message.error("图片上传失败");
}
function confirmManual() {
const v = manualPath.value.trim();
if (!v) {
Message.warning("请先粘贴图片路径");
return;
}
emit("update:modelValue", v);
manualPath.value = "";
manualOpen.value = false;
}
function clearValue() {
emit("update:modelValue", "");
}
</script>
<template>
<div class="image-asset-field">
<div class="iaf-row">
<Upload
v-if="!modelValue"
:action="action"
:show-upload-list="false"
:on-success="onUploaded"
:on-error="onUploadError"
accept="image/*"
>
<Button size="small" icon="ios-cloud-upload-outline" :loading="uploading">上传图片</Button>
</Upload>
<span v-else class="iaf-thumb">
<img :src="previewSrc" alt="" />
<span class="iaf-thumb-del" title="清除" @click="clearValue">
<Icon type="ios-close" />
</span>
</span>
<Button size="small" type="text" class="iaf-toggle" @click="manualOpen = !manualOpen">
{{ manualOpen ? "收起" : "手动粘贴路径" }}
</Button>
</div>
<Input
v-if="manualOpen"
v-model="manualPath"
size="small"
class="iaf-manual"
:placeholder="placeholder || '粘贴已上传图片的路径,如 /yschcms/upload/…/logo.png'"
>
<Button slot="append" size="small" type="primary" @click="confirmManual">填入</Button>
</Input>
<p v-else-if="modelValue" class="iaf-path">{{ modelValue }}</p>
</div>
</template>
<style scoped lang="scss">
.image-asset-field {
.iaf-row {
display: flex;
align-items: center;
gap: 8px;
}
.iaf-toggle {
color: #999;
&:hover {
color: #2d8cf0;
}
}
.iaf-thumb {
position: relative;
display: inline-flex;
img {
width: 64px;
height: 40px;
object-fit: cover;
border-radius: 4px;
border: 1px solid #dcdee2;
background: #f8f8f9;
}
.iaf-thumb-del {
position: absolute;
top: -7px;
right: -7px;
width: 16px;
height: 16px;
line-height: 14px;
text-align: center;
border-radius: 50%;
background: #ed4014;
color: #fff;
cursor: pointer;
font-size: 12px;
display: none;
}
&:hover .iaf-thumb-del {
display: block;
}
}
.iaf-manual {
max-width: 420px;
margin-top: 6px;
}
.iaf-path {
color: #999;
font-size: 12px;
margin-top: 2px;
word-break: break-all;
line-height: 1.5;
}
}
</style>