refactor: 优化反馈和布局
This commit is contained in:
@@ -1,231 +0,0 @@
|
||||
<template>
|
||||
<view class="wechat-profile">
|
||||
<!-- 头像选择 -->
|
||||
<view class="avatar-section">
|
||||
<view class="section-title">头像</view>
|
||||
<button class="avatar-button" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
|
||||
<image v-if="avatar" :src="avatar" class="avatar-image" mode="aspectFill" />
|
||||
<view v-else class="avatar-placeholder">
|
||||
<wd-icon name="camera" size="40" color="#999" />
|
||||
<text class="placeholder-text">选择头像</text>
|
||||
</view>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 昵称输入 -->
|
||||
<view class="nickname-section">
|
||||
<view class="section-title">昵称</view>
|
||||
<input
|
||||
v-model="nickname"
|
||||
type="nickname"
|
||||
class="nickname-input"
|
||||
placeholder="请输入昵称"
|
||||
:maxlength="20"
|
||||
@blur="onNicknameChange"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 性别选择 -->
|
||||
<view class="gender-section">
|
||||
<view class="section-title">性别</view>
|
||||
<wd-radio-group v-model="gender" shape="button" class="gender-group">
|
||||
<wd-radio :value="1" class="gender-radio">男</wd-radio>
|
||||
<wd-radio :value="2" class="gender-radio">女</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from "vue";
|
||||
import FileAPI, { type FileInfo } from "@/api/file";
|
||||
|
||||
/**
|
||||
* 微信小程序头像昵称组件
|
||||
*
|
||||
* @description 用于微信小程序环境下的头像、昵称和性别选择
|
||||
* @component WechatProfile
|
||||
* @example
|
||||
* <WechatProfile v-model="profileData" @change="onProfileChange" />
|
||||
*/
|
||||
|
||||
defineOptions({
|
||||
name: "WechatProfile",
|
||||
});
|
||||
|
||||
// Props
|
||||
interface ProfileData {
|
||||
avatar?: string;
|
||||
nickname?: string;
|
||||
gender?: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
modelValue?: ProfileData;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: () => ({
|
||||
avatar: "",
|
||||
nickname: "",
|
||||
gender: 1,
|
||||
}),
|
||||
});
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": [value: ProfileData];
|
||||
change: [value: ProfileData];
|
||||
}>();
|
||||
|
||||
// 响应式数据
|
||||
const avatar = ref(props.modelValue?.avatar || "");
|
||||
const nickname = ref(props.modelValue?.nickname || "");
|
||||
const gender = ref(props.modelValue?.gender || 1);
|
||||
|
||||
// 监听props变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
avatar.value = newValue.avatar || "";
|
||||
nickname.value = newValue.nickname || "";
|
||||
gender.value = newValue.gender || 1;
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
// 监听数据变化并发出事件
|
||||
const emitChange = () => {
|
||||
const value = {
|
||||
avatar: avatar.value,
|
||||
nickname: nickname.value,
|
||||
gender: gender.value,
|
||||
};
|
||||
emit("update:modelValue", value);
|
||||
emit("change", value);
|
||||
};
|
||||
|
||||
// 头像选择处理
|
||||
const onChooseAvatar = async (e: any) => {
|
||||
try {
|
||||
const { avatarUrl } = e.detail;
|
||||
|
||||
// 上传头像到服务器
|
||||
uni.showLoading({ title: "上传中..." });
|
||||
|
||||
const fileInfo: FileInfo = await FileAPI.upload(avatarUrl);
|
||||
avatar.value = fileInfo.url;
|
||||
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: "头像上传成功", icon: "success" });
|
||||
|
||||
emitChange();
|
||||
} catch (error) {
|
||||
uni.hideLoading();
|
||||
console.error("头像上传失败:", error);
|
||||
uni.showToast({ title: "头像上传失败", icon: "error" });
|
||||
}
|
||||
};
|
||||
|
||||
// 昵称变化处理
|
||||
const onNicknameChange = () => {
|
||||
emitChange();
|
||||
};
|
||||
|
||||
// 监听性别变化
|
||||
watch(gender, () => {
|
||||
emitChange();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wechat-profile {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin-bottom: 20rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--wot-color-text);
|
||||
}
|
||||
|
||||
.avatar-section {
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.avatar-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
padding: 0;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--wot-color-bg-light);
|
||||
border: 2rpx dashed var(--wot-color-border);
|
||||
border-radius: 50%;
|
||||
|
||||
.placeholder-text {
|
||||
margin-top: 10rpx;
|
||||
font-size: 24rpx;
|
||||
color: var(--wot-color-text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nickname-section {
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.nickname-input {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 28rpx;
|
||||
background: var(--wot-color-bg-light);
|
||||
border: 1rpx solid var(--wot-color-border);
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.gender-section {
|
||||
.gender-group {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
|
||||
.gender-radio {
|
||||
flex: 1;
|
||||
|
||||
:deep(.wd-radio) {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,11 +0,0 @@
|
||||
/**
|
||||
* 业务组件导出索引
|
||||
*/
|
||||
import WechatProfile from "./WechatProfile.vue";
|
||||
|
||||
export { WechatProfile };
|
||||
|
||||
// 用于批量注册组件
|
||||
export default {
|
||||
WechatProfile,
|
||||
};
|
||||
@@ -1,67 +0,0 @@
|
||||
<template>
|
||||
<template v-if="tagType">
|
||||
<wd-tag :type="tagType as any" :round="round">{{ label }}</wd-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<view>{{ label }}</view>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDictStore } from "@/store/modules/dict";
|
||||
const dictStore = useDictStore();
|
||||
|
||||
const props = defineProps({
|
||||
code: String,
|
||||
modelValue: [String, Number],
|
||||
round: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const label = ref("");
|
||||
const tagType = ref<string | undefined>();
|
||||
|
||||
const getLabelAndTagByValue = async (dictCode: string, value: any) => {
|
||||
// 按需加载字典数据
|
||||
await dictStore.loadDictItems(dictCode);
|
||||
|
||||
// 从缓存中获取字典数据
|
||||
const dictItems = dictStore.getDictItems(dictCode);
|
||||
|
||||
// 查找对应的字典项
|
||||
const dictItem = dictItems.find((item) => item.value == value);
|
||||
|
||||
return {
|
||||
label: dictItem?.label || "",
|
||||
tagType: dictItem?.tagType,
|
||||
};
|
||||
};
|
||||
|
||||
// 监听字典数据变化,确保WebSocket更新时刷新标签
|
||||
watch(
|
||||
() => props.code && dictStore.getDictItems(props.code),
|
||||
async () => {
|
||||
if (props.code) {
|
||||
await fetchLabelAndTag();
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 监听 props 的变化,获取并更新 label 和 tag
|
||||
const fetchLabelAndTag = async () => {
|
||||
if (!props.code || props.modelValue === undefined) return;
|
||||
|
||||
const result = await getLabelAndTagByValue(props.code, props.modelValue);
|
||||
label.value = result.label;
|
||||
tagType.value = result.tagType;
|
||||
};
|
||||
|
||||
// 首次挂载时获取字典数据
|
||||
onMounted(fetchLabelAndTag);
|
||||
|
||||
// 当 modelValue 发生变化时重新获取
|
||||
watch(() => props.modelValue, fetchLabelAndTag);
|
||||
</script>
|
||||
@@ -1,141 +0,0 @@
|
||||
<template>
|
||||
<wd-picker
|
||||
v-if="type === 'select' || type === 'radio'"
|
||||
v-model="selectedValue"
|
||||
:columns="options"
|
||||
:label="label"
|
||||
:placeholder="placeholder"
|
||||
clearable
|
||||
:rules="rules"
|
||||
@confirm="handleChange"
|
||||
/>
|
||||
<wd-select-picker
|
||||
v-else-if="type === 'checkbox'"
|
||||
v-model="selectedValue"
|
||||
:columns="options"
|
||||
:placeholder="placeholder"
|
||||
clearable
|
||||
:label="label"
|
||||
:rules="rules"
|
||||
@confirm="handleChange"
|
||||
/>
|
||||
<wd-input
|
||||
v-else
|
||||
v-model="selectedValue"
|
||||
:label="label"
|
||||
clearable
|
||||
:placeholder="placeholder"
|
||||
:rules="rules"
|
||||
@input="handleChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDictStore } from "@/store/modules/dict";
|
||||
|
||||
const dictStore = useDictStore();
|
||||
|
||||
const props = defineProps({
|
||||
code: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
modelValue: {
|
||||
type: [String, Number, Array],
|
||||
required: false,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: "select",
|
||||
validator: (value: string) => ["select", "radio", "checkbox"].includes(value),
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "请选择",
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
rules: {
|
||||
type: Array as PropType<any[]>,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const options = ref<Array<{ label: string; value: string | number }>>([]);
|
||||
|
||||
const selectedValue = ref<any>(
|
||||
typeof props.modelValue === "string" || typeof props.modelValue === "number"
|
||||
? props.modelValue
|
||||
: Array.isArray(props.modelValue)
|
||||
? props.modelValue
|
||||
: undefined
|
||||
);
|
||||
|
||||
// 监听 modelValue 变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
if (props.type === "checkbox") {
|
||||
selectedValue.value = Array.isArray(newValue) ? newValue : [];
|
||||
} else {
|
||||
selectedValue.value = newValue?.toString() || "";
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 监听 options 变化并重新匹配 selectedValue
|
||||
watch(
|
||||
() => options.value,
|
||||
(newOptions) => {
|
||||
// options 加载后,确保 selectedValue 可以正确匹配到 options
|
||||
if (newOptions.length > 0 && selectedValue.value !== undefined) {
|
||||
const matchedOption = newOptions.find((option) => option.value === selectedValue.value);
|
||||
if (!matchedOption && props.type !== "checkbox") {
|
||||
// 如果找不到匹配项,清空选中
|
||||
selectedValue.value = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 监听 selectedValue 的变化并触发 update:modelValue
|
||||
function handleChange(val: any) {
|
||||
emit("update:modelValue", val.value);
|
||||
}
|
||||
|
||||
// 获取字典数据
|
||||
onMounted(async () => {
|
||||
if (!props.code) {
|
||||
return;
|
||||
}
|
||||
// 按需加载字典数据
|
||||
await dictStore.loadDictItems(props.code);
|
||||
options.value = dictStore.getDictItems(props.code).map((item) => ({
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
}));
|
||||
});
|
||||
|
||||
// 监听字典数据变化,确保WebSocket更新时刷新选项
|
||||
watch(
|
||||
() => dictStore.getDictItems(props.code),
|
||||
(newItems) => {
|
||||
if (newItems.length > 0) {
|
||||
options.value = newItems.map((item) => ({
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
}));
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
@@ -2,6 +2,21 @@
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
|
||||
const { theme, themeVars } = useTheme();
|
||||
|
||||
// 状态栏高度
|
||||
const statusBarHeight = ref(0);
|
||||
|
||||
// 获取状态栏高度
|
||||
onMounted(() => {
|
||||
uni.getSystemInfo({
|
||||
success: (res) => {
|
||||
statusBarHeight.value = res.statusBarHeight || 20;
|
||||
},
|
||||
fail: () => {
|
||||
statusBarHeight.value = 20;
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -21,7 +36,9 @@ export default {
|
||||
custom-style="background-color: #f5f5f5;min-height: 100vh"
|
||||
:class="{ 'wot-theme-dark': theme === 'dark' }"
|
||||
>
|
||||
<slot />
|
||||
<view class="box-border w-full min-h-screen" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<slot />
|
||||
</view>
|
||||
<wd-notify />
|
||||
<wd-toast />
|
||||
<wd-message-box />
|
||||
|
||||
@@ -1,152 +1,10 @@
|
||||
<!--
|
||||
* @Author: weisheng
|
||||
* @Date: 2024-11-01 12:31:47
|
||||
* @LastEditTime: 2024-11-14 19:02:06
|
||||
* @LastEditors: weisheng
|
||||
* @Description:
|
||||
* @FilePath: \wot-demo\src\layouts\tabbar.vue
|
||||
* 记得注释
|
||||
* @Author: Ray.Hao
|
||||
* @Date: 2025-05-01 12:31:47
|
||||
* @LastEditTime: 2025-05-01 12:31:47
|
||||
* @LastEditors: Ray.Hao
|
||||
* @Description: Tabbar 布局组件
|
||||
-->
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, nextTick, onUnmounted } from "vue";
|
||||
import { onShow } from "@dcloudio/uni-app";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
|
||||
// 定义 TabbarItem 接口
|
||||
interface TabbarItem {
|
||||
name: string;
|
||||
value: number | null;
|
||||
active: boolean;
|
||||
title: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
// 根据 pages.json 配置的 tabbar 项目
|
||||
const tabbarItems = ref<TabbarItem[]>([
|
||||
{ name: "index", value: null, active: true, title: "首页", icon: "home" },
|
||||
{ name: "mine", value: null, active: false, title: "我的", icon: "user" },
|
||||
]);
|
||||
|
||||
const { theme, themeVars } = useTheme();
|
||||
|
||||
// 计算属性
|
||||
const tabbarList = computed(() => tabbarItems.value);
|
||||
|
||||
const activeTabbar = computed(() => {
|
||||
const item = tabbarItems.value.find((item) => item.active);
|
||||
return item || tabbarItems.value[0];
|
||||
});
|
||||
|
||||
// 方法
|
||||
const getTabbarItemValue = (name: string) => {
|
||||
const item = tabbarItems.value.find((item) => item.name === name);
|
||||
return item && item.value ? item.value : null;
|
||||
};
|
||||
|
||||
const setTabbarItemActive = (name: string) => {
|
||||
console.log(`设置 tabbar 激活状态: ${name}`);
|
||||
tabbarItems.value.forEach((item) => {
|
||||
if (item.name === name) {
|
||||
item.active = true;
|
||||
} else {
|
||||
item.active = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 根据当前路由更新 tabbar 激活状态
|
||||
const updateTabbarByRoute = () => {
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length > 0) {
|
||||
const currentPage = pages[pages.length - 1];
|
||||
const route = currentPage.route;
|
||||
|
||||
console.log("=== Tabbar 路由更新 ===");
|
||||
console.log("当前路由:", route);
|
||||
console.log("当前激活的 tabbar:", activeTabbar.value.name);
|
||||
|
||||
// 根据当前路由设置活跃的 tabbar
|
||||
if (route === "pages/index/index") {
|
||||
console.log("设置首页为激活状态");
|
||||
setTabbarItemActive("index");
|
||||
} else if (route === "pages/mine/index") {
|
||||
console.log("设置我的页面为激活状态");
|
||||
setTabbarItemActive("mine");
|
||||
} else {
|
||||
console.log("非 tabbar 页面,保持当前状态");
|
||||
}
|
||||
|
||||
console.log("更新后激活的 tabbar:", activeTabbar.value.name);
|
||||
console.log("=== 更新完成 ===");
|
||||
}
|
||||
};
|
||||
|
||||
function handleTabbarChange({ value }: { value: string }) {
|
||||
console.log(`用户点击 tabbar: ${value}`);
|
||||
|
||||
// 立即设置激活状态
|
||||
tabbarItems.value.forEach((item) => {
|
||||
item.active = item.name === value;
|
||||
});
|
||||
|
||||
// 导航到对应页面
|
||||
if (value === "index") {
|
||||
uni.reLaunch({
|
||||
url: "/pages/index/index",
|
||||
});
|
||||
} else if (value === "mine") {
|
||||
uni.reLaunch({
|
||||
url: "/pages/mine/index",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 监听页面栈变化
|
||||
const currentRoute = ref("");
|
||||
|
||||
const updateCurrentRoute = () => {
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length > 0) {
|
||||
const currentPage = pages[pages.length - 1];
|
||||
const newRoute = currentPage.route || "";
|
||||
|
||||
if (newRoute !== currentRoute.value) {
|
||||
currentRoute.value = newRoute;
|
||||
console.log("页面路由发生变化:", newRoute);
|
||||
updateTabbarByRoute();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
updateTabbarByRoute();
|
||||
updateCurrentRoute();
|
||||
});
|
||||
|
||||
// 监听页面发出的 tabbar 更新事件
|
||||
uni.$on("updateTabbar", (tabName: string) => {
|
||||
console.log("收到 tabbar 更新事件:", tabName);
|
||||
setTabbarItemActive(tabName);
|
||||
});
|
||||
});
|
||||
|
||||
// 页面显示时更新 tabbar 状态
|
||||
onShow(() => {
|
||||
nextTick(() => {
|
||||
updateCurrentRoute();
|
||||
});
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
uni.hideTabBar();
|
||||
// #endif
|
||||
});
|
||||
|
||||
// 组件卸载时移除事件监听
|
||||
onUnmounted(() => {
|
||||
uni.$off("updateTabbar");
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
@@ -165,7 +23,9 @@ export default {
|
||||
custom-style="min-height: 100vh"
|
||||
:class="{ 'wot-theme-dark': theme === 'dark' }"
|
||||
>
|
||||
<slot />
|
||||
<view class="box-border w-full min-h-screen" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<slot />
|
||||
</view>
|
||||
<wd-tabbar
|
||||
:model-value="activeTabbar.name"
|
||||
placeholder
|
||||
@@ -178,7 +38,7 @@ export default {
|
||||
v-for="(item, index) in tabbarList"
|
||||
:key="index"
|
||||
:name="item.name"
|
||||
:value="getTabbarItemValue(item.name)"
|
||||
:value="item.value"
|
||||
:title="item.title"
|
||||
:icon="item.icon"
|
||||
/>
|
||||
@@ -189,4 +49,93 @@ export default {
|
||||
</wd-config-provider>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
<script setup lang="ts">
|
||||
import { onShow } from "@dcloudio/uni-app";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
|
||||
// 定义 TabbarItem 接口
|
||||
interface TabbarItem {
|
||||
name: string;
|
||||
value: number | null;
|
||||
active: boolean;
|
||||
title: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
const { theme, themeVars } = useTheme();
|
||||
|
||||
// 状态栏高度
|
||||
const statusBarHeight = ref(0);
|
||||
|
||||
// tabbar 配置
|
||||
const tabbarItems = ref<TabbarItem[]>([
|
||||
{ name: "index", value: null, active: true, title: "首页", icon: "home" },
|
||||
{ name: "mine", value: null, active: false, title: "我的", icon: "user" },
|
||||
]);
|
||||
|
||||
// 计算属性
|
||||
const tabbarList = computed(() => tabbarItems.value);
|
||||
const activeTabbar = computed(
|
||||
() => tabbarItems.value.find((item) => item.active) || tabbarItems.value[0]
|
||||
);
|
||||
|
||||
// 更新 tabbar 状态
|
||||
const updateTabbarState = () => {
|
||||
const pages = getCurrentPages();
|
||||
if (!pages.length) return;
|
||||
|
||||
const route = pages[pages.length - 1].route;
|
||||
|
||||
if (route === "pages/index/index") {
|
||||
setTabbarActive("index");
|
||||
} else if (route === "pages/mine/index") {
|
||||
setTabbarActive("mine");
|
||||
}
|
||||
};
|
||||
|
||||
// 设置激活状态
|
||||
const setTabbarActive = (name: string) => {
|
||||
tabbarItems.value.forEach((item) => {
|
||||
item.active = item.name === name;
|
||||
});
|
||||
};
|
||||
|
||||
// 处理点击事件
|
||||
const handleTabbarChange = ({ value }: { value: string }) => {
|
||||
setTabbarActive(value);
|
||||
|
||||
const url = value === "index" ? "/pages/index/index" : "/pages/mine/index";
|
||||
uni.reLaunch({ url });
|
||||
};
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
// 获取状态栏高度(改为异步方法)
|
||||
uni.getSystemInfo({
|
||||
success: (res) => {
|
||||
statusBarHeight.value = res.statusBarHeight || 20;
|
||||
},
|
||||
fail: () => {
|
||||
statusBarHeight.value = 20;
|
||||
},
|
||||
});
|
||||
|
||||
// 初始化状态
|
||||
nextTick(updateTabbarState);
|
||||
|
||||
// 监听事件
|
||||
uni.$on("updateTabbar", setTabbarActive);
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
nextTick(updateTabbarState);
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
uni.hideTabBar();
|
||||
// #endif
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
uni.$off("updateTabbar");
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
"style": {},
|
||||
"layout": "tabbar"
|
||||
},
|
||||
{
|
||||
"path": "pages/work/index",
|
||||
"type": "page",
|
||||
"style": {}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/about/index",
|
||||
"type": "page",
|
||||
@@ -93,4 +98,4 @@
|
||||
]
|
||||
},
|
||||
"subPackages": []
|
||||
}
|
||||
}
|
||||
@@ -147,25 +147,25 @@ const navList = reactive([
|
||||
{
|
||||
icon: "/static/icons/user.png",
|
||||
title: "用户管理",
|
||||
url: "/pages/work/user/index",
|
||||
url: "/pages/work/index",
|
||||
prem: "sys:user:query",
|
||||
},
|
||||
{
|
||||
icon: "/static/icons/role.png",
|
||||
title: "角色管理",
|
||||
url: "/pages/work/role/index",
|
||||
url: "/pages/work/index",
|
||||
prem: "sys:role:query",
|
||||
},
|
||||
{
|
||||
icon: "/static/icons/notice.png",
|
||||
title: "通知公告",
|
||||
url: "/pages/work/notice/index",
|
||||
url: "/pages/work/index",
|
||||
prem: "sys:notice:query",
|
||||
},
|
||||
{
|
||||
icon: "/static/icons/setting.png",
|
||||
title: "系统配置",
|
||||
url: "/pages/work/config/index",
|
||||
url: "/pages/work/index",
|
||||
prem: "sys:config:query",
|
||||
},
|
||||
]);
|
||||
@@ -265,6 +265,4 @@ onShow(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style setup lang="scss">
|
||||
/* 已全部使用UnoCSS替代,不需要额外的样式 */
|
||||
</style>
|
||||
<style setup lang="scss"></style>
|
||||
|
||||
@@ -1,67 +1,62 @@
|
||||
<template>
|
||||
<view class="feedback-container">
|
||||
<!-- 问题类型选择 -->
|
||||
<wd-cell-group title="问题类型" border>
|
||||
<view class="radio-group">
|
||||
<label v-for="item in feedbackTypes" :key="item.value" class="radio-item">
|
||||
<text class="radio-text">{{ item.label }}</text>
|
||||
<radio
|
||||
:value="item.value"
|
||||
:checked="feedbackType === item.value"
|
||||
color="#0083ff"
|
||||
class="radio-button"
|
||||
style="transform: scale(0.8)"
|
||||
@click="handleRadioChange(item.value)"
|
||||
/>
|
||||
</label>
|
||||
</view>
|
||||
</wd-cell-group>
|
||||
<view class="app-container">
|
||||
<wd-navbar title="意见反馈" left-arrow @click-left="handleBack" />
|
||||
|
||||
<!-- 问题描述 -->
|
||||
<wd-cell-group title="问题描述" border>
|
||||
<wd-textarea
|
||||
v-model="description"
|
||||
placeholder="请详细描述您遇到的问题或建议..."
|
||||
:maxlength="500"
|
||||
show-count
|
||||
:rows="5"
|
||||
/>
|
||||
</wd-cell-group>
|
||||
123
|
||||
<wd-text size="small">选填,最多上传3张图片</wd-text>
|
||||
<wd-form ref="formRef" :model="formData" :rules="rules">
|
||||
<!-- 问题类型选择 -->
|
||||
<wd-form-item label="问题类型" prop="feedbackType">
|
||||
<wd-radio-group v-model="formData.feedbackType" inline>
|
||||
<wd-radio v-for="item in feedbackTypes" :key="item.value" :value="item.value">
|
||||
{{ item.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-form-item>
|
||||
|
||||
<!-- 图片上传 -->
|
||||
<wd-cell-group title="相关截图(选填)" border>
|
||||
<view class="upload-box">
|
||||
<!-- 问题描述 -->
|
||||
<wd-form-item label="问题描述" prop="description">
|
||||
<wd-textarea
|
||||
v-model="formData.description"
|
||||
placeholder="请详细描述您遇到的问题或建议..."
|
||||
:maxlength="120"
|
||||
show-word-limit
|
||||
/>
|
||||
</wd-form-item>
|
||||
|
||||
<!-- 图片上传 -->
|
||||
<wd-form-item label="相关截图" prop="fileList">
|
||||
<wd-upload
|
||||
v-model="fileList"
|
||||
v-model="formData.fileList"
|
||||
:max-count="3"
|
||||
:before-read="beforeRead"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
</wd-form-item>
|
||||
|
||||
<!-- 联系方式 -->
|
||||
<wd-form-item label="联系方式" prop="contact">
|
||||
<wd-input v-model="formData.contact" placeholder="请输入您的手机号或邮箱" clearable />
|
||||
<wd-text size="small">选填,便于我们与您联系</wd-text>
|
||||
</wd-form-item>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<view class="submit-btn">
|
||||
<wd-button type="primary" block :loading="submitting" @click="handleSubmit">
|
||||
提交反馈
|
||||
</wd-button>
|
||||
</view>
|
||||
</wd-cell-group>
|
||||
|
||||
<!-- 联系方式 -->
|
||||
<wd-cell-group title="联系方式(选填)" border>
|
||||
<wd-input v-model="contact" placeholder="请输入您的手机号或邮箱" clearable />
|
||||
</wd-cell-group>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<view class="submit-btn">
|
||||
<wd-button type="primary" block :loading="submitting" @click="handleSubmit">
|
||||
提交反馈
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<wd-toast />
|
||||
</wd-form>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue";
|
||||
import { useToast } from "wot-design-uni";
|
||||
<script setup lang="ts">
|
||||
import { checkLogin } from "@/utils/auth";
|
||||
import { useToast } from "wot-design-uni";
|
||||
import { FormRules } from "wot-design-uni/components/wd-form/types";
|
||||
|
||||
const toast = useToast();
|
||||
const formRef = ref();
|
||||
|
||||
// 检查登录状态
|
||||
onLoad(() => {
|
||||
@@ -76,14 +71,50 @@ const feedbackTypes = [
|
||||
];
|
||||
|
||||
// 表单数据
|
||||
const feedbackType = ref("bug");
|
||||
const description = ref("");
|
||||
const fileList = ref<any[]>([]);
|
||||
const contact = ref("");
|
||||
const formData = reactive({
|
||||
feedbackType: "bug",
|
||||
description: "",
|
||||
fileList: [] as Array<Record<string, any>>,
|
||||
contact: "",
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules: FormRules = {
|
||||
description: [
|
||||
{
|
||||
required: true,
|
||||
message: "请描述您遇到的问题",
|
||||
validator: (value) => {
|
||||
if (value && value.trim()) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return Promise.reject("请描述您遇到的问题");
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
contact: [
|
||||
{
|
||||
required: false,
|
||||
validator: (value) => {
|
||||
if (!value) return Promise.resolve(); // 非必填
|
||||
const emailReg = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/;
|
||||
const phoneReg = /^1[3456789]\d{9}$/;
|
||||
return emailReg.test(value) || phoneReg.test(value)
|
||||
? Promise.resolve()
|
||||
: Promise.reject("请输入正确的手机号或邮箱");
|
||||
},
|
||||
message: "请输入正确的手机号或邮箱",
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// 提交状态
|
||||
const submitting = ref(false);
|
||||
|
||||
// 图片上传前的校验
|
||||
const beforeRead = (file: any) => {
|
||||
const beforeRead = (file: Record<string, any>) => {
|
||||
// 验证文件类型
|
||||
const validTypes = ["image/jpeg", "image/png", "image/gif"];
|
||||
if (!validTypes.includes(file.type)) {
|
||||
@@ -99,95 +130,59 @@ const beforeRead = (file: any) => {
|
||||
};
|
||||
|
||||
// 删除图片
|
||||
const handleDelete = (detail: any) => {
|
||||
const handleDelete = (detail: { index: number }) => {
|
||||
const index = detail.index;
|
||||
fileList.value.splice(index, 1);
|
||||
};
|
||||
|
||||
// 处理单选框变化
|
||||
const handleRadioChange = (value: string) => {
|
||||
feedbackType.value = value;
|
||||
formData.fileList.splice(index, 1);
|
||||
};
|
||||
|
||||
// 提交反馈
|
||||
const handleSubmit = async () => {
|
||||
// 表单验证
|
||||
if (!description.value.trim()) {
|
||||
toast.error("请描述您遇到的问题");
|
||||
return;
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
// TODO: 调用提交反馈的接口
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500)); // 模拟提交
|
||||
toast.success("提交成功");
|
||||
// 重置表单
|
||||
description.value = "";
|
||||
fileList.value = [];
|
||||
contact.value = "";
|
||||
const { valid } = await formRef.value.validate();
|
||||
|
||||
// 延迟返回上一页
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1500);
|
||||
} catch {
|
||||
toast.error("提交失败,请重试");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
if (valid) {
|
||||
submitting.value = true;
|
||||
try {
|
||||
// TODO: 调用提交反馈的接口
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500)); // 模拟提交
|
||||
toast.success("提交成功");
|
||||
|
||||
// 重置表单
|
||||
formRef.value.reset();
|
||||
formData.feedbackType = "bug";
|
||||
formData.description = "";
|
||||
formData.fileList = [];
|
||||
formData.contact = "";
|
||||
|
||||
// 延迟返回上一页
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1500);
|
||||
} catch (_error) {
|
||||
toast.error("提交失败,请重试");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
} catch (_error) {
|
||||
// 表单验证失败
|
||||
console.log("表单验证失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 返回
|
||||
const handleBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.feedback-container {
|
||||
min-height: 100vh;
|
||||
padding: 20rpx 0;
|
||||
background-color: #f5f5f5;
|
||||
:deep(.wd-form-item) {
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
:deep(.wd-cell-group__title) {
|
||||
padding: 20rpx 30rpx 10rpx;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
padding: 4rpx 0;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.radio-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10rpx 30rpx;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.radio-text {
|
||||
font-size: 22rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.upload-box {
|
||||
padding: 20rpx 30rpx;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
margin: 40rpx 30rpx;
|
||||
}
|
||||
|
||||
:deep(.wd-textarea) {
|
||||
padding: 20rpx 30rpx;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.radio-button {
|
||||
margin-right: -8rpx;
|
||||
}
|
||||
.submit-btn {
|
||||
margin: 40rpx 30rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -15,7 +15,47 @@
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<view class="form-section">
|
||||
<view class="section-title">基本信息</view>
|
||||
<WechatProfile v-model="wechatProfileData" @change="onWechatProfileChange" />
|
||||
|
||||
<!-- 内联WechatProfile组件的内容 -->
|
||||
<view class="wechat-profile">
|
||||
<!-- 头像选择 -->
|
||||
<view class="avatar-section">
|
||||
<view class="section-title">头像</view>
|
||||
<button class="avatar-button" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
|
||||
<image
|
||||
v-if="profileForm.avatar"
|
||||
:src="profileForm.avatar"
|
||||
class="avatar-image"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view v-else class="avatar-placeholder">
|
||||
<wd-icon name="camera" size="40" color="#999" />
|
||||
<text class="placeholder-text">选择头像</text>
|
||||
</view>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 昵称输入 -->
|
||||
<view class="nickname-section">
|
||||
<view class="section-title">昵称</view>
|
||||
<input
|
||||
v-model="profileForm.nickname"
|
||||
type="nickname"
|
||||
class="nickname-input"
|
||||
placeholder="请输入昵称"
|
||||
:maxlength="20"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 性别选择 -->
|
||||
<view class="gender-section">
|
||||
<view class="section-title">性别</view>
|
||||
<wd-radio-group v-model="profileForm.gender" shape="button" class="gender-group">
|
||||
<wd-radio :value="1" class="gender-radio">男</wd-radio>
|
||||
<wd-radio :value="2" class="gender-radio">女</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
|
||||
@@ -120,7 +160,6 @@ import { useToast } from "wot-design-uni";
|
||||
import { useUserStore } from "@/store/modules/user.store";
|
||||
import UserAPI, { type UserProfileForm } from "@/api/user";
|
||||
import FileAPI, { type FileInfo } from "@/api/file";
|
||||
import WechatProfile from "@/components/business/WechatProfile.vue";
|
||||
|
||||
const toast = useToast();
|
||||
const userStore = useUserStore();
|
||||
@@ -136,13 +175,6 @@ const profileForm = reactive<UserProfileForm & { mobile?: string }>({
|
||||
mobile: "",
|
||||
});
|
||||
|
||||
// 微信头像昵称数据
|
||||
const wechatProfileData = ref({
|
||||
avatar: "",
|
||||
nickname: "",
|
||||
gender: 1,
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
nickname: [
|
||||
@@ -179,6 +211,26 @@ onLoad((options: any) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 微信小程序头像选择处理
|
||||
const onChooseAvatar = async (e: any) => {
|
||||
try {
|
||||
const { avatarUrl } = e.detail;
|
||||
|
||||
// 上传头像到服务器
|
||||
uni.showLoading({ title: "上传中..." });
|
||||
|
||||
const fileInfo: FileInfo = await FileAPI.upload(avatarUrl);
|
||||
profileForm.avatar = fileInfo.url;
|
||||
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: "头像上传成功", icon: "success" });
|
||||
} catch (error) {
|
||||
uni.hideLoading();
|
||||
console.error("头像上传失败:", error);
|
||||
uni.showToast({ title: "头像上传失败", icon: "error" });
|
||||
}
|
||||
};
|
||||
|
||||
// 选择头像
|
||||
const chooseAvatar = () => {
|
||||
// #ifdef MP-WEIXIN
|
||||
@@ -331,13 +383,6 @@ const handleSkip = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// 微信头像昵称变化处理
|
||||
const onWechatProfileChange = (data: { avatar?: string; nickname?: string; gender?: number }) => {
|
||||
profileForm.avatar = data.avatar || "";
|
||||
profileForm.nickname = data.nickname || "";
|
||||
profileForm.gender = data.gender || 1;
|
||||
};
|
||||
|
||||
// 返回
|
||||
function handleBack() {
|
||||
uni.navigateBack();
|
||||
@@ -510,4 +555,86 @@ function handleBack() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 内联WechatProfile组件的样式 */
|
||||
.wechat-profile {
|
||||
padding: 20rpx;
|
||||
|
||||
.avatar-section {
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.avatar-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
padding: 0;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--wot-color-bg-light);
|
||||
border: 2rpx dashed var(--wot-color-border);
|
||||
border-radius: 50%;
|
||||
|
||||
.placeholder-text {
|
||||
margin-top: 10rpx;
|
||||
font-size: 24rpx;
|
||||
color: var(--wot-color-text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nickname-section {
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.nickname-input {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 28rpx;
|
||||
background: var(--wot-color-bg-light);
|
||||
border: 1rpx solid var(--wot-color-border);
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.gender-section {
|
||||
.gender-group {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
|
||||
.gender-radio {
|
||||
flex: 1;
|
||||
|
||||
:deep(.wd-radio) {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
15
src/pages/work/index.vue
Normal file
15
src/pages/work/index.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<view class="app-container">
|
||||
<wd-navbar title="设置" left-arrow @click-left="handleBack" />
|
||||
|
||||
<wd-status-tip type="search" tip="建设中..." />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const handleBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
@@ -1,16 +0,0 @@
|
||||
export interface Todo {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
completed: boolean;
|
||||
deadline?: string;
|
||||
priority?: "low" | "medium" | "high";
|
||||
createdAt: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface TodoState {
|
||||
todos: Todo[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
Reference in New Issue
Block a user