feat: 积分商城-供应商-对账-发票4

This commit is contained in:
2026-09-08 20:12:40 +08:00
parent f42ea5b270
commit ff77ba16cc
39 changed files with 705 additions and 136 deletions
+15
View File
@@ -0,0 +1,15 @@
const STORAGE_KEY = 'supplied-h5-merchant-session'
export const clearMerchantSession = () => sessionStorage.removeItem(STORAGE_KEY)
export const saveMerchantSession = session => sessionStorage.setItem(STORAGE_KEY, JSON.stringify(session))
export const readMerchantSession = () => {
try {
const session = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || 'null')
if (session?.token && session?.user && Date.parse(session.expiresAt) > Date.now()) return session
} catch { /* 损坏的会话按未登录处理。 */ }
clearMerchantSession()
return null
}
export const merchantLoginLocation = () => {
const redirect = window.location.hash.slice(1) || '/joyful-selection/merchant'
return `#/joyful-selection/merchant/login?redirect=${encodeURIComponent(redirect)}`
}
+26 -4
View File
@@ -1,7 +1,20 @@
import { readMerchantSession, clearMerchantSession, merchantLoginLocation } from './merchant-session'
const API_BASE = process.env.VUE_APP_SUPPLIED_API_BASE || 'http://localhost:8888/api'
const normalizeBase = base => base.replace(/\/$/, '')
const resolveFileUrls = value => {
if (typeof value === 'string' && /^\/api\/(?:file|upload-preview)\?/.test(value)) {
return `${normalizeBase(API_BASE).replace(/\/api$/, '')}${value.replace('/api/upload-preview?', '/api/file?')}`
}
if (Array.isArray(value)) return value.map(resolveFileUrls)
if (value && typeof value === 'object') {
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, resolveFileUrls(item)]))
}
return value
}
export const getFunction = async (name, params = {}) => {
const url = new URL(`${normalizeBase(API_BASE)}/${name}`)
Object.entries(params).forEach(([key, value]) => {
@@ -18,25 +31,34 @@ export const getFunction = async (name, params = {}) => {
throw new Error(message)
}
return payload.data
return resolveFileUrls(payload.data)
}
export const postFunction = async (name, body = {}) => {
const response = await fetch(`${normalizeBase(API_BASE)}/${name}`, {
const merchantRequest = name.startsWith('merchant-') || ['login', 'admin-session', 'admin-logout'].includes(name)
const token = merchantRequest ? readMerchantSession()?.token : ''
const response = await fetch(`${normalizeBase(API_BASE)}${merchantRequest ? '/admin' : ''}/${name}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {})
},
body: JSON.stringify(body)
})
const payload = await response.json().catch(() => ({}))
if (merchantRequest && name !== 'login' && (response.status === 401 || payload.error?.code === 'UNAUTHORIZED')) {
clearMerchantSession()
if (name !== 'admin-session' && !window.location.hash.startsWith('#/joyful-selection/merchant/login')) {
window.location.replace(merchantLoginLocation())
}
}
if (!response.ok || payload.success === false) {
const message = payload.error?.message || payload.msg || '请求失败'
throw new Error(message)
}
return payload.data
return resolveFileUrls(payload.data)
}
export const uploadImageFunction = async (file, folder = 'after-sale') => {
+23 -3
View File
@@ -1,4 +1,7 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import { Toast } from 'vant'
import { postFunction } from '@/api/supplied-service'
import { readMerchantSession, clearMerchantSession } from '@/api/merchant-session'
import SupplierHome from '@/view/index.vue'
import JoyfulSelectionLayout from '@/view/joyful-selection/layout.vue'
import JoyfulSelectionHome from '@/view/joyful-selection/index.vue'
@@ -28,10 +31,11 @@ import HappyShopping from '@/view/happy-shopping/index.vue'
import FreshFruit from '@/view/fresh-fruit/index.vue'
import CuteSelection from '@/view/cute-selection/index.vue'
export default createRouter({
const router = createRouter({
history: createWebHashHistory(),
routes: [
{ path: '/', component: SupplierHome },
{ path: '/joyful-selection/merchant/login', component: () => import('@/view/joyful-selection/merchant/login.vue') },
{
path: '/joyful-selection',
component: JoyfulSelectionLayout,
@@ -54,8 +58,8 @@ export default createRouter({
{ path: 'after-sale/select', component: JoyfulSelectionAfterSaleSelect },
{ path: 'after-sale/return', component: JoyfulSelectionAfterSaleReturn },
{ path: 'after-sale/exchange', component: JoyfulSelectionAfterSaleExchange },
{ path: 'merchant', component: JoyfulSelectionMerchant },
{ path: 'merchant/after-sale-detail', component: JoyfulSelectionMerchantAfterSaleDetail },
{ path: 'merchant', component: JoyfulSelectionMerchant, meta: { merchantAuth: true } },
{ path: 'merchant/after-sale-detail', component: JoyfulSelectionMerchantAfterSaleDetail, meta: { merchantAuth: true } },
{ path: 'supplier-logs', component: JoyfulSelectionSupplierLogs },
{ path: 'profile', component: JoyfulSelectionProfile },
{ path: 'points', component: JoyfulSelectionPoints }
@@ -67,3 +71,19 @@ export default createRouter({
{ path: '/cute-selection', component: CuteSelection }
]
})
router.beforeEach(async to => {
if (!to.meta.merchantAuth) return true
const login = { path: '/joyful-selection/merchant/login', query: { redirect: to.fullPath } }
if (!readMerchantSession()) return login
try {
await postFunction('admin-session')
return true
} catch (error) {
clearMerchantSession()
Toast(error.message || '登录已失效,请重新登录')
return login
}
})
export default router
@@ -50,6 +50,10 @@ export default {
...mapActions(useCartStore, ['loadCart']),
...mapActions(useUserStore, ['hydrateFromRoute', 'fetchBridgeToken']),
async prepareUserSession() {
if (this.$route.meta.merchantAuth) {
this.sessionLoading = false
return
}
this.sessionLoading = true
try {
await this.hydrateFromRoute(this.$route.query)
@@ -336,7 +336,7 @@ export default {
this.loadDetail()
},
methods: {
...mapActions(useUserStore, ['fetchBridgeToken']),
...mapActions(useUserStore, ['fetchSupplierInfo']),
normalizeSteps(steps) {
const serviceState = Number(this.afterSale.serviceState)
return steps.map((step, index) => ({
@@ -359,9 +359,9 @@ export default {
async loadDetail() {
try {
this.loading = true
await this.fetchBridgeToken()
if (!this.supplierId && !this.$route.query.supplierId) await this.fetchSupplierInfo()
const data = await postFunction('merchant-after-sale-detail', {
supplierId: this.supplierId,
supplierId: String(this.$route.query.supplierId || this.supplierId),
serviceOrderId: String(this.$route.query.serviceOrderId || '')
})
@@ -407,7 +407,7 @@ export default {
message: auditStatus === 1 ? '通过后用户可寄回商品。' : '拒绝后该售后申请将结束。'
})
await postFunction('merchant-after-sale-audit', {
supplierId: this.supplierId,
supplierId: this.afterSale.supplierId || this.supplierId,
serviceOrderId: this.afterSale.serviceOrderId,
auditStatus,
auditRemark: auditStatus === 1 ? '审核通过' : '商家拒绝'
@@ -421,7 +421,7 @@ export default {
message: '确认后退货售后会进入完成,换货售后会进入待商家寄货。'
})
await postFunction('merchant-after-sale-receive', {
supplierId: this.supplierId,
supplierId: this.afterSale.supplierId || this.supplierId,
serviceOrderId: this.afterSale.serviceOrderId
})
Toast('已确认收货')
@@ -441,7 +441,7 @@ export default {
}
await postFunction('merchant-after-sale-ship', {
supplierId: this.supplierId,
supplierId: this.afterSale.supplierId || this.supplierId,
serviceOrderId: this.afterSale.serviceOrderId,
logisticsCompany: this.exchangeShipForm.logisticsCompany,
logisticsNo: this.exchangeShipForm.logisticsNo
@@ -252,7 +252,7 @@ export default {
this.prepareSupplier().then(() => this.loadAllTabs())
},
methods: {
...mapActions(useUserStore, ['fetchSupplierInfo', 'fetchBridgeToken']),
...mapActions(useUserStore, ['fetchSupplierInfo']),
...mapActions(useUiStore, ['loadUi', 'setMerchantTab']),
skuText(item = {}) {
const sku = String(item.sku || item.skuText || '').trim()
@@ -273,7 +273,6 @@ export default {
return count > 0 ? count : ''
},
async prepareSupplier() {
await this.fetchBridgeToken()
if (!this.supplierId) {
await this.fetchSupplierInfo()
}
@@ -353,7 +352,8 @@ export default {
this.$router.push({
path: '/joyful-selection/merchant/after-sale-detail',
query: {
serviceOrderId: item.serviceOrderId
serviceOrderId: item.serviceOrderId,
supplierId: item.supplierId || this.supplierId
}
})
},
@@ -0,0 +1,56 @@
<template>
<main class="merchant-login-page">
<van-nav-bar title="商家登录" left-arrow @click-left="$router.replace('/joyful-selection/home')" />
<section class="merchant-login-card">
<div class="login-icon"><van-icon name="shop-o" size="36" /></div>
<h1>商家处理台</h1><p>使用供应商管理账号登录</p>
<van-form @submit="submit">
<van-cell-group inset>
<van-field v-model="username" name="username" label="账号" placeholder="请输入账号" autocomplete="username" :rules="[{ required: true, message: '请输入账号' }]" />
<van-field v-model="password" name="password" type="password" label="密码" placeholder="请输入密码" autocomplete="current-password" :rules="[{ required: true, message: '请输入密码' }]" />
</van-cell-group>
<van-button block round type="primary" native-type="submit" :loading="submitting" :disabled="submitting" color="#ff5b20" class="login-submit">登录</van-button>
</van-form>
</section>
</main>
</template>
<script>
import { Toast } from 'vant'
import { postFunction } from '@/api/supplied-service'
import { saveMerchantSession, clearMerchantSession } from '@/api/merchant-session'
export default {
name: 'MerchantLogin',
data: () => ({ username: '', password: '', submitting: false }),
methods: {
async submit() {
if (this.submitting) return
this.submitting = true
try {
const session = await postFunction('login', { username: this.username.trim(), password: this.password })
saveMerchantSession(session)
await postFunction('admin-session')
this.password = ''
const target = typeof this.$route.query.redirect === 'string' ? this.$route.query.redirect : ''
const resolved = this.$router.resolve(target || '/joyful-selection/merchant')
await this.$router.replace(resolved.meta.merchantAuth ? resolved.fullPath : '/joyful-selection/merchant')
} catch (error) {
clearMerchantSession()
Toast(error.message || '登录失败')
} finally { this.submitting = false }
}
}
}
</script>
<style scoped>
.merchant-login-page { min-height: 100vh; background: #f7f8fa; }
.merchant-login-card { max-width: 420px; margin: 48px auto; padding: 28px 20px; background: #fff; border-radius: 16px; }
.login-icon { display: flex; align-items: center; justify-content: center; width: 72px; height: 72px; margin: 0 auto 20px; border-radius: 20px; color: #ff5b20; background: #fff1e9; }
.merchant-login-card h1 { margin: 0; text-align: center; font-size: 23px; }
.merchant-login-card > p { margin: 12px 0 28px; text-align: center; color: #8793a5; font-size: 14px; }
.merchant-login-card :deep(.van-cell-group--inset) { margin: 0; }
.login-submit { margin-top: 28px; }
@media (max-width: 480px) { .merchant-login-card { margin: 32px 16px; } }
</style>
@@ -43,7 +43,7 @@
<div class="goods-bottom">
<div class="price-stack">
<div><b>¥{{ item.price }}</b></div>
<div><span>已售{{ item.sales }}</span></div>
<div><span>已售{{ formatSales(item.sales) }}件</span></div>
</div>
<button type="button" class="cart-button" @click.stop="openSku(item)">
<van-icon name="cart-o" />
@@ -96,6 +96,11 @@ export default {
this.loadProducts()
},
methods: {
formatSales(value) {
const number = Number(value)
const count = Number.isFinite(number) ? Math.max(0, Math.floor(number)) : 0
return count >= 10000 ? `${Math.floor(count / 10000)}万` : String(count)
},
async loadProducts() {
this.loading = true
try {
@@ -15,7 +15,7 @@
src="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
/>
<div>
<b>张三</b>
<b>{{ nickname || '-' }}</b>
<p>欢迎来到悦享优选</p>
</div>
</div>
@@ -103,7 +103,7 @@ export default {
}
}),
computed: {
...mapState(useUserStore, ['pointsBalance', 'supplierId', 'userId']),
...mapState(useUserStore, ['pointsBalance', 'supplierId', 'userId', 'nickname']),
formattedPoints() {
return Number(this.pointsBalance || 0).toLocaleString('zh-CN', {
maximumFractionDigits: 2