Quellcode durchsuchen

微信登录,微信绑定,绑定志愿者功能

快乐的梦鱼 vor 1 Monat
Ursprung
Commit
b179ac3008

+ 20 - 13
src/App.vue

@@ -3,35 +3,42 @@
 </style>
 
 <script setup lang="ts">
-import AppConfig from '@/common/config/AppCofig'
+import AppConfig, { isTestEnv } from '@/common/config/AppCofig'
 import { onLaunch } from '@dcloudio/uni-app'
 import { useAuthStore } from './store/auth'
-import { useCollectStore } from './store/collect';
 import { configTheme } from './components/theme/ThemeDefine';
 import { getCurrentPageUrl, navTo } from './components/utils/PageAction';
+import { RequestApiConfig } from '@imengyu/imengyu-utils';
+import ApiCofig from './common/config/ApiCofig';
+import { useAppInit } from './common/composeabe/AppInit';
+import MemoryTimeOut from './common/composeabe/MemoryTimeOut';
 
 const authStore = useAuthStore();
-const collectStore = useCollectStore();
+const { init } = useAppInit();
+const redirectThrottle = new MemoryTimeOut('RedirectThrottle', 50000);
 
 onLaunch(async () => {
   console.log('App Launch');
   //加载登录信息。如果未登录,跳转登录页
-  if (!await authStore.loadLoginState()) {
-
-    const lastRedirectTime = uni.getStorageSync('lastRedirectTime') || 0;
-    if (Date.now() - lastRedirectTime < 50000)
-      return;
-    uni.setStorageSync('lastRedirectTime', Date.now());
-
+  if (!await authStore.loadLoginState() && redirectThrottle.isTimeout()) {
+    redirectThrottle.recordTime();
     setTimeout(() => {   
       const pageUrl = getCurrentPageUrl() || '';
       const noLoginPages = AppConfig.noLoginPages;
       if (noLoginPages.indexOf('/' + pageUrl) == -1 && noLoginPages.indexOf(pageUrl) == -1)
         navTo('/pages/user/login');
-    }, 1500);
+    }, 500);
   }
-  //加载采集板块信息
-  await collectStore.loadCollectableModules();
+    
+  await init();
+});
+
+//设置请求基础地址
+RequestApiConfig.setConfig({
+  ...RequestApiConfig.getConfig(),
+  BaseUrl: ApiCofig.serverProd,
+  EnableApiRequestLog: isTestEnv,
+  EnableApiDataLog: false,
 })
 
 //修改默认主题颜色

+ 2 - 0
src/api/CommonContent.ts

@@ -437,6 +437,8 @@ export class CommonContentApi extends AppServerRequestModule<DataModel> {
         data: data,
         header: {},
       }
