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
+13 -13
View File
@@ -119,7 +119,7 @@ watch(() => props.initialValue, value => {
<div class="flex items-start justify-between border-b border-slate-200 px-6 py-5">
<div>
<h3 class="text-xl font-semibold text-slate-900">编辑图片信息</h3>
<p class="mt-1 text-sm text-slate-500">修改后会同步影响个人主页、画廊和地图中的展示。</p>
<p class="oc-field-help">修改后会同步影响个人主页、画廊和地图中的展示。</p>
</div>
<button
type="button"
@@ -143,10 +143,10 @@ watch(() => props.initialValue, value => {
<div class="grid gap-5">
<div>
<label class="mb-1 block text-sm font-medium text-slate-700">类别</label>
<label class="oc-field-label">类别</label>
<select
:value="form.cloudCategoryId ?? ''"
class="w-full border border-slate-300 bg-white px-3 py-2.5 text-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-500"
class="oc-field-control"
@change="onCategoryChange"
>
<option value="" disabled>请选择</option>
@@ -158,36 +158,36 @@ watch(() => props.initialValue, value => {
</div>
<div v-if="form.cloudCategoryId === 'other'">
<label class="mb-1 block text-sm font-medium text-slate-700">自定义云型</label>
<label class="oc-field-label">自定义云型</label>
<input
v-model="form.customCloudType"
type="text"
class="w-full border border-slate-300 px-3 py-2.5 text-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-500"
class="oc-field-control"
/>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-slate-700">拍摄时间</label>
<label class="oc-field-label">拍摄时间</label>
<input
v-model="form.capturedAt"
type="datetime-local"
class="w-full border border-slate-300 px-3 py-2.5 text-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-500"
class="oc-field-control"
/>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-slate-700">位置名称</label>
<label class="oc-field-label">位置名称</label>
<input
v-model="form.locationName"
type="text"
class="w-full border border-slate-300 px-3 py-2.5 text-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-500"
class="oc-field-control"
placeholder="如:北京、成都"
/>
</div>
<div>
<div class="mb-1 flex items-center justify-between gap-3">
<label class="block text-sm font-medium text-slate-700">经纬度</label>
<label class="oc-field-label mb-0">经纬度</label>
<button
v-if="hasCoordinates"
type="button"
@@ -203,7 +203,7 @@ watch(() => props.initialValue, value => {
type="number"
step="any"
placeholder="纬度"
class="w-full border border-slate-300 px-3 py-2.5 text-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-500"
class="oc-field-control"
@input="onLatInput"
/>
<input
@@ -211,7 +211,7 @@ watch(() => props.initialValue, value => {
type="number"
step="any"
placeholder="经度"
class="w-full border border-slate-300 px-3 py-2.5 text-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-500"
class="oc-field-control"
@input="onLngInput"
/>
<button
@@ -227,7 +227,7 @@ watch(() => props.initialValue, value => {
</div>
<div>
<label class="mb-1 block text-sm font-medium text-slate-700">图片描述</label>
<label class="oc-field-label">图片描述</label>
<textarea
v-model="form.description"
rows="4"
+1 -1
View File
@@ -154,7 +154,7 @@ onBeforeUnmount(() => {
<div class="flex items-start justify-between border-b border-slate-200 px-6 py-5">
<div>
<h3 class="text-xl font-semibold text-slate-900">{{ title }}</h3>
<p class="mt-1 text-sm text-slate-500">{{ description }}</p>
<p class="oc-field-help">{{ description }}</p>
</div>
<button
type="button"
+1 -1
View File
@@ -101,7 +101,7 @@ onBeforeUnmount(() => {
<div v-if="hasCoordinates" class="mt-5 overflow-hidden border border-slate-200 bg-white">
<div class="flex items-center justify-between border-b border-slate-200 px-4 py-3">
<div>
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">位置地图</p>
<p class="oc-section-label">位置地图</p>
<p class="mt-1 text-sm font-medium text-slate-900">{{ locationName || '拍摄位置' }}</p>
</div>
<p class="text-right text-[11px] leading-5 text-slate-500">
+18 -18
View File
@@ -203,7 +203,7 @@ onBeforeUnmount(() => {
<div>
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-sky-700">Quick Upload</p>
<h2 class="mt-2 text-xl font-bold text-slate-950">快速上传云图</h2>
<p class="mt-1 text-sm text-slate-500">从地图当前位置带入经纬度,提交后进入审核队列。</p>
<p class="oc-field-help">从地图当前位置带入经纬度,提交后进入审核队列。</p>
</div>
<button
type="button"
@@ -242,7 +242,7 @@ onBeforeUnmount(() => {
>
<NIcon size="42" class="text-sky-600"><CloudUpload /></NIcon>
<p class="mt-3 text-base font-semibold text-slate-800">选择一张云图</p>
<p class="mt-1 text-sm text-slate-500">支持 JPG、PNG,可点击或拖拽上传。</p>
<p class="oc-field-help">支持 JPG、PNG,可点击或拖拽上传。</p>
</div>
<template v-else-if="activeItem">
@@ -259,10 +259,10 @@ onBeforeUnmount(() => {
<div class="space-y-4">
<div>
<label class="mb-1 block text-sm font-medium text-slate-700">类别 <span class="text-rose-500">*</span></label>
<label class="oc-field-label">类别 <span class="oc-field-required">*</span></label>
<select
:value="activeItem.cloudCategoryId ?? ''"
class="w-full border border-slate-300 bg-white px-3 py-2.5 text-sm text-slate-800 outline-none transition-colors focus:border-sky-500 focus:ring-2 focus:ring-sky-100"
class="oc-field-control"
@change="onCategoryChange"
>
<option value="" disabled>请选择</option>
@@ -276,25 +276,25 @@ onBeforeUnmount(() => {
v-model="activeItem.customCloudType"
type="text"
placeholder="输入云的类型"
class="mt-2 w-full border border-slate-300 px-3 py-2 text-sm outline-none transition-colors focus:border-sky-500 focus:ring-2 focus:ring-sky-100"
class="oc-field-control mt-2"
@input="activeItem.errors = {}"
/>
<p v-if="activeItem.errors.cloudCategory" class="mt-1 text-sm text-rose-600">{{ activeItem.errors.cloudCategory }}</p>
<p v-if="activeItem.errors.cloudCategory" class="oc-field-error">{{ activeItem.errors.cloudCategory }}</p>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-slate-700">拍摄时间 <span class="text-rose-500">*</span></label>
<label class="oc-field-label">拍摄时间 <span class="oc-field-required">*</span></label>
<input
type="datetime-local"
:value="formatDatetimeLocal(activeItem.capturedAt)"
class="w-full border border-slate-300 px-3 py-2 text-sm outline-none transition-colors focus:border-sky-500 focus:ring-2 focus:ring-sky-100"
class="oc-field-control"
@change="onCapturedAtChange"
/>
<p v-if="activeItem.errors.capturedAt" class="mt-1 text-sm text-rose-600">{{ activeItem.errors.capturedAt }}</p>
<p v-if="activeItem.errors.capturedAt" class="oc-field-error">{{ activeItem.errors.capturedAt }}</p>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-slate-700">经纬度</label>
<label class="oc-field-label">经纬度</label>
<div class="grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_40px] gap-2">
<div>
<input
@@ -302,10 +302,10 @@ onBeforeUnmount(() => {
type="number"
step="any"
placeholder="纬度"
class="w-full border border-slate-300 px-3 py-2 text-sm outline-none transition-colors focus:border-sky-500 focus:ring-2 focus:ring-sky-100"
class="oc-field-control"
@input="onLatInput"
/>
<p v-if="activeItem.errors.latitude" class="mt-1 text-sm text-rose-600">{{ activeItem.errors.latitude }}</p>
<p v-if="activeItem.errors.latitude" class="oc-field-error">{{ activeItem.errors.latitude }}</p>
</div>
<div>
<input
@@ -313,10 +313,10 @@ onBeforeUnmount(() => {
type="number"
step="any"
placeholder="经度"
class="w-full border border-slate-300 px-3 py-2 text-sm outline-none transition-colors focus:border-sky-500 focus:ring-2 focus:ring-sky-100"
class="oc-field-control"
@input="onLngInput"
/>
<p v-if="activeItem.errors.longitude" class="mt-1 text-sm text-rose-600">{{ activeItem.errors.longitude }}</p>
<p v-if="activeItem.errors.longitude" class="oc-field-error">{{ activeItem.errors.longitude }}</p>
</div>
<button
type="button"
@@ -334,22 +334,22 @@ onBeforeUnmount(() => {
</div>
<div>
<label class="mb-1 block text-sm font-medium text-slate-700">位置名称</label>
<label class="oc-field-label">位置名称</label>
<input
v-model="activeItem.locationName"
type="text"
placeholder="如:北京、成都"
class="w-full border border-slate-300 px-3 py-2 text-sm outline-none transition-colors focus:border-sky-500 focus:ring-2 focus:ring-sky-100"
class="oc-field-control"
/>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-slate-700">图片说明</label>
<label class="oc-field-label">图片说明</label>
<textarea
v-model="activeItem.description"
rows="3"
placeholder="描述一下这张图片..."
class="w-full resize-none border border-slate-300 px-3 py-2 text-sm outline-none transition-colors focus:border-sky-500 focus:ring-2 focus:ring-sky-100"
class="oc-field-control resize-none"
></textarea>
</div>