Merge branch 'main' into fix/security-audit
This commit is contained in:
@@ -0,0 +1,100 @@
|
|||||||
|
# OpenCloud 样式体系
|
||||||
|
|
||||||
|
这套体系用于让视觉规则集中、Vue 模板可读,同时保留 Tailwind 对一次性布局的灵活性。它不是“所有样式都写成全局类”,而是把会跨页面复用、带有产品语义或必须一致的规则集中管理。
|
||||||
|
|
||||||
|
## 分层
|
||||||
|
|
||||||
|
| 层级 | 文件 | 职责 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 设计令牌 | `src/styles/tokens.css` | 颜色、字体、渐变、阴影、动效时长等稳定决策 |
|
||||||
|
| 基础规则 | `src/styles/base.css` | 页面基线、选中文本、全局圆角策略 |
|
||||||
|
| 组件规则 | `src/styles/components.css` | 页面 Hero、容器、表单、面板、按钮等共享语义类 |
|
||||||
|
| 第三方主题 | `src/lib/theme.ts` | Naive UI 的全局主题契约;值应与设计令牌保持一致 |
|
||||||
|
| 单组件样式 | Vue 文件的 `<style scoped>` | 只属于一个功能的滑杆、动画、地图或媒体细节 |
|
||||||
|
| 一次性布局 | Vue 模板中的 Tailwind 类 | flex/grid、间距、响应式排列及确实只出现一次的视觉调整 |
|
||||||
|
|
||||||
|
`src/style.css` 只负责按以上顺序装配全局样式,不再直接堆放组件规则。
|
||||||
|
|
||||||
|
## 命名规则
|
||||||
|
|
||||||
|
- 所有共享类使用 `oc-` 前缀,避免与 Naive UI、AMap 和 Tailwind 冲突。
|
||||||
|
- 组件使用 `.oc-block`,内部结构使用 `.oc-block__element`,变体使用 `.oc-block--modifier`。
|
||||||
|
- 变体不能脱离基础类使用,例如 `oc-panel-button oc-panel-button--sky`。
|
||||||
|
- 类名描述用途,不描述当前颜色。例如使用 `oc-field-error`,而不是 `oc-red-text`。
|
||||||
|
- 只有至少跨两个页面复用,或承载全局产品规则时才新增共享类。仅服务单一功能的代码留在组件内。
|
||||||
|
|
||||||
|
## 常用契约
|
||||||
|
|
||||||
|
### 页面结构
|
||||||
|
|
||||||
|
```html
|
||||||
|
<section class="oc-page-hero">
|
||||||
|
<div class="oc-container oc-container--content oc-page-hero__inner">
|
||||||
|
<p class="oc-page-eyebrow">Cloud Upload</p>
|
||||||
|
<h1 class="oc-page-title">上传云图</h1>
|
||||||
|
<p class="oc-page-description">页面描述</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<main class="oc-container oc-container--content oc-page-content">
|
||||||
|
<!-- 页面内容 -->
|
||||||
|
</main>
|
||||||
|
```
|
||||||
|
|
||||||
|
容器宽度只有三个语义档位:`oc-container--narrow`、`oc-container--content`、`oc-container--wide`。不要在同类页面重复手写 `max-w-* mx-auto px-*`。
|
||||||
|
|
||||||
|
### 表单
|
||||||
|
|
||||||
|
```html
|
||||||
|
<label class="oc-field-label">
|
||||||
|
拍摄时间 <span class="oc-field-required">*</span>
|
||||||
|
</label>
|
||||||
|
<input class="oc-field-control" />
|
||||||
|
<p class="oc-field-help">补充说明</p>
|
||||||
|
<p class="oc-field-error">错误信息</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
原生 `input`、`select` 和 `textarea` 共用 `oc-field-control`。尺寸或布局差异可以追加少量 Tailwind 类,例如 `class="oc-field-control mt-2 resize-none"`。
|
||||||
|
|
||||||
|
### 信息与面板
|
||||||
|
|
||||||
|
- `oc-section-label`:详情面板中的字段标题。
|
||||||
|
- `oc-stat-label` / `oc-stat-value`:统计数据的标签和值。
|
||||||
|
- `oc-inset-panel`:详情中的浅色信息块。
|
||||||
|
- `oc-panel-card` / `oc-panel-card-soft`:Naive UI 卡片的硬阴影。
|
||||||
|
- `oc-surface`:普通 HTML 容器的标准白色表面。
|
||||||
|
- `oc-empty-card`:空状态表面。
|
||||||
|
|
||||||
|
### 按钮
|
||||||
|
|
||||||
|
Naive UI 按钮继续使用 `type="default"`,视觉语义由共享类控制:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<NButton class="oc-panel-button oc-panel-button--neutral" type="default">
|
||||||
|
取消
|
||||||
|
</NButton>
|
||||||
|
<NButton class="oc-panel-button oc-panel-button--sky" type="default">
|
||||||
|
保存
|
||||||
|
</NButton>
|
||||||
|
```
|
||||||
|
|
||||||
|
页面级主操作使用 `oc-primary-button`;面板内操作使用 `oc-panel-button`。可用变体为 `neutral`、`sky`、`teal`、`amber`、`danger`,不要临时创建新的强调色。
|
||||||
|
|
||||||
|
## 新增或修改样式的决策顺序
|
||||||
|
|
||||||
|
1. 先确认现有 `oc-*` 类能否表达该语义。
|
||||||
|
2. 一次性布局使用 Tailwind,不为单次 `flex`、`gap` 或响应式网格创建共享类。
|
||||||
|
3. 跨页面重复或属于产品视觉契约时,在 `components.css` 增加语义类;涉及稳定视觉值时先在 `tokens.css` 增加令牌。
|
||||||
|
4. 只属于一个交互或组件的规则放在对应 Vue 文件的 `<style scoped>` 中。
|
||||||
|
5. 修改 Naive UI 全局值时同步检查 `theme.ts` 与 `tokens.css`。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
提交前运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run check:styles
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
`check:styles` 会阻止已经淘汰的重复 class、非项目调色板以及缺少基础类的按钮修饰类回流;`build` 同时执行 Vue/TypeScript 类型检查和生产构建。
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
"check:styles": "node scripts/check-style-system.mjs",
|
||||||
"build": "vue-tsc -b && vite build",
|
"build": "vue-tsc -b && vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { readdir, readFile } from 'node:fs/promises'
|
||||||
|
import { extname, join, relative } from 'node:path'
|
||||||
|
|
||||||
|
const projectRoot = new URL('..', import.meta.url)
|
||||||
|
const sourceRoot = new URL('../src', import.meta.url)
|
||||||
|
|
||||||
|
const deprecatedPatterns = [
|
||||||
|
{
|
||||||
|
pattern: /\b(?:bg|border|ring|text)-(?:gray|indigo|purple|violet)-\d+\b/g,
|
||||||
|
message: '使用项目约定的 slate/sky/teal 语义色,不要重新引入灰色或紫色系。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /text-xs uppercase tracking-\[0\.18em\] text-slate-500/g,
|
||||||
|
message: '重复的小节标签应使用 oc-section-label。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /border border-slate-200 bg-slate-50 p-4/g,
|
||||||
|
message: '标准内嵌信息面板应使用 oc-inset-panel。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /border-b border-sky-100 bg-\[linear-gradient\(180deg,#e0f2fe_0%,#f8fafc_100%\)\]/g,
|
||||||
|
message: '标准页面头部应使用 oc-page-hero。',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
async function collectVueFiles(directory) {
|
||||||
|
const entries = await readdir(directory, { withFileTypes: true })
|
||||||
|
const files = await Promise.all(entries.map(async (entry) => {
|
||||||
|
const path = join(directory, entry.name)
|
||||||
|
return entry.isDirectory() ? collectVueFiles(path) : [path]
|
||||||
|
}))
|
||||||
|
|
||||||
|
return files.flat().filter((path) => extname(path) === '.vue')
|
||||||
|
}
|
||||||
|
|
||||||
|
function lineNumberAt(source, index) {
|
||||||
|
return source.slice(0, index).split('\n').length
|
||||||
|
}
|
||||||
|
|
||||||
|
const violations = []
|
||||||
|
|
||||||
|
for (const file of await collectVueFiles(sourceRoot.pathname)) {
|
||||||
|
const source = await readFile(file, 'utf8')
|
||||||
|
|
||||||
|
for (const rule of deprecatedPatterns) {
|
||||||
|
for (const match of source.matchAll(rule.pattern)) {
|
||||||
|
violations.push({
|
||||||
|
file: relative(projectRoot.pathname, file),
|
||||||
|
line: lineNumberAt(source, match.index),
|
||||||
|
message: rule.message,
|
||||||
|
value: match[0],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const match of source.matchAll(/(?:^|\s)class="([^"]+)"/gm)) {
|
||||||
|
const classes = match[1].split(/\s+/)
|
||||||
|
const panelModifier = classes.some((name) => name.startsWith('oc-panel-button--'))
|
||||||
|
const primaryModifier = classes.some((name) => name.startsWith('oc-primary-button--'))
|
||||||
|
|
||||||
|
if (panelModifier && !classes.includes('oc-panel-button')) {
|
||||||
|
violations.push({
|
||||||
|
file: relative(projectRoot.pathname, file),
|
||||||
|
line: lineNumberAt(source, match.index),
|
||||||
|
message: 'oc-panel-button 修饰类必须与基础类一起使用。',
|
||||||
|
value: match[0],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (primaryModifier && !classes.includes('oc-primary-button')) {
|
||||||
|
violations.push({
|
||||||
|
file: relative(projectRoot.pathname, file),
|
||||||
|
line: lineNumberAt(source, match.index),
|
||||||
|
message: 'oc-primary-button 修饰类必须与基础类一起使用。',
|
||||||
|
value: match[0],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (violations.length > 0) {
|
||||||
|
console.error('样式体系检查失败:')
|
||||||
|
for (const violation of violations) {
|
||||||
|
console.error(`- ${violation.file}:${violation.line} ${violation.message} (${violation.value})`)
|
||||||
|
}
|
||||||
|
process.exitCode = 1
|
||||||
|
} else {
|
||||||
|
console.log('样式体系检查通过。')
|
||||||
|
}
|
||||||
+5
-69
@@ -1,85 +1,21 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { NConfigProvider, NDialogProvider, NGlobalStyle, NMessageProvider, NNotificationProvider, type GlobalThemeOverrides } from 'naive-ui'
|
import { NConfigProvider, NDialogProvider, NGlobalStyle, NMessageProvider, NNotificationProvider } from 'naive-ui'
|
||||||
import AppHeader from '@/components/layout/AppHeader.vue'
|
import AppHeader from '@/components/layout/AppHeader.vue'
|
||||||
|
import { openCloudThemeOverrides } from '@/lib/theme'
|
||||||
import { Analytics } from "@vercel/analytics/vue"
|
import { Analytics } from "@vercel/analytics/vue"
|
||||||
import { SpeedInsights } from "@vercel/speed-insights/vue"
|
import { SpeedInsights } from "@vercel/speed-insights/vue"
|
||||||
|
|
||||||
const themeOverrides: GlobalThemeOverrides = {
|
|
||||||
common: {
|
|
||||||
primaryColor: '#0f766e',
|
|
||||||
primaryColorHover: '#115e59',
|
|
||||||
primaryColorPressed: '#134e4a',
|
|
||||||
primaryColorSuppl: '#0f766e',
|
|
||||||
infoColor: '#0369a1',
|
|
||||||
successColor: '#0f766e',
|
|
||||||
warningColor: '#d97706',
|
|
||||||
errorColor: '#dc2626',
|
|
||||||
borderRadius: '0px',
|
|
||||||
borderRadiusSmall: '0px',
|
|
||||||
fontFamily: '"IBM Plex Sans", "Noto Sans SC", "PingFang SC", sans-serif',
|
|
||||||
fontFamilyMono: '"IBM Plex Mono", "SFMono-Regular", monospace',
|
|
||||||
bodyColor: '#f3f7fb',
|
|
||||||
cardColor: '#ffffff',
|
|
||||||
modalColor: '#ffffff',
|
|
||||||
popoverColor: '#ffffff',
|
|
||||||
tableColor: '#ffffff',
|
|
||||||
textColorBase: '#0f172a',
|
|
||||||
textColor1: '#0f172a',
|
|
||||||
textColor2: '#334155',
|
|
||||||
textColor3: '#64748b',
|
|
||||||
textColorDisabled: '#94a3b8',
|
|
||||||
dividerColor: '#d9e3ee',
|
|
||||||
borderColor: '#d9e3ee',
|
|
||||||
inputColor: '#ffffff',
|
|
||||||
},
|
|
||||||
Button: {
|
|
||||||
borderRadiusTiny: '0px',
|
|
||||||
borderRadiusSmall: '0px',
|
|
||||||
borderRadiusMedium: '0px',
|
|
||||||
borderRadiusLarge: '0px',
|
|
||||||
textColorPrimary: '#0f172a',
|
|
||||||
textColorHoverPrimary: '#0f172a',
|
|
||||||
},
|
|
||||||
Card: {
|
|
||||||
borderRadius: '0px',
|
|
||||||
borderRadiusEmbedded: '0px',
|
|
||||||
paddingMedium: '24px',
|
|
||||||
},
|
|
||||||
Input: {
|
|
||||||
borderRadius: '0px',
|
|
||||||
},
|
|
||||||
Select: {
|
|
||||||
peers: {
|
|
||||||
InternalSelection: {
|
|
||||||
borderRadius: '0px',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Tabs: {
|
|
||||||
tabBorderRadius: '0px',
|
|
||||||
},
|
|
||||||
Tag: {
|
|
||||||
borderRadius: '0px',
|
|
||||||
},
|
|
||||||
Alert: {
|
|
||||||
borderRadius: '0px',
|
|
||||||
},
|
|
||||||
Modal: {
|
|
||||||
borderRadius: '0px',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NConfigProvider :theme-overrides="themeOverrides">
|
<NConfigProvider :theme-overrides="openCloudThemeOverrides">
|
||||||
<NGlobalStyle />
|
<NGlobalStyle />
|
||||||
<NDialogProvider>
|
<NDialogProvider>
|
||||||
<NNotificationProvider>
|
<NNotificationProvider>
|
||||||
<NMessageProvider>
|
<NMessageProvider>
|
||||||
<div class="min-h-screen flex flex-col bg-[radial-gradient(circle_at_top_left,_rgba(12,148,136,0.12),_transparent_28%),linear-gradient(180deg,#f8fbfd_0%,#eef4f8_100%)]">
|
<div class="oc-app-shell">
|
||||||
<AppHeader />
|
<AppHeader />
|
||||||
<main class="relative flex-1">
|
<main class="relative flex-1">
|
||||||
<div class="pointer-events-none absolute inset-0 bg-[linear-gradient(rgba(15,23,42,0.03)_1px,transparent_1px),linear-gradient(90deg,rgba(15,23,42,0.03)_1px,transparent_1px)] bg-[size:32px_32px] opacity-35"></div>
|
<div class="oc-app-grid"></div>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 class="flex items-start justify-between border-b border-slate-200 px-6 py-5">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-xl font-semibold text-slate-900">编辑图片信息</h3>
|
<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>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -143,10 +143,10 @@ watch(() => props.initialValue, value => {
|
|||||||
|
|
||||||
<div class="grid gap-5">
|
<div class="grid gap-5">
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-1 block text-sm font-medium text-slate-700">类别</label>
|
<label class="oc-field-label">类别</label>
|
||||||
<select
|
<select
|
||||||
:value="form.cloudCategoryId ?? ''"
|
: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"
|
@change="onCategoryChange"
|
||||||
>
|
>
|
||||||
<option value="" disabled>请选择</option>
|
<option value="" disabled>请选择</option>
|
||||||
@@ -158,36 +158,36 @@ watch(() => props.initialValue, value => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="form.cloudCategoryId === 'other'">
|
<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
|
<input
|
||||||
v-model="form.customCloudType"
|
v-model="form.customCloudType"
|
||||||
type="text"
|
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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-1 block text-sm font-medium text-slate-700">拍摄时间</label>
|
<label class="oc-field-label">拍摄时间</label>
|
||||||
<input
|
<input
|
||||||
v-model="form.capturedAt"
|
v-model="form.capturedAt"
|
||||||
type="datetime-local"
|
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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-1 block text-sm font-medium text-slate-700">位置名称</label>
|
<label class="oc-field-label">位置名称</label>
|
||||||
<input
|
<input
|
||||||
v-model="form.locationName"
|
v-model="form.locationName"
|
||||||
type="text"
|
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="如:北京、成都"
|
placeholder="如:北京、成都"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-1 flex items-center justify-between gap-3">
|
<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
|
<button
|
||||||
v-if="hasCoordinates"
|
v-if="hasCoordinates"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -203,7 +203,7 @@ watch(() => props.initialValue, value => {
|
|||||||
type="number"
|
type="number"
|
||||||
step="any"
|
step="any"
|
||||||
placeholder="纬度"
|
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="onLatInput"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
@@ -211,7 +211,7 @@ watch(() => props.initialValue, value => {
|
|||||||
type="number"
|
type="number"
|
||||||
step="any"
|
step="any"
|
||||||
placeholder="经度"
|
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"
|
@input="onLngInput"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@@ -227,7 +227,7 @@ watch(() => props.initialValue, value => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-1 block text-sm font-medium text-slate-700">图片描述</label>
|
<label class="oc-field-label">图片描述</label>
|
||||||
<textarea
|
<textarea
|
||||||
v-model="form.description"
|
v-model="form.description"
|
||||||
rows="4"
|
rows="4"
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ onBeforeUnmount(() => {
|
|||||||
<div class="flex items-start justify-between border-b border-slate-200 px-6 py-5">
|
<div class="flex items-start justify-between border-b border-slate-200 px-6 py-5">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-xl font-semibold text-slate-900">{{ title }}</h3>
|
<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>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ onBeforeUnmount(() => {
|
|||||||
<div v-if="hasCoordinates" class="mt-5 overflow-hidden border border-slate-200 bg-white">
|
<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 class="flex items-center justify-between border-b border-slate-200 px-4 py-3">
|
||||||
<div>
|
<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>
|
<p class="mt-1 text-sm font-medium text-slate-900">{{ locationName || '拍摄位置' }}</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-right text-[11px] leading-5 text-slate-500">
|
<p class="text-right text-[11px] leading-5 text-slate-500">
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ onBeforeUnmount(() => {
|
|||||||
<div>
|
<div>
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-sky-700">Quick Upload</p>
|
<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>
|
<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>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -242,7 +242,7 @@ onBeforeUnmount(() => {
|
|||||||
>
|
>
|
||||||
<NIcon size="42" class="text-sky-600"><CloudUpload /></NIcon>
|
<NIcon size="42" class="text-sky-600"><CloudUpload /></NIcon>
|
||||||
<p class="mt-3 text-base font-semibold text-slate-800">选择一张云图</p>
|
<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>
|
</div>
|
||||||
|
|
||||||
<template v-else-if="activeItem">
|
<template v-else-if="activeItem">
|
||||||
@@ -259,10 +259,10 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<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>
|
||||||
<select
|
<select
|
||||||
:value="activeItem.cloudCategoryId ?? ''"
|
: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"
|
@change="onCategoryChange"
|
||||||
>
|
>
|
||||||
<option value="" disabled>请选择</option>
|
<option value="" disabled>请选择</option>
|
||||||
@@ -276,25 +276,25 @@ onBeforeUnmount(() => {
|
|||||||
v-model="activeItem.customCloudType"
|
v-model="activeItem.customCloudType"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="输入云的类型"
|
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 = {}"
|
@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>
|
||||||
|
|
||||||
<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
|
<input
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
:value="formatDatetimeLocal(activeItem.capturedAt)"
|
: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"
|
@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>
|
||||||
|
|
||||||
<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 class="grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_40px] gap-2">
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
@@ -302,10 +302,10 @@ onBeforeUnmount(() => {
|
|||||||
type="number"
|
type="number"
|
||||||
step="any"
|
step="any"
|
||||||
placeholder="纬度"
|
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"
|
@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>
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
@@ -313,10 +313,10 @@ onBeforeUnmount(() => {
|
|||||||
type="number"
|
type="number"
|
||||||
step="any"
|
step="any"
|
||||||
placeholder="经度"
|
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"
|
@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>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -334,22 +334,22 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-1 block text-sm font-medium text-slate-700">位置名称</label>
|
<label class="oc-field-label">位置名称</label>
|
||||||
<input
|
<input
|
||||||
v-model="activeItem.locationName"
|
v-model="activeItem.locationName"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="如:北京、成都"
|
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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-1 block text-sm font-medium text-slate-700">图片说明</label>
|
<label class="oc-field-label">图片说明</label>
|
||||||
<textarea
|
<textarea
|
||||||
v-model="activeItem.description"
|
v-model="activeItem.description"
|
||||||
rows="3"
|
rows="3"
|
||||||
placeholder="描述一下这张图片..."
|
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>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import type { GlobalThemeOverrides } from 'naive-ui'
|
||||||
|
|
||||||
|
/** Keep these values aligned with the semantic tokens in styles/tokens.css. */
|
||||||
|
export const openCloudThemeOverrides: GlobalThemeOverrides = {
|
||||||
|
common: {
|
||||||
|
primaryColor: '#0f766e',
|
||||||
|
primaryColorHover: '#115e59',
|
||||||
|
primaryColorPressed: '#134e4a',
|
||||||
|
primaryColorSuppl: '#0f766e',
|
||||||
|
infoColor: '#0369a1',
|
||||||
|
successColor: '#0f766e',
|
||||||
|
warningColor: '#d97706',
|
||||||
|
errorColor: '#dc2626',
|
||||||
|
borderRadius: '0px',
|
||||||
|
borderRadiusSmall: '0px',
|
||||||
|
fontFamily: '"IBM Plex Sans", "Noto Sans SC", "PingFang SC", sans-serif',
|
||||||
|
fontFamilyMono: '"IBM Plex Mono", "SFMono-Regular", monospace',
|
||||||
|
bodyColor: '#f3f7fb',
|
||||||
|
cardColor: '#ffffff',
|
||||||
|
modalColor: '#ffffff',
|
||||||
|
popoverColor: '#ffffff',
|
||||||
|
tableColor: '#ffffff',
|
||||||
|
textColorBase: '#0f172a',
|
||||||
|
textColor1: '#0f172a',
|
||||||
|
textColor2: '#334155',
|
||||||
|
textColor3: '#64748b',
|
||||||
|
textColorDisabled: '#94a3b8',
|
||||||
|
dividerColor: '#d9e3ee',
|
||||||
|
borderColor: '#d9e3ee',
|
||||||
|
inputColor: '#ffffff',
|
||||||
|
},
|
||||||
|
Button: {
|
||||||
|
borderRadiusTiny: '0px',
|
||||||
|
borderRadiusSmall: '0px',
|
||||||
|
borderRadiusMedium: '0px',
|
||||||
|
borderRadiusLarge: '0px',
|
||||||
|
textColorPrimary: '#0f172a',
|
||||||
|
textColorHoverPrimary: '#0f172a',
|
||||||
|
},
|
||||||
|
Card: {
|
||||||
|
borderRadius: '0px',
|
||||||
|
borderRadiusEmbedded: '0px',
|
||||||
|
paddingMedium: '24px',
|
||||||
|
},
|
||||||
|
Input: { borderRadius: '0px' },
|
||||||
|
Select: {
|
||||||
|
peers: {
|
||||||
|
InternalSelection: { borderRadius: '0px' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Tabs: { tabBorderRadius: '0px' },
|
||||||
|
Tag: { borderRadius: '0px' },
|
||||||
|
Alert: { borderRadius: '0px' },
|
||||||
|
Modal: { borderRadius: '0px' },
|
||||||
|
}
|
||||||
+3
-225
@@ -1,227 +1,5 @@
|
|||||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&family=Noto+Sans+SC:wght@400;500;700;900&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&family=Noto+Sans+SC:wght@400;500;700;900&display=swap');
|
||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
|
@import './styles/tokens.css';
|
||||||
html,
|
@import './styles/base.css';
|
||||||
body,
|
@import './styles/components.css';
|
||||||
#app {
|
|
||||||
min-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
color: #0f172a;
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgba(15, 23, 42, 0.02), rgba(15, 23, 42, 0)),
|
|
||||||
#eef4f8;
|
|
||||||
}
|
|
||||||
|
|
||||||
::selection {
|
|
||||||
background: rgba(15, 118, 110, 0.18);
|
|
||||||
}
|
|
||||||
|
|
||||||
.oc-primary-button.n-button {
|
|
||||||
transition:
|
|
||||||
background-color 160ms ease,
|
|
||||||
color 160ms ease,
|
|
||||||
border-color 160ms ease,
|
|
||||||
box-shadow 160ms ease,
|
|
||||||
transform 160ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oc-primary-button.n-button .n-button__content {
|
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: 0.12em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oc-primary-button.n-button:not(.n-button--disabled):hover {
|
|
||||||
transform: translate(-1px, -1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.oc-primary-button--teal.n-button {
|
|
||||||
border-color: rgb(153 246 228) !important;
|
|
||||||
background: rgb(255, 255, 255) !important;
|
|
||||||
color: rgb(17 94 89) !important;
|
|
||||||
box-shadow: 4px 4px 0 0 rgba(20, 184, 166, 0.1) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oc-primary-button--teal.n-button:hover,
|
|
||||||
.oc-primary-button--teal.n-button:focus-visible {
|
|
||||||
border-color: rgb(94 234 212) !important;
|
|
||||||
background: rgb(234 250 245) !important;
|
|
||||||
color: rgb(19 78 74) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oc-primary-button--teal.n-button:not(.n-button--disabled):hover {
|
|
||||||
box-shadow: 5px 5px 0 0 rgba(20, 184, 166, 0.14) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oc-primary-button--sky.n-button {
|
|
||||||
border-color: rgb(186 230 253) !important;
|
|
||||||
background: rgb(255, 255, 255) !important;
|
|
||||||
color: rgb(7 89 133) !important;
|
|
||||||
box-shadow: 4px 4px 0 0 rgba(14, 165, 233, 0.14) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oc-primary-button--sky.n-button:hover,
|
|
||||||
.oc-primary-button--sky.n-button:focus-visible {
|
|
||||||
border-color: rgb(125 211 252) !important;
|
|
||||||
background: rgb(240 249 255) !important;
|
|
||||||
color: rgb(12 74 110) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oc-primary-button--sky.n-button:not(.n-button--disabled):hover {
|
|
||||||
box-shadow: 5px 5px 0 0 rgba(14, 165, 233, 0.18) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oc-panel-card {
|
|
||||||
box-shadow: 6px 6px 0 0 rgba(15, 23, 42, 0.05) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oc-panel-card-soft {
|
|
||||||
box-shadow: 6px 6px 0 0 rgba(15, 23, 42, 0.04) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oc-empty-card {
|
|
||||||
box-shadow: 6px 6px 0 0 rgba(15, 23, 42, 0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.oc-panel-button.n-button {
|
|
||||||
--n-color: rgb(255 255 255) !important;
|
|
||||||
--n-color-hover: rgb(240 253 250) !important;
|
|
||||||
--n-color-pressed: rgb(236 253 245) !important;
|
|
||||||
--n-border: 1px solid rgb(226 232 240) !important;
|
|
||||||
--n-border-hover: 1px solid rgb(153 246 228) !important;
|
|
||||||
--n-border-pressed: 1px solid rgb(94 234 212) !important;
|
|
||||||
--n-text-color: rgb(71 85 105) !important;
|
|
||||||
--n-text-color-hover: rgb(19 78 74) !important;
|
|
||||||
--n-text-color-pressed: rgb(17 94 89) !important;
|
|
||||||
border-color: rgb(226 232 240) !important;
|
|
||||||
background: rgb(255 255 255) !important;
|
|
||||||
color: rgb(71 85 105) !important;
|
|
||||||
box-shadow: 3px 3px 0 0 rgba(15, 23, 42, 0.06) !important;
|
|
||||||
transition:
|
|
||||||
background-color 160ms ease,
|
|
||||||
color 160ms ease,
|
|
||||||
border-color 160ms ease,
|
|
||||||
box-shadow 160ms ease,
|
|
||||||
transform 160ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oc-panel-button.n-button .n-button__content {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oc-panel-button.n-button:not(.n-button--disabled):hover,
|
|
||||||
.oc-panel-button.n-button:focus-visible {
|
|
||||||
transform: translate(-1px, -1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.oc-panel-button--neutral.n-button:hover,
|
|
||||||
.oc-panel-button--neutral.n-button:focus-visible {
|
|
||||||
border-color: rgb(153 246 228) !important;
|
|
||||||
background: rgb(240 253 250) !important;
|
|
||||||
color: rgb(19 78 74) !important;
|
|
||||||
box-shadow: 4px 4px 0 0 rgba(20, 184, 166, 0.1) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oc-panel-button--sky.n-button {
|
|
||||||
--n-color: rgb(224 242 254) !important;
|
|
||||||
--n-color-hover: rgb(240 249 255) !important;
|
|
||||||
--n-color-pressed: rgb(224 242 254) !important;
|
|
||||||
--n-border: 1px solid rgb(186 230 253) !important;
|
|
||||||
--n-border-hover: 1px solid rgb(125 211 252) !important;
|
|
||||||
--n-border-pressed: 1px solid rgb(56 189 248) !important;
|
|
||||||
--n-text-color: rgb(7 89 133) !important;
|
|
||||||
--n-text-color-hover: rgb(12 74 110) !important;
|
|
||||||
--n-text-color-pressed: rgb(12 74 110) !important;
|
|
||||||
border-color: rgb(186 230 253) !important;
|
|
||||||
background: rgb(224 242 254) !important;
|
|
||||||
color: rgb(7 89 133) !important;
|
|
||||||
box-shadow: 4px 4px 0 0 rgba(14, 165, 233, 0.14) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oc-panel-button--sky.n-button:hover,
|
|
||||||
.oc-panel-button--sky.n-button:focus-visible {
|
|
||||||
border-color: rgb(125 211 252) !important;
|
|
||||||
background: rgb(240 249 255) !important;
|
|
||||||
color: rgb(12 74 110) !important;
|
|
||||||
box-shadow: 5px 5px 0 0 rgba(14, 165, 233, 0.18) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oc-panel-button--teal.n-button {
|
|
||||||
--n-color: rgb(242 250 247) !important;
|
|
||||||
--n-color-hover: rgb(234 250 245) !important;
|
|
||||||
--n-color-pressed: rgb(220 252 231) !important;
|
|
||||||
--n-border: 1px solid rgb(153 246 228) !important;
|
|
||||||
--n-border-hover: 1px solid rgb(94 234 212) !important;
|
|
||||||
--n-border-pressed: 1px solid rgb(45 212 191) !important;
|
|
||||||
--n-text-color: rgb(17 94 89) !important;
|
|
||||||
--n-text-color-hover: rgb(19 78 74) !important;
|
|
||||||
--n-text-color-pressed: rgb(19 78 74) !important;
|
|
||||||
border-color: rgb(153 246 228) !important;
|
|
||||||
background: rgb(242 250 247) !important;
|
|
||||||
color: rgb(17 94 89) !important;
|
|
||||||
box-shadow: 4px 4px 0 0 rgba(20, 184, 166, 0.1) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oc-panel-button--teal.n-button:hover,
|
|
||||||
.oc-panel-button--teal.n-button:focus-visible {
|
|
||||||
border-color: rgb(94 234 212) !important;
|
|
||||||
background: rgb(234 250 245) !important;
|
|
||||||
color: rgb(19 78 74) !important;
|
|
||||||
box-shadow: 5px 5px 0 0 rgba(20, 184, 166, 0.14) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oc-panel-button--danger.n-button {
|
|
||||||
--n-color: rgb(255 241 242) !important;
|
|
||||||
--n-color-hover: rgb(255 228 230) !important;
|
|
||||||
--n-color-pressed: rgb(255 228 230) !important;
|
|
||||||
--n-border: 1px solid rgb(254 205 211) !important;
|
|
||||||
--n-border-hover: 1px solid rgb(253 164 175) !important;
|
|
||||||
--n-border-pressed: 1px solid rgb(251 113 133) !important;
|
|
||||||
--n-text-color: rgb(190 24 93) !important;
|
|
||||||
--n-text-color-hover: rgb(159 18 57) !important;
|
|
||||||
--n-text-color-pressed: rgb(159 18 57) !important;
|
|
||||||
border-color: rgb(254 205 211) !important;
|
|
||||||
background: rgb(255 241 242) !important;
|
|
||||||
color: rgb(190 24 93) !important;
|
|
||||||
box-shadow: 4px 4px 0 0 rgba(244, 63, 94, 0.1) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oc-panel-button--danger.n-button:hover,
|
|
||||||
.oc-panel-button--danger.n-button:focus-visible {
|
|
||||||
border-color: rgb(253 164 175) !important;
|
|
||||||
background: rgb(255 228 230) !important;
|
|
||||||
color: rgb(159 18 57) !important;
|
|
||||||
box-shadow: 5px 5px 0 0 rgba(244, 63, 94, 0.14) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oc-panel-button--amber.n-button {
|
|
||||||
--n-color: rgb(255 251 235) !important;
|
|
||||||
--n-color-hover: rgb(254 243 199) !important;
|
|
||||||
--n-color-pressed: rgb(253 230 138) !important;
|
|
||||||
--n-border: 1px solid rgb(253 230 138) !important;
|
|
||||||
--n-border-hover: 1px solid rgb(252 211 77) !important;
|
|
||||||
--n-border-pressed: 1px solid rgb(245 158 11) !important;
|
|
||||||
--n-text-color: rgb(146 64 14) !important;
|
|
||||||
--n-text-color-hover: rgb(120 53 15) !important;
|
|
||||||
--n-text-color-pressed: rgb(120 53 15) !important;
|
|
||||||
border-color: rgb(253 230 138) !important;
|
|
||||||
background: rgb(255 251 235) !important;
|
|
||||||
color: rgb(146 64 14) !important;
|
|
||||||
box-shadow: 4px 4px 0 0 rgba(245, 158, 11, 0.1) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oc-panel-button--amber.n-button:hover,
|
|
||||||
.oc-panel-button--amber.n-button:focus-visible {
|
|
||||||
border-color: rgb(252 211 77) !important;
|
|
||||||
background: rgb(254 243 199) !important;
|
|
||||||
color: rgb(120 53 15) !important;
|
|
||||||
box-shadow: 5px 5px 0 0 rgba(245, 158, 11, 0.14) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer utilities {
|
|
||||||
[class~='rounded'],
|
|
||||||
[class*='rounded-'] {
|
|
||||||
border-radius: 0 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
@layer base {
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#app {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
color: var(--oc-color-text);
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgb(15 23 42 / 2%), rgb(15 23 42 / 0%)),
|
||||||
|
var(--oc-color-canvas);
|
||||||
|
font-family: var(--oc-font-sans);
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: var(--oc-color-selection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
/* The atlas language is square by default. Documented exceptions may opt out. */
|
||||||
|
[class~='rounded'],
|
||||||
|
[class*='rounded-'] {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
@layer components {
|
||||||
|
/* Application and page structure */
|
||||||
|
.oc-app-shell {
|
||||||
|
@apply flex min-h-screen flex-col;
|
||||||
|
background: var(--oc-gradient-canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-app-grid {
|
||||||
|
@apply pointer-events-none absolute inset-0 opacity-35;
|
||||||
|
background-image: var(--oc-gradient-grid);
|
||||||
|
background-size: 32px 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-page-hero {
|
||||||
|
@apply border-b border-sky-100;
|
||||||
|
background: var(--oc-gradient-page);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-page-hero--soft {
|
||||||
|
background: var(--oc-gradient-page-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-container {
|
||||||
|
@apply mx-auto w-full px-4 sm:px-6 lg:px-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-container--narrow {
|
||||||
|
@apply max-w-4xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-container--content {
|
||||||
|
@apply max-w-6xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-container--wide {
|
||||||
|
@apply max-w-7xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-page-hero__inner {
|
||||||
|
@apply py-10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-page-content {
|
||||||
|
@apply py-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-page-eyebrow {
|
||||||
|
@apply text-sm font-medium uppercase tracking-[0.24em] text-sky-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-page-title {
|
||||||
|
@apply mt-3 text-4xl font-bold text-slate-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-page-description {
|
||||||
|
@apply mt-4 max-w-2xl text-sm leading-7 text-slate-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reusable text hierarchy */
|
||||||
|
.oc-section-label {
|
||||||
|
@apply text-xs uppercase tracking-[0.18em] text-slate-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-stat-label {
|
||||||
|
@apply text-xs uppercase tracking-[0.2em] text-slate-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-stat-value {
|
||||||
|
@apply mt-2 text-2xl font-bold text-slate-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-meta {
|
||||||
|
@apply text-sm text-slate-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-field-label {
|
||||||
|
@apply mb-1 block text-sm font-medium text-slate-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-field-required {
|
||||||
|
@apply text-rose-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-field-control {
|
||||||
|
@apply w-full border border-slate-300 bg-white px-3 py-2.5 text-sm text-slate-800 outline-none transition-colors;
|
||||||
|
@apply focus:border-sky-500 focus:ring-2 focus:ring-sky-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-field-error {
|
||||||
|
@apply mt-1 text-sm text-rose-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-field-help {
|
||||||
|
@apply mt-1 text-sm text-slate-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Surfaces */
|
||||||
|
.oc-panel-card {
|
||||||
|
box-shadow: var(--oc-shadow-md) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-panel-card-soft {
|
||||||
|
box-shadow: 6px 6px 0 0 rgb(15 23 42 / 4%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-empty-card {
|
||||||
|
box-shadow: 6px 6px 0 0 rgb(15 23 42 / 4%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-surface {
|
||||||
|
@apply border border-slate-200 bg-white;
|
||||||
|
box-shadow: var(--oc-shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-inset-panel {
|
||||||
|
@apply border border-slate-200 bg-slate-50 p-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-map-button {
|
||||||
|
@apply flex h-10 w-10 items-center justify-center bg-white shadow-md transition-colors hover:bg-slate-50;
|
||||||
|
border-radius: 0.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-system-page {
|
||||||
|
@apply flex min-h-[calc(100vh-4rem)] items-center justify-center px-4 py-10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-system-card {
|
||||||
|
@apply w-full max-w-2xl border border-slate-200 bg-white px-8 py-14 text-center;
|
||||||
|
box-shadow: var(--oc-shadow-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Naive UI button contract */
|
||||||
|
.oc-primary-button.n-button,
|
||||||
|
.oc-panel-button.n-button {
|
||||||
|
transition:
|
||||||
|
background-color var(--oc-duration-fast) var(--oc-ease-standard),
|
||||||
|
color var(--oc-duration-fast) var(--oc-ease-standard),
|
||||||
|
border-color var(--oc-duration-fast) var(--oc-ease-standard),
|
||||||
|
box-shadow var(--oc-duration-fast) var(--oc-ease-standard),
|
||||||
|
transform var(--oc-duration-fast) var(--oc-ease-standard);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-primary-button.n-button .n-button__content,
|
||||||
|
.oc-panel-button.n-button .n-button__content {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-primary-button.n-button .n-button__content {
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-primary-button.n-button:not(.n-button--disabled):hover,
|
||||||
|
.oc-panel-button.n-button:not(.n-button--disabled):hover,
|
||||||
|
.oc-panel-button.n-button:focus-visible {
|
||||||
|
transform: translate(-1px, -1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-primary-button--teal.n-button {
|
||||||
|
border-color: rgb(153 246 228) !important;
|
||||||
|
background: rgb(255 255 255) !important;
|
||||||
|
color: rgb(17 94 89) !important;
|
||||||
|
box-shadow: var(--oc-shadow-teal) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-primary-button--teal.n-button:hover,
|
||||||
|
.oc-primary-button--teal.n-button:focus-visible {
|
||||||
|
border-color: rgb(94 234 212) !important;
|
||||||
|
background: rgb(234 250 245) !important;
|
||||||
|
color: rgb(19 78 74) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-primary-button--teal.n-button:not(.n-button--disabled):hover {
|
||||||
|
box-shadow: 5px 5px 0 0 rgb(20 184 166 / 14%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-primary-button--sky.n-button {
|
||||||
|
border-color: rgb(186 230 253) !important;
|
||||||
|
background: rgb(255 255 255) !important;
|
||||||
|
color: rgb(7 89 133) !important;
|
||||||
|
box-shadow: var(--oc-shadow-sky) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-primary-button--sky.n-button:hover,
|
||||||
|
.oc-primary-button--sky.n-button:focus-visible {
|
||||||
|
border-color: rgb(125 211 252) !important;
|
||||||
|
background: rgb(240 249 255) !important;
|
||||||
|
color: rgb(12 74 110) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-primary-button--sky.n-button:not(.n-button--disabled):hover {
|
||||||
|
box-shadow: 5px 5px 0 0 rgb(14 165 233 / 18%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-panel-button.n-button {
|
||||||
|
--n-color: rgb(255 255 255) !important;
|
||||||
|
--n-color-hover: rgb(240 253 250) !important;
|
||||||
|
--n-color-pressed: rgb(236 253 245) !important;
|
||||||
|
--n-border: 1px solid rgb(226 232 240) !important;
|
||||||
|
--n-border-hover: 1px solid rgb(153 246 228) !important;
|
||||||
|
--n-border-pressed: 1px solid rgb(94 234 212) !important;
|
||||||
|
--n-text-color: rgb(71 85 105) !important;
|
||||||
|
--n-text-color-hover: rgb(19 78 74) !important;
|
||||||
|
--n-text-color-pressed: rgb(17 94 89) !important;
|
||||||
|
border-color: rgb(226 232 240) !important;
|
||||||
|
background: rgb(255 255 255) !important;
|
||||||
|
color: rgb(71 85 105) !important;
|
||||||
|
box-shadow: var(--oc-shadow-xs) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-panel-button--neutral.n-button:hover,
|
||||||
|
.oc-panel-button--neutral.n-button:focus-visible {
|
||||||
|
border-color: rgb(153 246 228) !important;
|
||||||
|
background: rgb(240 253 250) !important;
|
||||||
|
color: rgb(19 78 74) !important;
|
||||||
|
box-shadow: 4px 4px 0 0 rgb(20 184 166 / 10%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-panel-button--sky.n-button {
|
||||||
|
--n-color: rgb(224 242 254) !important;
|
||||||
|
--n-color-hover: rgb(240 249 255) !important;
|
||||||
|
--n-color-pressed: rgb(224 242 254) !important;
|
||||||
|
--n-border: 1px solid rgb(186 230 253) !important;
|
||||||
|
--n-border-hover: 1px solid rgb(125 211 252) !important;
|
||||||
|
--n-border-pressed: 1px solid rgb(56 189 248) !important;
|
||||||
|
--n-text-color: rgb(7 89 133) !important;
|
||||||
|
--n-text-color-hover: rgb(12 74 110) !important;
|
||||||
|
--n-text-color-pressed: rgb(12 74 110) !important;
|
||||||
|
border-color: rgb(186 230 253) !important;
|
||||||
|
background: rgb(224 242 254) !important;
|
||||||
|
color: rgb(7 89 133) !important;
|
||||||
|
box-shadow: var(--oc-shadow-sky) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-panel-button--sky.n-button:hover,
|
||||||
|
.oc-panel-button--sky.n-button:focus-visible {
|
||||||
|
border-color: rgb(125 211 252) !important;
|
||||||
|
background: rgb(240 249 255) !important;
|
||||||
|
color: rgb(12 74 110) !important;
|
||||||
|
box-shadow: 5px 5px 0 0 rgb(14 165 233 / 18%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-panel-button--teal.n-button {
|
||||||
|
--n-color: rgb(242 250 247) !important;
|
||||||
|
--n-color-hover: rgb(234 250 245) !important;
|
||||||
|
--n-color-pressed: rgb(220 252 231) !important;
|
||||||
|
--n-border: 1px solid rgb(153 246 228) !important;
|
||||||
|
--n-border-hover: 1px solid rgb(94 234 212) !important;
|
||||||
|
--n-border-pressed: 1px solid rgb(45 212 191) !important;
|
||||||
|
--n-text-color: rgb(17 94 89) !important;
|
||||||
|
--n-text-color-hover: rgb(19 78 74) !important;
|
||||||
|
--n-text-color-pressed: rgb(19 78 74) !important;
|
||||||
|
border-color: rgb(153 246 228) !important;
|
||||||
|
background: rgb(242 250 247) !important;
|
||||||
|
color: rgb(17 94 89) !important;
|
||||||
|
box-shadow: var(--oc-shadow-teal) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-panel-button--teal.n-button:hover,
|
||||||
|
.oc-panel-button--teal.n-button:focus-visible {
|
||||||
|
border-color: rgb(94 234 212) !important;
|
||||||
|
background: rgb(234 250 245) !important;
|
||||||
|
color: rgb(19 78 74) !important;
|
||||||
|
box-shadow: 5px 5px 0 0 rgb(20 184 166 / 14%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-panel-button--danger.n-button {
|
||||||
|
--n-color: rgb(255 241 242) !important;
|
||||||
|
--n-color-hover: rgb(255 228 230) !important;
|
||||||
|
--n-color-pressed: rgb(255 228 230) !important;
|
||||||
|
--n-border: 1px solid rgb(254 205 211) !important;
|
||||||
|
--n-border-hover: 1px solid rgb(253 164 175) !important;
|
||||||
|
--n-border-pressed: 1px solid rgb(251 113 133) !important;
|
||||||
|
--n-text-color: rgb(190 24 93) !important;
|
||||||
|
--n-text-color-hover: rgb(159 18 57) !important;
|
||||||
|
--n-text-color-pressed: rgb(159 18 57) !important;
|
||||||
|
border-color: rgb(254 205 211) !important;
|
||||||
|
background: rgb(255 241 242) !important;
|
||||||
|
color: rgb(190 24 93) !important;
|
||||||
|
box-shadow: 4px 4px 0 0 rgb(244 63 94 / 10%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-panel-button--danger.n-button:hover,
|
||||||
|
.oc-panel-button--danger.n-button:focus-visible {
|
||||||
|
border-color: rgb(253 164 175) !important;
|
||||||
|
background: rgb(255 228 230) !important;
|
||||||
|
color: rgb(159 18 57) !important;
|
||||||
|
box-shadow: 5px 5px 0 0 rgb(244 63 94 / 14%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-panel-button--amber.n-button {
|
||||||
|
--n-color: rgb(255 251 235) !important;
|
||||||
|
--n-color-hover: rgb(254 243 199) !important;
|
||||||
|
--n-color-pressed: rgb(253 230 138) !important;
|
||||||
|
--n-border: 1px solid rgb(253 230 138) !important;
|
||||||
|
--n-border-hover: 1px solid rgb(252 211 77) !important;
|
||||||
|
--n-border-pressed: 1px solid rgb(245 158 11) !important;
|
||||||
|
--n-text-color: rgb(146 64 14) !important;
|
||||||
|
--n-text-color-hover: rgb(120 53 15) !important;
|
||||||
|
--n-text-color-pressed: rgb(120 53 15) !important;
|
||||||
|
border-color: rgb(253 230 138) !important;
|
||||||
|
background: rgb(255 251 235) !important;
|
||||||
|
color: rgb(146 64 14) !important;
|
||||||
|
box-shadow: 4px 4px 0 0 rgb(245 158 11 / 10%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-panel-button--amber.n-button:hover,
|
||||||
|
.oc-panel-button--amber.n-button:focus-visible {
|
||||||
|
border-color: rgb(252 211 77) !important;
|
||||||
|
background: rgb(254 243 199) !important;
|
||||||
|
color: rgb(120 53 15) !important;
|
||||||
|
box-shadow: 5px 5px 0 0 rgb(245 158 11 / 14%) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* OpenCloud design tokens.
|
||||||
|
*
|
||||||
|
* Components consume semantic tokens instead of raw color values. Palette
|
||||||
|
* values live here so the Vue and Naive UI layers share one visual contract.
|
||||||
|
*/
|
||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
|
||||||
|
--oc-font-sans: 'IBM Plex Sans', 'Noto Sans SC', 'PingFang SC', sans-serif;
|
||||||
|
--oc-font-mono: 'IBM Plex Mono', 'SFMono-Regular', monospace;
|
||||||
|
|
||||||
|
--oc-color-canvas: #eef4f8;
|
||||||
|
--oc-color-surface: #ffffff;
|
||||||
|
--oc-color-surface-muted: #f8fafc;
|
||||||
|
--oc-color-text: #0f172a;
|
||||||
|
--oc-color-text-secondary: #334155;
|
||||||
|
--oc-color-text-muted: #64748b;
|
||||||
|
--oc-color-border: #d9e3ee;
|
||||||
|
--oc-color-primary: #0f766e;
|
||||||
|
--oc-color-primary-hover: #115e59;
|
||||||
|
--oc-color-primary-pressed: #134e4a;
|
||||||
|
--oc-color-info: #0369a1;
|
||||||
|
--oc-color-success: #0f766e;
|
||||||
|
--oc-color-warning: #d97706;
|
||||||
|
--oc-color-danger: #dc2626;
|
||||||
|
--oc-color-selection: rgb(15 118 110 / 18%);
|
||||||
|
|
||||||
|
--oc-gradient-canvas:
|
||||||
|
radial-gradient(circle at top left, rgb(12 148 136 / 12%), transparent 28%),
|
||||||
|
linear-gradient(180deg, #f8fbfd 0%, #eef4f8 100%);
|
||||||
|
--oc-gradient-page: linear-gradient(180deg, #e0f2fe 0%, #f8fafc 100%);
|
||||||
|
--oc-gradient-page-soft: linear-gradient(180deg, #f0fdfa 0%, #f8fafc 100%);
|
||||||
|
--oc-gradient-grid:
|
||||||
|
linear-gradient(rgb(15 23 42 / 3%) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgb(15 23 42 / 3%) 1px, transparent 1px);
|
||||||
|
|
||||||
|
--oc-shadow-xs: 3px 3px 0 0 rgb(15 23 42 / 6%);
|
||||||
|
--oc-shadow-sm: 4px 4px 0 0 rgb(15 23 42 / 6%);
|
||||||
|
--oc-shadow-md: 6px 6px 0 0 rgb(15 23 42 / 5%);
|
||||||
|
--oc-shadow-lg: 8px 8px 0 0 rgb(15 23 42 / 8%);
|
||||||
|
--oc-shadow-xl: 12px 12px 0 0 rgb(15 23 42 / 8%);
|
||||||
|
--oc-shadow-sky: 4px 4px 0 0 rgb(14 165 233 / 14%);
|
||||||
|
--oc-shadow-teal: 4px 4px 0 0 rgb(20 184 166 / 10%);
|
||||||
|
|
||||||
|
--oc-duration-fast: 160ms;
|
||||||
|
--oc-ease-standard: ease;
|
||||||
|
}
|
||||||
@@ -528,7 +528,7 @@ onMounted(loadAdminData)
|
|||||||
<div class="mb-4 flex flex-col gap-3 border border-slate-200 bg-white p-4 md:flex-row md:items-center md:justify-between">
|
<div class="mb-4 flex flex-col gap-3 border border-slate-200 bg-white p-4 md:flex-row md:items-center md:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-bold text-slate-950">待审核队列</h2>
|
<h2 class="text-xl font-bold text-slate-950">待审核队列</h2>
|
||||||
<p class="mt-1 text-sm text-slate-500">共 {{ pendingImages.length }} 张待处理图片。</p>
|
<p class="oc-field-help">共 {{ pendingImages.length }} 张待处理图片。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<NButton secondary strong type="default" class="oc-panel-button oc-panel-button--neutral" @click="toggleAllPendingSelection">
|
<NButton secondary strong type="default" class="oc-panel-button oc-panel-button--neutral" @click="toggleAllPendingSelection">
|
||||||
@@ -568,7 +568,7 @@ onMounted(loadAdminData)
|
|||||||
<div class="flex items-start justify-between gap-3">
|
<div class="flex items-start justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-lg font-semibold text-slate-950">{{ item.cloudTypeName }}</p>
|
<p class="text-lg font-semibold text-slate-950">{{ item.cloudTypeName }}</p>
|
||||||
<p class="mt-1 text-sm text-slate-500">上传者:{{ item.username }}</p>
|
<p class="oc-field-help">上传者:{{ item.username }}</p>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -654,7 +654,7 @@ onMounted(loadAdminData)
|
|||||||
<div class="mb-4 flex flex-col gap-3 border border-slate-200 bg-white p-4 md:flex-row md:items-center md:justify-between">
|
<div class="mb-4 flex flex-col gap-3 border border-slate-200 bg-white p-4 md:flex-row md:items-center md:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-bold text-slate-950">图片管理</h2>
|
<h2 class="text-xl font-bold text-slate-950">图片管理</h2>
|
||||||
<p class="mt-1 text-sm text-slate-500">最近 {{ images.length }} 张图片,可调整审核状态、可见性或删除。</p>
|
<p class="oc-field-help">最近 {{ images.length }} 张图片,可调整审核状态、可见性或删除。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<select
|
<select
|
||||||
@@ -792,22 +792,22 @@ onMounted(loadAdminData)
|
|||||||
@close="selectedImage = null"
|
@close="selectedImage = null"
|
||||||
>
|
>
|
||||||
<div class="grid gap-4 sm:grid-cols-2">
|
<div class="grid gap-4 sm:grid-cols-2">
|
||||||
<div class="border border-slate-200 bg-slate-50 p-4">
|
<div class="oc-inset-panel">
|
||||||
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">上传时间</p>
|
<p class="oc-section-label">上传时间</p>
|
||||||
<p class="mt-2 text-sm font-medium text-slate-900">{{ formatDateTime(selectedImage.created_at) }}</p>
|
<p class="mt-2 text-sm font-medium text-slate-900">{{ formatDateTime(selectedImage.created_at) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="border border-slate-200 bg-slate-50 p-4">
|
<div class="oc-inset-panel">
|
||||||
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">拍摄时间</p>
|
<p class="oc-section-label">拍摄时间</p>
|
||||||
<p class="mt-2 text-sm font-medium text-slate-900">{{ formatDateTime(selectedImage.captured_at) }}</p>
|
<p class="mt-2 text-sm font-medium text-slate-900">{{ formatDateTime(selectedImage.captured_at) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="border border-slate-200 bg-slate-50 p-4">
|
<div class="oc-inset-panel">
|
||||||
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">状态</p>
|
<p class="oc-section-label">状态</p>
|
||||||
<p class="mt-2 text-sm font-medium text-slate-900">
|
<p class="mt-2 text-sm font-medium text-slate-900">
|
||||||
{{ statusMeta[selectedImage.status].label }} / {{ selectedImage.is_hidden ? '隐藏' : '公开' }}
|
{{ statusMeta[selectedImage.status].label }} / {{ selectedImage.is_hidden ? '隐藏' : '公开' }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="border border-slate-200 bg-slate-50 p-4">
|
<div class="oc-inset-panel">
|
||||||
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">模糊化经纬度</p>
|
<p class="oc-section-label">模糊化经纬度</p>
|
||||||
<p class="mt-2 text-sm font-medium text-slate-900">
|
<p class="mt-2 text-sm font-medium text-slate-900">
|
||||||
纬度 {{ formatCoordinate(selectedImage.latitude) }} / 经度 {{ formatCoordinate(selectedImage.longitude) }}
|
纬度 {{ formatCoordinate(selectedImage.latitude) }} / 经度 {{ formatCoordinate(selectedImage.longitude) }}
|
||||||
</p>
|
</p>
|
||||||
@@ -815,7 +815,7 @@ onMounted(loadAdminData)
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-5 border border-slate-200 bg-white p-5">
|
<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">
|
<p class="mt-3 text-sm leading-7 text-slate-700">
|
||||||
{{ selectedImage.description || '上传者没有留下额外说明。' }}
|
{{ selectedImage.description || '上传者没有留下额外说明。' }}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -85,14 +85,14 @@ onUnmounted(() => {
|
|||||||
</p>
|
</p>
|
||||||
<div class="mt-8 grid gap-4 sm:grid-cols-2">
|
<div class="mt-8 grid gap-4 sm:grid-cols-2">
|
||||||
<div class="border border-slate-200 bg-white px-4 py-3">
|
<div class="border border-slate-200 bg-white px-4 py-3">
|
||||||
<div class="text-xs uppercase tracking-[0.2em] text-slate-500">Step 1</div>
|
<div class="oc-stat-label">Step 1</div>
|
||||||
<div class="mt-2 text-lg font-bold text-slate-900">发送邮件</div>
|
<div class="mt-2 text-lg font-bold text-slate-900">发送邮件</div>
|
||||||
<div class="mt-1 text-sm text-slate-500">使用注册邮箱请求重置链接</div>
|
<div class="oc-field-help">使用注册邮箱请求重置链接</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="border border-slate-200 bg-white px-4 py-3">
|
<div class="border border-slate-200 bg-white px-4 py-3">
|
||||||
<div class="text-xs uppercase tracking-[0.2em] text-slate-500">Step 2</div>
|
<div class="oc-stat-label">Step 2</div>
|
||||||
<div class="mt-2 text-lg font-bold text-slate-900">设置新密码</div>
|
<div class="mt-2 text-lg font-bold text-slate-900">设置新密码</div>
|
||||||
<div class="mt-1 text-sm text-slate-500">在专用页面完成新密码更新</div>
|
<div class="oc-field-help">在专用页面完成新密码更新</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -56,14 +56,14 @@ async function handleLogin() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="mt-8 flex gap-4">
|
<div class="mt-8 flex gap-4">
|
||||||
<div class="border border-slate-200 bg-white px-4 py-3">
|
<div class="border border-slate-200 bg-white px-4 py-3">
|
||||||
<div class="text-xs uppercase tracking-[0.2em] text-slate-500">Collection</div>
|
<div class="oc-stat-label">Collection</div>
|
||||||
<div class="mt-2 text-2xl font-bold text-slate-900">10</div>
|
<div class="oc-stat-value">10</div>
|
||||||
<div class="mt-1 text-sm text-slate-500">基础云属待收集</div>
|
<div class="oc-field-help">基础云属待收集</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="border border-slate-200 bg-white px-4 py-3">
|
<div class="border border-slate-200 bg-white px-4 py-3">
|
||||||
<div class="text-xs uppercase tracking-[0.2em] text-slate-500">Mode</div>
|
<div class="oc-stat-label">Mode</div>
|
||||||
<div class="mt-2 text-2xl font-bold text-slate-900">Atlas</div>
|
<div class="oc-stat-value">Atlas</div>
|
||||||
<div class="mt-1 text-sm text-slate-500">地图、图鉴、画廊一体</div>
|
<div class="oc-field-help">地图、图鉴、画廊一体</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -61,14 +61,14 @@ async function handleRegister() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="mt-8 flex gap-4">
|
<div class="mt-8 flex gap-4">
|
||||||
<div class="border border-slate-200 bg-white px-4 py-3">
|
<div class="border border-slate-200 bg-white px-4 py-3">
|
||||||
<div class="text-xs uppercase tracking-[0.2em] text-slate-500">Review</div>
|
<div class="oc-stat-label">Review</div>
|
||||||
<div class="mt-2 text-2xl font-bold text-slate-900">Queue</div>
|
<div class="oc-stat-value">Queue</div>
|
||||||
<div class="mt-1 text-sm text-slate-500">上传后进入审核队列</div>
|
<div class="oc-field-help">上传后进入审核队列</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="border border-slate-200 bg-white px-4 py-3">
|
<div class="border border-slate-200 bg-white px-4 py-3">
|
||||||
<div class="text-xs uppercase tracking-[0.2em] text-slate-500">Profile</div>
|
<div class="oc-stat-label">Profile</div>
|
||||||
<div class="mt-2 text-2xl font-bold text-slate-900">Journal</div>
|
<div class="oc-stat-value">Journal</div>
|
||||||
<div class="mt-1 text-sm text-slate-500">自动生成个人天空日志</div>
|
<div class="oc-field-help">自动生成个人天空日志</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -200,11 +200,11 @@ watch(() => route.params.id, () => {
|
|||||||
<NCard class="mt-8 border border-slate-200">
|
<NCard class="mt-8 border border-slate-200">
|
||||||
<p class="text-sm text-slate-500">你的收集状态</p>
|
<p class="text-sm text-slate-500">你的收集状态</p>
|
||||||
<template v-if="isUnlocked && collectionEntry">
|
<template v-if="isUnlocked && collectionEntry">
|
||||||
<p class="mt-2 text-2xl font-bold text-slate-900">已解锁</p>
|
<p class="oc-stat-value">已解锁</p>
|
||||||
<p class="mt-2 text-sm text-slate-600">首次解锁于 {{ formatDate(collectionEntry.unlocked_at) }}</p>
|
<p class="mt-2 text-sm text-slate-600">首次解锁于 {{ formatDate(collectionEntry.unlocked_at) }}</p>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<p class="mt-2 text-2xl font-bold text-slate-900">尚未解锁</p>
|
<p class="oc-stat-value">尚未解锁</p>
|
||||||
<p class="mt-2 text-sm text-slate-600">拍到并上传这种云朵后,这枚徽章就会被点亮。</p>
|
<p class="mt-2 text-sm text-slate-600">拍到并上传这种云朵后,这枚徽章就会被点亮。</p>
|
||||||
</template>
|
</template>
|
||||||
</NCard>
|
</NCard>
|
||||||
@@ -215,11 +215,11 @@ watch(() => route.params.id, () => {
|
|||||||
<section class="mt-6 grid gap-4 md:grid-cols-3">
|
<section class="mt-6 grid gap-4 md:grid-cols-3">
|
||||||
<NCard class="border border-slate-200 shadow-sm">
|
<NCard class="border border-slate-200 shadow-sm">
|
||||||
<p class="text-sm text-slate-500">稀有度</p>
|
<p class="text-sm text-slate-500">稀有度</p>
|
||||||
<p class="mt-2 text-2xl font-bold text-slate-900">{{ rarityMeta[cloudType.rarity].label }}</p>
|
<p class="oc-stat-value">{{ rarityMeta[cloudType.rarity].label }}</p>
|
||||||
</NCard>
|
</NCard>
|
||||||
<NCard class="border border-slate-200 shadow-sm">
|
<NCard class="border border-slate-200 shadow-sm">
|
||||||
<p class="text-sm text-slate-500">公开云图</p>
|
<p class="text-sm text-slate-500">公开云图</p>
|
||||||
<p class="mt-2 text-2xl font-bold text-slate-900">{{ publicCount }}</p>
|
<p class="oc-stat-value">{{ publicCount }}</p>
|
||||||
</NCard>
|
</NCard>
|
||||||
<NCard class="border border-slate-200 shadow-sm">
|
<NCard class="border border-slate-200 shadow-sm">
|
||||||
<p class="text-sm text-slate-500">首次解锁</p>
|
<p class="text-sm text-slate-500">首次解锁</p>
|
||||||
@@ -233,7 +233,7 @@ watch(() => route.params.id, () => {
|
|||||||
<div class="mb-4 flex items-end justify-between gap-4">
|
<div class="mb-4 flex items-end justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-2xl font-bold text-slate-900">该类型云图画廊</h2>
|
<h2 class="text-2xl font-bold text-slate-900">该类型云图画廊</h2>
|
||||||
<p class="mt-1 text-sm text-slate-500">来自社区公开可见的已通过云图。</p>
|
<p class="oc-field-help">来自社区公开可见的已通过云图。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -281,20 +281,20 @@ watch(() => route.params.id, () => {
|
|||||||
@close="selectedGalleryItem = null"
|
@close="selectedGalleryItem = null"
|
||||||
>
|
>
|
||||||
<div class="grid gap-4 sm:grid-cols-2">
|
<div class="grid gap-4 sm:grid-cols-2">
|
||||||
<div class="border border-slate-200 bg-slate-50 p-4">
|
<div class="oc-inset-panel">
|
||||||
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">上传时间</p>
|
<p class="oc-section-label">上传时间</p>
|
||||||
<p class="mt-2 text-sm font-medium text-slate-900">{{ formatDateTime(selectedGalleryItem.created_at) }}</p>
|
<p class="mt-2 text-sm font-medium text-slate-900">{{ formatDateTime(selectedGalleryItem.created_at) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="border border-slate-200 bg-slate-50 p-4">
|
<div class="oc-inset-panel">
|
||||||
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">拍摄时间</p>
|
<p class="oc-section-label">拍摄时间</p>
|
||||||
<p class="mt-2 text-sm font-medium text-slate-900">{{ formatDateTime(selectedGalleryItem.captured_at) }}</p>
|
<p class="mt-2 text-sm font-medium text-slate-900">{{ formatDateTime(selectedGalleryItem.captured_at) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="border border-slate-200 bg-slate-50 p-4">
|
<div class="oc-inset-panel">
|
||||||
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">位置</p>
|
<p class="oc-section-label">位置</p>
|
||||||
<p class="mt-2 text-sm font-medium text-slate-900">{{ selectedGalleryItem.location_name || '未填写位置名称' }}</p>
|
<p class="mt-2 text-sm font-medium text-slate-900">{{ selectedGalleryItem.location_name || '未填写位置名称' }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="border border-slate-200 bg-slate-50 p-4">
|
<div class="oc-inset-panel">
|
||||||
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">模糊化经纬度</p>
|
<p class="oc-section-label">模糊化经纬度</p>
|
||||||
<p class="mt-2 text-sm font-medium text-slate-900">
|
<p class="mt-2 text-sm font-medium text-slate-900">
|
||||||
纬度 {{ formatCoordinate(selectedGalleryItem.latitude) }} / 经度 {{ formatCoordinate(selectedGalleryItem.longitude) }}
|
纬度 {{ formatCoordinate(selectedGalleryItem.latitude) }} / 经度 {{ formatCoordinate(selectedGalleryItem.longitude) }}
|
||||||
</p>
|
</p>
|
||||||
@@ -302,7 +302,7 @@ watch(() => route.params.id, () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-5 border border-slate-200 bg-white p-5">
|
<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">
|
<p class="mt-3 text-sm leading-7 text-slate-700">
|
||||||
{{ selectedGalleryItem.description || '上传者没有留下额外说明。' }}
|
{{ selectedGalleryItem.description || '上传者没有留下额外说明。' }}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -62,13 +62,13 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="bg-[linear-gradient(180deg,#e0f2fe_0%,#f8fafc_100%)] border-b border-sky-100">
|
<div class="oc-page-hero">
|
||||||
<div class="max-w-6xl mx-auto px-4 py-10">
|
<div class="oc-container oc-container--content oc-page-hero__inner">
|
||||||
<div class="grid gap-6 lg:grid-cols-[1.4fr_0.9fr] lg:items-start">
|
<div class="grid gap-6 lg:grid-cols-[1.4fr_0.9fr] lg:items-start">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-medium tracking-[0.24em] text-sky-700 uppercase">Cloud Encyclopedia</p>
|
<p class="oc-page-eyebrow">Cloud Encyclopedia</p>
|
||||||
<h1 class="mt-3 text-4xl font-bold text-slate-900">云朵图鉴</h1>
|
<h1 class="oc-page-title">云朵图鉴</h1>
|
||||||
<p class="mt-4 max-w-2xl text-slate-600 leading-7">
|
<p class="oc-page-description">
|
||||||
收集 10 种基础云属。每拍到一种新的云型,图鉴里就会点亮一枚徽章,并记录你第一次遇见它的时间。
|
收集 10 种基础云属。每拍到一种新的云型,图鉴里就会点亮一枚徽章,并记录你第一次遇见它的时间。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,7 +83,7 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="max-w-6xl mx-auto px-4 py-8">
|
<div class="oc-container oc-container--content oc-page-content">
|
||||||
<NAlert
|
<NAlert
|
||||||
v-if="encyclopediaStore.collectionError && authStore.isLoggedIn"
|
v-if="encyclopediaStore.collectionError && authStore.isLoggedIn"
|
||||||
class="mb-6"
|
class="mb-6"
|
||||||
|
|||||||
@@ -341,17 +341,17 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<section class="border-b border-sky-100 bg-[linear-gradient(180deg,#e0f2fe_0%,#f8fafc_100%)]">
|
<section class="oc-page-hero">
|
||||||
<div class="max-w-7xl mx-auto px-4 py-10">
|
<div class="oc-container oc-container--wide oc-page-hero__inner">
|
||||||
<p class="text-sm font-medium uppercase tracking-[0.24em] text-sky-700">Community Gallery</p>
|
<p class="oc-page-eyebrow">Community Gallery</p>
|
||||||
<h1 class="mt-3 text-4xl font-bold text-slate-900">云图画廊</h1>
|
<h1 class="oc-page-title">云图画廊</h1>
|
||||||
<p class="mt-4 max-w-2xl text-sm leading-7 text-slate-600">
|
<p class="oc-page-description">
|
||||||
按上传时间倒序浏览社区云图。瀑布流会尽量保留图片原始比例,悬停即可快速查看基本信息,点开可看大图和详细记录。
|
按上传时间倒序浏览社区云图。瀑布流会尽量保留图片原始比例,悬停即可快速查看基本信息,点开可看大图和详细记录。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="max-w-7xl mx-auto px-4 py-8">
|
<div class="oc-container oc-container--wide oc-page-content">
|
||||||
<section>
|
<section>
|
||||||
<div class="mb-4 border border-slate-200 bg-white p-3 shadow-[6px_6px_0_0_rgba(15,23,42,0.05)]">
|
<div class="mb-4 border border-slate-200 bg-white p-3 shadow-[6px_6px_0_0_rgba(15,23,42,0.05)]">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
@@ -534,20 +534,20 @@ onUnmounted(() => {
|
|||||||
@close="closeDetail"
|
@close="closeDetail"
|
||||||
>
|
>
|
||||||
<div class="grid gap-4 sm:grid-cols-2">
|
<div class="grid gap-4 sm:grid-cols-2">
|
||||||
<div class="border border-slate-200 bg-slate-50 p-4">
|
<div class="oc-inset-panel">
|
||||||
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">上传时间</p>
|
<p class="oc-section-label">上传时间</p>
|
||||||
<p class="mt-2 text-sm font-medium text-slate-900">{{ formatUploadTime(selectedCloud) }}</p>
|
<p class="mt-2 text-sm font-medium text-slate-900">{{ formatUploadTime(selectedCloud) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="border border-slate-200 bg-slate-50 p-4">
|
<div class="oc-inset-panel">
|
||||||
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">拍摄时间</p>
|
<p class="oc-section-label">拍摄时间</p>
|
||||||
<p class="mt-2 text-sm font-medium text-slate-900">{{ formatCapturedTime(selectedCloud) }}</p>
|
<p class="mt-2 text-sm font-medium text-slate-900">{{ formatCapturedTime(selectedCloud) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="border border-slate-200 bg-slate-50 p-4">
|
<div class="oc-inset-panel">
|
||||||
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">位置</p>
|
<p class="oc-section-label">位置</p>
|
||||||
<p class="mt-2 text-sm font-medium text-slate-900">{{ selectedCloud.location_name || '未填写位置名称' }}</p>
|
<p class="mt-2 text-sm font-medium text-slate-900">{{ selectedCloud.location_name || '未填写位置名称' }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="border border-slate-200 bg-slate-50 p-4">
|
<div class="oc-inset-panel">
|
||||||
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">模糊化经纬度</p>
|
<p class="oc-section-label">模糊化经纬度</p>
|
||||||
<p class="mt-2 text-sm font-medium text-slate-900">
|
<p class="mt-2 text-sm font-medium text-slate-900">
|
||||||
纬度 {{ formatCoordinate(selectedCloud.latitude) }} / 经度 {{ formatCoordinate(selectedCloud.longitude) }}
|
纬度 {{ formatCoordinate(selectedCloud.latitude) }} / 经度 {{ formatCoordinate(selectedCloud.longitude) }}
|
||||||
</p>
|
</p>
|
||||||
@@ -555,7 +555,7 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-5 border border-slate-200 bg-white p-5">
|
<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">
|
<p class="mt-3 text-sm leading-7 text-slate-700">
|
||||||
{{ selectedCloud.description || '上传者没有留下额外说明。' }}
|
{{ selectedCloud.description || '上传者没有留下额外说明。' }}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
+12
-12
@@ -472,7 +472,7 @@ onUnmounted(() => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="w-10 h-10 rounded-lg shadow-md flex items-center justify-center"
|
class="w-10 h-10 rounded-lg shadow-md flex items-center justify-center"
|
||||||
:class="authStore.loading || !authStore.isLoggedIn ? 'cursor-not-allowed bg-slate-100 text-slate-300 shadow-none' : 'bg-white hover:bg-gray-50'"
|
:class="authStore.loading || !authStore.isLoggedIn ? 'cursor-not-allowed bg-slate-100 text-slate-300 shadow-none' : 'bg-white hover:bg-slate-50'"
|
||||||
:title="authStore.isLoggedIn ? '快速上传图片' : '请先登录后上传图片'"
|
:title="authStore.isLoggedIn ? '快速上传图片' : '请先登录后上传图片'"
|
||||||
aria-label="快速上传图片"
|
aria-label="快速上传图片"
|
||||||
:disabled="authStore.loading || !authStore.isLoggedIn"
|
:disabled="authStore.loading || !authStore.isLoggedIn"
|
||||||
@@ -482,14 +482,14 @@ onUnmounted(() => {
|
|||||||
<CloudUpload />
|
<CloudUpload />
|
||||||
</NIcon>
|
</NIcon>
|
||||||
</button>
|
</button>
|
||||||
<button @click="refresh" class="w-10 h-10 bg-white rounded-lg shadow-md flex items-center justify-center hover:bg-gray-50" title="刷新">
|
<button @click="refresh" class="oc-map-button" title="刷新">
|
||||||
<span class="text-lg">
|
<span class="text-lg">
|
||||||
<NIcon>
|
<NIcon>
|
||||||
<Refresh/>
|
<Refresh/>
|
||||||
</NIcon>
|
</NIcon>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button @click="toggleSat" class="w-10 h-10 bg-white rounded-lg shadow-md flex items-center justify-center hover:bg-gray-50" :title="satelliteOn ? '切换普通视图' : '切换卫星视图'">
|
<button @click="toggleSat" class="oc-map-button" :title="satelliteOn ? '切换普通视图' : '切换卫星视图'">
|
||||||
<NIcon size="20" style="display: inline-flex; vertical-align: middle;">
|
<NIcon size="20" style="display: inline-flex; vertical-align: middle;">
|
||||||
<Map v-if="satelliteOn" />
|
<Map v-if="satelliteOn" />
|
||||||
<Satellite v-else />
|
<Satellite v-else />
|
||||||
@@ -497,7 +497,7 @@ onUnmounted(() => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="w-10 h-10 bg-white rounded-lg shadow-md flex items-center justify-center hover:bg-gray-50"
|
class="oc-map-button"
|
||||||
:title="timelineControlsOpen ? '收起时间控件' : '展开时间控件'"
|
:title="timelineControlsOpen ? '收起时间控件' : '展开时间控件'"
|
||||||
:aria-label="timelineControlsOpen ? '收起时间控件' : '展开时间控件'"
|
:aria-label="timelineControlsOpen ? '收起时间控件' : '展开时间控件'"
|
||||||
@click="toggleTimelineControls"
|
@click="toggleTimelineControls"
|
||||||
@@ -549,7 +549,7 @@ onUnmounted(() => {
|
|||||||
<div class="relative">
|
<div class="relative">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="w-10 h-10 bg-white rounded-lg shadow-md flex items-center justify-center hover:bg-gray-50"
|
class="oc-map-button"
|
||||||
title="按日期或月份查看"
|
title="按日期或月份查看"
|
||||||
aria-label="按日期或月份查看"
|
aria-label="按日期或月份查看"
|
||||||
@click="archivePanelOpen = !archivePanelOpen"
|
@click="archivePanelOpen = !archivePanelOpen"
|
||||||
@@ -592,13 +592,13 @@ onUnmounted(() => {
|
|||||||
v-if="archiveKind === 'day'"
|
v-if="archiveKind === 'day'"
|
||||||
v-model="archiveDay"
|
v-model="archiveDay"
|
||||||
type="date"
|
type="date"
|
||||||
class="mt-3 w-full border border-slate-300 px-3 py-2 text-sm text-slate-800 outline-none focus:border-sky-500 focus:ring-2 focus:ring-sky-100"
|
class="oc-field-control mt-3"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
v-else
|
v-else
|
||||||
v-model="archiveMonth"
|
v-model="archiveMonth"
|
||||||
type="month"
|
type="month"
|
||||||
class="mt-3 w-full border border-slate-300 px-3 py-2 text-sm text-slate-800 outline-none focus:border-sky-500 focus:ring-2 focus:ring-sky-100"
|
class="oc-field-control mt-3"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -629,19 +629,19 @@ onUnmounted(() => {
|
|||||||
>
|
>
|
||||||
<div class="grid gap-4 sm:grid-cols-2">
|
<div class="grid gap-4 sm:grid-cols-2">
|
||||||
<div class="rounded-2xl bg-slate-50 p-4">
|
<div class="rounded-2xl bg-slate-50 p-4">
|
||||||
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">上传时间</p>
|
<p class="oc-section-label">上传时间</p>
|
||||||
<p class="mt-2 text-sm font-medium text-slate-900">{{ formatDateTime(previewCloud.createdAt) }}</p>
|
<p class="mt-2 text-sm font-medium text-slate-900">{{ formatDateTime(previewCloud.createdAt) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-2xl bg-slate-50 p-4">
|
<div class="rounded-2xl bg-slate-50 p-4">
|
||||||
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">拍摄时间</p>
|
<p class="oc-section-label">拍摄时间</p>
|
||||||
<p class="mt-2 text-sm font-medium text-slate-900">{{ formatDateTime(previewCloud.capturedAt) }}</p>
|
<p class="mt-2 text-sm font-medium text-slate-900">{{ formatDateTime(previewCloud.capturedAt) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-2xl bg-slate-50 p-4">
|
<div class="rounded-2xl bg-slate-50 p-4">
|
||||||
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">位置</p>
|
<p class="oc-section-label">位置</p>
|
||||||
<p class="mt-2 text-sm font-medium text-slate-900">{{ previewCloud.locationName || '未填写位置名称' }}</p>
|
<p class="mt-2 text-sm font-medium text-slate-900">{{ previewCloud.locationName || '未填写位置名称' }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-2xl bg-slate-50 p-4">
|
<div class="rounded-2xl bg-slate-50 p-4">
|
||||||
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">模糊化经纬度</p>
|
<p class="oc-section-label">模糊化经纬度</p>
|
||||||
<p class="mt-2 text-sm font-medium text-slate-900">
|
<p class="mt-2 text-sm font-medium text-slate-900">
|
||||||
纬度 {{ formatCoordinate(previewCloud.latitude) }} / 经度 {{ formatCoordinate(previewCloud.longitude) }}
|
纬度 {{ formatCoordinate(previewCloud.latitude) }} / 经度 {{ formatCoordinate(previewCloud.longitude) }}
|
||||||
</p>
|
</p>
|
||||||
@@ -649,7 +649,7 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-5 rounded-[28px] border border-slate-200 bg-white p-5">
|
<div class="mt-5 rounded-[28px] 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">
|
<p class="mt-3 text-sm leading-7 text-slate-700">
|
||||||
{{ previewCloud.description || '上传者没有留下额外说明。' }}
|
{{ previewCloud.description || '上传者没有留下额外说明。' }}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -86,9 +86,9 @@ async function savePassword() {
|
|||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
<p class="text-sm font-semibold uppercase tracking-[0.24em] text-sky-700">Account Settings</p>
|
<p class="oc-page-eyebrow">Account Settings</p>
|
||||||
<h1 class="mt-3 text-4xl font-bold text-slate-900">个人资料设置</h1>
|
<h1 class="oc-page-title">个人资料设置</h1>
|
||||||
<p class="mt-4 max-w-2xl text-sm leading-7 text-slate-600">
|
<p class="oc-page-description">
|
||||||
修改公开昵称和登录密码。昵称会显示在画廊、图鉴和地图中,且不能与其他用户重复。
|
修改公开昵称和登录密码。昵称会显示在画廊、图鉴和地图中,且不能与其他用户重复。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -473,8 +473,8 @@ watch(selectedUploadDate, async newValue => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="border-b border-sky-100 bg-[linear-gradient(180deg,#e0f2fe_0%,#f8fafc_100%)]">
|
<div class="oc-page-hero">
|
||||||
<div class="max-w-6xl mx-auto px-4 py-10">
|
<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]">
|
<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">
|
<NCard class="oc-panel-card border border-slate-200 shadow-sm" :bordered="false">
|
||||||
<NSkeleton class="h-6 w-1/3" />
|
<NSkeleton class="h-6 w-1/3" />
|
||||||
@@ -505,8 +505,8 @@ watch(selectedUploadDate, async newValue => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<p class="text-sm font-medium uppercase tracking-[0.24em] text-sky-700">Sky Journal</p>
|
<p class="oc-page-eyebrow">Sky Journal</p>
|
||||||
<h1 class="mt-3 text-4xl font-bold text-slate-900">
|
<h1 class="oc-page-title">
|
||||||
{{ pageTitle }}
|
{{ pageTitle }}
|
||||||
</h1>
|
</h1>
|
||||||
<div class="mt-3 flex flex-wrap items-center gap-3">
|
<div class="mt-3 flex flex-wrap items-center gap-3">
|
||||||
@@ -554,7 +554,7 @@ watch(selectedUploadDate, async newValue => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="max-w-6xl mx-auto px-4 py-8">
|
<div class="oc-container oc-container--content oc-page-content">
|
||||||
<NAlert
|
<NAlert
|
||||||
v-if="errorMsg"
|
v-if="errorMsg"
|
||||||
type="error"
|
type="error"
|
||||||
@@ -592,7 +592,7 @@ watch(selectedUploadDate, async newValue => {
|
|||||||
|
|
||||||
<NCard class="oc-panel-card border border-slate-200 shadow-sm" :bordered="false">
|
<NCard class="oc-panel-card border border-slate-200 shadow-sm" :bordered="false">
|
||||||
<p class="text-sm text-slate-500">最常拍云型</p>
|
<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">
|
<p class="mt-2 text-sm text-slate-500" v-if="mostCommonCloudType.count">
|
||||||
共 {{ mostCommonCloudType.count }} 张
|
共 {{ mostCommonCloudType.count }} 张
|
||||||
</p>
|
</p>
|
||||||
@@ -605,7 +605,7 @@ watch(selectedUploadDate, async newValue => {
|
|||||||
|
|
||||||
<NCard class="oc-panel-card border border-slate-200 shadow-sm" :bordered="false">
|
<NCard class="oc-panel-card border border-slate-200 shadow-sm" :bordered="false">
|
||||||
<p class="text-sm text-slate-500">{{ isOwnProfile ? '已通过 / 待审核' : '公开通过数' }}</p>
|
<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-if="isOwnProfile">{{ approvedShots }} / {{ pendingShots }}</template>
|
||||||
<template v-else>{{ approvedShots }}</template>
|
<template v-else>{{ approvedShots }}</template>
|
||||||
</p>
|
</p>
|
||||||
@@ -630,7 +630,7 @@ watch(selectedUploadDate, async newValue => {
|
|||||||
<div class="mb-5 flex items-end justify-between gap-4">
|
<div class="mb-5 flex items-end justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-2xl font-bold text-slate-900">天空日志</h2>
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -794,26 +794,26 @@ watch(selectedUploadDate, async newValue => {
|
|||||||
@close="selectedCloud = null"
|
@close="selectedCloud = null"
|
||||||
>
|
>
|
||||||
<div class="grid gap-4 sm:grid-cols-2">
|
<div class="grid gap-4 sm:grid-cols-2">
|
||||||
<div class="border border-slate-200 bg-slate-50 p-4">
|
<div class="oc-inset-panel">
|
||||||
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">上传时间</p>
|
<p class="oc-section-label">上传时间</p>
|
||||||
<p class="mt-2 text-sm font-medium text-slate-900">{{ formatDateTime(selectedCloud.created_at) }}</p>
|
<p class="mt-2 text-sm font-medium text-slate-900">{{ formatDateTime(selectedCloud.created_at) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="border border-slate-200 bg-slate-50 p-4">
|
<div class="oc-inset-panel">
|
||||||
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">拍摄时间</p>
|
<p class="oc-section-label">拍摄时间</p>
|
||||||
<p class="mt-2 text-sm font-medium text-slate-900">{{ formatDateTime(selectedCloud.captured_at) }}</p>
|
<p class="mt-2 text-sm font-medium text-slate-900">{{ formatDateTime(selectedCloud.captured_at) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="border border-slate-200 bg-slate-50 p-4">
|
<div class="oc-inset-panel">
|
||||||
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">位置</p>
|
<p class="oc-section-label">位置</p>
|
||||||
<p class="mt-2 text-sm font-medium text-slate-900">{{ selectedCloud.location_name || '未填写位置名称' }}</p>
|
<p class="mt-2 text-sm font-medium text-slate-900">{{ selectedCloud.location_name || '未填写位置名称' }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="border border-slate-200 bg-slate-50 p-4">
|
<div class="oc-inset-panel">
|
||||||
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">模糊化经纬度</p>
|
<p class="oc-section-label">模糊化经纬度</p>
|
||||||
<p class="mt-2 text-sm font-medium text-slate-900">
|
<p class="mt-2 text-sm font-medium text-slate-900">
|
||||||
纬度 {{ formatCoordinate(selectedCloud.latitude) }} / 经度 {{ formatCoordinate(selectedCloud.longitude) }}
|
纬度 {{ formatCoordinate(selectedCloud.latitude) }} / 经度 {{ formatCoordinate(selectedCloud.longitude) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="border border-slate-200 bg-slate-50 p-4">
|
<div class="oc-inset-panel">
|
||||||
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">{{ isOwnProfile ? '审核状态' : '可见性' }}</p>
|
<p class="oc-section-label">{{ isOwnProfile ? '审核状态' : '可见性' }}</p>
|
||||||
<p class="mt-2 text-sm font-medium text-slate-900">
|
<p class="mt-2 text-sm font-medium text-slate-900">
|
||||||
<template v-if="isOwnProfile">
|
<template v-if="isOwnProfile">
|
||||||
{{ statusMeta[selectedCloud.status].label }} / {{ selectedCloud.is_hidden ? '私密' : '公开' }}
|
{{ statusMeta[selectedCloud.status].label }} / {{ selectedCloud.is_hidden ? '私密' : '公开' }}
|
||||||
@@ -824,7 +824,7 @@ watch(selectedUploadDate, async newValue => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-5 border border-slate-200 bg-white p-5">
|
<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">
|
<p class="mt-3 text-sm leading-7 text-slate-700">
|
||||||
{{ selectedCloud.description || '上传者没有留下额外说明。' }}
|
{{ selectedCloud.description || '上传者没有留下额外说明。' }}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -28,12 +28,12 @@ const description = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex min-h-[calc(100vh-4rem)] items-center justify-center px-4 py-10">
|
<div class="oc-system-page">
|
||||||
<div class="w-full max-w-2xl border border-slate-200 bg-white px-8 py-14 text-center shadow-sm">
|
<div class="oc-system-card">
|
||||||
<div class="text-7xl leading-none">🔐</div>
|
<div class="text-7xl leading-none">🔐</div>
|
||||||
<p class="mt-6 text-sm font-medium uppercase tracking-[0.24em] text-sky-600">401 Login Required</p>
|
<p class="mt-6 text-sm font-medium uppercase tracking-[0.24em] text-sky-600">401 Login Required</p>
|
||||||
<h1 class="mt-3 text-4xl font-bold text-slate-900">{{ title }}</h1>
|
<h1 class="oc-page-title">{{ title }}</h1>
|
||||||
<p class="mx-auto mt-4 max-w-xl text-sm leading-7 text-slate-600">
|
<p class="oc-page-description mx-auto max-w-xl">
|
||||||
{{ description }}
|
{{ description }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import { RouterLink } from 'vue-router'
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex min-h-[calc(100vh-4rem)] items-center justify-center px-4 py-10">
|
<div class="oc-system-page">
|
||||||
<div class="w-full max-w-2xl border border-slate-200 bg-white px-8 py-14 text-center shadow-sm">
|
<div class="oc-system-card">
|
||||||
<div class="text-7xl leading-none">🖐️</div>
|
<div class="text-7xl leading-none">🖐️</div>
|
||||||
<p class="mt-6 text-sm font-medium uppercase tracking-[0.24em] text-rose-600">403 Forbidden</p>
|
<p class="mt-6 text-sm font-medium uppercase tracking-[0.24em] text-rose-600">403 Forbidden</p>
|
||||||
<h1 class="mt-3 text-4xl font-bold text-slate-900">这里不让进</h1>
|
<h1 class="oc-page-title">这里不让进</h1>
|
||||||
<p class="mx-auto mt-4 max-w-xl text-sm leading-7 text-slate-600">
|
<p class="oc-page-description mx-auto max-w-xl">
|
||||||
你当前没有访问这个页面的权限。通常是因为这个区域只对管理员开放。
|
你当前没有访问这个页面的权限。通常是因为这个区域只对管理员开放。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import { RouterLink } from 'vue-router'
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex min-h-[calc(100vh-4rem)] items-center justify-center px-4 py-10">
|
<div class="oc-system-page">
|
||||||
<div class="w-full max-w-2xl border border-slate-200 bg-white px-8 py-14 text-center shadow-sm">
|
<div class="oc-system-card">
|
||||||
<div class="text-7xl leading-none">🤔</div>
|
<div class="text-7xl leading-none">🤔</div>
|
||||||
<p class="mt-6 text-sm font-medium uppercase tracking-[0.24em] text-amber-600">404 Not Found</p>
|
<p class="mt-6 text-sm font-medium uppercase tracking-[0.24em] text-amber-600">404 Not Found</p>
|
||||||
<h1 class="mt-3 text-4xl font-bold text-slate-900">我也没想明白</h1>
|
<h1 class="oc-page-title">我也没想明白</h1>
|
||||||
<p class="mx-auto mt-4 max-w-xl text-sm leading-7 text-slate-600">
|
<p class="oc-page-description mx-auto max-w-xl">
|
||||||
这个页面不存在,或者它已经被移动到了别的地方。换个入口再试一次。
|
这个页面不存在,或者它已经被移动到了别的地方。换个入口再试一次。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@@ -203,17 +203,17 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<section class="border-b border-sky-100 bg-[linear-gradient(180deg,#e0f2fe_0%,#f8fafc_100%)]">
|
<section class="oc-page-hero">
|
||||||
<div class="max-w-6xl mx-auto px-4 py-10">
|
<div class="oc-container oc-container--content oc-page-hero__inner">
|
||||||
<p class="text-sm font-medium uppercase tracking-[0.24em] text-sky-700">Cloud Upload</p>
|
<p class="oc-page-eyebrow">Cloud Upload</p>
|
||||||
<h1 class="mt-3 text-4xl font-bold text-slate-900">上传云图</h1>
|
<h1 class="oc-page-title">上传云图</h1>
|
||||||
<p class="mt-4 max-w-2xl text-sm leading-7 text-slate-600">
|
<p class="oc-page-description">
|
||||||
批量整理你的云图记录。类别和拍摄时间必填,经纬度既可以手动输入,也可以在迷你地图里点选回填。
|
批量整理你的云图记录。类别和拍摄时间必填,经纬度既可以手动输入,也可以在迷你地图里点选回填。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="max-w-6xl mx-auto px-4 py-8">
|
<div class="oc-container oc-container--content oc-page-content">
|
||||||
<input ref="fileInput" type="file" accept="image/*" multiple class="hidden" @change="handleFileSelect" />
|
<input ref="fileInput" type="file" accept="image/*" multiple class="hidden" @change="handleFileSelect" />
|
||||||
|
|
||||||
<div v-if="successMsg" class="flex items-center justify-center min-h-[60vh]">
|
<div v-if="successMsg" class="flex items-center justify-center min-h-[60vh]">
|
||||||
@@ -231,13 +231,13 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="mt-5">
|
<div class="mt-5">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<h3 class="text-2xl font-bold text-gray-900">{{ badge.cloudName }}</h3>
|
<h3 class="text-2xl font-bold text-slate-900">{{ badge.cloudName }}</h3>
|
||||||
<NTag :bordered="false" type="warning">
|
<NTag :bordered="false" type="warning">
|
||||||
{{ rarityLabel(badge.rarity) }}
|
{{ rarityLabel(badge.rarity) }}
|
||||||
</NTag>
|
</NTag>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-sm text-gray-500">{{ badge.cloudNameEn }}</p>
|
<p class="mt-1 text-sm text-slate-500">{{ badge.cloudNameEn }}</p>
|
||||||
<p class="mt-4 text-sm text-gray-600">解锁时间:{{ formatUnlockedAt(badge.unlockedAt) }}</p>
|
<p class="mt-4 text-sm text-slate-600">解锁时间:{{ formatUnlockedAt(badge.unlockedAt) }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 flex gap-3">
|
<div class="mt-6 flex gap-3">
|
||||||
@@ -258,7 +258,7 @@ onUnmounted(() => {
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<div v-if="uploading" class="mb-6 border border-slate-200 bg-white p-5 shadow-sm">
|
<div v-if="uploading" class="mb-6 border border-slate-200 bg-white p-5 shadow-sm">
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<span class="text-sm font-medium text-gray-700">正在上传 {{ currentItemIndex }} / {{ totalItems }}...</span>
|
<span class="text-sm font-medium text-slate-700">正在上传 {{ currentItemIndex }} / {{ totalItems }}...</span>
|
||||||
<span class="text-sm font-medium text-sky-600">{{ overallProgress }}%</span>
|
<span class="text-sm font-medium text-sky-600">{{ overallProgress }}%</span>
|
||||||
</div>
|
</div>
|
||||||
<NProgress
|
<NProgress
|
||||||
@@ -278,17 +278,17 @@ onUnmounted(() => {
|
|||||||
@drop.prevent="handleDrop"
|
@drop.prevent="handleDrop"
|
||||||
@click="fileInput?.click()"
|
@click="fileInput?.click()"
|
||||||
class="flex flex-col items-center justify-center border-2 border-dashed py-20 cursor-pointer transition-colors bg-white shadow-sm"
|
class="flex flex-col items-center justify-center border-2 border-dashed py-20 cursor-pointer transition-colors bg-white shadow-sm"
|
||||||
:class="dragOver ? 'border-sky-400 bg-sky-50' : 'border-gray-300 hover:border-sky-400 hover:bg-gray-50'"
|
:class="dragOver ? 'border-sky-400 bg-sky-50' : 'border-slate-300 hover:border-sky-400 hover:bg-slate-50'"
|
||||||
>
|
>
|
||||||
<span class="text-5xl mb-4">☁️</span>
|
<span class="text-5xl mb-4">☁️</span>
|
||||||
<p class="text-lg font-medium text-gray-700 mb-1">点击或拖拽图片到此处</p>
|
<p class="mb-1 text-lg font-medium text-slate-700">点击或拖拽图片到此处</p>
|
||||||
<p class="text-sm text-gray-400">支持 JPG、PNG 格式,可一次选择多张</p>
|
<p class="text-sm text-slate-400">支持 JPG、PNG 格式,可一次选择多张</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="flex gap-6">
|
<div class="flex gap-6">
|
||||||
<div class="flex-shrink-0 w-[480px]">
|
<div class="flex-shrink-0 w-[480px]">
|
||||||
<div v-if="activeItem" class="bg-gray-900 overflow-hidden mb-3 border border-slate-900">
|
<div v-if="activeItem" class="mb-3 overflow-hidden border border-slate-900 bg-slate-900">
|
||||||
<img :src="activeItem.preview" alt="预览" class="w-full h-[360px] object-contain" />
|
<img :src="activeItem.preview" alt="预览" class="w-full h-[360px] object-contain" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2 overflow-x-auto pb-2">
|
<div class="flex gap-2 overflow-x-auto pb-2">
|
||||||
@@ -297,7 +297,7 @@ onUnmounted(() => {
|
|||||||
:key="item.id"
|
:key="item.id"
|
||||||
@click="selectItem(item.id)"
|
@click="selectItem(item.id)"
|
||||||
class="relative flex-shrink-0 w-16 h-16 overflow-hidden cursor-pointer border-2 transition-colors bg-white"
|
class="relative flex-shrink-0 w-16 h-16 overflow-hidden cursor-pointer border-2 transition-colors bg-white"
|
||||||
:class="activeId === item.id ? 'border-sky-500' : 'border-transparent hover:border-gray-300'"
|
:class="activeId === item.id ? 'border-sky-500' : 'border-transparent hover:border-slate-300'"
|
||||||
>
|
>
|
||||||
<img :src="item.preview" alt="" class="w-full h-full object-cover" />
|
<img :src="item.preview" alt="" class="w-full h-full object-cover" />
|
||||||
<div v-if="Object.keys(item.errors).length > 0" class="absolute top-0 right-0 w-2.5 h-2.5 bg-red-500"></div>
|
<div v-if="Object.keys(item.errors).length > 0" class="absolute top-0 right-0 w-2.5 h-2.5 bg-red-500"></div>
|
||||||
@@ -309,30 +309,30 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@click="fileInput?.click()"
|
@click="fileInput?.click()"
|
||||||
class="flex-shrink-0 w-16 h-16 border-2 border-dashed border-gray-300 flex items-center justify-center cursor-pointer bg-white hover:border-sky-400 hover:bg-gray-50 transition-colors"
|
class="flex h-16 w-16 flex-shrink-0 cursor-pointer items-center justify-center border-2 border-dashed border-slate-300 bg-white transition-colors hover:border-sky-400 hover:bg-slate-50"
|
||||||
>
|
>
|
||||||
<span class="text-gray-400 text-xl">+</span>
|
<span class="text-xl text-slate-400">+</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 min-w-0" v-if="activeItem">
|
<div class="flex-1 min-w-0" v-if="activeItem">
|
||||||
<NCard class="border border-gray-200 shadow-sm">
|
<NCard class="border border-slate-200 shadow-sm">
|
||||||
<div class="space-y-5">
|
<div class="space-y-5">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h2 class="text-lg font-semibold text-gray-900">图片信息</h2>
|
<h2 class="text-lg font-semibold text-slate-900">图片信息</h2>
|
||||||
<span class="text-sm text-gray-400">{{ items.findIndex(i => i.id === activeId) + 1 }} / {{ items.length }}</span>
|
<span class="text-sm text-slate-400">{{ items.findIndex(i => i.id === activeId) + 1 }} / {{ items.length }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 类别 * -->
|
<!-- 类别 * -->
|
||||||
<div :id="`field-${activeItem.id}-cloudCategory`">
|
<div :id="`field-${activeItem.id}-cloudCategory`">
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
<label class="oc-field-label">
|
||||||
类别 <span class="text-red-500">*</span>
|
类别 <span class="oc-field-required">*</span>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
:value="activeItem.cloudCategoryId ?? ''"
|
:value="activeItem.cloudCategoryId ?? ''"
|
||||||
@change="onCategoryChange"
|
@change="onCategoryChange"
|
||||||
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-transparent transition-colors bg-white"
|
class="oc-field-control"
|
||||||
>
|
>
|
||||||
<option value="" disabled>请选择</option>
|
<option value="" disabled>请选择</option>
|
||||||
<option v-for="ct in cloudsStore.cloudTypes" :key="ct.id" :value="ct.id">
|
<option v-for="ct in cloudsStore.cloudTypes" :key="ct.id" :value="ct.id">
|
||||||
@@ -346,35 +346,35 @@ onUnmounted(() => {
|
|||||||
type="text"
|
type="text"
|
||||||
placeholder="输入云的类型"
|
placeholder="输入云的类型"
|
||||||
@input="activeItem.errors = {}"
|
@input="activeItem.errors = {}"
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-transparent transition-colors"
|
class="oc-field-control"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="activeItem.errors.cloudCategory" class="text-sm text-red-500 mt-1">{{ activeItem.errors.cloudCategory }}</p>
|
<p v-if="activeItem.errors.cloudCategory" class="oc-field-error">{{ activeItem.errors.cloudCategory }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 拍摄时间 * -->
|
<!-- 拍摄时间 * -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
<label class="oc-field-label">
|
||||||
拍摄时间 <span class="text-red-500">*</span>
|
拍摄时间 <span class="oc-field-required">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
:value="formatDatetimeLocal(activeItem.capturedAt)"
|
:value="formatDatetimeLocal(activeItem.capturedAt)"
|
||||||
@change="onCapturedAtChange"
|
@change="onCapturedAtChange"
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-transparent transition-colors"
|
class="oc-field-control"
|
||||||
/>
|
/>
|
||||||
<p v-if="activeItem.errors.capturedAt" class="text-sm text-red-500 mt-1">{{ activeItem.errors.capturedAt }}</p>
|
<p v-if="activeItem.errors.capturedAt" class="oc-field-error">{{ activeItem.errors.capturedAt }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 经纬度 -->
|
<!-- 经纬度 -->
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-1 flex items-center justify-between gap-3">
|
<div class="mb-1 flex items-center justify-between gap-3">
|
||||||
<label class="block text-sm font-medium text-gray-700">经纬度</label>
|
<label class="oc-field-label mb-0">经纬度</label>
|
||||||
<button
|
<button
|
||||||
v-if="activeItem.latitude !== null || activeItem.longitude !== null"
|
v-if="activeItem.latitude !== null || activeItem.longitude !== null"
|
||||||
type="button"
|
type="button"
|
||||||
@click="clearCoordinates"
|
@click="clearCoordinates"
|
||||||
class="text-xs font-medium text-gray-400 transition-colors hover:text-gray-600"
|
class="text-xs font-medium text-slate-400 transition-colors hover:text-slate-600"
|
||||||
>
|
>
|
||||||
清空
|
清空
|
||||||
</button>
|
</button>
|
||||||
@@ -387,9 +387,9 @@ onUnmounted(() => {
|
|||||||
type="number"
|
type="number"
|
||||||
step="any"
|
step="any"
|
||||||
placeholder="纬度(如 39.90)"
|
placeholder="纬度(如 39.90)"
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-transparent transition-colors"
|
class="oc-field-control"
|
||||||
/>
|
/>
|
||||||
<p v-if="activeItem.errors.latitude" class="text-sm text-red-500 mt-1">{{ activeItem.errors.latitude }}</p>
|
<p v-if="activeItem.errors.latitude" class="oc-field-error">{{ activeItem.errors.latitude }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
@@ -398,9 +398,9 @@ onUnmounted(() => {
|
|||||||
type="number"
|
type="number"
|
||||||
step="any"
|
step="any"
|
||||||
placeholder="经度(如 116.40)"
|
placeholder="经度(如 116.40)"
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-transparent transition-colors"
|
class="oc-field-control"
|
||||||
/>
|
/>
|
||||||
<p v-if="activeItem.errors.longitude" class="text-sm text-red-500 mt-1">{{ activeItem.errors.longitude }}</p>
|
<p v-if="activeItem.errors.longitude" class="oc-field-error">{{ activeItem.errors.longitude }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
@@ -414,28 +414,28 @@ onUnmounted(() => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-400 mt-1">选填,可手动输入,或点击右侧小地图图标选择</p>
|
<p class="mt-1 text-xs text-slate-400">选填,可手动输入,或点击右侧小地图图标选择</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 位置名称 -->
|
<!-- 位置名称 -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">位置名称</label>
|
<label class="oc-field-label">位置名称</label>
|
||||||
<input
|
<input
|
||||||
v-model="activeItem.locationName"
|
v-model="activeItem.locationName"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="如:北京、成都"
|
placeholder="如:北京、成都"
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-transparent transition-colors"
|
class="oc-field-control"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 图片描述 -->
|
<!-- 图片描述 -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">图片描述</label>
|
<label class="oc-field-label">图片描述</label>
|
||||||
<textarea
|
<textarea
|
||||||
v-model="activeItem.description"
|
v-model="activeItem.description"
|
||||||
rows="3"
|
rows="3"
|
||||||
placeholder="描述一下这张图片..."
|
placeholder="描述一下这张图片..."
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-transparent transition-colors resize-none"
|
class="oc-field-control resize-none"
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -443,14 +443,14 @@ onUnmounted(() => {
|
|||||||
<div class="border border-slate-200 bg-slate-50 px-4 py-3">
|
<div class="border border-slate-200 bg-slate-50 px-4 py-3">
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-medium text-gray-800">公开展示</p>
|
<p class="text-sm font-medium text-slate-800">公开展示</p>
|
||||||
<p class="mt-1 text-xs text-gray-500">关闭后不会出现在画廊、地图和公开主页中。</p>
|
<p class="mt-1 text-xs text-slate-500">关闭后不会出现在画廊、地图和公开主页中。</p>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
:checked="!activeItem.isHidden"
|
:checked="!activeItem.isHidden"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:id="'isPublic-' + activeItem.id"
|
:id="'isPublic-' + activeItem.id"
|
||||||
class="h-5 w-5 border-gray-300 text-sky-500 focus:ring-sky-500"
|
class="h-5 w-5 border-slate-300 text-sky-500 focus:ring-sky-500"
|
||||||
@change="updateActivePublicState(($event.target as HTMLInputElement).checked)"
|
@change="updateActivePublicState(($event.target as HTMLInputElement).checked)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -461,7 +461,7 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 flex items-center justify-between">
|
<div class="mt-6 flex items-center justify-between">
|
||||||
<p class="text-sm text-gray-500">共 {{ items.length }} 张图片</p>
|
<p class="text-sm text-slate-500">共 {{ items.length }} 张图片</p>
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<NButton secondary strong type="default" class="oc-panel-button oc-panel-button--neutral" @click="clearAll()" :disabled="uploading">清空</NButton>
|
<NButton secondary strong type="default" class="oc-panel-button oc-panel-button--neutral" @click="clearAll()" :disabled="uploading">清空</NButton>
|
||||||
<NButton type="default" secondary strong class="oc-panel-button oc-panel-button--sky" @click="handleSubmit" :disabled="uploading">
|
<NButton type="default" secondary strong class="oc-panel-button oc-panel-button--sky" @click="handleSubmit" :disabled="uploading">
|
||||||
|
|||||||
Reference in New Issue
Block a user