change
This commit is contained in:
@@ -0,0 +1,381 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<style>
|
||||
.spread-editor-page { width: 100%; }
|
||||
.spread-editor-page .form-card { width: 100%; max-width: none; margin: 0; box-sizing: border-box; }
|
||||
.spread-editor-page .form-tip { color: #909399; font-size: 12px; line-height: 20px; }
|
||||
.spread-editor-page .footer-actions { padding: 18px 0 6px; text-align: center; }
|
||||
.spread-editor-page .slider-upload-head { width: 100%; margin-bottom: 10px; display: flex; align-items: center; justify-content: space-between; }
|
||||
.spread-editor-page .slider-upload-title { color: #303133; font-weight: 600; }
|
||||
.spread-editor-page .slider-upload-count { color: #909399; font-size: 13px; font-weight: 400; }
|
||||
.spread-editor-page .slider-meta-list { width: 100%; margin-top: 14px; }
|
||||
.spread-editor-page .slider-meta-toolbar { margin-bottom: 10px; display: flex; align-items: center; justify-content: space-between; }
|
||||
.spread-editor-page .slider-meta-item { margin-bottom: 12px; padding: 14px; display: grid; grid-template-columns: 130px minmax(220px, 1fr) 92px; gap: 14px; align-items: center; border: 1px solid #e4e7ed; border-radius: 8px; background: #fafafa; transition: border-color .2s, box-shadow .2s; }
|
||||
.spread-editor-page .slider-meta-item:hover { border-color: #c6e2ff; box-shadow: 0 5px 16px rgba(64,158,255,.10); }
|
||||
.spread-editor-page .slider-preview-wrap { position: relative; }
|
||||
.spread-editor-page .slider-index { min-width: 26px; height: 26px; padding: 0 7px; position: absolute; top: 6px; left: 6px; z-index: 2; box-sizing: border-box; color: #fff; font-size: 12px; line-height: 26px; text-align: center; border-radius: 13px; background: rgba(0,0,0,.58); }
|
||||
.spread-editor-page .slider-meta-image { width: 130px; height: 82px; display: block; object-fit: cover; border: 4px solid #fff; border-radius: 5px; box-sizing: border-box; background: #fff; box-shadow: 0 2px 8px rgba(0,0,0,.12); }
|
||||
.spread-editor-page .slider-field-label { margin-bottom: 7px; display: block; color: #606266; font-size: 13px; }
|
||||
.spread-editor-page .color-danger { color: #f56c6c; }
|
||||
.spread-editor-page .slider-item-actions { display: flex; flex-direction: column; align-items: stretch; }
|
||||
.spread-editor-page .slider-item-actions .el-button + .el-button { margin-top: 6px; margin-left: 0; }
|
||||
@media(max-width: 1050px) {
|
||||
.spread-editor-page .slider-meta-item { grid-template-columns: 110px minmax(0, 1fr) 92px; }
|
||||
.spread-editor-page .slider-meta-image { width: 110px; }
|
||||
.spread-editor-page .slider-item-actions { grid-column: 1 / -1; flex-direction: row; justify-content: flex-end; }
|
||||
.spread-editor-page .slider-item-actions .el-button + .el-button { margin-top: 0; margin-left: 8px; }
|
||||
}
|
||||
@media(max-width: 680px) {
|
||||
.spread-editor-page .slider-meta-item { grid-template-columns: 1fr; }
|
||||
.spread-editor-page .slider-meta-image { width: 100%; height: 180px; }
|
||||
.spread-editor-page .slider-item-actions { grid-column: auto; }
|
||||
}
|
||||
</style>
|
||||
<div id="app" class="spread-editor-page" v-cloak>
|
||||
<el-card class="form-card" shadow="never" v-loading="loading">
|
||||
<div slot="header">
|
||||
<span style="font-size: 17px;font-weight: 600">{{formData.id ? '编辑风采内容' : '发布风采内容'}}</span>
|
||||
</div>
|
||||
<el-form ref="contentForm" :model="formData" :rules="rules" label-width="105px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="所属栏目" prop="categoryId">
|
||||
<el-cascader v-model="formData.categoryId" :options="categoryOptions"
|
||||
:props="{value:'id', label:'name', children:'children', emitPath:false, checkStrictly:false}"
|
||||
filterable clearable style="width: 100%" placeholder="请选择末级栏目"></el-cascader>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="所属工会" prop="unionId">
|
||||
<el-select v-model="formData.unionId" filterable clearable style="width: 100%"
|
||||
placeholder="请选择所属工会" @change="contentUnionChanged">
|
||||
<el-option v-for="item in unionOptions" :key="item.id"
|
||||
:label="item.name" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="排序" prop="sortOrder">
|
||||
<el-input-number v-model="formData.sortOrder" :min="0" :max="9999" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="标题" prop="title">
|
||||
<el-input v-model.trim="formData.title" maxlength="300" show-word-limit></el-input>
|
||||
</el-form-item>
|
||||
<!-- 副标题当前无实际业务用途,暂不开放录入。
|
||||
<el-form-item label="副标题">
|
||||
<el-input v-model.trim="formData.subTitle" maxlength="255" show-word-limit></el-input>
|
||||
</el-form-item>
|
||||
-->
|
||||
|
||||
<el-form-item label="摘要" prop="summary">
|
||||
<el-input v-model="formData.summary" type="textarea" :rows="3" maxlength="1000" show-word-limit
|
||||
placeholder="用于风采墙列表展示"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="封面图" prop="thumbUrl">
|
||||
<file-upload :upload_number="1" :upload_size="10 * 1024 * 1024"
|
||||
:value.sync="formData.thumbUrl" accept=".jpg,.jpeg,.png,.webp"
|
||||
upload_mode="image" upload_result_category="interval"></file-upload>
|
||||
<div class="form-tip">建议使用横版图片,单张不超过 10MB。</div>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="sliderEnabled" label="轮播图片">
|
||||
<div class="slider-upload-head">
|
||||
<span class="slider-upload-title">点击下方“+”新增轮播图片</span>
|
||||
<span class="slider-upload-count">已添加 {{sliderMeta.length}} / 10 张</span>
|
||||
</div>
|
||||
<file-upload :upload_number="10" :upload_size="10 * 1024 * 1024"
|
||||
:value.sync="sliderFiles" accept=".jpg,.jpeg,.png,.webp"
|
||||
upload_mode="image" upload_result_category="array"></file-upload>
|
||||
<div class="form-tip">仅栏目开启“参与轮播”后生效,最多上传 10 张;每张图片只需填写标题,工会名称沿用内容所属工会。</div>
|
||||
<div v-if="sliderMeta.length" class="slider-meta-list">
|
||||
<div class="slider-meta-toolbar">
|
||||
<span class="form-tip">可调整顺序,轮播将按当前顺序展示。</span>
|
||||
<el-button type="text" icon="el-icon-delete" class="color-danger" @click="clearSliderImages">
|
||||
清空全部
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-for="(img, index) in sliderMeta" :key="img.src + '-' + index" class="slider-meta-item">
|
||||
<div class="slider-preview-wrap">
|
||||
<span class="slider-index">{{index + 1}}</span>
|
||||
<img class="slider-meta-image" :src="img.src" :alt="img.title || ('轮播图' + (index + 1))">
|
||||
</div>
|
||||
<div>
|
||||
<span class="slider-field-label"><i class="color-danger">*</i> 图片标题</span>
|
||||
<el-input v-model.trim="img.title" maxlength="100" show-word-limit
|
||||
:placeholder="'请输入第' + (index + 1) + '张图片标题'"></el-input>
|
||||
</div>
|
||||
|
||||
<div class="slider-item-actions">
|
||||
<el-button size="mini" icon="el-icon-top" :disabled="index === 0"
|
||||
@click="moveSliderImage(index, -1)">上移</el-button>
|
||||
<el-button size="mini" icon="el-icon-bottom" :disabled="index === sliderMeta.length - 1"
|
||||
@click="moveSliderImage(index, 1)">下移</el-button>
|
||||
<el-button size="mini" type="danger" plain icon="el-icon-delete"
|
||||
@click="removeSliderImage(index)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="正文" prop="contentBody">
|
||||
<text-editor v-model="formData.contentBody" :height="360" placeholder="请输入风采内容"></text-editor>
|
||||
</el-form-item>
|
||||
<el-form-item label="置顶">
|
||||
<el-switch v-model="formData.isTop" :active-value="1" :inactive-value="0"
|
||||
active-text="置顶" inactive-text="不置顶"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="发布状态">
|
||||
<el-radio-group v-model="formData.isPublished">
|
||||
<el-radio :label="0">保存草稿</el-radio>
|
||||
<el-radio :label="1">立即发布</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<div class="footer-actions">
|
||||
<el-button @click="resetForm">重置</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submit">保存</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
submitting: false,
|
||||
categoryOptions: [],
|
||||
unionOptions: [],
|
||||
sliderFiles: [],
|
||||
sliderMeta: [],
|
||||
formData: {},
|
||||
rules: {
|
||||
categoryId: [{required: true, message: "请选择所属栏目", trigger: "change"}],
|
||||
unionId: [{required: true, message: "请选择所属工会", trigger: "change"}],
|
||||
title: [{required: true, message: "请输入标题", trigger: "blur"}],
|
||||
summary: [{required: true, message: "请输入摘要", trigger: "blur"}],
|
||||
contentBody: [{required: true, message: "请输入正文", trigger: "change"}]
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 仅所选末级栏目及其上级栏目都开启轮播时,显示轮播图片配置。
|
||||
sliderEnabled() {
|
||||
if (!this.formData.categoryId) return false
|
||||
const findPath = (nodes, categoryId, parents) => {
|
||||
for (const node of nodes || []) {
|
||||
const path = parents.concat(node)
|
||||
if (node.id === categoryId) return path
|
||||
const childPath = findPath(node.children, categoryId, path)
|
||||
if (childPath) return childPath
|
||||
}
|
||||
return null
|
||||
}
|
||||
const categoryPath = findPath(this.categoryOptions, this.formData.categoryId, [])
|
||||
return !!categoryPath && categoryPath.every(item => Number(item.slider) === 1)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 生成新增内容时使用的默认表单。
|
||||
defaultForm() {
|
||||
return {
|
||||
categoryId: "", unionId: "", unionName: "", title: "", subTitle: "", summary: "", contentBody: "",
|
||||
thumbUrl: "", sliderImages: "", isTop: 0,
|
||||
viewCount: 0, sortOrder: 255, isPublished: 0
|
||||
}
|
||||
},
|
||||
// 读取地址栏中的编辑内容 ID。
|
||||
getEditId() {
|
||||
return new URLSearchParams(window.location.search).get("id") || ""
|
||||
},
|
||||
// 加载栏目树,内容只能选择末级栏目。
|
||||
loadCategories() {
|
||||
return this.$axios.post(loc() + "/categoryTree").then(resp => {
|
||||
if (resp.code === 0) {
|
||||
this.categoryOptions = resp.data || []
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
})
|
||||
},
|
||||
// 按当前登录人员权限加载可选择工会。
|
||||
loadUnionOptions() {
|
||||
return this.$axios.post(loc() + "/unionOptions").then(resp => {
|
||||
if (resp.code === 0) {
|
||||
this.unionOptions = resp.data || []
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
})
|
||||
},
|
||||
// 内容所属工会变化时同步保存工会名称。
|
||||
contentUnionChanged(unionId) {
|
||||
const union = this.unionOptions.find(item => item.id === unionId)
|
||||
this.formData.unionName = union ? union.name : ""
|
||||
},
|
||||
|
||||
// 分工会人员只有一个可选工会时自动带入,校工会管理员仍需主动选择。
|
||||
applyUnionDefaults() {
|
||||
if (this.unionOptions.length !== 1) return
|
||||
const union = this.unionOptions[0]
|
||||
if (!this.formData.unionId) {
|
||||
this.formData.unionId = union.id
|
||||
this.formData.unionName = union.name
|
||||
}
|
||||
|
||||
},
|
||||
// 编辑状态下加载内容详情并还原轮播图片数组。
|
||||
loadDetail(id) {
|
||||
if (!id) return Promise.resolve()
|
||||
this.loading = true
|
||||
return this.$axios.post(loc() + "/detailData", {id}).then(resp => {
|
||||
if (resp.code === 0) {
|
||||
this.formData = Object.assign(this.defaultForm(), resp.data || {})
|
||||
this.sliderMeta = this.parseSliderImages(this.formData.sliderImages)
|
||||
this.sliderFiles = this.sliderMeta.map(item => item.src)
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
}).finally(() => this.loading = false)
|
||||
},
|
||||
// 兼容历史图片地址数组,并转换为只包含图片地址和标题的轮播对象。
|
||||
parseSliderImages(value) {
|
||||
if (!value) return []
|
||||
let items = value
|
||||
if (!Array.isArray(items)) {
|
||||
try {
|
||||
items = JSON.parse(value)
|
||||
} catch (e) {
|
||||
items = String(value).split(",").filter(Boolean)
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(items)) return []
|
||||
return items.map(item => {
|
||||
if (typeof item === "string") {
|
||||
return {src: item, title: ""}
|
||||
}
|
||||
return {
|
||||
src: item.src || item.url || item.data || (item.response && item.response.data) || "",
|
||||
title: item.title || ""
|
||||
}
|
||||
}).filter(item => item.src)
|
||||
},
|
||||
// 取得上传组件返回的图片地址。
|
||||
getSliderFileUrl(item) {
|
||||
if (typeof item === "string") return item
|
||||
return item && (item.url || item.data || (item.response && item.response.data)) || ""
|
||||
},
|
||||
// 上传或删除图片后同步图片元数据,并保留已经填写的标题和工会。
|
||||
syncSliderMeta() {
|
||||
const oldItems = {}
|
||||
this.sliderMeta.forEach(item => oldItems[item.src] = item)
|
||||
this.sliderMeta = (this.sliderFiles || []).map(item => this.getSliderFileUrl(item)).filter(Boolean).map(src => {
|
||||
return oldItems[src] || {src: src, title: ""}
|
||||
})
|
||||
this.applyUnionDefaults()
|
||||
},
|
||||
// 删除单张轮播图片,同时更新上传组件和图片配置。
|
||||
removeSliderImage(index) {
|
||||
const files = (this.sliderFiles || []).slice()
|
||||
files.splice(index, 1)
|
||||
this.sliderFiles = files
|
||||
this.$message.success("已删除第" + (index + 1) + "张轮播图片")
|
||||
},
|
||||
// 调整轮播图片顺序,保存和展示均使用调整后的顺序。
|
||||
moveSliderImage(index, offset) {
|
||||
const targetIndex = index + offset
|
||||
if (targetIndex < 0 || targetIndex >= this.sliderMeta.length) return
|
||||
const files = (this.sliderFiles || []).slice()
|
||||
const file = files[index]
|
||||
files.splice(index, 1)
|
||||
files.splice(targetIndex, 0, file)
|
||||
this.sliderFiles = files
|
||||
},
|
||||
// 清空全部轮播图片,二次确认避免误操作。
|
||||
clearSliderImages() {
|
||||
this.$confirm("确定清空全部轮播图片吗?", "提示", {type: "warning"}).then(() => {
|
||||
this.sliderFiles = []
|
||||
this.sliderMeta = []
|
||||
this.$message.success("轮播图片已清空")
|
||||
}).catch(() => {})
|
||||
},
|
||||
// 将轮播图片地址和标题转换为可持久化 JSON。
|
||||
buildSliderImages() {
|
||||
this.syncSliderMeta()
|
||||
return JSON.stringify(this.sliderMeta.map(item => ({
|
||||
src: item.src,
|
||||
title: item.title
|
||||
})))
|
||||
},
|
||||
// 校验每张轮播图的标题。
|
||||
validateSliderMeta() {
|
||||
this.syncSliderMeta()
|
||||
const invalidIndex = this.sliderMeta.findIndex(item => !item.title)
|
||||
if (invalidIndex < 0) return true
|
||||
this.$message.warning("请输入第" + (invalidIndex + 1) + "张轮播图标题")
|
||||
return false
|
||||
},
|
||||
// 重置新增表单;编辑状态下恢复数据库内容。
|
||||
resetForm() {
|
||||
const id = this.getEditId()
|
||||
if (id) {
|
||||
this.loadDetail(id)
|
||||
} else {
|
||||
this.formData = this.defaultForm()
|
||||
this.sliderFiles = []
|
||||
this.sliderMeta = []
|
||||
this.applyUnionDefaults()
|
||||
}
|
||||
this.$nextTick(() => this.$refs.contentForm && this.$refs.contentForm.clearValidate())
|
||||
},
|
||||
// 校验并提交新增或编辑数据。
|
||||
submit() {
|
||||
this.$refs.contentForm.validate(valid => {
|
||||
if (!valid) return
|
||||
if (this.sliderEnabled && !this.validateSliderMeta()) return
|
||||
const data = Object.assign({}, this.formData)
|
||||
if (this.sliderEnabled) {
|
||||
data.sliderImages = this.buildSliderImages()
|
||||
} else {
|
||||
// 未开启轮播的栏目不提交隐藏字段,编辑时保留已有数据,避免切换栏目造成误删。
|
||||
delete data.sliderImages
|
||||
}
|
||||
// 推荐和头条没有对应业务逻辑,提交时不再携带这两个字段。
|
||||
delete data.isRecommend
|
||||
delete data.isHeadline
|
||||
const api = data.id ? "/doEdit" : "/doAdd"
|
||||
this.submitting = true
|
||||
this.$axios.post(loc() + api, data).then(resp => {
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg || "保存成功")
|
||||
if (!data.id) {
|
||||
this.resetForm()
|
||||
}
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
}).finally(() => this.submitting = false)
|
||||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 监听上传组件结果,及时生成对应的标题和工会填写行。
|
||||
sliderFiles: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.syncSliderMeta()
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.formData = this.defaultForm()
|
||||
const id = this.getEditId()
|
||||
Promise.all([this.loadCategories(), this.loadUnionOptions(), this.loadDetail(id)]).then(this.applyUnionDefaults)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
Reference in New Issue
Block a user