一、项目架构概览

二、后端实现详解
2.1 入口文件 - main.py
# 1. 导入依赖和路由
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from app.routers.file_router import router as file_router
from app.routers.zxbj_router import router as zxbj_router
from app.routers.zxyl_router import router as zxyl_router
from app.routers.zshd_router import router as zshd_router
from app.config import PORT, BASE_DIR
import os
# 2. 创建 FastAPI 实例
app = FastAPI(title="WDZT Document Management System", version="1.0.0")
# 3. 配置 CORS(跨域资源共享)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 4. 注册路由
app.include_router(file_router, prefix="/api")
app.include_router(zxbj_router, prefix="/api")
app.include_router(zxyl_router, prefix="/api")
app.include_router(zshd_router, prefix="/api")
# 5. 挂载静态文件(前端页面)
static_dir = os.path.join(BASE_DIR, "static")
if os.path.exists(static_dir):
#FastAPI的挂载方法,将指定路径与静态文件目录关联;这段代码的核心目的是:让FastAPI能够提供静态文件服务,使得前端可以通过/static路径访问项目中的静态资源(如CSS、JS、图片等)。
app.mount("/static", StaticFiles(directory=static_dir), name="static")
@app.get("/")
async def root():
index_path = os.path.join(static_dir, "index.html")
if os.path.exists(index_path):
return FileResponse(index_path)
return {"message": "WDZT Document Management System API"}
#用户访问:/view/123 → 触发 FastAPI 路由。
#后端响应:返回 static/index.html(静态入口文件)。
#前端加载:浏览器加载 index.html,执行前端框架代码。
#路由解析:前端从 URL 中提取 fileId=123,调用 API 获取对应文档数据。
#动态渲染:前端根据数据渲染页面(如显示文档标题、内容等)。
@app.get("/view/{fileId}")
async def view_page(fileId: str):
index_path = os.path.join(static_dir, "index.html")
if os.path.exists(index_path):
return FileResponse(index_path)
return {"message": "Page not found"}
@app.get("/edit/{fileId}")
async def edit_page(fileId: str):
index_path = os.path.join(static_dir, "index.html")
if os.path.exists(index_path):
return FileResponse(index_path)
return {"message": "Page not found"}
@app.get("/assets/{rest_of_path:path}")
async def assets(rest_of_path: str):
asset_path = os.path.join(static_dir, "assets", rest_of_path)
if os.path.exists(asset_path):
return FileResponse(asset_path)
return FileResponse(os.path.join(static_dir, "index.html"))
@app.get("/jssdk/{rest_of_path:path}")
async def jssdk(rest_of_path: str):
jssdk_path = os.path.join(static_dir, "jssdk", rest_of_path)
if os.path.exists(jssdk_path):
return FileResponse(jssdk_path)
return FileResponse(os.path.join(static_dir, "index.html"))
@app.get("/src/{rest_of_path:path}")
async def src_files(rest_of_path: str):
src_path = os.path.join(static_dir, "src", rest_of_path)
if os.path.exists(src_path):
return FileResponse(src_path)
return FileResponse(os.path.join(static_dir, "index.html"))
# 6. 启动服务
if __name__ == "__main__":
import uvicorn
import webbrowser
import threading
import time
def open_browser():
time.sleep(2)
webbrowser.open(f"http://10.229.12.41:{PORT}")
threading.Thread(target=open_browser, daemon=True).start()
uvicorn.run(app, host="0.0.0.0", port=PORT)
2.2 配置文件 - config.py
import os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
FILE_STORE_PATH = os.path.join(os.path.dirname(os.path.dirname(BASE_DIR)), "wdzt_demo", "filestore")
WDZT_CONFIG = {
"ak": "AK20260630VIGWZM",
"sk": "SKphhpzupwammqjj",
"host": "http://10.217.19.253/open",
"server_api": "http://10.229.12.41:8000"
}
PORT = 8000


