+
@@ -11,9 +16,15 @@ import { computed } from "vue";
const props = defineProps<{ res: any }>();
-const list = computed(() =>
- props.res.list[0].title.map((i: { context: string }) => i.context)
-);
+const noticeStyle = computed(() => props.res?.list?.[0] || {});
+
+const list = computed(() => {
+ const titles = props.res?.list?.[0]?.title;
+ if (!Array.isArray(titles)) return [];
+ return titles
+ .map((item: { context?: string }) => (item?.context == null ? "" : String(item.context)))
+ .filter((text: string) => text.length > 0);
+});
+
+
+
+
+
+
+
+
+
+ 地址管理
+
+
+
+
+ 我的足迹
+
+
+
+
+ 我的评价
+
+
+
+ 我的消息
+
+
+
+
+
+ 我的关注
+
+
+
+
+
+
+ 我的积分
+
+
+
+
+ 我的分销
+
+
+
+
+
+
+ 我的投诉
+
+
+
+
+ 优惠券
+
+
+
+
+
+
+
+ 每日签到
+
+
+
+
+
+
+
+
+
+ 领券中心
+
+
+
+
+
+
+ 砍价记录
+
+
+
+
+
+
+ 意见反馈
+
+
+
+
+ 关于
+
+
+
+
+ 店铺入驻
+
+
+
+
+
+
+
+
+
+ 设置
+
+
+
+
+
+
+
+
+ 请邀请用户扫描二维码或者将地址复制转发给其他用户
+
+
+
+
+
+
+ {{sharingLink}}
+
+
+ 关闭
+
+
+
+
+
+
+
+
+
+
diff --git a/utils/distributionBind.js b/utils/distributionBind.js
new file mode 100644
index 0000000..add8060
--- /dev/null
+++ b/utils/distributionBind.js
@@ -0,0 +1,190 @@
+/**
+ * 分销海报 / 商品分享扫码绑客(统一入口)
+ */
+import storage from '@/utils/storage.js'
+import store from '@/store'
+import { getGoodsDistribution } from '@/api/distribution.js'
+import { getMpScene } from '@/api/goods.js'
+
+const PENDING_KEY = 'pending_distribution_id'
+
+/** 本次冷启动内已尝试绑定的分销员 ID,避免重复请求 */
+const launchBoundIds = new Set()
+
+let flushing = false
+
+export function clearPendingDistributionId() {
+ try {
+ uni.removeStorageSync(PENDING_KEY)
+ } catch (e) {
+ // ignore
+ }
+}
+
+export function setPendingDistributionId(distributionId) {
+ if (!distributionId) return
+ try {
+ uni.setStorageSync(PENDING_KEY, String(distributionId))
+ } catch (e) {
+ console.warn('set pending distributionId failed', e)
+ }
+}
+
+export function getPendingDistributionId() {
+ try {
+ return uni.getStorageSync(PENDING_KEY) || ''
+ } catch (e) {
+ return ''
+ }
+}
+
+export function resetLaunchBindCache() {
+ launchBoundIds.clear()
+}
+
+/**
+ * 解析葵花码 scene:支持 shortLinkId 或明文 bind,{distributionId}
+ */
+export async function resolveDistributionIdFromScene(scene) {
+ if (!scene) return ''
+ const raw = decodeURIComponent(String(scene)).trim()
+ if (!raw) return ''
+
+ if (raw.startsWith('bind,')) {
+ const id = raw.split(',')[1]
+ return id ? String(id).trim() : ''
+ }
+
+ try {
+ const res = await getMpScene(raw)
+ const params = res?.data?.success ? String(res.data.result || '').trim() : ''
+ if (params.startsWith('bind,')) {
+ const id = params.split(',')[1]
+ return id ? String(id).trim() : ''
+ }
+ } catch (error) {
+ console.warn('resolve distribution scene failed', error)
+ }
+ return ''
+}
+
+function parseRecruitIdFromParams(params) {
+ if (!params) return ''
+ const text = String(params).trim()
+ if (text.startsWith('recruit_')) {
+ const id = text.substring('recruit_'.length)
+ return id ? String(id).trim() : ''
+ }
+ return ''
+}
+
+/**
+ * 解析图文邀请卡 scene:支持 shortLinkId 或明文 recruit_{distributionId}
+ */
+export async function resolveRecruitDistributionIdFromScene(scene) {
+ if (!scene) return ''
+ const raw = decodeURIComponent(String(scene)).trim()
+ if (!raw) return ''
+
+ const direct = parseRecruitIdFromParams(raw)
+ if (direct) return direct
+
+ try {
+ const res = await getMpScene(raw)
+ const params = res?.data?.success ? String(res.data.result || '').trim() : ''
+ return parseRecruitIdFromParams(params)
+ } catch (error) {
+ console.warn('resolve recruit scene failed', error)
+ }
+ return ''
+}
+
+const CLEAR_PENDING_CODES = new Set([22000, 22001])
+
+function shouldClearPendingOnFailure(res) {
+ const code = res?.data?.code
+ if (code != null && CLEAR_PENDING_CODES.has(Number(code))) {
+ return true
+ }
+ const message = String(res?.data?.message || '')
+ if (message.includes('分销员不存在') || message.includes('分销功能关闭')) {
+ return true
+ }
+ return false
+}
+
+/**
+ * 尝试绑定分销员:已登录直接请求;未登录写入 pending
+ */
+export async function tryBindDistribution(distributionId, options = {}) {
+ const id = String(distributionId || '').trim()
+ if (!id) return
+
+ const force = options.force === true
+ if (!force && launchBoundIds.has(id)) {
+ return
+ }
+ launchBoundIds.add(id)
+
+ if (!storage.getAccessToken()) {
+ setPendingDistributionId(id)
+ store.state.distributionId = id
+ return
+ }
+
+ try {
+ const res = await getGoodsDistribution(id)
+ if (res?.data?.success) {
+ clearPendingDistributionId()
+ store.state.distributionId = id
+ return
+ }
+ if (shouldClearPendingOnFailure(res)) {
+ clearPendingDistributionId()
+ }
+ } catch (error) {
+ console.warn('bind distribution failed', error)
+ const res = error?.data ? error : error?.response?.data ? { data: error.response.data } : null
+ if (res && shouldClearPendingOnFailure(res)) {
+ clearPendingDistributionId()
+ }
+ }
+}
+
+/**
+ * 登录成功后补绑 pending
+ */
+export async function flushPendingBind() {
+ if (flushing) return
+ const pendingId = getPendingDistributionId()
+ if (!pendingId || !storage.getAccessToken()) {
+ return
+ }
+ flushing = true
+ try {
+ launchBoundIds.delete(pendingId)
+ await tryBindDistribution(pendingId, { force: true })
+ } finally {
+ flushing = false
+ }
+}
+
+/**
+ * App onLaunch:有 scene 则解析绑客;无 scene 清除 pending
+ */
+export async function handleAppLaunchScene(options) {
+ resetLaunchBindCache()
+
+ const scene = options?.query?.scene
+ if (!scene) {
+ clearPendingDistributionId()
+ return
+ }
+
+ const distributionId = await resolveDistributionIdFromScene(scene)
+ if (!distributionId) {
+ return
+ }
+
+ await tryBindDistribution(distributionId)
+}
diff --git a/utils/distributionPosterCanvas.ts b/utils/distributionPosterCanvas.ts
new file mode 100644
index 0000000..658b534
--- /dev/null
+++ b/utils/distributionPosterCanvas.ts
@@ -0,0 +1,289 @@
+import { nextTick } from 'vue'
+import DrawPoster from '@/js_sdk/u-draw-poster'
+
+export const POSTER_WIDTH = 630
+export const POSTER_HEIGHT = 1000
+
+const QR_SIZE = 180
+const AVATAR_SIZE = 86
+const PADDING_H = 24
+const MEMBER_LEFT = PADDING_H + 16
+const BOTTOM_RATIO = 0.06
+const MEMBER_ROW_GAP = 12
+const NAME_FONT_SIZE = 30
+const SLOGAN_FONT_SIZE = 24
+const NAME_LINE_HEIGHT = 38
+const SLOGAN_LINE_HEIGHT = 32
+
+// #ifdef MP-WEIXIN
+const st2 = (size: number) => size * 2
+// #endif
+// #ifndef MP-WEIXIN
+const st2 = (size: number) => size
+// #endif
+
+export interface DistributionPosterOptions {
+ backgroundImage?: string
+ memberAvatar?: string
+ memberName?: string
+ slogan?: string
+ qrImage: string
+ textColor?: string
+ showMemberInfo?: boolean
+}
+
+function sleep(ms: number) {
+ return new Promise((resolve) => setTimeout(resolve, ms))
+}
+
+export async function resolveImagePath(url: string) {
+ if (!url) {
+ throw new Error('图片地址无效')
+ }
+ if (
+ url.startsWith('data:') ||
+ url.startsWith('wxfile://') ||
+ url.startsWith('http://tmp/') ||
+ url.startsWith('https://tmp/') ||
+ url.startsWith('file://')
+ ) {
+ return url
+ }
+ const res = await uni.downloadFile({ url })
+ if (res.statusCode === 200 && res.tempFilePath) {
+ return res.tempFilePath
+ }
+ throw new Error('图片下载失败,请检查图片域名配置')
+}
+
+function getImageInfo(src: string): Promise<{ width: number; height: number }> {
+ return new Promise((resolve, reject) => {
+ uni.getImageInfo({
+ src,
+ success: (res) => resolve({ width: res.width, height: res.height }),
+ fail: reject,
+ })
+ })
+}
+
+function calcCoverBox(imgW: number, imgH: number, boxW: number, boxH: number) {
+ const scale = Math.max(boxW / imgW, boxH / imgH)
+ const width = imgW * scale
+ const height = imgH * scale
+ return {
+ x: (boxW - width) / 2,
+ y: (boxH - height) / 2,
+ width,
+ height,
+ }
+}
+
+export function clearDrawPosterCache(selector: string) {
+ const pages = getCurrentPages()
+ const page = pages[pages.length - 1] as any
+ if (!page) return
+ delete page[`#${selector}__dp`]
+ delete page[`${selector}__dp`]
+}
+
+async function waitCanvasReady() {
+ await nextTick()
+ await sleep(150)
+}
+
+async function drawCircleImage(ctx: any, src: string, x: number, y: number, size: number) {
+ const radius = size / 2
+ const cx = x + radius
+ const cy = y + radius
+ ctx.save()
+ ctx.beginPath()
+ ctx.arc(st2(cx), st2(cy), st2(radius), 0, Math.PI * 2)
+ ctx.clip()
+ await ctx.drawImage(src, st2(x), st2(y), st2(size), st2(size))
+ ctx.restore()
+ ctx.save()
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)'
+ ctx.lineWidth = st2(2)
+ ctx.beginPath()
+ ctx.arc(st2(cx), st2(cy), st2(radius - 1), 0, Math.PI * 2)
+ ctx.stroke()
+ ctx.restore()
+}
+
+export async function composeDistributionPoster(
+ selector: string,
+ componentThis: any,
+ options: DistributionPosterOptions
+): Promise
{
+ const {
+ backgroundImage = '',
+ memberAvatar = '',
+ memberName = '分销员',
+ slogan = '',
+ qrImage,
+ textColor = '#1f2a44',
+ showMemberInfo = true,
+ } = options
+
+ if (!qrImage) {
+ throw new Error('葵花码未生成')
+ }
+
+ clearDrawPosterCache(selector)
+ await waitCanvasReady()
+
+ const dp = await DrawPoster.build({
+ selector,
+ componentThis,
+ loading: false,
+ debugging: false,
+ })
+
+ if (!dp?.canvas || !dp?.ctx) {
+ throw new Error('画布初始化失败,请重试')
+ }
+
+ // #ifdef MP-WEIXIN
+ dp.canvas.width = st2(POSTER_WIDTH)
+ dp.canvas.height = st2(POSTER_HEIGHT)
+ // #endif
+
+ const localQr = await resolveImagePath(qrImage)
+ let localBg = ''
+ if (backgroundImage) {
+ try {
+ localBg = await resolveImagePath(backgroundImage)
+ } catch (error) {
+ console.warn('poster background download failed', error)
+ }
+ }
+ let localAvatar = ''
+ if (showMemberInfo && memberAvatar) {
+ try {
+ localAvatar = await resolveImagePath(memberAvatar)
+ } catch (error) {
+ console.warn('poster avatar download failed', error)
+ }
+ }
+
+ const bottomOffset = POSTER_HEIGHT * BOTTOM_RATIO
+ const rowBottom = POSTER_HEIGHT - bottomOffset
+ const qrX = POSTER_WIDTH - PADDING_H - QR_SIZE
+ const qrY = rowBottom - QR_SIZE
+
+ await dp.draw((ctx: any) => {
+ ctx.fillStyle = '#ffffff'
+ ctx.fillRect(st2(0), st2(0), st2(POSTER_WIDTH), st2(POSTER_HEIGHT))
+ })
+
+ if (localBg) {
+ await dp.draw(async (ctx: any) => {
+ const { width: imgW, height: imgH } = await getImageInfo(localBg)
+ const fit = calcCoverBox(imgW, imgH, POSTER_WIDTH, POSTER_HEIGHT)
+ await ctx.drawImage(
+ localBg,
+ st2(fit.x),
+ st2(fit.y),
+ st2(fit.width),
+ st2(fit.height)
+ )
+ })
+ }
+
+ await dp.draw(async (ctx: any) => {
+ ctx.fillStyle = '#ffffff'
+ ctx.fillRoundRect(st2(qrX), st2(qrY), st2(QR_SIZE), st2(QR_SIZE), st2(12))
+ await ctx.drawImage(localQr, st2(qrX), st2(qrY), st2(QR_SIZE), st2(QR_SIZE))
+ })
+
+ if (showMemberInfo) {
+ await dp.draw(async (ctx: any) => {
+ const textMaxWidth = qrX - MEMBER_LEFT - 16
+ const memberBlockHeight =
+ AVATAR_SIZE +
+ MEMBER_ROW_GAP +
+ NAME_LINE_HEIGHT +
+ (slogan ? MEMBER_ROW_GAP + SLOGAN_LINE_HEIGHT : 0)
+ const blockHeight = Math.max(memberBlockHeight, QR_SIZE)
+ const blockTop = rowBottom - blockHeight
+
+ const avatarX = MEMBER_LEFT
+ const avatarY = blockTop
+ const nameY = avatarY + AVATAR_SIZE + MEMBER_ROW_GAP
+ const sloganY = nameY + NAME_LINE_HEIGHT + MEMBER_ROW_GAP
+
+ if (localAvatar) {
+ await drawCircleImage(ctx, localAvatar, avatarX, avatarY, AVATAR_SIZE)
+ } else {
+ const radius = AVATAR_SIZE / 2
+ ctx.save()
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'
+ ctx.beginPath()
+ ctx.arc(st2(avatarX + radius), st2(avatarY + radius), st2(radius), 0, Math.PI * 2)
+ ctx.fill()
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)'
+ ctx.lineWidth = st2(2)
+ ctx.stroke()
+ ctx.restore()
+ }
+
+ ctx.textAlign = 'left'
+ ctx.textBaseline = 'top'
+ ctx.fillStyle = textColor
+ ctx.font = `bold ${st2(NAME_FONT_SIZE)}px PingFang SC`
+ ctx.fillWarpText({
+ text: memberName,
+ maxWidth: st2(textMaxWidth),
+ x: st2(avatarX),
+ y: st2(nameY),
+ layer: 1,
+ lineHeight: st2(NAME_LINE_HEIGHT),
+ })
+
+ if (slogan) {
+ ctx.font = `${st2(SLOGAN_FONT_SIZE)}px PingFang SC`
+ ctx.fillWarpText({
+ text: slogan,
+ maxWidth: st2(textMaxWidth),
+ x: st2(avatarX),
+ y: st2(sloganY),
+ layer: 1,
+ lineHeight: st2(SLOGAN_LINE_HEIGHT),
+ })
+ }
+ })
+ }
+
+ const path = await dp.createImagePath()
+ if (!path || path === '---stop createImagePath---') {
+ throw new Error('海报图片导出失败')
+ }
+ return path
+}
+
+export function savePosterToAlbum(filePath: string) {
+ if (!filePath) return
+ uni.saveImageToPhotosAlbum({
+ filePath,
+ success: () => {
+ uni.showToast({ title: '已保存到相册', icon: 'none' })
+ },
+ fail: (err: any) => {
+ const errMsg = err?.errMsg || ''
+ if (errMsg.includes('auth deny') || errMsg.includes('authorize')) {
+ uni.showModal({
+ title: '提示',
+ content: '需要您授权保存图片到相册',
+ confirmText: '去设置',
+ success: (modalRes) => {
+ if (modalRes.confirm) {
+ uni.openSetting({})
+ }
+ },
+ })
+ return
+ }
+ uni.showToast({ title: '保存失败', icon: 'none' })
+ },
+ })
+}
diff --git a/utils/filters.js b/utils/filters.js
index 32087e1..4b3a8a0 100644
--- a/utils/filters.js
+++ b/utils/filters.js
@@ -528,6 +528,48 @@ export function serviceStatusList (val) {
return statusList[val];
}
+/**
+ * 格式化商品规格展示(兼容 SKU JSON 与纯文本)
+ */
+export function formatGoodsSpecs(specs) {
+ if (specs === null || specs === undefined) {
+ return '默认'
+ }
+ const trimmed = String(specs).trim()
+ if (!trimmed) {
+ return '默认'
+ }
+ if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
+ return trimmed
+ }
+ try {
+ const data = JSON.parse(trimmed)
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
+ return trimmed
+ }
+ const parts = []
+ Object.keys(data).forEach((key) => {
+ if (key === 'images') {
+ return
+ }
+ const value = data[key]
+ if (value === null || value === undefined || value === '') {
+ return
+ }
+ if (typeof value === 'object') {
+ return
+ }
+ const text = String(value).trim()
+ if (text) {
+ parts.push(text)
+ }
+ })
+ return parts.length ? parts.join(' ') : '默认'
+ } catch (e) {
+ return '默认'
+ }
+}
+
/**
* 订单状态列表
*/
diff --git a/utils/orderDelivery.js b/utils/orderDelivery.js
new file mode 100644
index 0000000..9ba1f0b
--- /dev/null
+++ b/utils/orderDelivery.js
@@ -0,0 +1,47 @@
+/**
+ * 订单配送展示工具(整单 / 拆单 / 无需配送)
+ */
+
+export function shouldLoadDelivery(order) {
+ return (
+ order?.deliveryMethod === 'LOGISTICS' &&
+ order?.allowOperationVO?.showLogistics === true
+ )
+}
+
+export function isNoDelivery(packageItem) {
+ return (
+ packageItem?.fulfillmentMode === 'NO_DELIVERY' ||
+ packageItem?.logisticsRequired === false
+ )
+}
+
+export function getPackageTitle(packageItem, index) {
+ if (isNoDelivery(packageItem)) return '无需配送'
+ if (packageItem?.fulfillmentMode === 'WHOLE') return '整单发货'
+ if (packageItem?.fulfillmentMode === 'SPLIT') return `包裹 ${index + 1}`
+ return `包裹 ${index + 1}`
+}
+
+export function getDeliveryEntryText(packages, orderStatus, legacyTraces) {
+ if (packages?.length) {
+ const allNoDelivery = packages.every(isNoDelivery)
+ if (allNoDelivery) return '查看配送说明'
+ if (orderStatus === 'PARTS_DELIVERED') {
+ return `已发出 ${packages.length} 个包裹,剩余商品待发货`
+ }
+ return `共 ${packages.length} 个配送包裹`
+ }
+
+ const traces = legacyTraces?.traces
+ if (Array.isArray(traces) && traces.length) {
+ const latest = traces[0]
+ return latest?.AcceptStation || '商家已发货,物流信息同步中'
+ }
+
+ return '商家已发货,物流信息同步中'
+}
+
+export function getOrderAllowOperation(order, orderDetail) {
+ return order?.allowOperationVO || orderDetail?.allowOperationVO || {}
+}
diff --git a/utils/storage.js b/utils/storage.js
index 053390e..9004538 100644
--- a/utils/storage.js
+++ b/utils/storage.js
@@ -9,13 +9,13 @@ const FACE_LOGIN = isDev ? "face_login_dev" : "face_login";
const FINGER_LOGIN = isDev ? "finger_login_dev" : "finger_login";
const CART_BACKBTN = isDev ? "cart_backbtn_dev" : "cart_backbtn";
const AFTERSALE_DATA = isDev ? "aftersale_data_dev" : "aftersale_data";
-export default {
- setInviter(val){
- uni.setStorageSync('inviter', val)
- },
- getInviter(){
- return uni.getStorageSync('inviter');
- },
+export default {
+ setInviter(val){
+ uni.setStorageSync('inviter', val)
+ },
+ getInviter(){
+ return uni.getStorageSync('inviter');
+ },
//写入自动发券
setAutoCp(val){
@@ -84,6 +84,14 @@ export default {
// 写入登录
setHasLogin(val) {
uni.setStorageSync(HAS_LOGIN, val);
+ if (val) {
+ try {
+ const { flushPendingBind } = require("@/utils/distributionBind.js");
+ flushPendingBind();
+ } catch (e) {
+ // ignore
+ }
+ }
},
// 获取是否登录
getHasLogin() {
diff --git a/vue.config.js b/vue.config.js
index 4fcbd16..c4415a1 100644
--- a/vue.config.js
+++ b/vue.config.js
@@ -1,20 +1,7 @@
+const path = require('path')
-// module.exports = {
-// /**
-// * 此处为发行h5,微信小程序,app中删除console
-// * 如需显示console 需要注释此处重新运行
-// */
-// chainWebpack: (config) => {
-// // 发行或运行时启用了压缩时会生效
-// config.optimization.minimizer('terser').tap((args) => {
-// const compress = args[0].terserOptions.compress
-// // 非 App 平台移除 console 代码(包含所有 console 方法,如 log,debug,info...)
-// compress.drop_console = true
-// compress.pure_funcs = [
-// '__f__', // App 平台 vue 移除日志代码
-// // 'console.debug' // 可移除指定的 console 方法
-// ]
-// return args
-// })
-// }
-// }
\ No newline at end of file
+module.exports = {
+ transpileDependencies: [
+ path.join(__dirname, 'js_sdk/u-draw-poster'),
+ ],
+}