Sfoglia il codice sorgente

重构登录校验功能;订单模块基本重构

Huanyi 3 giorni fa
parent
commit
cf64ed4c63

+ 11 - 4
api/auth/index.js → api/auth.js

@@ -1,13 +1,20 @@
 import request from '@/utils/request';
 
-export function wechatLogin(data) {
+const CLIENT_ID = 'e48ac397bff4f031b14d6e671eee49c3';
+
+/**
+ * 统一登录接口
+ * @param {Object} data - 登录参数
+ * @param {string} data.grantType - 授权类型: 'password' | 'wechatApplet'
+ */
+export function login(data) {
   return request({
     url: '/auth/login',
     method: 'post',
     data: {
-      ...data,
-      clientId: 'e48ac397bff4f031b14d6e671eee49c3',
-      grantType: 'wechatApplet'
+      clientId: CLIENT_ID,
+      userSource: 'sys_employee',
+      ...data
     }
   });
 }

+ 9 - 0
api/erp/client.js

@@ -0,0 +1,9 @@
+import request from '@/utils/request';
+
+/**
+ * 根据 RowID 列表批量查询客户
+ * @param {string} rowIds 逗号分隔的 rowId 字符串
+ */
+export function getClientByIds(rowIds) {
+	return request({ url: '/erp/client/listByIds', method: 'GET', params: { rowIds } });
+}

+ 11 - 0
api/erp/order.js

@@ -24,6 +24,17 @@ export function listMyOrder(query) {
   })
 }
 
+/**
+ * 按客户ID分页查询订单(不限制下单人,每页5条)
+ */
+export function listOrderByClientId(query) {
+  return request({
+    url: '/erp/order/listByClient',
+    method: 'get',
+    params: query
+  })
+}
+
 /**
  * 查询订单详情
  * @Author: Antigravity

+ 0 - 15
api/system/customer.js

@@ -1,15 +0,0 @@
-import request from '@/utils/request';
-
-/**
- * 获取当前登录客户信息
- */
-export function getMyInfo() {
-	return request({ url: '/system/customer/getInfo', method: 'GET' });
-}
-
-/**
- * 更新当前登录客户信息
- */
-export function updateMyInfo(data) {
-	return request({ url: '/system/customer/updateProfile', method: 'PUT', data });
-}

+ 15 - 0
api/system/employee.js

@@ -0,0 +1,15 @@
+import request from '@/utils/request';
+
+/**
+ * 获取当前登录员工信息
+ */
+export function getMyInfo() {
+	return request({ url: '/system/employee/getInfo', method: 'GET' });
+}
+
+/**
+ * 更新当前登录员工信息
+ */
+export function updateMyInfo(data) {
+	return request({ url: '/system/employee/updateProfile', method: 'PUT', data });
+}

+ 61 - 0
components/erp-submit-bar.vue

@@ -0,0 +1,61 @@
+<template>
+	<view class="erp-submit-bar">
+		<button class="submit-btn" :disabled="disabled" @click="handleClick">
+			<slot>{{ text }}</slot>
+		</button>
+	</view>
+</template>
+
+<script>
+export default {
+	name: 'ErpSubmitBar',
+	props: {
+		text: { type: String, default: '' },
+		disabled: { type: Boolean, default: false }
+	},
+	methods: {
+		handleClick() {
+			if (!this.disabled) {
+				this.$emit('click');
+			}
+		}
+	}
+}
+</script>
+
+<style scoped>
+.erp-submit-bar {
+	position: fixed;
+	bottom: 0;
+	left: 0;
+	width: 100%;
+	background: #fff;
+	padding: 20rpx 30rpx calc(env(safe-area-inset-bottom) + 20rpx);
+	box-sizing: border-box;
+	box-shadow: 0 -10rpx 30rpx rgba(0, 0, 0, 0.05);
+	z-index: 100;
+}
+
+.submit-btn {
+	background: #C1001C;
+	color: #fff;
+	height: 100rpx;
+	border-radius: 50rpx;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	font-size: 34rpx;
+	font-weight: bold;
+	border: none;
+}
+
+.submit-btn:active {
+	opacity: 0.9;
+	transform: scale(0.98);
+}
+
+.submit-btn[disabled] {
+	background: #e0e0e0;
+	color: #999;
+}
+</style>

+ 74 - 73
components/erp-tab-bar.vue

@@ -1,16 +1,22 @@
 <template>
 	<view class="tab-bar-container">
 		<view class="tab-bar-content">
-			<view class="tab-item" @click="switchTab('home')" :class="{active: active === 'home'}">
-				<image class="tab-icon" :src="active === 'home' ? '/static/tabs/home_active.png' : '/static/tabs/home.png'" mode="aspectFit"></image>
+			<view class="tab-item" @click="switchTab('home')" :class="{ active: active === 'home' }">
+				<image class="tab-icon"
+					:src="active === 'home' ? '/static/tabs/home_active.png' : '/static/tabs/home.png'"
+					mode="aspectFit"></image>
 				<text class="tab-text">首页</text>
 			</view>
-			<view class="tab-item" @click="switchTab('order')" :class="{active: active === 'order'}">
-				<image class="tab-icon" :src="active === 'order' ? '/static/tabs/order_active.png' : '/static/tabs/order.png'" mode="aspectFit"></image>
-				<text class="tab-text">下单</text>
+			<view class="tab-item" @click="switchTab('client')" :class="{ active: active === 'client' }">
+				<image class="tab-icon"
+					:src="active === 'client' ? '/static/tabs/client_active.png' : '/static/tabs/client.png'"
+					mode="aspectFit"></image>
+				<text class="tab-text">客户</text>
 			</view>
-			<view class="tab-item" @click="switchTab('mine')" :class="{active: active === 'mine'}">
-				<image class="tab-icon" :src="active === 'mine' ? '/static/tabs/mine_active.png' : '/static/tabs/mine.png'" mode="aspectFit"></image>
+			<view class="tab-item" @click="switchTab('mine')" :class="{ active: active === 'mine' }">
+				<image class="tab-icon"
+					:src="active === 'mine' ? '/static/tabs/mine_active.png' : '/static/tabs/mine.png'"
+					mode="aspectFit"></image>
 				<text class="tab-text">我的</text>
 			</view>
 		</view>
@@ -18,81 +24,76 @@
 </template>
 
 <script>
