feat: 积分商城-供应商-对账-发票4
This commit is contained in:
@@ -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)}`
|
||||
}
|
||||
@@ -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') => {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user