feat(worker-hall): 抢单大厅支持分类与全部按钮图标自定义及PNG透明通道修复
This commit is contained in:
@@ -225,12 +225,23 @@ export function refreshUploadedFileUrls(value: unknown) {
|
||||
}
|
||||
}
|
||||
|
||||
export const PUBLIC_SCENES = new Set(['category_icon', 'hall_icon', 'public', 'announcement'])
|
||||
|
||||
export function isPublicObjectKey(objectKey: string) {
|
||||
const normalized = normalizeObjectKey(objectKey)
|
||||
const firstSegment = normalized.split('/')[0] || ''
|
||||
return PUBLIC_SCENES.has(firstSegment)
|
||||
}
|
||||
|
||||
export function verifyFileAccessSignature(
|
||||
objectKey: string,
|
||||
expiresValue: unknown,
|
||||
signatureValue: unknown,
|
||||
nowSeconds = Math.floor(Date.now() / 1000),
|
||||
) {
|
||||
if (isPublicObjectKey(objectKey)) {
|
||||
return
|
||||
}
|
||||
const expires = Number(expiresValue || 0)
|
||||
const signature = String(signatureValue || '').trim()
|
||||
if (!Number.isSafeInteger(expires) || expires < nowSeconds || !signature) {
|
||||
@@ -388,22 +399,24 @@ async function generateImageVariants(objectKey: string, data: Buffer, contentTyp
|
||||
|
||||
for (const config of configs) {
|
||||
try {
|
||||
const content = await sharp(data)
|
||||
.rotate()
|
||||
.resize({
|
||||
width: config.maxSide,
|
||||
height: config.maxSide,
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true,
|
||||
})
|
||||
.flatten({ background: '#ffffff' })
|
||||
.jpeg({ quality: config.quality, mozjpeg: true })
|
||||
.toBuffer()
|
||||
const isAlpha = contentType === 'image/png' || contentType === 'image/webp'
|
||||
const pipeline = sharp(data).rotate().resize({
|
||||
width: config.maxSide,
|
||||
height: config.maxSide,
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true,
|
||||
})
|
||||
const content = isAlpha
|
||||
? await pipeline.webp({ quality: config.quality, alphaQuality: 95 }).toBuffer()
|
||||
: await pipeline
|
||||
.flatten({ background: '#ffffff' })
|
||||
.jpeg({ quality: config.quality, mozjpeg: true })
|
||||
.toBuffer()
|
||||
variants.push({
|
||||
name: config.name,
|
||||
key: imageVariantKey(objectKey, config.name),
|
||||
content,
|
||||
contentType: 'image/jpeg',
|
||||
contentType: isAlpha ? 'image/webp' : 'image/jpeg',
|
||||
})
|
||||
} catch {
|
||||
return variants
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { WORK_ORDER_STATUS } from '../../domain/work-order-status.js'
|
||||
import { countWorkOrders, listWorkerUsers } from '../../repositories/worker-platform/index.js'
|
||||
import { refreshStoredFileUrl } from '../file-storage/file-storage-service.js'
|
||||
import { publishWorkerHallRealtimeRefresh } from '../realtime/realtime-event-service.js'
|
||||
import { getWorkerHallConfig, saveWorkerHallConfig } from './worker-hall-config-service.js'
|
||||
|
||||
@@ -55,6 +56,7 @@ export async function getAdminWorkerHallConfig() {
|
||||
])
|
||||
return {
|
||||
...config,
|
||||
allIconUrl: config.allIconUrl ? refreshStoredFileUrl(config.allIconUrl) : null,
|
||||
visibleOrderCount: visibleOpenOrderCount,
|
||||
queuedOrderCount: Math.max(0, openOrderCount - visibleOpenOrderCount),
|
||||
}
|
||||
|
||||
@@ -17,7 +17,10 @@ import {
|
||||
} from '../../repositories/worker-platform/index.js'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
import { safeParseJson } from '../admin/admin-query-utils.js'
|
||||
import { refreshUploadedFileUrls } from '../file-storage/file-storage-service.js'
|
||||
import {
|
||||
refreshStoredFileUrl,
|
||||
refreshUploadedFileUrls,
|
||||
} from '../file-storage/file-storage-service.js'
|
||||
import { DEFAULT_DEPOSIT_THRESHOLD_AMOUNT } from './worker-finance-mappers.js'
|
||||
import { normalizeUploadedFiles } from './worker-input-normalizers.js'
|
||||
import { resolveFreezeDepositAmount } from './worker-permission-utils.js'
|
||||
@@ -34,7 +37,7 @@ export function mapWorkCategory(category: WorkCategoryRow | null | undefined) {
|
||||
categoryKey: category.category_key,
|
||||
name: category.name,
|
||||
parentId: category.parent_id ? Number(category.parent_id) : null,
|
||||
iconUrl: category.icon_url || null,
|
||||
iconUrl: category.icon_url ? refreshStoredFileUrl(category.icon_url) : null,
|
||||
sortOrder: Number(category.sort_order || 100),
|
||||
status: category.status,
|
||||
createdAt: category.created_at,
|
||||
|
||||
@@ -10,35 +10,45 @@ test('大厅配置默认展示 50 个订单', () => {
|
||||
assert.deepEqual(createDefaultWorkerHallConfig(), {
|
||||
maxVisibleOrders: 50,
|
||||
showSubcategories: true,
|
||||
allIconUrl: null,
|
||||
categoryMaxVisibleOrders: {},
|
||||
})
|
||||
assert.deepEqual(normalizeWorkerHallConfig({}), {
|
||||
maxVisibleOrders: 50,
|
||||
showSubcategories: true,
|
||||
allIconUrl: null,
|
||||
categoryMaxVisibleOrders: {},
|
||||
})
|
||||
assert.equal(normalizeWorkerHallConfig({ showSubcategories: false }).showSubcategories, false)
|
||||
assert.equal(
|
||||
normalizeWorkerHallConfig({ allIconUrl: ' https://example.com/icon.png ' }).allIconUrl,
|
||||
'https://example.com/icon.png',
|
||||
)
|
||||
})
|
||||
|
||||
test('大厅配置只接受 1 到 500 的整数', () => {
|
||||
assert.deepEqual(normalizeWorkerHallConfig({ maxVisibleOrders: 88 }), {
|
||||
maxVisibleOrders: 88,
|
||||
showSubcategories: true,
|
||||
allIconUrl: null,
|
||||
categoryMaxVisibleOrders: {},
|
||||
})
|
||||
assert.deepEqual(normalizeWorkerHallConfig({ max_visible_orders: 999 }), {
|
||||
maxVisibleOrders: 500,
|
||||
showSubcategories: true,
|
||||
allIconUrl: null,
|
||||
categoryMaxVisibleOrders: {},
|
||||
})
|
||||
assert.deepEqual(normalizeWorkerHallConfig({ maxVisibleOrders: 0 }), {
|
||||
maxVisibleOrders: 1,
|
||||
showSubcategories: true,
|
||||
allIconUrl: null,
|
||||
categoryMaxVisibleOrders: {},
|
||||
})
|
||||
assert.deepEqual(normalizeWorkerHallConfig({ maxVisibleOrders: 1.5 }), {
|
||||
maxVisibleOrders: 50,
|
||||
showSubcategories: true,
|
||||
allIconUrl: null,
|
||||
categoryMaxVisibleOrders: {},
|
||||
})
|
||||
assert.deepEqual(
|
||||
|
||||
@@ -5,6 +5,7 @@ import { readAppConfigEntry, saveAppConfigEntry } from '../config/app-config-sto
|
||||
export type WorkerHallConfig = {
|
||||
maxVisibleOrders: number
|
||||
showSubcategories: boolean
|
||||
allIconUrl: string | null
|
||||
categoryMaxVisibleOrders: Record<string, number>
|
||||
}
|
||||
|
||||
@@ -25,7 +26,12 @@ export async function saveWorkerHallConfig(rawValue: unknown): Promise<WorkerHal
|
||||
}
|
||||
|
||||
export function createDefaultWorkerHallConfig(): WorkerHallConfig {
|
||||
return { maxVisibleOrders: 50, showSubcategories: true, categoryMaxVisibleOrders: {} }
|
||||
return {
|
||||
maxVisibleOrders: 50,
|
||||
showSubcategories: true,
|
||||
allIconUrl: null,
|
||||
categoryMaxVisibleOrders: {},
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeWorkerHallConfig(rawValue: unknown): WorkerHallConfig {
|
||||
@@ -37,9 +43,20 @@ export function normalizeWorkerHallConfig(rawValue: unknown): WorkerHallConfig {
|
||||
: source.show_subcategories !== undefined
|
||||
? Boolean(source.show_subcategories)
|
||||
: true
|
||||
const allIconUrl =
|
||||
source.allIconUrl !== undefined
|
||||
? typeof source.allIconUrl === 'string' && source.allIconUrl.trim()
|
||||
? source.allIconUrl.trim()
|
||||
: null
|
||||
: source.all_icon_url !== undefined
|
||||
? typeof source.all_icon_url === 'string' && source.all_icon_url.trim()
|
||||
? source.all_icon_url.trim()
|
||||
: null
|
||||
: null
|
||||
return {
|
||||
maxVisibleOrders: Number.isInteger(value) ? Math.min(500, Math.max(1, value)) : 50,
|
||||
showSubcategories,
|
||||
allIconUrl,
|
||||
categoryMaxVisibleOrders: normalizeCategoryMaxVisibleOrders(
|
||||
source.categoryMaxVisibleOrders ?? source.category_max_visible_orders,
|
||||
),
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from '../../repositories/worker-platform/index.js'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
import { normalizePage, normalizePageSize } from '../admin/admin-query-utils.js'
|
||||
import { refreshStoredFileUrl } from '../file-storage/file-storage-service.js'
|
||||
import {
|
||||
mapWorkCategory,
|
||||
mapWorkOrderForWorker,
|
||||
@@ -91,6 +92,7 @@ export async function listWorkerHallOrders(query: JsonObject = {}, session: Work
|
||||
pagination: { page, pageSize, total },
|
||||
categories,
|
||||
showSubcategories: hallConfig.showSubcategories,
|
||||
allIconUrl: hallConfig.allIconUrl ? refreshStoredFileUrl(hallConfig.allIconUrl) : null,
|
||||
visibleDelaySeconds,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { UploadOutlined } from '@ant-design/icons'
|
||||
import {
|
||||
App,
|
||||
Avatar,
|
||||
@@ -6,11 +7,13 @@ import {
|
||||
Card,
|
||||
Descriptions,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Space,
|
||||
Switch,
|
||||
Table,
|
||||
Tag,
|
||||
Upload,
|
||||
} from 'antd'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
@@ -19,11 +22,13 @@ import {
|
||||
fetchAdminWorkerHallConfig,
|
||||
saveAdminWorkerHallConfig,
|
||||
} from '@/services/admin'
|
||||
import { uploadFile } from '@/services/files'
|
||||
import type { WorkCategory } from '@/types/worker-platform'
|
||||
|
||||
type HallConfigFormValues = {
|
||||
maxVisibleOrders: number
|
||||
showSubcategories: boolean
|
||||
allIconUrl?: string
|
||||
categoryMaxVisibleOrders: Record<string, number>
|
||||
}
|
||||
|
||||
@@ -32,6 +37,9 @@ export default function HallConfigPanel() {
|
||||
const queryClient = useQueryClient()
|
||||
const [form] = Form.useForm<HallConfigFormValues>()
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [uploadingAllIcon, setUploadingAllIcon] = useState(false)
|
||||
const currentAllIconUrl = Form.useWatch('allIconUrl', form)
|
||||
|
||||
const configQuery = useQuery({
|
||||
queryKey: ['admin-worker-hall-config'],
|
||||
queryFn: fetchAdminWorkerHallConfig,
|
||||
@@ -43,6 +51,7 @@ export default function HallConfigPanel() {
|
||||
form.setFieldsValue({
|
||||
maxVisibleOrders: config.maxVisibleOrders,
|
||||
showSubcategories: config.showSubcategories ?? true,
|
||||
allIconUrl: config.allIconUrl || '',
|
||||
categoryMaxVisibleOrders: config.categoryMaxVisibleOrders || {},
|
||||
})
|
||||
}
|
||||
@@ -74,12 +83,27 @@ export default function HallConfigPanel() {
|
||||
})
|
||||
}, [categoriesQuery.data?.data.items])
|
||||
|
||||
async function handleUploadAllIcon(file: File) {
|
||||
setUploadingAllIcon(true)
|
||||
try {
|
||||
const uploaded = await uploadFile(file, 'hall_icon', 'admin')
|
||||
form.setFieldValue('allIconUrl', uploaded.url)
|
||||
message.success('「全部」图标上传成功')
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '图标上传失败')
|
||||
} finally {
|
||||
setUploadingAllIcon(false)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async function saveConfig(values: HallConfigFormValues) {
|
||||
setSaving(true)
|
||||
try {
|
||||
await saveAdminWorkerHallConfig({
|
||||
maxVisibleOrders: Number(values.maxVisibleOrders),
|
||||
showSubcategories: Boolean(values.showSubcategories),
|
||||
allIconUrl: values.allIconUrl ? String(values.allIconUrl).trim() : null,
|
||||
categoryMaxVisibleOrders: values.categoryMaxVisibleOrders || {},
|
||||
})
|
||||
await Promise.all([
|
||||
@@ -96,7 +120,7 @@ export default function HallConfigPanel() {
|
||||
|
||||
return (
|
||||
<Card title="大厅配置" bordered={false} loading={configQuery.isLoading}>
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%', maxWidth: 640 }}>
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%', maxWidth: 700 }}>
|
||||
<Descriptions size="small" column={3}>
|
||||
<Descriptions.Item label="展示中">{config?.visibleOrderCount || 0}</Descriptions.Item>
|
||||
<Descriptions.Item label="等待入厅">{config?.queuedOrderCount || 0}</Descriptions.Item>
|
||||
@@ -110,7 +134,7 @@ export default function HallConfigPanel() {
|
||||
rules={[{ required: true, message: '请输入大厅订单总数' }]}
|
||||
style={{ marginBottom: 12 }}
|
||||
>
|
||||
<InputNumber min={1} max={500} precision={0} style={{ width: 160 }} />
|
||||
<InputNumber min={1} max={500} precision={0} style={{ width: 140 }} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="showSubcategories"
|
||||
@@ -121,6 +145,29 @@ export default function HallConfigPanel() {
|
||||
>
|
||||
<Switch checkedChildren="开启展示" unCheckedChildren="关闭展示" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="allIconUrl"
|
||||
label="「全部」按钮图标"
|
||||
tooltip="接单大厅第一个「全部」按钮展示的自定义图标。留空则显示默认应用图标。"
|
||||
style={{ marginBottom: 12, minWidth: 280 }}
|
||||
>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
{currentAllIconUrl ? (
|
||||
<Avatar
|
||||
src={currentAllIconUrl}
|
||||
size={32}
|
||||
shape="square"
|
||||
style={{ marginRight: 8, flexShrink: 0 }}
|
||||
/>
|
||||
) : null}
|
||||
<Input placeholder="图标 URL(可选)" allowClear />
|
||||
<Upload showUploadList={false} beforeUpload={handleUploadAllIcon} accept="image/*">
|
||||
<Button icon={<UploadOutlined />} loading={uploadingAllIcon}>
|
||||
上传
|
||||
</Button>
|
||||
</Upload>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
|
||||
<Form.Item>
|
||||
|
||||
@@ -79,6 +79,8 @@ export default function WorkerHallPage() {
|
||||
const [leaderboardPeriod, setLeaderboardPeriod] = useState<WorkerLeaderboardPeriod>('all')
|
||||
const [sharingQuantity, setSharingQuantity] = useState(1)
|
||||
const [submittingSharing, setSubmittingSharing] = useState(false)
|
||||
const [failedImageKeys, setFailedImageKeys] = useState<Record<string | number, boolean>>({})
|
||||
const [confirmOpen, setConfirmOpen] = useState(false)
|
||||
const [grabConfirmEnabled, setGrabConfirmEnabled] = useState(isGrabConfirmEnabled)
|
||||
const loadMoreRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
@@ -139,7 +141,15 @@ export default function WorkerHallPage() {
|
||||
const allCategories = firstPage?.categories || []
|
||||
const showSubcategoriesConfig = firstPage?.showSubcategories ?? true
|
||||
|
||||
const primaryCategories = allCategories.filter((c) => !c.parentId)
|
||||
const allCategoryFromList = allCategories.find(
|
||||
(c) => !c.parentId && (c.categoryKey === 'all' || c.name === '全部'),
|
||||
)
|
||||
const allIconUrl = firstPage?.allIconUrl || allCategoryFromList?.iconUrl || null
|
||||
|
||||
const primaryCategories = allCategories.filter(
|
||||
(c) => !c.parentId && c.categoryKey !== 'all' && c.name !== '全部',
|
||||
)
|
||||
const hasCategoryBar = primaryCategories.length > 0 || Boolean(allIconUrl)
|
||||
const subCategoriesMap = new Map<number, typeof allCategories>()
|
||||
for (const c of allCategories) {
|
||||
if (c.parentId) {
|
||||
@@ -365,7 +375,7 @@ export default function WorkerHallPage() {
|
||||
</div>
|
||||
|
||||
{/* 移动端一级分类/游戏卡片横向滑动条 */}
|
||||
{primaryCategories.length > 0 ? (
|
||||
{hasCategoryBar ? (
|
||||
<div className="worker-hall-game-cards-scroll">
|
||||
<button
|
||||
type="button"
|
||||
@@ -375,17 +385,26 @@ export default function WorkerHallPage() {
|
||||
<div
|
||||
className="worker-hall-game-icon-box"
|
||||
style={
|
||||
primaryCategoryId === 0
|
||||
primaryCategoryId === 0 && (!allIconUrl || failedImageKeys['all'])
|
||||
? { background: 'linear-gradient(135deg, #ff6a00 0%, #ff8e3c 100%)' }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<AppstoreOutlined
|
||||
style={{
|
||||
fontSize: 20,
|
||||
color: primaryCategoryId === 0 ? '#ffffff' : '#64748b',
|
||||
}}
|
||||
/>
|
||||
{allIconUrl && !failedImageKeys['all'] ? (
|
||||
<img
|
||||
src={allIconUrl}
|
||||
alt="全部"
|
||||
className="worker-hall-game-img"
|
||||
onError={() => setFailedImageKeys((prev) => ({ ...prev, all: true }))}
|
||||
/>
|
||||
) : (
|
||||
<AppstoreOutlined
|
||||
style={{
|
||||
fontSize: 20,
|
||||
color: primaryCategoryId === 0 ? '#ffffff' : '#64748b',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<span className="worker-hall-game-title">全部</span>
|
||||
</button>
|
||||
@@ -399,16 +418,22 @@ export default function WorkerHallPage() {
|
||||
<div
|
||||
className="worker-hall-game-icon-box"
|
||||
style={
|
||||
!category.iconUrl
|
||||
!category.iconUrl || failedImageKeys[category.categoryId]
|
||||
? { background: getCategoryGradient(category.name, category.categoryId) }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{category.iconUrl ? (
|
||||
{category.iconUrl && !failedImageKeys[category.categoryId] ? (
|
||||
<img
|
||||
src={category.iconUrl}
|
||||
alt={category.name}
|
||||
className="worker-hall-game-img"
|
||||
onError={() =>
|
||||
setFailedImageKeys((prev) => ({
|
||||
...prev,
|
||||
[category.categoryId]: true,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<span className="worker-hall-game-initial">{category.name.slice(0, 1)}</span>
|
||||
@@ -793,7 +818,7 @@ export default function WorkerHallPage() {
|
||||
|
||||
<Card className="worker-hall-board-card" bordered={false}>
|
||||
{/* 桌面端一级分类卡片与二级联动 */}
|
||||
{primaryCategories.length > 0 ? (
|
||||
{hasCategoryBar ? (
|
||||
<div style={{ marginBottom: 14 }}>
|
||||
<div className="worker-hall-game-cards-scroll">
|
||||
<button
|
||||
@@ -804,17 +829,26 @@ export default function WorkerHallPage() {
|
||||
<div
|
||||
className="worker-hall-game-icon-box"
|
||||
style={
|
||||
primaryCategoryId === 0
|
||||
primaryCategoryId === 0 && (!allIconUrl || failedImageKeys['all'])
|
||||
? { background: 'linear-gradient(135deg, #ff6a00 0%, #ff8e3c 100%)' }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<AppstoreOutlined
|
||||
style={{
|
||||
fontSize: 20,
|
||||
color: primaryCategoryId === 0 ? '#ffffff' : '#64748b',
|
||||
}}
|
||||
/>
|
||||
{allIconUrl && !failedImageKeys['all'] ? (
|
||||
<img
|
||||
src={allIconUrl}
|
||||
alt="全部"
|
||||
className="worker-hall-game-img"
|
||||
onError={() => setFailedImageKeys((prev) => ({ ...prev, all: true }))}
|
||||
/>
|
||||
) : (
|
||||
<AppstoreOutlined
|
||||
style={{
|
||||
fontSize: 20,
|
||||
color: primaryCategoryId === 0 ? '#ffffff' : '#64748b',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<span className="worker-hall-game-title">全部</span>
|
||||
</button>
|
||||
@@ -828,18 +862,24 @@ export default function WorkerHallPage() {
|
||||
<div
|
||||
className="worker-hall-game-icon-box"
|
||||
style={
|
||||
!category.iconUrl
|
||||
!category.iconUrl || failedImageKeys[category.categoryId]
|
||||
? {
|
||||
background: getCategoryGradient(category.name, category.categoryId),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{category.iconUrl ? (
|
||||
{category.iconUrl && !failedImageKeys[category.categoryId] ? (
|
||||
<img
|
||||
src={category.iconUrl}
|
||||
alt={category.name}
|
||||
className="worker-hall-game-img"
|
||||
onError={() =>
|
||||
setFailedImageKeys((prev) => ({
|
||||
...prev,
|
||||
[category.categoryId]: true,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<span className="worker-hall-game-initial">
|
||||
|
||||
@@ -89,7 +89,7 @@ export function fetchAdminWorkerHallConfig() {
|
||||
export function saveAdminWorkerHallConfig(
|
||||
payload: Pick<
|
||||
WorkerHallConfig,
|
||||
'maxVisibleOrders' | 'showSubcategories' | 'categoryMaxVisibleOrders'
|
||||
'maxVisibleOrders' | 'showSubcategories' | 'categoryMaxVisibleOrders' | 'allIconUrl'
|
||||
>,
|
||||
) {
|
||||
return apiPost<WorkerHallConfig>('/api/v1/admin/worker-platform/hall-config', payload)
|
||||
|
||||
@@ -304,6 +304,7 @@ export type WorkerAnnouncementConfig = {
|
||||
export type WorkerHallConfig = {
|
||||
maxVisibleOrders: number
|
||||
showSubcategories?: boolean
|
||||
allIconUrl?: string | null
|
||||
categoryMaxVisibleOrders: Record<string, number>
|
||||
visibleOrderCount: number
|
||||
queuedOrderCount: number
|
||||
@@ -718,6 +719,7 @@ export type WorkerHallOrdersResponse = {
|
||||
pagination: Pagination
|
||||
categories: WorkCategory[]
|
||||
showSubcategories?: boolean
|
||||
allIconUrl?: string | null
|
||||
visibleDelaySeconds?: number
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,11 @@ const IMAGE_UPLOAD_QUALITY: Record<string, number> = {
|
||||
}
|
||||
|
||||
export async function optimizeImageForUpload(file: File, scene: string) {
|
||||
// 分类图标与大厅图标属于透明或矢量UI素材,必须保留原始格式与透明通道,不进行有损压缩与白底填充
|
||||
if (['category_icon', 'hall_icon'].includes(scene)) {
|
||||
return file
|
||||
}
|
||||
|
||||
if (!file.type.startsWith('image/')) return file
|
||||
if (!['image/jpeg', 'image/png', 'image/webp'].includes(file.type)) return file
|
||||
if (typeof document === 'undefined') return file
|
||||
@@ -26,8 +31,13 @@ export async function optimizeImageForUpload(file: File, scene: string) {
|
||||
canvas.height = height
|
||||
const context = canvas.getContext('2d')
|
||||
if (!context) return file
|
||||
context.fillStyle = '#ffffff'
|
||||
context.fillRect(0, 0, width, height)
|
||||
|
||||
// 仅在原图为 JPEG 且不支持透明度时才填充白底;PNG 与 WebP 必须保留透明通道
|
||||
if (file.type === 'image/jpeg') {
|
||||
context.fillStyle = '#ffffff'
|
||||
context.fillRect(0, 0, width, height)
|
||||
}
|
||||
|
||||
context.drawImage(image, 0, 0, width, height)
|
||||
const blob = await canvasToBlob(canvas, 'image/webp', quality)
|
||||
if (!blob || blob.size >= file.size) return file
|
||||
|
||||
Reference in New Issue
Block a user