Ver código fonte

修改问题

快乐的梦鱼 1 semana atrás
pai
commit
7412902fdf
40 arquivos alterados com 1160 adições e 1569 exclusões
  1. 0 27
      package-lock.json
  2. 0 1
      package.json
  3. 1 0
      src/App.vue
  4. 48 55
      src/api/auth/UserApi.ts
  5. 0 3
      src/common/scss/common.scss
  6. 0 353
      src/common/scss/font.scss
  7. 0 4
      src/common/scss/font_num.scss
  8. 29 0
      src/components/composeabe/DataLoader.ts
  9. 0 4
      src/components/dynamic/DynamicForm.vue
  10. 18 3
      src/components/dynamic/DynamicFormControl.vue
  11. 29 13
      src/components/dynamic/wrappers/CheckBoxList.vue
  12. 3 0
      src/components/dynamic/wrappers/PickerCityField.ts
  13. 48 0
      src/components/dynamic/wrappers/PickerCityField.vue
  14. 3 4
      src/components/dynamic/wrappers/PickerIdField.vue
  15. 0 1
      src/components/form/Cascader.vue
  16. 10 0
      src/components/form/CheckBoxGroup.vue
  17. 26 6
      src/components/form/Field.vue
  18. 3 1
      src/components/form/Form.vue
  19. 1 0
      src/components/form/FormContext.ts
  20. 2 2
      src/components/layout/FlexView.vue
  21. 10 3
      src/components/layout/space/Height.vue
  22. 7 3
      src/components/layout/space/Width.vue
  23. 0 1
      src/components/list/FixedVirtualList.vue
  24. 2 1
      src/components/list/SimpleList.vue
  25. 4 6
      src/components/nav/NavBar.vue
  26. 10 0
      src/components/nav/TabBar.vue
  27. 8 10
      src/components/nav/Tabs.vue
  28. 2 2
      src/components/typography/HorizontalScrollText.vue
  29. 86 109
      src/pages.json
  30. 22 85
      src/pages/dig/forms/common.vue
  31. 187 186
      src/pages/dig/forms/forms.ts
  32. 0 26
      src/pages/dig/forms/success.vue
  33. 114 0
      src/pages/dig/index.vue
  34. 26 253
      src/pages/index.vue
  35. 28 68
      src/pages/user/index.vue
  36. 8 4
      src/pages/user/login.vue
  37. 54 151
      src/pages/user/update/password.vue
  38. 48 132
      src/pages/user/update/profile.vue
  39. 309 36
      src/static/css/font.css
  40. 14 16
      src/store/auth.ts

+ 0 - 27
package-lock.json

@@ -26,7 +26,6 @@
         "@dcloudio/uni-quickapp-webview": "3.0.0-4070620250821001",
         "@imengyu/imengyu-utils": "^0.0.16",
         "@imengyu/js-request-transform": "^0.3.7",
-        "@imengyu/vue-dynamic-form": "^0.1.1",
         "async-validator": "^4.2.5",
         "pinia": "^3.0.1",
         "tslib": "^2.8.1",
@@ -3067,17 +3066,6 @@
         "dayjs": "^1.11.7"
       }
     },
-    "node_modules/@imengyu/vue-dynamic-form": {
-      "version": "0.1.1",
-      "resolved": "https://registry.npmmirror.com/@imengyu/vue-dynamic-form/-/vue-dynamic-form-0.1.1.tgz",
-      "integrity": "sha512-xyzO7hSwAjp/B8ROwZEMHK4m3Id94ViTb0JSpD/Z7QYb78m72/Io0LkeKkL2t/qBAyo3dvvi3Dewv+Y+ljWP9Q==",
-      "license": "MIT",
-      "dependencies": {
-        "async-validator": "^4.2.5",
-        "scroll-into-view-if-needed": "^3.0.3",
-        "vue": "^3.2.45"
-      }
-    },
     "node_modules/@intlify/core-base": {
       "version": "9.1.9",
       "resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-9.1.9.tgz",
@@ -6183,12 +6171,6 @@
       "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==",
       "license": "MIT"
     },
-    "node_modules/compute-scroll-into-view": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz",
-      "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==",
-      "license": "MIT"
-    },
     "node_modules/computeds": {
       "version": "0.0.1",
       "resolved": "https://registry.npmmirror.com/computeds/-/computeds-0.0.1.tgz",
@@ -10546,15 +10528,6 @@
         "node": ">=10"
       }
     },
