修复文章发布报错
This commit is contained in:
@@ -489,6 +489,7 @@ h3 {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
background: transparent;
|
||||
|
||||
html:not(.dark) & {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
@@ -533,22 +534,40 @@ h3 {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
// 嵌套展开的子菜单容器保持透明,避免 Element Plus 注入的
|
||||
// --el-menu-bg-color 在二级及以下区域形成不透明背景块;
|
||||
// 二级 ul 不需要上下内边距
|
||||
.el-menu--inline {
|
||||
background: transparent !important;
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
// 子菜单样式
|
||||
.el-sub-menu {
|
||||
.el-sub-menu__title {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&.is-opened .el-sub-menu__title {
|
||||
background: rgba(255, 255, 255, 0.08) !important;
|
||||
}
|
||||
// 展开的父级菜单标题不加背景,背景只留给当前激活的菜单项
|
||||
|
||||
.el-menu-item {
|
||||
padding-left: 48px !important;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// 二级菜单项缩进 24px
|
||||
.el-menu--inline > .el-menu-item,
|
||||
.el-menu--inline > .el-sub-menu > .el-sub-menu__title {
|
||||
padding-left: 24px !important;
|
||||
}
|
||||
|
||||
// 三级菜单项缩进 48px
|
||||
.el-menu--inline .el-menu--inline > .el-menu-item,
|
||||
.el-menu--inline .el-menu--inline > .el-sub-menu > .el-sub-menu__title {
|
||||
padding-left: 48px !important;
|
||||
}
|
||||
|
||||
// 暗色主题适配
|
||||
html.dark & {
|
||||
.el-menu-item.is-active {
|
||||
@@ -566,10 +585,6 @@ h3 {
|
||||
background: rgba(255, 255, 255, 0.08) !important;
|
||||
color: var(--el-color-primary-light-3) !important;
|
||||
}
|
||||
|
||||
.el-sub-menu.is-opened .el-sub-menu__title {
|
||||
background: rgba(64, 158, 255, 0.08) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,18 @@
|
||||
:before-close="handleBeforeClose"
|
||||
>
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="80px">
|
||||
|
||||
<el-form-item label="是否转载" prop="is_trans">
|
||||
<el-radio-group v-model="form.is_trans">
|
||||
<el-radio-button :value="0">否</el-radio-button>
|
||||
<el-radio-button :value="1">是</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="发布地址" prop="transurl" v-if="form.is_trans === 1">
|
||||
<el-input v-model="form.transurl" placeholder="请输入转载文章地址" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="标题" prop="title">
|
||||
<el-input v-model="form.title" placeholder="请输入文章标题" />
|
||||
</el-form-item>
|
||||
@@ -128,17 +140,6 @@
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<el-form-item label="是否转载" prop="is_trans">
|
||||
<el-radio-group v-model="form.is_trans">
|
||||
<el-radio-button :value="0">否</el-radio-button>
|
||||
<el-radio-button :value="1">是</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="发布地址" prop="transurl" v-if="form.is_trans === 1">
|
||||
<el-input v-model="form.transurl" placeholder="请输入转载文章地址" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="内容" prop="content">
|
||||
<div class="editor-container">
|
||||
<UmoEditor v-model="form.content" />
|
||||
@@ -233,9 +234,14 @@ const rules = {
|
||||
// 获取分类列表
|
||||
async function fetchCategories() {
|
||||
try {
|
||||
const res = await listCategories();
|
||||
const res = await listCategories({ page: 1, limit: 1000 });
|
||||
if (res.code === 200) {
|
||||
const categories = Array.isArray(res.data) ? res.data : [];
|
||||
// 兼容两种返回结构:data 为数组 或 data.list 为数组
|
||||
const categories = Array.isArray(res.data)
|
||||
? res.data
|
||||
: Array.isArray(res.data?.list)
|
||||
? res.data.list
|
||||
: [];
|
||||
// 将分类转换为树形结构
|
||||
cateOptions.value = buildCategoryTree(categories);
|
||||
}
|
||||
@@ -452,11 +458,7 @@ async function handleConfirm(status = 1) {
|
||||
: res;
|
||||
}
|
||||
|
||||
if (
|
||||
resp &&
|
||||
resp.code === 200 &&
|
||||
(resp.message === "success" || resp.msg === "success")
|
||||
) {
|
||||
if (resp && resp.code === 200) {
|
||||
ElMessage.success(
|
||||
status === 0
|
||||
? "保存草稿成功"
|
||||
|
||||
@@ -87,9 +87,11 @@ const fetchCategories = async () => {
|
||||
|
||||
// 根据分类ID获取分类名称
|
||||
const getCategoryName = (cate) => {
|
||||
if (!cate) return "无分类";
|
||||
if (!cate && cate !== 0) return "无分类";
|
||||
// 确保比较时类型一致
|
||||
const cateId = Number(cate);
|
||||
// 传入的不是数字(已经是分类名),直接展示
|
||||
if (Number.isNaN(cateId)) return String(cate);
|
||||
const category = categoryOptions.value.find(
|
||||
(item) => Number(item.id) === cateId,
|
||||
);
|
||||
|
||||
@@ -41,11 +41,12 @@
|
||||
<el-date-picker
|
||||
v-model="dateFilter"
|
||||
type="daterange"
|
||||
value-format="yyyy-MM-dd"
|
||||
value-format="YYYY-MM-DD"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
style="width: 240px; margin-right: 10px"
|
||||
@change="handleFilterChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -232,6 +233,7 @@ const loading = ref(false);
|
||||
const articleList = ref([]);
|
||||
const searchQuery = ref("");
|
||||
const categoryFilter = ref("");
|
||||
const dateFilter = ref(null);
|
||||
const categoryOptions = ref([]);
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(10);
|
||||
@@ -301,6 +303,8 @@ async function fetchArticleList() {
|
||||
const params = {
|
||||
keyword: searchQuery.value,
|
||||
cate: categoryFilter.value,
|
||||
startDate: dateFilter.value?.[0] || "",
|
||||
endDate: dateFilter.value?.[1] || "",
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
};
|
||||
@@ -360,14 +364,15 @@ function handleEdit(row) {
|
||||
id: m.id,
|
||||
title: m.title || "",
|
||||
author: m.author || "",
|
||||
cate: m.cate || "",
|
||||
// 详情接口的 cate 返回的是分类名,回显/预览需要分类ID
|
||||
cate: m.cate_id ?? m.cate ?? "",
|
||||
content: m.content || "",
|
||||
desc: m.desc || "",
|
||||
publish_time: m.publish_time || null,
|
||||
_raw: m,
|
||||
};
|
||||
} else {
|
||||
currentRow.value = { ...row };
|
||||
currentRow.value = { ...row, cate: row.cate_id ?? row.cate };
|
||||
}
|
||||
dialogVisible.value = true;
|
||||
});
|
||||
@@ -388,7 +393,8 @@ function handleView(row) {
|
||||
id: m.id,
|
||||
title: m.title || "",
|
||||
author: m.author || "",
|
||||
cate: m.cate || "",
|
||||
// 详情接口的 cate 返回的是分类名,预览组件按分类ID匹配名称
|
||||
cate: m.cate_id ?? m.cate ?? "",
|
||||
content: m.content || "",
|
||||
desc: m.desc || "",
|
||||
view_count: m.view_count || 0,
|
||||
@@ -586,6 +592,7 @@ function handleSearch() {
|
||||
function handleRefresh() {
|
||||
searchQuery.value = "";
|
||||
categoryFilter.value = "";
|
||||
dateFilter.value = null;
|
||||
currentPage.value = 1;
|
||||
fetchArticleList();
|
||||
}
|
||||
|
||||
@@ -229,10 +229,13 @@ const beforeImageUpload = (file: any) => {
|
||||
|
||||
// 图片上传成功
|
||||
const handleImageSuccess = (response: any) => {
|
||||
if (response.code === 200) {
|
||||
// code 200=新上传成功,201=文件已存在(直接使用已有链接)
|
||||
if (response.code === 200 || response.code === 201) {
|
||||
// 直接保存相对路径,不拼接API_BASE_URL
|
||||
currentBanner.value.image = response.data.url || response.data.path;
|
||||
ElMessage.success("图片上传成功");
|
||||
ElMessage.success(
|
||||
response.code === 201 ? "文件已存在,直接使用" : "图片上传成功",
|
||||
);
|
||||
} else {
|
||||
ElMessage.error(response.msg || "图片上传失败");
|
||||
}
|
||||
|
||||
@@ -414,10 +414,11 @@ const beforeImageUpload = (file: any) => {
|
||||
|
||||
// 图片上传成功
|
||||
const handleImageSuccess = (response: any) => {
|
||||
if (response.code === 200) {
|
||||
// code 200=新上传成功,201=文件已存在(直接使用已有链接)
|
||||
if (response.code === 200 || response.code === 201) {
|
||||
// 直接保存相对路径,不拼接API_BASE_URL
|
||||
currentMenu.value.image = response.data.url || response.data.path;
|
||||
ElMessage.success('图片上传成功');
|
||||
ElMessage.success(response.code === 201 ? '文件已存在,直接使用' : '图片上传成功');
|
||||
} else {
|
||||
ElMessage.error(response.msg || '图片上传失败');
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ const isCreated = ref(false)
|
||||
|
||||
const options = {
|
||||
height: '100%',
|
||||
// umo-editor 会定时触发自动保存(onSave),内容已随表单统一提交,这里给空实现避免控制台报错
|
||||
onSave: async () => {},
|
||||
}
|
||||
|
||||
const onFileUpload = async (file) => {
|
||||
|
||||
@@ -0,0 +1,474 @@
|
||||
# 关于租户官网服务器端 nginx 的配置
|
||||
|
||||
> 适用场景:多租户官网系统。访客访问租户域名(如 `ceshi.yunzer.com.cn`)时,由 nginx 转发给 Go 服务,Go 按请求 Host 识别租户,用标签引擎解析 `themes/{模板编码}/` 模板后直出 HTML。
|
||||
>
|
||||
> 本文给出宝塔环境下两个主域名(`dh2.fun`、`yunzer.com.cn`)的完整 nginx 配置、设计原则与注意事项。
|
||||
|
||||
---
|
||||
|
||||
## 一、架构原理
|
||||
|
||||
```
|
||||
访客浏览器
|
||||
│ https://ceshi.yunzer.com.cn/
|
||||
▼
|
||||
nginx(宝塔)
|
||||
│ 全部请求 proxy_pass → Go 服务(upstream 统一管理地址)
|
||||
│ 透传 Host 头(Go 靠它识别租户)
|
||||
▼
|
||||
Go 服务(beego,默认 8081 端口)
|
||||
│ resolveTid:按 Host 查库识别租户
|
||||
│ tagengine:渲染 themes/{模板编码}/ 下的模板
|
||||
▼
|
||||
直出 HTML(无需前端 SPA)
|
||||
```
|
||||
|
||||
### 1.1 Go 侧提供的站点路由
|
||||
|
||||
| 路径 | 说明 |
|
||||
|---|---|
|
||||
| `GET /` | 租户官网首页(渲染 `index.html` 模板) |
|
||||
| `GET /news` | 新闻列表(`?page=N` 分页) |
|
||||
| `GET /news/:id` | 文章详情 |
|
||||
| `GET /page/:path` | 单页(如 `/page/about`) |
|
||||
| `/themes/*` | 模板静态资源(beego `SetStaticPath("/themes", "themes")`,对应 `go/themes/` 目录) |
|
||||
| `/uploads/*` | 上传文件静态访问 |
|
||||
| `/backend/*`、`/platform/*` 等 | 管理端接口(nginx 以 `/api/` 前缀转发并剥掉前缀) |
|
||||
|
||||
> Go 的官网渲染路由在 `APP_MODE=all`(默认值)或 `APP_MODE=index` 时注册。
|
||||
|
||||
### 1.2 Go 按 Host 识别租户的规则(tenant_site.go)
|
||||
|
||||
按顺序匹配:
|
||||
|
||||
1. dev 环境:支持 `X-Tenant-ID` 请求头切换租户(生产无效);
|
||||
2. `localhost` / `127.0.0.1` → 租户 1;
|
||||
3. `yz_system_domain_pool.main_domain` 命中(status=1)→ 租户 1(平台主站);
|
||||
4. `yz_system_tenant_domain.full_domain` 命中(status=1、未软删)→ 对应租户;
|
||||
5. 全部未命中 → 返回"站点不存在"404 页(**不再默认回落租户 1**)。
|
||||
|
||||
所以平台端"域名发放"时必须保证 `yz_system_tenant_domain.full_domain` 精确等于实际访问域名(如 `ceshi.yunzer.com.cn`)。
|
||||
|
||||
---
|
||||
|
||||
## 二、配置核心策略(为什么这么写)
|
||||
|
||||
### 2.1 upstream 统一管理后端地址 —— 迁移只改一行
|
||||
|
||||
所有 `proxy_pass` 不写死 IP,统一引用 upstream。迁移服务器时只改 upstream 里的 `server` 一行。
|
||||
|
||||
**upstream 写在哪里**:直接写在宝塔对应站点的配置文件里、**整个文件最顶部(第一个 `server {` 块之前)**,不需要配置到其他地方。宝塔站点配置文件会被 include 进主配置的 `http {}` 上下文,`upstream` 定义在 server 块之外即合法。常见报错:`unknown "go_backend_dh2" upstream`(顶部漏了 upstream 定义)、`"upstream" directive is not allowed here`(误写进了 server 块内)。
|
||||
|
||||
**注意:两个站点配置文件都会被 include 进同一个 http 上下文,upstream 重名会导致 `duplicate upstream` 启动失败,必须各起各的名字**(本文用 `go_backend_dh2` / `go_backend_yunzer`)。
|
||||
|
||||
### 2.2 默认全量转发 —— 以后加功能零配置
|
||||
|
||||
不采用"白名单式"逐个转发(`location /news`、`location /page/`……),而是:
|
||||
|
||||
- nginx 只保留**必须拦截**的规则:敏感文件/目录返回 404、`/api/` 剥前缀;
|
||||
- 其余一切请求走兜底 `location /` 转发给 Go;
|
||||
- Go 对不认识的路径返回自己的 404 页,行为闭环。
|
||||
|
||||
这样以后 Go 增加任何路由(新功能页面、新接口页),**nginx 一行都不用改**。
|
||||
|
||||
### 2.3 管理端 SPA 的放置建议
|
||||
|
||||
主域名与所有子域名都转发给 Go 后,这些域名上就不能再跑 history 模式的 SPA 了。建议:
|
||||
|
||||
- platform / backend 管理端部署在**专属子域名**(如 `admin.yunzer.com.cn`)或独立站点;
|
||||
- nginx 里为它单独建一个 `server_name` 精确匹配的 server 块。nginx 匹配顺序是**精确 server_name 优先于正则**,不会被"所有子域名"的正则块抢走;
|
||||
- 若确实需要静态文件与 Go 共存(纯静态、无 SPA 深链接),可用折中写法:
|
||||
|
||||
```nginx
|
||||
location / {
|
||||
try_files $uri @go; # 本地存在的文件先出静态,其余转 Go
|
||||
}
|
||||
location @go {
|
||||
proxy_pass http://go_backend_yunzer;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 `/api/` 的尾部斜杠不能删
|
||||
|
||||
```nginx
|
||||
location /api/ {
|
||||
proxy_pass http://go_backend_xxx/; # 尾部 / 负责把 /api/backend/login 映射成 /backend/login
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Go 的路由注册在 `/backend/*`、`/platform/*`(没有 `/api` 前缀),靠这个尾部斜杠剥前缀。
|
||||
|
||||
---
|
||||
|
||||
## 三、完整配置 —— 宝塔站点 `dh2.fun`
|
||||
|
||||
文件位置:宝塔 → 网站 → dh2.fun → 设置 → 配置文件。整份替换。
|
||||
|
||||
```nginx
|
||||
# ===== 后端 Go 服务地址统一定义,迁移时只改这一行 =====
|
||||
upstream go_backend_dh2 {
|
||||
server 10.31.100.2:8081; # 若 nginx 与 Go 同机,建议改为 127.0.0.1:8081(迁移零改动)
|
||||
}
|
||||
|
||||
# ==================== 主域名 dh2.fun(全量托管给 Go)====================
|
||||
server {
|
||||
listen 80;
|
||||
listen 443 ssl;
|
||||
listen 443 quic;
|
||||
http2 on;
|
||||
|
||||
# 只匹配主域名(子域名由下方独立 server 块处理)
|
||||
server_name dh2.fun;
|
||||
|
||||
#CERT-APPLY-CHECK--START
|
||||
include /www/server/panel/vhost/nginx/well-known/dh2.fun.conf;
|
||||
#CERT-APPLY-CHECK--END
|
||||
include /www/server/panel/vhost/nginx/extension/dh2.fun/*.conf;
|
||||
|
||||
#SSL-START SSL相关配置,请勿删除或修改下一行带注释的404规则
|
||||
#error_page 404/404.html;
|
||||
|
||||
#HTTP_TO_HTTPS_START
|
||||
set $isRedcert 1;
|
||||
if ($server_port != 443) {
|
||||
set $isRedcert 2;
|
||||
}
|
||||
if ( $uri ~ /\.well-known/ ) {
|
||||
set $isRedcert 1;
|
||||
}
|
||||
if ($isRedcert != 1) {
|
||||
rewrite ^(/.*)$ https://$host$1 permanent;
|
||||
}
|
||||
#HTTP_TO_HTTPS_END
|
||||
|
||||
ssl_certificate /www/server/panel/vhost/cert/dh2.fun/fullchain.pem;
|
||||
ssl_certificate_key /www/server/panel/vhost/cert/dh2.fun/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_session_tickets on;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 10m;
|
||||
add_header Strict-Transport-Security "max-age=31536000";
|
||||
error_page 497 https://$host$request_uri;
|
||||
#SSL-END
|
||||
|
||||
# --- 安全拦截:敏感文件 ---
|
||||
location ~* (\.user.ini|\.htaccess|\.htpasswd|\.env.*|\.project|\.bashrc|\.bash_profile|\.bash_logout|\.DS_Store|\.gitignore|\.gitattributes|LICENSE|README\.md|CLAUDE\.md|CHANGELOG\.md|CHANGELOG|CONTRIBUTING\.md|TODO\.md|FAQ\.md|composer\.json|composer\.lock|package(-lock)?\.json|yarn\.lock|pnpm-lock\.yaml|\.\w+~|\.swp|\.swo|\.bak(up)?|\.old|\.tmp|\.temp|\.log|\.sql(\.gz)?|docker-compose\.yml|docker\.env|Dockerfile|\.csproj|\.sln|Cargo\.toml|Cargo\.lock|go\.mod|go\.sum|phpunit\.xml|phpunit\.xml|pom\.xml|build\.gradl|pyproject\.toml|requirements\.txt|application(-\w+)?\.(ya?ml|properties))$ {
|
||||
return 404;
|
||||
}
|
||||
|
||||
# --- 安全拦截:敏感目录 ---
|
||||
location ~* /(\.git|\.svn|\.bzr|\.vscode|\.claude|\.idea|\.ssh|\.github|\.npm|\.yarn|\.pnpm|\.cache|\.husky|\.turbo|\.next|\.nuxt|node_modules|runtime)/ {
|
||||
return 404;
|
||||
}
|
||||
|
||||
# SSL 证书验证目录
|
||||
location ~ \.well-known {
|
||||
allow all;
|
||||
}
|
||||
|
||||
if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) {
|
||||
return 403;
|
||||
}
|
||||
|
||||
# /api/ 转发并剥掉 /api 前缀(尾部斜杠不能删)
|
||||
location /api/ {
|
||||
proxy_pass http://go_backend_dh2/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# 其余一切请求全部交给 Go:首页、/news、/page/xxx、/themes/ 静态资源、
|
||||
# /uploads/ 以及以后新增的任何路由,自动生效,nginx 永远不用再改
|
||||
location / {
|
||||
proxy_pass http://go_backend_dh2;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
access_log /www/wwwlogs/dh2.fun.log;
|
||||
error_log /www/wwwlogs/dh2.fun.error.log;
|
||||
}
|
||||
|
||||
# ==================== 租户官网渲染:*.dh2.fun 所有子域名 ====================
|
||||
server {
|
||||
listen 80;
|
||||
listen 443 ssl;
|
||||
http2 on;
|
||||
|
||||
# 匹配所有子域名,如 ceshi.dh2.fun(租户域名发放后无需再改 nginx)
|
||||
server_name ~^(?<subdomain>.+)\.dh2\.fun$;
|
||||
|
||||
#CERT-APPLY-CHECK--START
|
||||
include /www/server/panel/vhost/nginx/well-known/dh2.fun.conf;
|
||||
#CERT-APPLY-CHECK--END
|
||||
|
||||
#HTTP_TO_HTTPS_START
|
||||
set $isRedcert 1;
|
||||
if ($server_port != 443) {
|
||||
set $isRedcert 2;
|
||||
}
|
||||
if ( $uri ~ /\.well-known/ ) {
|
||||
set $isRedcert 1;
|
||||
}
|
||||
if ($isRedcert != 1) {
|
||||
rewrite ^(/.*)$ https://$host$1 permanent;
|
||||
}
|
||||
#HTTP_TO_HTTPS_END
|
||||
|
||||
# 注意:子域名 HTTPS 要求泛域名证书(*.dh2.fun),见文档"注意事项"
|
||||
ssl_certificate /www/server/panel/vhost/cert/dh2.fun/fullchain.pem;
|
||||
ssl_certificate_key /www/server/panel/vhost/cert/dh2.fun/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 10m;
|
||||
add_header Strict-Transport-Security "max-age=31536000";
|
||||
error_page 497 https://$host$request_uri;
|
||||
|
||||
location ~ \.well-known {
|
||||
allow all;
|
||||
}
|
||||
|
||||
# 全部请求转发给 Go,由 Go 按 Host(如 ceshi.dh2.fun)识别租户并渲染官网
|
||||
location / {
|
||||
proxy_pass http://go_backend_dh2;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
access_log /www/wwwlogs/dh2.fun.tenant.log;
|
||||
error_log /www/wwwlogs/dh2.fun.tenant.error.log;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、完整配置 —— 宝塔站点 `yunzer.com.cn`
|
||||
|
||||
文件位置:宝塔 → 网站 → yunzer.com.cn → 设置 → 配置文件。整份替换。
|
||||
|
||||
```nginx
|
||||
# ===== 后端 Go 服务地址统一定义,迁移时只改这一行 =====
|
||||
# 注意:不能与 dh2.fun 配置里的 upstream 同名,否则 nginx 报 duplicate upstream
|
||||
upstream go_backend_yunzer {
|
||||
server 10.31.100.2:8081; # 若 nginx 与 Go 同机,建议改为 127.0.0.1:8081(迁移零改动)
|
||||
}
|
||||
|
||||
# ==================== 主域名 yunzer.com.cn(全量托管给 Go)====================
|
||||
server {
|
||||
listen 80;
|
||||
listen 443 ssl;
|
||||
listen 443 quic;
|
||||
http2 on;
|
||||
|
||||
# 只匹配主域名(子域名由下方独立 server 块处理)
|
||||
server_name yunzer.com.cn;
|
||||
|
||||
#CERT-APPLY-CHECK--START
|
||||
include /www/server/panel/vhost/nginx/well-known/yunzer.com.cn.conf;
|
||||
#CERT-APPLY-CHECK--END
|
||||
include /www/server/panel/vhost/nginx/extension/yunzer.com.cn/*.conf;
|
||||
|
||||
#SSL-START SSL相关配置,请勿删除或修改下一行带注释的404规则
|
||||
#error_page 404/404.html;
|
||||
|
||||
#HTTP_TO_HTTPS_START
|
||||
set $isRedcert 1;
|
||||
if ($server_port != 443) {
|
||||
set $isRedcert 2;
|
||||
}
|
||||
if ( $uri ~ /\.well-known/ ) {
|
||||
set $isRedcert 1;
|
||||
}
|
||||
if ($isRedcert != 1) {
|
||||
rewrite ^(/.*)$ https://$host$1 permanent;
|
||||
}
|
||||
#HTTP_TO_HTTPS_END
|
||||
|
||||
ssl_certificate /www/server/panel/vhost/cert/yunzer.com.cn/fullchain.pem;
|
||||
ssl_certificate_key /www/server/panel/vhost/cert/yunzer.com.cn/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_session_tickets on;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 10m;
|
||||
add_header Strict-Transport-Security "max-age=31536000";
|
||||
error_page 497 https://$host$request_uri;
|
||||
#SSL-END
|
||||
|
||||
# --- 安全拦截:敏感文件 ---
|
||||
location ~* (\.user.ini|\.htaccess|\.htpasswd|\.env.*|\.project|\.bashrc|\.bash_profile|\.bash_logout|\.DS_Store|\.gitignore|\.gitattributes|LICENSE|README\.md|CLAUDE\.md|CHANGELOG\.md|CHANGELOG|CONTRIBUTING\.md|TODO\.md|FAQ\.md|composer\.json|composer\.lock|package(-lock)?\.json|yarn\.lock|pnpm-lock\.yaml|\.\w+~|\.swp|\.swo|\.bak(up)?|\.old|\.tmp|\.temp|\.log|\.sql(\.gz)?|docker-compose\.yml|docker\.env|Dockerfile|\.csproj|\.sln|Cargo\.toml|Cargo\.lock|go\.mod|go\.sum|phpunit\.xml|phpunit\.xml|pom\.xml|build\.gradl|pyproject\.toml|requirements\.txt|application(-\w+)?\.(ya?ml|properties))$ {
|
||||
return 404;
|
||||
}
|
||||
|
||||
# --- 安全拦截:敏感目录 ---
|
||||
location ~* /(\.git|\.svn|\.bzr|\.vscode|\.claude|\.idea|\.ssh|\.github|\.npm|\.yarn|\.pnpm|\.cache|\.husky|\.turbo|\.next|\.nuxt|node_modules|runtime)/ {
|
||||
return 404;
|
||||
}
|
||||
|
||||
# SSL 证书验证目录
|
||||
location ~ \.well-known {
|
||||
allow all;
|
||||
}
|
||||
|
||||
if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) {
|
||||
return 403;
|
||||
}
|
||||
|
||||
# /api/ 转发并剥掉 /api 前缀(尾部斜杠不能删)
|
||||
location /api/ {
|
||||
proxy_pass http://go_backend_yunzer/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# 其余一切请求全部交给 Go:首页、/news、/page/xxx、/themes/ 静态资源、
|
||||
# /uploads/ 以及以后新增的任何路由,自动生效,nginx 永远不用再改
|
||||
location / {
|
||||
proxy_pass http://go_backend_yunzer;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
access_log /www/wwwlogs/yunzer.com.cn.log;
|
||||
error_log /www/wwwlogs/yunzer.com.cn.error.log;
|
||||
}
|
||||
|
||||
# ==================== 租户官网渲染:*.yunzer.com.cn 所有子域名 ====================
|
||||
server {
|
||||
listen 80;
|
||||
listen 443 ssl;
|
||||
http2 on;
|
||||
|
||||
# 匹配所有子域名,如 ceshi.yunzer.com.cn(租户域名发放后无需再改 nginx)
|
||||
server_name ~^(?<subdomain>.+)\.yunzer\.com\.cn$;
|
||||
|
||||
#CERT-APPLY-CHECK--START
|
||||
include /www/server/panel/vhost/nginx/well-known/yunzer.com.cn.conf;
|
||||
#CERT-APPLY-CHECK--END
|
||||
|
||||
#HTTP_TO_HTTPS_START
|
||||
set $isRedcert 1;
|
||||
if ($server_port != 443) {
|
||||
set $isRedcert 2;
|
||||
}
|
||||
if ( $uri ~ /\.well-known/ ) {
|
||||
set $isRedcert 1;
|
||||
}
|
||||
if ($isRedcert != 1) {
|
||||
rewrite ^(/.*)$ https://$host$1 permanent;
|
||||
}
|
||||
#HTTP_TO_HTTPS_END
|
||||
|
||||
# 注意:子域名 HTTPS 要求泛域名证书(*.yunzer.com.cn),见文档"注意事项"
|
||||
ssl_certificate /www/server/panel/vhost/cert/yunzer.com.cn/fullchain.pem;
|
||||
ssl_certificate_key /www/server/panel/vhost/cert/yunzer.com.cn/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 10m;
|
||||
add_header Strict-Transport-Security "max-age=31536000";
|
||||
error_page 497 https://$host$request_uri;
|
||||
|
||||
location ~ \.well-known {
|
||||
allow all;
|
||||
}
|
||||
|
||||
# 全部请求转发给 Go,由 Go 按 Host(如 ceshi.yunzer.com.cn)识别租户并渲染官网
|
||||
location / {
|
||||
proxy_pass http://go_backend_yunzer;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
access_log /www/wwwlogs/yunzer.com.cn.tenant.log;
|
||||
error_log /www/wwwlogs/yunzer.com.cn.tenant.error.log;
|
||||
}
|
||||
```
|
||||
|
||||
> 若管理端(platform/backend 前端)部署在专属子域名(如 `admin.yunzer.com.cn`),在对应站点再加一个 `server_name admin.yunzer.com.cn;` **精确匹配**的 server 块指向其静态目录即可——精确匹配优先于子域名正则块,互不干扰。
|
||||
|
||||
---
|
||||
|
||||
## 五、重要注意事项
|
||||
|
||||
1. **子域名必须用泛域名证书**。租户域名是二级域名(`ceshi.yunzer.com.cn`),单域名证书会导致 HTTPS 证书错误。需在宝塔为 `*.yunzer.com.cn`、`*.dh2.fun` 申请泛域名证书(DNS 验证),并替换配置中的证书路径。
|
||||
2. **upstream 不能重名**。两个站点配置都在同一 http 上下文,本文分别用 `go_backend_dh2`、`go_backend_yunzer`;重名会 `duplicate upstream` 启动失败。
|
||||
3. **`/api/` 的 `proxy_pass` 尾部斜杠不能删**,它负责剥掉 `/api` 前缀(Go 路由注册在 `/backend/*`、`/platform/*`)。兜底 `location /` 的 `proxy_pass` 则**不带**尾部斜杠(原样转发 URI)。
|
||||
4. **不要再给官网域名加 `js|css` 正则缓存 location**。正则 location 优先级高于兜底前缀 location,会拦截 `/themes/xxx/static/main.js` 去本地 root 找文件导致 404。模板静态资源由 Go(beego)直接提供。
|
||||
5. **`proxy_set_header Host $host;` 必须保留**。Go 完全依赖 Host 识别租户;若透传成 upstream 地址,所有请求都会被识别失败。
|
||||
6. **Go 侧前置条件**:
|
||||
- 启动模式 `APP_MODE` 为 `all`(默认)或 `index`,否则官网渲染路由未注册;
|
||||
- `go/themes/` 目录与 Go 服务启动目录同级(模板文件在 **Go 项目目录** `go/themes/{编码}/`,与 platform/backend 前端项目无关);
|
||||
- 数据库已执行 `sql/yz_cms_frontend_tables.sql`。
|
||||
7. **域名发放数据必须精确**。`yz_system_tenant_domain.full_domain` 要与实际访问域名完全一致(不带协议、不带端口、不带路径),且 `status=1`、`delete_time` 为 NULL。
|
||||
8. **DNS 泛解析**。`*.yunzer.com.cn`、`*.dh2.fun` 需要配置泛解析 A 记录指向服务器,否则新发放的子域名无法访问(与 nginx 无关)。
|
||||
9. **HSTS 提醒**。配置了 `Strict-Transport-Security`,一旦浏览器记住,证书出问题期间该域名将无法用 HTTP 访问;测试阶段可先注释掉该行。
|
||||
10. **宝塔面板操作提示**。直接编辑站点配置文件后,用 `nginx -t` 校验再重载;宝塔后续"站点设置"的某些操作可能重写配置文件,改前建议备份。
|
||||
|
||||
---
|
||||
|
||||
## 六、验证与排查
|
||||
|
||||
### 6.1 部署后验证顺序
|
||||
|
||||
```bash
|
||||
# 1. 绕过 nginx 直接验证 Go(在服务器上执行)
|
||||
curl -H "Host: ceshi.yunzer.com.cn" http://10.31.100.2:8081/
|
||||
|
||||
# 2. 验证 nginx → Go 链路
|
||||
curl -k -H "Host: ceshi.yunzer.com.cn" https://127.0.0.1/
|
||||
|
||||
# 3. 浏览器访问 https://ceshi.yunzer.com.cn
|
||||
```
|
||||
|
||||
### 6.2 常见现象对照表
|
||||
|
||||
| 现象 | 原因 | 处理 |
|
||||
|---|---|---|
|
||||
| Go 页面显示"站点不存在" | Host 在 `yz_system_tenant_domain` / 域名池中查不到 | 核对 `full_domain` 拼写、`status`、`delete_time` |
|
||||
| beego 默认 404 | `APP_MODE` 不含 `all`/`index`,渲染路由未注册 | 检查 Go 启动环境变量 |
|
||||
| nginx 404(未到 Go) | 请求被本地静态/正则规则拦截 | 检查是否残留 `try_files /index.html`、`js|css` 正则 location |
|
||||
| 浏览器证书错误 | 非泛域名证书 | 申请 `*.域名` 泛域名证书并替换路径 |
|
||||
| 502 Bad Gateway | Go 服务未启动或 upstream 地址错误 | `curl http://127.0.0.1:8081/` 验证服务存活,改 upstream |
|
||||
| 首页正常但样式丢失 | `/themes/` 未到达 Go | 确认兜底转发存在且无 js/css 正则拦截 |
|
||||
|
||||
---
|
||||
|
||||
## 七、迁移服务器清单(以迁移到 192.168.1.101 为例)
|
||||
|
||||
1. **新机器部署 Go**:复制 `go/` 目录(含 `themes/`、`conf/app.conf`),修改 `app.conf` 数据库连接为新库地址,启动服务;
|
||||
2. **新库初始化**:导入业务数据,执行 `sql/` 下全部建表/变更 SQL(尤其 `yz_cms_frontend_tables.sql`);
|
||||
3. **nginx**:
|
||||
- nginx 与 Go 同机 → upstream 用 `127.0.0.1:8081`,配置零改动;
|
||||
- 跨机 → 只改两个 upstream 块中的 `server` 一行;
|
||||
4. **证书**:新机器宝塔上为两个域名重新申请/导入证书(含泛域名),路径与配置保持一致;
|
||||
5. **DNS**:将 `dh2.fun`、`yunzer.com.cn` 及泛解析 `*` 的 A 记录改指向新服务器 IP;
|
||||
6. **验证**:按第六节顺序逐一验证;
|
||||
7. 确认无误后到域名服务商处调低旧解析 TTL、观察切换。
|
||||
|
||||
---
|
||||
|
||||
*本文档对应代码:`go/controllers/tenant_site.go`(Host 识别与渲染)、`go/pkg/tagengine/`(标签引擎)、`go/themes/default/`(内置默认模板)、`sql/yz_cms_frontend_tables.sql`(建站数据表)。*
|
||||
@@ -307,6 +307,15 @@ func (c *BackendArticleController) List() {
|
||||
qs = qs.Filter("cate_id", cid)
|
||||
}
|
||||
}
|
||||
// 创建时间区间筛选(前端日期控件传 yyyy-MM-dd)
|
||||
startDate := strings.TrimSpace(c.GetString("startDate"))
|
||||
endDate := strings.TrimSpace(c.GetString("endDate"))
|
||||
if startDate != "" {
|
||||
qs = qs.Filter("create_time__gte", startDate+" 00:00:00")
|
||||
}
|
||||
if endDate != "" {
|
||||
qs = qs.Filter("create_time__lte", endDate+" 23:59:59")
|
||||
}
|
||||
|
||||
total, _ := qs.Count()
|
||||
var rows []models.CmsArticle
|
||||
|
||||
@@ -119,7 +119,32 @@ func (c *TenantSiteController) resolveTemplateCode(tid uint64) string {
|
||||
func (c *TenantSiteController) writeHTML(status int, html string) {
|
||||
c.Ctx.Output.SetStatus(status)
|
||||
c.Ctx.Output.Header("Content-Type", "text/html; charset=utf-8")
|
||||
c.Ctx.WriteString(html)
|
||||
c.Ctx.WriteString(c.forceHTTPSStorageURLs(html))
|
||||
}
|
||||
|
||||
// forceHTTPSStorageURLs 访客以 HTTPS 访问站点时,把页面中存储(CDN)域名的
|
||||
// http:// 资源升级为 https://,避免混合内容(Mixed Content)被浏览器拦截。
|
||||
// 仅替换存储配置里那个域名,不动正文里的其他外部链接。
|
||||
func (c *TenantSiteController) forceHTTPSStorageURLs(html string) string {
|
||||
proto := c.Ctx.Input.Header("X-Forwarded-Proto")
|
||||
if proto == "" && c.Ctx.Input.IsSecure() {
|
||||
proto = "https"
|
||||
}
|
||||
if proto != "https" {
|
||||
return html
|
||||
}
|
||||
cfg, err := models.GetStorageConfig()
|
||||
if err != nil || strings.TrimSpace(cfg.QiniuDomain) == "" {
|
||||
return html
|
||||
}
|
||||
host := strings.TrimSpace(cfg.QiniuDomain)
|
||||
host = strings.TrimPrefix(host, "http://")
|
||||
host = strings.TrimPrefix(host, "https://")
|
||||
host = strings.TrimRight(host, "/")
|
||||
if host == "" {
|
||||
return html
|
||||
}
|
||||
return strings.ReplaceAll(html, "http://"+host, "https://"+host)
|
||||
}
|
||||
|
||||
// render 解析并输出模板文件
|
||||
|
||||
+34
-21
@@ -42,8 +42,10 @@ func RegisterSelfClose(name string, h SelfCloseHandler) { selfCloseHandlers[name
|
||||
func RegisterGlobal(p GlobalProvider) { globalProvider = p }
|
||||
|
||||
var (
|
||||
// 最内层块标签(内部不再嵌套块标签),反复匹配直至收敛
|
||||
reBlock = regexp.MustCompile(`(?s)\{yz:([a-zA-Z]+)((?:\s+[a-zA-Z_][\w-]*\s*=\s*"[^"]*")*)\s*\}((?:(?!\{/?yz:).)*?)\{/yz:\1\}`)
|
||||
// 块标签开标签(带属性);自闭合标签因 "/" 不会被匹配
|
||||
reBlockOpen = regexp.MustCompile(`\{yz:([a-zA-Z]+)((?:\s+[a-zA-Z_][\w-]*\s*=\s*"[^"]*")*)\s*\}`)
|
||||
// 块标签闭标签
|
||||
reBlockClose = regexp.MustCompile(`\{/yz:([a-zA-Z]+)\}`)
|
||||
// 自闭合标签 {yz:name attr="v" /}
|
||||
reSelfClose = regexp.MustCompile(`\{yz:([a-zA-Z]+)((?:\s+[a-zA-Z_][\w-]*\s*=\s*"[^"]*")*)\s*/\}`)
|
||||
// 全局标签 {yz:word}
|
||||
@@ -122,28 +124,40 @@ func expandIncludes(dir, content string, depth int) (string, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// renderBlocks 反复解析最内层块标签直至全部展开
|
||||
// renderBlocks 逐个解析块标签:每次取全文第一个闭标签(必属最内层块),
|
||||
// 向前回溯同名开标签,按行展开后回填,重复至无块标签(Go RE2 不支持负向先行断言,故手工配对)
|
||||
func renderBlocks(content string, ctx *RenderCtx) (string, error) {
|
||||
out := content
|
||||
for i := 0; i < 10; i++ {
|
||||
if !reBlock.MatchString(out) {
|
||||
for i := 0; i < 100; i++ {
|
||||
c := reBlockClose.FindStringIndex(out)
|
||||
if c == nil {
|
||||
break
|
||||
}
|
||||
var ferr error
|
||||
out = reBlock.ReplaceAllStringFunc(out, func(m string) string {
|
||||
sub := reBlock.FindStringSubmatch(m)
|
||||
name := strings.ToLower(sub[1])
|
||||
attrs := parseAttrs(sub[2])
|
||||
body := sub[3]
|
||||
p, ok := blockProviders[name]
|
||||
if !ok {
|
||||
return "" // 未注册的块标签直接置空,避免标签泄漏到前台
|
||||
closeName := strings.ToLower(reBlockClose.FindStringSubmatch(out[c[0]:c[1]])[1])
|
||||
|
||||
// 在闭标签之前找最后一个同名开标签
|
||||
openIdx, openEnd := -1, -1
|
||||
attrs := ""
|
||||
for _, om := range reBlockOpen.FindAllStringSubmatchIndex(out[:c[0]], -1) {
|
||||
if strings.ToLower(out[om[2]:om[3]]) != closeName {
|
||||
continue
|
||||
}
|
||||
rows, err := p(ctx.Tid, attrs, ctx)
|
||||
openIdx, openEnd = om[0], om[1]
|
||||
attrs = out[om[4]:om[5]]
|
||||
}
|
||||
if openIdx < 0 {
|
||||
// 孤立的闭标签:移除后继续,避免死循环
|
||||
out = out[:c[0]] + out[c[1]:]
|
||||
continue
|
||||
}
|
||||
|
||||
rendered := ""
|
||||
if p, ok := blockProviders[closeName]; ok {
|
||||
rows, err := p(ctx.Tid, parseAttrs(attrs), ctx)
|
||||
if err != nil {
|
||||
ferr = err
|
||||
return ""
|
||||
return "", err
|
||||
}
|
||||
body := out[openEnd:c[0]]
|
||||
var sb strings.Builder
|
||||
for _, row := range rows {
|
||||
sb.WriteString(reField.ReplaceAllStringFunc(body, func(fm string) string {
|
||||
@@ -151,11 +165,10 @@ func renderBlocks(content string, ctx *RenderCtx) (string, error) {
|
||||
return row[field]
|
||||
}))
|
||||
}
|
||||
return sb.String()
|
||||
})
|
||||
if ferr != nil {
|
||||
return "", ferr
|
||||
rendered = sb.String()
|
||||
}
|
||||
// 未注册的块标签置空,避免标签泄漏到前台
|
||||
out = out[:openIdx] + rendered + out[c[1]:]
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
Vendored
+1
@@ -41,6 +41,7 @@ declare module 'vue' {
|
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
ElHeader: typeof import('element-plus/es')['ElHeader']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElImage: typeof import('element-plus/es')['ElImage']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||
ElLink: typeof import('element-plus/es')['ElLink']
|
||||
|
||||
Generated
+1
-1
@@ -7099,7 +7099,7 @@
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
|
||||
@@ -565,6 +565,7 @@ h3 {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
background: transparent;
|
||||
|
||||
html:not(.dark) & {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
@@ -609,19 +610,24 @@ h3 {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
// 嵌套展开的子菜单容器保持透明,避免 Element Plus 注入的
|
||||
// --el-menu-bg-color 在二级及以下区域形成不透明背景块;
|
||||
// 二级 ul 不需要上下内边距
|
||||
.el-menu--inline {
|
||||
background: transparent !important;
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
// 子菜单样式
|
||||
.el-sub-menu {
|
||||
.el-sub-menu__title {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&.is-opened .el-sub-menu__title {
|
||||
background: rgba(255, 255, 255, 0.12) !important;
|
||||
margin-left: -3px;
|
||||
}
|
||||
// 展开的父级菜单标题不加背景,背景只留给当前激活的菜单项
|
||||
|
||||
.el-menu-item {
|
||||
padding-left: 48px !important;
|
||||
font-size: 13px;
|
||||
|
||||
&.is-active {
|
||||
@@ -630,6 +636,18 @@ h3 {
|
||||
}
|
||||
}
|
||||
|
||||
// 二级菜单项缩进 24px
|
||||
.el-menu--inline > .el-menu-item,
|
||||
.el-menu--inline > .el-sub-menu > .el-sub-menu__title {
|
||||
padding-left: 24px !important;
|
||||
}
|
||||
|
||||
// 三级菜单项缩进 48px
|
||||
.el-menu--inline .el-menu--inline > .el-menu-item,
|
||||
.el-menu--inline .el-menu--inline > .el-sub-menu > .el-sub-menu__title {
|
||||
padding-left: 48px !important;
|
||||
}
|
||||
|
||||
// 暗色主题适配
|
||||
html.dark & {
|
||||
.el-menu-item.is-active {
|
||||
@@ -648,10 +666,6 @@ h3 {
|
||||
color: var(--el-color-primary-light-3) !important;
|
||||
}
|
||||
|
||||
.el-sub-menu.is-opened .el-sub-menu__title {
|
||||
background: rgba(64, 158, 255, 0.08) !important;
|
||||
}
|
||||
|
||||
.el-sub-menu .el-menu-item.is-active {
|
||||
background: rgba(64, 158, 255, 0.15) !important;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ const isCreated = ref(false)
|
||||
|
||||
const options = {
|
||||
height: '100%',
|
||||
// umo-editor 会定时触发自动保存(onSave),内容已随表单统一提交,这里给空实现避免控制台报错
|
||||
onSave: async () => {},
|
||||
}
|
||||
|
||||
const onFileUpload = async (file) => {
|
||||
|
||||
@@ -16,8 +16,13 @@
|
||||
</div>
|
||||
|
||||
<el-alert type="info" :closable="false" style="margin-bottom: 12px">
|
||||
模板文件存放于服务器 themes/{编码}/ 目录;上传 zip 或扫描目录后自动登记。
|
||||
租户端(backend)只能选用已启用的模板,模板编辑仅平台端可操作。
|
||||
<template #title>
|
||||
<div>模板文件存放在 <b>Go 后端项目目录</b>下的 <code>go/themes/{模板编码}/</code>(相对 go 服务启动目录即 <code>themes/{模板编码}/</code>),与 platform、backend 前端项目无关。</div>
|
||||
<div style="margin-top: 4px; font-weight: normal">
|
||||
手动放入模板目录后点「扫描目录」即可登记;也可直接上传 zip 自动解压到该目录并登记。
|
||||
租户端(backend)只能选用已启用的模板,模板上传/编辑/删除仅平台端可操作。
|
||||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
|
||||
<el-table :data="list" v-loading="loading" border style="width: 100%">
|
||||
|
||||
Reference in New Issue
Block a user