feat(app): 添加多模板关于我们页面及相关路由和404页面

- 新增404页面,优化未找到页面体验,避免被搜索引擎索引
- 增加文件代理接口,隐藏真实文件服务器地址,支持文件请求代理
- 实现/article、/case、/product及/page动态路由兼容列表与详情展示
- 添加动态CMS页面兼容入口处理旧式路径,统一路由与SEO设置
- 新增模板1、模板7、模板2、模板3关于我们页面,实现多模板支持
- 模板增强支持CMS单页内容加载及SEO信息动态设置
- 配置环境变量及Git忽略文件规则辅助开发和构建环境管理
This commit is contained in:
2026-09-08 12:13:44 +08:00
commit 2b69686795
381 changed files with 59891 additions and 0 deletions
+122
View File
@@ -0,0 +1,122 @@
<template>
<div v-if="list.length" class="mt-10 border-t border-gray-100 pt-8">
<h2 class="text-xl font-bold text-gray-900 mb-5">{{ title }}</h2>
<ul class="space-y-3">
<li
v-for="(file, idx) in sortedList"
:key="(file.url || '') + idx"
class="flex items-center gap-3 rounded-lg border border-gray-200 px-4 py-3 transition hover:border-blue-400 hover:shadow-sm"
>
<!-- 文件图标按扩展名换色统一用内联 SVG避免引入图标库依赖 -->
<span class="shrink-0" :style="{ color: iconColor(file.ext) }">
<svg
width="22"
height="22"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<path d="M14 2v6h6" />
</svg>
</span>
<!-- 文件名可点击下载 -->
<a
:href="resolveUrl(file.url)"
:download="file.name"
target="_blank"
rel="noopener"
class="min-w-0 flex-1 truncate text-blue-600 hover:underline"
:title="file.name"
>
{{ file.name }}
</a>
<!-- 扩展名徽标 -->
<span
v-if="file.ext"
class="shrink-0 rounded bg-gray-100 px-2 py-0.5 text-xs font-medium uppercase text-gray-500"
>{{ file.ext }}</span>
<!-- 文件大小 -->
<span v-if="file.size" class="shrink-0 text-xs text-gray-400">{{ formatSize(file.size) }}</span>
<!-- 下载入口 -->
<a
:href="resolveUrl(file.url)"
:download="file.name"
target="_blank"
rel="noopener"
class="shrink-0 text-sm text-blue-600 hover:underline"
>下载</a>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { PageAttachment } from '~/types'
import { ensureFullUrl } from '~/utils/image'
const props = withDefaults(
defineProps<{
/** 附件列表(来自 pageData.attachments */
attachments?: PageAttachment[]
/** 区块标题,默认「相关附件」 */
title?: string
}>(),
{
attachments: () => [],
title: '相关附件'
}
)
const list = computed<PageAttachment[]>(() => props.attachments || [])
/** 按 sort 升序,sort 缺失的排后面 */
const sortedList = computed<PageAttachment[]>(() =>
[...list.value].sort((a, b) => (a.sort ?? 9999) - (b.sort ?? 9999))
)
function resolveUrl(url?: string) {
return url ? ensureFullUrl(url) : ''
}
/** 字节转可读大小 */
function formatSize(bytes?: number) {
if (!bytes || bytes <= 0) return ''
const units = ['B', 'KB', 'MB', 'GB']
let val = bytes
let i = 0
while (val >= 1024 && i < units.length - 1) {
val /= 1024
i++
}
return `${val.toFixed(i === 0 ? 0 : 1)} ${units[i]}`
}
/** 不同扩展名用不同图标色,便于一眼区分文件类型 */
function iconColor(ext?: string) {
const map: Record<string, string> = {
pdf: '#e5484d',
doc: '#2b6cee',
docx: '#2b6cee',
xls: '#1f9d55',
xlsx: '#1f9d55',
csv: '#1f9d55',
ppt: '#e8833a',
pptx: '#e8833a',
zip: '#8a6df0',
rar: '#8a6df0',
'7z': '#8a6df0',
txt: '#6b7280'
}
return (ext && map[ext.toLowerCase()]) || '#6b7280'
}
</script>