| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534 |
- <template>
- <div class="page-home-config">
- <!-- 顶部操作栏 -->
- <div class="toolbar">
- <div class="toolbar-left">
- <h2 class="toolbar-title">权益介绍页配置</h2>
- <el-tag :type="pageStatusTag.type" size="small" effect="plain">{{ pageStatusTag.label }}</el-tag>
- </div>
- <div class="toolbar-right">
- <el-button size="small" @click="showLinkDialog">
- <el-icon><Link /></el-icon>获取链接
- </el-button>
- <el-button size="small" type="primary" plain @click="handleSave('draft')">保存草稿</el-button>
- <el-button v-if="canPublish" size="small" type="primary" @click="handleSave('publish')">发布上线</el-button>
- </div>
- </div>
- <!-- 客群Tab -->
- <el-tabs v-model="currentTier" @tab-click="onTierChange">
- <el-tab-pane
- v-for="tier in tiers"
- :key="tier.key"
- :label="configData[tier.key] ? configData[tier.key].tabName : tier.label"
- :name="tier.key"
- />
- </el-tabs>
- <!-- 配置表单 -->
- <div v-if="currentTierData" class="config-sections">
- <!-- Tab名称配置 -->
- <el-card class="section-card" shadow="hover">
- <template #header>
- <div class="section-header">
- <el-icon><FolderOpened /></el-icon> Tab名称配置
- </div>
- </template>
- <el-form label-width="80px">
- <el-form-item>
- <template #label>Tab名称 <span class="required-star">*</span></template>
- <el-input v-model="currentTierData.tabName" placeholder="例如:卓越理财" style="width: 50%" @input="markModified" />
- </el-form-item>
- </el-form>
- </el-card>
- <!-- 头部配置 -->
- <el-card class="section-card" shadow="hover">
- <template #header>
- <div class="section-header">
- <el-icon><PictureRounded /></el-icon> 头部配置
- </div>
- </template>
- <el-form class="header-config-form" label-width="130px">
- <el-form-item>
- <template #label>头图配图 <span class="required-star">*</span></template>
- <div class="image-upload-area">
- <div class="upload-box" @click="triggerUpload('header')">
- <img v-if="currentTierData.header.imageUrl" :src="currentTierData.header.imageUrl" class="preview-img" />
- <div v-else class="upload-placeholder">
- <el-icon><Picture /></el-icon>
- <span>未上传图片</span>
- </div>
- <div class="upload-overlay">
- <el-icon><Upload /></el-icon>
- <span>点击上传</span>
- </div>
- </div>
- <div class="upload-tip">建议图片尺寸:<b>686 × 296 px</b></div>
- </div>
- <input ref="headerUpload" type="file" accept="image/*" style="display:none" @change="onImageUpload($event, 'header')" />
- </el-form-item>
- <el-form-item>
- <template #label>主标题(粗体) <span class="required-star">*</span></template>
- <el-input v-model="currentTierData.header.mainTitle" @input="markModified" />
- </el-form-item>
- <el-form-item label="副标题(小字)">
- <el-input v-model="currentTierData.header.subTitle" @input="markModified" />
- </el-form-item>
- </el-form>
- </el-card>
- <!-- 按钮配置 -->
- <el-card class="section-card" shadow="hover">
- <template #header>
- <div class="section-header">
- <el-icon><Pointer /></el-icon> 按钮配置
- </div>
- </template>
- <el-form label-width="130px">
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item>
- <template #label>已解锁按钮文案 <span class="required-star">*</span></template>
- <el-input v-model="currentTierData.buttons.unlocked" placeholder="例如:进入权益专区" @input="markModified" />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item>
- <template #label>待解锁按钮文案 <span class="required-star">*</span></template>
- <el-input v-model="currentTierData.buttons.locked" placeholder="例如:待解锁" @input="markModified" />
- </el-form-item>
- </el-col>
- </el-row>
- </el-form>
- </el-card>
- <!-- 权益列表配置 -->
- <el-card class="section-card" shadow="hover">
- <template #header>
- <div class="section-header">
- <el-icon><Grid /></el-icon> 权益列表配置
- <div class="header-right">
- <el-checkbox v-model="currentTierData.privilegeImageEnabled" @change="onPrivilegeImageToggle">
- 展示权益图片
- </el-checkbox>
- <el-button type="text" @click="addPrivilege">
- <el-icon><CirclePlus /></el-icon>添加权益
- </el-button>
- </div>
- </div>
- </template>
- <div v-if="currentTierData.privileges.length === 0" class="empty-tip">
- 暂无权益项
- </div>
- <div v-for="(item, index) in currentTierData.privileges" :key="index" class="privilege-item">
- <div class="privilege-index">NO. {{ index + 1 }}</div>
- <div class="privilege-actions">
- <el-popover placement="bottom" trigger="click" width="180">
- <div>
- <div style="margin-bottom:8px;font-size:13px;color:#666">移动到序号:</div>
- <div style="display:flex;gap:8px">
- <el-input-number v-model="sortTarget" :min="1" :max="currentTierData.privileges.length" size="small" controls-position="right" style="width:100px" />
- <el-button size="small" type="primary" @click="movePrivilege(index, sortTarget - 1)">确定</el-button>
- </div>
- </div>
- <template #reference>
- <el-button size="small" circle @click="sortTarget = index + 1">
- <el-icon><Sort /></el-icon>
- </el-button>
- </template>
- </el-popover>
- <el-button size="small" type="danger" circle @click="removePrivilege(index)">
- <el-icon><Delete /></el-icon>
- </el-button>
- </div>
- <div class="privilege-content">
- <div v-if="currentTierData.privilegeImageEnabled" class="privilege-image-wrap">
- <div class="privilege-image" @click="triggerUpload('privilege', index)">
- <img v-if="item.imageUrl" :src="item.imageUrl" class="preview-img-sm" />
- <div v-else class="upload-placeholder-sm">
- <el-icon><Picture /></el-icon>
- <span>未上传</span>
- </div>
- <input :ref="'privilegeUpload' + index" type="file" accept="image/*" style="display:none" @change="onImageUpload($event, 'privilege', index)" />
- </div>
- <div class="privilege-text-tip">必填,建议尺寸:<br/>100 * 100 px</div>
- </div>
- <div class="privilege-fields">
- <el-form label-width="60px" inline>
- <el-form-item>
- <template #label>标题 <span class="required-star">*</span></template>
- <el-input v-model="item.title" style="width:160px" @input="markModified" />
- </el-form-item>
- <el-form-item label="描述">
- <el-input v-model="item.desc" style="width:300px" @input="markModified" />
- </el-form-item>
- </el-form>
- </div>
- </div>
- </div>
- </el-card>
- </div>
- <!-- 获取链接弹窗 -->
- <el-dialog title="获取页面链接" v-model="linkDialogVisible" width="680px">
- <el-form label-width="100px" inline>
- <el-form-item label="Campaign ID">
- <el-select v-model="linkForm.campaignId" clearable filterable placeholder="请选择 (无)" style="width:240px" @change="updateLinks">
- <el-option v-for="opt in campaignOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
- </el-select>
- </el-form-item>
- <el-form-item label="Channel ID">
- <el-select v-model="linkForm.channelId" clearable filterable placeholder="请选择 (无)" style="width:240px" @change="updateLinks">
- <el-option v-for="opt in channelOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
- </el-select>
- </el-form-item>
- </el-form>
- <div v-if="['published', 'modified'].includes(pageStatus)" class="link-section">
- <div class="link-label"><span class="dot dot-green"></span> 正式环境链接</div>
- <div class="link-row">
- <span class="link-type">H5链接</span>
- <el-input :value="prodH5Url" readonly size="small" />
- <el-button size="small" type="primary" plain @click="copyLink(prodH5Url)">复制</el-button>
- </div>
- <div class="link-row">
- <span class="link-type">小程序链接</span>
- <el-input :value="prodAppUrl" readonly size="small" />
- <el-button size="small" type="primary" plain @click="copyLink(prodAppUrl)">复制</el-button>
- </div>
- </div>
- <div class="link-section">
- <div class="link-label"><span class="dot dot-blue"></span> 草稿预览链接 (测试)</div>
- <div class="link-row">
- <el-input :value="testUrl" readonly size="small" />
- <el-button size="small" type="primary" plain @click="copyLink(testUrl)">复制</el-button>
- </div>
- </div>
- </el-dialog>
- </div>
- </template>
- <script>
- import { ElMessage, ElMessageBox } from 'element-plus'
- import request from '@/utils/request'
- import { fetchLinkOptions } from '@/utils/linkOptions'
- export default {
- name: 'PageHomeConfig',
- data() {
- return {
- currentTier: 'premier',
- tiers: [
- { key: 'premier', label: '卓越理财' },
- { key: 'elite', label: '卓越理财·尊尚' },
- { key: 'global', label: '环球私人银行' }
- ],
- configData: {},
- pageStatus: 'draft',
- isModified: false,
- sortTarget: 1,
- linkDialogVisible: false,
- linkForm: { campaignId: '', channelId: '' },
- campaignOptions: [],
- channelOptions: []
- }
- },
- computed: {
- currentTierData() {
- return this.configData[this.currentTier] || null
- },
- pageStatusTag() {
- if (this.isModified) return { type: 'warning', label: '有未保存修改' }
- if (this.pageStatus === 'published') return { type: 'success', label: '正式线上环境' }
- return { type: 'info', label: '草稿 (未发布)' }
- },
- canPublish() {
- return !this.isModified && ['draft', 'modified'].includes(this.pageStatus)
- },
- linkQueryStr() {
- const params = []
- if (this.linkForm.campaignId) params.push(`CampaignId=${this.linkForm.campaignId}`)
- if (this.linkForm.channelId) params.push(`Channelid=${this.linkForm.channelId}`)
- return params.length ? '?' + params.join('&') : ''
- },
- prodH5Url() {
- return `https://hsbc.com/home${this.linkQueryStr}`
- },
- prodAppUrl() {
- return `hsbc-app://home${this.linkQueryStr}`
- },
- testUrl() {
- const q = this.linkQueryStr
- return `https://test.hsbc.com/home${q ? q + '&' : '?'}draft=1`
- }
- },
- created() {
- this.fetchData()
- },
- beforeRouteLeave(to, from, next) {
- if (!this.isModified) {
- next()
- return
- }
- this.confirmLeaveUnsaved().then(() => next()).catch(() => next(false))
- },
- methods: {
- async fetchData() {
- const res = await request.get('/api/page-config/home')
- this.configData = this.normalizeHomeConfig(res.data.config || {})
- this.pageStatus = res.data.status
- this.isModified = false
- },
- normalizeHomeConfig(config) {
- Object.keys(config).forEach((key) => {
- const tier = config[key]
- if (!tier) return
- if (!Object.prototype.hasOwnProperty.call(tier, 'privilegeImageEnabled')) {
- tier.privilegeImageEnabled = false
- }
- if (!Array.isArray(tier.privileges)) {
- tier.privileges = []
- }
- })
- return config
- },
- async loadLinkOptions() {
- const { campaignOptions, channelOptions } = await fetchLinkOptions()
- this.campaignOptions = campaignOptions
- this.channelOptions = channelOptions
- },
- markModified() {
- this.isModified = true
- },
- isBlank(value) {
- return !(value || '').trim()
- },
- showSaveError(message) {
- ElMessage.error(`保存失败:${message}`)
- return false
- },
- getTierName(key, tier) {
- return (tier && tier.tabName) || (this.tiers.find(item => item.key === key)?.label) || '当前客群'
- },
- confirmLeaveUnsaved() {
- return ElMessageBox.confirm('当前有未保存修改,离开后修改内容将丢失,确定离开吗?', '未保存修改', {
- type: 'warning',
- confirmButtonText: '确定离开',
- cancelButtonText: '继续编辑'
- })
- },
- onTierChange() {},
- triggerUpload(type, index) {
- if (type === 'header') {
- this.$refs.headerUpload.click()
- } else {
- const ref = this.$refs['privilegeUpload' + index]
- if (ref) (Array.isArray(ref) ? ref[0] : ref).click()
- }
- },
- onImageUpload(event, type, index) {
- const file = event.target.files[0]
- if (!file) return
- const reader = new FileReader()
- reader.onload = (e) => {
- if (type === 'header') {
- this.currentTierData.header.imageUrl = e.target.result
- } else {
- this.currentTierData.privileges[index].imageUrl = e.target.result
- }
- this.markModified()
- }
- reader.readAsDataURL(file)
- event.target.value = ''
- },
- addPrivilege() {
- this.currentTierData.privileges.push({ imageUrl: '', title: '', desc: '' })
- this.markModified()
- },
- removePrivilege(index) {
- this.currentTierData.privileges.splice(index, 1)
- this.markModified()
- },
- movePrivilege(from, to) {
- if (to < 0 || to >= this.currentTierData.privileges.length || from === to) return
- const item = this.currentTierData.privileges.splice(from, 1)[0]
- this.currentTierData.privileges.splice(to, 0, item)
- this.markModified()
- },
- validateRequiredConfig() {
- const entries = Object.entries(this.configData || {})
- for (const [key, tier] of entries) {
- if (!tier) continue
- const tierName = this.getTierName(key, tier)
- if (this.isBlank(tier.tabName)) {
- this.currentTier = key
- return this.showSaveError(`${tierName}-Tab名称配置-Tab名称未填写`)
- }
- if (!tier.header?.imageUrl) {
- this.currentTier = key
- return this.showSaveError(`${tierName}-头部配置-头图配图未上传`)
- }
- if (this.isBlank(tier.header?.mainTitle)) {
- this.currentTier = key
- return this.showSaveError(`${tierName}-头部配置-主标题未填写`)
- }
- if (this.isBlank(tier.buttons?.unlocked)) {
- this.currentTier = key
- return this.showSaveError(`${tierName}-按钮配置-已解锁按钮文案未填写`)
- }
- if (this.isBlank(tier.buttons?.locked)) {
- this.currentTier = key
- return this.showSaveError(`${tierName}-按钮配置-待解锁按钮文案未填写`)
- }
- for (const [index, item] of (tier.privileges || []).entries()) {
- if (this.isBlank(item.title)) {
- this.currentTier = key
- return this.showSaveError(`${tierName}-权益列表配置-第 ${index + 1} 个权益-标题未填写`)
- }
- if (tier.privilegeImageEnabled && !item.imageUrl) {
- this.currentTier = key
- return this.showSaveError(`${tierName}-权益列表配置-第 ${index + 1} 个权益-图片未上传`)
- }
- }
- }
- return true
- },
- async handleSave(type) {
- if (!this.validateRequiredConfig()) return
- if (type === 'publish' && this.isModified) {
- ElMessage.warning('请先保存草稿后再发布上线')
- return
- }
- const status = type === 'publish' ? 'published' : (this.pageStatus === 'published' || this.pageStatus === 'modified' ? 'modified' : 'draft')
- await request.put('/api/page-config/home', { config: this.configData, status })
- this.pageStatus = status
- this.isModified = false
- ElMessage.success(type === 'publish' ? '已发布权益介绍页配置!' : '已保存权益介绍页配置草稿!')
- },
- onPrivilegeImageToggle(val) {
- this.markModified()
- if (!val) {
- this.currentTierData.privileges.forEach(p => { p.imageUrl = '' })
- }
- },
- async showLinkDialog() {
- if (this.isModified) {
- ElMessage.warning('当前链接基于上一次保存内容,最新修改需保存后生效')
- }
- await this.loadLinkOptions()
- this.linkForm = { campaignId: '', channelId: '' }
- this.linkDialogVisible = true
- },
- updateLinks() {},
- copyLink(text) {
- navigator.clipboard.writeText(text).then(() => {
- ElMessage.success('链接已复制到剪贴板')
- })
- }
- }
- }
- </script>
- <style scoped>
- .toolbar {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 16px;
- padding: 12px 16px;
- background: #fff;
- border-radius: 4px;
- box-shadow: 0 1px 4px rgba(0,0,0,.06);
- }
- .toolbar-left { display: flex; align-items: center; gap: 12px; }
- .toolbar-title { margin: 0; font-size: 18px; font-weight: 600; }
- .toolbar-right { display: flex; gap: 8px; }
- .required-star { color: #F56C6C; }
- .config-sections { margin-top: 16px; }
- .section-card { margin-bottom: 16px; }
- .section-header { display: flex; align-items: center; gap: 12px; font-size: 15px; font-weight: 600; color: #303133; }
- .section-header i { color: #909399; font-size: 18px; }
- .header-right { margin-left: auto; display: flex; align-items: center; gap: 12px; }
- .header-right :deep(.el-checkbox) { font-weight: normal; }
- .header-config-form .el-form-item__label { white-space: nowrap; }
- .image-upload-area { display: flex; align-items: center; gap: 16px; }
- .upload-box {
- width: 360px; height: 160px;
- background: #eef6fc; border: 1px solid #dcdfe6; border-radius: 4px;
- position: relative; overflow: hidden; cursor: pointer;
- display: flex; align-items: center; justify-content: center;
- }
- .upload-box:hover .upload-overlay { display: flex; }
- .preview-img { width: 100%; height: 100%; object-fit: cover; }
- .upload-placeholder {
- display: flex; flex-direction: column; align-items: center;
- color: #c0c4cc; font-size: 12px;
- }
- .upload-placeholder i { font-size: 28px; margin-bottom: 4px; }
- .upload-overlay {
- display: none; position: absolute; inset: 0;
- background: rgba(0,0,0,.5); color: #fff;
- flex-direction: column; align-items: center; justify-content: center;
- font-size: 14px;
- }
- .upload-overlay i { font-size: 28px; margin-bottom: 6px; }
- .upload-tip {
- font-size: 12px; color: #909399;
- background: #f5f7fa; padding: 8px 12px; border-radius: 4px; border: 1px solid #ebeef5;
- }
- .privilege-item {
- position: relative;
- border: 1px solid #ebeef5; border-radius: 8px;
- padding: 20px 16px 16px; margin-bottom: 12px;
- background: #fff;
- }
- .privilege-index {
- position: absolute; left: 0; top: 0;
- background: #409EFF; color: #fff;
- font-size: 11px; font-weight: 700;
- padding: 2px 10px; border-radius: 8px 0 8px 0;
- }
- .privilege-actions {
- position: absolute; right: 12px; top: 12px;
- display: flex; gap: 6px;
- }
- .privilege-content { display: flex; align-items: flex-start; gap: 12px; margin-top: 8px; }
- .privilege-image-wrap {
- width: 96px;
- flex-shrink: 0;
- display: flex;
- flex-direction: column;
- align-items: center;
- }
- .privilege-image {
- width: 56px; height: 56px; flex-shrink: 0;
- background: #eef6fc; border: 1px solid #dcdfe6; border-radius: 4px;
- display: flex; align-items: center; justify-content: center;
- cursor: pointer; overflow: hidden; position: relative;
- }
- .preview-img-sm { width: 100%; height: 100%; object-fit: cover; }
- .upload-placeholder-sm {
- display: flex; flex-direction: column; align-items: center;
- color: #c0c4cc; font-size: 10px;
- }
- .upload-placeholder-sm i { font-size: 18px; }
- .privilege-text-tip { font-size: 10px; color: #909399; text-align: center; line-height: 1.35; margin-top: 6px; }
- .privilege-fields { flex: 1; }
- .empty-tip {
- text-align: center; color: #c0c4cc; padding: 24px;
- border: 1px dashed #dcdfe6; border-radius: 4px;
- }
- .link-section { margin-top: 16px; padding: 12px; background: #fafafa; border-radius: 6px; border: 1px solid #ebeef5; }
- .link-section + .link-section { margin-top: 12px; }
- .link-label { font-weight: 600; margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }
- .link-row { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
- .link-type { font-size: 12px; color: #909399; width: 70px; flex-shrink: 0; }
- .dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
- .dot-green { background: #67C23A; }
- .dot-blue { background: #409EFF; }
- </style>
|