feat(建页方案): 本地开发目录只作为开发期 OSS 图片镜像使用,方便页面优先读取本地图片

This commit is contained in:
hzj 2026-06-01 16:29:48 +08:00
parent daf57343de
commit fb967f709e
3 changed files with 62 additions and 22 deletions

View File

@ -27,6 +27,8 @@
7. 安全白名单片段和权限页面片段也使用 route path 自动生成,不留空。
8. OSS 相对路径和本地开发目录根据页面类型和页面目录自动生成;用户后续可以自行修改。
9. 如果用户要求,或 md 中“是否需要 Codex 自动创建本地开发目录”为“是”,创建 public/oss/h5/Azizi/... 本地图片目录。
- 本地开发目录只作为开发期 OSS 图片镜像使用,方便页面优先读取本地图片;发布时图片由用户另行上传到真实 OSS不随项目提交 Git。
- 创建本地开发目录时只创建文件夹,不生成 `.gitkeep`;空目录不被 Git 追踪是正常现象。
10. 如果本地开发目录已存在且里面有图片,扫描图片文件并预填 3.1 多语言图片清单。
11. 根据页面类型、3.1 图片清单和模块预设,自动预填 3.2、3.3、4.x 模块配置。
12. 可按 route path 生成路由、白名单、权限片段;是否立即写入 `src/router/index.js``src/config/security.js``src/utils/permissionManager.js` 取决于用户要求和当前是否需要保持构建通过。
@ -75,7 +77,7 @@ src/views/Ranking/OverallSeason2
```text
scanDir = 专属 md 中的本地开发目录
只扫描常见图片扩展名png、jpg、jpeg、webp、gif、svg。
忽略 .gitkeep 等非图片文件。
忽略 .gitkeep 等非图片文件;历史遗留的 `.gitkeep` 按非图片文件处理,新建本地开发目录时不要生成 `.gitkeep`
文件名不带语言后缀时,记为 en 原图,例如 bg0.png -> bg0: en。
文件名带 _ar、_bn、_tr 等语言后缀时,按基础名合并,例如 rankBt.png + rankBt_ar.png -> rankBt: en, ar。
如果只有 rankBt_ar.png 且没有 rankBt.png也生成 rankBt: ar并在备注里标记缺少原图。
@ -104,6 +106,7 @@ rankingTopBg 统一解释为“排行榜前几名展示区背景”;历史 top
6. 只处理专属 md 中已填写的信息,未填写项跳过。
7. 以专属页面信息 md 为唯一生成源,同步更新 page.config.js把已填写的活动字段、预加载、tabs、sections、children、popups、features 写入配置。
8. 如果专属 md 中“是否需要 Codex 自动创建本地开发目录”为“是”,创建 public/oss/h5/Azizi/... 本地图片目录。
- 这里仍然保留自动创建文件夹的功能,用于开发期存放图片;不要为该目录生成 `.gitkeep`
9. 添加或确认路由。
10. 补充或确认 src/config/security.js。
11. 补充或确认 src/utils/permissionManager.js。

View File

@ -30,6 +30,8 @@
2. 在页面目录中创建 页面创建信息.md。
3. 根据 页面创建信息.md 创建 page.config.js 初始配置。
4. 按需要创建本地 OSS 图片目录。
- 本地 OSS 图片目录仅用于开发期存放图片,方便页面读取本地资源;发布时图片由你上传到真实 OSS不随项目提交 Git。
- Codex 仍会按需要创建这个文件夹,但不会生成 `.gitkeep`;空文件夹不被 Git 追踪是正常现象。
5. 预填 route、OSS、图片清单、预加载和模块配置。
6. 不生成 index.vue除非你明确要求继续创建页面。
```
@ -128,6 +130,8 @@ src/views/Activities/GulbenFestival/页面创建信息.md
- 是否需要 Codex 自动创建本地开发目录:是 / 否
- 图片是否已放入本地目录:是 / 否
说明:本地开发目录只作为开发期 OSS 图片镜像使用Codex 需要保留创建文件夹的能力,便于你放入开发用图片;目录内图片和空目录占位文件不作为 Git 提交内容,新建时不要生成 `.gitkeep`
### 3.1 多语言图片清单
| 图片名 | 支持语言 | 备注 |

View File

@ -2,15 +2,21 @@ import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite'
import { defineConfig } from 'vite'
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath, URL } from 'node:url'
import { existsSync } from 'node:fs'
import { isAbsolute, relative, resolve } from 'node:path'
import { VantResolver } from 'unplugin-vue-components/resolvers'
const PRODUCTION_LIKE_MODES = new Set(['production', 'production-atu'])
const STRIPPABLE_CONSOLE_METHODS = ['log', 'debug', 'info']
const PUBLIC_DIR = fileURLToPath(new URL('./public', import.meta.url))
const PUBLIC_ROOT = resolve(PUBLIC_DIR)
const LOCAL_OSS_MIME_TYPES = {
'.gif': 'image/gif',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.svg': 'image/svg+xml',
'.webp': 'image/webp',
}
function isEscaped(code, index) {
let backslashCount = 0
@ -259,12 +265,56 @@ function createBuildLogControlPlugin(stripDebugLogs) {
}
}
function createLocalOssAssetPlugin() {
const ossRoot = fileURLToPath(new URL('./public/oss', import.meta.url))
return {
name: 'likei-local-oss-assets',
apply: 'serve',
configureServer(server) {
server.middlewares.use('/oss', (req, res, next) => {
const rawPath = req.url?.split('?')[0] || '/'
let decodedPath = ''
try {
decodedPath = decodeURIComponent(rawPath)
} catch {
next()
return
}
const normalizedPath = path.normalize(decodedPath).replace(/^(\.\.[/\\])+/, '')
const filePath = path.join(ossRoot, normalizedPath)
const safeRoot = `${ossRoot}${path.sep}`
if (!filePath.startsWith(safeRoot)) {
next()
return
}
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
next()
return
}
const contentType = LOCAL_OSS_MIME_TYPES[path.extname(filePath).toLowerCase()]
if (contentType) {
res.setHeader('Content-Type', contentType)
}
fs.createReadStream(filePath).pipe(res)
})
},
}
}
export default defineConfig(({ mode }) => {
const keepBuildLogs = process.env.LIKEI_KEEP_BUILD_LOGS === 'true'
const stripDebugLogs = PRODUCTION_LIKE_MODES.has(mode) && !keepBuildLogs
return {
plugins: [
createLocalOssAssetPlugin(),
vue(),
Components({
resolvers: [VantResolver()],
@ -318,23 +368,6 @@ export default defineConfig(({ mode }) => {
target: 'https://cdn.azizichat.com',
changeOrigin: true,
secure: true,
bypass: (req) => {
const requestUrl = req.url || ''
try {
const pathname = decodeURIComponent(new URL(requestUrl, 'http://localhost').pathname)
const localPublicFile = resolve(PUBLIC_ROOT, `.${pathname}`)
const localPathInPublic = relative(PUBLIC_ROOT, localPublicFile)
if (!localPathInPublic.startsWith('..') && !isAbsolute(localPathInPublic) && existsSync(localPublicFile)) {
return pathname
}
} catch (error) {
console.warn('Failed to check local OSS fallback:', error)
}
return null
},
rewrite: (path) => path.replace(/^\/oss/, ''),
configure: (proxy) => {
proxy.on('proxyRes', (proxyRes) => {