refactor(styles): 建立统一的生产级样式体系 (#4)

## 变更内容

- 将全局样式拆分为设计令牌、基础规则和语义组件层
- 集中管理 Naive UI 主题配置
- 迁移页面容器、表单、信息面板和按钮等重复样式
- 增加样式体系文档与自动契约检查

## 验证

- `npm run check:styles`
- `npm run build`Reviewed-on: #4
This commit was merged in pull request #4.
This commit is contained in:
2026-07-23 02:41:04 +08:00
parent 51743645e2
commit 16b3dd4aaa
27 changed files with 828 additions and 480 deletions
+3 -3
View File
@@ -86,9 +86,9 @@ async function savePassword() {
</RouterLink>
<div class="mt-6">
<p class="text-sm font-semibold uppercase tracking-[0.24em] text-sky-700">Account Settings</p>
<h1 class="mt-3 text-4xl font-bold text-slate-900">个人资料设置</h1>
<p class="mt-4 max-w-2xl text-sm leading-7 text-slate-600">
<p class="oc-page-eyebrow">Account Settings</p>
<h1 class="oc-page-title">个人资料设置</h1>
<p class="oc-page-description">
修改公开昵称和登录密码。昵称会显示在画廊、图鉴和地图中,且不能与其他用户重复。
</p>
</div>
+19 -19
View File
@@ -473,8 +473,8 @@ watch(selectedUploadDate, async newValue => {
<template>
<div class="relative">
<div class="border-b border-sky-100 bg-[linear-gradient(180deg,#e0f2fe_0%,#f8fafc_100%)]">
<div class="max-w-6xl mx-auto px-4 py-10">
<div class="oc-page-hero">
<div class="oc-container oc-container--content oc-page-hero__inner">
<div v-if="loading" class="grid gap-6 lg:grid-cols-[1.25fr_0.95fr]">
<NCard class="oc-panel-card border border-slate-200 shadow-sm" :bordered="false">
<NSkeleton class="h-6 w-1/3" />
@@ -505,8 +505,8 @@ watch(selectedUploadDate, async newValue => {
</div>
<div class="min-w-0">
<p class="text-sm font-medium uppercase tracking-[0.24em] text-sky-700">Sky Journal</p>
<h1 class="mt-3 text-4xl font-bold text-slate-900">
<p class="oc-page-eyebrow">Sky Journal</p>
<h1 class="oc-page-title">
{{ pageTitle }}
</h1>
<div class="mt-3 flex flex-wrap items-center gap-3">
@@ -554,7 +554,7 @@ watch(selectedUploadDate, async newValue => {
</div>
</div>
<div class="max-w-6xl mx-auto px-4 py-8">
<div class="oc-container oc-container--content oc-page-content">
<NAlert
v-if="errorMsg"
type="error"
@@ -592,7 +592,7 @@ watch(selectedUploadDate, async newValue => {
<NCard class="oc-panel-card border border-slate-200 shadow-sm" :bordered="false">
<p class="text-sm text-slate-500">最常拍云型</p>
<p class="mt-2 text-2xl font-bold text-slate-900">{{ mostCommonCloudType.name }}</p>
<p class="oc-stat-value">{{ mostCommonCloudType.name }}</p>
<p class="mt-2 text-sm text-slate-500" v-if="mostCommonCloudType.count">
共 {{ mostCommonCloudType.count }} 张
</p>
@@ -605,7 +605,7 @@ watch(selectedUploadDate, async newValue => {
<NCard class="oc-panel-card border border-slate-200 shadow-sm" :bordered="false">
<p class="text-sm text-slate-500">{{ isOwnProfile ? '已通过 / 待审核' : '公开通过数' }}</p>
<p class="mt-2 text-2xl font-bold text-slate-900">
<p class="oc-stat-value">
<template v-if="isOwnProfile">{{ approvedShots }} / {{ pendingShots }}</template>
<template v-else>{{ approvedShots }}</template>
</p>
@@ -630,7 +630,7 @@ watch(selectedUploadDate, async newValue => {
<div class="mb-5 flex items-end justify-between gap-4">
<div>
<h2 class="text-2xl font-bold text-slate-900">天空日志</h2>
<p class="mt-1 text-sm text-slate-500">
<p class="oc-field-help">
按拍摄时间倒序展示,并按月份分组。热力图统计按上传日期计算。
</p>
</div>
@@ -794,26 +794,26 @@ watch(selectedUploadDate, async newValue => {
@close="selectedCloud = null"
>
<div class="grid gap-4 sm:grid-cols-2">
<div class="border border-slate-200 bg-slate-50 p-4">
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">上传时间</p>
<div class="oc-inset-panel">
<p class="oc-section-label">上传时间</p>
<p class="mt-2 text-sm font-medium text-slate-900">{{ formatDateTime(selectedCloud.created_at) }}</p>
</div>
<div class="border border-slate-200 bg-slate-50 p-4">
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">拍摄时间</p>
<div class="oc-inset-panel">
<p class="oc-section-label">拍摄时间</p>
<p class="mt-2 text-sm font-medium text-slate-900">{{ formatDateTime(selectedCloud.captured_at) }}</p>
</div>
<div class="border border-slate-200 bg-slate-50 p-4">
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">位置</p>
<div class="oc-inset-panel">
<p class="oc-section-label">位置</p>
<p class="mt-2 text-sm font-medium text-slate-900">{{ selectedCloud.location_name || '未填写位置名称' }}</p>
</div>
<div class="border border-slate-200 bg-slate-50 p-4">
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">模糊化经纬度</p>
<div class="oc-inset-panel">
<p class="oc-section-label">模糊化经纬度</p>
<p class="mt-2 text-sm font-medium text-slate-900">
纬度 {{ formatCoordinate(selectedCloud.latitude) }} / 经度 {{ formatCoordinate(selectedCloud.longitude) }}
</p>
</div>
<div class="border border-slate-200 bg-slate-50 p-4">
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">{{ isOwnProfile ? '审核状态' : '可见性' }}</p>
<div class="oc-inset-panel">
<p class="oc-section-label">{{ isOwnProfile ? '审核状态' : '可见性' }}</p>
<p class="mt-2 text-sm font-medium text-slate-900">
<template v-if="isOwnProfile">
{{ statusMeta[selectedCloud.status].label }} / {{ selectedCloud.is_hidden ? '私密' : '公开' }}
@@ -824,7 +824,7 @@ watch(selectedUploadDate, async newValue => {
</div>
<div class="mt-5 border border-slate-200 bg-white p-5">
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">图片说明</p>
<p class="oc-section-label">图片说明</p>
<p class="mt-3 text-sm leading-7 text-slate-700">
{{ selectedCloud.description || '上传者没有留下额外说明。' }}
</p>