refactor: 一些优化
continuous-integration/drone/push Build is passing Details

This commit is contained in:
AfyerCu 2025-11-19 22:50:28 +08:00
parent 5eba61b3b3
commit 9d100efbf4
26 changed files with 397 additions and 299 deletions

View File

@ -170,30 +170,13 @@ function toggleCollapse() {
//
function selectScore(score: number) {
if (scoreMode.value === 'onekey') {
//
currentScore.value = score
emit('score-selected', score)
submitScore(score)
}
else {
//
enableQuickScoreClickMode(score)
}
}
//
function addToScore(addValue: number) {
if (scoreMode.value === 'onekey') {
const newScore = Math.min(Math.max(currentScore.value + addValue, 0), props.fullScore)
currentScore.value = newScore
emit('score-selected', newScore)
submitScore(newScore)
}
else {
//
enableQuickScoreClickMode(Math.abs(addValue))
}
}
/**
@ -221,23 +204,8 @@ function enableQuickScoreClickMode(value: number) {
}
}
//
async function submitScore(score: number) {
try {
await markingData.submitRecord()
}
catch (error) {
console.error('提交分数失败:', error)
uni.showToast({
title: '提交失败',
icon: 'none',
})
}
}
//
async function submitCurrentScore() {
try {
// -1
if (currentScore.value === -1) {
if (currentMode.value === 'subtract') {
@ -250,24 +218,7 @@ async function submitCurrentScore() {
}
}
await markingData.submitRecord()
uni.showToast({
title: '提交成功',
icon: 'success',
})
//
if (settings.value.quickScoreClickMode) {
settings.value.quickScoreClickMode = false
settings.value.quickScoreClickValue = 0
}
}
catch (error) {
console.error('提交分数失败:', error)
uni.showToast({
title: '提交失败',
icon: 'none',
})
}
emit('score-selected', currentScore.value)
}
</script>

View File

@ -2,9 +2,9 @@
import type { DomMarkingData } from '../../composables/renderer/useMarkingDom'
import type { ExamStudentMarkingQuestionResponse } from '@/api'
import { parseOSSImageSize } from '@/utils/image'
import { MarkingTool } from '../../composables/renderer/useMarkingDom'
import { markingSettings } from '../../composables/useMarkingSettings'
import { useSimpleMarkingTool } from '../../composables/useSimpleMarkingTool'
import { MarkingTool } from '../../composables/renderer/useMarkingDom'
import QuestionRenderer from './QuestionRenderer.vue'
interface Props {
@ -46,10 +46,16 @@ const firstImageSize = ref({ width: 0, height: 0 })
// ID uni.createSelectorQuery
const scrollContainerId = `scroll-container-${Math.random().toString(36).slice(2)}`
const onTouchScale = ref(false)
/**
* 手势事件透传
*/
function handleTouchStart(e: TouchEvent) {
// Y
if (e.touches.length >= 2) {
onTouchScale.value = true
}
emit('touch-start', e)
}
@ -58,6 +64,7 @@ function handleTouchMove(e: TouchEvent) {
}
function handleTouchEnd(e: TouchEvent) {
onTouchScale.value = false
emit('touch-end', e)
}
@ -194,8 +201,8 @@ defineExpose({
<scroll-view
:id="scrollContainerId"
class="h-full w-full"
:scroll-y="scrollEnabled"
:scroll-x="scrollEnabled"
:scroll-y="scrollEnabled && !onTouchScale"
:scroll-x="scrollEnabled && !onTouchScale"
@scroll="handleScroll"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@ -209,7 +216,7 @@ defineExpose({
'items-center': markingSettings.imagePosition === 'center',
'items-end': markingSettings.imagePosition === 'right',
}"
:style="{ gap: '24px', padding: '12px' }"
:style="{ gap: '24px', padding: '12px 12px 48px' }"
>
<QuestionRenderer
v-for="(question, questionIndex) in questionDataList"

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import type { MarkingSubmitData } from '../../composables/useMarkingData'
import { useSessionStorage } from '@vueuse/core'
import { DictCode, useDict } from '@/composables/useDict'
import { svgToDataURL } from '@/utils/dom'
import { MarkingTool } from '../../composables/renderer/useMarkingDom'
import { markingSettings as settings } from '../../composables/useMarkingSettings'
@ -41,36 +41,146 @@ const emit = defineEmits<{
}>()
const currentTool = defineModel<MarkingTool>('currentTool', { required: true })
//
const toolbarRef = ref<HTMLElement>()
//
const isCollapsed = ref(uni.getStorageSync('marking-toolbar-collapsed') || false)
//
const shouldAnimate = ref(false)
watch(isCollapsed, (newVal) => {
uni.setStorageSync('marking-toolbar-collapsed', newVal)
//
if (!newVal) {
shouldAnimate.value = true
}
// / top/left anchored to top-left
})
const { getDictOptionsComputed } = useDict()
const { options: problemTypeOptions } = getDictOptionsComputed(DictCode.SCAN_ANOMALY_STATUS)
/**
* SVG 转换为 data URL用于小程序
*/
function svgToDataURL(svg: string): string {
svg = svg.replace(/data-(.*?=(['"]).*?\2)/g, '$1')
svg = svg.replace(/xlink-href=/g, 'xlink:href=')
svg = svg.replace(/view-box=/g, 'viewBox=')
svg = svg.replace(/<(title|desc|defs)>[\s\S]*?<\/\1>/g, '')
if (!/xmlns=/.test(svg))
svg = svg.replace(/<svg/, '<svg xmlns=\'http://www.w3.org/2000/svg\'')
svg = svg.replace(/\d+\.\d+/g, match => Number.parseFloat(Number.parseFloat(match).toFixed(2)) as any)
svg = svg.replace(/<!--[\s\S]*?-->/g, '')
svg = svg.replace(/\s+/g, ' ')
svg = svg.replace(/[{}|\\^~[\]`"<>#%]/g, (match) => {
return `%${match[0].charCodeAt(0).toString(16).toUpperCase()}`
})
svg = svg.replace(/'/g, '\\\'')
return `data:image/svg+xml,${svg.trim()}`
// --- Position & Dragging Logic ---
const position = ref<{ x: number, y: number } | null>(null)
const storageKey = computed(() => `marking-toolbar-pos-${props.isLandscape ? 'landscape' : 'portrait'}`)
//
function loadPosition() {
const stored = uni.getStorageSync(storageKey.value)
if (stored) {
position.value = stored
}
else {
//
const { windowHeight } = uni.getSystemInfoSync()
const offset = props.isLandscape ? 12 : 48 // CSS bottom/left
position.value = {
x: offset,
y: windowHeight - offset - (props.isLandscape ? 48 : 80), //
}
}
}
watch(() => props.isLandscape, () => {
loadPosition()
}, { immediate: true })
//
let isDragging = false
let hasMoved = false //
let startTouch = { x: 0, y: 0 }
let startPos = { x: 0, y: 0 }
let touchStartTime = 0 //
//
const CLICK_DISTANCE_THRESHOLD = 20 // 20px
const CLICK_TIME_THRESHOLD = 300 // 300ms
function onTouchStart(e: TouchEvent) {
if (e.touches.length !== 1)
return
//
// e.stopPropagation() // 使 .stop
const touch = e.touches[0]
startTouch = { x: touch.clientX, y: touch.clientY }
touchStartTime = Date.now()
isDragging = true
hasMoved = false
if (position.value) {
startPos = { ...position.value }
}
else {
// CSS
// 使 uni.createSelectorQuery
const query = uni.createSelectorQuery().in(getCurrentInstance())
query.select('.trace-toolbar').boundingClientRect((data) => {
const rect = data as UniApp.NodeInfo
if (rect) {
position.value = { x: rect.left || 0, y: rect.top || 0 }
startPos = { x: rect.left || 0, y: rect.top || 0 }
}
}).exec()
}
}
function onTouchMove(e: TouchEvent) {
if (!isDragging || e.touches.length !== 1)
return
const touch = e.touches[0]
const dx = touch.clientX - startTouch.x
const dy = touch.clientY - startTouch.y
const distance = Math.sqrt(dx * dx + dy * dy)
//
if (distance > CLICK_DISTANCE_THRESHOLD) {
hasMoved = true
}
// Query
if (!position.value && dx === 0 && dy === 0)
return
//
// position query
// startPos
// startPos 0,0 position null (Query )
// Query move
if (position.value) {
const newX = startPos.x + dx
const newY = startPos.y + dy
//
const { windowWidth, windowHeight } = uni.getSystemInfoSync()
//
// position.value = {
// x: Math.max(0, Math.min(newX, windowWidth - 40)),
// y: Math.max(0, Math.min(newY, windowHeight - 40))
// }
position.value = { x: newX, y: newY }
}
}
function onTouchEnd() {
const touchDuration = Date.now() - touchStartTime
//
const isClick = !hasMoved && touchDuration < CLICK_TIME_THRESHOLD
if (isDragging && position.value && !isClick) {
//
uni.setStorageSync(storageKey.value, position.value)
}
else if (isClick) {
//
position.value = startPos.x !== 0 || startPos.y !== 0 ? { ...startPos } : position.value
}
isDragging = false
hasMoved = false
}
/**
@ -179,46 +289,47 @@ defineExpose({
</script>
<template>
<wd-root-portal>
<div
ref="toolbarRef"
class="fixed z-5 flex flex-col items-start transition-all duration-300 ease-out"
class="trace-toolbar fixed z-15 flex flex-col items-start transition-opacity duration-300 ease-out"
:class="[
isLandscape
!position && (isLandscape
? 'bottom-12px left-1/2 -translate-x-1/2 items-center'
: 'bottom-48rpx left-12rpx items-start max-w-[75vw]',
: 'bottom-48rpx left-12rpx items-start'), // Removed max-w-[75vw]
]"
@touchend.stop
@touchmove.stop
@touchstart.stop
:style="position ? { left: `${position.x}px`, top: `${position.y}px` } : {}"
>
<!-- 收起状态 (FAB) -->
<!-- 收起状态 (FAB) - 可以拖拽 -->
<div
v-if="isCollapsed"
class="flex cursor-pointer items-center justify-center rounded-full bg-white shadow-lg transition-all active:scale-95"
:class="isLandscape ? 'size-48px' : 'size-80rpx'"
@click="toggleCollapse"
@click.stop="toggleCollapse"
@touchend.stop="onTouchEnd"
@touchmove.stop.prevent="onTouchMove"
@touchstart.stop="onTouchStart"
>
<div class="i-fluent:edit-24-regular text-blue-600" :class="isLandscape ? 'size-24px' : 'size-40rpx'" />
</div>
<!-- 展开状态 -->
<!-- 展开状态 - 不可拖拽 -->
<div
v-else
class="flex items-center rounded-full bg-white shadow-xl ring-1 ring-slate-900 ring-opacity-5 transition-all"
:class="[
isLandscape ? 'px-8px py-4px gap-4px' : 'pl-12rpx pr-6rpx py-10rpx gap-4rpx',
{ 'animate-fade-in-up': !isCollapsed },
isLandscape ? 'px-8px py-4px gap-4px' : 'pl-8rpx pr-4rpx py-8rpx gap-2rpx', //
]"
>
<!-- 收起按钮 -->
<div
class="flex flex-shrink-0 cursor-pointer items-center justify-center rounded-full text-slate-400 active:scale-95 hover:bg-slate-100"
:class="isLandscape ? 'size-32px' : 'size-60rpx'"
@click="toggleCollapse"
:class="isLandscape ? 'size-32px' : 'size-48rpx'"
@click.stop="toggleCollapse"
>
<!-- 竖屏尺寸减小 size-60rpx -> size-48rpx -->
<div
:class="[
isLandscape ? 'i-carbon:chevron-down size-20px' : 'i-carbon:chevron-left size-36rpx',
isLandscape ? 'i-carbon:chevron-down size-20px' : 'i-carbon:chevron-left size-32rpx', // icon
]"
/>
</div>
@ -226,26 +337,26 @@ defineExpose({
<!-- 分隔线 -->
<div class="h-4/5 w-1px flex-shrink-0 bg-slate-200" />
<!-- 工具内容区域 (竖屏时可滚动) -->
<scroll-view
:scroll-x="true"
:show-scrollbar="false"
<!-- 工具内容区域 (竖屏时不滚动全显示) -->
<!-- 移除 scroll-view改为 div flex -->
<div
class="flex whitespace-nowrap"
:class="isLandscape ? 'w-auto' : 'max-w-[65vw]'"
:class="isLandscape ? 'w-auto' : ''"
>
<div class="flex items-center" :class="isLandscape ? 'gap-6px' : 'gap-16rpx'">
<div class="flex items-center" :class="isLandscape ? 'gap-6px' : 'gap-8rpx'">
<!-- 标记组 -->
<div class="flex items-center rounded-full bg-slate-50" :class="isLandscape ? 'p-4px gap-4px' : 'p-6rpx gap-8rpx'">
<div class="flex items-center rounded-full bg-slate-50" :class="isLandscape ? 'p-4px gap-4px' : 'p-4rpx gap-4rpx'">
<!-- 竖屏 padding/gap 减小 -->
<wd-tooltip :content="isLandscape ? '正确' : ''" placement="top">
<div
class="flex cursor-pointer items-center justify-center rounded-full transition-all active:scale-95"
:class="[
currentTool === MarkingTool.CORRECT ? 'bg-green-500 text-white shadow-md' : 'text-slate-500 hover:bg-white hover:text-green-600',
isLandscape ? 'size-32px' : 'size-64rpx',
isLandscape ? 'size-32px' : 'size-48rpx', //
]"
@click="handleMarkTool('correct')"
@click.stop="handleMarkTool('correct')"
>
<div class="i-fluent:checkmark-24-regular" :class="isLandscape ? 'size-20px' : 'size-40rpx'" />
<div class="i-fluent:checkmark-24-regular" :class="isLandscape ? 'size-20px' : 'size-32rpx'" />
</div>
</wd-tooltip>
@ -254,11 +365,11 @@ defineExpose({
class="flex cursor-pointer items-center justify-center rounded-full transition-all active:scale-95"
:class="[
currentTool === MarkingTool.WRONG ? 'bg-red-500 text-white shadow-md' : 'text-slate-500 hover:bg-white hover:text-red-600',
isLandscape ? 'size-32px' : 'size-64rpx',
isLandscape ? 'size-32px' : 'size-48rpx',
]"
@click="handleMarkTool('wrong')"
@click.stop="handleMarkTool('wrong')"
>
<div class="i-fluent:dismiss-24-regular" :class="isLandscape ? 'size-20px' : 'size-40rpx'" />
<div class="i-fluent:dismiss-24-regular" :class="isLandscape ? 'size-20px' : 'size-32rpx'" />
</div>
</wd-tooltip>
@ -267,12 +378,12 @@ defineExpose({
class="flex cursor-pointer items-center justify-center rounded-full transition-all active:scale-95"
:class="[
currentTool === MarkingTool.HALF ? 'bg-yellow-500 text-white shadow-md' : 'text-slate-500 hover:bg-white hover:text-yellow-600',
isLandscape ? 'size-32px' : 'size-64rpx',
isLandscape ? 'size-32px' : 'size-48rpx',
]"
@click="handleMarkTool('half')"
@click.stop="handleMarkTool('half')"
>
<view
:class="isLandscape ? 'size-20px' : 'size-40rpx'"
:class="isLandscape ? 'size-20px' : 'size-32rpx'"
:style="{
backgroundImage: `url('${halfMarkSvg}')`,
backgroundSize: 'contain',
@ -288,17 +399,17 @@ defineExpose({
<div class="h-20px w-1px flex-shrink-0 bg-slate-200" />
<!-- 绘图组 -->
<div class="flex items-center rounded-full bg-slate-50" :class="isLandscape ? 'p-4px gap-4px' : 'p-6rpx gap-8rpx'">
<div class="flex items-center rounded-full bg-slate-50" :class="isLandscape ? 'p-4px gap-4px' : 'p-4rpx gap-4rpx'">
<wd-tooltip :content="isLandscape ? '画笔' : ''" placement="top">
<div
class="flex cursor-pointer items-center justify-center rounded-full transition-all active:scale-95"
:class="[
currentTool === MarkingTool.PEN ? 'bg-blue-600 text-white shadow-md' : 'text-slate-500 hover:bg-white hover:text-blue-600',
isLandscape ? 'size-32px' : 'size-64rpx',
isLandscape ? 'size-32px' : 'size-48rpx',
]"
@click="switchTool(MarkingTool.PEN)"
@click.stop="switchTool(MarkingTool.PEN)"
>
<div class="i-fluent:pen-24-regular" :class="isLandscape ? 'size-20px' : 'size-40rpx'" />
<div class="i-fluent:pen-24-regular" :class="isLandscape ? 'size-20px' : 'size-32rpx'" />
</div>
</wd-tooltip>
@ -307,11 +418,11 @@ defineExpose({
class="flex cursor-pointer items-center justify-center rounded-full transition-all active:scale-95"
:class="[
currentTool === MarkingTool.TEXT ? 'bg-blue-600 text-white shadow-md' : 'text-slate-500 hover:bg-white hover:text-blue-600',
isLandscape ? 'size-32px' : 'size-64rpx',
isLandscape ? 'size-32px' : 'size-48rpx',
]"
@click="switchTool(MarkingTool.TEXT)"
@click.stop="switchTool(MarkingTool.TEXT)"
>
<div class="i-fluent:text-24-regular" :class="isLandscape ? 'size-20px' : 'size-40rpx'" />
<div class="i-fluent:text-24-regular" :class="isLandscape ? 'size-20px' : 'size-32rpx'" />
</div>
</wd-tooltip>
@ -320,11 +431,11 @@ defineExpose({
class="flex cursor-pointer items-center justify-center rounded-full transition-all active:scale-95"
:class="[
currentTool === MarkingTool.ERASER ? 'bg-slate-600 text-white shadow-md' : 'text-slate-500 hover:bg-white hover:text-slate-700',
isLandscape ? 'size-32px' : 'size-64rpx',
isLandscape ? 'size-32px' : 'size-48rpx',
]"
@click="switchTool(MarkingTool.ERASER)"
@click.stop="switchTool(MarkingTool.ERASER)"
>
<div class="i-fluent:eraser-24-regular" :class="isLandscape ? 'size-20px' : 'size-40rpx'" />
<div class="i-fluent:eraser-24-regular" :class="isLandscape ? 'size-20px' : 'size-32rpx'" />
</div>
</wd-tooltip>
</div>
@ -333,7 +444,7 @@ defineExpose({
<div class="h-20px w-1px flex-shrink-0 bg-slate-200" />
<!-- 特殊标记组 -->
<div class="flex items-center" :class="isLandscape ? 'gap-6px' : 'gap-12rpx'">
<div class="flex items-center" :class="isLandscape ? 'gap-6px' : 'gap-6rpx'">
<div
v-for="type in ['excellent', 'typical', 'problem']"
:key="type"
@ -342,32 +453,16 @@ defineExpose({
specialMarks[type as keyof typeof specialMarks]
? (type === 'excellent' ? 'bg-yellow-100 text-yellow-700' : type === 'typical' ? 'bg-blue-100 text-blue-700' : 'bg-red-100 text-red-700')
: 'bg-slate-50 text-slate-500 hover:bg-slate-100',
isLandscape ? 'h-32px px-12px text-12px' : 'h-64rpx px-20rpx text-24rpx',
isLandscape ? 'h-32px px-12px text-12px' : 'h-48rpx px-12rpx text-20rpx', //
]"
@click="toggleSpecialMark(type as any)"
@click.stop="toggleSpecialMark(type as any)"
>
{{ type === 'excellent' ? '优秀' : type === 'typical' ? '典例' : '问题' }}
</div>
</div>
</div>
</scroll-view>
</div>
</div>
</div>
</wd-root-portal>
</template>
<style scoped>
.animate-fade-in-up {
animation: fadeInUp 0.2s ease-out;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View File

@ -1,4 +1,4 @@
import { useStorage } from '@vueuse/core'
import { useStorage, watchDeep } from '@vueuse/core'
const SETTINGS_VERSION = 2 // 当前版本号
@ -100,7 +100,7 @@ const defaultSettings: MarkingSettings = {
}
const rawSettings = ref<MarkingSettings>(uni.getStorageSync('marking_settings') || defaultSettings)
watch(rawSettings, (newVal) => {
watchDeep(rawSettings, (newVal) => {
uni.setStorageSync('marking_settings', newVal)
})

View File

@ -23,6 +23,7 @@ import { cachedImages, DefaultMarkingDataProvider, DefaultMarkingHistoryProvider
import { provideMarkingHistory } from '@/composables/marking/useMarkingHistory'
import { provideMarkingNavigation } from '@/composables/marking/useMarkingNavigation'
import { useSafeArea } from '@/composables/useSafeArea'
import { svgToDataURL } from '@/utils/dom'
defineOptions({
name: 'GradingPage',
@ -399,9 +400,41 @@ whenever(currentTask, (task, oldTask) => {
queryClient.invalidateQueries({ queryKey: ['marking-question', task.id] })
})
//
const showScoreFeedback = ref(false)
const feedbackScore = ref(0)
const doubleUnderlineSvg = `<svg width="100%" height="100%" viewBox="0 0 120 40" fill="none" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none">
<path d="M5 12 Q 60 5 115 12" stroke="red" stroke-width="5" stroke-linecap="round" fill="none" />
<path d="M10 28 Q 60 22 110 30" stroke="red" stroke-width="5" stroke-linecap="round" fill="none" />
</svg>`
//
function handleQuickScoreSelect(score: number) {
async function handleQuickScoreSelect(score: number) {
console.log('选择分数:', score)
//
markingData.currentScore.value = score
//
feedbackScore.value = score
showScoreFeedback.value = true
try {
await markingData.submitSingleRecord()
}
catch (error) {
console.error('提交失败:', error)
uni.showToast({
title: error.message || '提交失败',
icon: 'none',
})
}
finally {
//
setTimeout(() => {
showScoreFeedback.value = false
}, 400)
}
}
//
@ -511,7 +544,7 @@ const nextQuestionImages = computed(() => {
<template>
<div
class="relative h-screen w-100vw flex flex-col touch-pan-x touch-pan-y overflow-hidden overscroll-none"
:style="{ paddingTop: `${(safeAreaInsets?.top || 0)}px` }"
:style="{ paddingTop: `${(isLandscape ? 0 : safeAreaInsets?.top || 0)}px` }"
@touchstart="handleGlobalTouchStart"
@touchmove="handleGlobalTouchMove"
@touchend="handleGlobalTouchEnd"
@ -614,6 +647,18 @@ const nextQuestionImages = computed(() => {
v-model="showFullscreenImage"
:image-urls="currentImageUrls"
/>
<!-- 提交分数反馈动画 -->
<view
v-if="showScoreFeedback"
class="pointer-events-none fixed left-1/2 top-1/2 z-999 flex flex-col items-center justify-center text-red-500"
style="transform: translate(-50%, -50%) rotate(-10deg)"
>
<text class="text-96px font-bold leading-none" style="font-family: 'KaiTi', 'STKaiti', serif">
{{ feedbackScore }}
</text>
<view class="h-7 w-20" :style="{ backgroundImage: `url('${svgToDataURL(doubleUnderlineSvg)}')`, backgroundSize: 'contain', backgroundRepeat: 'no-repeat', backgroundPosition: 'center' }" />
</view>
</div>
</template>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 574 B

After

Width:  |  Height:  |  Size: 733 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 780 B

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 985 B

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
src/static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1,33 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_图层_2" data-name="图层 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 113.39 113.39">
<defs>
<style>
.cls-1 {
fill: none;
}
.cls-2 {
fill: #d14328;
}
.cls-3 {
fill: #2c8d3a;
}
</style>
</defs>
<g id="_图层_1-2" data-name="图层 1">
<g>
<rect class="cls-1" width="113.39" height="113.39" />
<g>
<path class="cls-3"
d="M86.31,11.34H25.08c-8.14,0-14.74,6.6-14.74,14.74v61.23c0,8.14,6.6,14.74,14.74,14.74h61.23c.12,0,.24-.02,.37-.02-9.76-.2-17.64-8.18-17.64-17.99,0-.56,.03-1.12,.08-1.67H34.1c-1.57,0-2.83-1.27-2.83-2.83V32.43c0-.78,.63-1.42,1.42-1.42h9.17c.78,0,1.42,.63,1.42,1.42v36.52c0,.78,.63,1.42,1.42,1.42h22.02c.78,0,1.42-.63,1.42-1.42V32.43c0-.78,.63-1.42,1.42-1.42h9.17c.78,0,1.42,.63,1.42,1.42v34.99c2.13-.89,4.47-1.39,6.92-1.39,5.66,0,10.7,2.63,14.01,6.72V26.08c0-8.14-6.6-14.74-14.74-14.74Z" />
<g>
<path class="cls-2"
d="M87.04,68.03c-8.83,0-16.01,7.18-16.01,16.01s7.18,16.01,16.01,16.01,16.01-7.18,16.01-16.01-7.18-16.01-16.01-16.01Zm-.27,24.84h-7.2v-3h1.18v-10.48h4.58v2.81h1.42c.84,0,1.46-.16,1.88-.48s.62-.87,.62-1.64c0-.69-.25-1.17-.74-1.45s-1.19-.42-2.09-.42h-6.84v-3h7.2c2.38,0,4.15,.38,5.31,1.15,1.16,.77,1.74,1.93,1.74,3.48,0,1.71-.83,2.93-2.5,3.64,1.07,.4,1.87,.95,2.39,1.65s.79,1.56,.79,2.58c0,3.44-2.58,5.16-7.73,5.16Z" />
<path class="cls-2"
d="M86.49,85.17h-1.16v4.7h1.8c.81,0,1.46-.18,1.94-.55s.72-.95,.72-1.73c0-.86-.25-1.48-.74-1.85s-1.35-.56-2.56-.56Z" />
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -145,3 +145,19 @@ export function getElementRectSync(element: HTMLElement | null): ElementRect | n
y: top,
}
}
export function svgToDataURL(svg: string): string {
svg = svg.replace(/data-(.*?=(['"]).*?\2)/g, '$1')
svg = svg.replace(/xlink-href=/g, 'xlink:href=')
svg = svg.replace(/view-box=/g, 'viewBox=')
svg = svg.replace(/<(title|desc|defs)>[\s\S]*?<\/\1>/g, '')
if (!/xmlns=/.test(svg))
svg = svg.replace(/<svg/, '<svg xmlns=\'http://www.w3.org/2000/svg\'')
svg = svg.replace(/\d+\.\d+/g, match => Number.parseFloat(Number.parseFloat(match).toFixed(2)) as any)
svg = svg.replace(/<!--[\s\S]*?-->/g, '')
svg = svg.replace(/\s+/g, ' ')
svg = svg.replace(/[{}|\\^~[\]`"<>#%]/g, (match) => {
return `%${match[0].charCodeAt(0).toString(16).toUpperCase()}`
})
svg = svg.replace(/'/g, '\\\'')
return `data:image/svg+xml,${svg.trim()}`
}

View File

@ -2,12 +2,28 @@
import { presetUni } from '@uni-helper/unocss-preset-uni'
import {
defineConfig,
definePreset,
presetAttributify,
presetIcons,
transformerDirectives,
transformerVariantGroup,
} from 'unocss'
export const presetRemToRpx = definePreset(() => {
const baseFontSize = 16
const remRE = /(-?[.\d]+)rem/g
return {
name: '@unocss/preset-rem-to-px',
postprocess: (util) => {
util.entries.forEach((i) => {
const value = i[1]
if (typeof value === 'string' && remRE.test(value))
i[1] = value.replace(remRE, (_, p1) => `${p1 * baseFontSize * 2}rpx`)
})
},
}
})
export default defineConfig({
presets: [
presetUni({
@ -26,6 +42,7 @@ export default defineConfig({
}),
// 支持css class属性化
presetAttributify(),
presetRemToRpx(),
],
transformers: [
// 启用指令功能:主要用于支持 @apply、@screen 和 theme() 等 CSS 指令