-    "node_modules/scroll-into-view-if-needed": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz",
-      "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==",
-      "license": "MIT",
-      "dependencies": {
-        "compute-scroll-into-view": "^3.0.2"
-      }
-    },
     "node_modules/scule": {
       "version": "1.3.0",
       "resolved": "https://registry.npmmirror.com/scule/-/scule-1.3.0.tgz",

+ 0 - 1
package.json

@@ -53,7 +53,6 @@
     "@dcloudio/uni-quickapp-webview": "3.0.0-4070620250821001",
     "@imengyu/imengyu-utils": "^0.0.16",
     "@imengyu/js-request-transform": "^0.3.7",
-    "@imengyu/vue-dynamic-form": "^0.1.1",
     "async-validator": "^4.2.5",
     "pinia": "^3.0.1",
     "tslib": "^2.8.1",

+ 1 - 0
src/App.vue

@@ -42,4 +42,5 @@ configTheme((theme) => {
 
 <style>
 	/*每个页面公共css */
+  @import "@/static/css/font.css";
 </style>

+ 48 - 55
src/api/auth/UserApi.ts

@@ -5,49 +5,46 @@ import AppCofig from '@/common/config/AppCofig';
 export class LoginResult extends DataModel<LoginResult> {
   constructor() {
     super(LoginResult, "登录结果");
-    //this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      token: { clientSide: 'string', clientSideRequired: true },
+      userInfo: { clientSide: 'object', clientSideChildDataModel: UserInfo }
+    };
+    this._nameMapperServer = {
+      'userinfo': 'userInfo',
+    }
     this._beforeSolveServer = (data, self) => {
-      if (!data.userinfo && data.user)
-         data.userinfo = data.user;
-      if (!data.auth && data.userinfo)
-        data.auth = data.userinfo;
-      if (!data.mainBodyUserInfo && data.userinfo)
-        data.mainBodyUserInfo = data.userinfo;
+      if (data.userinfo)
+      data.token = (data.userinfo as any).token;
       return data;
-    };
-    this._convertTable = {
-      mainBodyUserInfo: { clientSide: 'object', clientSideRequired: true, clientSideChildDataModel: UserInfo },
-      auth: { 
-        clientSide: 'object', 
-        clientSideRequired: true, 
-        clientSideChildDataModel: {
-          nameMapperServer: {
-            user_id: 'userId',
-            expires_in: 'expiresIn',
-          },
-          convertTable: {
-            token: { clientSide: 'string', clientSideRequired: true },
-            userId: { clientSide: 'number' },
-            expiresIn: { clientSide: 'number' },
-          }
-        } 
-      },
+    }
+    this._afterSolveServer = () => {
+      if (!this.userInfo.id)
+        this.userInfo.id = this.id;
+      if (!this.userInfo.mobile)
+        this.userInfo.mobile = this.mobile;
+      if (!this.userInfo.nickname)
+        this.userInfo.nickname = this.nickname;
+      if (!this.userInfo.avatar)
+        this.userInfo.avatar = this.avatar;
+      if (!this.userInfo.username)
+        this.userInfo.username = this.username;
     }
   }
-  auth !: {
-    id: number,
-    username: string,
-    nickname: string,
-    mobile: string,
-    avatar: string,
-    score: number,
-    token: string,
-    userId: number,
-    createtime: Date,
-    expiretime: Date,
-    expiresIn: number,
-  };
-  mainBodyUserInfo !:UserInfo;
+  id = 0;
+  username = '';
+  nickname = '';
+  mobile = '';
+  avatar = '';
+  bio = '';
+  score = null as number|null;
+  token = '';
+  userId = null as number|null;
+  createtime = null as number|null;
+  expiretime = null as number|null;
+  expiresIn = null as number|null;
+  inheritorId = null as number|null;
+  loginType = 0;
+  userInfo = new UserInfo();
 }
 export class UserInfo extends DataModel<UserInfo> {
   constructor() {
@@ -55,22 +52,17 @@ export class UserInfo extends DataModel<UserInfo> {
     this.setNameMapperCase('Camel', 'Snake');
     this._convertTable = {
       id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
-      userId: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
-      mainBodyId: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
-      loginTime: { clientSide: 'date', serverSide: 'string' },
     }
   }
 
   id = 0;
   userId = 0;
-  mainBodyId = 0;
+  mobile = '';
   nickname = '';
   avatar = '';
-  intro = '';
-  fans = '';
-  score = '';
-  loginTime = null as null|Date;
-  diyname = '';
+  username = '';
+  regionId = 0;
+  isReviewer = false;
 }
 
 export class UserApi extends AppServerRequestModule<DataModel> {
@@ -108,17 +100,15 @@ export class UserApi extends AppServerRequestModule<DataModel> {
       password: data?.password,
     }, '登录', undefined, LoginResult)).data as LoginResult;
   }
-  async getUserInfo(main_body_user_id: number) {
-    return (await this.post('/content/main_body_user/getMainBodyUser', {
-      main_body_user_id,
-    }, '获取用户信息', undefined, UserInfo)).data as UserInfo;
-  }
   async updatePassword(data: {
     newpassword: string,
     oldpassword: string,
   }) {
     return (await this.post('/content/main_body_user/changepwd', data, '更新密码'))
   }
+  async getUserInfo() {
+    return (await this.post('/content/main_body_user/getMainBodyUser', {}, '获取用户信息', undefined, UserInfo)).data as UserInfo;
+  }  
   async updateUserInfo(data: {
     nickname?: string,
     avatar?: string,
@@ -128,20 +118,23 @@ export class UserApi extends AppServerRequestModule<DataModel> {
     return (await this.post('/content/main_body_user/editMainBodyUser', data, '更新用户信息'))
   }
   async updateSystemUserInfo(data: {
+    username?: string,
     nickname?: string,
     avatar?: string,
     bio?: string,
   }) {
     return (await this.post('/user/profile', {
+      username: data.username,
       nickname: data?.nickname,
       avatar: data?.avatar,
       bio: data?.bio,
     }, '更新用户信息'))
   }
   async refresh() {
-    return (await this.post('/content/main_body_user/refreshUser', {
-    }, '刷新用户', undefined, LoginResult)).data as LoginResult;
+    return (await this.post('/ich/inheritor/refresh', {}, '刷新token', undefined, LoginResult)).data as LoginResult;
   }
+
+  
 }
 
 export default new UserApi();

+ 0 - 3
src/common/scss/common.scss

@@ -1,6 +1,3 @@
-@use "font.scss" as *;
-@use "font_num.scss" as *;
-
 page {
   background: #F8F8F8;
   color: #111111;

Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 353
src/common/scss/font.scss


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 4
src/common/scss/font_num.scss


+ 29 - 0
src/components/composeabe/DataLoader.ts

@@ -0,0 +1,29 @@
+import { ref } from "vue"
+
+export function useDataLoader<T>(loader: () => Promise<T>, options: { immediate?: boolean }) {
+  const data = ref<T>()
+  const loading = ref(false)
+  const error = ref<Error>()
+
+  function load() {
+    loading.value = true
+    loader().then((res) => {
+      data.value = res
+      loading.value = false
+    }).catch((err) => {
+      error.value = err
+      loading.value = false
+    })
+  }
+
+  if (options.immediate) {
+    load()
+  }
+
+  return {
+    data,
+    loading,
+    error,
+    load,
+  }
+}

+ 0 - 4
src/components/dynamic/DynamicForm.vue

@@ -65,9 +65,6 @@ const formGlobalParams = toRef(props.formGlobalParams);
 provide('formTopModel', formModel);
 provide('formGlobalParams', formGlobalParams);
 
-watch(formRules, (v) => {
-  formRef.value?.setRules(v);
-});
 watch(() => props.formDefine, (v) => {
   reloadFormData();
 });
@@ -135,7 +132,6 @@ function resetForm() {
 function reloadFormData() {
   if (!formModel.value)
     loadFormData();
-  formRef.value.setRules(formRules.value);
 }
 
 onMounted(() => {

+ 18 - 3
src/components/dynamic/DynamicFormControl.vue

@@ -10,17 +10,21 @@
     </text>
     <Field
       ref="formItemRef"
-      v-if="formDefineItem.type === 'text'"
+      v-else-if="formDefineItem.type === 'text'"
+      :label="label"
       :modelValue="modelValue"
       @update:modelValue="onValueChanged"
       :maxlength="260"
+      :required="Boolean(formDefineItem.rules?.length)"
       v-bind="params"
     />
     <Field
       ref="formItemRef"
-      v-if="formDefineItem.type === 'textarea'"
+      v-else-if="formDefineItem.type === 'textarea'"
       multiline
+      :label="label"
       :modelValue="modelValue"
+      :required="Boolean(formDefineItem.rules?.length)"
       @update:modelValue="onValueChanged"
       v-bind="params"
     />
@@ -73,6 +77,14 @@
           v-bind="(params as any as IDynamicFormItemSelectIdFormItemProps)"
         />
       </template>
+      <template v-else-if="formDefineItem.type === 'select-city'">
+        <PickerCityField
+          ref="itemRef"
+          :modelValue="modelValue"
+          @update:modelValue="onValueChanged"
+          v-bind="(params as any)"
+        />
+      </template>
       <template v-else-if="formDefineItem.type === 'check-box'">
         <CheckBox
           ref="itemRef"
@@ -160,6 +172,9 @@ import CheckBoxList from './wrappers/CheckBoxList.vue';
 import CheckBoxToInt from './wrappers/CheckBoxToInt.vue';
 import type { IDynamicFormItemRadioValueFormItemProps } from './wrappers/RadioValue';
 import type { IDynamicFormItemSelectIdFormItemProps } from './wrappers/PickerIdField';
+import type { CascaderProps } from '../form/Cascader.vue';
+import CascaderField, { type CascaderFieldProps } from '../form/CascaderField.vue';
+import PickerCityField from './wrappers/PickerCityField.vue';
 
 const props = defineProps({	
   parentModel: {
@@ -201,7 +216,7 @@ function evaluateCallbackObj(val: Record<string, unknown|IFormItemCallback<unkno
 }
 
 const params = computed(() => evaluateCallbackObj(props.formDefineItem.params as any))
-const label = computed(() => evaluateCallback(props.formDefineItem.label))
+const label = computed(() => evaluateCallback(props.formDefineItem.label) as string)
 const show = computed(() => props.formDefineItem.hidden == undefined || evaluateCallback(props.formDefineItem.hidden) !== false)
 
 const itemRef = ref();

+ 29 - 13
src/components/dynamic/wrappers/CheckBoxList.vue

@@ -1,42 +1,54 @@
 <template>
-  <view class="check-box-list">
-    <a-spin v-if="loadStatus === 'loading'" />
-    <a-alert 
+  <view>
+    <ActivityIndicator v-if="loadStatus === 'loading'" />
+    <Alert
       v-else-if="loadStatus === 'error'" 
       message="加载失败" 
       description="点击重新加载" 
       type="error" 
-      show-icon 
-      class="pointer"
       @click="handleLoadData"
     />
-    <a-checkbox-group 
+    <CheckBoxGroup 
       v-else 
-      :value="modelValue" 
+      :modelValue="modelValue"
       :disabled="disabled"
-      :options="data2"
-      @update:value="handleChange" 
+      :multiple="multiple"
+      @update:modelValue="handleChange" 
     >
-    </a-checkbox-group>
+      <CheckBox
+        v-for="value in data2"
+        :key="value.value"
+        :value="value.value"
+        :text="value.text" 
+        :disabled="value.disable"
+      />
+    </CheckBoxGroup>
   </view>
 </template>
 
 <script setup lang="ts">
+import ActivityIndicator from '@/components/basic/ActivityIndicator.vue';
+import Alert from '@/components/feedback/Alert.vue';
+import CheckBox from '@/components/form/CheckBox.vue';
+import CheckBoxGroup from '@/components/form/CheckBoxGroup.vue';
 import { onMounted, ref, type PropType } from 'vue';
 
 export interface CheckBoxListItem {
-  label: string;
+  text: string;
   value: any;
   disable?: boolean;
 }
 export interface CheckBoxListProps {
+  multiple?: boolean,
+  disabled?: boolean,
+  className?: string,
   loadData: () => Promise<CheckBoxListItem[]>;
 }
 
 const props = defineProps({
   modelValue: {
-    type: [Array, null] as PropType<any[] | null>,
-    default: () => null
+    type: Array as PropType<string[]>,
+    default: () => []
   },
   loadData: {
     type: Function as PropType<CheckBoxListProps['loadData']>,
@@ -46,6 +58,10 @@ const props = defineProps({
     type: Boolean,
     default: false
   },
+  multiple: {
+    type: Boolean,
+    default: false
+  },
   className: {
     type: String,
     default: ''

+ 3 - 0
src/components/dynamic/wrappers/PickerCityField.ts

@@ -0,0 +1,3 @@
+export interface PickerCityFieldProps {
+  cityDataUrl?: string;
+}

+ 48 - 0
src/components/dynamic/wrappers/PickerCityField.vue

@@ -0,0 +1,48 @@
+<template>
+  <CascaderField
+    :modelValue="modelValue"
+    @update:modelValue="$emit('update:modelValue', $event)"
+    textKey="name"
+    valueKey="code"
+    childrenKey="children"
+    placeholder="请选择省市区" 
+    :data="(ChinaCityData.data.value as CascaderItem[]) || []"
+    v-bind="$attrs"
+  />
+</template>
+
+<script setup lang="ts">
+import { useDataLoader } from '@/components/composeabe/DataLoader';
+import type { CascaderItem } from '@/components/form/Cascader.vue';
+import CascaderField from '@/components/form/CascaderField.vue';
+import type { PropType } from 'vue';
+
+const props = defineProps({
+  modelValue: {
+    type: Array as PropType<string[]>,
+    default: () => [],
+  },
+  cityDataUrl: {
+    type: String,
+    default: 'https://mn.wenlvti.net/app_static/xiangan/city-data.json',
+  }
+})
+defineEmits(['update:modelValue'])
+
+const ChinaCityData = useDataLoader(() => {
+  return new Promise((resolve, reject) => {
+    uni.request({
+      url: props.cityDataUrl,
+      method: 'GET',
+      success: (res) => {
+        resolve(res.data)
+      },
+      fail: (err) => {
+        reject(err)
+      }
+    })
+  })
+}, {
+  immediate: true,
+});
+</script>

+ 3 - 4
src/components/dynamic/wrappers/PickerIdField.vue

@@ -1,6 +1,6 @@
 <template>
   <PickerField 
-    :columns="[ loader.content.value || [] ]"
+    :columns="[ loader.data.value || [] ]"
     v-bind="$attrs"
   />
 </template>
@@ -8,11 +8,10 @@
 <script setup lang="ts">
 import PickerField from '@/components/form/PickerField.vue';
 import type { IDynamicFormItemSelectIdFormItemProps, IDynamicFormItemSelectIdOption } from './PickerIdField';
-import { useSimplePageContentLoader } from '@/common/composeabe/SimplePageContentLoader';
+import { useDataLoader } from '@/components/composeabe/DataLoader';
 
 const props = defineProps<IDynamicFormItemSelectIdFormItemProps>();
-
-const loader = useSimplePageContentLoader<IDynamicFormItemSelectIdOption[]>(props.loadData, {
+const loader = useDataLoader<IDynamicFormItemSelectIdOption[]>(props.loadData, {
   immediate: true,
 });
 </script>

+ 0 - 1
src/components/form/Cascader.vue

@@ -262,6 +262,5 @@ defineOptions({
   position: relative;
   display: flex;
   flex-direction: column;
-  align-items: center;
 }
 </style>

+ 10 - 0
src/components/form/CheckBoxGroup.vue

@@ -17,6 +17,11 @@ export interface CheckBoxGroupProps {
    * @default false
    */
   disabled?: boolean;
+  /**
+   * 是否支持多选。仅限制用户选择,通过代码设置值时,不受到此限制。
+   * @default true
+   */
+  multiple?: boolean;
 }
 export interface CheckBoxGroupInterface {
   /**
@@ -47,6 +52,7 @@ export interface CheckBoxGroupContextInfo {
 const props = withDefaults(defineProps<CheckBoxGroupProps>(), {
   modelValue: () => [] ,
   disabled: false,
+  multiple: true,
 });
 
 const {
@@ -71,6 +77,10 @@ const emit = defineEmits([ 'update:modelValue' ]);
 const allCheck = new Map<string, boolean>();
 
 function onValueChange(name: string|undefined, checked: boolean) {
+  if (checked && !props.multiple) {
+    updateValue([name as string]);
+    return;
+  }
   const valueNew = (value.value || []).concat();
   if (checked)
     ArrayUtils.addOnce(valueNew, name as string);

+ 26 - 6
src/components/form/Field.vue

@@ -1,5 +1,5 @@
 <template>
-  <FlexRow
+  <FlexView
     :touchable="touchable || childOnClickListener !== undefined"
     :pressedColor="themeContext.resolveThemeColor('FieldPressedColor', 'pressed.white')"
     :innerStyle="{ 
@@ -8,7 +8,8 @@
       ...(focused ? activeFieldStyle : {}),
       ...(error || finalErrorMessage ? errorFieldStyle : {})
     }"
-    :center="center"
+    :direction="labelPosition === 'top' ? 'column' : 'row'"
+    :center="labelPosition === 'top' ? false : true"
     @click="onClick"
   >
     <!-- 左边的标签区域 -->
@@ -49,7 +50,7 @@
               textAlign: inputAlign,
             }"
             :autoHeight="autoHeight"
-            :value="modelValue"
+            :value="inputValue"
             :password="type==='password'"
             :placeholder="placeholder"
             :placeholder-style="`color: ${themeContext.resolveThemeColor(error ? errorTextColor : placeholderTextColor)}`"
@@ -73,7 +74,7 @@
               color: themeContext.resolveThemeColor(disabled ? inputDisableColor : (error ? errorTextColor : inputColor)),
               textAlign: inputAlign,
             }"
-            :value="modelValue"
+            :value="inputValue"
             :password="type==='password'"
             :placeholder="placeholder"
             :placeholder-style="`color: ${themeContext.resolveThemeColor(error ? errorTextColor : placeholderTextColor)}`"
@@ -116,11 +117,11 @@
       v-bind="clearButtonProps"
       @click="onClear"
     />
-  </FlexRow>
+  </FlexView>
 </template>
 
 <script setup lang="ts">
-import { computed, inject, onBeforeUnmount, provide, ref } from 'vue';
+import { computed, inject, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue';
 import { propGetThemeVar, useTheme, type TextStyle, type ViewStyle } from '../theme/ThemeDefine';
 import { FormItemContextContextKey, propGetFormContext, type FormContext, type FormItemContext, type FormItemInternalContext } from './FormContext';
 import { DynamicColor, DynamicSize, DynamicVar, selectStyleType } from '../theme/ThemeTools';
@@ -129,6 +130,7 @@ import Icon from '../basic/Icon.vue';
 import IconButton from '../basic/IconButton.vue';
 import FlexCol from '../layout/FlexCol.vue';
 import FlexRow from '../layout/FlexRow.vue';
+import FlexView from '../layout/FlexView.vue';
 
 
 export interface FieldInstance {
@@ -243,6 +245,11 @@ export interface FieldProps {
    */
   labelAlign?: 'left'|'center'|'right';
   /**
+   * 左侧文本的位置
+   * @default 'left'
+   */
+  labelPosition?: 'top'|'left';
+  /**
    * 左侧文本的flex占比
    * @default undefined
    */
@@ -392,6 +399,7 @@ const props = withDefaults(defineProps<FieldProps>(), {
   clearButtonMode: () => propGetFormContext()?.fieldProps.value?.clearButtonMode ?? 'always',
   labelWidth: () => propGetFormContext()?.labelWidth.value ?? "left",
   labelAlign: () => propGetFormContext()?.labelAlign.value ?? "left",
+  labelPosition: () => propGetFormContext()?.labelPosition.value ?? 'left',
   inputAlign: () => propGetFormContext()?.fieldProps.value?.inputAlign ?? "left",
   type: () => propGetFormContext()?.fieldProps.value?.type ?? "text",
   formatTrigger: () => propGetFormContext()?.fieldProps.value?.formatTrigger ?? 'input',
@@ -401,6 +409,8 @@ const props = withDefaults(defineProps<FieldProps>(), {
   disabled: false,
   readonly: false,
   autoHeight: true,
+  maxLength: 100,
+  modelValue: undefined,
 });
 
 //#region Context
@@ -525,11 +535,21 @@ const finalErrorMessage = computed(() => {
   return props.errorMessage || error.value || '';
 })
 
+const inputValue = ref();
+
 const focused = ref(false);
 const inputRef = ref();
 
+watch(() => props.modelValue, (newValue) => {
+  inputValue.value = newValue;
+});
+onMounted(() => {
+  inputValue.value = props.modelValue ?? formItemContext.getFormModelValue();
+});
+
 function emitChangeText(text: string) {
   emit('update:modelValue', text);
+  inputValue.value = text;
   formItemContext.onFieldChange(text);
 }
 function doFormatter(text: string) {

+ 3 - 1
src/components/form/Form.vue

@@ -38,6 +38,7 @@ export interface FormProps {
   name?: string,
   labelFlex?: number, 
   labelWidth?: string|number;
+  labelPosition?: 'top'|'left';
   labelAlign?: "left" | "center" | "right";
   inputFlex?: number,
   /**
@@ -151,7 +152,7 @@ function accessFormModel(keyName: string, isSet: boolean, setValue: unknown) : u
   return ret;
 } 
 const { 
-  labelFlex, inputFlex, colon, labelAlign, labelWidth, model, rules,
+  labelFlex, inputFlex, colon, labelAlign, labelPosition, labelWidth, model, rules,
   validateTrigger, showLabel, addRequireMark, name, fieldProps,
 } = toRefs(props);
 
@@ -197,6 +198,7 @@ const formContext : FormContext = {
   addRequireMark,
   colon,
   labelAlign,
+  labelPosition,
   labelFlex,
   labelWidth,
   inputFlex,

+ 1 - 0
src/components/form/FormContext.ts

@@ -34,6 +34,7 @@ export type FormContext = {
   colon: Ref<boolean|undefined>;
   labelWidth: Ref<string|number|undefined>;
   labelAlign: Ref<"left"|"center"|"right"|undefined>;
+  labelPosition: Ref<'top'|'left'|undefined>;
   labelFlex: Ref<number|undefined>;
   inputFlex: Ref<number|undefined>;
   showLabel: Ref<boolean|undefined>;

+ 2 - 2
src/components/layout/FlexView.vue

@@ -1,6 +1,6 @@
 <template>
   <view
-    :id="id"
+    :id="innerId"
     :class="[
       'nana-flex-layout', { 
         'nana-flex-row': direction === 'row', 
@@ -36,7 +36,7 @@ export type FlexAlignType = "stretch"|'center'|'start'|'end'|'flex-start' | 'fle
 export type StateType = 'default' | 'active' | 'pressed';
 
 export interface FlexProps {
-  id?: string,
+  innerId?: string,
   /**
    * 盒子定位
    */ 

+ 10 - 3
src/components/layout/space/Height.vue

@@ -1,7 +1,7 @@
 <template>
   <div 
     class="nana-space" 
-    :style="{ height: resolveThemeSize(size) }"
+    :style="{ height: resolveThemeSize(size ?? height) }"
   >
     <slot />
   </div>
@@ -16,8 +16,15 @@ defineProps({
    */
   size: {
     type: [ Number, String ],
-    default: '',
-  }
+    default: undefined,
+  },
+  /**
+   * 高度
+   */
+  height: {
+    type: [ Number, String ],
+    default: undefined,
+  },
 })
 
 const { resolveThemeSize } = useTheme();

+ 7 - 3
src/components/layout/space/Width.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="nana-space" :style="{ width: resolveThemeSize(size) }">
+  <div class="nana-space" :style="{ width: resolveThemeSize(size ?? width) }">
     <slot />
   </div>
 </template>
@@ -13,8 +13,12 @@ defineProps({
    */
   size: {
     type: [ Number, String ],
-    default: '',
-  }
+    default: undefined,
+  },
+  width: {
+    type: [ Number, String ],
+    default: undefined,
+  },
 })
 
 const { resolveThemeSize } = useTheme();

+ 0 - 1
src/components/list/FixedVirtualList.vue

@@ -9,7 +9,6 @@
       :scroll-y="direction === 'vertical'"
       :scroll-x="direction === 'horizontal'"
       @scroll="handleScroll"
-      v-bind="$attrs"
     >
       <slot name="prefix" />
       <!-- 占位容器,用于撑开滚动区域 -->

+ 2 - 1
src/components/list/SimpleList.vue

@@ -19,9 +19,11 @@
         :checked="checkedList.indexOf(item) >= 0"
         @click="onItemPress(item, index)"
       >
+        <!-- #ifndef MP -->
         <template v-if="$slots.itemContent" #itemContent>
           <slot name="itemContent" :item="item" :index="index" />
         </template>
+        <!-- #endif -->
       </SimpleListItem>
     </template>
     <template #empty>
@@ -67,7 +69,6 @@ import Empty from '../feedback/Empty.vue';
 import FixedVirtualList from './FixedVirtualList.vue';
 import SimpleListItem from './SimpleListItem.vue';
 
-
 export interface SimpleListProps<T> {
   /**
    * 是否使用虚拟列表

+ 4 - 6
src/components/nav/NavBar.vue

@@ -33,9 +33,8 @@
         }"
         :color="textColor"
         :textAlign="align"
-      > 
-        {{ title }}
-      </HorizontalScrollText>
+        :text="title"
+      />
       <Text
         v-else
         :outerStyle="{
@@ -45,9 +44,8 @@
         }"
         :textAlign="align"
         :color="textColor"
-      >
-        {{ title }}
-      </Text>
+        :text="title"
+      />
     </slot>
     <view 
       class="nana-nav-button-wrapper-end"

+ 10 - 0
src/components/nav/TabBar.vue

@@ -2,6 +2,12 @@
   <view class="nana-tab-bar" :style="{
     ...themeStyles.tabBar.value, 
     ...innerStyle,
+    ...(fixed ? {
+      position: 'fixed',
+      bottom: 0,
+      left: 0,
+      right: 0,
+    } : {}),
     paddingBottom: xbarSpace ? 
       `${safeAreaBottom}px` 
       : themeStyles.tabBar.value.paddingBottom
@@ -43,6 +49,10 @@ export interface TabBarProps {
    * 预留底部安全区间距
    */
   xbarSpace?: boolean,
+  /**
+   * 是否固定在底部
+   */
+  fixed?: boolean,
 }
 
 defineOptions({

+ 8 - 10
src/components/nav/Tabs.vue

@@ -27,10 +27,10 @@
           :width="itemWidthArr[index].width" 
           :active="currentIndex == index"
           :onClick="() => onTabClick(index)"
-          :id="`tab-item-${index}`"
+          :tabId="`${idPrefix}${index}`"
         >
           <FlexView
-            :id="`tab-item-${index}`"
+            :innerId="`${idPrefix}${index}`"
             direction="row"
             :pressedColor="themedUnderlayColor"
             :innerStyle="itemStyle"
@@ -80,11 +80,14 @@
 </template>
 
 <script setup lang="ts">
-import { computed, getCurrentInstance, nextTick, onMounted, ref, watch } from 'vue';
+import { computed, nextTick, onMounted, ref, watch } from 'vue';
 import type { BadgeProps } from '../display/Badge.vue';
 import Badge from '../display/Badge.vue';
 import FlexView from '../layout/FlexView.vue';
 import { propGetThemeVar, useTheme, type TextStyle, type ViewStyle } from '../theme/ThemeDefine';
+import { RandomUtils } from '@imengyu/imengyu-utils';
+
+const idPrefix = RandomUtils.genNonDuplicateIDHEX(8) + '-tab-item-';
 
 export interface TabsItemData {
   /**
@@ -264,8 +267,7 @@ const itemWidthArr = computed(() => {
         width: itemWidth,
         indicatorWidth: itemIndicatorWidth,
       });
-      if (itemWidth < 0) 
-        nextTick(() => measureTab(i))
+      nextTick(() => measureTab(i))
     }
   }
   return result;
@@ -276,12 +278,8 @@ const currentScrollPos = ref(0);
 const currentIndicatorPos = ref(0);
 const currentIndicatorWidth = ref(0);
 
-
-
-const instance = getCurrentInstance();
-
 function measureTab(index: number) {
-  const tabItem = uni.createSelectorQuery().in(instance).select(`#tab-item-${index}`);
+  const tabItem = uni.createSelectorQuery().select(`#${idPrefix}${index}`);
   tabItem.boundingClientRect().exec((res) => {
     if (res[0])
       mersuredTabs.value[index] = res[0].width;

+ 2 - 2
src/components/typography/HorizontalScrollText.vue

@@ -54,7 +54,7 @@ async function getTextWidth() {
   return await new Promise<number>((resolve) => {
     uni.createSelectorQuery()
       .select(`#${id} .inner .real-text`)
-      .boundingClientRect((data) => resolve((data as UniApp.NodeInfo).width ?? 0))
+      .boundingClientRect((data) => resolve((data as UniApp.NodeInfo)?.width ?? 0))
       .exec();
   })
 }
@@ -68,7 +68,7 @@ async function lodScrollInfo() {
   const conWidth = await new Promise<number>((resolve) => {
     uni.createSelectorQuery()
       .select(`#${id}`)
-      .boundingClientRect((data) => resolve((data as UniApp.NodeInfo).width ?? 0))
+      .boundingClientRect((data) => resolve((data as UniApp.NodeInfo)?.width ?? 0))
       .exec();
   })
 

+ 86 - 109
src/pages.json

@@ -39,7 +39,92 @@
         "navigationBarTitleText": "修改个人信息",
         "enablePullDownRefresh": false
       }
+    },
+    {
+      "path": "pages/dig/details",
+      "style": {
+        "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-详情",
+        "enablePullDownRefresh": false
+      }
+    },
+    {
+      "path": "pages/dig/admin",
+      "style": {
+        "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-管理员",
+        "enablePullDownRefresh": false
+      }
+    },
+    {
+      "path": "pages/dig/task/environment",
+      "style": {
+        "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-环境格局",
+        "enablePullDownRefresh": false
+      }
+    },
+    {
+      "path": "pages/dig/task/food",
+      "style": {
+        "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-地道美食",
+        "enablePullDownRefresh": false
+      }
+    },
+    {
+      "path": "pages/dig/task/mine",
+      "style": {
+        "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-物产资源",
+        "enablePullDownRefresh": false
+      }
+    },
+    {
+      "path": "pages/dig/task/summary",
+      "style": {
+        "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-村落概况",
+        "enablePullDownRefresh": false
+      }
+    },
+    {
+      "path": "pages/dig/task/building",
+      "style": {
+        "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-传统建筑",
+        "enablePullDownRefresh": false
+      }
+    },
+    {
+      "path": "pages/dig/task/trip",
+      "style": {
+        "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-旅游路线",
+        "enablePullDownRefresh": false
+      }
+    },
+    {
+      "path": "pages/dig/task/custom",
+      "style": {
+        "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-民俗文化",
+        "enablePullDownRefresh": false
+      }
+    },
+    {
+      "path": "pages/dig/task/history",
+      "style": {
+        "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-历史文化",
+        "enablePullDownRefresh": false
+      }
+    },
+    {
+      "path": "pages/dig/forms/common",
+      "style": {
+        "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-提交信息",
+        "enablePullDownRefresh": false
+      }
+    },
+    {
+      "path": "pages/dig/forms/list",
+      "style": {
+        "navigationBarTitleText": "信息列表",
+        "enablePullDownRefresh": false
+      }
     }
+
   ],
   "globalStyle": {
     "navigationBarTextStyle": "white",
@@ -47,113 +132,5 @@
     "navigationBarBackgroundColor": "#FF8719",
     "backgroundColor": "#F8F8F8"
   },
-  "subPackages": [
-    {
-      "root": "pages/dig",
-      "pages": [
-        {
-          "path": "details",
-          "style": {
-            "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-详情",
-            "enablePullDownRefresh": false
-          }
-        },
-        {
-          "path": "admin",
-          "style": {
-            "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-管理员",
-            "enablePullDownRefresh": false
-          }
-        },
-        {
-          "path": "task/environment",
-          "style": {
-            "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-环境格局",
-            "enablePullDownRefresh": false
-          }
-        },
-        {
-          "path": "task/food",
-          "style": {
-            "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-地道美食",
-            "enablePullDownRefresh": false
-          }
-        },
-        {
-          "path": "task/mine",
-          "style": {
-            "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-物产资源",
-            "enablePullDownRefresh": false
-          }
-        },
-        {
-          "path": "task/summary",
-          "style": {
-            "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-村落概况",
-            "enablePullDownRefresh": false
-          }
-        },
-        {
-          "path": "task/building",
-          "style": {
-            "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-传统建筑",
-            "enablePullDownRefresh": false
-          }
-        },
-        {
-          "path": "task/trip",
-          "style": {
-            "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-旅游路线",
-            "enablePullDownRefresh": false
-          }
-        },
-        {
-          "path": "task/custom",
-          "style": {
-            "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-民俗文化",
-            "enablePullDownRefresh": false
-          }
-        },
-        {
-          "path": "task/history",
-          "style": {
-            "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-历史文化",
-            "enablePullDownRefresh": false
-          }
-        },
-        {
-          "path": "forms/common",
-          "style": {
-            "navigationBarTitleText": "乡源·乡村文化资源挖掘平台-提交信息",
-            "enablePullDownRefresh": false
-          }
-        },
-        {
-          "path": "forms/list",
-          "style": {
-            "navigationBarTitleText": "信息列表",
-            "enablePullDownRefresh": false
-          }
-        }
-      ]
-    }
-  ],
-  "uniIdRouter": {},
-  "tabBar": {
-    "selectedColor": "#FF8719",
-    "list": [
-      {
-        "pagePath": "pages/index",
-        "iconPath": "/static/images/tabs/home.png",
-        "selectedIconPath": "/static/images/tabs/home-active.png",
-        "text": "村史"
-      },
-      {
-        "pagePath": "pages/user/index",
-        "iconPath": "/static/images/tabs/user.png",
-        "selectedIconPath": "/static/images/tabs/user-active.png",
-        "text": "我的"
-      }
-    ]
-  }
+  "uniIdRouter": {}
 }

+ 22 - 85
src/pages/dig/forms/common.vue

@@ -1,19 +1,20 @@
 <template>
-  <view class="main">
-    <u-loading-page :loading="loading" /> 
-    <DynamicForm
-      ref="formRef"
-      :formDefine="formDefine"
-      :formProps="{
-        labelPosition: 'top',
-        labelWidth: '720rpx',
-      }"
-      :formGlobalParams="querys"
-    />
-    <u-button type="primary" @click="submit">提交</u-button>
-    <u-safe-bottom />
-    <Success ref="popupRef" @back="backPrev" />
-  </view>
+  <CommonRoot class="main">
+    <LoadingPage v-if="loading" /> 
+    <FlexCol :padding="30">
+      <DynamicForm
+        ref="formRef"
+        :formDefine="formDefine"
+        :formProps="{
+          labelPosition: 'top',
+          labelAlign: 'left',
+        }"
+        :formGlobalParams="querys"
+      />
+      <Height :height="20" />
+      <Button type="primary" @click="submit">提交</Button>
+    </FlexCol>
+  </CommonRoot>
 </template>
 
 <script setup lang="ts">
@@ -22,11 +23,15 @@ import { showError } from '@/common/composeabe/ErrorDisplay';
 import { useLoadQuerys } from '@/common/composeabe/LoadQuerys';
 import { getVillageInfoForm } from './forms';
 import { backAndCallOnPageBack } from '@imengyu/imengyu-utils/dist/uniapp/PageAction';
+import { RequestApiError } from '@imengyu/imengyu-utils/dist/request';
 import VillageInfoApi, { CommonInfoModel } from '@/api/inhert/VillageInfoApi';
 import DynamicForm from '@/components/dynamic/DynamicForm.vue';
-import Success from './success.vue';
-import { RequestApiError } from '@imengyu/imengyu-utils/dist/request';
+import LoadingPage from '@/components/display/loading/LoadingPage.vue';
+import Button from '@/components/basic/Button.vue';
+import CommonRoot from '@/components/dialog/CommonRoot.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
 import type { FormDefine, FormExport } from '@/components/dynamic';
+import Height from '@/components/layout/space/Height.vue';
 
 const popupRef = ref();
 const loading = ref(false);
@@ -99,72 +104,4 @@ const { querys } = useLoadQuerys({
     formRef.value.loadFormData(formData);
   }
 });
-
-
 </script>
-
-<style lang="scss" scoped>
-.form-block {
-  margin-bottom: 32rpx;
-  padding: 24rpx 26rpx;
-  background: #fff;
-  border-radius: 10rpx;
-}
-
-::v-deep .uni-forms-item__label {
-  font-weight: 600;
-  font-size: 28rpx;
-  color: #23262D;
-  line-height: 36rpx;
-}
-::v-deep .uni-input-placeholder,::v-deep .uni-textarea-placeholder, ::v-deep .uni-select__input-placeholder, ::v-deep .uni-date__x-input, ::v-deep .is-disabled .uni-easyinput__placeholder-class{
-  font-weight: 400;
-  font-size: 28rpx;
-  color: #999999;
-}
-::v-deep .uni-easyinput__content.is-disabled {
-  background-color: #fff;
-  cursor: pointer;
-}
-button[type="primary"] {
-  background: #FF8719;
-  border-radius: 16rpx;
-  font-size: 28rpx;
-  padding: 8rpx;
-  color: #FFFFFF;
-  font-weight: 600;
-}
-.address-select {
-  position: relative;
-  width: 100%;  // 添加宽度
-  height: 100%;
-}
-.input-wrapper {
-  width: 100%;
-  height: 100%;
-  pointer-events: none;  /* 禁用内部元素的点击事件 */
-}
-::v-deep .uni-easyinput__content.is-disabled {
-  background-color: #fff;
-  cursor: pointer;
-  width: 100%;  // 确保输入框宽度占满
-}
-::v-deep .popup-content{
-  text-align: center;
-  background: #FFFFFF;
-  border-radius: 20rpx;
-  padding: 22rpx 44rpx 37rpx;
-  image{
-    width: 158rpx;
-    height:186rpx;
-    display: block;
-    margin: 0 auto;
-    margin-bottom: 12rpx;
-  }
-  text{
-    font-weight: 500;
-    font-size: 24rpx;
-    color: #666666;
-  }
-}
-</style>

Diferenças do arquivo suprimidas por serem muito extensas
+ 187 - 186
src/pages/dig/forms/forms.ts


+ 0 - 26
src/pages/dig/forms/success.vue

@@ -1,26 +0,0 @@
-<template>
-  <uni-popup ref="popupRef">
-    <view class="popup-content">
-      <image mode="aspectFill" src="https://mn.wenlvti.net/uploads/20250313/46adb2f039c6f23a3e69149526eb7e61.png">
-      </image>
-      <text class="text">信息已提交,请耐心等待审核结果</text>
-      <view class="mt-3">
-        <u-button type="primary" @click="$emit('back', true)">好的</u-button>
-      </view>
-    </view>
-  </uni-popup>
-</template>
-
-<script setup lang="ts">
-import { ref } from 'vue';
-
-const popupRef = ref();
-
-defineEmits([ 'back' ])
-defineExpose({
-  open: () => {
-    popupRef.value.open();
-  },
-});
-
-</script>

+ 114 - 0
src/pages/dig/index.vue

@@ -0,0 +1,114 @@
+<template>
+  <view class="submit_main">
+    <Image 
+      mode="aspectFill" 
+      src="https://mn.wenlvti.net/app_static/xiangan/banner_submit.jpg"
+      width="100%"
+    />
+    <view class="main">
+      <SubTitle title="我的村庄" />
+      <RequireLogin unLoginMessage="登录后查看我认领的村庄">
+        <SimplePageContentLoader
+          :loader="villageListLoader"
+          :showEmpty="villageListLoader.content.value?.length == 0"
+          :emptyView="{
+            text: '你还没有认领的村庄',
+            button: true,
+            buttonText: '联系管理员认领',
+            buttonClick: () => {},
+          }"
+        >
+          <FlexCol>
+            <FlexRow 
+              v-for="item in villageListLoader.content.value"
+              :key="item.id"
+              backgroundColor="white"
+              :radius="20"
+              :padding="20"
+            >
+              <Image 
+                mode="aspectFill"
+                :src="item.image" 
+                :radius="20"
+                round
+                :width="130"
+                :height="130"
+              />
+              <Width :size="20" />
+              <FlexCol>
+                <H3>{{ item.villageName }}</H3>
+                <FlexRow align="center" :flex="1" :gap="10">
+                  <Button type="primary" icon="work-filling" @click="goManagePage(item)">管理</Button>
+                  <Button type="default" icon="edit-filling" @click="goSubmitDigPage(item)">采编</Button>
+                </FlexRow>
+              </FlexCol>
+            </FlexRow>
+          </FlexCol>
+        </SimplePageContentLoader>
+      </RequireLogin>
+
+      <SubTitle title="我的贡献" />
+      <RequireLogin unLoginMessage="登录后贡献,加入排行榜">
+        <FlexRow backgroundColor="white" :radius="20" :padding="[40,20]">
+          <FlexCol :flex="1" :gap="10" center>
+            <Text :fontSize="60" fontFamily="Rockwell" color="primary">{{ volunteerInfoLoader.content.value?.points || 0 }}</Text>
+            <Text>文化积分</Text>
+          </FlexCol>
+          <FlexCol :flex="1" :gap="10" center>
+            <Text :fontSize="60" fontFamily="Rockwell" color="primary">Lv.{{ volunteerInfoLoader.content.value?.level || 1 }}</Text>
+            <Text>等级</Text>
+          </FlexCol>
+        </FlexRow>
+      </RequireLogin>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import VillageApi, { VillageListItem } from '@/api/inhert/VillageApi';
+import RequireLogin from '@/common/components/RequireLogin.vue';
+import SimplePageContentLoader from '@/common/components/SimplePageContentLoader.vue';
+import Button from '@/components/basic/Button.vue';
+import Image from '@/components/basic/Image.vue';
+import SubTitle from '@/components/display/title/SubTitle.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
+import Width from '@/components/layout/space/Width.vue';
+import H3 from '@/components/typography/H3.vue';
+import { navTo } from '@/components/utils/PageAction';
+import { useAuthStore } from '@/store/auth';
+import { useCollectStore } from '@/store/collect';
+import { useSimpleDataLoader } from '@/common/composeabe/SimpleDataLoader';
+import Text from '@/components/basic/Text.vue';
+import Icon from '@/components/basic/Icon.vue';
+
+const authStore = useAuthStore();
+const collectStore = useCollectStore();
+const villageListLoader = useSimpleDataLoader(async () => await VillageApi.getClaimedVallageList(), true);
+const volunteerInfoLoader = useSimpleDataLoader(async () =>{
+  const res = await VillageApi.getVolunteerInfo();
+  const collectableModules = (volunteerInfoLoader.content.value?.collectModule as string)?.split(',') || [];
+  collectStore.setCollectableModules(collectableModules);
+  return res;
+}, true);
+const rankListLoader = useSimpleDataLoader(async () => await VillageApi.getVolunteerRanklist(), true);
+
+function goSubmitDigPage(item: VillageListItem) {
+  navTo('./dig/details', { 
+    id: item.villageId,
+    name: item.villageName,
+    villageVolunteerId: item.villageVolunteerId,
+    points: volunteerInfoLoader.content.value?.points,
+    level: volunteerInfoLoader.content.value?.level,
+  })
+}
+function goManagePage(item: VillageListItem) {
+  navTo('./dig/admin', { 
+    id: item.villageId,
+    name: item.villageName,
+    villageVolunteerId: item.villageVolunteerId,
+    points: volunteerInfoLoader.content.value?.points,
+    level: volunteerInfoLoader.content.value?.level,
+  })
+}
+</script>

+ 26 - 253
src/pages/index.vue

@@ -1,260 +1,33 @@
 <template>
-	<view class="submit_main">
-    <view class="img-banner">
-      <image mode="aspectFill" src="https://mn.wenlvti.net/app_static/xiangan/banner_submit.jpg"></image>
-    </view>
-    <view class="main">
-      <view class="cat">
-        <text>我的村庄</text>
-      </view>
-      <RequireLogin unLoginMessage="登录后查看我认领的村庄">
-        <SimplePageContentLoader
-          :loader="villageListLoader"
-          :showEmpty="villageListLoader.content.value?.length == 0"
-          :emptyView="{
-            text: '你还没有认领的村庄',
-            button: true,
-            buttonText: '联系管理员认领',
-            buttonClick: () => {},
-          }"
-        >
-          <view class="village-list">
-            <view 
-              v-for="item in villageListLoader.content.value"
-              :key="item.id"
-              class="item"
-            >
-              <ImageWrapper 
-                mode="aspectFill"
-                :src="item.image" 
-                width="154rpx"
-                height="154rpx"
-              />
-              <view class="info">
-                <view class="name">{{ item.villageName }}</view>
-                <view class="d-flex flex-row align-center">
-                  <view class="btn p-1 pr-2" @click="goManagePage(item)">
-                    <text class="iconfont icon-view"></text>管理
-                  </view>
-                  <view class="btn p-1 pr-2 active" @click="goSubmitDigPage(item)">
-                    <text class="iconfont icon-search"></text>采编
-                  </view>
-                </view>
-              </view>
-            </view>
-          </view>
-        </SimplePageContentLoader>
-      </RequireLogin>
-
-      <view class="cat">
-        <text>我的贡献</text>
-      </view>
-      <RequireLogin unLoginMessage="登录后贡献,加入排行榜">
-        <view class="retribution">
-          <view class="item">
-            <text class="iconfont icon-total-points"></text>
-            <view class="num">{{ volunteerInfoLoader.content.value?.points || 0 }}</view>
-            <view>文化积分</view>
-          </view>
-          <view>
-            <text class="iconfont icon-art-arrow"></text>
-          </view>
-          <view class="item">
-            <text class="iconfont icon-level"></text>
-            <view class="level">Lv.{{ volunteerInfoLoader.content.value?.level || 1 }}</view>
-            <view>等级</view>
-          </view>
-        </view>
-      </RequireLogin>
-    </view>
+	<view>
+    <StatusBarSpace backgroundColor="primary" />
+    <NavBar 
+      title="乡源·乡村文化资源挖掘平台"
+      backgroundColor="primary"
+      textColor="white"
+      :titleScroll="false"
+    />
+    <DigIndex v-if="tabIndex === 0" />
+    <UserIndex v-else-if="tabIndex === 1" />
+    <TabBar
+      v-model:selectedTabIndex="tabIndex"
+      fixed
+      xbarSpace
+    >
+      <TabBarItem :icon="tabIndex === 0 ? '/static/images/tabs/home-active.png' : '/static/images/tabs/home.png'" text="采集" />
+      <TabBarItem :icon="tabIndex === 1 ? '/static/images/tabs/user-active.png' : '/static/images/tabs/user.png'" text="我的" />
+    </TabBar>
 	</view>
 </template>
 
 <script setup lang="ts">
-import VillageApi, { VillageListItem } from '@/api/inhert/VillageApi';
-import ImageWrapper from '@/common/components/ImageWrapper.vue';
-import RequireLogin from '@/common/components/RequireLogin.vue';
-import SimplePageContentLoader from '@/common/components/SimplePageContentLoader.vue';
-import { useReqireLogin } from '@/common/composeabe/RequireLogin';
-import { useSimpleDataLoader } from '@/common/composeabe/SimpleDataLoader';
-import { useAuthStore } from '@/store/auth';
-import { navTo } from '@imengyu/imengyu-utils/dist/uniapp/PageAction';
-import { useCollectStore } from '@/store/collect';
-
-const authStore = useAuthStore();
-const collectStore = useCollectStore();
-const villageListLoader = useSimpleDataLoader(async () => await VillageApi.getClaimedVallageList(), true);
-const volunteerInfoLoader = useSimpleDataLoader(async () =>{
-  const res = await VillageApi.getVolunteerInfo();
-  const collectableModules = (volunteerInfoLoader.content.value?.collectModule as string)?.split(',') || [];
-  collectStore.setCollectableModules(collectableModules);
-  return res;
-}, true);
-const rankListLoader = useSimpleDataLoader(async () => await VillageApi.getVolunteerRanklist(), true);
+import { ref } from 'vue';
+import StatusBarSpace from '@/components/layout/space/StatusBarSpace.vue';
+import NavBar from '@/components/nav/NavBar.vue';
+import TabBar from '@/components/nav/TabBar.vue';
+import TabBarItem from '@/components/nav/TabBarItem.vue';
+import DigIndex from './dig/index.vue';
+import UserIndex from './user/index.vue';
 
-const { requireLogin } = useReqireLogin();
-
-function goClamVillage() {
-  requireLogin(() => navTo('forms/village_claim'), '登录后才能认领村庄哦!');
-}
-function goSubmitDigPage(item: VillageListItem) {
-  navTo('./dig/details', { 
-    id: item.villageId,
-    name: item.villageName,
-    villageVolunteerId: item.villageVolunteerId,
-    points: volunteerInfoLoader.content.value?.points,
-    level: volunteerInfoLoader.content.value?.level,
-  })
-}
-function goManagePage(item: VillageListItem) {
-  navTo('./dig/admin', { 
-    id: item.villageId,
-    name: item.villageName,
-    villageVolunteerId: item.villageVolunteerId,
-    points: volunteerInfoLoader.content.value?.points,
-    level: volunteerInfoLoader.content.value?.level,
-  })
-}
+const tabIndex = ref(0);
 </script>
-
-<style lang="scss">
-.submit_main {
-  .img-banner{
-    border-radius: 0;
-    height: 466rpx;
-  }
-  .cat{
-    font-weight: 600;
-    display: flex;
-    margin: 36rpx 0 40rpx;
-    align-items: center;
-    text{
-      display: block;
-      font-size: 36rpx;
-      color: #312520;
-      flex:1;
-    }
-    .btn{
-      border-radius: 28rpx;
-      border: 1px solid #999999;
-      font-size: 27rpx;
-      color: #666666;
-      padding:15rpx 28rpx;
-      display: inline-block;
-      margin-left: 18rpx;
-      &.active{
-        color:#fff;
-        background: #FF8719;
-        border-color: #FF8719;
-      }
-    }
-  }
-  .village-list{
-    .item{
-      background: #FFFFFF;
-      border-radius: 10rpx;
-      padding:18rpx 14rpx 11rpx;
-      display: flex;
-      margin-bottom: 27rpx;
-      image,.image-wrapper {
-        border-radius: 10rpx;
-        width: 154rpx;
-        height: 154rpx;
-        display: block;
-        margin-right: 25rpx;
-        overflow: hidden;
-      }
-      .info{
-        .name{
-          font-size: 30rpx;
-          color: #333333;
-          margin-bottom: 35rpx;
-          margin-top: 14rpx;
-        }
-        .btn{
-          border-radius: 10rpx;
-          border: 1px solid #25515E;
-          padding:16rpx 14rpx;
-          font-size: 28rpx;
-          color:#25515E;
-          margin-right: 34rpx;
-          display: inline-block;
-          &.active{
-            background: #FF8719;
-            color:#fff;
-            border-color: #FF8719;
-          }
-          text.iconfont{
-            display: inline-block;
-            margin-right: 15rpx;
-            font-size: 36rpx;
-          }
-        }
-      }
-    }
-  }
-  .retribution{
-    background: #FFFFFF;
-    border-radius: 10rpx;
-    display: flex;
-    justify-content: space-around;
-    .item{
-      text-align: center;
-      font-size: 24rpx;
-      color: #312520;
-      padding: 35rpx 0 33rpx;
-      text.iconfont{
-        font-size: 56rpx;
-        color:#25515E;
-        display: block;
-      }
-      .num{
-        font-weight: 600;
-        font-size: 48rpx;
-        color: #FF8719;
-        margin-top: 12rpx;
-        line-height: 48rpx;
-        margin-bottom: 15rpx;
-      }
-      .level{
-        margin-top: 12rpx;
-        margin-bottom: 15rpx;
-        font-family: Rockwell;
-        font-size: 44rpx;
-        line-height: 48rpx;
-        color: #FF8719;
-      }
-    }
-  }
-  .people-list{
-    .item{
-      width: 100%;
-      display: flex;
-      align-items: center;
-      padding: 28rpx 21rpx 28rpx 10rpx;
-      text-align: left;
-      .rank{
-        position: relative;
-        top:inherit;
-        left:inherit;
-        margin-right: 25rpx;
-        width: 77rpx;
-      }
-      .info{
-        flex:1;
-      }
-      .level{
-        font-family: Rockwell;
-        font-size: 36rpx;
-        color: #FF8719;
-      }
-      image.avatar{
-        margin-bottom: 0;
-        margin-right: 51rpx;
-        background-color: #efefef;
-        border-radius: 50%;
-      }
-    }
-  }
-}
-</style>

+ 28 - 68
src/pages/user/index.vue

@@ -1,22 +1,27 @@
 <template>
   <view class="main" style="padding-bottom:50rpx;padding-top:44px;">
-    <view v-if="userInfo" class="user-info" @click="navTo('/pages/user/update/profile')">
-      <image :src="userInfo.avatar" mode="aspectFill" class="avatar"></image>
-      <view class="info">
-        <text class="nickname">{{ userInfo.nickname }}</text>
+    <FlexRow align="center">
+      <Image 
+        :src="userInfo?.avatar || UserHead"
+        mode="aspectFill" 
+        class="avatar" 
+        width="100rpx"
+        height="100rpx"
+        round
+      />
+      <Width :width="20" />
+      <FlexCol v-if="userInfo" touchable @click="navTo('/pages/user/update/profile')" :flex="1">
+        <H4>{{ userInfo.nickname }}</H4>
         <text class="extra"><text class="label">守护编号</text><text>{{ userInfo.id }}</text><text class="label point-label">积分</text><text>{{ userInfo.totalCheckins }}</text></text>
-      </view>
-      <text class="iconfont icon-arrow-right"></text>
-    </view>
-    <view v-else class="user-info" @click="navTo('/pages/user/login')">
-      <image :src="UserHead" mode="aspectFill" class="avatar"></image>
-      <view class="info">
-        <text class="nickname">点击登录</text>
-        <text class="extra"> 登录后您将获得更多权益</text>
-      </view>
-      <text class="iconfont icon-arrow-right"></text>
-    </view>
-    <CellGroup inset v-if="userInfo">
+      </FlexCol>
+      <FlexCol v-else touchable @click="navTo('/pages/user/login')" :flex="1">
+        <H4 class="nickname">请登录</H4>
+      </FlexCol>
+      <Width :width="20" />
+      <Icon icon="arrow-right" />
+    </FlexRow>
+    <Height :height="50" />
+    <CellGroup v-if="userInfo">
       <Cell icon="https://mn.wenlvti.net/uploads/20250313/cbc47d0b9cad7891e6154359952858c6.png" title="退出登录" showArrow touchable @click="doLogout" />
     </CellGroup>
   </view>
@@ -30,6 +35,13 @@ import { computed } from 'vue';
 import UserHead from '@/static/images/home/UserHead.png';
 import CellGroup from '@/components/basic/CellGroup.vue';
 import Cell from '@/components/basic/Cell.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
+import Image from '@/components/basic/Image.vue';
+import Icon from '@/components/basic/Icon.vue';
+import H4 from '@/components/typography/H4.vue';
+import Width from '@/components/layout/space/Width.vue';
+import Height from '@/components/layout/space/Height.vue';
 
 const authStore = useAuthStore();
 const userInfo = computed(() => authStore.userInfo);
@@ -43,55 +55,3 @@ function doLogout() {
   });
 }
 </script>
-
-<style lang="scss" scoped>
-.user-info{
-  display: flex;
-  align-items: center;
-  padding: 24rpx 8rpx 60rpx 8rpx;
-  image.avatar{
-    width: 127rpx;
-    height: 127rpx;
-    border-radius: 50%;
-    margin-right: 24rpx;
-  }
-  .info{
-    color:#111111;
-    flex:1;
-    .nickname{
-      font-weight: bold;
-      display: block;
-      font-size: 36rpx;
-      color: #333333;
-      margin-bottom: 20rpx;
-   }
-    .extra{
-      font-size: 24rpx;
-      text{
-        color: #333333;
-        font-weight: 600;
-      }
-      text.label{
-        display: inline-block;
-        margin-right: 10rpx;
-        color:#666666;
-        &.point-label{
-          margin-left: 24rpx;
-        }
-      }
-    }
-  }
-}
-.btn{
-  width: 148rpx;
-  height: 54rpx;
-  background: linear-gradient(0deg, #299365, rgba(41, 147, 101, 0.8));
-  border-radius: 27rpx;
-  font-weight: 400;
-  font-size: 24rpx;
-  color: #FFFFFF;
-  line-height: 54rpx;
-  text-align: center;
-  margin-right: 27rpx;
-}
-</style>

+ 8 - 4
src/pages/user/login.vue

@@ -11,6 +11,7 @@
           <Field
             label="用户名"
             name="mobile"
+            type="text"
             placeholder="请输入用户名"
           />
           <Field
@@ -114,7 +115,7 @@ function loginWechat() {
       //return;
       authStore.loginWechart(res[0].code, res[1]).then(() => {
         setTimeout(() => {
-          back();
+          navTo('/pages/index');
           toast({
             type: 'success',  
             content: '登录成功',
@@ -139,7 +140,7 @@ function loginMobile() {
       type: 'success',  
       content: '登录成功',
     });
-    setTimeout(back, 200);
+    setTimeout(() => navTo('/pages/index'), 200);
   }).catch((e) => { 
     closeToast()
     showError(e); 
@@ -147,9 +148,12 @@ function loginMobile() {
 }
 
 onMounted(() => {
-  uni.showLoading({ title: '请稍后...' });
+  toast({
+    type: 'loading',  
+    content: '请稍后...',
+  });
   setTimeout(() => {
-    uni.hideLoading();
+    closeToast();
     if (authStore.isLogged) 
       navTo('/pages/index');
   }, 800);

+ 54 - 151
src/pages/user/update/password.vue

@@ -1,101 +1,76 @@
 <template>
-  <view class="password-page">
-    <view class="content">
-      <uni-forms ref="formRef" :modelValue="formData" :rules="rules">
-        <uni-forms-item name="oldpassword" label="当前密码" required>
-          <uni-easyinput 
-            type="password" 
-            v-model="formData.oldpassword" 
-            placeholder="请输入当前密码"
-            maxlength="20"
-          />
-        </uni-forms-item>
-        
-        <uni-forms-item name="newpassword" label="新密码" required>
-          <uni-easyinput 
-            type="password" 
-            v-model="formData.newpassword" 
-            placeholder="请输入新密码"
-            maxlength="20"
-          />
-        </uni-forms-item>
-        
-        <uni-forms-item name="confirmPassword" label="确认新密码" required>
-          <uni-easyinput 
-            type="password" 
-            v-model="formData.confirmPassword" 
-            placeholder="请再次输入新密码"
-            maxlength="20"
-          />
-        </uni-forms-item>
-        
-        <view class="tips">
-          密码建议:8-20位,包含字母和数字,不要使用过于简单的密码
-        </view>
-        
-        <u-button type="primary" :loading="loading"  @click="submitForm" >
-          保存修改
-        </u-button>
-      </uni-forms>
-    </view>
-  </view>
+  <FlexCol :padding="30">
+    <Form 
+      ref="formRef" 
+      :model="formData" 
+      :rules="rules"
+      :labelFlex="2"
+      :inputFlex="4"
+    >
+      <Field name="oldpassword" type="password" label="当前密码" placeholder="请输入当前密码" :maxlength="20" required />
+      <Field name="newpassword" type="password" label="新密码" placeholder="请输入新密码" :maxlength="20" required />
+      <Field name="confirmPassword" type="password" label="确认新密码" placeholder="请再次输入新密码" :maxlength="20" required />
+      
+      <text class="tips">
+        密码建议:8-20位,包含字母和数字,不要使用过于简单的密码
+      </text>
+    </Form>
+    <Height :height="40" />
+    <Button type="primary" :loading="loading" @click="submitForm" >
+      保存修改
+    </Button>
+  </FlexCol>
 </template>
 
 <script setup lang="ts">
 import { ref, reactive } from 'vue';
 import userApi from '@/api/auth/UserApi';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import Form from '@/components/form/Form.vue';
+import Field from '@/components/form/Field.vue';
+import Height from '@/components/layout/space/Height.vue';
+import Button from '@/components/basic/Button.vue';
+import type { Rules } from 'async-validator';
 
 const formRef = ref<any>(null);
 const loading = ref(false);
 
-// 表单数据
 const formData = reactive({
   oldpassword: '',
   newpassword: '',
   confirmPassword: ''
 });
-
-// 表单验证规则
-const rules = {
-  oldpassword: {
-    rules: [
-      { required: true, errorMessage: '请输入当前密码' },
-      { minLength: 6, errorMessage: '密码长度至少6位' }
-    ] 
-  },
-  newpassword: {
-    rules: [
-      { required: true, errorMessage: '请输入新密码' },
-      { minLength: 6, errorMessage: '新密码长度至少6位' },
-      {
-        validator: (rule: any, value: string) => {
-          // 密码强度校验:至少包含字母和数字
-          const hasLetter = /[a-zA-Z]/.test(value);
-          const hasNumber = /[0-9]/.test(value);
-          return hasLetter && hasNumber;
-        },
-        errorMessage: '新密码必须包含字母和数字'
-      }
-    ] 
-  },
-  confirmPassword: {
-    rules: [
-      { required: true, errorMessage: '请确认新密码' },
-      {
-        validator: (rule: any, value: string) => {
-          return value === formData.newpassword;
-        },
-        errorMessage: '两次输入的密码不一致'
-      }
-    ] 
-  }
+const rules : Rules = {
+  oldpassword: [
+    { required: true, message: '请输入当前密码' },
+    { min: 6, message: '密码长度至少6位' }
+  ] ,
+  newpassword: [
+    { required: true, message: '请输入新密码' },
+    { min: 6, message: '新密码长度至少6位' },
+    {
+      validator: (rule: any, value: string) => {
+        // 密码强度校验:至少包含字母和数字
+        const hasLetter = /[a-zA-Z]/.test(value);
+        const hasNumber = /[0-9]/.test(value);
+        return hasLetter && hasNumber;
+      },
+      message: '新密码必须包含字母和数字'
+    }
+  ] ,
+  confirmPassword: [
+    { required: true, message: '请确认新密码' },
+    {
+      validator: (rule: any, value: string) => {
+        return value === formData.newpassword;
+      },
+      message: '两次输入的密码不一致'
+    }
+  ] 
 };
 
 // 提交表单
 const submitForm = async () => {
-  console.log('submitForm:', formData);  
-
-  // 表单验证
   try {
     await formRef.value?.validate();
   } catch (error) {
@@ -106,26 +81,20 @@ const submitForm = async () => {
   loading.value = true;
   
   try {
-    // 调用修改密码API
     await userApi.updatePassword({
       oldpassword: formData.oldpassword,
       newpassword: formData.newpassword
     });
-    
-    // 显示成功提示
     uni.showToast({
       title: '密码修改成功',
       icon: 'success',
       duration: 2000
     });
-    
-    // 修改成功后返回上一页
     setTimeout(() => {
       uni.navigateBack();
     }, 2000);
     
   } catch (error: any) {
-    // 显示错误提示
     uni.showToast({
       title: error?.message || '密码修改失败,请稍后重试',
       icon: 'none',
@@ -138,76 +107,10 @@ const submitForm = async () => {
 </script>
 
 <style scoped>
-.password-page {
-  min-height: 100vh;
-  background-color: #f5f5f5;
-  display: flex;
-  flex-direction: column;
-}
-
-.header {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  padding: 0 16px;
-  height: 44px;
-  background-color: #ffffff;
-  position: sticky;
-  top: 0;
-  z-index: 10;
-  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
-}
-
-.title {
-  font-size: 16px;
-  font-weight: 600;
-  color: #333333;
-}
-
-.placeholder {
-  width: 24px;
-}
-
-.content {
-  flex: 1;
-  padding: 16px;
-}
-
-uni-forms {
-  background-color: #ffffff;
-  border-radius: 8px;
-  padding: 16px;
-  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
-}
-
-uni-forms-item {
-  margin-bottom: 20px;
-}
-
-uni-forms-item:last-child {
-  margin-bottom: 0;
-}
-
-uni-easyinput {
-  border-bottom: 1px solid #f0f0f0;
-  padding: 8px 0;
-}
-
-uni-easyinput:focus {
-  border-bottom-color: #007aff;
-}
-
 .tips {
   font-size: 12px;
   color: #999999;
   margin: 16px 0;
   line-height: 1.5;
 }
-
-.submit-btn {
-  margin-top: 24px;
-  border-radius: 8px;
-  height: 44px;
-  font-size: 16px;
-}
 </style>

+ 48 - 132
src/pages/user/update/profile.vue

@@ -1,86 +1,72 @@
 <template>
-  <view class="profile-page">
-    <uni-forms ref="formRef" :model="formModel" :rules="rules" validate-trigger="submit">
+  <FlexCol :padding="30">
+    <Form 
+      ref="formRef"
+      :model="formModel"
+      :rules="rules" 
+      validateTrigger="submit"
+      :labelWidth="140"
+    >
       <!-- 头像 -->
       <view class="avatar-section">
         <view class="avatar-container" @click="handleAvatarClick">
           <image 
-            :src="formModel.avatar || '/static/images/default-avatar.png'" 
+            :src="formModel.avatar || DefaultAvatar" 
             class="avatar-image" 
             mode="aspectFill"
           ></image>
-          <view class="avatar-edit-mask">
-            <uni-icons type="camera" size="24" color="#ffffff"></uni-icons>
-          </view>
         </view>
       </view>
 
-      <!-- 昵称 -->
-      <uni-forms-item name="nickname" label="昵称" required>
-        <uni-easyinput 
-          v-model="formModel.nickname" 
-          placeholder="请输入昵称"
-          maxlength="20"
-        />
-      </uni-forms-item>
+      <Field name="nickname" label="昵称" placeholder="请输入昵称" required />
+      <Field name="bio" multiline label="个人简介" placeholder="输入个人简介" :inputStyle="{width: '240px'}" />
+    </Form>
 
-      <!-- 个人简介 -->
-      <uni-forms-item name="bio" label="个人简介">
-        <uni-easyinput 
-          v-model="formModel.bio" 
-          type="textarea"
-          placeholder="介绍一下自己吧"
-          maxlength="100"
-          :height="100"
-          show-word-limit
-        />
-      </uni-forms-item>
+    <Height :height="40" />
 
-      <!-- 提交按钮 -->    
-      <u-button type="primary" :loading="loading" @click="submitForm" >
-        保存修改
-      </u-button>
-      <view class="mt-3" /> 
-      <u-button type="primary" :plain="true" @click="navTo('/pages/user/update/password')">
-        修改密码
-      </u-button>
-    </uni-forms>
-  </view>
+    <Button type="primary" :loading="loading" @click="submitForm" >
+      保存修改
+    </Button>
+    <Height :height="20" />
+    <Button type="primary" :plain="true" @click="navTo('/pages/user/update/password')">
+      修改密码
+    </Button>
+  </FlexCol>
 </template>
 
 <script setup lang="ts">
 import { ref, onMounted } from 'vue';
-import userApi from '@/api/auth/UserApi';
-import CommonContent from '@/api/CommonContent';
 import { useAuthStore } from '@/store/auth';
 import { navTo } from '@imengyu/imengyu-utils/dist/uniapp/PageAction';
+import UserApi from '@/api/auth/UserApi';
+import DefaultAvatar from '/static/images/home/UserHead.png';
+import CommonContent from '@/api/CommonContent';
+import Form from '@/components/form/Form.vue';
+import Field from '@/components/form/Field.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import Button from '@/components/basic/Button.vue';
+import Height from '@/components/layout/space/Height.vue';
+import type { Rules } from 'async-validator';
 
 const authStore = useAuthStore();
 const formRef = ref<any>(null);
 const loading = ref(false);
 const uploading = ref(false);
 
-// 表单数据
 const formModel = ref({
   avatar: '',
   nickname: '',
   bio: '',
 });
-
-// 表单验证规则
-const rules = {
-  nickname: {
-    rules: [
-      { required: true, errorMessage: '请输入昵称' },
-      { minLength: 2, errorMessage: '昵称长度至少2个字符' },
-      { maxLength: 20, errorMessage: '昵称长度最多20个字符' }
-    ]
-  },
-  bio: {
-    rules: [
-      { maxLength: 100, errorMessage: '个人简介最多100个字符' }
-    ]
-  }
+const rules : Rules = {
+  nickname: [
+    { required: true, message: '请输入昵称' },
+    { min: 2, message: '昵称长度至少2个字符' },
+    { max: 20, message: '昵称长度最多20个字符' }
+  ],
+  bio: [
+    { max: 100, message: '个人简介最多100个字符' }
+  ],
 };
 
 // 处理头像点击事件
@@ -114,22 +100,17 @@ const handleAvatarClick = async () => {
     uploading.value = false;
   }
 };
-
 // 更新头像到服务器
 const updateAvatar = async (avatarUrl: string) => {
   try {
-    // 调用修改头像API
-    await userApi.updateSystemUserInfo({
+    await UserApi.updateSystemUserInfo({
       avatar: avatarUrl
     });
-    
-    // 更新表单数据和store
     formModel.value.avatar = avatarUrl;
     if (authStore.userInfo) {
       authStore.userInfo.avatar = avatarUrl;
+      authStore.saveLoginState();
     }
-    
-    // 显示成功提示
     uni.showToast({
       title: '头像更新成功',
       icon: 'success',
@@ -141,8 +122,8 @@ const updateAvatar = async (avatarUrl: string) => {
   }
 };
 
-// 页面加载时获取用户信息
 onMounted(() => {
+  console.log(authStore.userInfo);
   if (authStore.userInfo) {
     formModel.value.avatar = authStore.userInfo.avatar || '';
     formModel.value.nickname = authStore.userInfo.nickname || '';
@@ -152,40 +133,34 @@ onMounted(() => {
 
 // 提交表单
 const submitForm = async () => {
-  // 表单验证
-  const valid = await formRef.value?.validate();
-  if (!valid) return;
+  try {
+    await formRef.value?.validate();
+  } catch {
+    return;
+  }
   
   loading.value = true;
   
   try {
-    // 调用修改个人信息API
-    await userApi.updateSystemUserInfo({
+    await UserApi.updateSystemUserInfo({
       nickname: formModel.value.nickname,
       bio: formModel.value.bio
     });
-    
-    // 更新store中的用户信息
     if (authStore.userInfo) {
       authStore.userInfo.nickname = formModel.value.nickname;
       authStore.userInfo.avatar = formModel.value.avatar;
       authStore.userInfo.intro = formModel.value.bio;
     }
-    
-    // 显示成功提示
     uni.showToast({
       title: '个人信息更新成功',
       icon: 'success',
       duration: 2000
     });
-    
-    // 成功后返回上一页
     setTimeout(() => {
       uni.navigateBack();
     }, 2000);
     
   } catch (error: any) {
-    // 显示错误提示
     uni.showToast({
       title: error?.message || '更新失败,请稍后重试',
       icon: 'none',
@@ -198,12 +173,6 @@ const submitForm = async () => {
 </script>
 
 <style scoped>
-.profile-page {
-  min-height: 100vh;
-  background-color: #f5f5f5;
-  padding: 16px;
-}
-
 .avatar-section {
   text-align: center;
 }
@@ -229,26 +198,6 @@ const submitForm = async () => {
   border: 2px solid #e0e0e0;
   background-color: #f5f5f5;
 }
-
-.avatar-edit-mask {
-  position: absolute;
-  top: 0;
-  left: 0;
-  width: 100%;
-  height: 100%;
-  border-radius: 50%;
-  background-color: rgba(0, 0, 0, 0.3);
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  opacity: 0;
-  transition: opacity 0.3s ease;
-}
-
-.avatar-container:hover .avatar-edit-mask {
-  opacity: 1;
-}
-
 .avatar-hint {
   font-size: 12px;
   color: #007aff;
@@ -268,46 +217,13 @@ const submitForm = async () => {
   align-items: center;
   z-index: 9999;
 }
-
 .uploading-content {
   background-color: #ffffff;
   padding: 20px;
   border-radius: 8px;
   text-align: center;
 }
-
 .uploading-content uni-loading {
   margin-bottom: 10px;
 }
-
-uni-forms {
-  background-color: #ffffff;
-  border-radius: 8px;
-  padding: 16px;
-  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
-}
-
-uni-forms-item {
-  margin-bottom: 20px;
-}
-
-uni-forms-item:last-child {
-  margin-bottom: 0;
-}
-
-uni-easyinput {
-  border-bottom: 1px solid #f0f0f0;
-  padding: 8px 0;
-}
-
-uni-easyinput:focus {
-  border-bottom-color: #007aff;
-}
-
-.submit-btn {
-  margin-top: 32px;
-  border-radius: 8px;
-  height: 44px;
-  font-size: 16px;
-}
 </style>

Diferenças do arquivo suprimidas por serem muito extensas
+ 309 - 36
src/static/css/font.css


+ 14 - 16
src/store/auth.ts

@@ -20,7 +20,6 @@ export const useAuthStore = defineStore('auth', {
         const authInfo = JSON.parse(res.data);
         this.token = authInfo.token;
         this.userId = authInfo.userId;
-        this.expireAt = authInfo.expireAt;
         this.userInfo = authInfo.userInfo;
         this.loginType = authInfo.loginType;
         setToken(this.token);
@@ -62,15 +61,21 @@ export const useAuthStore = defineStore('auth', {
       this.loginResultHandle(loginResult, loginType);
     },
     async loginResultHandle(loginResult: LoginResult, loginType: number) {
-      this.token = loginResult.auth.token;
-      this.userId = loginResult.mainBodyUserInfo.id;
-      this.userInfo = loginResult.mainBodyUserInfo;
-      this.expireAt = loginResult.auth.expiresIn + Date.now();
+      this.token = loginResult.token;
+      this.userId = loginResult.userInfo.id;
+      this.userInfo = loginResult.userInfo;
       this.loginType = loginType;
-
-      console.log('loginResultHandle');
-      
       setToken(this.token);
+      this.saveLoginState();
+    },
+    async logout() {
+      this.token = '';
+      this.userId = 0;
+      this.userInfo = null;
+      setToken('');
+      uni.removeStorage({ key: STORAGE_KEY });
+    },
+    saveLoginState() {
       uni.setStorage({ 
         key: STORAGE_KEY, 
         data: JSON.stringify({ 
@@ -78,17 +83,10 @@ export const useAuthStore = defineStore('auth', {
           userId: this.userId ,
           expireAt: this.expireAt,
           userInfo: this.userInfo,
-          loginType,
+          loginType: this.loginType,
         }) 
       });
     },
-    async logout() {
-      this.token = '';
-      this.userId = 0;
-      this.userInfo = null;
-      setToken('');
-      uni.removeStorage({ key: STORAGE_KEY });
-    },
     getInternalAuth() {
       return { 
         token: this.token,