feat: 阿里云 OSS 资源桶接入,商品图与icon支持远程基址
This commit is contained in:
@@ -226,6 +226,18 @@ npm run dev:h5
|
||||
- 小程序包体积长期保持 < 500KB;
|
||||
- 后续更换产品图只需在图床后台操作,无需重新发版。
|
||||
|
||||
### 阿里云 OSS 实际操作
|
||||
|
||||
1. 在阿里云 OSS 创建 Bucket,建议区域选择离用户近的国内地域(如 `oss-cn-hangzhou`),读写权限选择**公共读**;如果后续要用于用户上传,再配置单独 Bucket 或后端预签名。
|
||||
2. 把 `src/img/` 上传到 Bucket 根目录 `img/`,把 `src/icon/` 上传到 Bucket 根目录 `icon/`(保持路径一致,前端无需改文件名)。
|
||||
3. 打开 `src/utils/asset.ts`,将 `ASSET_BASE_URL` 填成桶地址:
|
||||
```ts
|
||||
export const ASSET_BASE_URL = 'https://your-bucket.oss-cn-hangzhou.aliyuncs.com'
|
||||
```
|
||||
4. 项目中商品图、首页成品图、商品 icon、设计清单/订单里已存的 icon 会自动拼接成远程 URL;本地路径作为兜底,后端没就绪时不裂图。
|
||||
5. 等 OSS 图片稳定后,可把 `config/index.js` 中 `src/img` 的 copy 规则移除,进一步压缩 dist 体积;`src/icon` 按需保留或也迁到 OSS。
|
||||
6. 微信小程序后台需要在「开发管理 - 服务器域名」配置 `downloadFile` 合法域名,且必须是 HTTPS。
|
||||
|
||||
---
|
||||
|
||||
## 最近更新记录(2026-08-06)
|
||||
@@ -307,7 +319,7 @@ npm run dev:h5
|
||||
- [ ] 订单物流接口接入(目前为静态 mock)
|
||||
|
||||
### 中优先级
|
||||
- [ ] 将实物照片迁移到 CDN(腾讯云 COS / 阿里云 OSS)
|
||||
- [ ] 上传商品图/icon 到阿里云 OSS,并填写 `ASSET_BASE_URL`(前端资源基址已接好)
|
||||
- [ ] 添加更多词云底图模板
|
||||
- [ ] 支持从微信聊天记录导入 Excel 名单
|
||||
- [ ] 实现设计稿保存到草稿箱功能
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback } from 'react'
|
||||
import './index.scss'
|
||||
import { getDesignList, removeDesigns, type DesignItem } from '../../utils/store'
|
||||
import { PRODUCT_ICON_MAP } from '../../utils/productConfig'
|
||||
import { assetUrl } from '../../utils/asset'
|
||||
import { useThemeContext } from '../../context/ThemeContext'
|
||||
import { useSafeArea } from '../../hooks/useSafeArea'
|
||||
import { useStatusBar } from '../../hooks/useStatusBar'
|
||||
@@ -164,9 +165,10 @@ export default function DesignListPage() {
|
||||
<ScrollView className='design-list' scrollY>
|
||||
{filtered.map(item => {
|
||||
const style = STATUS_STYLE[item.status]
|
||||
const iconSrc = item.productIcon?.startsWith('/icon/')
|
||||
const rawIcon = item.productIcon?.startsWith('/icon/') || /^https?:/i.test(item.productIcon || '')
|
||||
? item.productIcon
|
||||
: PRODUCT_ICON_MAP[item.productId] || '/icon/四角星.png'
|
||||
const iconSrc = assetUrl(rawIcon)
|
||||
const isSelected = selected.has(item.id)
|
||||
return (
|
||||
<View
|
||||
|
||||
+12
-11
@@ -3,6 +3,7 @@ import Taro from '@tarojs/taro'
|
||||
import { useState } from 'react'
|
||||
import './index.scss'
|
||||
import { CATEGORIES } from '../../utils/productConfig'
|
||||
import { assetUrl } from '../../utils/asset'
|
||||
import { useThemeContext } from '../../context/ThemeContext'
|
||||
import { useSafeArea } from '../../hooks/useSafeArea'
|
||||
import { useStatusBar } from '../../hooks/useStatusBar'
|
||||
@@ -10,21 +11,21 @@ import ThemedPageMeta from '../../components/ThemedPageMeta'
|
||||
|
||||
// 成品展示配图(从 img 里取对应实物图)
|
||||
const SHOWCASE_LIST = [
|
||||
{ title: '毕业纪念笔记本', desc: '全班名字组成校徽', image: '/img/book_small/The1.jpg' },
|
||||
{ title: '铜质杯垫', desc: '金属质感桌面艺术', image: '/img/cup/The1.jpg' },
|
||||
{ title: '竹制笔盒', desc: '自然竹纹文房雅器', image: '/img/penbox/The1.jpg' },
|
||||
{ title: '书本型灯', desc: '温暖光影点亮心意', image: '/img/booklight/The1.jpg' },
|
||||
{ title: '情侣定制礼', desc: '两个人的名字交织', image: '/img/book_big/The1.jpg' },
|
||||
{ title: '企业年会礼', desc: '员工名字组成Logo', image: '/img/penbox/The2.jpg' }
|
||||
{ title: '毕业纪念笔记本', desc: '全班名字组成校徽', image: assetUrl('/img/book_small/The1.jpg') },
|
||||
{ title: '铜质杯垫', desc: '金属质感桌面艺术', image: assetUrl('/img/cup/The1.jpg') },
|
||||
{ title: '竹制笔盒', desc: '自然竹纹文房雅器', image: assetUrl('/img/penbox/The1.jpg') },
|
||||
{ title: '书本型灯', desc: '温暖光影点亮心意', image: assetUrl('/img/booklight/The1.jpg') },
|
||||
{ title: '情侣定制礼', desc: '两个人的名字交织', image: assetUrl('/img/book_big/The1.jpg') },
|
||||
{ title: '企业年会礼', desc: '员工名字组成Logo', image: assetUrl('/img/penbox/The2.jpg') }
|
||||
]
|
||||
|
||||
// 品类对应的实物照片映射(首页卡片顶部大图)
|
||||
const PRODUCT_IMG_MAP: Record<string, string> = {
|
||||
'notebook-small': '/img/book_small/The1.jpg',
|
||||
'notebook-large': '/img/book_big/The1.jpg',
|
||||
'coaster': '/img/cup/The1.jpg',
|
||||
'penbox': '/img/penbox/The1.jpg',
|
||||
'booklamp': '/img/booklight/The1.jpg'
|
||||
'notebook-small': assetUrl('/img/book_small/The1.jpg'),
|
||||
'notebook-large': assetUrl('/img/book_big/The1.jpg'),
|
||||
'coaster': assetUrl('/img/cup/The1.jpg'),
|
||||
'penbox': assetUrl('/img/penbox/The1.jpg'),
|
||||
'booklamp': assetUrl('/img/booklight/The1.jpg')
|
||||
}
|
||||
|
||||
export default function Index() {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useStatusBar } from '../../hooks/useStatusBar'
|
||||
import ThemedPageMeta from '../../components/ThemedPageMeta'
|
||||
import { getOrderList, getDefaultAddress, getAddressList, type AddressItem, type OrderItem } from '../../utils/store'
|
||||
import { PRODUCT_ICON_MAP } from '../../utils/productConfig'
|
||||
import { assetUrl } from '../../utils/asset'
|
||||
|
||||
export default function OrderDetailPage() {
|
||||
const { theme, resolvedTheme } = useThemeContext()
|
||||
@@ -138,9 +139,9 @@ export default function OrderDetailPage() {
|
||||
<View className='star-badge' />
|
||||
<View className='product-row'>
|
||||
<View className='product-icon-wrapper'>
|
||||
<Image
|
||||
<Image
|
||||
className='product-icon-img'
|
||||
src={order.productIcon?.startsWith('/icon/') ? order.productIcon : '/icon/四角星.png'}
|
||||
src={assetUrl(order.productIcon?.startsWith('/icon/') || /^https?:/i.test(order.productIcon || '') ? order.productIcon : PRODUCT_ICON_MAP[order.productId] || '/icon/四角星.png')}
|
||||
mode='aspectFit'
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState, useEffect } from 'react'
|
||||
import './index.scss'
|
||||
import { getOrderList, type OrderItem } from '../../utils/store'
|
||||
import { PRODUCT_ICON_MAP } from '../../utils/productConfig'
|
||||
import { assetUrl } from '../../utils/asset'
|
||||
import { useThemeContext } from '../../context/ThemeContext'
|
||||
import { useSafeArea } from '../../hooks/useSafeArea'
|
||||
import { useStatusBar } from '../../hooks/useStatusBar'
|
||||
@@ -105,9 +106,10 @@ export default function OrdersPage() {
|
||||
<ScrollView className='orders-list' scrollY>
|
||||
{filteredOrders.map((order) => {
|
||||
const status = STATUS_MAP[order.statusCode] || STATUS_MAP.pending
|
||||
const iconSrc = order.productIcon?.startsWith('/icon/')
|
||||
const rawIcon = order.productIcon?.startsWith('/icon/') || /^https?:/i.test(order.productIcon || '')
|
||||
? order.productIcon
|
||||
: '/icon/四角星.png'
|
||||
const iconSrc = assetUrl(rawIcon)
|
||||
return (
|
||||
<View key={order.id} className='order-card dashed-card' onTap={() => goDetail(order)}>
|
||||
<View className='star-badge' />
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 静态资源基址。
|
||||
*
|
||||
* 留空表示继续使用小程序包内本地资源(/icon、/img);
|
||||
* 配置阿里云 OSS / CDN 后,所有经 assetUrl 处理的资源会自动拼接远程地址。
|
||||
*/
|
||||
export const ASSET_BASE_URL = ''
|
||||
|
||||
/** 把本地资源路径转成远程 URL;已经是 http(s) 或 wxfile/data 的保持不变 */
|
||||
export function assetUrl(path?: string | null): string {
|
||||
if (!path) return ''
|
||||
if (/^(https?:|wxfile:|data:)/i.test(path)) return path
|
||||
if (!ASSET_BASE_URL) return path
|
||||
const base = ASSET_BASE_URL.replace(/\/+$/, '')
|
||||
const normalized = path.startsWith('/') ? path : `/${path}`
|
||||
return `${base}${normalized}`
|
||||
}
|
||||
|
||||
/** 从旧存储中解析商品图标:兼容本地 /icon、远程 URL 和老版 emoji 缺省值 */
|
||||
export function resolveStoredIcon(
|
||||
value?: string,
|
||||
fallback = '/icon/四角星.png',
|
||||
): string {
|
||||
if (!value) return assetUrl(fallback)
|
||||
if (value.startsWith('/icon/') || /^https?:/i.test(value)) {
|
||||
return assetUrl(value)
|
||||
}
|
||||
return assetUrl(fallback)
|
||||
}
|
||||
@@ -6,8 +6,9 @@
|
||||
*/
|
||||
|
||||
import { ProductCategory } from '../types'
|
||||
import { assetUrl } from './asset'
|
||||
|
||||
export const PRODUCTS: ProductCategory[] = [
|
||||
const PRODUCTS_LOCAL: ProductCategory[] = [
|
||||
{
|
||||
id: 'notebook-small',
|
||||
name: '微雕笔记本(小)',
|
||||
@@ -169,6 +170,13 @@ export const PRODUCTS: ProductCategory[] = [
|
||||
}
|
||||
]
|
||||
|
||||
/** 导出产品列表:资源地址统一经过 assetUrl,配置 ASSET_BASE_URL 后自动切远程 */
|
||||
export const PRODUCTS = PRODUCTS_LOCAL.map(p => ({
|
||||
...p,
|
||||
images: (p.images || []).map(image => assetUrl(image)),
|
||||
iconImg: assetUrl(p.iconImg),
|
||||
}))
|
||||
|
||||
/** CATEGORIES 为 PRODUCTS 的别名,供页面引用 */
|
||||
export const CATEGORIES = PRODUCTS
|
||||
|
||||
@@ -184,11 +192,11 @@ export const getProductById = (id: string): ProductCategory | undefined => {
|
||||
* 用于兼容旧数据(旧数据 productIcon 存的是 emoji)
|
||||
*/
|
||||
export const PRODUCT_ICON_MAP: Record<string, string> = {
|
||||
'notebook-small': '/icon/书本.png',
|
||||
'notebook-large': '/icon/书本_大.png',
|
||||
'coaster': '/icon/杯子.png',
|
||||
'penbox': '/icon/笔盒.png',
|
||||
'booklamp': '/icon/书灯.png'
|
||||
'notebook-small': assetUrl('/icon/书本.png'),
|
||||
'notebook-large': assetUrl('/icon/书本_大.png'),
|
||||
'coaster': assetUrl('/icon/杯子.png'),
|
||||
'penbox': assetUrl('/icon/笔盒.png'),
|
||||
'booklamp': assetUrl('/icon/书灯.png')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -198,7 +206,7 @@ export const PRODUCT_ICON_MAP: Record<string, string> = {
|
||||
* - 兜底:返回四角星
|
||||
*/
|
||||
export function getProductIconImg(product: any): string {
|
||||
if (product?.iconImg) return product.iconImg
|
||||
if (product?.id) return PRODUCT_ICON_MAP[product.id] || '/icon/四角星.png'
|
||||
return '/icon/四角星.png'
|
||||
if (product?.iconImg) return assetUrl(product.iconImg)
|
||||
if (product?.id) return assetUrl(PRODUCT_ICON_MAP[product.id] || '/icon/四角星.png')
|
||||
return assetUrl('/icon/四角星.png')
|
||||
}
|
||||
|
||||
+5
-2
@@ -1,6 +1,7 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import type { DesignItem, OrderItem, AddressItem, StickerItem } from '../types'
|
||||
import { PRODUCT_ICON_MAP } from './productConfig'
|
||||
import { assetUrl } from './asset'
|
||||
|
||||
import { invalidateAuthCache } from './authState'
|
||||
// 重新导出类型,供各页面从 store 直接引用(修正原「声明但不导出」的编译错误)
|
||||
@@ -175,7 +176,7 @@ export function setDesignList(list: DesignItem[]) {
|
||||
|
||||
export function addDesign(product: any, count: number): DesignItem {
|
||||
const list = getDesignList()
|
||||
const iconImg = product?.iconImg || PRODUCT_ICON_MAP[product?.id] || '/icon/四角星.png'
|
||||
const iconImg = assetUrl(product?.iconImg || PRODUCT_ICON_MAP[product?.id] || '/icon/四角星.png')
|
||||
const item: DesignItem = {
|
||||
id: 'DSG' + Date.now(),
|
||||
productId: product.id,
|
||||
@@ -226,10 +227,12 @@ export function designToOrder(designId: string): OrderItem | null {
|
||||
const design = dList.find(d => d.id === designId)
|
||||
if (!design) return null
|
||||
|
||||
const iconImg = design.productIcon?.startsWith('/icon/')
|
||||
const rawIcon = design.productIcon?.startsWith('/icon/') || /^https?:/i.test(design.productIcon || '')
|
||||
? design.productIcon
|
||||
: PRODUCT_ICON_MAP[design.productId] || '/icon/四角星.png'
|
||||
|
||||
const iconImg = assetUrl(rawIcon)
|
||||
|
||||
const order: OrderItem = {
|
||||
id: 'ORD' + Date.now().toString().slice(-9),
|
||||
productName: design.productName,
|
||||
|
||||
Reference in New Issue
Block a user