-	export default {
-		props: {
-			active: { type: String, default: 'order' }
-		},
-		data() {
-			return { }
-		},
-		methods: {
-			switchTab(code) {
-				let url = '';
-				switch(code) {
-					case 'home': url = '/pages/index/index'; break;
-					case 'order': url = '/pages/order/index'; break;
-					case 'mine': url = '/pages/mine/index'; break;
-				}
-				if(url) uni.reLaunch({ url });
+export default {
+	props: {
+		active: { type: String, default: 'client' }
+	},
+	methods: {
+		switchTab(code) {
+			let url = '';
+			switch (code) {
+				case 'home': url = '/pages/index/index'; break;
+				case 'client': url = '/pages/client/index'; break;
+				case 'mine': url = '/pages/mine/index'; break;
 			}
+			if (url) uni.reLaunch({ url });
 		}
 	}
+}
 </script>
 
 <style scoped>
-	.tab-bar-container { 
-		position: fixed; 
-		bottom: 0; 
-		left: 0; 
-		right: 0; 
-		z-index: 999;
-		display: flex;
-		flex-direction: column;
-		background: rgba(255, 255, 255, 0.98);
-		backdrop-filter: blur(10px);
-		box-shadow: 0 -4rpx 30rpx rgba(0, 0, 0, 0.06);
-		/* 确保底部安全区也被该背景色填充 */
-		padding-bottom: env(safe-area-inset-bottom);
-	}
+.tab-bar-container {
+	position: fixed;
+	bottom: 0;
+	left: 0;
+	right: 0;
+	z-index: 999;
+	display: flex;
+	flex-direction: column;
+	background: rgba(255, 255, 255, 0.98);
+	backdrop-filter: blur(10px);
+	box-shadow: 0 -4rpx 30rpx rgba(0, 0, 0, 0.06);
+	padding-bottom: env(safe-area-inset-bottom);
+}
 
-	.tab-bar-content {
-		position: relative;
-		z-index: 2;
-		height: 100rpx;
-		display: flex;
-		align-items: center;
-		justify-content: space-around;
-	}
+.tab-bar-content {
+	position: relative;
+	z-index: 2;
+	height: 100rpx;
+	display: flex;
+	align-items: center;
+	justify-content: space-around;
+}
 
-	.tab-item {
-		flex: 1;
-		display: flex;
-		flex-direction: column;
-		align-items: center;
-		justify-content: center;
-		height: 100%;
-	}
+.tab-item {
+	flex: 1;
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	justify-content: center;
+	height: 100%;
+}
 
-	.tab-icon {
-		width: 52rpx;
-		height: 52rpx;
-		margin-bottom: 4rpx;
-		display: block;
-	}
+.tab-icon {
+	width: 52rpx;
+	height: 52rpx;
+	margin-bottom: 4rpx;
+	display: block;
+}
 
-	.tab-text {
-		font-size: 22rpx;
-		color: #999;
-		line-height: 1;
-	}
+.tab-text {
+	font-size: 22rpx;
+	color: #999;
+	line-height: 1;
+}
 
-	.tab-item.active .tab-text {
-		color: #C1001C;
-		font-weight: 600;
-	}
-	
-	.tab-item.active .tab-icon {
-		/* 去掉微动效,防止加载感延迟 */
-		transform: none;
-	}
+.tab-item.active .tab-text {
+	color: #C1001C;
+	font-weight: 600;
+}
+
+.tab-item.active .tab-icon {
+	transform: none;
+}
 </style>

+ 4 - 0
pages.json

@@ -8,6 +8,10 @@
 			"path": "pages/login/index",
 			"style": { "navigationStyle": "custom" }
 		},
+		{
+			"path": "pages/client/index",
+			"style": { "navigationStyle": "custom" }
+		},
 		{
 			"path": "pages/order/index",
 			"style": { "navigationStyle": "custom" }

+ 371 - 0
pages/client/index.vue

@@ -0,0 +1,371 @@
+<template>
+    <view class="client-page">
+        <erp-nav-bar title="授权客户" :showBack="false" />
+
+        <view class="page-body">
+            <!-- 加载中 -->
+            <view class="loading-wrap" v-if="isLoading">
+                <text class="loading-text">加载中...</text>
+            </view>
+
+            <!-- 空状态 -->
+            <view class="empty-wrap" v-else-if="clientList.length === 0">
+                <view class="empty-icon-box">
+                    <text class="empty-icon">📋</text>
+                </view>
+                <text class="empty-title">暂无授权客户</text>
+                <text class="empty-desc">请联系管理员为您分配客户</text>
+            </view>
+
+            <!-- 客户卡片列表 -->
+            <view class="client-list" v-else>
+                <view class="client-card" v-for="(client, idx) in clientList" :key="client.rowId || idx"
+                    @click="goClientOrders(client)">
+                    <view class="card-header">
+                        <view class="card-title-row">
+                            <view class="card-index">{{ idx + 1 }}</view>
+                            <view class="card-name-group">
+                                <text class="card-name">{{ client.name || '-' }}</text>
+                                <text class="card-num" v-if="client.num">{{ client.num }}</text>
+                            </view>
+                        </view>
+                        <view class="card-tag" :class="client.stopUse === 0 ? 'tag-active' : 'tag-stop'">
+                            {{ client.stopUse === 0 ? '正常' : '停用' }}
+                        </view>
+                    </view>
+
+                    <view class="card-divider"></view>
+
+                    <view class="card-body">
+                        <view class="info-grid">
+                            <view class="info-item">
+                                <text class="info-label">分类</text>
+                                <text class="info-value">{{ client.clientClass || '-' }}</text>
+                            </view>
+                            <view class="info-item">
+                                <text class="info-label">业务员</text>
+                                <text class="info-value">{{ client.saleMan || '-' }}</text>
+                            </view>
+                        </view>
+
+                        <view class="contact-row" v-if="client.contactMan || client.contactTel || client.contactMobile">
+                            <image class="contact-icon" src="https://img.icons8.com/ios-glyphs/28/999999/phone--v1.png"
+                                mode="aspectFit"></image>
+                            <text class="contact-text">{{ client.contactMan || '' }} {{ client.contactMobile ||
+                                client.contactTel || '' }}</text>
+                        </view>
+
+                        <view class="addr-row" v-if="client.contactAddr">
+                            <image class="addr-icon" src="https://img.icons8.com/ios-glyphs/28/999999/marker--v1.png"
+                                mode="aspectFit"></image>
+                            <text class="addr-text">{{ client.contactAddr }}</text>
+                        </view>
+                    </view>
+
+                    <view class="card-footer" v-if="client.enterDate">
+                        <text class="footer-text">加入时间 {{ formatDate(client.enterDate) }}</text>
+                    </view>
+                </view>
+            </view>
+
+            <!-- 底部安全距离 -->
+            <view class="safe-bottom"></view>
+        </view>
+
+        <erp-tab-bar active="client"></erp-tab-bar>
+    </view>
+</template>
+
+<script>
+import ErpNavBar from '@/components/erp-nav-bar.vue';
+import ErpTabBar from '@/components/erp-tab-bar.vue';
+import { getMyInfo } from '@/api/system/employee.js';
+import { getClientByIds } from '@/api/erp/client.js';
+
+export default {
+    components: { ErpNavBar, ErpTabBar },
+    data() {
+        return {
+            isLoading: true,
+            clientList: []
+        }
+    },
+    computed: {},
+    async onLoad() {
+        await this.loadClients();
+    },
+    methods: {
+        async loadClients() {
+            try {
+                const infoRes = await getMyInfo();
+                const ids = infoRes.data?.authClientFRowIDs;
+                if (ids) {
+                    const clientRes = await getClientByIds(ids);
+                    this.clientList = clientRes.data || [];
+                }
+            } catch (e) {
+                console.error('加载客户列表失败', e);
+                uni.showToast({ title: e || '加载失败', icon: 'none' });
+            } finally {
+                this.isLoading = false;
+            }
+        },
+        formatDate(dateStr) {
+            if (!dateStr) return '';
+            const d = new Date(dateStr.replace(/-/g, '/'));
+            const y = d.getFullYear();
+            const m = String(d.getMonth() + 1).padStart(2, '0');
+            const day = String(d.getDate()).padStart(2, '0');
+            return `${y}-${m}-${day}`;
+        },
+        goClientOrders(client) {
+            uni.navigateTo({
+                url: `/pages/order/list/index?clientId=${client.rowId}&clientName=${encodeURIComponent(client.name || '')}`
+            });
+        }
+    }
+}
+</script>
+
+<style scoped>
+.client-page {
+    width: 100vw;
+    min-height: 100vh;
+    background: #f5f6f8;
+    display: flex;
+    flex-direction: column;
+}
+
+.page-body {
+    flex: 1;
+    padding: 24rpx 32rpx 0;
+}
+
+/* ========== 加载 & 空状态 ========== */
+.loading-wrap {
+    display: flex;
+    justify-content: center;
+    padding: 120rpx 0;
+}
+
+.loading-text {
+    font-size: 28rpx;
+    color: #bbb;
+}
+
+.empty-wrap {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    padding: 140rpx 0;
+}
+
+.empty-icon-box {
+    width: 120rpx;
+    height: 120rpx;
+    border-radius: 50%;
+    background: #f0f1f5;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-bottom: 28rpx;
+}
+
+.empty-icon {
+    font-size: 56rpx;
+}
+
+.empty-title {
+    font-size: 30rpx;
+    font-weight: 600;
+    color: #555;
+    margin-bottom: 12rpx;
+}
+
+.empty-desc {
+    font-size: 26rpx;
+    color: #bbb;
+}
+
+/* ========== 客户卡片 ========== */
+.client-list {
+    display: flex;
+    flex-direction: column;
+    gap: 20rpx;
+}
+
+.client-card {
+    background: #fff;
+    border-radius: 20rpx;
+    padding: 28rpx 32rpx 0;
+    box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.03);
+    overflow: hidden;
+    transition: transform 0.15s;
+}
+
+.client-card:active {
+    transform: scale(0.98);
+}
+
+.card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: flex-start;
+}
+
+.card-title-row {
+    display: flex;
+    align-items: flex-start;
+    flex: 1;
+    min-width: 0;
+}
+
+.card-index {
+    width: 44rpx;
+    height: 44rpx;
+    border-radius: 12rpx;
+    background: #f0f1f5;
+    color: #888;
+    font-size: 22rpx;
+    font-weight: 700;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-right: 16rpx;
+    flex-shrink: 0;
+    margin-top: 2rpx;
+}
+
+.card-name-group {
+    flex: 1;
+    min-width: 0;
+}
+
+.card-name {
+    font-size: 32rpx;
+    font-weight: 600;
+    color: #1a1a1a;
+    display: block;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}
+
+.card-num {
+    font-size: 24rpx;
+    color: #b0b0b0;
+    display: block;
+    margin-top: 4rpx;
+}
+
+.card-tag {
+    font-size: 22rpx;
+    font-weight: 500;
+    padding: 6rpx 18rpx;
+    border-radius: 20rpx;
+    flex-shrink: 0;
+}
+
+.tag-active {
+    background: rgba(193, 0, 28, 0.06);
+    color: #C1001C;
+}
+
+.tag-stop {
+    background: #f5f5f5;
+    color: #bbb;
+}
+
+.card-divider {
+    height: 1rpx;
+    background: #f0f0f0;
+    margin: 22rpx 0;
+}
+
+.card-body {
+    padding-bottom: 4rpx;
+}
+
+.info-grid {
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+    gap: 20rpx 32rpx;
+}
+
+.info-item {
+    display: flex;
+    flex-direction: column;
+    gap: 6rpx;
+}
+
+.info-label {
+    font-size: 22rpx;
+    color: #bbb;
+}
+
+.info-value {
+    font-size: 26rpx;
+    color: #444;
+    font-weight: 500;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}
+
+.contact-row {
+    display: flex;
+    align-items: center;
+    margin-top: 20rpx;
+    padding-top: 20rpx;
+    border-top: 1rpx solid #f7f7f7;
+}
+
+.contact-icon {
+    width: 28rpx;
+    height: 28rpx;
+    margin-right: 10rpx;
+    flex-shrink: 0;
+}
+
+.contact-text {
+    font-size: 24rpx;
+    color: #888;
+}
+
+.addr-row {
+    display: flex;
+    align-items: center;
+    margin-top: 12rpx;
+}
+
+.addr-icon {
+    width: 28rpx;
+    height: 28rpx;
+    margin-right: 10rpx;
+    flex-shrink: 0;
+}
+
+.addr-text {
+    font-size: 24rpx;
+    color: #888;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}
+
+.card-footer {
+    border-top: 1rpx solid #f7f7f7;
+    padding: 18rpx 0;
+    margin-top: 20rpx;
+}
+
+.footer-text {
+    font-size: 22rpx;
+    color: #ccc;
+}
+
+/* ========== 底部 ========== */
+.safe-bottom {
+    height: 140rpx;
+}
+</style>

+ 531 - 0
pages/login/components/AccountLogin.vue

@@ -0,0 +1,531 @@
+<template>
+	<view class="account-login-container">
+		<view class="content-wrapper">
+			<!-- 欢迎语 -->
+			<view class="welcome-section">
+				<text class="welcome-title">欢迎回来</text>
+				<text class="welcome-desc">请输入您的账号密码登录系统</text>
+			</view>
+
+			<!-- 表单卡片 -->
+			<view class="form-card">
+				<view class="input-item">
+					<view class="input-icon-box">
+						<image class="input-icon" src="https://img.icons8.com/ios-glyphs/40/999999/user--v1.png"
+							mode="aspectFit"></image>
+					</view>
+					<input class="form-input" v-model="account" placeholder="手机号" type="number" maxlength="11" />
+				</view>
+
+				<view class="input-divider"></view>
+
+				<view class="input-item">
+					<view class="input-icon-box">
+						<image class="input-icon" src="https://img.icons8.com/ios-glyphs/40/999999/lock--v1.png"
+							mode="aspectFit"></image>
+					</view>
+					<input class="form-input" v-model="password" placeholder="密码" :password="!showPwd" maxlength="20" />
+					<view class="pwd-toggle" @click="showPwd = !showPwd">
+						<text class="pwd-toggle-text">{{ showPwd ? '隐藏' : '显示' }}</text>
+					</view>
+				</view>
+			</view>
+
+			<!-- 登录按钮 -->
+			<view class="login-btn-wrapper">
+				<view class="login-btn-shadow"></view>
+				<button class="login-btn" :class="{ 'is-disabled': !canSubmit }" :loading="submitting"
+					@click="handleLogin">
+					<text v-if="!submitting">登 录</text>
+				</button>
+			</view>
+
+			<!-- 协议 -->
+			<view class="agreement-box" @click="toggleAgreed">
+				<view class="agreement-check" :class="{ checked: isAgreed }">
+					<text v-if="isAgreed" class="check-mark">✓</text>
+				</view>
+				<text class="agreement-text">
+					已阅读并同意<text class="link" @click.stop="showProtocol('user')">《用户协议》</text>和<text class="link"
+						@click.stop="showProtocol('privacy')">《隐私政策》</text>
+				</text>
+			</view>
+		</view>
+
+		<!-- 页脚 -->
+		<view class="footer-section">
+			<text>ERP Order System</text>
+		</view>
+
+		<!-- ========== 弹窗 ========== -->
+		<view class="global-mask" v-if="activeModal" @click="closeAllModals"></view>
+
+		<!-- 协议拦截弹窗 -->
+		<view class="dialog-overlay" v-if="activeModal === 'confirm'">
+			<view class="dialog-card">
+				<view class="dialog-icon-wrap">
+					<text class="dialog-icon">📋</text>
+				</view>
+				<text class="dialog-title">服务协议提示</text>
+				<text class="dialog-body">请您阅读并同意我们的协议内容,以便为您提供更安全的服务体验。</text>
+				<view class="dialog-btns">
+					<view class="dialog-btn cancel" @click="activeModal = ''">拒绝</view>
+					<view class="dialog-btn confirm" @click="agreeAndClose">同意并继续</view>
+				</view>
+			</view>
+		</view>
+
+		<!-- 协议富文本弹窗 -->
+		<view class="dialog-overlay" v-if="activeModal === 'protocol'">
+			<view class="dialog-card dialog-protocol">
+				<view class="dialog-header">
+					<text class="dialog-header-title">{{ currentProtocol.title }}</text>
+					<view class="dialog-close" @click="activeModal = ''">
+						<text class="dialog-close-text">✕</text>
+					</view>
+				</view>
+				<scroll-view scroll-y class="dialog-scroll">
+					<rich-text :nodes="currentProtocol.content"></rich-text>
+				</scroll-view>
+				<view class="dialog-bottom">
+					<button class="dialog-confirm-btn" @click="activeModal = ''">我已了解</button>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+import { getAgreement } from '@/api/system/agreement.js';
+import { login } from '@/api/auth.js';
+
+export default {
+	name: 'AccountLogin',
+	emits: ['login-success'],
+	data() {
+		return {
+			account: '',
+			password: '',
+			showPwd: false,
+			submitting: false,
+			isAgreed: false,
+			activeModal: '',
+			currentProtocol: { title: '', content: '' },
+			protocols: {
+				user: { title: '', content: '' },
+				privacy: { title: '', content: '' }
+			}
+		}
+	},
+	computed: {
+		canSubmit() {
+			return this.account.trim().length === 11 && this.password.trim().length >= 6;
+		}
+	},
+	methods: {
+		toggleAgreed() {
+			this.isAgreed = !this.isAgreed;
+		},
+		async handleLogin() {
+			if (!this.canSubmit) return;
+			if (!this.isAgreed) {
+				this.activeModal = 'confirm';
+				return;
+			}
+			this.submitting = true;
+			try {
+				const res = await login({
+					username: this.account.trim(),
+					password: this.password,
+					grantType: 'password'
+				});
+				if (res.data && res.data.access_token) {
+					uni.setStorageSync('token', res.data.access_token);
+					uni.setStorageSync('isLogin', true);
+					uni.showToast({ title: '登录成功', icon: 'success' });
+					setTimeout(() => {
+						uni.reLaunch({ url: '/pages/order/index' });
+					}, 800);
+				} else {
+					uni.showToast({ title: '登录失败,请检查账号密码', icon: 'none' });
+				}
+			} catch (e) {
+				console.error('登录错误:', e);
+				uni.showToast({ title: e?.msg || e || '登录失败', icon: 'none' });
+			} finally {
+				this.submitting = false;
+			}
+		},
+		agreeAndClose() {
+			this.isAgreed = true;
+			this.activeModal = '';
+		},
+		showProtocol(type) {
+			this.currentProtocol = this.protocols[type];
+			this.activeModal = 'protocol';
+		},
+		closeAllModals() {
+			this.activeModal = '';
+		}
+	},
+	async mounted() {
+		try {
+			const [userRes, privacyRes] = await Promise.all([
+				getAgreement(1),
+				getAgreement(2)
+			]);
+			this.protocols.user = { title: userRes.data.title, content: userRes.data.content };
+			this.protocols.privacy = { title: privacyRes.data.title, content: privacyRes.data.content };
+		} catch (e) {
+			console.error('[协议] 加载失败', e);
+		}
+	}
+}
+</script>
+
+<style scoped>
+.account-login-container {
+	width: 100%;
+	min-height: 100vh;
+	display: flex;
+	flex-direction: column;
+}
+
+.content-wrapper {
+	flex: 1;
+	padding: 60rpx 56rpx 0;
+	display: flex;
+	flex-direction: column;
+}
+
+/* ========== 欢迎语 ========== */
+.welcome-section {
+	margin-bottom: 56rpx;
+}
+
+.welcome-title {
+	font-size: 52rpx;
+	font-weight: 700;
+	color: #1a1a1a;
+	display: block;
+	line-height: 1.3;
+}
+
+.welcome-desc {
+	font-size: 28rpx;
+	color: #999;
+	display: block;
+	margin-top: 12rpx;
+	letter-spacing: 1rpx;
+}
+
+/* ========== 表单卡片 ========== */
+.form-card {
+	background: #fff;
+	border-radius: 24rpx;
+	box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.04);
+	padding: 16rpx 32rpx;
+}
+
+.input-item {
+	display: flex;
+	align-items: center;
+	height: 104rpx;
+}
+
+.input-icon-box {
+	width: 52rpx;
+	height: 52rpx;
+	border-radius: 16rpx;
+	background: #f5f6fa;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	margin-right: 20rpx;
+	flex-shrink: 0;
+}
+
+.input-icon {
+	width: 28rpx;
+	height: 28rpx;
+}
+
+.form-input {
+	flex: 1;
+	font-size: 30rpx;
+	color: #1a1a1a;
+	height: 100%;
+}
+
+.input-divider {
+	height: 1rpx;
+	background: #f0f0f0;
+	margin: 0 8rpx;
+}
+
+.pwd-toggle {
+	flex-shrink: 0;
+	padding: 8rpx 0 8rpx 20rpx;
+}
+
+.pwd-toggle-text {
+	font-size: 24rpx;
+	color: #b0b0b0;
+}
+
+/* ========== 登录按钮 ========== */
+.login-btn-wrapper {
+	position: relative;
+	margin-top: 48rpx;
+}
+
+.login-btn-shadow {
+	position: absolute;
+	bottom: -6rpx;
+	left: 6%;
+	width: 88%;
+	height: 100rpx;
+	border-radius: 50rpx;
+	background: rgba(193, 0, 28, 0.18);
+	filter: blur(16rpx);
+}
+
+.login-btn {
+	position: relative;
+	width: 100%;
+	height: 100rpx;
+	background: linear-gradient(135deg, #C1001C, #e0243f);
+	border-radius: 50rpx;
+	color: #fff;
+	font-size: 34rpx;
+	font-weight: 600;
+	letter-spacing: 16rpx;
+	border: none;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	transition: all 0.3s ease;
+	box-shadow: 0 8rpx 24rpx rgba(193, 0, 28, 0.3);
+}
+
+.login-btn::after {
+	border: none;
+}
+
+.login-btn.is-disabled {
+	opacity: 0.45;
+	box-shadow: none;
+}
+
+/* ========== 协议 ========== */
+.agreement-box {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	margin-top: 40rpx;
+}
+
+.agreement-check {
+	width: 34rpx;
+	height: 34rpx;
+	border-radius: 50%;
+	border: 2rpx solid #d0d0d0;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	margin-right: 12rpx;
+	flex-shrink: 0;
+	transition: all 0.2s ease;
+}
+
+.agreement-check.checked {
+	background: #C1001C;
+	border-color: #C1001C;
+}
+
+.check-mark {
+	color: #fff;
+	font-size: 20rpx;
+	font-weight: bold;
+}
+
+.agreement-text {
+	font-size: 24rpx;
+	color: #b0b0b0;
+	line-height: 1.5;
+}
+
+.link {
+	color: #576b95;
+}
+
+/* ========== 页脚 ========== */
+.footer-section {
+	text-align: center;
+	padding: 48rpx 0;
+}
+
+.footer-section text {
+	font-size: 22rpx;
+	color: #d0d0d0;
+	letter-spacing: 2rpx;
+}
+
+/* ========== 弹窗通用 ========== */
+.global-mask {
+	position: fixed;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	background: rgba(0, 0, 0, 0.45);
+	z-index: 998;
+}
+
+.dialog-overlay {
+	position: fixed;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	z-index: 1000;
+	padding: 60rpx;
+}
+
+.dialog-card {
+	width: 100%;
+	background: #fff;
+	border-radius: 28rpx;
+	padding: 52rpx 40rpx 36rpx;
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+}
+
+.dialog-protocol {
+	padding: 0;
+	align-items: stretch;
+	max-height: 70vh;
+}
+
+.dialog-icon-wrap {
+	width: 80rpx;
+	height: 80rpx;
+	border-radius: 50%;
+	background: #fff5f5;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	margin-bottom: 28rpx;
+}
+
+.dialog-icon {
+	font-size: 40rpx;
+}
+
+.dialog-title {
+	font-size: 34rpx;
+	font-weight: 600;
+	color: #1a1a1a;
+	margin-bottom: 16rpx;
+	text-align: center;
+}
+
+.dialog-body {
+	font-size: 28rpx;
+	color: #888;
+	line-height: 1.7;
+	text-align: center;
+	margin-bottom: 40rpx;
+}
+
+.dialog-btns {
+	width: 100%;
+	display: flex;
+	gap: 20rpx;
+}
+
+.dialog-btn {
+	flex: 1;
+	height: 88rpx;
+	border-radius: 44rpx;
+	font-size: 30rpx;
+	font-weight: 500;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+
+.dialog-btn.cancel {
+	background: #f5f5f5;
+	color: #999;
+}
+
+.dialog-btn.confirm {
+	background: #1a1a1a;
+	color: #fff;
+}
+
+/* ========== 协议内容弹窗 ========== */
+.dialog-header {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	padding: 36rpx 40rpx 24rpx;
+	border-bottom: 1rpx solid #f5f5f5;
+}
+
+.dialog-header-title {
+	font-size: 34rpx;
+	font-weight: 600;
+	color: #1a1a1a;
+}
+
+.dialog-close {
+	width: 48rpx;
+	height: 48rpx;
+	border-radius: 50%;
+	background: #f5f5f5;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+
+.dialog-close-text {
+	font-size: 26rpx;
+	color: #999;
+}
+
+.dialog-scroll {
+	max-height: 50vh;
+	padding: 28rpx 40rpx;
+	color: #555;
+	font-size: 28rpx;
+	line-height: 1.8;
+}
+
+.dialog-bottom {
+	padding: 24rpx 40rpx 36rpx;
+}
+
+.dialog-confirm-btn {
+	width: 100%;
+	height: 88rpx;
+	background: #1a1a1a;
+	color: #fff;
+	border-radius: 44rpx;
+	font-size: 30rpx;
+	font-weight: 500;
+	border: none;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+
+.dialog-confirm-btn::after {
+	border: none;
+}
+</style>

+ 682 - 0
pages/login/components/WechatLogin.vue

@@ -0,0 +1,682 @@
+<template>
+	<view class="wechat-login-container">
+		<view class="content-wrapper">
+			<view class="action-section">
+				<button class="main-btn" @click="startLoginFlow">
+					<image class="btn-icon" :src="assets.wechat" mode="aspectFit"></image>
+					<text>授权手机号码登录</text>
+				</button>
+
+				<view class="agreement-box">
+					<label class="checkbox-label" @click="toggleAgreed">
+						<checkbox :checked="isAgreed" color="#C1001C" style="transform:scale(0.7)" />
+						<text class="agreement-text">我已阅读并同意
+							<text class="link" @click.stop="showProtocol('user')">《用户协议》</text> 与
+							<text class="link" @click.stop="showProtocol('privacy')">《隐私政策》</text>
+						</text>
+					</label>
+				</view>
+			</view>
+
+			<view class="footer-section">
+				<text>© 2026 ERP Order System. All Rights Reserved.</text>
+			</view>
+		</view>
+
+		<view class="global-mask" v-if="activeModal" @click="closeAllModals"></view>
+
+		<!-- 协议拦截确认弹窗 -->
+		<view class="confirm-modal center-card" v-if="activeModal === 'confirm'">
+			<view class="card-title">服务协议提示</view>
+			<view class="card-body">请您阅读并同意我们的协议内容,以便为您提供更安全的服务体验。</view>
+			<view class="card-footer-btns">
+				<view class="btn-item cancel" @click="activeModal = ''">拒绝</view>
+				<view class="btn-item agree" @click="agreeAndClose">同意并继续</view>
+			</view>
+		</view>
+
+		<!-- 头像昵称授权弹窗 -->
+		<view class="simulated-profile-pop bottom-pop" v-if="activeModal === 'profile'">
+			<view class="pop-header-bar">
+				<text class="pop-cancel" @click="activeModal = ''">取消</text>
+				<text class="pop-main-title">获取头像昵称</text>
+				<text class="pop-done" @click="activeModal = 'phone'">保存</text>
+			</view>
+			<view class="profile-edit-content">
+				<view class="avatar-edit-box">
+					<button class="avatar-wrapper-btn" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
+						<image class="current-avatar"
+							:src="avatarPreviewUrl || 'https://img.icons8.com/color/144/user.png'"></image>
+						<view class="camera-icon">
+							<image src="https://img.icons8.com/ios-glyphs/30/999999/camera.png" mode="aspectFit">
+							</image>
+						</view>
+					</button>
+					<text class="edit-hint">点击修改头像</text>
+				</view>
+				<view class="nickname-edit-box">
+					<text class="label">昵称</text>
+					<input class="nickname-input" type="nickname" :value="userName" placeholder="请输入昵称"
+						@blur="onNicknameBlur" @input="onNicknameChange" />
+				</view>
+				<view class="auth-notice-box">
+					<text class="notice-text">授权后,开发者将获得您的头像和昵称,用于展示您的个人资料。</text>
+				</view>
+			</view>
+			<view class="bottom-action">
+				<button class="confirm-btn-fixed" @click="goToPhoneAuth">确定</button>
+			</view>
+		</view>
+
+		<!-- 手机号授权弹窗 -->
+		<view class="phone-auth-pop bottom-pop" v-if="activeModal === 'phone'">
+			<view class="p-header">
+				<image class="p-mini-logo" :src="assets.logo" mode="aspectFill"></image>
+				<text class="p-app-name">ERP 智能下单系统 申请</text>
+			</view>
+			<view class="p-body">
+				<text class="p-title">获取您的手机号</text>
+				<text class="p-phone-hint">是否允许我们获取您的手机号,用于登录和订单通知?</text>
+			</view>
+			<view class="p-footer-btns">
+				<button class="p-btn-fixed p-deny" @click="activeModal = ''">拒绝</button>
+				<button class="p-btn-fixed p-allow" open-type="getPhoneNumber"
+					@getphonenumber="handleGetPhoneNumber">允许</button>
+			</view>
+		</view>
+
+		<!-- 协议富文本弹窗 -->
+		<view class="protocol-modal center-card" v-if="activeModal === 'protocol'">
+			<view class="p-pop-header">
+				<text class="p-pop-title">{{ currentProtocol.title }}</text>
+				<text class="p-pop-close" @click="activeModal = ''">×</text>
+			</view>
+			<scroll-view scroll-y class="p-pop-scroll">
+				<view class="rich-text-wrapper">
+					<rich-text :nodes="currentProtocol.content"></rich-text>
+				</view>
+			</scroll-view>
+			<view class="p-pop-footer">
+				<button class="p-pop-btn" @click="activeModal = ''">我已了解</button>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+import assets from '@/utils/assets.js';
+import { getAgreement } from '@/api/system/agreement.js';
+import { login, getWechatPhone, wechatRegister } from '@/api/auth.js';
+import { uploadFile } from '@/api/resource/oss.js';
+
+export default {
+	name: 'WechatLogin',
+	emits: ['login-success'],
+	data() {
+		return {
+			assets,
+			isAgreed: false,
+			activeModal: '',
+			avatarOssId: null,
+			avatarPreviewUrl: '',
+			userName: '微信用户',
+			currentProtocol: { title: '', content: '' },
+			protocols: {
+				user: { title: '', content: '' },
+				privacy: { title: '', content: '' }
+			},
+			openId: '',
+			unionId: '',
+			phoneNumber: ''
+		}
+	},
+	methods: {
+		toggleAgreed() {
+			this.isAgreed = !this.isAgreed;
+		},
+		startLoginFlow() {
+			if (!this.isAgreed) {
+				this.activeModal = 'confirm';
+			} else {
+				this.performLogin();
+			}
+		},
+		agreeAndClose() {
+			this.isAgreed = true;
+			this.activeModal = '';
+		},
+		async performLogin() {
+			try {
+				uni.showLoading({ title: '登录中...' });
+
+				const loginRes = await new Promise((resolve, reject) => {
+					wx.login({ success: resolve, fail: reject });
+				});
+
+				if (!loginRes.code) {
+					uni.hideLoading();
+					uni.showToast({ title: '获取登录凭证失败', icon: 'none' });
+					return;
+				}
+
+				const res = await login({ loginCode: loginRes.code, grantType: 'wechatApplet' });
+				uni.hideLoading();
+
+				if (res.data && res.data.access_token) {
+					uni.setStorageSync('token', res.data.access_token);
+					uni.setStorageSync('isLogin', true);
+					uni.showToast({ title: '登录成功', icon: 'success' });
+					setTimeout(() => {
+						uni.reLaunch({ url: '/pages/order/index' });
+					}, 1000);
+				} else if (res.data && res.data.openid) {
+					this.openId = res.data.openid;
+					this.unionId = res.data.unionid || '';
+					this.activeModal = 'profile';
+				} else {
+					uni.showToast({ title: '登录失败', icon: 'none' });
+				}
+			} catch (error) {
+				uni.hideLoading();
+				console.error('登录错误:', error);
+				uni.showToast({ title: error || '登录失败', icon: 'none' });
+			}
+		},
+		async onChooseAvatar(e) {
+			const tempPath = e.detail.avatarUrl;
+			console.log('[微信信息] 头像临时路径:', tempPath);
+			this.avatarPreviewUrl = tempPath;
+			try {
+				uni.showLoading({ title: '上传头像...' });
+				const res = await uploadFile(tempPath);
+				uni.hideLoading();
+				this.avatarOssId = res.ossId;
+				this.avatarPreviewUrl = res.url;
+				console.log('[微信信息] 头像OSS上传成功, ossId:', this.avatarOssId);
+			} catch (err) {
+				uni.hideLoading();
+				console.error('[微信信息] 头像上传失败:', err);
+				uni.showToast({ title: err || '头像上传失败', icon: 'none' });
+			}
+		},
+		onNicknameBlur(e) {
+			this.userName = e.detail.value;
+			console.log('[微信信息] 昵称(blur):', this.userName);
+		},
+		onNicknameChange(e) {
+			this.userName = e.detail.value;
+			console.log('[微信信息] 昵称(input):', this.userName);
+		},
+		async goToPhoneAuth() {
+			this.phoneNumber = '';
+			this.activeModal = 'phone';
+		},
+		async handleGetPhoneNumber(e) {
+			if (e.detail.errMsg !== 'getPhoneNumber:ok') {
+				uni.showToast({ title: '获取手机号失败,请重试', icon: 'none' });
+				return;
+			}
+
+			try {
+				uni.showLoading({ title: '获取手机号中...' });
+
+				const phoneRes = await getWechatPhone({
+					phoneCode: e.detail.code,
+					openId: this.openId
+				});
+
+				uni.hideLoading();
+
+				this.phoneNumber = phoneRes.data;
+
+				uni.showLoading({ title: '注册中...' });
+
+				const registerRes = await wechatRegister({
+					openId: this.openId,
+					unionId: this.unionId,
+					phone: this.phoneNumber,
+					nickname: this.userName,
+					avatar: this.avatarOssId
+				});
+
+				uni.hideLoading();
+
+				this.performLogin();
+			} catch (error) {
+				uni.hideLoading();
+				console.error('注册错误:', error);
+				uni.showToast({ title: error || '注册失败', icon: 'none' });
+			}
+		},
+		showProtocol(type) {
+			this.currentProtocol = this.protocols[type];
+			this.activeModal = 'protocol';
+		},
+		closeAllModals() {
+			this.activeModal = '';
+		}
+	},
+	async mounted() {
+		try {
+			const [userRes, privacyRes] = await Promise.all([
+				getAgreement(1),
+				getAgreement(2)
+			]);
+			this.protocols.user = { title: userRes.data.title, content: userRes.data.content };
+			this.protocols.privacy = { title: privacyRes.data.title, content: privacyRes.data.content };
+		} catch (e) {
+			console.error('[协议] 加载失败', e);
+			uni.showToast({ title: e || '加载协议失败', icon: 'none' });
+		}
+	}
+}
+</script>
+
+<style scoped>
+.wechat-login-container {
+	width: 100%;
+	position: relative;
+	display: flex;
+	flex-direction: column;
+}
+
+.content-wrapper {
+	position: relative;
+	z-index: 2;
+	flex: 1;
+	display: flex;
+	flex-direction: column;
+	padding: 40rpx 80rpx 0;
+	box-sizing: border-box;
+}
+
+.main-btn {
+	width: 100%;
+	height: 100rpx;
+	background: linear-gradient(135deg, #C1001C 0%, #FF4D4F 100%);
+	border-radius: 50rpx;
+	color: #fff;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	font-size: 32rpx;
+	font-weight: bold;
+	box-shadow: 0 12rpx 30rpx rgba(193, 0, 28, 0.2);
+	border: none;
+	margin-bottom: 40rpx;
+}
+
+.btn-icon {
+	width: 48rpx;
+	height: 48rpx;
+	margin-right: 16rpx;
+}
+
+.agreement-text {
+	font-size: 24rpx;
+	color: #999;
+}
+
+.link {
+	color: #C1001C;
+	margin: 0 4rpx;
+	font-weight: 500;
+}
+
+.footer-section {
+	margin-top: auto;
+	padding-bottom: 60rpx;
+	text-align: center;
+	font-size: 20rpx;
+	color: #dcdcdc;
+}
+
+/* ========== 弹窗通用 ========== */
+.global-mask {
+	position: fixed;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	background: rgba(0, 0, 0, 0.5);
+	z-index: 998;
+}
+
+.center-card {
+	position: fixed;
+	top: 50%;
+	left: 50%;
+	transform: translate(-50%, -50%);
+	width: 620rpx;
+	background: #fff;
+	border-radius: 32rpx;
+	z-index: 1000;
+	box-shadow: 0 30rpx 80rpx rgba(0, 0, 0, 0.15);
+	padding: 50rpx 40rpx;
+	display: flex;
+	flex-direction: column;
+}
+
+.bottom-pop {
+	position: fixed;
+	bottom: 0;
+	left: 0;
+	right: 0;
+	background: #fff;
+	border-radius: 40rpx 40rpx 0 0;
+	z-index: 1001;
+	padding: 40rpx;
+	padding-bottom: calc(50rpx + env(safe-area-inset-bottom));
+	box-shadow: 0 -10rpx 40rpx rgba(0, 0, 0, 0.05);
+}
+
+button {
+	display: flex !important;
+	align-items: center !important;
+	justify-content: center !important;
+	padding: 0 !important;
+	line-height: normal !important;
+}
+
+button::after {
+	border: none;
+}
+
+/* ========== 协议拦截弹窗 ========== */
+.card-title {
+	font-size: 38rpx;
+	font-weight: bold;
+	text-align: center;
+	margin-bottom: 30rpx;
+}
+
+.card-body {
+	font-size: 28rpx;
+	color: #666;
+	line-height: 1.6;
+	text-align: center;
+	margin-bottom: 50rpx;
+}
+
+.card-footer-btns {
+	display: flex;
+	gap: 24rpx;
+}
+
+.btn-item {
+	flex: 1;
+	height: 90rpx;
+	border-radius: 45rpx;
+	font-size: 30rpx;
+	display: flex !important;
+	align-items: center !important;
+	justify-content: center !important;
+	text-align: center;
+	line-height: 90rpx;
+}
+
+.btn-item.cancel {
+	background: #f8f8f8;
+	color: #999;
+}
+
+.btn-item.agree {
+	background: #C1001C;
+	color: #fff;
+	font-weight: bold;
+}
+
+/* ========== 协议内容弹窗 ========== */
+.p-pop-header {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	margin-bottom: 30rpx;
+}
+
+.p-pop-title {
+	font-size: 36rpx;
+	font-weight: bold;
+	color: #1a1a1a;
+}
+
+.p-pop-close {
+	font-size: 48rpx;
+	color: #ccc;
+	padding: 10rpx;
+}
+
+.p-pop-scroll {
+	max-height: 55vh;
+	margin-bottom: 30rpx;
+}
+
+.rich-text-wrapper {
+	padding: 10rpx 0;
+	color: #444;
+	font-size: 28rpx;
+}
+
+.p-pop-footer {
+	padding-top: 20rpx;
+}
+
+.p-pop-btn {
+	width: 100%;
+	height: 90rpx;
+	background: #C1001C;
+	color: #fff;
+	border-radius: 45rpx;
+	font-size: 30rpx;
+	font-weight: bold;
+}
+
+/* ========== 头像授权弹窗 ========== */
+.pop-header-bar {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	margin-bottom: 60rpx;
+}
+
+.pop-cancel {
+	font-size: 30rpx;
+	color: #999;
+}
+
+.pop-main-title {
+	font-size: 32rpx;
+	font-weight: bold;
+}
+
+.pop-done {
+	font-size: 30rpx;
+	color: #C1001C;
+	font-weight: bold;
+}
+
+.profile-edit-content {
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+}
+
+.avatar-wrapper-btn {
+	width: 170rpx;
+	height: 170rpx;
+	border-radius: 85rpx;
+	background: #f8f8f8;
+	position: relative;
+	margin-bottom: 24rpx;
+	padding: 0 !important;
+	overflow: visible;
+	display: flex !important;
+	align-items: center;
+	justify-content: center;
+	border: none;
+}
+
+.current-avatar {
+	width: 100%;
+	height: 100%;
+	border-radius: 85rpx;
+	border: 4rpx solid #fff;
+	box-shadow: 0 4rpx 15rpx rgba(0, 0, 0, 0.05);
+}
+
+.camera-icon {
+	position: absolute;
+	bottom: 0;
+	right: 0;
+	background: #fff;
+	width: 56rpx;
+	height: 56rpx;
+	border-radius: 28rpx;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.12);
+	z-index: 5;
+}
+
+.camera-icon image {
+	width: 30rpx;
+	height: 30rpx;
+}
+
+.edit-hint {
+	font-size: 24rpx;
+	color: #999;
+	margin-bottom: 70rpx;
+	width: 100%;
+	text-align: center;
+	display: block;
+}
+
+.nickname-edit-box {
+	width: 100%;
+	display: flex;
+	align-items: center;
+	padding: 36rpx 0;
+	border-top: 1rpx solid #f0f0f0;
+	border-bottom: 1rpx solid #f0f0f0;
+	margin-bottom: 40rpx;
+}
+
+.nickname-edit-box .label {
+	width: 130rpx;
+	font-size: 32rpx;
+}
+
+.nickname-input {
+	flex: 1;
+	font-size: 32rpx;
+}
+
+.notice-text {
+	font-size: 24rpx;
+	color: #bfbfbf;
+	text-align: center;
+	display: block;
+	margin-bottom: 60rpx;
+}
+
+.confirm-btn-fixed {
+	width: 100%;
+	height: 96rpx;
+	background: #C1001C;
+	color: #fff;
+	border-radius: 16rpx;
+	font-size: 32rpx;
+	font-weight: bold;
+}
+
+/* ========== 手机号授权弹窗 ========== */
+.p-header {
+	display: flex;
+	align-items: center;
+	margin-bottom: 50rpx;
+}
+
+.p-mini-logo {
+	width: 44rpx;
+	height: 44rpx;
+	border-radius: 8rpx;
+	margin-right: 16rpx;
+}
+
+.p-app-name {
+	font-size: 28rpx;
+	color: #7f7f7f;
+}
+
+.p-title {
+	font-size: 40rpx;
+	font-weight: bold;
+	color: #000;
+	margin-bottom: 44rpx;
+	display: block;
+}
+
+.p-phone-hint {
+	font-size: 30rpx;
+	color: #666;
+	line-height: 1.5;
+	display: block;
+	margin-bottom: 60rpx;
+}
+
+.p-number-card {
+	background: #fbfbfb;
+	padding: 36rpx;
+	border-radius: 20rpx;
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	margin-bottom: 30rpx;
+	border: 1rpx solid #f0f0f0;
+}
+
+.p-real-num {
+	font-size: 36rpx;
+	font-weight: bold;
+	color: #1a1a1a;
+	display: block;
+}
+
+.p-num-hint {
+	font-size: 24rpx;
+	color: #999;
+}
+
+.p-other-link {
+	font-size: 28rpx;
+	color: #576b95;
+	display: block;
+	margin-bottom: 60rpx;
+}
+
+.p-footer-btns {
+	display: flex;
+	gap: 30rpx;
+}
+
+.p-btn-fixed {
+	flex: 1;
+	height: 96rpx;
+	border-radius: 16rpx;
+	font-size: 32rpx;
+	border: none;
+}
+
+.p-deny {
+	background: #f2f2f2;
+	color: #C1001C;
+}
+
+.p-allow {
+	background: #C1001C;
+	color: #fff;
+	font-weight: bold;
+}
+</style>

+ 18 - 661
pages/login/index.vue

@@ -1,10 +1,8 @@
 <template>
-	<view class="login-container">
-		<!-- 顶部高级渐变背景 -->
+	<view class="login-page">
 		<view class="gradient-bg"></view>
 
 		<view class="content-wrapper">
-			<!-- Logo 区域 -->
 			<view class="logo-section">
 				<view class="logo-outer">
 					<image class="logo-img" :src="assets.logo" mode="aspectFill"></image>
@@ -13,281 +11,36 @@
 				<text class="app-subtitle">简洁 · 高效 · 数字化管理</text>
 			</view>
 
-			<!-- 按钮区域 -->
-			<view class="action-section">
-				<button class="main-btn" @click="startLoginFlow">
-					<image class="btn-icon" :src="assets.wechat" mode="aspectFit"></image>
-					<text>授权手机号码登录</text>
-				</button>
-
-				<view class="agreement-box">
-					<label class="checkbox-label" @click="toggleAgreed">
-						<checkbox :checked="isAgreed" color="#C1001C" style="transform:scale(0.7)" />
-						<text class="agreement-text">我已阅读并同意
-							<text class="link" @click.stop="showProtocol('user')">《用户协议》</text> 与
-							<text class="link" @click.stop="showProtocol('privacy')">《隐私政策》</text>
-						</text>
-					</label>
-				</view>
-			</view>
-
-			<!-- 页脚 -->
-			<view class="footer-section">
-				<text>© 2026 ERP Order System. All Rights Reserved.</text>
-			</view>
-		</view>
-
-		<!-- 全局遮罩 -->
-		<view class="global-mask" v-if="activeModal" @click="closeAllModals"></view>
-
-		<!-- 1. 协议拦截确认弹窗 -->
-		<view class="confirm-modal center-card" v-if="activeModal === 'confirm'">
-			<view class="card-title">服务协议提示</view>
-			<view class="card-body">请您阅读并同意我们的协议内容,以便为您提供更安全的服务体验。</view>
-			<view class="card-footer-btns">
-				<view class="btn-item cancel" @click="activeModal = ''">拒绝</view>
-				<view class="btn-item agree" @click="agreeAndClose">同意并继续</view>
-			</view>
-		</view>
-
-		<!-- 2. 头像昵称授权弹窗 (原生能力适配) -->
-		<view class="simulated-profile-pop bottom-pop" v-if="activeModal === 'profile'">
-			<view class="pop-header-bar">
-				<text class="pop-cancel" @click="activeModal = ''">取消</text>
-				<text class="pop-main-title">获取头像昵称</text>
-				<text class="pop-done" @click="activeModal = 'phone'">保存</text>
-			</view>
-			<view class="profile-edit-content">
-				<view class="avatar-edit-box">
-					<!-- 使用微信原生头像选择能力 -->
-					<button class="avatar-wrapper-btn" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
-						<image class="current-avatar"
-							:src="avatarPreviewUrl || 'https://img.icons8.com/color/144/user.png'"></image>
-						<view class="camera-icon">
-							<image src="https://img.icons8.com/ios-glyphs/30/999999/camera.png" mode="aspectFit">
-							</image>
-						</view>
-					</button>
-					<text class="edit-hint">点击修改头像</text>
-				</view>
-				<view class="nickname-edit-box">
-					<text class="label">昵称</text>
-					<!-- 使用微信原生昵称填写能力 -->
-					<input class="nickname-input" type="nickname" :value="userName" placeholder="请输入昵称"
-						@blur="onNicknameBlur" @input="onNicknameChange" />
-				</view>
-				<view class="auth-notice-box">
-					<text class="notice-text">授权后,开发者将获得您的头像和昵称,用于展示您的个人资料。</text>
-				</view>
-			</view>
-			<view class="bottom-action">
-				<button class="confirm-btn-fixed" @click="goToPhoneAuth">确定</button>
-			</view>
-		</view>
-
-		<!-- 3. 模拟手机号授权 -->
-		<view class="phone-auth-pop bottom-pop" v-if="activeModal === 'phone'">
-			<view class="p-header">
-				<image class="p-mini-logo" :src="assets.logo" mode="aspectFill"></image>
-				<text class="p-app-name">ERP 智能下单系统 申请</text>
-			</view>
-			<view class="p-body">
-				<text class="p-title">获取您的手机号</text>
-				<text class="p-phone-hint">是否允许我们获取您的手机号,用于登录和订单通知?</text>
-			</view>
-			<view class="p-footer-btns">
-				<button class="p-btn-fixed p-deny" @click="activeModal = ''">拒绝</button>
-				<button class="p-btn-fixed p-allow" open-type="getPhoneNumber"
-					@getphonenumber="handleGetPhoneNumber">允许</button>
-			</view>
-		</view>
-
-		<!-- 4. 协议富文本弹窗 (样式专项修复) -->
-		<view class="protocol-modal center-card" v-if="activeModal === 'protocol'">
-			<view class="p-pop-header">
-				<text class="p-pop-title">{{ currentProtocol.title }}</text>
-				<text class="p-pop-close" @click="activeModal = ''">×</text>
-			</view>
-			<scroll-view scroll-y class="p-pop-scroll">
-				<view class="rich-text-wrapper">
-					<rich-text :nodes="currentProtocol.content"></rich-text>
-				</view>
-			</scroll-view>
-			<view class="p-pop-footer">
-				<button class="p-pop-btn" @click="activeModal = ''">我已了解</button>
-			</view>
+			<AccountLogin @login-success="onLoginSuccess" />
 		</view>
 	</view>
 </template>
 
 <script>
 import assets from '@/utils/assets.js';
-import { getAgreement } from '@/api/system/agreement.js';
-import { wechatLogin, getWechatPhone, wechatRegister } from '@/api/auth/index.js';
-import { uploadFile } from '@/api/resource/oss.js';
+import AccountLogin from './components/AccountLogin.vue';
+
 export default {
+	components: { AccountLogin },
 	data() {
 		return {
-			assets, isAgreed: false, activeModal: '',
-			avatarOssId: null, avatarPreviewUrl: '', userName: '微信用户',
-			currentProtocol: { title: '', content: '' },
-			protocols: {
-				user: { title: '', content: '' },
-				privacy: { title: '', content: '' }
-			},
-			openId: '',
-			unionId: '',
-			phoneNumber: ''
+			assets
 		}
 	},
 	methods: {
-		toggleAgreed() { this.isAgreed = !this.isAgreed; },
-		startLoginFlow() {
-			if (!this.isAgreed) this.activeModal = 'confirm';
-			else this.performLogin();
-		},
-		agreeAndClose() {
-			this.isAgreed = true;
-			this.activeModal = '';
-		},
-		async performLogin() {
-			try {
-				uni.showLoading({ title: '登录中...' });
-
-				const loginRes = await new Promise((resolve, reject) => {
-					wx.login({
-						success: resolve,
-						fail: reject
-					});
-				});
-
-				if (!loginRes.code) {
-					uni.hideLoading();
-					uni.showToast({ title: '获取登录凭证失败', icon: 'none' });
-					return;
-				}
-
-				const res = await wechatLogin({ loginCode: loginRes.code });
-
-				uni.hideLoading();
-
-				if (res.data && res.data.access_token) {
-					uni.setStorageSync('token', res.data.access_token);
-					uni.setStorageSync('isLogin', true);
-					uni.showToast({ title: '登录成功', icon: 'success' });
-					setTimeout(() => {
-						uni.reLaunch({ url: '/pages/order/index' });
-					}, 1000);
-				} else if (res.data && res.data.openid) {
-					this.openId = res.data.openid;
-					this.unionId = res.data.unionid || '';
-					this.activeModal = 'profile';
-				} else {
-					uni.showToast({ title: '登录失败', icon: 'none' });
-				}
-			} catch (error) {
-				uni.hideLoading();
-				console.error('登录错误:', error);
-				uni.showToast({ title: error || '登录失败', icon: 'none' });
-			}
-		},
-		async onChooseAvatar(e) {
-			const tempPath = e.detail.avatarUrl;
-			console.log('[微信信息] 头像临时路径:', tempPath);
-			this.avatarPreviewUrl = tempPath;
-			try {
-				uni.showLoading({ title: '上传头像...' });
-				const res = await uploadFile(tempPath);
-				uni.hideLoading();
-				this.avatarOssId = res.ossId;
-				this.avatarPreviewUrl = res.url;
-				console.log('[微信信息] 头像OSS上传成功, ossId:', this.avatarOssId);
-			} catch (err) {
-				uni.hideLoading();
-				console.error('[微信信息] 头像上传失败:', err);
-				uni.showToast({ title: err || '头像上传失败', icon: 'none' });
-			}
-		},
-		onNicknameBlur(e) {
-			this.userName = e.detail.value;
-			console.log('[微信信息] 昵称(blur):', this.userName);
-		},
-		onNicknameChange(e) {
-			this.userName = e.detail.value;
-			console.log('[微信信息] 昵称(input):', this.userName);
-		},
-		async goToPhoneAuth() {
-			this.phoneNumber = '';
-			this.activeModal = 'phone';
-		},
-		async handleGetPhoneNumber(e) {
-			if (e.detail.errMsg !== 'getPhoneNumber:ok') {
-				uni.showToast({ title: '获取手机号失败,请重试', icon: 'none' });
-				return;
-			}
-
-			try {
-				uni.showLoading({ title: '获取手机号中...' });
-
-				const phoneRes = await getWechatPhone({
-					phoneCode: e.detail.code,
-					openId: this.openId
-				});
-
-				uni.hideLoading();
-
-				this.phoneNumber = phoneRes.data;
-
-				uni.showLoading({ title: '注册中...' });
-
-				const registerRes = await wechatRegister({
-					openId: this.openId,
-					unionId: this.unionId,
-					phone: this.phoneNumber,
-					nickname: this.userName,
-					avatar: this.avatarOssId
-				});
-
-				uni.hideLoading();
-
-				this.performLogin();
-			} catch (error) {
-				uni.hideLoading();
-				console.error('注册错误:', error);
-				uni.showToast({ title: error || '注册失败', icon: 'none' });
-			}
-		},
-		showProtocol(type) {
-			this.currentProtocol = this.protocols[type];
-			this.activeModal = 'protocol';
-		},
-		closeAllModals() { this.activeModal = ''; }
-	},
-	async mounted() {
-		try {
-			const [userRes, privacyRes] = await Promise.all([
-				getAgreement(1),
-				getAgreement(2)
-			]);
-			this.protocols.user = { title: userRes.data.title, content: userRes.data.content };
-			this.protocols.privacy = { title: privacyRes.data.title, content: privacyRes.data.content };
-		} catch (e) {
-			console.error('[协议] 加载失败', e);
-			uni.showToast({ title: e || '加载协议失败', icon: 'none' });
+		onLoginSuccess() {
+			uni.reLaunch({ url: '/pages/order/index' });
 		}
 	}
 }
 </script>
 
 <style scoped>
-/* 基础容器 */
-.login-container {
+.login-page {
 	width: 100%;
 	min-height: 100vh;
 	background: #fff;
 	position: relative;
-	display: flex;
-	flex-direction: column;
 }
 
 .gradient-bg {
@@ -303,32 +56,27 @@ export default {
 .content-wrapper {
 	position: relative;
 	z-index: 2;
-	flex: 1;
-	display: flex;
-	flex-direction: column;
-	padding: 0 80rpx;
-	box-sizing: border-box;
 }
 
 .logo-section {
 	display: flex;
 	flex-direction: column;
 	align-items: center;
-	margin-top: 360rpx;
-	margin-bottom: 120rpx;
+	padding-top: 200rpx;
+	margin-bottom: 60rpx;
 }
 
 .logo-outer {
-	width: 200rpx;
-	height: 200rpx;
+	width: 140rpx;
+	height: 140rpx;
 	background: #fff;
-	border-radius: 48rpx;
-	box-shadow: 0 40rpx 80rpx rgba(193, 0, 28, 0.35), 0 10rpx 30rpx rgba(0, 0, 0, 0.1), inset 0 4rpx 10rpx rgba(255, 255, 255, 0.8);
+	border-radius: 36rpx;
+	box-shadow: 0 30rpx 60rpx rgba(193, 0, 28, 0.3), 0 8rpx 20rpx rgba(0, 0, 0, 0.08), inset 0 4rpx 10rpx rgba(255, 255, 255, 0.8);
 	display: flex;
 	align-items: center;
 	justify-content: center;
 	overflow: hidden;
-	margin-bottom: 40rpx;
+	margin-bottom: 24rpx;
 }
 
 .logo-img {
@@ -337,7 +85,7 @@ export default {
 }
 
 .app-title {
-	font-size: 48rpx;
+	font-size: 40rpx;
 	font-weight: bold;
 	color: #1a1a1a;
 	letter-spacing: 2rpx;
@@ -346,398 +94,7 @@ export default {
 .app-subtitle {
 	font-size: 26rpx;
 	color: #999;
-	margin-top: 10rpx;
+	margin-top: 8rpx;
 	letter-spacing: 6rpx;
 }
-
-.main-btn {
-	width: 100%;
-	height: 100rpx;
-	background: linear-gradient(135deg, #C1001C 0%, #FF4D4F 100%);
-	border-radius: 50rpx;
-	color: #fff;
-	display: flex;
-	align-items: center;
-	justify-content: center;
-	font-size: 32rpx;
-	font-weight: bold;
-	box-shadow: 0 12rpx 30rpx rgba(193, 0, 28, 0.2);
-	border: none;
-	margin-bottom: 40rpx;
-}
-
-.btn-icon {
-	width: 48rpx;
-	height: 48rpx;
-	margin-right: 16rpx;
-}
-
-.agreement-text {
-	font-size: 24rpx;
-	color: #999;
-}
-
-.link {
-	color: #C1001C;
-	margin: 0 4rpx;
-	font-weight: 500;
-}
-
-.footer-section {
-	margin-top: auto;
-	padding-bottom: 60rpx;
-	text-align: center;
-	font-size: 20rpx;
-	color: #dcdcdc;
-}
-
-/* 弹窗通用基础 */
-.global-mask {
-	position: fixed;
-	top: 0;
-	left: 0;
-	right: 0;
-	bottom: 0;
-	background: rgba(0, 0, 0, 0.5);
-	z-index: 998;
-}
-
-.center-card {
-	position: fixed;
-	top: 50%;
-	left: 50%;
-	transform: translate(-50%, -50%);
-	width: 620rpx;
-	background: #fff;
-	border-radius: 32rpx;
-	z-index: 1000;
-	box-shadow: 0 30rpx 80rpx rgba(0, 0, 0, 0.15);
-	padding: 50rpx 40rpx;
-	display: flex;
-	flex-direction: column;
-}
-
-.bottom-pop {
-	position: fixed;
-	bottom: 0;
-	left: 0;
-	right: 0;
-	background: #fff;
-	border-radius: 40rpx 40rpx 0 0;
-	z-index: 1001;
-	padding: 40rpx;
-	padding-bottom: calc(50rpx + env(safe-area-inset-bottom));
-	box-shadow: 0 -10rpx 40rpx rgba(0, 0, 0, 0.05);
-}
-
-/* 按钮对齐辅助 */
-button {
-	display: flex !important;
-	align-items: center !important;
-	justify-content: center !important;
-	padding: 0 !important;
-	line-height: normal !important;
-}
-
-button::after {
-	border: none;
-}
-
-/* 协议拦截弹窗 */
-.card-title {
-	font-size: 38rpx;
-	font-weight: bold;
-	text-align: center;
-	margin-bottom: 30rpx;
-}
-
-.card-body {
-	font-size: 28rpx;
-	color: #666;
-	line-height: 1.6;
-	text-align: center;
-	margin-bottom: 50rpx;
-}
-
-.card-footer-btns {
-	display: flex;
-	gap: 24rpx;
-}
-
-.btn-item {
-	flex: 1;
-	height: 90rpx;
-	border-radius: 45rpx;
-	font-size: 30rpx;
-	display: flex !important;
-	align-items: center !important;
-	justify-content: center !important;
-	text-align: center;
-	line-height: 90rpx;
-}
-
-.btn-item.cancel {
-	background: #f8f8f8;
-	color: #999;
-}
-
-.btn-item.agree {
-	background: #C1001C;
-	color: #fff;
-	font-weight: bold;
-}
-
-/* 协议内容弹窗专项修复 */
-.p-pop-header {
-	display: flex;
-	justify-content: space-between;
-	align-items: center;
-	margin-bottom: 30rpx;
-}
-
-.p-pop-title {
-	font-size: 36rpx;
-	font-weight: bold;
-	color: #1a1a1a;
-}
-
-.p-pop-close {
-	font-size: 48rpx;
-	color: #ccc;
-	padding: 10rpx;
-}
-
-.p-pop-scroll {
-	max-height: 55vh;
-	margin-bottom: 30rpx;
-}
-
-.rich-text-wrapper {
-	padding: 10rpx 0;
-	color: #444;
-	font-size: 28rpx;
-}
-
-.p-pop-footer {
-	padding-top: 20rpx;
-}
-
-.p-pop-btn {
-	width: 100%;
-	height: 90rpx;
-	background: #C1001C;
-	color: #fff;
-	border-radius: 45rpx;
-	font-size: 30rpx;
-	font-weight: bold;
-}
-
-/* 头像授权弹窗 */
-.pop-header-bar {
-	display: flex;
-	justify-content: space-between;
-	align-items: center;
-	margin-bottom: 60rpx;
-}
-
-.pop-cancel {
-	font-size: 30rpx;
-	color: #999;
-}
-
-.pop-main-title {
-	font-size: 32rpx;
-	font-weight: bold;
-}
-
-.pop-done {
-	font-size: 30rpx;
-	color: #C1001C;
-	font-weight: bold;
-}
-
-.profile-edit-content {
-	display: flex;
-	flex-direction: column;
-	align-items: center;
-}
-
-.avatar-wrapper-btn {
-	width: 170rpx;
-	height: 170rpx;
-	border-radius: 85rpx;
-	background: #f8f8f8;
-	position: relative;
-	margin-bottom: 24rpx;
-	padding: 0 !important;
-	overflow: visible;
-	display: flex !important;
-	align-items: center;
-	justify-content: center;
-	border: none;
-}
-
-.current-avatar {
-	width: 100%;
-	height: 100%;
-	border-radius: 85rpx;
-	border: 4rpx solid #fff;
-	box-shadow: 0 4rpx 15rpx rgba(0, 0, 0, 0.05);
-}
-
-.camera-icon {
-	position: absolute;
-	bottom: 0;
-	right: 0;
-	background: #fff;
-	width: 56rpx;
-	height: 56rpx;
-	border-radius: 28rpx;
-	display: flex;
-	align-items: center;
-	justify-content: center;
-	box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.12);
-	z-index: 5;
-}
-
-.camera-icon image {
-	width: 30rpx;
-	height: 30rpx;
-}
-
-.edit-hint {
-	font-size: 24rpx;
-	color: #999;
-	margin-bottom: 70rpx;
-	width: 100%;
-	text-align: center;
-	display: block;
-}
-
-.nickname-edit-box {
-	width: 100%;
-	display: flex;
-	align-items: center;
-	padding: 36rpx 0;
-	border-top: 1rpx solid #f0f0f0;
-	border-bottom: 1rpx solid #f0f0f0;
-	margin-bottom: 40rpx;
-}
-
-.nickname-edit-box .label {
-	width: 130rpx;
-	font-size: 32rpx;
-}
-
-.nickname-input {
-	flex: 1;
-	font-size: 32rpx;
-}
-
-.notice-text {
-	font-size: 24rpx;
-	color: #bfbfbf;
-	text-align: center;
-	display: block;
-	margin-bottom: 60rpx;
-}
-
-.confirm-btn-fixed {
-	width: 100%;
-	height: 96rpx;
-	background: #C1001C;
-	color: #fff;
-	border-radius: 16rpx;
-	font-size: 32rpx;
-	font-weight: bold;
-}
-
-/* 手机号授权弹窗 */
-.p-header {
-	display: flex;
-	align-items: center;
-	margin-bottom: 50rpx;
-}
-
-.p-mini-logo {
-	width: 44rpx;
-	height: 44rpx;
-	border-radius: 8rpx;
-	margin-right: 16rpx;
-}
-
-.p-app-name {
-	font-size: 28rpx;
-	color: #7f7f7f;
-}
-
-.p-title {
-	font-size: 40rpx;
-	font-weight: bold;
-	color: #000;
-	margin-bottom: 44rpx;
-	display: block;
-}
-
-.p-phone-hint {
-	font-size: 30rpx;
-	color: #666;
-	line-height: 1.5;
-	display: block;
-	margin-bottom: 60rpx;
-}
-
-.p-number-card {
-	background: #fbfbfb;
-	padding: 36rpx;
-	border-radius: 20rpx;
-	display: flex;
-	justify-content: space-between;
-	align-items: center;
-	margin-bottom: 30rpx;
-	border: 1rpx solid #f0f0f0;
-}
-
-.p-real-num {
-	font-size: 36rpx;
-	font-weight: bold;
-	color: #1a1a1a;
-	display: block;
-}
-
-.p-num-hint {
-	font-size: 24rpx;
-	color: #999;
-}
-
-.p-other-link {
-	font-size: 28rpx;
-	color: #576b95;
-	display: block;
-	margin-bottom: 60rpx;
-}
-
-.p-footer-btns {
-	display: flex;
-	gap: 30rpx;
-}
-
-.p-btn-fixed {
-	flex: 1;
-	height: 96rpx;
-	border-radius: 16rpx;
-	font-size: 32rpx;
-	border: none;
-}
-
-.p-deny {
-	background: #f2f2f2;
-	color: #C1001C;
-}
-
-.p-allow {
-	background: #C1001C;
-	color: #fff;
-	font-weight: bold;
-}
 </style>

+ 11 - 11
pages/mine/index.vue

@@ -13,13 +13,13 @@
 				<view class="user-card" v-if="isLogin">
 					<view class="avatar-box">
 						<image class="avatar-img"
-							:src="customerInfo && customerInfo.avatarUrl ? customerInfo.avatarUrl : 'https://img.icons8.com/color/144/user.png'"
+							:src="employeeInfo && employeeInfo.avatarUrl ? employeeInfo.avatarUrl : 'https://img.icons8.com/color/144/user.png'"
 							mode="aspectFill">
 						</image>
 					</view>
 					<view class="info-box">
-						<text class="nickname">{{ customerInfo ? customerInfo.userName : '' }}</text>
-						<text class="phone-text">{{ customerInfo ? customerInfo.phone : '' }}</text>
+						<text class="nickname">{{ employeeInfo ? employeeInfo.name : '' }}</text>
+						<text class="phone-text">{{ employeeInfo ? employeeInfo.phone : '' }}</text>
 					</view>
 					<!-- 新增:右侧设置图标 -->
 					<view class="settings-btn" @click="goToSettings">
@@ -87,8 +87,8 @@
 import ErpTabBar from '@/components/erp-tab-bar.vue';
 import ErpNavBar from '@/components/erp-nav-bar.vue';
 import assets from '@/utils/assets.js';
-import { getMyInfo } from '@/api/system/customer.js';
-import { logout } from '@/api/auth/index.js';
+import { getMyInfo } from '@/api/system/employee.js';
+import { logout } from '@/api/auth.js';
 import { getPhone } from '@/api/system/phone.js';
 
 export default {
@@ -97,7 +97,7 @@ export default {
 		return {
 			assets,
 			isLogin: false,
-			customerInfo: null,
+			employeeInfo: null,
 			servicePhone: '13888888888',
 			orderStates: [
 				{ label: '待审核', tabIndex: 2, icon: assets.minePendingReview },
@@ -116,7 +116,7 @@ export default {
 	onShow() {
 		this.isLogin = !!uni.getStorageSync('isLogin');
 		if (this.isLogin) {
-			this.loadCustomerInfo();
+			this.loadEmployeeInfo();
 		}
 		this.loadServicePhone();
 	},
@@ -132,13 +132,13 @@ export default {
 				uni.showToast({ title: e || '加载客服电话失败', icon: 'none' });
 			}
 		},
-		async loadCustomerInfo() {
+		async loadEmployeeInfo() {
 			try {
 				const res = await getMyInfo();
-				this.customerInfo = res.data;
+				this.employeeInfo = res.data;
 			} catch (e) {
-				console.error('[mine] 加载客户信息失败', e);
-				uni.showToast({ title: e || '加载客户信息失败', icon: 'none' });
+				console.error('[mine] 加载员工信息失败', e);
+				uni.showToast({ title: e || '加载员工信息失败', icon: 'none' });
 			}
 		},
 		goToLogin() {

+ 32 - 58
pages/mine/settings/index.vue

@@ -21,7 +21,7 @@
 			<view class="item-row" @click="doEditName">
 				<text class="item-label">用户昵称</text>
 				<view class="item-right">
-					<text class="item-value">{{ myInfo.userName }}</text>
+					<text class="item-value">{{ myInfo.name }}</text>
 					<text class="icon-more"></text>
 				</view>
 			</view>
@@ -58,26 +58,22 @@
 				<text class="no-client-text">暂无授权客户</text>
 			</view>
 		</view>
-
-		<view class="footer-bar">
-			<button class="btn-confirm" @click="saveProfile" :disabled="uploading">确认保存</button>
-		</view>
 	</view>
 </template>
 
 <script>
 import ErpNavBar from '@/components/erp-nav-bar.vue';
-import { getMyInfo, updateMyInfo } from '@/api/system/customer.js';
+import { getMyInfo, updateMyInfo } from '@/api/system/employee.js';
+import { getClientByIds } from '@/api/erp/client.js';
 import { uploadFile } from '@/api/resource/oss.js';
 export default {
 	components: { ErpNavBar },
 	data() {
 		return {
 			uploading: false,
-			pendingAvatarOssId: null,
 			myInfo: {
 				avatarUrl: '',
-				userName: '',
+				name: '',
 				phone: '',
 				avatar: null,
 				authClientList: []
@@ -94,14 +90,22 @@ export default {
 				const res = await getMyInfo();
 				uni.hideLoading();
 				const d = res.data;
+				let clientList = [];
+				if (d.authClientFRowIDs) {
+					try {
+						const clientRes = await getClientByIds(d.authClientFRowIDs);
+						clientList = clientRes.data || [];
+					} catch (e) {
+						console.error('加载授权客户失败', e);
+					}
+				}
 				this.myInfo = {
 					avatarUrl: d.avatarUrl || '',
-					userName: d.userName || '',
+					name: d.name || '',
 					phone: d.phone || '',
 					avatar: d.avatar || null,
-					authClientList: d.authClientList || []
+					authClientList: clientList
 				};
-				this.pendingAvatarOssId = null;
 			} catch (e) {
 				uni.hideLoading();
 				uni.showToast({ title: e || '加载失败', icon: 'none' });
@@ -114,15 +118,21 @@ export default {
 				sourceType: ['album', 'camera'],
 				success: async (res) => {
 					const tempPath = res.tempFilePaths[0];
+					const previousUrl = this.myInfo.avatarUrl;
 					this.myInfo.avatarUrl = tempPath;
 					this.uploading = true;
 					try {
 						const uploadRes = await uploadFile(tempPath);
-						this.pendingAvatarOssId = uploadRes.ossId;
-						uni.showToast({ title: '头像上传成功', icon: 'success' });
+						uni.showLoading({ title: '保存中' });
+						await updateMyInfo({ avatar: uploadRes.ossId });
+						uni.hideLoading();
+						this.myInfo.avatar = uploadRes.ossId;
+						this.myInfo.avatarUrl = uploadRes.url;
+						uni.showToast({ title: '头像更新成功', icon: 'success' });
 					} catch (e) {
-						uni.showToast({ title: e || '头像上传失败', icon: 'none' });
-						this.myInfo.avatarUrl = '';
+						uni.hideLoading();
+						this.myInfo.avatarUrl = previousUrl;
+						uni.showToast({ title: e || '头像更新失败', icon: 'none' });
 					} finally {
 						this.uploading = false;
 					}
@@ -132,39 +142,22 @@ export default {
 		doEditName() {
 			uni.showModal({
 				title: '设置昵称',
-				content: this.myInfo.userName,
+				content: this.myInfo.name,
 				editable: true,
 				confirmColor: '#C1001C',
-				success: (res) => {
-					if (res.confirm) {
-						this.myInfo.userName = res.content || this.myInfo.userName;
-					}
-				}
-			});
-		},
-		saveProfile() {
-			if (this.uploading) return;
-			uni.showModal({
-				title: '确认保存',
-				content: `昵称:${this.myInfo.userName}\n手机:${this.myInfo.phone}\n请确认以上信息是否填写正确?`,
-				confirmText: '确认',
-				cancelText: '取消',
-				confirmColor: '#C1001C',
 				success: async (res) => {
 					if (!res.confirm) return;
+					const newName = res.content || this.myInfo.name;
+					if (newName === this.myInfo.name) return;
 					try {
 						uni.showLoading({ title: '保存中' });
-						const payload = { userName: this.myInfo.userName };
-						if (this.pendingAvatarOssId !== null) {
-							payload.avatar = this.pendingAvatarOssId;
-						}
-						await updateMyInfo(payload);
+						await updateMyInfo({ name: newName });
 						uni.hideLoading();
-						uni.showToast({ title: '保存成功', icon: 'success' });
-						setTimeout(() => { uni.navigateBack(); }, 1200);
+						this.myInfo.name = newName;
+						uni.showToast({ title: '昵称更新成功', icon: 'success' });
 					} catch (e) {
 						uni.hideLoading();
-						uni.showToast({ title: e || '保存失败', icon: 'none' });
+						uni.showToast({ title: e || '昵称更新失败', icon: 'none' });
 					}
 				}
 			});
@@ -268,25 +261,6 @@ export default {
 	transform: rotate(45deg);
 }
 
-.footer-bar {
-	padding: 40rpx;
-	margin-top: auto;
-	padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
-}
-
-.btn-confirm {
-	width: 100%;
-	height: 90rpx;
-	background: #C1001C;
-	color: #fff;
-	border-radius: 45rpx;
-	font-size: 32rpx;
-	font-weight: bold;
-	display: flex;
-	align-items: center;
-	justify-content: center;
-}
-
 .section-card {
 	background: #fff;
 	border-radius: 24rpx;

+ 7 - 38
pages/order/add-model/index.vue

@@ -116,9 +116,7 @@
 			</view>
 		</scroll-view>
 
-		<view class="fixed-submit-bar">
-			<button class="submit-btn" @click="confirmAddModel">确认并添加</button>
-		</view>
+		<erp-submit-bar text="确认并添加" @click="confirmAddModel" />
 
 		<!-- 型号选择 -->
 		<view class="custom-picker-mask" v-if="showTypePicker" @click="closeTypePicker" @touchmove.stop.prevent>
@@ -145,12 +143,12 @@
 				</view>
 				<!-- 列表区 -->
 				<scroll-view scroll-y class="item-list">
-					<view class="option-item kind-item" v-for="(item, index) in modelKindList" :key="'mk'+index"
+					<view class="option-item kind-item" v-for="(item, index) in modelKindList" :key="'mk' + index"
 						@click="drillDownType(item)">
 						<text class="option-text kind-text">{{ item.num }} - {{ item.name }}</text>
 						<text class="arrow-right">›</text>
 					</view>
-					<view class="option-item" v-for="(item, index) in modelItemList" :key="'mi'+index"
+					<view class="option-item" v-for="(item, index) in modelItemList" :key="'mi' + index"
 						:class="{ active: tempSelectedIndex === index }">
 						<text class="option-text" @click="selectTypeItem(index)">{{ item.num }} - {{ item.name }}</text>
 					</view>
@@ -181,12 +179,12 @@
 				</view>
 				<!-- 列表区 -->
 				<scroll-view scroll-y class="item-list">
-					<view class="option-item kind-item" v-for="(item, index) in colorKindList" :key="'ck'+index"
+					<view class="option-item kind-item" v-for="(item, index) in colorKindList" :key="'ck' + index"
 						@click="drillDownSurface(item)">
 						<text class="option-text kind-text">{{ item.num }} - {{ item.name }}</text>
 						<text class="arrow-right">›</text>
 					</view>
-					<view class="option-item" v-for="(item, index) in colorItemList" :key="'ci'+index"
+					<view class="option-item" v-for="(item, index) in colorItemList" :key="'ci' + index"
 						:class="{ active: tempSurfaceIndex === index }">
 						<text class="option-text" @click="selectSurfaceItem(index)">{{ item.num }} - {{ item.name
 						}}</text>
@@ -232,6 +230,7 @@
 
 <script>
 import ErpNavBar from '@/components/erp-nav-bar.vue';
+import ErpSubmitBar from '@/components/erp-submit-bar.vue';
 import { listModel } from '@/api/erp/model.js';
 import { listModelKind } from '@/api/erp/modelKind.js';
 import { listColor } from '@/api/erp/color.js';
@@ -239,7 +238,7 @@ import { listColorKind } from '@/api/erp/colorKind.js';
 import { listPagePack } from '@/api/erp/pack.js';
 import { addOrderDetail } from '@/api/erp/orderDetail.js';
 export default {
-	components: { ErpNavBar },
+	components: { ErpNavBar, ErpSubmitBar },
 	data() {
 		return {
 			showTypePicker: false, showSurfacePicker: false, showPackagePicker: false,
@@ -721,36 +720,6 @@ export default {
 	font-size: 26rpx !important;
 }
 
-.fixed-submit-bar {
-	position: fixed;
-	bottom: 0;
-	left: 0;
-	width: 100%;
-	background: #fff;
-	padding: 20rpx 30rpx calc(env(safe-area-inset-bottom) + 20rpx);
-	box-sizing: border-box;
-	box-shadow: 0 -10rpx 30rpx rgba(0, 0, 0, 0.05);
-	z-index: 100;
-}
-
-.submit-btn {
-	background: #C1001C;
-	color: #fff;
-	height: 100rpx;
-	border-radius: 50rpx;
-	display: flex;
-	align-items: center;
-	justify-content: center;
-	font-size: 34rpx;
-	font-weight: bold;
-	border: none;
-}
-
-.submit-btn:active {
-	opacity: 0.9;
-	transform: scale(0.98);
-}
-
 .custom-picker-mask {
 	position: fixed;
 	top: 0;

+ 187 - 49
pages/order/index.vue

@@ -1,6 +1,6 @@
 <template>
 	<view class="order-container">
-		<erp-nav-bar title="ERP 下单" :show-back="false" />
+		<erp-nav-bar title="ERP 下单" />
 		<!-- 0. 全屏加载过渡 -->
 		<view class="loading-state-full" v-if="isLoading">
 			<view class="loading-anim-box">
@@ -25,7 +25,7 @@
 		</view>
 
 		<!-- 2. 已登录但未分配授权客户 -->
-		<view class="auth-waiting-full" v-else-if="!myInfo.authClientFRowID">
+		<view class="auth-waiting-full" v-else-if="!myInfo.authClientFRowIDs">
 			<view class="auth-card">
 				<image class="auth-icon" src="https://img.icons8.com/color/192/hourglass-sand-top.png" mode="aspectFit">
 				</image>
@@ -35,6 +35,25 @@
 			</view>
 		</view>
 
+		<!-- 2.5 已授权但未选择客户:客户选择页 -->
+		<view class="client-pick-full" v-else-if="waitingClientPick">
+			<view class="pick-card">
+				<text class="pick-title">选择下单客户</text>
+				<text class="pick-desc">请选择本次下单的客户</text>
+				<view class="pick-list">
+					<view class="pick-item" v-for="(c, i) in myInfo.authClientList" :key="c.rowId || i"
+						@click="selectClient(c)">
+						<view class="pick-index">{{ i + 1 }}</view>
+						<view class="pick-info">
+							<text class="pick-name">{{ c.name || '-' }}</text>
+							<text class="pick-code" v-if="c.num">{{ c.num }}</text>
+						</view>
+						<view class="pick-arrow">›</view>
+					</view>
+				</view>
+			</view>
+		</view>
+
 		<!-- 3. 已登录且已分配授权客户:显示下单页面 -->
 		<template v-else>
 			<scroll-view scroll-y class="order-scroll-list" :show-scrollbar="false" :enhanced="true">
@@ -136,9 +155,6 @@
 			</view>
 		</template>
 
-		<!-- 底部菜单栏 -->
-		<erp-tab-bar active="order"></erp-tab-bar>
-
 		<!-- 批量修改 - 表面处理选择弹层 -->
 		<view class="custom-picker-mask" v-if="showBatchSurfacePicker" @click="showBatchSurfacePicker = false"
 			@touchmove.stop.prevent>
@@ -163,12 +179,12 @@
 				</view>
 				<!-- 列表区 -->
 				<scroll-view scroll-y class="item-list">
-					<view class="option-item kind-item" v-for="(item, index) in batchSurfaceKindList" :key="'bsk'+index"
-						@click="drillDownBatchSurface(item)">
+					<view class="option-item kind-item" v-for="(item, index) in batchSurfaceKindList"
+						:key="'bsk' + index" @click="drillDownBatchSurface(item)">
 						<text class="option-text kind-text">{{ item.num }} - {{ item.name }}</text>
 						<text class="arrow-right">›</text>
 					</view>
-					<view class="option-item" v-for="item in batchSurfaceItemList" :key="'bsi'+item.rowId"
+					<view class="option-item" v-for="item in batchSurfaceItemList" :key="'bsi' + item.rowId"
 						:class="{ active: batchSurfaceId === item.rowId }">
 						<text class="option-text" @click="selectBatchSurfaceItem(item)">{{ item.num }} - {{ item.name
 						}}</text>
@@ -211,25 +227,28 @@
 </template>
 
 <script>
-import ErpTabBar from '@/components/erp-tab-bar.vue';
 import ErpNavBar from '@/components/erp-nav-bar.vue';
 import OrderConfirmPopup from './components/order-confirm-popup.vue';
-import { getMyInfo } from '@/api/system/customer.js';
+import { getMyInfo } from '@/api/system/employee.js';
+import { getClientByIds } from '@/api/erp/client.js';
 import { listOrderDetail, batchUpdateOrderDetail } from '@/api/erp/orderDetail.js';
 import { listColor } from '@/api/erp/color.js';
 import { listColorKind } from '@/api/erp/colorKind.js';
 import { listPagePack } from '@/api/erp/pack.js';
 import { addOrder } from '@/api/erp/order.js';
 export default {
-	components: { ErpNavBar, ErpTabBar, OrderConfirmPopup },
+	components: { ErpNavBar, OrderConfirmPopup },
 	data() {
 		return {
 			isLoggedIn: false,
-			isLoading: true, // 初始为加载中
+			isLoading: true,
 			myInfo: {},
 			selectedModels: [],
 			selectedClientId: '',
 			selectedClientName: '',
+			waitingClientPick: false,
+			clientIdParam: '',
+			clientNameParam: '',
 			// 批量修改相关
 			batchMode: false,
 			selectedIds: [],
@@ -266,12 +285,12 @@ export default {
 			return this.selectedModels.length > 0 && this.selectedIds.length === this.selectedModels.length;
 		}
 	},
-	onLoad() {
-		// 监听添加型号的事件 - 重新从后端加载
+	onLoad(options) {
+		if (options.clientId) this.clientIdParam = options.clientId;
+		if (options.clientName) this.clientNameParam = decodeURIComponent(options.clientName);
 		uni.$on('add_order_item', (data) => {
-			this.loadOrderItems(false); // 添加时静默加载,不显示全屏动画
+			this.loadOrderItems(false);
 		});
-		// 监听修改型号的事件 - 重新从后端加载
 		uni.$on('update_order_item', (res) => {
 			this.loadOrderItems(false);
 		});
@@ -307,13 +326,27 @@ export default {
 				this.myInfo = {};
 				this.selectedClientId = '';
 				this.selectedClientName = '';
+				this.waitingClientPick = false;
 				this.isLoading = false;
 				return;
 			}
 			try {
 				const res = await getMyInfo();
 				this.isLoggedIn = true;
-				this.myInfo = res.data || {};
+				const info = res.data || {};
+				if (info.authClientFRowIDs) {
+					try {
+						const clientRes = await getClientByIds(info.authClientFRowIDs);
+						info.authClientList = clientRes.data || [];
+					} catch (e) {
+						console.error('加载授权客户失败', e);
+						info.authClientList = [];
+					}
+				} else {
+					info.authClientList = [];
+				}
+				this.myInfo = info;
+				this.resolveClient();
 				this.loadOrderItems();
 			} catch (e) {
 				uni.showToast({ title: e || '登录状态校验失败', icon: 'none' });
@@ -323,9 +356,38 @@ export default {
 				this.myInfo = {};
 				this.selectedClientId = '';
 				this.selectedClientName = '';
+				this.waitingClientPick = false;
 				this.isLoading = false;
 			}
 		},
+		resolveClient() {
+			const clientList = this.myInfo.authClientList || [];
+			if (clientList.length === 0) {
+				this.waitingClientPick = false;
+				return;
+			}
+			if (this.clientIdParam) {
+				const matched = clientList.find(c => c.rowId === this.clientIdParam);
+				if (matched) {
+					this.selectedClientId = matched.rowId;
+					this.selectedClientName = matched.name;
+					this.waitingClientPick = false;
+					return;
+				}
+			}
+			if (clientList.length === 1) {
+				this.selectedClientId = clientList[0].rowId;
+				this.selectedClientName = clientList[0].name;
+				this.waitingClientPick = false;
+			} else {
+				this.waitingClientPick = true;
+			}
+		},
+		selectClient(client) {
+			this.selectedClientId = client.rowId;
+			this.selectedClientName = client.name;
+			this.waitingClientPick = false;
+		},
 		goToLogin() {
 			uni.reLaunch({ url: '/pages/login/index' });
 		},
@@ -588,36 +650,13 @@ export default {
 		 */
 		async submitFinalOrder() {
 			if (this.selectedModels.length === 0) return;
-
-			const clientList = this.myInfo.authClientList || [];
-			if (clientList.length === 0) {
-				uni.showToast({ title: '暂无授权客户,无法下单', icon: 'none' });
-				return;
-			}
-
-			// 单个授权客户:直接弹出
-			if (clientList.length === 1) {
-				this.confirmClientId = clientList[0].rowId;
-				this.confirmClientName = clientList[0].name;
-				this.showConfirmPopup = true;
+			if (!this.selectedClientId) {
+				uni.showToast({ title: '请先选择下单客户', icon: 'none' });
 				return;
 			}
-
-			// 多个授权客户:先选择再弹出
-			const clientNames = clientList.map(c => c.name);
-			const res = await new Promise((resolve) => {
-				uni.showActionSheet({
-					itemList: clientNames,
-					success: (r) => resolve(r),
-					fail: () => resolve(null)
-				});
-			});
-			if (res && res.tapIndex !== undefined) {
-				const chosen = clientList[res.tapIndex];
-				this.confirmClientId = chosen.rowId;
-				this.confirmClientName = chosen.name;
-				this.showConfirmPopup = true;
-			}
+			this.confirmClientId = this.selectedClientId;
+			this.confirmClientName = this.selectedClientName;
+			this.showConfirmPopup = true;
 		},
 		/**
 		 * 确认弹窗回调 - 正式执行下单
@@ -738,6 +777,104 @@ export default {
 	margin-top: 30rpx;
 }
 
+/* 客户选择页 */
+.client-pick-full {
+	flex: 1;
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	padding: 80rpx 40rpx;
+	background: #f7f8fa;
+}
+
+.pick-card {
+	width: 100%;
+	max-width: 680rpx;
+	background: #fff;
+	border-radius: 24rpx;
+	padding: 48rpx 36rpx 36rpx;
+	box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.04);
+}
+
+.pick-title {
+	display: block;
+	font-size: 36rpx;
+	font-weight: 700;
+	color: #1a1a1a;
+	text-align: center;
+	margin-bottom: 12rpx;
+}
+
+.pick-desc {
+	display: block;
+	font-size: 26rpx;
+	color: #999;
+	text-align: center;
+	margin-bottom: 36rpx;
+}
+
+.pick-list {
+	display: flex;
+	flex-direction: column;
+	gap: 16rpx;
+}
+
+.pick-item {
+	display: flex;
+	align-items: center;
+	padding: 28rpx 24rpx;
+	border-radius: 16rpx;
+	background: #f9fafb;
+	transition: background 0.15s;
+}
+
+.pick-item:active {
+	background: #f0f1f5;
+}
+
+.pick-index {
+	width: 48rpx;
+	height: 48rpx;
+	border-radius: 12rpx;
+	background: #eef0f4;
+	color: #888;
+	font-size: 24rpx;
+	font-weight: 700;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	margin-right: 20rpx;
+	flex-shrink: 0;
+}
+
+.pick-info {
+	flex: 1;
+	display: flex;
+	flex-direction: column;
+	min-width: 0;
+}
+
+.pick-name {
+	font-size: 30rpx;
+	font-weight: 600;
+	color: #333;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+
+.pick-code {
+	font-size: 24rpx;
+	color: #bbb;
+	margin-top: 4rpx;
+}
+
+.pick-arrow {
+	font-size: 36rpx;
+	color: #ccc;
+	margin-left: 12rpx;
+}
+
 /* 已授权列表页样式 */
 .order-container {
 	width: 100%;
@@ -755,8 +892,7 @@ export default {
 
 .list-wrapper {
 	padding: 30rpx;
-	/* 确保最后一条数据不被底部双层固定栏遮挡:汇总栏130 + 菜单栏110 + 安全区 + 缓冲余量 */
-	padding-bottom: calc(280rpx + env(safe-area-inset-bottom));
+	padding-bottom: calc(170rpx + env(safe-area-inset-bottom));
 }
 
 .list-header {
@@ -979,7 +1115,7 @@ export default {
 /* 底部汇总栏:微调间距与样式 */
 .footer-summary-bar {
 	position: fixed;
-	bottom: calc(110rpx + env(safe-area-inset-bottom));
+	bottom: 0;
 	left: 0;
 	width: 100%;
 	height: 130rpx;
@@ -989,6 +1125,7 @@ export default {
 	align-items: center;
 	justify-content: space-between;
 	padding: 0 40rpx;
+	padding-bottom: env(safe-area-inset-bottom);
 	box-sizing: border-box;
 	z-index: 99;
 	box-shadow: 0 -10rpx 40rpx rgba(0, 0, 0, 0.05);
@@ -1225,7 +1362,7 @@ export default {
 /* 批量操作栏 */
 .batch-action-bar {
 	position: fixed;
-	bottom: calc(110rpx + env(safe-area-inset-bottom));
+	bottom: 0;
 	left: 0;
 	width: 100%;
 	height: 110rpx;
@@ -1235,6 +1372,7 @@ export default {
 	align-items: center;
 	justify-content: space-between;
 	padding: 0 30rpx;
+	padding-bottom: env(safe-area-inset-bottom);
 	box-sizing: border-box;
 	z-index: 100;
 	box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);

+ 26 - 7
pages/order/list/index.vue

@@ -1,9 +1,10 @@
 <template>
 	<view class="list-page-container">
-		<erp-nav-bar title="全部订单" />
+		<erp-nav-bar :title="clientName ? clientName + ' · 订单' : '全部订单'" />
 
 		<!-- 2. 分类切换:使用 scroll-view 滚动,彻底释放所有综合状态 -->
-		<scroll-view scroll-x class="tabs-fixed" :show-scrollbar="false" :scroll-with-animation="true" :scroll-into-view="'tab-' + currentTab">
+		<scroll-view scroll-x class="tabs-fixed" :show-scrollbar="false" :scroll-with-animation="true"
+			:scroll-into-view="'tab-' + currentTab">
 			<view class="tabs-box">
 				<view v-for="(tab, index) in tabs" :key="index" :id="'tab-' + index" class="tab-item"
 					:class="{ active: currentTab === index }" @click="switchTab(index)">
@@ -76,6 +77,8 @@
 			</view>
 		</scroll-view>
 
+		<erp-submit-bar text="选材下单" @click="goCreateOrder" />
+
 		<!-- 底部菜单栏 -->
 		<!-- <erp-tab-bar active="order"></erp-tab-bar> -->
 	</view>
@@ -84,19 +87,22 @@
 <script>
 import ErpTabBar from '@/components/erp-tab-bar.vue';
 import ErpNavBar from '@/components/erp-nav-bar.vue';
-import { listMyOrder } from '@/api/erp/order.js';
+import ErpSubmitBar from '@/components/erp-submit-bar.vue';
+import { listMyOrder, listOrderByClientId } from '@/api/erp/order.js';
 export default {
-	components: { ErpTabBar, ErpNavBar },
+	components: { ErpTabBar, ErpNavBar, ErpSubmitBar },
 	data() {
 		return {
 			statusBarHeight: 20,
 			navBarHeight: 44,
-			tabBarHeight: 50, // 对应 100rpx
+			tabBarHeight: 50,
 			currentTab: 0,
 			loading: false,
 			noMore: false,
 			pageNum: 1,
 			tabs: ['全部', '待确认', '已确认', '已审核', '已签批', '挤压完成', '生产完成', '已取消'],
+			clientId: '',
+			clientName: '',
 			allOrders: [],
 			displayList: []
 		}
@@ -113,6 +119,8 @@ export default {
 		this.tabBarHeight = winInfo.windowWidth / 750 * 110;
 
 		if (options.tab) this.currentTab = parseInt(options.tab);
+		if (options.clientId) this.clientId = options.clientId;
+		if (options.clientName) this.clientName = decodeURIComponent(options.clientName);
 		this.refresh();
 	},
 	methods: {
@@ -128,12 +136,16 @@ export default {
 					pageNum: this.pageNum,
 					pageSize: 5
 				};
+				if (this.clientId) {
+					params.clientId = this.clientId;
+				}
 				if (this.currentTab > 0) {
-					// 对应 status:0-待确认 1-已确认 2-已审核 3-已签批 4-挤压完成 5-生产完成 6-已取消
 					const indexToStatus = [undefined, 0, 1, 2, 3, 4, 5, 6];
 					params.status = indexToStatus[this.currentTab];
 				}
-				const res = await listMyOrder(params);
+				const res = this.clientId
+					? await listOrderByClientId(params)
+					: await listMyOrder(params);
 				const rows = res.rows || [];
 
 				const formattedRows = rows.map(item => {
@@ -179,6 +191,13 @@ export default {
 			uni.navigateTo({
 				url: `/pages/order/detail/index?rowId=${item.rowId}`
 			});
+		},
+		goCreateOrder() {
+			let url = '/pages/order/index';
+			if (this.clientId) {
+				url += `?clientId=${this.clientId}&clientName=${encodeURIComponent(this.clientName)}`;
+			}
+			uni.navigateTo({ url });
 		}
 	}
 }

BIN
static/tabs/client.png


BIN
static/tabs/client_active.png


+ 2 - 2
utils/request.js

@@ -1,6 +1,6 @@
-// const BASE_URL = 'http://127.0.0.1:8080';
+const BASE_URL = 'http://127.0.0.1:8080';
 // const BASE_URL = 'http://192.168.1.205:8080';
-const BASE_URL = 'https://app.jxhsal.com/api';
+// const BASE_URL = 'https://app.jxhsal.com/api';
 
 const CLIENT_ID = 'e48ac397bff4f031b14d6e671eee49c3';