页面处理流程:
用户访问 http://localhost:8000/view/123
↓
┌─────────────────────────────────────────────────────┐
│ 第1步:浏览器请求到达后端 (FastAPI) │
└─────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ 第2步:后端路由匹配 /view/{fileId} │
│ 返回 index.html │
└─────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ 第3步:浏览器解析 HTML,加载资源 │
│ - index.html │
│ - jssdk/open-jssdk-v0.0.3.umd.js │
│ - src/main.js → 触发 Vue 应用初始化 │
└─────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ 第4步:前端路由匹配 (Vue Router) │
│ /view/:fileId → FileView.vue 组件 │
└─────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ 第5步:前端调用后端 API │
│ GET /api/zxyl/getLink/123 │
└─────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ 第6步:后端调用文档中台 API (HMAC签名认证) │
│ 返回预览链接 │
└─────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ 第7步:前端使用 OpenSDK 挂载文档预览 │
│ document.querySelector(‘#office’) │
└─────────────────────────────────────────────────────┘
↓
用户看到文档预览页面 ✅
2.3 数据模型 - models.py
from pydantic import BaseModel
from typing import Optional, List, Dict
class UserAccessControl(BaseModel):
read: Optional[bool] = True
write: Optional[bool] = True
class Watermark(BaseModel):
text: Optional[str] = None
font: Optional[str] = None
color: Optional[str] = None
opacity: Optional[float] = None
class FileMeta(BaseModel):
id: str
name: str
version: int
size: int
creator: Optional[str] = None
create_time: int
modifier: Optional[str] = None
modify_time: int
download_url: Optional[str] = None
preview_pages: Optional[int] = 0
readonly: Optional[bool] = False
user_acl: Optional[UserAccessControl] = None
watermark: Optional[Watermark] = None
class User(BaseModel):
id: str
name: str
permission: Optional[str] = "write"
avatar_url: Optional[str] = None
class HistoryFile(BaseModel):
id: str
name: str
version: int
size: int
creator: Optional[User] = None
create_time: int
modifier: Optional[User] = None
modify_time: int
download_url: Optional[str] = None
class ZSHD(BaseModel):
pass
class Result(BaseModel):
success: bool
code: int
msg: str
data: Optional[dict] = None
@classmethod
def success(cls, data=None, msg="success"):
return cls(success=True, code=0, msg=msg, data=data)
@classmethod
def error(cls, code=-1, msg="error"):
return cls(success=False, code=code, msg=msg)
关键点 :
- Pydantic :数据校验和序列化工具
- BaseModel :定义数据结构,自动生成 JSON Schema
- Optional :可选字段,默认值为 None
2.4 文件存储 - filestore.py
import os
from typing import Dict, List, Optional
from app.models.models import FileMeta
from app.config import FILE_STORE_PATH
file_map: Dict[str, FileMeta] = {}
def _init_file_map():
if not os.path.exists(FILE_STORE_PATH):
os.makedirs(FILE_STORE_PATH)
for filename in os.listdir(FILE_STORE_PATH):
if filename.endswith(".store"):
filepath = os.path.join(FILE_STORE_PATH, filename)
with open(filepath, "r", encoding="utf-8") as f:
content = f.read().strip()
if content:
parts = content.split(";")
if len(parts) >= 7:
file_meta = FileMeta(
id=parts[0],
name=parts[1],
size=int(parts[2]),
create_time=int(parts[3]),
modify_time=int(parts[4]),
download_url=parts[5],
version=int(parts[6])
)
key = f"{file_meta.id}v{file_meta.version}"
file_map[key] = file_meta
_init_file_map()
def contains_key(key: str) -> bool:
return key in file_map
def get_file_meta(key: str) -> Optional[FileMeta]:
return file_map.get(key)
def get_last_version_file(file_id: str) -> Optional[FileMeta]:
max_version = 0
result = None
for key, meta in file_map.items():
if meta.id == file_id and meta.version >= max_version:
max_version = meta.version
result = meta
return result
def get_version_file(file_id: str, version: str) -> Optional[FileMeta]:
for key, meta in file_map.items():
if meta.id == file_id and str(meta.version) == version:
return meta
return None
def get_file_meta_list() -> List[FileMeta]:
if not file_map:
return []
return sorted(file_map.values(), key=lambda x: f"{x.id}{x.version}", reverse=True)
def get_file_meta_list_by_id(file_id: str) -> List[FileMeta]:
if not file_map:
return []
filtered = [meta for meta in file_map.values() if meta.id == file_id]
return sorted(filtered, key=lambda x: x.version, reverse=True)
def write_store(file_meta: FileMeta) -> None:
store_filename = f"{file_meta.id}v{file_meta.version}.store"
store_path = os.path.join(FILE_STORE_PATH, store_filename)
content = ";".join([
file_meta.id,
file_meta.name,
str(file_meta.size),
str(file_meta.create_time),
str(file_meta.modify_time),
file_meta.download_url,
str(file_meta.version)
])
with open(store_path, "w", encoding="utf-8") as f:
f.write(content)
key = f"{file_meta.id}v{file_meta.version}"
file_map[key] = file_meta
设计思路 :
- 使用 .store 文本文件存储元数据,格式为id;name;size;create_time;modify_time;download_url;version
- 内存 file_map 提供快速查询
- 文件持久化保证重启后数据不丢失
2.5 API 路由 - file_router.py
import os
from fastapi import APIRouter, UploadFile, File, HTTPException, Path
from fastapi.responses import FileResponse
from typing import Dict
from app.models.models import FileMeta, Result
from app.utils.filestore import write_store, get_file_meta, get_file_meta_list, contains_key
from app.config import FILE_STORE_PATH, WDZT_CONFIG
router = APIRouter(prefix="/file", tags=["文件管理"])
@router.post("/upload")
async def upload(file: UploadFile = File(...)) -> Dict:
try:
filename = file.filename
if not filename:
return {"success": False, "code": -1, "msg": "文件名不能为空"}
suffix = filename.split(".")[-1] if "." in filename else ""
mills = str(int(datetime.now().timestamp() * 1000))
store_name = f"{mills}v1"
file_meta = FileMeta(
id=mills,
name=filename,
size=0,
create_time=int(mills) // 1000,
modify_time=int(mills) // 1000,
version=1,
download_url=f"{WDZT_CONFIG['server_api']}/file/download/{store_name}.{suffix}"
)
file_store_name = f"{store_name}.{suffix}"
file_path = os.path.join(FILE_STORE_PATH, file_store_name)
contents = await file.read()
file_meta.size = len(contents)
with open(file_path, "wb") as f:
f.write(contents)
write_store(file_meta)
return {"success": True, "code": 0, "msg": "上传成功", "data": {"file_id": mills}}
except Exception as e:
return {"success": False, "code": -1, "msg": f"上传失败: {str(e)}"}
@router.get("/download/{key}")
async def download(key: str = Path(...)) -> FileResponse:
if not key:
raise HTTPException(status_code=404, detail="文件不存在")
if ".." in key or "\\" in key:
raise HTTPException(status_code=400, detail="无效的文件路径")
file_id = key.split(".")[0] if "." in key else key
if not contains_key(file_id):
raise HTTPException(status_code=404, detail="文件不存在")
file_meta = get_file_meta(file_id)
if not file_meta:
raise HTTPException(status_code=404, detail="文件不存在")
file_path = os.path.join(FILE_STORE_PATH, key)
if not os.path.exists(file_path):
raise HTTPException(status_code=404, detail="文件不存在")
return FileResponse(
file_path,
filename=file_meta.name,
media_type="application/octet-stream",
headers={"Content-Disposition": f"attachment; filename={file_meta.name}"}
)
@router.get("/list")
async def list_files() -> Dict:
file_list = get_file_meta_list()
return {
"list": [meta.dict() for meta in file_list],
"code": 0,
"size": len(file_list)
}
from datetime import datetime
关键点 :
- APIRouter :模块化路由管理
- UploadFile :FastAPI 文件上传对象
- FileResponse :返回文件流供下载
- Path(…) :路径参数校验
2.6 在线预览/编辑路由
import requests
import json
from fastapi import APIRouter, Header, Path, Body, UploadFile, File
from typing import Dict, Optional, List
from app.models.models import Result, FileMeta
from app.utils.filestore import get_last_version_file, get_file_meta, get_file_meta_list_by_id, write_store, get_version_file
from app.utils.utils import hmac_sha256, get_gmt_date, get_type_by_filename
from app.config import WDZT_CONFIG, FILE_STORE_PATH
import os
from datetime import datetime
router = APIRouter(prefix="/zxyl", tags=["在线预览"])
@router.get("/getLink/{fileId}")
async def get_link(fileId: str = Path(...)) -> Dict:
file_meta = get_last_version_file(fileId)
if not file_meta:
return {"success": False, "code": -1, "msg": "fileid is not exist!"}
file_type = get_type_by_filename(file_meta.name)
result_str = get_zxyl_link(fileId, file_type)
if not result_str:
return {"success": False, "code": -1, "msg": "调用文档中台接口失败,返回结果为空!"}
import json
return {"success": True, "code": 0, "msg": "success", "data": json.loads(result_str)}
def get_zxyl_link(file_id: str, file_type: str) -> str:
preview_mode = "high_definition"
url = f"/api/preview/v1/files/{file_id}/link?type={file_type}&preview_mode={preview_mode}&_w_third_filename=111.doc"
headers = {}
content_type = "application/json"
date = get_gmt_date()
try:
signature = hmac_sha256(f"WPS-4GET{url}{content_type}{date}", WDZT_CONFIG["sk"])
except Exception as e:
print(f"HMAC签名失败: {e}")
return ""
headers["Content-Type"] = content_type
headers["Wps-Docs-Date"] = date
headers["Wps-Docs-Authorization"] = f"WPS-4 {WDZT_CONFIG['ak']}:{signature}"
try:
response = requests.get(f"{WDZT_CONFIG['host']}{url}", headers=headers)
return response.text
except Exception as e:
print(f"请求失败: {e}")
return ""
@router.post("/preload/{fileId}/{versionId}")
async def preload(
fileId: str = Path(...),
versionId: str = Path(...),
x_wps_weboffice_token: Optional[str] = Header(None)
) -> Dict:
app_id = "YUZLUTZMHBZNLYZU"
secret_key = "SKeawmznqnqbyghg"
webhook = "http://10.229.12.14:10021/zshd/callback"
url = f"/api/preview/v2/files/{fileId}/versions/{versionId}/preload"
request_body = {"webhook": webhook}
content = json.dumps(request_body)
date = get_gmt_date()
signature = hmac_sha256(f"WPS-4POST{url}application/json{date}", secret_key)
headers = {
"Content-Type": "application/json",
"Wps-Docs-Date": date,
"Wps-Docs-Authorization": f"WPS-4 {app_id}:{signature}"
}
try:
response = requests.post(f"http://10.226.48.49/open{url}", data=content, headers=headers)
response_data = response.json()
if "data" in response_data and "build_status" in response_data["data"]:
return {"success": True, "code": 0, "msg": "success", "data": response_data["data"]["build_status"]}
else:
return {"success": True, "code": 0, "msg": "success", "data": None}
except Exception as e:
print(f"预处理接口调用失败: {e}")
return {"success": False, "code": -1, "msg": f"预处理失败: {str(e)}"}
@router.get("/v1/3rd/")
async def v13rd_file_info(
x_weboffice_file_id: str = Header(...),
x_wps_weboffice_token: Optional[str] = Header(None)
) -> Dict:
print(f"在线预览回调:{x_weboffice_file_id}")
result_map = {}
file_meta = get_last_version_file(x_weboffice_file_id)
if file_meta:
file_meta.id = x_weboffice_file_id
file_meta.creator = "id1"
file_meta.modifier = "id1"
file_meta.preview_pages = 0
result_map["file"] = file_meta.dict()
user = {
"id": "1",
"name": "id1",
"permission": "read"
}
result_map["user"] = user
return result_map
@router.get("/v1/3rd/edit/info")
async def v13rd_edit_info(
x_weboffice_file_id: str = Header(...),
x_wps_weboffice_token: Optional[str] = Header(None)
) -> Dict:
print(f"应用文档编辑回调:{x_weboffice_file_id}")
result_map = {}
file_meta = get_last_version_file(x_weboffice_file_id)
if file_meta:
file_meta.id = x_weboffice_file_id
file_meta.preview_pages = 0
result_map["file"] = file_meta.dict()
user = {
"id": "1",
"name": "id1",
"permission": "read"
}
result_map["user"] = user
return result_map
@router.post("/v1/3rd/user/info")
async def v13rd_user_info(
x_weboffice_file_id: str = Header(...),
x_wps_weboffice_token: Optional[str] = Header(None),
params: Dict[str, List[str]] = Body(...)
) -> Dict:
print(f"获取用户信息:{params}")
result_map = {}
ids = params.get("ids", [])
users = []
for uid in ids:
user = {
"id": uid,
"name": "id1",
"avatar_url": ""
}
users.append(user)
result_map["users"] = users
return result_map
@router.get("/v1/3rd/user/auth")
async def v13rd_user_auth(
x_weboffice_file_id: str = Header(...),
x_wps_weboffice_token: Optional[str] = Header(None)
) -> Dict:
result_map = {}
result_map["id"] = "1"
return result_map
@router.post("/v1/3rd/file/save")
async def v13rd_file_save(
x_weboffice_file_id: str = Header(...),
x_wps_save_type: str = Header(...),
x_wps_weboffice_token: Optional[str] = Header(None),
file: UploadFile = File(...)
) -> Dict:
print(f"上传文件新版本:{x_weboffice_file_id} {x_wps_save_type}")
result_map = {}
file_meta = get_last_version_file(x_weboffice_file_id)
if file_meta:
new_version = file_meta.version + 1
new_file_name = f"{x_weboffice_file_id}v{new_version}"
suffix = file.filename.split(".")[-1] if "." in file.filename else "dat"
new_file_meta = FileMeta(
id=x_weboffice_file_id,
name=file_meta.name,
size=file.size,
create_time=int(datetime.now().timestamp()),
modify_time=int(datetime.now().timestamp()),
download_url=f"{WDZT_CONFIG['server_api']}/file/download/{new_file_name}.{suffix}",
version=new_version,
creator="id1",
modifier="id1"
)
file_path = os.path.join(FILE_STORE_PATH, f"{new_file_name}.{suffix}")
with open(file_path, "wb") as f:
f.write(await file.read())
write_store(new_file_meta)
result_map["file"] = new_file_meta.dict()
return result_map
@router.get("/v1/3rd/file/version/{version}")
async def v13rd_file_version(
x_weboffice_file_id: str = Header(...),
x_wps_weboffice_token: Optional[str] = Header(None),
version: str = Path(...)
) -> Dict:
print(f"获取指定版本的历史版本:{x_weboffice_file_id} {version}")
result_map = {}
version_file = get_version_file(x_weboffice_file_id, version)
if version_file:
version_file.creator = "id1"
version_file.modifier = "id1"
result_map["file"] = version_file.dict()
return result_map
@router.post("/v1/3rd/file/history")
async def v13rd_file_history(
x_weboffice_file_id: str = Header(...),
x_wps_weboffice_token: Optional[str] = Header(None),
params: Dict[str, str] = Body(...)
) -> Dict:
print(f"获取所有历史版本:{params}")
result_map = {}
file_list = get_file_meta_list_by_id(x_weboffice_file_id)
histories = []
user = {
"id": "1",
"name": "id1"
}
for file_meta in file_list:
history_file = {
"id": x_weboffice_file_id,
"name": file_meta.name,
"size": file_meta.size,
"version": file_meta.version,
"create_time": file_meta.create_time,
"modify_time": file_meta.modify_time,
"download_url": file_meta.download_url,
"creator": user,
"modifier": user
}
histories.append(history_file)
result_map["histories"] = histories
return result_map
@router.post("/v1/3rd/onnotify")
async def v13rd_onnotify(
x_weboffice_file_id: str = Header(...),
params: Dict[str, object] = Body(...)
) -> Dict:
print(f"回调通知:{params}")
return {"success": True, "code": 0, "msg": "success"}
2.7 工具函数 - utils.py
import hashlib
import hmac
from datetime import datetime
from typing import Dict
def hmac_sha256(data: str, key: str) -> str:
signature = hmac.new(
key.encode("utf-8"),
data.encode("utf-8"),
hashlib.sha256
).hexdigest()
return signature
def get_sha256_bytes(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def get_gmt_date() -> str:
return datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S GMT")
TYPE_MAP: Dict[str, str] = {}
DOC_TYPES = ["doc", "dot", "wps", "wpt", "docx", "dotx", "docm", "dotm"]
SPREADSHEET_TYPES = ["xls", "xlt", "et", "xlsx", "xltx", "xlsm", "xltm"]
PRESENTATION_TYPES = ["ppt", "pptx", "pptm", "ppsx", "ppsm", "pps", "potx", "potm", "dpt", "dps"]
PDF_TYPES = ["pdf", "ofd"]
OTHER_TYPES = ["jpeg", "jpg", "png", "gif", "bmp", "tif", "tiff", "svg", "psd",
"tar", "zip", "7z", "jar", "rar", "gzip", "md", "c", "cpp", "java",
"js", "css", "lrc", "h", "asm", "s", "asp", "bat", "bas", "prg",
"cmd", "xml", "log", "ini", "inf", "cdr", "vsd", "vsdx"]
for t in DOC_TYPES:
TYPE_MAP[t] = "w"
for t in SPREADSHEET_TYPES:
TYPE_MAP[t] = "s"
for t in PRESENTATION_TYPES:
TYPE_MAP[t] = "p"
for t in PDF_TYPES:
TYPE_MAP[t] = "f"
for t in OTHER_TYPES:
TYPE_MAP[t] = "x"
def get_type_by_filename(filename: str) -> str:
if "." in filename:
suffix = filename.split(".")[-1].lower()
return TYPE_MAP.get(suffix, "w")
return "w"
三、前端实现详解
3.1 API 请求封装 - api/index.js
import axios from 'axios'
const instance = axios.create({
baseURL: '/api',
timeout: 10000
})
export const fileApi = {
upload: (file) => {
const formData = new FormData()
formData.append('file', file)
return instance.post('/file/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
},
list: () => instance.get('/file/list'),
download: (key) => instance.get(`/file/download/${key}`, { responseType: 'blob' })
}
export const zxbjApi = {
getLink: (fileId) => instance.get(`/zxbj/getLink/${fileId}`)
}
export const zxylApi = {
getLink: (fileId) => instance.get(`/zxyl/getLink/${fileId}`),
preload: (fileId, versionId) => instance.post(`/zxyl/preload/${fileId}/${versionId}`)
}
export default instance
3.2 路由配置 - router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import FileList from '../views/FileList.vue'
import FileView from '../views/FileView.vue'
import FileEdit from '../views/FileEdit.vue'
const routes = [
{
path: '/',
name: 'FileList',
component: FileList
},
{
path: '/view/:fileId',
name: 'FileView',
component: FileView
},
{
path: '/edit/:fileId',
name: 'FileEdit',
component: FileEdit
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
3.3 文件列表页面 - FileList.vue
<template>
<div class="min-h-screen bg-gray-50">
<header class="bg-white shadow-sm border-b">
<div class="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between">
<h1 class="text-xl font-bold text-gray-800">WDZT 文档管理系统</h1>
<div class="flex items-center gap-4">
<label class="flex items-center gap-2 px-4 py-2 bg-blue-500 text-white rounded-lg cursor-pointer hover:bg-blue-600 transition">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM6.293 6.707a1 1 0 010-1.414l3-3a1 1 0 011.414 0l3 3a1 1 0 01-1.414 1.414L11 5.414V13a1 1 0 11-2 0V5.414L7.707 6.707a1 1 0 01-1.414 0z" clip-rule="evenodd" />
</svg>
选择文件
<input type="file" class="hidden" @change="handleFileSelect" />
</label>
<button
v-if="selectedFile"
@click="uploadFile"
class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition flex items-center gap-2"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd" />
</svg>
上传
</button>
<button
@click="refreshList"
class="px-4 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition flex items-center gap-2"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clip-rule="evenodd" />
</svg>
刷新
</button>
</div>
</div>
</header>
<main class="max-w-7xl mx-auto px-4 py-6">
<div class="bg-white rounded-xl shadow-sm overflow-hidden">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">文件名</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">版本</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">大小</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="file in fileList" :key="`${file.id}-${file.version}`" class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ file.id }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 max-w-xs truncate">{{ file.name }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ file.version }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ formatSize(file.size) }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div class="flex items-center gap-2">
<button
@click="handleView(file.id)"
class="px-3 py-1 text-sm bg-blue-500 text-white rounded hover:bg-blue-600 transition"
>
预览
</button>
<button
@click="handleEdit(file.id)"
class="px-3 py-1 text-sm bg-green-500 text-white rounded hover:bg-green-600 transition"
>
编辑
</button>
<button
@click="handleDownload(file)"
class="px-3 py-1 text-sm bg-gray-500 text-white rounded hover:bg-gray-600 transition"
>
下载
</button>
<button
@click="handlePreload(file.id)"
class="px-3 py-1 text-sm bg-yellow-500 text-white rounded hover:bg-yellow-600 transition"
>
预处理
</button>
</div>
</td>
</tr>
<tr v-if="fileList.length === 0">
<td colspan="5" class="px-6 py-12 text-center text-gray-500">
暂无文件
</td>
</tr>
</tbody>
</table>
</div>
<div class="px-6 py-4 bg-gray-50 border-t">
<span class="text-sm text-gray-500">共 {{ fileList.length }} 个文件</span>
</div>
</div>
</main>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { fileApi, zxylApi } from '../api'
const fileList = ref([])
const selectedFile = ref(null)
const formatSize = (bytes) => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
const loadFileList = async () => {
try {
const response = await fileApi.list()
fileList.value = response.data.list || []
} catch (error) {
console.error('加载文件列表失败:', error)
alert('加载文件列表失败')
}
}
const refreshList = () => {
loadFileList()
}
const handleFileSelect = (event) => {
const file = event.target.files[0]
if (file) {
selectedFile.value = file
}
}
const uploadFile = async () => {
if (!selectedFile.value) return
try {
await fileApi.upload(selectedFile.value)
alert('上传成功')
selectedFile.value = null
loadFileList()
} catch (error) {
console.error('上传失败:', error)
alert('上传失败')
}
}
const handleView = (fileId) => {
window.open(`/view/${fileId}`, '_blank')
}
const handleEdit = (fileId) => {
window.open(`/edit/${fileId}`, '_blank')
}
const handleDownload = (file) => {
if (file.download_url) {
window.location.href = `/api${file.download_url.replace('http://10.229.12.41:8000', '')}`
}
}
const handlePreload = async (fileId) => {
try {
const response = await zxylApi.preload(fileId, 1)
if (response.data.code === 0) {
alert('预处理完成')
} else {
alert('预处理失败')
}
} catch (error) {
console.error('预处理失败:', error)
alert('预处理失败')
}
}
onMounted(() => {
loadFileList()
})
</script>
3.4 文件预览页面 - FileView.vue
<template>
<div class="min-h-screen bg-gray-100">
<header class="bg-white shadow-sm border-b">
<div class="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between">
<div class="flex items-center gap-4">
<button
@click="goBack"
class="px-4 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition flex items-center gap-2"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
返回
</button>
<h1 class="text-xl font-bold text-gray-800">文件预览</h1>
</div>
<span class="text-sm text-gray-500">文件ID: {{ fileId }}</span>
</div>
</header>
<main class="max-w-7xl mx-auto px-4 py-6">
<div v-if="error" class="flex items-center justify-center h-96">
<div class="text-center">
<p class="text-red-500 text-lg">{{ error }}</p>
<button
@click="loadPreview"
class="mt-4 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition"
>
重试
</button>
</div>
</div>
<div v-else class="bg-white rounded-xl shadow-sm overflow-hidden" style="position: relative;">
<div id="office" style="width: 100%; height: 800px; position: relative;"></div>
<div v-if="loading" class="absolute inset-0 bg-white/80 flex items-center justify-center z-10">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
</div>
</div>
</main>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
import { zxylApi } from '../api'
const route = useRoute()
const fileId = ref('')
const loading = ref(true)
const error = ref('')
const goBack = () => {
window.history.back()
}
const loadPreview = async () => {
loading.value = true
error.value = ''
try {
const response = await zxylApi.getLink(fileId.value)
if (response.data.code === 0 && response.data.data && response.data.data.data) {
const link = response.data.data.data.link
if (!window.OpenSDK && !window.WPS) {
error.value = 'OpenSDK 未加载,请检查 jssdk 文件'
loading.value = false
return
}
const officeEl = document.querySelector('#office')
if (!officeEl) {
error.value = '挂载元素不存在'
loading.value = false
return
}
if (window.WebOfficeSDK) {
window.WebOfficeSDK.config({
url: link,
mount: officeEl
})
} else if (window.OpenSDK) {
window.OpenSDK.config({
url: link,
mount: officeEl
})
} else if (window.WPS) {
window.WPS.config({
url: link,
mount: officeEl
})
}
loading.value = false
} else {
error.value = response.data.msg || '获取预览链接失败'
loading.value = false
}
} catch (err) {
console.error('加载预览失败:', err)
error.value = '加载预览失败,请检查网络连接'
loading.value = false
}
}
onMounted(() => {
fileId.value = route.params.fileId
loadPreview()
})
onUnmounted(() => {
if (window.jssdk) {
window.jssdk.destroy?.()
}
})
</script>
四、完整请求流程
4.1 文件上传流程
用户选择文件 → 前端 FileList.vue → axios POST /api/file/upload
↓
后端 file_router.py
↓
保存文件到 FILE_STORE_PATH
↓
生成 .store 元数据文件
↓
返回 { success: true, file_id: “xxx” }
4.2 文件预览流程
用户点击"预览" → 前端打开 /view/{fileId} → FileView.vue 加载
↓
axios GET /api/zxyl/getLink/{fileId}
↓
后端调用文档中台 API(HMAC签名)
↓
返回预览链接
↓
OpenSDK.config({ url, mount })
↓
浏览器嵌入文档预览
五、关键技术点总结

六、问题总结
6.1 App.vue代码的作用

1万+

被折叠的 条评论
为什么被折叠?



