style: 更新UI样式和字体设置

This commit is contained in:
2026-03-20 21:13:55 +08:00
parent 1e198af3d3
commit 9586c67a46
21 changed files with 2859 additions and 3606 deletions
+443 -188
View File
@@ -1,117 +1,168 @@
<template>
<div class="plink-page pt-4 flex justify-between" :style="boxStyle">
<div class="left ml-4 ">
<n-card class="card w-100 shadow">
<template #header>
<div class="flex items-center gap-4 text-primary text-xl font-bold">
<icon-loading class="zhuan text-primary w-6 h-6" />
申请友链
</div>
</template>
<div>
<n-form :model="plinkData" ref="formRef" :rules="rules" label-width="80" :inline="false"
feedback-style="font-size: 12px">
<n-form-item path="name">
<template #label>
<div class="flex items-center gap-1 text-sm text-gray-500">
<user-icon></user-icon>
昵称
</div>
</template>
<n-input v-model:value="plinkData.name" placeholder="请输入你的昵称" />
</n-form-item>
<n-form-item path="title">
<template #label>
<div class="flex items-center gap-1 text-sm text-gray-500">
<site-icon></site-icon>
网站名称
</div>
</template>
<n-input v-model:value="plinkData.title" placeholder="请输入你的网站名称" />
</n-form-item>
<n-form-item path="url">
<template #label>
<div class="flex items-center gap-1 text-sm text-gray-500">
<url-icon></url-icon>
站点地址
</div>
</template>
<n-input v-model:value="plinkData.url" placeholder="请输入你的网站地址" />
</n-form-item>
<n-form-item path="avater">
<template #label>
<div class="flex items-center gap-1 text-sm text-gray-500">
<ava-icon></ava-icon>
头像链接
</div>
</template>
<n-input v-model:value="plinkData.avater" placeholder="请输入你的头像链接" />
</n-form-item>
<n-form-item path="desc">
<template #label>
<div class="flex items-center gap-1 text-sm text-gray-500">
<desc-icon></desc-icon>
站点介绍
</div>
</template>
<n-input type="textarea" :autosize="{ minRows: 2, maxRows: 3 }" maxlength="50"
v-model:value="plinkData.desc" placeholder="请用一句话简短的描述你的站点" />
</n-form-item>
<n-form-item path="tagname">
<template #label>
<div class="flex items-center gap-1 text-sm text-gray-500">
<tag-icon></tag-icon>
标签
</div>
</template>
<n-input type="textarea" :autosize="{ minRows: 2, maxRows: 3 }" maxlength="50"
v-model:value="plinkData.tagname" placeholder="输入你的标签,用逗号隔开,注意每个标签最多3个字符,且只显示前3个标签哦~" />
</n-form-item>
<n-form-item>
<n-button class="w-full" type="primary" @click="submitPut">申请友链</n-button>
</n-form-item>
</n-form>
</div>
</n-card>
<div class="mt-4 text-right text-primary text-sm font-italic">申请完不显示是因为需要审核奥~~</div>
<div
class="relative overflow-hidden bg-[radial-gradient(circle_at_12%_18%,rgba(215,177,123,0.24),transparent_24%),radial-gradient(circle_at_86%_12%,rgba(120,162,182,0.18),transparent_24%),radial-gradient(circle_at_50%_100%,rgba(176,198,172,0.14),transparent_28%),linear-gradient(180deg,#efe7da_0%,#e8f0ef_44%,#f3f1e8_100%)]"
:style="boxStyle">
<div
class="pointer-events-none absolute left-[-5rem] top-6 h-80 w-80 rounded-full bg-[rgba(234,176,107,0.45)] opacity-48 blur-[88px]">
</div>
<div
class="pointer-events-none absolute right-[-5rem] top-32 h-[22rem] w-[22rem] rounded-full bg-[rgba(118,176,214,0.32)] opacity-48 blur-[88px]">
</div>
<div
class="pointer-events-none absolute inset-0 opacity-14 [background-image:linear-gradient(rgba(255,255,255,0.18)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.18)_1px,transparent_1px)] [background-size:36px_36px] [mask-image:linear-gradient(180deg,rgba(0,0,0,0.8),transparent_85%)]">
</div>
<n-scrollbar class="h-full">
<div class="right h-full grid grid-cols-3 gap-5 flex-1 px-12">
<div
class="rounded-lg bg-white shadow flex flex-col items-center justify-center p-4 cursor-pointer transition-transform duration-300 box-border hover:-translate-y-1.5"
v-for="p in pList" :key="p.pid" @click="gotoWebsite(p.url)">
<div class="ava">
<n-avatar round size="large" :src="p.avater"></n-avatar>
</div>
<em class="bg-[#fbf2e3] px-4 rounded-full text-sm text-primary mb-4">{{ p.name }}</em>
<div class="font-bold text-xl text-gray-700 mb-1">
{{ p.title }}
</div>
<div class="flex gap-1 truncate mb-1 text-sm">
<div v-for="i in getTags(p.tagname)" :key="i.tid"
:style="{ backgroundColor: getDictValue(tagColorList, 'tag', i, 'color') }"
class="flex items-center gap-1 cursor-pointer text-sm text-white rounded-full px-3">
<icon-tag2 class="w-3 h-3 " />{{ i }}
<div class="relative z-[1] mx-auto flex h-full w-[86%] gap-6 py-5">
<aside class="hidden w-[25rem] shrink-0 xl:block">
<div class="sticky top-4">
<section
class="relative rounded-[28px] border border-white/62 bg-[linear-gradient(145deg,rgba(255,251,246,0.68),rgba(237,246,244,0.56))] p-5 shadow-[0_20px_50px_rgba(110,124,112,0.11)] backdrop-blur-[20px] after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:bg-[linear-gradient(120deg,rgba(255,255,255,0.24),transparent_45%)] after:content-['']">
<div class="relative z-[1]">
<h1 class="mt-2 text-[1.65rem] leading-[1.2] text-[#2c3a4a]">留下你的站点名片</h1>
<section
class="mt-4 rounded-[24px] border border-white/72 bg-white/58 p-4 shadow-[0_14px_32px_rgba(112,128,118,0.1)]">
<div class="mb-3 flex items-center justify-between gap-3 text-primary">
<div class="flex items-center gap-2">
<icon-loading class="zhuan h-4 w-4" />
<span class="text-sm font-semibold">申请友链</span>
</div>
<span
class="rounded-full bg-[rgba(250,239,220,0.92)] px-2.5 py-1 text-[11px] cursor-pointer text-primary"
@click="fillForm">一键填入</span>
</div>
<n-form ref="formRef" :model="plinkData" :rules="rules" class="compact-side-form" label-placement="top"
require-mark-placement="right-hanging" size="small">
<n-form-item v-for="field in formFields" :key="field.key" :path="field.key">
<template #label>
<div class="flex items-center gap-2 text-sm font-medium text-[#526376]">
<component :is="field.icon" class="h-4 w-4" />
{{ field.label }}
</div>
</template>
<n-input :value="plinkData[field.key]" :type="field.inputType"
:maxlength="field.maxlength" :placeholder="field.placeholder" :rows="field.rows"
@update:value="updateField(field.key, $event)" />
</n-form-item>
<div class="!mb-0 btn">
<n-button
class="!h-10 !w-full !rounded-full !border-none !bg-[linear-gradient(135deg,#ec66ab,#f48a6f)] !text-sm !font-semibold shadow-[0_14px_28px_rgba(236,102,171,0.24)]"
type="primary" @click="submitPut">
提交友链申请
</n-button>
</div>
</n-form>
</section>
<p class="mt-3 text-right text-[12px] leading-5 text-[#7a8797]">
如果暂未显示通常是因为还在审核中...
</p>
</div>
</div>
<div class="truncate w-full text-gray-400 text-sm text-center">
{{ p.desc }}
</div>
</section>
</div>
</div>
</n-scrollbar>
</aside>
<section
class="min-w-0 flex-1 overflow-hidden rounded-[30px] border border-white/70 bg-transparent p-4 ring-1 ring-inset ring-[rgba(255,255,255,0.38)] shadow-[0_20px_46px_rgba(109,126,117,0.1)] sm:p-6">
<n-scrollbar :style="contentStyle" trigger="none" class="pr-1">
<div class="space-y-5 pr-1">
<section class="rounded-[26px] bg-transparent p-5 sm:p-6">
<div class="mb-5 flex items-center justify-between">
<div>
<div class="text-[0.82rem] font-semibold tracking-[0.16em] text-[#718195]">已通过友链</div>
<h3 class="mt-2 text-[1.4rem] font-semibold text-[#2f4050]">一起逛逛这些站点</h3>
</div>
<div
class="rounded-full border border-white/70 bg-white/42 px-4 py-2 text-sm text-[#526376] shadow-[0_8px_18px_rgba(112,128,118,0.08)]">
{{ pList.length }} 个站点
</div>
</div>
<div class="grid grid-cols-1 gap-5 md:grid-cols-2 2xl:grid-cols-3">
<article v-for="p in pList" :key="p.pid"
class="cursor-pointer rounded-[26px] border border-white/72 bg-[linear-gradient(145deg,rgba(255,251,246,0.78),rgba(237,246,244,0.62))] p-6 shadow-[0_16px_34px_rgba(116,132,124,0.12)] backdrop-blur-[18px] transition-[transform,box-shadow,border-color] duration-[320ms] [transition-timing-function:cubic-bezier(0.22,1,0.36,1)] hover:-translate-y-1 hover:border-white/90 hover:shadow-[0_22px_42px_rgba(102,123,112,0.16)]"
@click="gotoWebsite(p.url)">
<div class="flex items-center gap-8">
<n-avatar round :size="56" :src="p.avater"
class="shrink-0 ring-2 ring-white/80 shadow-[0_10px_22px_rgba(88,108,125,0.14)]" />
<div class="min-w-0 flex-1">
<div
class="inline-flex items-center rounded-full bg-[rgba(250,239,220,0.92)] px-3 py-1 text-sm text-primary">
{{ p.name }}
</div>
<h4 class="mt-3 truncate text-[1.3rem] font-semibold text-[#2f4050]">
{{ p.title }}
</h4>
<p class="mt-2 line-clamp-2 min-h-10 text-sm text-[#6a7a88]">
{{ p.desc }}
</p>
</div>
</div>
<div class="mt-5 flex flex-wrap gap-3 justify-center">
<div v-for="tag in getTags(p.tagname)" :key="tag" :style="{ backgroundColor: getTagColor(tag) }"
class="inline-flex items-center gap-1 rounded-full px-3 py-1 text-sm text-white shadow-[0_8px_20px_rgba(88,108,125,0.16)]">
<icon-tag2 class="h-4 w-4" />
{{ tag }}
</div>
</div>
</article>
</div>
<div v-if="!pList.length" class="py-14 text-center text-[#7a8797]">
暂无友链数据等你来留下第一张站点名片
</div>
</section>
<section
class="rounded-[26px] border border-white/66 bg-transparent p-5 ring-1 ring-inset ring-[rgba(255,255,255,0.32)] shadow-[0_16px_34px_rgba(116,132,124,0.09)] xl:hidden">
<div class="mb-4 flex items-center gap-3 text-primary">
<icon-loading class="zhuan h-5 w-5" />
<span class="text-base font-semibold">申请友链</span>
</div>
<n-form ref="mobileFormRef" :model="plinkData" :rules="rules" label-placement="top"
require-mark-placement="right-hanging">
<n-form-item v-for="field in formFields" :key="field.key" :path="field.key">
<template #label>
<div class="flex items-center gap-2 text-sm font-medium text-[#526376]">
<component :is="field.icon" class="h-4 w-4" />
{{ field.label }}
</div>
</template>
<n-input :value="plinkData[field.key]" :type="field.inputType" :rows="field.rows"
:maxlength="field.maxlength" :placeholder="field.placeholder"
@update:value="updateField(field.key, $event)" />
</n-form-item>
<n-form-item class="btn">
<n-button
class="!h-11 !w-full !rounded-full !border-none !bg-[linear-gradient(135deg,#ec66ab,#f48a6f)] !font-semibold shadow-[0_14px_28px_rgba(236,102,171,0.24)]"
type="primary" @click="submitPut">
提交友链申请
</n-button>
</n-form-item>
</n-form>
</section>
</div>
</n-scrollbar>
</section>
</div>
</div>
</template>
<script setup lang="ts">
import avaIcon from '@/icon/plink/ava.svg';
import descIcon from '@/icon/plink/desc.svg';
import siteIcon from '@/icon/plink/site.svg';
import tagIcon from '@/icon/plink/tag.svg';
import urlIcon from '@/icon/plink/uri.svg';
import userIcon from '@/icon/plink/user.svg';
import { getDictValue } from '@/util';
import avaIcon from '@/icon/plink/ava.svg'
import descIcon from '@/icon/plink/desc.svg'
import siteIcon from '@/icon/plink/site.svg'
import tagIcon from '@/icon/plink/tag.svg'
import urlIcon from '@/icon/plink/uri.svg'
import userIcon from '@/icon/plink/user.svg'
import type { FormInst, FormRules } from 'naive-ui'
import type { Component } from 'vue'
definePage({
name: 'plink',
@@ -119,109 +170,263 @@ definePage({
title: '友链',
}
})
// 友链页逻辑
const boxStyle: any = ref({})
const nav: any = $store.nav.useNavStore()
const plinkData = reactive<any>({
interface PlinkFormData {
name: string
title: string
avater: string
url: string
desc: string
tagname: string
}
interface PlinkItem {
pid: string | number
name: string
title: string
avater: string
url: string
desc: string
tagname: string
}
interface PlinkListResponse {
data: PlinkItem[]
}
interface TagColorItem {
tag: string
color: string
}
interface UserCookieInfo {
nickname?: string
ava_url?: string
}
type PlinkFieldKey = keyof PlinkFormData
interface PlinkFormField {
key: PlinkFieldKey
label: string
placeholder: string
icon: Component | string
inputType?: 'text' | 'textarea'
maxlength?: number
rows?: number
}
const SOFT_TAG_PALETTE = [
'#d46a7c',
'#c97956',
'#b9894d',
'#6f9d86',
'#5d92a8',
'#7d88b8',
'#9a78b5',
'#8f7d6b',
'#c86b8f',
'#cf7f6a',
'#a98f55',
'#7aa37b',
'#5d9f95',
'#6d8fc7',
'#8a80c4',
'#a16fa1',
'#b67b67',
'#927f72',
'#6f8b5f',
'#4f8aa8',
'#7a96a1',
'#b38754',
'#a66767',
'#7b6aa8',
] as const
const TEXTAREA_AUTOSIZE = {
minRows: 1,
maxRows: 2,
} as const
const formFields: PlinkFormField[] = [
{
key: 'name',
label: '昵称',
placeholder: '请输入你的昵称',
icon: userIcon,
inputType: 'text',
},
{
key: 'title',
label: '网站名称',
placeholder: '请输入你的网站名称',
icon: siteIcon,
inputType: 'text',
},
{
key: 'url',
label: '站点地址',
placeholder: '请输入你的网站地址',
icon: urlIcon,
inputType: 'text',
},
{
key: 'avater',
label: '头像链接',
placeholder: '请输入你的头像链接',
icon: avaIcon,
inputType: 'text',
},
{
key: 'desc',
label: '站点介绍',
placeholder: '请用一句话简短描述你的站点',
icon: descIcon,
inputType: 'textarea',
maxlength: 50,
rows:2
},
{
key: 'tagname',
label: '标签',
placeholder: '用逗号隔开,每个标签最多 4 个字,只显示前 3 个',
icon: tagIcon,
inputType: 'textarea',
maxlength: 50,
rows:2
},
]
const boxStyle = ref<Record<string, string>>({})
const contentStyle = ref<Record<string, string>>({})
const screenWidth = ref(typeof window === 'undefined' ? 0 : window.innerWidth)
const nav = $store.nav.useNavStore() as { navH: number }
const formRef = ref<FormInst | null>(null)
const mobileFormRef = ref<FormInst | null>(null)
const pList = ref<PlinkItem[]>([])
const tagColorList = ref<TagColorItem[]>([])
const plinkData = reactive<PlinkFormData>({
name: '',
title: '',
avater: '',
url: '',
desc: '',
tagname: '',
});
const rules: any = {
name: [{ required: true, message: '昵称不能为空~~', trigger: ['blur'] }],
title: [{ required: true, message: '网站名称不能为空~~', trigger: ['blur'] }],
url: [{ required: true, message: '站点地址不能为空~~', trigger: ['blur'] }],
avater: [{ required: true, message: '头像链接不能为空~~', trigger: ['blur'] }],
desc: [{ required: true, message: '站点介绍不能为空~~', trigger: ['blur'] }],
tagname: [{ required: true, message: '标签不能为空~~', trigger: ['blur'] }],
}
const pList = ref<any>([])
const formRef = ref<any>(null)
const tagColorList: any = ref([])
async function getPlinkList() {
const res = await $http.plink.getPlinkList()
const list: Array<string> = []
// 对res.data.tags进行去重
res.data.forEach((i: any) => {
const t = getTags(i.tagname)
t.forEach((ii: string) => {
if (!list.includes(ii)) list.push(ii)
})
});
})
tagColorList.value = list.map((i: string) => {
return {
tag: i,
color: getRandomFromPalette()
}
})
const rules: FormRules = {
name: [{ required: true, message: '昵称不能为空', trigger: ['blur'] }],
title: [{ required: true, message: '网站名称不能为空', trigger: ['blur'] }],
url: [{ required: true, message: '站点地址不能为空', trigger: ['blur'] }],
avater: [{ required: true, message: '头像链接不能为空', trigger: ['blur'] }],
desc: [{ required: true, message: '站点介绍不能为空', trigger: ['blur'] }],
tagname: [{ required: true, message: '标签不能为空', trigger: ['blur'] }],
}
function updateLayout() {
const viewportHeight = window.innerHeight - nav.navH - 1
screenWidth.value = window.innerWidth
boxStyle.value = { height: `${viewportHeight}px` }
contentStyle.value = { height: `${Math.max(viewportHeight - 48, 280)}px` }
}
function updateField(key: PlinkFieldKey, value: string) {
plinkData[key] = value
}
function normalizeTag(tag: string) {
return tag.trim().slice(0, 4)
}
function getTags(str: string) {
const normalized = (str || '').replace(//g, ',')
return normalized.split(',').slice(0, 3).map(normalizeTag).filter(Boolean)
}
function buildTagColorMap(tags: string[]): TagColorItem[] {
return tags.map((tag, index) => ({
tag,
color: SOFT_TAG_PALETTE[index % SOFT_TAG_PALETTE.length],
}))
}
function getTagColor(tag: string) {
return tagColorList.value.find((item) => item.tag === tag)?.color || SOFT_TAG_PALETTE[0]
}
async function getPlinkList() {
const res = await $http.plink.getPlinkList() as PlinkListResponse
const uniqueTags = Array.from(new Set(res.data.flatMap((item) => getTags(item.tagname))))
tagColorList.value = buildTagColorMap(uniqueTags)
pList.value = res.data
}
function getTags(str: any) {
str = str.replaceAll('', ',')
let tags = str.split(',').slice(0, 3)
tags.forEach((tag: any, idx: number) => {
tags[idx] = tag.trim().slice(0, 4)
});
return tags
}
function gotoWebsite(url: string) {
if (!url) return
window.open(url, '_blank')
}
function submitPut() {
formRef.value?.validate(async (valid: boolean) => {
if (valid) {
$msg.error('请完善表单~~')
} else {
const res = await $http.plink.addPlink(plinkData)
if (res?.code !== 200) {
$msg.error(res.msg)
return
}
$msg.success(res.msg)
plinkData.name = ''
plinkData.title = ''
plinkData.avater = ''
plinkData.url = ''
plinkData.desc = ''
plinkData.tagname = ''
getPlinkList()
}
})
function resetFormData() {
plinkData.name = ''
plinkData.title = ''
plinkData.avater = ''
plinkData.url = ''
plinkData.desc = ''
plinkData.tagname = ''
}
// 生成一个随机的鲜艳颜色,和白色能对比
function getRandomFromPalette(): string {
const hue = Math.floor(Math.random() * 360);
return hslToHex(hue, 80, 50);
}
function hslToHex(h: number, s: number, l: number): any {
l /= 100;
const a = s * Math.min(l, 1 - l) / 100;
const f = (n: number) => {
const k = (n + h / 30) % 12;
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return Math.round(255 * color).toString(16).padStart(2, '0');
function fillForm() {
const token = $cookies.get('token')
if (!token) {
$msg.warning('请先登录后再一键填入')
return
}
return `#${f(0)}${f(8)}${f(4)}`;
const userinfo = ($cookies.get('userinfo') || {}) as UserCookieInfo
plinkData.name = userinfo.nickname || ''
plinkData.avater = userinfo.ava_url || ''
}
function getActiveForm() {
if (screenWidth.value >= 1280) return formRef.value || mobileFormRef.value
return mobileFormRef.value || formRef.value
}
async function submitPut() {
const activeForm = getActiveForm()
if (!activeForm) return
try {
await activeForm.validate()
} catch {
$msg.error('请先完善表单内容')
return
}
const res = await $http.plink.addPlink(plinkData)
if (res?.code !== 200) {
$msg.error(res.msg)
return
}
$msg.success(res.msg)
resetFormData()
await getPlinkList()
}
onMounted(() => {
boxStyle.value = {
height: window.innerHeight - nav.navH - 1 + 'px',
}
getPlinkList()
updateLayout()
void getPlinkList()
window.addEventListener('resize', updateLayout)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', updateLayout)
})
</script>
<style scoped>
/* 无限旋转动画 */
<style scoped lang="less">
@keyframes spin {
0% {
transform: rotate(0deg);
@@ -236,8 +441,58 @@ onMounted(() => {
animation: spin 1.5s linear infinite;
}
:deep(.n-form-item) {
grid-template-rows: unset !important;
}
:deep(.btn.n-form-item) {
.n-form-item-feedback-wrapper {
display: none !important;
}
}
:deep(.compact-side-form .n-input) {
--n-height: 38px !important;
}
:deep(.n-form-item-feedback__line) {
text-align: right;
font-size: 12px !important;
color: #8b96a3;
}
</style>
:deep(.n-input),
:deep(.n-input .n-input-wrapper),
:deep(.n-input .n-input__textarea) {
background: rgba(255, 255, 255, 0.72) !important;
border-radius: 18px !important;
}
:deep(.n-input) {
--n-border: 1px solid rgba(214, 224, 226, 0.92) !important;
--n-border-hover: 1px solid rgba(190, 206, 214, 0.98) !important;
--n-border-focus: 1px solid rgba(236, 102, 171, 0.52) !important;
--n-box-shadow-focus: 0 0 0 4px rgba(236, 102, 171, 0.1) !important;
}
:deep(.n-input__input-el),
:deep(.n-input__textarea-el) {
color: #314155 !important;
}
:deep(.n-input__placeholder) {
color: #95a3b2 !important;
}
:deep(.n-input__placeholder) {
font-size: 13px;
span {
font-size: 13px;
}
}
:deep(.n-form-item-feedback__line) {
color: red;
}
</style>