feat(site): integrate multilingual content management / 集成多语言内容管理

完善官网、管理后台与后端的多语言内容编辑和发布流程,调整站点页面及相关接口,并同步数据库脚本。

新增容器部署配置,改用环境变量注入数据库凭据;补充仓库忽略规则,并停止跟踪本地环境文件和 Maven 构建产物。
This commit is contained in:
liuwenxin 2026-09-10 14:46:06 +08:00
parent 4ee6e9ae83
commit 85083ab962
1056 changed files with 15449 additions and 132933 deletions

10
.env.example Normal file
View File

@ -0,0 +1,10 @@
# Docker Compose database settings
MYSQL_ROOT_PASSWORD=replace-with-a-strong-root-password
MYSQL_DATABASE=huayang
MYSQL_USER=huayang
MYSQL_PASSWORD=replace-with-a-strong-application-password
# Spring datasource settings
DB_URL=jdbc:mysql://localhost:3306/huayang?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
DB_USERNAME=huayang
DB_PASSWORD=replace-with-a-strong-application-password

49
.gitignore vendored Normal file
View File

@ -0,0 +1,49 @@
# Dependencies
node_modules/
vendor/
.venv/
venv/
__pycache__/
.gradle/
# Build outputs
target/
build/
!admin/src/views/tool/build/
!admin/src/views/tool/build/**
dist/
out/
coverage/
# Environment and local runtime data
.env
.env.*
!.env.example
logs/
*.log
*.out
*.err
uploadPath/
mysql-data/
# Editors and operating systems
.idea/
.vscode/
*.swp
*.swo
.DS_Store
Thumbs.db
# Temporary and archive files
*.tmp
*.bak
*.cache
*.zip
~$*
/.codex_tmp/
/package-lock.json
# AI auxiliary files
AI_CONTEXT.md
.ai_context.md
.codex/

30
PROJECT_PROGRESS.md Normal file
View File

@ -0,0 +1,30 @@
<!-- 本文件由 AI 自动维护,请勿手动编辑 -->
# 项目进度
## 当前阶段
- 阶段:开发与联调
- 整体完成度:75%(估算)
## 模块状态
| 模块 | 状态 | 完成度 |
| --- | --- | ---: |
| 公众网站 | 进行中 | 80% |
| 管理后台 | 进行中 | 75% |
| 后端接口 | 进行中 | 80% |
| 多语言与翻译 | 进行中 | 75% |
| 数据库脚本 | 进行中 | 75% |
| 部署配置 | 进行中 | 60% |
## 当前任务
- 整理并提交官网、管理后台、后端、国际化及数据库脚本变更。
- 清理版本库中的本地环境文件和 Maven 构建产物。
- 官网、管理后台和后端构建验证已完成,国际化键同步检查已通过。
## 下一步
- 执行核心接口与多语言发布流程的集成测试。
- 在目标环境验证数据库迁移和容器部署配置。

View File

@ -1,14 +0,0 @@
# 页面标题
VUE_APP_TITLE = 华阳食品管理系统
# 开发环境配置
ENV = 'development'
# 华阳食品管理系统/开发环境
VUE_APP_BASE_API = '/dev-api'
# 路由懒加载
VUE_CLI_BABEL_TRANSPILE_MODULES = true
# 门户站点根地址(首页工作台「打开门户首页」按钮,默认本地 Vite)
VUE_APP_PORTAL_URL=http://localhost:5173/

View File

@ -1,11 +0,0 @@
# 页面标题
VUE_APP_TITLE = 华阳食品管理系统
# 生产环境配置
ENV = 'production'
# 华阳食品管理系统/生产环境
VUE_APP_BASE_API = '/prod-api'
# 门户官网地址(首页工作台预览;生产环境请改为实际域名,如 https://www.example.com/)
# VUE_APP_PORTAL_URL=https://www.example.com/

View File

@ -1,12 +0,0 @@
# 页面标题
VUE_APP_TITLE = 华阳食品管理系统
BABEL_ENV = production
NODE_ENV = production
# 测试环境配置
ENV = 'staging'
# 华阳食品管理系统/测试环境
VUE_APP_BASE_API = '/stage-api'

View File

@ -6,6 +6,7 @@
"license": "MIT",
"scripts": {
"dev": "vue-cli-service serve",
"build": "vue-cli-service build",
"build:prod": "vue-cli-service build",
"build:stage": "vue-cli-service build --mode staging",
"preview": "node build/index.js --preview"
@ -43,7 +44,7 @@
"vue": "2.6.12",
"vue-count-to": "1.0.13",
"vue-cropper": "0.5.5",
"vue-i18n": "^8.28.2",
"vue-i18n": "^7.3.2",
"vue-router": "3.4.9",
"vuedraggable": "2.24.3",
"vuex": "3.6.0"

View File

@ -1,39 +0,0 @@
import request from '@/utils/request'
export function listCompanyTech(query) {
return request({
url: '/huayang/companyTech/list',
method: 'get',
params: query
})
}
export function getCompanyTech(techId) {
return request({
url: '/huayang/companyTech/' + techId,
method: 'get'
})
}
export function addCompanyTech(data) {
return request({
url: '/huayang/companyTech',
method: 'post',
data: data
})
}
export function updateCompanyTech(data) {
return request({
url: '/huayang/companyTech',
method: 'put',
data: data
})
}
export function delCompanyTech(techId) {
return request({
url: '/huayang/companyTech/' + techId,
method: 'delete'
})
}

View File

@ -1,31 +0,0 @@
import request from '@/utils/request'
export function listJoinApply(query) {
return request({
url: '/huayang/joinApply/list',
method: 'get',
params: query
})
}
export function getJoinApply(applyId) {
return request({
url: '/huayang/joinApply/' + applyId,
method: 'get'
})
}
export function handleJoinApply(data) {
return request({
url: '/huayang/joinApply/handle',
method: 'put',
data: data
})
}
export function deleteJoinApply(applyIds) {
return request({
url: '/huayang/joinApply/' + applyIds,
method: 'delete'
})
}

View File

@ -1,47 +0,0 @@
import request from '@/utils/request'
export function listNews(query) {
return request({
url: '/huayang/news/list',
method: 'get',
params: query
})
}
export function getNews(newsId) {
return request({
url: '/huayang/news/' + newsId,
method: 'get'
})
}
export function addNews(data) {
return request({
url: '/huayang/news',
method: 'post',
data: data
})
}
export function updateNews(data) {
return request({
url: '/huayang/news',
method: 'put',
data: data
})
}
export function deleteNews(newsIds) {
return request({
url: '/huayang/news/' + newsIds,
method: 'delete'
})
}
export function exportNews(query) {
return request({
url: '/huayang/news/export',
method: 'get',
params: query
})
}

View File

@ -1,39 +0,0 @@
import request from '@/utils/request'
export function listNewsCategory(query) {
return request({
url: '/huayang/newsCategory/list',
method: 'get',
params: query
})
}
export function getNewsCategory(dictCode) {
return request({
url: '/huayang/newsCategory/' + dictCode,
method: 'get'
})
}
export function addNewsCategory(data) {
return request({
url: '/huayang/newsCategory',
method: 'post',
data: data
})
}
export function updateNewsCategory(data) {
return request({
url: '/huayang/newsCategory',
method: 'put',
data: data
})
}
export function delNewsCategory(dictCode) {
return request({
url: '/huayang/newsCategory/' + dictCode,
method: 'delete'
})
}

View File

@ -1,16 +0,0 @@
import request from '@/utils/request'
export function getNewsPageBanner() {
return request({
url: '/huayang/newsPage/banner',
method: 'get'
})
}
export function updateNewsPageBanner(data) {
return request({
url: '/huayang/newsPage/banner',
method: 'put',
data
})
}

View File

@ -1,32 +1,131 @@
import request from '@/utils/request'
export function listTranslation(query) {
/**
* 列出可翻译页面(左侧页面树)
*/
export function listTranslationPages() {
return request({
url: '/huayang/translation/pages',
method: 'get'
})
}
/**
* 列出当前启用的目标语种(不含默认 zh_cn)
*/
export function listTargetLangs() {
return request({
url: '/huayang/translation/targetLangs',
method: 'get'
})
}
/**
* 实时扫描某页面下的全部可翻译条目
* @param {string} pageModule 页面模块标识,如 home / product / culture
*/
export function scanTranslation(pageModule) {
return request({
url: '/huayang/translation/scan',
method: 'get',
params: { pageModule }
})
}
/**
* 批量保存翻译条目
* @param {{ pageModule: string, items: Array<{langCode: string, key: string, value: string}> }} payload
* items[].value 为空字符串时表示删除该 (langCode, key) 记录
*/
export function batchSaveTranslation(payload) {
return request({
url: '/huayang/translation/batch',
method: 'put',
data: payload
})
}
/**
* 全局完成度统计
*/
export function translationStats() {
return request({
url: '/huayang/translation/stats',
method: 'get'
})
}
/**
* 获取翻译菜单树(用于手动选择显示菜单)
*/
export function getTranslationMenuOptions() {
return request({
url: '/huayang/translation/menuOptions',
method: 'get'
})
}
/**
* 获取已选择菜单 ID 列表
*/
export function getTranslationMenuSelection() {
return request({
url: '/huayang/translation/menuSelection',
method: 'get'
})
}
/**
* 保存菜单选择
* @param {{ menuIds: number[] }} payload
*/
export function saveTranslationMenuSelection(payload) {
return request({
url: '/huayang/translation/menuSelection',
method: 'put',
data: payload
})
}
/**
* 查询翻译条目列表(走多语言国际化通用接口)
*/
export function listTranslationEntries(params) {
return request({
url: '/huayang/translation/list',
method: 'get',
params: query
params
})
}
export function addTranslation(data) {
/**
* 新增翻译条目(按 key 维护静态文案)
*/
export function addTranslationEntry(data) {
return request({
url: '/huayang/translation',
method: 'post',
data: data
data
})
}
export function updateTranslation(data) {
/**
* 修改翻译条目(按 key 维护静态文案)
*/
export function updateTranslationEntry(data) {
return request({
url: '/huayang/translation',
method: 'put',
data: data
data
})
}
export function deleteTranslation(i18nIds) {
/**
* 删除翻译条目
*/
export function deleteTranslationEntry(i18nIds) {
return request({
url: '/huayang/translation/' + i18nIds,
url: `/huayang/translation/${i18nIds}`,
method: 'delete'
})
}

View File

@ -33,7 +33,7 @@
append-to-body
custom-class="editor-media-dialog"
>
<p class="editor-media-dialog__hint">请选择要插入的类型(新闻正文支持图片与视频混排)</p>
<p class="editor-media-dialog__hint">请选择要插入的类型(正文支持图片与视频混排)</p>
<div class="editor-media-dialog__actions">
<el-button type="primary" plain icon="el-icon-picture" @click="onPickImageFromDialog">插入图片</el-button>
<el-button type="primary" plain icon="el-icon-video-camera" @click="onPickVideoFileFromDialog">插入视频(上传文件)</el-button>
@ -104,7 +104,7 @@ export default {
/* 上传文件大小限制(MB) */
fileSize: {
type: Number,
default: 5,
default: null,
},
/* 类型(base64格式、url格式) */
type: {
@ -326,14 +326,6 @@ export default {
this.$message.error(`图片格式错误,请使用 jpg/png/gif/webp/svg`)
return false
}
// 校检文件大小
if (this.fileSize) {
const isLt = file.size / 1024 / 1024 < this.fileSize
if (!isLt) {
this.$message.error(`上传文件大小不能超过 ${this.fileSize} MB!`)
return false
}
}
return true
},
handleUploadSuccess(res, file) {

View File

@ -25,7 +25,6 @@
<!-- 上传提示 -->
<div class="el-upload__tip" slot="tip" v-if="showTip && !disabled">
请上传
<template v-if="fileSize"> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> </template>
<template v-if="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template>
的文件
</div>
@ -68,13 +67,13 @@ export default {
},
// 大小限制(MB)
fileSize: {
type: Number,
default: 5
type: Number,
default: null
},
// 文件类型, 例如['png', 'jpg', 'jpeg']
fileType: {
type: Array,
default: () => ["png", "jpg", "jpeg"]
default: () => ["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp"]
},
// 是否显示提示
isShowTip: {
@ -90,6 +89,11 @@ export default {
drag: {
type: Boolean,
default: true
},
// 兼容历史页面传入的 spec,界面不再展示建议规格
spec: {
type: String,
default: ''
}
},
data() {
@ -128,16 +132,7 @@ export default {
// 首先将值转为数组
const list = Array.isArray(val) ? val : this.value.split(',')
// 然后将数组转为对象数组
this.fileList = list.map(item => {
if (typeof item === "string") {
if (item.indexOf(this.baseUrl) === -1 && !isExternal(item)) {
item = { name: this.baseUrl + item, url: this.baseUrl + item }
} else {
item = { name: item, url: item }
}
}
return item
})
this.fileList = list.map(item => this.normalizeFileItem(item))
} else {
this.fileList = []
return []
@ -150,8 +145,8 @@ export default {
computed: {
// 是否显示提示
showTip() {
return this.isShowTip && (this.fileType || this.fileSize)
},
return this.isShowTip && this.fileType
}
},
methods: {
// 上传前loading加载
@ -179,15 +174,9 @@ export default {
this.$modal.msgError('文件名不正确,不能包含英文逗号!')
return false
}
if (this.fileSize) {
const isLt = file.size / 1024 / 1024 < this.fileSize
if (!isLt) {
this.$modal.msgError(`上传头像图片大小不能超过 ${this.fileSize} MB!`)
return false
}
}
this.$modal.loading("正在上传图片,请稍候...")
this.number++
return true
},
// 文件个数超出
handleExceed() {
@ -197,7 +186,7 @@ export default {
handleUploadSuccess(res, file) {
if (res.code == 200) {
const path = res.fileName || res.url || ''
this.uploadList.push({ name: path, url: path })
this.uploadList.push(this.normalizeFileItem(path))
this.uploadedSuccessfully()
} else {
this.number--
@ -244,10 +233,35 @@ export default {
separator = separator || ","
for (let i in list) {
if (list[i].url) {
strs += list[i].url.replace(this.baseUrl, "") + separator
strs += this.getStorePath(list[i]) + separator
}
}
return strs != '' ? strs.substr(0, strs.length - 1) : ''
},
normalizeFileItem(item) {
if (typeof item === "string") {
return { name: item, url: this.getPreviewUrl(item) }
}
const path = item.url || item.name || ""
return {
...item,
name: item.name || path,
url: this.getPreviewUrl(path)
}
},
getPreviewUrl(path) {
if (!path || isExternal(path)) {
return path
}
if (path.indexOf(this.baseUrl) === 0) {
return path
}
const normalizedPath = path.charAt(0) === "/" ? path : `/${path}`
return this.baseUrl + normalizedPath
},
getStorePath(file) {
const path = file.url || file.name || ""
return path.indexOf(this.baseUrl) === 0 ? path.replace(this.baseUrl, "") : path
}
}
}
@ -272,5 +286,6 @@ export default {
opacity: 0;
transform: translateY(0);
}
</style>

View File

@ -5,9 +5,13 @@ import messages from './lang'
Vue.use(VueI18n)
const cookieLang = Cookies.get('language') || 'zh'
const cookieLangRaw = Cookies.get('language') || 'zh'
if (cookieLangRaw === 'ko') {
Cookies.set('language', 'zh')
}
const cookieLang = cookieLangRaw === 'ko' ? 'zh' : cookieLangRaw
const i18n = new VueI18n({
locale: cookieLang === 'ko' ? 'zh' : cookieLang,
locale: cookieLang,
messages
})

View File

@ -22,24 +22,6 @@
<header-notice id="header-notice" class="right-menu-item hover-effect" />
</el-tooltip>
<el-dropdown class="right-menu-item hover-effect" trigger="hover">
<span class="language-dropdown">
<i class="el-icon-language"></i>
<span>{{ languageName }}</span>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="changeLanguage('zh')">
<span :class="{ 'active-language': locale === 'zh' }">简体中文</span>
</el-dropdown-item>
<el-dropdown-item @click.native="changeLanguage('en')">
<span :class="{ 'active-language': locale === 'en' }">English</span>
</el-dropdown-item>
<el-dropdown-item @click.native="changeLanguage('ja')">
<span :class="{ 'active-language': locale === 'ja' }">日本語</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
<el-dropdown class="avatar-container right-menu-item hover-effect" trigger="hover">
@ -91,14 +73,7 @@ export default {
HeaderNotice
},
data() {
return {
locale: 'zh',
languageMap: {
zh: '简体中文',
en: 'English',
ja: '日本語'
}
}
return {}
},
computed: {
...mapGetters([
@ -121,9 +96,6 @@ export default {
get() {
return this.$store.state.settings.sidebarLogo
}
},
languageName() {
return this.languageMap[this.locale] || '简体中文'
}
},
methods: {
@ -149,12 +121,6 @@ export default {
location.href = '/index'
})
}).catch(() => {})
},
changeLanguage(lang) {
this.locale = lang
this.$i18n.locale = lang
Cookies.set('language', lang)
location.reload()
}
}
}
@ -241,15 +207,6 @@ export default {
}
}
.language-dropdown {
display: flex;
align-items: center;
font-size: 14px;
i {
margin-right: 4px;
}
}
.avatar-container {
margin-right: 0px;
padding-right: 0px;
@ -285,9 +242,4 @@ export default {
}
}
}
.active-language {
color: #409EFF;
font-weight: 600;
}
</style>

View File

@ -80,7 +80,6 @@
<span v-else>无</span>
</template>
</el-table-column>
<el-table-column label="荣誉描述" align="center" prop="honorDesc" :show-overflow-tooltip="true" />
<el-table-column label="排序" align="center" prop="sortOrder" width="80" />
<el-table-column label="状态" align="center" prop="status" width="100">
<template slot-scope="scope">
@ -128,13 +127,7 @@
</el-radio-group>
</el-form-item>
<el-form-item label="图片" prop="honorImage">
<image-upload v-model="form.honorImage" :limit="1" />
</el-form-item>
<el-form-item label="荣誉描述" prop="honorDesc">
<el-input v-model="form.honorDesc" type="textarea" maxlength="500" placeholder="请输入荣誉描述" />
</el-form-item>
<el-form-item label="跳转链接" prop="linkUrl">
<el-input v-model="form.linkUrl" maxlength="500" placeholder="可选,填写后点击荣誉可跳转" />
<image-upload v-model="form.honorImage" :limit="1" spec="荣誉/证书图片,建议 900×1200px 或 3:4,证书文字清晰,单张不超过 5MB" />
</el-form-item>
<el-form-item label="排序" prop="sortOrder">
<el-input-number v-model="form.sortOrder" :min="0" :max="9999" controls-position="right" />
@ -233,8 +226,6 @@ export default {
honorTitle: undefined,
honorType: 'certificate',
honorImage: undefined,
honorDesc: undefined,
linkUrl: undefined,
sortOrder: 0,
status: '0'
}
@ -262,14 +253,19 @@ export default {
submitForm() {
this.$refs['form'].validate((valid) => {
if (valid) {
const payload = {
...this.form,
honorDesc: '',
linkUrl: ''
}
if (this.form.honorId) {
updateCompanyHonor(this.form).then(() => {
updateCompanyHonor(payload).then(() => {
this.$modal.msgSuccess('修改成功')
this.open = false
this.getList()
})
} else {
addCompanyHonor(this.form).then(() => {
addCompanyHonor(payload).then(() => {
this.$modal.msgSuccess('新增成功')
this.open = false
this.getList()
@ -292,4 +288,4 @@ export default {
}
}
}
</script>
</script>

View File

@ -9,21 +9,6 @@
title="当前列表无数据;请点击下方「编辑公司简介页」打开表单(若库中尚无 about_intro 将引导新增)。执行过库初始化脚本后一般会在此看到 home_about、about_intro 等记录。"
/>
<el-row :gutter="10" class="mb8">
<el-col :span="24">
<el-button
type="primary"
icon="el-icon-edit-outline"
size="small"
v-hasPermi="['huayang:companySection:query', 'huayang:companySection:add', 'huayang:companySection:edit']"
@click="openAboutIntroQuickEdit"
>
编辑公司简介页(关于我们 · about_intro)
</el-button>
<span class="quick-tip">无需在列表中找数据;门户「关于我们」与此处标识码 about_intro 对应。</span>
</el-col>
</el-row>
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="88px">
<el-form-item label="板块标题" prop="sectionTitle">
<el-input
@ -124,44 +109,30 @@
</el-form-item>
<template v-if="isAboutIntroForm">
<el-form-item label="页面文字" prop="introCombinedHtml">
<editor v-model="introCombinedHtml" :min-height="280" />
<p class="form-tip">主标题与正文在同一编辑器中编辑;请使用一级标题(H1)作为页面主标题。</p>
<el-form-item label="页面标题" prop="sectionTitle">
<el-input
v-model="form.sectionTitle"
maxlength="200"
show-word-limit
placeholder="如:公司简介"
/>
</el-form-item>
<el-form-item label="正文内容" prop="sectionContent">
<editor v-model="form.sectionContent" :min-height="280" />
<p class="form-tip">正文仅在下方编辑器中编辑;页面主标题请在上方的「页面标题」中填写。</p>
</el-form-item>
<el-form-item label="顶部横幅图">
<image-upload v-model="form.bannerImage" :limit="1" />
<image-upload v-model="form.bannerImage" :limit="1" spec="关于我们顶部横幅,建议 1920×520px 或 16:5,主体居中,单张不超过 5MB" />
</el-form-item>
<el-form-item label="内容区宽图">
<image-upload v-model="form.contentImage1" :limit="1" />
<image-upload v-model="form.contentImage1" :limit="1" spec="正文通栏宽图,建议 1200×520px 或 21:9,单张不超过 5MB" />
<p class="form-tip">标题与正文下方通栏配图(可选)</p>
</el-form-item>
<el-form-item label="内容区左图">
<image-upload v-model="form.contentImage2" :limit="1" />
<image-upload v-model="form.contentImage2" :limit="1" spec="内容区左图,建议 600×420px 或 4:3,与右图保持同尺寸" />
</el-form-item>
<el-form-item label="内容区右图">
<image-upload v-model="form.contentImage3" :limit="1" />
</el-form-item>
</template>
<template v-else-if="isQualityPageForm">
<p class="form-tip" style="margin-bottom: 12px;">
顶部横幅图与「公司简介」页相同,请在「公司简介板块」中编辑标识为 <strong>about_intro</strong> 的记录,修改其中的「顶部横幅图」即可同步到品质技术页。
</p>
<el-form-item label="板块标题" prop="sectionTitle">
<el-input v-model="form.sectionTitle" maxlength="200" show-word-limit placeholder="后台列表显示用" />
</el-form-item>
<el-form-item label="页顶说明" prop="sectionContent">
<editor v-model="form.sectionContent" :min-height="220" />
<p class="form-tip">显示在品质技术各区块列表上方(可选)。各区块标题、正文与配图请在「品质技术」菜单维护。</p>
</el-form-item>
<el-form-item label="页顶配图1">
<image-upload v-model="form.contentImage1" :limit="1" />
</el-form-item>
<el-form-item label="页顶配图2">
<image-upload v-model="form.contentImage2" :limit="1" />
</el-form-item>
<el-form-item label="页顶配图3">
<image-upload v-model="form.contentImage3" :limit="1" />
<image-upload v-model="form.contentImage3" :limit="1" spec="内容区右图,建议 600×420px 或 4:3,与左图保持同尺寸" />
</el-form-item>
</template>
@ -199,35 +170,27 @@
import {
listCompanySection,
getCompanySection,
getCompanySectionByCode,
addCompanySection,
updateCompanySection,
delCompanySection
} from '@/api/huayang/companySection'
const ABOUT_INTRO_CODE = 'about_intro'
const QUALITY_PAGE_CODE = 'quality_page'
const DISABLED_SECTION_CODES = ['quality_page']
export default {
name: 'HuayangCompanySection',
dicts: ['sys_normal_disable'],
data() {
const validateIntroCombined = (rule, value, callback) => {
if (!this.isAboutIntroForm) {
callback()
return
}
const v = (this.introCombinedHtml || '').replace(/<[^>]+>/g, '').trim()
if (!v) {
callback(new Error('页面文字不能为空'))
} else {
callback()
}
}
const validateSectionContent = (rule, value, callback) => {
const code = this.form.sectionCode || ''
if (code === ABOUT_INTRO_CODE || code === QUALITY_PAGE_CODE) {
callback()
if (code === ABOUT_INTRO_CODE) {
const v = (value || '').replace(/<[^>]+>/g, '').trim()
if (!v) {
callback(new Error('正文内容不能为空'))
} else {
callback()
}
return
}
if (!value || !String(value).trim()) {
@ -237,10 +200,6 @@ export default {
}
}
const validateSectionTitle = (rule, value, callback) => {
if ((this.form.sectionCode || '') === ABOUT_INTRO_CODE) {
callback()
return
}
if (!value || !String(value).trim()) {
callback(new Error('板块标题不能为空'))
} else {
@ -257,7 +216,6 @@ export default {
sectionList: [],
title: '',
open: false,
introCombinedHtml: '',
queryParams: {
pageNum: 1,
pageSize: 10,
@ -269,7 +227,6 @@ export default {
sectionCode: [{ required: true, message: '板块标识不能为空', trigger: 'blur' }],
sectionTitle: [{ validator: validateSectionTitle, trigger: 'blur' }],
sectionContent: [{ validator: validateSectionContent, trigger: 'blur' }],
introCombinedHtml: [{ validator: validateIntroCombined, trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
}
}
@ -278,34 +235,27 @@ export default {
isAboutIntroForm() {
return (this.form.sectionCode || '') === ABOUT_INTRO_CODE
},
isQualityPageForm() {
return (this.form.sectionCode || '') === QUALITY_PAGE_CODE
},
isWideSectionForm() {
return this.isAboutIntroForm || this.isQualityPageForm
return this.isAboutIntroForm
}
},
created() {
this.getList()
},
watch: {
'form.sectionCode'(val, oldVal) {
if (!this.open) return
if (val === ABOUT_INTRO_CODE) {
if (!this.introCombinedHtml) {
this.introCombinedHtml = this.buildIntroCombinedFromForm()
}
} else if (oldVal === ABOUT_INTRO_CODE) {
this.introCombinedHtml = ''
}
}
},
methods: {
isCompanySectionRecord(section) {
const code = String(section?.sectionCode || section?.section_code || '').trim().toLowerCase()
if (!code) return true
if (DISABLED_SECTION_CODES.includes(code)) return false
return !code.startsWith('join_')
},
getList() {
this.loading = true
listCompanySection(this.queryParams).then((response) => {
this.sectionList = response.rows
this.total = response.total
const rows = Array.isArray(response.rows) ? response.rows : []
const filteredRows = rows.filter((row) => this.isCompanySectionRecord(row))
this.sectionList = filteredRows
this.total = filteredRows.length
this.loading = false
})
},
@ -321,31 +271,20 @@ export default {
this.open = false
this.reset()
},
buildIntroCombinedFromForm() {
const title = this.form.sectionTitle || '公司简介'
const content = this.form.sectionContent || ''
if (/<h1[\s>]/i.test(content)) {
return content
}
return `<h1>${this.escapeForHtmlText(title)}</h1>${content}`
},
escapeForHtmlText(s) {
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
},
splitIntroCombinedToForm() {
const raw = this.introCombinedHtml || ''
/** 旧版在同一编辑器内用 H1 表示标题:载入时若正文仍以 H1 开头则拆回标题字段(一次性兼容) */
normalizeLegacyAboutIntroHtml() {
const raw = String(this.form.sectionContent || '').trim()
if (!raw) return
const tmp = document.createElement('div')
tmp.innerHTML = raw.trim()
tmp.innerHTML = raw
const h1 = tmp.querySelector('h1')
if (h1) {
this.form.sectionTitle = (h1.textContent || '').trim() || '公司简介'
h1.remove()
} else {
this.form.sectionTitle = '公司简介'
if (!h1) return
const fromH1 = (h1.textContent || '').trim()
const hasTitle = String(this.form.sectionTitle || '').trim()
if (fromH1 && !hasTitle) {
this.form.sectionTitle = fromH1
}
h1.remove()
this.form.sectionContent = tmp.innerHTML
},
/** 保证图片字段存在且为字符串,便于 v-model 与 JSON 提交(兼容 snake_case 返回) */
@ -364,7 +303,6 @@ export default {
}
},
reset() {
this.introCombinedHtml = ''
this.form = {
sectionId: undefined,
sectionCode: undefined,
@ -389,48 +327,16 @@ export default {
this.open = true
this.title = '添加公司简介板块'
},
/** 快速打开「关于我们」主板块:有则编辑,无则带 about_intro 新增 */
openAboutIntroQuickEdit() {
getCompanySectionByCode(ABOUT_INTRO_CODE).then((response) => {
const row = response && response.data
this.reset()
if (row && row.sectionId != null) {
this.form = this.normalizeSectionForm(row)
if (this.isAboutIntroForm) {
this.introCombinedHtml = this.buildIntroCombinedFromForm()
}
this.open = true
this.title = '修改公司简介页(关于我们)'
return
}
this.form.sectionCode = ABOUT_INTRO_CODE
this.form.sectionTitle = '公司简介'
this.form.sortOrder = 1
this.form.status = '0'
this.form.bannerImage = ''
this.form.contentImage1 = ''
this.form.contentImage2 = ''
this.form.contentImage3 = ''
this.introCombinedHtml = this.buildIntroCombinedFromForm()
this.open = true
this.title = '新增公司简介页(about_intro)'
})
},
handleUpdate(row) {
this.reset()
const sectionId = row.sectionId || this.ids[0]
getCompanySection(sectionId).then((response) => {
this.form = this.normalizeSectionForm(response.data)
if (this.isAboutIntroForm) {
this.introCombinedHtml = this.buildIntroCombinedFromForm()
this.normalizeLegacyAboutIntroHtml()
}
this.open = true
const code = this.form.sectionCode || ''
if (code === QUALITY_PAGE_CODE) {
this.title = '修改品质技术页(顶部与页顶区)'
} else {
this.title = '修改公司简介板块'
}
this.title = '修改公司简介板块'
})
},
submitForm() {
@ -438,10 +344,12 @@ export default {
if (!valid) {
return
}
if (this.isAboutIntroForm) {
this.splitIntroCombinedToForm()
const code = String(this.form.sectionCode || '').trim().toLowerCase()
if (DISABLED_SECTION_CODES.includes(code)) {
this.$modal.msgError('该板块标识已停用,不能在管理端维护')
return
}
if (this.isAboutIntroForm || this.isQualityPageForm) {
if (this.isAboutIntroForm) {
this.form.bannerImage = this.form.bannerImage == null ? '' : String(this.form.bannerImage)
this.form.contentImage1 = this.form.contentImage1 == null ? '' : String(this.form.contentImage1)
this.form.contentImage2 = this.form.contentImage2 == null ? '' : String(this.form.contentImage2)
@ -485,10 +393,4 @@ export default {
color: #909399;
line-height: 1.5;
}
.quick-tip {
margin-left: 12px;
font-size: 12px;
color: #909399;
vertical-align: middle;
}
</style>

View File

@ -1,401 +0,0 @@
<template>
<div class="app-container">
<el-alert
type="info"
:closable="false"
show-icon
style="margin-bottom: 16px"
title="说明"
description="「历程页轮播」用于门户网站「发展历程」底部轮播;「店面展示页」用于门户网站店面展示页底部图集;二者仅需标题、展示图与排序。「店面位置」用于全国门店页左侧网点列表(名称、电话、地址)及底部轮播按名称配图;右侧大图请在「站点配置」配置键 store_location_map_image。「展厅」用于首页等通用店面展示。"
/>
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="88px">
<el-form-item label="门店名称" prop="storeName">
<el-input
v-model="queryParams.storeName"
placeholder="请输入门店名称"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="门店类型" prop="storeType">
<el-select v-model="queryParams.storeType" placeholder="全部类型" clearable>
<el-option label="展厅" value="showroom" />
<el-option label="历程页轮播" value="history" />
<el-option label="店面展示页" value="about_showcase" />
<el-option label="店面位置" value="map_location" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="状态" clearable>
<el-option
v-for="dict in dict.type.sys_normal_disable"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-hasPermi="['huayang:companyStore:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['huayang:companyStore:remove']"
>删除</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="storeList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="门店名称" align="center" prop="storeName" :show-overflow-tooltip="true" min-width="100" />
<el-table-column label="电话" align="center" prop="storePhone" width="120" :show-overflow-tooltip="true" />
<el-table-column label="地址" align="left" prop="storeAddress" min-width="160" :show-overflow-tooltip="true" />
<el-table-column label="类型" align="center" prop="storeType" width="140">
<template slot-scope="scope">
<el-tag size="small" v-if="scope.row.storeType === 'showroom'">展厅</el-tag>
<el-tag size="small" type="success" v-else-if="scope.row.storeType === 'shop'">直营店</el-tag>
<el-tag size="small" type="warning" v-else-if="scope.row.storeType === 'history'">历程页轮播</el-tag>
<el-tag size="small" type="primary" v-else-if="scope.row.storeType === 'about_showcase'">店面展示页</el-tag>
<el-tag size="small" type="danger" v-else-if="scope.row.storeType === 'map_location'">店面位置</el-tag>
<el-tag size="small" type="info" v-else>{{ scope.row.storeType }}</el-tag>
</template>
</el-table-column>
<el-table-column label="图片" align="center" prop="storeImage" width="120">
<template slot-scope="scope">
<el-image
v-if="scope.row.storeImage"
:src="resolveImageUrl(scope.row.storeImage)"
:preview-src-list="[resolveImageUrl(scope.row.storeImage)]"
fit="cover"
style="width: 60px; height: 60px"
/>
</template>
</el-table-column>
<el-table-column label="主店" align="center" prop="isMain" width="80">
<template slot-scope="scope">
<el-tag :type="scope.row.isMain === '1' ? 'danger' : 'info'">{{ scope.row.isMain === '1' ? '是' : '否' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="排序" align="center" prop="sortOrder" width="80" />
<el-table-column label="状态" align="center" prop="status" width="100">
<template slot-scope="scope">
<dict-tag :options="dict.type.sys_normal_disable" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="140">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['huayang:companyStore:edit']"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['huayang:companyStore:remove']"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<el-dialog :title="title" :visible.sync="open" width="640px" append-to-body>
<el-form ref="form" :model="form" :rules="dynamicRules" label-width="108px">
<el-form-item :label="isPageCarouselType ? '轮播标题' : '门店名称'" prop="storeName">
<el-input
v-model="form.storeName"
:placeholder="isPageCarouselType ? '图片下方展示的名称' : '请输入门店名称'"
/>
</el-form-item>
<el-form-item label="门店类型" prop="storeType">
<el-radio-group v-model="form.storeType" class="store-type-radio-group">
<el-radio label="showroom">展厅</el-radio>
<el-radio label="history">历程页轮播(发展历程页底部)</el-radio>
<el-radio label="about_showcase">店面展示页(门户网站店面展示 Tab)</el-radio>
<el-radio label="map_location">店面位置(全国门店页左侧网点列表)</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="!isMapLocationType" label="展示图片" prop="storeImage">
<image-upload v-model="form.storeImage" :limit="1" />
</el-form-item>
<template v-if="isMapLocationType">
<el-form-item label="门店地址" prop="storeAddress">
<el-input v-model="form.storeAddress" type="textarea" :rows="2" placeholder="请输入详细地址" />
</el-form-item>
<el-form-item label="联系电话" prop="storePhone">
<el-input v-model="form.storePhone" placeholder="请输入联系电话" />
</el-form-item>
<el-form-item label="配图">
<image-upload v-model="form.storeImage" :limit="1" />
<div style="color:#909399;font-size:12px;line-height:1.5;margin-top:6px">选填;填写后可用于底部轮播与网点配图匹配。</div>
</el-form-item>
</template>
<template v-if="!isPageCarouselType && !isMapLocationType">
<el-form-item label="门店地址" prop="storeAddress">
<el-input v-model="form.storeAddress" placeholder="请输入门店详细地址" />
</el-form-item>
<el-form-item label="联系电话" prop="storePhone">
<el-input v-model="form.storePhone" placeholder="请输入门店联系电话" />
</el-form-item>
<el-form-item label="营业时间" prop="businessHours">
<el-input v-model="form.businessHours" placeholder="如:周一至周日 9:00-18:00" />
</el-form-item>
<el-form-item label="门店简介" prop="storeDesc">
<el-input v-model="form.storeDesc" type="textarea" placeholder="请输入门店简介" />
</el-form-item>
<el-form-item label="是否主店" prop="isMain">
<el-radio-group v-model="form.isMain">
<el-radio label="1">是</el-radio>
<el-radio label="0">否</el-radio>
</el-radio-group>
</el-form-item>
</template>
<el-form-item label="排序" prop="sortOrder">
<el-input-number v-model="form.sortOrder" :min="0" :max="9999" controls-position="right" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in dict.type.sys_normal_disable"
:key="dict.value"
:label="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listCompanyStore, getCompanyStore, addCompanyStore, updateCompanyStore, delCompanyStore } from '@/api/huayang/companyStore'
export default {
name: 'HuayangCompanyStore',
dicts: ['sys_normal_disable'],
data() {
return {
loading: true,
showSearch: true,
ids: [],
single: true,
multiple: true,
total: 0,
storeList: [],
title: '',
open: false,
queryParams: {
pageNum: 1,
pageSize: 10,
storeName: undefined,
storeType: undefined,
status: undefined
},
form: {}
}
},
computed: {
/** 历程页轮播与店面展示页专用:仅需图片与标题 */
isPageCarouselType() {
return (
this.form && (this.form.storeType === 'history' || this.form.storeType === 'about_showcase')
)
},
/** 全国门店页左侧「店面位置」列表 */
isMapLocationType() {
return this.form && this.form.storeType === 'map_location'
},
dynamicRules() {
const r = {
storeName: [{ required: true, message: '名称不能为空', trigger: 'blur' }],
storeType: [{ required: true, message: '门店类型不能为空', trigger: 'change' }],
status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
}
const st = this.form && this.form.storeType
if (st === 'map_location') {
r.storeAddress = [{ required: true, message: '请输入门店地址', trigger: 'blur' }]
r.storePhone = [{ required: true, message: '请输入联系电话', trigger: 'blur' }]
return r
}
r.storeImage = [{ required: true, message: '展示图片不能为空', trigger: 'blur' }]
return r
}
},
watch: {
'form.storeType'(val) {
if (val === 'history' || val === 'about_showcase') {
this.form.storeAddress = ''
this.form.storePhone = ''
this.form.businessHours = ''
this.form.storeDesc = ''
this.form.isMain = '0'
}
this.$nextTick(() => {
if (this.$refs.form) this.$refs.form.clearValidate()
})
}
},
created() {
this.getList()
},
methods: {
resolveImageUrl(raw) {
if (!raw) return ''
const value = String(raw).trim()
if (!value) return ''
if (/^https?:\/\//i.test(value)) return value
const baseApi = process.env.VUE_APP_BASE_API || ''
const baseOrigin = baseApi ? baseApi.replace(/\/api\/?$/, '') : ''
if (value.startsWith('/assets/')) return value
if (value.startsWith('/profile/')) return `${baseOrigin}${value}`
if (value.startsWith('profile/')) return `${baseOrigin}/${value}`
if (value.startsWith('/')) return `${baseOrigin}${value}`
return `${baseOrigin}/${value}`
},
getList() {
this.loading = true
listCompanyStore(this.queryParams).then((response) => {
this.storeList = response.rows
this.total = response.total
this.loading = false
})
},
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
resetQuery() {
this.resetForm('queryForm')
this.handleQuery()
},
cancel() {
this.open = false
this.reset()
},
reset() {
this.form = {
storeId: undefined,
storeName: undefined,
storeType: 'showroom',
storeImage: undefined,
storeAddress: undefined,
storePhone: undefined,
storeDesc: undefined,
businessHours: undefined,
isMain: '0',
sortOrder: 0,
status: '0'
}
this.resetForm('form')
},
handleSelectionChange(selection) {
this.ids = selection.map((item) => item.storeId)
this.single = selection.length !== 1
this.multiple = !selection.length
},
handleAdd() {
this.reset()
this.open = true
this.title = '添加店面展示'
},
handleUpdate(row) {
this.reset()
const storeId = row.storeId || this.ids[0]
getCompanyStore(storeId).then((response) => {
this.form = response.data
if (this.form.storeType === 'shop') {
this.form.storeType = 'showroom'
}
this.open = true
this.title = '修改店面展示'
})
},
submitForm() {
if (this.form.storeType === 'history' || this.form.storeType === 'about_showcase') {
this.form.storeAddress = ''
this.form.storePhone = ''
this.form.businessHours = ''
this.form.storeDesc = ''
this.form.isMain = '0'
}
this.$refs['form'].validate((valid) => {
if (valid) {
if (this.form.storeId) {
updateCompanyStore(this.form).then(() => {
this.$modal.msgSuccess('修改成功')
this.open = false
this.getList()
})
} else {
addCompanyStore(this.form).then(() => {
this.$modal.msgSuccess('新增成功')
this.open = false
this.getList()
})
}
}
})
},
handleDelete(row) {
const storeIds = row.storeId ? [row.storeId] : this.ids
this.$modal.confirm('是否确认删除选中的数据项?')
.then(() => {
return delCompanyStore(storeIds)
})
.then(() => {
this.getList()
this.$modal.msgSuccess('删除成功')
})
.catch(() => {})
}
}
}
</script>
<style scoped>
.store-type-radio-group {
display: flex;
flex-wrap: wrap;
gap: 8px 16px;
line-height: 1.6;
}
.store-type-radio-group .el-radio {
margin-right: 0;
}
</style>

View File

@ -1,264 +0,0 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="88px">
<el-form-item label="技术标题" prop="techTitle">
<el-input
v-model="queryParams.techTitle"
placeholder="请输入技术标题"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="状态" clearable>
<el-option
v-for="dict in dict.type.sys_normal_disable"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-hasPermi="['huayang:companyTech:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['huayang:companyTech:remove']"
>删除</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="techList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="标题" align="center" prop="techTitle" :show-overflow-tooltip="true" />
<el-table-column label="分类" align="center" prop="techCategory" width="120">
<template slot-scope="scope">
<el-tag size="small">{{ scope.row.techCategory }}</el-tag>
</template>
</el-table-column>
<el-table-column label="排序" align="center" prop="sortOrder" width="80" />
<el-table-column label="状态" align="center" prop="status" width="100">
<template slot-scope="scope">
<dict-tag :options="dict.type.sys_normal_disable" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="160">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="140">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['huayang:companyTech:edit']"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['huayang:companyTech:remove']"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<el-dialog :title="title" :visible.sync="open" width="780px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item label="技术标题" prop="techTitle">
<el-input v-model="form.techTitle" maxlength="200" show-word-limit placeholder="请输入技术标题" />
</el-form-item>
<el-form-item label="所属分类" prop="techCategory">
<el-input v-model="form.techCategory" placeholder="如:原料、设备、工艺等" />
</el-form-item>
<el-form-item label="详细内容" prop="techContent">
<editor v-model="form.techContent" :min-height="192"/>
</el-form-item>
<el-form-item label="展示图片" prop="techImages">
<image-upload v-model="form.techImages" :limit="12" />
<p class="form-tip">多张图以逗号分隔存储,前台按顺序展示在对应区块正文下方。</p>
</el-form-item>
<el-form-item label="排序" prop="sortOrder">
<el-input-number v-model="form.sortOrder" :min="0" :max="9999" controls-position="right" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in dict.type.sys_normal_disable"
:key="dict.value"
:label="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listCompanyTech, getCompanyTech, addCompanyTech, updateCompanyTech, delCompanyTech } from '@/api/huayang/companyTech'
export default {
name: 'HuayangCompanyTech',
dicts: ['sys_normal_disable'],
data() {
return {
loading: true,
showSearch: true,
ids: [],
single: true,
multiple: true,
total: 0,
techList: [],
title: '',
open: false,
queryParams: {
pageNum: 1,
pageSize: 10,
techTitle: undefined,
status: undefined
},
form: {},
rules: {
techTitle: [{ required: true, message: '技术标题不能为空', trigger: 'blur' }],
techContent: [{ required: true, message: '内容不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
}
}
},
created() {
this.getList()
},
methods: {
getList() {
this.loading = true
listCompanyTech(this.queryParams).then((response) => {
this.techList = response.rows
this.total = response.total
this.loading = false
})
},
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
resetQuery() {
this.resetForm('queryForm')
this.handleQuery()
},
cancel() {
this.open = false
this.reset()
},
reset() {
this.form = {
techId: undefined,
techTitle: undefined,
techContent: undefined,
techCategory: undefined,
techImages: undefined,
sortOrder: 0,
status: '0'
}
this.resetForm('form')
},
handleSelectionChange(selection) {
this.ids = selection.map((item) => item.techId)
this.single = selection.length !== 1
this.multiple = !selection.length
},
handleAdd() {
this.reset()
this.open = true
this.title = '添加品质技术'
},
handleUpdate(row) {
this.reset()
const techId = row.techId || this.ids[0]
getCompanyTech(techId).then((response) => {
this.form = response.data
this.open = true
this.title = '修改品质技术'
})
},
submitForm() {
this.$refs['form'].validate((valid) => {
if (valid) {
if (this.form.techId) {
updateCompanyTech(this.form).then(() => {
this.$modal.msgSuccess('修改成功')
this.open = false
this.getList()
})
} else {
addCompanyTech(this.form).then(() => {
this.$modal.msgSuccess('新增成功')
this.open = false
this.getList()
})
}
}
})
},
handleDelete(row) {
const techIds = row.techId ? [row.techId] : this.ids
this.$modal.confirm('是否确认删除选中的数据项?')
.then(() => {
return delCompanyTech(techIds)
})
.then(() => {
this.getList()
this.$modal.msgSuccess('删除成功')
})
.catch(() => {})
}
}
}
</script>
<style scoped>
.form-tip {
margin: 6px 0 0;
font-size: 12px;
color: #909399;
line-height: 1.5;
}
</style>

View File

@ -1,13 +1,5 @@
<template>
<div class="app-container">
<el-alert
type="info"
:closable="false"
show-icon
style="margin-bottom: 16px"
title="门户网站「发展历程」页"
description="本页维护时间线节点。该页底部「店面展示」请在菜单「店面展示」中维护:门店类型选「历程页轮播」(与门户网站「店面展示页」数据源相互独立)。填写轮播标题、上传展示图片并设置排序即可。"
/>
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="88px">
<el-form-item label="事件标题" prop="eventTitle">
<el-input
@ -58,16 +50,6 @@
v-hasPermi="['huayang:companyTimeline:remove']"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
v-hasPermi="['huayang:companyTimeline:export']"
>导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
@ -83,18 +65,6 @@
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="配图" align="center" prop="eventImage" width="100">
<template slot-scope="scope">
<el-image
v-if="scope.row.eventImage"
:src="resolveImageUrl(scope.row.eventImage)"
:preview-src-list="[resolveImageUrl(scope.row.eventImage)]"
fit="cover"
style="width: 60px; height: 60px"
/>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="排序" align="center" prop="sortOrder" width="80" />
<el-table-column label="状态" align="center" prop="status" width="100">
<template slot-scope="scope">
@ -146,9 +116,6 @@
<el-form-item label="事件详情" prop="eventDetail">
<editor v-model="form.eventDetail" :min-height="200" />
</el-form-item>
<el-form-item label="配图" prop="eventImage">
<image-upload v-model="form.eventImage" :limit="1" />
</el-form-item>
<el-form-item label="重点里程碑" prop="isHighlight">
<el-radio-group v-model="form.isHighlight">
<el-radio label="0">否</el-radio>
@ -177,7 +144,7 @@
</template>
<script>
import { listCompanyTimeline, getCompanyTimeline, addCompanyTimeline, updateCompanyTimeline, delCompanyTimeline, exportCompanyTimeline } from '@/api/huayang/companyTimeline'
import { listCompanyTimeline, getCompanyTimeline, addCompanyTimeline, updateCompanyTimeline, delCompanyTimeline } from '@/api/huayang/companyTimeline'
export default {
name: 'HuayangCompanyTimeline',
@ -212,19 +179,6 @@ export default {
this.getList()
},
methods: {
resolveImageUrl(raw) {
if (!raw) return ''
const value = String(raw).trim()
if (!value) return ''
if (/^https?:\/\//i.test(value)) return value
const baseApi = process.env.VUE_APP_BASE_API || ''
const baseOrigin = baseApi ? baseApi.replace(/\/api\/?$/, '') : ''
if (value.startsWith('/assets/')) return value
if (value.startsWith('/profile/')) return `${baseOrigin}${value}`
if (value.startsWith('profile/')) return `${baseOrigin}/${value}`
if (value.startsWith('/')) return `${baseOrigin}${value}`
return `${baseOrigin}/${value}`
},
getList() {
this.loading = true
listCompanyTimeline(this.queryParams).then((response) => {
@ -288,15 +242,6 @@ export default {
this.$modal.msgSuccess('删除成功')
}).catch(() => {})
},
handleExport() {
exportCompanyTimeline(this.queryParams).then((response) => {
const blob = new Blob([response], { type: 'application/vnd.ms-excel' })
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = '发展历程数据_' + this.parseTime(new Date(), '{y}{m}{d}{h}{i}{s}') + '.xlsx'
link.click()
})
},
submitForm() {
this.$refs['form'].validate((valid) => {
if (valid) {

View File

@ -1,5 +1,13 @@
<template>
<div class="app-container">
<el-alert
type="info"
:closable="false"
show-icon
class="mb8"
:title="pageTitle"
:description="pageDescription"
/>
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="配置键名" prop="configKey">
<el-input
@ -104,7 +112,7 @@
<el-input v-model="form.configKey" placeholder="请输入配置键名" />
</el-form-item>
<el-form-item label="配置值" prop="configValue">
<image-upload v-if="isImageConfigKey(form.configKey)" v-model="form.configValue" :limit="1" />
<image-upload v-if="isImageConfigKey(form.configKey)" v-model="form.configValue" :limit="1" spec="站点配置图片,请按具体展示位置上传;横幅建议 1920px 宽,图标/Logo 建议透明底 PNG/SVG" />
<el-input v-else v-model="form.configValue" type="textarea" placeholder="请输入内容" />
</el-form-item>
<el-form-item label="备注" prop="remark">
@ -128,6 +136,14 @@ export default {
excludeConfigKeys: {
type: Array,
default: () => []
},
pageTitle: {
type: String,
default: '站点参数管理'
},
pageDescription: {
type: String,
default: 'SEO、备案、联系方式等站点参数在此维护(对应门户 /site/config/public/list 接口)。'
}
},
data() {

View File

@ -1,114 +1,345 @@
<template>
<div class="app-container">
<el-card class="mb8" shadow="never">
<el-card shadow="never">
<div slot="header" class="clearfix">
<span>联系我们页面配置</span>
</div>
<el-form label-width="120px">
<el-form-item label="顶部图片">
<image-upload v-model="pageConfig.contact_top_image" :limit="1" />
</el-form-item>
<el-form-item label="我们的位置图片">
<image-upload v-model="pageConfig.contact_map_image" :limit="1" />
<image-upload v-model="pageConfig.contact_top_image" :limit="1" spec="联系我们顶部横幅,建议 1920×520px 或 16:5,主体居中,单张不超过 5MB" />
</el-form-item>
<el-form-item label="联系我们内容">
<el-input
v-model="pageConfig.contact_content"
type="textarea"
:rows="5"
placeholder="请输入联系我们内容,支持多行"
/>
</el-form-item>
<el-form-item label="联系电话">
<el-input v-model="pageConfig.contact_phone" placeholder="请输入联系电话" />
<el-input
v-model="pageConfig.contact_phones"
type="textarea"
:rows="4"
placeholder="每行一个电话,前台「商务热线」区块内展示全部号码"
/>
<div class="field-tip">在同一电话区块内增加号码:直接换行输入,无需新增其他类型</div>
</el-form-item>
<el-form-item label="联系邮箱">
<el-input v-model="pageConfig.contact_email" placeholder="请输入联系邮箱" />
<el-input
v-model="pageConfig.contact_emails"
type="textarea"
:rows="4"
placeholder="每行一个邮箱,前台「E-mail」区块内展示全部邮箱"
/>
<div class="field-tip">在同一邮箱区块内增加地址:直接换行输入</div>
</el-form-item>
<el-form-item label="联系地址">
<el-input v-model="pageConfig.contact_address" placeholder="请输入联系地址" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="savingPageConfig" @click="savePageConfig">保存页面配置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script>
import { listConfig, addConfig, updateConfig } from '@/api/huayang/config'
import { listContact, addContact, deleteContact } from '@/api/huayang/contact'
export default {
name: 'HuayangContact',
data() {
return {
savingPageConfig: false,
pageConfig: {
contact_top_image: '',
contact_map_image: '',
contact_content: '',
contact_phone: '',
contact_email: '',
contact_phones: '',
contact_emails: '',
contact_address: ''
},
pageConfigMeta: {
contact_top_image: null,
contact_map_image: null,
contact_content: null,
contact_phone: null,
contact_email: null,
contact_address: null
}
}
},
created() {
this.getPageConfig()
},
methods: {
parseLines(raw) {
return String(raw || '')
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
},
async getPageConfig() {
const keys = ['contact_top_image', 'contact_map_image', 'contact_content', 'contact_phone', 'contact_email', 'contact_address']
const keys = ['contact_top_image', 'contact_content', 'contact_phone', 'contact_email', 'contact_address']
const rows = await Promise.all(keys.map((key) => this.fetchConfigByKey(key)))
rows.forEach((row, idx) => {
const key = keys[idx]
this.pageConfig[key] = row?.configValue || ''
const value = row?.configValue || ''
if (key === 'contact_top_image' || key === 'contact_content' || key === 'contact_address') {
this.pageConfig[key] = value
}
if (key === 'contact_phone') {
this.pageConfig.contact_phones = value
}
if (key === 'contact_email') {
this.pageConfig.contact_emails = value
}
this.pageConfigMeta[key] = row || null
})
},
async fetchConfigByKey(key) {
const response = await listConfig({ pageNum: 1, pageSize: 1, configKey: key })
const rows = Array.isArray(response?.rows) ? response.rows : []
return rows.find(item => item.configKey === key) || null
},
async fetchAllContacts() {
const response = await listContact({ pageNum: 1, pageSize: 200 })
return Array.isArray(response?.rows) ? response.rows : []
},
async syncContactGroup(type, lines, defaultLabel, sortOrder) {
const allContacts = await this.fetchAllContacts()
const existing = allContacts.filter((item) => item.contactType === type)
if (existing.length) {
await deleteContact(existing.map((item) => item.contactId).join(','))
}
if (!lines.length) return
await addContact({
contactType: type,
contactLabel: defaultLabel,
contactValue: lines.join('\n'),
sortOrder,
isEnabled: '1'
})
},
async savePageConfig() {
this.savingPageConfig = true
try {
const phoneLines = this.parseLines(this.pageConfig.contact_phones)
const emailLines = this.parseLines(this.pageConfig.contact_emails)
const address = String(this.pageConfig.contact_address || '').trim()
await this.upsertConfig('contact_top_image', '联系我们顶部图片', this.pageConfig.contact_top_image, '联系我们页顶部横幅图片')
await this.upsertConfig('contact_map_image', '我们的位置图片', this.pageConfig.contact_map_image, '联系我们页位置展示图片')
await this.upsertConfig('contact_content', '联系我们内容', this.pageConfig.contact_content, '联系我们页顶部说明文案')
await this.upsertConfig('contact_phone', '联系电话', this.pageConfig.contact_phone, '联系我们页联系电话')
await this.upsertConfig('contact_email', '联系邮箱', this.pageConfig.contact_email, '联系我们页联系邮箱')
await this.upsertConfig('contact_address', '联系地址', this.pageConfig.contact_address, '联系我们页联系地址')
await this.upsertConfig('contact_phone', '联系电话', phoneLines.join('\n'), '联系我们页联系电话(每行一个,首条同步页脚)')
await this.upsertConfig('contact_email', '联系邮箱', emailLines.join('\n'), '联系我们页联系邮箱(每行一个,首条同步页脚)')
await this.upsertConfig('contact_address', '联系地址', address, '联系我们页联系地址')
await this.syncContactGroup('phone', phoneLines, '商务热线', 0)
await this.syncContactGroup('email', emailLines, 'E-mail', 1)
await this.syncContactGroup('address', address ? [address] : [], '地址', 2)
this.$modal.msgSuccess('页面配置保存成功')
await this.getPageConfig()
} finally {
this.savingPageConfig = false
}
},
async upsertConfig(configKey, configName, configValue, remark) {
const meta = this.pageConfigMeta[configKey]
const payload = {
configKey,
configName,
configValue: configValue == null ? '' : String(configValue),
remark
}
if (meta?.configId) {
await updateConfig({ ...meta, ...payload })
} else {
await addConfig(payload)
}
}
}
}
</script>
</script>
<style scoped>
.field-tip {
margin-top: 6px;
font-size: 12px;
color: #909399;
line-height: 1.5;
}
</style>

View File

@ -1,13 +1,5 @@
<template>
<div class="app-container">
<el-alert
v-hasPermi="['huayang:navigation:list']"
type="info"
:closable="false"
show-icon
class="mb8"
title="门户文化页底部导航栏请在「页脚一站式配置」或「导航管理(底部导航)」中维护。"
/>
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="72px">
<el-form-item label="名称" prop="categoryName">
<el-input v-model="queryParams.categoryName" placeholder="分类名称" clearable @keyup.enter.native="handleQuery" />

View File

@ -1,15 +1,8 @@
<template>
<div class="app-container">
<el-alert
title="请先在「文化分类」中维护分类;此处按分类维护官网卡片标题与详情。"
type="info"
:closable="false"
show-icon
class="mb12"
/>
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="88px">
<el-form-item label="所属分类" prop="categoryId">
<el-select v-model="queryParams.categoryId" placeholder="请选择分类" clearable filterable style="width: 220px" @change="handleQuery">
<el-select v-model="queryParams.categoryId" placeholder="所属分类" clearable filterable style="width: 220px">
<el-option v-for="c in categoryOptions" :key="c.categoryId" :label="c.categoryName" :value="c.categoryId" />
</el-select>
</el-form-item>
@ -29,13 +22,18 @@
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" :disabled="!queryParams.categoryId" @click="handleAdd" v-hasPermi="['huayang:cultureTag:add']">新增</el-button>
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['huayang:cultureTag:add']">新增</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="dataList" border>
<el-table-column label="ID" prop="tagId" width="72" align="center" />
<el-table-column label="所属分类" min-width="120" align="center">
<template slot-scope="scope">
<span>{{ categoryLabel(scope.row.categoryId) }}</span>
</template>
</el-table-column>
<el-table-column label="标题" prop="tagTitle" min-width="160" />
<el-table-column label="排序" prop="sortOrder" width="80" align="center" />
<el-table-column label="阅读" prop="readCount" width="80" align="center" />
@ -56,14 +54,14 @@
<el-dialog :title="dialogTitle" :visible.sync="open" width="900px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="96px">
<el-form-item label="所属分类" prop="categoryId">
<el-select v-model="form.categoryId" placeholder="请选择" filterable style="width: 100%">
<el-option v-for="c in categoryOptions" :key="c.categoryId" :label="c.categoryName" :value="c.categoryId" />
</el-select>
</el-form-item>
<el-form-item label="标题" prop="tagTitle">
<el-input v-model="form.tagTitle" maxlength="200" show-word-limit placeholder="官网卡片显示名称" />
</el-form-item>
<el-form-item label="所属分类" prop="categoryId">
<el-select v-model="form.categoryId" placeholder="请选择所属分类" filterable style="width: 100%">
<el-option v-for="c in categoryOptions" :key="c.categoryId" :label="c.categoryName" :value="c.categoryId" />
</el-select>
</el-form-item>
<el-form-item label="排序" prop="sortOrder">
<el-input-number v-model="form.sortOrder" :min="0" :max="9999" controls-position="right" />
</el-form-item>
@ -73,7 +71,7 @@
</el-radio-group>
</el-form-item>
<el-form-item label="封面图" prop="coverImage">
<image-upload v-model="form.coverImage" :limit="1" />
<image-upload v-model="form.coverImage" :limit="1" spec="文化内容封面,建议 1200×800px 或 3:2,主体居中,单张不超过 5MB" />
</el-form-item>
<el-form-item label="详情内容" prop="tagContent">
<editor v-model="form.tagContent" :min-height="280" />
@ -115,7 +113,7 @@ export default {
},
form: {},
rules: {
categoryId: [{ required: true, message: '请选择分类', trigger: 'change' }],
categoryId: [{ required: true, message: '请选择所属分类', trigger: 'change' }],
tagTitle: [{ required: true, message: '标题不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
}
@ -133,16 +131,14 @@ export default {
})
},
getList() {
if (!this.queryParams.categoryId) {
this.dataList = []
this.total = 0
this.loading = false
return
}
this.loading = true
listCultureTag(this.queryParams).then((res) => {
this.dataList = res.rows
this.total = res.total
}).catch(() => {
this.dataList = []
this.total = 0
}).finally(() => {
this.loading = false
})
},
@ -158,7 +154,7 @@ export default {
resetFormData() {
this.form = {
tagId: undefined,
categoryId: this.queryParams.categoryId,
categoryId: undefined,
tagTitle: undefined,
tagContent: undefined,
coverImage: undefined,
@ -168,13 +164,13 @@ export default {
}
this.resetForm('form')
},
categoryLabel(categoryId) {
if (categoryId == null || categoryId === '') return '-'
const c = this.categoryOptions.find((x) => String(x.categoryId) === String(categoryId))
return c ? c.categoryName : categoryId
},
handleAdd() {
if (!this.queryParams.categoryId) {
this.$modal.msgWarning('请先选择「所属分类」')
return
}
this.resetFormData()
this.form.categoryId = this.queryParams.categoryId
this.open = true
this.dialogTitle = '新增文化详情'
},

View File

@ -2,16 +2,9 @@
<div class="app-container">
<el-card>
<div slot="header">华阳文化 · 页顶横幅图</div>
<el-alert
type="info"
:closable="false"
show-icon
class="mb12"
title="此处设置官网「华阳文化」页面顶部通栏大图(导航下方横幅);保存后前台刷新页面即可生效。"
/>
<el-form label-width="120px">
<el-form-item label="横幅图片">
<image-upload v-model="form.backgroundImage" :limit="1" />
<image-upload v-model="form.backgroundImage" :limit="1" spec="文化页顶横幅,建议 1920×520px 或 16:5,主体居中,单张不超过 5MB" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submit" v-hasPermi="['huayang:cultureBanner:edit']">保存</el-button>

View File

@ -16,6 +16,9 @@
<el-form-item label="描述文字" v-if="showText">
<div v-html="mainForm.sectionText || '暂无文字'"></div>
</el-form-item>
<el-form-item v-if="sectionUsageTip">
<p class="form-tip">{{ sectionUsageTip }}</p>
</el-form-item>
</el-form>
</el-card>
@ -26,23 +29,6 @@
<el-button style="float: right; padding: 3px 0" type="primary" icon="el-icon-plus" size="mini" @click="handleAdd">新增子项</el-button>
</div>
<el-alert
v-if="editableSectionName && !showItemIconField"
type="info"
:closable="false"
show-icon
class="item-list-tip"
title="子项对应首页「关于华阳」右侧四张数据卡:「标题」填主数字与单位,中间用竖线分隔,如 1992|年、60000|㎡、10|+;「描述」填卡片底部说明文字;「图片」为卡片右侧图标。"
/>
<el-alert
v-if="showItemIconField"
type="info"
:closable="false"
show-icon
class="item-list-tip"
title="对应首页「华阳七道鲜」每一竖列:主背景图=整列大图;小图标=大字上方的白色图标;大字标题=单列主字(如「选」);底部说明=小字文案。"
/>
<el-table v-loading="loading" :data="itemList">
<el-table-column label="主图" align="center" prop="itemImage" width="100">
<template slot-scope="scope">
@ -78,11 +64,11 @@
<el-input
v-model="mainForm.sectionName"
:disabled="!editableSectionName"
:placeholder="editableSectionName ? (showItemIconField ? '如:华阳七道鲜' : '前台「关于华阳」大标题,如:关于华阳') : ''"
:placeholder="editableSectionName ? (showItemIconField ? '请输入模块主标题' : '前台「关于华阳」大标题,如:关于华阳') : ''"
/>
</el-form-item>
<el-form-item label="背景图" v-if="showBg">
<image-upload v-model="mainForm.backgroundImage" :limit="1" :drag="false"/>
<image-upload v-model="mainForm.backgroundImage" :limit="1" :drag="false" :spec="mainImageSpec"/>
</el-form-item>
<el-form-item label="描述文字" v-if="showText">
<editor v-model="mainForm.sectionText" :min-height="192"/>
@ -101,21 +87,23 @@
<el-form-item :label="showItemIconField ? '大字标题' : '标题'" prop="itemTitle">
<el-input
v-model="form.itemTitle"
:placeholder="editableSectionName ? '主数字|单位,如 1992|年、60000|㎡、10|+' : (showItemIconField ? '如:选、留、测(单列主字)' : '请输入标题')"
:placeholder="itemTitlePlaceholder"
/>
<p v-if="itemTitleTip" class="form-tip">{{ itemTitleTip }}</p>
</el-form-item>
<el-form-item :label="showItemIconField ? '底部说明' : '描述'" prop="itemDesc">
<el-input
v-model="form.itemDesc"
type="textarea"
:placeholder="editableSectionName ? '卡片底部说明,如 公司成立时间' : (showItemIconField ? '该列底部小字说明' : '请输入内容')"
:placeholder="itemDescPlaceholder"
/>
<p v-if="itemDescTip" class="form-tip">{{ itemDescTip }}</p>
</el-form-item>
<el-form-item :label="showItemIconField ? '主背景图' : '图片'" prop="itemImage">
<image-upload v-model="form.itemImage"/>
<image-upload v-model="form.itemImage" :limit="itemImageLimit" :spec="itemImageSpec"/>
</el-form-item>
<el-form-item v-if="showItemIconField" label="小图标" prop="itemIcon">
<image-upload v-model="form.itemIcon" :limit="1" :drag="false"/>
<image-upload v-model="form.itemIcon" :limit="1" :drag="false" spec="列顶小图标,建议 96×96px 或 SVG/PNG 透明底,单张不超过 1MB"/>
</el-form-item>
<el-form-item v-if="!editableSectionName" label="链接" prop="itemLink">
<el-input v-model="form.itemLink" placeholder="请输入跳转链接" />
@ -176,7 +164,7 @@ export default {
type: String,
default: "slide"
},
/** 为 true 时子项多一个「小图标」字段(华阳七道鲜列顶图标) */
/** 为 true 时子项多一个「小图标」字段(列顶图标) */
showItemIconField: {
type: Boolean,
default: false
@ -199,6 +187,56 @@ export default {
computed: {
hasMainConfig() {
return this.showBg || this.showText || this.editableSectionName;
},
mainImageSpec() {
if (this.sectionCode === 'section4') {
return '首页「华阳文化」整屏背景,建议 1920×1080px,主体避开中部文字和卡片区域'
}
return '板块背景图,建议 1920×900px 或 16:9,主体居中,单张不超过 5MB'
},
itemImageSpec() {
if (this.sectionCode === 'section1') {
return '首页轮播图,建议 1920×900px 或 16:9;可上传多张,按顺序轮播'
}
if (this.sectionCode === 'section4') {
return '竖列卡片主图,建议 420×560px 或 3:4,主体居中,单张不超过 5MB'
}
if (this.sectionCode === 'section2') {
return '关于华阳数据卡片图,建议 600×420px 或 4:3,单张不超过 5MB'
}
return '板块子项图片,建议 800×600px 或按页面展示比例上传,单张不超过 5MB'
},
itemImageLimit() {
return this.sectionCode === 'section1' ? 8 : 1
},
sectionUsageTip() {
if (this.sectionCode === 'section2') {
return '首页「关于华阳」四宫格:标题填写“数字|单位”,描述填写下方标签。'
}
if (this.sectionCode === 'section4') {
return '首页「华阳文化」竖列卡片:大字只填单字,底部说明填写对应短语。'
}
return ''
},
itemTitlePlaceholder() {
if (this.sectionCode === 'section2') return '如:1992|年、60000|㎡、10|+'
if (this.sectionCode === 'section4') return '如:选、留、测、藏、制'
return this.editableSectionName ? '' : '请输入标题'
},
itemDescPlaceholder() {
if (this.sectionCode === 'section2') return '如:公司成立时间、占地面积、标准化加工车间'
if (this.sectionCode === 'section4') return '如:深海鲜冻海珍、急速冻鲜加工'
return this.editableSectionName ? '' : '请输入内容'
},
itemTitleTip() {
if (this.sectionCode === 'section2') return '竖线前是滚动数字,竖线后是单位。'
if (this.sectionCode === 'section4') return '前台卡片中部展示的大字,建议保持单字。'
return ''
},
itemDescTip() {
if (this.sectionCode === 'section2') return '前台显示在数字下方,例如“公司成立时间”。'
if (this.sectionCode === 'section4') return '前台显示在卡片底部,不再填写“华阳/大连/食品”等占位词。'
return ''
}
},
created() {
@ -300,9 +338,6 @@ export default {
<style scoped>
.mb20 { margin-bottom: 20px; }
.item-list-tip {
margin-bottom: 12px;
}
.form-tip {
margin-top: 8px;
color: #909399;

View File

@ -1,12 +1,12 @@
<template>
<fixed-section-manager
section-code="section4"
:section-id="4"
:show-bg="true"
:show-text="false"
<fixed-section-manager
section-code="section4"
:section-id="4"
:show-bg="true"
:show-text="false"
:editable-section-name="true"
:show-item-icon-field="true"
list-title="华阳七道鲜 · 竖列卡片"
list-title="竖列卡片"
item-type="card"
/>
</template>

View File

@ -1,29 +1,151 @@
<template>
<div class="app-container footer-console">
<el-alert
type="info"
:closable="false"
show-icon
class="mb16"
title="站点参数(页脚相关)"
description="备案、SEO、外链及页脚红条文案等在此按配置键维护(与门户 /site/config/public/list 对应)。"
/>
<site-config-manage :exclude-config-keys="excludeConfigKeys" />
<el-card class="mb16" shadow="never">
<div slot="header" class="card-header">
<span>快捷设置</span>
</div>
<el-form label-width="140px" size="small">
<el-form-item label="底部导航背景图">
<image-upload v-model="quickConfig.footer_nav_bg" :limit="1" spec="页脚整体背景,建议 1920×640px 或 3:1,画面偏暗/留白适合叠加文字" />
<div class="hint">作用于门户页脚整体背景(.s_tmpl_footer)。</div>
</el-form-item>
<el-form-item label="版权文案">
<el-input v-model="quickConfig.site_copyright" />
</el-form-item>
<el-form-item label="备案号">
<el-input v-model="quickConfig.site_icp" />
</el-form-item>
<el-form-item label="公司名称">
<el-input v-model="quickConfig.footer_company_name" />
</el-form-item>
<el-form-item label="服务热线">
<el-input v-model="quickConfig.contact_phone" />
</el-form-item>
<el-form-item label="E-mail">
<el-input v-model="quickConfig.contact_email" />
</el-form-item>
<el-form-item label="地址">
<el-input v-model="quickConfig.contact_address" />
</el-form-item>
<el-form-item label="隐私政策文案">
<el-input v-model="quickConfig.footer_policy_text" />
</el-form-item>
<el-form-item label="隐私政策链接">
<el-input v-model="quickConfig.footer_policy_link" />
</el-form-item>
<el-form-item label="SEO文案">
<el-input v-model="quickConfig.footer_seo_text" />
</el-form-item>
<el-form-item label="SEO链接">
<el-input v-model="quickConfig.footer_seo_link" />
</el-form-item>
<el-form-item label="网址建设文案">
<el-input v-model="quickConfig.footer_builder_text" />
</el-form-item>
<el-form-item label="网址建设链接">
<el-input v-model="quickConfig.footer_builder_link" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving" @click="saveQuickConfig">保存</el-button>
<el-button :disabled="saving" @click="reloadQuickConfig">刷新</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script>
import SiteConfigManage from '@/views/huayang/config/index'
import { listConfig, addConfig, updateConfig } from '@/api/huayang/config'
export default {
name: 'HuayangFooterConsole',
components: {
SiteConfigManage
},
data() {
return {
// 「联系我们」页面专属字段,不应出现在页脚一站式配置中,避免与独立页面管理冲突
excludeConfigKeys: ['contact_top_image', 'contact_map_image', 'contact_content']
quickConfig: {
footer_nav_bg: '',
site_copyright: '',
site_icp: '',
footer_company_name: '',
contact_phone: '',
contact_email: '',
contact_address: '',
footer_policy_text: '',
footer_policy_link: '',
footer_seo_text: '',
footer_seo_link: '',
footer_builder_text: '',
footer_builder_link: ''
},
quickConfigIdMap: {
footer_nav_bg: null,
site_copyright: null,
site_icp: null,
footer_company_name: null,
contact_phone: null,
contact_email: null,
contact_address: null,
footer_policy_text: null,
footer_policy_link: null,
footer_seo_text: null,
footer_seo_link: null,
footer_builder_text: null,
footer_builder_link: null
},
saving: false
}
},
created() {
this.reloadQuickConfig()
},
methods: {
async reloadQuickConfig() {
try {
const res = await listConfig({ pageNum: 1, pageSize: 9999 })
const rows = Array.isArray(res?.rows) ? res.rows : []
const keys = Object.keys(this.quickConfig)
keys.forEach((key) => {
const hit = rows.find((r) => String(r?.configKey || '').trim() === key)
this.quickConfig[key] = hit?.configValue || ''
this.quickConfigIdMap[key] = hit?.configId ?? null
})
} catch (e) {
// ignore
}
},
async upsertConfig(configKey, configValue, configName, remark) {
const payload = { configKey, configValue, configName, remark }
const existingId = this.quickConfigIdMap[configKey]
if (existingId != null) {
await updateConfig({ ...payload, configId: existingId })
return
}
const res = await addConfig(payload)
if (res) {
await this.reloadQuickConfig()
}
},
async saveQuickConfig() {
this.saving = true
try {
await this.upsertConfig('footer_nav_bg', this.quickConfig.footer_nav_bg, '底部导航栏背景图', '作用于前端 .s_tmpl_footer;支持 none/transparent 清除')
await this.upsertConfig('site_copyright', this.quickConfig.site_copyright, '页脚版权文案', '作用于页脚版权区域')
await this.upsertConfig('site_icp', this.quickConfig.site_icp, '页脚备案号', '作用于页脚备案号区域')
await this.upsertConfig('footer_company_name', this.quickConfig.footer_company_name, '页脚公司名称', '作用于页脚公司信息标题')
await this.upsertConfig('contact_phone', this.quickConfig.contact_phone, '联系电话', '作用于全站联系方式(含页脚)')
await this.upsertConfig('contact_email', this.quickConfig.contact_email, '联系邮箱', '作用于全站联系方式(含页脚)')
await this.upsertConfig('contact_address', this.quickConfig.contact_address, '联系地址', '作用于全站联系方式(含页脚)')
await this.upsertConfig('footer_policy_text', this.quickConfig.footer_policy_text, '页脚隐私政策文案', '作用于页脚政策链接文案')
await this.upsertConfig('footer_policy_link', this.quickConfig.footer_policy_link, '页脚隐私政策链接', '作用于页脚政策链接 URL')
await this.upsertConfig('footer_seo_text', this.quickConfig.footer_seo_text, '页脚SEO文案', '作用于页脚 SEO 链接文案')
await this.upsertConfig('footer_seo_link', this.quickConfig.footer_seo_link, '页脚SEO链接', '作用于页脚 SEO 链接 URL')
await this.upsertConfig('footer_builder_text', this.quickConfig.footer_builder_text, '页脚网址建设文案', '作用于页脚网址建设文案')
await this.upsertConfig('footer_builder_link', this.quickConfig.footer_builder_link, '页脚网址建设链接', '作用于页脚网址建设链接 URL')
this.$modal?.msgSuccess?.('保存成功')
} catch (e) {
this.$modal?.msgError?.('保存失败')
} finally {
this.saving = false
}
}
}
}
@ -33,4 +155,20 @@ export default {
.mb16 {
margin-bottom: 16px;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.hint {
margin-top: 6px;
color: #909399;
font-size: 12px;
line-height: 1.5;
}
.hint code {
padding: 0 4px;
background: #f5f7fa;
border-radius: 4px;
}
</style>

View File

@ -104,10 +104,10 @@
<el-input v-model="form.bannerTitle" placeholder="请输入标题" />
</el-form-item>
<el-form-item label="PC图片" prop="bannerImage">
<image-upload v-model="form.bannerImage"/>
<image-upload v-model="form.bannerImage" :limit="1" spec="PC 首页首屏横幅,建议 1920×900px 或 16:9,主体居中,单张不超过 5MB" />
</el-form-item>
<el-form-item label="移动端图片" prop="mobileImage">
<image-upload v-model="form.mobileImage"/>
<image-upload v-model="form.mobileImage" :limit="1" spec="移动端首屏横幅,建议 750×1100px 或 3:4,重要内容居中上方,单张不超过 5MB" />
</el-form-item>
<el-form-item label="跳转链接" prop="linkUrl">
<el-input v-model="form.linkUrl" placeholder="请输入跳转链接" />

View File

@ -103,7 +103,7 @@
<el-input v-model="form.featureDesc" type="textarea" placeholder="请输入业务简述" />
</el-form-item>
<el-form-item label="图标/图片" prop="featureIcon">
<image-upload v-model="form.featureIcon"/>
<image-upload v-model="form.featureIcon" :limit="1" spec="首页业务图标/配图,图标建议 160×160px 透明底;配图建议 600×420px 或 4:3" />
</el-form-item>
<el-form-item label="跳转链接" prop="linkUrl">
<el-input v-model="form.linkUrl" placeholder="请输入跳转链接" />

View File

@ -17,16 +17,6 @@
>打开门户首页</el-button>
<span class="portal-hint">预览地址来自环境变量 VUE_APP_PORTAL_URL</span>
</div>
<div class="quick-links">
<span class="quick-label">独立菜单:</span>
<el-button
v-for="l in quickLinks"
:key="l.path"
size="small"
plain
@click="go(l.path)"
>{{ l.label }}</el-button>
</div>
</el-row>
<el-tabs v-model="activeTab" type="border-card">
@ -35,7 +25,7 @@
<el-tab-pane label="首页轮播图" name="s1" lazy><section1-panel v-if="activeTab === 'fixed' && fixedTab === 's1'" /></el-tab-pane>
<el-tab-pane label="首页核心业务" name="s2" lazy><section2-panel v-if="activeTab === 'fixed' && fixedTab === 's2'" /></el-tab-pane>
<el-tab-pane label="展示区3-背景轮播" name="s3" lazy><section3-panel v-if="activeTab === 'fixed' && fixedTab === 's3'" /></el-tab-pane>
<el-tab-pane label="华阳七道鲜(板块四)" name="s4" lazy><section4-panel v-if="activeTab === 'fixed' && fixedTab === 's4'" /></el-tab-pane>
<el-tab-pane label="板块四" name="s4" lazy><section4-panel v-if="activeTab === 'fixed' && fixedTab === 's4'" /></el-tab-pane>
<el-tab-pane label="展示区5-背景文字" name="s5" lazy><section5-panel v-if="activeTab === 'fixed' && fixedTab === 's5'" /></el-tab-pane>
</el-tabs>
</el-tab-pane>
@ -62,17 +52,7 @@ export default {
data() {
return {
activeTab: 'fixed',
fixedTab: 's1',
quickLinks: [
{ label: '固定板块-首页轮播图', path: '/huayangSite/fixedSection/section1' },
{ label: '固定板块-首页核心业务', path: '/huayangSite/fixedSection/section2' },
{ label: '固定板块-展示区3', path: '/huayangSite/fixedSection/section3' },
{ label: '固定板块-华阳七道鲜', path: '/huayangSite/fixedSection/section4' },
{ label: '产品管理', path: '/huayangSite/productCenter/product' },
{ label: '新闻管理', path: '/huayangSite/newsCenter/news' },
{ label: '公司简介分块', path: '/huayangSite/brand/companySection' },
{ label: '翻译管理', path: '/huayangSite/i18nCenter/translation' }
]
fixedTab: 's1'
}
},
created() {
@ -87,12 +67,9 @@ export default {
return 'fixed'
},
openPortal() {
const base = (process.env.VUE_APP_PORTAL_URL || 'http://localhost:5173').replace(/\/$/, '')
const base = (process.env.VUE_APP_PORTAL_URL || 'http://192.168.1.159:7002').replace(/\/$/, '')
const url = `${base}/?page=home`
window.open(url, '_blank', 'noopener,noreferrer')
},
go(path) {
this.$router.push(path).catch(() => {})
}
}
}
@ -114,14 +91,6 @@ export default {
font-size: 12px;
color: #909399;
}
.quick-links {
text-align: right;
}
.quick-label {
font-size: 12px;
color: #606266;
margin-right: 6px;
}
.nested-fixed-tabs {
margin-top: 4px;
}

View File

@ -1,162 +0,0 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="88px">
<el-form-item label="申请人" prop="applyName">
<el-input v-model="queryParams.applyName" placeholder="请输入申请人姓名" clearable />
</el-form-item>
<el-form-item label="处理状态" prop="applyStatus">
<el-select v-model="queryParams.applyStatus" placeholder="处理状态" clearable>
<el-option v-for="dict in dict.type.huayang_apply_status" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete"
v-hasPermi="['huayang:joinApply:remove']">删除</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="applyList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="编号" align="center" prop="applyId" width="80" />
<el-table-column label="申请人" align="center" prop="applyName" width="100" />
<el-table-column label="联系电话" align="center" prop="applyPhone" width="130" />
<el-table-column label="公司/店铺" align="center" prop="applyCompany" :show-overflow-tooltip="true" />
<el-table-column label="所在地区" align="center" prop="applyArea" width="120" />
<el-table-column label="合作意向" align="center" prop="applyIntent" :show-overflow-tooltip="true" />
<el-table-column label="状态" align="center" prop="applyStatus" width="100">
<template slot-scope="scope">
<dict-tag :options="dict.type.huayang_apply_status" :value="scope.row.applyStatus" />
</template>
</el-table-column>
<el-table-column label="申请时间" align="center" prop="createTime" width="160" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="180">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-view" @click="handleView(scope.row)"
v-hasPermi="['huayang:joinApply:query']">查看</el-button>
<el-button size="mini" type="text" icon="el-icon-check" @click="handleProcess(scope.row)"
v-hasPermi="['huayang:joinApply:handle']" v-if="scope.row.applyStatus === '0'">处理</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
v-hasPermi="['huayang:joinApply:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
<!-- 查看/处理对话框 -->
<el-dialog :title="dialogTitle" :visible.sync="openDialog" width="600px" append-to-body>
<el-form ref="dialogForm" :model="dialogForm" label-width="120px">
<el-form-item label="申请人">{{ dialogForm.applyName }}</el-form-item>
<el-form-item label="联系电话">{{ dialogForm.applyPhone }}</el-form-item>
<el-form-item label="公司/店铺">{{ dialogForm.applyCompany || '-' }}</el-form-item>
<el-form-item label="所在地区">{{ dialogForm.applyArea || '-' }}</el-form-item>
<el-form-item label="合作意向">{{ dialogForm.applyIntent || '-' }}</el-form-item>
<el-form-item label="申请时间">{{ dialogForm.createTime }}</el-form-item>
<el-form-item label="处理状态">
<dict-tag :options="dict.type.huayang_apply_status" :value="dialogForm.applyStatus" />
</el-form-item>
<el-form-item label="处理备注" v-if="dialogForm.handleRemark">{{ dialogForm.handleRemark }}</el-form-item>
<el-form-item label="处理状态" v-if="dialogForm.applyStatus === '0'">
<el-radio-group v-model="dialogForm.applyStatusNew">
<el-radio label="1">已联系</el-radio>
<el-radio label="2">已拒绝</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="处理备注" v-if="dialogForm.applyStatus === '0'">
<el-input v-model="dialogForm.handleRemark" type="textarea" :rows="3" placeholder="请输入处理备注" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitProcess" v-if="dialogForm.applyStatus === '0'">确 定</el-button>
<el-button @click="openDialog = false">关 闭</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listJoinApply, handleJoinApply, deleteJoinApply } from '@/api/huayang/joinApply'
export default {
name: 'HuayangJoinApply',
dicts: ['huayang_apply_status'],
data() {
return {
loading: true,
showSearch: true,
ids: [],
multiple: true,
total: 0,
applyList: [],
queryParams: { pageNum: 1, pageSize: 10, applyName: undefined, applyStatus: undefined },
openDialog: false,
dialogTitle: '',
dialogForm: {}
}
},
created() {
this.getList()
},
methods: {
getList() {
this.loading = true
listJoinApply(this.queryParams).then(response => {
this.applyList = response.rows
this.total = response.total
this.loading = false
})
},
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
resetQuery() {
this.resetForm('queryForm')
this.handleQuery()
},
handleSelectionChange(selection) {
this.ids = selection.map(item => item.applyId)
this.multiple = !selection.length
},
handleView(row) {
this.dialogTitle = '申请详情'
this.dialogForm = { ...row, applyStatusNew: '1', handleRemark: '' }
this.openDialog = true
},
handleProcess(row) {
this.dialogTitle = '处理申请'
this.dialogForm = { ...row, applyStatusNew: '1', handleRemark: '' }
this.openDialog = true
},
submitProcess() {
handleJoinApply({
applyId: this.dialogForm.applyId,
applyStatus: this.dialogForm.applyStatusNew,
handleRemark: this.dialogForm.handleRemark,
updateBy: this.$store.state.user.name
}).then(() => {
this.$modal.msgSuccess('处理成功')
this.openDialog = false
this.getList()
})
},
handleDelete(row) {
const applyIds = row.applyId || this.ids
this.$modal.confirm('是否确认删除选中的申请数据项?').then(() => {
return deleteJoinApply(applyIds)
}).then(() => {
this.getList()
this.$modal.msgSuccess('删除成功')
}).catch(() => {})
}
}
}
</script>

View File

@ -1,439 +0,0 @@
<template>
<div class="app-container">
<el-alert
type="info"
:closable="false"
show-icon
class="mb8"
:title="activeTab === 'join_rules' ? joinRulesAlert : defaultAlert"
/>
<el-alert
v-hasPermi="['huayang:navigation:list']"
type="info"
:closable="false"
show-icon
class="mb8"
title="门户招商加盟页底部导航栏请在「页脚一站式配置」或「导航管理(底部导航)」中维护。"
/>
<el-tabs v-model="activeTab" @tab-click="onTabClick">
<el-tab-pane label="加盟细则" name="join_rules" />
<el-tab-pane label="加盟流程" name="join_process" />
<el-tab-pane label="加盟资质" name="join_qualification" />
</el-tabs>
<!-- 加盟细则:顶部图 + 优势区(文+图) + 双栏 -->
<el-form
v-if="activeTab === 'join_rules'"
ref="form"
:model="form"
:rules="joinRulesFormRules"
label-width="128px"
class="join-form"
>
<el-form-item label="板块标题" prop="sectionTitle">
<el-input v-model="form.sectionTitle" maxlength="200" show-word-limit placeholder="后台识别用,如:招商加盟-加盟细则" />
</el-form-item>
<el-card shadow="never" class="block-card">
<div slot="header" class="card-header">页顶横幅</div>
<el-form-item label="顶部横幅图" prop="bannerImage">
<image-upload v-model="form.bannerImage" :limit="1" />
<p class="form-tip">对应整页最上方通栏视觉(握手/品牌图等)。</p>
</el-form-item>
</el-card>
<el-card shadow="never" class="block-card">
<div slot="header" class="card-header">加盟优势 + 右侧配图</div>
<el-form-item label="右侧主图">
<image-upload v-model="form.contentImage1" :limit="1" />
<p class="form-tip">与左侧红色「加盟优势」文案并排展示的主图(如菜品图)。</p>
</el-form-item>
<el-form-item label="优势区标题">
<el-input v-model="joinRulesDetail.advantageTitle" maxlength="100" show-word-limit placeholder="默认:加盟优势" />
</el-form-item>
<el-form-item label="优势区正文">
<editor v-model="joinRulesDetail.advantageBody" :min-height="220" />
<p class="form-tip">支持列表、加粗等;门户左侧红底区域内容。</p>
</el-form-item>
</el-card>
<el-card shadow="never" class="block-card">
<div slot="header" class="card-header">下方双栏说明</div>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="左栏标题">
<el-input v-model="joinRulesDetail.col1Title" maxlength="200" show-word-limit />
</el-form-item>
<el-form-item label="左栏正文">
<editor v-model="joinRulesDetail.col1Body" :min-height="200" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="右栏标题">
<el-input v-model="joinRulesDetail.col2Title" maxlength="200" show-word-limit />
</el-form-item>
<el-form-item label="右栏正文">
<editor v-model="joinRulesDetail.col2Body" :min-height="200" />
</el-form-item>
</el-col>
</el-row>
</el-card>
<el-form-item label="排序" prop="sortOrder">
<el-input-number v-model="form.sortOrder" :min="0" :max="9999" controls-position="right" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio v-for="dict in dict.type.sys_normal_disable" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving" @click="submitForm" v-hasPermi="['huayang:joinPageContent:edit']">保存当前标签</el-button>
<el-button @click="loadCurrent" v-hasPermi="['huayang:joinPageContent:query']">重新加载</el-button>
</el-form-item>
</el-form>
<!-- 加盟流程 / 加盟资质:沿用整块富文本 + 多图 -->
<el-form v-else ref="form" :model="form" :rules="genericFormRules" label-width="120px" class="join-form">
<el-form-item label="板块标题" prop="sectionTitle">
<el-input v-model="form.sectionTitle" maxlength="200" show-word-limit placeholder="后台列表与识别用标题" />
</el-form-item>
<el-form-item label="正文内容" prop="sectionContent">
<editor v-model="form.sectionContent" :min-height="320" />
<p class="form-tip">
{{
activeTab === 'join_process'
? '可排版标题、列表等;保存后门户「加盟流程」区块展示。本页仅维护正文与配图一。'
: activeTab === 'join_qualification'
? '可排版标题、列表等;保存后门户「加盟资质」区块展示。不含顶部横幅与配图三,请用正文与配图一、二排版。'
: '可排版标题、列表等;保存后由门户端按标识码注入对应区域。'
}}
</p>
</el-form-item>
<el-form-item label="配图一">
<image-upload v-model="form.contentImage1" :limit="1" />
</el-form-item>
<template v-if="activeTab === 'join_qualification'">
<el-form-item label="配图二">
<image-upload v-model="form.contentImage2" :limit="1" />
</el-form-item>
</template>
<el-form-item label="排序" prop="sortOrder">
<el-input-number v-model="form.sortOrder" :min="0" :max="9999" controls-position="right" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio v-for="dict in dict.type.sys_normal_disable" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving" @click="submitForm" v-hasPermi="['huayang:joinPageContent:edit']">保存当前标签</el-button>
<el-button @click="loadCurrent" v-hasPermi="['huayang:joinPageContent:query']">重新加载</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import {
getCompanySectionByCode,
addCompanySection,
updateCompanySection
} from '@/api/huayang/companySection'
const TAB_META = {
join_rules: { title: '招商加盟-加盟细则', sortOrder: 80 },
join_process: { title: '招商加盟-加盟流程', sortOrder: 81 },
join_qualification: { title: '招商加盟-加盟资质', sortOrder: 82 }
}
const JOIN_RULES_JSON_VERSION = 1
function stripHtml(html) {
return (html || '').replace(/<[^>]+>/g, '').replace(/&nbsp;/gi, ' ').trim()
}
export default {
name: 'HuayangJoinPageContent',
dicts: ['sys_normal_disable'],
data() {
return {
defaultAlert:
'维护门户「招商加盟」页三个子块。流程/资质支持整块富文本与配图;数据保存在公司简介板块表(固定 section_code)。',
joinRulesAlert:
'「加盟细则」拆分为:页顶横幅图、加盟优势区(标题+正文)与右侧主图、下方双栏(各标题+正文)。保存时同时写入结构化 JSON(extraData)与合并 HTML(sectionContent),便于门户读取。',
activeTab: 'join_rules',
saving: false,
form: {},
joinRulesDetail: {
advantageTitle: '加盟优势',
advantageBody: '<p></p>',
col1Title: '互动直分销模式解读——互动',
col1Body: '<p></p>',
col2Title: '互动直分销模式解读——分销',
col2Body: '<p></p>'
},
genericFormRules: {
sectionTitle: [{ required: true, message: '板块标题不能为空', trigger: 'blur' }],
sectionContent: [
{
required: true,
validator: (rule, value, callback) => {
if (!stripHtml(value)) {
callback(new Error('正文不能为空'))
} else {
callback()
}
},
trigger: 'blur'
}
],
status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
},
joinRulesFormRules: {
sectionTitle: [{ required: true, message: '板块标题不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
}
}
},
created() {
this.resetFormShell()
this.loadCurrent()
},
methods: {
defaultJoinRulesDetail() {
return {
advantageTitle: '加盟优势',
advantageBody: '<p></p>',
col1Title: '互动直分销模式解读——互动',
col1Body: '<p></p>',
col2Title: '互动直分销模式解读——分销',
col2Body: '<p></p>'
}
},
onTabClick() {
this.loadCurrent()
},
resetFormShell() {
this.joinRulesDetail = this.defaultJoinRulesDetail()
this.form = {
sectionId: undefined,
sectionCode: this.activeTab,
sectionTitle: (TAB_META[this.activeTab] && TAB_META[this.activeTab].title) || '',
sectionContent: '<p></p>',
extraData: '',
bannerImage: '',
contentImage1: '',
contentImage2: '',
contentImage3: '',
sortOrder: (TAB_META[this.activeTab] && TAB_META[this.activeTab].sortOrder) || 0,
status: '0'
}
},
normalizeSection(d) {
if (!d) return {}
const pick = (camel, snake) => {
const v = d[camel] !== undefined && d[camel] !== null ? d[camel] : d[snake]
return v === undefined || v === null ? '' : String(v)
}
return {
...d,
extraData: pick('extraData', 'extra_data'),
bannerImage: pick('bannerImage', 'banner_image'),
contentImage1: pick('contentImage1', 'content_image1'),
contentImage2: pick('contentImage2', 'content_image2'),
contentImage3: pick('contentImage3', 'content_image3')
}
},
applyJoinRulesFromRow(extraDataRaw, legacySectionContent) {
const base = this.defaultJoinRulesDetail()
if (extraDataRaw && String(extraDataRaw).trim()) {
try {
const o = JSON.parse(extraDataRaw)
if (o && typeof o === 'object') {
if (o.advantageTitle != null) base.advantageTitle = String(o.advantageTitle)
if (o.advantageBody != null) base.advantageBody = String(o.advantageBody)
if (o.col1Title != null) base.col1Title = String(o.col1Title)
if (o.col1Body != null) base.col1Body = String(o.col1Body)
if (o.col2Title != null) base.col2Title = String(o.col2Title)
if (o.col2Body != null) base.col2Body = String(o.col2Body)
}
} catch (e) {
/* 保持默认 */
}
} else if (stripHtml(legacySectionContent)) {
base.advantageBody = legacySectionContent
}
this.joinRulesDetail = base
},
escapeHtmlTitle(s) {
return String(s || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/"/g, '&quot;')
},
buildJoinRulesMergedHtml(d) {
const t1 = this.escapeHtmlTitle(d.advantageTitle)
const h1 = this.escapeHtmlTitle(d.col1Title)
const h2 = this.escapeHtmlTitle(d.col2Title)
return (
`<div class="hy-join-rules" data-hy-join-rules="1">` +
`<section class="hy-adv-block"><h3 class="hy-adv-title">${t1}</h3>` +
`<div class="hy-adv-body">${d.advantageBody || ''}</div></section>` +
`<div class="hy-two-col"><div class="hy-col hy-col1"><h4 class="hy-col-title">${h1}</h4>` +
`<div class="hy-col-body">${d.col1Body || ''}</div></div>` +
`<div class="hy-col hy-col2"><h4 class="hy-col-title">${h2}</h4>` +
`<div class="hy-col-body">${d.col2Body || ''}</div></div></div></div>`
)
},
buildJoinRulesExtraJson() {
return JSON.stringify({
version: JOIN_RULES_JSON_VERSION,
advantageTitle: this.joinRulesDetail.advantageTitle,
advantageBody: this.joinRulesDetail.advantageBody,
col1Title: this.joinRulesDetail.col1Title,
col1Body: this.joinRulesDetail.col1Body,
col2Title: this.joinRulesDetail.col2Title,
col2Body: this.joinRulesDetail.col2Body
})
},
loadCurrent() {
const code = this.activeTab
this.resetFormShell()
this.form.sectionCode = code
this.form.sortOrder = (TAB_META[code] && TAB_META[code].sortOrder) || 0
this.form.sectionTitle = (TAB_META[code] && TAB_META[code].title) || code
getCompanySectionByCode(code)
.then((res) => {
const row = res && res.data
if (row && row.sectionId != null) {
this.form = this.normalizeSection(row)
this.form.sectionCode = code
if (code === 'join_rules') {
this.applyJoinRulesFromRow(this.form.extraData, this.form.sectionContent)
}
} else {
this.form.sectionCode = code
this.form.sectionTitle = (TAB_META[code] && TAB_META[code].title) || code
this.form.sectionContent = '<p></p>'
this.form.sortOrder = (TAB_META[code] && TAB_META[code].sortOrder) || 0
if (code === 'join_rules') {
this.joinRulesDetail = this.defaultJoinRulesDetail()
}
}
if (code === 'join_process') {
this.form.bannerImage = ''
this.form.contentImage2 = ''
this.form.contentImage3 = ''
}
if (code === 'join_qualification') {
this.form.bannerImage = ''
this.form.contentImage3 = ''
}
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
})
.catch(() => {
this.form.sectionCode = code
this.form.sectionTitle = (TAB_META[code] && TAB_META[code].title) || code
this.form.sectionContent = '<p></p>'
this.form.sortOrder = (TAB_META[code] && TAB_META[code].sortOrder) || 0
if (code === 'join_rules') {
this.joinRulesDetail = this.defaultJoinRulesDetail()
}
if (code === 'join_process') {
this.form.bannerImage = ''
this.form.contentImage2 = ''
this.form.contentImage3 = ''
}
if (code === 'join_qualification') {
this.form.bannerImage = ''
this.form.contentImage3 = ''
}
})
},
submitForm() {
this.$refs.form.validate((valid) => {
if (!valid) return
if (this.activeTab === 'join_rules') {
const ok =
stripHtml(this.joinRulesDetail.advantageBody) ||
stripHtml(this.joinRulesDetail.col1Body) ||
stripHtml(this.joinRulesDetail.col2Body)
if (!ok) {
this.$modal.msgError('请填写加盟优势正文,或下方至少一栏正文')
return
}
}
this.saving = true
let payload = {
...this.form,
sectionCode: this.activeTab,
bannerImage: this.form.bannerImage == null ? '' : String(this.form.bannerImage),
contentImage1: this.form.contentImage1 == null ? '' : String(this.form.contentImage1),
contentImage2: this.form.contentImage2 == null ? '' : String(this.form.contentImage2),
contentImage3: this.form.contentImage3 == null ? '' : String(this.form.contentImage3)
}
if (this.activeTab === 'join_rules') {
payload.extraData = this.buildJoinRulesExtraJson()
payload.sectionContent = this.buildJoinRulesMergedHtml(this.joinRulesDetail)
payload.contentImage2 = ''
payload.contentImage3 = ''
} else {
payload.extraData = this.form.extraData && String(this.form.extraData).trim() ? this.form.extraData : null
}
if (this.activeTab === 'join_process') {
payload.bannerImage = ''
payload.contentImage2 = ''
payload.contentImage3 = ''
}
if (this.activeTab === 'join_qualification') {
payload.bannerImage = ''
payload.contentImage3 = ''
}
const done = () => {
this.saving = false
}
if (this.form.sectionId) {
updateCompanySection(payload)
.then(() => {
this.$modal.msgSuccess('保存成功')
this.loadCurrent()
})
.finally(done)
} else {
addCompanySection(payload)
.then(() => {
this.$modal.msgSuccess('已新增并保存')
this.loadCurrent()
})
.finally(done)
}
})
}
}
}
</script>
<style scoped>
.mb8 {
margin-bottom: 8px;
}
.join-form {
max-width: 1040px;
margin-top: 16px;
}
.form-tip {
margin: 6px 0 0;
font-size: 12px;
color: #909399;
line-height: 1.5;
}
.block-card {
margin-bottom: 16px;
border-radius: 8px;
}
.card-header {
font-weight: 600;
color: #303133;
}
</style>

View File

@ -1,58 +1,34 @@
<template>
<div class="app-container">
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane label="语言列表" name="languages">
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAddLang">新增语言</el-button>
</el-col>
</el-row>
<el-table v-loading="loading" :data="languageList">
<el-table-column label="编号" align="center" prop="langId" width="80" />
<el-table-column label="语言名称" align="center" prop="langName" />
<el-table-column label="语言代码" align="center" prop="langCode" />
<el-table-column label="排序" align="center" prop="sortOrder" width="80" />
<el-table-column label="状态" align="center" prop="status" width="100">
<template slot-scope="scope">
<el-tag :type="scope.row.status === '0' ? 'success' : 'danger'">{{ scope.row.status === '0' ? '显示' : '隐藏' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdateLang(scope.row)">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDeleteLang(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-alert
title="已停用 i18n 翻译词典,仅保留语言列表配置。"
type="warning"
:closable="false"
show-icon
class="mb12"
/>
<el-table v-loading="loading" :data="languageList">
<el-table-column label="语言名称" align="center" prop="langName" />
<el-table-column label="语言代码" align="center" prop="langCode" />
<el-table-column label="排序" align="center" prop="sortOrder" width="80" />
<el-table-column label="状态" align="center" prop="status" width="100">
<template slot-scope="scope">
<el-tag :type="scope.row.status === '0' ? 'success' : 'danger'">{{ scope.row.status === '0' ? '显示' : '隐藏' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="默认" align="center" prop="isDefault" width="100">
<template slot-scope="scope">
<el-tag :type="scope.row.isDefault === '1' ? 'warning' : 'info'">{{ scope.row.isDefault === '1' ? '默认' : '否' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdateLang(scope.row)" v-hasPermi="['huayang:language:edit']">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-check" :disabled="scope.row.isDefault === '1'" @click="handleSetDefault(scope.row)" v-hasPermi="['huayang:language:edit']">设为默认</el-button>
</template>
</el-table-column>
</el-table>
<el-tab-pane label="翻译词典" name="translations">
<el-form :inline="true" size="small">
<el-form-item label="语言">
<el-select v-model="currentLangCode" placeholder="请选择语言" @change="getI18nList">
<el-option v-for="l in languageList" :key="l.langCode" :label="l.langName" :value="l.langCode" />
</el-select>
</el-form-item>
<el-form-item label="关键词">
<el-input v-model="i18nSearchKey" placeholder="输入Key进行搜索" clearable @input="filterI18n" />
</el-form-item>
<el-form-item>
<el-button type="primary" size="mini" @click="handleSaveAllI18n">保存当前页修改</el-button>
</el-form-item>
</el-form>
<el-table :data="filteredI18nList" border stripe>
<el-table-column label="翻译键位 (Key)" prop="i18nKey" width="250" />
<el-table-column label="翻译内容 (Value)">
<template slot-scope="scope">
<el-input v-model="scope.row.i18nValue" size="small" placeholder="请输入内容" />
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
<!-- 语言编辑弹窗 -->
<el-dialog :title="langTitle" :visible.sync="langOpen" width="400px" append-to-body>
<el-form ref="langForm" :model="langForm" :rules="langRules" label-width="80px">
<el-form-item label="名称" prop="langName">
@ -73,18 +49,14 @@
</template>
<script>
// 这里假设你已有对应的 api/huayang/language.js 和 api/huayang/i18n.js
import { listLanguage, updateLanguage, setDefaultLanguage } from "@/api/huayang/language";
export default {
name: "HuayangLanguage",
data() {
return {
activeTab: "languages",
loading: false,
languageList: [],
i18nList: [],
filteredI18nList: [],
currentLangCode: "zh_cn",
i18nSearchKey: "",
langOpen: false,
langTitle: "",
langForm: {},
@ -98,52 +70,39 @@ export default {
this.getList();
},
methods: {
getList() {
async getList() {
this.loading = true;
// 模拟请求
setTimeout(() => {
this.languageList = [
{ langId: 1, langName: "简体中文", langCode: "zh_cn", status: "0", sortOrder: 1 },
{ langId: 2, langName: "English", langCode: "en_us", status: "0", sortOrder: 2 },
{ langId: 3, langName: "日本語", langCode: "ja_jp", status: "0", sortOrder: 3 }
];
try {
const { rows } = await listLanguage({});
this.languageList = Array.isArray(rows) ? rows : [];
} finally {
this.loading = false;
this.getI18nList();
}, 500);
},
getI18nList() {
// 获取翻译列表逻辑...
this.i18nList = [
{ i18nKey: "common.title.home", i18nValue: this.currentLangCode === 'zh_cn' ? '首页' : 'Home' },
{ i18nKey: "common.button.submit", i18nValue: this.currentLangCode === 'zh_cn' ? '提交' : 'Submit' },
{ i18nKey: "common.placeholder.phone", i18nValue: this.currentLangCode === 'zh_cn' ? '请输入电话' : 'Enter Phone' }
];
this.filterI18n();
},
filterI18n() {
if (!this.i18nSearchKey) {
this.filteredI18nList = this.i18nList;
} else {
this.filteredI18nList = this.i18nList.filter(i => i.i18nKey.includes(this.i18nSearchKey));
}
},
handleSaveAllI18n() {
this.$modal.msgSuccess("保存成功 (模拟)");
},
handleAddLang() {
this.langForm = { sortOrder: 0 };
this.langOpen = true;
this.langTitle = "新增语言";
},
handleUpdateLang(row) {
this.langForm = { ...row };
this.langOpen = true;
this.langTitle = "修改语言";
},
submitLangForm() {
this.$modal.msgSuccess("操作成功");
async submitLangForm() {
await this.$refs["langForm"].validate();
await updateLanguage(this.langForm);
this.$modal.msgSuccess("更新成功");
this.langOpen = false;
await this.getList();
},
async handleSetDefault(row) {
await this.$modal.confirm(`确认将 ${row.langName} 设为默认语言吗?`);
await setDefaultLanguage(row.langCode);
this.$modal.msgSuccess("设置成功");
await this.getList();
}
}
};
</script>
<style scoped>
.mb12 {
margin-bottom: 12px;
}
</style>

View File

@ -1,13 +1,6 @@
<template>
<div>
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="88px">
<el-form-item label="Logo类型" prop="logoType">
<el-select v-model="queryParams.logoType" placeholder="Logo类型" clearable>
<el-option label="头部Logo" value="header" />
<el-option label="底部Logo" value="footer" />
<el-option label="版权信息" value="copyright" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="isEnabled">
<el-select v-model="queryParams.isEnabled" placeholder="状态" clearable>
<el-option label="启用" value="1" />
@ -48,13 +41,6 @@
<el-table v-loading="loading" :data="logoList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="Logo标题" align="center" prop="logoTitle" width="150" />
<el-table-column label="类型" align="center" prop="logoType" width="100">
<template slot-scope="scope">
<el-tag v-if="scope.row.logoType === 'header'" type="primary">头部Logo</el-tag>
<el-tag v-else-if="scope.row.logoType === 'footer'" type="info">底部Logo</el-tag>
<el-tag v-else type="success">版权信息</el-tag>
</template>
</el-table-column>
<el-table-column label="Logo预览" align="center" prop="logoImage" width="200">
<template slot-scope="scope">
<el-image
@ -114,13 +100,6 @@
<el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item label="Logo类型" prop="logoType">
<el-radio-group v-model="form.logoType">
<el-radio label="header">头部Logo</el-radio>
<el-radio label="footer">底部Logo</el-radio>
<el-radio label="copyright">版权信息</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="form.logoType === 'copyright' ? '版权内容' : 'Logo标题'" prop="logoTitle">
<el-input
v-model="form.logoTitle"
@ -130,7 +109,7 @@
/>
</el-form-item>
<el-form-item v-if="form.logoType !== 'copyright'" label="Logo图片" prop="logoImage">
<image-upload v-model="form.logoImage" :limit="1" />
<image-upload v-model="form.logoImage" :limit="1" spec="站点 Logo,建议透明底 PNG/SVG,宽约 240px,高 60-100px;避免留白过多" />
</el-form-item>
<el-form-item label="跳转链接" prop="logoUrl">
<el-input v-model="form.logoUrl" placeholder="请输入点击跳转链接,留空则跳转到首页" />
@ -176,12 +155,10 @@ export default {
queryParams: {
pageNum: 1,
pageSize: 10,
logoType: null,
isEnabled: null
},
form: {},
rules: {
logoType: [{ required: true, message: 'Logo类型不能为空', trigger: 'change' }],
logoTitle: [{ required: true, message: 'Logo标题不能为空', trigger: 'blur' }],
isEnabled: [{ required: true, message: '状态不能为空', trigger: 'change' }]
}
@ -239,6 +216,7 @@ export default {
const logoId = row.logoId || this.ids
getLogo(logoId).then(response => {
this.form = response.data
if (!this.form.logoType || this.form.logoType === 'footer') this.form.logoType = 'header'
this.open = true
this.title = '修改Logo配置'
})

View File

@ -12,7 +12,7 @@
>保存背景</el-button>
</div>
<p class="nav-left-bg-card__tip">配置键 <code>nav_menu_left_bg</code>。仅作用于门户「菜单列表 + 右侧配图」区域;左侧头像/Menu 窄条保持透明。填 <code>transparent</code> 表示该区域无背景图。各菜单项大图请在对应一级导航的「侧栏配图」维护。</p>
<image-upload v-model="navLeftBgConfig.configValue" :limit="1" />
<image-upload v-model="navLeftBgConfig.configValue" :limit="1" spec="主导航展开区背景,建议 900×760px 或接近菜单展开区域比例,画面不宜过亮" />
</el-card>
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="88px">
@ -136,29 +136,25 @@
<el-dialog :title="title" :visible.sync="open" width="720px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item label="导航类型" prop="navType">
<el-form-item v-if="!form.navId" label="导航类型" prop="navType">
<el-radio-group v-model="form.navType" @change="handleNavTypeChange">
<el-radio label="main">主导航</el-radio>
<el-radio label="footer">底部导航</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="导航编码" prop="navCode">
<el-form-item v-if="!form.navId" label="导航编码" prop="navCode">
<el-input v-model="form.navCode" placeholder="请输入导航编码" />
</el-form-item>
<el-form-item label="导航标题" prop="navTitle">
<el-input v-model="form.navTitle" placeholder="请输入导航标题" maxlength="200" show-word-limit />
</el-form-item>
<el-form-item label="链接地址" prop="navUrl">
<el-form-item v-if="!form.navId" label="链接地址" prop="navUrl">
<el-input v-model="form.navUrl" placeholder="请输入链接地址" />
</el-form-item>
<el-form-item label="图标" prop="navIcon">
<image-upload v-model="form.navIcon" :limit="1" />
</el-form-item>
<el-form-item v-if="form.navType === 'main' && Number(form.parentId) === 0" label="侧栏配图" prop="navCover">
<image-upload v-model="form.navCover" :limit="1" />
<image-upload v-model="form.navCover" :limit="1" spec="一级导航右侧大图,建议 720×520px 或 4:3,主体居中,单张不超过 5MB" />
<div class="form-item-tip">与门户模板中主导航一级项顺序对应,展示在菜单展开区右侧大图位。</div>
</el-form-item>
<el-form-item label="父级" prop="parentId">
<el-form-item v-if="!form.navId" label="父级" prop="parentId">
<el-select v-model="form.parentId" placeholder="请选择父级导航" clearable @change="handleParentChange">
<el-option label="顶级导航" :value="0" />
<el-option
@ -172,7 +168,7 @@
<el-form-item label="排序" prop="sortOrder">
<el-input-number v-model="form.sortOrder" :min="0" controls-position="right" />
</el-form-item>
<el-form-item label="新窗口" prop="isBlank">
<el-form-item v-if="!form.navId" label="新窗口" prop="isBlank">
<el-radio-group v-model="form.isBlank">
<el-radio label="0">否</el-radio>
<el-radio label="1">是</el-radio>
@ -187,7 +183,7 @@
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-form-item v-if="!form.navId" label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" maxlength="500" show-word-limit />
</el-form-item>
</el-form>

View File

@ -1,14 +1,5 @@
<template>
<div class="app-container">
<el-alert
v-hasPermi="['huayang:navigation:list']"
type="info"
:closable="false"
show-icon
class="mb16"
title="页脚维护入口"
description="若需在同一处维护底部多列链接、Logo/版权、联系方式与站点参数(备案、SEO、页脚红条文案等),请使用菜单「导航与布局 → 页脚一站式配置」。Logo 请通过本模块下的「Logo配置」二级菜单维护。"
/>
<navigation-manage />
</div>
</template>
@ -23,9 +14,3 @@ export default {
}
}
</script>
<style scoped>
.mb16 {
margin-bottom: 16px;
}
</style>

View File

@ -1,358 +0,0 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="88px">
<el-form-item label="新闻标题" prop="newsTitle">
<el-input
v-model="queryParams.newsTitle"
placeholder="请输入新闻标题"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="新闻类型" prop="newsType">
<el-select v-model="queryParams.newsType" placeholder="新闻类型" clearable>
<el-option
v-for="dict in dict.type.huayang_news_type"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="状态" clearable>
<el-option
v-for="dict in dict.type.sys_normal_disable"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-hasPermi="['huayang:news:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['huayang:news:remove']"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
v-hasPermi="['huayang:news:export']"
>导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="newsList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="编号" align="center" prop="newsId" width="80" />
<el-table-column label="新闻标题" align="center" prop="newsTitle" :show-overflow-tooltip="true" />
<el-table-column label="类型" align="center" prop="newsType" width="120">
<template slot-scope="scope">
<dict-tag :options="dict.type.huayang_news_type" :value="scope.row.newsType" />
</template>
</el-table-column>
<el-table-column label="封面图片" align="center" prop="newsCover" width="120">
<template slot-scope="scope">
<el-image
v-if="scope.row.newsCover"
style="width: 60px; height: 60px"
:src="resolveImageUrl(scope.row.newsCover)"
fit="cover"
:preview-src-list="[resolveImageUrl(scope.row.newsCover)]"
/>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="作者" align="center" prop="newsAuthor" width="100" />
<el-table-column label="发布时间" align="center" prop="publishTime" width="160">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.publishTime) }}</span>
</template>
</el-table-column>
<el-table-column label="阅读数" align="center" prop="readCount" width="80" />
<el-table-column label="状态" align="center" prop="status" width="100">
<template slot-scope="scope">
<dict-tag :options="dict.type.sys_normal_disable" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="首页推荐" align="center" prop="isHome" width="80">
<template slot-scope="scope">
<el-switch
v-model="scope.row.isHome"
active-value="1"
inactive-value="0"
@change="handleIsHomeChange(scope.row)"
></el-switch>
</template>
</el-table-column>
<el-table-column label="排序" align="center" prop="sortOrder" width="80" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="140">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['huayang:news:edit']"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['huayang:news:remove']"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<el-dialog :title="title" :visible.sync="open" width="1000px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item label="新闻标题" prop="newsTitle">
<el-input v-model="form.newsTitle" maxlength="200" show-word-limit placeholder="请输入新闻标题" />
</el-form-item>
<el-form-item label="新闻类型" prop="newsType">
<el-select v-model="form.newsType" placeholder="请选择新闻类型" style="width: 100%">
<el-option
v-for="dict in dict.type.huayang_news_type"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="作者" prop="newsAuthor">
<el-input v-model="form.newsAuthor" maxlength="100" placeholder="请输入作者" />
</el-form-item>
<el-form-item label="发布时间" prop="publishTime">
<el-date-picker
v-model="form.publishTime"
type="datetime"
placeholder="选择发布时间"
value-format="yyyy-MM-dd HH:mm:ss"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="排序" prop="sortOrder">
<el-input-number v-model="form.sortOrder" :min="0" :max="9999" controls-position="right" />
</el-form-item>
<el-form-item label="首页推荐" prop="isHome">
<el-radio-group v-model="form.isHome">
<el-radio label="1">是</el-radio>
<el-radio label="0">否</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in dict.type.sys_normal_disable"
:key="dict.value"
:label="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="封面图片" prop="newsCover">
<image-upload v-model="form.newsCover" :limit="1" />
</el-form-item>
<el-form-item label="新闻内容" prop="newsContent">
<editor v-model="form.newsContent" :min-height="280" :enable-media-choice="true" :video-file-size="100" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listNews, getNews, addNews, updateNews, deleteNews, exportNews } from '@/api/huayang/news'
export default {
name: 'HuayangNews',
dicts: ['sys_normal_disable', 'huayang_news_type'],
data() {
return {
loading: true,
showSearch: true,
ids: [],
single: true,
multiple: true,
total: 0,
newsList: [],
title: '',
open: false,
queryParams: {
pageNum: 1,
pageSize: 10,
newsTitle: undefined,
newsType: undefined,
status: undefined,
isHome: undefined
},
form: {},
rules: {
newsTitle: [{ required: true, message: '新闻标题不能为空', trigger: 'blur' }],
newsType: [{ required: true, message: '新闻类型不能为空', trigger: 'change' }],
status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
}
}
},
created() {
this.getList()
},
methods: {
resolveImageUrl(raw) {
if (!raw) return ''
const value = String(raw).trim()
if (!value) return ''
if (/^https?:\/\//i.test(value)) return value
const baseApi = process.env.VUE_APP_BASE_API || ''
const baseOrigin = baseApi ? baseApi.replace(/\/api\/?$/, '') : ''
if (value.startsWith('/assets/')) return value
if (value.startsWith('/profile/')) return `${baseOrigin}${value}`
if (value.startsWith('profile/')) return `${baseOrigin}/${value}`
if (value.startsWith('/')) return `${baseOrigin}${value}`
return `${baseOrigin}/${value}`
},
handleIsHomeChange(row) {
let text = row.isHome === "1" ? "启用" : "停用";
this.$modal.confirm('确认要"' + text + '""' + row.newsTitle + '"的首页推荐吗?').then(function() {
return updateNews({ newsId: row.newsId, isHome: row.isHome });
}).then(() => {
this.$modal.msgSuccess(text + "成功");
}).catch(function() {
row.isHome = row.isHome === "1" ? "0" : "1";
});
},
getList() {
this.loading = true
listNews(this.queryParams).then((response) => {
this.newsList = response.rows
this.total = response.total
this.loading = false
})
},
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
resetQuery() {
this.resetForm('queryForm')
this.handleQuery()
},
handleSelectionChange(selection) {
this.ids = selection.map((item) => item.newsId)
this.single = selection.length !== 1
this.multiple = !selection.length
},
cancel() {
this.open = false
this.reset()
},
reset() {
this.form = {
newsId: undefined,
newsTitle: undefined,
newsContent: undefined,
newsType: undefined,
newsCover: undefined,
newsAuthor: undefined,
publishTime: undefined,
sortOrder: 0,
status: '0',
isHome: '0'
}
this.resetForm('form')
},
handleAdd() {
this.reset()
this.open = true
this.title = '添加新闻'
},
handleUpdate(row) {
this.reset()
const newsId = row.newsId || this.ids[0]
getNews(newsId).then((response) => {
this.form = response.data
this.open = true
this.title = '修改新闻'
})
},
handleDelete(row) {
const newsIds = row.newsId || this.ids
this.$modal.confirm('是否确认删除新闻编号为"' + newsIds + '"的数据项?').then(() => {
return deleteNews(newsIds)
}).then(() => {
this.getList()
this.$modal.msgSuccess('删除成功')
}).catch(() => {})
},
handleExport() {
exportNews(this.queryParams).then((response) => {
const blob = new Blob([response], { type: 'application/vnd.ms-excel' })
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = '新闻数据_' + this.parseTime(new Date(), '{y}{m}{d}{h}{i}{s}') + '.xlsx'
link.click()
})
},
submitForm() {
this.$refs['form'].validate((valid) => {
if (valid) {
if (this.form.newsId != undefined) {
updateNews(this.form).then(() => {
this.$modal.msgSuccess('修改成功')
this.open = false
this.getList()
})
} else {
addNews(this.form).then(() => {
this.$modal.msgSuccess('新增成功')
this.open = false
this.getList()
})
}
}
})
}
}
}
</script>

View File

@ -1,192 +0,0 @@
<template>
<div class="app-container">
<el-alert
type="warning"
:closable="false"
show-icon
class="mb12"
title="此处维护「新闻类型」字典(huayang_news_type)。字典键值将写入新闻数据的 newsType 字段;修改键值可能影响已发布新闻的分类归属,请谨慎操作。"
/>
<el-alert
v-hasPermi="['huayang:navigation:list']"
type="info"
:closable="false"
show-icon
class="mb12"
title="门户新闻页底部导航栏请在「页脚一站式配置」或「导航管理(底部导航)」中维护。"
/>
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="88px">
<el-form-item label="类型标签" prop="dictLabel">
<el-input v-model="queryParams.dictLabel" placeholder="字典标签" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="状态" clearable>
<el-option v-for="d in dict.type.sys_normal_disable" :key="d.value" :label="d.label" :value="d.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['huayang:newsCategory:add']">新增</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="dataList" border>
<el-table-column label="编码" prop="dictCode" width="88" align="center" />
<el-table-column label="标签" prop="dictLabel" min-width="120" />
<el-table-column label="键值" prop="dictValue" min-width="120" show-overflow-tooltip />
<el-table-column label="排序" prop="dictSort" width="80" align="center" />
<el-table-column label="状态" prop="status" width="100" align="center">
<template slot-scope="scope">
<dict-tag :options="dict.type.sys_normal_disable" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="备注" prop="remark" min-width="140" show-overflow-tooltip />
<el-table-column label="操作" width="160" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['huayang:newsCategory:edit']">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['huayang:newsCategory:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
<el-dialog :title="dialogTitle" :visible.sync="open" width="560px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item label="类型标签" prop="dictLabel">
<el-input v-model="form.dictLabel" maxlength="100" placeholder="前台展示名称,如:企业资讯" />
</el-form-item>
<el-form-item label="类型键值" prop="dictValue">
<el-input v-model="form.dictValue" maxlength="100" placeholder="英文键值,如:news(勿随意修改已有数据)" />
</el-form-item>
<el-form-item label="排序" prop="dictSort">
<el-input-number v-model="form.dictSort" :min="0" :max="9999" controls-position="right" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio v-for="d in dict.type.sys_normal_disable" :key="d.value" :label="d.value">{{ d.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" rows="2" maxlength="500" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="open = false">取 消</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listNewsCategory, getNewsCategory, addNewsCategory, updateNewsCategory, delNewsCategory } from '@/api/huayang/newsCategory'
export default {
name: 'HuayangNewsCategory',
dicts: ['sys_normal_disable'],
data() {
return {
loading: true,
showSearch: true,
total: 0,
dataList: [],
open: false,
dialogTitle: '',
queryParams: {
pageNum: 1,
pageSize: 10,
dictLabel: undefined,
status: undefined
},
form: {},
rules: {
dictLabel: [{ required: true, message: '类型标签不能为空', trigger: 'blur' }],
dictValue: [{ required: true, message: '类型键值不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
}
}
},
created() {
this.getList()
},
methods: {
getList() {
this.loading = true
listNewsCategory(this.queryParams).then((res) => {
this.dataList = res.rows
this.total = res.total
this.loading = false
})
},
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
resetQuery() {
this.resetForm('queryForm')
this.handleQuery()
},
resetFormData() {
this.form = {
dictCode: undefined,
dictLabel: undefined,
dictValue: undefined,
dictSort: 0,
status: '0',
remark: undefined
}
this.resetForm('form')
},
handleAdd() {
this.resetFormData()
this.open = true
this.dialogTitle = '新增新闻类型'
},
handleUpdate(row) {
this.resetFormData()
getNewsCategory(row.dictCode).then((res) => {
this.form = res.data
if (this.form.dictSort == null) this.form.dictSort = 0
this.open = true
this.dialogTitle = '修改新闻类型'
})
},
handleDelete(row) {
this.$modal.confirm('是否确认删除「' + row.dictLabel + '」?若已有新闻使用该类型,请先调整新闻类型再删除。').then(() => {
return delNewsCategory(row.dictCode)
}).then(() => {
this.$modal.msgSuccess('删除成功')
this.getList()
}).catch(() => {})
},
submitForm() {
this.$refs.form.validate((valid) => {
if (!valid) return
const req = this.form.dictCode ? updateNewsCategory : addNewsCategory
req(this.form).then(() => {
this.$modal.msgSuccess('保存成功')
this.open = false
this.getList()
})
})
}
}
}
</script>
<style scoped>
.mb12 {
margin-bottom: 12px;
}
.mb8 {
margin-bottom: 8px;
}
</style>

View File

@ -1,82 +0,0 @@
<template>
<div class="app-container">
<el-card>
<div slot="header">新闻动态 · 页顶横幅图</div>
<el-alert
type="info"
:closable="false"
show-icon
class="mb12"
title="下方第一项为「新闻动态」独立页顶通栏图;第二项为官网首页「华阳新闻」整屏区块背景。保存后前台强刷首页即可看到背景(接口为 no-store)。"
/>
<el-form label-width="140px">
<el-form-item label="新闻页横幅图">
<image-upload v-model="form.backgroundImage" :limit="1" />
<div class="field-tip">与新闻动态页顶部通栏一致;保存时会同步更新首页顶部轮播第一张图</div>
</el-form-item>
<el-form-item label="首页新闻区背景">
<image-upload v-model="form.homeNewsBackgroundImage" :limit="1" />
<div class="field-tip">仅影响首页「华阳新闻」所在整屏 slide 的 background(cover / center)</div>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submit" v-hasPermi="['huayang:newsBanner:edit']">保存</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script>
import { getNewsPageBanner, updateNewsPageBanner } from '@/api/huayang/newsPageBanner'
export default {
name: 'HuayangNewsPageBanner',
data() {
return {
form: {
backgroundImage: '',
homeNewsBackgroundImage: ''
}
}
},
created() {
getNewsPageBanner().then((res) => {
const d = res.data || {}
this.form = {
backgroundImage: d.backgroundImage || '',
homeNewsBackgroundImage: d.homeNewsBackgroundImage || ''
}
})
},
methods: {
normalizeSingleImage(value) {
if (!value || typeof value !== 'string') return ''
return value.split(',').map((s) => s.trim()).filter(Boolean)[0] || ''
},
submit() {
const bg = this.normalizeSingleImage(this.form.backgroundImage)
const homeNewsBg = this.normalizeSingleImage(this.form.homeNewsBackgroundImage)
updateNewsPageBanner({
backgroundImage: bg,
homeNewsBackgroundImage: homeNewsBg
}).then(() => {
this.$modal.msgSuccess('保存成功')
this.form.backgroundImage = bg
this.form.homeNewsBackgroundImage = homeNewsBg
})
}
}
}
</script>
<style scoped>
.mb12 {
margin-bottom: 12px;
}
.field-tip {
font-size: 12px;
color: #909399;
line-height: 1.4;
}
</style>

View File

@ -1,30 +1,5 @@
<template>
<div class="app-container">
<el-card class="mb8" shadow="never">
<div slot="header" class="clearfix">
<span>产品详情联系方式配置</span>
</div>
<el-form label-width="120px">
<el-form-item label="联系电话">
<el-input v-model="detailContactConfig.product_detail_contact_phone" placeholder="4006-535-700" />
</el-form-item>
<el-form-item label="联系邮箱">
<el-input v-model="detailContactConfig.product_detail_contact_email" placeholder="haiho@haihofoods.com" />
</el-form-item>
<el-form-item label="联系地址">
<el-input
v-model="detailContactConfig.product_detail_contact_address"
type="textarea"
:rows="2"
placeholder="中国•烟台•芝罘岛东路51号"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="savingDetailContactConfig" @click="saveDetailContactConfig">保存联系方式配置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="88px">
<el-form-item label="产品名称" prop="productName">
<el-input
@ -82,16 +57,6 @@
v-hasPermi="['huayang:product:remove']"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
v-hasPermi="['huayang:product:export']"
>导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
@ -113,12 +78,6 @@
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="价格" align="center" prop="productPrice" width="100">
<template slot-scope="scope">
<span v-if="scope.row.productPrice">¥{{ scope.row.productPrice }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status" width="100">
<template slot-scope="scope">
<dict-tag :options="dict.type.sys_normal_disable" :value="scope.row.status" />
@ -169,14 +128,6 @@
/>
<el-dialog :title="title" :visible.sync="open" width="1000px" append-to-body>
<el-alert
type="info"
:closable="false"
show-icon
class="mb16"
title="字段说明(与官网产品详情对应)"
description="产品主图:列表封面与详情顶部轮播/大图。产品介绍:富文本,对应详情正文。"
/>
<el-form ref="form" :model="form" :rules="rules" label-width="108px">
<el-form-item label="产品名称" prop="productName">
<el-input v-model="form.productName" maxlength="200" show-word-limit placeholder="请输入产品名称" />
@ -191,9 +142,6 @@
/>
</el-select>
</el-form-item>
<el-form-item label="产品价格" prop="productPrice">
<el-input-number v-model="form.productPrice" :min="0" :precision="2" controls-position="right" placeholder="请输入产品价格" />
</el-form-item>
<el-form-item label="排序" prop="sortOrder">
<el-input-number v-model="form.sortOrder" :min="0" :max="9999" controls-position="right" />
</el-form-item>
@ -213,7 +161,7 @@
</el-radio-group>
</el-form-item>
<el-form-item label="产品主图" prop="productImages">
<image-upload v-model="form.productImages" :limit="8" />
<image-upload v-model="form.productImages" :limit="8" spec="产品主图/详情轮播,建议 1000×1000px 或 4:3,白底/浅底,主体居中;最多 8 张" />
<div class="form-tip">用于列表缩略图与详情页顶部展示;多张时官网为轮播。</div>
</el-form-item>
<el-form-item label="产品介绍" prop="productDesc">
@ -230,9 +178,8 @@
</template>
<script>
import { listProduct, getProduct, addProduct, updateProduct, deleteProduct, exportProduct } from '@/api/huayang/product'
import { listProduct, getProduct, addProduct, updateProduct, deleteProduct } from '@/api/huayang/product'
import { listProductCategory } from '@/api/huayang/productCategory'
import { listConfig, addConfig, updateConfig } from '@/api/huayang/config'
export default {
name: 'HuayangProduct',
@ -247,17 +194,6 @@ export default {
total: 0,
productList: [],
categoryOptions: [],
savingDetailContactConfig: false,
detailContactConfig: {
product_detail_contact_phone: '',
product_detail_contact_email: '',
product_detail_contact_address: ''
},
detailContactConfigMeta: {
product_detail_contact_phone: null,
product_detail_contact_email: null,
product_detail_contact_address: null
},
title: '',
open: false,
queryParams: {
@ -274,7 +210,6 @@ export default {
categoryId: undefined,
productImages: undefined,
productDesc: undefined,
productPrice: undefined,
sortOrder: 0,
status: '0',
isHome: '0'
@ -289,64 +224,8 @@ export default {
created() {
this.getList()
this.getCategoryOptions()
this.getDetailContactConfig()
},
methods: {
async getDetailContactConfig() {
const keys = ['product_detail_contact_phone', 'product_detail_contact_email', 'product_detail_contact_address']
const rows = await Promise.all(keys.map((key) => this.fetchConfigByKey(key)))
rows.forEach((row, idx) => {
const key = keys[idx]
this.detailContactConfig[key] = row?.configValue || ''
this.detailContactConfigMeta[key] = row || null
})
},
async fetchConfigByKey(key) {
const response = await listConfig({ pageNum: 1, pageSize: 1, configKey: key })
const rows = Array.isArray(response?.rows) ? response.rows : []
return rows.find(item => item.configKey === key) || null
},
async saveDetailContactConfig() {
this.savingDetailContactConfig = true
try {
await this.upsertConfig(
'product_detail_contact_phone',
'产品详情联系方式-电话',
this.detailContactConfig.product_detail_contact_phone,
'产品详情页侧栏联系方式中的联系电话'
)
await this.upsertConfig(
'product_detail_contact_email',
'产品详情联系方式-邮箱',
this.detailContactConfig.product_detail_contact_email,
'产品详情页侧栏联系方式中的联系邮箱'
)
await this.upsertConfig(
'product_detail_contact_address',
'产品详情联系方式-地址',
this.detailContactConfig.product_detail_contact_address,
'产品详情页侧栏联系方式中的联系地址'
)
this.$modal.msgSuccess('联系方式配置保存成功')
await this.getDetailContactConfig()
} finally {
this.savingDetailContactConfig = false
}
},
async upsertConfig(configKey, configName, configValue, remark) {
const meta = this.detailContactConfigMeta[configKey]
const payload = {
configKey,
configName,
configValue: configValue == null ? '' : String(configValue),
remark
}
if (meta?.configId) {
await updateConfig({ ...meta, ...payload })
} else {
await addConfig(payload)
}
},
getList() {
this.loading = true
listProduct(this.queryParams).then((response) => {
@ -452,7 +331,6 @@ export default {
categoryId: undefined,
productImages: undefined,
productDesc: undefined,
productPrice: undefined,
sortOrder: 0,
status: '0',
isHome: '0'
@ -505,15 +383,6 @@ export default {
this.$modal.msgSuccess('删除成功')
}).catch(() => {})
},
handleExport() {
exportProduct(this.queryParams).then((response) => {
const blob = new Blob([response], { type: 'application/vnd.ms-excel' })
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = '产品数据_' + this.parseTime(new Date(), '{y}{m}{d}{h}{i}{s}') + '.xlsx'
link.click()
})
},
submitForm() {
this.$refs['form'].validate((valid) => {
if (valid) {

View File

@ -25,14 +25,6 @@
</el-form-item>
</el-form>
<el-alert
v-hasPermi="['huayang:navigation:list']"
type="info"
:closable="false"
show-icon
class="mb8"
title="门户「产品中心」页底部导航栏由「页脚一站式配置」与「导航管理(底部导航)」统一维护,不在本表逐行配置。"
/>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
@ -120,7 +112,7 @@
<el-input v-model="form.categoryName" maxlength="100" show-word-limit placeholder="请输入分类名称" />
</el-form-item>
<el-form-item label="分类图标" prop="categoryIcon">
<image-upload v-model="form.categoryIcon" :limit="1" :drag="false" />
<image-upload v-model="form.categoryIcon" :limit="1" :drag="false" spec="产品分类图标,建议 160×160px,透明底 PNG/SVG,单张不超过 1MB" />
</el-form-item>
<el-form-item label="排序" prop="sortOrder">
<el-input-number v-model="form.sortOrder" :min="0" :max="9999" controls-position="right" />

View File

@ -49,7 +49,7 @@
<el-table-column label="编号" align="center" prop="inquiryId" width="80" />
<el-table-column label="咨询人" align="center" prop="inquiryName" width="100" />
<el-table-column label="联系电话" align="center" prop="inquiryPhone" width="130" />
<el-table-column label="意向产品" align="center" prop="inquiryProduct" :show-overflow-tooltip="true" />
<el-table-column label="邮箱" align="center" prop="inquiryProduct" :show-overflow-tooltip="true" />
<el-table-column label="咨询内容" align="center" prop="inquiryMessage" :show-overflow-tooltip="true" />
<el-table-column label="处理状态" align="center" prop="inquiryStatus" width="100">
<template slot-scope="scope">
@ -102,7 +102,7 @@
<el-descriptions :column="2" border>
<el-descriptions-item label="咨询人">{{ detailForm.inquiryName }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ detailForm.inquiryPhone }}</el-descriptions-item>
<el-descriptions-item label="意向产品" :span="2">{{ detailForm.inquiryProduct || '-' }}</el-descriptions-item>
<el-descriptions-item label="邮箱" :span="2">{{ detailForm.inquiryProduct || '-' }}</el-descriptions-item>
<el-descriptions-item label="咨询内容" :span="2">{{ detailForm.inquiryMessage || '-' }}</el-descriptions-item>
<el-descriptions-item label="处理状态">
<el-tag v-if="detailForm.inquiryStatus === '0'" type="warning">待处理</el-tag>

View File

@ -1,64 +1,57 @@
<template>
<div class="app-container">
<el-card>
<div slot="header">产品营销中心 · 页顶横幅图</div>
<el-alert
type="info"
:closable="false"
show-icon
class="mb12"
title="此处设置官网「产品中心」页面顶部通栏大图(导航下方横幅);保存后前台刷新页面即可生效。建议宽幅横图,与现有版式一致。"
/>
<el-form label-width="120px">
<el-form-item label="横幅图片">
<image-upload v-model="form.backgroundImage" :limit="1" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submit" v-hasPermi="['huayang:productBanner:edit']">保存</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script>
import { getProductPageBanner, updateProductPageBanner } from '@/api/huayang/productPageBanner'
export default {
name: 'HuayangProductPageBanner',
data() {
return {
form: {
backgroundImage: ''
}
}
},
created() {
getProductPageBanner().then((res) => {
const d = res.data || {}
this.form = {
backgroundImage: d.backgroundImage || ''
}
})
},
methods: {
submit() {
let bg = this.form.backgroundImage
if (bg && typeof bg === 'string') {
bg = bg.split(',').map((s) => s.trim()).filter(Boolean)[0] || ''
}
updateProductPageBanner({ backgroundImage: bg }).then(() => {
this.$modal.msgSuccess('保存成功')
this.form.backgroundImage = bg
})
}
}
}
</script>
<style scoped>
.mb12 {
margin-bottom: 12px;
}
</style>
<template>
<div class="app-container">
<el-card>
<div slot="header">产品营销中心 · 页顶横幅图</div>
<el-form label-width="120px">
<el-form-item label="横幅图片">
<image-upload v-model="form.backgroundImage" :limit="1" spec="产品中心页顶横幅,建议 1920×520px 或 16:5,主体居中,单张不超过 5MB" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submit" v-hasPermi="['huayang:productBanner:edit']">保存</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script>
import { getProductPageBanner, updateProductPageBanner } from '@/api/huayang/productPageBanner'
export default {
name: 'HuayangProductPageBanner',
data() {
return {
form: {
backgroundImage: ''
}
}
},
created() {
getProductPageBanner().then((res) => {
const d = res.data || {}
this.form = {
backgroundImage: d.backgroundImage || ''
}
})
},
methods: {
submit() {
let bg = this.form.backgroundImage
if (bg && typeof bg === 'string') {
bg = bg.split(',').map((s) => s.trim()).filter(Boolean)[0] || ''
}
updateProductPageBanner({ backgroundImage: bg }).then(() => {
this.$modal.msgSuccess('保存成功')
this.form.backgroundImage = bg
})
}
}
}
</script>
<style scoped>
.mb12 {
margin-bottom: 12px;
}
</style>

View File

@ -0,0 +1,278 @@
<template>
<div class="app-container">
<el-alert
title="用于维护无法在业务页面直接编辑的静态文案;建议统一使用 key 命名(如 home.banner.title)。"
type="info"
:closable="false"
show-icon
class="mb12"
/>
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="80px">
<el-form-item label="语言代码" prop="langCode">
<el-select v-model="queryParams.langCode" placeholder="全部语言" clearable filterable style="width: 160px">
<el-option v-for="item in languageOptions" :key="item.langCode" :label="item.langName" :value="item.langCode" />
</el-select>
</el-form-item>
<el-form-item label="模块" prop="pageModule">
<el-input v-model="queryParams.pageModule" placeholder="如 common / home" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="文案Key" prop="i18nKey">
<el-input v-model="queryParams.i18nKey" placeholder="请输入 i18nKey" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="范围" prop="onlyStatic">
<el-select v-model="queryParams.onlyStatic" placeholder="请选择范围" style="width: 140px">
<el-option label="仅静态文案" value="1" />
<el-option label="全部文案" value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-hasPermi="['huayang:translation:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['huayang:translation:remove']"
>删除</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
</el-row>
<el-table v-loading="loading" :data="list" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="ID" align="center" prop="i18nId" width="80" />
<el-table-column label="语言" align="center" prop="langCode" width="120" />
<el-table-column label="模块" align="center" prop="pageModule" width="120" />
<el-table-column label="文案Key" align="center" prop="i18nKey" min-width="220" show-overflow-tooltip />
<el-table-column label="文案内容" align="center" prop="i18nValue" min-width="260" show-overflow-tooltip />
<el-table-column label="状态" align="center" prop="translateStatus" width="90">
<template slot-scope="scope">
<el-tag :type="scope.row.translateStatus === '1' ? 'success' : 'info'">
{{ scope.row.translateStatus === '1' ? '发布' : '草稿' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="140">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['huayang:translation:edit']">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['huayang:translation:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<el-dialog :title="title" :visible.sync="open" width="720px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="语言代码" prop="langCode">
<el-select v-model="form.langCode" placeholder="请选择语言" filterable>
<el-option v-for="item in languageOptions" :key="item.langCode" :label="item.langName" :value="item.langCode" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="页面模块" prop="pageModule">
<el-input v-model="form.pageModule" placeholder="如 common / home / product" maxlength="50" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="文案Key" prop="i18nKey">
<el-input v-model="form.i18nKey" placeholder="仅支持字母数字._-,如 home.banner.title" maxlength="100" />
</el-form-item>
<el-form-item label="文案内容" prop="i18nValue">
<el-input v-model="form.i18nValue" type="textarea" :rows="5" placeholder="请输入翻译文案内容" />
</el-form-item>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="回退语言" prop="fallbackLangCode">
<el-input v-model="form.fallbackLangCode" placeholder="默认 zh_cn" maxlength="20" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="发布状态" prop="translateStatus">
<el-radio-group v-model="form.translateStatus">
<el-radio label="0">草稿</el-radio>
<el-radio label="1">发布</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
addTranslationEntry,
deleteTranslationEntry,
listTranslationEntries,
updateTranslationEntry
} from '@/api/huayang/translation'
import { listLanguage } from '@/api/huayang/language'
export default {
name: 'HuayangStaticText',
data() {
return {
loading: false,
showSearch: true,
total: 0,
list: [],
ids: [],
multiple: true,
open: false,
title: '',
languageOptions: [],
queryParams: {
pageNum: 1,
pageSize: 10,
langCode: undefined,
pageModule: undefined,
i18nKey: undefined,
onlyStatic: '1'
},
form: {},
rules: {
langCode: [{ required: true, message: '语言代码不能为空', trigger: 'change' }],
pageModule: [{ required: true, message: '页面模块不能为空', trigger: 'blur' }],
i18nKey: [
{ required: true, message: '文案Key不能为空', trigger: 'blur' },
{ pattern: /^[a-zA-Z0-9_.-]{2,100}$/, message: 'key 仅支持字母数字._-,长度2-100', trigger: 'blur' }
],
i18nValue: [{ required: true, message: '文案内容不能为空', trigger: 'blur' }]
}
}
},
created() {
this.getLanguageOptions()
this.getList()
},
methods: {
async getLanguageOptions() {
try {
const { rows } = await listLanguage({ status: '0', pageNum: 1, pageSize: 100 })
this.languageOptions = Array.isArray(rows) ? rows : []
} catch (e) {
this.languageOptions = []
}
},
async getList() {
this.loading = true
try {
const res = await listTranslationEntries(this.queryParams)
this.list = Array.isArray(res.rows) ? res.rows : []
this.total = Number(res.total || 0)
} finally {
this.loading = false
}
},
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
resetQuery() {
this.resetForm('queryForm')
this.queryParams.onlyStatic = '1'
this.handleQuery()
},
handleSelectionChange(selection) {
this.ids = selection.map((item) => item.i18nId)
this.multiple = !selection.length
},
reset() {
this.form = {
i18nId: undefined,
langCode: '',
i18nKey: '',
i18nValue: '',
pageModule: 'common',
fallbackLangCode: 'zh_cn',
translateStatus: '1'
}
this.resetForm('form')
},
handleAdd() {
this.reset()
this.open = true
this.title = '新增静态文案'
},
handleUpdate(row) {
this.reset()
this.form = { ...row }
if (!this.form.fallbackLangCode) {
this.form.fallbackLangCode = 'zh_cn'
}
if (!this.form.translateStatus) {
this.form.translateStatus = '1'
}
this.open = true
this.title = '修改静态文案'
},
cancel() {
this.open = false
this.reset()
},
async submitForm() {
await this.$refs.form.validate()
if (this.form.i18nId) {
await updateTranslationEntry(this.form)
this.$modal.msgSuccess('修改成功')
} else {
await addTranslationEntry(this.form)
this.$modal.msgSuccess('新增成功')
}
this.open = false
await this.getList()
},
async handleDelete(row) {
const i18nIds = row && row.i18nId ? row.i18nId : this.ids.join(',')
if (!i18nIds) return
await this.$modal.confirm(`确认删除文案 ID 为 "${i18nIds}" 的数据项吗?`)
await deleteTranslationEntry(i18nIds)
this.$modal.msgSuccess('删除成功')
await this.getList()
}
}
}
</script>
<style scoped>
.mb12 {
margin-bottom: 12px;
}
.mb8 {
margin-bottom: 8px;
}
</style>

View File

@ -0,0 +1,197 @@
<template>
<el-dialog
:visible.sync="innerVisible"
title="富文本翻译"
width="80%"
top="5vh"
append-to-body
:close-on-click-modal="false"
@closed="handleClosed"
>
<div v-if="field" class="rt-dialog">
<div class="rt-meta">
<span class="rt-key">{{ field.key }}</span>
<span class="rt-label">{{ field.fieldLabel }}</span>
</div>
<el-tabs v-model="activeLang" type="border-card">
<el-tab-pane
v-for="lang in tabs"
:key="lang.langCode"
:name="lang.langCode"
>
<span slot="label">
<i v-if="isFilled(lang.langCode)" class="el-icon-check filled" />
<i v-else-if="lang.langCode !== 'zh_cn'" class="el-icon-warning-outline empty" />
{{ lang.langName }}
</span>
<div v-if="lang.langCode === 'zh_cn'" class="zh-readonly">
<p class="readonly-tip">
<i class="el-icon-info" />
中文为业务表只读原文,要修改请到对应业务模块(产品/文化 等)
</p>
<div class="zh-content" v-html="field.sourceText"></div>
</div>
<div v-else class="rt-edit-pane">
<editor
:value="getDraft(lang.langCode)"
:min-height="320"
:enable-media-choice="true"
:video-file-size="100"
@input="onInput(lang.langCode, $event)"
/>
</div>
</el-tab-pane>
</el-tabs>
</div>
<div slot="footer" class="dialog-footer">
<el-button size="small" @click="innerVisible = false">取消</el-button>
<el-button
size="small"
type="primary"
:disabled="!hasChange"
@click="handleConfirm"
>应用更改</el-button>
</div>
</el-dialog>
</template>
<script>
export default {
name: 'RichTextDialog',
props: {
visible: { type: Boolean, default: false },
field: { type: Object, default: null },
targetLangs: { type: Array, default: () => [] },
dirtyKeys: { type: Array, default: () => [] }
},
data() {
return {
innerVisible: false,
activeLang: '',
drafts: {}
}
},
computed: {
tabs() {
// 中文 + 各目标语种
return [{ langCode: 'zh_cn', langName: '中文(原文)' }, ...this.targetLangs]
},
hasChange() {
if (!this.field) return false
for (const lang of this.targetLangs) {
if (this.getDraft(lang.langCode) !== this.originalValue(lang.langCode)) {
return true
}
}
return false
}
},
watch: {
visible(v) {
this.innerVisible = v
if (v) this.initDrafts()
},
innerVisible(v) {
this.$emit('update:visible', v)
}
},
methods: {
initDrafts() {
if (!this.field) return
const drafts = {}
for (const lang of this.targetLangs) {
const t = this.field.translations && this.field.translations[lang.langCode]
drafts[lang.langCode] = t && t.value != null ? String(t.value) : ''
}
this.drafts = drafts
this.activeLang = this.targetLangs.length ? this.targetLangs[0].langCode : 'zh_cn'
},
getDraft(langCode) {
return this.drafts[langCode] != null ? this.drafts[langCode] : ''
},
originalValue(langCode) {
if (!this.field) return ''
if (this.field._original && this.field._original[langCode] != null) {
return this.field._original[langCode]
}
const t = this.field.translations && this.field.translations[langCode]
return t && t.value != null ? String(t.value) : ''
},
onInput(langCode, html) {
this.$set(this.drafts, langCode, html == null ? '' : String(html))
},
isFilled(langCode) {
const v = this.getDraft(langCode)
return v != null && String(v).trim() !== ''
},
handleConfirm() {
if (!this.field) return
for (const lang of this.targetLangs) {
const next = this.getDraft(lang.langCode)
const orig = this.originalValue(lang.langCode)
if (next !== orig) {
this.$emit('field-change', {
key: this.field.key,
langCode: lang.langCode,
value: next,
originalValue: orig
})
}
}
this.innerVisible = false
},
handleClosed() {
this.drafts = {}
}
}
}
</script>
<style scoped>
.rt-dialog { padding: 0 4px; }
.rt-meta {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.rt-key {
font-size: 12px;
color: #94a3b8;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
background: #f1f5f9;
padding: 2px 8px;
border-radius: 4px;
}
.rt-label {
font-size: 14px;
font-weight: 600;
color: #1f2937;
}
.zh-readonly { padding: 12px; }
.readonly-tip {
font-size: 12px;
color: #64748b;
background: #fffbeb;
border: 1px solid #fde68a;
border-radius: 4px;
padding: 6px 10px;
margin: 0 0 12px;
}
.zh-content {
background: #f8fafc;
border-radius: 4px;
padding: 12px;
max-height: 400px;
overflow-y: auto;
line-height: 1.7;
color: #1f2937;
}
.zh-content :deep(img) { max-width: 100%; height: auto; }
.rt-edit-pane { padding: 8px; }
.filled { color: #10b981; }
.empty { color: #ef4444; }
</style>

View File

@ -0,0 +1,285 @@
<template>
<div class="translation-section">
<div class="section-header">
<span class="section-name">{{ section.sectionName }}</span>
<span class="section-meta">{{ totalFields }} 个字段 · {{ section.items.length }} 条记录</span>
</div>
<div class="section-body">
<template v-for="item in section.items">
<div :key="item.sourceId" class="item-block">
<div class="item-header">
<span class="item-id">#{{ item.sourceId }}</span>
<span class="item-label">{{ item.sourceLabel || '(无标题)' }}</span>
</div>
<div
v-for="field in item.fields"
:key="field.key"
:class="['field-card', { 'is-dirty': isDirty(field) }]"
>
<div class="field-row source-row">
<span class="field-label">{{ field.fieldLabel }}(中文 · 只读)</span>
<div v-if="field.fieldType === 'richtext'" class="source-richtext" v-html="field.sourceText"></div>
<div v-else class="source-text">{{ field.sourceText }}</div>
<div class="field-key" :title="field.key">{{ field.key }}</div>
</div>
<div
v-for="lang in targetLangs"
:key="lang.langCode"
class="field-row lang-row"
>
<span class="field-label">
{{ lang.langName }}
<i v-if="isFieldLangFilled(field, lang.langCode)" class="el-icon-check filled" />
<i v-else class="el-icon-warning-outline empty" />
</span>
<template v-if="field.fieldType === 'richtext'">
<div
class="rt-preview"
v-html="getValue(field, lang.langCode) || '<span class=\'rt-empty\'>※ 未翻译</span>'"
></div>
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="$emit('open-richtext', field)"
>编辑富文本</el-button>
</template>
<template v-else-if="field.fieldType === 'textarea'">
<el-input
type="textarea"
:rows="2"
:value="getValue(field, lang.langCode)"
:placeholder="`请输入 ${lang.langName} 翻译`"
@input="onInput(field, lang.langCode, $event)"
/>
</template>
<template v-else>
<el-input
:value="getValue(field, lang.langCode)"
:placeholder="`请输入 ${lang.langName} 翻译`"
@input="onInput(field, lang.langCode, $event)"
/>
</template>
</div>
</div>
<div v-if="!item.fields.length" class="empty-fields">该记录无可翻译字段</div>
</div>
</template>
</div>
</div>
</template>
<script>
export default {
name: 'TranslationSection',
props: {
section: { type: Object, required: true },
targetLangs: { type: Array, default: () => [] },
dirtyKeys: { type: Array, default: () => [] }
},
computed: {
dirtyKeySet() {
return new Set(this.dirtyKeys)
},
totalFields() {
let n = 0
for (const item of this.section.items || []) {
n += (item.fields || []).length
}
return n
}
},
methods: {
getValue(field, langCode) {
const t = field.translations && field.translations[langCode]
return t && t.value != null ? t.value : ''
},
isFieldLangFilled(field, langCode) {
const v = this.getValue(field, langCode)
return v != null && String(v).trim() !== ''
},
isDirty(field) {
for (const lang of this.targetLangs) {
if (this.dirtyKeySet.has(`${lang.langCode}::${field.key}`)) return true
}
return false
},
onInput(field, langCode, value) {
const original = this.originalValue(field, langCode)
this.$emit('field-change', {
key: field.key,
langCode,
value,
originalValue: original
})
},
originalValue(field, langCode) {
// 从 field._original 取(首次扫描的值),如果没存就用当前 translation 值兜底
if (field._original && field._original[langCode] != null) {
return field._original[langCode]
}
const t = field.translations && field.translations[langCode]
return t && t.value != null ? t.value : ''
}
},
watch: {
section: {
immediate: true,
handler(sec) {
// 首次加载(或 section 整体替换)时,将当前 translations 值快照到 _original
if (!sec || !Array.isArray(sec.items)) return
for (const item of sec.items) {
for (const f of item.fields || []) {
if (!f._original) {
const snap = {}
for (const lang of this.targetLangs) {
const t = f.translations && f.translations[lang.langCode]
snap[lang.langCode] = t && t.value != null ? t.value : ''
}
this.$set(f, '_original', snap)
}
}
}
}
}
}
}
</script>
<style scoped>
.translation-section {
margin-bottom: 16px;
border: 1px solid #ebeef5;
border-radius: 8px;
background: #ffffff;
overflow: hidden;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 14px;
background: #f8fafc;
border-bottom: 1px solid #ebeef5;
}
.section-name {
font-size: 14px;
font-weight: 600;
color: #1f2937;
}
.section-meta {
font-size: 12px;
color: #94a3b8;
}
.section-body {
padding: 8px 14px;
}
.item-block {
padding: 8px 0 12px;
border-bottom: 1px dashed #f1f5f9;
}
.item-block:last-child { border-bottom: none; }
.item-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.item-id {
font-size: 12px;
color: #64748b;
background: #f1f5f9;
padding: 1px 6px;
border-radius: 4px;
}
.item-label {
font-size: 13px;
color: #1f2937;
font-weight: 500;
}
.field-card {
margin: 8px 0;
padding: 10px 12px;
border: 1px solid #e2e8f0;
border-radius: 6px;
background: #ffffff;
}
.field-card.is-dirty {
border-color: #fbbf24;
background: #fffbeb;
}
.field-row {
display: grid;
grid-template-columns: 140px 1fr;
align-items: start;
gap: 8px;
padding: 6px 0;
}
.field-row + .field-row {
border-top: 1px solid #f1f5f9;
}
.field-label {
font-size: 12px;
color: #64748b;
padding-top: 6px;
display: inline-flex;
align-items: center;
gap: 4px;
}
.field-label .filled { color: #10b981; }
.field-label .empty { color: #ef4444; }
.field-key {
grid-column: 2;
font-size: 11px;
color: #cbd5e1;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
margin-top: 2px;
}
.source-row .source-text,
.source-row .source-richtext {
font-size: 13px;
color: #1f2937;
background: #f8fafc;
padding: 6px 10px;
border-radius: 4px;
word-break: break-word;
max-height: 120px;
overflow-y: auto;
line-height: 1.6;
}
.source-row .source-richtext :deep(img) {
max-width: 100%;
height: auto;
}
.rt-preview {
font-size: 13px;
color: #1f2937;
background: #f8fafc;
padding: 6px 10px;
border-radius: 4px;
border: 1px dashed #e2e8f0;
max-height: 120px;
overflow-y: auto;
line-height: 1.6;
}
.rt-preview :deep(img) {
max-width: 100%;
height: auto;
}
.rt-empty { color: #ef4444; font-size: 12px; }
.empty-fields {
font-size: 12px;
color: #94a3b8;
text-align: center;
padding: 8px;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -3,8 +3,7 @@
<!-- 顶部欢迎区 -->
<div class="welcome-section">
<div class="welcome-text">
<h2>欢迎使用华阳食品管理系统</h2>
<p>烟台华阳食品有限公司 — 网站内容管理后台</p>
<h2>华阳网站管理系统</h2>
</div>
<div class="welcome-time">
<span class="time-text">{{ currentTime }}</span>
@ -14,8 +13,8 @@
<!-- 统计卡片 -->
<el-row :gutter="20" class="stats-row">
<el-col :xs="12" :sm="12" :md="6" :lg="6">
<div class="stat-card stat-card--blue" @click="navigateTo('/huayangProduct/product')">
<el-col :xs="12" :sm="12" :md="12" :lg="12">
<div class="stat-card stat-card--blue" @click="navigateTo(['/huayangSite/productCenter/product', '/huayangProduct/product'])">
<div class="stat-icon">
<i class="el-icon-goods"></i>
</div>
@ -25,19 +24,8 @@
</div>
</div>
</el-col>
<el-col :xs="12" :sm="12" :md="6" :lg="6">
<div class="stat-card stat-card--green" @click="navigateTo('/news')">
<div class="stat-icon">
<i class="el-icon-document"></i>
</div>
<div class="stat-info">
<span class="stat-value">{{ stats.newsCount }}</span>
<span class="stat-label">新闻文章</span>
</div>
</div>
</el-col>
<el-col :xs="12" :sm="12" :md="6" :lg="6">
<div class="stat-card stat-card--orange" @click="navigateTo('/huayangCustomer/message')">
<el-col :xs="12" :sm="12" :md="12" :lg="12">
<div class="stat-card stat-card--orange" @click="navigateTo(['/huayangSite/productCenter/productInquiry', '/huayangProduct/productInquiry'])">
<div class="stat-icon">
<i class="el-icon-chat-dot-round"></i>
</div>
@ -47,65 +35,15 @@
</div>
</div>
</el-col>
<el-col :xs="12" :sm="12" :md="6" :lg="6">
<div class="stat-card stat-card--purple" @click="navigateTo('/huayangCustomer/joinApply')">
<div class="stat-icon">
<i class="el-icon-s-cooperation"></i>
</div>
<div class="stat-info">
<span class="stat-value">{{ stats.joinApplyCount }}</span>
<span class="stat-label">招商申请</span>
</div>
</div>
</el-col>
</el-row>
<el-row :gutter="20">
<!-- 快捷入口 -->
<el-col :xs="24" :sm="24" :md="12" :lg="8">
<el-card class="box-card shortcut-card">
<div slot="header" class="card-header">
<span><i class="el-icon-menu"></i> 快捷入口</span>
</div>
<div class="shortcut-grid">
<div class="shortcut-item" @click="navigateTo('/huayangProduct/product')">
<i class="el-icon-goods"></i>
<span>产品管理</span>
</div>
<div class="shortcut-item" @click="navigateTo('/huayangProduct/productCategory')">
<i class="el-icon-s-grid"></i>
<span>产品分类</span>
</div>
<div class="shortcut-item" @click="navigateTo('/news')">
<i class="el-icon-document"></i>
<span>新闻管理</span>
</div>
<div class="shortcut-item" @click="navigateTo('/companyHonor')">
<i class="el-icon-trophy"></i>
<span>资质荣誉</span>
</div>
<div class="shortcut-item" @click="navigateTo('/navigation')">
<i class="el-icon-s-operation"></i>
<span>导航配置</span>
</div>
<div class="shortcut-item" @click="navigateTo('/huayangResource/resource')">
<i class="el-icon-picture-outline"></i>
<span>媒体资源</span>
</div>
<div class="shortcut-item" @click="navigateTo('/huayangLang/language')">
<i class="el-icon-s-flag"></i>
<span>多语言</span>
</div>
</div>
</el-card>
</el-col>
<!-- 最近留言 -->
<el-col :xs="24" :sm="24" :md="12" :lg="8">
<el-col :xs="24" :sm="24" :md="24" :lg="24">
<el-card class="box-card">
<div slot="header" class="card-header">
<span><i class="el-icon-chat-dot-round"></i> 最近留言</span>
<el-button type="text" @click="navigateTo('/huayangCustomer/message')">查看全部</el-button>
<el-button type="text" @click="navigateTo(['/huayangSite/productCenter/productInquiry', '/huayangProduct/productInquiry'])">查看全部</el-button>
</div>
<div v-if="recentMessages.length === 0" class="empty-tip">暂无留言数据</div>
<div v-else class="recent-list">
@ -120,39 +58,13 @@
</el-card>
</el-col>
<!-- 最近招商申请 -->
<el-col :xs="24" :sm="24" :md="24" :lg="8">
<el-card class="box-card">
<div slot="header" class="card-header">
<span><i class="el-icon-s-cooperation"></i> 最近招商申请</span>
<el-button type="text" @click="navigateTo('/huayangCustomer/joinApply')">查看全部</el-button>
</div>
<div v-if="recentJoinApplies.length === 0" class="empty-tip">暂无招商申请数据</div>
<div v-else class="recent-list">
<div v-for="item in recentJoinApplies" :key="item.applyId" class="recent-item">
<div class="recent-item-header">
<span class="recent-name">{{ item.applicantName || '匿名' }}</span>
<el-tag size="mini" :type="item.status === '0' ? 'warning' : 'success'">
{{ item.status === '0' ? '待处理' : '已处理' }}
</el-tag>
</div>
<div class="recent-content">
<span v-if="item.applicantPhone">{{ item.applicantPhone }}</span>
<span v-if="item.applicantArea"> · {{ item.applicantArea }}</span>
</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script>
import { listProduct } from '@/api/huayang/product'
import { listNews } from '@/api/huayang/news'
import { listMessage } from '@/api/huayang/message'
import { listJoinApply } from '@/api/huayang/joinApply'
export default {
name: 'Index',
@ -163,12 +75,9 @@ export default {
timer: null,
stats: {
productCount: 0,
newsCount: 0,
messageCount: 0,
joinApplyCount: 0
messageCount: 0
},
recentMessages: [],
recentJoinApplies: []
recentMessages: []
}
},
created() {
@ -190,33 +99,36 @@ export default {
},
async loadStats() {
try {
const [productRes, newsRes, messageRes, joinRes] = await Promise.allSettled([
const [productRes, messageRes] = await Promise.allSettled([
listProduct({ pageNum: 1, pageSize: 1 }),
listNews({ pageNum: 1, pageSize: 1 }),
listMessage({ pageNum: 1, pageSize: 5 }),
listJoinApply({ pageNum: 1, pageSize: 5 })
listMessage({ pageNum: 1, pageSize: 5 })
])
if (productRes.status === 'fulfilled') {
this.stats.productCount = productRes.value.total || 0
}
if (newsRes.status === 'fulfilled') {
this.stats.newsCount = newsRes.value.total || 0
}
if (messageRes.status === 'fulfilled') {
this.stats.messageCount = messageRes.value.total || 0
this.recentMessages = (messageRes.value.rows || []).slice(0, 5)
}
if (joinRes.status === 'fulfilled') {
this.stats.joinApplyCount = joinRes.value.total || 0
this.recentJoinApplies = (joinRes.value.rows || []).slice(0, 5)
}
} catch (e) {
// 静默处理,首页不因接口错误而崩溃
console.warn('Dashboard stats load error:', e)
}
},
navigateTo(path) {
this.$router.push(path).catch(() => {})
navigateTo(pathOrPaths) {
const targetPath = this.resolveFirstAvailablePath(pathOrPaths)
this.$router.push(targetPath).catch(() => {})
},
resolveFirstAvailablePath(pathOrPaths) {
const pathList = Array.isArray(pathOrPaths) ? pathOrPaths : [pathOrPaths]
for (const candidatePath of pathList) {
const resolved = this.$router.resolve(candidatePath)
if (resolved && resolved.route && resolved.route.path !== '/404') {
return candidatePath
}
}
// 未找到可匹配路由时,回退到第一个候选,避免点击无响应
return pathList[0]
}
}
}
@ -285,15 +197,9 @@ export default {
.stat-card--blue {
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
}
.stat-card--green {
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
}
.stat-card--orange {
background: linear-gradient(135deg, #fa8c16 0%, #ffa940 100%);
}
.stat-card--purple {
background: linear-gradient(135deg, #722ed1 0%, #9254de 100%);
}
.stat-icon {
font-size: 36px;
margin-right: 16px;
@ -329,41 +235,6 @@ export default {
margin-right: 6px;
}
/* 快捷入口 */
.shortcut-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.shortcut-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 14px 8px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
background: #f5f7fa;
}
.shortcut-item:hover {
background: #ecf5ff;
color: #1890ff;
transform: translateY(-2px);
}
.shortcut-item i {
font-size: 24px;
margin-bottom: 8px;
color: #1890ff;
}
.shortcut-item span {
font-size: 12px;
color: #606266;
white-space: nowrap;
}
.shortcut-item:hover span {
color: #1890ff;
}
/* 最近列表 */
.recent-list {
max-height: 300px;
@ -415,8 +286,5 @@ export default {
text-align: center;
margin-top: 12px;
}
.shortcut-grid {
grid-template-columns: repeat(4, 1fr);
}
}
</style>

View File

@ -75,8 +75,8 @@ export default {
footerContent: defaultSettings.footerContent,
codeUrl: "",
loginForm: {
username: "admin",
password: "admin123",
username: "",
password: "",
rememberMe: false,
code: "",
uuid: ""
@ -124,11 +124,9 @@ export default {
const username = Cookies.get("username")
const password = Cookies.get("password")
const rememberMe = Cookies.get('rememberMe')
this.loginForm = {
username: username === undefined ? this.loginForm.username : username,
password: password === undefined ? this.loginForm.password : decrypt(password),
rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
}
this.loginForm.username = username === undefined ? this.loginForm.username : username
this.loginForm.password = password === undefined ? this.loginForm.password : decrypt(password)
this.loginForm.rememberMe = rememberMe === undefined ? false : Boolean(rememberMe)
},
handleLogin() {
this.$refs.loginForm.validate(valid => {

View File

@ -18,7 +18,7 @@
label-width="100px"
>
<el-col :span="24">
<el-form-item label="生成类型" prop="type">
<el-form-item label="鐢熸垚绫诲瀷" prop="type">
<el-radio-group v-model="formData.type">
<el-radio-button
v-for="(item, index) in typeOptions"
@ -30,8 +30,8 @@
</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item v-if="showFileName" label="文件名" prop="fileName">
<el-input v-model="formData.fileName" placeholder="请输入文件名" clearable />
<el-form-item v-if="showFileName" label="鏂囦欢鍚? prop="fileName">
<el-input v-model="formData.fileName" placeholder="" clearable />
</el-form-item>
</el-col>
</el-form>
@ -39,10 +39,10 @@
<div slot="footer">
<el-button @click="close">
取消
鍙栨秷
</el-button>
<el-button type="primary" @click="handleConfirm">
确定
纭畾
</el-button>
</div>
</el-dialog>
@ -61,20 +61,20 @@ export default {
rules: {
fileName: [{
required: true,
message: '请输入文件名',
message: '璇疯緭鍏ユ枃浠跺悕',
trigger: 'blur'
}],
type: [{
required: true,
message: '生成类型不能为空',
message: '鐢熸垚绫诲瀷涓嶈兘涓虹┖',
trigger: 'change'
}]
},
typeOptions: [{
label: '页面',
label: '椤甸潰',
value: 'file'
}, {
label: '弹窗',
label: '寮圭獥',
value: 'dialog'
}]
}

View File

@ -9,9 +9,9 @@ const CompressionPlugin = require('compression-webpack-plugin')
const name = process.env.VUE_APP_TITLE || '华阳食品管理系统' // 网页标题
const baseUrl = 'http://localhost:8080' // 后端接口
const baseUrl = 'http://localhost:7003' // 后端接口
const port = process.env.port || process.env.npm_config_port || 80 // 端口
const port = process.env.port || process.env.npm_config_port || 7001 // 端口
// vue.config.js 配置说明
//官方vue.config.js 参考文档 https://cli.vuejs.org/zh/config/#css-loaderoptions

7
backend/Dockerfile Normal file
View File

@ -0,0 +1,7 @@
# 使用 JDK8 镜像(RuoYi 默认用 8,如果是 17 请换成 openjdk:17-jdk-alpine)
FROM eclipse-temurin:17-jdk-alpine
WORKDIR /app
# 复制本地已经打包好的 jar 包 (前提是你已经在本地执行过 mvn clean package)
COPY ruoyi-admin/target/ruoyi-admin.jar app.jar
EXPOSE 7003
ENTRYPOINT ["java", "-jar", "app.jar"]

View File

@ -18,6 +18,7 @@ import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.file.FileUploadUtils;
import com.ruoyi.common.utils.file.FileUtils;
import com.ruoyi.common.utils.file.ImageUploadUtils;
import com.ruoyi.framework.config.ServerConfig;
/**
@ -79,7 +80,7 @@ public class CommonController
// 上传文件路径
String filePath = RuoYiConfig.getUploadPath();
// 上传并返回新文件名称
String fileName = FileUploadUtils.upload(filePath, file);
String fileName = upload(filePath, file);
String url = serverConfig.getUrl() + fileName;
AjaxResult ajax = AjaxResult.success();
ajax.put("url", url);
@ -111,7 +112,7 @@ public class CommonController
for (MultipartFile file : files)
{
// 上传并返回新文件名称
String fileName = FileUploadUtils.upload(filePath, file);
String fileName = upload(filePath, file);
String url = serverConfig.getUrl() + fileName;
urls.add(url);
fileNames.add(fileName);
@ -159,4 +160,10 @@ public class CommonController
log.error("下载文件失败", e);
}
}
private String upload(String filePath, MultipartFile file) throws Exception
{
return ImageUploadUtils.isImage(file) ? ImageUploadUtils.upload(filePath, file)
: FileUploadUtils.upload(filePath, file);
}
}

View File

@ -0,0 +1,87 @@
package com.ruoyi.web.controller.common;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;
import java.nio.charset.StandardCharsets;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.CacheControl;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.config.RuoYiConfig;
/**
* Handles uploaded portal assets before the generic static resource handler.
*/
@RestController
public class ProfileUploadController
{
private static final String PROFILE_PREFIX = "/profile/";
private static final byte[] MISSING_IMAGE_BYTES = Base64.getDecoder().decode(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=");
@RequestMapping(value = "/profile/upload/**", method = { RequestMethod.GET, RequestMethod.HEAD })
public ResponseEntity<Resource> getUploadResource(HttpServletRequest request) throws IOException
{
String requestUri = request.getRequestURI();
String relativePath = requestUri.startsWith(PROFILE_PREFIX) ? requestUri.substring(PROFILE_PREFIX.length()) : "";
relativePath = URLDecoder.decode(relativePath, StandardCharsets.UTF_8);
Path root = Path.of(RuoYiConfig.getProfile()).toAbsolutePath().normalize();
Path target = root.resolve(relativePath).normalize();
if (target.startsWith(root) && Files.isRegularFile(target) && Files.isReadable(target))
{
MediaType mediaType = resolveMediaType(target);
return ResponseEntity.ok()
.contentType(mediaType)
.cacheControl(CacheControl.noCache())
.body(new FileSystemResource(target));
}
if (isImagePath(relativePath))
{
return ResponseEntity.ok()
.contentType(MediaType.IMAGE_PNG)
.cacheControl(CacheControl.noStore())
.body(new ByteArrayResource(MISSING_IMAGE_BYTES));
}
return ResponseEntity.notFound().build();
}
private static MediaType resolveMediaType(Path path)
{
try
{
String contentType = Files.probeContentType(path);
if (contentType != null && !contentType.isBlank())
{
return MediaType.parseMediaType(contentType);
}
}
catch (IOException | IllegalArgumentException ignored)
{
}
return MediaType.APPLICATION_OCTET_STREAM;
}
private static boolean isImagePath(String path)
{
if (path == null)
{
return false;
}
String lowerPath = path.toLowerCase();
return lowerPath.endsWith(".png") || lowerPath.endsWith(".jpg") || lowerPath.endsWith(".jpeg")
|| lowerPath.endsWith(".gif") || lowerPath.endsWith(".webp") || lowerPath.endsWith(".svg")
|| lowerPath.endsWith(".ico");
}
}

View File

@ -20,7 +20,7 @@ import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.file.FileUploadUtils;
import com.ruoyi.common.utils.file.ImageUploadUtils;
import com.ruoyi.framework.config.ServerConfig;
import com.ruoyi.system.domain.HuayangCompanyHonor;
import com.ruoyi.system.service.IHuayangCompanyHonorService;
@ -113,7 +113,7 @@ public class HuayangCompanyHonorController extends BaseController
// 上传文件路径
String filePath = RuoYiConfig.getUploadPath() + "/huayang/honor";
// 上传并返回新文件名称
String fileName = FileUploadUtils.upload(filePath, file);
String fileName = ImageUploadUtils.upload(filePath, file);
String url = serverConfig.getUrl() + fileName;
AjaxResult ajax = AjaxResult.success();
ajax.put("url", url);
@ -125,4 +125,4 @@ public class HuayangCompanyHonorController extends BaseController
return error("上传失败:" + e.getMessage());
}
}
}
}

View File

@ -47,7 +47,7 @@ public class HuayangCompanySectionController extends BaseController
/**
* 按板块标识查询(用于后台快速打开 about_intro 等,须在 /{sectionId} 之前声明)
*/
@PreAuthorize("@ss.hasAnyPermi('huayang:companySection:query,huayang:joinPageContent:query')")
@PreAuthorize("@ss.hasPermi('huayang:companySection:query')")
@GetMapping("/byCode/{sectionCode}")
public AjaxResult getBySectionCode(@PathVariable("sectionCode") String sectionCode)
{
@ -67,7 +67,7 @@ public class HuayangCompanySectionController extends BaseController
/**
* 新增公司简介
*/
@PreAuthorize("@ss.hasAnyPermi('huayang:companySection:add,huayang:joinPageContent:edit')")
@PreAuthorize("@ss.hasPermi('huayang:companySection:add')")
@Log(title = "公司简介", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody HuayangCompanySection huayangCompanySection)
@ -79,7 +79,7 @@ public class HuayangCompanySectionController extends BaseController
/**
* 修改公司简介
*/
@PreAuthorize("@ss.hasAnyPermi('huayang:companySection:edit,huayang:joinPageContent:edit')")
@PreAuthorize("@ss.hasPermi('huayang:companySection:edit')")
@Log(title = "公司简介", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody HuayangCompanySection huayangCompanySection)

View File

@ -1,91 +0,0 @@
package com.ruoyi.web.controller.huayang;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.system.domain.HuayangCompanyTech;
import com.ruoyi.system.service.IHuayangCompanyTechService;
/**
* 品质技术管理Controller
*
* @author huayang
*/
@RestController
@RequestMapping("/huayang/companyTech")
public class HuayangCompanyTechController extends BaseController
{
@Autowired
private IHuayangCompanyTechService huayangCompanyTechService;
/**
* 查询品质技术列表
*/
@PreAuthorize("@ss.hasPermi('huayang:companyTech:list')")
@GetMapping("/list")
public TableDataInfo list(HuayangCompanyTech huayangCompanyTech)
{
startPage();
List<HuayangCompanyTech> list = huayangCompanyTechService.selectHuayangCompanyTechList(huayangCompanyTech);
return getDataTable(list);
}
/**
* 获取品质技术详情
*/
@PreAuthorize("@ss.hasPermi('huayang:companyTech:query')")
@GetMapping(value = "/{techId}")
public AjaxResult getInfo(@PathVariable("techId") Long techId)
{
return success(huayangCompanyTechService.selectHuayangCompanyTechByTechId(techId));
}
/**
* 新增品质技术
*/
@PreAuthorize("@ss.hasPermi('huayang:companyTech:add')")
@Log(title = "品质技术", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody HuayangCompanyTech huayangCompanyTech)
{
huayangCompanyTech.setCreateBy(getUsername());
return toAjax(huayangCompanyTechService.insertHuayangCompanyTech(huayangCompanyTech));
}
/**
* 修改品质技术
*/
@PreAuthorize("@ss.hasPermi('huayang:companyTech:edit')")
@Log(title = "品质技术", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody HuayangCompanyTech huayangCompanyTech)
{
huayangCompanyTech.setUpdateBy(getUsername());
return toAjax(huayangCompanyTechService.updateHuayangCompanyTech(huayangCompanyTech));
}
/**
* 删除品质技术
*/
@PreAuthorize("@ss.hasPermi('huayang:companyTech:remove')")
@Log(title = "品质技术", businessType = BusinessType.DELETE)
@DeleteMapping("/{techIds}")
public AjaxResult remove(@PathVariable Long[] techIds)
{
return toAjax(huayangCompanyTechService.deleteHuayangCompanyTechByTechIds(techIds));
}
}

View File

@ -1,60 +0,0 @@
package com.ruoyi.web.controller.huayang;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.system.domain.HuayangJoinApply;
import com.ruoyi.system.service.IHuayangJoinApplyService;
/**
* 招商申请管理端Controller
*
* @author huayang
*/
@RestController
@RequestMapping("/huayang/joinApply")
public class HuayangJoinApplyController extends BaseController
{
@Autowired
private IHuayangJoinApplyService joinApplyService;
@PreAuthorize("@ss.hasPermi('huayang:joinApply:list')")
@GetMapping("/list")
public TableDataInfo list(HuayangJoinApply apply)
{
startPage();
List<HuayangJoinApply> list = joinApplyService.selectHuayangJoinApplyList(apply);
return getDataTable(list);
}
@PreAuthorize("@ss.hasPermi('huayang:joinApply:query')")
@GetMapping(value = "/{applyId}")
public AjaxResult getInfo(@PathVariable("applyId") Long applyId)
{
return success(joinApplyService.selectHuayangJoinApplyById(applyId));
}
@PreAuthorize("@ss.hasPermi('huayang:joinApply:remove')")
@Log(title = "招商申请", businessType = BusinessType.DELETE)
@DeleteMapping("/{applyIds}")
public AjaxResult remove(@PathVariable Long[] applyIds)
{
return toAjax(joinApplyService.deleteHuayangJoinApplyByIds(applyIds));
}
@PreAuthorize("@ss.hasPermi('huayang:joinApply:handle')")
@Log(title = "处理招商申请", businessType = BusinessType.UPDATE)
@PutMapping("/handle")
public AjaxResult handle(@Validated @RequestBody HuayangJoinApply apply)
{
apply.setUpdateBy(getUsername());
return toAjax(joinApplyService.handleApply(apply));
}
}

View File

@ -1,9 +1,14 @@
package com.ruoyi.web.controller.huayang;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.apache.commons.lang3.StringUtils;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
@ -21,6 +26,8 @@ import com.ruoyi.system.service.IHuayangI18nService;
@RequestMapping("/huayang")
public class HuayangLanguageController extends BaseController
{
private static final Pattern I18N_KEY_PATTERN = Pattern.compile("^[a-zA-Z0-9_.-]{2,100}$");
@Autowired
private IHuayangLanguageService languageService;
@ -84,4 +91,277 @@ public class HuayangLanguageController extends BaseController
{
return toAjax(i18nService.deleteHuayangI18nByIds(i18nIds));
}
/**
* 批量导入翻译(支持草稿模式、统计结果、逐条错误信息)
*/
@PreAuthorize("@ss.hasPermi('huayang:translation:add')")
@PostMapping("/translation/import")
public AjaxResult importTranslation(@RequestBody TranslationImportRequest request)
{
if (request == null || request.getItems() == null || request.getItems().isEmpty())
{
return error("导入数据不能为空");
}
String defaultStatus = StringUtils.defaultIfBlank(request.getDefaultStatus(), "0");
if (!"0".equals(defaultStatus) && !"1".equals(defaultStatus))
{
return error("默认发布状态仅支持 0(草稿) 或 1(已发布)");
}
int total = request.getItems().size();
int inserted = 0;
int updated = 0;
int skipped = 0;
List<String> errors = new ArrayList<>();
int index = 0;
for (TranslationImportItem item : request.getItems())
{
index++;
String rowPrefix = "第" + index + "行";
if (item == null)
{
skipped++;
errors.add(rowPrefix + ":空记录已跳过");
continue;
}
String langCode = StringUtils.lowerCase(StringUtils.trimToEmpty(item.getLangCode()));
String pageModule = StringUtils.trimToEmpty(item.getPageModule());
String i18nKey = StringUtils.trimToEmpty(item.getI18nKey());
String i18nValue = item.getI18nValue() == null ? "" : item.getI18nValue();
String fallbackLangCode = StringUtils.lowerCase(StringUtils.trimToEmpty(item.getFallbackLangCode()));
String translateStatus = StringUtils.trimToEmpty(item.getTranslateStatus());
if (StringUtils.isBlank(langCode) || StringUtils.isBlank(i18nKey))
{
skipped++;
errors.add(rowPrefix + ":langCode / i18nKey 不能为空");
continue;
}
if (!I18N_KEY_PATTERN.matcher(i18nKey).matches())
{
skipped++;
errors.add(rowPrefix + ":i18nKey 格式非法(仅支持字母数字._-,长度2-100)");
continue;
}
if (StringUtils.isBlank(pageModule))
{
pageModule = "common";
}
if (StringUtils.isBlank(fallbackLangCode))
{
fallbackLangCode = "zh_cn";
}
if (StringUtils.isBlank(translateStatus))
{
translateStatus = defaultStatus;
}
if (!"0".equals(translateStatus) && !"1".equals(translateStatus))
{
skipped++;
errors.add(rowPrefix + ":translateStatus 仅支持 0/1");
continue;
}
HuayangI18n exists = i18nService.selectHuayangI18nByKey(langCode, i18nKey);
if (exists != null)
{
exists.setI18nValue(i18nValue);
exists.setPageModule(pageModule);
exists.setFallbackLangCode(fallbackLangCode);
exists.setTranslateStatus(translateStatus);
exists.setUpdateBy(getUsername());
i18nService.updateHuayangI18n(exists);
updated++;
}
else
{
HuayangI18n record = new HuayangI18n();
record.setLangCode(langCode);
record.setPageModule(pageModule);
record.setI18nKey(i18nKey);
record.setI18nValue(i18nValue);
record.setFallbackLangCode(fallbackLangCode);
record.setTranslateStatus(translateStatus);
record.setCreateBy(getUsername());
i18nService.insertHuayangI18n(record);
inserted++;
}
}
Map<String, Object> result = new LinkedHashMap<>();
result.put("total", total);
result.put("inserted", inserted);
result.put("updated", updated);
result.put("skipped", skipped);
result.put("errors", errors);
return success(result);
}
/**
* 按筛选条件一键发布(将草稿改为已发布)
*/
@PreAuthorize("@ss.hasPermi('huayang:translation:edit')")
@PostMapping("/translation/publish")
public AjaxResult publishTranslation(@RequestBody TranslationPublishRequest request)
{
HuayangI18n query = new HuayangI18n();
if (request != null)
{
query.setLangCode(request.getLangCode());
query.setPageModule(request.getPageModule());
query.setI18nKey(request.getI18nKey());
}
List<HuayangI18n> rows = i18nService.selectHuayangI18nList(query);
int affected = 0;
for (HuayangI18n row : rows)
{
if ("1".equals(row.getTranslateStatus()))
{
continue;
}
row.setTranslateStatus("1");
row.setUpdateBy(getUsername());
i18nService.updateHuayangI18n(row);
affected++;
}
Map<String, Object> result = new LinkedHashMap<>();
result.put("matched", rows.size());
result.put("published", affected);
return success(result);
}
public static class TranslationImportRequest
{
private String defaultStatus;
private List<TranslationImportItem> items;
public String getDefaultStatus()
{
return defaultStatus;
}
public void setDefaultStatus(String defaultStatus)
{
this.defaultStatus = defaultStatus;
}
public List<TranslationImportItem> getItems()
{
return items;
}
public void setItems(List<TranslationImportItem> items)
{
this.items = items;
}
}
public static class TranslationImportItem
{
private String langCode;
private String pageModule;
private String i18nKey;
private String i18nValue;
private String fallbackLangCode;
private String translateStatus;
public String getLangCode()
{
return langCode;
}
public void setLangCode(String langCode)
{
this.langCode = langCode;
}
public String getPageModule()
{
return pageModule;
}
public void setPageModule(String pageModule)
{
this.pageModule = pageModule;
}
public String getI18nKey()
{
return i18nKey;
}
public void setI18nKey(String i18nKey)
{
this.i18nKey = i18nKey;
}
public String getI18nValue()
{
return i18nValue;
}
public void setI18nValue(String i18nValue)
{
this.i18nValue = i18nValue;
}
public String getFallbackLangCode()
{
return fallbackLangCode;
}
public void setFallbackLangCode(String fallbackLangCode)
{
this.fallbackLangCode = fallbackLangCode;
}
public String getTranslateStatus()
{
return translateStatus;
}
public void setTranslateStatus(String translateStatus)
{
this.translateStatus = translateStatus;
}
}
public static class TranslationPublishRequest
{
private String langCode;
private String pageModule;
private String i18nKey;
public String getLangCode()
{
return langCode;
}
public void setLangCode(String langCode)
{
this.langCode = langCode;
}
public String getPageModule()
{
return pageModule;
}
public void setPageModule(String pageModule)
{
this.pageModule = pageModule;
}
public String getI18nKey()
{
return i18nKey;
}
public void setI18nKey(String i18nKey)
{
this.i18nKey = i18nKey;
}
}
}

View File

@ -1,102 +0,0 @@
package com.ruoyi.web.controller.huayang;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysDictData;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.service.ISysDictDataService;
/**
* 新闻类型(字典 huayang_news_type)管理,供「新闻分类」子菜单使用
*/
@RestController
@RequestMapping("/huayang/newsCategory")
public class HuayangNewsCategoryController extends BaseController
{
public static final String NEWS_TYPE_DICT = "huayang_news_type";
@Autowired
private ISysDictDataService dictDataService;
@PreAuthorize("@ss.hasPermi('huayang:newsCategory:list')")
@GetMapping("/list")
public TableDataInfo list(SysDictData query)
{
query.setDictType(NEWS_TYPE_DICT);
if (StringUtils.isNotEmpty(query.getDictLabel()))
{
query.setDictLabel(query.getDictLabel().trim());
}
startPage();
List<SysDictData> list = dictDataService.selectDictDataList(query);
return getDataTable(list);
}
@PreAuthorize("@ss.hasPermi('huayang:newsCategory:query')")
@GetMapping(value = "/{dictCode}")
public AjaxResult getInfo(@PathVariable Long dictCode)
{
SysDictData data = dictDataService.selectDictDataById(dictCode);
if (data == null || !NEWS_TYPE_DICT.equals(data.getDictType()))
{
return error("数据不存在或无权访问");
}
return success(data);
}
@PreAuthorize("@ss.hasPermi('huayang:newsCategory:add')")
@Log(title = "新闻类型字典", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody SysDictData data)
{
data.setDictType(NEWS_TYPE_DICT);
data.setCreateBy(getUsername());
return toAjax(dictDataService.insertDictData(data));
}
@PreAuthorize("@ss.hasPermi('huayang:newsCategory:edit')")
@Log(title = "新闻类型字典", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody SysDictData data)
{
SysDictData existing = dictDataService.selectDictDataById(data.getDictCode());
if (existing == null || !NEWS_TYPE_DICT.equals(existing.getDictType()))
{
return error("数据不存在或无权修改");
}
data.setDictType(NEWS_TYPE_DICT);
data.setUpdateBy(getUsername());
return toAjax(dictDataService.updateDictData(data));
}
@PreAuthorize("@ss.hasPermi('huayang:newsCategory:remove')")
@Log(title = "新闻类型字典", businessType = BusinessType.DELETE)
@DeleteMapping("/{dictCodes}")
public AjaxResult remove(@PathVariable Long[] dictCodes)
{
for (Long dictCode : dictCodes)
{
SysDictData d = dictDataService.selectDictDataById(dictCode);
if (d == null || !NEWS_TYPE_DICT.equals(d.getDictType()))
{
return error("包含非法数据,已中止删除");
}
}
dictDataService.deleteDictDataByIds(dictCodes);
return success();
}
}

View File

@ -1,106 +0,0 @@
package com.ruoyi.web.controller.huayang;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.system.domain.HuayangNews;
import com.ruoyi.system.service.IHuayangNewsService;
/**
* 新闻动态管理端Controller
*
* @author huayang
*/
@RestController
@RequestMapping("/huayang/news")
public class HuayangNewsController extends BaseController
{
@Autowired
private IHuayangNewsService newsService;
/**
* 查询新闻动态列表
*/
@PreAuthorize("@ss.hasPermi('huayang:news:list')")
@GetMapping("/list")
public TableDataInfo list(HuayangNews news)
{
startPage();
List<HuayangNews> list = newsService.selectHuayangNewsList(news);
return getDataTable(list);
}
/**
* 导出新闻动态列表
*/
@PreAuthorize("@ss.hasPermi('huayang:news:export')")
@Log(title = "新闻动态", businessType = BusinessType.EXPORT)
@GetMapping("/export")
public AjaxResult export(HuayangNews news)
{
List<HuayangNews> list = newsService.selectHuayangNewsList(news);
ExcelUtil<HuayangNews> util = new ExcelUtil<HuayangNews>(HuayangNews.class);
return util.exportExcel(list, "新闻数据");
}
/**
* 获取新闻动态详情
*/
@PreAuthorize("@ss.hasPermi('huayang:news:query')")
@GetMapping(value = "/{newsId}")
public AjaxResult getInfo(@PathVariable("newsId") Long newsId)
{
return success(newsService.selectHuayangNewsById(newsId));
}
/**
* 新增新闻动态
*/
@PreAuthorize("@ss.hasPermi('huayang:news:add')")
@Log(title = "新闻动态", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody HuayangNews news)
{
news.setCreateBy(getUsername());
return toAjax(newsService.insertHuayangNews(news));
}
/**
* 修改新闻动态
*/
@PreAuthorize("@ss.hasPermi('huayang:news:edit')")
@Log(title = "新闻动态", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody HuayangNews news)
{
news.setUpdateBy(getUsername());
return toAjax(newsService.updateHuayangNews(news));
}
/**
* 删除新闻动态
*/
@PreAuthorize("@ss.hasPermi('huayang:news:remove')")
@Log(title = "新闻动态", businessType = BusinessType.DELETE)
@DeleteMapping("/{newsIds}")
public AjaxResult remove(@PathVariable Long[] newsIds)
{
return toAjax(newsService.deleteHuayangNewsByIds(newsIds));
}
}

View File

@ -1,220 +0,0 @@
package com.ruoyi.web.controller.huayang;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.system.domain.HuayangFixedSection;
import com.ruoyi.system.domain.HuayangFixedSectionItem;
import com.ruoyi.system.service.IHuayangFixedSectionService;
/**
* 官网「新闻动态」页通栏横幅图(固定板块 news_banner,section_id=7)
*/
@RestController
@RequestMapping("/huayang/newsPage")
public class HuayangNewsPageBannerController extends BaseController
{
private static final Long NEWS_BANNER_SECTION_ID = 8L;
private static final String NEWS_BANNER_SECTION_CODE = "news_banner";
private static final String HOME_BANNER_SECTION_CODE = "section1";
private static final String NEWS_BANNER_ITEM_TYPE_HOME_NEWS_BG = "home_news_bg";
@Autowired
private IHuayangFixedSectionService huayangFixedSectionService;
@PreAuthorize("@ss.hasPermi('huayang:newsBanner:query')")
@GetMapping("/banner")
public AjaxResult getBanner()
{
HuayangFixedSection section = huayangFixedSectionService.selectHuayangFixedSectionBySectionId(NEWS_BANNER_SECTION_ID);
HuayangFixedSection full = huayangFixedSectionService.selectHuayangFixedSectionByCode(NEWS_BANNER_SECTION_CODE);
Map<String, Object> data = new HashMap<>(4);
data.put("backgroundImage", section != null ? section.getBackgroundImage() : null);
data.put("sectionText", section != null ? section.getSectionText() : null);
data.put("homeBackgroundImage", getHomeFirstBannerImage());
data.put("homeNewsBackgroundImage", getHomeNewsBackgroundImage(full));
return AjaxResult.success(data);
}
@PreAuthorize("@ss.hasPermi('huayang:newsBanner:edit')")
@Log(title = "新闻动态页顶横幅", businessType = BusinessType.UPDATE)
@PutMapping("/banner")
public AjaxResult saveBanner(@RequestBody NewsPageBannerSaveRequest body)
{
HuayangFixedSection existing = huayangFixedSectionService.selectHuayangFixedSectionBySectionId(NEWS_BANNER_SECTION_ID);
if (existing == null || !NEWS_BANNER_SECTION_CODE.equals(existing.getSectionCode()))
{
return AjaxResult.error("新闻动态顶图未初始化:请在数据库执行 backend/sql/huayang_news_dynamic_submenus.sql(含 fixed_section section_id=8)");
}
existing.setBackgroundImage(body != null ? body.getBackgroundImage() : null);
existing.setSectionText(body != null ? body.getSectionText() : null);
existing.setUpdateBy(getUsername());
int updated = huayangFixedSectionService.updateHuayangFixedSection(existing);
if (updated <= 0)
{
return toAjax(updated);
}
// 与首页顶部轮播图同步:保存新闻页横幅时,同时更新首页 section1 第一张轮播图
if (existing.getBackgroundImage() != null && !existing.getBackgroundImage().trim().isEmpty())
{
upsertHomeFirstBannerImage(existing.getBackgroundImage().trim());
}
if (body != null && body.getHomeNewsBackgroundImage() != null)
{
upsertHomeNewsBackgroundImage(body.getHomeNewsBackgroundImage());
}
return toAjax(updated);
}
private String getHomeFirstBannerImage()
{
HuayangFixedSection homeSection = huayangFixedSectionService.selectHuayangFixedSectionByCode(HOME_BANNER_SECTION_CODE);
if (homeSection == null || homeSection.getItemList() == null || homeSection.getItemList().isEmpty())
{
return null;
}
HuayangFixedSectionItem first = homeSection.getItemList().get(0);
return first != null ? first.getItemImage() : null;
}
private void upsertHomeFirstBannerImage(String image)
{
HuayangFixedSection homeSection = huayangFixedSectionService.selectHuayangFixedSectionByCode(HOME_BANNER_SECTION_CODE);
if (homeSection == null)
{
return;
}
List<HuayangFixedSectionItem> items = homeSection.getItemList();
HuayangFixedSectionItem first = (items != null && !items.isEmpty()) ? items.get(0) : null;
if (first != null && first.getItemId() != null)
{
HuayangFixedSectionItem update = new HuayangFixedSectionItem();
update.setItemId(first.getItemId());
update.setItemImage(image);
update.setUpdateBy(getUsername());
huayangFixedSectionService.updateHuayangFixedSectionItem(update);
return;
}
HuayangFixedSectionItem create = new HuayangFixedSectionItem();
create.setSectionId(homeSection.getSectionId());
create.setItemTitle("首页轮播图1");
create.setItemType("slide");
create.setSortOrder(1);
create.setStatus("0");
create.setItemImage(image);
create.setCreateBy(getUsername());
huayangFixedSectionService.insertHuayangFixedSectionItem(create);
}
private String getHomeNewsBackgroundImage(HuayangFixedSection newsBanner)
{
if (newsBanner == null || newsBanner.getItemList() == null || newsBanner.getItemList().isEmpty())
{
return null;
}
HuayangFixedSectionItem target = newsBanner.getItemList().stream()
.filter(it -> it != null && NEWS_BANNER_ITEM_TYPE_HOME_NEWS_BG.equals(it.getItemType()))
.findFirst()
.orElse(null);
return target != null ? target.getItemImage() : null;
}
private void upsertHomeNewsBackgroundImage(String image)
{
HuayangFixedSection banner = huayangFixedSectionService.selectHuayangFixedSectionByCode(NEWS_BANNER_SECTION_CODE);
if (banner == null || banner.getSectionId() == null)
{
return;
}
List<HuayangFixedSectionItem> items = banner.getItemList();
HuayangFixedSectionItem existing = null;
if (items != null)
{
for (HuayangFixedSectionItem it : items)
{
if (it != null && NEWS_BANNER_ITEM_TYPE_HOME_NEWS_BG.equals(it.getItemType()))
{
existing = it;
break;
}
}
}
if (existing != null && existing.getItemId() != null)
{
HuayangFixedSectionItem update = new HuayangFixedSectionItem();
update.setItemId(existing.getItemId());
update.setItemImage(image);
update.setUpdateBy(getUsername());
huayangFixedSectionService.updateHuayangFixedSectionItem(update);
return;
}
HuayangFixedSectionItem create = new HuayangFixedSectionItem();
create.setSectionId(banner.getSectionId());
create.setItemTitle("首页华阳新闻背景图");
create.setItemType(NEWS_BANNER_ITEM_TYPE_HOME_NEWS_BG);
create.setSortOrder(999);
create.setStatus("0");
create.setItemImage(image);
create.setCreateBy(getUsername());
huayangFixedSectionService.insertHuayangFixedSectionItem(create);
}
public static class NewsPageBannerSaveRequest
{
private String backgroundImage;
private String sectionText;
private String homeBackgroundImage;
private String homeNewsBackgroundImage;
public String getBackgroundImage()
{
return backgroundImage;
}
public void setBackgroundImage(String backgroundImage)
{
this.backgroundImage = backgroundImage;
}
public String getSectionText()
{
return sectionText;
}
public void setSectionText(String sectionText)
{
this.sectionText = sectionText;
}
public String getHomeBackgroundImage()
{
return homeBackgroundImage;
}
public void setHomeBackgroundImage(String homeBackgroundImage)
{
this.homeBackgroundImage = homeBackgroundImage;
}
public String getHomeNewsBackgroundImage()
{
return homeNewsBackgroundImage;
}
public void setHomeNewsBackgroundImage(String homeNewsBackgroundImage)
{
this.homeNewsBackgroundImage = homeNewsBackgroundImage;
}
}
}

View File

@ -0,0 +1,675 @@
package com.ruoyi.web.controller.huayang;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysMenu;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.system.domain.HuayangI18n;
import com.ruoyi.system.domain.HuayangLanguage;
import com.ruoyi.system.service.IHuayangSiteConfigService;
import com.ruoyi.system.service.IHuayangI18nService;
import com.ruoyi.system.service.IHuayangLanguageService;
import com.ruoyi.system.service.ISysMenuService;
import com.ruoyi.system.service.translation.ITranslationScanner;
import com.ruoyi.system.service.translation.TranslationScannerRegistry;
import com.ruoyi.system.service.translation.vo.PageInfoVO;
import com.ruoyi.system.service.translation.vo.PageTranslationVO;
import com.ruoyi.system.service.translation.vo.TranslationSectionVO;
/**
* 翻译管理(按页面维度)
*
* <p>提供"翻译管理"菜单的核心能力:</p>
* <ul>
* <li>列出所有可翻译页面</li>
* <li>实时扫描某页面下所有可翻译条目(中文原文 + 已存在翻译)</li>
* <li>批量保存翻译</li>
* <li>全局完成度统计</li>
* </ul>
*
* @author huayang
*/
@RestController
@RequestMapping("/huayang/translation")
public class HuayangTranslationController extends BaseController
{
/** 默认语种(不参与翻译,只作为原文兜底) */
private static final String DEFAULT_LANG = "zh_cn";
/** 翻译菜单选择配置 key(保存 sys_menu.menu_id 列表,逗号分隔) */
private static final String MENU_SELECTION_CONFIG_KEY = "translation_menu_selected_ids";
@Autowired
private TranslationScannerRegistry scannerRegistry;
@Autowired
private IHuayangLanguageService languageService;
@Autowired
private IHuayangI18nService i18nService;
@Autowired
private ISysMenuService sysMenuService;
@Autowired
private IHuayangSiteConfigService siteConfigService;
/**
* 列出可翻译页面(用于左侧页面树)
*/
@PreAuthorize("@ss.hasPermi('huayang:translation:list')")
@GetMapping("/pages")
public AjaxResult listPages()
{
Set<String> selectedModules = resolveSelectedModules();
List<PageInfoVO> pages = new ArrayList<>();
for (PageInfoVO page : scannerRegistry.listAllPages())
{
if (selectedModules.isEmpty() || selectedModules.contains(page.getPageModule()))
{
pages.add(page);
}
}
return success(pages);
}
/**
* 列出当前启用的目标语种(不含默认语种 zh_cn)
*/
@PreAuthorize("@ss.hasPermi('huayang:translation:list')")
@GetMapping("/targetLangs")
public AjaxResult listTargetLangs()
{
List<String> langs = resolveTargetLangCodes();
List<HuayangLanguage> all = languageService.selectEnabledLanguageList();
// 返回完整对象(含 langName),便于前端展示
List<HuayangLanguage> result = new ArrayList<>();
if (all != null)
{
for (HuayangLanguage l : all)
{
if (l != null && langs.contains(l.getLangCode()))
{
result.add(l);
}
}
}
return success(result);
}
/**
* 扫描某页面,返回完整翻译条目(含中文原文与已存在翻译)
*/
@PreAuthorize("@ss.hasPermi('huayang:translation:list')")
@GetMapping("/scan")
public AjaxResult scan(@RequestParam("pageModule") String pageModule)
{
if (StringUtils.isBlank(pageModule))
{
return error("参数 pageModule 必填");
}
ITranslationScanner scanner = scannerRegistry.getScanner(pageModule);
if (scanner == null)
{
return error("未知页面模块:" + pageModule);
}
List<String> targetLangs = resolveTargetLangCodes();
if (targetLangs.isEmpty())
{
return error("未配置除默认语种外的可翻译语言,请先在「语言配置」中启用语种");
}
PageTranslationVO data = scanner.scan(targetLangs);
Set<String> allowedSections = resolveAllowedSections(pageModule);
if (!allowedSections.isEmpty())
{
List<TranslationSectionVO> sections = new ArrayList<>();
for (TranslationSectionVO section : data.getSections())
{
if (section != null && allowedSections.contains(section.getSectionKey()))
{
sections.add(section);
}
}
data.setSections(sections);
recalculatePageStats(data, targetLangs);
}
return success(data);
}
/**
* 获取菜单树选项(一级+二级)
*/
@PreAuthorize("@ss.hasPermi('huayang:translation:list')")
@GetMapping("/menuOptions")
public AjaxResult menuOptions()
{
List<SysMenu> rawMenus = sysMenuService.selectMenuList(new SysMenu(), getUserId());
List<SysMenu> candidateMenus = filterTranslationCandidateMenus(rawMenus);
List<SysMenu> tree = sysMenuService.buildMenuTree(candidateMenus);
List<MenuTreeNode> nodes = toMenuTreeNodes(tree);
return success(nodes);
}
/**
* 获取当前已选择的菜单 ID 列表
*/
@PreAuthorize("@ss.hasPermi('huayang:translation:list')")
@GetMapping("/menuSelection")
public AjaxResult menuSelection()
{
return success(new ArrayList<>(resolveSelectedMenuIds()));
}
/**
* 保存菜单选择
*/
@PreAuthorize("@ss.hasPermi('huayang:translation:edit')")
@Log(title = "翻译管理-菜单选择", businessType = BusinessType.UPDATE)
@PutMapping("/menuSelection")
public AjaxResult saveMenuSelection(@RequestBody MenuSelectionRequest request)
{
List<Long> menuIds = request == null ? Collections.emptyList() : request.getMenuIds();
String value = joinMenuIds(menuIds);
siteConfigService.upsertConfigValueByKey(
MENU_SELECTION_CONFIG_KEY,
value,
"翻译管理-菜单选择",
"翻译管理左侧菜单显示控制(sys_menu.menu_id 列表)"
);
return success();
}
/**
* 批量保存翻译条目
* <p>请求体示例:</p>
* <pre>
* {
* "pageModule": "home",
* "items": [
* { "langCode": "en_us", "key": "home.banner.1.title", "value": "Ocean Food" },
* { "langCode": "ja_jp", "key": "home.banner.1.title", "value": "" } // 空字符串=删除
* ]
* }
* </pre>
*/
@PreAuthorize("@ss.hasPermi('huayang:translation:edit')")
@Log(title = "翻译管理-批量保存", businessType = BusinessType.UPDATE)
@PutMapping("/batch")
public AjaxResult batchSave(@RequestBody BatchSaveRequest request)
{
if (request == null || request.getItems() == null || request.getItems().isEmpty())
{
return error("无可保存的条目");
}
if (StringUtils.isBlank(request.getPageModule()))
{
return error("参数 pageModule 必填");
}
List<String> validLangs = resolveTargetLangCodes();
List<HuayangI18n> rows = new ArrayList<>();
for (BatchSaveItem item : request.getItems())
{
if (item == null) continue;
if (StringUtils.isBlank(item.getLangCode()) || StringUtils.isBlank(item.getKey()))
{
continue;
}
// 安全校验:禁止写入未启用的语种或默认语种
if (DEFAULT_LANG.equals(item.getLangCode()))
{
continue;
}
if (!validLangs.contains(item.getLangCode()))
{
continue;
}
HuayangI18n row = new HuayangI18n();
row.setLangCode(item.getLangCode());
row.setI18nKey(item.getKey());
row.setI18nValue(item.getValue());
row.setPageModule(request.getPageModule());
rows.add(row);
}
if (rows.isEmpty())
{
return error("没有有效的条目可保存(语种未启用或缺失 key)");
}
IHuayangI18nService.BatchUpsertStat stat =
i18nService.batchUpsertTranslations(rows, getUsername());
Map<String, Object> data = new LinkedHashMap<>();
data.put("upserted", stat.getUpserted());
data.put("deleted", stat.getDeleted());
return AjaxResult.success("保存成功", data);
}
/**
* 全局完成度统计:每个页面 × 每个语种 → 已翻译数 / 总数
*/
@PreAuthorize("@ss.hasPermi('huayang:translation:list')")
@GetMapping("/stats")
public AjaxResult stats()
{
List<PageInfoVO> pages = scannerRegistry.listAllPages();
List<String> targetLangs = resolveTargetLangCodes();
List<Map<String, Object>> result = new ArrayList<>();
for (PageInfoVO page : pages)
{
ITranslationScanner scanner = scannerRegistry.getScanner(page.getPageModule());
if (scanner == null) continue;
// 实时扫描以获取总 key 数(数据量小,性能可接受;若数据量增长后可改为缓存)
PageTranslationVO pageVO = scanner.scan(targetLangs);
Map<String, Object> row = new LinkedHashMap<>();
row.put("pageModule", page.getPageModule());
row.put("pageName", page.getPageName());
row.put("totalKeys", pageVO.getTotalKeys());
row.put("translated", pageVO.getTranslatedByLang());
result.add(row);
}
Map<String, Object> data = new LinkedHashMap<>();
data.put("targetLangs", targetLangs);
data.put("pages", result);
return success(data);
}
/**
* 解析当前启用、且非默认语种的目标语种列表
*/
private List<String> resolveTargetLangCodes()
{
List<HuayangLanguage> langs = languageService.selectEnabledLanguageList();
if (langs == null) return Collections.emptyList();
List<String> result = new ArrayList<>();
for (HuayangLanguage l : langs)
{
if (l == null) continue;
String code = l.getLangCode();
if (StringUtils.isBlank(code) || DEFAULT_LANG.equals(code)) continue;
result.add(code);
}
return result;
}
private Set<Long> resolveSelectedMenuIds()
{
String value = siteConfigService.selectConfigValueByKey(MENU_SELECTION_CONFIG_KEY);
if (StringUtils.isBlank(value))
{
return Collections.emptySet();
}
Set<Long> result = new LinkedHashSet<>();
for (String part : value.split(","))
{
if (StringUtils.isBlank(part))
{
continue;
}
String trimmed = part.trim();
if (!trimmed.matches("\\d+"))
{
continue;
}
result.add(Long.valueOf(trimmed));
}
return result;
}
private Set<String> resolveSelectedModules()
{
Set<Long> selectedIds = resolveSelectedMenuIds();
if (selectedIds.isEmpty())
{
return Collections.emptySet();
}
Set<String> modules = new HashSet<>();
for (SysMenu menu : sysMenuService.selectMenuList(new SysMenu(), getUserId()))
{
if (menu == null || !selectedIds.contains(menu.getMenuId()))
{
continue;
}
String module = resolveMenuModule(menu);
if (StringUtils.isNotBlank(module))
{
modules.add(module);
}
}
return modules;
}
private Set<String> resolveAllowedSections(String pageModule)
{
Set<Long> selectedIds = resolveSelectedMenuIds();
if (selectedIds.isEmpty())
{
return Collections.emptySet();
}
Set<String> sections = new LinkedHashSet<>();
for (SysMenu menu : sysMenuService.selectMenuList(new SysMenu(), getUserId()))
{
if (menu == null || !selectedIds.contains(menu.getMenuId()))
{
continue;
}
String module = resolveMenuModule(menu);
if (module == null || !module.equals(pageModule))
{
continue;
}
String section = resolveSectionKey(pageModule, menu);
if (StringUtils.isNotBlank(section))
{
sections.add(section);
}
}
return sections;
}
private String resolveMenuModule(SysMenu menu)
{
String component = lower(menu.getComponent());
String path = lower(menu.getPath());
String perms = lower(menu.getPerms());
if (component.contains("homeworkbench")
|| component.contains("huayang/home")
|| component.contains("huayang/company")
|| component.contains("huayang/fixedsection")
|| path.contains("home")
|| perms.startsWith("huayang:home")
|| perms.startsWith("huayang:company")
|| perms.startsWith("huayang:fixedsection"))
{
return "home";
}
if (component.contains("huayang/product")
|| path.contains("product")
|| perms.startsWith("huayang:product")
|| perms.startsWith("huayang:productcategory")
|| perms.startsWith("huayang:productinquiry")
|| perms.startsWith("huayang:productbanner"))
{
return "product";
}
return null;
}
private String resolveSectionKey(String pageModule, SysMenu menu)
{
String perms = lower(menu.getPerms());
String path = lower(menu.getPath());
String component = lower(menu.getComponent());
if ("product".equals(pageModule))
{
if (perms.equals("huayang:productcategory:list") || path.equals("productcategory") || component.contains("huayang/productcategory/index"))
{
return "category";
}
if (perms.equals("huayang:product:list") || path.equals("product") || component.contains("huayang/product/index"))
{
return "item";
}
}
// home 目前不细分到 section,避免误过滤
return null;
}
private void recalculatePageStats(PageTranslationVO page, List<String> targetLangs)
{
int totalKeys = 0;
Map<String, Integer> translated = new LinkedHashMap<>();
for (String lang : targetLangs)
{
translated.put(lang, 0);
}
for (TranslationSectionVO section : page.getSections())
{
if (section == null || section.getItems() == null)
{
continue;
}
for (var item : section.getItems())
{
if (item == null || item.getFields() == null)
{
continue;
}
for (var field : item.getFields())
{
if (field == null)
{
continue;
}
totalKeys++;
for (String lang : targetLangs)
{
if (field.getTranslations() != null
&& field.getTranslations().get(lang) != null
&& StringUtils.isNotBlank(field.getTranslations().get(lang).getValue()))
{
translated.put(lang, translated.get(lang) + 1);
}
}
}
}
}
page.setTotalKeys(totalKeys);
page.setTranslatedByLang(translated);
}
private List<SysMenu> filterTranslationCandidateMenus(List<SysMenu> menus)
{
if (menus == null)
{
return Collections.emptyList();
}
Map<Long, SysMenu> byId = new HashMap<>();
for (SysMenu m : menus)
{
if (m != null && m.getMenuId() != null)
{
byId.put(m.getMenuId(), m);
}
}
Set<Long> includeIds = new HashSet<>();
for (SysMenu m : menus)
{
if (m == null || m.getMenuId() == null)
{
continue;
}
if (!isTranslationRelatedMenu(m))
{
continue;
}
includeIds.add(m.getMenuId());
Long parentId = m.getParentId();
while (parentId != null && parentId > 0 && byId.containsKey(parentId))
{
includeIds.add(parentId);
parentId = byId.get(parentId).getParentId();
}
}
List<SysMenu> result = new ArrayList<>();
for (SysMenu m : menus)
{
if (m != null && includeIds.contains(m.getMenuId()))
{
result.add(m);
}
}
result.sort(Comparator.comparing(SysMenu::getParentId, Comparator.nullsLast(Long::compareTo))
.thenComparing(SysMenu::getOrderNum, Comparator.nullsLast(Integer::compareTo))
.thenComparing(SysMenu::getMenuId, Comparator.nullsLast(Long::compareTo)));
return result;
}
private boolean isTranslationRelatedMenu(SysMenu menu)
{
if (menu == null)
{
return false;
}
if ("底部导航栏显示".equals(menu.getMenuName()))
{
return false;
}
String menuType = lower(menu.getMenuType());
if ("f".equals(menuType))
{
return false;
}
String perms = lower(menu.getPerms());
String component = lower(menu.getComponent());
String path = lower(menu.getPath());
return perms.startsWith("huayang:")
|| component.startsWith("huayang/")
|| path.startsWith("huayangsite/");
}
private List<MenuTreeNode> toMenuTreeNodes(List<SysMenu> menus)
{
List<MenuTreeNode> result = new ArrayList<>();
if (menus == null)
{
return result;
}
for (SysMenu m : menus)
{
if (m == null || m.getMenuId() == null)
{
continue;
}
MenuTreeNode node = new MenuTreeNode();
node.setId(m.getMenuId());
node.setLabel(m.getMenuName());
node.setPageModule(resolveMenuModule(m));
node.setSectionKey(resolveSectionKey(resolveMenuModule(m), m));
node.setPath(m.getPath());
node.setComponent(m.getComponent());
node.setPerms(m.getPerms());
node.setRemark(m.getRemark());
node.setChildren(toMenuTreeNodes(m.getChildren()));
result.add(node);
}
return result;
}
private String joinMenuIds(List<Long> menuIds)
{
if (menuIds == null || menuIds.isEmpty())
{
return "";
}
Set<Long> unique = new LinkedHashSet<>(menuIds);
StringBuilder sb = new StringBuilder();
for (Long id : unique)
{
if (id == null || id <= 0)
{
continue;
}
if (sb.length() > 0)
{
sb.append(",");
}
sb.append(id);
}
return sb.toString();
}
private String lower(String value)
{
return value == null ? "" : value.toLowerCase();
}
// ============== 请求体 DTO ==============
public static class MenuTreeNode
{
private Long id;
private String label;
private String pageModule;
private String sectionKey;
private String path;
private String component;
private String perms;
private String remark;
private List<MenuTreeNode> children = new ArrayList<>();
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getLabel() { return label; }
public void setLabel(String label) { this.label = label; }
public String getPageModule() { return pageModule; }
public void setPageModule(String pageModule) { this.pageModule = pageModule; }
public String getSectionKey() { return sectionKey; }
public void setSectionKey(String sectionKey) { this.sectionKey = sectionKey; }
public String getPath() { return path; }
public void setPath(String path) { this.path = path; }
public String getComponent() { return component; }
public void setComponent(String component) { this.component = component; }
public String getPerms() { return perms; }
public void setPerms(String perms) { this.perms = perms; }
public String getRemark() { return remark; }
public void setRemark(String remark) { this.remark = remark; }
public List<MenuTreeNode> getChildren() { return children; }
public void setChildren(List<MenuTreeNode> children) { this.children = children; }
}
public static class MenuSelectionRequest
{
private List<Long> menuIds = new ArrayList<>();
public List<Long> getMenuIds() { return menuIds; }
public void setMenuIds(List<Long> menuIds) { this.menuIds = menuIds; }
}
public static class BatchSaveRequest
{
private String pageModule;
private List<BatchSaveItem> items;
public String getPageModule() { return pageModule; }
public void setPageModule(String pageModule) { this.pageModule = pageModule; }
public List<BatchSaveItem> getItems() { return items; }
public void setItems(List<BatchSaveItem> items) { this.items = items; }
}
public static class BatchSaveItem
{
private String langCode;
private String key;
private String value;
public String getLangCode() { return langCode; }
public void setLangCode(String langCode) { this.langCode = langCode; }
public String getKey() { return key; }
public void setKey(String key) { this.key = key; }
public String getValue() { return value; }
public void setValue(String value) { this.value = value; }
}
}

View File

@ -1,8 +1,12 @@
package com.ruoyi.web.controller.site;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
@ -10,6 +14,8 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
@ -48,10 +54,31 @@ public class SiteCompanyController extends BaseController
@Autowired
private IHuayangI18nService i18nService;
/** 占位串完整匹配表(逗号分隔初始化),调整时请改此类静态代码块 */
private static final int PLACEHOLDER_PLAIN_MAX_LEN = 200;
private static final int PLACEHOLDER_SINGLE_CHAR_REPEAT_MAX_LEN = 16;
private static final Set<String> PLACEHOLDER_EXACT_MATCH_SET;
static
{
Set<String> s = new HashSet<>();
for (String part : "qq,qw,qwe,qwer,qweqwe,qweqeqe,qweqweqweqwe,qqq,qqqq,ww,www,wwww,vv,vvv,vvvv,test,xxx,asd,asdf".split(","))
{
String t = part.trim().toLowerCase(Locale.ROOT);
if (!t.isEmpty())
{
s.add(t);
}
}
PLACEHOLDER_EXACT_MATCH_SET = Collections.unmodifiableSet(s);
}
/**
* 获取公司简介分块。
* <p>可选 {@code lang}:非 zh_cn 时,用 i18n key {@code company.section.{sectionCode}.title}、
* {@code company.section.{sectionCode}.content} 覆盖(content 受库表 i18n_value 长度限制,长文建议后续扩展字段)。</p>
* <p>可选 {@code lang}:非 zh_cn 时,优先 {@code extra_data.langContent}(翻译管理),缺省再用词典
* {@code company.section.{sectionCode}.title|content}。</p>
*/
@GetMapping("/sections")
public AjaxResult sections(@RequestParam(value = "lang", required = false) String lang)
@ -63,11 +90,22 @@ public class SiteCompanyController extends BaseController
return success(list);
}
/** 与门户 {@code normalizeLangCode} 对齐:query 可为 ja-JP、JA_JP、jp 等 */
private static String normalizeSiteLang(String lang) {
if (StringUtils.isBlank(lang)) {
return "zh_cn";
}
return lang.trim().toLowerCase();
String s = lang.trim().toLowerCase(Locale.ROOT).replace('-', '_');
if (s.startsWith("zh")) {
return "zh_cn";
}
if (s.startsWith("en")) {
return "en_us";
}
if (s.startsWith("ja") || s.startsWith("jp")) {
return "ja_jp";
}
return s;
}
private void applySectionLocale(String langNorm, List<HuayangCompanySection> list) {
@ -83,25 +121,207 @@ public class SiteCompanyController extends BaseController
keys.add("company.section." + s.getSectionCode() + ".content");
}
Map<String, String> dict = i18nService.selectI18nValueMapByLangAndKeys(langNorm, keys);
if (dict.isEmpty()) {
return;
}
for (HuayangCompanySection s : list) {
if (StringUtils.isBlank(s.getSectionCode())) {
continue;
}
String prefix = "company.section." + s.getSectionCode() + ".";
String t = dict.get(prefix + "title");
// 翻译管理写入的 extra_data.langContent 优先,但测试占位串不会下发到门户。
// 无有效手工译文时再尝试 huayang_i18n 词典。
String manualTitle = getManualLangTitleFromExtraData(s.getExtraData(), langNorm);
String dictTitle = dict.get(prefix + "title");
String t = pickBestLocalizedSectionText(manualTitle, dictTitle);
if (StringUtils.isNotEmpty(t)) {
s.setSectionTitle(t);
}
String c = dict.get(prefix + "content");
String manualContent = getManualLangContentFromExtraData(s.getExtraData(), langNorm);
String dictContent = dict.get(prefix + "content");
String c = pickBestLocalizedSectionText(manualContent, dictContent);
if (StringUtils.isNotEmpty(c)) {
s.setSectionContent(c);
}
}
}
/**
* 手工译文(翻译管理)优先;无手工时再尝试词典。
*/
private static String pickBestLocalizedSectionText(String manual, String dict) {
if (StringUtils.isNotBlank(manual) && !looksLikePlaceholderTranslation(manual)) {
return manual.trim();
}
if (StringUtils.isNotBlank(dict) && !looksLikePlaceholderTranslation(dict)) {
return dict;
}
return null;
}
/**
* 翻译后台误填的测试文案(完整匹配见 {@link #PLACEHOLDER_EXACT_MATCH_SET})。
*/
private static boolean looksLikePlaceholderTranslation(String raw) {
if (StringUtils.isBlank(raw)) {
return true;
}
String trimmed = raw.trim();
String plain =
trimmed.replaceAll("(?i)<br\\s*/?>", " ")
.replaceAll("<[^>]+>", " ")
.replaceAll("&nbsp;", " ")
.replaceAll("\\s+", " ")
.trim();
if (StringUtils.isBlank(plain)) {
return true;
}
if (plain.length() > PLACEHOLDER_PLAIN_MAX_LEN) {
return false;
}
String lower = plain.toLowerCase(Locale.ROOT);
if (PLACEHOLDER_EXACT_MATCH_SET.contains(lower)) {
return true;
}
if (lower.matches("^(qwe|wer|asd|zxc|www|vvv|xxx)+$")) {
return true;
}
if (plain.length() <= PLACEHOLDER_SINGLE_CHAR_REPEAT_MAX_LEN && plain.codePoints().distinct().count() == 1) {
return true;
}
return false;
}
/**
* 从 section.extra_data 解析手动翻译(标题)。
* <p>约定:{@code langContent.{lang}} 可为 {@code {"title":"...","content":"..."}},
* 标题从此处读取;旧版仅有正文 JSON 字符串时不返回标题。</p>
*/
private String getManualLangTitleFromExtraData(String extraData, String langNorm) {
Object entry = getLangContentEntry(extraData, langNorm);
if (!(entry instanceof JSONObject)) {
return null;
}
String title = ((JSONObject) entry).getString("title");
return StringUtils.isNotBlank(title) ? title : null;
}
/**
* 从 section.extra_data 解析手动翻译(正文)。
* <p>约定:
* <ul>
* <li>新结构:{@code langContent.en_us} = {@code {"title":"...","content":"..."}}</li>
* <li>旧结构:{@code langContent.en_us} = 整段 HTML 字符串(兼容)</li>
* </ul>
*/
private String getManualLangContentFromExtraData(String extraData, String langNorm) {
Object entry = getLangContentEntry(extraData, langNorm);
if (entry instanceof JSONObject) {
String content = ((JSONObject) entry).getString("content");
return StringUtils.isNotBlank(content) ? content : null;
}
if (entry instanceof String) {
String value = (String) entry;
return StringUtils.isNotBlank(value) ? value : null;
}
return null;
}
private Object getLangContentEntry(String extraData, String langNorm) {
if (StringUtils.isBlank(extraData) || StringUtils.isBlank(langNorm)) {
return null;
}
try {
JSONObject root = JSON.parseObject(extraData);
if (root == null) {
return null;
}
JSONObject langContent = root.getJSONObject("langContent");
if (langContent == null) {
langContent = root.getJSONObject("lang_content");
}
if (langContent == null) {
return null;
}
String canonical = normalizeSiteLang(langNorm);
Object direct = langContent.get(canonical);
if (direct != null) {
return direct;
}
for (String k : langContent.keySet()) {
if (canonical.equals(normalizeSiteLang(k))) {
return langContent.get(k);
}
}
return null;
} catch (Exception ex) {
return null;
}
}
private String storeExtraJsonField(String extraData, String langNorm, String field) {
Object entry = getLangContentEntry(extraData, langNorm);
if (!(entry instanceof JSONObject)) {
return null;
}
String v = ((JSONObject) entry).getString(field);
return StringUtils.isNotBlank(v) && !looksLikePlaceholderTranslation(v) ? v : null;
}
/**
* 店面展示页(about_showcase)条目的多语言:i18n 或 extra_data.langContent 的 name/address/phone。
*/
private void applyAboutShowcaseStoreLocale(String langNorm, List<HuayangCompanyStore> list) {
if ("zh_cn".equals(langNorm) || list == null || list.isEmpty()) {
return;
}
List<String> keys = new ArrayList<>();
for (HuayangCompanyStore s : list) {
if (s.getStoreId() == null) {
continue;
}
String sid = String.valueOf(s.getStoreId());
keys.add("company.store." + sid + ".name");
keys.add("company.store." + sid + ".address");
keys.add("company.store." + sid + ".phone");
}
Map<String, String> dict = i18nService.selectI18nValueMapByLangAndKeys(langNorm, keys);
for (HuayangCompanyStore s : list) {
if (s.getStoreId() == null) {
continue;
}
String sid = String.valueOf(s.getStoreId());
String p = "company.store." + sid + ".";
String name = dict.get(p + "name");
if (StringUtils.isBlank(name)) {
name = storeExtraJsonField(s.getExtraData(), langNorm, "name");
}
if (StringUtils.isBlank(name)) {
name = storeExtraJsonField(s.getExtraData(), langNorm, "title");
}
if (StringUtils.isNotEmpty(name)) {
s.setStoreName(name);
}
String address = dict.get(p + "address");
if (StringUtils.isBlank(address)) {
address = storeExtraJsonField(s.getExtraData(), langNorm, "address");
}
if (StringUtils.isNotEmpty(address)) {
s.setStoreAddress(address);
}
String phone = dict.get(p + "phone");
if (StringUtils.isBlank(phone)) {
phone = storeExtraJsonField(s.getExtraData(), langNorm, "phone");
}
if (StringUtils.isBlank(phone)) {
phone = storeExtraJsonField(s.getExtraData(), langNorm, "tel");
}
if (StringUtils.isNotEmpty(phone)) {
s.setStorePhone(phone);
}
}
}
/**
* 获取店面展示列表(首页等通用场景,不含页面专用/地图专表类型)
*/
@ -117,29 +337,18 @@ public class SiteCompanyController extends BaseController
return success(list);
}
/**
* 门户「发展历程」页底部店面轮播:仅 {@code store_type = history}
*/
@GetMapping("/stores/history")
public AjaxResult storesForHistoryPage()
{
HuayangCompanyStore query = new HuayangCompanyStore();
query.setStatus("0");
query.setStoreType("history");
List<HuayangCompanyStore> list = storeService.selectHuayangCompanyStoreList(query);
return success(list);
}
/**
* 门户「店面展示页」(关于我们模块):仅 {@code store_type = about_showcase}
* <p>可选 {@code lang}:详见 {@link #applyAboutShowcaseStoreLocale}。</p>
*/
@GetMapping("/stores/about-showcase")
public AjaxResult storesForAboutPage()
public AjaxResult storesForAboutPage(@RequestParam(value = "lang", required = false) String lang)
{
HuayangCompanyStore query = new HuayangCompanyStore();
query.setStatus("0");
query.setStoreType("about_showcase");
List<HuayangCompanyStore> list = storeService.selectHuayangCompanyStoreList(query);
applyAboutShowcaseStoreLocale(normalizeSiteLang(lang), list);
return success(list);
}
@ -147,20 +356,20 @@ public class SiteCompanyController extends BaseController
* 门户「全国门店」页左侧「店面位置」列表:仅 {@code store_type = map_location}
*/
@GetMapping("/stores/map-locations")
public AjaxResult storesForMapPage()
public AjaxResult storesForMapPage(@RequestParam(value = "lang", required = false) String lang)
{
HuayangCompanyStore query = new HuayangCompanyStore();
query.setStatus("0");
query.setStoreType("map_location");
List<HuayangCompanyStore> list = storeService.selectHuayangCompanyStoreList(query);
applyAboutShowcaseStoreLocale(normalizeSiteLang(lang), list);
return success(list);
}
/** 不参与通用 {@link #stores()} 列表的类型(专页/专块数据) */
private static boolean isExcludedFromGeneralStoresList(String storeType)
{
return "history".equals(storeType) || "about_showcase".equals(storeType)
|| "map_location".equals(storeType);
return "about_showcase".equals(storeType) || "map_location".equals(storeType);
}
/**
@ -176,14 +385,53 @@ public class SiteCompanyController extends BaseController
}
/**
* 获取品质技术列表
* 获取品质技术列表。
* <p>可选 {@code lang}:非 zh_cn 时,优先使用 {@code extra_data.langContent}(翻译管理手工保存),
* 缺省再使用词典 {@code company.tech.{techId}.title|.content}。</p>
*/
@GetMapping("/tech")
public AjaxResult techList()
public AjaxResult techList(@RequestParam(value = "lang", required = false) String lang)
{
HuayangCompanyTech query = new HuayangCompanyTech();
query.setStatus("0");
List<HuayangCompanyTech> list = techService.selectHuayangCompanyTechList(query);
applyTechLocale(normalizeSiteLang(lang), list);
return success(list);
}
private void applyTechLocale(String langNorm, List<HuayangCompanyTech> list) {
if ("zh_cn".equals(langNorm) || list == null || list.isEmpty()) {
return;
}
List<String> keys = new ArrayList<>();
for (HuayangCompanyTech t : list) {
if (t.getTechId() == null) {
continue;
}
String id = String.valueOf(t.getTechId());
keys.add("company.tech." + id + ".title");
keys.add("company.tech." + id + ".content");
}
Map<String, String> dict = i18nService.selectI18nValueMapByLangAndKeys(langNorm, keys);
for (HuayangCompanyTech t : list) {
if (t.getTechId() == null) {
continue;
}
String id = String.valueOf(t.getTechId());
String prefix = "company.tech." + id + ".";
// 翻译管理写入的 extra_data.langContent 优先于词典:避免扫描任务把中文误写入 i18n 后盖住手工英/日文
String manualTitle = getManualLangTitleFromExtraData(t.getExtraData(), langNorm);
String dictTitle = dict.get(prefix + "title");
String title = pickBestLocalizedSectionText(manualTitle, dictTitle);
if (StringUtils.isNotEmpty(title)) {
t.setTechTitle(title);
}
String manualContent = getManualLangContentFromExtraData(t.getExtraData(), langNorm);
String dictContent = dict.get(prefix + "content");
String content = pickBestLocalizedSectionText(manualContent, dictContent);
if (StringUtils.isNotEmpty(content)) {
t.setTechContent(content);
}
}
}
}

View File

@ -1,6 +1,8 @@
package com.ruoyi.web.controller.site;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@ -13,7 +15,9 @@ import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.system.domain.HuayangCultureCategory;
import com.ruoyi.system.domain.HuayangCultureTag;
import com.ruoyi.system.service.IHuayangCultureCategoryService;
import com.ruoyi.system.service.IHuayangI18nService;
import com.ruoyi.system.service.IHuayangCultureTagService;
import org.apache.commons.lang3.StringUtils;
/**
* 门户:华阳文化页公开数据
@ -30,17 +34,22 @@ public class SiteCultureController extends BaseController
@Autowired
private IHuayangCultureTagService cultureTagService;
@Autowired
private IHuayangI18nService i18nService;
@Anonymous
@GetMapping("/categories/public")
public AjaxResult categoriesPublic()
public AjaxResult categoriesPublic(@RequestParam(value = "lang", required = false) String lang)
{
List<HuayangCultureCategory> list = cultureCategoryService.selectHuayangCultureCategoryPublicList();
applyCultureCategoryLocale(normalizeSiteLang(lang), list);
return success(list);
}
@Anonymous
@GetMapping("/tags/public")
public AjaxResult tagsPublic(@RequestParam("categoryId") Long categoryId)
public AjaxResult tagsPublic(@RequestParam("categoryId") Long categoryId,
@RequestParam(value = "lang", required = false) String lang)
{
if (categoryId == null || categoryId <= 0)
{
@ -52,12 +61,14 @@ public class SiteCultureController extends BaseController
return error("分类不存在或已停用");
}
List<HuayangCultureTag> list = cultureTagService.selectHuayangCultureTagPublicListByCategoryId(categoryId);
applyCultureTagLocale(normalizeSiteLang(lang), list);
return success(list);
}
@Anonymous
@GetMapping("/tags/{tagId}/public")
public AjaxResult tagDetailPublic(@PathVariable("tagId") Long tagId)
public AjaxResult tagDetailPublic(@PathVariable("tagId") Long tagId,
@RequestParam(value = "lang", required = false) String lang)
{
HuayangCultureTag tag = cultureTagService.selectHuayangCultureTagPublicById(tagId);
if (tag == null)
@ -70,6 +81,64 @@ public class SiteCultureController extends BaseController
return error("内容不存在或已下架");
}
cultureTagService.incrementReadCount(tagId);
return success(cultureTagService.selectHuayangCultureTagById(tagId));
HuayangCultureTag detail = cultureTagService.selectHuayangCultureTagById(tagId);
applyCultureTagDetailLocale(normalizeSiteLang(lang), detail);
return success(detail);
}
private static String normalizeSiteLang(String lang) {
if (StringUtils.isBlank(lang)) {
return "zh_cn";
}
return lang.trim().toLowerCase();
}
private void applyCultureCategoryLocale(String langNorm, List<HuayangCultureCategory> list) {
if ("zh_cn".equals(langNorm) || list == null || list.isEmpty()) return;
List<String> keys = new ArrayList<>();
for (HuayangCultureCategory item : list) {
if (item.getCategoryId() != null) {
keys.add("culture.category." + item.getCategoryId() + ".name");
}
}
Map<String, String> dict = i18nService.selectI18nValueMapByLangAndKeys(langNorm, keys);
for (HuayangCultureCategory item : list) {
if (item.getCategoryId() == null) continue;
String v = dict.get("culture.category." + item.getCategoryId() + ".name");
if (StringUtils.isNotEmpty(v)) item.setCategoryName(v);
}
}
private void applyCultureTagLocale(String langNorm, List<HuayangCultureTag> list) {
if ("zh_cn".equals(langNorm) || list == null || list.isEmpty()) return;
List<String> keys = new ArrayList<>();
for (HuayangCultureTag item : list) {
if (item.getTagId() != null) {
keys.add("culture.tag." + item.getTagId() + ".title");
keys.add("culture.tag." + item.getTagId() + ".content");
}
}
Map<String, String> dict = i18nService.selectI18nValueMapByLangAndKeys(langNorm, keys);
for (HuayangCultureTag item : list) {
if (item.getTagId() == null) continue;
String prefix = "culture.tag." + item.getTagId() + ".";
String t = dict.get(prefix + "title");
if (StringUtils.isNotEmpty(t)) item.setTagTitle(t);
String c = dict.get(prefix + "content");
if (StringUtils.isNotEmpty(c)) item.setTagContent(c);
}
}
private void applyCultureTagDetailLocale(String langNorm, HuayangCultureTag detail) {
if ("zh_cn".equals(langNorm) || detail == null || detail.getTagId() == null) return;
String prefix = "culture.tag." + detail.getTagId() + ".";
List<String> keys = new ArrayList<>(2);
keys.add(prefix + "title");
keys.add(prefix + "content");
Map<String, String> dict = i18nService.selectI18nValueMapByLangAndKeys(langNorm, keys);
String t = dict.get(prefix + "title");
if (StringUtils.isNotEmpty(t)) detail.setTagTitle(t);
String c = dict.get(prefix + "content");
if (StringUtils.isNotEmpty(c)) detail.setTagContent(c);
}
}

View File

@ -3,6 +3,7 @@ package com.ruoyi.web.controller.site;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.ArrayList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.ruoyi.common.annotation.Anonymous;
@ -10,10 +11,9 @@ import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.HuayangContact;
import com.ruoyi.system.domain.HuayangJoinApply;
import com.ruoyi.system.domain.HuayangMessage;
import com.ruoyi.system.service.IHuayangContactService;
import com.ruoyi.system.service.IHuayangJoinApplyService;
import com.ruoyi.system.service.IHuayangI18nService;
import com.ruoyi.system.service.IHuayangMessageService;
import com.ruoyi.system.service.IHuayangSiteConfigService;
@ -33,34 +33,69 @@ public class SiteCustomerController extends BaseController
private IHuayangMessageService messageService;
@Autowired
private IHuayangJoinApplyService joinApplyService;
private IHuayangSiteConfigService siteConfigService;
@Autowired
private IHuayangSiteConfigService siteConfigService;
private IHuayangI18nService i18nService;
/**
* 获取联系信息(公开)
*/
@Anonymous
@GetMapping("/contact/list")
public AjaxResult contactList()
public AjaxResult contactList(@RequestParam(value = "lang", required = false) String lang)
{
List<HuayangContact> list = contactService.selectEnabledContactList();
Map<String, String> siteValueByType = new HashMap<>(3);
siteValueByType.put("phone", siteConfigService.selectConfigValueByKey("contact_phone"));
siteValueByType.put("email", siteConfigService.selectConfigValueByKey("contact_email"));
siteValueByType.put("address", siteConfigService.selectConfigValueByKey("contact_address"));
for (HuayangContact item : list)
if (list == null || list.isEmpty())
{
String siteValue = siteValueByType.get(item.getContactType());
if (StringUtils.isNotBlank(siteValue))
list = buildFallbackContacts();
}
else
{
Map<String, String> siteValueByType = new HashMap<>(3);
siteValueByType.put("phone", siteConfigService.selectConfigValueByKey("contact_phone"));
siteValueByType.put("email", siteConfigService.selectConfigValueByKey("contact_email"));
siteValueByType.put("address", siteConfigService.selectConfigValueByKey("contact_address"));
for (HuayangContact item : list)
{
item.setContactValue(siteValue);
if (item == null || StringUtils.isNotBlank(item.getContactValue()))
{
continue;
}
String siteValue = siteValueByType.get(item.getContactType());
if (StringUtils.isNotBlank(siteValue))
{
item.setContactValue(siteValue);
}
}
}
applyContactLocale(normalizeSiteLang(lang), list);
return success(list);
}
private List<HuayangContact> buildFallbackContacts()
{
List<HuayangContact> list = new ArrayList<>();
appendConfigGroup(list, siteConfigService.selectConfigValueByKey("contact_phone"), "phone", "商务热线", 0);
appendConfigGroup(list, siteConfigService.selectConfigValueByKey("contact_email"), "email", "E-mail", 1);
appendConfigGroup(list, siteConfigService.selectConfigValueByKey("contact_address"), "address", "地址", 2);
return list;
}
private void appendConfigGroup(List<HuayangContact> list, String raw, String type, String defaultLabel, int sortOrder)
{
if (StringUtils.isBlank(raw))
{
return;
}
HuayangContact item = new HuayangContact();
item.setContactType(type);
item.setContactLabel(defaultLabel);
item.setContactValue(raw.trim());
item.setSortOrder(sortOrder);
list.add(item);
}
/**
* 提交在线留言(公开)
*/
@ -78,20 +113,31 @@ public class SiteCustomerController extends BaseController
return error("提交失败,请稍后重试");
}
/**
* 提交招商申请(公开)
*/
@Anonymous
@PostMapping("/contact/join")
public AjaxResult submitJoinApply(@RequestBody HuayangJoinApply apply)
{
apply.setCreateBy("anonymous");
apply.setApplyStatus("0");
int rows = joinApplyService.insertHuayangJoinApply(apply);
if (rows > 0)
{
return success("申请提交成功,我们的招商经理会尽快与您联系!");
private static String normalizeSiteLang(String lang) {
if (StringUtils.isBlank(lang)) {
return "zh_cn";
}
return lang.trim().toLowerCase();
}
private void applyContactLocale(String langNorm, List<HuayangContact> list) {
if ("zh_cn".equals(langNorm) || list == null || list.isEmpty()) {
return;
}
List<String> keys = new ArrayList<>(3);
keys.add("contact.phone.label");
keys.add("contact.email.label");
keys.add("contact.address.label");
Map<String, String> dict = i18nService.selectI18nValueMapByLangAndKeys(langNorm, keys);
for (HuayangContact item : list) {
if (item == null || StringUtils.isBlank(item.getContactType())) {
continue;
}
String labelKey = "contact." + item.getContactType() + ".label";
String translatedLabel = dict.get(labelKey);
if (StringUtils.isNotBlank(translatedLabel)) {
item.setContactLabel(translatedLabel);
}
}
return error("提交失败,请稍后重试");
}
}

View File

@ -1,14 +1,26 @@
package com.ruoyi.web.controller.site;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.system.service.IHuayangFixedSectionService;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.system.domain.HuayangFixedSection;
import com.ruoyi.system.domain.HuayangFixedSectionItem;
import com.ruoyi.system.domain.HuayangI18n;
import com.ruoyi.system.mapper.HuayangI18nMapper;
import com.ruoyi.system.service.IHuayangFixedSectionService;
import com.ruoyi.system.service.IHuayangI18nService;
/**
* 门户固定板块公开接口
@ -18,15 +30,166 @@ import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/site/fixedSection")
public class SiteFixedSectionController extends BaseController
{
private static final String DEFAULT_LANG = "zh_cn";
@Autowired
private IHuayangFixedSectionService fixedSectionService;
@Autowired
private HuayangI18nMapper i18nMapper;
@Autowired
private IHuayangI18nService i18nService;
/**
* 按编码获取固定板块(包含启用子项)
*/
@GetMapping("/code/{sectionCode}")
public AjaxResult getByCode(@PathVariable("sectionCode") String sectionCode)
public AjaxResult getByCode(@PathVariable("sectionCode") String sectionCode,
@RequestParam(value = "lang", required = false) String lang)
{
return success(fixedSectionService.selectHuayangFixedSectionByCode(sectionCode));
HuayangFixedSection section = fixedSectionService.selectHuayangFixedSectionByCode(sectionCode);
applyTranslations(section, normalizeLang(lang));
return success(section);
}
private void applyTranslations(HuayangFixedSection section, String lang)
{
if (section == null || DEFAULT_LANG.equals(lang))
{
return;
}
String code = section.getSectionCode();
if (StringUtils.isBlank(code))
{
return;
}
List<String> keys = new ArrayList<>();
keys.add("home." + code + ".main.title");
keys.add("home." + code + ".main.text");
if (section.getItemList() != null)
{
for (HuayangFixedSectionItem item : section.getItemList())
{
if (item == null || item.getItemId() == null)
{
continue;
}
keys.add("home." + code + ".item." + item.getItemId() + ".title");
keys.add("home." + code + ".item." + item.getItemId() + ".desc");
}
}
Map<String, String> translations = selectRawTranslations(lang, keys);
if (translations == null || translations.isEmpty())
{
return;
}
String mainTitle = translations.get("home." + code + ".main.title");
if (StringUtils.isNotBlank(mainTitle))
{
section.setSectionName(mainTitle);
}
String mainText = translations.get("home." + code + ".main.text");
if (StringUtils.isNotBlank(mainText))
{
section.setSectionText(mainText);
}
if (section.getItemList() != null)
{
for (HuayangFixedSectionItem item : section.getItemList())
{
if (item == null || item.getItemId() == null)
{
continue;
}
String prefix = "home." + code + ".item." + item.getItemId();
String title = translations.get(prefix + ".title");
if (StringUtils.isNotBlank(title))
{
item.setItemTitle(title);
}
String desc = translations.get(prefix + ".desc");
if (StringUtils.isNotBlank(desc))
{
item.setItemDesc(desc);
}
}
}
}
private Map<String, String> selectRawTranslations(String lang, List<String> keys)
{
Map<String, String> translations = new HashMap<>();
if (StringUtils.isBlank(lang) || keys == null || keys.isEmpty())
{
return translations;
}
try
{
List<HuayangI18n> rows = i18nMapper.selectHuayangI18nByLangAndKeyList(lang, keys);
if (rows != null)
{
for (HuayangI18n row : rows)
{
if (row != null && StringUtils.isNotBlank(row.getI18nKey()) && row.getI18nValue() != null)
{
translations.put(row.getI18nKey(), row.getI18nValue());
}
}
}
}
catch (RuntimeException | LinkageError ignored)
{
List<Map<String, String>> rows = i18nService.selectTranslationByLang(lang);
if (rows == null)
{
return translations;
}
for (Map<String, String> row : rows)
{
String key = row.get("i18nKey");
if (StringUtils.isBlank(key))
{
key = row.get("i18n_key");
}
if (!keys.contains(key))
{
continue;
}
String value = row.get("i18nValue");
if (value == null)
{
value = row.get("i18n_value");
}
if (value != null)
{
translations.put(key, value);
}
}
}
return translations;
}
private String normalizeLang(String raw)
{
String value = StringUtils.defaultString(raw).trim().toLowerCase(Locale.ROOT).replace('-', '_');
if (StringUtils.isBlank(value) || value.startsWith("zh"))
{
return DEFAULT_LANG;
}
if (value.startsWith("en"))
{
return "en_us";
}
if (value.startsWith("ja") || value.startsWith("jp"))
{
return "ja_jp";
}
return value;
}
}

View File

@ -6,12 +6,10 @@ import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.system.domain.HuayangHomeBanner;
import com.ruoyi.system.domain.HuayangHomeFeature;
import com.ruoyi.system.domain.HuayangProduct;
import com.ruoyi.system.domain.HuayangNews;
import com.ruoyi.system.service.IHuayangHomeBannerService;
import com.ruoyi.system.service.IHuayangHomeFeatureService;
import com.ruoyi.system.service.IHuayangI18nService;
import com.ruoyi.system.service.IHuayangProductService;
import com.ruoyi.system.service.IHuayangNewsService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
@ -34,8 +32,6 @@ public class SiteHomeController extends BaseController {
@Autowired
private IHuayangProductService productService;
@Autowired
private IHuayangNewsService newsService;
@Autowired
private IHuayangI18nService i18nService;
/**
@ -45,7 +41,6 @@ public class SiteHomeController extends BaseController {
* <li>轮播标题:{@code home.banner.{bannerId}.title}</li>
* <li>特色标题/描述:{@code home.feature.{featureId}.title}、{@code home.feature.{featureId}.desc}</li>
* <li>推荐产品名称:{@code home.product.{productId}.name}</li>
* <li>推荐新闻标题:{@code home.news.{newsId}.title}</li>
* </ul>
*/
@Anonymous
@ -59,25 +54,17 @@ public class SiteHomeController extends BaseController {
queryFeature.setStatus("0");
List<HuayangHomeFeature> features = featureService.selectHuayangHomeFeatureList(queryFeature);
// 首页推荐产品
HuayangProduct queryProduct = new HuayangProduct();
queryProduct.setStatus("0");
queryProduct.setIsHome("1"); // 推荐到首页
queryProduct.setIsHome("1");
List<HuayangProduct> products = productService.selectHuayangProductList(queryProduct);
// 首页推荐新闻
HuayangNews queryNews = new HuayangNews();
queryNews.setStatus("0");
queryNews.setIsHome("1"); // 推荐到首页
List<HuayangNews> news = newsService.selectHuayangNewsList(queryNews);
applyHomePublicLocale(normalizeSiteLang(lang), banners, features, products, news);
applyHomePublicLocale(normalizeSiteLang(lang), banners, features, products);
Map<String, Object> data = new HashMap<>();
data.put("banners", banners);
data.put("features", features);
data.put("products", products);
data.put("news", news);
return success(data);
}
@ -88,11 +75,8 @@ public class SiteHomeController extends BaseController {
return lang.trim().toLowerCase();
}
/**
* 中文库表为基准;其它语言在 huayang_i18n 中维护对应 key 后在此覆盖。
*/
private void applyHomePublicLocale(String langNorm, List<HuayangHomeBanner> banners,
List<HuayangHomeFeature> features, List<HuayangProduct> products, List<HuayangNews> news) {
List<HuayangHomeFeature> features, List<HuayangProduct> products) {
if ("zh_cn".equals(langNorm)) {
return;
}
@ -119,13 +103,6 @@ public class SiteHomeController extends BaseController {
}
}
}
if (news != null) {
for (HuayangNews n : news) {
if (n.getNewsId() != null) {
keys.add("home.news." + n.getNewsId() + ".title");
}
}
}
Map<String, String> dict = i18nService.selectI18nValueMapByLangAndKeys(langNorm, keys);
if (dict.isEmpty()) {
return;
@ -167,16 +144,5 @@ public class SiteHomeController extends BaseController {
}
}
}
if (news != null) {
for (HuayangNews n : news) {
if (n.getNewsId() == null) {
continue;
}
String v = dict.get("home.news." + n.getNewsId() + ".title");
if (StringUtils.isNotEmpty(v)) {
n.setNewsTitle(v);
}
}
}
}
}

View File

@ -1,8 +1,12 @@
package com.ruoyi.web.controller.site;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Anonymous;
@ -10,6 +14,7 @@ import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.system.domain.HuayangCompanyHonor;
import com.ruoyi.system.service.IHuayangCompanyHonorService;
import com.ruoyi.system.service.IHuayangI18nService;
/**
* 门户站点公开接口:资质荣誉
@ -23,14 +28,69 @@ public class SiteHonorController extends BaseController
@Autowired
private IHuayangCompanyHonorService huayangCompanyHonorService;
@Autowired
private IHuayangI18nService i18nService;
/**
* 对外公开:正常状态的资质荣誉列表(无需登录)
*/
@Anonymous
@GetMapping("/public")
public AjaxResult publicList()
public AjaxResult publicList(@RequestParam(value = "lang", required = false) String lang)
{
List<HuayangCompanyHonor> list = huayangCompanyHonorService.selectHuayangCompanyHonorPublicList();
applyHonorLocale(normalizeSiteLang(lang), list);
return success(list);
}
private static String normalizeSiteLang(String lang)
{
if (StringUtils.isBlank(lang))
{
return "zh_cn";
}
return lang.trim().toLowerCase();
}
private void applyHonorLocale(String langNorm, List<HuayangCompanyHonor> list)
{
if ("zh_cn".equals(langNorm) || list == null || list.isEmpty())
{
return;
}
List<String> keys = new ArrayList<>();
for (HuayangCompanyHonor item : list)
{
if (item == null || item.getHonorId() == null)
{
continue;
}
keys.add("company.honor." + item.getHonorId() + ".title");
keys.add("company.honor." + item.getHonorId() + ".desc");
}
if (keys.isEmpty())
{
return;
}
Map<String, String> dict = i18nService.selectI18nValueMapByLangAndKeys(langNorm, keys);
for (HuayangCompanyHonor item : list)
{
if (item == null || item.getHonorId() == null)
{
continue;
}
String titleKey = "company.honor." + item.getHonorId() + ".title";
String descKey = "company.honor." + item.getHonorId() + ".desc";
String title = dict.get(titleKey);
String desc = dict.get(descKey);
if (StringUtils.isNotBlank(title))
{
item.setHonorTitle(title);
}
if (StringUtils.isNotBlank(desc))
{
item.setHonorDesc(desc);
}
}
}
}

View File

@ -1,148 +0,0 @@
package com.ruoyi.web.controller.site;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysDictData;
import com.ruoyi.system.domain.HuayangNews;
import com.ruoyi.system.service.IHuayangNewsService;
import com.ruoyi.system.service.ISysDictDataService;
/**
* 门户站点公开接口:新闻动态
*
* @author huayang
*/
@RestController
@RequestMapping("/site/news")
public class SiteNewsController extends BaseController
{
private static final String NEWS_TYPE_DICT = "huayang_news_type";
@Autowired
private IHuayangNewsService newsService;
@Autowired
private ISysDictDataService dictDataService;
/**
* 对外公开:新闻列表(支持类型筛选、分页)
*/
@Anonymous
@GetMapping("/public")
public AjaxResult publicList(
@RequestParam(required = false) String newsType,
@RequestParam(required = false) Integer pageNum,
@RequestParam(required = false) Integer pageSize)
{
// 默认返回10条
if (pageSize == null || pageSize <= 0) {
pageSize = 10;
}
if (pageNum == null || pageNum <= 0) {
pageNum = 1;
}
int offset = (pageNum - 1) * pageSize;
long total = newsService.countNewsPublic(newsType);
List<HuayangNews> list = newsService.selectNewsForHome(newsType, offset, pageSize);
Map<String, Object> data = new HashMap<>(4);
data.put("rows", list);
data.put("total", total);
return success(data);
}
/**
* 对外公开:新闻详情
*/
@Anonymous
@GetMapping("/{newsId}/public")
public AjaxResult publicDetail(@PathVariable("newsId") Long newsId)
{
HuayangNews news = newsService.selectHuayangNewsById(newsId);
if (news == null || !"0".equals(news.getStatus()))
{
return error("新闻不存在或已下架");
}
// 增加阅读次数
newsService.incrementReadCount(newsId);
return success(news);
}
/**
* 对外公开:首页推荐新闻(与 /site/home/public 中 news 列表一致,供首页聚合接口失败时单独拉取)
*/
@Anonymous
@GetMapping("/homeRecommend")
public AjaxResult homeRecommend(@RequestParam(required = false) Integer limit)
{
if (limit == null || limit <= 0)
{
limit = 3;
}
if (limit > 20)
{
limit = 20;
}
HuayangNews query = new HuayangNews();
query.setStatus("0");
query.setIsHome("1");
List<HuayangNews> list = newsService.selectHuayangNewsList(query);
if (list == null)
{
list = List.of();
}
else if (list.size() > limit)
{
list = new ArrayList<>(list.subList(0, limit));
}
return success(list);
}
/**
* 对外公开:获取最新新闻(用于首页展示)
*/
@Anonymous
@GetMapping("/latest")
public AjaxResult latestNews(
@RequestParam(required = false) String newsType,
@RequestParam(required = false) Integer limit)
{
if (limit == null || limit <= 0) {
limit = 5;
}
List<HuayangNews> list = newsService.selectNewsForHome(newsType, null, limit);
return success(list);
}
/**
* 对外公开:新闻类型(来源 sys_dict_data,dict_type=huayang_news_type)
*/
@Anonymous
@GetMapping("/types")
public AjaxResult types()
{
SysDictData query = new SysDictData();
query.setDictType(NEWS_TYPE_DICT);
query.setStatus("0");
List<SysDictData> list = dictDataService.selectDictDataList(query);
List<Map<String, Object>> data = list.stream().map(item -> {
Map<String, Object> row = new HashMap<>(4);
row.put("label", item.getDictLabel());
row.put("value", item.getDictValue());
row.put("sort", item.getDictSort());
return row;
}).collect(Collectors.toList());
return success(data);
}
}

View File

@ -1,6 +1,8 @@
package com.ruoyi.web.controller.site;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@ -17,7 +19,9 @@ import com.ruoyi.system.domain.HuayangProductCategory;
import com.ruoyi.system.domain.HuayangProductInquiry;
import com.ruoyi.system.service.IHuayangProductCategoryService;
import com.ruoyi.system.service.IHuayangProductInquiryService;
import com.ruoyi.system.service.IHuayangI18nService;
import com.ruoyi.system.service.IHuayangProductService;
import org.apache.commons.lang3.StringUtils;
/**
* 门户站点公开接口:产品中心
@ -37,14 +41,18 @@ public class SiteProductController extends BaseController
@Autowired
private IHuayangProductInquiryService productInquiryService;
@Autowired
private IHuayangI18nService i18nService;
/**
* 对外公开:产品分类列表(无需登录)
*/
@Anonymous
@GetMapping("/category/public")
public AjaxResult publicCategoryList()
public AjaxResult publicCategoryList(@RequestParam(required = false) String lang)
{
List<HuayangProductCategory> list = productCategoryService.selectHuayangProductCategoryPublicList();
applyProductCategoryLocale(normalizeSiteLang(lang), list);
return success(list);
}
@ -55,9 +63,11 @@ public class SiteProductController extends BaseController
@GetMapping("/public")
public AjaxResult publicList(
@RequestParam(required = false) Long categoryId,
@RequestParam(required = false) String keyword)
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String lang)
{
List<HuayangProduct> list = productService.selectHuayangProductPublicList(categoryId, keyword);
applyProductLocale(normalizeSiteLang(lang), list);
return success(list);
}
@ -66,13 +76,15 @@ public class SiteProductController extends BaseController
*/
@Anonymous
@GetMapping("/{productId}/public")
public AjaxResult publicDetail(@PathVariable("productId") Long productId)
public AjaxResult publicDetail(@PathVariable("productId") Long productId,
@RequestParam(required = false) String lang)
{
HuayangProduct product = productService.selectHuayangProductById(productId);
if (product == null || !"0".equals(product.getStatus()))
{
return error("产品不存在或已下架");
}
applyProductDetailLocale(normalizeSiteLang(lang), product);
return success(product);
}
@ -93,4 +105,62 @@ public class SiteProductController extends BaseController
}
return error("提交失败,请稍后重试");
}
private static String normalizeSiteLang(String lang) {
if (StringUtils.isBlank(lang)) {
return "zh_cn";
}
return lang.trim().toLowerCase();
}
private void applyProductCategoryLocale(String langNorm, List<HuayangProductCategory> list) {
if ("zh_cn".equals(langNorm) || list == null || list.isEmpty()) return;
List<String> keys = new ArrayList<>();
for (HuayangProductCategory item : list) {
if (item.getCategoryId() != null) {
keys.add("product.category." + item.getCategoryId() + ".name");
}
}
Map<String, String> dict = i18nService.selectI18nValueMapByLangAndKeys(langNorm, keys);
for (HuayangProductCategory item : list) {
if (item.getCategoryId() == null) continue;
String v = dict.get("product.category." + item.getCategoryId() + ".name");
if (StringUtils.isNotEmpty(v)) {
item.setCategoryName(v);
}
}
}
private void applyProductLocale(String langNorm, List<HuayangProduct> list) {
if ("zh_cn".equals(langNorm) || list == null || list.isEmpty()) return;
List<String> keys = new ArrayList<>();
for (HuayangProduct item : list) {
if (item.getProductId() != null) {
keys.add("product.item." + item.getProductId() + ".name");
keys.add("product.item." + item.getProductId() + ".desc");
}
}
Map<String, String> dict = i18nService.selectI18nValueMapByLangAndKeys(langNorm, keys);
for (HuayangProduct item : list) {
if (item.getProductId() == null) continue;
String prefix = "product.item." + item.getProductId() + ".";
String n = dict.get(prefix + "name");
if (StringUtils.isNotEmpty(n)) item.setProductName(n);
String d = dict.get(prefix + "desc");
if (StringUtils.isNotEmpty(d)) item.setProductDesc(d);
}
}
private void applyProductDetailLocale(String langNorm, HuayangProduct product) {
if ("zh_cn".equals(langNorm) || product == null || product.getProductId() == null) return;
List<String> keys = new ArrayList<>();
String prefix = "product.item." + product.getProductId() + ".";
keys.add(prefix + "name");
keys.add(prefix + "desc");
Map<String, String> dict = i18nService.selectI18nValueMapByLangAndKeys(langNorm, keys);
String n = dict.get(prefix + "name");
if (StringUtils.isNotEmpty(n)) product.setProductName(n);
String d = dict.get(prefix + "desc");
if (StringUtils.isNotEmpty(d)) product.setProductDesc(d);
}
}

View File

@ -12,9 +12,7 @@ import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.system.domain.HuayangProduct;
import com.ruoyi.system.domain.HuayangNews;
import com.ruoyi.system.service.IHuayangProductService;
import com.ruoyi.system.service.IHuayangNewsService;
/**
* 全站搜索接口
@ -29,11 +27,8 @@ public class SiteSearchController extends BaseController
@Autowired
private IHuayangProductService productService;
@Autowired
private IHuayangNewsService newsService;
/**
* 根据关键词搜索产品和新闻
* 根据关键词搜索产品
*/
@GetMapping("/all")
public AjaxResult search(@RequestParam(value = "keyword", required = false) String keyword)
@ -41,17 +36,10 @@ public class SiteSearchController extends BaseController
Map<String, Object> results = new HashMap<>();
if (keyword != null && !keyword.trim().isEmpty()) {
// 搜索产品
HuayangProduct productQuery = new HuayangProduct();
productQuery.setProductName(keyword);
List<HuayangProduct> products = productService.selectHuayangProductList(productQuery);
results.put("products", products);
// 搜索新闻
HuayangNews newsQuery = new HuayangNews();
newsQuery.setNewsTitle(keyword);
List<HuayangNews> news = newsService.selectHuayangNewsList(newsQuery);
results.put("news", news);
}
return success(results);

View File

@ -1,15 +1,22 @@
package com.ruoyi.web.controller.site;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.system.domain.HuayangCompanyTimeline;
import com.ruoyi.system.service.IHuayangCompanyTimelineService;
import com.ruoyi.system.service.IHuayangI18nService;
/**
* 门户站点公开接口:发展历程
@ -23,14 +30,131 @@ public class SiteTimelineController extends BaseController
@Autowired
private IHuayangCompanyTimelineService huayangCompanyTimelineService;
@Autowired
private IHuayangI18nService i18nService;
/**
* 对外公开:正常状态的发展历程列表(无需登录)
* 对外公开:正常状态的发展历程列表(无需登录)。
* <p>可选 {@code lang}:非 zh_cn 时用 i18n key {@code company.timeline.{timelineId}.title|summary|detail},
* 暂无则读 {@code extra_data.langContent}(对象字段 title、summary、detail;旧版整段字符串视为 detail)。</p>
*/
@Anonymous
@GetMapping("/public")
public AjaxResult publicList()
public AjaxResult publicList(@RequestParam(value = "lang", required = false) String lang)
{
List<HuayangCompanyTimeline> list = huayangCompanyTimelineService.selectHuayangCompanyTimelinePublicList();
applyTimelineLocale(normalizeSiteLang(lang), list);
return success(list);
}
private static String normalizeSiteLang(String lang) {
if (StringUtils.isBlank(lang)) {
return "zh_cn";
}
return lang.trim().toLowerCase();
}
private void applyTimelineLocale(String langNorm, List<HuayangCompanyTimeline> list)
{
if ("zh_cn".equals(langNorm) || list == null || list.isEmpty()) {
return;
}
List<String> keys = new ArrayList<>();
for (HuayangCompanyTimeline t : list) {
if (t.getTimelineId() == null) {
continue;
}
String id = String.valueOf(t.getTimelineId());
keys.add("company.timeline." + id + ".title");
keys.add("company.timeline." + id + ".summary");
keys.add("company.timeline." + id + ".detail");
}
Map<String, String> dict = i18nService.selectI18nValueMapByLangAndKeys(langNorm, keys);
for (HuayangCompanyTimeline t : list) {
if (t.getTimelineId() == null) {
continue;
}
String id = String.valueOf(t.getTimelineId());
String p = "company.timeline." + id + ".";
String title = dict.get(p + "title");
if (StringUtils.isBlank(title)) {
title = timelineManualField(t.getExtraData(), langNorm, "title");
}
if (StringUtils.isNotEmpty(title)) {
t.setEventTitle(title);
}
String summary = dict.get(p + "summary");
if (StringUtils.isBlank(summary)) {
summary = timelineManualField(t.getExtraData(), langNorm, "summary");
}
if (StringUtils.isNotEmpty(summary)) {
t.setEventContent(summary);
}
String detail = dict.get(p + "detail");
if (StringUtils.isBlank(detail)) {
detail = timelineManualDetailFromExtra(t.getExtraData(), langNorm);
}
if (StringUtils.isNotEmpty(detail)) {
t.setEventDetail(detail);
}
}
}
private String timelineManualField(String extraData, String langNorm, String field)
{
Object entry = getLangContentEntry(extraData, langNorm);
if (entry instanceof JSONObject) {
JSONObject o = (JSONObject) entry;
if ("summary".equals(field)) {
String s = o.getString("summary");
if (StringUtils.isBlank(s)) {
s = o.getString("content");
}
return StringUtils.isNotBlank(s) ? s : null;
}
if ("title".equals(field)) {
String s = o.getString("title");
return StringUtils.isNotBlank(s) ? s : null;
}
}
return null;
}
/** summary/detail 落空时:旧版仅存一段字符串 → 视作 detail */
private String timelineManualDetailFromExtra(String extraData, String langNorm)
{
Object entry = getLangContentEntry(extraData, langNorm);
if (entry instanceof JSONObject) {
String detail = ((JSONObject) entry).getString("detail");
return StringUtils.isNotBlank(detail) ? detail : null;
}
if (entry instanceof String) {
String s = (String) entry;
return StringUtils.isNotBlank(s) ? s : null;
}
return null;
}
private Object getLangContentEntry(String extraData, String langNorm)
{
if (StringUtils.isBlank(extraData) || StringUtils.isBlank(langNorm)) {
return null;
}
try {
JSONObject root = JSON.parseObject(extraData);
if (root == null) {
return null;
}
JSONObject langContent = root.getJSONObject("langContent");
if (langContent == null) {
return null;
}
return langContent.get(langNorm);
} catch (Exception ex) {
return null;
}
}
}

View File

@ -20,9 +20,8 @@ import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.file.FileUploadUtils;
import com.ruoyi.common.utils.file.FileUtils;
import com.ruoyi.common.utils.file.MimeTypeUtils;
import com.ruoyi.common.utils.file.ImageUploadUtils;
import com.ruoyi.framework.web.service.TokenService;
import com.ruoyi.system.service.ISysUserService;
@ -128,7 +127,7 @@ public class SysProfileController extends BaseController
if (!file.isEmpty())
{
LoginUser loginUser = getLoginUser();
String avatar = FileUploadUtils.upload(RuoYiConfig.getAvatarPath(), file, MimeTypeUtils.IMAGE_EXTENSION, true);
String avatar = ImageUploadUtils.upload(RuoYiConfig.getAvatarPath(), file, true);
if (userService.updateUserAvatar(loginUser.getUserId(), avatar))
{
String oldAvatar = loginUser.getUser().getAvatar();

View File

@ -6,9 +6,9 @@ spring:
druid:
# 主库数据源
master:
url: jdbc:mysql://localhost:3306/hauyang?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: root
url: ${DB_URL:jdbc:mysql://localhost:3306/huayang?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true}
username: ${DB_USERNAME:huayang}
password: ${DB_PASSWORD:}
# 从库数据源
slave:
# 从数据源开关/默认关闭
@ -58,4 +58,4 @@ spring:
merge-sql: true
wall:
config:
multi-statement-allow: true
multi-statement-allow: true

View File

@ -0,0 +1,17 @@
# 生产环境(与 druid 数据源一起启用,例如:--spring.profiles.active=prod,druid)
# 请在服务器用环境变量或独立配置覆盖:数据源、Redis、ruoyi.profile 上传目录、token.secret 等
spring:
devtools:
restart:
enabled: false
logging:
level:
com.ruoyi: info
org.springframework: warn
springdoc:
api-docs:
enabled: false
swagger-ui:
enabled: false

View File

@ -7,7 +7,8 @@ ruoyi:
# 版权年份
copyrightYear: 2026
# 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
profile: D:/ruoyi/uploadPath
# 开发环境使用项目内目录,避免本机不存在 D:/ruoyi/uploadPath 时上传图全部 404
profile: ./uploadPath
# 获取ip地址开关
addressEnabled: false
# 验证码类型 math 数字计算 char 字符验证
@ -16,7 +17,7 @@ ruoyi:
# 开发环境配置
server:
# 服务器的HTTP端口,默认为8080
port: 8080
port: 7003
servlet:
# 应用的访问路径
context-path: /
@ -135,8 +136,7 @@ referer:
# 防盗链开关
enabled: false
# 允许的域名列表
allowed-domains: localhost,127.0.0.1,ruoyi.vip,www.ruoyi.vip
allowed-domains: localhost,127.0.0.1,192.168.1.159,ruoyi.vip,www.ruoyi.vip
# 防止XSS攻击
xss:
# 过滤开关

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 日志存放路径 -->
<property name="log.path" value="/home/ruoyi/logs" />
<property name="log.path" value="./logs" />
<!-- 日志输出格式 -->
<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />

View File

@ -1 +0,0 @@
restart.include.json=/com.alibaba.fastjson2.*.jar

View File

@ -1,61 +0,0 @@
# 数据源配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
druid:
# 主库数据源
master:
url: jdbc:mysql://localhost:3306/hauyang?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: root
# 从库数据源
slave:
# 从数据源开关/默认关闭
enabled: false
url:
username:
password:
# 初始连接数
initialSize: 5
# 最小连接池数量
minIdle: 10
# 最大连接池数量
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置连接超时时间
connectTimeout: 30000
# 配置网络超时时间
socketTimeout: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
# 配置一个连接在池中最大生存的时间,单位是毫秒
maxEvictableIdleTimeMillis: 900000
# 配置检测连接是否有效
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
webStatFilter:
enabled: true
statViewServlet:
enabled: true
# 设置白名单,不填则允许所有访问
allow:
url-pattern: /druid/*
# 控制台管理用户名和密码
login-username: ruoyi
login-password: 123456
filter:
stat:
enabled: true
# 慢SQL记录
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: true
wall:
config:
multi-statement-allow: true

View File

@ -1,147 +0,0 @@
# 项目相关配置
ruoyi:
# 名称
name: RuoYi
# 版本
version: 3.9.2
# 版权年份
copyrightYear: 2026
# 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
profile: D:/ruoyi/uploadPath
# 获取ip地址开关
addressEnabled: false
# 验证码类型 math 数字计算 char 字符验证
captchaType: math
# 开发环境配置
server:
# 服务器的HTTP端口,默认为8080
port: 8080
servlet:
# 应用的访问路径
context-path: /
tomcat:
# tomcat的URI编码
uri-encoding: UTF-8
# 连接数满后的排队数,默认为100
accept-count: 1000
threads:
# tomcat最大线程数,默认为200
max: 800
# Tomcat启动初始化的线程数,默认值10
min-spare: 100
# 日志配置
logging:
level:
com.ruoyi: debug
org.springframework: warn
# 用户配置
user:
password:
# 密码最大错误次数
maxRetryCount: 5
# 密码锁定时间(默认10分钟)
lockTime: 10
# Spring配置
spring:
# 资源信息
messages:
# 国际化资源文件路径
basename: i18n/messages
profiles:
active: druid
# 文件上传
servlet:
multipart:
# 单个文件大小
max-file-size: 10MB
# 设置总上传的文件大小
max-request-size: 20MB
jackson:
time-zone: GMT+8
date-format: yyyy-MM-dd HH:mm:ss
# 服务模块
devtools:
restart:
# 热部署开关
enabled: true
data:
# redis 配置
redis:
# 地址
host: localhost
# 端口,默认为6379
port: 6379
# 数据库索引
database: 0
# 密码
password:
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 8
# 连接池的最大数据库连接数
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# token配置
token:
# 令牌自定义标识
header: Authorization
# 令牌密钥
secret: abcdefghijklmnopqrstuvwxyz
# 令牌有效期(默认30分钟)
expireTime: 30
# MyBatis配置
mybatis:
# 搜索指定包别名
typeAliasesPackage: com.ruoyi.**.domain
# 配置mapper的扫描,找到所有的mapper.xml映射文件
mapperLocations: classpath*:mapper/**/*Mapper.xml
# 加载全局的配置文件
configLocation: classpath:mybatis/mybatis-config.xml
# PageHelper分页插件
pagehelper:
helperDialect: mysql
supportMethodsArguments: true
params: count=countSql
# Springdoc配置
springdoc:
api-docs:
path: /v3/api-docs
swagger-ui:
enabled: true
path: /swagger-ui.html
tags-sorter: alpha
group-configs:
- group: 'default'
display-name: '测试模块'
paths-to-match: '/**'
packages-to-scan: com.ruoyi.web.controller.tool
# 防盗链配置
referer:
# 防盗链开关
enabled: false
# 允许的域名列表
allowed-domains: localhost,127.0.0.1,ruoyi.vip,www.ruoyi.vip
# 防止XSS攻击
xss:
# 过滤开关
enabled: true
# 排除链接(多个用逗号分隔)
excludes: /system/notice
# 匹配链接
urlPatterns: /system/*,/monitor/*,/tool/*

View File

@ -1,24 +0,0 @@
Application Version: ${ruoyi.version}
Spring Boot Version: ${spring-boot.version}
////////////////////////////////////////////////////////////////////
// _ooOoo_ //
// o8888888o //
// 88" . "88 //
// (| ^_^ |) //
// O\ = /O //
// ____/`---'\____ //
// .' \\| |// `. //
// / \\||| : |||// \ //
// / _||||| -:- |||||- \ //
// | | \\\ - /// | | //
// | \_| ''\---/'' | | //
// \ .-\__ `-` ___/-. / //
// ___`. .' /--.--\ `. . ___ //
// ."" '< `.___\_<|>_/___.' >'"". //
// | | : `- \`.;`\ _ /`;.`/ - ` : | | //
// \ \ `-. \_ __\ /__ _/ .-` / / //
// ========`-.____`-.___\_____/___.-`____.-'======== //
// `=---=' //
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //
// 佛祖保佑 永不宕机 永无BUG //
////////////////////////////////////////////////////////////////////

Some files were not shown because too many files have changed in this diff Show More