This commit is contained in:
2025-09-27 22:57:59 +08:00
parent 8a458ff0a4
commit ed26244cdb
12585 changed files with 1914308 additions and 3474 deletions

View File

@@ -43,9 +43,9 @@ export const HOME_BANNER_IMG = String(envHomeBanner || storageHomeBanner || '/st
// KPI 图标(可按需覆盖),避免在页面里硬编码
export const KPI_ICONS = {
todaySales: '/static/icons/sale.png',
monthSales: '/static/icons/report.png',
monthProfit: '/static/icons/report.png',
todaySales: '/static/icons/webwxgetmsgimg.jpg',
monthSales: '/static/icons/webwxgetmsgimg.jpg',
monthProfit: '/static/icons/icons8-profit-50.png',
stockCount: '/static/icons/product.png'
}

View File

@@ -24,8 +24,17 @@ function buildAuthHeaders(base = {}) {
const claims = parseJwtClaims(token)
if (claims && claims.shopId) headers['X-Shop-Id'] = claims.shopId
if (claims && claims.userId) headers['X-User-Id'] = claims.userId
} else if (ENABLE_DEFAULT_USER && DEFAULT_USER_ID) {
if (headers['Authorization']) delete headers['Authorization']
headers['X-User-Id'] = headers['X-User-Id'] || DEFAULT_USER_ID
if (SHOP_ID) headers['X-Shop-Id'] = headers['X-Shop-Id'] || SHOP_ID
}
} catch (_) { /* noop: 未登录不注入任何店铺/用户头 */ }
} catch (_) {
if (ENABLE_DEFAULT_USER && DEFAULT_USER_ID) {
headers['X-User-Id'] = headers['X-User-Id'] || DEFAULT_USER_ID
if (SHOP_ID) headers['X-Shop-Id'] = headers['X-Shop-Id'] || SHOP_ID
}
}
return headers
}
@@ -67,7 +76,12 @@ export function post(path, body = {}) {
const headers = buildAuthHeaders({ 'Content-Type': 'application/json' })
const options = { url: buildUrl(path), method: 'POST', data: body, header: headers }
const p = String(path || '')
if (p.includes('/api/auth/wxmp/login') || p.includes('/api/auth/sms/login') || p.includes('/api/auth/sms/send') || p.includes('/api/auth/password/login') || p.includes('/api/auth/register')) options.__noRetry = true
if (
p.includes('/api/auth/wxmp/login') ||
p.includes('/api/auth/sms/login') || p.includes('/api/auth/sms/send') ||
p.includes('/api/auth/email/login') || p.includes('/api/auth/email/send') ||
p.includes('/api/auth/password/login') || p.includes('/api/auth/register')
) options.__noRetry = true
requestWithFallback(options, API_BASE_URL_CANDIDATES, 0, resolve, reject)
})
}
@@ -97,6 +111,7 @@ function uploadWithFallback(options, candidates, idx, resolve, reject) {
...uploadOptions,
success: (res) => {
const statusCode = res.statusCode || 0
// 2xx: 正常返回
if (statusCode >= 200 && statusCode < 300) {
try {
const data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
@@ -105,8 +120,23 @@ function uploadWithFallback(options, candidates, idx, resolve, reject) {
return resolve(res.data)
}
}
// 4xx: 不重试,透传后端 JSON如 400 未识别、413 文件过大),给调用方展示 message
if (statusCode >= 400 && statusCode < 500) {
try {
const data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
return resolve(data)
} catch (e) {
return resolve({ success: false, message: 'HTTP ' + statusCode })
}
}
// 5xx优先尝试下一个候选地址到达最后一个候选时尽力解析 JSON 供前端展示
if (idx + 1 < candidates.length) return uploadWithFallback(options, candidates, idx + 1, resolve, reject)
reject(new Error('HTTP ' + statusCode))
try {
const data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
return resolve(data)
} catch (e) {
return resolve({ success: false, message: 'HTTP ' + statusCode })
}
},
fail: (err) => {
if (idx + 1 < candidates.length) return uploadWithFallback(options, candidates, idx + 1, resolve, reject)

View File

@@ -15,11 +15,11 @@
@change="onMoving(index, $event)"
@touchend="onMoveEnd(index)"
>
<image :src="img.url" mode="aspectFill" class="thumb" @click="preview(index)" />
<view class="remove" @click.stop="remove(index)">×</view>
<image :src="img.url" mode="aspectFill" class="thumb" @click="preview(index)" />
<image class="remove" src="/static/icons/icons8-close-48.png" mode="aspectFit" @click.stop="remove(index)" />
</movable-view>
<view v-if="innerList.length < max" class="adder" @click="choose">
<view v-if="innerList.length < max" class="adder" :style="adderStyle" @click="choose">
<text></text>
</view>
</movable-area>
@@ -29,6 +29,7 @@
<script>
import { upload } from '../common/http.js'
import { API_BASE_URL } from '../common/config.js'
const ITEM_SIZE = 210 // rpx
const GAP = 18 // rpx
@@ -57,6 +58,14 @@ export default {
areaHeight() {
const rows = Math.ceil((this.innerList.length + 1) / COLS) || 1
return rows * ITEM_SIZE + (rows - 1) * GAP
},
adderStyle() {
const index = this.innerList.length
const row = Math.floor(index / COLS)
const col = index % COLS
const x = px(col * (ITEM_SIZE + GAP))
const y = px(row * (ITEM_SIZE + GAP))
return { left: x + 'rpx', top: y + 'rpx' }
}
},
watch: {
@@ -65,7 +74,7 @@ export default {
handler(list) {
const mapped = (list || []).map((u, i) => ({
uid: String(i) + '_' + (u.id || u.url || Math.random().toString(36).slice(2)),
url: typeof u === 'string' ? u : (u.url || ''),
url: this.ensureAbsoluteUrl(typeof u === 'string' ? u : (u.url || '')),
x: this.posOf(i).x,
y: this.posOf(i).y
}))
@@ -74,6 +83,12 @@ export default {
}
},
methods: {
ensureAbsoluteUrl(u) {
if (!u) return ''
const s = String(u)
if (s.startsWith('http://') || s.startsWith('https://')) return s
return API_BASE_URL + (s.startsWith('/') ? s : '/' + s)
},
posOf(index) {
const row = Math.floor(index / COLS)
const col = index % COLS
@@ -105,7 +120,7 @@ export default {
async doUpload(filePath) {
try {
const resp = await upload(this.uploadPath, filePath, this.formData, this.uploadFieldName)
const url = resp?.url || resp?.data?.url || resp?.path || ''
const url = this.ensureAbsoluteUrl(resp?.url || resp?.data?.url || resp?.path || '')
if (!url) throw new Error('上传响应无 url')
this.innerList.push({ uid: Math.random().toString(36).slice(2), url, ...this.posOf(this.innerList.length) })
this.reflow()
@@ -155,7 +170,7 @@ export default {
.area { width: 100%; position: relative; }
.cell { position: absolute; border-radius: 12rpx; overflow: hidden; box-shadow: 0 0 1rpx rgba(0,0,0,0.08); }
.thumb { width: 100%; height: 100%; }
.remove { position: absolute; right: 6rpx; top: 6rpx; background: rgba(0,0,0,0.45); color: #fff; width: 40rpx; height: 40rpx; text-align: center; line-height: 40rpx; border-radius: 20rpx; font-size: 28rpx; }
.remove { position: absolute; right: 6rpx; top: 6rpx; width: 42rpx; height: 42rpx; }
.adder { width: 210rpx; height: 210rpx; border: 2rpx dashed #ccc; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; color: #999; position: absolute; left: 0; top: 0; }
</style>

View File

@@ -24,6 +24,19 @@
"navigationBarTitleText": "货品列表"
}
},
{
"path": "pages/product/submit",
"style": {
"navigationBarTitleText": "提交配件",
"navigationBarBackgroundColor": "#ffffff"
}
},
{
"path": "pages/product/submissions",
"style": {
"navigationBarTitleText": "我的提交"
}
},
{
"path": "pages/product/form",
"style": {
@@ -126,6 +139,12 @@
"navigationBarTitleText": "关于与协议"
}
},
{
"path": "pages/my/security",
"style": {
"navigationBarTitleText": "账号与安全"
}
},
{
"path": "pages/my/vip",
"style": {
@@ -136,6 +155,12 @@
"backgroundTextStyle": "light"
}
},
{
"path": "pages/my/orders",
"style": {
"navigationBarTitleText": "我的订单"
}
},
{
"path": "pages/report/index",
"style": {

View File

@@ -1,418 +1,155 @@
<template>
<view class="login-container">
<!-- 背景装饰 -->
<view class="background-decoration">
<view class="circle circle-1"></view>
<view class="circle circle-2"></view>
<view class="circle circle-3"></view>
</view>
<!-- 主要内容卡片 -->
<view class="login-card">
<!-- 顶部Logo区域 -->
<view class="header-section">
<view class="logo-container">
<view class="logo-icon">
<svg viewBox="0 0 24 24" class="icon">
<path d="M12 2C13.1 2 14 2.9 14 4C14 5.1 13.1 6 12 6C10.9 6 10 5.1 10 4C10 2.9 10.9 2 12 2ZM21 9V7L15 4V6C15 7.66 13.66 9 12 9S9 7.66 9 6V4L3 7V9C3 10.1 3.9 11 5 11V17C5 18.1 5.9 19 7 19H9C9 20.1 9.9 21 11 21H13C14.1 21 15 20.1 15 19H17C18.1 19 19 18.1 19 17V11C20.1 11 21 10.1 21 9Z"/>
</svg>
</view>
<text class="app-name">配件询价</text>
</view>
<text class="welcome-text">欢迎回来</text>
<text class="subtitle">请登录您的账户</text>
</view>
<view class="auth-page">
<view class="tabs">
<view :class="['tab', tab==='login'?'active':'']" @click="tab='login'">登录</view>
<view :class="['tab', tab==='register'?'active':'']" @click="tab='register'">注册</view>
<view :class="['tab', tab==='reset'?'active':'']" @click="tab='reset'">忘记密码</view>
</view>
<!-- 表单区域 -->
<view class="form-section">
<view class="input-group">
<view class="input-container" :class="{ focused: phoneFocused, filled: form.phone }">
<view class="input-icon">
<svg viewBox="0 0 24 24" class="icon">
<path d="M6.62,10.79C8.06,13.62 10.38,15.94 13.21,17.38L15.41,15.18C15.69,14.9 16.08,14.82 16.43,14.93C17.55,15.3 18.75,15.5 20,15.5A1,1 0 0,1 21,16.5V20A1,1 0 0,1 20,21A17,17 0 0,1 3,4A1,1 0 0,1 4,3H7.5A1,1 0 0,1 8.5,4C8.5,5.25 8.7,6.45 9.07,7.57C9.18,7.92 9.1,8.31 8.82,8.59L6.62,10.79Z"/>
</svg>
</view>
<input
class="input-field"
v-model.trim="form.phone"
type="number"
placeholder="请输入手机号"
maxlength="11"
@focus="phoneFocused = true"
@blur="phoneFocused = false"
/>
</view>
</view>
<view class="input-group">
<view class="input-container" :class="{ focused: passwordFocused, filled: form.password }">
<view class="input-icon">
<svg viewBox="0 0 24 24" class="icon">
<path d="M12,17A2,2 0 0,0 14,15C14,13.89 13.1,13 12,13A2,2 0 0,0 10,15A2,2 0 0,0 12,17M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V10C4,8.89 4.9,8 6,8H7V6A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,3A3,3 0 0,0 9,6V8H15V6A3,3 0 0,0 12,3Z"/>
</svg>
</view>
<input
class="input-field"
v-model.trim="form.password"
password
placeholder="请输入密码"
@focus="passwordFocused = true"
@blur="passwordFocused = false"
/>
</view>
</view>
</view>
<view v-if="tab==='login'" class="panel">
<input class="input" type="text" v-model.trim="loginForm.email" placeholder="输入邮箱" />
<input class="input" type="password" v-model="loginForm.password" placeholder="输入密码" />
<button class="btn primary" :disabled="loading" @click="onLogin">登录</button>
</view>
<!-- 按钮区域 -->
<view class="actions-section">
<button class="login-button" @click="onLogin">
<text class="button-text">登录</text>
</button>
<button class="register-button" @click="onGoRegister">
<text class="button-text">注册新账户</text>
</button>
</view>
<view v-else-if="tab==='register'" class="panel">
<input class="input" type="text" v-model.trim="regForm.name" placeholder="输入用户名" />
<input class="input" type="text" v-model.trim="regForm.email" placeholder="输入邮箱" />
<view class="row">
<input class="input flex1" type="text" v-model.trim="regForm.code" placeholder="邮箱验证码" />
<button class="btn ghost" :disabled="regCountdown>0 || loading" @click="sendRegCode">{{ regCountdown>0? (regCountdown+'s') : '获取验证码' }}</button>
</view>
<input class="input" type="password" v-model="regForm.password" placeholder="输入密码(≥6位)" />
<input class="input" type="password" v-model="regForm.password2" placeholder="再次输入密码" />
<button class="btn primary" :disabled="loading" @click="onRegister">注册新用户</button>
</view>
<!-- 提示信息 -->
<view class="footer-section"></view>
</view>
</view>
<view v-else class="panel">
<input class="input" type="text" v-model.trim="resetForm.email" placeholder="输入邮箱" />
<view class="row">
<input class="input flex1" type="text" v-model.trim="resetForm.code" placeholder="邮箱验证码" />
<button class="btn ghost" :disabled="resetCountdown>0 || loading" @click="sendResetCode">{{ resetCountdown>0? (resetCountdown+'s') : '获取验证码' }}</button>
</view>
<input class="input" type="password" v-model="resetForm.password" placeholder="新密码(≥6位)" />
<input class="input" type="password" v-model="resetForm.password2" placeholder="再次输入新密码" />
<button class="btn primary" :disabled="loading" @click="onReset">重置密码</button>
</view>
</view>
</template>
<script>
import { post } from '../../common/http.js'
import { get, post } from '../../common/http.js'
export default {
data() {
return {
form: { phone: '', password: '' },
phoneFocused: false,
passwordFocused: false
}
},
methods: {
validate() {
const p = String(this.form.phone||'').trim()
const okPhone = /^1[3-9]\d{9}$/.test(p)
if (!okPhone) { uni.showToast({ title: '请输入正确的手机号', icon: 'none' }); return false }
return true
},
async onLogin() {
if (!this.validate()) return
try {
const phone = String(this.form.phone||'').trim()
const password = String(this.form.password||'')
const res = await post('/api/auth/password/login', { phone, password })
// 统一存储 TOKEN 并跳转首页
if (res && res.token) {
uni.setStorageSync('TOKEN', res.token)
if (res.user && res.user.phone) uni.setStorageSync('USER_MOBILE', res.user.phone)
uni.showToast({ title: '登录成功', icon: 'none' })
setTimeout(() => { uni.reLaunch({ url: '/pages/index/index' }) }, 200)
}
} catch(e) {
uni.showToast({ title: (e && e.message) || '登录失败', icon: 'none' })
}
},
onGoRegister() {
uni.navigateTo({
url: '/pages/auth/register'
})
}
}
data(){
return {
loading: false,
tab: 'login',
loginForm: { email: '', password: '' },
regForm: { name: '', email: '', code: '', password: '', password2: '' },
resetForm: { email: '', code: '', password: '', password2: '' },
regCountdown: 0,
resetCountdown: 0,
_timers: []
}
},
beforeUnmount(){ this._timers.forEach(t=>clearInterval(t)) },
methods: {
toast(msg){ try{ uni.showToast({ title: String(msg||'操作失败'), icon: 'none' }) } catch(_){} },
validateEmail(v){ return /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/.test(String(v||'').trim()) },
startCountdown(key){
if (this[key] > 0) return
this[key] = 60
const timer = setInterval(()=>{ this[key] = Math.max(0, this[key]-1); if (this[key]===0) clearInterval(timer) }, 1000)
this._timers.push(timer)
},
async onLogin(){
const { email, password } = this.loginForm
if (!this.validateEmail(email)) return this.toast('请输入正确邮箱')
if (!password || password.length<6) return this.toast('请输入至少6位密码')
this.loading = true
try{
const data = await post('/api/auth/password/login', { email, password })
this.afterLogin(data)
}catch(e){ this.toast(e.message) }
finally{ this.loading=false }
},
afterLogin(data){
try{
if (data && data.token) {
uni.setStorageSync('TOKEN', data.token)
if (data.user && data.user.shopId) uni.setStorageSync('SHOP_ID', data.user.shopId)
uni.setStorageSync('ENABLE_DEFAULT_USER', 'false')
uni.removeStorageSync('DEFAULT_USER_ID')
this.toast('登录成功')
setTimeout(()=>{ uni.reLaunch({ url: '/pages/index/index' }) }, 300)
}else{
this.toast('登录失败')
}
}catch(_){ this.toast('登录失败') }
},
async sendRegCode(){
if (!this.validateEmail(this.regForm.email)) return this.toast('请输入正确邮箱')
this.loading = true
try{
const r = await post('/api/auth/email/send', { email: this.regForm.email, scene: 'register' })
if (r && r.ok) this.startCountdown('regCountdown')
this.toast(r && r.ok ? '验证码已发送' : '发送过于频繁')
}catch(e){ this.toast(e.message) }
finally{ this.loading=false }
},
async onRegister(){
const f = this.regForm
if (!f.name || f.name.trim().length<1) return this.toast('请输入用户名')
if (!this.validateEmail(f.email)) return this.toast('请输入正确邮箱')
if (!f.code) return this.toast('请输入验证码')
if (!f.password || f.password.length<6) return this.toast('密码至少6位')
if (f.password !== f.password2) return this.toast('两次密码不一致')
this.loading = true
try{
const data = await post('/api/auth/email/register', { name: f.name.trim(), email: f.email.trim(), code: f.code.trim(), password: f.password })
this.afterLogin(data)
}catch(e){ this.toast(e.message) }
finally{ this.loading=false }
},
async sendResetCode(){
if (!this.validateEmail(this.resetForm.email)) return this.toast('请输入正确邮箱')
this.loading = true
try{
const r = await post('/api/auth/email/send', { email: this.resetForm.email, scene: 'reset' })
if (r && r.ok) this.startCountdown('resetCountdown')
this.toast(r && r.ok ? '验证码已发送' : '发送过于频繁')
}catch(e){ this.toast(e.message) }
finally{ this.loading=false }
},
async onReset(){
const f = this.resetForm
if (!this.validateEmail(f.email)) return this.toast('请输入正确邮箱')
if (!f.code) return this.toast('请输入验证码')
if (!f.password || f.password.length<6) return this.toast('新密码至少6位')
if (f.password !== f.password2) return this.toast('两次密码不一致')
this.loading = true
try{
const r = await post('/api/auth/email/reset-password', { email: f.email.trim(), code: f.code.trim(), newPassword: f.password, confirmPassword: f.password2 })
if (r && r.ok) { this.toast('已重置请使用新密码登录'); this.tab='login'; this.loginForm.email=f.email; }
else this.toast('重置失败')
}catch(e){ this.toast(e.message) }
finally{ this.loading=false }
}
}
}
</script>
<style lang="scss">
// 主容器
.login-container {
position: relative;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 40rpx 20rpx;
overflow: hidden;
}
// 背景装饰圆形
.background-decoration {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 0;
.circle {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
&.circle-1 {
width: 200rpx;
height: 200rpx;
top: 10%;
left: 10%;
animation: float 6s ease-in-out infinite;
}
&.circle-2 {
width: 150rpx;
height: 150rpx;
top: 60%;
right: 15%;
animation: float 8s ease-in-out infinite reverse;
}
&.circle-3 {
width: 100rpx;
height: 100rpx;
bottom: 20%;
left: 20%;
animation: float 5s ease-in-out infinite;
}
}
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-20px); }
}
// 主卡片容器
.login-card {
position: relative;
z-index: 1;
width: 90%;
max-width: 680rpx;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
border-radius: 32rpx;
padding: 60rpx 40rpx 50rpx;
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.1);
border: 1rpx solid rgba(255, 255, 255, 0.2);
}
// 头部区域
.header-section {
text-align: center;
margin-bottom: 50rpx;
.logo-container {
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
margin-bottom: 20rpx;
.logo-icon {
width: 60rpx;
height: 60rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
.icon {
width: 36rpx;
height: 36rpx;
fill: white;
}
}
.app-name {
font-size: 36rpx;
font-weight: 700;
color: #2d3748;
letter-spacing: 1rpx;
}
}
.welcome-text {
display: block;
font-size: 48rpx;
font-weight: 700;
color: #2d3748;
margin-bottom: 8rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.subtitle {
display: block;
font-size: 28rpx;
color: #718096;
font-weight: 400;
}
}
// 表单区域
.form-section {
margin-bottom: 40rpx;
.input-group {
margin-bottom: 28rpx;
.input-container {
position: relative;
background: #f7fafc;
border: 2rpx solid #e2e8f0;
border-radius: 16rpx;
display: flex;
align-items: center;
transition: all 0.3s ease;
&.focused {
border-color: #667eea;
background: #ffffff;
box-shadow: 0 0 0 6rpx rgba(102, 126, 234, 0.1);
transform: translateY(-2rpx);
}
&.filled {
background: #ffffff;
border-color: #cbd5e0;
}
.input-icon {
display: flex;
align-items: center;
justify-content: center;
width: 50rpx;
margin-left: 20rpx;
.icon {
width: 32rpx;
height: 32rpx;
fill: #a0aec0;
transition: fill 0.3s ease;
}
}
&.focused .input-icon .icon {
fill: #667eea;
}
.input-field {
flex: 1;
background: transparent;
border: none;
padding: 24rpx 20rpx 24rpx 12rpx;
font-size: 32rpx;
color: #2d3748;
&::placeholder {
color: #a0aec0;
font-size: 28rpx;
}
}
}
}
}
// 按钮区域
.actions-section {
margin-bottom: 30rpx;
.login-button {
width: 100%;
height: 96rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 16rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.3);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
&:active {
transform: translateY(2rpx);
box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.3);
}
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255,255,255,0.2) 0%, transparent 50%);
opacity: 0;
transition: opacity 0.3s ease;
}
&:active::before {
opacity: 1;
}
.button-text {
font-size: 32rpx;
font-weight: 600;
color: white;
letter-spacing: 1rpx;
}
}
.register-button {
width: 100%;
height: 86rpx;
background: transparent;
border: 2rpx solid #e2e8f0;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
&:active {
background: #f7fafc;
border-color: #cbd5e0;
transform: translateY(1rpx);
}
.button-text {
font-size: 28rpx;
font-weight: 500;
color: #718096;
}
}
}
// 页脚区域
.footer-section {
text-align: center;
.hint-text {
font-size: 24rpx;
color: #a0aec0;
line-height: 1.5;
background: rgba(160, 174, 192, 0.1);
padding: 16rpx 20rpx;
border-radius: 12rpx;
border: 1rpx solid rgba(160, 174, 192, 0.2);
}
}
// 响应式设计
@media (max-width: 750rpx) {
.login-card {
margin: 20rpx;
padding: 50rpx 30rpx 40rpx;
}
.header-section .welcome-text {
font-size: 42rpx;
}
}
.auth-page{ padding: 32rpx; display:flex; flex-direction: column; gap: 24rpx; }
.tabs{ display:flex; gap: 24rpx; }
.tab{ padding: 12rpx 20rpx; border-radius: 999rpx; background:#f2f4f8; color:#5b6b80; font-weight:700; }
.tab.active{ background:#2d6be6; color:#fff; }
.panel{ display:flex; flex-direction: column; gap: 16rpx; background:#fff; padding: 24rpx; border-radius: 16rpx; border:2rpx solid #eef2f9; }
.input{ background:#f7f9ff; border:2rpx solid rgba(45,107,230,0.12); border-radius: 12rpx; padding: 22rpx 20rpx; font-size: 28rpx; }
.row{ display:flex; gap: 12rpx; align-items:center; }
.flex1{ flex:1; }
.btn{ padding: 22rpx 20rpx; border-radius: 12rpx; font-weight: 800; text-align:center; }
.btn.primary{ background: linear-gradient(135deg, #4788ff 0%, #2d6be6 100%); color:#fff; }
.btn.ghost{ background:#eef3ff; color:#2d6be6; }
</style>

View File

@@ -63,29 +63,46 @@
</view>
</view>
<!-- 手机号 -->
<!-- 邮箱 -->
<view class="input-group">
<view class="input-container" :class="{ focused: phoneFocused, filled: form.phone }">
<view class="input-container" :class="{ focused: emailFocused, filled: form.email }">
<view class="input-icon">
<svg viewBox="0 0 24 24" class="icon">
<path d="M6.62,10.79C8.06,13.62 10.38,15.94 13.21,17.38L15.41,15.18C15.69,14.9 16.08,14.82 16.43,14.93C17.55,15.3 18.75,15.5 20,15.5A1,1 0 0,1 21,16.5V20A1,1 0 0,1 20,21A17,17 0 0,1 3,4A1,1 0 0,1 4,3H7.5A1,1 0 0,1 8.5,4C8.5,5.25 8.7,6.45 9.07,7.57C9.18,7.92 9.1,8.31 8.82,8.59L6.62,10.79Z"/>
<path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-1 4l-7 4-7-4V6l7 4 7-4v2z"/>
</svg>
</view>
<input
class="input-field"
v-model.trim="form.phone"
type="number"
placeholder="请输入手机号"
maxlength="11"
@focus="phoneFocused = true"
@blur="phoneFocused = false"
v-model.trim="form.email"
type="text"
placeholder="请输入邮箱地址"
@focus="emailFocused = true"
@blur="emailFocused = false"
/>
</view>
</view>
<!-- 验证码 -->
<view class="input-group">
<view class="input-container" :class="{ focused: codeFocused, filled: form.code }">
<view class="input-icon">
<svg viewBox="0 0 24 24" class="icon">
<path d="M3 10h18v2H3v-2zm0 6h12v2H3v-2zM3 6h18v2H3V6z"/>
</svg>
</view>
<input
class="input-field"
v-model.trim="form.code"
type="number" maxlength="6"
placeholder="请输入6位验证码"
@focus="codeFocused = true"
@blur="codeFocused = false"
/>
</view>
</view>
<!-- 密码 -->
<view class="input-group">
<view class="input-container" :class="{ focused: passwordFocused, filled: form.password }">
<view class="input-container" :class="{ focused: pwdFocused, filled: form.password }">
<view class="input-icon">
<svg viewBox="0 0 24 24" class="icon">
<path d="M12,17A2,2 0 0,0 14,15C14,13.89 13.1,13 12,13A2,2 0 0,0 10,15A2,2 0 0,0 12,17M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V10C4,8.89 4.9,8 6,8H7V6A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,3A3,3 0 0,0 9,6V8H15V6A3,3 0 0,0 12,3Z"/>
@@ -95,30 +112,16 @@
class="input-field"
v-model.trim="form.password"
password
placeholder="请输入密码至少6位"
@focus="passwordFocused = true"
@blur="passwordFocused = false"
placeholder="请设置登录密码至少6位"
@focus="pwdFocused = true"
@blur="pwdFocused = false"
/>
</view>
</view>
<!-- 确认密码 -->
<!-- 发送验证码按钮 -->
<view class="input-group">
<view class="input-container" :class="{ focused: confirmPasswordFocused, filled: form.confirmPassword }">
<view class="input-icon">
<svg viewBox="0 0 24 24" class="icon">
<path d="M12,17A2,2 0 0,0 14,15C14,13.89 13.1,13 12,13A2,2 0 0,0 10,15A2,2 0 0,0 12,17M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V10C4,8.89 4.9,8 6,8H7V6A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,3A3,3 0 0,0 9,6V8H15V6A3,3 0 0,0 12,3Z"/>
</svg>
</view>
<input
class="input-field"
v-model.trim="form.confirmPassword"
password
placeholder="请再次输入密码"
@focus="confirmPasswordFocused = true"
@blur="confirmPasswordFocused = false"
/>
</view>
<button class="login-button" :disabled="countdown>0 || sending" @click="sendCode">{{ btnText }}</button>
</view>
</view>
@@ -148,39 +151,70 @@ export default {
form: {
shopName: '',
name: '',
phone: '',
password: '',
confirmPassword: ''
email: '',
code: '',
password: ''
},
shopNameFocused: false,
nameFocused: false,
phoneFocused: false,
passwordFocused: false,
confirmPasswordFocused: false
emailFocused: false,
codeFocused: false,
pwdFocused: false,
countdown: 0,
timer: null,
sending: false
}
},
computed: {
btnText(){
if (this.countdown > 0) return `${this.countdown}s`
if (this.sending) return '发送中...'
return '获取验证码'
}
},
methods: {
validate() {
const phone = String(this.form.phone || '').trim();
const ok = /^1[3-9]\d{9}$/.test(phone);
if (!ok) { uni.showToast({ title: '请输入正确的手机号', icon: 'none' }); return false }
if (!this.form.password) { uni.showToast({ title: '请输入密码', icon: 'none' }); return false }
if (this.form.password.length < 6) { uni.showToast({ title: '密码至少6位', icon: 'none' }); return false }
if (!this.form.confirmPassword) { uni.showToast({ title: '请确认密码', icon: 'none' }); return false }
if (this.form.password !== this.form.confirmPassword) { uni.showToast({ title: '两次密码不一致', icon: 'none' }); return false }
const email = String(this.form.email || '').trim();
const ok = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/.test(email);
if (!ok) { uni.showToast({ title: '请输入正确的邮箱地址', icon: 'none' }); return false }
if (!/^\d{6}$/.test(String(this.form.code||'').trim())) { uni.showToast({ title: '验证码格式不正确', icon: 'none' }); return false }
if (String(this.form.password||'').length < 6) { uni.showToast({ title: '密码至少6位', icon: 'none' }); return false }
return true;
},
startCountdown(sec){
this.countdown = sec
if (this.timer) clearInterval(this.timer)
this.timer = setInterval(() => {
if (this.countdown<=1) { clearInterval(this.timer); this.timer=null; this.countdown=0; return }
this.countdown--
}, 1000)
},
async sendCode(){
if (this.sending || this.countdown>0) return
const e = String(this.form.email||'').trim()
const ok = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/.test(e)
if (!ok) return uni.showToast({ title: '请输入正确的邮箱地址', icon: 'none' })
this.sending = true
try {
const res = await post('/api/auth/email/send', { email: e, scene: 'login' })
const cd = Number(res && res.cooldownSec || 60)
this.startCountdown(cd)
uni.showToast({ title: '验证码已发送', icon: 'none' })
} catch(e) {
const msg = (e && e.message) || '发送失败'
uni.showToast({ title: msg, icon: 'none' })
} finally { this.sending=false }
},
async onRegister() {
if (!this.validate()) return;
const phone = String(this.form.phone||'').trim();
const email = String(this.form.email||'').trim();
const name = String(this.form.name||'').trim();
const password = String(this.form.password||'');
try {
const data = await post('/api/auth/register', { phone, name: name || undefined, password });
const data = await post('/api/auth/email/register', { email, code: String(this.form.code||'').trim(), name, password: String(this.form.password||'') });
if (data && data.token) {
uni.setStorageSync('TOKEN', data.token)
if (data.user && data.user.phone) uni.setStorageSync('USER_MOBILE', data.user.phone)
if (data.user && data.user.email) uni.setStorageSync('USER_EMAIL', data.user.email)
if (name) try { uni.setStorageSync('USER_NAME', name) } catch(_) {}
uni.showToast({ title: '注册成功', icon: 'none' })
setTimeout(() => { uni.reLaunch({ url: '/pages/index/index' }) }, 300)
}

View File

@@ -6,7 +6,6 @@
<view class="row"><text class="label">手机</text><text v-if="!editing" class="value">{{ d.mobile || '—' }}</text><input v-else class="value-input" v-model="form.mobile" placeholder="可选" /></view>
<view class="row"><text class="label">电话</text><text v-if="!editing" class="value">{{ d.phone || '—' }}</text><input v-else class="value-input" v-model="form.phone" placeholder="可选(座机)" /></view>
<view class="row"><text class="label">地址</text><text v-if="!editing" class="value">{{ d.address || '—' }}</text><input v-else class="value-input" v-model="form.address" placeholder="可选" /></view>
<view class="row"><text class="label">等级</text><text v-if="!editing" class="value">{{ d.level || '—' }}</text><input v-else class="value-input" v-model="form.level" placeholder="可选,如 VIP/A/B" /></view>
<view class="row"><text class="label">售价档位</text>
<text v-if="!editing" class="value">{{ d.priceLevel }}</text>
<picker v-else :range="priceLabels" :value="priceIdx" @change="onPriceChange"><view class="value">{{ priceLabels[priceIdx] }}</view></picker>
@@ -26,7 +25,7 @@
<script>
import { get, put } from '../../common/http.js'
export default {
data(){ return { id: null, d: {}, editing: false, form: { name:'', contactName:'', mobile:'', phone:'', address:'', level:'', priceLevel:'零售价', arOpening:0, remark:'' }, priceLevels:['零售价','批发价','大单报价'], priceLabels:['零售价','批发价','大单报价'], priceIdx:0 } },
data(){ return { id: null, d: {}, editing: false, form: { name:'', contactName:'', mobile:'', phone:'', address:'', priceLevel:'零售价', arOpening:0, remark:'' }, priceLevels:['零售价','批发价','大单报价'], priceLabels:['零售价','批发价','大单报价'], priceIdx:0 } },
onLoad(q){ if (q && q.id) { this.id = Number(q.id); this.fetch() } },
methods: {
async fetch(){
@@ -35,7 +34,7 @@ export default {
// 初始化表单
this.form = {
name: this.d.name || '', contactName: this.d.contactName || '', mobile: this.d.mobile || '', phone: this.d.phone || '',
address: this.d.address || '', level: this.d.level || '', priceLevel: this.d.priceLevel || 'retail', arOpening: Number(this.d.arOpening||0), remark: this.d.remark || ''
address: this.d.address || '', priceLevel: this.d.priceLevel || 'retail', arOpening: Number(this.d.arOpening||0), remark: this.d.remark || ''
}
const idx = this.priceLevels.indexOf(this.form.priceLevel); this.priceIdx = idx >= 0 ? idx : 0
} catch(e){ uni.showToast({ title:'加载失败', icon:'none' }) }

View File

@@ -1,7 +1,6 @@
<template>
<view class="page">
<view class="field"><text class="label">客户名称</text><input class="value" v-model="form.name" placeholder="必填" /></view>
<view class="field"><text class="label">客户等级</text><input class="value" v-model="form.level" placeholder="可选,如 VIP/A/B" /></view>
<view class="field">
<text class="label">售价档位</text>
<picker :range="priceLabels" :value="priceIdx" @change="onPriceChange">
@@ -20,20 +19,39 @@
</template>
<script>
import { post, put } from '../../common/http.js'
import { get, post, put } from '../../common/http.js'
export default {
data() {
return {
id: null,
form: { name:'', level:'', priceLevel:'retail', contactName:'', mobile:'', phone:'', address:'', arOpening:0, remark:'' },
form: { name:'', priceLevel:'retail', contactName:'', mobile:'', phone:'', address:'', arOpening:0, remark:'' },
priceLevels: ['零售价','批发价','大单报价'],
priceLabels: ['零售价','批发价','大单报价'],
priceIdx: 0
}
},
onLoad(query) { if (query && query.id) { this.id = Number(query.id) } },
onLoad(query) { if (query && query.id) { this.id = Number(query.id); this.load() } },
methods: {
onPriceChange(e){ this.priceIdx = Number(e.detail.value); this.form.priceLevel = this.priceLevels[this.priceIdx] },
async load() {
if (!this.id) return
try {
const d = await get(`/api/customers/${this.id}`)
this.form = {
name: d?.name || '',
priceLevel: d?.priceLevel || '零售价',
contactName: d?.contactName || '',
mobile: d?.mobile || '',
phone: d?.phone || '',
address: d?.address || '',
arOpening: Number(d?.arOpening || 0),
remark: d?.remark || ''
}
// 同步 priceIdx
const idx = this.priceLevels.indexOf(this.form.priceLevel || '零售价')
this.priceIdx = idx >= 0 ? idx : 0
} catch(e) { uni.showToast({ title: e?.message || '加载失败', icon: 'none' }) }
},
async save() {
if (!this.form.name) return uni.showToast({ title:'请填写客户名称', icon:'none' })
try {

View File

@@ -1,7 +1,7 @@
<template>
<view class="page">
<!-- 业务类型侧边切换销售/进货/收款/资金/盘点 -->
<!-- 业务类型侧边切换销售/进货/收款/资金 -->
<view class="content">
<view class="biz-tabs">
<view v-for="b in bizList" :key="b.key" :class="['biz', biz===b.key && 'active']" @click="switchBiz(b.key)">{{ b.name }}</view>
@@ -42,8 +42,10 @@
<view class="amount" :class="{ in: Number(it.amount||0) >= 0, out: Number(it.amount||0) < 0 }">¥ {{ (it.amount || 0).toFixed(2) }}</view>
<view class="arrow"></view>
</view>
</block>
<view v-else class="empty">暂无数据</view>
</block>
<view v-else class="empty">暂无数据</view>
<view v-if="items.length && !finished" class="loading" v-show="loading">加载中...</view>
<view v-if="finished && items.length" class="finished">没有更多了</view>
</scroll-view>
<!-- 右下角新增按钮根据业务类型跳转对应开单页或创建页 -->
@@ -60,8 +62,7 @@ const API_OF = {
sale: '/api/orders',
purchase: '/api/purchase-orders',
collect: '/api/payments',
fund: '/api/other-transactions',
stock: '/api/inventories/logs'
fund: '/api/other-transactions'
}
export default {
@@ -72,18 +73,19 @@ export default {
{ key: 'sale', name: '出货' },
{ key: 'purchase', name: '进货' },
{ key: 'collect', name: '收款' },
{ key: 'fund', name: '资金' },
{ key: 'stock', name: '盘点' }
{ key: 'fund', name: '资金' }
],
range: 'month',
query: { kw: '' },
items: [],
page: 1,
size: 20,
size: 15,
finished: false,
loading: false,
startDate: '',
endDate: ''
endDate: '',
nonVipRetentionDays: 0,
isVip: false
}
},
computed: {
@@ -124,7 +126,7 @@ export default {
async loadMore() {
if (this.loading || this.finished) return
this.loading = true
try {
try {
const path = API_OF[this.biz] || '/api/orders'
const params = { kw: this.query.kw, page: this.page, size: this.size, startDate: this.startDate, endDate: this.endDate, biz: this.biz }
if (this.biz === 'sale') params.type = 'out'
@@ -133,12 +135,37 @@ export default {
this.items = this.items.concat(list)
if (list.length < this.size) this.finished = true
this.page += 1
await this.hintIfNonVipOutOfWindow()
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally { this.loading = false }
},
async hintIfNonVipOutOfWindow(){
try {
if (this.isVip && this.isVip === true) return
// 缓存一轮,避免每次调用
if (!this.nonVipRetentionDays) {
const v = await get('/api/vip/status')
this.isVip = !!v?.isVip
this.nonVipRetentionDays = Number(v?.nonVipRetentionDays || 60)
if (this.isVip) return
}
if (!this.startDate) return
const start = new Date(this.startDate).getTime()
const threshold = Date.now() - this.nonVipRetentionDays * 24 * 3600 * 1000
if (start < threshold) {
uni.showModal({
title: '提示',
content: `普通用户仅显示近${this.nonVipRetentionDays}天数据开通VIP可查看全部历史。`,
confirmText: '去开通VIP',
cancelText: '我知道了',
success: (r) => { if (r.confirm) uni.navigateTo({ url: '/pages/my/vip' }) }
})
}
} catch (e) {}
},
formatDate(s) { if (!s) return ''; try { const d = new Date(s); const pad = n => String(n).padStart(2, '0'); return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}` } catch (_) { return String(s).slice(0,10) } },
onCreate() { if (this.biz === 'sale') { uni.switchTab({ url: '/pages/order/create' }); return } uni.showToast({ title: '该类型创建页待实现', icon: 'none' }) },
onCreate() { if (this.biz === 'sale') { uni.switchTab({ url: '/pages/order/create' }); return } uni.showToast({ title: '该类型创建页待实现', icon: 'none' }) },
openDetail(it) { uni.showToast({ title: '详情开发中', icon: 'none' }) }
}
}
@@ -154,18 +181,21 @@ export default {
.biz { flex:0 0 120rpx; display:flex; align-items:center; justify-content:center; color:$uni-color-primary; }
.biz.active { background:rgba(76,141,255,0.10); color:$uni-color-primary; font-weight:700; }
.panel { flex:1; display:flex; flex-direction: column; background:#fff; margin: 16rpx; border-radius: 16rpx; padding: 12rpx; border:2rpx solid $uni-border-color; }
.panel { flex:1; display:flex; flex-direction: column; background:#fff; margin: 16rpx; border-radius: 16rpx; padding: 12rpx; }
.toolbar { display:flex; flex-direction: column; gap: 10rpx; padding: 10rpx 6rpx 6rpx; border-bottom:2rpx solid $uni-border-color; }
.period-group { display:flex; align-items:center; gap: 8rpx; background:#f6f8fb; border:2rpx solid #e6ebf2; border-radius: 10rpx; padding: 8rpx 10rpx; }
.period-group { display:flex; align-items:center; gap: 8rpx; background:#f6f8fb; border-radius: 10rpx; padding: 8rpx 10rpx; }
.period-label { color:#6b778c; }
.date-chip { padding: 8rpx 12rpx; background:#fff; border:2rpx solid #e6ebf2; border-radius: 8rpx; }
.sep { color:#99a2b3; padding: 0 6rpx; }
.search-row { display:flex; align-items:center; gap: 10rpx; }
.search { flex:1; }
.search-input { width:100%; background:#fff; border-radius: 12rpx; padding: 12rpx; color:$uni-text-color; border:2rpx solid #e6ebf2; }
.btn { background:$uni-color-primary; color:#fff; border: none; }
.search-row { display:flex; align-items:center; gap: 16rpx; }
.search { flex:1; min-width:0; display:flex; }
.search-input { flex:1; height: 72rpx; line-height: 72rpx; padding: 0 24rpx; box-sizing:border-box; background:#fff; border-radius: 12rpx; color:$uni-text-color; border:2rpx solid #e6ebf2; font-size: 26rpx; }
.btn { flex-shrink:0; display:flex; align-items:center; justify-content:center; height: 72rpx; padding: 0 32rpx; margin-left: 4rpx; border-radius: 12rpx; background:$uni-color-primary; color:#fff; border:none; font-size: 26rpx; box-sizing:border-box; }
.btn::after { border:none; }
.total { color:$uni-color-primary; font-weight: 700; padding: 10rpx 6rpx 12rpx; background:#fff; }
.list { flex:1; }
.loading { text-align:center; padding: 20rpx 0; color:$uni-text-color-grey; }
.finished { text-align:center; padding: 20rpx 0; color:$uni-text-color-grey; }
.item { display:grid; grid-template-columns: 1fr auto auto; align-items:center; gap: 8rpx; padding: 18rpx 12rpx; border-bottom: 1rpx solid $uni-border-color; }
.item-left { display:flex; flex-direction: column; }
.date { color:$uni-text-color-grey; font-size: 24rpx; }

View File

@@ -1,26 +1,11 @@
<template>
<view class="home">
<!-- 公告栏置顶显示可点击查看详情 -->
<view class="notice">
<view class="notice-left">公告</view>
<view v-if="loadingNotices" class="notice-swiper" style="display:flex;align-items:center;color:#6b5a2a;">加载中...</view>
<view v-else-if="noticeError" class="notice-swiper" style="display:flex;align-items:center;color:#dd524d;">{{ noticeError }}</view>
<view v-else-if="!notices.length" class="notice-swiper" style="display:flex;align-items:center;color:#6b5a2a;">暂无公告</view>
<swiper v-else class="notice-swiper" circular autoplay interval="4000" duration="400" vertical>
<swiper-item v-for="(n, idx) in notices" :key="idx">
<view class="notice-item" @click="onNoticeTap(n)">
<text class="notice-text">{{ n.text }}</text>
<text v-if="n.tag" class="notice-tag">{{ n.tag }}</text>
</view>
</swiper-item>
</swiper>
</view>
<!-- 顶部统计卡片 -->
<view class="hero">
<view class="hero-top">
<text class="brand">五金配件管家</text>
<view class="cta">
<text class="cta-text">咨询</text>
<view class="cta" @click="onConsultTap" hover-class="cta-active" hover-stay-time="80">
<text class="cta-text">{{ consultLabel }}</text>
</view>
</view>
<view class="kpi kpi-grid">
@@ -55,7 +40,33 @@
</view>
</view>
<!-- 公告栏已上移到顶部 -->
<!-- 咨询输入弹层 -->
<view v-if="consultDialogVisible" class="dialog-mask" @touchmove.stop.prevent @click.stop>
<view class="dialog">
<view class="dialog-title">咨询</view>
<textarea class="dialog-textarea" v-model="consultMessage" placeholder="请输入咨询内容..." maxlength="500"></textarea>
<view class="dialog-actions">
<view class="btn" @click="closeConsultDialog">取消</view>
<view class="btn primary" @click="submitConsult">提交</view>
</view>
</view>
</view>
<!-- 公告栏放在常用功能上方KPI 下方 -->
<view class="notice">
<view class="notice-left">公告</view>
<view v-if="loadingNotices" class="notice-swiper" style="display:flex;align-items:center;color:#6b5a2a;">加载中...</view>
<view v-else-if="noticeError" class="notice-swiper" style="display:flex;align-items:center;color:#dd524d;">{{ noticeError }}</view>
<view v-else-if="!notices.length" class="notice-swiper" style="display:flex;align-items:center;color:#6b5a2a;">暂无公告</view>
<swiper v-else class="notice-swiper" circular autoplay interval="4000" duration="400" vertical>
<swiper-item v-for="(n, idx) in notices" :key="idx">
<view class="notice-item" @click="onNoticeTap(n)">
<text class="notice-text">{{ n.text }}</text>
<text v-if="n.tag" class="notice-tag">{{ n.tag }}</text>
</view>
</swiper-item>
</swiper>
</view>
<!-- 分割标题产品与功能 -->
<view class="section-title">
@@ -81,7 +92,7 @@
</template>
<script>
import { get } from '../../common/http.js'
import { get, post, put } from '../../common/http.js'
import { ROUTES } from '../../common/constants.js'
import { KPI_ICONS as KPI_ICON_MAP } from '../../common/config.js'
export default {
@@ -93,17 +104,19 @@
notices: [],
loadingNotices: false,
noticeError: '',
consultLabel: '咨询',
consultDialogVisible: false,
consultMessage: '',
features: [
{ key: 'product', title: '货品', img: '/static/icons/product.png', emoji: '📦' },
{ key: 'customer', title: '客户', img: '/static/icons/customer.png', emoji: '👥' },
{ key: 'sale', title: '销售', img: '/static/icons/sale.png', emoji: '💰' },
{ key: 'account', title: '账户', img: '/static/icons/account.png', emoji: '💳' },
{ key: 'supplier', title: '供应商', img: '/static/icons/supplier.png', emoji: '🚚' },
{ key: 'purchase', title: '进货', img: '/static/icons/purchase.png', emoji: '🛒' },
{ key: 'otherPay', title: '其他支出', img: '/static/icons/other-pay.png', emoji: '💸' },
{ key: 'vip', title: 'VIP会员', img: '/static/icons/vip.png', emoji: '👑' },
{ key: 'report', title: '报表', img: '/static/icons/report.png', emoji: '📊' },
{ key: 'more', title: '更多', img: '/static/icons/more.png', emoji: '⋯' }
{ key: 'customer', title: '客户', img: '/static/icons/webwxgetmsgimg.png', emoji: '👥' },
{ key: 'sale', title: '销售', img: '/static/icons/webwxgetmsgimg.jpg', emoji: '💰' },
{ key: 'account', title: '账户', img: '/static/icons/icons8-profile-50.png', emoji: '💳' },
{ key: 'supplier', title: '供应商', img: '/static/icons/icons8-supplier-50.png', emoji: '🚚' },
{ key: 'purchase', title: '进货', img: '/static/icons/icons8-dollar-ethereum-exchange-50.png', emoji: '🛒' },
{ key: 'otherPay', title: '其他支出', img: '/static/icons/icons8-expenditure-64.png', emoji: '💸' },
{ key: 'vip', title: 'VIP会员', img: '/static/icons/icons8-vip-48.png', emoji: '👑' },
{ key: 'report', title: '报表', img: '/static/icons/icons8-graph-report-50.png', emoji: '📊' }
]
}
},
@@ -117,6 +130,7 @@
}
this.fetchMetrics()
this.fetchNotices()
this.fetchLatestConsult()
},
methods: {
async fetchMetrics() {
@@ -134,6 +148,47 @@
// 忽略错误,保留默认值
}
},
async fetchLatestConsult() {
try {
const d = await get('/api/consults')
if (d && d.replied) this.consultLabel = '已回复'
else this.consultLabel = '咨询'
this._latestConsult = d
} catch(e) { this.consultLabel = '咨询' }
},
onConsultTap() {
if (this.consultLabel === '已回复' && this._latestConsult && this._latestConsult.id) {
// 展示最近一次咨询与管理员最新回复
const msg = (this._latestConsult.latestReply ? (this._latestConsult.latestReply) : (this._latestConsult.message || ''))
uni.showModal({ title: '咨询回复', content: msg || '暂无内容', showCancel: false, success: async (res) => {
if (!res || res.confirm !== true) return
try {
const r = await put(`/api/consults/${this._latestConsult.id}/ack`, {})
this.consultLabel = '咨询'
this._latestConsult = null
setTimeout(() => this.fetchLatestConsult(), 200)
} catch(e) {
try { uni.showToast({ title: (e && e.message) || '已读同步失败', icon: 'none' }) } catch(_){}
}
}})
return
}
this.consultMessage = ''
this.consultDialogVisible = true
},
closeConsultDialog() { this.consultDialogVisible = false },
async submitConsult() {
const text = String(this.consultMessage || '').trim()
if (!text) { uni.showToast({ title: '请输入咨询内容', icon: 'none' }); return }
try {
await post('/api/consults', { message: text })
this.consultDialogVisible = false
uni.showToast({ title: '已提交', icon: 'success' })
setTimeout(() => this.fetchLatestConsult(), 300)
} catch (e) {
uni.showToast({ title: (e && e.message) || '提交失败', icon: 'none' })
}
},
async fetchNotices() {
this.loadingNotices = true
this.noticeError = ''
@@ -215,12 +270,21 @@
</script>
<style lang="scss">
page {
height: 100%;
overflow: hidden;
background: linear-gradient(180deg, #f8fbff 0%, #ffffff 60%);
}
.home {
padding-bottom: 140rpx;
position: relative;
/* 渐变背景:顶部淡蓝过渡到白色 */
background: linear-gradient(180deg, #f8fbff 0%, #ffffff 60%);
min-height: 100vh;
height: 100vh;
display: flex;
flex-direction: column;
padding-bottom: calc(env(safe-area-inset-bottom) + 32rpx);
position: relative;
/* 渐变背景:顶部淡蓝过渡到白色 */
background: linear-gradient(180deg, #f8fbff 0%, #ffffff 60%);
overflow: hidden;
box-sizing: border-box;
}
@@ -256,19 +320,20 @@
/* 分割标题 */
.section-title { display: flex; align-items: center; gap: 16rpx; padding: 10rpx 28rpx 0; }
.section-title { display: flex; align-items: center; gap: 16rpx; padding: 10rpx 28rpx 0; flex: 0 0 auto; }
.section-title::before { content: ''; display: block; width: 8rpx; height: 28rpx; border-radius: 8rpx; background: $uni-color-primary; }
.section-text { color: $uni-text-color; font-size: 30rpx; font-weight: 700; letter-spacing: 1rpx; }
/* 顶部英雄区:浅色玻璃卡片,带金色描边与柔和阴影 */
.hero {
margin: 16rpx 20rpx;
padding: 18rpx;
padding: 18rpx 18rpx 12rpx;
border-radius: 20rpx;
background: #ffffff;
border: 2rpx solid $uni-border-color;
box-shadow: none;
color: $uni-text-color;
flex: 0 0 auto;
}
.hero-top {
@@ -297,17 +362,26 @@
.cta-text { color: #fff; font-size: 30rpx; font-weight: 700; letter-spacing: 1rpx; }
/* KPI 卡片化布局2×2 */
.kpi { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16rpx; }
/* 简易弹层样式 */
.dialog-mask { position: fixed; left:0; right:0; top:0; bottom:0; background: rgba(0,0,0,0.45); display:flex; align-items:center; justify-content:center; z-index: 10000; }
.dialog { width: 82vw; background: #fff; border-radius: 16rpx; padding: 20rpx; border: 2rpx solid #eef2f6; }
.dialog-title { font-size: 32rpx; font-weight: 800; color: $uni-text-color; margin-bottom: 16rpx; }
.dialog-textarea { width: 100%; min-height: 180rpx; border: 2rpx solid #e8eef8; border-radius: 12rpx; padding: 12rpx; box-sizing: border-box; }
.dialog-actions { display:flex; justify-content:flex-end; gap: 18rpx; margin-top: 16rpx; }
.btn { padding: 10rpx 22rpx; border-radius: 999rpx; background: #f3f6fb; color: #334155; border: 2rpx solid #e2e8f0; font-weight: 700; }
.btn.primary { background: #4C8DFF; color: #fff; border-color: #4C8DFF; }
/* KPI 卡片化布局:横向铺满 */
.kpi { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12rpx; }
.kpi-item { text-align: center; background:#ffffff; border: 2rpx solid $uni-border-color; border-radius: 16rpx; padding: 16rpx 8rpx; }
/* KPI 卡片(更扁平,降低高度) */
.kpi-grid { grid-template-columns: repeat(2, 1fr); gap: 16rpx; }
.kpi-card { display:flex; align-items:center; gap: 12rpx; text-align:left; padding: 12rpx 14rpx; border-radius: 12rpx; background:#fff; border:2rpx solid #eef2f6; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04); }
.kpi-grid { gap: 12rpx; }
.kpi-card { display:flex; flex-direction:column; align-items:flex-start; justify-content:center; gap: 8rpx; text-align:left; padding: 12rpx 14rpx; border-radius: 14rpx; background:#fff; border:2rpx solid #eef2f6; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04); min-height: 120rpx; }
.kpi-icon { width: 44rpx; height: 44rpx; opacity: .9; }
.kpi-content { display:flex; flex-direction:column; }
.kpi-label { color:#6b778c; font-weight:700; font-size: 24rpx; line-height: 30rpx; }
.kpi-value { color:#4C8DFF; font-size: 36rpx; line-height: 40rpx; margin-top: 0; font-weight: 800; }
.kpi-value { color:#4C8DFF; font-size: 34rpx; line-height: 38rpx; margin-top: 0; font-weight: 800; }
/* 常用功能:胶囊+阴影卡片样式的图标栅格(旧风格保留以防回退) */
@@ -318,21 +392,27 @@
/* 功能容器:更轻的留白 */
.grid-wrap {
margin: 8rpx 12rpx 24rpx;
padding: 8rpx 8rpx 0;
border-radius: 20rpx;
background: transparent;
border: 0;
flex: 1 1 auto;
display:flex;
align-items:stretch;
justify-content:center;
margin: 16rpx 20rpx 28rpx;
padding: 32rpx 30rpx;
border-radius: 26rpx;
background: rgba(255,255,255,0.96);
border: 2rpx solid #edf2f9;
box-shadow: 0 12rpx 28rpx rgba(32,75,143,0.10);
box-sizing: border-box;
}
/* 功能卡片宫格:方形竖排,图标在上文字在下(与截图一致) */
.feature-grid { display:grid; grid-template-columns: repeat(3, 1fr); gap: 14rpx; padding: 8rpx 8rpx 18rpx; }
.feature-card { height: 164rpx; background:#fff; border:2rpx solid #eef2f6; border-radius: 16rpx; box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.04); padding: 12rpx; display:flex; flex-direction: column; align-items:center; justify-content:center; }
.fc-icon { width: 86rpx; height: 86rpx; border-radius: 18rpx; background: #f7faff; border:2rpx solid #e8eef8; display:flex; align-items:center; justify-content:center; }
.fc-img { width: 56rpx; height: 56rpx; opacity: .95; }
.feature-grid { flex: 1 1 auto; width: 100%; height: 100%; display:grid; grid-template-columns: repeat(3, minmax(0, 1fr)); grid-auto-rows: 1fr; gap: 32rpx 28rpx; align-content: space-evenly; justify-items: center; }
.feature-card { width: 168rpx; height: 176rpx; background:#fff; border:2rpx solid #eef2f6; border-radius: 20rpx; box-shadow: 0 10rpx 24rpx rgba(0,0,0,0.05); padding: 18rpx 16rpx; display:flex; flex-direction: column; align-items:center; justify-content:center; }
.fc-icon { width: 78rpx; height: 78rpx; border-radius: 18rpx; background: #f7faff; display:flex; align-items:center; justify-content:center; }
.fc-img { width: 54rpx; height: 54rpx; opacity: .95; }
.fc-emoji { font-size: 48rpx; }
.fc-placeholder { width: 56rpx; height: 56rpx; border-radius: 12rpx; background: $uni-bg-color-hover; border: 2rpx solid #e8eef8; }
.fc-title { margin-top: 10rpx; font-size: 26rpx; font-weight: 700; color: $uni-text-color; }
.fc-title { margin-top: 12rpx; font-size: 28rpx; font-weight: 700; color: $uni-text-color; letter-spacing: 1rpx; }
/* 底部操作条:浅色半透明 + 金色主按钮 */
.bottom-bar {

View File

@@ -1,21 +1,25 @@
<template>
<view class="me">
<view v-if="!isLoggedIn" class="card login">
<view class="login-title">登录/注册以同步数据</view>
<button class="login-btn" type="primary" @click="goLogin">登录</button>
<button class="login-btn minor" @click="goRegister">注册</button>
</view>
<view class="card user">
<image class="avatar" :src="avatarUrl" mode="aspectFill" @error="onAvatarError" />
<view class="card user" v-if="isLoggedIn">
<image class="avatar" :src="avatarDisplay" mode="aspectFill" @error="onAvatarError" />
<view class="meta">
<text class="name">{{ shopName }}</text>
<text class="phone">{{ mobileDisplay }}</text>
<text class="phone">{{ emailDisplay }}</text>
<text class="role">老板</text>
</view>
</view>
<view v-else class="card user guest">
<image class="avatar" src="/static/icons/icons8-login-50.png" mode="aspectFill" />
<view class="meta">
<text class="name">未登录</text>
<text class="phone">登录后同步数据</text>
<text class="role">访客</text>
</view>
<button class="login-entry" @click="goLogin">登录</button>
</view>
<!-- VIP 卡片置于会员与订单分组上方 -->
<view class="card vip" :class="{ active: vipIsVip }">
<view v-if="isLoggedIn" class="card vip" :class="{ active: vipIsVip }">
<view class="vip-row">
<text class="vip-badge">{{ vipIsVip ? 'VIP' : '非VIP' }}</text>
<text class="vip-title">会员状态</text>
@@ -35,7 +39,11 @@
<view class="group">
<view class="group-title">会员与订单</view>
<view class="cell" @click="goVip">
<text>VIP会员</text>
<view class="cell-left">
<text>VIP会员</text>
<text v-if="vipIsVip" class="vip-tag">已开通</text>
<text v-else class="vip-tag pending">待开通</text>
</view>
<text class="arrow"></text>
</view>
<view class="cell" @click="goMyOrders">
@@ -44,42 +52,16 @@
</view>
</view>
<view class="group">
<view class="group-title">基础管理</view>
<view class="cell" @click="goSupplier">
<text>供应商管理</text>
<text class="arrow"></text>
</view>
<view class="cell" @click="goCustomer">
<text>客户管理</text>
<text class="arrow"></text>
</view>
<view class="cell" @click="goCustomerQuote">
<text>客户报价</text>
<text class="arrow"></text>
</view>
<view class="cell" @click="goShop">
<text>店铺管理</text>
<text class="arrow"></text>
</view>
</view>
<view class="group">
<view class="group-title">设置中心</view>
<view class="cell" @click="editProfile">
<text>账号与安全</text>
<text class="desc">修改头像姓名密码</text>
<text class="arrow"></text>
</view>
<view class="cell" @click="goProductSettings">
<text>商品设置</text>
<text class="arrow"></text>
</view>
<view class="cell" @click="goSystemParams">
<text>系统参数</text>
<text class="desc">低价提示默认收款单行折扣等</text>
<text class="desc">修改头像姓名密码电话</text>
<text class="arrow"></text>
</view>
<view class="cell" @click="goAbout">
<text>关于与协议</text>
<text class="arrow"></text>
@@ -93,79 +75,158 @@
<script>
import { get } from '../../common/http.js'
import { API_BASE_URL } from '../../common/config.js'
function normalizeAvatar(url) {
if (!url) return '/static/icons/icons8-mitt-24.png'
const s = String(url)
if (/^https?:\/\//i.test(s)) return s
if (!API_BASE_URL) return s
if (s.startsWith('/')) return `${API_BASE_URL}${s}`
return `${API_BASE_URL}/${s}`
}
export default {
data() {
return {
avatarUrl: '/static/logo.png',
shopName: '未登录',
mobile: '',
pendingJsCode: '',
logging: false,
vipIsVip: false,
vipStart: '',
vipEnd: ''
}
},
onShow() {
this.fetchProfile()
this.loadVipFromStorage()
try {
if (uni.getStorageSync('TOKEN')) {
// 已登录时刷新资料并隐藏登录卡片
this.$forceUpdate && this.$forceUpdate()
}
} catch(e) {}
},
computed: {
isLoggedIn() { try { return !!uni.getStorageSync('TOKEN') } catch(e){ return false } },
mobileDisplay() {
const m = String(this.mobile || '')
return m.length === 11 ? m.slice(0,3) + '****' + m.slice(7) : (m || '未绑定手机号')
},
vipStartDisplay() { return this.formatDisplay(this.vipStart) },
vipEndDisplay() { return this.formatDisplay(this.vipEnd) }
},
methods: {
data() {
return {
avatarUrl: '/static/icons/icons8-mitt-24.png',
shopName: '未登录',
mobile: '',
pendingJsCode: '',
logging: false,
vipIsVip: false,
vipStart: '',
vipEnd: ''
}
},
onShow() {
this.fetchProfile()
this.loadVip()
try {
if (uni.getStorageSync('TOKEN')) {
// 已登录时刷新资料并隐藏登录卡片
this.$forceUpdate && this.$forceUpdate()
}
} catch(e) {}
},
computed: {
isLoggedIn() { try { return !!uni.getStorageSync('TOKEN') } catch(e){ return false } },
avatarDisplay() { return normalizeAvatar(this.avatarUrl) },
emailDisplay() {
if (!this.isLoggedIn) return ''
const e = String(uni.getStorageSync('USER_EMAIL') || '')
if (!e) return '未绑定邮箱'
const at = e.indexOf('@')
if (at > 1) {
const name = e.slice(0, at)
const domain = e.slice(at)
return (name.length <= 2 ? name[0] + '*' : name.slice(0,2) + '***') + domain
}
return e
},
vipStartDisplay() { return this.formatDisplay(this.vipStart) },
vipEndDisplay() { return this.formatDisplay(this.vipEnd) }
},
methods: {
// 登录相关方法已移除
async fetchProfile() {
// 未登录则不触发任何用户/店铺接口,也不加载本地用户字段
const hasToken = (() => { try { return !!uni.getStorageSync('TOKEN') } catch(e){ return false } })()
if (!hasToken) {
this.shopName = '未登录'
this.avatarUrl = '/static/logo.png'
this.mobile = ''
return
}
// 已登录:拉取概览以确认在线状态,并回填本地用户信息
try { await get('/api/dashboard/overview') } catch(e) {}
try {
const storeName = uni.getStorageSync('SHOP_NAME') || ''
const avatar = uni.getStorageSync('USER_AVATAR') || ''
const phone = uni.getStorageSync('USER_MOBILE') || ''
if (storeName) this.shopName = storeName
if (avatar) this.avatarUrl = avatar
this.mobile = phone
} catch(e) {}
},
loadVipFromStorage() {
try {
const isVip = String(uni.getStorageSync('USER_VIP_IS_VIP') || 'false').toLowerCase() === 'true'
const start = uni.getStorageSync('USER_VIP_START') || ''
const end = uni.getStorageSync('USER_VIP_END') || ''
this.vipIsVip = isVip
this.vipStart = start
this.vipEnd = end
} catch(e) {}
},
formatDisplay(value) {
if (!value) return '-'
const s = String(value)
// 简单规范化:只保留到分钟
const m = s.match(/^(\d{4}-\d{2}-\d{2})([ T](\d{2}:\d{2}))/)
if (m) return `${m[1]} ${m[3]}`
return s
},
async fetchProfile() {
const hasToken = (() => { try { return !!uni.getStorageSync('TOKEN') } catch(e){ return false } })()
if (!hasToken) {
this.shopName = '未登录'
this.avatarUrl = '/static/icons/icons8-mitt-24.png'
this.mobile = ''
return
}
try {
const profile = await get('/api/user/me')
const latestAvatar = profile?.avatarUrl || ''
if (latestAvatar) {
const bust = `${latestAvatar}${latestAvatar.includes('?') ? '&' : '?'}t=${Date.now()}`
this.avatarUrl = bust
try {
uni.setStorageSync('USER_AVATAR_RAW', latestAvatar)
uni.setStorageSync('USER_AVATAR', latestAvatar)
} catch(_){}
} else {
const cached = uni.getStorageSync('USER_AVATAR') || ''
this.avatarUrl = cached || '/static/icons/icons8-mitt-24.png'
}
const storeName = profile?.name || uni.getStorageSync('SHOP_NAME') || '未命名店铺'
this.shopName = storeName
const phone = profile?.phone || uni.getStorageSync('USER_MOBILE') || ''
this.mobile = phone
} catch(e) {
try {
const storeName = uni.getStorageSync('SHOP_NAME') || ''
const avatar = uni.getStorageSync('USER_AVATAR') || ''
const phone = uni.getStorageSync('USER_MOBILE') || ''
if (storeName) this.shopName = storeName
if (avatar) this.avatarUrl = avatar
this.mobile = phone
} catch(_){}
}
},
async loadVip() {
try {
const hasToken = (() => { try { return !!uni.getStorageSync('TOKEN') } catch(e){ return false } })()
if (!hasToken) {
// 未登录:不读取本地缓存,直接复位为默认值并隐藏卡片(由 v-if 控制)
this.vipIsVip = false
this.vipStart = ''
this.vipEnd = ''
return
}
const data = await get('/api/vip/status')
const active = !!data?.isVip
this.vipIsVip = active
this.vipEnd = data?.expireAt || ''
// 根据结束时间动态计算开始时间(固定退 1 个月)
let computedStart = ''
const exp = this.vipEnd
if (exp) {
const m = String(exp).match(/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2}))?)?/)
if (m) {
const y = Number(m[1])
const mo = Number(m[2]) - 1
const da = Number(m[3])
const hh = Number(m[4] || '0')
const mm = Number(m[5] || '0')
const ss = Number(m[6] || '0')
const startDate = new Date(y, mo - 1, da, hh, mm, ss)
const y2 = startDate.getFullYear()
const m2 = (startDate.getMonth() + 1).toString().padStart(2, '0')
const d2 = startDate.getDate().toString().padStart(2, '0')
const h2 = startDate.getHours().toString().padStart(2, '0')
const i2 = startDate.getMinutes().toString().padStart(2, '0')
computedStart = `${y2}-${m2}-${d2} ${h2}:${i2}`
}
}
// 写入到本地与内存
this.vipStart = computedStart
// 同步到本地,便于其他页面读取
try {
uni.setStorageSync('USER_VIP_IS_VIP', String(active))
uni.setStorageSync('USER_VIP_END', this.vipEnd)
if (this.vipStart) uni.setStorageSync('USER_VIP_START', this.vipStart); else uni.removeStorageSync('USER_VIP_START')
} catch(_) {}
} catch(e) {
// 网络异常回退到缓存
try {
const isVip = String(uni.getStorageSync('USER_VIP_IS_VIP') || 'false').toLowerCase() === 'true'
this.vipIsVip = isVip
this.vipStart = uni.getStorageSync('USER_VIP_START') || ''
this.vipEnd = uni.getStorageSync('USER_VIP_END') || ''
} catch(_) {}
}
},
formatDisplay(value) {
if (!value) return '-'
const s = String(value)
// 简单规范化:只保留到分钟
const m = s.match(/^(\d{4}-\d{2}-\d{2})([ T](\d{2}:\d{2}))/)
if (m) return `${m[1]} ${m[3]}`
return s
},
startLogin() {
if (this.logging) return
this.logging = true
@@ -194,7 +255,6 @@ export default {
}, fail: () => { this.logging = false; uni.showToast({ title: '微信登录失败', icon: 'none' }) } })
},
goLogin(){ uni.navigateTo({ url: '/pages/auth/login' }) },
goRegister(){ uni.navigateTo({ url: '/pages/auth/register' }) },
onGetPhoneNumber(e) {
if (this.logging) return
this.logging = true
@@ -208,17 +268,11 @@ export default {
},
goSmsLogin(){ uni.navigateTo({ url: '/pages/my/sms-login' }) },
onAvatarError() {
this.avatarUrl = '/static/logo.png'
this.avatarUrl = '/static/icons/icons8-mitt-24.png'
},
goVip() { uni.navigateTo({ url: '/pages/my/vip' }) },
goMyOrders() { uni.switchTab({ url: '/pages/detail/index' }) },
goSupplier() { uni.navigateTo({ url: '/pages/supplier/select' }) },
goCustomer() { uni.navigateTo({ url: '/pages/customer/select' }) },
goCustomerQuote() { uni.showToast({ title: '客户报价(开发中)', icon: 'none' }) },
goShop() { uni.showToast({ title: '店铺管理(开发中)', icon: 'none' }) },
editProfile() { uni.showToast({ title: '账号与安全(开发中)', icon: 'none' }) },
goProductSettings() { uni.navigateTo({ url: '/pages/product/settings' }) },
goSystemParams() { uni.showToast({ title: '系统参数(开发中)', icon: 'none' }) },
goMyOrders() { uni.navigateTo({ url: '/pages/my/orders' }) },
editProfile() { uni.navigateTo({ url: '/pages/my/security' }) },
goAbout() { uni.navigateTo({ url: '/pages/my/about' }) },
logout() {
try {
@@ -228,9 +282,14 @@ export default {
uni.removeStorageSync('DEFAULT_USER_ID')
uni.setStorageSync('ENABLE_DEFAULT_USER', 'false')
uni.removeStorageSync('USER_AVATAR')
uni.removeStorageSync('USER_AVATAR_RAW')
uni.removeStorageSync('USER_NAME')
uni.removeStorageSync('USER_MOBILE')
uni.removeStorageSync('USER_EMAIL')
uni.removeStorageSync('SHOP_NAME')
uni.removeStorageSync('USER_VIP_IS_VIP')
uni.removeStorageSync('USER_VIP_START')
uni.removeStorageSync('USER_VIP_END')
uni.showToast({ title: '已清理本地信息', icon: 'none' })
setTimeout(() => { uni.reLaunch({ url: '/pages/index/index' }) }, 300)
} catch(e) { uni.reLaunch({ url: '/pages/index/index' }) }
@@ -247,6 +306,9 @@ export default {
.login-btn.minor { background: $uni-bg-color-hover; color: $uni-text-color; }
.hint { font-size: 22rpx; color: $uni-text-color-grey; }
.card.user { display: flex; gap: 18rpx; padding: 22rpx; background: $uni-bg-color-grey; border-radius: 16rpx; box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.16); align-items: center; }
.card.user.guest { justify-content: space-between; }
.card.user.guest .meta { flex: 1; }
.card.user.guest .login-entry { padding: 12rpx 30rpx; border-radius: 999rpx; background: $uni-color-primary; color: #fff; font-size: 28rpx; font-weight: 600; }
.avatar { width: 120rpx; height: 120rpx; border-radius: 60rpx; background: $uni-bg-color-hover; }
.meta { display: flex; flex-direction: column; gap: 6rpx; }
.name { font-size: 34rpx; font-weight: 700; color: $uni-text-color; }
@@ -266,7 +328,10 @@ export default {
.group { margin-top: 24rpx; background: $uni-bg-color-grey; border-radius: 16rpx; overflow: hidden; }
.group-title { padding: 18rpx 22rpx; font-size: 26rpx; color: $uni-text-color-grey; background: $uni-bg-color-hover; }
.cell { display: flex; align-items: center; padding: 26rpx 22rpx; border-top: 1rpx solid $uni-border-color; color: $uni-text-color; }
.cell { display: flex; align-items: center; padding: 26rpx 22rpx; border-top: 1rpx solid $uni-border-color; color: $uni-text-color; gap: 18rpx; }
.cell-left { display:flex; align-items:center; gap: 14rpx; }
.vip-tag { padding: 4rpx 12rpx; border-radius: 999rpx; background: rgba(76,141,255,0.15); color: $uni-color-primary; font-size: 22rpx; }
.vip-tag.pending { background: rgba(76,141,255,0.06); color: #99a2b3; }
.cell .desc { margin-left: auto; margin-right: 8rpx; font-size: 22rpx; color: $uni-text-color-grey; }
.cell .arrow { margin-left: auto; color: #99a2b3; }
.cell.danger { color: #dd524d; justify-content: center; font-weight: 700; }

View File

@@ -0,0 +1,65 @@
<template>
<view class="orders">
<view class="hint" v-if="!isLoggedIn">请先登录后查看VIP支付记录</view>
<view v-else>
<view class="item" v-for="it in list" :key="it.id">
<view class="row1">
<text class="price">¥ {{ toMoney(it.price) }}</text>
<text class="channel">{{ it.channel || '支付' }}</text>
</view>
<view class="row2">
<text class="date">{{ fmt(it.createdAt) }}</text>
<text class="duration">{{ it.durationDays }} </text>
</view>
<view class="row3" v-if="it.expireTo">
<text class="expire">有效期至 {{ fmt(it.expireTo) }}</text>
</view>
</view>
<view class="empty" v-if="list.length===0">暂无支付记录</view>
</view>
</view>
</template>
<script>
import { get } from '../../common/http.js'
export default {
data(){
return { list: [], page: 1, size: 20, loading: false }
},
onShow(){ this.fetch(true) },
computed: {
isLoggedIn(){ try { return !!uni.getStorageSync('TOKEN') } catch(e){ return false } }
},
methods: {
async fetch(reset=false){
if (!this.isLoggedIn) return
if (this.loading) return
this.loading = true
try {
const p = reset ? 1 : this.page
const data = await get('/api/vip/recharges', { page: p, size: this.size })
const arr = Array.isArray(data?.list) ? data.list : []
this.list = reset ? arr : (this.list || []).concat(arr)
this.page = p + 1
} finally { this.loading = false }
},
fmt(v){ if (!v) return ''; const s = String(v); const m = s.match(/^(\d{4}-\d{2}-\d{2})([ T](\d{2}:\d{2}))/); return m ? `${m[1]} ${m[3]}` : s },
toMoney(v){ try { return Number(v).toFixed(2) } catch(_) { return v } }
}
}
</script>
<style lang="scss">
.orders { padding: 16rpx 16rpx calc(env(safe-area-inset-bottom) + 16rpx); }
.hint { color: $uni-text-color-grey; padding: 24rpx; text-align: center; }
.item { background:#fff; border:1rpx solid $uni-border-color; border-radius: 16rpx; padding: 18rpx; margin: 12rpx 0; }
.row1 { display:flex; justify-content: space-between; align-items:center; margin-bottom: 6rpx; }
.price { color:#111; font-weight: 800; font-size: 34rpx; }
.channel { color:#666; font-size: 24rpx; }
.row2 { display:flex; justify-content: space-between; color:#666; font-size: 24rpx; }
.row3 { margin-top: 6rpx; color:#4C8DFF; font-size: 24rpx; }
.empty { text-align:center; color:#999; padding: 40rpx 0; }
</style>

View File

@@ -0,0 +1,200 @@
<template>
<view class="security">
<view class="card">
<view class="cell" @click="openAvatarDialog">
<text class="cell-label">头像</text>
<image class="avatar-preview" :src="avatarPreview" mode="aspectFill" />
<text class="arrow"></text>
</view>
<view class="cell">
<text class="cell-label">姓名</text>
<input class="cell-input" type="text" v-model.trim="form.name" placeholder="请输入姓名" />
</view>
<button class="btn" type="primary" :loading="savingProfile" @click="saveProfile">保存资料</button>
</view>
<view class="card">
<view class="row">
<text class="label">旧密码</text>
<input class="input" password v-model.trim="pwd.oldPassword" placeholder="如从未设置可留空" />
</view>
<view class="row">
<text class="label">新密码</text>
<input class="input" password v-model.trim="pwd.newPassword" placeholder="至少6位" />
</view>
<button class="btn" :loading="savingPwd" @click="changePassword">修改密码</button>
</view>
<view class="card">
<view class="row">
<text class="label">手机号</text>
<input class="input" type="text" v-model.trim="phone.phone" placeholder="11位手机号" />
</view>
<button class="btn" :loading="savingPhone" @click="changePhoneDirect">保存手机号</button>
</view>
</view>
</template>
<script>
import { get, put, post, upload } from '../../common/http.js'
import { API_BASE_URL } from '../../common/config.js'
export default {
data() {
return {
form: { name: '', avatarUrl: '' },
pwd: { oldPassword: '', newPassword: '' },
phone: { phone: '' },
savingProfile: false,
savingPwd: false,
savingPhone: false,
sendingCode: false,
originalAvatarUrl: ''
}
},
onShow() { this.loadProfile() },
computed: {
avatarPreview() {
return this.normalizeAvatar(this.form.avatarUrl)
},
canSendPhone() {
const p = String(this.phone.phone || '').trim()
return /^1\d{10}$/.test(p)
}
},
methods: {
async loadProfile() {
try {
const data = await get('/api/user/me')
const rawAvatar = data?.avatarUrl || (uni.getStorageSync('USER_AVATAR_RAW') || '')
this.originalAvatarUrl = rawAvatar
this.form.name = data?.name || (uni.getStorageSync('USER_NAME') || '')
this.form.avatarUrl = rawAvatar
} catch (e) {}
},
normalizeAvatar(url) {
if (!url) return '/static/icons/icons8-mitt-24.png'
const s = String(url)
if (/^https?:\/\//i.test(s)) return s
const base = API_BASE_URL || ''
if (!base) return s
if (s.startsWith('/')) return `${base}${s}`
return `${base}/${s}`
},
openAvatarDialog() {
uni.showActionSheet({
itemList: ['粘贴图片URL', '从相册选择并上传'],
success: (res) => {
if (res.tapIndex === 0) {
uni.showModal({
title: '头像URL',
editable: true,
placeholderText: 'https://...',
success: async (m) => {
if (m.confirm && m.content) {
this.form.avatarUrl = m.content.trim()
await this.saveProfile({ auto: true })
}
}
})
} else if (res.tapIndex === 1) {
uni.chooseImage({ count: 1, sizeType: ['compressed'], success: (ci) => {
const filePath = (ci.tempFilePaths && ci.tempFilePaths[0]) || ''
if (!filePath) return
uni.showLoading({ title: '上传中...' })
upload('/api/attachments', filePath, { ownerType: 'user_avatar', ownerId: 0 }).then(async (data) => {
const url = data && (data.url || data.path)
if (url) {
this.form.avatarUrl = url
await this.saveProfile({ auto: true })
}
uni.showToast({ title: '已上传', icon: 'success' })
}).catch(e => {
uni.showToast({ title: (e && e.message) || '上传失败', icon: 'none' })
}).finally(() => { uni.hideLoading() })
} })
}
}
})
},
async saveProfile(opts = {}) {
const auto = opts && opts.auto
const payload = {}
if (this.form.name && this.form.name !== uni.getStorageSync('USER_NAME')) payload.name = this.form.name
if (this.form.avatarUrl && this.form.avatarUrl !== this.originalAvatarUrl) payload.avatarUrl = this.form.avatarUrl
if (Object.keys(payload).length === 0) {
if (!auto) uni.showToast({ title: '无需修改', icon: 'none' })
return
}
if (this.savingProfile) return
this.savingProfile = true
try {
await put('/api/user/me', payload)
try {
if (payload.name) uni.setStorageSync('USER_NAME', payload.name)
if (payload.avatarUrl) {
const rawUrl = payload.avatarUrl
const displayUrl = `${rawUrl}${rawUrl.includes('?') ? '&' : '?'}t=${Date.now()}`
uni.setStorageSync('USER_AVATAR_RAW', rawUrl)
uni.setStorageSync('USER_AVATAR', rawUrl)
this.originalAvatarUrl = rawUrl
this.form.avatarUrl = rawUrl
}
} catch(_) {}
if (!payload.avatarUrl && this.form.avatarUrl) {
uni.setStorageSync('USER_AVATAR_RAW', this.form.avatarUrl)
uni.setStorageSync('USER_AVATAR', this.form.avatarUrl)
}
uni.showToast({ title: auto ? '头像已更新' : '已保存', icon: 'success' })
} catch (e) {
const msg = (e && e.message) || '保存失败'
uni.showToast({ title: msg, icon: 'none' })
} finally {
this.savingProfile = false
}
},
async changePassword() {
if (!this.pwd.newPassword || this.pwd.newPassword.length < 6) return uni.showToast({ title: '新密码至少6位', icon: 'none' })
this.savingPwd = true
try {
await put('/api/user/me/password', { oldPassword: this.pwd.oldPassword || undefined, newPassword: this.pwd.newPassword })
this.pwd.oldPassword = ''
this.pwd.newPassword = ''
uni.showToast({ title: '密码已修改', icon: 'success' })
} catch (e) {
uni.showToast({ title: (e && e.message) || '修改失败', icon: 'none' })
} finally { this.savingPwd = false }
},
async changePhoneDirect() {
if (!this.canSendPhone) return uni.showToast({ title: '请输入正确手机号', icon: 'none' })
this.savingPhone = true
try {
await put('/api/user/me/phone', { phone: this.phone.phone })
uni.setStorageSync('USER_MOBILE', this.phone.phone)
uni.showToast({ title: '手机号已保存', icon: 'success' })
} catch (e) {
uni.showToast({ title: (e && e.message) || '保存失败', icon: 'none' })
} finally { this.savingPhone = false }
}
}
}
</script>
<style lang="scss">
.security { padding: 24rpx; }
.card { background: $uni-bg-color-grey; border-radius: 16rpx; padding: 22rpx; margin-bottom: 24rpx; box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.08); }
.cell { display: flex; align-items: center; gap: 16rpx; padding: 20rpx 0; border-bottom: 1rpx solid $uni-border-color; }
.cell:last-of-type { border-bottom: none; }
.cell-label { flex: 1; font-size: 28rpx; color: $uni-text-color; }
.cell-input { flex: 2; height: 72rpx; padding: 0 16rpx; border: 1rpx solid $uni-border-color; border-radius: 10rpx; background: #fff; color: $uni-text-color; }
.avatar-preview { width: 100rpx; height: 100rpx; border-radius: 16rpx; background: $uni-bg-color-hover; }
.arrow { margin-left: 12rpx; color: #99a2b3; font-size: 32rpx; }
.row { display: flex; align-items: center; gap: 16rpx; margin-bottom: 16rpx; }
.label { width: 160rpx; color: $uni-text-color; font-size: 28rpx; }
.input { flex: 1; height: 72rpx; padding: 0 16rpx; border: 1rpx solid $uni-border-color; border-radius: 10rpx; background: #fff; color: $uni-text-color; }
.btn { margin-top: 8rpx; }
.btn.minor { background: $uni-bg-color-hover; color: $uni-text-color; }
</style>

View File

@@ -1,27 +1,26 @@
<template>
<view class="page sms-login">
<view class="card">
<view class="title">短信验证码登录</view>
<view class="title">邮箱验证码登录</view>
<view class="form">
<input class="input" type="number" maxlength="11" placeholder="请输入手机号" v-model="phone"/>
<input class="input" type="text" placeholder="请输入邮箱地址" v-model="email"/>
<view class="row">
<input class="input code" type="number" maxlength="6" placeholder="请输入验证码" v-model="code"/>
<button class="send" :disabled="countdown>0 || sending" @click="sendCode">{{ btnText }}</button>
</view>
<button class="login" type="primary" :disabled="logging" @click="doLogin">登录/注册</button>
<button class="login" :disabled="logging" @click="quickRegister">注册为店主</button>
</view>
<view class="hint">首次登录将自动创建店铺与店主用户</view>
<view class="debug">
<view class="debug-title" @click="toggleDebug">请求体示例点击{{ showDebug? '收起':'展开' }}</view>
<view v-if="showDebug" class="debug-body">
<view class="code-title">POST /api/auth/sms/send</view>
<view class="code-title">POST /api/auth/email/send</view>
<view class="code-wrap">
<text class="code">{{ sendBodyJson }}</text>
<button size="mini" class="copy" @click="copy(sendBodyJson)">复制</button>
</view>
<view class="code-title">POST /api/auth/sms/login</view>
<view class="code-title">POST /api/auth/email/login</view>
<view class="code-wrap">
<text class="code">{{ loginBodyJson }}</text>
<button size="mini" class="copy" @click="copy(loginBodyJson)">复制</button>
@@ -37,18 +36,18 @@ import { post } from '../../common/http.js'
export default {
data(){
return { phone: '', code: '', countdown: 0, timer: null, sending: false, logging: false, showDebug: true }
return { email: '', code: '', countdown: 0, timer: null, sending: false, logging: false, showDebug: true }
},
computed:{
btnText(){ return this.countdown>0 ? `${this.countdown}s` : (this.sending ? '发送中...' : '获取验证码') }
,
trimmedPhone(){ return String(this.phone||'').trim() },
sendBodyJson(){ return JSON.stringify({ phone: this.trimmedPhone, scene: 'login' }, null, 2) },
loginBodyJson(){ return JSON.stringify({ phone: this.trimmedPhone, code: String(this.code||'').trim() }, null, 2) }
trimmedEmail(){ return String(this.email||'').trim() },
sendBodyJson(){ return JSON.stringify({ email: this.trimmedEmail, scene: 'login' }, null, 2) },
loginBodyJson(){ return JSON.stringify({ email: this.trimmedEmail, code: String(this.code||'').trim() }, null, 2) }
},
onUnload(){ if (this.timer) clearInterval(this.timer) },
methods:{
validatePhone(p){ return /^1\d{10}$/.test(String(p||'').trim()) },
validateEmail(e){ return /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/.test(String(e||'').trim()) },
startCountdown(sec){
this.countdown = sec
if (this.timer) clearInterval(this.timer)
@@ -59,11 +58,11 @@ export default {
},
async sendCode(){
if (this.sending || this.countdown>0) return
const p = String(this.phone||'').trim()
if (!this.validatePhone(p)) return uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
const e = String(this.email||'').trim()
if (!this.validateEmail(e)) return uni.showToast({ title: '请输入正确的邮箱地址', icon: 'none' })
this.sending = true
try {
const res = await post('/api/auth/sms/send', { phone: p, scene: 'login' })
const res = await post('/api/auth/email/send', { email: e, scene: 'login' })
const cd = Number(res && res.cooldownSec || 60)
this.startCountdown(cd)
uni.showToast({ title: '验证码已发送', icon: 'none' })
@@ -74,16 +73,16 @@ export default {
},
async doLogin(){
if (this.logging) return
const p = String(this.phone||'').trim()
const e = String(this.email||'').trim()
const c = String(this.code||'').trim()
if (!this.validatePhone(p)) return uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
if (!this.validateEmail(e)) return uni.showToast({ title: '请输入正确的邮箱地址', icon: 'none' })
if (!/^\d{6}$/.test(c)) return uni.showToast({ title: '验证码格式不正确', icon: 'none' })
this.logging = true
try {
const data = await post('/api/auth/sms/login', { phone: p, code: c })
const data = await post('/api/auth/email/login', { email: e, code: c })
if (data && data.token) {
uni.setStorageSync('TOKEN', data.token)
if (data.user && data.user.phone) uni.setStorageSync('USER_MOBILE', data.user.phone)
if (data.user && data.user.email) uni.setStorageSync('USER_EMAIL', data.user.email)
uni.showToast({ title: '登录成功', icon: 'none' })
setTimeout(() => { uni.reLaunch({ url: '/pages/index/index' }) }, 300)
}
@@ -93,25 +92,6 @@ export default {
} finally { this.logging=false }
}
,
async quickRegister(){
if (this.logging) return
const p = String(this.phone||'').trim()
if (!this.validatePhone(p)) return uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
this.logging = true
try {
const data = await post('/api/auth/register', { phone: p })
if (data && data.token) {
uni.setStorageSync('TOKEN', data.token)
if (data.user && data.user.phone) uni.setStorageSync('USER_MOBILE', data.user.phone)
uni.showToast({ title: '注册成功', icon: 'none' })
setTimeout(() => { uni.reLaunch({ url: '/pages/index/index' }) }, 300)
}
} catch(e) {
const msg = (e && e.message) || '注册失败'
uni.showToast({ title: msg, icon: 'none' })
} finally { this.logging=false }
}
,
copy(text){
try { uni.setClipboardData({ data: String(text||'') }); uni.showToast({ title: '已复制', icon: 'none' }) } catch(e) {}
}

View File

@@ -1,94 +1,116 @@
<template>
<view class="vip-page" style="background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); min-height: 100vh;">
<!-- 主要内容区域 -->
<view class="main-content">
<!-- VIP状态头部 -->
<view class="vip-header">
<view class="vip-crown">
<text class="crown-icon">👑</text>
</view>
<text class="vip-title">{{ isVip ? 'VIP会员' : '成为VIP会员' }}</text>
<text class="vip-subtitle">{{ isVip ? '尊享专属特权' : '解锁更多权益' }}</text>
<view class="vip-status" :class="{ active: isVip }">
<text class="status-text">{{ isVip ? 'VIP会员' : '普通用户' }}</text>
</view>
<view class="vip-page">
<view class="vip-hero">
<image class="hero-icon" src="/static/icons/icons8-vip-48 (1).png" mode="aspectFit" />
<view class="hero-text">
<text class="hero-title">{{ isVip ? 'VIP会员' : '升级 VIP 会员' }}</text>
<text class="hero-subtitle">{{ isVip ? '尊享完整数据与高效体验' : '开通后可查看全部历史数据并解锁高级功能' }}</text>
</view>
<!-- 会员功能 -->
<view class="features-section">
<text class="section-title">会员功能</text>
<view class="feature-card">
<view class="feature-icon">💾</view>
<text class="feature-text">永久存储数据</text>
</view>
</view>
<!-- VIP状态信息 -->
<view v-if="isVip" class="vip-info">
<view class="info-card">
<text class="info-label">会员状态</text>
<text class="info-value active">已激活</text>
</view>
<view class="info-card">
<text class="info-label">有效期至</text>
<text class="info-value">{{ expireDisplay }}</text>
</view>
</view>
<!-- 价格和购买 -->
<view v-if="!isVip" class="purchase-section">
<view class="price-card">
<text class="price-label">会员价格</text>
<view class="price-display">
<text class="price-symbol">¥</text>
<text class="price-amount">{{ price }}</text>
<text class="price-period">/</text>
</view>
</view>
<button class="purchase-btn" @click="onPay">
<text class="btn-text">立即开通VIP</text>
</button>
<view class="status-pill" :class="{ active: isVip }">
<text>{{ isVip ? '已开通' : '普通用户' }}</text>
</view>
</view>
<!-- 背景装饰 -->
<view class="bg-decoration">
<view class="decoration-circle circle-1"></view>
<view class="decoration-circle circle-2"></view>
<view class="decoration-circle circle-3"></view>
<view class="vip-summary" v-if="isVip">
<view class="summary-item">
<text class="summary-label">会员状态</text>
<text class="summary-value success">已激活</text>
</view>
<view class="summary-item">
<text class="summary-label">有效期至</text>
<text class="summary-value">{{ expireDisplay }}</text>
</view>
</view>
<view class="vip-summary" v-else>
<view class="summary-item">
<text class="summary-label">当前身份</text>
<text class="summary-value">普通用户</text>
</view>
<view class="summary-item">
<text class="summary-label">会员价格</text>
<text class="summary-value highlight">¥{{ priceDisplay }}/</text>
</view>
</view>
<view class="benefit-section">
<view class="section-header">
<text class="section-title">会员特权</text>
<text class="section-subtitle">聚焦数据留存与专业形象让经营更有底气</text>
</view>
<view class="benefit-grid">
<view v-for="item in benefits" :key="item.key" class="benefit-card">
<image v-if="item.icon" :src="item.icon" class="benefit-icon" mode="aspectFit" />
<text class="benefit-title">{{ item.title }}</text>
<text class="benefit-desc">{{ item.desc }}</text>
</view>
</view>
</view>
<view v-if="!isVip" class="purchase-card">
<view class="purchase-text">
<text class="purchase-title">立即升级 VIP</text>
<text class="purchase-desc">不限历史数据专属标识助您高效管账</text>
</view>
<button class="purchase-btn" @click="onPay">
<text>立即开通</text>
</button>
</view>
</view>
</template>
<script>
import { VIP_PRICE_PER_MONTH } from '../../common/config.js'
import { get, post } from '../../common/http.js'
export default {
data(){
return {
isVip: false,
expire: '',
price: VIP_PRICE_PER_MONTH
isVip: false,
expire: '',
price: 0,
benefits: []
}
},
onShow(){
this.loadVip()
this.composeBenefits()
},
computed: {
expireDisplay(){
const s = String(this.expire || '')
return s || '11年11月11日'
},
priceDisplay(){
const n = Number(this.price)
return Number.isFinite(n) && n > 0 ? n.toFixed(2) : '0.00'
}
},
methods: {
loadVip(){
try {
this.isVip = String(uni.getStorageSync('USER_VIP_IS_VIP') || 'false').toLowerCase() === 'true'
this.expire = uni.getStorageSync('USER_VIP_END') || ''
} catch(e) {}
composeBenefits(){
this.benefits = [
{ key: 'history', title: '完整历史留存', desc: '无限期保留交易、库存与客户数据', icon: '/static/icons/icons8-graph-report-50.png' },
{ key: 'analysis', title: '高级统计面板', desc: '秒级汇总销售毛利,掌握生意节奏', icon: '/static/icons/icons8-profit-50.png' },
{ key: 'priority', title: '优先客服支持', desc: '遇到问题优先处理,响应更迅速', icon: '/static/icons/icons8-account-male-100.png' }
]
},
onPay(){
uni.showToast({ title: '静态页面演示:支付功能未接入', icon: 'none' })
async loadVip(){
try {
const data = await get('/api/vip/status')
this.isVip = !!data?.isVip
this.expire = data?.expireAt || ''
if (typeof data?.price === 'number') this.price = data.price
} catch(e) {
// 保底不回退到硬编码价格仅展示0并提示可开通
this.isVip = false
}
},
async onPay(){
try {
await post('/api/vip/pay', {})
uni.showToast({ title: '已开通VIP', icon: 'success' })
await this.loadVip()
} catch(e) {
uni.showToast({ title: String(e.message || '开通失败'), icon: 'none' })
}
}
}
}
@@ -96,272 +118,238 @@ export default {
<style lang="scss">
page {
background: #1a1a2e !important;
background: linear-gradient(180deg, #f8fbff 0%, #ffffff 60%) !important;
}
.vip-page {
min-height: 100vh;
width: 100%;
position: relative;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%) !important;
overflow: hidden;
padding: 32rpx 24rpx 120rpx;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 28rpx;
}
.main-content {
.vip-hero {
display: flex;
align-items: center;
gap: 20rpx;
padding: 26rpx 28rpx;
border-radius: 24rpx;
background: rgba(255,255,255,0.98);
border: 2rpx solid #edf2f9;
box-shadow: 0 10rpx 30rpx rgba(76,141,255,0.12);
}
.hero-icon {
width: 88rpx;
height: 88rpx;
border-radius: 24rpx;
background: #f0f6ff;
padding: 12rpx;
}
.hero-text {
flex: 1;
padding: 60rpx 40rpx 40rpx;
position: relative;
z-index: 10;
display: flex;
flex-direction: column;
gap: 8rpx;
}
/* VIP头部区域 */
.vip-header {
text-align: center;
margin-bottom: 80rpx;
.vip-crown {
margin-bottom: 30rpx;
.crown-icon {
font-size: 80rpx;
filter: drop-shadow(0 4rpx 12rpx rgba(255, 215, 0, 0.3));
}
}
.vip-title {
display: block;
font-size: 48rpx;
font-weight: 700;
color: #fff;
margin-bottom: 16rpx;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.vip-subtitle {
display: block;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 40rpx;
}
.vip-status {
display: inline-block;
padding: 16rpx 32rpx;
border-radius: 50rpx;
background: rgba(0, 0, 0, 0.2);
backdrop-filter: blur(10rpx);
border: 1rpx solid rgba(255, 215, 0, 0.4);
&.active {
background: linear-gradient(45deg, #ffd700, #ffed4e);
border: 1rpx solid rgba(255, 215, 0, 0.3);
.status-text {
color: #333;
}
}
.status-text {
font-size: 26rpx;
font-weight: 600;
color: #fff;
}
}
.hero-title {
font-size: 36rpx;
font-weight: 800;
color: $uni-color-primary;
letter-spacing: 1rpx;
}
/* 会员功能区域 */
.features-section {
margin-bottom: 60rpx;
.section-title {
display: block;
font-size: 36rpx;
font-weight: 600;
color: #fff;
text-align: center;
margin-bottom: 40rpx;
}
.feature-card {
background: rgba(0, 0, 0, 0.15);
backdrop-filter: blur(15rpx);
border-radius: 24rpx;
padding: 40rpx;
text-align: center;
border: 1rpx solid rgba(255, 215, 0, 0.3);
transition: all 0.3s ease;
&:hover {
transform: translateY(-4rpx);
box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.15);
}
.feature-icon {
font-size: 60rpx;
margin-bottom: 24rpx;
filter: drop-shadow(0 4rpx 12rpx rgba(255, 215, 0, 0.3));
}
.feature-text {
display: block;
font-size: 32rpx;
font-weight: 600;
color: #fff;
letter-spacing: 1rpx;
}
}
.hero-subtitle {
font-size: 26rpx;
color: #5175b5;
line-height: 36rpx;
}
/* VIP信息卡片 */
.vip-info {
margin-bottom: 60rpx;
.info-card {
background: rgba(0, 0, 0, 0.15);
backdrop-filter: blur(15rpx);
border-radius: 20rpx;
padding: 32rpx;
margin-bottom: 20rpx;
border: 1rpx solid rgba(255, 215, 0, 0.3);
display: flex;
justify-content: space-between;
align-items: center;
.info-label {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
}
.info-value {
font-size: 30rpx;
font-weight: 600;
color: #fff;
&.active {
color: #ffd700;
}
}
}
.status-pill {
flex: 0 0 auto;
padding: 12rpx 20rpx;
border-radius: 999rpx;
background: #e6edfb;
color: #4463a6;
font-size: 24rpx;
font-weight: 700;
border: 2rpx solid rgba(76,141,255,0.2);
}
/* 购买区域 */
.purchase-section {
.price-card {
background: rgba(0, 0, 0, 0.15);
backdrop-filter: blur(15rpx);
border-radius: 24rpx;
padding: 40rpx;
text-align: center;
margin-bottom: 40rpx;
border: 1rpx solid rgba(255, 215, 0, 0.3);
.price-label {
display: block;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 16rpx;
}
.price-display {
display: flex;
align-items: baseline;
justify-content: center;
gap: 8rpx;
.price-symbol {
font-size: 32rpx;
color: #ffd700;
font-weight: 600;
}
.price-amount {
font-size: 60rpx;
font-weight: 700;
color: #ffd700;
}
.price-period {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
}
}
}
.purchase-btn {
width: 100%;
height: 96rpx;
background: linear-gradient(45deg, #ffd700, #ffed4e);
border-radius: 50rpx;
border: none;
box-shadow: 0 8rpx 24rpx rgba(255, 215, 0, 0.3);
transition: all 0.3s ease;
&:active {
transform: translateY(2rpx);
box-shadow: 0 4rpx 16rpx rgba(255, 215, 0, 0.4);
}
.btn-text {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
}
.status-pill.active {
background: #4c8dff;
color: #fff;
border-color: #4c8dff;
}
/* 背景装饰 */
.bg-decoration {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 1;
.decoration-circle {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.05);
&.circle-1 {
width: 300rpx;
height: 300rpx;
top: -150rpx;
right: -100rpx;
}
&.circle-2 {
width: 200rpx;
height: 200rpx;
bottom: 200rpx;
left: -100rpx;
}
&.circle-3 {
width: 150rpx;
height: 150rpx;
top: 50%;
right: 50rpx;
transform: translateY(-50%);
}
}
.vip-summary {
display: grid;
grid-template-columns: repeat(2, minmax(0,1fr));
gap: 16rpx;
background: rgba(255,255,255,0.98);
padding: 24rpx;
border-radius: 24rpx;
border: 2rpx solid #eef3fb;
box-shadow: 0 8rpx 24rpx rgba(99,132,191,0.10);
}
.summary-item {
background: #f6f9ff;
border-radius: 18rpx;
padding: 22rpx 24rpx;
display: flex;
flex-direction: column;
gap: 12rpx;
border: 2rpx solid rgba(76,141,255,0.12);
}
.summary-label {
font-size: 24rpx;
color: #5f7394;
}
.summary-value {
font-size: 30rpx;
font-weight: 700;
color: #1f2c3d;
}
.summary-value.success {
color: #1ead91;
}
.summary-value.highlight {
color: #2f58d1;
}
.benefit-section {
background: rgba(255,255,255,0.98);
border-radius: 24rpx;
padding: 28rpx;
border: 2rpx solid #edf2f9;
box-shadow: 0 12rpx 28rpx rgba(32,75,143,0.10);
display: flex;
flex-direction: column;
gap: 24rpx;
}
.section-header {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.section-title {
font-size: 34rpx;
font-weight: 800;
color: $uni-text-color;
}
.section-subtitle {
font-size: 24rpx;
color: #5f7394;
line-height: 34rpx;
}
.benefit-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 20rpx;
}
.benefit-card {
background: #f7faff;
border-radius: 20rpx;
padding: 24rpx 20rpx;
border: 2rpx solid rgba(76,141,255,0.12);
box-shadow: 0 8rpx 20rpx rgba(0,0,0,0.04);
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 14rpx;
}
.benefit-icon {
width: 48rpx;
height: 48rpx;
}
.benefit-title {
font-size: 28rpx;
font-weight: 700;
color: $uni-text-color;
}
.benefit-desc {
font-size: 24rpx;
line-height: 34rpx;
color: #5f7394;
}
.purchase-card {
margin-top: auto;
background: linear-gradient(135deg, rgba(76,141,255,0.14) 0%, rgba(76,141,255,0.06) 100%);
border-radius: 28rpx;
padding: 30rpx 28rpx;
display: flex;
align-items: center;
gap: 24rpx;
border: 2rpx solid rgba(76,141,255,0.18);
box-shadow: 0 10rpx 24rpx rgba(76,141,255,0.15);
}
.purchase-text {
flex: 1;
display: flex;
flex-direction: column;
gap: 10rpx;
}
.purchase-title {
font-size: 32rpx;
font-weight: 800;
color: $uni-color-primary;
}
.purchase-desc {
font-size: 24rpx;
color: #4463a6;
line-height: 34rpx;
}
.purchase-btn {
flex: 0 0 auto;
padding: 20rpx 36rpx;
border-radius: 999rpx;
border: none;
background: linear-gradient(135deg, #4788ff 0%, #2d6be6 100%);
color: #fff;
font-size: 28rpx;
font-weight: 700;
box-shadow: 0 10rpx 22rpx rgba(45,107,230,0.20);
}
.purchase-btn:active {
opacity: 0.88;
}
/* 响应式调整 */
@media (max-width: 375px) {
.benefits-grid {
.vip-summary {
grid-template-columns: 1fr;
}
.vip-header .vip-title {
font-size: 42rpx;
.benefit-grid {
grid-template-columns: 1fr;
}
.main-content {
padding: 40rpx 30rpx;
.purchase-card {
flex-direction: column;
align-items: stretch;
}
.status-pill {
display: none;
}
}
</style>

View File

@@ -151,7 +151,6 @@
<!-- 购物车空态 -->
<view class="empty" v-if="!items.length">
<image src="/static/icons/icons8-shopping-cart-100.png" mode="widthFix" class="empty-img"></image>
<text class="empty-text">购物车里空空如也</text>
<text class="empty-sub">扫描或点击 + 选择商品吧</text>
</view>
@@ -217,14 +216,14 @@
showMore: false,
SEG_ICONS: {
sale: {
out: '/static/icons/sale.png',
return: '/static/icons/other-pay.png',
collect: '/static/icons/report.png'
out: '/static/icons/icons8-shopping-cart-100.png',
return: '/static/icons/icons8-return-purchase-50.png',
collect: '/static/icons/icons8-profit-50.png'
},
purchase: {
in: '/static/icons/purchase.png',
return: '/static/icons/other-pay.png',
pay: '/static/icons/account.png'
in: '/static/icons/icons8-purchase-order-100.png',
return: '/static/icons/icons8-return-purchase-50.png',
pay: '/static/icons/icons8-dollar-ethereum-exchange-50.png'
}
}
}
@@ -281,11 +280,26 @@
}
},
methods: {
async fetchCategories() {
fixMojibake(s) {
try {
if (!s) return s
const bad = /[ÂÃæåé¼½¢]/.test(s)
if (!bad) return s
return decodeURIComponent(escape(s))
} catch(_) { return s }
},
normalizeCats(list) {
if (!Array.isArray(list)) return []
return list.map(it => ({
key: it && it.key || '',
label: this.fixMojibake((it && it.label) || '')
})).filter(it => it.key && it.label)
},
async fetchCategories() {
try {
const res = await get('/api/finance/categories')
if (res && Array.isArray(res.incomeCategories)) this._incomeCategories = res.incomeCategories
if (res && Array.isArray(res.expenseCategories)) this._expenseCategories = res.expenseCategories
if (res && Array.isArray(res.incomeCategories)) this._incomeCategories = this.normalizeCats(res.incomeCategories)
if (res && Array.isArray(res.expenseCategories)) this._expenseCategories = this.normalizeCats(res.expenseCategories)
this.ensureActiveCategory()
} catch (_) { this.ensureActiveCategory() }
},
@@ -397,47 +411,51 @@
.tabs text { color: $uni-text-color-grey; }
.tabs text.active { color: $uni-text-color; font-weight: 700; }
/* 三段式胶囊切换 */
.seg3 { display:flex; gap: 0; margin: 12rpx 16rpx; padding: 6rpx; background:#fff; border:2rpx solid #e6ebf2; border-radius: 999rpx; box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.04); }
.seg3-item { flex:1; display:flex; align-items:center; justify-content:center; gap: 8rpx; padding: 12rpx 0; color:$uni-text-color; border-radius: 999rpx; }
.seg3 { display:flex; gap: 0; margin: 12rpx 16rpx; padding: 6rpx; background:#fff; border-radius: 999rpx; box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.04); }
.seg3-item { flex:1; display:flex; align-items:center; justify-content:center; gap: 8rpx; padding: 12rpx 0; color:$uni-text-color; border-radius: 999rpx; transition: box-shadow .2s ease, background .2s ease; }
/* 间隔通过内边距处理,避免空选择器 */
.seg3-item.active { background:#fff; color:#4C8DFF; box-shadow: 0 4rpx 12rpx rgba(76,141,255,0.20), 0 0 0 2rpx #4C8DFF inset; }
.seg3-item.active { background:#fff; color:#4C8DFF; box-shadow: 0 3rpx 10rpx rgba(76,141,255,0.16); }
.seg3-icon { width: 28rpx; height: 28rpx; opacity: .9; }
.field { display:flex; justify-content: space-between; padding: 22rpx 24rpx; background: $uni-bg-color-grey; border-bottom: 1rpx solid $uni-border-color; }
.label { color:$uni-text-color-grey; }
.value { color:$uni-text-color; }
.field { display:flex; align-items:center; justify-content: flex-start; padding: 22rpx 24rpx; background: #f8faff; gap: 16rpx; }
.label { width: 160rpx; color:$uni-text-color-grey; }
.value { flex:1; color:$uni-text-color; text-align:right; }
/* 汇总卡片:白底卡片+主色按钮 */
.summary { display:flex; justify-content: space-between; align-items:center; padding: 16rpx 18rpx; margin: 12rpx 16rpx; background:#fff; border:2rpx solid $uni-border-color; border-radius: 16rpx; color:$uni-text-color; }
.summary { display:flex; justify-content: space-between; align-items:center; padding: 18rpx 20rpx; margin: 16rpx 18rpx 10rpx; background: none; border-radius: 18rpx; color:$uni-text-color; }
/* 加号改为图标按钮 */
.add { margin: 18rpx auto; width: 120rpx; height: 120rpx; border-radius: 24rpx; background: #fff; border:2rpx solid $uni-color-primary; color:$uni-color-primary; font-size: 72rpx; display:flex; align-items:center; justify-content:center; box-shadow: 0 6rpx 16rpx rgba(76,141,255,0.12); }
.add { margin: 24rpx auto 18rpx; width: 120rpx; height: 120rpx; border-radius: 28rpx; background: none; border:0; color:$uni-color-primary; font-size: 72rpx; display:flex; align-items:center; justify-content:center; box-shadow: none; }
.empty { display:flex; flex-direction: column; align-items:center; padding: 60rpx 0; color:$uni-text-color-grey; }
.empty-img { width: 160rpx; margin-bottom: 16rpx; }
.empty-text { margin-bottom: 8rpx; }
.list { background:#fff; margin: 0 16rpx 12rpx; border:2rpx solid $uni-border-color; border-radius: 16rpx; overflow: hidden; }
.row { display:grid; grid-template-columns: 1.5fr 1fr 1fr 1fr; gap: 12rpx; padding: 16rpx 12rpx; align-items:center; border-bottom: 1rpx solid $uni-border-color; }
.list { background:#fff; margin: 0 18rpx 20rpx; border-radius: 18rpx; overflow: hidden; box-shadow: 0 8rpx 24rpx rgba(0,0,0,0.06); }
.row { display:grid; grid-template-columns: 1.5fr 1fr 1fr 1fr; gap: 12rpx; padding: 18rpx 16rpx; align-items:center; }
.col.name { padding-left: 12rpx; }
.col.amount { text-align:right; padding-right: 12rpx; color:$uni-text-color; }
.bottom { position: fixed; left:0; right:0; bottom:0; background:$uni-bg-color-grey; padding: 16rpx 24rpx calc(env(safe-area-inset-bottom) + 16rpx); box-shadow: 0 -4rpx 12rpx rgba(0,0,0,0.16); }
.primary { width: 100%; background: $uni-color-primary; color:#fff; border-radius: 999rpx; padding: 20rpx 0; font-weight:800; }
.bottom { position: fixed; left:0; right:0; bottom:0; background:$uni-bg-color-grey; padding: 6rpx 18rpx calc(env(safe-area-inset-bottom) + 2rpx); box-shadow: 0 -4rpx 12rpx rgba(0,0,0,0.16); }
.order .bottom button { margin: 0; }
/* 仅限开单页底部按钮样式(缩小高度) */
.order .bottom .primary { width: 100%; background: $uni-color-primary; color:#fff; border-radius: 999rpx; padding: 14rpx 0; font-weight:700; font-size: 28rpx; }
.order .bottom .ghost { background: transparent; color: $uni-color-primary; border: 0; border-radius: 999rpx; padding: 12rpx 0; font-size: 28rpx; }
/* 收款/付款页样式 */
.pay-row .pay-input { text-align: right; color:$uni-text-color; }
.textarea { position: relative; padding: 16rpx 24rpx; background:$uni-bg-color-grey; border-top: 1rpx solid $uni-border-color; }
.amount-badge { position: absolute; right: 24rpx; top: -36rpx; background: $uni-color-primary; color:#fff; padding: 8rpx 16rpx; border-radius: 12rpx; font-size: 24rpx; }
.textarea { position: relative; padding: 16rpx 24rpx; background:#f8faff; }
.amount-badge { position: absolute; right: 24rpx; top: -32rpx; background: $uni-color-primary; color:#fff; padding: 10rpx 20rpx; border-radius: 14rpx; font-size: 24rpx; }
.date-mini { position: absolute; right: 24rpx; bottom: 20rpx; color:$uni-text-color-grey; font-size: 24rpx; }
/* 分类chips样式选中后文字变红 */
.chips { display:flex; flex-wrap: wrap; gap: 12rpx; padding: 12rpx 24rpx; }
.chip { padding: 10rpx 20rpx; border-radius: 999rpx; background: $uni-bg-color-hover; color:$uni-text-color-grey; }
.chip.active { background: $uni-color-primary; color:#fff; }
.chips { display:grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12rpx 16rpx; padding: 12rpx 24rpx; }
.chip { padding: 12rpx 20rpx; border-radius: 999rpx; background: $uni-bg-color-hover; color:$uni-text-color-grey; text-align:center; }
.chip.active { background: rgba(76,141,255,0.15); color:$uni-color-primary; }
/* 顶部业务 Tabs 显示 */
/* 快捷操作宫格 */
/* 信息卡片式表达(更稳重) */
.info-card { display:grid; grid-template-columns: 1fr 1fr auto; gap: 10rpx; margin: 10rpx 12rpx 0; background:transparent; padding: 0; align-items:center; }
.info-field { background:#fff; border:2rpx solid #e6ebf2; border-radius: 12rpx; padding: 10rpx 12rpx; box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.04); }
.info-field { background:#fff; border:0; border-radius: 14rpx; padding: 12rpx 14rpx; box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.06); }
.info-label { color:$uni-text-color-grey; font-size: 24rpx; margin-right: 8rpx; }
.info-value { color:$uni-text-color; font-weight: 700; }
.info-action { display:flex; align-items:center; gap: 6rpx; background:$uni-color-primary; color:#fff; border-radius: 12rpx; padding: 14rpx 16rpx; box-shadow: 0 8rpx 18rpx rgba(76,141,255,0.26); }
.info-icon { width: 32rpx; height: 32rpx; }
/* 缩小“加商品”按钮尺寸,仅在本页卡片内 */
.order .info-card .info-action { display:flex; align-items:center; gap: 6rpx; background:$uni-color-primary; color:#fff; border-radius: 12rpx; padding: 8rpx 12rpx; box-shadow: 0 5rpx 12rpx rgba(76,141,255,0.18); font-size: 26rpx; }
.order .info-card .info-icon { width: 24rpx; height: 24rpx; }
</style>

View File

@@ -5,20 +5,22 @@
<text class="title">编辑货品</text>
<text class="sub">完善基础信息与价格</text>
</view>
<view class="card">
<view v-if="form.platformStatus==='platform'" class="tip platform">平台推荐货品建议谨慎修改核心字段</view>
<view v-else-if="form.sourceSubmissionId" class="tip custom">此货品源于我的提交审核通过后已入库</view>
<view class="section">
<view class="row">
<text class="label">商品名称</text>
<input v-model.trim="form.name" placeholder="必填" />
</view>
<view class="row">
<text class="label">条形码</text>
<input v-model.trim="form.barcode" placeholder="可扫码或输入" />
<!-- #ifdef APP-PLUS -->
<button size="mini" @click="scan"></button>
<view class="row">
<text class="label">条形码</text>
<input class="input-long" v-model.trim="form.barcode" placeholder="可扫码或输入" />
<!-- #ifdef MP-WEIXIN -->
<button size="mini" class="picker-btn" @click="chooseAndScanBarcode">图片识</button>
<!-- #endif -->
<!-- #ifdef APP-PLUS -->
<button size="mini" class="picker-btn" @click="chooseAndScanBarcode">图片识码</button>
<!-- #endif -->
</view>
<view class="row">
<text class="label">品牌/型号/规格/产地</text>
</view>
<view class="row">
<input v-model.trim="form.brand" placeholder="品牌" />
@@ -42,7 +44,7 @@
</view>
</view>
<view class="card">
<view class="section">
<view class="row">
<text class="label">库存与安全库存</text>
</view>
@@ -53,7 +55,7 @@
</view>
</view>
<view class="card">
<view class="section">
<view class="row">
<text class="label">价格进价/零售/批发/大单</text>
</view>
@@ -65,17 +67,17 @@
</view>
</view>
<view class="card">
<view class="section">
<text class="label">图片</text>
<ImageUploader v-model="form.images" :formData="{ ownerType: 'product' }" />
</view>
<view class="card">
<view class="section">
<text class="label">备注</text>
<textarea v-model.trim="form.remark" placeholder="可选" auto-height />
</view>
<view class="fixed">
<view class="fixed" :style="{ bottom: (keyboardHeight || 0) + 'px' }">
<button class="ghost" @click="save(false)">保存</button>
<button class="primary" @click="save(true)">保存并继续</button>
</view>
@@ -84,7 +86,7 @@
<script>
import ImageUploader from '../../components/ImageUploader.vue'
import { get, post, put } from '../../common/http.js'
import { get, post, put, upload } from '../../common/http.js'
export default {
components: { ImageUploader },
@@ -96,15 +98,21 @@ export default {
categoryId: '', unitId: '',
stock: null, safeMin: null, safeMax: null,
purchasePrice: null, retailPrice: null, wholesalePrice: null, bigClientPrice: null,
images: [], remark: ''
images: [], remark: '',
platformStatus: '', sourceSubmissionId: ''
},
units: [],
categories: []
categories: [],
keyboardHeight: 0
}
},
onLoad(query) {
this.id = query?.id || ''
this.bootstrap()
this.initKeyboardListener()
},
onUnload() {
this.disposeKeyboardListener()
},
computed: {
unitNames() { return this.units.map(u => u.name) },
@@ -123,6 +131,22 @@ export default {
await Promise.all([this.fetchUnits(), this.fetchCategories()])
if (this.id) this.loadDetail()
},
initKeyboardListener() {
try {
this.__keyboardListener = (e) => {
const h = (e && (e.height || e.targetHeight || 0)) || 0
this.keyboardHeight = h
}
uni.onKeyboardHeightChange && uni.onKeyboardHeightChange(this.__keyboardListener)
} catch (_) {}
},
disposeKeyboardListener() {
try {
if (this.__keyboardListener && uni.offKeyboardHeightChange) {
uni.offKeyboardHeightChange(this.__keyboardListener)
}
} catch (_) {}
},
async fetchUnits() {
try {
const res = await get('/api/product-units')
@@ -143,11 +167,27 @@ export default {
const idx = Number(e.detail.value); const c = this.categories[idx]
this.form.categoryId = c ? c.id : ''
},
scan() {
uni.scanCode({ onlyFromCamera: false, success: (res) => {
this.form.barcode = res.result
}})
},
async chooseAndScanBarcode() {
try {
const chooseRes = await uni.chooseImage({ count: 1, sourceType: ['camera','album'], sizeType: ['compressed'] })
let filePath = chooseRes.tempFilePaths[0]
try {
const comp = await uni.compressImage({ src: filePath, quality: 80 })
filePath = comp.tempFilePath || filePath
} catch (e) {}
const data = await upload('/api/barcode/scan', filePath, {}, 'file')
if (data && data.success && data.barcode) {
this.form.barcode = data.barcode
uni.showToast({ title: '识别成功', icon: 'success', mask: false })
return
}
const msg = (data && (data.message || data.error || data.msg)) || '未识别'
uni.showToast({ title: msg, icon: 'none', mask: false })
} catch (e) {
const msg = (e && e.message) ? String(e.message) : '网络异常或服务不可用'
uni.showToast({ title: msg, icon: 'none', mask: false })
}
},
async loadDetail() {
try {
const data = await get('/api/products/' + this.id)
@@ -159,7 +199,10 @@ export default {
safeMin: data.safeMin, safeMax: data.safeMax,
purchasePrice: data.purchasePrice, retailPrice: data.retailPrice,
wholesalePrice: data.wholesalePrice, bigClientPrice: data.bigClientPrice,
images: (data.images || []).map(i => i.url || i)
images: (data.images || []).map(i => i.url || i),
remark: data.remark || '',
platformStatus: data.platformStatus || '',
sourceSubmissionId: data.sourceSubmissionId || ''
})
} catch (_) {}
},
@@ -185,19 +228,20 @@ export default {
}
},
async save(goOn) {
try { uni.hideKeyboard && uni.hideKeyboard() } catch (_) {}
if (!this.validate()) return
const payload = this.buildPayload()
try {
if (this.id) await put('/api/products/' + this.id, payload)
else await post('/api/products', payload)
uni.showToast({ title: '保存成功', icon: 'success' })
uni.showToast({ title: '保存成功', icon: 'success', mask: false })
if (goOn && !this.id) {
this.form = { name: '', barcode: '', brand: '', model: '', spec: '', origin: '', categoryId: '', unitId: '', stock: null, safeMin: null, safeMax: null, purchasePrice: null, retailPrice: null, wholesalePrice: null, bigClientPrice: null, images: [], remark: '' }
this.form = { name: '', barcode: '', brand: '', model: '', spec: '', origin: '', categoryId: '', unitId: '', stock: null, safeMin: null, safeMax: null, purchasePrice: null, retailPrice: null, wholesalePrice: null, bigClientPrice: null, images: [], remark: '', platformStatus: '', sourceSubmissionId: '' }
} else {
setTimeout(() => uni.navigateBack(), 400)
}
} catch (e) {
uni.showToast({ title: '保存失败', icon: 'none' })
uni.showToast({ title: '保存失败', icon: 'none', mask: false })
}
}
}
@@ -205,19 +249,28 @@ export default {
</script>
<style lang="scss">
.page { background:$uni-bg-color; height: 100vh; }
.hero.small { margin: 16rpx; padding: 16rpx; background: #ffffff; border: 2rpx solid $uni-border-color; border-radius: 16rpx; }
.hero.small .title { font-size: 32rpx; font-weight: 800; color: $uni-text-color; }
.hero.small .sub { margin-left: 12rpx; color: $uni-text-color-grey; font-size: 24rpx; }
.card { background:#ffffff; margin: 16rpx; padding: 16rpx; border-radius: 16rpx; border: 2rpx solid $uni-border-color; }
.row { display:flex; gap: 12rpx; align-items: center; margin-bottom: 12rpx; }
.label { width: 180rpx; color:$uni-text-color-grey; }
.row input { flex:1; background:$uni-bg-color-hover; border-radius: 12rpx; padding: 14rpx; color:$uni-text-color; border: 2rpx solid $uni-border-color; }
.picker { padding: 10rpx 14rpx; background:$uni-bg-color-hover; border-radius: 12rpx; color:$uni-text-color-grey; margin-left: 8rpx; border: 2rpx solid $uni-border-color; }
.page { background:$uni-bg-color; min-height: 100vh; padding-bottom: 160rpx; box-sizing: border-box; }
.hero.small { margin: 22rpx 24rpx 12rpx; padding: 0 4rpx 18rpx; color: $uni-text-color; border-bottom: 2rpx solid rgba(94,124,174,0.12); }
.hero.small .title { font-size: 34rpx; font-weight: 800; }
.hero.small .sub { display: block; margin-top: 6rpx; color: $uni-text-color-grey; font-size: 24rpx; }
.section { margin: 0 24rpx 28rpx; padding-bottom: 6rpx; border-bottom: 2rpx solid rgba(94,124,174,0.10); }
.section:last-of-type { border-bottom: 0; margin-bottom: 0; }
.section .row:first-child .label { font-weight: 700; color: $uni-text-color; }
.row { display:flex; gap: 8rpx; align-items: center; margin-top: 18rpx; }
.row .input-long { flex: 1.2; }
.row:first-child { margin-top: 0; }
.label { width: 150rpx; color:$uni-text-color-grey; font-size: 26rpx; }
.row input { flex:1; background:#f7f9fc; border-radius: 14rpx; padding: 18rpx 20rpx; color:$uni-text-color; border: 0; box-shadow: inset 0 0 0 2rpx rgba(134,155,191,0.06); }
.picker-btn { background:#ffffff; border: 2rpx solid rgba($uni-color-primary, .45); color:$uni-color-primary; padding: 0 24rpx; border-radius: 999rpx; font-size: 24rpx; }
.picker { padding: 16rpx 22rpx; background:#f7f9fc; border-radius: 14rpx; color:$uni-text-color-grey; margin-left: 8rpx; border: 0; box-shadow: inset 0 0 0 2rpx rgba(134,155,191,0.06); }
.prices input { width: 30%; }
.fixed { position: fixed; left: 0; right: 0; bottom: 0; background:#ffffff; padding: 12rpx 16rpx; display:flex; gap: 16rpx; border-top: 2rpx solid $uni-border-color; }
.section textarea { width: 100%; min-height: 160rpx; background:#f7f9fc; border-radius: 14rpx; padding: 20rpx 22rpx; box-sizing: border-box; color:$uni-text-color; border: 0; box-shadow: inset 0 0 0 2rpx rgba(134,155,191,0.06); }
.fixed { position: fixed; left: 0; right: 0; bottom: env(safe-area-inset-bottom); background:#ffffff; padding: 16rpx 16rpx calc(16rpx + constant(safe-area-inset-bottom)) 16rpx; display:flex; gap: 16rpx; box-shadow: 0 -6rpx 18rpx rgba(24,55,105,0.08); z-index: 999; }
.fixed .primary { flex:1; background: $uni-color-primary; color:#fff; border-radius: 999rpx; padding: 18rpx 0; font-weight: 700; }
.fixed .ghost { flex:1; background:#ffffff; color:$uni-color-primary; border: 2rpx solid rgba($uni-color-primary, .45); border-radius: 999rpx; padding: 18rpx 0; }
.tip { margin: 0 30rpx 20rpx; padding: 16rpx 20rpx; border-radius: 16rpx; font-size: 24rpx; }
.tip.platform { background: rgba(45,140,240,0.12); color: #2d8cf0; }
.tip.custom { background: rgba(103,194,58,0.12); color: #67c23a; }
</style>

View File

@@ -3,6 +3,7 @@
<view class="tabs">
<view class="tab" :class="{active: tab==='all'}" @click="switchTab('all')">全部</view>
<view class="tab" :class="{active: tab==='category'}" @click="switchTab('category')">按类别</view>
<view class="tab extra" @click="goMySubmissions">我的提交</view>
</view>
<view class="search">
@@ -18,7 +19,11 @@
<view class="item" v-for="it in items" :key="it.id" @click="openForm(it.id)">
<image v-if="it.cover" :src="it.cover" class="thumb" mode="aspectFill" />
<view class="content">
<view class="name">{{ it.name }}</view>
<view class="name">
<text>{{ it.name }}</text>
<text v-if="it.platformStatus==='platform'" class="tag-platform">平台推荐</text>
<text v-else-if="it.sourceSubmissionId" class="tag-custom">我的提交</text>
</view>
<view class="meta">{{ it.brand || '-' }} {{ it.model || '' }} {{ it.spec || '' }}</view>
<view class="meta">库存{{ it.stock ?? 0 }}
<text class="price">零售价¥{{ (it.retailPrice ?? it.price ?? 0).toFixed(2) }}</text>
@@ -31,7 +36,7 @@
</view>
</scroll-view>
<view class="fab" @click="openForm()"></view>
<!-- 保留我的提交页的此处不显示 -->
</view>
</template>
@@ -117,6 +122,9 @@ export default {
openForm(id) {
const url = '/pages/product/form' + (id ? ('?id=' + id) : '')
uni.navigateTo({ url })
},
goMySubmissions() {
uni.navigateTo({ url: '/pages/product/submissions' })
}
}
}
@@ -127,6 +135,7 @@ export default {
.tabs { display:flex; background:$uni-bg-color-grey; }
.tab { flex:1; text-align:center; padding: 20rpx 0; color:$uni-text-color-grey; }
.tab.active { color:$uni-color-primary; font-weight: 600; }
.tab.extra { flex: 0 0 180rpx; color:$uni-color-primary; font-weight: 600; }
.search { display:flex; gap: 12rpx; padding: 16rpx; background:$uni-bg-color-grey; align-items: center; }
.search input { flex:1; background:$uni-bg-color-hover; border-radius: 12rpx; padding: 12rpx; color: $uni-text-color; }
.picker { padding: 8rpx 12rpx; background:$uni-bg-color-hover; border-radius: 10rpx; color:$uni-text-color-grey; }
@@ -134,7 +143,9 @@ export default {
.item { display:flex; padding: 20rpx; background:$uni-bg-color-grey; border-bottom: 1rpx solid $uni-border-color; }
.thumb { width: 120rpx; height: 120rpx; border-radius: 12rpx; margin-right: 16rpx; background:$uni-bg-color-hover; }
.content { flex:1; }
.name { color:$uni-text-color; margin-bottom: 6rpx; font-weight: 600; }
.name { color:$uni-text-color; margin-bottom: 6rpx; font-weight: 600; display:flex; align-items:center; gap: 12rpx; }
.tag-platform { font-size: 22rpx; color:#fff; background:#2d8cf0; padding: 4rpx 10rpx; border-radius: 8rpx; }
.tag-custom { font-size: 22rpx; color:#fff; background:#67c23a; padding: 4rpx 10rpx; border-radius: 8rpx; }
.meta { color:$uni-text-color-grey; font-size: 24rpx; }
.price { margin-left: 20rpx; color:$uni-color-primary; }
.empty { height: 60vh; display:flex; align-items:center; justify-content:center; color:$uni-text-color-grey; }

View File

@@ -0,0 +1,190 @@
<template>
<scroll-view scroll-y class="page" v-if="detail">
<view class="header">
<text class="model">{{ detail.model }}</text>
<text :class="['status', statusClass(detail.status)]">{{ statusLabel(detail.status) }}</text>
</view>
<view class="section">
<view class="row"><text class="label">名称</text><text class="value">{{ detail.name || '-' }}</text></view>
<view class="row"><text class="label">品牌</text><text class="value">{{ detail.brand || '-' }}</text></view>
<view class="row"><text class="label">规格</text><text class="value">{{ detail.spec || '-' }}</text></view>
<view class="row"><text class="label">产地</text><text class="value">{{ detail.origin || '-' }}</text></view>
<view class="row"><text class="label">条码</text><text class="value">{{ detail.barcode || '-' }}</text></view>
<view class="row"><text class="label">单位</text><text class="value">{{ unitName }}</text></view>
<view class="row"><text class="label">类别</text><text class="value">{{ categoryName }}</text></view>
<view class="row"><text class="label">安全库存</text><text class="value">{{ stockRange }}</text></view>
</view>
<view class="section">
<view class="block-title">参数</view>
<view v-if="parameterPairs.length" class="params">
<view class="param" v-for="item in parameterPairs" :key="item.key">
<text class="param-key">{{ item.key }}</text>
<text class="param-val">{{ item.value }}</text>
</view>
</view>
<view v-else class="placeholder">未填写参数</view>
</view>
<view class="section">
<view class="block-title">图片</view>
<view v-if="detail.images && detail.images.length" class="images">
<image v-for="(img, idx) in detail.images" :key="idx" :src="img" class="image" mode="aspectFill" @click="preview(idx)" />
</view>
<view v-else class="placeholder">未上传图片</view>
</view>
<view class="section">
<view class="block-title">备注</view>
<view class="placeholder">{{ detail.remark || '无' }}</view>
</view>
<view class="section">
<view class="row"><text class="label">提交时间</text><text class="value">{{ formatTime(detail.createdAt) }}</text></view>
<view class="row"><text class="label">审核时间</text><text class="value">{{ formatTime(detail.reviewedAt) }}</text></view>
<view class="row" v-if="detail.reviewRemark"><text class="label">审核说明</text><text class="value">{{ detail.reviewRemark }}</text></view>
</view>
<view class="footer">
<button size="mini" @click="back">返回</button>
<button size="mini" type="warn" v-if="detail.status === 'rejected'" @click="resubmit">重新提交</button>
</view>
</scroll-view>
<view v-else class="loading">加载中...</view>
</template>
<script>
import { get } from '../../common/http.js'
export default {
data() {
return {
id: '',
detail: null,
unitName: '-',
categoryName: '-'
}
},
async onLoad(query) {
this.id = query?.id || ''
if (!this.id) {
uni.showToast({ title: '参数缺失', icon: 'none' })
return
}
await this.loadDetail()
},
methods: {
async loadDetail() {
try {
const data = await get(`/api/products/submissions/${this.id}`)
this.detail = data
this.unitName = this.unitLookup(data.unitId)
this.categoryName = this.categoryLookup(data.categoryId)
} catch (e) {
const msg = e?.message || '加载失败'
uni.showToast({ title: msg, icon: 'none' })
}
},
statusLabel(s) {
if (s === 'approved') return '已通过'
if (s === 'rejected') return '已驳回'
return '待审核'
},
statusClass(s) {
if (s === 'approved') return 'approved'
if (s === 'rejected') return 'rejected'
return 'pending'
},
parameterPairs() {
const params = this.detail?.parameters
if (!params || typeof params !== 'object') return []
return Object.keys(params).map(k => ({ key: k, value: params[k] }))
},
preview(idx) {
if (!this.detail?.images || !this.detail.images.length) return
uni.previewImage({ urls: this.detail.images, current: idx })
},
formatTime(value) {
if (!value) return '-'
try {
const d = new Date(value)
if (!Number.isFinite(d.getTime())) return value
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hh = String(d.getHours()).padStart(2, '0')
const mm = String(d.getMinutes()).padStart(2, '0')
return `${y}-${m}-${day} ${hh}:${mm}`
} catch (_) { return value }
},
unitLookup(id) {
try {
const list = uni.getStorageSync('CACHE_UNITS') || []
const found = list.find(x => String(x.id) === String(id))
return found ? found.name : '-'
} catch (_) { return '-' }
},
categoryLookup(id) {
try {
const list = uni.getStorageSync('CACHE_CATEGORIES') || []
const found = list.find(x => String(x.id) === String(id))
return found ? found.name : '-'
} catch (_) { return '-' }
},
back() {
uni.navigateBack({ delta: 1 })
},
resubmit() {
const payload = {
model: this.detail.model,
name: this.detail.name,
brand: this.detail.brand,
spec: this.detail.spec,
origin: this.detail.origin,
unitId: this.detail.unitId,
categoryId: this.detail.categoryId,
remark: this.detail.remark,
barcode: this.detail.barcode,
parameters: this.detail.parameters
}
const query = encodeURIComponent(JSON.stringify(payload))
uni.navigateTo({ url: `/pages/product/submit?prefill=${query}` })
}
},
computed: {
stockRange() {
const min = this.detail?.safeMin
const max = this.detail?.safeMax
if (min == null && max == null) return '-'
if (min != null && max != null) return `${min} ~ ${max}`
if (min != null) return `${min}`
return `${max}`
}
}
}
</script>
<style lang="scss">
.page { padding: 24rpx 24rpx 160rpx; background: #f6f7fb; }
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20rpx; }
.model { font-size: 36rpx; font-weight: 700; color: #2d3a4a; }
.status { font-size: 26rpx; padding: 6rpx 18rpx; border-radius: 999rpx; }
.status.pending { background: rgba(246, 190, 0, 0.15); color: #c47f00; }
.status.approved { background: rgba(103,194,58,0.15); color: #409eff; }
.status.rejected { background: rgba(255,87,115,0.18); color: #f56c6c; }
.section { background: #fff; border-radius: 16rpx; padding: 20rpx 22rpx; margin-bottom: 24rpx; box-shadow: 0 8rpx 24rpx rgba(0,0,0,0.04); }
.row { display: flex; justify-content: space-between; padding: 12rpx 0; border-bottom: 1rpx solid #f1f2f5; }
.row:last-child { border-bottom: none; }
.label { width: 160rpx; font-size: 26rpx; color: #7a8899; }
.value { flex: 1; text-align: right; font-size: 26rpx; color: #2d3a4a; word-break: break-all; }
.block-title { font-size: 28rpx; font-weight: 600; color: #2d3a4a; margin-bottom: 12rpx; }
.placeholder { font-size: 26rpx; color: #7a8899; }
.params { display: flex; flex-direction: column; gap: 12rpx; }
.param { display: flex; justify-content: space-between; font-size: 26rpx; color: #2d3a4a; }
.param-key { color: #7a8899; }
.images { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12rpx; }
.image { width: 100%; height: 200rpx; border-radius: 16rpx; background: #f0f2f5; }
.footer { display: flex; justify-content: flex-end; gap: 20rpx; }
.loading { height: 100vh; display: flex; align-items: center; justify-content: center; color: #7a8899; }
</style>

View File

@@ -0,0 +1,194 @@
<template>
<view class="page">
<view class="hero">
<text class="title">我的配件提交</text>
<text class="desc">查看待审核已通过已驳回的记录</text>
</view>
<view class="tabs">
<view class="tab" :class="{ active: status === '' }" @click="switchStatus('')">全部</view>
<view class="tab" :class="{ active: status === 'pending' }" @click="switchStatus('pending')">待审核</view>
<view class="tab" :class="{ active: status === 'approved' }" @click="switchStatus('approved')">已通过</view>
<view class="tab" :class="{ active: status === 'rejected' }" @click="switchStatus('rejected')">已驳回</view>
</view>
<scroll-view scroll-y class="list" @scrolltolower="loadMore">
<view v-if="items.length" class="cards">
<view class="card" v-for="item in items" :key="item.id">
<view class="card-header">
<text class="model">{{ item.model || '-' }}</text>
<text :class="['status', statusClass(item.status)]">{{ statusLabel(item.status) }}</text>
</view>
<view class="card-body">
<text class="name">{{ item.name || '未填写名称' }}</text>
<text class="brand">品牌{{ item.brand || '-' }}</text>
<text class="time">提交{{ formatTime(item.createdAt) }}</text>
<text class="time" v-if="item.reviewedAt">审核{{ formatTime(item.reviewedAt) }}</text>
</view>
<view class="card-footer">
<button size="mini" @click="viewDetail(item.id)">详情</button>
<button size="mini" type="primary" v-if="item.status === 'pending'" @click="notifyPending">等待审核</button>
<button size="mini" type="warn" v-else-if="item.status === 'rejected'" @click="resubmit(item)">重新提交</button>
</view>
</view>
</view>
<view v-else class="empty">
<text>暂无提交记录快去提交新的配件吧</text>
<button size="mini" class="primary" @click="goSubmit">立即提交</button>
</view>
<view v-if="loading" class="loading">加载中...</view>
<view v-else-if="finished && items.length" class="finished">没有更多了</view>
</scroll-view>
<view class="fab" @click="goSubmit"></view>
</view>
</template>
<script>
import { get } from '../../common/http.js'
export default {
data() {
return {
status: '',
items: [],
page: 1,
size: 20,
total: 0,
loading: false,
finished: false,
cacheUnitsLoaded: false,
cacheCategoriesLoaded: false
}
},
onShow() {
this.preloadDictionaries()
this.reload()
},
methods: {
async preloadDictionaries() {
try {
const [units, categories] = await Promise.all([
this.cacheUnitsLoaded ? Promise.resolve(null) : get('/api/product-units'),
this.cacheCategoriesLoaded ? Promise.resolve(null) : get('/api/product-categories')
])
if (units) {
const list = Array.isArray(units?.list) ? units.list : (Array.isArray(units) ? units : [])
uni.setStorageSync('CACHE_UNITS', list)
this.cacheUnitsLoaded = true
}
if (categories) {
const list = Array.isArray(categories?.list) ? categories.list : (Array.isArray(categories) ? categories : [])
uni.setStorageSync('CACHE_CATEGORIES', list)
this.cacheCategoriesLoaded = true
}
} catch (_) {
// 忽略缓存失败
}
},
switchStatus(s) {
if (this.status === s) return
this.status = s
this.reload()
},
async reload() {
this.page = 1
this.items = []
this.finished = false
await this.loadMore()
},
async loadMore() {
if (this.loading || this.finished) return
this.loading = true
try {
const params = { page: this.page, size: this.size }
if (this.status) params.status = this.status
const res = await get('/api/products/submissions', params)
const list = Array.isArray(res?.list) ? res.list : []
this.items = this.items.concat(list)
this.total = Number(res?.total || this.items.length)
if (list.length < this.size) this.finished = true
this.page += 1
} catch (e) {
console.warn('加载提交记录失败', e)
const msg = e?.message || '加载失败'
uni.showToast({ title: msg, icon: 'none' })
} finally {
this.loading = false
}
},
statusLabel(s) {
if (s === 'approved') return '已通过'
if (s === 'rejected') return '已驳回'
return '待审核'
},
statusClass(s) {
if (s === 'approved') return 'approved'
if (s === 'rejected') return 'rejected'
return 'pending'
},
formatTime(value) {
if (!value) return '-'
try {
const d = new Date(value)
if (!Number.isFinite(d.getTime())) return value
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hh = String(d.getHours()).padStart(2, '0')
const mm = String(d.getMinutes()).padStart(2, '0')
return `${y}-${m}-${day} ${hh}:${mm}`
} catch (_) { return value }
},
viewDetail(id) {
uni.navigateTo({ url: `/pages/product/submission-detail?id=${id}` })
},
notifyPending() {
uni.showToast({ title: '审核中,请耐心等待', icon: 'none' })
},
resubmit(item) {
const payload = {
model: item.model,
name: item.name,
brand: item.brand,
spec: item.spec,
origin: item.origin,
unitId: item.unitId,
categoryId: item.categoryId,
remark: item.remark
}
const query = encodeURIComponent(JSON.stringify(payload))
uni.navigateTo({ url: `/pages/product/submit?prefill=${query}` })
},
goSubmit() {
uni.navigateTo({ url: '/pages/product/submit' })
}
}
}
</script>
<style lang="scss">
.page { display: flex; flex-direction: column; height: 100vh; background: #f6f7fb; padding-bottom: 140rpx; }
.hero { padding: 24rpx; background: #fff; box-shadow: 0 10rpx 30rpx rgba(0,0,0,0.04); }
.title { font-size: 34rpx; font-weight: 700; color: #2d3a4a; }
.desc { font-size: 24rpx; color: #7a8899; margin-top: 8rpx; }
.tabs { display: flex; background: #fff; margin: 16rpx; border-radius: 999rpx; overflow: hidden; box-shadow: inset 0 0 0 1rpx rgba(76,141,255,0.1); }
.tab { flex: 1; text-align: center; padding: 20rpx 0; font-size: 28rpx; color: #7a8899; }
.tab.active { background: linear-gradient(135deg, #4c8dff, #6ab7ff); color: #fff; font-weight: 600; }
.list { flex: 1; padding: 0 20rpx; }
.cards { display: flex; flex-direction: column; gap: 20rpx; padding-bottom: 40rpx; }
.card { background: #fff; border-radius: 18rpx; padding: 22rpx; box-shadow: 0 10rpx 30rpx rgba(0,0,0,0.05); }
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12rpx; }
.model { font-size: 30rpx; font-weight: 700; color: #2d3a4a; }
.status { font-size: 24rpx; padding: 6rpx 18rpx; border-radius: 999rpx; }
.status.pending { background: rgba(246, 190, 0, 0.15); color: #c47f00; }
.status.approved { background: rgba(103,194,58,0.15); color: #409eff; }
.status.rejected { background: rgba(255,87,115,0.18); color: #f56c6c; }
.card-body { display: flex; flex-direction: column; gap: 6rpx; color: #4f5969; font-size: 26rpx; }
.name { font-weight: 600; color: #2d3a4a; }
.card-footer { display: flex; gap: 12rpx; margin-top: 16rpx; }
.empty { height: 60vh; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #8894a3; gap: 20rpx; }
.empty .primary { background: #4c8dff; color: #fff; border-radius: 999rpx; padding: 12rpx 30rpx; }
.loading, .finished { text-align: center; padding: 20rpx 0; color: #7a8899; }
.fab { position: fixed; right: 30rpx; bottom: 120rpx; width: 100rpx; height: 100rpx; background: linear-gradient(135deg, #4c8dff, #6ab7ff); color: #fff; border-radius: 50rpx; display: flex; align-items: center; justify-content: center; font-size: 48rpx; box-shadow: 0 20rpx 40rpx rgba(0,0,0,0.2); }
</style>

View File

@@ -0,0 +1,299 @@
<template>
<scroll-view scroll-y class="page">
<view class="hero">
<text class="title">提交配件</text>
<text class="desc">填写型号名称参数与图片提交后进入待审核状态</text>
</view>
<view class="section">
<view class="row required">
<text class="label">型号</text>
<input v-model.trim="form.model" placeholder="请输入型号(必填)" />
</view>
<view class="row">
<text class="label">品牌</text>
<input v-model.trim="form.brand" placeholder="品牌/厂商" />
</view>
<view class="row">
<text class="label">条码</text>
<input v-model.trim="form.barcode" placeholder="可选,建议扫码录入" />
<button size="mini" class="picker-btn" @click="scanBarcode">识码</button>
</view>
</view>
<view class="section">
<view class="row">
<text class="label">类别</text>
<picker mode="selector" :range="categoryNames" @change="onPickCategory">
<view class="picker">{{ categoryLabel }}</view>
</picker>
</view>
</view>
<view class="section">
<view class="row">
<text class="label">模板</text>
</view>
<view class="row">
<picker mode="selector" :range="templateNames" @change="onPickTemplate">
<view class="picker">{{ templateLabel }}</view>
</picker>
</view>
</view>
<view class="section">
<view class="row">
<text class="label">图片</text>
</view>
<ImageUploader v-model="form.images" :max="9" :formData="{ ownerType: 'submission' }" />
</view>
<view class="section">
<view class="row">
<text class="label">备注</text>
</view>
<textarea class="textarea" v-model.trim="form.remark" placeholder="选填:补充说明" />
</view>
<view class="section">
<view class="row">
<text class="label">安全库存</text>
</view>
<view class="row triple">
<input type="number" v-model.number="form.safeMin" placeholder="下限" />
<input type="number" v-model.number="form.safeMax" placeholder="上限" />
</view>
</view>
<view class="fixed">
<button class="primary" :loading="submitting" @click="submit">提交审核</button>
<button class="primary" style="margin-top:16rpx;background:#7aa9ff" :loading="checking" @click="checkModel">查重</button>
</view>
</scroll-view>
</template>
<script>
import ImageUploader from '../../components/ImageUploader.vue'
import { get, post, upload } from '../../common/http.js'
export default {
components: { ImageUploader },
data() {
return {
form: {
model: '',
brand: '',
barcode: '',
categoryId: '',
templateId: '',
parameters: {},
images: [],
remark: '',
safeMin: null,
safeMax: null
},
templates: [],
paramValues: {},
checking: false,
parameterText: '',
categories: [],
submitting: false,
paramPlaceholder: '可输入 JSON如 {"颜色":"黑","材质":"钢"}'
}
},
computed: {
categoryNames() { return this.categories.map(c => c.name) },
templateNames() { return this.templates.map(t => t.name) },
categoryLabel() {
const c = this.categories.find(x => String(x.id) === String(this.form.categoryId))
return c ? c.name : '选择类别'
},
selectedTemplate() {
return this.templates.find(t => String(t.id) === String(this.form.templateId))
},
templateLabel() {
const t = this.selectedTemplate
return t ? `${t.name}` : '选择模板'
}
},
onLoad(options) {
this.bootstrap()
if (options && options.prefill) {
try {
const data = JSON.parse(decodeURIComponent(options.prefill))
Object.assign(this.form, {
model: data.model || '',
brand: data.brand || '',
barcode: data.barcode || '',
categoryId: data.categoryId || '',
remark: data.remark || ''
})
if (data.parameters && typeof data.parameters === 'object') {
this.parameterText = JSON.stringify(data.parameters, null, 2)
}
} catch (_) {}
}
},
methods: {
async bootstrap() {
await Promise.all([ this.fetchCategories()])
await this.fetchTemplates()
},
async fetchCategories() {
try {
const res = await get('/api/product-categories')
this.categories = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
} catch (_) {
this.categories = []
}
},
async fetchTemplates() {
try {
const res = await get('/api/product-templates', this.form.categoryId ? { categoryId: this.form.categoryId } : {})
const list = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
this.templates = list
} catch (_) { this.templates = [] }
},
onPickCategory(e) {
const idx = Number(e.detail.value)
const c = this.categories[idx]
this.form.categoryId = c ? c.id : ''
this.fetchTemplates()
},
onPickTemplate(e) {
const idx = Number(e.detail.value)
const t = this.templates[idx]
this.form.templateId = t ? t.id : ''
this.paramValues = {}
},
onPickEnum(p, e) {
const idx = Number(e.detail.value)
const arr = p.enumOptions || []
this.paramValues[p.fieldKey] = arr[idx]
},
async scanBarcode() {
try {
const chooseRes = await uni.chooseImage({ count: 1, sourceType: ['camera','album'], sizeType: ['compressed'] })
let filePath = chooseRes.tempFilePaths[0]
try {
const comp = await uni.compressImage({ src: filePath, quality: 80 })
filePath = comp.tempFilePath || filePath
} catch (_) {}
const data = await upload('/api/barcode/scan', filePath, {}, 'file')
const barcode = data?.barcode || data?.data?.barcode
if (barcode) {
this.form.barcode = barcode
uni.showToast({ title: '识别成功', icon: 'success' })
} else {
uni.showToast({ title: '未识别到条码', icon: 'none' })
}
} catch (e) {
const msg = e?.message || '识码失败'
uni.showToast({ title: msg, icon: 'none' })
}
},
async checkModel() {
if (!this.form.model) return uni.showToast({ title: '请填写型号', icon: 'none' })
try {
this.checking = true
const res = await post('/api/products/submissions/check-model', { templateId: this.form.templateId, model: this.form.model })
if (res && res.available) {
uni.showToast({ title: '可用,无重复', icon: 'success' })
} else {
uni.showToast({ title: '已存在相同型号提交', icon: 'none' })
}
} catch (e) {
const msg = e?.message || '校验失败'
uni.showToast({ title: msg, icon: 'none' })
} finally { this.checking = false }
},
async submit() {
if (this.submitting) return
if (!this.form.model) {
return uni.showToast({ title: '请填写型号', icon: 'none' })
}
let paramsObj = null
if (this.parameterText) {
try {
paramsObj = JSON.parse(this.parameterText)
} catch (e) {
return uni.showToast({ title: '参数 JSON 不合法', icon: 'none' })
}
}
if (this.form.safeMin != null && this.form.safeMax != null && Number(this.form.safeMin) > Number(this.form.safeMax)) {
return uni.showToast({ title: '安全库存区间不合法', icon: 'none' })
}
// 模板必填校验与参数整理
let paramsForSubmit = paramsObj
if (this.selectedTemplate) {
// 校验必填
for (const p of (this.selectedTemplate.params || [])) {
if (p.required && (this.paramValues[p.fieldKey] === undefined || this.paramValues[p.fieldKey] === null || this.paramValues[p.fieldKey] === '')) {
return uni.showToast({ title: `请填写 ${p.fieldLabel}`, icon: 'none' })
}
}
// 类型规范化
const shaped = {}
for (const p of (this.selectedTemplate.params || [])) {
let v = this.paramValues[p.fieldKey]
if (p.type === 'number' && v !== undefined && v !== null && v !== '') v = Number(v)
if (p.type === 'boolean') v = !!v
shaped[p.fieldKey] = v
}
paramsForSubmit = shaped
}
const payload = {
model: this.form.model,
brand: this.form.brand,
barcode: this.form.barcode,
categoryId: this.form.categoryId || null,
templateId: this.form.templateId || null,
parameters: paramsForSubmit,
images: this.form.images,
remark: this.form.remark,
safeMin: this.form.safeMin,
safeMax: this.form.safeMax
}
this.submitting = true
try {
await post('/api/products/submissions', payload)
uni.showToast({ title: '提交成功', icon: 'success' })
setTimeout(() => {
uni.redirectTo({ url: '/pages/product/submissions' })
}, 400)
} catch (e) {
const msg = e?.message || '提交失败'
uni.showToast({ title: msg, icon: 'none' })
} finally {
this.submitting = false
}
}
}
}
</script>
<style lang="scss">
.page { padding: 24rpx 24rpx 120rpx; background: #f6f7fb; }
.hero { padding: 24rpx; background: linear-gradient(135deg, #4c8dff, #6ab7ff); border-radius: 20rpx; color: #fff; margin-bottom: 24rpx; }
.title { font-size: 36rpx; font-weight: 700; }
.desc { font-size: 26rpx; margin-top: 8rpx; opacity: 0.9; }
.section { background: #fff; border-radius: 16rpx; padding: 20rpx 22rpx; margin-bottom: 24rpx; box-shadow: 0 10rpx 30rpx rgba(0,0,0,0.04); }
.row { display: flex; align-items: center; gap: 16rpx; padding: 16rpx 0; border-bottom: 1rpx solid #f1f2f5; }
.row:last-child { border-bottom: none; }
.row.required .label::after { content: '*'; color: #ff5b5b; margin-left: 6rpx; }
.label { width: 130rpx; font-size: 28rpx; color: #2d3a4a; }
input { flex: 1; background: #f8f9fb; border-radius: 12rpx; padding: 16rpx 18rpx; font-size: 28rpx; color: #222; }
.textarea { width: 100%; min-height: 160rpx; background: #f8f9fb; border-radius: 12rpx; padding: 18rpx; font-size: 28rpx; color: #222; }
.picker { flex: 1; background: #f8f9fb; border-radius: 12rpx; padding: 18rpx; font-size: 28rpx; color: #222; }
.picker-btn { background: #4c8dff; color: #fff; border-radius: 999rpx; padding: 10rpx 22rpx; }
.triple input { flex: 1; }
.fixed { position: fixed; left: 0; right: 0; bottom: 0; padding: 20rpx 24rpx 40rpx; background: rgba(255,255,255,0.96); box-shadow: 0 -6rpx 20rpx rgba(0,0,0,0.08); }
.primary { width: 100%; height: 88rpx; border-radius: 999rpx; background: #4c8dff; color: #fff; font-size: 32rpx; font-weight: 600; }
</style>

View File

@@ -1,11 +1,6 @@
<template>
<view class="report">
<view class="modes">
<view class="mode-tab" :class="{active: mode==='sale'}" @click="setMode('sale')">销售统计</view>
<view class="mode-tab" :class="{active: mode==='purchase'}" @click="setMode('purchase')">进货统计</view>
<view class="mode-tab" :class="{active: mode==='inventory'}" @click="setMode('inventory')">库存统计</view>
<view class="mode-tab" :class="{active: mode==='arap'}" @click="setMode('arap')">应收/应付对账</view>
</view>
<view class="header">销售报表</view>
<view class="toolbar">
<picker mode="date" :value="startDate" @change="onStartChange"><view class="date">{{ startDate }}</view></picker>
@@ -13,39 +8,35 @@
<picker mode="date" :value="endDate" @change="onEndChange"><view class="date">{{ endDate }}</view></picker>
</view>
<view class="tabs" v-if="mode==='sale'">
<view class="tab" :class="{active: dim==='customer'}" @click="dim='customer'; refresh()">按客户</view>
<view class="tab" :class="{active: dim==='product'}" @click="dim='product'; refresh()">按货品</view>
</view>
<view class="tabs" v-else-if="mode==='purchase'">
<view class="tab" :class="{active: dim==='supplier'}" @click="dim='supplier'; refresh()">按供应商</view>
<view class="tab" :class="{active: dim==='product'}" @click="dim='product'; refresh()">按货品</view>
</view>
<view class="tabs" v-else-if="mode==='inventory'">
<view class="tab" :class="{active: dim==='qty'}" @click="dim='qty'; refresh()">按数量</view>
<view class="tab" :class="{active: dim==='amount'}" @click="dim='amount'; refresh()">按金额</view>
</view>
<view class="tabs" v-else-if="mode==='arap'">
<view class="tab" :class="{active: dim==='ar'}" @click="dim='ar'; refresh()">应收对账</view>
<view class="tab" :class="{active: dim==='ap'}" @click="dim='ap'; refresh()">应付对账</view>
</view>
<view class="summary">
<view class="item"><text class="label">销售额</text><text class="value"> {{ fmt(total.sales) }}</text></view>
<view class="item"><text class="label">成本</text><text class="value"> {{ fmt(total.cost) }}</text></view>
<view class="item"><text class="label">利润</text><text class="value"> {{ fmt(total.profit) }}</text></view>
<view class="item"><text class="label">利润率</text><text class="value">{{ profitRate }}</text></view>
<view class="tabs">
<view class="tab" :class="{active: dim==='customer'}" @click="setDimension('customer')">按客户</view>
<view class="tab" :class="{active: dim==='product'}" @click="setDimension('product')">按货品</view>
</view>
<view v-for="(row, idx) in rows" :key="idx" class="card">
<view class="row-head">
<image v-if="row.avatar" class="thumb" :src="row.avatar" />
<view class="title">{{ row.name }}</view>
<view class="summary" v-if="summaryItems.length">
<view class="summary-item" v-for="(item, ix) in summaryItems" :key="ix">
<text class="label">{{ item.label }}</text>
<text class="value">{{ item.value }}</text>
</view>
<view class="row-body">
<text>销售额 {{ fmt(row.sales) }}</text>
<text style="margin-left: 18rpx;">成本 {{ fmt(row.cost) }}</text>
<text style="margin-left: 18rpx;">利润 {{ fmt(row.profit) }}</text>
</view>
<view v-if="loading" class="loading">加载中...</view>
<view v-else-if="error" class="empty">{{ error }}</view>
<view v-else-if="!rows.length" class="empty">暂无统计数据</view>
<view v-else>
<view v-for="(row, idx) in rows" :key="idx" class="card">
<view class="row-head">
<view class="row-title">
<view class="title">{{ row.name }}</view>
<view class="subtitle" v-if="showProductSpec(row)">{{ row.spec }}</view>
</view>
</view>
<view class="row-body">
<view class="metric" v-for="(metric, mIdx) in rowMetrics(row)" :key="mIdx">
<text class="metric-label">{{ metric.label }}</text>
<text class="metric-value">{{ metric.value }}</text>
</view>
</view>
</view>
</view>
</view>
@@ -68,224 +59,113 @@ export default {
return {
startDate: formatDate(start),
endDate: formatDate(now),
mode: 'sale',
dim: 'customer',
rows: [],
total: { sales: 0, cost: 0, profit: 0 }
summary: { salesAmount: 0, costAmount: 0, profit: 0, profitRate: 0, itemCount: 0 },
loading: false,
error: ''
}
},
onLoad(query) {
try {
const m = query && query.mode
const d = query && query.dim
if (m) this.mode = m
if (d) this.dim = d
if (d === 'product' || d === 'customer') this.dim = d
} catch(e){}
this.refresh()
},
computed: {
profitRate() {
const { sales, profit } = this.total
if (!sales) return '0.00%'
return ((profit / sales) * 100).toFixed(2) + '%'
profitRateText() {
const rate = Number(this.summary?.profitRate || 0)
return rate.toFixed(2) + '%'
},
summaryItems() {
if (!this.rows.length) return []
return [
{ label: '销售额', value: `${this.fmt(this.summary.salesAmount)}` },
{ label: '成本', value: `${this.fmt(this.summary.costAmount)}` },
{ label: '利润', value: `${this.fmt(this.summary.profit)}` },
{ label: '利润率', value: this.profitRateText }
]
}
},
methods: {
fmt(n) { return Number(n || 0).toFixed(2) },
setMode(m) {
this.mode = m
this.dim = m === 'sale' ? 'customer' : m === 'purchase' ? 'supplier' : m === 'inventory' ? 'qty' : 'ar'
showProductSpec(row) { return this.dim === 'product' && row && row.spec },
rowMetrics(row) {
if (!row) return []
return [
{ label: '销售额', value: `${this.fmt(row.salesAmount)}` },
{ label: '成本', value: `${this.fmt(row.costAmount)}` },
{ label: '利润', value: `${this.fmt(row.profit)}` },
{ label: '利润率', value: `${Number(row.profitRate || 0).toFixed(2)}%` }
]
},
setDimension(d) {
if (d !== 'customer' && d !== 'product') return
if (this.dim === d) return
this.dim = d
this.refresh()
},
onStartChange(e) { this.startDate = e.detail.value; this.refresh() },
onEndChange(e) { this.endDate = e.detail.value; this.refresh() },
async refresh() {
if (this.mode === 'sale') {
if (this.dim === 'customer') return this.loadByCustomer()
if (this.dim === 'product') return this.loadByProduct()
}
if (this.mode === 'purchase') {
if (this.dim === 'supplier') return this.loadPurchaseBySupplier()
if (this.dim === 'product') return this.loadPurchaseByProduct()
}
if (this.mode === 'inventory') {
if (this.dim === 'qty') return this.loadInventoryByQty()
if (this.dim === 'amount') return this.loadInventoryByAmount()
}
if (this.mode === 'arap') {
if (this.dim === 'ar') return this.loadAR()
if (this.dim === 'ap') return this.loadAP()
}
},
async loadByCustomer() {
// 数据来源:/api/orders?biz=sale&type=out 与 /api/products/{id} 获取成本(近似),或由订单明细返回单价与估算成本
// 当前后端列表返回字段包含 amount、customerName缺少明细成本采用二段法
// 1) 列表聚合销售额2) 如存在 productId 与单位进价可获取成本;暂以 0 成本占位,保留接口演进点。
this.loading = true
this.error = ''
try {
const listResp = await get('/api/orders', { biz: 'sale', type: 'out', startDate: this.startDate, endDate: this.endDate, page: 1, size: 200 })
const list = (listResp && (listResp.list || listResp)) || []
const map = new Map()
let totalSales = 0
for (const it of list) {
const name = it.customerName || '未知客户'
const amount = Number(it.amount || 0)
totalSales += amount
if (!map.has(name)) map.set(name, { name, sales: 0, cost: 0, profit: 0 })
const row = map.get(name)
row.sales += amount
const resp = await get('/api/report/sales', {
dimension: this.dim,
startDate: this.startDate,
endDate: this.endDate
})
const items = Array.isArray(resp?.items) ? resp.items : []
this.rows = items.map(it => ({
name: it?.name || (this.dim === 'product' ? '未命名商品' : '未指定客户'),
spec: it?.spec || '',
salesAmount: Number(it?.salesAmount || 0),
costAmount: Number(it?.costAmount || 0),
profit: Number(it?.profit || 0),
profitRate: Number(it?.profitRate || 0)
}))
this.summary = {
salesAmount: Number(resp?.summary?.salesAmount || 0),
costAmount: Number(resp?.summary?.costAmount || 0),
profit: Number(resp?.summary?.profit || 0),
profitRate: Number(resp?.summary?.profitRate || 0),
itemCount: Number(resp?.summary?.itemCount || this.rows.length)
}
const rows = Array.from(map.values()).map(r => ({ ...r, profit: r.sales - r.cost }))
const total = { sales: totalSales, cost: 0, profit: totalSales }
this.rows = rows
this.total = total
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
this.error = (e && e.message) || '报表加载失败'
} finally {
this.loading = false
}
},
async loadByProduct() {
try {
const listResp = await get('/api/orders', { biz: 'sale', type: 'out', startDate: this.startDate, endDate: this.endDate, page: 1, size: 200 })
const list = (listResp && (listResp.list || listResp)) || []
// 订单详情接口包含明细,逐单补拉详情聚合(规模较小时可接受;后端如提供汇总接口可替换)
const agg = new Map()
for (const it of list) {
try {
const d = await get(`/api/orders/${it.id}`)
const items = d && d.items || []
for (const m of items) {
const key = String(m.productId || m.name)
if (!agg.has(key)) agg.set(key, { name: m.name || ('#'+key), sales: 0, cost: 0, profit: 0 })
const row = agg.get(key)
const sales = Number(m.amount || 0)
// 近似成本:缺后端返回进价,暂以 0待后端扩展返回 purchasePrice
row.sales += sales
}
} catch(_) {}
}
const rows = Array.from(agg.values()).map(r => ({ ...r, profit: r.sales - r.cost }))
const totalSales = rows.reduce((s, r) => s + r.sales, 0)
this.rows = rows
this.total = { sales: totalSales, cost: 0, profit: totalSales }
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
}
},
async loadPurchaseBySupplier() {
try {
const listResp = await get('/api/purchase-orders', { startDate: this.startDate, endDate: this.endDate, page: 1, size: 200 })
const list = (listResp && (listResp.list || listResp)) || []
const map = new Map(); let total = 0
for (const it of list) {
const name = it.supplierName || '未知供应商'
const amount = Number(it.amount || 0)
total += amount
if (!map.has(name)) map.set(name, { name, sales: 0, cost: 0, profit: 0 })
const row = map.get(name)
// 在进货统计语境里sales 用来展示“进货额”cost/profit 保持 0
row.sales += amount
}
this.rows = Array.from(map.values())
this.total = { sales: total, cost: 0, profit: 0 }
} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }
},
async loadPurchaseByProduct() {
try {
const listResp = await get('/api/purchase-orders', { startDate: this.startDate, endDate: this.endDate, page: 1, size: 200 })
const list = (listResp && (listResp.list || listResp)) || []
const agg = new Map()
for (const it of list) {
try {
const d = await get(`/api/purchase-orders/${it.id}`)
for (const m of (d?.items || [])) {
const key = String(m.productId || m.name)
if (!agg.has(key)) agg.set(key, { name: m.name || ('#'+key), sales: 0, cost: 0, profit: 0 })
const row = agg.get(key)
row.sales += Number(m.amount || 0) // 这里的 sales 表示“进货额”
}
} catch(_){}
}
const rows = Array.from(agg.values())
const total = rows.reduce((s, r)=> s + r.sales, 0)
this.rows = rows; this.total = { sales: total, cost: 0, profit: 0 }
} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }
},
async loadInventoryByQty() {
try {
const resp = await get('/api/inventories/logs', { startDate: this.startDate, endDate: this.endDate, page: 1, size: 200 })
const list = (resp && (resp.list || resp)) || []
const map = new Map(); let totalQty = 0
for (const it of list) {
const key = it.productId || '未知'
if (!map.has(key)) map.set(key, { name: String(key), sales: 0, cost: 0, profit: 0 })
const row = map.get(key)
const q = Number(it.qtyDelta || 0)
row.sales += q
totalQty += q
}
this.rows = Array.from(map.values())
this.total = { sales: totalQty, cost: 0, profit: 0 }
} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }
},
async loadInventoryByAmount() {
try {
const resp = await get('/api/inventories/logs', { startDate: this.startDate, endDate: this.endDate, page: 1, size: 200 })
const list = (resp && (resp.list || resp)) || []
const map = new Map(); let totalAmt = 0
for (const it of list) {
const key = it.productId || '未知'
if (!map.has(key)) map.set(key, { name: String(key), sales: 0, cost: 0, profit: 0 })
const row = map.get(key)
const a = Number(it.amount || it.amountDelta || 0)
row.sales += a
totalAmt += a
}
this.rows = Array.from(map.values())
this.total = { sales: totalAmt, cost: 0, profit: 0 }
} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }
},
async loadAR() {
// 读取客户列表含 receivable 字段,作为对账口径(期末=期初+增加-收回-抹零);后端如提供期间变动接口再替换
try {
const res = await get('/api/customers', { page: 1, size: 100, debtOnly: false })
const list = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
const rows = list.map(c => ({ name: c.name, sales: Number(c.receivable || 0), cost: 0, profit: 0 }))
const total = rows.reduce((s, r)=> s + r.sales, 0)
this.rows = rows; this.total = { sales: total, cost: 0, profit: 0 }
} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }
},
async loadAP() {
// 供应商暂未返回应付字段先展示总览为0并提示后端扩展遵循“不开假数据”
try {
const res = await get('/api/suppliers', { page: 1, size: 100 })
const list = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
const rows = list.map(s => ({ name: s.name, sales: Number(s.apPayable || 0), cost: 0, profit: 0 }))
const total = rows.reduce((s, r)=> s + r.sales, 0)
this.rows = rows; this.total = { sales: total, cost: 0, profit: 0 }
} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }
}
}
}
}
}
</script>
<style lang="scss">
.report { padding: 20rpx; }
.modes { display: flex; gap: 12rpx; margin-bottom: 14rpx; }
.mode-tab { flex: 1; text-align: center; padding: 16rpx 0; border-radius: 999rpx; background: $uni-bg-color-hover; color: $uni-text-color-grey; border: 1rpx solid $uni-border-color; }
.mode-tab.active { background: $uni-color-primary; color: #fff; border-color: $uni-color-primary; font-weight: 700; }
.toolbar { display: flex; align-items: center; gap: 8rpx; background: $uni-bg-color-grey; padding: 14rpx 16rpx; border-radius: 12rpx; }
.date { padding: 10rpx 16rpx; border: 1rpx solid $uni-border-color; border-radius: 8rpx; color: $uni-text-color; }
.tabs { display: flex; gap: 16rpx; margin-top: 14rpx; }
.tab { padding: 12rpx 18rpx; border-radius: 999rpx; background: $uni-bg-color-hover; color: $uni-text-color-grey; }
.tab.active { background: $uni-color-primary; color: #fff; }
.summary { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8rpx; margin-top: 14rpx; }
.summary .item { background: $uni-bg-color-grey; border-radius: 12rpx; padding: 16rpx; }
.summary .label { font-size: 22rpx; color: $uni-text-color-grey; }
.summary .value { display: block; margin-top: 8rpx; font-weight: 700; color: $uni-text-color; }
.card { margin-top: 16rpx; background: $uni-bg-color-grey; border-radius: 12rpx; padding: 16rpx; }
.row-head { display: flex; align-items: center; gap: 12rpx; }
.thumb { width: 72rpx; height: 72rpx; border-radius: 8rpx; background: $uni-bg-color-hover; }
.title { font-size: 28rpx; font-weight: 700; color: $uni-text-color; }
.row-body { margin-top: 10rpx; color: $uni-text-color-grey; }
.report { padding: 24rpx 20rpx 36rpx; display:flex; flex-direction:column; gap: 18rpx; }
.header { font-size: 34rpx; font-weight: 700; color:#1f2a44; padding-left: 8rpx; }
.toolbar { display:flex; align-items:center; justify-content:center; gap: 12rpx; background: #f7f9fc; border-radius: 16rpx; padding: 18rpx; }
.date { min-width: 200rpx; padding: 12rpx 18rpx; border-radius: 12rpx; background: #fff; border: 1rpx solid rgba(91,107,139,0.16); text-align:center; color:#32445b; }
.tabs { display:flex; gap: 12rpx; justify-content:center; }
.tab { padding: 10rpx 20rpx; border-radius: 999rpx; background: #f0f4ff; color:#5b6b8b; transition: all .2s ease; }
.tab.active { background: rgba(76,141,255,0.18); color:#3467d6; box-shadow: inset 0 0 0 2rpx rgba(76,141,255,0.45); }
.summary { display:grid; grid-template-columns: repeat(auto-fill, minmax(240rpx,1fr)); gap: 12rpx; }
.summary-item { background: #f7f9fc; border-radius: 16rpx; padding: 20rpx; display:flex; flex-direction:column; gap: 10rpx; }
.summary-item .label { font-size: 24rpx; color:#6e7a96; }
.summary-item .value { font-size: 32rpx; font-weight:700; color:#1f2a44; }
.card { background:#fff; border-radius: 18rpx; padding: 20rpx; box-shadow: 0 8rpx 20rpx rgba(31,42,68,0.08); display:flex; flex-direction:column; gap: 14rpx; }
.row-head { display:flex; justify-content:space-between; align-items:flex-start; }
.row-title { display:flex; flex-direction:column; gap: 6rpx; }
.title { font-size: 30rpx; font-weight:700; color:#1f2a44; }
.subtitle { font-size: 24rpx; color:#6e7a96; }
.row-body { display:flex; flex-wrap:wrap; gap: 12rpx 24rpx; }
.metric { display:flex; gap: 8rpx; align-items:center; background:#f4f6fb; border-radius: 12rpx; padding: 10rpx 16rpx; }
.metric-label { font-size: 24rpx; color:#6e7a96; }
.metric-value { font-size: 28rpx; color:#1f2a44; font-weight:600; }
.empty { text-align:center; padding: 80rpx 0; color:#9aa4be; font-size: 26rpx; }
.loading { text-align:center; padding: 40rpx 0; color:#5b6b8b; font-size: 24rpx; }
</style>

View File

@@ -14,7 +14,7 @@
</template>
<script>
import { post, put } from '../../common/http.js'
import { get, post, put } from '../../common/http.js'
export default {
data() {
return {
@@ -22,8 +22,24 @@ export default {
form: { name:'', contactName:'', mobile:'', phone:'', address:'', apOpening:0, apPayable:0, remark:'' }
}
},
onLoad(query) { if (query && query.id) { this.id = Number(query.id) } },
onLoad(query) { if (query && query.id) { this.id = Number(query.id); this.load() } },
methods: {
async load() {
if (!this.id) return
try {
const d = await get(`/api/suppliers/${this.id}`)
this.form = {
name: d?.name || '',
contactName: d?.contactName || '',
mobile: d?.mobile || '',
phone: d?.phone || '',
address: d?.address || '',
apOpening: Number(d?.apOpening || 0),
apPayable: Number(d?.apPayable || 0),
remark: d?.remark || ''
}
} catch(e) { uni.showToast({ title: e?.message || '加载失败', icon: 'none' }) }
},
async save() {
if (!this.form.name) return uni.showToast({ title:'请填写供应商名称', icon:'none' })
try {

View File

@@ -35,12 +35,21 @@
},
createSupplier() { uni.navigateTo({ url: '/pages/supplier/form' }) },
select(s) {
const opener = getCurrentPages()[getCurrentPages().length-2]
if (opener && opener.$vm) {
opener.$vm.order.supplierId = s.id
opener.$vm.supplierName = s.name
try {
const pages = getCurrentPages()
const opener = pages && pages.length >= 2 ? pages[pages.length - 2] : null
const vm = opener && opener.$vm ? opener.$vm : null
const canPick = !!(vm && vm.order)
if (canPick) {
vm.order.supplierId = s.id
if (Object.prototype.hasOwnProperty.call(vm, 'supplierName')) vm.supplierName = s.name
uni.navigateBack()
} else {
uni.navigateTo({ url: `/pages/supplier/form?id=${s.id}` })
}
} catch (_) {
uni.navigateTo({ url: `/pages/supplier/form?id=${s.id}` })
}
uni.navigateBack()
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 607 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 990 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 964 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 845 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"version":3,"file":"assets.js","sources":["static/icons/icons8-shopping-cart-100.png","static/logo.png"],"sourcesContent":["export default \"__VITE_ASSET__c6fa5b3f__\"","export default \"__VITE_ASSET__46719607__\""],"names":[],"mappings":";AAAA,MAAe,eAAA;ACAf,MAAe,aAAA;;;"}
{"version":3,"file":"assets.js","sources":["static/icons/icons8-shopping-cart-100.png","static/icons/icons8-login-50.png","static/logo.png","static/icons/icons8-vip-48 (1).png","static/icons/icons8-close-48.png"],"sourcesContent":["export default \"__VITE_ASSET__c6fa5b3f__\"","export default \"__VITE_ASSET__1aee3610__\"","export default \"__VITE_ASSET__46719607__\"","export default \"__VITE_ASSET__ec48be62__\"","export default \"__VITE_ASSET__ff6c5e80__\""],"names":[],"mappings":";AAAA,MAAe,eAAA;ACAf,MAAe,eAAA;ACAf,MAAe,eAAA;ACAf,MAAe,eAAA;ACAf,MAAe,aAAA;;;;;;"}

View File

@@ -1 +1 @@
{"version":3,"file":"config.js","sources":["common/config.js"],"sourcesContent":["// 统一配置:禁止在业务代码中硬编码\n// 优先级:环境变量(Vite/HBuilderX 构建注入) > 本地存储 > 默认值\n\nconst envBaseUrl = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_API_BASE_URL || process.env.API_BASE_URL)) || '';\nconst storageBaseUrl = typeof uni !== 'undefined' ? (uni.getStorageSync('API_BASE_URL') || '') : '';\nconst fallbackBaseUrl = 'http://127.0.0.1:8080';\n\nexport const API_BASE_URL = (envBaseUrl || storageBaseUrl || fallbackBaseUrl).replace(/\\/$/, '');\n\n// 多地址候选(按优先级顺序,自动去重与去尾斜杠)\nconst candidateBases = [envBaseUrl, storageBaseUrl, fallbackBaseUrl, 'http://127.0.0.1:8080', 'http://localhost:8080'];\nexport const API_BASE_URL_CANDIDATES = Array.from(new Set(candidateBases.filter(Boolean))).map(u => String(u).replace(/\\/$/, ''));\n\nconst envShopId = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_SHOP_ID || process.env.SHOP_ID)) || '';\nconst storageShopId = typeof uni !== 'undefined' ? (uni.getStorageSync('SHOP_ID') || '') : '';\nexport const SHOP_ID = Number(envShopId || storageShopId || 1);\n\n\n// 默认用户(可移除):\n// - 用途:开发/演示环境自动将用户固定为“张老板”id=2\n// - 开关优先级:环境变量 > 本地存储 > 默认值\n// - 生产默认关闭false开发可通过本地存储或环境变量开启\nconst envEnableDefaultUser = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_ENABLE_DEFAULT_USER || process.env.ENABLE_DEFAULT_USER)) || '';\nconst storageEnableDefaultUser = typeof uni !== 'undefined' ? (uni.getStorageSync('ENABLE_DEFAULT_USER') || '') : '';\nexport const ENABLE_DEFAULT_USER = String(envEnableDefaultUser || storageEnableDefaultUser || 'false').toLowerCase() === 'true';\n\nconst envDefaultUserId = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_DEFAULT_USER_ID || process.env.DEFAULT_USER_ID)) || '';\nconst storageDefaultUserId = typeof uni !== 'undefined' ? (uni.getStorageSync('DEFAULT_USER_ID') || '') : '';\nexport const DEFAULT_USER_ID = Number(envDefaultUserId || storageDefaultUserId || 0);\n\n\n// 会员价格(单位:元/月):环境 > 本地存储 > 默认值\nconst envVipPrice = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_VIP_PRICE || process.env.VIP_PRICE)) || '';\nconst storageVipPrice = typeof uni !== 'undefined' ? (uni.getStorageSync('VIP_PRICE') || '') : '';\nexport const VIP_PRICE_PER_MONTH = Number(envVipPrice || storageVipPrice || 15);\n\n\n// 首页横幅图片(公告上方),避免硬编码\n// 优先级:环境变量 > 本地存储 > 默认值(放置于 /static/icons/ 下)\nconst envHomeBanner = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_HOME_BANNER_IMG || process.env.HOME_BANNER_IMG)) || '';\nconst storageHomeBanner = typeof uni !== 'undefined' ? (uni.getStorageSync('HOME_BANNER_IMG') || '') : '';\nexport const HOME_BANNER_IMG = String(envHomeBanner || storageHomeBanner || '/static/icons/home-banner.png');\n\n// KPI 图标(可按需覆盖),避免在页面里硬编码\nexport const KPI_ICONS = {\n todaySales: '/static/icons/sale.png',\n monthSales: '/static/icons/report.png',\n monthProfit: '/static/icons/report.png',\n stockCount: '/static/icons/product.png'\n}\n\n"],"names":["uni"],"mappings":";;AAGA,MAAM,aAAc,OAAO,YAAY,eAAe,QAAQ,QAAQ,QAAQ,IAAI,yBAAyB,QAAQ,IAAI,iBAAkB;AACzI,MAAM,iBAAiB,OAAOA,cAAG,UAAK,cAAeA,cAAAA,MAAI,eAAe,cAAc,KAAK,KAAM;AACjG,MAAM,kBAAkB;AAEZ,MAAC,gBAAgB,cAAc,kBAAkB,iBAAiB,QAAQ,OAAO,EAAE;AAG/F,MAAM,iBAAiB,CAAC,YAAY,gBAAgB,iBAAiB,yBAAyB,uBAAuB;AACzG,MAAC,0BAA0B,MAAM,KAAK,IAAI,IAAI,eAAe,OAAO,OAAO,CAAC,CAAC,EAAE,IAAI,OAAK,OAAO,CAAC,EAAE,QAAQ,OAAO,EAAE,CAAC;AAE7G,OAAO,YAAY,eAAe,QAAQ,QAAQ,QAAQ,IAAI,oBAAoB,QAAQ,IAAI,YAAa;AACxG,OAAOA,cAAG,UAAK,cAAeA,cAAG,MAAC,eAAe,SAAS,KAAK,KAAM;AAQ3F,MAAM,uBAAwB,OAAO,YAAY,eAAe,QAAQ,QAAQ,QAAQ,IAAI,gCAAgC,QAAQ,IAAI,wBAAyB;AACjK,MAAM,2BAA2B,OAAOA,cAAG,UAAK,cAAeA,cAAAA,MAAI,eAAe,qBAAqB,KAAK,KAAM;AAC/E,OAAO,wBAAwB,4BAA4B,OAAO,EAAE,YAAa,MAAK;AAE/F,OAAO,YAAY,eAAe,QAAQ,QAAQ,QAAQ,IAAI,4BAA4B,QAAQ,IAAI,oBAAqB;AACxH,OAAOA,cAAG,UAAK,cAAeA,cAAG,MAAC,eAAe,iBAAiB,KAAK,KAAM;AAK1G,MAAM,cAAe,OAAO,YAAY,eAAe,QAAQ,QAAQ,QAAQ,IAAI,sBAAsB,QAAQ,IAAI,cAAe;AACpI,MAAM,kBAAkB,OAAOA,cAAG,UAAK,cAAeA,cAAAA,MAAI,eAAe,WAAW,KAAK,KAAM;AACnF,MAAC,sBAAsB,OAAO,eAAe,mBAAmB,EAAE;AAKvD,OAAO,YAAY,eAAe,QAAQ,QAAQ,QAAQ,IAAI,4BAA4B,QAAQ,IAAI,oBAAqB;AACxH,OAAOA,cAAG,UAAK,cAAeA,cAAG,MAAC,eAAe,iBAAiB,KAAK,KAAM;AAI3F,MAAC,YAAY;AAAA,EACrB,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,YAAY;AAChB;;;;;"}
{"version":3,"file":"config.js","sources":["common/config.js"],"sourcesContent":["// 统一配置:禁止在业务代码中硬编码\n// 优先级:环境变量(Vite/HBuilderX 构建注入) > 本地存储 > 默认值\n\nconst envBaseUrl = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_API_BASE_URL || process.env.API_BASE_URL)) || '';\nconst storageBaseUrl = typeof uni !== 'undefined' ? (uni.getStorageSync('API_BASE_URL') || '') : '';\nconst fallbackBaseUrl = 'http://127.0.0.1:8080';\n\nexport const API_BASE_URL = (envBaseUrl || storageBaseUrl || fallbackBaseUrl).replace(/\\/$/, '');\n\n// 多地址候选(按优先级顺序,自动去重与去尾斜杠)\nconst candidateBases = [envBaseUrl, storageBaseUrl, fallbackBaseUrl, 'http://127.0.0.1:8080', 'http://localhost:8080'];\nexport const API_BASE_URL_CANDIDATES = Array.from(new Set(candidateBases.filter(Boolean))).map(u => String(u).replace(/\\/$/, ''));\n\nconst envShopId = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_SHOP_ID || process.env.SHOP_ID)) || '';\nconst storageShopId = typeof uni !== 'undefined' ? (uni.getStorageSync('SHOP_ID') || '') : '';\nexport const SHOP_ID = Number(envShopId || storageShopId || 1);\n\n\n// 默认用户(可移除):\n// - 用途:开发/演示环境自动将用户固定为“张老板”id=2\n// - 开关优先级:环境变量 > 本地存储 > 默认值\n// - 生产默认关闭false开发可通过本地存储或环境变量开启\nconst envEnableDefaultUser = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_ENABLE_DEFAULT_USER || process.env.ENABLE_DEFAULT_USER)) || '';\nconst storageEnableDefaultUser = typeof uni !== 'undefined' ? (uni.getStorageSync('ENABLE_DEFAULT_USER') || '') : '';\nexport const ENABLE_DEFAULT_USER = String(envEnableDefaultUser || storageEnableDefaultUser || 'false').toLowerCase() === 'true';\n\nconst envDefaultUserId = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_DEFAULT_USER_ID || process.env.DEFAULT_USER_ID)) || '';\nconst storageDefaultUserId = typeof uni !== 'undefined' ? (uni.getStorageSync('DEFAULT_USER_ID') || '') : '';\nexport const DEFAULT_USER_ID = Number(envDefaultUserId || storageDefaultUserId || 0);\n\n\n// 会员价格(单位:元/月):环境 > 本地存储 > 默认值\nconst envVipPrice = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_VIP_PRICE || process.env.VIP_PRICE)) || '';\nconst storageVipPrice = typeof uni !== 'undefined' ? (uni.getStorageSync('VIP_PRICE') || '') : '';\nexport const VIP_PRICE_PER_MONTH = Number(envVipPrice || storageVipPrice || 15);\n\n\n// 首页横幅图片(公告上方),避免硬编码\n// 优先级:环境变量 > 本地存储 > 默认值(放置于 /static/icons/ 下)\nconst envHomeBanner = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_HOME_BANNER_IMG || process.env.HOME_BANNER_IMG)) || '';\nconst storageHomeBanner = typeof uni !== 'undefined' ? (uni.getStorageSync('HOME_BANNER_IMG') || '') : '';\nexport const HOME_BANNER_IMG = String(envHomeBanner || storageHomeBanner || '/static/icons/home-banner.png');\n\n// KPI 图标(可按需覆盖),避免在页面里硬编码\nexport const KPI_ICONS = {\n todaySales: '/static/icons/webwxgetmsgimg.jpg',\n monthSales: '/static/icons/webwxgetmsgimg.jpg',\n monthProfit: '/static/icons/icons8-profit-50.png',\n stockCount: '/static/icons/product.png'\n}\n\n"],"names":["uni"],"mappings":";;AAGA,MAAM,aAAc,OAAO,YAAY,eAAe,QAAQ,QAAQ,QAAQ,IAAI,yBAAyB,QAAQ,IAAI,iBAAkB;AACzI,MAAM,iBAAiB,OAAOA,cAAG,UAAK,cAAeA,cAAAA,MAAI,eAAe,cAAc,KAAK,KAAM;AACjG,MAAM,kBAAkB;AAEZ,MAAC,gBAAgB,cAAc,kBAAkB,iBAAiB,QAAQ,OAAO,EAAE;AAG/F,MAAM,iBAAiB,CAAC,YAAY,gBAAgB,iBAAiB,yBAAyB,uBAAuB;AACzG,MAAC,0BAA0B,MAAM,KAAK,IAAI,IAAI,eAAe,OAAO,OAAO,CAAC,CAAC,EAAE,IAAI,OAAK,OAAO,CAAC,EAAE,QAAQ,OAAO,EAAE,CAAC;AAEhI,MAAM,YAAa,OAAO,YAAY,eAAe,QAAQ,QAAQ,QAAQ,IAAI,oBAAoB,QAAQ,IAAI,YAAa;AAC9H,MAAM,gBAAgB,OAAOA,cAAG,UAAK,cAAeA,cAAAA,MAAI,eAAe,SAAS,KAAK,KAAM;AAC/E,MAAC,UAAU,OAAO,aAAa,iBAAiB,CAAC;AAO7D,MAAM,uBAAwB,OAAO,YAAY,eAAe,QAAQ,QAAQ,QAAQ,IAAI,gCAAgC,QAAQ,IAAI,wBAAyB;AACjK,MAAM,2BAA2B,OAAOA,cAAG,UAAK,cAAeA,cAAAA,MAAI,eAAe,qBAAqB,KAAK,KAAM;AACtG,MAAC,sBAAsB,OAAO,wBAAwB,4BAA4B,OAAO,EAAE,YAAW,MAAO;AAEzH,MAAM,mBAAoB,OAAO,YAAY,eAAe,QAAQ,QAAQ,QAAQ,IAAI,4BAA4B,QAAQ,IAAI,oBAAqB;AACrJ,MAAM,uBAAuB,OAAOA,cAAG,UAAK,cAAeA,cAAAA,MAAI,eAAe,iBAAiB,KAAK,KAAM;AAC9F,MAAC,kBAAkB,OAAO,oBAAoB,wBAAwB,CAAC;AAI9D,OAAO,YAAY,eAAe,QAAQ,QAAQ,QAAQ,IAAI,sBAAsB,QAAQ,IAAI,cAAe;AAC5G,OAAOA,cAAG,UAAK,cAAeA,cAAG,MAAC,eAAe,WAAW,KAAK,KAAM;AAMxE,OAAO,YAAY,eAAe,QAAQ,QAAQ,QAAQ,IAAI,4BAA4B,QAAQ,IAAI,oBAAqB;AACxH,OAAOA,cAAG,UAAK,cAAeA,cAAG,MAAC,eAAe,iBAAiB,KAAK,KAAM;AAI3F,MAAC,YAAY;AAAA,EACrB,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,YAAY;AAChB;;;;;;;"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"version":3,"file":"orders.js","sources":["pages/my/orders.vue","../../../../Downloads/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniPage:/cGFnZXMvbXkvb3JkZXJzLnZ1ZQ"],"sourcesContent":["<template>\r\n\t<view class=\"orders\">\r\n\t\t<view class=\"hint\" v-if=\"!isLoggedIn\">请先登录后查看VIP支付记录</view>\r\n\t\t<view v-else>\r\n\t\t\t<view class=\"item\" v-for=\"it in list\" :key=\"it.id\">\r\n\t\t\t\t<view class=\"row1\">\r\n\t\t\t\t\t<text class=\"price\">¥ {{ toMoney(it.price) }}</text>\r\n\t\t\t\t\t<text class=\"channel\">{{ it.channel || '支付' }}</text>\r\n\t\t\t\t</view>\r\n\t\t\t\t<view class=\"row2\">\r\n\t\t\t\t\t<text class=\"date\">{{ fmt(it.createdAt) }}</text>\r\n\t\t\t\t\t<text class=\"duration\">{{ it.durationDays }} 天</text>\r\n\t\t\t\t</view>\r\n\t\t\t\t<view class=\"row3\" v-if=\"it.expireTo\">\r\n\t\t\t\t\t<text class=\"expire\">有效期至 {{ fmt(it.expireTo) }}</text>\r\n\t\t\t\t</view>\r\n\t\t\t</view>\r\n\t\t\t<view class=\"empty\" v-if=\"list.length===0\">暂无支付记录</view>\r\n\t\t</view>\r\n\t</view>\r\n</template>\r\n\r\n<script>\r\nimport { get } from '../../common/http.js'\r\n\r\nexport default {\r\n\tdata(){\r\n\t\treturn { list: [], page: 1, size: 20, loading: false }\r\n\t},\r\n\tonShow(){ this.fetch(true) },\r\n\tcomputed: {\r\n\t\tisLoggedIn(){ try { return !!uni.getStorageSync('TOKEN') } catch(e){ return false } }\r\n\t},\r\n\tmethods: {\r\n\t\tasync fetch(reset=false){\r\n\t\t\tif (!this.isLoggedIn) return\r\n\t\t\tif (this.loading) return\r\n\t\t\tthis.loading = true\r\n\t\t\ttry {\r\n\t\t\t\tconst p = reset ? 1 : this.page\r\n\t\t\t\tconst data = await get('/api/vip/recharges', { page: p, size: this.size })\r\n\t\t\t\tconst arr = Array.isArray(data?.list) ? data.list : []\r\n\t\t\t\tthis.list = reset ? arr : (this.list || []).concat(arr)\r\n\t\t\t\tthis.page = p + 1\r\n\t\t\t} finally { this.loading = false }\r\n\t\t},\r\n\t\tfmt(v){ if (!v) return ''; const s = String(v); const m = s.match(/^(\\d{4}-\\d{2}-\\d{2})([ T](\\d{2}:\\d{2}))/); return m ? `${m[1]} ${m[3]}` : s },\r\n\t\ttoMoney(v){ try { return Number(v).toFixed(2) } catch(_) { return v } }\r\n\t}\r\n}\r\n</script>\r\n\r\n<style lang=\"scss\">\r\n.orders { padding: 16rpx 16rpx calc(env(safe-area-inset-bottom) + 16rpx); }\r\n.hint { color: $uni-text-color-grey; padding: 24rpx; text-align: center; }\r\n.item { background:#fff; border:1rpx solid $uni-border-color; border-radius: 16rpx; padding: 18rpx; margin: 12rpx 0; }\r\n.row1 { display:flex; justify-content: space-between; align-items:center; margin-bottom: 6rpx; }\r\n.price { color:#111; font-weight: 800; font-size: 34rpx; }\r\n.channel { color:#666; font-size: 24rpx; }\r\n.row2 { display:flex; justify-content: space-between; color:#666; font-size: 24rpx; }\r\n.row3 { margin-top: 6rpx; color:#4C8DFF; font-size: 24rpx; }\r\n.empty { text-align:center; color:#999; padding: 40rpx 0; }\r\n</style>\r\n\r\n\r\n","import MiniProgramPage from 'C:/Users/21826/Desktop/Wj/PartsInquiry/frontend/pages/my/orders.vue'\nwx.createPage(MiniProgramPage)"],"names":["uni","get"],"mappings":";;;AAyBA,MAAK,YAAU;AAAA,EACd,OAAM;AACL,WAAO,EAAE,MAAM,CAAA,GAAI,MAAM,GAAG,MAAM,IAAI,SAAS,MAAM;AAAA,EACrD;AAAA,EACD,SAAQ;AAAE,SAAK,MAAM,IAAI;AAAA,EAAG;AAAA,EAC5B,UAAU;AAAA,IACT,aAAY;AAAE,UAAI;AAAE,eAAO,CAAC,CAACA,cAAAA,MAAI,eAAe,OAAO;AAAA,MAAE,SAAQ,GAAE;AAAE,eAAO;AAAA;IAAQ;AAAA,EACpF;AAAA,EACD,SAAS;AAAA,IACR,MAAM,MAAM,QAAM,OAAM;AACvB,UAAI,CAAC,KAAK;AAAY;AACtB,UAAI,KAAK;AAAS;AAClB,WAAK,UAAU;AACf,UAAI;AACH,cAAM,IAAI,QAAQ,IAAI,KAAK;AAC3B,cAAM,OAAO,MAAMC,YAAG,IAAC,sBAAsB,EAAE,MAAM,GAAG,MAAM,KAAK,MAAM;AACzE,cAAM,MAAM,MAAM,QAAQ,6BAAM,IAAI,IAAI,KAAK,OAAO,CAAC;AACrD,aAAK,OAAO,QAAQ,OAAO,KAAK,QAAQ,CAAA,GAAI,OAAO,GAAG;AACtD,aAAK,OAAO,IAAI;AAAA;AACL,aAAK,UAAU;AAAA,MAAM;AAAA,IACjC;AAAA,IACD,IAAI,GAAE;AAAE,UAAI,CAAC;AAAG,eAAO;AAAI,YAAM,IAAI,OAAO,CAAC;AAAG,YAAM,IAAI,EAAE,MAAM,yCAAyC;AAAG,aAAO,IAAI,GAAG,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK;AAAA,IAAG;AAAA,IAChJ,QAAQ,GAAE;AAAE,UAAI;AAAE,eAAO,OAAO,CAAC,EAAE,QAAQ,CAAC;AAAA,MAAE,SAAQ,GAAG;AAAE,eAAO;AAAA,MAAA;AAAA,IAAI;AAAA,EACvE;AACD;;;;;;;;;;;;;;;;;;;;;;AChDA,GAAG,WAAW,eAAe;"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"version":3,"file":"mine.js","sources":["pages/parts/mine.vue","../../../../Downloads/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniPage:/cGFnZXMvcGFydHMvbWluZS52dWU"],"sourcesContent":["<template>\r\n\t<view class=\"page\">\r\n\t\t<view class=\"toolbar\">\r\n\t\t\t<picker mode=\"selector\" :range=\"statusOptions\" @change=\"onPickStatus\"><view class=\"picker\">状态:{{ statusLabel }}</view></picker>\r\n\t\t\t<input class=\"input\" v-model.trim=\"q.kw\" placeholder=\"关键词\" />\r\n\t\t\t<button class=\"btn\" @click=\"reload\">查询</button>\r\n\t\t</view>\r\n\t\t<scroll-view class=\"list\" scroll-y @scrolltolower=\"loadMore\">\r\n\t\t\t<view class=\"item\" v-for=\"it in rows\" :key=\"it.id\">\r\n\t\t\t\t<view class=\"title\">{{ it.model }}<text class=\"brand\">{{ it.brand||'' }}</text></view>\r\n\t\t\t\t<view class=\"meta\">状态:{{ it.status }} 提交时间:{{ it.createdAt }}</view>\r\n\t\t\t\t<view class=\"meta\">规格:{{ it.spec||'-' }}</view>\r\n\t\t\t</view>\r\n\t\t\t<view v-if=\"!rows.length\" class=\"empty\">暂无提交</view>\r\n\t\t</scroll-view>\r\n\t</view>\r\n</template>\r\n\r\n<script>\r\nimport { get } from '../../common/http.js'\r\nexport default {\r\n\tdata(){\r\n\t\treturn { q:{ status:'', kw:'', page:1, size:20 }, rows:[], finished:false, loading:false }\r\n\t},\r\n\tcomputed:{\r\n\t\tstatusOptions(){ return ['全部','pending','published','rejected'] },\r\n\t\tstatusLabel(){ return this.q.status||'全部' }\r\n\t},\r\n\tonShow(){ this.reload() },\r\n\tmethods:{\r\n\t\tonPickStatus(e){ const v=this.statusOptions[Number(e.detail.value)]; this.q.status= (v==='全部'?'':v); this.reload() },\r\n\t\treload(){ this.rows=[]; this.q.page=1; this.finished=false; this.loadMore() },\r\n\t\tasync loadMore(){\r\n\t\t\tif (this.loading||this.finished) return\r\n\t\t\tthis.loading=true\r\n\t\t\ttry{\r\n\t\t\t\tconst res = await get('/api/part-submissions/mine', this.q)\r\n\t\t\t\tconst list = Array.isArray(res?.list)?res.list:(Array.isArray(res)?res:[])\r\n\t\t\t\tthis.rows = this.rows.concat(list)\r\n\t\t\t\tif (list.length < this.q.size) this.finished=true\r\n\t\t\t\tthis.q.page += 1\r\n\t\t\t}catch(e){ uni.showToast({ title:String(e.message||'加载失败'), icon:'none' }) }\r\n\t\t\tfinally{ this.loading=false }\r\n\t\t}\r\n\t}\r\n}\r\n</script>\r\n\r\n<style lang=\"scss\">\r\n.page{ display:flex; flex-direction: column; height: 100vh; }\r\n.toolbar{ display:flex; gap:12rpx; padding: 16rpx; background:#fff; border-bottom: 2rpx solid #eef2f9; }\r\n.picker{ padding: 10rpx 16rpx; background:#f7f9ff; border-radius: 12rpx; color:#5b6b80; }\r\n.input{ flex:1; background:#f7f9ff; border-radius: 12rpx; padding: 12rpx 16rpx; }\r\n.btn{ background:#eef3ff; color:#2d6be6; padding: 12rpx 16rpx; border-radius: 12rpx; }\r\n.list{ flex:1; }\r\n.item{ padding: 18rpx; border-bottom: 2rpx solid #f1f4fa; background:#fff; }\r\n.title{ font-weight: 700; }\r\n.brand{ margin-left: 12rpx; color:#5b6b80; font-weight: 400; }\r\n.meta{ color:#6b7a99; margin-top: 6rpx; }\r\n.empty{ height: 60vh; display:flex; align-items:center; justify-content:center; color:#6b7a99; }\r\n</style>\r\n\r\n\r\n","import MiniProgramPage from 'C:/Users/21826/Desktop/Wj/PartsInquiry/frontend/pages/parts/mine.vue'\nwx.createPage(MiniProgramPage)"],"names":["get","uni"],"mappings":";;;AAoBA,MAAK,YAAU;AAAA,EACd,OAAM;AACL,WAAO,EAAE,GAAE,EAAE,QAAO,IAAI,IAAG,IAAI,MAAK,GAAG,MAAK,GAAI,GAAE,MAAK,CAAA,GAAI,UAAS,OAAO,SAAQ,MAAM;AAAA,EACzF;AAAA,EACD,UAAS;AAAA,IACR,gBAAe;AAAE,aAAO,CAAC,MAAK,WAAU,aAAY,UAAU;AAAA,IAAG;AAAA,IACjE,cAAa;AAAE,aAAO,KAAK,EAAE,UAAQ;AAAA,IAAK;AAAA,EAC1C;AAAA,EACD,SAAQ;AAAE,SAAK;EAAU;AAAA,EACzB,SAAQ;AAAA,IACP,aAAa,GAAE;AAAE,YAAM,IAAE,KAAK,cAAc,OAAO,EAAE,OAAO,KAAK,CAAC;AAAG,WAAK,EAAE,SAAS,MAAI,OAAK,KAAG;AAAI,WAAK;IAAU;AAAA,IACpH,SAAQ;AAAE,WAAK,OAAK,CAAA;AAAI,WAAK,EAAE,OAAK;AAAG,WAAK,WAAS;AAAO,WAAK;IAAY;AAAA,IAC7E,MAAM,WAAU;AACf,UAAI,KAAK,WAAS,KAAK;AAAU;AACjC,WAAK,UAAQ;AACb,UAAG;AACF,cAAM,MAAM,MAAMA,YAAAA,IAAI,8BAA8B,KAAK,CAAC;AAC1D,cAAM,OAAO,MAAM,QAAQ,2BAAK,IAAI,IAAE,IAAI,OAAM,MAAM,QAAQ,GAAG,IAAE,MAAI,CAAA;AACvE,aAAK,OAAO,KAAK,KAAK,OAAO,IAAI;AACjC,YAAI,KAAK,SAAS,KAAK,EAAE;AAAM,eAAK,WAAS;AAC7C,aAAK,EAAE,QAAQ;AAAA,MACf,SAAM,GAAE;AAAEC,sBAAAA,MAAI,UAAU,EAAE,OAAM,OAAO,EAAE,WAAS,MAAM,GAAG,MAAK,OAAQ,CAAA;AAAA,MAAE;AAClE,aAAK,UAAQ;AAAA,MAAM;AAAA,IAC7B;AAAA,EACD;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;AC5CA,GAAG,WAAW,eAAe;"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"version":3,"file":"select.js","sources":["pages/supplier/select.vue","../../../../Downloads/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniPage:/cGFnZXMvc3VwcGxpZXIvc2VsZWN0LnZ1ZQ"],"sourcesContent":["<template>\r\n\t<view class=\"page\">\r\n\t\t<view class=\"search\">\r\n\t\t\t<input v-model=\"kw\" placeholder=\"搜索供应商名称/电话\" @confirm=\"search\" />\r\n\t\t\t<button size=\"mini\" @click=\"search\">搜索</button>\r\n\t\t\t<button size=\"mini\" :type=\"debtOnly ? 'primary' : 'default'\" @click=\"toggleDebtOnly\">只看欠款</button>\r\n\t\t</view>\r\n\t\t<scroll-view scroll-y class=\"list\">\r\n\t\t\t<view class=\"item\" v-for=\"s in suppliers\" :key=\"s.id\" @click=\"select(s)\">\r\n\t\t\t\t<view class=\"name\">{{ s.name }}</view>\r\n\t\t\t\t<view class=\"meta\">\r\n\t\t\t\t\t{{ s.mobile || '—' }}\r\n\t\t\t\t\t<text v-if=\"typeof s.apPayable === 'number'\">|应付:¥ {{ Number(s.apPayable).toFixed(2) }}</text>\r\n\t\t\t\t</view>\r\n\t\t\t</view>\r\n\t\t</scroll-view>\r\n\t\t<view class=\"bottom\">\r\n\t\t\t<button class=\"primary\" @click=\"createSupplier\">新增供应商</button>\r\n\t\t</view>\r\n\t</view>\r\n</template>\r\n\r\n<script>\r\n\timport { get } from '../../common/http.js'\r\n\texport default {\r\n data() { return { kw: '', debtOnly: false, suppliers: [] } },\r\n\t\tonLoad() { this.search() },\r\n\t\tmethods: {\r\n\t\t\ttoggleDebtOnly() { this.debtOnly = !this.debtOnly; this.search() },\r\n\t\t\tasync search() {\r\n\t\t\t\ttry {\r\n\t\t\t\t\tconst res = await get('/api/suppliers', { kw: this.kw, debtOnly: this.debtOnly, page: 1, size: 50 })\r\n\t\t\t\t\tthis.suppliers = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])\r\n\t\t\t\t} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }\r\n\t\t\t},\r\n\t\t\tcreateSupplier() { uni.navigateTo({ url: '/pages/supplier/form' }) },\r\n\t\t\tselect(s) {\r\n\t\t\t\tconst opener = getCurrentPages()[getCurrentPages().length-2]\r\n\t\t\t\tif (opener && opener.$vm) {\r\n\t\t\t\t\topener.$vm.order.supplierId = s.id\r\n\t\t\t\t\topener.$vm.supplierName = s.name\r\n\t\t\t\t}\r\n\t\t\t\tuni.navigateBack()\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n</script>\r\n\r\n<style>\r\n\t.page { display:flex; flex-direction: column; height: 100vh; }\r\n\t.search { display:flex; gap: 12rpx; padding: 16rpx; background:#fff; }\r\n\t.search input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx; }\r\n\t.list { flex:1; }\r\n\t.item { padding: 20rpx 24rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }\r\n\t.name { color:#333; margin-bottom: 6rpx; }\r\n\t.meta { color:#888; font-size: 24rpx; }\r\n</style>\r\n\r\n\r\n\r\n","import MiniProgramPage from 'C:/Users/21826/Desktop/Wj/PartsInquiry/frontend/pages/supplier/select.vue'\nwx.createPage(MiniProgramPage)"],"names":["get","uni"],"mappings":";;;AAwBC,MAAK,YAAU;AAAA,EACZ,OAAO;AAAE,WAAO,EAAE,IAAI,IAAI,UAAU,OAAO,WAAW,CAAA;EAAM;AAAA,EAC9D,SAAS;AAAE,SAAK;EAAU;AAAA,EAC1B,SAAS;AAAA,IACR,iBAAiB;AAAE,WAAK,WAAW,CAAC,KAAK;AAAU,WAAK;IAAU;AAAA,IAClE,MAAM,SAAS;AACd,UAAI;AACH,cAAM,MAAM,MAAMA,YAAG,IAAC,kBAAkB,EAAE,IAAI,KAAK,IAAI,UAAU,KAAK,UAAU,MAAM,GAAG,MAAM,IAAI;AACnG,aAAK,YAAY,MAAM,QAAQ,2BAAK,IAAI,IAAI,IAAI,OAAQ,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAA;AAAA,eAC5E,GAAG;AAAEC,sBAAAA,MAAI,UAAU,EAAE,OAAO,QAAQ,MAAM,OAAO,CAAC;AAAA,MAAE;AAAA,IAC5D;AAAA,IACD,iBAAiB;AAAEA,oBAAAA,MAAI,WAAW,EAAE,KAAK,uBAAqB,CAAG;AAAA,IAAG;AAAA,IACpE,OAAO,GAAG;AACT,YAAM,SAAS,gBAAiB,EAAC,gBAAe,EAAG,SAAO,CAAC;AAC3D,UAAI,UAAU,OAAO,KAAK;AACzB,eAAO,IAAI,MAAM,aAAa,EAAE;AAChC,eAAO,IAAI,eAAe,EAAE;AAAA,MAC7B;AACAA,oBAAAA,MAAI,aAAa;AAAA,IAClB;AAAA,EACD;AACD;;;;;;;;;;;;;;;;;;;;;;;;;AC5CD,GAAG,WAAW,eAAe;"}
{"version":3,"file":"select.js","sources":["pages/supplier/select.vue","../../../../Downloads/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniPage:/cGFnZXMvc3VwcGxpZXIvc2VsZWN0LnZ1ZQ"],"sourcesContent":["<template>\r\n\t<view class=\"page\">\r\n\t\t<view class=\"search\">\r\n\t\t\t<input v-model=\"kw\" placeholder=\"搜索供应商名称/电话\" @confirm=\"search\" />\r\n\t\t\t<button size=\"mini\" @click=\"search\">搜索</button>\r\n\t\t\t<button size=\"mini\" :type=\"debtOnly ? 'primary' : 'default'\" @click=\"toggleDebtOnly\">只看欠款</button>\r\n\t\t</view>\r\n\t\t<scroll-view scroll-y class=\"list\">\r\n\t\t\t<view class=\"item\" v-for=\"s in suppliers\" :key=\"s.id\" @click=\"select(s)\">\r\n\t\t\t\t<view class=\"name\">{{ s.name }}</view>\r\n\t\t\t\t<view class=\"meta\">\r\n\t\t\t\t\t{{ s.mobile || '—' }}\r\n\t\t\t\t\t<text v-if=\"typeof s.apPayable === 'number'\">|应付:¥ {{ Number(s.apPayable).toFixed(2) }}</text>\r\n\t\t\t\t</view>\r\n\t\t\t</view>\r\n\t\t</scroll-view>\r\n\t\t<view class=\"bottom\">\r\n\t\t\t<button class=\"primary\" @click=\"createSupplier\">新增供应商</button>\r\n\t\t</view>\r\n\t</view>\r\n</template>\r\n\r\n<script>\r\n\timport { get } from '../../common/http.js'\r\n\texport default {\r\n data() { return { kw: '', debtOnly: false, suppliers: [] } },\r\n\t\tonLoad() { this.search() },\r\n\t\tmethods: {\r\n\t\t\ttoggleDebtOnly() { this.debtOnly = !this.debtOnly; this.search() },\r\n\t\t\tasync search() {\r\n\t\t\t\ttry {\r\n\t\t\t\t\tconst res = await get('/api/suppliers', { kw: this.kw, debtOnly: this.debtOnly, page: 1, size: 50 })\r\n\t\t\t\t\tthis.suppliers = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])\r\n\t\t\t\t} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }\r\n\t\t\t},\r\n\t\t\tcreateSupplier() { uni.navigateTo({ url: '/pages/supplier/form' }) },\r\n\t\t\tselect(s) {\r\n\t\t\t\ttry {\r\n\t\t\t\t\tconst pages = getCurrentPages()\r\n\t\t\t\t\tconst opener = pages && pages.length >= 2 ? pages[pages.length - 2] : null\r\n\t\t\t\t\tconst vm = opener && opener.$vm ? opener.$vm : null\r\n\t\t\t\t\tconst canPick = !!(vm && vm.order)\r\n\t\t\t\t\tif (canPick) {\r\n\t\t\t\t\t\tvm.order.supplierId = s.id\r\n\t\t\t\t\t\tif (Object.prototype.hasOwnProperty.call(vm, 'supplierName')) vm.supplierName = s.name\r\n\t\t\t\t\t\tuni.navigateBack()\r\n\t\t\t\t\t} else {\r\n\t\t\t\t\t\tuni.navigateTo({ url: `/pages/supplier/form?id=${s.id}` })\r\n\t\t\t\t\t}\r\n\t\t\t\t} catch (_) {\r\n\t\t\t\t\tuni.navigateTo({ url: `/pages/supplier/form?id=${s.id}` })\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n</script>\r\n\r\n<style>\r\n\t.page { display:flex; flex-direction: column; height: 100vh; }\r\n\t.search { display:flex; gap: 12rpx; padding: 16rpx; background:#fff; }\r\n\t.search input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx; }\r\n\t.list { flex:1; }\r\n\t.item { padding: 20rpx 24rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }\r\n\t.name { color:#333; margin-bottom: 6rpx; }\r\n\t.meta { color:#888; font-size: 24rpx; }\r\n</style>\r\n\r\n\r\n\r\n","import MiniProgramPage from 'C:/Users/21826/Desktop/Wj/PartsInquiry/frontend/pages/supplier/select.vue'\nwx.createPage(MiniProgramPage)"],"names":["get","uni"],"mappings":";;;AAwBC,MAAK,YAAU;AAAA,EACZ,OAAO;AAAE,WAAO,EAAE,IAAI,IAAI,UAAU,OAAO,WAAW,CAAA;EAAM;AAAA,EAC9D,SAAS;AAAE,SAAK;EAAU;AAAA,EAC1B,SAAS;AAAA,IACR,iBAAiB;AAAE,WAAK,WAAW,CAAC,KAAK;AAAU,WAAK;IAAU;AAAA,IAClE,MAAM,SAAS;AACd,UAAI;AACH,cAAM,MAAM,MAAMA,YAAG,IAAC,kBAAkB,EAAE,IAAI,KAAK,IAAI,UAAU,KAAK,UAAU,MAAM,GAAG,MAAM,IAAI;AACnG,aAAK,YAAY,MAAM,QAAQ,2BAAK,IAAI,IAAI,IAAI,OAAQ,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAA;AAAA,eAC5E,GAAG;AAAEC,sBAAAA,MAAI,UAAU,EAAE,OAAO,QAAQ,MAAM,OAAO,CAAC;AAAA,MAAE;AAAA,IAC5D;AAAA,IACD,iBAAiB;AAAEA,oBAAAA,MAAI,WAAW,EAAE,KAAK,uBAAqB,CAAG;AAAA,IAAG;AAAA,IACpE,OAAO,GAAG;AACT,UAAI;AACH,cAAM,QAAQ,gBAAgB;AAC9B,cAAM,SAAS,SAAS,MAAM,UAAU,IAAI,MAAM,MAAM,SAAS,CAAC,IAAI;AACtE,cAAM,KAAK,UAAU,OAAO,MAAM,OAAO,MAAM;AAC/C,cAAM,UAAU,CAAC,EAAE,MAAM,GAAG;AAC5B,YAAI,SAAS;AACZ,aAAG,MAAM,aAAa,EAAE;AACxB,cAAI,OAAO,UAAU,eAAe,KAAK,IAAI,cAAc;AAAG,eAAG,eAAe,EAAE;AAClFA,wBAAAA,MAAI,aAAa;AAAA,eACX;AACNA,8BAAI,WAAW,EAAE,KAAK,2BAA2B,EAAE,EAAE,IAAI;AAAA,QAC1D;AAAA,MACD,SAAS,GAAG;AACXA,4BAAI,WAAW,EAAE,KAAK,2BAA2B,EAAE,EAAE,IAAI;AAAA,MAC1D;AAAA,IACD;AAAA,EACD;AACD;;;;;;;;;;;;;;;;;;;;;;;;;ACrDD,GAAG,WAAW,eAAe;"}

View File

@@ -6,6 +6,8 @@ if (!Math) {
"./pages/order/create.js";
"./pages/product/select.js";
"./pages/product/list.js";
"./pages/product/submit.js";
"./pages/product/submissions.js";
"./pages/product/form.js";
"./pages/product/categories.js";
"./pages/product/units.js";
@@ -23,7 +25,9 @@ if (!Math) {
"./pages/auth/register.js";
"./pages/my/index.js";
"./pages/my/about.js";
"./pages/my/security.js";
"./pages/my/vip.js";
"./pages/my/orders.js";
"./pages/report/index.js";
}
const _sfc_main = {

View File

@@ -4,6 +4,8 @@
"pages/order/create",
"pages/product/select",
"pages/product/list",
"pages/product/submit",
"pages/product/submissions",
"pages/product/form",
"pages/product/categories",
"pages/product/units",
@@ -21,7 +23,9 @@
"pages/auth/register",
"pages/my/index",
"pages/my/about",
"pages/my/security",
"pages/my/vip",
"pages/my/orders",
"pages/report/index"
],
"window": {

View File

@@ -1,6 +1,12 @@
"use strict";
const _imports_0$1 = "/static/icons/icons8-shopping-cart-100.png";
const _imports_0 = "/static/logo.png";
exports._imports_0 = _imports_0$1;
exports._imports_0$1 = _imports_0;
const _imports_0$4 = "/static/icons/icons8-shopping-cart-100.png";
const _imports_0$3 = "/static/icons/icons8-login-50.png";
const _imports_0$2 = "/static/logo.png";
const _imports_0$1 = "/static/icons/icons8-vip-48 (1).png";
const _imports_0 = "/static/icons/icons8-close-48.png";
exports._imports_0 = _imports_0$4;
exports._imports_0$1 = _imports_0$3;
exports._imports_0$2 = _imports_0$2;
exports._imports_0$3 = _imports_0$1;
exports._imports_0$4 = _imports_0;
//# sourceMappingURL=../../.sourcemap/mp-weixin/common/assets.js.map

View File

@@ -6,26 +6,29 @@ const fallbackBaseUrl = "http://127.0.0.1:8080";
const API_BASE_URL = (envBaseUrl || storageBaseUrl || fallbackBaseUrl).replace(/\/$/, "");
const candidateBases = [envBaseUrl, storageBaseUrl, fallbackBaseUrl, "http://127.0.0.1:8080", "http://localhost:8080"];
const API_BASE_URL_CANDIDATES = Array.from(new Set(candidateBases.filter(Boolean))).map((u) => String(u).replace(/\/$/, ""));
typeof process !== "undefined" && process.env && (process.env.VITE_APP_SHOP_ID || process.env.SHOP_ID) || "";
typeof common_vendor.index !== "undefined" ? common_vendor.index.getStorageSync("SHOP_ID") || "" : "";
const envShopId = typeof process !== "undefined" && process.env && (process.env.VITE_APP_SHOP_ID || process.env.SHOP_ID) || "";
const storageShopId = typeof common_vendor.index !== "undefined" ? common_vendor.index.getStorageSync("SHOP_ID") || "" : "";
const SHOP_ID = Number(envShopId || storageShopId || 1);
const envEnableDefaultUser = typeof process !== "undefined" && process.env && (process.env.VITE_APP_ENABLE_DEFAULT_USER || process.env.ENABLE_DEFAULT_USER) || "";
const storageEnableDefaultUser = typeof common_vendor.index !== "undefined" ? common_vendor.index.getStorageSync("ENABLE_DEFAULT_USER") || "" : "";
String(envEnableDefaultUser || storageEnableDefaultUser || "false").toLowerCase() === "true";
typeof process !== "undefined" && process.env && (process.env.VITE_APP_DEFAULT_USER_ID || process.env.DEFAULT_USER_ID) || "";
typeof common_vendor.index !== "undefined" ? common_vendor.index.getStorageSync("DEFAULT_USER_ID") || "" : "";
const envVipPrice = typeof process !== "undefined" && process.env && (process.env.VITE_APP_VIP_PRICE || process.env.VIP_PRICE) || "";
const storageVipPrice = typeof common_vendor.index !== "undefined" ? common_vendor.index.getStorageSync("VIP_PRICE") || "" : "";
const VIP_PRICE_PER_MONTH = Number(envVipPrice || storageVipPrice || 15);
const ENABLE_DEFAULT_USER = String(envEnableDefaultUser || storageEnableDefaultUser || "false").toLowerCase() === "true";
const envDefaultUserId = typeof process !== "undefined" && process.env && (process.env.VITE_APP_DEFAULT_USER_ID || process.env.DEFAULT_USER_ID) || "";
const storageDefaultUserId = typeof common_vendor.index !== "undefined" ? common_vendor.index.getStorageSync("DEFAULT_USER_ID") || "" : "";
const DEFAULT_USER_ID = Number(envDefaultUserId || storageDefaultUserId || 0);
typeof process !== "undefined" && process.env && (process.env.VITE_APP_VIP_PRICE || process.env.VIP_PRICE) || "";
typeof common_vendor.index !== "undefined" ? common_vendor.index.getStorageSync("VIP_PRICE") || "" : "";
typeof process !== "undefined" && process.env && (process.env.VITE_APP_HOME_BANNER_IMG || process.env.HOME_BANNER_IMG) || "";
typeof common_vendor.index !== "undefined" ? common_vendor.index.getStorageSync("HOME_BANNER_IMG") || "" : "";
const KPI_ICONS = {
todaySales: "/static/icons/sale.png",
monthSales: "/static/icons/report.png",
monthProfit: "/static/icons/report.png",
todaySales: "/static/icons/webwxgetmsgimg.jpg",
monthSales: "/static/icons/webwxgetmsgimg.jpg",
monthProfit: "/static/icons/icons8-profit-50.png",
stockCount: "/static/icons/product.png"
};
exports.API_BASE_URL = API_BASE_URL;
exports.API_BASE_URL_CANDIDATES = API_BASE_URL_CANDIDATES;
exports.DEFAULT_USER_ID = DEFAULT_USER_ID;
exports.ENABLE_DEFAULT_USER = ENABLE_DEFAULT_USER;
exports.KPI_ICONS = KPI_ICONS;
exports.VIP_PRICE_PER_MONTH = VIP_PRICE_PER_MONTH;
exports.SHOP_ID = SHOP_ID;
//# sourceMappingURL=../../.sourcemap/mp-weixin/common/config.js.map

View File

@@ -30,8 +30,19 @@ function buildAuthHeaders(base = {}) {
headers["X-Shop-Id"] = claims.shopId;
if (claims && claims.userId)
headers["X-User-Id"] = claims.userId;
} else if (common_config.ENABLE_DEFAULT_USER && common_config.DEFAULT_USER_ID) {
if (headers["Authorization"])
delete headers["Authorization"];
headers["X-User-Id"] = headers["X-User-Id"] || common_config.DEFAULT_USER_ID;
if (common_config.SHOP_ID)
headers["X-Shop-Id"] = headers["X-Shop-Id"] || common_config.SHOP_ID;
}
} catch (_) {
if (common_config.ENABLE_DEFAULT_USER && common_config.DEFAULT_USER_ID) {
headers["X-User-Id"] = headers["X-User-Id"] || common_config.DEFAULT_USER_ID;
if (common_config.SHOP_ID)
headers["X-Shop-Id"] = headers["X-Shop-Id"] || common_config.SHOP_ID;
}
}
return headers;
}
@@ -71,7 +82,7 @@ function post(path, body = {}) {
const headers = buildAuthHeaders({ "Content-Type": "application/json" });
const options = { url: buildUrl(path), method: "POST", data: body, header: headers };
const p = String(path || "");
if (p.includes("/api/auth/wxmp/login") || p.includes("/api/auth/sms/login") || p.includes("/api/auth/sms/send") || p.includes("/api/auth/password/login") || p.includes("/api/auth/register"))
if (p.includes("/api/auth/wxmp/login") || p.includes("/api/auth/sms/login") || p.includes("/api/auth/sms/send") || p.includes("/api/auth/email/login") || p.includes("/api/auth/email/send") || p.includes("/api/auth/password/login") || p.includes("/api/auth/register"))
options.__noRetry = true;
requestWithFallback(options, common_config.API_BASE_URL_CANDIDATES, 0, resolve, reject);
});
@@ -106,9 +117,22 @@ function uploadWithFallback(options, candidates, idx, resolve, reject) {
return resolve(res.data);
}
}
if (statusCode >= 400 && statusCode < 500) {
try {
const data = typeof res.data === "string" ? JSON.parse(res.data) : res.data;
return resolve(data);
} catch (e) {
return resolve({ success: false, message: "HTTP " + statusCode });
}
}
if (idx + 1 < candidates.length)
return uploadWithFallback(options, candidates, idx + 1, resolve, reject);
reject(new Error("HTTP " + statusCode));
try {
const data = typeof res.data === "string" ? JSON.parse(res.data) : res.data;
return resolve(data);
} catch (e) {
return resolve({ success: false, message: "HTTP " + statusCode });
}
},
fail: (err) => {
if (idx + 1 < candidates.length)

View File

@@ -1,6 +1,8 @@
"use strict";
const common_vendor = require("../common/vendor.js");
const common_http = require("../common/http.js");
const common_config = require("../common/config.js");
const common_assets = require("../common/assets.js");
const ITEM_SIZE = 210;
const GAP = 18;
const COLS = 3;
@@ -25,6 +27,14 @@ const _sfc_main = {
areaHeight() {
const rows = Math.ceil((this.innerList.length + 1) / COLS) || 1;
return rows * ITEM_SIZE + (rows - 1) * GAP;
},
adderStyle() {
const index = this.innerList.length;
const row = Math.floor(index / COLS);
const col = index % COLS;
const x = px(col * (ITEM_SIZE + GAP));
const y = px(row * (ITEM_SIZE + GAP));
return { left: x + "rpx", top: y + "rpx" };
}
},
watch: {
@@ -33,7 +43,7 @@ const _sfc_main = {
handler(list) {
const mapped = (list || []).map((u, i) => ({
uid: String(i) + "_" + (u.id || u.url || Math.random().toString(36).slice(2)),
url: typeof u === "string" ? u : u.url || "",
url: this.ensureAbsoluteUrl(typeof u === "string" ? u : u.url || ""),
x: this.posOf(i).x,
y: this.posOf(i).y
}));
@@ -42,6 +52,14 @@ const _sfc_main = {
}
},
methods: {
ensureAbsoluteUrl(u) {
if (!u)
return "";
const s = String(u);
if (s.startsWith("http://") || s.startsWith("https://"))
return s;
return common_config.API_BASE_URL + (s.startsWith("/") ? s : "/" + s);
},
posOf(index) {
const row = Math.floor(index / COLS);
const col = index % COLS;
@@ -75,7 +93,7 @@ const _sfc_main = {
var _a;
try {
const resp = await common_http.upload(this.uploadPath, filePath, this.formData, this.uploadFieldName);
const url = (resp == null ? void 0 : resp.url) || ((_a = resp == null ? void 0 : resp.data) == null ? void 0 : _a.url) || (resp == null ? void 0 : resp.path) || "";
const url = this.ensureAbsoluteUrl((resp == null ? void 0 : resp.url) || ((_a = resp == null ? void 0 : resp.data) == null ? void 0 : _a.url) || (resp == null ? void 0 : resp.path) || "");
if (!url)
throw new Error("上传响应无 url");
this.innerList.push({ uid: Math.random().toString(36).slice(2), url, ...this.posOf(this.innerList.length) });
@@ -131,12 +149,14 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
i: common_vendor.o(($event) => $options.onMoveEnd(index), img.uid)
};
}),
b: $data.innerList.length < $props.max
b: common_assets._imports_0$4,
c: $data.innerList.length < $props.max
}, $data.innerList.length < $props.max ? {
c: common_vendor.o((...args) => $options.choose && $options.choose(...args))
d: common_vendor.s($options.adderStyle),
e: common_vendor.o((...args) => $options.choose && $options.choose(...args))
} : {}, {
d: $options.areaHeight + "rpx",
e: $options.areaHeight + "rpx"
f: $options.areaHeight + "rpx",
g: $options.areaHeight + "rpx"
});
}
const Component = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);

View File

@@ -1 +1 @@
<view class="uploader"><view class="grid" style="{{'height:' + e}}"><movable-area class="area" style="{{'height:' + d}}"><movable-view wx:for="{{a}}" wx:for-item="img" wx:key="d" class="cell" style="{{img.e}}" direction="{{'all'}}" damping="{{40}}" friction="{{2}}" x="{{img.f}}" y="{{img.g}}" bindchange="{{img.h}}" bindtouchend="{{img.i}}"><image src="{{img.a}}" mode="aspectFill" class="thumb" bindtap="{{img.b}}"/><view class="remove" catchtap="{{img.c}}">×</view></movable-view><view wx:if="{{b}}" class="adder" bindtap="{{c}}"><text></text></view></movable-area></view></view>
<view class="uploader"><view class="grid" style="{{'height:' + g}}"><movable-area class="area" style="{{'height:' + f}}"><movable-view wx:for="{{a}}" wx:for-item="img" wx:key="d" class="cell" style="{{img.e}}" direction="{{'all'}}" damping="{{40}}" friction="{{2}}" x="{{img.f}}" y="{{img.g}}" bindchange="{{img.h}}" bindtouchend="{{img.i}}"><image src="{{img.a}}" mode="aspectFill" class="thumb" bindtap="{{img.b}}"/><image class="remove" src="{{b}}" mode="aspectFit" catchtap="{{img.c}}"/></movable-view><view wx:if="{{c}}" class="adder" style="{{d}}" bindtap="{{e}}"><text></text></view></movable-area></view></view>

View File

@@ -9,7 +9,7 @@
}
.thumb { width: 100%; height: 100%;
}
.remove { position: absolute; right: 6rpx; top: 6rpx; background: rgba(0,0,0,0.45); color: #fff; width: 40rpx; height: 40rpx; text-align: center; line-height: 40rpx; border-radius: 20rpx; font-size: 28rpx;
.remove { position: absolute; right: 6rpx; top: 6rpx; width: 42rpx; height: 42rpx;
}
.adder { width: 210rpx; height: 210rpx; border: 2rpx dashed #ccc; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; color: #999; position: absolute; left: 0; top: 0;
}

View File

@@ -4,92 +4,215 @@ const common_http = require("../../common/http.js");
const _sfc_main = {
data() {
return {
form: { phone: "", password: "" },
phoneFocused: false,
passwordFocused: false
loading: false,
tab: "login",
loginForm: { email: "", password: "" },
regForm: { name: "", email: "", code: "", password: "", password2: "" },
resetForm: { email: "", code: "", password: "", password2: "" },
regCountdown: 0,
resetCountdown: 0,
_timers: []
};
},
beforeUnmount() {
this._timers.forEach((t) => clearInterval(t));
},
methods: {
validate() {
const p = String(this.form.phone || "").trim();
const okPhone = /^1[3-9]\d{9}$/.test(p);
if (!okPhone) {
common_vendor.index.showToast({ title: "请输入正确的手机号", icon: "none" });
return false;
toast(msg) {
try {
common_vendor.index.showToast({ title: String(msg || "操作失败"), icon: "none" });
} catch (_) {
}
return true;
},
validateEmail(v) {
return /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/.test(String(v || "").trim());
},
startCountdown(key) {
if (this[key] > 0)
return;
this[key] = 60;
const timer = setInterval(() => {
this[key] = Math.max(0, this[key] - 1);
if (this[key] === 0)
clearInterval(timer);
}, 1e3);
this._timers.push(timer);
},
async onLogin() {
if (!this.validate())
return;
const { email, password } = this.loginForm;
if (!this.validateEmail(email))
return this.toast("请输入正确邮箱");
if (!password || password.length < 6)
return this.toast("请输入至少6位密码");
this.loading = true;
try {
const phone = String(this.form.phone || "").trim();
const password = String(this.form.password || "");
const res = await common_http.post("/api/auth/password/login", { phone, password });
if (res && res.token) {
common_vendor.index.setStorageSync("TOKEN", res.token);
if (res.user && res.user.phone)
common_vendor.index.setStorageSync("USER_MOBILE", res.user.phone);
common_vendor.index.showToast({ title: "登录成功", icon: "none" });
setTimeout(() => {
common_vendor.index.reLaunch({ url: "/pages/index/index" });
}, 200);
}
const data = await common_http.post("/api/auth/password/login", { email, password });
this.afterLogin(data);
} catch (e) {
common_vendor.index.showToast({ title: e && e.message || "登录失败", icon: "none" });
this.toast(e.message);
} finally {
this.loading = false;
}
},
onGoRegister() {
common_vendor.index.navigateTo({
url: "/pages/auth/register"
});
afterLogin(data) {
try {
if (data && data.token) {
common_vendor.index.setStorageSync("TOKEN", data.token);
if (data.user && data.user.shopId)
common_vendor.index.setStorageSync("SHOP_ID", data.user.shopId);
common_vendor.index.setStorageSync("ENABLE_DEFAULT_USER", "false");
common_vendor.index.removeStorageSync("DEFAULT_USER_ID");
this.toast("登录成功");
setTimeout(() => {
common_vendor.index.reLaunch({ url: "/pages/index/index" });
}, 300);
} else {
this.toast("登录失败");
}
} catch (_) {
this.toast("登录失败");
}
},
async sendRegCode() {
if (!this.validateEmail(this.regForm.email))
return this.toast("请输入正确邮箱");
this.loading = true;
try {
const r = await common_http.post("/api/auth/email/send", { email: this.regForm.email, scene: "register" });
if (r && r.ok)
this.startCountdown("regCountdown");
this.toast(r && r.ok ? "验证码已发送" : "发送过于频繁");
} catch (e) {
this.toast(e.message);
} finally {
this.loading = false;
}
},
async onRegister() {
const f = this.regForm;
if (!f.name || f.name.trim().length < 1)
return this.toast("请输入用户名");
if (!this.validateEmail(f.email))
return this.toast("请输入正确邮箱");
if (!f.code)
return this.toast("请输入验证码");
if (!f.password || f.password.length < 6)
return this.toast("密码至少6位");
if (f.password !== f.password2)
return this.toast("两次密码不一致");
this.loading = true;
try {
const data = await common_http.post("/api/auth/email/register", { name: f.name.trim(), email: f.email.trim(), code: f.code.trim(), password: f.password });
this.afterLogin(data);
} catch (e) {
this.toast(e.message);
} finally {
this.loading = false;
}
},
async sendResetCode() {
if (!this.validateEmail(this.resetForm.email))
return this.toast("请输入正确邮箱");
this.loading = true;
try {
const r = await common_http.post("/api/auth/email/send", { email: this.resetForm.email, scene: "reset" });
if (r && r.ok)
this.startCountdown("resetCountdown");
this.toast(r && r.ok ? "验证码已发送" : "发送过于频繁");
} catch (e) {
this.toast(e.message);
} finally {
this.loading = false;
}
},
async onReset() {
const f = this.resetForm;
if (!this.validateEmail(f.email))
return this.toast("请输入正确邮箱");
if (!f.code)
return this.toast("请输入验证码");
if (!f.password || f.password.length < 6)
return this.toast("新密码至少6位");
if (f.password !== f.password2)
return this.toast("两次密码不一致");
this.loading = true;
try {
const r = await common_http.post("/api/auth/email/reset-password", { email: f.email.trim(), code: f.code.trim(), newPassword: f.password, confirmPassword: f.password2 });
if (r && r.ok) {
this.toast("已重置,请使用新密码登录");
this.tab = "login";
this.loginForm.email = f.email;
} else
this.toast("重置失败");
} catch (e) {
this.toast(e.message);
} finally {
this.loading = false;
}
}
}
};
if (!Array) {
const _component_path = common_vendor.resolveComponent("path");
const _component_svg = common_vendor.resolveComponent("svg");
(_component_path + _component_svg)();
}
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return {
a: common_vendor.p({
d: "M12 2C13.1 2 14 2.9 14 4C14 5.1 13.1 6 12 6C10.9 6 10 5.1 10 4C10 2.9 10.9 2 12 2ZM21 9V7L15 4V6C15 7.66 13.66 9 12 9S9 7.66 9 6V4L3 7V9C3 10.1 3.9 11 5 11V17C5 18.1 5.9 19 7 19H9C9 20.1 9.9 21 11 21H13C14.1 21 15 20.1 15 19H17C18.1 19 19 18.1 19 17V11C20.1 11 21 10.1 21 9Z"
}),
b: common_vendor.p({
viewBox: "0 0 24 24"
}),
c: common_vendor.p({
d: "M6.62,10.79C8.06,13.62 10.38,15.94 13.21,17.38L15.41,15.18C15.69,14.9 16.08,14.82 16.43,14.93C17.55,15.3 18.75,15.5 20,15.5A1,1 0 0,1 21,16.5V20A1,1 0 0,1 20,21A17,17 0 0,1 3,4A1,1 0 0,1 4,3H7.5A1,1 0 0,1 8.5,4C8.5,5.25 8.7,6.45 9.07,7.57C9.18,7.92 9.1,8.31 8.82,8.59L6.62,10.79Z"
}),
d: common_vendor.p({
viewBox: "0 0 24 24"
}),
e: common_vendor.o(($event) => $data.phoneFocused = true),
f: common_vendor.o(($event) => $data.phoneFocused = false),
g: $data.form.phone,
h: common_vendor.o(common_vendor.m(($event) => $data.form.phone = $event.detail.value, {
return common_vendor.e({
a: common_vendor.n($data.tab === "login" ? "active" : ""),
b: common_vendor.o(($event) => $data.tab = "login"),
c: common_vendor.n($data.tab === "register" ? "active" : ""),
d: common_vendor.o(($event) => $data.tab = "register"),
e: common_vendor.n($data.tab === "reset" ? "active" : ""),
f: common_vendor.o(($event) => $data.tab = "reset"),
g: $data.tab === "login"
}, $data.tab === "login" ? {
h: $data.loginForm.email,
i: common_vendor.o(common_vendor.m(($event) => $data.loginForm.email = $event.detail.value, {
trim: true
})),
i: $data.phoneFocused ? 1 : "",
j: $data.form.phone ? 1 : "",
k: common_vendor.p({
d: "M12,17A2,2 0 0,0 14,15C14,13.89 13.1,13 12,13A2,2 0 0,0 10,15A2,2 0 0,0 12,17M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V10C4,8.89 4.9,8 6,8H7V6A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,3A3,3 0 0,0 9,6V8H15V6A3,3 0 0,0 12,3Z"
}),
l: common_vendor.p({
viewBox: "0 0 24 24"
}),
m: common_vendor.o(($event) => $data.passwordFocused = true),
n: common_vendor.o(($event) => $data.passwordFocused = false),
o: $data.form.password,
p: common_vendor.o(common_vendor.m(($event) => $data.form.password = $event.detail.value, {
j: $data.loginForm.password,
k: common_vendor.o(($event) => $data.loginForm.password = $event.detail.value),
l: $data.loading,
m: common_vendor.o((...args) => $options.onLogin && $options.onLogin(...args))
} : $data.tab === "register" ? {
o: $data.regForm.name,
p: common_vendor.o(common_vendor.m(($event) => $data.regForm.name = $event.detail.value, {
trim: true
})),
q: $data.passwordFocused ? 1 : "",
r: $data.form.password ? 1 : "",
s: common_vendor.o((...args) => $options.onLogin && $options.onLogin(...args)),
t: common_vendor.o((...args) => $options.onGoRegister && $options.onGoRegister(...args))
};
q: $data.regForm.email,
r: common_vendor.o(common_vendor.m(($event) => $data.regForm.email = $event.detail.value, {
trim: true
})),
s: $data.regForm.code,
t: common_vendor.o(common_vendor.m(($event) => $data.regForm.code = $event.detail.value, {
trim: true
})),
v: common_vendor.t($data.regCountdown > 0 ? $data.regCountdown + "s" : "获取验证码"),
w: $data.regCountdown > 0 || $data.loading,
x: common_vendor.o((...args) => $options.sendRegCode && $options.sendRegCode(...args)),
y: $data.regForm.password,
z: common_vendor.o(($event) => $data.regForm.password = $event.detail.value),
A: $data.regForm.password2,
B: common_vendor.o(($event) => $data.regForm.password2 = $event.detail.value),
C: $data.loading,
D: common_vendor.o((...args) => $options.onRegister && $options.onRegister(...args))
} : {
E: $data.resetForm.email,
F: common_vendor.o(common_vendor.m(($event) => $data.resetForm.email = $event.detail.value, {
trim: true
})),
G: $data.resetForm.code,
H: common_vendor.o(common_vendor.m(($event) => $data.resetForm.code = $event.detail.value, {
trim: true
})),
I: common_vendor.t($data.resetCountdown > 0 ? $data.resetCountdown + "s" : "获取验证码"),
J: $data.resetCountdown > 0 || $data.loading,
K: common_vendor.o((...args) => $options.sendResetCode && $options.sendResetCode(...args)),
L: $data.resetForm.password,
M: common_vendor.o(($event) => $data.resetForm.password = $event.detail.value),
N: $data.resetForm.password2,
O: common_vendor.o(($event) => $data.resetForm.password2 = $event.detail.value),
P: $data.loading,
Q: common_vendor.o((...args) => $options.onReset && $options.onReset(...args))
}, {
n: $data.tab === "register"
});
}
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);
wx.createPage(MiniProgramPage);

View File

@@ -1 +1 @@
<view class="login-container"><view class="background-decoration"><view class="circle circle-1"></view><view class="circle circle-2"></view><view class="circle circle-3"></view></view><view class="login-card"><view class="header-section"><view class="logo-container"><view class="logo-icon"><svg wx:if="{{b}}" u-s="{{['d']}}" class="icon" u-i="5ce84e30-0" bind:__l="__l" u-p="{{b}}"><path wx:if="{{a}}" u-i="5ce84e30-1,5ce84e30-0" bind:__l="__l" u-p="{{a}}"/></svg></view><text class="app-name">配件询价</text></view><text class="welcome-text">欢迎回来</text><text class="subtitle">请登录您的账户</text></view><view class="form-section"><view class="input-group"><view class="{{['input-container', i && 'focused', j && 'filled']}}"><view class="input-icon"><svg wx:if="{{d}}" u-s="{{['d']}}" class="icon" u-i="5ce84e30-2" bind:__l="__l" u-p="{{d}}"><path wx:if="{{c}}" u-i="5ce84e30-3,5ce84e30-2" bind:__l="__l" u-p="{{c}}"/></svg></view><input class="input-field" type="number" placeholder="输入手机号" maxlength="11" bindfocus="{{e}}" bindblur="{{f}}" value="{{g}}" bindinput="{{h}}"/></view></view><view class="input-group"><view class="{{['input-container', q && 'focused', r && 'filled']}}"><view class="input-icon"><svg wx:if="{{l}}" u-s="{{['d']}}" class="icon" u-i="5ce84e30-4" bind:__l="__l" u-p="{{l}}"><path wx:if="{{k}}" u-i="5ce84e30-5,5ce84e30-4" bind:__l="__l" u-p="{{k}}"/></svg></view><input class="input-field" password placeholder="请输入密码" bindfocus="{{m}}" bindblur="{{n}}" value="{{o}}" bindinput="{{p}}"/></view></view></view><view class="actions-section"><button class="login-button" bindtap="{{s}}"><text class="button-text">登录</text></button><button class="register-button" bindtap="{{t}}"><text class="button-text">注册新账户</text></button></view><view class="footer-section"></view></view></view>
<view class="auth-page"><view class="tabs"><view class="{{['tab', a]}}" bindtap="{{b}}">登录</view><view class="{{['tab', c]}}" bindtap="{{d}}">注册</view><view class="{{['tab', e]}}" bindtap="{{f}}">忘记密码</view></view><view wx:if="{{g}}" class="panel"><input class="input" type="text" placeholder="输入邮箱" value="{{h}}" bindinput="{{i}}"/><input class="input" type="password" placeholder="输入密码" value="{{j}}" bindinput="{{k}}"/><button class="btn primary" disabled="{{l}}" bindtap="{{m}}">登录</button></view><view wx:elif="{{n}}" class="panel"><input class="input" type="text" placeholder="输入用户名" value="{{o}}" bindinput="{{p}}"/><input class="input" type="text" placeholder="输入邮箱" value="{{q}}" bindinput="{{r}}"/><view class="row"><input class="input flex1" type="text" placeholder="邮箱验证码" value="{{s}}" bindinput="{{t}}"/><button class="btn ghost" disabled="{{w}}" bindtap="{{x}}">{{v}}</button></view><input class="input" type="password" placeholder="输入密码(≥6位)" value="{{y}}" bindinput="{{z}}"/><input class="input" type="password" placeholder="再次输入密码" value="{{A}}" bindinput="{{B}}"/><button class="btn primary" disabled="{{C}}" bindtap="{{D}}">注册新用户</button></view><view wx:else class="panel"><input class="input" type="text" placeholder="输入邮箱" value="{{E}}" bindinput="{{F}}"/><view class="row"><input class="input flex1" type="text" placeholder="邮箱验证码" value="{{G}}" bindinput="{{H}}"/><button class="btn ghost" disabled="{{J}}" bindtap="{{K}}">{{I}}</button></view><input class="input" type="password" placeholder="新密码(≥6位)" value="{{L}}" bindinput="{{M}}"/><input class="input" type="password" placeholder="再次输入新密码" value="{{N}}" bindinput="{{O}}"/><button class="btn primary" disabled="{{P}}" bindtap="{{Q}}">重置密码</button></view></view>

View File

@@ -24,257 +24,62 @@
/* 垂直间距 */
/* 透明度 */
/* 文章场景相关 */
.login-container {
position: relative;
min-height: 100vh;
.auth-page {
padding: 32rpx;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 40rpx 20rpx;
overflow: hidden;
flex-direction: column;
gap: 24rpx;
}
.background-decoration {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 0;
}
.background-decoration .circle {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
}
.background-decoration .circle.circle-1 {
width: 200rpx;
height: 200rpx;
top: 10%;
left: 10%;
animation: float 6s ease-in-out infinite;
}
.background-decoration .circle.circle-2 {
width: 150rpx;
height: 150rpx;
top: 60%;
right: 15%;
animation: float 8s ease-in-out infinite reverse;
}
.background-decoration .circle.circle-3 {
width: 100rpx;
height: 100rpx;
bottom: 20%;
left: 20%;
animation: float 5s ease-in-out infinite;
}
@keyframes float {
0%, 100% {
transform: translateY(0px);
}
50% {
transform: translateY(-20px);
}
}
.login-card {
position: relative;
z-index: 1;
width: 90%;
max-width: 680rpx;
background: rgba(255, 255, 255, 0.95);
-webkit-backdrop-filter: blur(20rpx);
backdrop-filter: blur(20rpx);
border-radius: 32rpx;
padding: 60rpx 40rpx 50rpx;
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.1);
border: 1rpx solid rgba(255, 255, 255, 0.2);
}
.header-section {
text-align: center;
margin-bottom: 50rpx;
}
.header-section .logo-container {
.tabs {
display: flex;
align-items: center;
justify-content: center;
gap: 24rpx;
}
.tab {
padding: 12rpx 20rpx;
border-radius: 999rpx;
background: #f2f4f8;
color: #5b6b80;
font-weight: 700;
}
.tab.active {
background: #2d6be6;
color: #fff;
}
.panel {
display: flex;
flex-direction: column;
gap: 16rpx;
margin-bottom: 20rpx;
}
.header-section .logo-container .logo-icon {
width: 60rpx;
height: 60rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: #fff;
padding: 24rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
border: 2rpx solid #eef2f9;
}
.header-section .logo-container .logo-icon .icon {
width: 36rpx;
height: 36rpx;
fill: white;
}
.header-section .logo-container .app-name {
font-size: 36rpx;
font-weight: 700;
color: #2d3748;
letter-spacing: 1rpx;
}
.header-section .welcome-text {
display: block;
font-size: 48rpx;
font-weight: 700;
color: #2d3748;
margin-bottom: 8rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.header-section .subtitle {
display: block;
.input {
background: #f7f9ff;
border: 2rpx solid rgba(45, 107, 230, 0.12);
border-radius: 12rpx;
padding: 22rpx 20rpx;
font-size: 28rpx;
color: #718096;
font-weight: 400;
}
.form-section {
margin-bottom: 40rpx;
}
.form-section .input-group {
margin-bottom: 28rpx;
}
.form-section .input-group .input-container {
position: relative;
background: #f7fafc;
border: 2rpx solid #e2e8f0;
border-radius: 16rpx;
.row {
display: flex;
gap: 12rpx;
align-items: center;
transition: all 0.3s ease;
}
.form-section .input-group .input-container.focused {
border-color: #667eea;
background: #ffffff;
box-shadow: 0 0 0 6rpx rgba(102, 126, 234, 0.1);
transform: translateY(-2rpx);
}
.form-section .input-group .input-container.filled {
background: #ffffff;
border-color: #cbd5e0;
}
.form-section .input-group .input-container .input-icon {
display: flex;
align-items: center;
justify-content: center;
width: 50rpx;
margin-left: 20rpx;
}
.form-section .input-group .input-container .input-icon .icon {
width: 32rpx;
height: 32rpx;
fill: #a0aec0;
transition: fill 0.3s ease;
}
.form-section .input-group .input-container.focused .input-icon .icon {
fill: #667eea;
}
.form-section .input-group .input-container .input-field {
.flex1 {
flex: 1;
background: transparent;
border: none;
padding: 24rpx 20rpx 24rpx 12rpx;
font-size: 32rpx;
color: #2d3748;
}
.form-section .input-group .input-container .input-field::-webkit-input-placeholder {
color: #a0aec0;
font-size: 28rpx;
}
.form-section .input-group .input-container .input-field::placeholder {
color: #a0aec0;
font-size: 28rpx;
}
.actions-section {
margin-bottom: 30rpx;
}
.actions-section .login-button {
width: 100%;
height: 96rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 16rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.3);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.actions-section .login-button:active {
transform: translateY(2rpx);
box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.3);
}
.actions-section .login-button::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 0%, transparent 50%);
opacity: 0;
transition: opacity 0.3s ease;
}
.actions-section .login-button:active::before {
opacity: 1;
}
.actions-section .login-button .button-text {
font-size: 32rpx;
font-weight: 600;
color: white;
letter-spacing: 1rpx;
}
.actions-section .register-button {
width: 100%;
height: 86rpx;
background: transparent;
border: 2rpx solid #e2e8f0;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.actions-section .register-button:active {
background: #f7fafc;
border-color: #cbd5e0;
transform: translateY(1rpx);
}
.actions-section .register-button .button-text {
font-size: 28rpx;
font-weight: 500;
color: #718096;
}
.footer-section {
.btn {
padding: 22rpx 20rpx;
border-radius: 12rpx;
font-weight: 800;
text-align: center;
}
.footer-section .hint-text {
font-size: 24rpx;
color: #a0aec0;
line-height: 1.5;
background: rgba(160, 174, 192, 0.1);
padding: 16rpx 20rpx;
border-radius: 12rpx;
border: 1rpx solid rgba(160, 174, 192, 0.2);
}
@media (max-width: 750rpx) {
.login-card {
margin: 20rpx;
padding: 50rpx 30rpx 40rpx;
}
.header-section .welcome-text {
font-size: 42rpx;
.btn.primary {
background: linear-gradient(135deg, #4788ff 0%, #2d6be6 100%);
color: #fff;
}
.btn.ghost {
background: #eef3ff;
color: #2d6be6;
}

View File

@@ -7,55 +7,97 @@ const _sfc_main = {
form: {
shopName: "",
name: "",
phone: "",
password: "",
confirmPassword: ""
email: "",
code: "",
password: ""
},
shopNameFocused: false,
nameFocused: false,
phoneFocused: false,
passwordFocused: false,
confirmPasswordFocused: false
emailFocused: false,
codeFocused: false,
pwdFocused: false,
countdown: 0,
timer: null,
sending: false
};
},
computed: {
btnText() {
if (this.countdown > 0)
return `${this.countdown}s`;
if (this.sending)
return "发送中...";
return "获取验证码";
}
},
methods: {
validate() {
const phone = String(this.form.phone || "").trim();
const ok = /^1[3-9]\d{9}$/.test(phone);
const email = String(this.form.email || "").trim();
const ok = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/.test(email);
if (!ok) {
common_vendor.index.showToast({ title: "请输入正确的手机号", icon: "none" });
common_vendor.index.showToast({ title: "请输入正确的邮箱地址", icon: "none" });
return false;
}
if (!this.form.password) {
common_vendor.index.showToast({ title: "请输入密码", icon: "none" });
if (!/^\d{6}$/.test(String(this.form.code || "").trim())) {
common_vendor.index.showToast({ title: "验证码格式不正确", icon: "none" });
return false;
}
if (this.form.password.length < 6) {
if (String(this.form.password || "").length < 6) {
common_vendor.index.showToast({ title: "密码至少6位", icon: "none" });
return false;
}
if (!this.form.confirmPassword) {
common_vendor.index.showToast({ title: "请确认密码", icon: "none" });
return false;
}
if (this.form.password !== this.form.confirmPassword) {
common_vendor.index.showToast({ title: "两次密码不一致", icon: "none" });
return false;
}
return true;
},
startCountdown(sec) {
this.countdown = sec;
if (this.timer)
clearInterval(this.timer);
this.timer = setInterval(() => {
if (this.countdown <= 1) {
clearInterval(this.timer);
this.timer = null;
this.countdown = 0;
return;
}
this.countdown--;
}, 1e3);
},
async sendCode() {
if (this.sending || this.countdown > 0)
return;
const e = String(this.form.email || "").trim();
const ok = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/.test(e);
if (!ok)
return common_vendor.index.showToast({ title: "请输入正确的邮箱地址", icon: "none" });
this.sending = true;
try {
const res = await common_http.post("/api/auth/email/send", { email: e, scene: "login" });
const cd = Number(res && res.cooldownSec || 60);
this.startCountdown(cd);
common_vendor.index.showToast({ title: "验证码已发送", icon: "none" });
} catch (e2) {
const msg = e2 && e2.message || "发送失败";
common_vendor.index.showToast({ title: msg, icon: "none" });
} finally {
this.sending = false;
}
},
async onRegister() {
if (!this.validate())
return;
const phone = String(this.form.phone || "").trim();
const email = String(this.form.email || "").trim();
const name = String(this.form.name || "").trim();
const password = String(this.form.password || "");
try {
const data = await common_http.post("/api/auth/register", { phone, name: name || void 0, password });
const data = await common_http.post("/api/auth/email/register", { email, code: String(this.form.code || "").trim(), name, password: String(this.form.password || "") });
if (data && data.token) {
common_vendor.index.setStorageSync("TOKEN", data.token);
if (data.user && data.user.phone)
common_vendor.index.setStorageSync("USER_MOBILE", data.user.phone);
if (data.user && data.user.email)
common_vendor.index.setStorageSync("USER_EMAIL", data.user.email);
if (name)
try {
common_vendor.index.setStorageSync("USER_NAME", name);
} catch (_) {
}
common_vendor.index.showToast({ title: "注册成功", icon: "none" });
setTimeout(() => {
common_vendor.index.reLaunch({ url: "/pages/index/index" });
@@ -115,49 +157,52 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
q: $data.nameFocused ? 1 : "",
r: $data.form.name ? 1 : "",
s: common_vendor.p({
d: "M6.62,10.79C8.06,13.62 10.38,15.94 13.21,17.38L15.41,15.18C15.69,14.9 16.08,14.82 16.43,14.93C17.55,15.3 18.75,15.5 20,15.5A1,1 0 0,1 21,16.5V20A1,1 0 0,1 20,21A17,17 0 0,1 3,4A1,1 0 0,1 4,3H7.5A1,1 0 0,1 8.5,4C8.5,5.25 8.7,6.45 9.07,7.57C9.18,7.92 9.1,8.31 8.82,8.59L6.62,10.79Z"
d: "M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-1 4l-7 4-7-4V6l7 4 7-4v2z"
}),
t: common_vendor.p({
viewBox: "0 0 24 24"
}),
v: common_vendor.o(($event) => $data.phoneFocused = true),
w: common_vendor.o(($event) => $data.phoneFocused = false),
x: $data.form.phone,
y: common_vendor.o(common_vendor.m(($event) => $data.form.phone = $event.detail.value, {
v: common_vendor.o(($event) => $data.emailFocused = true),
w: common_vendor.o(($event) => $data.emailFocused = false),
x: $data.form.email,
y: common_vendor.o(common_vendor.m(($event) => $data.form.email = $event.detail.value, {
trim: true
})),
z: $data.phoneFocused ? 1 : "",
A: $data.form.phone ? 1 : "",
z: $data.emailFocused ? 1 : "",
A: $data.form.email ? 1 : "",
B: common_vendor.p({
d: "M12,17A2,2 0 0,0 14,15C14,13.89 13.1,13 12,13A2,2 0 0,0 10,15A2,2 0 0,0 12,17M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V10C4,8.89 4.9,8 6,8H7V6A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,3A3,3 0 0,0 9,6V8H15V6A3,3 0 0,0 12,3Z"
d: "M3 10h18v2H3v-2zm0 6h12v2H3v-2zM3 6h18v2H3V6z"
}),
C: common_vendor.p({
viewBox: "0 0 24 24"
}),
D: common_vendor.o(($event) => $data.passwordFocused = true),
E: common_vendor.o(($event) => $data.passwordFocused = false),
F: $data.form.password,
G: common_vendor.o(common_vendor.m(($event) => $data.form.password = $event.detail.value, {
D: common_vendor.o(($event) => $data.codeFocused = true),
E: common_vendor.o(($event) => $data.codeFocused = false),
F: $data.form.code,
G: common_vendor.o(common_vendor.m(($event) => $data.form.code = $event.detail.value, {
trim: true
})),
H: $data.passwordFocused ? 1 : "",
I: $data.form.password ? 1 : "",
H: $data.codeFocused ? 1 : "",
I: $data.form.code ? 1 : "",
J: common_vendor.p({
d: "M12,17A2,2 0 0,0 14,15C14,13.89 13.1,13 12,13A2,2 0 0,0 10,15A2,2 0 0,0 12,17M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V10C4,8.89 4.9,8 6,8H7V6A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,3A3,3 0 0,0 9,6V8H15V6A3,3 0 0,0 12,3Z"
}),
K: common_vendor.p({
viewBox: "0 0 24 24"
}),
L: common_vendor.o(($event) => $data.confirmPasswordFocused = true),
M: common_vendor.o(($event) => $data.confirmPasswordFocused = false),
N: $data.form.confirmPassword,
O: common_vendor.o(common_vendor.m(($event) => $data.form.confirmPassword = $event.detail.value, {
L: common_vendor.o(($event) => $data.pwdFocused = true),
M: common_vendor.o(($event) => $data.pwdFocused = false),
N: $data.form.password,
O: common_vendor.o(common_vendor.m(($event) => $data.form.password = $event.detail.value, {
trim: true
})),
P: $data.confirmPasswordFocused ? 1 : "",
Q: $data.form.confirmPassword ? 1 : "",
R: common_vendor.o((...args) => $options.onRegister && $options.onRegister(...args)),
S: common_vendor.o((...args) => $options.onGoLogin && $options.onGoLogin(...args))
P: $data.pwdFocused ? 1 : "",
Q: $data.form.password ? 1 : "",
R: common_vendor.t($options.btnText),
S: $data.countdown > 0 || $data.sending,
T: common_vendor.o((...args) => $options.sendCode && $options.sendCode(...args)),
U: common_vendor.o((...args) => $options.onRegister && $options.onRegister(...args)),
V: common_vendor.o((...args) => $options.onGoLogin && $options.onGoLogin(...args))
};
}
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);

View File

@@ -1 +1 @@
<view class="register-container"><view class="background-decoration"><view class="circle circle-1"></view><view class="circle circle-2"></view><view class="circle circle-3"></view></view><view class="register-card"><view class="header-section"><view class="logo-container"><view class="logo-icon"><svg wx:if="{{b}}" u-s="{{['d']}}" class="icon" u-i="41009218-0" bind:__l="__l" u-p="{{b}}"><path wx:if="{{a}}" u-i="41009218-1,41009218-0" bind:__l="__l" u-p="{{a}}"/></svg></view><text class="app-name">配件询价</text></view><text class="welcome-text">创建账户</text><text class="subtitle">请填写以下信息完成注册</text></view><view class="form-section"><view class="input-group"><view class="{{['input-container', i && 'focused', j && 'filled']}}"><view class="input-icon"><svg wx:if="{{d}}" u-s="{{['d']}}" class="icon" u-i="41009218-2" bind:__l="__l" u-p="{{d}}"><path wx:if="{{c}}" u-i="41009218-3,41009218-2" bind:__l="__l" u-p="{{c}}"/></svg></view><input class="input-field" type="text" placeholder="请输入店铺名称" bindfocus="{{e}}" bindblur="{{f}}" value="{{g}}" bindinput="{{h}}"/></view></view><view class="input-group"><view class="{{['input-container', q && 'focused', r && 'filled']}}"><view class="input-icon"><svg wx:if="{{l}}" u-s="{{['d']}}" class="icon" u-i="41009218-4" bind:__l="__l" u-p="{{l}}"><path wx:if="{{k}}" u-i="41009218-5,41009218-4" bind:__l="__l" u-p="{{k}}"/></svg></view><input class="input-field" type="text" placeholder="请输入您的姓名" bindfocus="{{m}}" bindblur="{{n}}" value="{{o}}" bindinput="{{p}}"/></view></view><view class="input-group"><view class="{{['input-container', z && 'focused', A && 'filled']}}"><view class="input-icon"><svg wx:if="{{t}}" u-s="{{['d']}}" class="icon" u-i="41009218-6" bind:__l="__l" u-p="{{t}}"><path wx:if="{{s}}" u-i="41009218-7,41009218-6" bind:__l="__l" u-p="{{s}}"/></svg></view><input class="input-field" type="number" placeholder="请输入手机号" maxlength="11" bindfocus="{{v}}" bindblur="{{w}}" value="{{x}}" bindinput="{{y}}"/></view></view><view class="input-group"><view class="{{['input-container', H && 'focused', I && 'filled']}}"><view class="input-icon"><svg wx:if="{{C}}" u-s="{{['d']}}" class="icon" u-i="41009218-8" bind:__l="__l" u-p="{{C}}"><path wx:if="{{B}}" u-i="41009218-9,41009218-8" bind:__l="__l" u-p="{{B}}"/></svg></view><input class="input-field" password placeholder="请输入密码至少6位" bindfocus="{{D}}" bindblur="{{E}}" value="{{F}}" bindinput="{{G}}"/></view></view><view class="input-group"><view class="{{['input-container', P && 'focused', Q && 'filled']}}"><view class="input-icon"><svg wx:if="{{K}}" u-s="{{['d']}}" class="icon" u-i="41009218-10" bind:__l="__l" u-p="{{K}}"><path wx:if="{{J}}" u-i="41009218-11,41009218-10" bind:__l="__l" u-p="{{J}}"/></svg></view><input class="input-field" password placeholder="请再次输入密码" bindfocus="{{L}}" bindblur="{{M}}" value="{{N}}" bindinput="{{O}}"/></view></view></view><view class="actions-section"><button class="register-button" bindtap="{{R}}"><text class="button-text">立即注册</text></button><button class="login-button" bindtap="{{S}}"><text class="button-text">已有账户?去登录</text></button></view><view class="footer-section"><text class="hint-text">注册即表示您同意我们的服务条款和隐私政策</text></view></view></view>
<view class="register-container"><view class="background-decoration"><view class="circle circle-1"></view><view class="circle circle-2"></view><view class="circle circle-3"></view></view><view class="register-card"><view class="header-section"><view class="logo-container"><view class="logo-icon"><svg wx:if="{{b}}" u-s="{{['d']}}" class="icon" u-i="41009218-0" bind:__l="__l" u-p="{{b}}"><path wx:if="{{a}}" u-i="41009218-1,41009218-0" bind:__l="__l" u-p="{{a}}"/></svg></view><text class="app-name">配件询价</text></view><text class="welcome-text">创建账户</text><text class="subtitle">请填写以下信息完成注册</text></view><view class="form-section"><view class="input-group"><view class="{{['input-container', i && 'focused', j && 'filled']}}"><view class="input-icon"><svg wx:if="{{d}}" u-s="{{['d']}}" class="icon" u-i="41009218-2" bind:__l="__l" u-p="{{d}}"><path wx:if="{{c}}" u-i="41009218-3,41009218-2" bind:__l="__l" u-p="{{c}}"/></svg></view><input class="input-field" type="text" placeholder="请输入店铺名称" bindfocus="{{e}}" bindblur="{{f}}" value="{{g}}" bindinput="{{h}}"/></view></view><view class="input-group"><view class="{{['input-container', q && 'focused', r && 'filled']}}"><view class="input-icon"><svg wx:if="{{l}}" u-s="{{['d']}}" class="icon" u-i="41009218-4" bind:__l="__l" u-p="{{l}}"><path wx:if="{{k}}" u-i="41009218-5,41009218-4" bind:__l="__l" u-p="{{k}}"/></svg></view><input class="input-field" type="text" placeholder="请输入您的姓名" bindfocus="{{m}}" bindblur="{{n}}" value="{{o}}" bindinput="{{p}}"/></view></view><view class="input-group"><view class="{{['input-container', z && 'focused', A && 'filled']}}"><view class="input-icon"><svg wx:if="{{t}}" u-s="{{['d']}}" class="icon" u-i="41009218-6" bind:__l="__l" u-p="{{t}}"><path wx:if="{{s}}" u-i="41009218-7,41009218-6" bind:__l="__l" u-p="{{s}}"/></svg></view><input class="input-field" type="text" placeholder="请输入邮箱地址" bindfocus="{{v}}" bindblur="{{w}}" value="{{x}}" bindinput="{{y}}"/></view></view><view class="input-group"><view class="{{['input-container', H && 'focused', I && 'filled']}}"><view class="input-icon"><svg wx:if="{{C}}" u-s="{{['d']}}" class="icon" u-i="41009218-8" bind:__l="__l" u-p="{{C}}"><path wx:if="{{B}}" u-i="41009218-9,41009218-8" bind:__l="__l" u-p="{{B}}"/></svg></view><input class="input-field" type="number" maxlength="6" placeholder="请输入6位验证码" bindfocus="{{D}}" bindblur="{{E}}" value="{{F}}" bindinput="{{G}}"/></view></view><view class="input-group"><view class="{{['input-container', P && 'focused', Q && 'filled']}}"><view class="input-icon"><svg wx:if="{{K}}" u-s="{{['d']}}" class="icon" u-i="41009218-10" bind:__l="__l" u-p="{{K}}"><path wx:if="{{J}}" u-i="41009218-11,41009218-10" bind:__l="__l" u-p="{{J}}"/></svg></view><input class="input-field" password placeholder="请设置登录密码至少6位" bindfocus="{{L}}" bindblur="{{M}}" value="{{N}}" bindinput="{{O}}"/></view></view><view class="input-group"><button class="login-button" disabled="{{S}}" bindtap="{{T}}">{{R}}</button></view></view><view class="actions-section"><button class="register-button" bindtap="{{U}}"><text class="button-text">立即注册</text></button><button class="login-button" bindtap="{{V}}"><text class="button-text">已有账户?去登录</text></button></view><view class="footer-section"><text class="hint-text">注册即表示您同意我们的服务条款和隐私政策</text></view></view></view>

View File

@@ -3,7 +3,7 @@ const common_vendor = require("../../common/vendor.js");
const common_http = require("../../common/http.js");
const _sfc_main = {
data() {
return { id: null, d: {}, editing: false, form: { name: "", contactName: "", mobile: "", phone: "", address: "", level: "", priceLevel: "零售价", arOpening: 0, remark: "" }, priceLevels: ["零售价", "批发价", "大单报价"], priceLabels: ["零售价", "批发价", "大单报价"], priceIdx: 0 };
return { id: null, d: {}, editing: false, form: { name: "", contactName: "", mobile: "", phone: "", address: "", priceLevel: "零售价", arOpening: 0, remark: "" }, priceLevels: ["零售价", "批发价", "大单报价"], priceLabels: ["零售价", "批发价", "大单报价"], priceIdx: 0 };
},
onLoad(q) {
if (q && q.id) {
@@ -21,7 +21,6 @@ const _sfc_main = {
mobile: this.d.mobile || "",
phone: this.d.phone || "",
address: this.d.address || "",
level: this.d.level || "",
priceLevel: this.d.priceLevel || "retail",
arOpening: Number(this.d.arOpening || 0),
remark: this.d.remark || ""
@@ -111,44 +110,37 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
}, {
v: !$data.editing
}, !$data.editing ? {
w: common_vendor.t($data.d.level || "—")
w: common_vendor.t($data.d.priceLevel)
} : {
x: $data.form.level,
y: common_vendor.o(($event) => $data.form.level = $event.detail.value)
x: common_vendor.t($data.priceLabels[$data.priceIdx]),
y: $data.priceLabels,
z: $data.priceIdx,
A: common_vendor.o((...args) => $options.onPriceChange && $options.onPriceChange(...args))
}, {
z: !$data.editing
B: !$data.editing
}, !$data.editing ? {
A: common_vendor.t($data.d.priceLevel)
C: common_vendor.t(Number($data.d.arOpening || 0).toFixed(2))
} : {
B: common_vendor.t($data.priceLabels[$data.priceIdx]),
C: $data.priceLabels,
D: $data.priceIdx,
E: common_vendor.o((...args) => $options.onPriceChange && $options.onPriceChange(...args))
}, {
F: !$data.editing
}, !$data.editing ? {
G: common_vendor.t(Number($data.d.arOpening || 0).toFixed(2))
} : {
H: $data.form.arOpening,
I: common_vendor.o(common_vendor.m(($event) => $data.form.arOpening = $event.detail.value, {
D: $data.form.arOpening,
E: common_vendor.o(common_vendor.m(($event) => $data.form.arOpening = $event.detail.value, {
number: true
}))
}, {
J: common_vendor.t(Number($data.d.receivable || 0).toFixed(2)),
K: !$data.editing
F: common_vendor.t(Number($data.d.receivable || 0).toFixed(2)),
G: !$data.editing
}, !$data.editing ? {
L: common_vendor.t($data.d.remark || "—")
H: common_vendor.t($data.d.remark || "—")
} : {
M: $data.form.remark,
N: common_vendor.o(($event) => $data.form.remark = $event.detail.value)
I: $data.form.remark,
J: common_vendor.o(($event) => $data.form.remark = $event.detail.value)
}, {
O: common_vendor.t($data.editing ? "取消" : "编辑"),
P: common_vendor.o((...args) => $options.toggleEdit && $options.toggleEdit(...args)),
Q: $data.editing
K: common_vendor.t($data.editing ? "取消" : "编辑"),
L: common_vendor.o((...args) => $options.toggleEdit && $options.toggleEdit(...args)),
M: $data.editing
}, $data.editing ? {
R: common_vendor.o((...args) => $options.save && $options.save(...args))
N: common_vendor.o((...args) => $options.save && $options.save(...args))
} : {
S: common_vendor.o((...args) => $options.choose && $options.choose(...args))
O: common_vendor.o((...args) => $options.choose && $options.choose(...args))
});
}
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);

View File

@@ -1 +1 @@
<view class="page"><view class="card"><view class="row"><text class="label">名称</text><text wx:if="{{a}}" class="value">{{b}}</text><input wx:else class="value-input" placeholder="必填" value="{{c}}" bindinput="{{d}}"/></view><view class="row"><text class="label">联系人</text><text wx:if="{{e}}" class="value">{{f}}</text><input wx:else class="value-input" placeholder="可选" value="{{g}}" bindinput="{{h}}"/></view><view class="row"><text class="label">手机</text><text wx:if="{{i}}" class="value">{{j}}</text><input wx:else class="value-input" placeholder="可选" value="{{k}}" bindinput="{{l}}"/></view><view class="row"><text class="label">电话</text><text wx:if="{{m}}" class="value">{{n}}</text><input wx:else class="value-input" placeholder="可选(座机)" value="{{o}}" bindinput="{{p}}"/></view><view class="row"><text class="label">地址</text><text wx:if="{{q}}" class="value">{{r}}</text><input wx:else class="value-input" placeholder="可选" value="{{s}}" bindinput="{{t}}"/></view><view class="row"><text class="label">等级</text><text wx:if="{{v}}" class="value">{{w}}</text><input wx:else class="value-input" placeholder="可选,如 VIP/A/B" value="{{x}}" bindinput="{{y}}"/></view><view class="row"><text class="label">售价档位</text><text wx:if="{{z}}" class="value">{{A}}</text><picker wx:else range="{{C}}" value="{{D}}" bindchange="{{E}}"><view class="value">{{B}}</view></picker></view><view class="row"><text class="label">初始应收</text><text wx:if="{{F}}" class="value">¥ {{G}}</text><input wx:else class="value-input" type="digit" placeholder="0.00" value="{{H}}" bindinput="{{I}}"/></view><view class="row"><text class="label">当前应收</text><text class="value emp">¥ {{J}}</text></view><view class="row"><text class="label">备注</text><text wx:if="{{K}}" class="value">{{L}}</text><input wx:else class="value-input" placeholder="—" value="{{M}}" bindinput="{{N}}"/></view></view><view class="bottom"><button class="ghost" bindtap="{{P}}">{{O}}</button><button wx:if="{{Q}}" class="primary" bindtap="{{R}}">保存</button><button wx:else class="primary" bindtap="{{S}}">选择此客户</button></view></view>
<view class="page"><view class="card"><view class="row"><text class="label">名称</text><text wx:if="{{a}}" class="value">{{b}}</text><input wx:else class="value-input" placeholder="必填" value="{{c}}" bindinput="{{d}}"/></view><view class="row"><text class="label">联系人</text><text wx:if="{{e}}" class="value">{{f}}</text><input wx:else class="value-input" placeholder="可选" value="{{g}}" bindinput="{{h}}"/></view><view class="row"><text class="label">手机</text><text wx:if="{{i}}" class="value">{{j}}</text><input wx:else class="value-input" placeholder="可选" value="{{k}}" bindinput="{{l}}"/></view><view class="row"><text class="label">电话</text><text wx:if="{{m}}" class="value">{{n}}</text><input wx:else class="value-input" placeholder="可选(座机)" value="{{o}}" bindinput="{{p}}"/></view><view class="row"><text class="label">地址</text><text wx:if="{{q}}" class="value">{{r}}</text><input wx:else class="value-input" placeholder="可选" value="{{s}}" bindinput="{{t}}"/></view><view class="row"><text class="label">售价档位</text><text wx:if="{{v}}" class="value">{{w}}</text><picker wx:else range="{{y}}" value="{{z}}" bindchange="{{A}}"><view class="value">{{x}}</view></picker></view><view class="row"><text class="label">初始应收</text><text wx:if="{{B}}" class="value">¥ {{C}}</text><input wx:else class="value-input" type="digit" placeholder="0.00" value="{{D}}" bindinput="{{E}}"/></view><view class="row"><text class="label">当前应收</text><text class="value emp">¥ {{F}}</text></view><view class="row"><text class="label">备注</text><text wx:if="{{G}}" class="value">{{H}}</text><input wx:else class="value-input" placeholder="—" value="{{I}}" bindinput="{{J}}"/></view></view><view class="bottom"><button class="ghost" bindtap="{{L}}">{{K}}</button><button wx:if="{{M}}" class="primary" bindtap="{{N}}">保存</button><button wx:else class="primary" bindtap="{{O}}">选择此客户</button></view></view>

View File

@@ -5,7 +5,7 @@ const _sfc_main = {
data() {
return {
id: null,
form: { name: "", level: "", priceLevel: "retail", contactName: "", mobile: "", phone: "", address: "", arOpening: 0, remark: "" },
form: { name: "", priceLevel: "retail", contactName: "", mobile: "", phone: "", address: "", arOpening: 0, remark: "" },
priceLevels: ["零售价", "批发价", "大单报价"],
priceLabels: ["零售价", "批发价", "大单报价"],
priceIdx: 0
@@ -14,6 +14,7 @@ const _sfc_main = {
onLoad(query) {
if (query && query.id) {
this.id = Number(query.id);
this.load();
}
},
methods: {
@@ -21,6 +22,27 @@ const _sfc_main = {
this.priceIdx = Number(e.detail.value);
this.form.priceLevel = this.priceLevels[this.priceIdx];
},
async load() {
if (!this.id)
return;
try {
const d = await common_http.get(`/api/customers/${this.id}`);
this.form = {
name: (d == null ? void 0 : d.name) || "",
priceLevel: (d == null ? void 0 : d.priceLevel) || "零售价",
contactName: (d == null ? void 0 : d.contactName) || "",
mobile: (d == null ? void 0 : d.mobile) || "",
phone: (d == null ? void 0 : d.phone) || "",
address: (d == null ? void 0 : d.address) || "",
arOpening: Number((d == null ? void 0 : d.arOpening) || 0),
remark: (d == null ? void 0 : d.remark) || ""
};
const idx = this.priceLevels.indexOf(this.form.priceLevel || "零售价");
this.priceIdx = idx >= 0 ? idx : 0;
} catch (e) {
common_vendor.index.showToast({ title: (e == null ? void 0 : e.message) || "加载失败", icon: "none" });
}
},
async save() {
if (!this.form.name)
return common_vendor.index.showToast({ title: "请填写客户名称", icon: "none" });
@@ -41,27 +63,25 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return {
a: $data.form.name,
b: common_vendor.o(($event) => $data.form.name = $event.detail.value),
c: $data.form.level,
d: common_vendor.o(($event) => $data.form.level = $event.detail.value),
e: common_vendor.t($data.priceLabels[$data.priceIdx]),
f: $data.priceLabels,
g: $data.priceIdx,
h: common_vendor.o((...args) => $options.onPriceChange && $options.onPriceChange(...args)),
i: $data.form.contactName,
j: common_vendor.o(($event) => $data.form.contactName = $event.detail.value),
k: $data.form.mobile,
l: common_vendor.o(($event) => $data.form.mobile = $event.detail.value),
m: $data.form.phone,
n: common_vendor.o(($event) => $data.form.phone = $event.detail.value),
o: $data.form.address,
p: common_vendor.o(($event) => $data.form.address = $event.detail.value),
q: $data.form.arOpening,
r: common_vendor.o(common_vendor.m(($event) => $data.form.arOpening = $event.detail.value, {
c: common_vendor.t($data.priceLabels[$data.priceIdx]),
d: $data.priceLabels,
e: $data.priceIdx,
f: common_vendor.o((...args) => $options.onPriceChange && $options.onPriceChange(...args)),
g: $data.form.contactName,
h: common_vendor.o(($event) => $data.form.contactName = $event.detail.value),
i: $data.form.mobile,
j: common_vendor.o(($event) => $data.form.mobile = $event.detail.value),
k: $data.form.phone,
l: common_vendor.o(($event) => $data.form.phone = $event.detail.value),
m: $data.form.address,
n: common_vendor.o(($event) => $data.form.address = $event.detail.value),
o: $data.form.arOpening,
p: common_vendor.o(common_vendor.m(($event) => $data.form.arOpening = $event.detail.value, {
number: true
})),
s: $data.form.remark,
t: common_vendor.o(($event) => $data.form.remark = $event.detail.value),
v: common_vendor.o((...args) => $options.save && $options.save(...args))
q: $data.form.remark,
r: common_vendor.o(($event) => $data.form.remark = $event.detail.value),
s: common_vendor.o((...args) => $options.save && $options.save(...args))
};
}
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);

View File

@@ -1 +1 @@
<view class="page"><view class="field"><text class="label">客户名称</text><input class="value" placeholder="必填" value="{{a}}" bindinput="{{b}}"/></view><view class="field"><text class="label">客户等级</text><input class="value" placeholder="可选,如 VIP/A/B" value="{{c}}" bindinput="{{d}}"/></view><view class="field"><text class="label">售价档位</text><picker range="{{f}}" value="{{g}}" bindchange="{{h}}"><view class="value">{{e}}</view></picker></view><view class="field"><text class="label">联系人</text><input class="value" placeholder="可选" value="{{i}}" bindinput="{{j}}"/></view><view class="field"><text class="label">手机</text><input class="value" placeholder="可选" value="{{k}}" bindinput="{{l}}"/></view><view class="field"><text class="label">电话</text><input class="value" placeholder="可选(座机)" value="{{m}}" bindinput="{{n}}"/></view><view class="field"><text class="label">送货地址</text><input class="value" placeholder="可选" value="{{o}}" bindinput="{{p}}"/></view><view class="field"><text class="label">初始应收</text><input class="value" type="digit" placeholder="默认 0.00" value="{{q}}" bindinput="{{r}}"/></view><view class="textarea"><block wx:if="{{r0}}"><textarea maxlength="200" placeholder="备注最多200字" value="{{s}}" bindinput="{{t}}"></textarea></block></view><view class="bottom"><button class="primary" bindtap="{{v}}">保存</button></view></view>
<view class="page"><view class="field"><text class="label">客户名称</text><input class="value" placeholder="必填" value="{{a}}" bindinput="{{b}}"/></view><view class="field"><text class="label">售价档位</text><picker range="{{d}}" value="{{e}}" bindchange="{{f}}"><view class="value">{{c}}</view></picker></view><view class="field"><text class="label">联系人</text><input class="value" placeholder="可选" value="{{g}}" bindinput="{{h}}"/></view><view class="field"><text class="label">手机</text><input class="value" placeholder="可选" value="{{i}}" bindinput="{{j}}"/></view><view class="field"><text class="label">电话</text><input class="value" placeholder="可选(座机)" value="{{k}}" bindinput="{{l}}"/></view><view class="field"><text class="label">送货地址</text><input class="value" placeholder="可选" value="{{m}}" bindinput="{{n}}"/></view><view class="field"><text class="label">初始应收</text><input class="value" type="digit" placeholder="默认 0.00" value="{{o}}" bindinput="{{p}}"/></view><view class="textarea"><block wx:if="{{r0}}"><textarea maxlength="200" placeholder="备注最多200字" value="{{q}}" bindinput="{{r}}"></textarea></block></view><view class="bottom"><button class="primary" bindtap="{{s}}">保存</button></view></view>

View File

@@ -5,8 +5,7 @@ const API_OF = {
sale: "/api/orders",
purchase: "/api/purchase-orders",
collect: "/api/payments",
fund: "/api/other-transactions",
stock: "/api/inventories/logs"
fund: "/api/other-transactions"
};
const _sfc_main = {
data() {
@@ -16,18 +15,19 @@ const _sfc_main = {
{ key: "sale", name: "出货" },
{ key: "purchase", name: "进货" },
{ key: "collect", name: "收款" },
{ key: "fund", name: "资金" },
{ key: "stock", name: "盘点" }
{ key: "fund", name: "资金" }
],
range: "month",
query: { kw: "" },
items: [],
page: 1,
size: 20,
size: 15,
finished: false,
loading: false,
startDate: "",
endDate: ""
endDate: "",
nonVipRetentionDays: 0,
isVip: false
};
},
computed: {
@@ -56,7 +56,7 @@ const _sfc_main = {
return;
}
try {
common_vendor.index.__f__("log", "at pages/detail/index.vue:102", "[detail] onLoad route = pages/detail/index");
common_vendor.index.__f__("log", "at pages/detail/index.vue:104", "[detail] onLoad route = pages/detail/index");
} catch (e) {
}
this.computeRange();
@@ -133,12 +133,43 @@ const _sfc_main = {
if (list.length < this.size)
this.finished = true;
this.page += 1;
await this.hintIfNonVipOutOfWindow();
} catch (e) {
common_vendor.index.showToast({ title: "加载失败", icon: "none" });
} finally {
this.loading = false;
}
},
async hintIfNonVipOutOfWindow() {
try {
if (this.isVip && this.isVip === true)
return;
if (!this.nonVipRetentionDays) {
const v = await common_http.get("/api/vip/status");
this.isVip = !!(v == null ? void 0 : v.isVip);
this.nonVipRetentionDays = Number((v == null ? void 0 : v.nonVipRetentionDays) || 60);
if (this.isVip)
return;
}
if (!this.startDate)
return;
const start = new Date(this.startDate).getTime();
const threshold = Date.now() - this.nonVipRetentionDays * 24 * 3600 * 1e3;
if (start < threshold) {
common_vendor.index.showModal({
title: "提示",
content: `普通用户仅显示近${this.nonVipRetentionDays}天数据开通VIP可查看全部历史。`,
confirmText: "去开通VIP",
cancelText: "我知道了",
success: (r) => {
if (r.confirm)
common_vendor.index.navigateTo({ url: "/pages/my/vip" });
}
});
}
} catch (e) {
}
},
formatDate(s) {
if (!s)
return "";
@@ -201,8 +232,14 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
};
})
} : {}, {
p: common_vendor.o((...args) => $options.loadMore && $options.loadMore(...args)),
q: common_vendor.o((...args) => $options.onCreate && $options.onCreate(...args))
p: $data.items.length && !$data.finished
}, $data.items.length && !$data.finished ? {
q: $data.loading
} : {}, {
r: $data.finished && $data.items.length
}, $data.finished && $data.items.length ? {} : {}, {
s: common_vendor.o((...args) => $options.loadMore && $options.loadMore(...args)),
t: common_vendor.o((...args) => $options.onCreate && $options.onCreate(...args))
});
}
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);

View File

@@ -1 +1 @@
<view class="page"><view class="content"><view class="biz-tabs"><view wx:for="{{a}}" wx:for-item="b" wx:key="b" class="{{['biz', b.c]}}" bindtap="{{b.d}}">{{b.a}}</view></view><view class="panel"><view class="toolbar"><view class="period-group"><text class="period-label">期间</text><picker mode="date" value="{{c}}" bindchange="{{d}}"><view class="date-chip">{{b}}</view></picker><text class="sep">~</text><picker mode="date" value="{{f}}" bindchange="{{g}}"><view class="date-chip">{{e}}</view></picker></view><view class="search-row"><view class="search"><input class="search-input" placeholder="{{h}}" bindconfirm="{{i}}" value="{{j}}" bindinput="{{k}}"/></view><button class="btn" size="mini" bindtap="{{l}}">查询</button></view></view><view class="total">合计:¥{{m}}</view><scroll-view scroll-y class="list" bindscrolltolower="{{p}}"><block wx:if="{{n}}"><view wx:for="{{o}}" wx:for-item="it" wx:key="g" class="item" bindtap="{{it.h}}"><view class="item-left"><view class="date">{{it.a}}</view><view class="name">{{it.b}}</view><view class="no">{{it.c}}</view></view><view class="{{['amount', it.e && 'in', it.f && 'out']}}">¥ {{it.d}}</view><view class="arrow"></view></view></block><view wx:else class="empty">暂无数据</view></scroll-view><view class="fab" bindtap="{{q}}"></view></view></view></view>
<view class="page"><view class="content"><view class="biz-tabs"><view wx:for="{{a}}" wx:for-item="b" wx:key="b" class="{{['biz', b.c]}}" bindtap="{{b.d}}">{{b.a}}</view></view><view class="panel"><view class="toolbar"><view class="period-group"><text class="period-label">期间</text><picker mode="date" value="{{c}}" bindchange="{{d}}"><view class="date-chip">{{b}}</view></picker><text class="sep">~</text><picker mode="date" value="{{f}}" bindchange="{{g}}"><view class="date-chip">{{e}}</view></picker></view><view class="search-row"><view class="search"><input class="search-input" placeholder="{{h}}" bindconfirm="{{i}}" value="{{j}}" bindinput="{{k}}"/></view><button class="btn" size="mini" bindtap="{{l}}">查询</button></view></view><view class="total">合计:¥{{m}}</view><scroll-view scroll-y class="list" bindscrolltolower="{{s}}"><block wx:if="{{n}}"><view wx:for="{{o}}" wx:for-item="it" wx:key="g" class="item" bindtap="{{it.h}}"><view class="item-left"><view class="date">{{it.a}}</view><view class="name">{{it.b}}</view><view class="no">{{it.c}}</view></view><view class="{{['amount', it.e && 'in', it.f && 'out']}}">¥ {{it.d}}</view><view class="arrow"></view></view></block><view wx:else class="empty">暂无数据</view><view wx:if="{{p}}" class="loading" hidden="{{!q}}">加载中...</view><view wx:if="{{r}}" class="finished">没有更多了</view></scroll-view><view class="fab" bindtap="{{t}}"></view></view></view></view>

View File

@@ -66,7 +66,6 @@
margin: 16rpx;
border-radius: 16rpx;
padding: 12rpx;
border: 2rpx solid #e5e7eb;
}
.toolbar {
display: flex;
@@ -80,7 +79,6 @@
align-items: center;
gap: 8rpx;
background: #f6f8fb;
border: 2rpx solid #e6ebf2;
border-radius: 10rpx;
padding: 8rpx 10rpx;
}
@@ -100,23 +98,42 @@
.search-row {
display: flex;
align-items: center;
gap: 10rpx;
gap: 16rpx;
}
.search {
flex: 1;
min-width: 0;
display: flex;
}
.search-input {
width: 100%;
flex: 1;
height: 72rpx;
line-height: 72rpx;
padding: 0 24rpx;
box-sizing: border-box;
background: #fff;
border-radius: 12rpx;
padding: 12rpx;
color: #111;
border: 2rpx solid #e6ebf2;
font-size: 26rpx;
}
.btn {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
height: 72rpx;
padding: 0 32rpx;
margin-left: 4rpx;
border-radius: 12rpx;
background: #4C8DFF;
color: #fff;
border: none;
font-size: 26rpx;
box-sizing: border-box;
}
.btn::after {
border: none;
}
.total {
color: #4C8DFF;
@@ -127,6 +144,16 @@
.list {
flex: 1;
}
.loading {
text-align: center;
padding: 20rpx 0;
color: #444;
}
.finished {
text-align: center;
padding: 20rpx 0;
color: #444;
}
.item {
display: grid;
grid-template-columns: 1fr auto auto;

View File

@@ -12,17 +12,19 @@ const _sfc_main = {
notices: [],
loadingNotices: false,
noticeError: "",
consultLabel: "咨询",
consultDialogVisible: false,
consultMessage: "",
features: [
{ key: "product", title: "货品", img: "/static/icons/product.png", emoji: "📦" },
{ key: "customer", title: "客户", img: "/static/icons/customer.png", emoji: "👥" },
{ key: "sale", title: "销售", img: "/static/icons/sale.png", emoji: "💰" },
{ key: "account", title: "账户", img: "/static/icons/account.png", emoji: "💳" },
{ key: "supplier", title: "供应商", img: "/static/icons/supplier.png", emoji: "🚚" },
{ key: "purchase", title: "进货", img: "/static/icons/purchase.png", emoji: "🛒" },
{ key: "otherPay", title: "其他支出", img: "/static/icons/other-pay.png", emoji: "💸" },
{ key: "vip", title: "VIP会员", img: "/static/icons/vip.png", emoji: "👑" },
{ key: "report", title: "报表", img: "/static/icons/report.png", emoji: "📊" },
{ key: "more", title: "更多", img: "/static/icons/more.png", emoji: "⋯" }
{ key: "customer", title: "客户", img: "/static/icons/webwxgetmsgimg.png", emoji: "👥" },
{ key: "sale", title: "销售", img: "/static/icons/webwxgetmsgimg.jpg", emoji: "💰" },
{ key: "account", title: "账户", img: "/static/icons/icons8-profile-50.png", emoji: "💳" },
{ key: "supplier", title: "供应商", img: "/static/icons/icons8-supplier-50.png", emoji: "🚚" },
{ key: "purchase", title: "进货", img: "/static/icons/icons8-dollar-ethereum-exchange-50.png", emoji: "🛒" },
{ key: "otherPay", title: "其他支出", img: "/static/icons/icons8-expenditure-64.png", emoji: "💸" },
{ key: "vip", title: "VIP会员", img: "/static/icons/icons8-vip-48.png", emoji: "👑" },
{ key: "report", title: "报表", img: "/static/icons/icons8-graph-report-50.png", emoji: "📊" }
]
};
},
@@ -42,6 +44,7 @@ const _sfc_main = {
}
this.fetchMetrics();
this.fetchNotices();
this.fetchLatestConsult();
},
methods: {
async fetchMetrics() {
@@ -58,6 +61,59 @@ const _sfc_main = {
} catch (e) {
}
},
async fetchLatestConsult() {
try {
const d = await common_http.get("/api/consults");
if (d && d.replied)
this.consultLabel = "已回复";
else
this.consultLabel = "咨询";
this._latestConsult = d;
} catch (e) {
this.consultLabel = "咨询";
}
},
onConsultTap() {
if (this.consultLabel === "已回复" && this._latestConsult && this._latestConsult.id) {
const msg = this._latestConsult.latestReply ? this._latestConsult.latestReply : this._latestConsult.message || "";
common_vendor.index.showModal({ title: "咨询回复", content: msg || "暂无内容", showCancel: false, success: async (res) => {
if (!res || res.confirm !== true)
return;
try {
const r = await common_http.put(`/api/consults/${this._latestConsult.id}/ack`, {});
this.consultLabel = "咨询";
this._latestConsult = null;
setTimeout(() => this.fetchLatestConsult(), 200);
} catch (e) {
try {
common_vendor.index.showToast({ title: e && e.message || "已读同步失败", icon: "none" });
} catch (_) {
}
}
} });
return;
}
this.consultMessage = "";
this.consultDialogVisible = true;
},
closeConsultDialog() {
this.consultDialogVisible = false;
},
async submitConsult() {
const text = String(this.consultMessage || "").trim();
if (!text) {
common_vendor.index.showToast({ title: "请输入咨询内容", icon: "none" });
return;
}
try {
await common_http.post("/api/consults", { message: text });
this.consultDialogVisible = false;
common_vendor.index.showToast({ title: "已提交", icon: "success" });
setTimeout(() => this.fetchLatestConsult(), 300);
} catch (e) {
common_vendor.index.showToast({ title: e && e.message || "提交失败", icon: "none" });
}
},
async fetchNotices() {
this.loadingNotices = true;
this.noticeError = "";
@@ -128,7 +184,7 @@ const _sfc_main = {
},
goDetail() {
try {
common_vendor.index.__f__("log", "at pages/index/index.vue:198", "[index] goDetail → /pages/detail/index");
common_vendor.index.__f__("log", "at pages/index/index.vue:253", "[index] goDetail → /pages/detail/index");
} catch (e) {
}
common_vendor.index.switchTab({ url: "/pages/detail/index" });
@@ -150,11 +206,32 @@ const _sfc_main = {
};
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return common_vendor.e({
a: $data.loadingNotices
a: common_vendor.t($data.consultLabel),
b: common_vendor.o((...args) => $options.onConsultTap && $options.onConsultTap(...args)),
c: $data.KPI_ICONS.todaySales,
d: common_vendor.t($data.kpi.todaySales),
e: $data.KPI_ICONS.monthSales,
f: common_vendor.t($data.kpi.monthSales),
g: $data.KPI_ICONS.monthProfit,
h: common_vendor.t($data.kpi.monthProfit),
i: $data.KPI_ICONS.stockCount,
j: common_vendor.t($data.kpi.stockCount),
k: $data.consultDialogVisible
}, $data.consultDialogVisible ? {
l: $data.consultMessage,
m: common_vendor.o(($event) => $data.consultMessage = $event.detail.value),
n: common_vendor.o((...args) => $options.closeConsultDialog && $options.closeConsultDialog(...args)),
o: common_vendor.o((...args) => $options.submitConsult && $options.submitConsult(...args)),
p: common_vendor.o(() => {
}),
q: common_vendor.o(() => {
})
} : {}, {
r: $data.loadingNotices
}, $data.loadingNotices ? {} : $data.noticeError ? {
c: common_vendor.t($data.noticeError)
t: common_vendor.t($data.noticeError)
} : !$data.notices.length ? {} : {
e: common_vendor.f($data.notices, (n, idx, i0) => {
w: common_vendor.f($data.notices, (n, idx, i0) => {
return common_vendor.e({
a: common_vendor.t(n.text),
b: n.tag
@@ -166,17 +243,9 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
});
})
}, {
b: $data.noticeError,
d: !$data.notices.length,
f: $data.KPI_ICONS.todaySales,
g: common_vendor.t($data.kpi.todaySales),
h: $data.KPI_ICONS.monthSales,
i: common_vendor.t($data.kpi.monthSales),
j: $data.KPI_ICONS.monthProfit,
k: common_vendor.t($data.kpi.monthProfit),
l: $data.KPI_ICONS.stockCount,
m: common_vendor.t($data.kpi.stockCount),
n: common_vendor.f($data.features, (item, k0, i0) => {
s: $data.noticeError,
v: !$data.notices.length,
x: common_vendor.f($data.features, (item, k0, i0) => {
return common_vendor.e({
a: item.img
}, item.img ? {

View File

@@ -1 +1 @@
<view class="home"><view class="notice"><view class="notice-left">公告</view><view wx:if="{{a}}" class="notice-swiper" style="display:flex;align-items:center;color:#6b5a2a">加载中...</view><view wx:elif="{{b}}" class="notice-swiper" style="display:flex;align-items:center;color:#dd524d">{{c}}</view><view wx:elif="{{d}}" class="notice-swiper" style="display:flex;align-items:center;color:#6b5a2a">暂无公告</view><swiper wx:else class="notice-swiper" circular autoplay interval="4000" duration="400" vertical><swiper-item wx:for="{{e}}" wx:for-item="n" wx:key="e"><view class="notice-item" bindtap="{{n.d}}"><text class="notice-text">{{n.a}}</text><text wx:if="{{n.b}}" class="notice-tag">{{n.c}}</text></view></swiper-item></swiper></view><view class="hero"><view class="hero-top"><text class="brand">五金配件管家</text><view class="cta"><text class="cta-text">咨询</text></view></view><view class="kpi kpi-grid"><view class="kpi-item kpi-card"><image src="{{f}}" class="kpi-icon" mode="aspectFit"></image><view class="kpi-content"><text class="kpi-label">今日销售额</text><text class="kpi-value">{{g}}</text></view></view><view class="kpi-item kpi-card"><image src="{{h}}" class="kpi-icon" mode="aspectFit"></image><view class="kpi-content"><text class="kpi-label">本月销售额</text><text class="kpi-value">{{i}}</text></view></view><view class="kpi-item kpi-card"><image src="{{j}}" class="kpi-icon" mode="aspectFit"></image><view class="kpi-content"><text class="kpi-label">本月利润</text><text class="kpi-value">{{k}}</text></view></view><view class="kpi-item kpi-card"><image src="{{l}}" class="kpi-icon" mode="aspectFit"></image><view class="kpi-content"><text class="kpi-label">库存商品数量</text><text class="kpi-value">{{m}}</text></view></view></view></view><view class="section-title"><text class="section-text">常用功能</text></view><view class="grid-wrap"><view class="feature-grid"><view wx:for="{{n}}" wx:for-item="item" wx:key="g" class="feature-card" bindtap="{{item.h}}"><view class="fc-icon"><image wx:if="{{item.a}}" src="{{item.b}}" class="fc-img" mode="aspectFit" binderror="{{item.c}}"></image><text wx:elif="{{item.d}}" class="fc-emoji">{{item.e}}</text><view wx:else class="fc-placeholder"></view></view><view class="fc-title">{{item.f}}</view></view></view></view></view>
<view class="home"><view class="hero"><view class="hero-top"><text class="brand">五金配件管家</text><view class="cta" bindtap="{{b}}" hover-class="cta-active" hover-stay-time="80"><text class="cta-text">{{a}}</text></view></view><view class="kpi kpi-grid"><view class="kpi-item kpi-card"><image src="{{c}}" class="kpi-icon" mode="aspectFit"></image><view class="kpi-content"><text class="kpi-label">今日销售额</text><text class="kpi-value">{{d}}</text></view></view><view class="kpi-item kpi-card"><image src="{{e}}" class="kpi-icon" mode="aspectFit"></image><view class="kpi-content"><text class="kpi-label">本月销售额</text><text class="kpi-value">{{f}}</text></view></view><view class="kpi-item kpi-card"><image src="{{g}}" class="kpi-icon" mode="aspectFit"></image><view class="kpi-content"><text class="kpi-label">本月利润</text><text class="kpi-value">{{h}}</text></view></view><view class="kpi-item kpi-card"><image src="{{i}}" class="kpi-icon" mode="aspectFit"></image><view class="kpi-content"><text class="kpi-label">库存商品数量</text><text class="kpi-value">{{j}}</text></view></view></view></view><view wx:if="{{k}}" class="dialog-mask" catchtouchmove="{{p}}" catchtap="{{q}}"><view class="dialog"><view class="dialog-title">咨询</view><block wx:if="{{r0}}"><textarea class="dialog-textarea" placeholder="请输入咨询内容..." maxlength="500" value="{{l}}" bindinput="{{m}}"></textarea></block><view class="dialog-actions"><view class="btn" bindtap="{{n}}">取消</view><view class="btn primary" bindtap="{{o}}">提交</view></view></view></view><view class="notice"><view class="notice-left">公告</view><view wx:if="{{r}}" class="notice-swiper" style="display:flex;align-items:center;color:#6b5a2a">加载中...</view><view wx:elif="{{s}}" class="notice-swiper" style="display:flex;align-items:center;color:#dd524d">{{t}}</view><view wx:elif="{{v}}" class="notice-swiper" style="display:flex;align-items:center;color:#6b5a2a">暂无公告</view><swiper wx:else class="notice-swiper" circular autoplay interval="4000" duration="400" vertical><swiper-item wx:for="{{w}}" wx:for-item="n" wx:key="e"><view class="notice-item" bindtap="{{n.d}}"><text class="notice-text">{{n.a}}</text><text wx:if="{{n.b}}" class="notice-tag">{{n.c}}</text></view></swiper-item></swiper></view><view class="section-title"><text class="section-text">常用功能</text></view><view class="grid-wrap"><view class="feature-grid"><view wx:for="{{x}}" wx:for-item="item" wx:key="g" class="feature-card" bindtap="{{item.h}}"><view class="fc-icon"><image wx:if="{{item.a}}" src="{{item.b}}" class="fc-img" mode="aspectFit" binderror="{{item.c}}"></image><text wx:elif="{{item.d}}" class="fc-emoji">{{item.e}}</text><view wx:else class="fc-placeholder"></view></view><view class="fc-title">{{item.f}}</view></view></view></view></view>

View File

@@ -24,12 +24,21 @@
/* 垂直间距 */
/* 透明度 */
/* 文章场景相关 */
page {
height: 100%;
overflow: hidden;
background: linear-gradient(180deg, #f8fbff 0%, #ffffff 60%);
}
.home {
padding-bottom: 140rpx;
height: 100vh;
display: flex;
flex-direction: column;
padding-bottom: calc(env(safe-area-inset-bottom) + 32rpx);
position: relative;
/* 渐变背景:顶部淡蓝过渡到白色 */
background: linear-gradient(180deg, #f8fbff 0%, #ffffff 60%);
min-height: 100vh;
overflow: hidden;
box-sizing: border-box;
}
/* 首页横幅(移除) */
@@ -92,6 +101,7 @@
align-items: center;
gap: 16rpx;
padding: 10rpx 28rpx 0;
flex: 0 0 auto;
}
.section-title::before {
content: "";
@@ -111,12 +121,13 @@
/* 顶部英雄区:浅色玻璃卡片,带金色描边与柔和阴影 */
.hero {
margin: 16rpx 20rpx;
padding: 18rpx;
padding: 18rpx 18rpx 12rpx;
border-radius: 20rpx;
background: #ffffff;
border: 2rpx solid #e5e7eb;
box-shadow: none;
color: #111;
flex: 0 0 auto;
}
.hero-top {
display: flex;
@@ -159,11 +170,65 @@
letter-spacing: 1rpx;
}
/* KPI 卡片化布局2×2 */
/* 简易弹层样式 */
.dialog-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.45);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
}
.dialog {
width: 82vw;
background: #fff;
border-radius: 16rpx;
padding: 20rpx;
border: 2rpx solid #eef2f6;
}
.dialog-title {
font-size: 32rpx;
font-weight: 800;
color: #111;
margin-bottom: 16rpx;
}
.dialog-textarea {
width: 100%;
min-height: 180rpx;
border: 2rpx solid #e8eef8;
border-radius: 12rpx;
padding: 12rpx;
box-sizing: border-box;
}
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: 18rpx;
margin-top: 16rpx;
}
.btn {
padding: 10rpx 22rpx;
border-radius: 999rpx;
background: #f3f6fb;
color: #334155;
border: 2rpx solid #e2e8f0;
font-weight: 700;
}
.btn.primary {
background: #4C8DFF;
color: #fff;
border-color: #4C8DFF;
}
/* KPI 卡片化布局:横向铺满 */
.kpi {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16rpx;
grid-template-columns: repeat(4, 1fr);
gap: 12rpx;
}
.kpi-item {
text-align: center;
@@ -175,19 +240,21 @@
/* KPI 卡片(更扁平,降低高度) */
.kpi-grid {
grid-template-columns: repeat(2, 1fr);
gap: 16rpx;
gap: 12rpx;
}
.kpi-card {
display: flex;
align-items: center;
gap: 12rpx;
flex-direction: column;
align-items: flex-start;
justify-content: center;
gap: 8rpx;
text-align: left;
padding: 12rpx 14rpx;
border-radius: 12rpx;
border-radius: 14rpx;
background: #fff;
border: 2rpx solid #eef2f6;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
min-height: 120rpx;
}
.kpi-icon {
width: 44rpx;
@@ -206,8 +273,8 @@
}
.kpi-value {
color: #4C8DFF;
font-size: 36rpx;
line-height: 40rpx;
font-size: 34rpx;
line-height: 38rpx;
margin-top: 0;
font-weight: 800;
}
@@ -241,45 +308,56 @@
/* 功能容器:更轻的留白 */
.grid-wrap {
margin: 8rpx 12rpx 24rpx;
padding: 8rpx 8rpx 0;
border-radius: 20rpx;
background: transparent;
border: 0;
flex: 1 1 auto;
display: flex;
align-items: stretch;
justify-content: center;
margin: 16rpx 20rpx 28rpx;
padding: 32rpx 30rpx;
border-radius: 26rpx;
background: rgba(255, 255, 255, 0.96);
border: 2rpx solid #edf2f9;
box-shadow: 0 12rpx 28rpx rgba(32, 75, 143, 0.1);
box-sizing: border-box;
}
/* 功能卡片宫格:方形竖排,图标在上文字在下(与截图一致) */
.feature-grid {
flex: 1 1 auto;
width: 100%;
height: 100%;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14rpx;
padding: 8rpx 8rpx 18rpx;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-auto-rows: 1fr;
gap: 32rpx 28rpx;
align-content: space-evenly;
justify-items: center;
}
.feature-card {
height: 164rpx;
width: 168rpx;
height: 176rpx;
background: #fff;
border: 2rpx solid #eef2f6;
border-radius: 16rpx;
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.04);
padding: 12rpx;
border-radius: 20rpx;
box-shadow: 0 10rpx 24rpx rgba(0, 0, 0, 0.05);
padding: 18rpx 16rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.fc-icon {
width: 86rpx;
height: 86rpx;
width: 78rpx;
height: 78rpx;
border-radius: 18rpx;
background: #f7faff;
border: 2rpx solid #e8eef8;
display: flex;
align-items: center;
justify-content: center;
}
.fc-img {
width: 56rpx;
height: 56rpx;
width: 54rpx;
height: 54rpx;
opacity: 0.95;
}
.fc-emoji {
@@ -293,10 +371,11 @@
border: 2rpx solid #e8eef8;
}
.fc-title {
margin-top: 10rpx;
font-size: 26rpx;
margin-top: 12rpx;
font-size: 28rpx;
font-weight: 700;
color: #111;
letter-spacing: 1rpx;
}
/* 底部操作条:浅色半透明 + 金色主按钮 */

View File

@@ -16,7 +16,7 @@ const _sfc_main = {
};
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return {
a: common_assets._imports_0$1,
a: common_assets._imports_0$2,
b: common_vendor.o((...args) => $options.openPolicy && $options.openPolicy(...args)),
c: common_vendor.o((...args) => $options.openTerms && $options.openTerms(...args)),
d: common_vendor.o((...args) => $options.openComplaint && $options.openComplaint(...args))

View File

@@ -1,10 +1,24 @@
"use strict";
const common_vendor = require("../../common/vendor.js");
const common_http = require("../../common/http.js");
const common_config = require("../../common/config.js");
const common_assets = require("../../common/assets.js");
function normalizeAvatar(url) {
if (!url)
return "/static/icons/icons8-mitt-24.png";
const s = String(url);
if (/^https?:\/\//i.test(s))
return s;
if (!common_config.API_BASE_URL)
return s;
if (s.startsWith("/"))
return `${common_config.API_BASE_URL}${s}`;
return `${common_config.API_BASE_URL}/${s}`;
}
const _sfc_main = {
data() {
return {
avatarUrl: "/static/logo.png",
avatarUrl: "/static/icons/icons8-mitt-24.png",
shopName: "未登录",
mobile: "",
pendingJsCode: "",
@@ -16,7 +30,7 @@ const _sfc_main = {
},
onShow() {
this.fetchProfile();
this.loadVipFromStorage();
this.loadVip();
try {
if (common_vendor.index.getStorageSync("TOKEN")) {
this.$forceUpdate && this.$forceUpdate();
@@ -32,9 +46,22 @@ const _sfc_main = {
return false;
}
},
mobileDisplay() {
const m = String(this.mobile || "");
return m.length === 11 ? m.slice(0, 3) + "****" + m.slice(7) : m || "未绑定手机号";
avatarDisplay() {
return normalizeAvatar(this.avatarUrl);
},
emailDisplay() {
if (!this.isLoggedIn)
return "";
const e = String(common_vendor.index.getStorageSync("USER_EMAIL") || "");
if (!e)
return "未绑定邮箱";
const at = e.indexOf("@");
if (at > 1) {
const name = e.slice(0, at);
const domain = e.slice(at);
return (name.length <= 2 ? name[0] + "*" : name.slice(0, 2) + "***") + domain;
}
return e;
},
vipStartDisplay() {
return this.formatDisplay(this.vipStart);
@@ -55,35 +82,100 @@ const _sfc_main = {
})();
if (!hasToken) {
this.shopName = "未登录";
this.avatarUrl = "/static/logo.png";
this.avatarUrl = "/static/icons/icons8-mitt-24.png";
this.mobile = "";
return;
}
try {
await common_http.get("/api/dashboard/overview");
} catch (e) {
}
try {
const storeName = common_vendor.index.getStorageSync("SHOP_NAME") || "";
const avatar = common_vendor.index.getStorageSync("USER_AVATAR") || "";
const phone = common_vendor.index.getStorageSync("USER_MOBILE") || "";
if (storeName)
this.shopName = storeName;
if (avatar)
this.avatarUrl = avatar;
const profile = await common_http.get("/api/user/me");
const latestAvatar = (profile == null ? void 0 : profile.avatarUrl) || "";
if (latestAvatar) {
const bust = `${latestAvatar}${latestAvatar.includes("?") ? "&" : "?"}t=${Date.now()}`;
this.avatarUrl = bust;
try {
common_vendor.index.setStorageSync("USER_AVATAR_RAW", latestAvatar);
common_vendor.index.setStorageSync("USER_AVATAR", latestAvatar);
} catch (_) {
}
} else {
const cached = common_vendor.index.getStorageSync("USER_AVATAR") || "";
this.avatarUrl = cached || "/static/icons/icons8-mitt-24.png";
}
const storeName = (profile == null ? void 0 : profile.name) || common_vendor.index.getStorageSync("SHOP_NAME") || "未命名店铺";
this.shopName = storeName;
const phone = (profile == null ? void 0 : profile.phone) || common_vendor.index.getStorageSync("USER_MOBILE") || "";
this.mobile = phone;
} catch (e) {
try {
const storeName = common_vendor.index.getStorageSync("SHOP_NAME") || "";
const avatar = common_vendor.index.getStorageSync("USER_AVATAR") || "";
const phone = common_vendor.index.getStorageSync("USER_MOBILE") || "";
if (storeName)
this.shopName = storeName;
if (avatar)
this.avatarUrl = avatar;
this.mobile = phone;
} catch (_) {
}
}
},
loadVipFromStorage() {
async loadVip() {
try {
const isVip = String(common_vendor.index.getStorageSync("USER_VIP_IS_VIP") || "false").toLowerCase() === "true";
const start = common_vendor.index.getStorageSync("USER_VIP_START") || "";
const end = common_vendor.index.getStorageSync("USER_VIP_END") || "";
this.vipIsVip = isVip;
this.vipStart = start;
this.vipEnd = end;
const hasToken = (() => {
try {
return !!common_vendor.index.getStorageSync("TOKEN");
} catch (e) {
return false;
}
})();
if (!hasToken) {
this.vipIsVip = false;
this.vipStart = "";
this.vipEnd = "";
return;
}
const data = await common_http.get("/api/vip/status");
const active = !!(data == null ? void 0 : data.isVip);
this.vipIsVip = active;
this.vipEnd = (data == null ? void 0 : data.expireAt) || "";
let computedStart = "";
const exp = this.vipEnd;
if (exp) {
const m = String(exp).match(/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2}))?)?/);
if (m) {
const y = Number(m[1]);
const mo = Number(m[2]) - 1;
const da = Number(m[3]);
const hh = Number(m[4] || "0");
const mm = Number(m[5] || "0");
const ss = Number(m[6] || "0");
const startDate = new Date(y, mo - 1, da, hh, mm, ss);
const y2 = startDate.getFullYear();
const m2 = (startDate.getMonth() + 1).toString().padStart(2, "0");
const d2 = startDate.getDate().toString().padStart(2, "0");
const h2 = startDate.getHours().toString().padStart(2, "0");
const i2 = startDate.getMinutes().toString().padStart(2, "0");
computedStart = `${y2}-${m2}-${d2} ${h2}:${i2}`;
}
}
this.vipStart = computedStart;
try {
common_vendor.index.setStorageSync("USER_VIP_IS_VIP", String(active));
common_vendor.index.setStorageSync("USER_VIP_END", this.vipEnd);
if (this.vipStart)
common_vendor.index.setStorageSync("USER_VIP_START", this.vipStart);
else
common_vendor.index.removeStorageSync("USER_VIP_START");
} catch (_) {
}
} catch (e) {
try {
const isVip = String(common_vendor.index.getStorageSync("USER_VIP_IS_VIP") || "false").toLowerCase() === "true";
this.vipIsVip = isVip;
this.vipStart = common_vendor.index.getStorageSync("USER_VIP_START") || "";
this.vipEnd = common_vendor.index.getStorageSync("USER_VIP_END") || "";
} catch (_) {
}
}
},
formatDisplay(value) {
@@ -136,9 +228,6 @@ const _sfc_main = {
goLogin() {
common_vendor.index.navigateTo({ url: "/pages/auth/login" });
},
goRegister() {
common_vendor.index.navigateTo({ url: "/pages/auth/register" });
},
onGetPhoneNumber(e) {
if (this.logging)
return;
@@ -161,34 +250,16 @@ const _sfc_main = {
common_vendor.index.navigateTo({ url: "/pages/my/sms-login" });
},
onAvatarError() {
this.avatarUrl = "/static/logo.png";
this.avatarUrl = "/static/icons/icons8-mitt-24.png";
},
goVip() {
common_vendor.index.navigateTo({ url: "/pages/my/vip" });
},
goMyOrders() {
common_vendor.index.switchTab({ url: "/pages/detail/index" });
},
goSupplier() {
common_vendor.index.navigateTo({ url: "/pages/supplier/select" });
},
goCustomer() {
common_vendor.index.navigateTo({ url: "/pages/customer/select" });
},
goCustomerQuote() {
common_vendor.index.showToast({ title: "客户报价(开发中)", icon: "none" });
},
goShop() {
common_vendor.index.showToast({ title: "店铺管理(开发中)", icon: "none" });
common_vendor.index.navigateTo({ url: "/pages/my/orders" });
},
editProfile() {
common_vendor.index.showToast({ title: "账号与安全(开发中)", icon: "none" });
},
goProductSettings() {
common_vendor.index.navigateTo({ url: "/pages/product/settings" });
},
goSystemParams() {
common_vendor.index.showToast({ title: "系统参数(开发中)", icon: "none" });
common_vendor.index.navigateTo({ url: "/pages/my/security" });
},
goAbout() {
common_vendor.index.navigateTo({ url: "/pages/my/about" });
@@ -201,9 +272,14 @@ const _sfc_main = {
common_vendor.index.removeStorageSync("DEFAULT_USER_ID");
common_vendor.index.setStorageSync("ENABLE_DEFAULT_USER", "false");
common_vendor.index.removeStorageSync("USER_AVATAR");
common_vendor.index.removeStorageSync("USER_AVATAR_RAW");
common_vendor.index.removeStorageSync("USER_NAME");
common_vendor.index.removeStorageSync("USER_MOBILE");
common_vendor.index.removeStorageSync("USER_EMAIL");
common_vendor.index.removeStorageSync("SHOP_NAME");
common_vendor.index.removeStorageSync("USER_VIP_IS_VIP");
common_vendor.index.removeStorageSync("USER_VIP_START");
common_vendor.index.removeStorageSync("USER_VIP_END");
common_vendor.index.showToast({ title: "已清理本地信息", icon: "none" });
setTimeout(() => {
common_vendor.index.reLaunch({ url: "/pages/index/index" });
@@ -216,32 +292,32 @@ const _sfc_main = {
};
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return common_vendor.e({
a: !$options.isLoggedIn
}, !$options.isLoggedIn ? {
b: common_vendor.o((...args) => $options.goLogin && $options.goLogin(...args)),
c: common_vendor.o((...args) => $options.goRegister && $options.goRegister(...args))
} : {}, {
d: $data.avatarUrl,
e: common_vendor.o((...args) => $options.onAvatarError && $options.onAvatarError(...args)),
f: common_vendor.t($data.shopName),
g: common_vendor.t($options.mobileDisplay),
h: common_vendor.t($data.vipIsVip ? "VIP" : "非VIP"),
i: common_vendor.t($options.vipStartDisplay),
j: common_vendor.t($options.vipEndDisplay),
k: $data.vipIsVip ? 1 : "",
l: common_vendor.o((...args) => $options.goVip && $options.goVip(...args)),
m: common_vendor.o((...args) => $options.goMyOrders && $options.goMyOrders(...args)),
n: common_vendor.o((...args) => $options.goSupplier && $options.goSupplier(...args)),
o: common_vendor.o((...args) => $options.goCustomer && $options.goCustomer(...args)),
p: common_vendor.o((...args) => $options.goCustomerQuote && $options.goCustomerQuote(...args)),
q: common_vendor.o((...args) => $options.goShop && $options.goShop(...args)),
r: common_vendor.o((...args) => $options.editProfile && $options.editProfile(...args)),
s: common_vendor.o((...args) => $options.goProductSettings && $options.goProductSettings(...args)),
t: common_vendor.o((...args) => $options.goSystemParams && $options.goSystemParams(...args)),
v: common_vendor.o((...args) => $options.goAbout && $options.goAbout(...args)),
w: $options.isLoggedIn
a: $options.isLoggedIn
}, $options.isLoggedIn ? {
x: common_vendor.o((...args) => $options.logout && $options.logout(...args))
b: $options.avatarDisplay,
c: common_vendor.o((...args) => $options.onAvatarError && $options.onAvatarError(...args)),
d: common_vendor.t($data.shopName),
e: common_vendor.t($options.emailDisplay)
} : {
f: common_assets._imports_0$1,
g: common_vendor.o((...args) => $options.goLogin && $options.goLogin(...args))
}, {
h: $options.isLoggedIn
}, $options.isLoggedIn ? {
i: common_vendor.t($data.vipIsVip ? "VIP" : "非VIP"),
j: common_vendor.t($options.vipStartDisplay),
k: common_vendor.t($options.vipEndDisplay),
l: $data.vipIsVip ? 1 : ""
} : {}, {
m: $data.vipIsVip
}, $data.vipIsVip ? {} : {}, {
n: common_vendor.o((...args) => $options.goVip && $options.goVip(...args)),
o: common_vendor.o((...args) => $options.goMyOrders && $options.goMyOrders(...args)),
p: common_vendor.o((...args) => $options.editProfile && $options.editProfile(...args)),
q: common_vendor.o((...args) => $options.goAbout && $options.goAbout(...args)),
r: $options.isLoggedIn
}, $options.isLoggedIn ? {
s: common_vendor.o((...args) => $options.logout && $options.logout(...args))
} : {});
}
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);

View File

@@ -1 +1 @@
<view class="me"><view wx:if="{{a}}" class="card login"><view class="login-title">登录/注册以同步数据</view><button class="login-btn" type="primary" bindtap="{{b}}">登录</button><button class="login-btn minor" bindtap="{{c}}">注册</button></view><view class="card user"><image class="avatar" src="{{d}}" mode="aspectFill" binderror="{{e}}"/><view class="meta"><text class="name">{{f}}</text><text class="phone">{{g}}</text><text class="role">老板</text></view></view><view class="{{['card', 'vip', k && 'active']}}"><view class="vip-row"><text class="vip-badge">{{h}}</text><text class="vip-title">会员状态</text></view><view class="vip-meta"><view class="item"><text class="label">开始</text><text class="value">{{i}}</text></view><view class="item"><text class="label">结束</text><text class="value">{{j}}</text></view></view></view><view class="group"><view class="group-title">会员与订单</view><view class="cell" bindtap="{{l}}"><text>VIP会员</text><text class="arrow"></text></view><view class="cell" bindtap="{{m}}"><text>我的订单</text><text class="arrow"></text></view></view><view class="group"><view class="group-title">基础管理</view><view class="cell" bindtap="{{n}}"><text>供应商管理</text><text class="arrow"></text></view><view class="cell" bindtap="{{o}}"><text>客户管理</text><text class="arrow"></text></view><view class="cell" bindtap="{{p}}"><text>客户报价</text><text class="arrow"></text></view><view class="cell" bindtap="{{q}}"><text>店铺管理</text><text class="arrow"></text></view></view><view class="group"><view class="group-title">设置中心</view><view class="cell" bindtap="{{r}}"><text>账号与安全</text><text class="desc">修改头像、姓名、密码</text><text class="arrow"></text></view><view class="cell" bindtap="{{s}}"><text>商品设置</text><text class="arrow"></text></view><view class="cell" bindtap="{{t}}"><text>系统参数</text><text class="desc">低价提示、默认收款、单行折扣等</text><text class="arrow"></text></view><view class="cell" bindtap="{{v}}"><text>关于与协议</text><text class="arrow"></text></view><view wx:if="{{w}}" class="cell danger" bindtap="{{x}}"><text>退出登录</text></view></view></view>
<view class="me"><view wx:if="{{a}}" class="card user"><image class="avatar" src="{{b}}" mode="aspectFill" binderror="{{c}}"/><view class="meta"><text class="name">{{d}}</text><text class="phone">{{e}}</text><text class="role">老板</text></view></view><view wx:else class="card user guest"><image class="avatar" src="{{f}}" mode="aspectFill"/><view class="meta"><text class="name">未登录</text><text class="phone">登录后同步数据</text><text class="role">访客</text></view><button class="login-entry" bindtap="{{g}}">登录</button></view><view wx:if="{{h}}" class="{{['card', 'vip', l && 'active']}}"><view class="vip-row"><text class="vip-badge">{{i}}</text><text class="vip-title">会员状态</text></view><view class="vip-meta"><view class="item"><text class="label">开始</text><text class="value">{{j}}</text></view><view class="item"><text class="label">结束</text><text class="value">{{k}}</text></view></view></view><view class="group"><view class="group-title">会员与订单</view><view class="cell" bindtap="{{n}}"><view class="cell-left"><text>VIP会员</text><text wx:if="{{m}}" class="vip-tag">已开通</text><text wx:else class="vip-tag pending">待开通</text></view><text class="arrow"></text></view><view class="cell" bindtap="{{o}}"><text>我的订单</text><text class="arrow"></text></view></view><view class="group"><view class="group-title">设置中心</view><view class="cell" bindtap="{{p}}"><text>账号与安全</text><text class="desc">修改头像、姓名、密码、电话</text><text class="arrow"></text></view><view class="cell" bindtap="{{q}}"><text>关于与协议</text><text class="arrow"></text></view><view wx:if="{{r}}" class="cell danger" bindtap="{{s}}"><text>退出登录</text></view></view></view>

View File

@@ -57,6 +57,20 @@
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.16);
align-items: center;
}
.card.user.guest {
justify-content: space-between;
}
.card.user.guest .meta {
flex: 1;
}
.card.user.guest .login-entry {
padding: 12rpx 30rpx;
border-radius: 999rpx;
background: #4C8DFF;
color: #fff;
font-size: 28rpx;
font-weight: 600;
}
.avatar {
width: 120rpx;
height: 120rpx;
@@ -151,6 +165,23 @@
padding: 26rpx 22rpx;
border-top: 1rpx solid #e5e7eb;
color: #111;
gap: 18rpx;
}
.cell-left {
display: flex;
align-items: center;
gap: 14rpx;
}
.vip-tag {
padding: 4rpx 12rpx;
border-radius: 999rpx;
background: rgba(76, 141, 255, 0.15);
color: #4C8DFF;
font-size: 22rpx;
}
.vip-tag.pending {
background: rgba(76, 141, 255, 0.06);
color: #99a2b3;
}
.cell .desc {
margin-left: auto;

View File

@@ -0,0 +1,75 @@
"use strict";
const common_vendor = require("../../common/vendor.js");
const common_http = require("../../common/http.js");
const _sfc_main = {
data() {
return { list: [], page: 1, size: 20, loading: false };
},
onShow() {
this.fetch(true);
},
computed: {
isLoggedIn() {
try {
return !!common_vendor.index.getStorageSync("TOKEN");
} catch (e) {
return false;
}
}
},
methods: {
async fetch(reset = false) {
if (!this.isLoggedIn)
return;
if (this.loading)
return;
this.loading = true;
try {
const p = reset ? 1 : this.page;
const data = await common_http.get("/api/vip/recharges", { page: p, size: this.size });
const arr = Array.isArray(data == null ? void 0 : data.list) ? data.list : [];
this.list = reset ? arr : (this.list || []).concat(arr);
this.page = p + 1;
} finally {
this.loading = false;
}
},
fmt(v) {
if (!v)
return "";
const s = String(v);
const m = s.match(/^(\d{4}-\d{2}-\d{2})([ T](\d{2}:\d{2}))/);
return m ? `${m[1]} ${m[3]}` : s;
},
toMoney(v) {
try {
return Number(v).toFixed(2);
} catch (_) {
return v;
}
}
}
};
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return common_vendor.e({
a: !$options.isLoggedIn
}, !$options.isLoggedIn ? {} : common_vendor.e({
b: common_vendor.f($data.list, (it, k0, i0) => {
return common_vendor.e({
a: common_vendor.t($options.toMoney(it.price)),
b: common_vendor.t(it.channel || "支付"),
c: common_vendor.t($options.fmt(it.createdAt)),
d: common_vendor.t(it.durationDays),
e: it.expireTo
}, it.expireTo ? {
f: common_vendor.t($options.fmt(it.expireTo))
} : {}, {
g: it.id
});
}),
c: $data.list.length === 0
}, $data.list.length === 0 ? {} : {}));
}
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);
wx.createPage(MiniProgramPage);
//# sourceMappingURL=../../../.sourcemap/mp-weixin/pages/my/orders.js.map

View File

@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "我的订单",
"usingComponents": {}
}

View File

@@ -0,0 +1 @@
<view class="orders"><view wx:if="{{a}}" class="hint">请先登录后查看VIP支付记录</view><view wx:else><view wx:for="{{b}}" wx:for-item="it" wx:key="g" class="item"><view class="row1"><text class="price">¥ {{it.a}}</text><text class="channel">{{it.b}}</text></view><view class="row2"><text class="date">{{it.c}}</text><text class="duration">{{it.d}} 天</text></view><view wx:if="{{it.e}}" class="row3"><text class="expire">有效期至 {{it.f}}</text></view></view><view wx:if="{{c}}" class="empty">暂无支付记录</view></view></view>

View File

@@ -24,58 +24,49 @@
/* 垂直间距 */
/* 透明度 */
/* 文章场景相关 */
.login {
min-height: 100vh;
padding: 24rpx;
background: #ffffff;
}
.card {
margin-top: 60rpx;
background: #fff;
border: 2rpx solid #e5e7eb;
border-radius: 16rpx;
padding: 24rpx;
}
.title {
font-size: 36rpx;
font-weight: 800;
margin-bottom: 16rpx;
color: #111;
}
.field {
position: relative;
margin-bottom: 16rpx;
}
.label {
display: block;
margin-bottom: 8rpx;
color: #444;
}
.input {
width: 100%;
background: #f1f1f1;
border: 2rpx solid #e5e7eb;
border-radius: 12rpx;
padding: 14rpx;
color: #111;
}
.toggle {
position: absolute;
right: 12rpx;
top: 64rpx;
color: #4C8DFF;
font-size: 26rpx;
.orders {
padding: 16rpx 16rpx calc(env(safe-area-inset-bottom) + 16rpx);
}
.hint {
color: #444;
font-size: 24rpx;
margin: 8rpx 0 16rpx;
padding: 24rpx;
text-align: center;
}
.primary {
width: 100%;
background: #4C8DFF;
color: #fff;
border-radius: 999rpx;
padding: 20rpx 0;
.item {
background: #fff;
border: 1rpx solid #e5e7eb;
border-radius: 16rpx;
padding: 18rpx;
margin: 12rpx 0;
}
.row1 {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6rpx;
}
.price {
color: #111;
font-weight: 800;
font-size: 34rpx;
}
.channel {
color: #666;
font-size: 24rpx;
}
.row2 {
display: flex;
justify-content: space-between;
color: #666;
font-size: 24rpx;
}
.row3 {
margin-top: 6rpx;
color: #4C8DFF;
font-size: 24rpx;
}
.empty {
text-align: center;
color: #999;
padding: 40rpx 0;
}

View File

@@ -1,77 +0,0 @@
"use strict";
const common_vendor = require("../../common/vendor.js");
const _sfc_main = {
data() {
return {
form: { phone: "", password: "" },
showPwd: false,
submitting: false
};
},
methods: {
validatePhone(p) {
return /^1\d{10}$/.test(String(p || ""));
},
validate() {
if (!this.validatePhone(this.form.phone)) {
common_vendor.index.showToast({ title: "请输入有效手机号", icon: "none" });
return false;
}
if (!this.form.password) {
common_vendor.index.showToast({ title: "请输入密码", icon: "none" });
return false;
}
if (this.form.password.length < 6) {
common_vendor.index.showToast({ title: "密码至少6位", icon: "none" });
return false;
}
return true;
},
submit() {
if (this.submitting)
return;
if (!this.validate())
return;
this.submitting = true;
try {
common_vendor.index.setStorageSync("LOGIN_STATUS", "logged_in");
common_vendor.index.setStorageSync("LOGIN_PHONE", this.form.phone);
try {
const uid = common_vendor.index.getStorageSync("DEFAULT_USER_ID") || "";
const enable = common_vendor.index.getStorageSync("ENABLE_DEFAULT_USER") || "";
if (!enable)
common_vendor.index.setStorageSync("ENABLE_DEFAULT_USER", "true");
if (!uid)
common_vendor.index.setStorageSync("DEFAULT_USER_ID", "2");
} catch (e) {
}
common_vendor.index.showToast({ title: "登录成功(本地)", icon: "none" });
setTimeout(() => {
common_vendor.index.reLaunch({ url: "/pages/index/index" });
}, 500);
} finally {
this.submitting = false;
}
}
}
};
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return {
a: $data.form.phone,
b: common_vendor.o(common_vendor.m(($event) => $data.form.phone = $event.detail.value, {
trim: true
})),
c: !$data.showPwd,
d: $data.form.password,
e: common_vendor.o(common_vendor.m(($event) => $data.form.password = $event.detail.value, {
trim: true
})),
f: common_vendor.t($data.showPwd ? "隐藏" : "显示"),
g: common_vendor.o(($event) => $data.showPwd = !$data.showPwd),
h: $data.submitting,
i: common_vendor.o((...args) => $options.submit && $options.submit(...args))
};
}
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);
wx.createPage(MiniProgramPage);
//# sourceMappingURL=../../../.sourcemap/mp-weixin/pages/my/password-login.js.map

View File

@@ -1,4 +0,0 @@
{
"navigationBarTitleText": "账号登录",
"usingComponents": {}
}

Some files were not shown because too many files have changed in this diff Show More