Ver Fonte

feat(user): 新增用户中心页面及相关功能组件

- 添加用户信息、兑换记录和设置页面
- 实现用户信息修改、密码修改和退出登录功能
- 新增兑换记录展示组件
- 优化上传组件逻辑
- 更新用户类型定义和服务接口
whj há 11 horas atrás
pai
commit
a73951a324

+ 4 - 15
.cool/types/user.ts

@@ -1,17 +1,6 @@
 export type UserInfo = {
-	unionid: string; // 用户唯一id
-	id: number; // 用户id
-	nickName: string; // 昵称
-	avatarUrl?: string; // 头像
-	phone: string; // 手机号
-	gender: number; // 性别
-	status: number; // 状态
-	description?: string; // 描述
-	loginType: number; // 登录类型
-	province?: string; // 省份
-	city?: string; // 城市
-	district?: string; // 区县
-	birthday?: string; // 生日
-	createTime: string; // 创建时间
-	updateTime: string; // 更新时间
+	userInfo: any
+	sysRoles: any[]
+	roles: string[]
+	permissions: string[]
 };

+ 8 - 98
.cool/upload/index.ts

@@ -55,14 +55,6 @@ export type LocalUploadResponse = {
 	data: string;
 };
 
-// 获取上传模式(本地/云端及云类型)
-async function getUploadMode(): Promise<UploadMode> {
-	const res = await request({
-		url: "/app/base/comm/uploadMode"
-	});
-
-	return parse<UploadMode>(res!)!;
-}
 
 /**
  * 路径上传
@@ -88,14 +80,6 @@ export async function uploadFile(
 ): Promise<string> {
 	const { user } = useStore();
 
-	// 获取上传模式和类型
-	const { mode, type } = await getUploadMode();
-
-	// 判断是否本地上传
-	const isLocal = mode == "local";
-
-	// 判断是否是云上传
-	const isCloud = mode == "cloud";
 
 	// 获取文件路径
 	const filePath = file.path;
@@ -117,11 +101,6 @@ export async function uploadFile(
 	// 生成唯一key: 原文件名_uuid.扩展名
 	let key = `${filename(fileName)}_${uuid()}.${ext}`;
 
-	// 云上传需要加上时间戳路径
-	if (isCloud) {
-		key = `app/${Date.now()}/${key}`;
-	}
-
 	// 支持多种上传方式
 	return new Promise((resolve, reject) => {
 		/**
@@ -136,26 +115,16 @@ export async function uploadFile(
 				name: "file",
 				header: {
 					// 本地上传带token
-					Authorization: isLocal ? user.token : null
+					Authorization: user.token
 				},
 				formData: {
 					...(data as UTSJSONObject),
 					key
 				},
 				success(res) {
-					if (isLocal) {
-						// 本地上传返回处理
-						const { code, data, message } = parseObject<LocalUploadResponse>(res.data)!;
-
-						if (code == 1000) {
-							resolve(data);
-						} else {
-							reject(message);
-						}
-					} else {
-						// 云上传直接拼接url
-						resolve(pathJoin(preview ?? url, key!));
-					}
+					// 本地上传返回处理
+					const { code, data, message } = parseObject<LocalUploadResponse>(res.data)!;
+					resolve(data);
 				},
 				fail(err) {
 					console.error(err);
@@ -187,68 +156,9 @@ export async function uploadFile(
 		}
 
 		// 本地上传
-		if (isLocal) {
-			next({
-				url: config.baseUrl + "/app/base/comm/upload",
-				data: {}
-			});
-		} else {
-			// 云上传
-			const data = {} as UTSJSONObject;
-
-			// AWS需要提前传key
-			if (type == "aws") {
-				data.key = key;
-			}
-
-			// 获取云上传参数
-			request({
-				url: "/app/base/comm/upload",
-				method: "POST",
-				data
-			})
-				.then((res) => {
-					const d = parse<CloudUploadResponse>(res!)!;
-
-					switch (type) {
-						// 腾讯云COS
-						case "cos":
-							next({
-								url: d.url!,
-								data: d.credentials!
-							});
-							break;
-						// 阿里云OSS
-						case "oss":
-							next({
-								url: d.host!,
-								data: {
-									OSSAccessKeyId: d.OSSAccessKeyId,
-									policy: d.policy,
-									signature: d.signature
-								}
-							});
-							break;
-						// 七牛云
-						case "qiniu":
-							next({
-								url: d.uploadUrl!,
-								preview: d.publicDomain,
-								data: {
-									token: d.token
-								}
-							});
-							break;
-						// 亚马逊AWS
-						case "aws":
-							next({
-								url: d.url!,
-								data: d.fields!
-							});
-							break;
-					}
-				})
-				.catch(reject);
-		}
+		next({
+			url: config.baseUrl + "/upms/files/upload",
+			data: {}
+		});
 	});
 }

+ 7 - 0
pages.json

@@ -40,6 +40,13 @@
 				"navigationStyle": "custom",
 				"disableScroll": true
 			}
+		},
+		{
+			"path": "pages/user/info",
+			"style": {
+				"navigationStyle": "custom",
+				"disableScroll": true
+			}
 		}
 	],
 	"globalStyle": {

+ 3 - 3
pages/index/home.uvue

@@ -55,9 +55,9 @@ function handlePage(val: any) {
 		// case 'exchange':
 		// 	url = '/pages/index/exchange'
 		// 	break;
-		// case 'user':
-		// 	url = '/pages/index/user'
-		// 	break;
+		case 'user':
+			params.path = '/pages/user/info'
+			break;
 		default:
 			break;
 	}

+ 60 - 0
pages/user/components/conversion.uvue

@@ -0,0 +1,60 @@
+<script setup lang='ts'>
+import { fetchSubjectFeeRecord } from '@/services/subject/record'
+import { onMounted, ref } from 'vue'
+import { user } from '@/.cool'
+const recordList = ref<any[]>([])
+onMounted(() => {
+  fetchSubjectFeeRecord({
+    userId: user.info.value?.userInfo.userId
+  }).then(res => {
+    recordList.value = res
+  })
+})
+</script>
+<template>
+  <view class=" bg-[#fff] shadow rounded-xl mt-[20px] text-[#00A9FF] ">
+    <scroll-view direction="vertical" :show-scrollbar="false" class="sidebar">
+      <view class="w-full grid grid-cols-2 gap-4" v-if="recordList.length > 0">
+        <view v-for="item in recordList" :key="item" class="box flex flex-row  items-center gap-2">
+          <view class="h-full">
+            <cl-image src="https://oss.xiaoxiongcode.com/static/个人中心/图层 13.png" mode="heightFix" height="100%"
+              width="auto"></cl-image>
+          </view>
+          <view class="flex flex-col justify-around h-full">
+            <view class="text-[16px] font-bold text-[#FC500F]">
+              {{ item.mainTitle }}
+            </view>
+            <view class="text-[14px] text-[#999]">
+              兑换时间:{{ item.createTime }}
+            </view>
+            <view class="text-[14px] text-[#999]">
+              到期时间:{{ item.updateTime }}
+            </view>
+          </view>
+        </view>
+      </view>
+      <view v-else class="w-full h-full flex flex-col justify-center items-center">
+        <view class="text-center text-[#999] text-[14px]">
+          暂无兑换记录
+        </view>
+      </view>
+    </scroll-view>
+  </view>
+</template>
+<style lang="scss" scoped>
+.sidebar {
+  height: calc(80vh - 90px);
+  max-height: calc(620px - 90px);
+  padding: 20px;
+}
+
+.box {
+  width: 98%;
+  // 宽高比3/1
+  aspect-ratio: 4 / 1;
+  padding: 5px;
+  border-radius: 10px;
+  background: linear-gradient(0deg, #FFFBF7 0%, #FFE0CC 100%);
+  box-shadow: 3px 6px 4px 0px rgba(239, 103, 36, 0.48);
+}
+</style>

+ 103 - 0
pages/user/components/set.uvue

@@ -0,0 +1,103 @@
+<script setup lang='ts'>
+import { ref } from 'vue'
+import { useUi, type ClFormRule, useForm } from "@/uni_modules/cool-ui";
+import { encryptPassword, user, router } from '@/.cool';
+import { changeUserPwd } from "@/services/user";
+const ui = useUi();
+const formData = ref({
+  oldPassword: '',
+  password: ''
+})
+const { formRef, validate, clearValidate } = useForm();
+const rules = new Map<string, ClFormRule[]>([
+  [
+    "oldPassword",
+    [
+      { required: true, message: '旧密码不能为空' },
+    ]
+  ],
+  [
+    "password",
+    [
+      { required: true, message: '新密码不能为空' },
+    ]
+  ]
+])
+const handleEdit = () => {
+  validate((valid, errors) => {
+    if (valid) {
+      ui.showConfirm({
+        title: "修改密码",
+        message: "确定要修改密码吗?",
+        async callback(action) {
+          if (action === "confirm") {
+            await changeUserPwd({
+              oldPassword: encryptPassword(formData.value.oldPassword),
+              password: encryptPassword(formData.value.password)
+            })
+            // 执行删除操作
+          } else {
+            console.log("用户取消操作");
+          }
+        },
+      });
+    } else {
+      ui.showToast({
+        message: "请填写完整信息",
+      });
+    }
+  });
+
+}
+const handleLogout = () => {
+  ui.showConfirm({
+    title: "退出登录",
+    message: "确定要退出登录吗?",
+    async callback(action) {
+      if (action === "confirm") {
+        user.logout()
+        ui.showToast({
+          message: "退出登录成功",
+        });
+      } else {
+        console.log("用户取消操作");
+      }
+    },
+  });
+}
+</script>
+<template>
+  <view class=" w-full bg-[#fff] shadow rounded-xl mt-[20px] text-[#00A9FF] sidebar flex  items-center">
+    <view class="ww">
+      <cl-form ref="formRef" :showMessage="false" v-model="formData" labelPosition="left" :rules="rules">
+        <cl-form-item labelPosition="left" :pt="{ className: '!mb-4' }" label="旧密码" prop="oldPassword">
+          <cl-input password v-model="formData.oldPassword" placeholder="请输入旧密码"></cl-input>
+        </cl-form-item>
+        <cl-form-item labelPosition="left" :pt="{ className: '!mb-4' }" label="新密码" prop="password">
+          <cl-input password v-model="formData.password" placeholder="请输入新密码"></cl-input>
+        </cl-form-item>
+      </cl-form>
+    </view>
+    <uni-button
+      class="h-[30px] rounded-2xl flex flex-row items-center justify-center  w-[200px] text-[14px] active:bg-[#045bb9]"
+      type="primary" block @tap="handleEdit">修改密码</uni-button>
+    <uni-button
+      class="flex flex-row w-[200px] items-center justify-center gap-2 h-[30px] bg-[#09BA07] rounded-2xl px-[10px] py-[5px] text-[14px] !text-[#fff] active:bg-[#099508] my-[10px]"><cl-image
+        src="https://oss.xiaoxiongcode.com/static/个人中心/微信.png" mode="heightFix" height="20px" width="auto"></cl-image>
+      微信账号快捷登录</uni-button>
+    <uni-button
+      class="h-[30px] rounded-2xl flex flex-row items-center justify-center text-[14px]  w-[200px] active:bg-[#045bb9]"
+      type="primary" block @tap="handleLogout">退出登录</uni-button>
+  </view>
+</template>
+<style lang="scss" scoped>
+.sidebar {
+  height: calc(80vh - 90px);
+  max-height: calc(620px - 90px);
+  padding: 20px;
+}
+
+.ww {
+  width: 70%;
+}
+</style>

+ 133 - 0
pages/user/components/user.uvue

@@ -0,0 +1,133 @@
+<script setup lang='ts'>
+import { user } from '@/.cool'
+import { computed, ref, type Ref, onMounted, nextTick } from 'vue'
+import { config } from '@/config'
+import { updateUserInfo } from '@/services/user'
+const userInfo = computed(() => user.info.value?.userInfo)
+type FormData = {
+  username: string;
+  nickName: string;
+  phone: string;
+  realName: string;
+  birthDay: string;
+  score: number;
+  userId: string;
+  avatar: string;
+};
+const formData = ref<FormData>({
+  username: "",
+  nickName: "",
+  phone: "",
+  realName: "",
+  birthDay: "",
+  score: 0,
+  userId: "",
+  avatar: ""
+})
+onMounted(() => {
+  console.log(userInfo.value)
+  nextTick(() => {
+    formData.value.username = userInfo.value.username
+    formData.value.nickName = userInfo.value.nickName
+    formData.value.phone = userInfo.value.phone
+    formData.value.realName = userInfo.value.realName
+    formData.value.birthDay = userInfo.value.birthDay
+    formData.value.score = userInfo.value.score
+    formData.value.userId = userInfo.value.userId
+    formData.value.avatar = userInfo.value.avatar
+    avatar.value = userInfo.value.avatar ? config.baseUrl + userInfo.value.avatar : ''
+  })
+})
+async function handleSubmit() {
+  console.log(formData.value)
+  await updateUserInfo(formData.value)
+  await user.get()
+  uni.showToast({
+    title: '更新成功',
+    icon: 'success',
+    duration: 2000
+  })
+
+}
+function handleSuccess(res: any) {
+  formData.value.avatar = res.url
+  avatar.value = config.baseUrl + res.url
+}
+const avatar = ref('')
+</script>
+<template>
+  <view class="w-full h-full py-[20px] pb-[30px] ">
+    <cl-row :gutter="20" class="w-full h-full">
+      <cl-col :span="8">
+        <view class="w-full h-full bg-[#00A9FF] rounded-3xl flex justify-end items-center shadow">
+          <view class="w-[88px] h-[88px] border border-[#fff] border-solid border-[4px] rounded-full z-[1]">
+            <cl-avatar :size="80"
+              :src="userInfo.avatar ? config.baseUrl + userInfo.avatar : 'https://oss.xiaoxiongcode.com/static/个人中心/图层 506.png'"
+              rounded></cl-avatar>
+          </view>
+          <view
+            class="bg-[#FDF6E9] hh rounded-3xl p-[10px] w-full mt-[-30px] pt-[30px] px-[20px] text-black flex flex-col justify-between ">
+            <view>
+              <view class="py-1 text-[14px]">
+                用户账号:{{ userInfo.username }}
+              </view>
+              <view class="py-1 text-[14px]">
+                昵称:{{ userInfo.nickName }}
+              </view>
+              <view class="py-1 text-[14px]">
+                积分 :{{ userInfo.score }}
+              </view>
+            </view>
+            <view class="btn my-1 h-[40px] rounded-full mx-auto flex justify-center items-center text-white"
+              @click="handleSubmit">
+              保存
+            </view>
+          </view>
+        </view>
+      </cl-col>
+      <cl-col :span="16">
+        <view class="w-full h-full bg-[#fff] shadow rounded-xl px-[10px] py-[10px] text-[#00A9FF] ">
+          <scroll-view direction="vertical" :show-scrollbar="false" class="sidebar">
+            <cl-form v-model="formData" labelPosition="left">
+              <cl-form-item :pt="{ className: '!mb-4' }" label="头像" prop="avatar">
+                <cl-upload v-model="avatar" @success="handleSuccess"></cl-upload>
+              </cl-form-item>
+              <cl-form-item :pt="{ className: '!mb-4' }" label="昵称" prop="nickName">
+                <cl-input v-model="formData.nickName" placeholder="请输入昵称"></cl-input>
+              </cl-form-item>
+              <cl-form-item :pt="{ className: '!mb-4' }" label="手机号" prop="phone">
+                <cl-input v-model="formData.phone" placeholder="请输入手机号"></cl-input>
+              </cl-form-item>
+              <cl-form-item :pt="{ className: '!mb-4' }" label="姓名" prop="realName">
+                <cl-input v-model="formData.realName" placeholder="请输入姓名"></cl-input>
+              </cl-form-item>
+              <cl-form-item :pt="{ className: '!mb-4' }" label="生日" prop="birthDay">
+                <cl-select-date v-model="formData.birthDay" type="date" label-format="YYYY年MM月DD日"
+                  value-format="YYYY-MM-DD" title="选择日期"></cl-select-date>
+              </cl-form-item>
+            </cl-form>
+          </scroll-view>
+        </view>
+      </cl-col>
+    </cl-row>
+  </view>
+</template>
+<style lang="scss" scoped>
+.shadow {
+  box-shadow: 3px 6px 10px 0px rgba(0, 128, 216, 0.86);
+}
+
+.sidebar {
+  height: calc(80vh - 120px);
+  max-height: calc(620px - 120px);
+}
+
+.hh {
+  height: calc(100% - 80px);
+}
+
+.btn {
+  width: 80%;
+  background: linear-gradient(0deg, #34BAFF 0%, #B9EBFE 100%);
+}
+</style>

+ 51 - 0
pages/user/info.uvue

@@ -0,0 +1,51 @@
+<script lang="ts" setup>
+import Back from '@/components/back.uvue'
+import user from './components/user.uvue'
+import conversion from './components/conversion.uvue'
+import setting from './components/set.uvue'
+import { ref, computed } from 'vue'
+const active = ref<'user' | 'conversion' | 'set'>('user')
+
+
+</script>
+
+<template>
+  <cl-page>
+    <Back />
+    <img src="https://oss.xiaoxiongcode.com/static/个人中心/bg.png" alt="" class="w-full h-full object-cover">
+    <view class="content">
+      <view class="flex flex-row justify-between items-center">
+        <view
+          class="flex flex-row items-center justify-center gap-2 w-[120px] bg-[#B1E5FE] rounded-xl px-[10px] py-[5px] text-[#00A9FF]"
+          :class="{ 'bg-[#00A9FF] !text-white': active === 'user' }" @click="active = 'user'">
+          <cl-image src="https://oss.xiaoxiongcode.com/static/个人中心/图层 506.png" mode="heightFix" height="20px"
+            width="auto"></cl-image> 个人信息
+        </view>
+        <view
+          class="flex flex-row items-center justify-center gap-2 w-[120px] bg-[#B1E5FE] rounded-xl px-[10px] py-[5px] text-[#00A9FF]"
+          :class="{ 'bg-[#00A9FF] !text-white': active === 'conversion' }" @click="active = 'conversion'">
+          <cl-image src="https://oss.xiaoxiongcode.com/static/个人中心/图层 503.png" mode="heightFix" height="20px"
+            width="auto"></cl-image> 兑换记录
+        </view>
+        <view
+          class="flex flex-row items-center justify-center gap-2 w-[120px] bg-[#B1E5FE] rounded-xl px-[10px] py-[5px] text-[#00A9FF]"
+          :class="{ 'bg-[#00A9FF] !text-white': active === 'set' }" @click="active = 'set'">
+          <cl-image src="https://oss.xiaoxiongcode.com/static/个人中心/图层 504.png" mode="heightFix" height="20px"
+            width="auto"></cl-image> 设置
+        </view>
+      </view>
+      <view class="w-full h-full">
+        <user v-if="active === 'user'" />
+        <conversion v-else-if="active === 'conversion'" />
+        <setting v-else-if="active === 'set'" />
+      </view>
+    </view>
+  </cl-page>
+</template>
+
+<style lang="scss" scoped>
+.content {
+  @apply absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-[#76DAF9] w-[80vw] h-[80vh] max-w-[1000px] max-h-[620px] rounded-[30px] p-[20px] text-white;
+
+}
+</style>

+ 5 - 0
services/subject/record.ts

@@ -0,0 +1,5 @@
+import { usePost, stringify, useGet } from "@/.cool";
+
+export function fetchSubjectFeeRecord(parameter: any) {
+  return useGet(`/subject/fee-record`, parameter) as Promise<any>
+}

+ 7 - 1
services/user.ts

@@ -1,4 +1,4 @@
-import { usePost, stringify, useGet } from "@/.cool";
+import { usePost, stringify, useGet, usePut } from "@/.cool";
 export type LoginData = {
   access_token: string;
   refresh_token: string;
@@ -51,4 +51,10 @@ export function getUsersInfo() {
 }
 export function dictData() {
   return useGet(`/upms/dicts/data`) as Promise<DICT_DATA>
+}
+export function updateUserInfo(parameter: any) {
+  return usePut(`/upms/users/change-info`, parameter) as Promise<any>
+}
+export function changeUserPwd(parameter: any) {
+  return usePut(`/upms/users/change-pwd`, parameter) as Promise<any>
 }

BIN
static/rain_experiment.mp3


+ 4 - 0
uni.scss

@@ -74,3 +74,7 @@ $uni-color-subtitle: #555555; // 二级标题颜色
 $uni-font-size-subtitle:26px;
 $uni-color-paragraph: #3F536E; // 文章段落颜色
 $uni-font-size-paragraph:15px;
+
+*{
+  box-sizing:  border-box !important;
+}