+      console.log(url);
+      
       if (this.config.requestInceptor) {
         const { newReq, newUrl } = this.config.requestInceptor(url, req);
         url = newUrl;

+ 13 - 3
src/api/auth/UserApi.ts

@@ -1,13 +1,15 @@
 import { DataModel } from '@imengyu/js-request-transform';
 import { AppServerRequestModule } from '../RequestModules';
 import AppCofig from '@/common/config/AppCofig';
+import { VolunteerInfo } from '../inhert/VillageApi';
 
 export class LoginResult extends DataModel<LoginResult> {
   constructor() {
     super(LoginResult, "登录结果");
     this._convertTable = {
       token: { clientSide: 'string', clientSideRequired: true },
-      userInfo: { clientSide: 'object', clientSideChildDataModel: UserInfo }
+      userInfo: { clientSide: 'object', clientSideChildDataModel: UserInfo },
+      villageVolunteer: { clientSide: 'object', clientSideChildDataModel: VolunteerInfo },
     };
     this._nameMapperServer = {
       'userinfo': 'userInfo',
@@ -45,11 +47,16 @@ export class LoginResult extends DataModel<LoginResult> {
   inheritorId = null as number|null;
   loginType = 0;
   userInfo = new UserInfo();
+  villageVolunteer : VolunteerInfo|null = null;
 }
 export class UserInfo extends DataModel<UserInfo> {
   constructor() {
     super(UserInfo, "用户信息");
     this.setNameMapperCase('Camel', 'Snake');
+    this._nameMapperServer = {
+      'userid': 'userId',
+      'openid': 'openId',
+    }
     this._convertTable = {
       id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
     }
@@ -62,6 +69,7 @@ export class UserInfo extends DataModel<UserInfo> {
   avatar = '';
   username = '';
   regionId = 0;
+  openId = '';
   isReviewer = false;
 }
 
@@ -71,6 +79,9 @@ export class UserApi extends AppServerRequestModule<DataModel> {
     super();
   }
 
+  static LOGIN_TYPE_ADMIN = 1;
+  static LOGIN_TYPE_USER = 0;
+
   async loginThird(data?: {
     code: string,
     platform: 'wechat',
@@ -79,9 +90,8 @@ export class UserApi extends AppServerRequestModule<DataModel> {
     raw_data: string,
     signature: string,
   }) {
-    return (await this.post('/content/main_body_user/third', {
+    return (await this.post('/village/volunteer/third', {
       appid: AppCofig.appId,
-      main_body_id: 2, //AppCofig.mainBodyId,
       ...data,
     }, '登录', undefined, LoginResult)).data as LoginResult;
   }

+ 18 - 0
src/api/inhert/VillageApi.ts

@@ -1,6 +1,8 @@
 import { CONVERTER_ADD_DEFAULT, DataModel, transformArrayDataModel } from '@imengyu/js-request-transform';
 import { AppServerRequestModule } from '../RequestModules';
 import { findAProp, transformSomeToArray } from '../Utils';
+import { LoginResult } from '../auth/UserApi';
+import AppCofig from '@/common/config/AppCofig';
 
 export class VillageListItem extends DataModel<VillageListItem> {
   constructor() {
@@ -245,6 +247,22 @@ export class VillageApi extends AppServerRequestModule<DataModel> {
       village_id: villageId,
     }, '删除志愿者')) ;
   }
+  async shareAddVolunteer(data: VolunteerInfo) {
+    return (await (this.post('/village/volunteer/shareAdd', data.toServerSide(), '分享添加志愿者', undefined, LoginResult))).data as LoginResult;
+  }
+  async bindVolunteer(data: {
+    account: string,
+    password: string
+  }) {
+    return (await (this.post('/village/volunteer/bindVolunteer', data, '绑定志愿者', undefined, LoginResult))).data as LoginResult;
+  }
+  async bindWechat(data: { code: string }) {
+    return (this.post('/village/volunteer/bindWechat', {
+      code: data.code,
+      appid: AppCofig.appId
+    }, '绑定微信')) ;
+  }
+
   async getVolunteerInfoByIdAdmin(id: number) {
     return (await this.post('/village/volunteer/info', {
       id,

+ 3 - 4
src/common/components/SimplePageContentLoader.vue

@@ -15,10 +15,7 @@
     >
       <Button type="primary" text="刷新" @click="handleRetry" />
     </Empty>
-  </view>
-  <template v-else-if="loader?.loadStatus.value == 'finished' || loader?.loadStatus.value == 'nomore'">
-    <slot />
-  </template>
+  </view> 
   <view
     v-if="showEmpty || loader?.loadStatus.value == 'nomore'"
     class="loader-view"
@@ -43,6 +40,8 @@
     src="https://mn.wenlvti.net/app_static/empty.jpg"
     style="width:0px;height:0px"
   />
+
+  <slot />
 </template>
 
 <script setup lang="ts">

+ 3 - 4
src/common/components/SimplePageListLoader.vue

@@ -1,8 +1,8 @@
 <template>
+  <slot />
   <Loadmore
-    v-if="
-    loader.loadStatus.value == 'loading' 
-    || (loader.loadStatus.value == 'nomore' && !$slots.empty)" 
+    v-if="loader.loadStatus.value == 'loading' 
+      || (loader.loadStatus.value == 'nomore' && !$slots.empty)" 
     :status="loader.loadStatus.value" 
   />
   <slot v-else-if="loader.loadStatus.value == 'empty' && $slots.empty" name="empty" />
@@ -12,7 +12,6 @@
     :loadmoreText="loader.loadError.value" 
     @loadmore="handleRetry" 
   />
-  <slot v-else />
 </template>
 
 <script setup lang="ts">

+ 13 - 0
src/common/composeabe/AppInit.ts

@@ -0,0 +1,13 @@
+import { useCollectStore } from "@/store/collect";
+
+export function useAppInit() {
+  
+  const collectStore = useCollectStore();
+
+  return {
+    async init() {
+      //加载采集板块信息
+      await collectStore.loadCollectableModules();
+    },
+  }
+}

+ 76 - 0
src/common/composeabe/MemoryTimeOut.ts

@@ -0,0 +1,76 @@
+/**
+ * MemoryTimeOut 工具类
+ * 用于记录操作时间,判断是否超时,支持重置功能
+ * 适用于 uni-app 环境
+ */
+
+export class MemoryTimeOut {
+  /** 唯一键名 */
+  private key: string;
+  /** 超时时间(毫秒) */
+  private timeout: number;
+
+  /**
+   * 构造函数
+   * @param uniqueKey 唯一键名,用于区分不同的超时记录
+   * @param timeout 超时时间(毫秒),默认3600000毫秒(1小时)
+   */
+  constructor(uniqueKey: string, timeout: number = 3600000) {
+    this.key = `MemoryTimeOut_${uniqueKey}`;
+    this.timeout = timeout; // 默认1小时
+  }
+
+  /**
+   * 记录当前时间
+   */
+  public recordTime(): void {
+    const timestamp = Date.now();
+    uni.setStorageSync(this.key, timestamp);
+  }
+
+  /**
+   * 判断是否超时
+   * @returns boolean 是否超时
+   */
+  public isTimeout(): boolean {
+    const storedTime = uni.getStorageSync(this.key);
+    if (!storedTime) {
+      return true; // 没有记录时间,视为超时
+    }
+    return Date.now() - storedTime > this.timeout;
+  }
+
+  /**
+   * 重置超时记录(清除存储的时间)
+   */
+  public reset(): void {
+    uni.removeStorageSync(this.key);
+  }
+
+  /**
+   * 获取剩余时间(毫秒)
+   * @returns number 剩余时间
+   */
+  public getRemainingTime(): number {
+    const storedTime = uni.getStorageSync(this.key);
+    if (!storedTime) {
+      return 0;
+    }
+    const remaining = this.timeout - (Date.now() - storedTime);
+    return Math.max(0, remaining);
+  }
+
+  /**
+   * 获取已过时间(毫秒)
+   * @returns number 已过时间
+   */
+  public getElapsedTime(): number {
+    const storedTime = uni.getStorageSync(this.key);
+    if (!storedTime) {
+      return 0;
+    }
+    return Date.now() - storedTime;
+  }
+}
+
+export default MemoryTimeOut;

+ 1 - 1
src/common/composeabe/SimplePageListLoader.ts

@@ -43,7 +43,7 @@ export function useSimplePageListLoader<T, P = any>(
       const res = (await loader(page.value, pageSize, lastParams));
       list.value = list.value.concat(res.list as T[]);
       total.value = res.total;
-      loadStatus.value = res.list.length > 0 ? 'finished' : (total.value > 0 ? 'nomore' : 'empty');
+      loadStatus.value = res.list.length > 0 ? 'finished' : (list.value.length > 0 ? 'nomore' : 'empty');
       loadError.value = '';
       loading = false;
     } catch(e) {

+ 6 - 1
src/common/config/AppCofig.ts

@@ -14,6 +14,7 @@ export default {
     '/pages/user/reset-password',
     '/pages/dig/sharereg/share-reg-link',
     '/pages/dig/sharereg/share-reg-page',
+    '/pages/dig/sharereg/bind',
   ],
   defaultImage: 'https://mncdn.wenlvti.net/app_static/minnan/EmptyImage.png',
 }
@@ -24,4 +25,8 @@ export default {
 export function configAiMap() {
 }
 
-export const isDev = process.env.NODE_ENV === 'development';
+export const isDev = process.env.NODE_ENV === 'development';
+
+const accountInfo = uni.getAccountInfoSync();
+export const envVersion = accountInfo.miniProgram.envVersion;
+export const isTestEnv = envVersion === 'develop' || envVersion === 'trial';

+ 22 - 0
src/components/utils/PageAction.ts

@@ -27,6 +27,26 @@ function backReturnData(data: Record<string, unknown>) {
   uni.navigateBack({ delta: 1 });
 }
 /**
+ * 页面跳转: 关闭当前页面,跳转到应用内的某个页面
+ * @param url 页面路径
+ * @param data 要传递的数据
+ */
+function redirectTo(url: string, data: Record<string, unknown> = {}) {
+  var dataString = '';
+
+  for (const key in data) {
+    if (Object.prototype.hasOwnProperty.call(data, key))
+      dataString += `&${key}=${data[key]}`;
+  }
+
+  uni.redirectTo({ 
+    url: url + '?' + dataString,
+    fail: (err) => {
+      console.error('页面跳转失败:', err);
+    },
+  });
+}
+/**
  * 页面跳转: 跳转到指定页面
  * @param url 页面路径
  * @param data 要传递的数据
@@ -74,6 +94,7 @@ function backAndCallOnPageBack(name: string, data: Record<string, unknown>) {
 }
 
 export {
+  redirectTo,
   back,
   backReturnData,
   backAndCallOnPageBack, 
@@ -90,6 +111,7 @@ export function getCurrentPageUrl() {
 export default {
   install(app: App<Element>) : void {
     const p = {
+      redirectTo,
       back,
       backReturnData,
       backAndCallOnPageBack,

+ 16 - 2
src/pages.json

@@ -46,10 +46,10 @@
       }
     },
     {
-      "path": "pages/dig/admin",
+      "path": "pages/dig/admin/index",
       "style": {
         "navigationBarTitleText": "村社文化资源挖掘平台-管理员",
-        "enablePullDownRefresh": false
+        "enablePullDownRefresh": true
       }
     },
     {
@@ -166,6 +166,20 @@
       }
     },
     {
+      "path": "pages/dig/sharereg/bind",
+      "style": {
+        "navigationBarTitleText": "绑定志愿者",
+        "enablePullDownRefresh": false
+      }
+    },
+    {
+      "path": "pages/dig/sharereg/bind-wx",
+      "style": {
+        "navigationBarTitleText": "绑定微信",
+        "enablePullDownRefresh": false
+      }
+    },
+    {
       "path": "pages/home/about/about",
       "style": {
         "navigationBarTitleText": "关于我们",

+ 3 - 3
src/pages/dig/admin.vue

@@ -154,14 +154,14 @@ const listLoader = useSimplePageListLoader(8, async (page, pageSize, params) =>
 });
 
 function newData() {
-  navTo('./admin/volunteer', { 
+  navTo('./volunteer', { 
     id: -1,
     villageId: querys.value.id,  
     villageVolunteerId: querys.value.villageVolunteerId,  
   });
 }
 function goDetail(id: number, onlyPassword: boolean = false) {
-  navTo('./admin/volunteer', { 
+  navTo('./volunteer', { 
     id,
     villageId: querys.value.id,  
     villageVolunteerId: querys.value.villageVolunteerId,  
@@ -197,7 +197,7 @@ function search() {
 onShareAppMessage(() => ({
   title: '邀请你成为村社挖掘志愿者',
   desc: '分享给你的志愿者可以采编村社文化资源信息,加入志愿者队伍,为村社贡献力量。',
-  path: '/pages/dig/sharereg/share-reg-page',
+  path: '/pages/dig/sharereg/share-reg-page?villageId=' + querys.value.villageId,
   imageUrl: 'https://mn.wenlvti.net/app_static/xiangyuan/images/share-post.jpg',
 }))
 </script>

+ 3 - 2
src/pages/dig/forms/submits.vue

@@ -37,7 +37,7 @@
             <FlexCol>
               <H4 :size="36">{{ item.title }}</H4>
               <Height :height="10" />
-              <Text :size="23" :text="`栏目: ${item.catalogName}`" />
+              <Text :size="23" :text="`栏目: ${item.catalogName || item.collectModuleName || ''}`" />
               <Text :size="23" :text="`时间: ${DataDateUtils.formatDate(item.updatedAt, 'YYYY-MM-dd')}`" />
               <FlexRow align="center">
                 <Text :size="23" :text="`状态:`" />
@@ -88,6 +88,7 @@ import XBarSpace from '@/components/layout/space/XBarSpace.vue';
 import FlexRow from '@/components/layout/FlexRow.vue';
 import Tag from '@/components/display/Tag.vue';
 import Icon from '@/components/basic/Icon.vue';
+import { waitTimeOut } from '@imengyu/imengyu-utils';
 
 const searchText = ref('');
 const authStore = useAuthStore();
@@ -121,7 +122,7 @@ function goDetail(item: CommonInfoModel) {
     subType:  collectStore.getCollectModuleInternalNameById(item.collectModuleId),
     subKey: '',
     subId: -1,
-    subTitle: item.catalogName,
+    subTitle: item.catalogName || item.collectModuleName || '',
   });
 }
 function search() {

+ 21 - 13
src/pages/dig/index.vue

@@ -23,6 +23,7 @@
               backgroundColor="white"
               :radius="20"
               :padding="20"
+              align="center"
               justify="space-between"
             >
               <FlexRow align="center" :gap="20">
@@ -50,21 +51,28 @@
                 ]">
                   <Button v-if="authStore.isAdmin" icon="edit-filling" size="small">管理</Button>
                 </BubbleBox>
-                <BubbleBox :items="[
-                  {
-                    icon: 'edit-filling',
-                    text: '采编',
-                    onClick: () => goSubmitDigPage(item),
-                  },
-                  {
-                    icon: 'browse',
-                    text: '我的投稿',
-                    onClick: () => goMyDigPage(item),
-                  },
-                ]">
+                <BubbleBox 
+                  v-if="authStore.isAdmin" 
+                  :items="[
+                    {
+                      icon: 'edit-filling',
+                      text: '采编',
+                      onClick: () => goSubmitDigPage(item),
+                    },
+                    {
+                      icon: 'browse',
+                      text: '我的投稿',
+                      onClick: () => goMyDigPage(item),
+                    },
+                  ]"
+                >
                   <Button type="primary" size="small" icon="edit-filling">采编</Button>
                 </BubbleBox>
               </ButtonGroup>
+              <ButtonGroup v-if="!authStore.isAdmin">
+                <Button size="small" icon="browse" @click="goMyDigPage(item)">我的投稿</Button>
+                <Button type="primary" size="small" icon="edit-filling" @click="goSubmitDigPage(item)">采编</Button>
+              </ButtonGroup>
             </FlexRow>
           </FlexCol>
         </SimplePageContentLoader>
@@ -123,7 +131,7 @@ function goSubmitDigPage(item: VillageListItem) {
   })
 }
 function goManagePage(item: VillageListItem) {
-  navTo('./dig/admin', { 
+  navTo('./dig/admin/index', { 
     id: item.villageId,
     name: item.villageName,
     villageId: item.villageId,

+ 55 - 0
src/pages/dig/sharereg/bind-wx.vue

@@ -0,0 +1,55 @@
+<template>
+  <CommonRoot>
+    <FlexCol :gap="20" :padding="30">
+      <Image src="https://mn.wenlvti.net/app_static/xiangyuan/images/bind-banner.jpg" width="100%" height="400" />
+      <Button type="primary" @click="submit" :loading="loading">立即绑定</Button>
+      <Button @click="doBack">稍后再说</Button>
+    </FlexCol>
+  </CommonRoot>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+import { useAppInit } from '@/common/composeabe/AppInit';
+import { useLoadQuerys } from '@/common/composeabe/LoadQuerys';
+import { toast } from '@/components/dialog/CommonRoot';
+import { showError } from '@/common/composeabe/ErrorDisplay';
+import { back, redirectTo } from '@/components/utils/PageAction';
+import Button from '@/components/basic/Button.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import CommonRoot from '@/components/dialog/CommonRoot.vue';
+import VillageApi from '@/api/inhert/VillageApi';
+import Image from '@/components/basic/Image.vue';
+
+/**
+ *  账号绑定微信页面
+ */
+
+const { querys } = useLoadQuerys({
+  fromLogin: false
+});
+const { init } = useAppInit();
+const loading = ref(false);
+
+async function submit() {
+  try {
+    loading.value = true;
+    const res = await uni.login({ provider: 'weixin' });
+    await VillageApi.bindWechat({ code: res.code });
+    await init();
+    toast({ content: '绑定成功' });
+    setTimeout(doBack, 800);
+  } catch (e) {
+    showError(e);
+  } finally {
+    loading.value = false;
+  }
+}
+function doBack() {
+  if (querys.value.fromLogin) {
+    redirectTo('/pages/index');
+  } else {
+    back();
+  }
+}
+</script>

+ 104 - 0
src/pages/dig/sharereg/bind.vue

@@ -0,0 +1,104 @@
+<template>
+  <CommonRoot>
+    <FlexCol :gap="20" :padding="30">
+      <DynamicForm
+        ref="registerFormRef"
+        :model="registerFormModel"
+        :options="registerFormDefine"
+      />
+      <Height :height="20" />
+      <Button type="primary" @click="registerSubmit" :loading="registerFormLoading">立即绑定</Button>
+      <Button @click="cancel" :loading="registerFormLoading">稍后再说</Button>
+    </FlexCol>
+  </CommonRoot>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+import { useAuthStore } from '@/store/auth';
+import { useAppInit } from '@/common/composeabe/AppInit';
+import { toast } from '@/components/dialog/CommonRoot';
+import { showError } from '@/common/composeabe/ErrorDisplay';
+import { navTo } from '@/components/utils/PageAction';
+import type { IDynamicFormOptions, IDynamicFormRef } from '@/components/dynamic';
+import type { FieldProps } from '@/components/form/Field.vue';
+import type { FormProps } from '@/components/form/Form.vue';
+import { UserApi } from '@/api/auth/UserApi';
+import Button from '@/components/basic/Button.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import Height from '@/components/layout/space/Height.vue';
+import CommonRoot from '@/components/dialog/CommonRoot.vue';
+import DynamicForm from '@/components/dynamic/DynamicForm.vue';
+import VillageApi from '@/api/inhert/VillageApi';
+
+/**
+ * 微信绑定志愿者账号页面
+ */
+
+const authStore = useAuthStore();
+const { init } = useAppInit();
+
+const registerFormLoading = ref(false);
+const registerFormRef = ref<IDynamicFormRef>();
+const registerFormModel = ref({
+  username: '',
+  password: '',
+});
+const registerFormDefine : IDynamicFormOptions = {
+  formItems: [
+    { 
+      label: '登录账号', name: 'username', type: 'text',
+      additionalProps: { 
+        placeholder: '请输入用户名',
+      },
+      rules: [{ required: true, message: '请输入用户名' }],
+    },
+    {
+      label: '密码',
+      name: 'password',
+      type: 'text',
+      additionalProps: { 
+        placeholder: '请输入密码',
+        type: 'password',
+      } as FieldProps,
+      rules: [{ required: true, message: '请输入密码' }],
+    },
+  ],
+  formAdditionaProps: {
+    labelWidth: '160rpx',
+    labelAlign: 'right',
+    innerStyle: {
+      radius: '10rpx',
+    },
+  } as Omit<FormProps, 'model'>,
+}
+
+async function registerSubmit() {
+  if (!registerFormRef.value || !registerFormModel.value)
+    return;
+  try {
+    await registerFormRef.value.validate();
+  } catch (e) {
+    toast({ content: '有必填项未填写,请检查' });
+    return;
+  }
+  try {
+    registerFormLoading.value = true;
+    const loginRes = await VillageApi.bindVolunteer({
+      account: registerFormModel.value.username,
+      password: registerFormModel.value.password,
+    });
+    await authStore.loginResultHandle(loginRes, UserApi.LOGIN_TYPE_USER);
+    await init();
+    toast({ content: '注册成功' });
+    setTimeout(() => navTo('/pages/index'), 800);
+  } catch (e) {
+    showError(e);
+  } finally {
+    registerFormLoading.value = false;
+  }
+}
+function cancel() {
+  navTo('/pages/index');
+}
+</script>

+ 217 - 4
src/pages/dig/sharereg/share-reg-page.vue

@@ -1,8 +1,9 @@
 <template>
   <CommonRoot>
     <FlexCol :gap="20" :padding="30">
+      <!--完成-->
       <Result 
-        v-if="authStore.isLogged" 
+        v-if="step === 'already'" 
         status="success"
         title="您已经是志愿者"
         desc="赶快去采编村社文化资源信息吧"
@@ -10,6 +11,30 @@
         <Height :size="20" />
         <Button type="primary" @click="navTo('/pages/index')">返回首页</Button>
       </Result>
+      <!--注册-->
+      <FlexCol v-else-if="step === 'register'" center>
+        <FlexCol :padding="30">
+          <DynamicForm
+            ref="registerFormRef"
+            :model="registerFormModel"
+            :options="registerFormDefine"
+            :formGlobalParams="querys"
+          />
+          <Height :height="20" />
+          <Button type="primary" @click="registerSubmit" :loading="registerFormLoading">提交</Button>
+        </FlexCol>
+      </FlexCol>
+      <!--注册完成-->
+      <Result 
+        v-if="step === 'finished'" 
+        status="success"
+        title="注册志愿者成功"
+        desc="请等待管理员审核,在此期间,可以在社区中先逛逛,学习如何采编村社文化资源信息吧"
+      >
+        <Height :size="20" />
+        <Button type="primary" @click="navTo('/pages/index')">进入首页</Button>
+      </Result>
+      <!--登录-->
       <FlexCol v-else center :height="400">
         
         <Icon icon="smile-filling" color="primary" :size="156" />
@@ -21,6 +46,10 @@
         <Button type="primary" block text="微信登录" @click="loginWechat" />
         <Height :size="20" />
         <!-- #endif -->
+        <!-- #ifndef MP-WEIXIN -->
+        <Result status="warning" title="提示" desc="当前环境不支持微信登录" />
+        <!-- #endif -->
+
       </FlexCol>
     </FlexCol>
   </CommonRoot>
@@ -34,12 +63,51 @@ import Height from '@/components/layout/space/Height.vue';
 import Icon from '@/components/basic/Icon.vue';
 import Text from '@/components/basic/Text.vue';
 import CommonRoot from '@/components/dialog/CommonRoot.vue';
+import DynamicForm from '@/components/dynamic/DynamicForm.vue';
 import { navTo } from '@/components/utils/PageAction';
 import { useAuthStore } from '@/store/auth';
+import { useAliOssUploadCo } from '@/common/components/upload/AliOssUploadCo';
 import { closeToast, toast } from '@/components/dialog/CommonRoot';
 import { showError } from '@/common/composeabe/ErrorDisplay';
+import { useLoadQuerys } from '@/common/composeabe/LoadQuerys';
+import { onMounted, ref } from 'vue';
+import VillageApi, { type VolunteerInfo } from '@/api/inhert/VillageApi';
+import CommonContent from '@/api/CommonContent';
+import type { IDynamicFormItemCallbackAdditionalProps, IDynamicFormOptions, IDynamicFormRef } from '@/components/dynamic';
+import type { FieldProps } from '@/components/form/Field.vue';
+import type { RuleItem } from 'async-validator';
+import type { PickerIdFieldProps } from '@/components/dynamic/wrappers/PickerIdField';
+import type { RadioValueProps } from '@/components/dynamic/wrappers/RadioValue';
+import type { UploaderFieldProps } from '@/components/form/UploaderField.vue';
+import type { FormProps } from '@/components/form/Form.vue';
+import { useAppInit } from '@/common/composeabe/AppInit';
+import { UserApi } from '@/api/auth/UserApi';
+
+/**
+ * 分享注册页面
+ * 
+ * 用户从管理员分享链接进入,需要注册为志愿者
+ * 0. 如果已经登录,且有志愿者信息,直接跳转已经注册页, 否则进入注册流程
+ * 1. 登录微信, 登录成功后, 如果有志愿者信息, 直接跳转成功页, 否则进入注册流程
+ * 2. 注册流程中, 提交成功后, 跳转成功页
+ */
 
 const authStore = useAuthStore();
+const { init } = useAppInit();
+
+const { querys } = useLoadQuerys({ 
+  villageId: 0,  
+});
+const step = ref<''|'register'|'finished'|'already'>('');
+
+onMounted(() => {
+  if (authStore.isLogged) {
+    if (authStore.userInfo?.villageVolunteer)
+      step.value = 'already';
+    else 
+      step.value = 'register';
+  }
+});
 
 function loginWechat() {
   toast({
@@ -52,16 +120,161 @@ function loginWechat() {
     uni.getUserProfile({ desc: '用于完善会员资料' }),
   ])
     .then((res) => {
-      authStore.loginWechart(res[0].code, res[1]).then(() => {
+      authStore.loginWechart(res[0].code, res[1]).then((res) => {
         toast({
           type: 'success',  
           content: '登录成功',
         });
-        //collectStore.loadCollectableModules();
-        //setTimeout(() => redirectToIndex(), 200);
+
+        if (res.villageVolunteer) {
+          //有志愿者信息,表示是志愿者,直接跳转
+          step.value = 'already';
+          return;
+        }
+        
+        step.value = 'register';
       }).catch(showError);
     })
     .catch(showError)
     .finally(() => closeToast());
 }
+
+const registerFormLoading = ref(false);
+const registerFormRef = ref<IDynamicFormRef>();
+const registerFormModel = ref<VolunteerInfo>();
+const registerFormDefine : IDynamicFormOptions = {
+  formItems: [
+    {
+      name: 'groupBase',
+      type: 'flat-simple',
+      children: [
+        { 
+          label: '登录账号', name: 'username', type: 'text',
+          additionalProps: { 
+            placeholder: '请输入用户名',
+          },
+          rules: [{ required: true, message: '请输入用户名' }],
+        },
+        {
+          label: '密码',
+          name: 'password',
+          type: 'text',
+          additionalProps: { 
+            placeholder: '请输入密码',
+            type: 'password',
+          } as FieldProps,
+          rules: [{ required: true, message: '请输入密码' }],
+        },
+        {
+          label: '确认密码',
+          name: 'passwordRepeat',
+          type: 'text',
+          additionalProps: { 
+            placeholder: '请再输入一次密码',
+            type: 'password',
+          } as FieldProps,
+          rules: [
+            { required: true, message: '请再输入一次密码' },
+            {
+              async validator(rule, value) {
+                if (value != registerFormRef.value?.getValueByPath('password'))
+                  throw '两次输入密码不一致,请检查';
+              },
+            }
+          ] as RuleItem[],
+        },
+      ]
+    },
+    {
+      name: 'groupExtra',
+      type: 'flat-simple',
+      childrenColProps: {
+        span: 24,
+      },
+      children: [
+        {
+          label: '真实名称', name: 'name', type: 'text',
+          additionalProps: { placeholder: '请输入真实名称' },
+          rules: [{ required: true, message: '请输入真实名称' }],
+        },
+        {
+          label: '手机号', name: 'mobile', type: 'text',
+          additionalProps: { placeholder: '请输入手机号' },
+          rules: [{ required: true, message: '请输入手机号' }],
+        },
+        { 
+          label: '区域', name: 'regionId', type: 'select-id',
+          additionalProps: {
+            placeholder: '请选择区域',
+            loadData: async () => (await CommonContent.getCategoryList(1)).map(p => ({ text: p.title, value: p.id, raw: p }))
+          } as IDynamicFormItemCallbackAdditionalProps<PickerIdFieldProps>,
+          rules: [{ required: true, message: '请选择区域' }],
+        },
+        {
+          label: '性别', name: 'sex', type: 'radio-value',
+          additionalProps: {
+            options: [
+              { text: '男', value: 1 },
+              { text: '女', value: 2 }
+            ]
+          } as RadioValueProps,
+        },
+        { 
+          label: '头像', name: 'image', type: 'uploader',
+          additionalProps: {
+            single: true,
+            maxFileSize: 1024 * 1024 * 10,
+            upload: useAliOssUploadCo('xiangyuan/volunteer/images')
+          } as UploaderFieldProps,
+        },
+        { label: '地址', name: 'address', type: 'text', additionalProps: { placeholder: '请输入地址' } },
+        { 
+          label: '介绍', 
+          name: 'intro', 
+          type: 'textarea', 
+          additionalProps: { 
+            placeholder: '请输入介绍',
+            showWordLimit: true,
+            maxLength: 200,
+          } as FieldProps,
+        },
+        { 
+          label: '村落认领说明', name: 'claimReason', type: 'text', 
+          additionalProps: { placeholder: '请输入村落认领说明' } ,
+        },
+      ]
+    },
+  ],
+  formAdditionaProps: {
+    labelWidth: '160rpx',
+    labelAlign: 'right',
+    innerStyle: {
+      radius: '10rpx',
+    },
+  } as Omit<FormProps, 'model'>,
+}
+
+async function registerSubmit() {
+  if (!registerFormRef.value || !registerFormModel.value)
+    return;
+  try {
+    await registerFormRef.value.validate();
+  } catch (e) {
+    toast({ content: '有必填项未填写,请检查' });
+    return;
+  }
+  try {
+    registerFormLoading.value = true;
+    registerFormModel.value!.villageId = querys.value.villageId;
+    const loginRes = await VillageApi.shareAddVolunteer(registerFormModel.value!);
+    await authStore.loginResultHandle(loginRes, UserApi.LOGIN_TYPE_USER);
+    await init();
+    toast({ content: '注册成功' });
+    step.value = 'finished';
+  } catch (e) {
+    showError(e);
+  } finally {
+    registerFormLoading.value = false;
+  }
+}
 </script>

+ 3 - 1
src/pages/editor/editor.vue

@@ -101,12 +101,14 @@ function upinImage(tempFiles: any, editorCtx: any) {
   path = tempFiles[0].path;
   // #endif
 
+  console.log('==== upinImage :', path);
+  
   CommonContent.uploadFile(path, 'image', 'file').then((res) => {
     editorCtx.insertImage({
       src: res.fullurl,
       width: '80%',
       success: function () {}
     })
-  });
+  }).catch((e) => showError(e));
 }
 </script>

+ 5 - 3
src/pages/user/index.vue

@@ -25,8 +25,9 @@
     </Touchable>
     <Height :height="50" />
     <CellGroup round>
-      <Cell icon="/static/images/user/icon-edit.png" title="我的投稿" showArrow touchable @click="navTo('/pages/dig/forms/submits')" />
+      <Cell icon="/static/images/user/icon-edit.png" title="我的投稿" showArrow touchable @click="navTo('/pages/dig/forms/submits', { villageVolunteerId: userInfo?.id })" />
       <Cell icon="/static/images/user/icon-profile.png" title="编辑资料" showArrow touchable @click="goUserProfile" />
+      <Cell v-if="!isBindWx" icon="wechat" title="绑定微信" showArrow touchable @click="navTo('/pages/dig/sharereg/bind-wx')" />
       <Cell icon="/static/images/user/icon-function.png" title="关于我们" showArrow touchable @click="navTo('/pages/home/about/about')" />
       <button open-type="contact" class="remove-button-style">
         <Cell icon="/static/images/user/icon-service.png" title="联系客服" showArrow touchable />
@@ -45,10 +46,11 @@
 </template>
 
 <script setup lang="ts">
-import { useAuthStore } from '@/store/auth';
 import { computed } from 'vue';
 import { navTo } from '@/components/utils/PageAction';
 import { alert, confirm } from '@/components/dialog/CommonRoot';
+import { DateUtils } from '@imengyu/imengyu-utils';
+import { useAuthStore } from '@/store/auth';
 import UserHead from '@/static/images/user/avatar.png';
 import CellGroup from '@/components/basic/CellGroup.vue';
 import Cell from '@/components/basic/Cell.vue';
@@ -59,10 +61,10 @@ import Touchable from '@/components/feedback/Touchable.vue';
 import FlexCol from '@/components/layout/FlexCol.vue';
 import Text from '@/components/basic/Text.vue';
 import AppCofig from '@/common/config/AppCofig';
-import { DateUtils } from '@imengyu/imengyu-utils';
 
 const authStore = useAuthStore();
 const userInfo = computed(() => authStore.userInfo);
+const isBindWx = computed(() => Boolean(userInfo.value?.openId));
 const buildTime = `${__BUILD_TIMESTAMP__}`
 const buildInfo = `${__BUILD_GUID__}`
 

+ 115 - 47
src/pages/user/login.vue

@@ -66,17 +66,25 @@
         <Height :size="20" />
         <!-- #endif -->
         <Button type="default" block size="large" text="用户名密码登录" @click="type='mobile'" />
+
+        <FlexRow v-if="isTestEnv" position="absolute" :left="10" :bottom="10">
+          <CheckBox v-model="isTestCode" />
+        </FlexRow>
       </FlexCol>
     </FlexCol>
   </CommonRoot>
 </template>
 
 <script setup lang="ts">
-import baseLogo from '/static/logo.png';
+import { useTheme } from '@/components/theme/ThemeDefine';
 import { useAuthStore } from '@/store/auth';
-import { useCollectStore } from '@/store/collect';
+import { useAppInit } from '@/common/composeabe/AppInit';
+import { RequestApiError, waitTimeOut } from '@imengyu/imengyu-utils';
 import { onMounted, ref } from 'vue';
 import { showError } from '@/common/composeabe/ErrorDisplay';
+import { alert, closeToast, confirm, toast } from '@/components/dialog/CommonRoot';
+import { isTestEnv } from '@/common/config/AppCofig';
+import type { Rules } from 'async-validator';
 import FlexCol from '@/components/layout/FlexCol.vue';
 import Form from '@/components/form/Form.vue';
 import Field from '@/components/form/Field.vue';
@@ -84,21 +92,26 @@ import RadioGroup from '@/components/form/RadioGroup.vue';
 import Radio from '@/components/form/Radio.vue';
 import Button from '@/components/basic/Button.vue';
 import Height from '@/components/layout/space/Height.vue';
-import type { Rules } from 'async-validator';
-import { closeToast, toast } from '@/components/dialog/CommonRoot';
+import baseLogo from '/static/logo.png';
 import FlexRow from '@/components/layout/FlexRow.vue';
 import CommonRoot from '@/components/dialog/CommonRoot.vue';
 import StatusBarSpace from '@/components/layout/space/StatusBarSpace.vue';
 import Image from '@/components/basic/Image.vue';
 import Text from '@/components/basic/Text.vue';
-import { useTheme } from '@/components/theme/ThemeDefine';
-import { DynamicColor } from '@/components/theme/ThemeTools';
-import type { back } from '@/components/utils/PageAction';
+import CheckBox from '@/components/form/CheckBox.vue';
+import VillageApi from '@/api/inhert/VillageApi';
+import MemoryTimeOut from '@/common/composeabe/MemoryTimeOut';
+
+/**
+ * 登录页面
+ * 
+ * 登录页面,支持微信登录和用户名密码登录
+ */
 
 const type = ref('wechat');
 const authStore = useAuthStore();
-const collectStore = useCollectStore();
 const themeContext = useTheme();
+const { init } = useAppInit();
 
 const loginFormModel = ref({
   mobile: '',
@@ -118,8 +131,7 @@ const loginFormRules : Rules = {
     required: true,
     message: '请填写密码(6-16位)',
   },
-}
-
+};
 const fieldStyle = themeContext.useThemeStyle({
   backgroundColor: '#ececec',
   paddingVertical: '30rpx',
@@ -128,55 +140,111 @@ const fieldStyle = themeContext.useThemeStyle({
   marginBottom: '20rpx',
 });
 
-function loginWechat() {
+const isTestCode = ref(false);
+const tipBindWechat = new MemoryTimeOut('TipBindWechat', 1000 * 3600 * 12);
+
+async function loginWechat() {
 
   toast({
     type: 'loading',  
     content: '登录中...',
   })
 
-  Promise.all([
-    uni.login({ provider: 'weixin' }),
-    uni.getUserProfile({ desc: '用于完善会员资料' }),
-  ])
-    .then((res) => {
-      console.log(res);
-      //return;
-      authStore.loginWechart(res[0].code, res[1]).then(() => {
-        toast({
-          type: 'success',  
-          content: '登录成功',
-        });
-        collectStore.loadCollectableModules();
-        setTimeout(() => redirectToIndex(), 200);
-      }).catch(showError);
-    })
-    .catch(showError)
-    .finally(() => closeToast());
+  try {
+    const res = await Promise.all([
+      uni.login({ provider: 'weixin' }),
+      uni.getUserProfile({ desc: '用于完善会员资料' }),
+    ])
+
+    //测试code功能
+    if (isTestCode.value) {
+      uni.setClipboardData({
+        data: res[0].code,
+      });
+      alert({
+        title: '测试登录',
+        content: '已复制登录信息到剪贴板\n' + JSON.stringify(res),
+      });
+      return;
+    }
+
+    //登录微信
+    await authStore.loginWechart(res[0].code, res[1]);
+    toast({  type: 'success',content: '登录成功' });
+    await loginAfter();
+  } catch(e) {
+    showError(e);
+  } finally {
+    closeToast();
+  }
 }
-function loginMobile() {
+async function loginMobile() {
   toast({
     type: 'loading',  
     content: '登录中...',
   })
-  authStore.loginMobile(
-    loginFormModel.value.mobile, 
-    loginFormModel.value.password,
-    loginFormModel.value.loginType,
-  ).then(() => {
-    toast({
-      type: 'success',  
-      content: '登录成功',
-    });
-    setTimeout(() => {
-      //加载采集板块信息
-      collectStore.loadCollectableModules();
-      redirectToIndex()
-    }, 200);
-  }).catch((e) => { 
+
+  try {
+    await authStore.loginMobile(
+      loginFormModel.value.mobile, 
+      loginFormModel.value.password,
+      loginFormModel.value.loginType,
+    );
+    toast({ type: 'success',content: '登录成功' });
+    await loginAfter(true);
+
+  } catch (e) { 
     closeToast()
     showError(e); 
-  })
+  }
+}
+async function loginAfter(isMobileLogin = false) {
+  await waitTimeOut(200);
+  //检查是否有志愿者信息,跳转至不同的页面
+  //已认领志愿者,跳转至首页
+  //未认领志愿者,跳转至绑定账号页面
+  try {
+    await VillageApi.getVolunteerInfo();
+  } catch(e) {
+
+    //已登录但是没绑定志愿者信息,提示用户绑定
+    if ((e as RequestApiError).errorMessage.includes('请认领')) {
+      const goBind = await confirm({
+        title: '提示',
+        content: '欢迎进入平台,您的账号未绑定志愿者账号,目前不能提交信息,是否前往绑定?',
+        confirmText: '前往绑定',
+        cancelText: '先看看,稍后绑定',
+        width: 580,
+      });
+      if (goBind)
+        uni.redirectTo({ url: '/pages/dig/sharereg/bind' });
+      else
+        redirectToIndex();
+      return;
+    }
+
+    throw e;
+  }
+
+  //刷新用户信息
+  await init();
+
+  //如果用户未绑定微信,提示用户绑定微信
+  if (isMobileLogin && !authStore.userInfo?.openId && tipBindWechat.isTimeout()) {
+    tipBindWechat.recordTime();
+
+    if (await confirm({
+      title: '提示',
+      content: '绑定微信账号后登录更方便,是否前往绑定?',
+      confirmText: '前往绑定',
+      cancelText: '稍后绑定',
+      width: 580,
+    })) {
+      uni.redirectTo({ url: '/pages/dig/sharereg/bind-wx?fromLogin=true' });
+      return;
+    }
+  }
+  redirectToIndex();
 }
 function redirectToIndex() {
   uni.redirectTo({ url: '/pages/index' });
@@ -190,7 +258,7 @@ onMounted(() => {
   setTimeout(() => {
     closeToast();
     if (authStore.isLogged) 
-      redirectToIndex();
+      loginAfter();
   }, 800);
 })
 

+ 4 - 2
src/store/auth.ts

@@ -44,7 +44,8 @@ export const useAuthStore = defineStore('auth', {
         raw_data: JSON.parse(res.rawData),
         signature: res.signature,
       })
-      this.loginResultHandle(loginResult, 0);
+      await this.loginResultHandle(loginResult, 0);
+      return loginResult;
     },
     async loginMobile(account: string, password: string, loginType: number) {
       let loginResult;
@@ -60,7 +61,8 @@ export const useAuthStore = defineStore('auth', {
         })
       } else
         throw 'login type error';
-      this.loginResultHandle(loginResult, loginType);
+      await this.loginResultHandle(loginResult, loginType);
+      return loginResult;
     },
     async loginResultHandle(loginResult: LoginResult, loginType: number) {
       this.token = loginResult.token;