Prechádzať zdrojové kódy

fix(ui): unify form styles, fix header transparency and auto role login

- Install @dcloudio/uni-ui and replace inputs with uni-easyinput
- Fix TopBar and home/profile header transparent gradient issue
- Unify .form-input styles in global.scss, remove duplicate scoped styles
- Login page uses native inputs and auto-detects role from backend roleType
- Translate backend error messages (e.g. Password mismatch -> 密码错误)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
xuyunhui 2 týždňov pred
rodič
commit
e833ed9b31

+ 7 - 0
package-lock.json

@@ -24,6 +24,7 @@
         "@dcloudio/uni-mp-weixin": "^3.0.0-4080420251103001",
         "@dcloudio/uni-mp-xhs": "3.0.0-4080420251103001",
         "@dcloudio/uni-quickapp-webview": "3.0.0-4080420251103001",
+        "@dcloudio/uni-ui": "^1.5.12",
         "pinia": "^2.3.1",
         "uni-ui": "^1.0.0",
         "vue": "^3.4.21",
@@ -2592,6 +2593,12 @@
         "debug": "4.3.7"
       }
     },
+    "node_modules/@dcloudio/uni-ui": {
+      "version": "1.5.12",
+      "resolved": "https://registry.npmmirror.com/@dcloudio/uni-ui/-/uni-ui-1.5.12.tgz",
+      "integrity": "sha512-mGDl2OZSz7D8xcUAzJegWDHOqB4MEFBSW9Esb/oJiu2/3Gk9+P/Z4bA4JZ9jv9VWBYbMrYwaTfK1Z728kABdYg==",
+      "license": "Apache-2.0"
+    },
     "node_modules/@dcloudio/vite-plugin-uni": {
       "version": "3.0.0-4080420251103001",
       "resolved": "https://registry.npmmirror.com/@dcloudio/vite-plugin-uni/-/vite-plugin-uni-3.0.0-4080420251103001.tgz",

+ 1 - 0
package.json

@@ -30,6 +30,7 @@
     "@dcloudio/uni-mp-weixin": "^3.0.0-4080420251103001",
     "@dcloudio/uni-mp-xhs": "3.0.0-4080420251103001",
     "@dcloudio/uni-quickapp-webview": "3.0.0-4080420251103001",
+    "@dcloudio/uni-ui": "^1.5.12",
     "pinia": "^2.3.1",
     "uni-ui": "^1.0.0",
     "vue": "^3.4.21",

+ 10 - 3
src/api/request.ts

@@ -1,6 +1,6 @@
 // HTTP 请求封装
 
-const BASE_URL = 'http://localhost:8080/api'
+const BASE_URL = 'http://localhost:8081/api'
 
 interface RequestOptions {
   url: string
@@ -18,6 +18,13 @@ interface ApiResponse<T = any> {
   timestamp: number
 }
 
+function translateErrorMsg(msg: string): string {
+  if (msg.includes('Password mismatch')) return '密码错误'
+  if (msg.includes('User not found') || msg.includes('Username does not exist')) return '用户不存在'
+  if (msg.includes('Account disabled') || msg.includes('Account locked')) return '账号已被禁用'
+  return msg
+}
+
 export async function request<T = any>(options: RequestOptions): Promise<T> {
   const { url, method = 'GET', data, params, header = {}, loading = true } = options
 
@@ -62,8 +69,8 @@ export async function request<T = any>(options: RequestOptions): Promise<T> {
           if (response.code === 200) {
             resolve(response.data)
           } else {
-            uni.showToast({ title: response.msg || '请求失败', icon: 'none' })
-            reject(new Error(response.msg))
+            uni.showToast({ title: translateErrorMsg(response.msg) || '请求失败', icon: 'none' })
+            reject(new Error(translateErrorMsg(response.msg)))
           }
         } else if (res.statusCode === 401) {
           uni.showToast({ title: '登录已过期,请重新登录', icon: 'none' })

+ 7 - 8
src/components/common/TopBar.vue

@@ -1,8 +1,9 @@
 <template>
   <view
-  class="sticky top-0 z-40"
-  :class="props.gradient ? 'bg-gradient-to-r from-blue-500 to-blue-600' : 'bg-white border-b border-gray-100'"
->
+    class="sticky top-0 z-40"
+    :class="props.gradient ? '' : 'bg-white border-b border-gray-100'"
+    :style="props.gradient ? { background: 'linear-gradient(135deg, #3b82f6, #2563eb)' } : {}"
+  >
     <view class="status-bar" :style="{ height: `${statusBarHeight}px` }"></view>
     <view class="top-bar px-4 flex items-center justify-between">
       <view class="flex items-center flex-1 min-w-0">
@@ -12,11 +13,9 @@
           @click="goBack"
         >
           <text
-            class="text-lg"
-            :class="props.gradient ? 'text-white' : 'text-gray-600'"
-          >
-            ←
-          </text>
+            class="uni-icons text-xl"
+            :class="props.gradient ? 'uniui-arrow-left text-white' : 'uniui-arrow-left text-gray-600'"
+          />
         </view>
         <slot name="left" />
         <text

+ 0 - 2
src/components/home/ConstructionHome.vue

@@ -16,8 +16,6 @@
         </view>
       </template>
     </TopBar>
-
-    <!-- 主内容区域 -->
     <view class="px-4 py-4 gap-y-4 pb-24">
       <!-- 统计卡片 -->
       <uni-card title="工作统计" :is-shadow="false">

+ 0 - 2
src/components/home/DispatchHome.vue

@@ -16,8 +16,6 @@
         </view>
       </template>
     </TopBar>
-
-    <!-- 主内容区域 -->
     <view class="px-4 py-4 gap-y-4 pb-24">
       <!-- 统计卡片 -->
       <uni-card title="今日任务统计" :is-shadow="false" :is-full="false">

+ 0 - 2
src/components/home/SalesHome.vue

@@ -17,8 +17,6 @@
         </view>
       </template>
     </TopBar>
-
-    <!-- 主内容区域 -->
     <view class="px-4 py-4 gap-y-4">
       <!-- 统计卡片 -->
       <uni-card title="数据概览" :is-shadow="false" :is-full="false">

+ 1 - 1
src/components/my/ProfileView.vue

@@ -1,7 +1,7 @@
 <template>
   <view class="app-container pb-20">
     <!-- 顶部个人信息 -->
-    <view class="bg-gradient-to-br from-blue-900 to-blue-800 px-4 pb-6 rounded-b-3xl">
+    <view class="px-4 pb-6 rounded-b-3xl" style="background: linear-gradient(135deg, #1e3a8a, #1e40af);">
       <StatusBar />
       <view class="flex items-center pt-3">
         <view class="w-16 h-16 rounded-full flex items-center justify-center mr-4" style="background-color: rgba(255, 255, 255, 0.2);">

+ 41 - 23
src/pages/login/login.vue

@@ -6,24 +6,24 @@
       <view class="w-20 h-20 bg-primary rounded-2xl flex items-center justify-center mb-4 mx-auto shadow-card">
         <text class="text-3xl font-bold text-white">清</text>
       </view>
-      <text class="text-gray-800 text-2xl font-bold">清道夫系统</text>
+      <text class="text-gray-800 text-2xl font-bold">绿水青山</text>
       <text class="text-gray-500 text-sm mt-2 block">环境服务管理平台</text>
     </view>
 
     <!-- 角色选择 -->
-    <view class="w-full mb-4">
-      <view class="flex bg-surface rounded-xl p-1 border border-gray-200">
-        <view
-          v-for="item in roleOptions"
-          :key="item.value"
-          class="flex-1 py-2 text-center text-sm font-medium rounded-lg transition-all"
-          :class="selectedRole === item.value ? 'bg-primary text-white shadow-card' : 'text-gray-500'"
-          @click="selectedRole = item.value"
-        >
-          {{ item.label }}
-        </view>
-      </view>
-    </view>
+<!--    <view class="w-full mb-4">-->
+<!--      <view class="flex bg-surface rounded-xl p-1 border border-gray-200">-->
+<!--        <view-->
+<!--          v-for="item in roleOptions"-->
+<!--          :key="item.value"-->
+<!--          class="flex-1 py-2 text-center text-sm font-medium rounded-lg transition-all"-->
+<!--          :class="selectedRole === item.value ? 'bg-primary text-white shadow-card' : 'text-gray-500'"-->
+<!--          @click="selectedRole = item.value"-->
+<!--        >-->
+<!--          {{ item.label }}-->
+<!--        </view>-->
+<!--      </view>-->
+<!--    </view>-->
 
     <!-- 登录表单 -->
     <view class="w-full bg-white rounded-2xl p-6 shadow-card border border-gray-200">
@@ -33,10 +33,10 @@
         <!-- 用户名 -->
         <view>
           <text class="text-sm text-gray-500 block mb-2">用户名</text>
-          <view class="flex items-center bg-surface rounded-xl px-3 py-3 border border-gray-200">
-            <text class="text-gray-400 mr-2"></text>
+          <view class="flex items-center bg-surface rounded-xl px-3 border border-gray-200 login-input-wrap">
+            <text class="uni-icons uniui-person text-gray-400 mr-2"></text>
             <input
-              class="flex-1 bg-transparent text-sm text-gray-800"
+              class="flex-1 login-input"
               placeholder="请输入用户名"
               v-model="form.username"
             />
@@ -46,10 +46,10 @@
         <!-- 密码 -->
         <view>
           <text class="text-sm text-gray-500 block mb-2">密码</text>
-          <view class="flex items-center bg-surface rounded-xl px-3 py-3 border border-gray-200">
-            <text class="text-gray-400 mr-2"></text>
+          <view class="flex items-center bg-surface rounded-xl px-3 border border-gray-200 login-input-wrap">
+            <text class="uni-icons uniui-locked text-gray-400 mr-2"></text>
             <input
-              class="flex-1 bg-transparent text-sm text-gray-800"
+              class="flex-1 login-input"
               placeholder="请输入密码"
               type="password"
               v-model="form.password"
@@ -81,7 +81,6 @@
 <script setup lang="ts">
 import { ref, computed, onMounted } from 'vue'
 import { useAuthStore } from '../../stores/auth'
-import { showToast } from '../../utils'
 
 const authStore = useAuthStore()
 
@@ -118,10 +117,11 @@ async function handleLogin() {
 
   loading.value = true
   try {
-    await authStore.doLogin(form.value.username, form.value.password, selectedRole.value)
+    await authStore.doLogin(form.value.username, form.value.password)
     uni.switchTab({ url: '/pages/home/home' })
   } catch (error: any) {
-    showToast({ title: error.message || '登录失败', icon: 'error' })
+    // 请求封装已统一弹出 toast,这里避免重复提示
+    console.error('登录失败', error)
   } finally {
     loading.value = false
   }
@@ -129,4 +129,22 @@ async function handleLogin() {
 </script>
 
 <style scoped>
+.login-input-wrap {
+  height: 48px;
+}
+
+.login-input {
+  height: 46px;
+  min-height: 46px;
+  font-size: 14px;
+  color: #1f2937;
+  background-color: transparent;
+  border: none;
+  outline: none;
+}
+
+.login-input::placeholder {
+  color: #9ca3af;
+  font-size: 14px;
+}
 </style>

+ 3 - 3
src/stores/auth.ts

@@ -16,8 +16,8 @@ export const useAuthStore = defineStore('auth', () => {
 
   // Actions
   async function doLogin(username: string, password: string, selectedRole?: 'sales' | 'dispatch' | 'construction') {
-    const res = await login({ username, password, roleType: selectedRole })
-    
+    const res = await login({ username, password })
+
     // 后端 roleType: front/dispatch/construction
     // 前端 role: sales/dispatch/construction
     const roleMap: Record<string, 'sales' | 'dispatch' | 'construction'> = {
@@ -26,7 +26,7 @@ export const useAuthStore = defineStore('auth', () => {
       construction: 'construction',
     }
 
-    // 优先使用后端返回的角色,如果后端没有返回则使用用户选择的角色
+    // 优先使用后端返回的角色
     const backendRole = roleMap[res.roleType]
     const finalRole = backendRole || selectedRole || 'sales'
 

+ 57 - 7
src/styles/global.scss

@@ -140,19 +140,69 @@ page {
 }
 
 /* 表单 */
-.form-input {
+.form-input,
+.form-select,
+.form-textarea {
   width: 100%;
+  font-size: 15px;
+  color: $text-primary;
+  box-sizing: border-box;
+  line-height: 1.4;
+}
+
+.form-input,
+.form-input :deep(.uni-easyinput__content) {
+  min-height: 44px;
+  height: 44px;
+  padding: 0 14px !important;
+  border: 1px solid $border-color !important;
+  border-radius: $radius-button !important;
+  background-color: $card-bg !important;
+}
+
+.form-input :deep(.uni-easyinput__content-input) {
+  height: 42px;
+  min-height: 42px;
+  font-size: 15px;
+  color: $text-primary;
+}
+
+.form-input :deep(.uni-easyinput__placeholder-class) {
+  color: $text-tertiary !important;
+  font-size: 15px;
+}
+
+/* uni-easyinput 聚焦状态 */
+.form-input :deep(.uni-easyinput__content.focus) {
+  border-color: $primary !important;
+}
+
+/* uni-easyinput 错误状态 */
+.form-input :deep(.uni-easyinput__content.uni-easyinput__content-error) {
+  border-color: $danger !important;
+}
+
+.form-textarea {
   padding: 12px 14px;
   border: 1px solid $border-color;
   border-radius: $radius-button;
-  font-size: 15px;
-  color: $text-primary;
   background-color: $card-bg;
+  min-height: 100px;
+}
 
-  &:focus {
-    outline: none;
-    border-color: $primary;
-  }
+.form-input-readonly {
+  width: 100%;
+  min-height: 44px;
+  padding: 12px 14px;
+  border: 1px solid $border-color;
+  border-radius: $radius-button;
+  font-size: 15px;
+  color: $text-secondary;
+  background-color: $surface;
+  box-sizing: border-box;
+  line-height: 1.4;
+  display: flex;
+  align-items: center;
 }
 
 .form-label {

+ 16 - 16
src/subPackages/pages-common/addProject.vue

@@ -11,7 +11,7 @@
               项目名称
               <text class="text-red-500">*</text>
             </text>
-            <input class="form-input" placeholder="请输入项目名称" v-model="form.name" @blur="validateField('name')" />
+            <uni-easyinput class="form-input" placeholder="请输入项目名称" v-model="form.name" @blur="validateField('name')" />
             <text v-if="errors.name" class="text-red-500 text-xs mt-1">{{ errors.name }}</text>
           </view>
 
@@ -32,15 +32,15 @@
           <view class="grid grid-cols-3 gap-3">
             <view>
               <text class="form-label">省</text>
-              <input class="form-input" placeholder="省" v-model="form.province" />
+              <uni-easyinput class="form-input" placeholder="省" v-model="form.province" />
             </view>
             <view>
               <text class="form-label">市</text>
-              <input class="form-input" placeholder="市" v-model="form.city" />
+              <uni-easyinput class="form-input" placeholder="市" v-model="form.city" />
             </view>
             <view>
               <text class="form-label">区/县</text>
-              <input class="form-input" placeholder="区/县" v-model="form.district" />
+              <uni-easyinput class="form-input" placeholder="区/县" v-model="form.district" />
             </view>
           </view>
 
@@ -49,7 +49,7 @@
               详细地址
               <text class="text-red-500">*</text>
             </text>
-            <input class="form-input" placeholder="请输入详细地址" v-model="form.address" @blur="validateField('address')" />
+            <uni-easyinput class="form-input" placeholder="请输入详细地址" v-model="form.address" @blur="validateField('address')" />
             <text v-if="errors.address" class="text-red-500 text-xs mt-1">{{ errors.address }}</text>
           </view>
         </view>
@@ -63,7 +63,7 @@
               客户名称
               <text class="text-red-500">*</text>
             </text>
-            <input class="form-input" placeholder="请输入客户名称" v-model="form.customerName" @blur="validateField('customerName')" />
+            <uni-easyinput class="form-input" placeholder="请输入客户名称" v-model="form.customerName" @blur="validateField('customerName')" />
             <text v-if="errors.customerName" class="text-red-500 text-xs mt-1">{{ errors.customerName }}</text>
           </view>
 
@@ -90,7 +90,7 @@
         <view class="gap-y-4">
           <view>
             <text class="form-label">合同金额(元)</text>
-            <input class="form-input" placeholder="请输入合同金额" v-model="form.contractAmount" type="digit" />
+            <uni-easyinput class="form-input" placeholder="请输入合同金额" v-model="form.contractAmount" type="digit" />
           </view>
 
           <view class="grid grid-cols-2 gap-3">
@@ -117,7 +117,7 @@
 
           <view>
             <text class="form-label">服务周期</text>
-            <input class="form-input" placeholder="如:每周2次" v-model="form.serviceCycle" />
+            <uni-easyinput class="form-input" placeholder="如:每周2次" v-model="form.serviceCycle" />
           </view>
         </view>
       </uni-card>
@@ -138,9 +138,9 @@
               </text>
             </view>
             <view class="gap-y-3">
-              <input class="form-input" placeholder="姓名" v-model="contact.name" />
-              <input class="form-input" placeholder="角色/职位" v-model="contact.role" />
-              <input class="form-input" placeholder="联系电话" v-model="contact.phone" type="number" maxlength="11" />
+              <uni-easyinput class="form-input" placeholder="姓名" v-model="contact.name" />
+              <uni-easyinput class="form-input" placeholder="角色/职位" v-model="contact.role" />
+              <uni-easyinput class="form-input" placeholder="联系电话" v-model="contact.phone" type="number" maxlength="11" />
             </view>
           </view>
 
@@ -167,11 +167,11 @@
               </text>
             </view>
             <view class="gap-y-3">
-              <input class="form-input" placeholder="服务点名称" v-model="point.name" />
-              <input class="form-input" placeholder="服务点地址" v-model="point.address" />
-              <input class="form-input" placeholder="服务类型" v-model="point.serviceType" />
-              <input class="form-input" placeholder="联系人" v-model="point.contactName" />
-              <input class="form-input" placeholder="联系电话" v-model="point.contactPhone" type="number" maxlength="11" />
+              <uni-easyinput class="form-input" placeholder="服务点名称" v-model="point.name" />
+              <uni-easyinput class="form-input" placeholder="服务点地址" v-model="point.address" />
+              <uni-easyinput class="form-input" placeholder="服务类型" v-model="point.serviceType" />
+              <uni-easyinput class="form-input" placeholder="联系人" v-model="point.contactName" />
+              <uni-easyinput class="form-input" placeholder="联系电话" v-model="point.contactPhone" type="number" maxlength="11" />
             </view>
           </view>
 

+ 3 - 21
src/subPackages/pages-common/changePassword.vue

@@ -9,19 +9,19 @@
           <text class="form-label">
             <text class="text-red-500">*</text>当前密码
           </text>
-          <input class="form-input" type="password" v-model="currentPassword" placeholder="请输入当前密码" />
+          <uni-easyinput class="form-input"type="password" v-model="currentPassword" placeholder="请输入当前密码" />
         </view>
         <view class="form-group">
           <text class="form-label">
             <text class="text-red-500">*</text>新密码
           </text>
-          <input class="form-input" type="password" v-model="newPassword" placeholder="请输入新密码(6-20位)" />
+          <uni-easyinput class="form-input"type="password" v-model="newPassword" placeholder="请输入新密码(6-20位)" />
         </view>
         <view class="form-group">
           <text class="form-label">
             <text class="text-red-500">*</text>确认新密码
           </text>
-          <input class="form-input" type="password" v-model="confirmNewPassword" placeholder="请再次输入新密码" />
+          <uni-easyinput class="form-input"type="password" v-model="confirmNewPassword" placeholder="请再次输入新密码" />
         </view>
 
         <view class="mt-4">
@@ -93,22 +93,4 @@ function submitChangePassword() {
 .form-group {
   margin-bottom: 16px;
 }
-
-.form-label {
-  display: block;
-  font-size: 14px;
-  color: #666;
-  margin-bottom: 8px;
-}
-
-.form-input {
-  width: 100%;
-  padding: 12px 16px;
-  border: 1px solid #e5e7eb;
-  border-radius: 8px;
-  font-size: 14px;
-  color: #333;
-  background: #fff;
-  box-sizing: border-box;
-}
 </style>

+ 4 - 22
src/subPackages/pages-common/publishTask.vue

@@ -101,7 +101,7 @@
           <text class="form-label">
             <text class="text-red-500">*</text>故障位置
           </text>
-          <input class="form-input" v-model="faultLocation" placeholder="请输入故障位置" />
+          <uni-easyinput class="form-input" v-model="faultLocation" placeholder="请输入故障位置" />
         </view>
 
         <!-- 地理位置 -->
@@ -219,19 +219,19 @@
         <!-- 预计作业量 -->
         <view class="form-group">
           <text class="form-label">预计作业量</text>
-          <input class="form-input" v-model="estimatedQuantity" placeholder="如 2个堵点" />
+          <uni-easyinput class="form-input" v-model="estimatedQuantity" placeholder="如 2个堵点" />
         </view>
 
         <!-- 预计工时 -->
         <view class="form-group">
           <text class="form-label">预计工时</text>
-          <input class="form-input" v-model="estimatedHours" placeholder="如 2小时" />
+          <uni-easyinput class="form-input" v-model="estimatedHours" placeholder="如 2小时" />
         </view>
 
         <!-- 验收要求 -->
         <view class="form-group">
           <text class="form-label">验收要求</text>
-          <input class="form-input" v-model="acceptanceReq" placeholder="如 正常走水" />
+          <uni-easyinput class="form-input" v-model="acceptanceReq" placeholder="如 正常走水" />
         </view>
 
         <!-- 验收方式 -->
@@ -458,24 +458,6 @@ function submitTask() {
   margin-bottom: 16px;
 }
 
-.form-label {
-  display: block;
-  font-size: 14px;
-  color: #666;
-  margin-bottom: 8px;
-}
-
-.form-input {
-  width: 100%;
-  padding: 12px 16px;
-  border: 1px solid #e5e7eb;
-  border-radius: 8px;
-  font-size: 14px;
-  color: #333;
-  background: #fff;
-  box-sizing: border-box;
-}
-
 .form-select {
   width: 100%;
   padding: 12px 16px;

+ 0 - 17
src/subPackages/pages-dispatch/taskDetail.vue

@@ -337,23 +337,6 @@ async function arrangeTeam() {
   margin-bottom: 16px;
 }
 
-.form-label {
-  display: block;
-  font-size: 14px;
-  color: #666;
-  margin-bottom: 8px;
-}
-
-.form-input {
-  width: 100%;
-  padding: 12px 16px;
-  border: 1px solid #e5e7eb;
-  border-radius: 8px;
-  font-size: 14px;
-  color: #333;
-  background: #fff;
-}
-
 .form-textarea {
   width: 100%;
   padding: 12px 16px;

+ 4 - 0
vite.config.ts

@@ -5,6 +5,10 @@ import path from 'path'
 // https://vitejs.dev/config/
 export default defineConfig({
   plugins: [uni()],
+  server: {
+    host: '0.0.0.0',
+    port: 5173,
+  },
   resolve: {
     alias: {
       '@': path.resolve(__dirname, 'src'),