Skip to content

Instantly share code, notes, and snippets.

@ZvonimirSun
Last active February 28, 2026 07:03
Show Gist options
  • Select an option

  • Save ZvonimirSun/b893394aa5e491febacf4939d3111ff7 to your computer and use it in GitHub Desktop.

Select an option

Save ZvonimirSun/b893394aa5e491febacf4939d3111ff7 to your computer and use it in GitHub Desktop.
vue-autoindex-single

单Vue页面Nginx文件列表展示

说明

  1. example.com.conf:
    • nginx配置文件
    • 其中/usr/share/nginx/html/vendor为放置文件的本地路径,也是需要展示文件列表的路径
    • 模拟了一个/api/list/用于查询文件列表json,由于nginx无法解码url参数,所以没法模拟成url参数方式。
  2. index.html: 单Vue文件列表页面,需要放置到/usr/share/nginx/html/vendor下。

功能

  1. 文件及文件夹下有index.html的,会按照nginx默认加载逻辑。
  2. 其他将会返回/index.html内容,显示文件列表或404。

TODO

现在404也会返回/index.html,只能在前端判断是否是404

server
{
listen 80;
listen [::]:80;
server_name example.com ;
root /usr/share/nginx/html/vendor;
index index.html index.htm;
add_header Access-Control-Allow-Origin $http_origin;
location /api/list/ {
alias /usr/share/nginx/html/vendor/;
index off;
autoindex on;
autoindex_format json;
autoindex_localtime on;
# 只允许目录访问
if (!-d $request_filename) {
return 404;
}
}
location / {
try_files $uri $uri/ /index.html =404;
error_page 403 = /index.html;
}
}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>目录浏览 SPA</title>
<!-- Import Map: 模块别名 -->
<script type="importmap">
{
"imports": {
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.prod.js",
"vue-router": "https://unpkg.com/vue-router@5/dist/vue-router.esm-browser.prod.js"
}
}
</script>
<style>
:root {
color-scheme: light dark;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
html,
body,
#app {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
display: block;
}
h2 {
margin: 0;
}
.wrapper {
width: 100%;
height: 100%;
box-sizing: border-box;
overflow: auto;
padding: 1rem;
gap: 1rem;
display: flex;
flex-direction: column;
}
.name-wrapper {
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-size: 16px;
width: 100%;
}
.name-wrapper a {
width: 100%;
}
/* 基础表格 */
table {
width: 100%;
border-collapse: collapse;
/* 每行连续 */
font-family: sans-serif;
}
/* 表头 */
th {
text-align: center;
padding: 10px;
background: #f5f5f5;
border-bottom: 2px solid #ccc;
}
/* 表格单元格 */
td {
padding: 10px;
text-align: left;
border-bottom: 1px solid #ddd;
/* 轻微分隔 */
}
td:nth-child(2),
td:nth-child(3) {
text-align: center;
}
/* Hover 整行高亮 */
tbody tr:hover {
background-color: var(--hover-bg);
cursor: pointer;
transition: background 0.2s;
}
/* 深浅色模式变量 */
:root {
--bg-header: #f5f5f5;
--border-color: #ccc;
--hover-bg: #e0f0ff;
}
th {
background: var(--bg-header);
border-bottom-color: var(--border-color);
}
td {
border-bottom-color: var(--border-color);
}
/* 深色模式 */
@media (prefers-color-scheme: dark) {
:root {
--bg-header: #2a2a2a;
--border-color: #444;
--hover-bg: #3a3a3a;
}
tbody tr:hover {
background-color: var(--hover-bg);
}
}
</style>
</head>
<body>
<div id="app">
<router-view></router-view> <!-- Vue Router 占位符 -->
</div>
<script type="text/html" id="fileTemplate">
<template v-if="status.inited">
<div v-if="!status.is404" class="wrapper">
<h2>目录: <a href="../" @click="exit">{{ parentPath }}</a>{{ currentPathName }}/</h2>
<div v-if="!status.files.length">目录为空或不存在</div>
<table v-else>
<thead>
<tr>
<th>名称</th>
<th>修改时间</th>
<th>大小</th>
</tr>
</thead>
<tbody>
<tr v-for="item in status.files" :key="item.name">
<td>
<div class="name-wrapper">
<span v-if="item.type === 'directory'">📁</span>
<span v-else>📄</span>
<a :href="item.link" :title="item.name" @click="enter($event, item)">{{ item.name }}</a>
</div>
</td>
<td>{{ item.mtime?.toLocaleString() }}</td>
<td>{{ item.size > 0 ? `${(item.size / 1024).toFixed(2)} KB` : '-' }}</td>
</tr>
</tbody>
</table>
</div>
<span v-else>404 Not Found</span>
</div>
</script>
<script type="module">
import { createApp, onMounted, watch, reactive, computed } from "vue"
import { createRouter, createWebHistory, useRoute, useRouter } from "vue-router"
const template = document.getElementById('fileTemplate')
// 文件列表组件
const FileList = {
template: template.innerHTML,
setup() {
const route = useRoute()
const router = useRouter()
const status = reactive({
files: [],
is404: false,
inited: false
})
const parentPath = computed(() => {
if (route.path === '/') {
return ''
} else {
const tmp = route.path.split('/')
tmp.splice(tmp.length - 2, 1)
return tmp.join('/')
}
})
const currentPathName = computed(() => {
if (route.path === '/') {
return ''
} else {
const tmp = route.path.split('/')
return tmp[tmp.length - 2]
}
})
const fetchData = async () => {
try {
const res = await fetch(`/api/list${route.path}`)
if (res.status === 404) {
status.files = []
status.is404 = true
} else {
const data = await res.json()
let fileList = (data || []).map(({ name, mtime, size, type }) => {
return {
name,
mtime: mtime ? new Date(mtime) : null,
size,
type,
link: type === "directory" ? `./${name}/` : `./${name}`,
}
})
if (route.path === '/') {
fileList = fileList.filter(file => file.name !== 'index.html')
} else {
if (route.path !== '/') {
fileList.unshift({
name: '..',
mtime: null,
size: 0,
type: 'directory',
link: '../',
})
}
}
status.files = fileList
}
} catch (e) {
status.files = []
status.is404 = true
} finally {
status.inited = true
}
}
onMounted(fetchData)
// 监听 URL query.path 变化
watch(() => route.path, () => {
fetchData()
})
const enter = (e, item) => {
if (item.type === "directory") {
router.push(item.link)
e.preventDefault()
return false
}
}
const exit = (e) => {
e.preventDefault()
if (route.path !== '/') {
router.push('../')
}
}
return { status, route, enter, exit, parentPath, currentPathName }
}
}
// 路由配置
const routes = [
{ path: '/:catchAll(.*)', component: FileList }
]
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
return savedPosition || { top: 0 }
}
})
// 创建 Vue 应用
createApp({}).use(router).mount('#app')
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment