From eaee7652396806b7181016ca3700678981f59e78 Mon Sep 17 00:00:00 2001
From: Zhou Weidong <502333560@qq.com>
Date: Sat, 24 Oct 2026 17:30:29 +0800
Subject: [PATCH 1/5] =?UTF-8?q?fix(app-version):=20=E4=BF=AE=E5=A4=8D?=
=?UTF-8?q?=E7=89=88=E6=9C=AC=E9=A1=B5=E7=B1=BB=E5=9E=8B=E6=A3=80=E6=9F=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/views/systemManagement/AppVersionView.vue | 27 +++++++++++--------
1 file changed, 16 insertions(+), 11 deletions(-)
diff --git a/src/views/systemManagement/AppVersionView.vue b/src/views/systemManagement/AppVersionView.vue
index 90da577..903f4f7 100644
--- a/src/views/systemManagement/AppVersionView.vue
+++ b/src/views/systemManagement/AppVersionView.vue
@@ -3,13 +3,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/contentManagement/CategoryDramaView.vue b/src/views/contentManagement/CategoryDramaView.vue
index 8107ff2..ed235bb 100644
--- a/src/views/contentManagement/CategoryDramaView.vue
+++ b/src/views/contentManagement/CategoryDramaView.vue
@@ -6,6 +6,12 @@ import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { AppIcon } from '@/components/AppIcon'
import { AppIcons } from '@/constants/icons'
+import {
+ languageCodes,
+ languageLabel,
+ localesFromColumns,
+ payloadLocales
+} from '@/constants/contentLanguages'
import {
bindClassificationDramasApi,
createClassificationApi,
@@ -18,6 +24,8 @@ import {
} from '@/api/admin/dramaClassification'
import { statusClass } from '@/utils/status'
import { useRbac } from '@/stores/rbac'
+import ClassificationDramaDialogs from './ClassificationDramaDialogs.vue'
+import ClassificationSideList from './ClassificationSideList.vue'
import PageHeading from '@/views/components/PageHeading.vue'
const { can } = useRbac()
@@ -46,7 +54,9 @@ const keyword = ref('')
const dialogVisible = ref(false)
const saving = ref(false)
const editingId = ref(null)
-const form = reactive({ name: '', sort: 1, status: 1 })
+const languages = ref(['zh'])
+const emptyForm = () => ({ locales: { zh: '' }, sort: 1, status: 1 })
+const form = reactive(emptyForm())
const candidateVisible = ref(false)
const candidateLoading = ref(false)
@@ -110,25 +120,39 @@ function changePage(value) {
function openCreate() {
editingId.value = null
+ languages.value = ['zh']
const maxSort = classifications.value.reduce((max, item) => Math.max(max, item.sort || 0), 0)
- Object.assign(form, { name: '', sort: maxSort + 1, status: 1 })
+ Object.assign(form, emptyForm(), { sort: maxSort + 1 })
dialogVisible.value = true
}
function openEdit(row) {
editingId.value = row.id
- Object.assign(form, { name: row.name, sort: row.sort ?? 0, status: row.status ?? 1 })
+ const locales = localesFromColumns(row.locales, row.name, row.nameEn, row.nameId)
+ languages.value = languageCodes(locales)
+ Object.assign(form, {
+ locales,
+ sort: row.sort ?? 0,
+ status: row.status ?? 1
+ })
dialogVisible.value = true
}
async function saveClassification() {
- if (!form.name.trim()) {
- ElMessage.warning('请填写分类名称')
+ const locales = payloadLocales(form.locales, languages.value)
+ const missing = languages.value.find((code) => !locales[code])
+ if (!locales.zh || missing) {
+ ElMessage.warning(missing ? `请填写${languageLabel(missing)}名称` : '请填写中文名称')
return
}
saving.value = true
try {
- const payload = { name: form.name.trim(), sort: form.sort, status: form.status }
+ const payload = {
+ name: locales.zh,
+ locales,
+ sort: form.sort,
+ status: form.status
+ }
if (editingId.value) {
await updateClassificationApi(editingId.value, payload)
ElMessage.success('分类已更新')
@@ -256,48 +280,13 @@ onMounted(loadClassifications)
-
+
@@ -376,77 +365,27 @@ onMounted(loadClassifications)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 取消
- 保存
-
-
-
-
-
-
- 查询
-
-
-
-
-
- {{ row.totalEpisodes || 0 }}
-
-
-
-
- {{ STATUS_TEXT[row.status] || row.status }}
-
-
-
-
-
-
-
- 取消
-
- 确定关联{{ selectedDramas.length ? `(${selectedDramas.length})` : '' }}
-
-
-
+
diff --git a/src/views/contentManagement/CategoryManagementView.vue b/src/views/contentManagement/CategoryManagementView.vue
index ea00632..7e6e67a 100644
--- a/src/views/contentManagement/CategoryManagementView.vue
+++ b/src/views/contentManagement/CategoryManagementView.vue
@@ -4,6 +4,12 @@
import { onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { AppIcons } from '@/constants/icons'
+import {
+ languageCodes,
+ languageLabel,
+ localesFromColumns,
+ payloadLocales
+} from '@/constants/contentLanguages'
import {
createDramaCategoryApi,
deleteDramaCategoryApi,
@@ -11,6 +17,7 @@ import {
updateDramaCategoryApi
} from '@/api/admin/drama'
import { useRbac } from '@/stores/rbac'
+import LocaleTextFields from '@/views/components/LocaleTextFields.vue'
import PageHeading from '@/views/components/PageHeading.vue'
const { can } = useRbac()
@@ -20,7 +27,8 @@ const list = ref([])
const dialogVisible = ref(false)
const saving = ref(false)
const editingId = ref(null)
-const form = reactive({ name: '', sort: 1, status: 1 })
+const languages = ref(['zh'])
+const form = reactive({ locales: { zh: '' }, sort: 1, status: 1 })
async function load() {
loading.value = true
@@ -38,21 +46,29 @@ function nextSort() {
function openCreate() {
editingId.value = null
- Object.assign(form, { name: '', sort: nextSort(), status: 1 })
+ languages.value = ['zh']
+ Object.assign(form, { locales: { zh: '' }, sort: nextSort(), status: 1 })
dialogVisible.value = true
}
function openEdit(row) {
editingId.value = row.id
- Object.assign(form, { name: row.name, sort: row.sort ?? 0, status: row.status ?? 1 })
+ const locales = localesFromColumns(row.locales, row.name, row.nameEn, row.nameId)
+ languages.value = languageCodes(locales)
+ Object.assign(form, { locales, sort: row.sort ?? 0, status: row.status ?? 1 })
dialogVisible.value = true
}
async function saveCategory() {
- if (!form.name.trim()) return ElMessage.warning('请填写类型名称')
+ const locales = payloadLocales(form.locales, languages.value)
+ const missing = languages.value.find((code) => !locales[code])
+ if (!locales.zh || missing) {
+ ElMessage.warning(missing ? `请填写${languageLabel(missing)}名称` : '请填写中文名称')
+ return
+ }
saving.value = true
try {
- const payload = { name: form.name.trim(), sort: form.sort, status: form.status }
+ const payload = { name: locales.zh, locales, sort: form.sort, status: form.status }
if (editingId.value) {
await updateDramaCategoryApi(editingId.value, payload)
ElMessage.success('类型已更新')
@@ -115,7 +131,20 @@ onMounted(load)
empty-text="还没有短剧类型"
>
-
+
+
+
+ {{ languageLabel(code) }}
+
+
{{ row.dramaCount || 0 }} 部
@@ -146,11 +175,14 @@ onMounted(load)
-
+
-
-
-
+
共 {{ total }} 部短剧
@@ -465,24 +466,17 @@ onMounted(async () => {
>
{{
name
}}
—
-
{{ dramaTypeLabel(type) }}
{{ row.tagName }}—
{
}}
-
热门—
{
-
-
- 基础信息
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 付费设置
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 数值越大越靠前,相同权重按 ID 倒序
-
-
-
-
-
- 互动数据
-
-
-
-
-
- {{ formatCount(editingLikeCount) }}
- 全剧分集汇总,只读
-
-
-
-
-
- {{ formatCount(editingCommentCount) }}
- 全剧汇总,实时统计,只读
-
-
-
-
+
封面
@@ -776,6 +635,117 @@ onMounted(async () => {
+
+
+ 基础信息
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 付费设置
+
+
+
+
+
+
+
+
+
+ 数值越大越靠前,相同权重按 ID 倒序
+
+
+
+
+
+ 互动数据
+
+
+
+
+
+ {{ formatCount(editingLikeCount) }}
+ 全剧分集汇总,只读
+
+
+
+
+
+ {{ formatCount(editingCommentCount) }}
+ 全剧汇总,实时统计,只读
+
+
+
@@ -816,6 +786,20 @@ onMounted(async () => {
min-width: 0;
}
+.drama-editor-form :deep(.el-form-item) {
+ margin-right: 0;
+}
+
+.drama-editor-form :deep(.el-form-item__content) {
+ min-width: 0;
+}
+
+.drama-editor-form :deep(.el-input),
+.drama-editor-form :deep(.el-textarea),
+.drama-editor-form :deep(.el-select) {
+ width: 100%;
+}
+
.tag-wrap {
display: flex;
flex-wrap: wrap;
diff --git a/src/views/contentManagement/DramaPlaylistView.vue b/src/views/contentManagement/DramaPlaylistView.vue
index 5115a74..1174f8a 100644
--- a/src/views/contentManagement/DramaPlaylistView.vue
+++ b/src/views/contentManagement/DramaPlaylistView.vue
@@ -6,6 +6,12 @@ import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { AppIcon } from '@/components/AppIcon'
import { AppIcons } from '@/constants/icons'
+import {
+ languageCodes,
+ languageLabel,
+ localesFromColumns,
+ payloadLocales
+} from '@/constants/contentLanguages'
import {
bindPlaylistCategoryDramasApi,
createPlaylistCategoryApi,
@@ -18,6 +24,7 @@ import {
} from '@/api/admin/dramaPlaylistCategory'
import { statusClass } from '@/utils/status'
import { useRbac } from '@/stores/rbac'
+import LocaleTextFields from '@/views/components/LocaleTextFields.vue'
import PageHeading from '@/views/components/PageHeading.vue'
const { can } = useRbac()
@@ -46,7 +53,14 @@ const keyword = ref('')
const dialogVisible = ref(false)
const saving = ref(false)
const editingId = ref(null)
-const form = reactive({ name: '', description: '', sort: 1, status: 1 })
+const languages = ref(['zh'])
+const emptyForm = () => ({
+ locales: { zh: '' },
+ descriptionLocales: { zh: '' },
+ sort: 1,
+ status: 1
+})
+const form = reactive(emptyForm())
const candidateVisible = ref(false)
const candidateLoading = ref(false)
@@ -108,18 +122,37 @@ function changePage(value) {
loadDramas()
}
+function changeLanguages(codes) {
+ languages.value = codes
+ codes.forEach((code) => {
+ if (form.descriptionLocales[code] == null) form.descriptionLocales[code] = ''
+ })
+}
+
function openCreate() {
editingId.value = null
+ languages.value = ['zh']
const maxSort = categories.value.reduce((max, item) => Math.max(max, item.sort || 0), 0)
- Object.assign(form, { name: '', description: '', sort: maxSort + 1, status: 1 })
+ Object.assign(form, emptyForm(), { sort: maxSort + 1 })
dialogVisible.value = true
}
function openEdit(row) {
editingId.value = row.id
+ const locales = localesFromColumns(row.locales, row.name, row.nameEn, row.nameId)
+ languages.value = languageCodes(locales)
+ const descriptionLocales = localesFromColumns(
+ row.descriptionLocales,
+ row.description,
+ row.descriptionEn,
+ row.descriptionId
+ )
+ languages.value.forEach((code) => {
+ if (descriptionLocales[code] == null) descriptionLocales[code] = ''
+ })
Object.assign(form, {
- name: row.name,
- description: row.description || '',
+ locales,
+ descriptionLocales,
sort: row.sort ?? 0,
status: row.status ?? 1
})
@@ -127,15 +160,18 @@ function openEdit(row) {
}
async function saveCategory() {
- if (!form.name.trim()) {
- ElMessage.warning('请填写剧单分类名称')
+ const locales = payloadLocales(form.locales, languages.value)
+ const missing = languages.value.find((code) => !locales[code])
+ if (!locales.zh || missing) {
+ ElMessage.warning(missing ? `请填写${languageLabel(missing)}名称` : '请填写中文名称')
return
}
saving.value = true
try {
const payload = {
- name: form.name.trim(),
- description: form.description.trim(),
+ name: locales.zh,
+ locales,
+ descriptionLocales: payloadLocales(form.descriptionLocales, languages.value),
sort: form.sort,
status: form.status
}
@@ -390,22 +426,25 @@ onMounted(loadCategories)
-
-
-
-
-
-
+
+
@@ -95,7 +97,7 @@ export default defineComponent({
}
}
-@media (max-width: 768px) {
+@media (width <= 768px) {
.@{prefix-cls} {
padding-right: 8px;
padding-left: 8px;
diff --git a/src/layout/components/TranscodeNoticeButton.vue b/src/layout/components/TranscodeNoticeButton.vue
new file mode 100644
index 0000000..6bf6944
--- /dev/null
+++ b/src/layout/components/TranscodeNoticeButton.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/permission.ts b/src/permission.ts
index dc61a1a..557b305 100644
--- a/src/permission.ts
+++ b/src/permission.ts
@@ -96,7 +96,12 @@ router.beforeEach(async (to, from, next) => {
const redirectPath = from.query.redirect || to.path
const redirect = decodeURIComponent(redirectPath as string)
- const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect }
+ // 只按路径重进。刷新时动态路由还没注册,先被兜底路由接住;
+ // 若把当时匹配到的 name 带回去,会一直停在 404。
+ const nextData =
+ to.path === redirect
+ ? { path: to.path, query: to.query, hash: to.hash, replace: true }
+ : { path: redirect, replace: true }
permissionStore.setIsAddRouters(true)
next(nextData)
}
diff --git a/src/router/index.ts b/src/router/index.ts
index 123ec04..a6f450a 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -52,6 +52,35 @@ export const constantRouterMap: AppRouteRecordRaw[] = [
title: '404',
noTagsView: true
}
+ },
+ {
+ path: '/notifications',
+ component: Layout,
+ name: 'Notifications',
+ meta: {
+ hidden: true,
+ title: '通知'
+ },
+ children: [
+ {
+ path: 'transcode',
+ name: 'transcode-notices',
+ component: () => import('@/views/common/TranscodeNoticeView.vue'),
+ meta: {
+ title: '转码通知'
+ }
+ }
+ ]
+ },
+ {
+ path: '/:pathMatch(.*)*',
+ name: 'UnresolvedPath',
+ component: () => import('@/views/Error/404.vue'),
+ meta: {
+ hidden: true,
+ title: '404',
+ noTagsView: true
+ }
}
]
@@ -232,7 +261,14 @@ const router = createRouter({
})
export const resetRouter = (): void => {
- const resetWhiteNameList = ['Redirect', 'RedirectParent', 'Login', 'NoFind', 'Root']
+ const resetWhiteNameList = [
+ 'Redirect',
+ 'RedirectParent',
+ 'Login',
+ 'NoFind',
+ 'Root',
+ 'UnresolvedPath'
+ ]
router.getRoutes().forEach((route) => {
const { name } = route
if (name && !resetWhiteNameList.includes(name as string)) {
diff --git a/src/utils/transcodeNoticeSeen.ts b/src/utils/transcodeNoticeSeen.ts
new file mode 100644
index 0000000..ea6b829
--- /dev/null
+++ b/src/utils/transcodeNoticeSeen.ts
@@ -0,0 +1,51 @@
+// Author: fruit.com.cn
+
+const SEEN_KEY = 'transcode-notice-seen'
+
+export const TRANSCODE_NOTICE_SEEN = 'transcode-notice-seen'
+
+export interface NoticeCounts {
+ running: number
+ success: number
+ failed: number
+}
+
+/** 上次打开通知页时已经看过的数量。 */
+export function readSeen(): NoticeCounts {
+ try {
+ const raw = localStorage.getItem(SEEN_KEY)
+ if (!raw) return { running: 0, success: 0, failed: 0 }
+ const data = JSON.parse(raw) as NoticeCounts
+ return {
+ running: Number(data.running || 0),
+ success: Number(data.success || 0),
+ failed: Number(data.failed || 0)
+ }
+ } catch {
+ return { running: 0, success: 0, failed: 0 }
+ }
+}
+
+/** 比上次查看多出来的数量,看过的不再计入角标。 */
+export function unreadCounts(current: NoticeCounts): NoticeCounts {
+ const seen = readSeen()
+ return {
+ running: Math.max(0, current.running - seen.running),
+ success: Math.max(0, current.success - seen.success),
+ failed: Math.max(0, current.failed - seen.failed)
+ }
+}
+
+/** 把当前数量记为已读,并通知顶栏收起角标。 */
+export function markTranscodeNoticeSeen(current: NoticeCounts) {
+ const seen = readSeen()
+ localStorage.setItem(
+ SEEN_KEY,
+ JSON.stringify({
+ running: Math.max(seen.running, current.running),
+ success: Math.max(seen.success, current.success),
+ failed: Math.max(seen.failed, current.failed)
+ })
+ )
+ window.dispatchEvent(new Event(TRANSCODE_NOTICE_SEEN))
+}
diff --git a/src/views/common/DashboardView.vue b/src/views/common/DashboardView.vue
index c7e4bfa..43feaae 100644
--- a/src/views/common/DashboardView.vue
+++ b/src/views/common/DashboardView.vue
@@ -5,6 +5,8 @@ import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { AppIcons } from '@/constants/icons'
import { pageDramasApi } from '@/api/admin/drama'
+import { getDashboardOverviewApi, getDashboardTrendApi } from '@/api/admin/dashboard'
+import DashboardMetricWall from '@/views/dashboard/DashboardMetricWall.vue'
import { useRbac } from '@/stores/rbac'
import { formatCount } from '@/utils/format'
import { statusClass } from '@/utils/status'
@@ -15,6 +17,8 @@ const { can } = useRbac()
const loading = ref(false)
const dramas = ref([])
const total = ref(0)
+const overview = ref(null)
+const trend = ref(null)
const totalViews = computed(() =>
dramas.value.reduce((sum, item) => sum + Number(item.views || 0), 0)
@@ -25,6 +29,12 @@ const reviewingCount = computed(
const publishedCount = computed(
() => dramas.value.filter((item) => item.status === 'published').length
)
+const metricFallback = computed(() => ({
+ 'content.dramaTotal': total.value,
+ 'content.dramaReviewing': reviewingCount.value,
+ 'content.dramaPublished': publishedCount.value,
+ 'playback.viewTotal': totalViews.value
+}))
const recentDramas = computed(() =>
[...dramas.value]
.sort((a, b) => String(b.updatedAt || '').localeCompare(String(a.updatedAt || '')))
@@ -34,10 +44,16 @@ const recentDramas = computed(() =>
onMounted(async () => {
loading.value = true
try {
- const res = await pageDramasApi({ pageIndex: 1, pageSize: 200 })
+ const [res, board, trendRes] = await Promise.all([
+ pageDramasApi({ pageIndex: 1, pageSize: 200 }),
+ getDashboardOverviewApi().catch(() => null),
+ getDashboardTrendApi().catch(() => null)
+ ])
// 接口失败时 axios 拦截器会把业务错误吞成 undefined,这里不能让页面跟着崩
dramas.value = res?.data?.list || []
total.value = res?.data?.total || 0
+ overview.value = board?.data || null
+ trend.value = trendRes?.data || null
} finally {
loading.value = false
}
@@ -45,72 +61,83 @@ onMounted(async () => {
-
-
-
-
{{ formatCount(totalViews) }}
-
-
-
- 短剧管理
+
-
{{ scope?.row?.episodes }} 集{{ formatCount(scope?.row?.views) }}{{ scope.row.statusText }}
-
+ :overview="overview"
+ :trend="trend"
+ :fallback="metricFallback"
+ />
+
+ {{ scope?.row?.episodes }} 集{{
+ formatCount(scope?.row?.views)
+ }}{{ scope.row.statusText }}
+
+
+
+
diff --git a/src/views/common/TranscodeNoticeView.vue b/src/views/common/TranscodeNoticeView.vue
new file mode 100644
index 0000000..1ba7fbf
--- /dev/null
+++ b/src/views/common/TranscodeNoticeView.vue
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+ 刷新
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ episodeText(row) }}
+
+
+ {{ row.progress ?? 0 }}%
+
+
+
+ {{ formatDateTime(row.updatedAt) }}
+
+
+
+
+
+
diff --git a/src/views/dashboard/BusinessDashboardView.vue b/src/views/dashboard/BusinessDashboardView.vue
index caa645d..1b2f9e9 100644
--- a/src/views/dashboard/BusinessDashboardView.vue
+++ b/src/views/dashboard/BusinessDashboardView.vue
@@ -1,8 +1,9 @@
+
+
+
+
+
+
+
+
+
+ {{ item.value }}
+ {{ item.label }}
+
+
+
+
+
+
+ 近 7 日新增与播放
+
+
+
+ 活跃设备占比
+
+
+
+ 用户留存
+
+
+ {{ retentionDay == null ? '--' : `${retentionDay.toFixed(1)}%` }}
+ 次日留存
+
+
+ {{ retentionWeek == null ? '--' : `${retentionWeek.toFixed(1)}%` }}
+ 7 日留存
+
+
+
+
+
+
+
+ {{ item.value }}
+ {{ item.label }}
+
+
+
+
+
+
From 33bf5bd3bbc1a151db52d4bc1d58d39094d3ce64 Mon Sep 17 00:00:00 2001
From: Zhou Weidong <502333560@qq.com>
Date: Sat, 24 Oct 2026 17:32:12 +0800
Subject: [PATCH 4/5] =?UTF-8?q?feat(admin):=20=E7=99=BB=E5=BD=95=E9=A1=B5?=
=?UTF-8?q?=E8=A1=A5=E5=85=85=E6=B0=B4=E6=9E=9C=E8=A3=85=E9=A5=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/views/Login/Login.vue | 157 +++++++++++++++++++++++++++++++++++---
1 file changed, 146 insertions(+), 11 deletions(-)
diff --git a/src/views/Login/Login.vue b/src/views/Login/Login.vue
index f98d12b..93ce65f 100644
--- a/src/views/Login/Login.vue
+++ b/src/views/Login/Login.vue
@@ -31,6 +31,94 @@ const themeChange = () => {
{{ underlineToHump(appStore.getTitle) }}
+
+
+
+
+
{{ t('login.consoleLabel') }}
{{ t('login.welcome') }}
@@ -81,16 +169,58 @@ const themeChange = () => {
flex-direction: column;
justify-content: space-between;
+ &::before,
&::after {
position: absolute;
- right: -80px;
- bottom: -120px;
- width: 360px;
- height: 360px;
- border: 1px solid rgb(255 255 255 / 16%);
+ border: 1px solid rgb(255 255 255 / 12%);
border-radius: 50%;
content: '';
}
+
+ &::before {
+ top: -140px;
+ left: -80px;
+ width: 420px;
+ height: 420px;
+ }
+
+ &::after {
+ right: -120px;
+ bottom: -160px;
+ width: 460px;
+ height: 460px;
+ }
+}
+
+.hero-fruits {
+ position: absolute;
+ inset: 0;
+ z-index: 0;
+ pointer-events: none;
+}
+
+.fruit {
+ position: absolute;
+ filter: drop-shadow(0 18px 24px rgb(0 0 0 / 28%));
+}
+
+.cherry {
+ top: 16%;
+ right: 14%;
+ width: 150px;
+}
+
+.mango {
+ top: 30%;
+ left: 38%;
+ width: 210px;
+}
+
+.banana {
+ top: 54%;
+ left: 12%;
+ width: 200px;
+ transform: rotate(-8deg);
}
.hero-brand {
@@ -113,13 +243,14 @@ const themeChange = () => {
.hero-copy {
position: relative;
z-index: 1;
- max-width: 460px;
+ max-width: 420px;
h1 {
- margin: 10px 0 12px;
- font-size: 40px;
- font-weight: 700;
- line-height: 1.2;
+ margin: 12px 0 0;
+ font-size: 44px;
+ font-weight: 600;
+ line-height: 1.15;
+ letter-spacing: -0.03em;
color: #fff;
}
}
@@ -191,8 +322,12 @@ const themeChange = () => {
padding: 16px 24px 20px;
}
+ .hero-fruits {
+ display: none;
+ }
+
.hero-copy h1 {
- margin: 6px 0 8px;
+ margin: 6px 0 0;
font-size: 28px;
}
From d42a09fa368ab59aa49607aa1366768b9d6675dd Mon Sep 17 00:00:00 2001
From: Zhou Weidong <502333560@qq.com>
Date: Sat, 24 Oct 2026 17:32:32 +0800
Subject: [PATCH 5/5] =?UTF-8?q?feat(admin):=20=E5=90=8E=E5=8F=B0=E9=A1=B5?=
=?UTF-8?q?=E9=9D=A2=E6=98=BE=E7=A4=BA=E7=89=88=E6=9D=83=E6=A0=87=E8=AF=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/App.vue | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/src/App.vue b/src/App.vue
index 678312c..4b81793 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -22,6 +22,7 @@ appStore.initTheme()
+
@@ -48,4 +49,15 @@ body {
.@{prefix-cls}-grey-mode {
filter: grayscale(100%);
}
+
+.app-copyright {
+ position: fixed;
+ right: 20px;
+ bottom: 10px;
+ z-index: 5;
+ font-size: 12px;
+ line-height: 1;
+ color: var(--el-text-color-secondary, #909399);
+ pointer-events: none;
+}