feat: 初始化项目基础结构和配置
添加项目基础文件结构、依赖配置和核心功能模块 集成uni-app+Vue3+TypeScript开发环境 配置ESLint、Prettier、Stylelint等代码规范工具 实现主题管理、路由、状态管理等核心功能 添加登录认证、文件上传等常用API模块
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { onLaunch, onShow, onHide } from "@dcloudio/uni-app";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
|
||||
const { initTheme } = useTheme();
|
||||
|
||||
onLaunch(() => {
|
||||
console.log("App Launch");
|
||||
// 初始化主题
|
||||
initTheme();
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
console.log("App Show");
|
||||
});
|
||||
|
||||
onHide(() => {
|
||||
console.log("App Hide");
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
@@ -0,0 +1,110 @@
|
||||
import request, { publicRequest } from "@/utils/request";
|
||||
|
||||
const AUTH_BASE_URL = "/api/v1/auth";
|
||||
|
||||
export interface LoginData {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface WxLoginData {
|
||||
code: string;
|
||||
encryptedData?: string;
|
||||
iv?: string;
|
||||
phoneCode?: string;
|
||||
}
|
||||
|
||||
export interface LoginResult {
|
||||
accessToken: string;
|
||||
refreshToken?: string;
|
||||
tokenType: string;
|
||||
expiresIn: number;
|
||||
isNewUser?: boolean;
|
||||
isProfileComplete?: boolean;
|
||||
}
|
||||
|
||||
const AuthAPI = {
|
||||
/**
|
||||
* 账号密码登录
|
||||
* @param data 登录表单数据
|
||||
* @returns 登录结果
|
||||
*/
|
||||
login(data: LoginData): Promise<LoginResult> {
|
||||
const formData = {
|
||||
username: data.username,
|
||||
password: data.password,
|
||||
};
|
||||
|
||||
return publicRequest<LoginResult>({
|
||||
url: `${AUTH_BASE_URL}/login`,
|
||||
method: "POST",
|
||||
data: formData,
|
||||
header: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 微信小程序授权登录 (仅使用code获取OpenID)
|
||||
* @param code 微信登录凭证
|
||||
* @returns 登录结果
|
||||
*/
|
||||
loginByWxMiniAppCode(code: string): Promise<LoginResult> {
|
||||
return publicRequest<LoginResult>({
|
||||
url: `${AUTH_BASE_URL}/wx/miniapp/code-login`,
|
||||
method: "POST",
|
||||
data: { code },
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 微信小程序手机号授权登录
|
||||
* @param data 包含code、encryptedData、iv等手机号相关数据
|
||||
* @returns 登录结果
|
||||
*/
|
||||
loginByWxMiniAppPhone(data: WxLoginData): Promise<LoginResult> {
|
||||
return publicRequest<LoginResult>({
|
||||
url: `${AUTH_BASE_URL}/wx/miniapp/phone-login`,
|
||||
method: "POST",
|
||||
data,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查会话有效性
|
||||
* @returns 会话是否有效
|
||||
*/
|
||||
checkSession(): Promise<{ valid: boolean }> {
|
||||
return request<{ valid: boolean }>({
|
||||
url: `${AUTH_BASE_URL}/check-session`,
|
||||
method: "GET",
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 登出
|
||||
* @returns 登出结果
|
||||
*/
|
||||
logout(): Promise<any> {
|
||||
return request<any>({
|
||||
url: `${AUTH_BASE_URL}/logout`,
|
||||
method: "POST",
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 刷新令牌
|
||||
* @param refreshToken 刷新令牌
|
||||
* @returns 新的访问令牌
|
||||
*/
|
||||
refreshToken(refreshToken: string): Promise<{ accessToken: string; expiresIn: number }> {
|
||||
return publicRequest<{ accessToken: string; expiresIn: number }>({
|
||||
url: `${AUTH_BASE_URL}/refresh-token`,
|
||||
method: "POST",
|
||||
data: { refreshToken },
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default AuthAPI;
|
||||
@@ -0,0 +1,75 @@
|
||||
import { getToken } from "@/utils/storage";
|
||||
import { ApiCode } from "@/enums/api-code.enum";
|
||||
|
||||
// H5 使用 VITE_APP_BASE_API 作为代理路径,其他平台使用 VITE_APP_API_URL 作为请求路径
|
||||
let baseApi = import.meta.env.VITE_APP_API_URL;
|
||||
// #ifdef H5
|
||||
baseApi = import.meta.env.VITE_APP_BASE_API;
|
||||
// #endif
|
||||
|
||||
const FileAPI = {
|
||||
/**
|
||||
* 文件上传地址
|
||||
*/
|
||||
uploadUrl: baseApi + "/api/v1/files",
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*
|
||||
* @param filePath
|
||||
*/
|
||||
upload(filePath: string): Promise<FileInfo> {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.uploadFile({
|
||||
url: this.uploadUrl,
|
||||
filePath: filePath,
|
||||
name: "file",
|
||||
header: {
|
||||
Authorization: getToken() ? `Bearer ${getToken()}` : "",
|
||||
},
|
||||
formData: {},
|
||||
success: (response) => {
|
||||
const resData = JSON.parse(response.data) as ResponseData<FileInfo>;
|
||||
// 业务状态码 00000 表示成功
|
||||
if (resData.code === ApiCode.SUCCESS) {
|
||||
resolve(resData.data);
|
||||
} else {
|
||||
// 其他业务处理失败
|
||||
uni.showToast({
|
||||
title: resData.msg || "文件上传失败",
|
||||
icon: "none",
|
||||
});
|
||||
reject({
|
||||
message: resData.msg || "业务处理失败",
|
||||
code: resData.code,
|
||||
});
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
console.log("fail error", error);
|
||||
uni.showToast({
|
||||
title: "文件上传请求失败",
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
reject({
|
||||
message: "文件上传请求失败",
|
||||
error,
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default FileAPI;
|
||||
|
||||
/**
|
||||
* 文件API类型声明
|
||||
*/
|
||||
export interface FileInfo {
|
||||
/** 文件名 */
|
||||
name: string;
|
||||
/** 文件路径 */
|
||||
url: string;
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
const USER_BASE_URL = "/api/v1/users";
|
||||
|
||||
const UserAPI = {
|
||||
/**
|
||||
* 获取当前登录用户信息
|
||||
*
|
||||
* @returns 登录用户昵称、头像信息,包括角色和权限
|
||||
*/
|
||||
getUserInfo(): Promise<UserInfo> {
|
||||
return request<UserInfo>({
|
||||
url: `${USER_BASE_URL}/me`,
|
||||
method: "GET",
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取用户分页列表
|
||||
*
|
||||
* @param queryParams 查询参数
|
||||
*/
|
||||
getPage(queryParams: UserPageQuery) {
|
||||
return request<PageResult<UserPageVO[]>>({
|
||||
url: `${USER_BASE_URL}/page`,
|
||||
method: "GET",
|
||||
data: queryParams,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 添加用户
|
||||
*
|
||||
* @param data 用户表单数据
|
||||
*/
|
||||
add(data: UserForm) {
|
||||
return request({
|
||||
url: `${USER_BASE_URL}`,
|
||||
method: "POST",
|
||||
data: data,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取用户表单详情
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @returns 用户表单详情
|
||||
*/
|
||||
getFormData(userId: number) {
|
||||
return request<UserForm>({
|
||||
url: `${USER_BASE_URL}/${userId}/form`,
|
||||
method: "GET",
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 修改用户
|
||||
*
|
||||
* @param id 用户ID
|
||||
* @param data 用户表单数据
|
||||
*/
|
||||
update(id: number, data: UserForm) {
|
||||
return request({
|
||||
url: `${USER_BASE_URL}/${id}`,
|
||||
method: "PUT",
|
||||
data: data,
|
||||
});
|
||||
},
|
||||
|
||||
/** 获取个人中心用户信息 */
|
||||
getProfile() {
|
||||
return request<UserProfileVO>({
|
||||
url: `${USER_BASE_URL}/profile`,
|
||||
method: "GET",
|
||||
});
|
||||
},
|
||||
|
||||
/** 修改个人中心用户信息 */
|
||||
updateProfile(data: UserProfileForm) {
|
||||
return request({
|
||||
url: `${USER_BASE_URL}/profile`,
|
||||
method: "PUT",
|
||||
data: data,
|
||||
});
|
||||
},
|
||||
|
||||
/** 修改个人中心用户密码 */
|
||||
changePassword(data: PasswordChangeForm) {
|
||||
return request({
|
||||
url: `${USER_BASE_URL}/password`,
|
||||
method: "PUT",
|
||||
data: data,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 发送手机/邮箱验证码
|
||||
*
|
||||
* @param contact 联系方式 手机号/邮箱
|
||||
* @param contactType 联系方式类型 MOBILE:手机;EMAIL:邮箱
|
||||
*/
|
||||
sendVerificationCode(contact: string, contactType: string) {
|
||||
return request({
|
||||
url: `${USER_BASE_URL}/send-verification-code?contact=${contact}&contactType=${contactType}`,
|
||||
method: "POST",
|
||||
});
|
||||
},
|
||||
|
||||
/** 绑定个人中心用户手机 */
|
||||
bindMobile(data: MobileBindingForm) {
|
||||
return request({
|
||||
url: `${USER_BASE_URL}/mobile`,
|
||||
method: "PUT",
|
||||
data: data,
|
||||
});
|
||||
},
|
||||
|
||||
/** 绑定个人中心用户邮箱 */
|
||||
bindEmail(data: EmailBindingForm) {
|
||||
return request({
|
||||
url: `${USER_BASE_URL}/email`,
|
||||
method: "PUT",
|
||||
data: data,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 批量删除用户,多个以英文逗号(,)分割
|
||||
*
|
||||
* @param ids 用户ID字符串,多个以英文逗号(,)分割
|
||||
*/
|
||||
deleteByIds(ids: string) {
|
||||
return request({
|
||||
url: `${USER_BASE_URL}/${ids}`,
|
||||
method: "DELETE",
|
||||
});
|
||||
},
|
||||
|
||||
/** 获取微信手机号 */
|
||||
getPhoneNumber(data: WechatPhoneData): Promise<PhoneNumberResult> {
|
||||
return request<PhoneNumberResult>({
|
||||
url: `${USER_BASE_URL}/wechat-phone`,
|
||||
method: "POST",
|
||||
data: data,
|
||||
});
|
||||
},
|
||||
};
|
||||
export default UserAPI;
|
||||
|
||||
/** 登录用户信息 */
|
||||
export interface UserInfo {
|
||||
/** 用户ID */
|
||||
userId?: number;
|
||||
|
||||
/** 用户名 */
|
||||
username?: string;
|
||||
|
||||
/** 昵称 */
|
||||
nickname?: string;
|
||||
|
||||
/** 头像URL */
|
||||
avatar?: string;
|
||||
|
||||
/** 角色 */
|
||||
roles?: string[];
|
||||
|
||||
/** 权限 */
|
||||
perms?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户分页查询对象
|
||||
*/
|
||||
export interface UserPageQuery extends PageQuery {
|
||||
/** 搜索关键字 */
|
||||
keywords?: string;
|
||||
|
||||
/** 用户状态 */
|
||||
status?: number;
|
||||
|
||||
/** 部门ID */
|
||||
deptId?: number;
|
||||
|
||||
/** 开始时间 */
|
||||
createTime?: [string, string] | string;
|
||||
|
||||
/** 排序字段 */
|
||||
field?: string;
|
||||
|
||||
/** 排序方式(asc:正序,desc:倒序) */
|
||||
direction?: string;
|
||||
}
|
||||
|
||||
/** 用户分页对象 */
|
||||
export interface UserPageVO {
|
||||
/** 用户头像URL */
|
||||
avatar?: string;
|
||||
/** 创建时间 */
|
||||
createTime?: string;
|
||||
/** 部门名称 */
|
||||
deptName?: string;
|
||||
/** 用户邮箱 */
|
||||
email?: string;
|
||||
/** 性别 */
|
||||
gender?: number;
|
||||
/** 用户ID */
|
||||
id: number;
|
||||
/** 手机号 */
|
||||
mobile?: string;
|
||||
/** 用户昵称 */
|
||||
nickname?: string;
|
||||
/** 角色名称,多个使用英文逗号(,)分割 */
|
||||
roleNames?: string;
|
||||
/** 用户状态(1:启用;0:禁用) */
|
||||
status?: number;
|
||||
/** 用户名 */
|
||||
username?: string;
|
||||
}
|
||||
|
||||
/** 个人中心用户信息 */
|
||||
export interface UserProfileVO {
|
||||
/** 用户ID */
|
||||
id?: number;
|
||||
|
||||
/** 用户名 */
|
||||
username?: string;
|
||||
|
||||
/** 昵称 */
|
||||
nickname?: string;
|
||||
|
||||
/** 头像URL */
|
||||
avatar?: string;
|
||||
|
||||
/** 性别 */
|
||||
gender?: number;
|
||||
|
||||
/** 手机号 */
|
||||
mobile?: string;
|
||||
|
||||
/** 邮箱 */
|
||||
email?: string;
|
||||
|
||||
/** 部门名称 */
|
||||
deptName?: string;
|
||||
|
||||
/** 角色名称,多个使用英文逗号(,)分割 */
|
||||
roleNames?: string;
|
||||
|
||||
/** 创建时间 */
|
||||
createTime?: string;
|
||||
}
|
||||
|
||||
/** 个人中心用户信息表单 */
|
||||
export interface UserProfileForm {
|
||||
/** 用户ID */
|
||||
id?: number;
|
||||
|
||||
/** 用户名 */
|
||||
username?: string;
|
||||
|
||||
/** 昵称 */
|
||||
nickname?: string;
|
||||
|
||||
/** 头像URL */
|
||||
avatar?: string;
|
||||
|
||||
/** 性别 */
|
||||
gender?: number;
|
||||
|
||||
/** 手机号 */
|
||||
mobile?: string;
|
||||
|
||||
/** 邮箱 */
|
||||
email?: string;
|
||||
}
|
||||
|
||||
/** 修改密码表单 */
|
||||
export interface PasswordChangeForm {
|
||||
/** 原密码 */
|
||||
oldPassword?: string;
|
||||
/** 新密码 */
|
||||
newPassword?: string;
|
||||
/** 确认新密码 */
|
||||
confirmPassword?: string;
|
||||
}
|
||||
|
||||
/** 修改手机表单 */
|
||||
export interface MobileBindingForm {
|
||||
/** 手机号 */
|
||||
mobile?: string;
|
||||
/** 验证码 */
|
||||
code?: string;
|
||||
}
|
||||
|
||||
/** 修改邮箱表单 */
|
||||
export interface EmailBindingForm {
|
||||
/** 邮箱 */
|
||||
email?: string;
|
||||
/** 验证码 */
|
||||
code?: string;
|
||||
}
|
||||
|
||||
/** 用户表单 */
|
||||
export interface UserForm {
|
||||
/** 用户头像 */
|
||||
avatar?: string;
|
||||
/** 部门ID */
|
||||
deptId?: number;
|
||||
/** 用户邮箱 */
|
||||
email?: string;
|
||||
/** 性别 */
|
||||
gender?: number;
|
||||
/** 用户ID */
|
||||
id?: number;
|
||||
/** 手机号 */
|
||||
mobile?: string;
|
||||
/** 昵称 */
|
||||
nickname?: string;
|
||||
/** 角色ID集合 */
|
||||
roleIds: number[];
|
||||
/** 用户状态(1:正常;0:禁用) */
|
||||
status?: number;
|
||||
/** 用户名 */
|
||||
username?: string;
|
||||
}
|
||||
|
||||
/** 微信手机号授权数据 */
|
||||
export interface WechatPhoneData {
|
||||
/** 微信授权码 */
|
||||
code: string;
|
||||
/** 加密数据 */
|
||||
encryptedData?: string;
|
||||
/** 初始向量 */
|
||||
iv?: string;
|
||||
}
|
||||
|
||||
/** 手机号获取结果 */
|
||||
export interface PhoneNumberResult {
|
||||
/** 手机号 */
|
||||
phoneNumber: string;
|
||||
/** 纯手机号(去除+86) */
|
||||
purePhoneNumber?: string;
|
||||
/** 国家代码 */
|
||||
countryCode?: string;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<wd-calendar
|
||||
v-model="dateRange"
|
||||
:label="label"
|
||||
type="daterange"
|
||||
:placeholder="placeholder"
|
||||
@confirm="handleConfirm"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { dayjs } from "wot-design-uni";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [Array, String] as PropType<[string, string] | string | undefined>,
|
||||
default: () => undefined,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "请选择时间范围",
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const dateRange = ref<number[] | number | null>(null);
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (Array.isArray(val) && val.length === 2 && val[0] && val[1]) {
|
||||
dateRange.value = val.map((item) => new Date(item).getTime());
|
||||
} else if (typeof val === "string" && val.includes(",")) {
|
||||
const [startDate, endDate] = val.split(",");
|
||||
|
||||
if (
|
||||
startDate &&
|
||||
endDate &&
|
||||
!isNaN(new Date(startDate).getTime()) &&
|
||||
!isNaN(new Date(endDate).getTime())
|
||||
) {
|
||||
dateRange.value = [new Date(startDate).getTime(), new Date(endDate).getTime()];
|
||||
} else {
|
||||
dateRange.value = null;
|
||||
}
|
||||
} else {
|
||||
dateRange.value = null;
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
// 确认选择时间
|
||||
const handleConfirm = () => {
|
||||
if (Array.isArray(dateRange.value) && dateRange.value.length === 2) {
|
||||
const startDate = dayjs(dateRange.value[0]).format("YYYY-MM-DD");
|
||||
const endDate = dayjs(dateRange.value[1]).format("YYYY-MM-DD");
|
||||
|
||||
let newVal: any = [startDate, endDate];
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
newVal = `${startDate},${endDate}`;
|
||||
// #endif
|
||||
|
||||
console.log("newVal", newVal);
|
||||
emit("update:modelValue", newVal);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.time-filter {
|
||||
padding: 16rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<wd-picker
|
||||
v-model="selectedValues"
|
||||
:columns="pickerColumns"
|
||||
:display-format="displayFormat"
|
||||
:column-change="handleColumnChange"
|
||||
:label="label"
|
||||
:placeholder="pickerColumns.length ? '请选择' : '加载中...'"
|
||||
:required="required"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PickerViewInstance } from "wot-design-uni/components/wd-picker-view/types";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [Number, String],
|
||||
},
|
||||
data: {
|
||||
required: true,
|
||||
type: Array as () => OptionType[],
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(["update:modelValue"]);
|
||||
|
||||
// 定义响应式变量
|
||||
const selectedValues = ref<number[] | string[]>([]);
|
||||
const pickerColumns = ref<Array<Array<{ label: string; value: string | number }>>>([]);
|
||||
|
||||
// 监听 modelValue 的变化,更新 selectedValues
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
selectedValues.value = val ? findTreePath(val) : [];
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 根据节点值查找路径
|
||||
* 示例数据: [{"value":"1","label":"公司","children":[{"value":"2","label":"研发部"}]}]
|
||||
* 查找部门ID为2的路径,返回结果:[1, 2]
|
||||
*/
|
||||
const findTreePath = (value: number | string): number[] | string[] => {
|
||||
const numberPath: number[] = [];
|
||||
const stringPath: string[] = [];
|
||||
const list = props.data;
|
||||
|
||||
const find = (value: number | string, list: OptionType[]): boolean => {
|
||||
for (const item of list) {
|
||||
if (item.value === value) {
|
||||
typeof value === "number" ? numberPath.push(value) : stringPath.push(value);
|
||||
return true;
|
||||
}
|
||||
if (item.children?.length) {
|
||||
typeof item.value === "number" ? numberPath.push(item.value) : stringPath.push(item.value);
|
||||
if (find(value, item.children)) return true;
|
||||
typeof item.value === "number" ? numberPath.pop() : stringPath.pop();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
find(value, list);
|
||||
return typeof value === "number" ? numberPath : stringPath;
|
||||
};
|
||||
|
||||
/**
|
||||
* 将树形数据转换为 Picker 所需的 columns 格式
|
||||
*
|
||||
* @param treeData 树形数据
|
||||
* @returns Picker 所需的 columns 格式
|
||||
*/
|
||||
const transformTreeToColumns = (
|
||||
treeData: OptionType[]
|
||||
): Array<Array<{ label: string; value: string | number }>> => {
|
||||
const columns: Array<Array<{ label: string; value: string | number }>> = [];
|
||||
|
||||
for (let depth = 0; depth <= selectedValues.value.length; depth++) {
|
||||
const currentColumn = treeData.map((node) => ({ label: node.label, value: node.value }));
|
||||
if (!currentColumn.length) break;
|
||||
|
||||
const selectedId = selectedValues.value[depth];
|
||||
if (!currentColumn.some((item) => item.value === selectedId)) {
|
||||
selectedValues.value[depth] = currentColumn[0]?.value;
|
||||
}
|
||||
|
||||
columns.push(currentColumn);
|
||||
const selectedNode = treeData.find((node) => node.value == selectedValues.value[depth]);
|
||||
treeData = selectedNode?.children || [];
|
||||
}
|
||||
|
||||
return columns;
|
||||
};
|
||||
|
||||
// 监听 data 的变化,更新 pickerColumns
|
||||
watch(
|
||||
() => props.data,
|
||||
(val) => {
|
||||
console.log("监听 data 的变化", val);
|
||||
pickerColumns.value = transformTreeToColumns(val);
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 处理列的变化,动态更新后续列的数据
|
||||
*/
|
||||
function handleColumnChange(
|
||||
pickerView: PickerViewInstance,
|
||||
value: Record<string, any> | Record<string, any>[],
|
||||
columnIndex: number,
|
||||
resolve: () => void
|
||||
) {
|
||||
const selectedValue = selectedValues.value[selectedValues.value.length - 1] || undefined;
|
||||
emits("update:modelValue", selectedValue);
|
||||
|
||||
const item = Array.isArray(value) ? value[columnIndex] : value.value;
|
||||
updatePickerColumns(pickerView, item.value, columnIndex);
|
||||
resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态更新所有后续列的数据
|
||||
*/
|
||||
function updatePickerColumns(
|
||||
pickerView: PickerViewInstance,
|
||||
parentId: string | number,
|
||||
columnIndex: number
|
||||
) {
|
||||
const nextColumnIndex = columnIndex + 1;
|
||||
const children = findChildren(parentId, props.data);
|
||||
|
||||
if (children.length > 0 && nextColumnIndex < 3) {
|
||||
pickerView.setColumnData(nextColumnIndex, children);
|
||||
updatePickerColumns(pickerView, children[0].value, nextColumnIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据节点value查找其子节点数据
|
||||
*/
|
||||
function findChildren(
|
||||
parentId: string | number,
|
||||
list: Record<string, any>[]
|
||||
): Record<string, any>[] {
|
||||
for (const item of list) {
|
||||
if (item.value === parentId && item.children) {
|
||||
return item.children;
|
||||
}
|
||||
if (item.children) {
|
||||
const children = findChildren(parentId, item.children);
|
||||
if (children.length) return children;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// 格式化显示选中项(显示最后一个子节点的 label)
|
||||
const displayFormat = (items: any) => {
|
||||
return items.length > 0 ? items[items.length - 1].label : "";
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<view class="container loading1">
|
||||
<view class="shape shape1" />
|
||||
<view class="shape shape2" />
|
||||
<view class="shape shape3" />
|
||||
<view class="shape shape4" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "loading1",
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped="true">
|
||||
.container {
|
||||
position: relative;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
.container.loading1 {
|
||||
-webkit-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.container .shape {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 1px;
|
||||
}
|
||||
.container .shape.shape1 {
|
||||
left: 0;
|
||||
background-color: #1890ff;
|
||||
}
|
||||
.container .shape.shape2 {
|
||||
right: 0;
|
||||
background-color: #91cb74;
|
||||
}
|
||||
.container .shape.shape3 {
|
||||
bottom: 0;
|
||||
background-color: #fac858;
|
||||
}
|
||||
.container .shape.shape4 {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ee6666;
|
||||
}
|
||||
|
||||
.loading1 .shape1 {
|
||||
-webkit-animation: animation1shape1 0.5s ease 0s infinite alternate;
|
||||
animation: animation1shape1 0.5s ease 0s infinite alternate;
|
||||
}
|
||||
|
||||
@-webkit-keyframes animation1shape1 {
|
||||
from {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: translate(16px, 16px);
|
||||
transform: translate(16px, 16px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animation1shape1 {
|
||||
from {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: translate(16px, 16px);
|
||||
transform: translate(16px, 16px);
|
||||
}
|
||||
}
|
||||
.loading1 .shape2 {
|
||||
-webkit-animation: animation1shape2 0.5s ease 0s infinite alternate;
|
||||
animation: animation1shape2 0.5s ease 0s infinite alternate;
|
||||
}
|
||||
|
||||
@-webkit-keyframes animation1shape2 {
|
||||
from {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: translate(-16px, 16px);
|
||||
transform: translate(-16px, 16px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animation1shape2 {
|
||||
from {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: translate(-16px, 16px);
|
||||
transform: translate(-16px, 16px);
|
||||
}
|
||||
}
|
||||
.loading1 .shape3 {
|
||||
-webkit-animation: animation1shape3 0.5s ease 0s infinite alternate;
|
||||
animation: animation1shape3 0.5s ease 0s infinite alternate;
|
||||
}
|
||||
|
||||
@-webkit-keyframes animation1shape3 {
|
||||
from {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: translate(16px, -16px);
|
||||
transform: translate(16px, -16px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animation1shape3 {
|
||||
from {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: translate(16px, -16px);
|
||||
transform: translate(16px, -16px);
|
||||
}
|
||||
}
|
||||
.loading1 .shape4 {
|
||||
-webkit-animation: animation1shape4 0.5s ease 0s infinite alternate;
|
||||
animation: animation1shape4 0.5s ease 0s infinite alternate;
|
||||
}
|
||||
|
||||
@-webkit-keyframes animation1shape4 {
|
||||
from {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: translate(-16px, -16px);
|
||||
transform: translate(-16px, -16px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animation1shape4 {
|
||||
from {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: translate(-16px, -16px);
|
||||
transform: translate(-16px, -16px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<view class="container loading2">
|
||||
<view class="shape shape1" />
|
||||
<view class="shape shape2" />
|
||||
<view class="shape shape3" />
|
||||
<view class="shape shape4" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "loading2",
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped="true">
|
||||
.container {
|
||||
position: relative;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.container.loading2 {
|
||||
-webkit-transform: rotate(10deg);
|
||||
transform: rotate(10deg);
|
||||
-webkit-animation: rotation 1s infinite;
|
||||
animation: rotation 1s infinite;
|
||||
}
|
||||
.container.loading2 .shape {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.container .shape {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 1px;
|
||||
}
|
||||
.container .shape.shape1 {
|
||||
left: 0;
|
||||
background-color: #1890ff;
|
||||
}
|
||||
.container .shape.shape2 {
|
||||
right: 0;
|
||||
background-color: #91cb74;
|
||||
}
|
||||
.container .shape.shape3 {
|
||||
bottom: 0;
|
||||
background-color: #fac858;
|
||||
}
|
||||
.container .shape.shape4 {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ee6666;
|
||||
}
|
||||
|
||||
.loading2 .shape1 {
|
||||
-webkit-animation: animation2shape1 0.5s ease 0s infinite alternate;
|
||||
animation: animation2shape1 0.5s ease 0s infinite alternate;
|
||||
}
|
||||
|
||||
@-webkit-keyframes animation2shape1 {
|
||||
from {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: translate(20px, 20px);
|
||||
transform: translate(20px, 20px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animation2shape1 {
|
||||
from {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: translate(20px, 20px);
|
||||
transform: translate(20px, 20px);
|
||||
}
|
||||
}
|
||||
.loading2 .shape2 {
|
||||
-webkit-animation: animation2shape2 0.5s ease 0s infinite alternate;
|
||||
animation: animation2shape2 0.5s ease 0s infinite alternate;
|
||||
}
|
||||
|
||||
@-webkit-keyframes animation2shape2 {
|
||||
from {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: translate(-20px, 20px);
|
||||
transform: translate(-20px, 20px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animation2shape2 {
|
||||
from {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: translate(-20px, 20px);
|
||||
transform: translate(-20px, 20px);
|
||||
}
|
||||
}
|
||||
.loading2 .shape3 {
|
||||
-webkit-animation: animation2shape3 0.5s ease 0s infinite alternate;
|
||||
animation: animation2shape3 0.5s ease 0s infinite alternate;
|
||||
}
|
||||
|
||||
@-webkit-keyframes animation2shape3 {
|
||||
from {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: translate(20px, -20px);
|
||||
transform: translate(20px, -20px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animation2shape3 {
|
||||
from {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: translate(20px, -20px);
|
||||
transform: translate(20px, -20px);
|
||||
}
|
||||
}
|
||||
.loading2 .shape4 {
|
||||
-webkit-animation: animation2shape4 0.5s ease 0s infinite alternate;
|
||||
animation: animation2shape4 0.5s ease 0s infinite alternate;
|
||||
}
|
||||
|
||||
@-webkit-keyframes animation2shape4 {
|
||||
from {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: translate(-20px, -20px);
|
||||
transform: translate(-20px, -20px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animation2shape4 {
|
||||
from {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: translate(-20px, -20px);
|
||||
transform: translate(-20px, -20px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<view class="container loading3">
|
||||
<view class="shape shape1" />
|
||||
<view class="shape shape2" />
|
||||
<view class="shape shape3" />
|
||||
<view class="shape shape4" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "loading3",
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped="true">
|
||||
.container {
|
||||
position: relative;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.container.loading3 {
|
||||
-webkit-animation: rotation 1s infinite;
|
||||
animation: rotation 1s infinite;
|
||||
}
|
||||
.container.loading3 .shape1 {
|
||||
border-top-left-radius: 10px;
|
||||
}
|
||||
.container.loading3 .shape2 {
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
.container.loading3 .shape3 {
|
||||
border-bottom-left-radius: 10px;
|
||||
}
|
||||
.container.loading3 .shape4 {
|
||||
border-bottom-right-radius: 10px;
|
||||
}
|
||||
|
||||
.container .shape {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 1px;
|
||||
}
|
||||
.container .shape.shape1 {
|
||||
left: 0;
|
||||
background-color: #1890ff;
|
||||
}
|
||||
.container .shape.shape2 {
|
||||
right: 0;
|
||||
background-color: #91cb74;
|
||||
}
|
||||
.container .shape.shape3 {
|
||||
bottom: 0;
|
||||
background-color: #fac858;
|
||||
}
|
||||
.container .shape.shape4 {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ee6666;
|
||||
}
|
||||
|
||||
.loading3 .shape1 {
|
||||
-webkit-animation: animation3shape1 0.5s ease 0s infinite alternate;
|
||||
animation: animation3shape1 0.5s ease 0s infinite alternate;
|
||||
}
|
||||
|
||||
@-webkit-keyframes animation3shape1 {
|
||||
from {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: translate(5px, 5px);
|
||||
transform: translate(5px, 5px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animation3shape1 {
|
||||
from {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: translate(5px, 5px);
|
||||
transform: translate(5px, 5px);
|
||||
}
|
||||
}
|
||||
.loading3 .shape2 {
|
||||
-webkit-animation: animation3shape2 0.5s ease 0s infinite alternate;
|
||||
animation: animation3shape2 0.5s ease 0s infinite alternate;
|
||||
}
|
||||
|
||||
@-webkit-keyframes animation3shape2 {
|
||||
from {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: translate(-5px, 5px);
|
||||
transform: translate(-5px, 5px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animation3shape2 {
|
||||
from {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: translate(-5px, 5px);
|
||||
transform: translate(-5px, 5px);
|
||||
}
|
||||
}
|
||||
.loading3 .shape3 {
|
||||
-webkit-animation: animation3shape3 0.5s ease 0s infinite alternate;
|
||||
animation: animation3shape3 0.5s ease 0s infinite alternate;
|
||||
}
|
||||
|
||||
@-webkit-keyframes animation3shape3 {
|
||||
from {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: translate(5px, -5px);
|
||||
transform: translate(5px, -5px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animation3shape3 {
|
||||
from {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: translate(5px, -5px);
|
||||
transform: translate(5px, -5px);
|
||||
}
|
||||
}
|
||||
.loading3 .shape4 {
|
||||
-webkit-animation: animation3shape4 0.5s ease 0s infinite alternate;
|
||||
animation: animation3shape4 0.5s ease 0s infinite alternate;
|
||||
}
|
||||
|
||||
@-webkit-keyframes animation3shape4 {
|
||||
from {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: translate(-5px, -5px);
|
||||
transform: translate(-5px, -5px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animation3shape4 {
|
||||
from {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: translate(-5px, -5px);
|
||||
transform: translate(-5px, -5px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,219 @@
|
||||
<template>
|
||||
<view class="container loading5">
|
||||
<view class="shape shape1" />
|
||||
<view class="shape shape2" />
|
||||
<view class="shape shape3" />
|
||||
<view class="shape shape4" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "loading5",
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped="true">
|
||||
.container {
|
||||
position: relative;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.container.loading5 .shape {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.container .shape {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 1px;
|
||||
}
|
||||
.container .shape.shape1 {
|
||||
left: 0;
|
||||
background-color: #1890ff;
|
||||
}
|
||||
.container .shape.shape2 {
|
||||
right: 0;
|
||||
background-color: #91cb74;
|
||||
}
|
||||
.container .shape.shape3 {
|
||||
bottom: 0;
|
||||
background-color: #fac858;
|
||||
}
|
||||
.container .shape.shape4 {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ee6666;
|
||||
}
|
||||
|
||||
.loading5 .shape1 {
|
||||
animation: animation5shape1 2s ease 0s infinite reverse;
|
||||
}
|
||||
|
||||
@-webkit-keyframes animation5shape1 {
|
||||
0% {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
25% {
|
||||
-webkit-transform: translate(0, 15px);
|
||||
transform: translate(0, 15px);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: translate(15px, 15px);
|
||||
transform: translate(15px, 15px);
|
||||
}
|
||||
75% {
|
||||
-webkit-transform: translate(15px, 0);
|
||||
transform: translate(15px, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animation5shape1 {
|
||||
0% {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
25% {
|
||||
-webkit-transform: translate(0, 15px);
|
||||
transform: translate(0, 15px);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: translate(15px, 15px);
|
||||
transform: translate(15px, 15px);
|
||||
}
|
||||
75% {
|
||||
-webkit-transform: translate(15px, 0);
|
||||
transform: translate(15px, 0);
|
||||
}
|
||||
}
|
||||
.loading5 .shape2 {
|
||||
animation: animation5shape2 2s ease 0s infinite reverse;
|
||||
}
|
||||
|
||||
@-webkit-keyframes animation5shape2 {
|
||||
0% {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
25% {
|
||||
-webkit-transform: translate(-15px, 0);
|
||||
transform: translate(-15px, 0);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: translate(-15px, 15px);
|
||||
transform: translate(-15px, 15px);
|
||||
}
|
||||
75% {
|
||||
-webkit-transform: translate(0, 15px);
|
||||
transform: translate(0, 15px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animation5shape2 {
|
||||
0% {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
25% {
|
||||
-webkit-transform: translate(-15px, 0);
|
||||
transform: translate(-15px, 0);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: translate(-15px, 15px);
|
||||
transform: translate(-15px, 15px);
|
||||
}
|
||||
75% {
|
||||
-webkit-transform: translate(0, 15px);
|
||||
transform: translate(0, 15px);
|
||||
}
|
||||
}
|
||||
.loading5 .shape3 {
|
||||
animation: animation5shape3 2s ease 0s infinite reverse;
|
||||
}
|
||||
|
||||
@-webkit-keyframes animation5shape3 {
|
||||
0% {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
25% {
|
||||
-webkit-transform: translate(15px, 0);
|
||||
transform: translate(15px, 0);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: translate(15px, -15px);
|
||||
transform: translate(15px, -15px);
|
||||
}
|
||||
75% {
|
||||
-webkit-transform: translate(0, -15px);
|
||||
transform: translate(0, -15px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animation5shape3 {
|
||||
0% {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
25% {
|
||||
-webkit-transform: translate(15px, 0);
|
||||
transform: translate(15px, 0);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: translate(15px, -15px);
|
||||
transform: translate(15px, -15px);
|
||||
}
|
||||
75% {
|
||||
-webkit-transform: translate(0, -15px);
|
||||
transform: translate(0, -15px);
|
||||
}
|
||||
}
|
||||
.loading5 .shape4 {
|
||||
animation: animation5shape4 2s ease 0s infinite reverse;
|
||||
}
|
||||
|
||||
@-webkit-keyframes animation5shape4 {
|
||||
0% {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
25% {
|
||||
-webkit-transform: translate(0, -15px);
|
||||
transform: translate(0, -15px);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: translate(-15px, -15px);
|
||||
transform: translate(-15px, -15px);
|
||||
}
|
||||
75% {
|
||||
-webkit-transform: translate(-15px, 0);
|
||||
transform: translate(-15px, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animation5shape4 {
|
||||
0% {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
25% {
|
||||
-webkit-transform: translate(0, -15px);
|
||||
transform: translate(0, -15px);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: translate(-15px, -15px);
|
||||
transform: translate(-15px, -15px);
|
||||
}
|
||||
75% {
|
||||
-webkit-transform: translate(-15px, 0);
|
||||
transform: translate(-15px, 0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,226 @@
|
||||
<template>
|
||||
<view class="container loading6">
|
||||
<view class="shape shape1" />
|
||||
<view class="shape shape2" />
|
||||
<view class="shape shape3" />
|
||||
<view class="shape shape4" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "loading6",
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped="true">
|
||||
.container {
|
||||
position: relative;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.container.loading6 {
|
||||
-webkit-animation: rotation 1s infinite;
|
||||
animation: rotation 1s infinite;
|
||||
}
|
||||
.container.loading6 .shape {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.container .shape {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 1px;
|
||||
}
|
||||
.container .shape.shape1 {
|
||||
left: 0;
|
||||
background-color: #1890ff;
|
||||
}
|
||||
.container .shape.shape2 {
|
||||
right: 0;
|
||||
background-color: #91cb74;
|
||||
}
|
||||
.container .shape.shape3 {
|
||||
bottom: 0;
|
||||
background-color: #fac858;
|
||||
}
|
||||
.container .shape.shape4 {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ee6666;
|
||||
}
|
||||
|
||||
.loading6 .shape1 {
|
||||
-webkit-animation: animation6shape1 2s linear 0s infinite normal;
|
||||
animation: animation6shape1 2s linear 0s infinite normal;
|
||||
}
|
||||
|
||||
@-webkit-keyframes animation6shape1 {
|
||||
0% {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
25% {
|
||||
-webkit-transform: translate(0, 18px);
|
||||
transform: translate(0, 18px);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: translate(18px, 18px);
|
||||
transform: translate(18px, 18px);
|
||||
}
|
||||
75% {
|
||||
-webkit-transform: translate(18px, 0);
|
||||
transform: translate(18px, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animation6shape1 {
|
||||
0% {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
25% {
|
||||
-webkit-transform: translate(0, 18px);
|
||||
transform: translate(0, 18px);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: translate(18px, 18px);
|
||||
transform: translate(18px, 18px);
|
||||
}
|
||||
75% {
|
||||
-webkit-transform: translate(18px, 0);
|
||||
transform: translate(18px, 0);
|
||||
}
|
||||
}
|
||||
.loading6 .shape2 {
|
||||
-webkit-animation: animation6shape2 2s linear 0s infinite normal;
|
||||
animation: animation6shape2 2s linear 0s infinite normal;
|
||||
}
|
||||
|
||||
@-webkit-keyframes animation6shape2 {
|
||||
0% {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
25% {
|
||||
-webkit-transform: translate(-18px, 0);
|
||||
transform: translate(-18px, 0);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: translate(-18px, 18px);
|
||||
transform: translate(-18px, 18px);
|
||||
}
|
||||
75% {
|
||||
-webkit-transform: translate(0, 18px);
|
||||
transform: translate(0, 18px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animation6shape2 {
|
||||
0% {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
25% {
|
||||
-webkit-transform: translate(-18px, 0);
|
||||
transform: translate(-18px, 0);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: translate(-18px, 18px);
|
||||
transform: translate(-18px, 18px);
|
||||
}
|
||||
75% {
|
||||
-webkit-transform: translate(0, 18px);
|
||||
transform: translate(0, 18px);
|
||||
}
|
||||
}
|
||||
.loading6 .shape3 {
|
||||
-webkit-animation: animation6shape3 2s linear 0s infinite normal;
|
||||
animation: animation6shape3 2s linear 0s infinite normal;
|
||||
}
|
||||
|
||||
@-webkit-keyframes animation6shape3 {
|
||||
0% {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
25% {
|
||||
-webkit-transform: translate(18px, 0);
|
||||
transform: translate(18px, 0);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: translate(18px, -18px);
|
||||
transform: translate(18px, -18px);
|
||||
}
|
||||
75% {
|
||||
-webkit-transform: translate(0, -18px);
|
||||
transform: translate(0, -18px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animation6shape3 {
|
||||
0% {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
25% {
|
||||
-webkit-transform: translate(18px, 0);
|
||||
transform: translate(18px, 0);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: translate(18px, -18px);
|
||||
transform: translate(18px, -18px);
|
||||
}
|
||||
75% {
|
||||
-webkit-transform: translate(0, -18px);
|
||||
transform: translate(0, -18px);
|
||||
}
|
||||
}
|
||||
.loading6 .shape4 {
|
||||
-webkit-animation: animation6shape4 2s linear 0s infinite normal;
|
||||
animation: animation6shape4 2s linear 0s infinite normal;
|
||||
}
|
||||
|
||||
@-webkit-keyframes animation6shape4 {
|
||||
0% {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
25% {
|
||||
-webkit-transform: translate(0, -18px);
|
||||
transform: translate(0, -18px);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: translate(-18px, -18px);
|
||||
transform: translate(-18px, -18px);
|
||||
}
|
||||
75% {
|
||||
-webkit-transform: translate(-18px, 0);
|
||||
transform: translate(-18px, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animation6shape4 {
|
||||
0% {
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
25% {
|
||||
-webkit-transform: translate(0, -18px);
|
||||
transform: translate(0, -18px);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: translate(-18px, -18px);
|
||||
transform: translate(-18px, -18px);
|
||||
}
|
||||
75% {
|
||||
-webkit-transform: translate(-18px, 0);
|
||||
transform: translate(-18px, 0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<view>
|
||||
<Loading1 v-if="loadingType == 1" />
|
||||
<Loading2 v-if="loadingType == 2" />
|
||||
<Loading3 v-if="loadingType == 3" />
|
||||
<Loading4 v-if="loadingType == 4" />
|
||||
<Loading5 v-if="loadingType == 5" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Loading1 from "./loading1.vue";
|
||||
import Loading2 from "./loading2.vue";
|
||||
import Loading3 from "./loading3.vue";
|
||||
import Loading4 from "./loading4.vue";
|
||||
import Loading5 from "./loading5.vue";
|
||||
export default {
|
||||
name: "qiun-loading",
|
||||
components: { Loading1, Loading2, Loading3, Loading4, Loading5 },
|
||||
props: {
|
||||
loadingType: {
|
||||
type: Number,
|
||||
default: 2,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -0,0 +1,444 @@
|
||||
/*
|
||||
* uCharts®
|
||||
* 高性能跨平台图表库,支持H5、APP、小程序(微信/支付宝/百度/头条/QQ/360)、Vue、Taro等支持canvas的框架平台
|
||||
* Copyright (c) 2021 QIUN®秋云 https://www.ucharts.cn All rights reserved.
|
||||
* Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
* 复制使用请保留本段注释,感谢支持开源!
|
||||
*
|
||||
* uCharts®官方网站
|
||||
* https://www.uCharts.cn
|
||||
*
|
||||
* 开源地址:
|
||||
* https://gitee.com/uCharts/uCharts
|
||||
*
|
||||
* uni-app插件市场地址:
|
||||
* http://ext.dcloud.net.cn/plugin?id=271
|
||||
*
|
||||
*/
|
||||
|
||||
// 通用配置项
|
||||
|
||||
// 主题颜色配置:如每个图表类型需要不同主题,请在对应图表类型上更改color属性
|
||||
const color = [
|
||||
"#1890FF",
|
||||
"#91CB74",
|
||||
"#FAC858",
|
||||
"#EE6666",
|
||||
"#73C0DE",
|
||||
"#3CA272",
|
||||
"#FC8452",
|
||||
"#9A60B4",
|
||||
"#ea7ccc",
|
||||
];
|
||||
|
||||
const cfe = {
|
||||
//demotype为自定义图表类型
|
||||
type: [
|
||||
"pie",
|
||||
"ring",
|
||||
"rose",
|
||||
"funnel",
|
||||
"line",
|
||||
"column",
|
||||
"area",
|
||||
"radar",
|
||||
"gauge",
|
||||
"candle",
|
||||
"demotype",
|
||||
],
|
||||
//增加自定义图表类型,如果需要categories,请在这里加入您的图表类型例如最后的"demotype"
|
||||
categories: ["line", "column", "area", "radar", "gauge", "candle", "demotype"],
|
||||
//instance为实例变量承载属性,option为eopts承载属性,不要删除
|
||||
instance: {},
|
||||
option: {},
|
||||
//下面是自定义format配置,因除H5端外的其他端无法通过props传递函数,只能通过此属性对应下标的方式来替换
|
||||
formatter: {
|
||||
tooltipDemo1: function (res) {
|
||||
let result = "";
|
||||
for (let i in res) {
|
||||
if (i == 0) {
|
||||
result += res[i].axisValueLabel + "年销售额";
|
||||
}
|
||||
let value = "--";
|
||||
if (res[i].data !== null) {
|
||||
value = res[i].data;
|
||||
}
|
||||
// #ifdef H5
|
||||
result += "\n" + res[i].seriesName + ":" + value + " 万元";
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
result += "<br/>" + res[i].marker + res[i].seriesName + ":" + value + " 万元";
|
||||
// #endif
|
||||
}
|
||||
return result;
|
||||
},
|
||||
legendFormat: function (name) {
|
||||
return "自定义图例+" + name;
|
||||
},
|
||||
yAxisFormatDemo: function (value, index) {
|
||||
return value + "元";
|
||||
},
|
||||
seriesFormatDemo: function (res) {
|
||||
return res.name + "年" + res.value + "元";
|
||||
},
|
||||
},
|
||||
//这里演示了自定义您的图表类型的option,可以随意命名,之后在组件上 type="demotype" 后,组件会调用这个花括号里的option,如果组件上还存在eopts参数,会将demotype与eopts中option合并后渲染图表。
|
||||
demotype: {
|
||||
color: color,
|
||||
//在这里填写echarts的option即可
|
||||
},
|
||||
//下面是自定义配置,请添加项目所需的通用配置
|
||||
column: {
|
||||
color: color,
|
||||
title: {
|
||||
text: "",
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
},
|
||||
grid: {
|
||||
top: 30,
|
||||
bottom: 50,
|
||||
right: 15,
|
||||
left: 40,
|
||||
},
|
||||
legend: {
|
||||
bottom: "left",
|
||||
},
|
||||
toolbox: {
|
||||
show: false,
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
axisLabel: {
|
||||
color: "#666666",
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: "#CCCCCC",
|
||||
},
|
||||
},
|
||||
boundaryGap: true,
|
||||
data: [],
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
axisLabel: {
|
||||
color: "#666666",
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: "#CCCCCC",
|
||||
},
|
||||
},
|
||||
},
|
||||
seriesTemplate: {
|
||||
name: "",
|
||||
type: "bar",
|
||||
data: [],
|
||||
barwidth: 20,
|
||||
label: {
|
||||
show: true,
|
||||
color: "#666666",
|
||||
position: "top",
|
||||
},
|
||||
},
|
||||
},
|
||||
line: {
|
||||
color: color,
|
||||
title: {
|
||||
text: "",
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
},
|
||||
grid: {
|
||||
top: 30,
|
||||
bottom: 50,
|
||||
right: 15,
|
||||
left: 40,
|
||||
},
|
||||
legend: {
|
||||
bottom: "left",
|
||||
},
|
||||
toolbox: {
|
||||
show: false,
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
axisLabel: {
|
||||
color: "#666666",
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: "#CCCCCC",
|
||||
},
|
||||
},
|
||||
boundaryGap: true,
|
||||
data: [],
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
axisLabel: {
|
||||
color: "#666666",
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: "#CCCCCC",
|
||||
},
|
||||
},
|
||||
},
|
||||
seriesTemplate: {
|
||||
name: "",
|
||||
type: "line",
|
||||
data: [],
|
||||
barwidth: 20,
|
||||
label: {
|
||||
show: true,
|
||||
color: "#666666",
|
||||
position: "top",
|
||||
},
|
||||
},
|
||||
},
|
||||
area: {
|
||||
color: color,
|
||||
title: {
|
||||
text: "",
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
},
|
||||
grid: {
|
||||
top: 30,
|
||||
bottom: 50,
|
||||
right: 15,
|
||||
left: 40,
|
||||
},
|
||||
legend: {
|
||||
bottom: "left",
|
||||
},
|
||||
toolbox: {
|
||||
show: false,
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
axisLabel: {
|
||||
color: "#666666",
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: "#CCCCCC",
|
||||
},
|
||||
},
|
||||
boundaryGap: true,
|
||||
data: [],
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
axisLabel: {
|
||||
color: "#666666",
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: "#CCCCCC",
|
||||
},
|
||||
},
|
||||
},
|
||||
seriesTemplate: {
|
||||
name: "",
|
||||
type: "line",
|
||||
data: [],
|
||||
areaStyle: {},
|
||||
label: {
|
||||
show: true,
|
||||
color: "#666666",
|
||||
position: "top",
|
||||
},
|
||||
},
|
||||
},
|
||||
pie: {
|
||||
color: color,
|
||||
title: {
|
||||
text: "",
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
},
|
||||
grid: {
|
||||
top: 40,
|
||||
bottom: 30,
|
||||
right: 15,
|
||||
left: 15,
|
||||
},
|
||||
legend: {
|
||||
bottom: "left",
|
||||
},
|
||||
seriesTemplate: {
|
||||
name: "",
|
||||
type: "pie",
|
||||
data: [],
|
||||
radius: "50%",
|
||||
label: {
|
||||
show: true,
|
||||
color: "#666666",
|
||||
position: "top",
|
||||
},
|
||||
},
|
||||
},
|
||||
ring: {
|
||||
color: color,
|
||||
title: {
|
||||
text: "",
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
},
|
||||
grid: {
|
||||
top: 40,
|
||||
bottom: 30,
|
||||
right: 15,
|
||||
left: 15,
|
||||
},
|
||||
legend: {
|
||||
bottom: "left",
|
||||
},
|
||||
seriesTemplate: {
|
||||
name: "",
|
||||
type: "pie",
|
||||
data: [],
|
||||
radius: ["40%", "70%"],
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: true,
|
||||
color: "#666666",
|
||||
position: "top",
|
||||
},
|
||||
labelLine: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
rose: {
|
||||
color: color,
|
||||
title: {
|
||||
text: "",
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
},
|
||||
legend: {
|
||||
top: "bottom",
|
||||
},
|
||||
seriesTemplate: {
|
||||
name: "",
|
||||
type: "pie",
|
||||
data: [],
|
||||
radius: "55%",
|
||||
center: ["50%", "50%"],
|
||||
roseType: "area",
|
||||
},
|
||||
},
|
||||
funnel: {
|
||||
color: color,
|
||||
title: {
|
||||
text: "",
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
formatter: "{b} : {c}%",
|
||||
},
|
||||
legend: {
|
||||
top: "bottom",
|
||||
},
|
||||
seriesTemplate: {
|
||||
name: "",
|
||||
type: "funnel",
|
||||
left: "10%",
|
||||
top: 60,
|
||||
bottom: 60,
|
||||
width: "80%",
|
||||
min: 0,
|
||||
max: 100,
|
||||
minSize: "0%",
|
||||
maxSize: "100%",
|
||||
sort: "descending",
|
||||
gap: 2,
|
||||
label: {
|
||||
show: true,
|
||||
position: "inside",
|
||||
},
|
||||
labelLine: {
|
||||
length: 10,
|
||||
lineStyle: {
|
||||
width: 1,
|
||||
type: "solid",
|
||||
},
|
||||
},
|
||||
itemStyle: {
|
||||
bordercolor: "#fff",
|
||||
borderwidth: 1,
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
fontSize: 20,
|
||||
},
|
||||
},
|
||||
data: [],
|
||||
},
|
||||
},
|
||||
gauge: {
|
||||
color: color,
|
||||
tooltip: {
|
||||
formatter: "{a} <br/>{b} : {c}%",
|
||||
},
|
||||
seriesTemplate: {
|
||||
name: "业务指标",
|
||||
type: "gauge",
|
||||
detail: { formatter: "{value}%" },
|
||||
data: [{ value: 50, name: "完成率" }],
|
||||
},
|
||||
},
|
||||
candle: {
|
||||
xAxis: {
|
||||
data: [],
|
||||
},
|
||||
yAxis: {},
|
||||
color: color,
|
||||
title: {
|
||||
text: "",
|
||||
},
|
||||
dataZoom: [
|
||||
{
|
||||
type: "inside",
|
||||
xAxisIndex: [0, 1],
|
||||
start: 10,
|
||||
end: 100,
|
||||
},
|
||||
{
|
||||
show: true,
|
||||
xAxisIndex: [0, 1],
|
||||
type: "slider",
|
||||
bottom: 10,
|
||||
start: 10,
|
||||
end: 100,
|
||||
},
|
||||
],
|
||||
seriesTemplate: {
|
||||
name: "",
|
||||
type: "k",
|
||||
data: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default cfe;
|
||||
@@ -0,0 +1,676 @@
|
||||
/*
|
||||
* uCharts®
|
||||
* 高性能跨平台图表库,支持H5、APP、小程序(微信/支付宝/百度/头条/QQ/360)、Vue、Taro等支持canvas的框架平台
|
||||
* Copyright (c) 2021 QIUN®秋云 https://www.ucharts.cn All rights reserved.
|
||||
* Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
* 复制使用请保留本段注释,感谢支持开源!
|
||||
*
|
||||
* uCharts®官方网站
|
||||
* https://www.uCharts.cn
|
||||
*
|
||||
* 开源地址:
|
||||
* https://gitee.com/uCharts/uCharts
|
||||
*
|
||||
* uni-app插件市场地址:
|
||||
* http://ext.dcloud.net.cn/plugin?id=271
|
||||
*
|
||||
*/
|
||||
|
||||
// 主题颜色配置:如每个图表类型需要不同主题,请在对应图表类型上更改color属性
|
||||
const color = [
|
||||
"#1890FF",
|
||||
"#91CB74",
|
||||
"#FAC858",
|
||||
"#EE6666",
|
||||
"#73C0DE",
|
||||
"#3CA272",
|
||||
"#FC8452",
|
||||
"#9A60B4",
|
||||
"#ea7ccc",
|
||||
];
|
||||
|
||||
//事件转换函数,主要用作格式化x轴为时间轴,根据需求自行修改
|
||||
const formatDateTime = (timeStamp, returnType) => {
|
||||
var date = new Date();
|
||||
date.setTime(timeStamp * 1000);
|
||||
var y = date.getFullYear();
|
||||
var m = date.getMonth() + 1;
|
||||
m = m < 10 ? "0" + m : m;
|
||||
var d = date.getDate();
|
||||
d = d < 10 ? "0" + d : d;
|
||||
var h = date.getHours();
|
||||
h = h < 10 ? "0" + h : h;
|
||||
var minute = date.getMinutes();
|
||||
var second = date.getSeconds();
|
||||
minute = minute < 10 ? "0" + minute : minute;
|
||||
second = second < 10 ? "0" + second : second;
|
||||
if (returnType == "full") {
|
||||
return y + "-" + m + "-" + d + " " + h + ":" + minute + ":" + second;
|
||||
}
|
||||
if (returnType == "y-m-d") {
|
||||
return y + "-" + m + "-" + d;
|
||||
}
|
||||
if (returnType == "h:m") {
|
||||
return h + ":" + minute;
|
||||
}
|
||||
if (returnType == "h:m:s") {
|
||||
return h + ":" + minute + ":" + second;
|
||||
}
|
||||
return [y, m, d, h, minute, second];
|
||||
};
|
||||
|
||||
const cfu = {
|
||||
//demotype为自定义图表类型,一般不需要自定义图表类型,只需要改根节点上对应的类型即可
|
||||
type: [
|
||||
"pie",
|
||||
"ring",
|
||||
"rose",
|
||||
"word",
|
||||
"funnel",
|
||||
"map",
|
||||
"arcbar",
|
||||
"line",
|
||||
"column",
|
||||
"mount",
|
||||
"bar",
|
||||
"area",
|
||||
"radar",
|
||||
"gauge",
|
||||
"candle",
|
||||
"mix",
|
||||
"tline",
|
||||
"tarea",
|
||||
"scatter",
|
||||
"bubble",
|
||||
"demotype",
|
||||
],
|
||||
range: [
|
||||
"饼状图",
|
||||
"圆环图",
|
||||
"玫瑰图",
|
||||
"词云图",
|
||||
"漏斗图",
|
||||
"地图",
|
||||
"圆弧进度条",
|
||||
"折线图",
|
||||
"柱状图",
|
||||
"山峰图",
|
||||
"条状图",
|
||||
"区域图",
|
||||
"雷达图",
|
||||
"仪表盘",
|
||||
"K线图",
|
||||
"混合图",
|
||||
"时间轴折线",
|
||||
"时间轴区域",
|
||||
"散点图",
|
||||
"气泡图",
|
||||
"自定义类型",
|
||||
],
|
||||
//增加自定义图表类型,如果需要categories,请在这里加入您的图表类型,例如最后的"demotype"
|
||||
//自定义类型时需要注意"tline","tarea","scatter","bubble"等时间轴(矢量x轴)类图表,没有categories,不需要加入categories
|
||||
categories: [
|
||||
"line",
|
||||
"column",
|
||||
"mount",
|
||||
"bar",
|
||||
"area",
|
||||
"radar",
|
||||
"gauge",
|
||||
"candle",
|
||||
"mix",
|
||||
"demotype",
|
||||
],
|
||||
//instance为实例变量承载属性,不要删除
|
||||
instance: {},
|
||||
//option为opts及eopts承载属性,不要删除
|
||||
option: {},
|
||||
//下面是自定义format配置,因除H5端外的其他端无法通过props传递函数,只能通过此属性对应下标的方式来替换
|
||||
formatter: {
|
||||
yAxisDemo1: function (val, index, opts) {
|
||||
return val + "元";
|
||||
},
|
||||
yAxisDemo2: function (val, index, opts) {
|
||||
return val.toFixed(2);
|
||||
},
|
||||
xAxisDemo1: function (val, index, opts) {
|
||||
return val + "年";
|
||||
},
|
||||
xAxisDemo2: function (val, index, opts) {
|
||||
return formatDateTime(val, "h:m");
|
||||
},
|
||||
seriesDemo1: function (val, index, series, opts) {
|
||||
return val + "元";
|
||||
},
|
||||
tooltipDemo1: function (item, category, index, opts) {
|
||||
if (index == 0) {
|
||||
return "随便用" + item.data + "年";
|
||||
} else {
|
||||
return "其他我没改" + item.data + "天";
|
||||
}
|
||||
},
|
||||
pieDemo: function (val, index, series, opts) {
|
||||
if (index !== undefined) {
|
||||
return series[index].name + ":" + series[index].data + "元";
|
||||
}
|
||||
},
|
||||
},
|
||||
//这里演示了自定义您的图表类型的option,可以随意命名,之后在组件上 type="demotype" 后,组件会调用这个花括号里的option,如果组件上还存在opts参数,会将demotype与opts中option合并后渲染图表。
|
||||
demotype: {
|
||||
//我这里把曲线图当做了自定义图表类型,您可以根据需要随意指定类型或配置
|
||||
type: "line",
|
||||
color: color,
|
||||
padding: [15, 10, 0, 15],
|
||||
xAxis: {
|
||||
disableGrid: true,
|
||||
},
|
||||
yAxis: {
|
||||
gridType: "dash",
|
||||
dashLength: 2,
|
||||
},
|
||||
legend: {},
|
||||
extra: {
|
||||
line: {
|
||||
type: "curve",
|
||||
width: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
//下面是自定义配置,请添加项目所需的通用配置
|
||||
pie: {
|
||||
type: "pie",
|
||||
color: color,
|
||||
padding: [5, 5, 5, 5],
|
||||
extra: {
|
||||
pie: {
|
||||
activeOpacity: 0.5,
|
||||
activeRadius: 10,
|
||||
offsetAngle: 0,
|
||||
labelWidth: 15,
|
||||
border: true,
|
||||
borderWidth: 3,
|
||||
borderColor: "#FFFFFF",
|
||||
},
|
||||
},
|
||||
},
|
||||
ring: {
|
||||
type: "ring",
|
||||
color: color,
|
||||
padding: [5, 5, 5, 5],
|
||||
rotate: false,
|
||||
dataLabel: true,
|
||||
legend: {
|
||||
show: true,
|
||||
position: "right",
|
||||
lineHeight: 25,
|
||||
},
|
||||
title: {
|
||||
name: "收益率",
|
||||
fontSize: 15,
|
||||
color: "#666666",
|
||||
},
|
||||
subtitle: {
|
||||
name: "70%",
|
||||
fontSize: 25,
|
||||
color: "#7cb5ec",
|
||||
},
|
||||
extra: {
|
||||
ring: {
|
||||
ringWidth: 30,
|
||||
activeOpacity: 0.5,
|
||||
activeRadius: 10,
|
||||
offsetAngle: 0,
|
||||
labelWidth: 15,
|
||||
border: true,
|
||||
borderWidth: 3,
|
||||
borderColor: "#FFFFFF",
|
||||
},
|
||||
},
|
||||
},
|
||||
rose: {
|
||||
type: "rose",
|
||||
color: color,
|
||||
padding: [5, 5, 5, 5],
|
||||
legend: {
|
||||
show: true,
|
||||
position: "left",
|
||||
lineHeight: 25,
|
||||
},
|
||||
extra: {
|
||||
rose: {
|
||||
type: "area",
|
||||
minRadius: 50,
|
||||
activeOpacity: 0.5,
|
||||
activeRadius: 10,
|
||||
offsetAngle: 0,
|
||||
labelWidth: 15,
|
||||
border: false,
|
||||
borderWidth: 2,
|
||||
borderColor: "#FFFFFF",
|
||||
},
|
||||
},
|
||||
},
|
||||
word: {
|
||||
type: "word",
|
||||
color: color,
|
||||
extra: {
|
||||
word: {
|
||||
type: "normal",
|
||||
autoColors: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
funnel: {
|
||||
type: "funnel",
|
||||
color: color,
|
||||
padding: [15, 15, 0, 15],
|
||||
extra: {
|
||||
funnel: {
|
||||
activeOpacity: 0.3,
|
||||
activeWidth: 10,
|
||||
border: true,
|
||||
borderWidth: 2,
|
||||
borderColor: "#FFFFFF",
|
||||
fillOpacity: 1,
|
||||
labelAlign: "right",
|
||||
},
|
||||
},
|
||||
},
|
||||
map: {
|
||||
type: "map",
|
||||
color: color,
|
||||
padding: [0, 0, 0, 0],
|
||||
dataLabel: true,
|
||||
extra: {
|
||||
map: {
|
||||
border: true,
|
||||
borderWidth: 1,
|
||||
borderColor: "#666666",
|
||||
fillOpacity: 0.6,
|
||||
activeBorderColor: "#F04864",
|
||||
activeFillColor: "#FACC14",
|
||||
activeFillOpacity: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
arcbar: {
|
||||
type: "arcbar",
|
||||
color: color,
|
||||
title: {
|
||||
name: "百分比",
|
||||
fontSize: 25,
|
||||
color: "#00FF00",
|
||||
},
|
||||
subtitle: {
|
||||
name: "默认标题",
|
||||
fontSize: 15,
|
||||
color: "#666666",
|
||||
},
|
||||
extra: {
|
||||
arcbar: {
|
||||
type: "default",
|
||||
width: 12,
|
||||
backgroundColor: "#E9E9E9",
|
||||
startAngle: 0.75,
|
||||
endAngle: 0.25,
|
||||
gap: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
line: {
|
||||
type: "line",
|
||||
color: color,
|
||||
padding: [15, 10, 0, 15],
|
||||
xAxis: {
|
||||
disableGrid: true,
|
||||
},
|
||||
yAxis: {
|
||||
gridType: "dash",
|
||||
dashLength: 2,
|
||||
},
|
||||
legend: {},
|
||||
extra: {
|
||||
line: {
|
||||
type: "straight",
|
||||
width: 2,
|
||||
activeType: "hollow",
|
||||
},
|
||||
},
|
||||
},
|
||||
tline: {
|
||||
type: "line",
|
||||
color: color,
|
||||
padding: [15, 10, 0, 15],
|
||||
xAxis: {
|
||||
disableGrid: false,
|
||||
boundaryGap: "justify",
|
||||
},
|
||||
yAxis: {
|
||||
gridType: "dash",
|
||||
dashLength: 2,
|
||||
data: [
|
||||
{
|
||||
min: 0,
|
||||
max: 80,
|
||||
},
|
||||
],
|
||||
},
|
||||
legend: {},
|
||||
extra: {
|
||||
line: {
|
||||
type: "curve",
|
||||
width: 2,
|
||||
activeType: "hollow",
|
||||
},
|
||||
},
|
||||
},
|
||||
tarea: {
|
||||
type: "area",
|
||||
color: color,
|
||||
padding: [15, 10, 0, 15],
|
||||
xAxis: {
|
||||
disableGrid: true,
|
||||
boundaryGap: "justify",
|
||||
},
|
||||
yAxis: {
|
||||
gridType: "dash",
|
||||
dashLength: 2,
|
||||
data: [
|
||||
{
|
||||
min: 0,
|
||||
max: 80,
|
||||
},
|
||||
],
|
||||
},
|
||||
legend: {},
|
||||
extra: {
|
||||
area: {
|
||||
type: "curve",
|
||||
opacity: 0.2,
|
||||
addLine: true,
|
||||
width: 2,
|
||||
gradient: true,
|
||||
activeType: "hollow",
|
||||
},
|
||||
},
|
||||
},
|
||||
column: {
|
||||
type: "column",
|
||||
color: color,
|
||||
padding: [15, 15, 0, 5],
|
||||
xAxis: {
|
||||
disableGrid: true,
|
||||
},
|
||||
yAxis: {
|
||||
data: [{ min: 0 }],
|
||||
},
|
||||
legend: {},
|
||||
extra: {
|
||||
column: {
|
||||
type: "group",
|
||||
width: 30,
|
||||
activeBgColor: "#000000",
|
||||
activeBgOpacity: 0.08,
|
||||
},
|
||||
},
|
||||
},
|
||||
mount: {
|
||||
type: "mount",
|
||||
color: color,
|
||||
padding: [15, 15, 0, 5],
|
||||
xAxis: {
|
||||
disableGrid: true,
|
||||
},
|
||||
yAxis: {
|
||||
data: [{ min: 0 }],
|
||||
},
|
||||
legend: {},
|
||||
extra: {
|
||||
mount: {
|
||||
type: "mount",
|
||||
widthRatio: 1.5,
|
||||
},
|
||||
},
|
||||
},
|
||||
bar: {
|
||||
type: "bar",
|
||||
color: color,
|
||||
padding: [15, 30, 0, 5],
|
||||
xAxis: {
|
||||
boundaryGap: "justify",
|
||||
disableGrid: false,
|
||||
min: 0,
|
||||
axisLine: false,
|
||||
},
|
||||
yAxis: {},
|
||||
legend: {},
|
||||
extra: {
|
||||
bar: {
|
||||
type: "group",
|
||||
width: 30,
|
||||
meterBorde: 1,
|
||||
meterFillColor: "#FFFFFF",
|
||||
activeBgColor: "#000000",
|
||||
activeBgOpacity: 0.08,
|
||||
},
|
||||
},
|
||||
},
|
||||
area: {
|
||||
type: "area",
|
||||
color: color,
|
||||
padding: [15, 15, 0, 15],
|
||||
xAxis: {
|
||||
disableGrid: true,
|
||||
},
|
||||
yAxis: {
|
||||
gridType: "dash",
|
||||
dashLength: 2,
|
||||
},
|
||||
legend: {},
|
||||
extra: {
|
||||
area: {
|
||||
type: "straight",
|
||||
opacity: 0.2,
|
||||
addLine: true,
|
||||
width: 2,
|
||||
gradient: false,
|
||||
activeType: "hollow",
|
||||
},
|
||||
},
|
||||
},
|
||||
radar: {
|
||||
type: "radar",
|
||||
color: color,
|
||||
padding: [5, 5, 5, 5],
|
||||
dataLabel: false,
|
||||
legend: {
|
||||
show: true,
|
||||
position: "right",
|
||||
lineHeight: 25,
|
||||
},
|
||||
extra: {
|
||||
radar: {
|
||||
gridType: "radar",
|
||||
gridColor: "#CCCCCC",
|
||||
gridCount: 3,
|
||||
opacity: 0.2,
|
||||
max: 200,
|
||||
labelShow: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
gauge: {
|
||||
type: "gauge",
|
||||
color: color,
|
||||
title: {
|
||||
name: "66Km/H",
|
||||
fontSize: 25,
|
||||
color: "#2fc25b",
|
||||
offsetY: 50,
|
||||
},
|
||||
subtitle: {
|
||||
name: "实时速度",
|
||||
fontSize: 15,
|
||||
color: "#1890ff",
|
||||
offsetY: -50,
|
||||
},
|
||||
extra: {
|
||||
gauge: {
|
||||
type: "default",
|
||||
width: 30,
|
||||
labelColor: "#666666",
|
||||
startAngle: 0.75,
|
||||
endAngle: 0.25,
|
||||
startNumber: 0,
|
||||
endNumber: 100,
|
||||
labelFormat: "",
|
||||
splitLine: {
|
||||
fixRadius: 0,
|
||||
splitNumber: 10,
|
||||
width: 30,
|
||||
color: "#FFFFFF",
|
||||
childNumber: 5,
|
||||
childWidth: 12,
|
||||
},
|
||||
pointer: {
|
||||
width: 24,
|
||||
color: "auto",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
candle: {
|
||||
type: "candle",
|
||||
color: color,
|
||||
padding: [15, 15, 0, 15],
|
||||
enableScroll: true,
|
||||
enableMarkLine: true,
|
||||
dataLabel: false,
|
||||
xAxis: {
|
||||
labelCount: 4,
|
||||
itemCount: 40,
|
||||
disableGrid: true,
|
||||
gridColor: "#CCCCCC",
|
||||
gridType: "solid",
|
||||
dashLength: 4,
|
||||
scrollShow: true,
|
||||
scrollAlign: "left",
|
||||
scrollColor: "#A6A6A6",
|
||||
scrollBackgroundColor: "#EFEBEF",
|
||||
},
|
||||
yAxis: {},
|
||||
legend: {},
|
||||
extra: {
|
||||
candle: {
|
||||
color: {
|
||||
upLine: "#f04864",
|
||||
upFill: "#f04864",
|
||||
downLine: "#2fc25b",
|
||||
downFill: "#2fc25b",
|
||||
},
|
||||
average: {
|
||||
show: true,
|
||||
name: ["MA5", "MA10", "MA30"],
|
||||
day: [5, 10, 20],
|
||||
color: ["#1890ff", "#2fc25b", "#facc14"],
|
||||
},
|
||||
},
|
||||
markLine: {
|
||||
type: "dash",
|
||||
dashLength: 5,
|
||||
data: [
|
||||
{
|
||||
value: 2150,
|
||||
lineColor: "#f04864",
|
||||
showLabel: true,
|
||||
},
|
||||
{
|
||||
value: 2350,
|
||||
lineColor: "#f04864",
|
||||
showLabel: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
mix: {
|
||||
type: "mix",
|
||||
color: color,
|
||||
padding: [15, 15, 0, 15],
|
||||
xAxis: {
|
||||
disableGrid: true,
|
||||
},
|
||||
yAxis: {
|
||||
disabled: false,
|
||||
disableGrid: false,
|
||||
splitNumber: 5,
|
||||
gridType: "dash",
|
||||
dashLength: 4,
|
||||
gridColor: "#CCCCCC",
|
||||
padding: 10,
|
||||
showTitle: true,
|
||||
data: [],
|
||||
},
|
||||
legend: {},
|
||||
extra: {
|
||||
mix: {
|
||||
column: {
|
||||
width: 20,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
scatter: {
|
||||
type: "scatter",
|
||||
color: color,
|
||||
padding: [15, 15, 0, 15],
|
||||
dataLabel: false,
|
||||
xAxis: {
|
||||
disableGrid: false,
|
||||
gridType: "dash",
|
||||
splitNumber: 5,
|
||||
boundaryGap: "justify",
|
||||
min: 0,
|
||||
},
|
||||
yAxis: {
|
||||
disableGrid: false,
|
||||
gridType: "dash",
|
||||
},
|
||||
legend: {},
|
||||
extra: {
|
||||
scatter: {},
|
||||
},
|
||||
},
|
||||
bubble: {
|
||||
type: "bubble",
|
||||
color: color,
|
||||
padding: [15, 15, 0, 15],
|
||||
xAxis: {
|
||||
disableGrid: false,
|
||||
gridType: "dash",
|
||||
splitNumber: 5,
|
||||
boundaryGap: "justify",
|
||||
min: 0,
|
||||
max: 250,
|
||||
},
|
||||
yAxis: {
|
||||
disableGrid: false,
|
||||
gridType: "dash",
|
||||
data: [
|
||||
{
|
||||
min: 0,
|
||||
max: 150,
|
||||
},
|
||||
],
|
||||
},
|
||||
legend: {},
|
||||
extra: {
|
||||
bubble: {
|
||||
border: 2,
|
||||
opacity: 0.5,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default cfu;
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,6 @@
|
||||
# uCharts JSSDK说明
|
||||
|
||||
1、如不使用uCharts组件,可直接引用u-charts.js,打包编译后会`自动压缩`,压缩后体积约为`120kb`。
|
||||
2、如果120kb的体积仍需压缩,请手到uCharts官网通过在线定制选择您需要的图表。
|
||||
3、config-ucharts.js为uCharts组件的用户配置文件,升级前请`自行备份config-ucharts.js`文件,以免被强制覆盖。
|
||||
4、config-echarts.js为ECharts组件的用户配置文件,升级前请`自行备份config-echarts.js`文件,以免被强制覆盖。
|
||||
@@ -0,0 +1,368 @@
|
||||
import { Client, type IMessage, type StompSubscription } from "@stomp/stompjs";
|
||||
import { getAccessToken } from "@/utils/auth";
|
||||
|
||||
export interface UseStompOptions {
|
||||
/** WebSocket 地址,不传时使用 VITE_APP_WS_ENDPOINT 环境变量 */
|
||||
brokerURL?: string;
|
||||
/** 用于鉴权的 token,不传时使用 getAccessToken() 的返回值 */
|
||||
token?: string;
|
||||
/** 重连延迟,单位毫秒,默认为 8000 */
|
||||
reconnectDelay?: number;
|
||||
/** 连接超时时间,单位毫秒,默认为 10000 */
|
||||
connectionTimeout?: number;
|
||||
/** 是否开启指数退避重连策略 */
|
||||
useExponentialBackoff?: boolean;
|
||||
/** 最大重连次数,默认为 5 */
|
||||
maxReconnectAttempts?: number;
|
||||
/** 最大重连延迟,单位毫秒,默认为 60000 */
|
||||
maxReconnectDelay?: number;
|
||||
/** 是否开启调试日志 */
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* STOMP WebSocket连接组合式函数
|
||||
* 用于管理WebSocket连接的建立、断开、重连和消息订阅
|
||||
*/
|
||||
export function useStomp(options: UseStompOptions = {}) {
|
||||
// 默认值:brokerURL 从环境变量中获取,token 从 getAccessToken() 获取
|
||||
const defaultBrokerURL = import.meta.env.VITE_APP_WS_ENDPOINT || "";
|
||||
|
||||
const brokerURL = ref(options.brokerURL ?? defaultBrokerURL);
|
||||
// 默认配置参数
|
||||
const reconnectDelay = options.reconnectDelay ?? 15000; // 默认15秒重连间隔
|
||||
const connectionTimeout = options.connectionTimeout ?? 10000;
|
||||
const useExponentialBackoff = options.useExponentialBackoff ?? false;
|
||||
const maxReconnectAttempts = options.maxReconnectAttempts ?? 3; // 最多重连3次
|
||||
const maxReconnectDelay = options.maxReconnectDelay ?? 60000;
|
||||
|
||||
// 连接状态标记
|
||||
const isConnected = ref(false);
|
||||
// 重连尝试次数
|
||||
const reconnectCount = ref(0);
|
||||
// 重连计时器
|
||||
let reconnectTimer: any = null;
|
||||
// 连接超时计时器
|
||||
let connectionTimeoutTimer: any = null;
|
||||
// 存储所有订阅
|
||||
const subscriptions = new Map<string, StompSubscription>();
|
||||
|
||||
// 用于保存 STOMP 客户端的实例
|
||||
const client = ref<Client | null>(null);
|
||||
// 防止重复连接的标志
|
||||
let isConnecting = false;
|
||||
let isManualDisconnect = false;
|
||||
|
||||
/**
|
||||
* 初始化 STOMP 客户端
|
||||
*/
|
||||
const initializeClient = () => {
|
||||
// 如果客户端已存在且正在连接或已连接,直接返回
|
||||
if (client.value && (client.value.active || client.value.connected)) {
|
||||
console.log("STOMP客户端已存在且处于活动状态,跳过初始化");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查WebSocket端点是否配置
|
||||
if (!brokerURL.value) {
|
||||
console.error("WebSocket连接失败: 未配置WebSocket端点URL");
|
||||
return;
|
||||
}
|
||||
|
||||
// 每次连接前重新获取最新令牌,不依赖之前的token值
|
||||
const currentToken = getAccessToken();
|
||||
|
||||
// 检查令牌是否为空,如果为空则不进行连接
|
||||
if (!currentToken) {
|
||||
console.error("WebSocket连接失败:授权令牌为空,请先登录");
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果有旧的客户端,先清理
|
||||
if (client.value) {
|
||||
try {
|
||||
client.value.deactivate();
|
||||
} catch (error) {
|
||||
console.warn("清理旧客户端时出错:", error);
|
||||
}
|
||||
client.value = null;
|
||||
}
|
||||
|
||||
// 创建 STOMP 客户端
|
||||
client.value = new Client({
|
||||
brokerURL: brokerURL.value,
|
||||
connectHeaders: {
|
||||
Authorization: `Bearer ${currentToken}`,
|
||||
},
|
||||
debug: options.debug ? console.log : () => {},
|
||||
reconnectDelay: 0, // 禁用内置重连机制,使用自定义重连
|
||||
heartbeatIncoming: 4000,
|
||||
heartbeatOutgoing: 4000,
|
||||
});
|
||||
|
||||
// 设置连接监听器
|
||||
client.value.onConnect = () => {
|
||||
isConnected.value = true;
|
||||
isConnecting = false;
|
||||
reconnectCount.value = 0;
|
||||
clearTimeout(connectionTimeoutTimer);
|
||||
clearTimeout(reconnectTimer);
|
||||
console.log("WebSocket连接已建立");
|
||||
};
|
||||
|
||||
// 设置断开连接监听器
|
||||
client.value.onDisconnect = () => {
|
||||
isConnected.value = false;
|
||||
isConnecting = false;
|
||||
console.log("WebSocket连接已断开");
|
||||
|
||||
// 如果不是手动断开且未达到最大重连次数,则尝试重连
|
||||
if (!isManualDisconnect && reconnectCount.value < maxReconnectAttempts) {
|
||||
handleReconnect();
|
||||
}
|
||||
};
|
||||
|
||||
// 设置 Web Socket 关闭监听器
|
||||
client.value.onWebSocketClose = (event: CloseEvent) => {
|
||||
isConnected.value = false;
|
||||
isConnecting = false;
|
||||
console.log(`WebSocket已关闭: ${event?.code} ${event?.reason}`);
|
||||
|
||||
// 如果是手动断开,不要重连
|
||||
if (isManualDisconnect) {
|
||||
console.log("手动断开连接,不进行重连");
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是授权问题导致的关闭,尝试重连
|
||||
if (
|
||||
(event?.code === 1000 || event?.code === 1006 || event?.code === 1008) &&
|
||||
reconnectCount.value < maxReconnectAttempts
|
||||
) {
|
||||
console.log("检测到连接异常关闭,将尝试重连");
|
||||
|
||||
// 通过 handleReconnect 统一处理重连,避免重复计数
|
||||
handleReconnect();
|
||||
}
|
||||
};
|
||||
|
||||
// 设置错误监听器
|
||||
client.value.onStompError = (frame: any) => {
|
||||
console.error("STOMP错误:", frame.headers, frame.body);
|
||||
isConnecting = false;
|
||||
|
||||
// 检查是否是授权错误
|
||||
if (
|
||||
frame.headers?.message?.includes("Unauthorized") ||
|
||||
frame.body?.includes("Unauthorized") ||
|
||||
frame.body?.includes("Token")
|
||||
) {
|
||||
console.warn("WebSocket授权错误,请检查登录状态");
|
||||
// 授权错误不进行重连
|
||||
isManualDisconnect = true;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理重连逻辑
|
||||
*/
|
||||
const handleReconnect = () => {
|
||||
// 如果已经在连接中或手动断开,不重连
|
||||
if (isConnecting || isManualDisconnect) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (reconnectCount.value >= maxReconnectAttempts) {
|
||||
console.error(`已达到最大重连次数(${maxReconnectAttempts}),停止重连`);
|
||||
return;
|
||||
}
|
||||
|
||||
reconnectCount.value++;
|
||||
console.log(`准备重连(${reconnectCount.value}/${maxReconnectAttempts})...`);
|
||||
|
||||
// 使用指数退避策略增加重连间隔
|
||||
const delay = useExponentialBackoff
|
||||
? Math.min(reconnectDelay * Math.pow(2, reconnectCount.value - 1), maxReconnectDelay)
|
||||
: reconnectDelay;
|
||||
|
||||
// 清除之前的计时器
|
||||
if (reconnectTimer) {
|
||||
clearTimeout(reconnectTimer);
|
||||
}
|
||||
|
||||
// 设置重连计时器
|
||||
reconnectTimer = setTimeout(() => {
|
||||
if (!isConnected.value && !isManualDisconnect && !isConnecting) {
|
||||
console.log(`开始重连...`);
|
||||
connect();
|
||||
}
|
||||
}, delay);
|
||||
};
|
||||
|
||||
// 监听 brokerURL 的变化,若地址改变则重新初始化
|
||||
watch(brokerURL, (newURL, oldURL) => {
|
||||
if (newURL !== oldURL) {
|
||||
console.log(`brokerURL changed from ${oldURL} to ${newURL}`);
|
||||
// 断开当前连接,重新激活客户端
|
||||
if (client.value && client.value.connected) {
|
||||
client.value.deactivate();
|
||||
}
|
||||
brokerURL.value = newURL;
|
||||
initializeClient(); // 重新初始化客户端
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化客户端
|
||||
initializeClient();
|
||||
|
||||
/**
|
||||
* 激活连接(如果已经连接或正在激活则直接返回)
|
||||
*/
|
||||
const connect = () => {
|
||||
// 重置手动断开标志
|
||||
isManualDisconnect = false;
|
||||
|
||||
// 检查是否有配置WebSocket端点
|
||||
if (!brokerURL.value) {
|
||||
console.error("WebSocket连接失败: 未配置WebSocket端点URL");
|
||||
return;
|
||||
}
|
||||
|
||||
// 防止重复连接
|
||||
if (isConnecting) {
|
||||
console.log("WebSocket正在连接中,跳过重复连接请求");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!client.value) {
|
||||
initializeClient();
|
||||
}
|
||||
|
||||
if (!client.value) {
|
||||
console.error("STOMP客户端初始化失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 避免重复连接:检查是否已连接
|
||||
if (client.value.connected) {
|
||||
console.log("WebSocket已经连接,跳过重复连接");
|
||||
isConnected.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置连接标志
|
||||
isConnecting = true;
|
||||
|
||||
// 设置连接超时
|
||||
clearTimeout(connectionTimeoutTimer);
|
||||
connectionTimeoutTimer = setTimeout(() => {
|
||||
if (!isConnected.value && isConnecting) {
|
||||
console.warn("WebSocket连接超时");
|
||||
isConnecting = false;
|
||||
if (!isManualDisconnect && reconnectCount.value < maxReconnectAttempts) {
|
||||
handleReconnect();
|
||||
}
|
||||
}
|
||||
}, connectionTimeout);
|
||||
|
||||
try {
|
||||
client.value.activate();
|
||||
console.log("正在建立WebSocket连接...");
|
||||
} catch (error) {
|
||||
console.error("激活WebSocket连接失败:", error);
|
||||
isConnecting = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 订阅指定主题
|
||||
* @param destination 目标主题地址
|
||||
* @param callback 接收到消息时的回调函数
|
||||
* @returns 返回订阅 id,用于后续取消订阅
|
||||
*/
|
||||
const subscribe = (destination: string, callback: (_message: IMessage) => void): string => {
|
||||
if (!client.value || !client.value.connected) {
|
||||
console.warn(`尝试订阅 ${destination} 失败: 客户端未连接`);
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
const subscription = client.value.subscribe(destination, callback);
|
||||
const subscriptionId = subscription.id;
|
||||
subscriptions.set(subscriptionId, subscription);
|
||||
console.log(`订阅成功: ${destination}, ID: ${subscriptionId}`);
|
||||
return subscriptionId;
|
||||
} catch (error) {
|
||||
console.error(`订阅 ${destination} 失败:`, error);
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 取消订阅
|
||||
* @param subscriptionId 订阅 id
|
||||
*/
|
||||
const unsubscribe = (subscriptionId: string) => {
|
||||
const subscription = subscriptions.get(subscriptionId);
|
||||
if (subscription) {
|
||||
subscription.unsubscribe();
|
||||
subscriptions.delete(subscriptionId);
|
||||
console.log(`已取消订阅: ${subscriptionId}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 断开WebSocket连接
|
||||
*/
|
||||
const disconnect = () => {
|
||||
// 设置手动断开标志
|
||||
isManualDisconnect = true;
|
||||
|
||||
// 清除所有计时器
|
||||
if (reconnectTimer) {
|
||||
clearTimeout(reconnectTimer);
|
||||
reconnectTimer = null;
|
||||
}
|
||||
|
||||
if (connectionTimeoutTimer) {
|
||||
clearTimeout(connectionTimeoutTimer);
|
||||
connectionTimeoutTimer = null;
|
||||
}
|
||||
|
||||
// 清除所有订阅
|
||||
for (const [id, subscription] of subscriptions.entries()) {
|
||||
try {
|
||||
subscription.unsubscribe();
|
||||
} catch (error) {
|
||||
console.warn(`取消订阅 ${id} 时出错:`, error);
|
||||
}
|
||||
}
|
||||
subscriptions.clear();
|
||||
|
||||
// 断开连接
|
||||
if (client.value) {
|
||||
try {
|
||||
if (client.value.connected || client.value.active) {
|
||||
client.value.deactivate();
|
||||
console.log("WebSocket连接已主动断开");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("断开WebSocket连接时出错:", error);
|
||||
}
|
||||
client.value = null;
|
||||
}
|
||||
|
||||
isConnected.value = false;
|
||||
isConnecting = false;
|
||||
reconnectCount.value = 0;
|
||||
};
|
||||
|
||||
return {
|
||||
isConnected,
|
||||
connect,
|
||||
subscribe,
|
||||
unsubscribe,
|
||||
disconnect,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
export interface TabbarItem {
|
||||
name: string;
|
||||
value: number | null;
|
||||
active: boolean;
|
||||
title: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
const tabbarItems = ref<TabbarItem[]>([
|
||||
{ name: "home", value: null, active: true, title: "首页", icon: "home" },
|
||||
{ name: "mine", value: null, active: false, title: "我的", icon: "user" },
|
||||
]);
|
||||
|
||||
export function useTabbar() {
|
||||
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 setTabbarItem = (name: string, value: number) => {
|
||||
const tabbarItem = tabbarItems.value.find((item) => item.name === name);
|
||||
if (tabbarItem) {
|
||||
tabbarItem.value = value;
|
||||
}
|
||||
};
|
||||
|
||||
const setTabbarItemActive = (name: string) => {
|
||||
tabbarItems.value.forEach((item) => {
|
||||
if (item.name === name) {
|
||||
item.active = true;
|
||||
} else {
|
||||
item.active = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
tabbarList,
|
||||
activeTabbar,
|
||||
getTabbarItemValue,
|
||||
setTabbarItem,
|
||||
setTabbarItemActive,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import type { ConfigProviderThemeVars } from "wot-design-uni";
|
||||
|
||||
// 定义主题色选项
|
||||
export interface ThemeColorOption {
|
||||
name: string;
|
||||
value: string;
|
||||
primary: string;
|
||||
}
|
||||
|
||||
// 预定义的主题色选项
|
||||
export const themeColorOptions: ThemeColorOption[] = [
|
||||
{ name: "默认蓝", value: "blue", primary: "#4D7FFF" },
|
||||
{ name: "活力橙", value: "orange", primary: "#FF7D00" },
|
||||
{ name: "薄荷绿", value: "green", primary: "#07C160" },
|
||||
{ name: "樱花粉", value: "pink", primary: "#FF69B4" },
|
||||
{ name: "紫罗兰", value: "purple", primary: "#8A2BE2" },
|
||||
{ name: "朱砂红", value: "red", primary: "#FF4757" },
|
||||
];
|
||||
|
||||
export function useTheme() {
|
||||
// 状态定义
|
||||
const theme = ref<"light" | "dark">("light");
|
||||
const followSystem = ref(true); // 是否跟随系统主题
|
||||
const hasUserSet = ref(false); // 用户是否手动设置过主题
|
||||
const currentThemeColor = ref<ThemeColorOption>(themeColorOptions[0]);
|
||||
const showThemeColorSheet = ref(false);
|
||||
|
||||
const themeVars = reactive<ConfigProviderThemeVars>({
|
||||
darkBackground: "#0f0f0f",
|
||||
darkBackground2: "#1a1a1a",
|
||||
darkBackground3: "#242424",
|
||||
darkBackground4: "#2f2f2f",
|
||||
darkBackground5: "#3d3d3d",
|
||||
darkBackground6: "#4a4a4a",
|
||||
darkBackground7: "#606060",
|
||||
darkColor: "#ffffff",
|
||||
darkColor2: "#e0e0e0",
|
||||
darkColor3: "#a0a0a0",
|
||||
colorTheme: themeColorOptions[0].primary,
|
||||
});
|
||||
|
||||
// 计算属性
|
||||
const isDark = computed(() => theme.value === "dark");
|
||||
|
||||
/* 手动切换主题 */
|
||||
function toggleTheme(mode?: "light" | "dark") {
|
||||
theme.value = mode || (theme.value === "light" ? "dark" : "light");
|
||||
hasUserSet.value = true; // 标记用户已手动设置
|
||||
followSystem.value = false; // 不再跟随系统
|
||||
setNavigationBarColor();
|
||||
}
|
||||
|
||||
/* 设置是否跟随系统主题 */
|
||||
function setFollowSystem(follow: boolean) {
|
||||
followSystem.value = follow;
|
||||
if (follow) {
|
||||
hasUserSet.value = false;
|
||||
initTheme(); // 重新获取系统主题
|
||||
}
|
||||
}
|
||||
|
||||
/* 设置导航栏颜色 */
|
||||
function setNavigationBarColor() {
|
||||
uni.setNavigationBarColor({
|
||||
frontColor: theme.value === "light" ? "#000000" : "#ffffff",
|
||||
backgroundColor: theme.value === "light" ? "#ffffff" : "#000000",
|
||||
});
|
||||
}
|
||||
|
||||
/* 设置主题色 */
|
||||
function setCurrentThemeColor(color: ThemeColorOption) {
|
||||
currentThemeColor.value = color;
|
||||
themeVars.colorTheme = color.primary;
|
||||
}
|
||||
|
||||
/* 获取系统主题 */
|
||||
function getSystemTheme(): "light" | "dark" {
|
||||
try {
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信小程序使用 getAppBaseInfo
|
||||
const appBaseInfo = uni.getAppBaseInfo();
|
||||
if (appBaseInfo && appBaseInfo.theme) {
|
||||
return appBaseInfo.theme as "light" | "dark";
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
// 其他平台使用 getSystemInfoSync
|
||||
const systemInfo = uni.getSystemInfoSync();
|
||||
if (systemInfo && systemInfo.theme) {
|
||||
return systemInfo.theme as "light" | "dark";
|
||||
}
|
||||
// #endif
|
||||
} catch (error) {
|
||||
console.warn("获取系统主题失败:", error);
|
||||
}
|
||||
return "light"; // 默认返回 light
|
||||
}
|
||||
|
||||
/* 初始化主题 */
|
||||
function initTheme() {
|
||||
// 如果用户已手动设置且不跟随系统,保持当前主题
|
||||
if (hasUserSet.value && !followSystem.value) {
|
||||
console.log("使用用户设置的主题:", theme.value);
|
||||
setNavigationBarColor();
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取系统主题
|
||||
const systemTheme = getSystemTheme();
|
||||
|
||||
// 如果是首次启动或跟随系统,使用系统主题
|
||||
if (!hasUserSet.value || followSystem.value) {
|
||||
theme.value = systemTheme;
|
||||
if (!hasUserSet.value) {
|
||||
followSystem.value = true;
|
||||
console.log("首次启动,使用系统主题:", theme.value);
|
||||
} else {
|
||||
console.log("跟随系统主题:", theme.value);
|
||||
}
|
||||
}
|
||||
|
||||
setNavigationBarColor();
|
||||
}
|
||||
|
||||
/* 打开主题色选择 */
|
||||
function openThemeColorPicker() {
|
||||
showThemeColorSheet.value = true;
|
||||
}
|
||||
|
||||
/* 关闭主题色选择 */
|
||||
function closeThemeColorPicker() {
|
||||
showThemeColorSheet.value = false;
|
||||
}
|
||||
|
||||
/* 选择主题色 */
|
||||
function selectThemeColor(option: ThemeColorOption) {
|
||||
setCurrentThemeColor(option);
|
||||
closeThemeColorPicker();
|
||||
}
|
||||
|
||||
// 检查函数是否存在的工具函数
|
||||
const isFunction = (fn: any): boolean => typeof fn === "function";
|
||||
|
||||
onBeforeMount(() => {
|
||||
initTheme();
|
||||
if (isFunction(uni.onThemeChange)) {
|
||||
uni.onThemeChange((res) => {
|
||||
toggleTheme(res.theme);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (isFunction(uni.offThemeChange)) {
|
||||
uni.offThemeChange((res) => {
|
||||
toggleTheme(res.theme);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
theme: computed(() => theme.value),
|
||||
isDark,
|
||||
followSystem: computed(() => followSystem.value),
|
||||
hasUserSet: computed(() => hasUserSet.value),
|
||||
currentThemeColor: computed(() => currentThemeColor.value),
|
||||
showThemeColorSheet,
|
||||
themeVars,
|
||||
themeColorOptions,
|
||||
initTheme,
|
||||
toggleTheme,
|
||||
setFollowSystem,
|
||||
openThemeColorPicker,
|
||||
closeThemeColorPicker,
|
||||
selectThemeColor,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* 微信授权服务
|
||||
* 处理微信登录授权、获取用户信息等功能
|
||||
*/
|
||||
|
||||
import { ref } from "vue";
|
||||
import { getAccessToken } from "@/utils/auth";
|
||||
|
||||
export function useWechat() {
|
||||
// 定义微信授权状态
|
||||
const authState = ref({
|
||||
isLogining: false,
|
||||
authDenied: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取微信登录凭证code
|
||||
* @returns Promise 返回登录凭证code
|
||||
*/
|
||||
const getLoginCode = (): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.login({
|
||||
provider: "weixin",
|
||||
success: (res) => {
|
||||
if (res.code) {
|
||||
resolve(res.code);
|
||||
} else {
|
||||
reject(new Error("获取微信登录凭证失败"));
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
reject(new Error("当前环境不支持微信登录"));
|
||||
// #endif
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取微信用户手机号
|
||||
* @param e 微信授权返回的事件对象
|
||||
* @returns Promise 返回包含手机号加密数据的对象
|
||||
*/
|
||||
const getPhoneNumber = (
|
||||
e: any
|
||||
): Promise<{ code: string; encryptedData?: string; iv?: string }> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
authState.value.isLogining = true;
|
||||
|
||||
// 判断授权是否成功
|
||||
if (e.detail.errMsg !== "getPhoneNumber:ok") {
|
||||
authState.value.isLogining = false;
|
||||
authState.value.authDenied = true;
|
||||
reject(new Error("用户拒绝授权"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取登录凭证code
|
||||
getLoginCode()
|
||||
.then((code) => {
|
||||
// 在微信小程序环境下,可以获取encryptedData和iv
|
||||
// #ifdef MP-WEIXIN
|
||||
resolve({
|
||||
code,
|
||||
encryptedData: e.detail.encryptedData,
|
||||
iv: e.detail.iv,
|
||||
});
|
||||
// #endif
|
||||
|
||||
// 其他环境或新版本接口
|
||||
// #ifndef MP-WEIXIN
|
||||
resolve({
|
||||
code,
|
||||
// 新版本接口在e.detail.code中包含手机号获取凭证
|
||||
...(e.detail.code ? { phoneCode: e.detail.code } : {}),
|
||||
});
|
||||
// #endif
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
})
|
||||
.finally(() => {
|
||||
authState.value.isLogining = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查会话有效性
|
||||
* @returns Promise 返回会话是否有效
|
||||
*/
|
||||
const checkSession = (): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
const token = getAccessToken();
|
||||
|
||||
if (!token) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用后端接口验证token有效性
|
||||
uni.request({
|
||||
url: "/api/v1/auth/check-session",
|
||||
method: "GET",
|
||||
header: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
success: (res: any) => {
|
||||
if (res.statusCode === 200 && res.data.valid) {
|
||||
resolve(true);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
resolve(false);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取用户头像昵称
|
||||
* 注意:此接口已于2021年弃用,仅作为兼容保留
|
||||
* 推荐使用button组件的open-type="chooseAvatar"让用户选择头像
|
||||
*/
|
||||
const getUserProfile = (): Promise<any> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.getUserProfile({
|
||||
desc: "用于完善用户资料",
|
||||
success: (res) => {
|
||||
resolve(res.userInfo);
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
reject(new Error("当前环境不支持获取用户信息"));
|
||||
// #endif
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
authState,
|
||||
getLoginCode,
|
||||
getPhoneNumber,
|
||||
checkSession,
|
||||
getUserProfile,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* 常量统一导出
|
||||
*/
|
||||
|
||||
// 存储相关常量
|
||||
export * from "./storage.constant";
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* 存储键常量统一管理
|
||||
* 包括 localStorage、sessionStorage 等各种存储的键名
|
||||
*/
|
||||
|
||||
// 🔐 用户认证相关
|
||||
export const ACCESS_TOKEN_KEY = "access_token";
|
||||
export const REFRESH_TOKEN_KEY = "refresh_token";
|
||||
|
||||
// 📊 用户缓存相关
|
||||
export const USER_INFO_KEY = "user_info";
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* API响应码枚举
|
||||
*/
|
||||
export const enum ApiCode {
|
||||
/**
|
||||
* 成功
|
||||
*/
|
||||
SUCCESS = "00000",
|
||||
|
||||
/**
|
||||
* 通用错误
|
||||
*/
|
||||
ERROR = "B0001",
|
||||
|
||||
/**
|
||||
* 令牌无效或过期
|
||||
*/
|
||||
TOKEN_INVALID = "A0230",
|
||||
|
||||
/**
|
||||
* 令牌已过期
|
||||
*/
|
||||
TOKEN_EXPIRED = "A0231",
|
||||
|
||||
/**
|
||||
* 未授权访问
|
||||
*/
|
||||
UNAUTHORIZED = "A0232",
|
||||
|
||||
/**
|
||||
* 禁止访问
|
||||
*/
|
||||
FORBIDDEN = "A0233",
|
||||
|
||||
/**
|
||||
* 参数校验失败
|
||||
*/
|
||||
PARAM_INVALID = "A0400",
|
||||
|
||||
/**
|
||||
* 资源不存在
|
||||
*/
|
||||
NOT_FOUND = "A0404",
|
||||
|
||||
/**
|
||||
* 服务器内部错误
|
||||
*/
|
||||
INTERNAL_ERROR = "B0500",
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module "*.vue" {
|
||||
import type { DefineComponent } from "vue";
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<script lang="ts" setup>
|
||||
const { theme, themeVars } = useTheme();
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
virtualHost: true,
|
||||
styleIsolation: "shared",
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<wd-config-provider :theme-vars="themeVars" :theme="theme" :custom-class="`page-wraper ${theme}`">
|
||||
<slot />
|
||||
<wd-notify />
|
||||
<wd-toast />
|
||||
<wd-message-box />
|
||||
</wd-config-provider>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-wraper {
|
||||
box-sizing: border-box;
|
||||
min-height: calc(100vh - var(--window-top));
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.wot-theme-dark.page-wraper {
|
||||
background: #222;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<wd-config-provider :theme-vars="themeVars" :custom-class="`page-wraper ${theme}`" :theme="theme">
|
||||
<slot />
|
||||
<wd-tabbar
|
||||
:model-value="activeTabbar.name"
|
||||
placeholder
|
||||
bordered
|
||||
safe-area-inset-bottom
|
||||
fixed
|
||||
@change="handleTabbarChange"
|
||||
>
|
||||
<wd-tabbar-item
|
||||
v-for="(item, index) in tabbarList"
|
||||
:key="index"
|
||||
:name="item.name"
|
||||
:value="getTabbarItemValue(item.name)"
|
||||
:title="item.title"
|
||||
:icon="item.icon"
|
||||
/>
|
||||
</wd-tabbar>
|
||||
<wd-notify />
|
||||
<wd-toast />
|
||||
<wd-message-box />
|
||||
</wd-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter, useRoute } from "uni-mini-router";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import { useTabbar } from "@/composables/useTabbar";
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const { themeVars, theme } = useTheme();
|
||||
const { activeTabbar, getTabbarItemValue, setTabbarItemActive, tabbarList } = useTabbar();
|
||||
|
||||
function handleTabbarChange({ value }: { value: string }) {
|
||||
setTabbarItemActive(value);
|
||||
router.pushTab({ name: value });
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// #ifdef APP-PLUS
|
||||
uni.hideTabBar();
|
||||
// #endif
|
||||
nextTick(() => {
|
||||
if (route.name && route.name !== activeTabbar.value.name) {
|
||||
setTabbarItemActive(route.name);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
virtualHost: true,
|
||||
styleIsolation: "shared",
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page-wraper {
|
||||
box-sizing: border-box;
|
||||
min-height: calc(100vh - var(--window-top));
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.wot-theme-dark.page-wraper {
|
||||
background: #222;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,19 @@
|
||||
import { createSSRApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
|
||||
import "uno.css";
|
||||
import "@/styles/index.scss";
|
||||
|
||||
import { setupStore } from "@/store";
|
||||
import router from "./router";
|
||||
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App);
|
||||
|
||||
setupStore(app);
|
||||
app.use(router);
|
||||
|
||||
return {
|
||||
app,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"name": "vue-uniapp-template",
|
||||
"appid": "",
|
||||
"description": "有来移动端跨端解决方案开发模板",
|
||||
"versionName": "1.0.0",
|
||||
"versionCode": "100",
|
||||
"transformPx": false,
|
||||
"app-plus": {
|
||||
"usingComponents": true,
|
||||
"nvueStyleCompiler": "uni-app",
|
||||
"compilerVersion": 3,
|
||||
"splashscreen": {
|
||||
"alwaysShowBeforeRender": true,
|
||||
"waiting": true,
|
||||
"autoclose": true,
|
||||
"delay": 0
|
||||
},
|
||||
"modules": {},
|
||||
"distribute": {
|
||||
"android": {
|
||||
"permissions": [
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||
]
|
||||
},
|
||||
"ios": {},
|
||||
"sdkConfigs": {}
|
||||
}
|
||||
},
|
||||
"quickapp": {},
|
||||
"mp-weixin": {
|
||||
"appid": "wx99a151dc43d2637b",
|
||||
"setting": {
|
||||
"urlCheck": false
|
||||
},
|
||||
"usingComponents": true,
|
||||
"darkmode": true,
|
||||
"themeLocation": "theme.json"
|
||||
},
|
||||
"mp-alipay": {
|
||||
"usingComponents": true
|
||||
},
|
||||
"mp-baidu": {
|
||||
"usingComponents": true
|
||||
},
|
||||
"mp-toutiao": {
|
||||
"usingComponents": true
|
||||
},
|
||||
"uniStatistics": {
|
||||
"enable": false
|
||||
},
|
||||
"vueVersion": "3",
|
||||
"h5": {
|
||||
"darkmode": true,
|
||||
"themeLocation": "theme.json"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"type": "home",
|
||||
"name": "home",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
},
|
||||
"layout": "tabbar"
|
||||
},
|
||||
{
|
||||
"path": "pages/login/index",
|
||||
"type": "page"
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/index",
|
||||
"type": "page",
|
||||
"name": "mine",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
},
|
||||
"layout": "tabbar"
|
||||
},
|
||||
{
|
||||
"path": "pages/work/index",
|
||||
"type": "page",
|
||||
"name": "work",
|
||||
"style": {
|
||||
"navigationBarTitleText": "工作台"
|
||||
},
|
||||
"meta": {
|
||||
"requireAuth": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/about/index",
|
||||
"type": "page"
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/faq/index",
|
||||
"type": "page"
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/feedback/index",
|
||||
"type": "page"
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/profile/complete-profile",
|
||||
"type": "page"
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/profile/index",
|
||||
"type": "page"
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/settings/index",
|
||||
"type": "page"
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/settings/account/index",
|
||||
"type": "page"
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/settings/agreement/index",
|
||||
"type": "page"
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/settings/network/index",
|
||||
"type": "page"
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/settings/theme/index",
|
||||
"type": "page"
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarBackgroundColor": "@navBgColor",
|
||||
"navigationBarTextStyle": "@navTxtStyle",
|
||||
"navigationBarTitleText": "Wot-Demo",
|
||||
"backgroundColor": "@bgColor",
|
||||
"backgroundTextStyle": "@bgTxtStyle",
|
||||
"backgroundColorTop": "@bgColorTop",
|
||||
"backgroundColorBottom": "@bgColorBottom",
|
||||
"enablePullDownRefresh": false,
|
||||
"onReachBottomDistance": 50,
|
||||
"animationType": "pop-in",
|
||||
"animationDuration": 300
|
||||
},
|
||||
"tabBar": {
|
||||
"custom": true,
|
||||
"customize": true,
|
||||
"overlay": true,
|
||||
"height": "0",
|
||||
"color": "@tabColor",
|
||||
"selectedColor": "@tabSelectedColor",
|
||||
"backgroundColor": "@tabBgColor",
|
||||
"borderStyle": "@tabBorderStyle",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/index/index"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/mine/index"
|
||||
}
|
||||
]
|
||||
},
|
||||
"__esModule": true,
|
||||
"subPackages": []
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
<template>
|
||||
<view class="app-container">
|
||||
<wd-swiper
|
||||
v-model:current="current"
|
||||
:list="swiperList"
|
||||
autoplay
|
||||
@click="handleClick"
|
||||
@change="onChange"
|
||||
/>
|
||||
|
||||
<!-- 快捷导航 -->
|
||||
<wd-grid clickable :column="4" class="mt-2">
|
||||
<wd-grid-item
|
||||
v-for="(item, index) in navList"
|
||||
:key="index"
|
||||
use-slot
|
||||
@click="handleNavClick(item)"
|
||||
>
|
||||
<view class="p-2">
|
||||
<image class="w-72rpx h-72rpx rounded-8rpx" :src="item.icon" />
|
||||
</view>
|
||||
<view class="text-sm text-center">{{ item.title }}</view>
|
||||
</wd-grid-item>
|
||||
</wd-grid>
|
||||
|
||||
<!-- 通知公告 -->
|
||||
<wd-notice-bar
|
||||
text="vue-uniapp-template 是一个基于 Vue3 + UniApp 的前端模板项目,提供了一套完整的前端解决方案,包括登录、权限、字典、接口请求、状态管理、页面布局、组件封装等功能。"
|
||||
color="#34D19D"
|
||||
type="info"
|
||||
>
|
||||
<template #prefix>
|
||||
<wd-tag color="#FAA21E" bg-color="#FAA21E" plain custom-style="margin-right:10rpx">
|
||||
通知公告
|
||||
</wd-tag>
|
||||
</template>
|
||||
</wd-notice-bar>
|
||||
|
||||
<!-- 数据统计 -->
|
||||
<wd-grid :column="2" :gutter="2">
|
||||
<wd-grid-item use-slot custom-class="h-80px">
|
||||
<view class="flex justify-start pl-5">
|
||||
<view class="flex items-center">
|
||||
<image class="w-80rpx h-80rpx rounded-8rpx" src="/static/icons/visitor.png" />
|
||||
<view class="ml-5 text-left">
|
||||
<view class="font-bold">访客数</view>
|
||||
<view class="mt-2">{{ visitStatsData.todayUvCount }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</wd-grid-item>
|
||||
<wd-grid-item use-slot custom-class="h-80px">
|
||||
<view class="flex justify-start pl-5">
|
||||
<view class="flex items-center">
|
||||
<image class="w-80rpx h-80rpx rounded-8rpx" src="/static/icons/browser.png" />
|
||||
<view class="ml-5 text-left">
|
||||
<view class="font-bold">浏览量</view>
|
||||
<view class="mt-2">{{ visitStatsData.todayPvCount }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</wd-grid-item>
|
||||
</wd-grid>
|
||||
|
||||
<wd-card>
|
||||
<template #title>
|
||||
<view class="flex justify-between items-center">
|
||||
<view>访问趋势</view>
|
||||
<view>
|
||||
<wd-radio-group
|
||||
v-model="recentDaysRange"
|
||||
shape="button"
|
||||
inline
|
||||
@change="handleDataRangeChange"
|
||||
>
|
||||
<wd-radio :value="7">近7天</wd-radio>
|
||||
<wd-radio :value="15">近15天</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view class="w-full h-300px mb-40rpx">
|
||||
<qiun-data-charts type="area" :chartData="chartData" :opts="chartOpts" />
|
||||
</view>
|
||||
</wd-card>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { dayjs } from "wot-design-uni";
|
||||
|
||||
// 定义访问统计数据类型
|
||||
interface VisitStatsVO {
|
||||
todayUvCount: number;
|
||||
uvGrowthRate: number;
|
||||
totalUvCount: number;
|
||||
todayPvCount: number;
|
||||
pvGrowthRate: number;
|
||||
totalPvCount: number;
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
const current = ref<number>(0);
|
||||
|
||||
const visitStatsData = ref<VisitStatsVO>({
|
||||
todayUvCount: 1234,
|
||||
uvGrowthRate: 15.6,
|
||||
totalUvCount: 45678,
|
||||
todayPvCount: 5678,
|
||||
pvGrowthRate: 23.4,
|
||||
totalPvCount: 123456,
|
||||
});
|
||||
|
||||
// 图表数据
|
||||
const chartData = ref({});
|
||||
|
||||
const chartOpts = ref({
|
||||
padding: [20, 0, 20, 0],
|
||||
xAxis: {
|
||||
fontSize: 10,
|
||||
rotateLabel: true,
|
||||
rotateAngle: 30,
|
||||
},
|
||||
yAxis: {
|
||||
disabled: true,
|
||||
},
|
||||
extra: {
|
||||
area: {
|
||||
type: "curve",
|
||||
opacity: 0.2,
|
||||
addLine: true,
|
||||
width: 2,
|
||||
gradient: true,
|
||||
activeType: "hollow",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 日期范围
|
||||
const recentDaysRange = ref(7);
|
||||
|
||||
const swiperList = ref(["https://www.youlai.tech/storage/blog/banner9.png"]);
|
||||
|
||||
// 快捷导航列表
|
||||
const navList = reactive([
|
||||
{
|
||||
icon: "/static/icons/user.png",
|
||||
title: "用户管理",
|
||||
url: "/pages/work/index",
|
||||
prem: "sys:user:query",
|
||||
},
|
||||
{
|
||||
icon: "/static/icons/role.png",
|
||||
title: "角色管理",
|
||||
url: "/pages/work/index",
|
||||
prem: "sys:role:query",
|
||||
},
|
||||
{
|
||||
icon: "/static/icons/notice.png",
|
||||
title: "通知公告",
|
||||
url: "/pages/work/index",
|
||||
prem: "sys:notice:query",
|
||||
},
|
||||
{
|
||||
icon: "/static/icons/setting.png",
|
||||
title: "系统配置",
|
||||
url: "/pages/work/index",
|
||||
prem: "sys:config:query",
|
||||
},
|
||||
]);
|
||||
|
||||
// 处理导航点击
|
||||
function handleNavClick(item: any) {
|
||||
// 使用路由系统进行导航,这样会触发路由守卫
|
||||
router.push({ path: item.url });
|
||||
}
|
||||
|
||||
// 生成静态的访问趋势数据
|
||||
const generateStaticTrendData = (days: number) => {
|
||||
const dates = [];
|
||||
const ipList = [];
|
||||
const pvList = [];
|
||||
|
||||
const today = new Date();
|
||||
|
||||
for (let i = days - 1; i >= 0; i--) {
|
||||
const date = new Date(today);
|
||||
date.setDate(today.getDate() - i);
|
||||
dates.push(dayjs(date).format("MM-DD"));
|
||||
|
||||
// 生成模拟数据
|
||||
ipList.push(Math.floor(Math.random() * 500) + 200);
|
||||
pvList.push(Math.floor(Math.random() * 1000) + 500);
|
||||
}
|
||||
|
||||
return {
|
||||
dates,
|
||||
ipList,
|
||||
pvList,
|
||||
};
|
||||
};
|
||||
|
||||
function handleClick(e: any) {
|
||||
console.log(e);
|
||||
}
|
||||
function onChange(e: any) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
// 加载访问统计数据(使用静态数据)
|
||||
const loadVisitStatsData = async () => {
|
||||
// 模拟异步加载
|
||||
setTimeout(() => {
|
||||
visitStatsData.value = {
|
||||
todayUvCount: 1234,
|
||||
uvGrowthRate: 15.6,
|
||||
totalUvCount: 45678,
|
||||
todayPvCount: 5678,
|
||||
pvGrowthRate: 23.4,
|
||||
totalPvCount: 123456,
|
||||
};
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// 加载访问趋势数据(使用静态数据)
|
||||
const loadVisitTrendData = () => {
|
||||
// 模拟异步加载
|
||||
setTimeout(() => {
|
||||
const data = generateStaticTrendData(recentDaysRange.value);
|
||||
|
||||
const res = {
|
||||
categories: data.dates,
|
||||
series: [
|
||||
{
|
||||
name: "访客数(UV)",
|
||||
data: data.ipList,
|
||||
},
|
||||
{
|
||||
name: "浏览量(PV)",
|
||||
data: data.pvList,
|
||||
},
|
||||
],
|
||||
};
|
||||
chartData.value = JSON.parse(JSON.stringify(res));
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// 数据范围变化
|
||||
const handleDataRangeChange = ({ value }: { value: number }) => {
|
||||
console.log("handleDataRangeChange", value);
|
||||
recentDaysRange.value = value;
|
||||
loadVisitTrendData();
|
||||
};
|
||||
|
||||
onReady(() => {
|
||||
loadVisitStatsData();
|
||||
loadVisitTrendData();
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
// 确保 tabbar 状态正确
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length > 0) {
|
||||
const currentPage = pages[pages.length - 1];
|
||||
if (currentPage.route === "pages/index/index") {
|
||||
// 通过事件通知 tabbar 布局更新状态
|
||||
uni.$emit("updateTabbar", "index");
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"name": "home",
|
||||
"style": { "navigationStyle": "custom" },
|
||||
"layout": "tabbar"
|
||||
}
|
||||
</route>
|
||||
<style setup lang="scss"></style>
|
||||
@@ -0,0 +1,626 @@
|
||||
<template>
|
||||
<view class="app-container">
|
||||
<!-- 背景图 -->
|
||||
<image src="/static/images/login-bg.svg" mode="aspectFill" class="login-bg" />
|
||||
|
||||
<!-- Logo和标题区域 -->
|
||||
<view class="header"></view>
|
||||
|
||||
<view class="login-card">
|
||||
<view class="form-wrap">
|
||||
<!-- 账号密码登录表单 -->
|
||||
<wd-form v-if="loginType === 'account'" ref="loginFormRef" :model="loginFormData">
|
||||
<!-- 用户名输入框 -->
|
||||
<view class="form-item">
|
||||
<wd-icon
|
||||
name="user"
|
||||
size="22"
|
||||
:color="isDarkMode ? '#7AC5FF' : '#333'"
|
||||
class="input-icon"
|
||||
/>
|
||||
<input
|
||||
v-model="loginFormData.username"
|
||||
class="form-input input-transparent"
|
||||
placeholder="请输入用户名"
|
||||
placeholder-class="input-placeholder"
|
||||
/>
|
||||
</view>
|
||||
<view class="divider"></view>
|
||||
|
||||
<!-- 密码输入框 -->
|
||||
<view class="form-item">
|
||||
<wd-icon
|
||||
name="lock-on"
|
||||
size="22"
|
||||
:color="isDarkMode ? '#7AC5FF' : '#333'"
|
||||
class="input-icon"
|
||||
/>
|
||||
<input
|
||||
v-model="loginFormData.password"
|
||||
class="form-input input-transparent"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
placeholder="请输入密码"
|
||||
placeholder-class="input-placeholder"
|
||||
/>
|
||||
<wd-icon
|
||||
:name="showPassword ? 'eye-open' : 'eye-close'"
|
||||
size="18"
|
||||
:color="isDarkMode ? '#7AC5FF' : '#9ca3af'"
|
||||
class="eye-icon"
|
||||
@click="showPassword = !showPassword"
|
||||
/>
|
||||
</view>
|
||||
<view class="divider"></view>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<button
|
||||
class="login-btn"
|
||||
:disabled="loading"
|
||||
:style="loading ? 'opacity: 0.7;' : ''"
|
||||
@click="handleAccountLogin"
|
||||
>
|
||||
{{ loading ? "登录中..." : "账号登录" }}
|
||||
</button>
|
||||
|
||||
<!-- 切换登录方式 -->
|
||||
<view class="switch-login-type" @click="loginType = 'phone'">
|
||||
<text>使用手机号一键登录</text>
|
||||
<wd-icon name="arrow-right" size="12" />
|
||||
</view>
|
||||
</wd-form>
|
||||
|
||||
<!-- 手机号登录 -->
|
||||
<view v-else class="phone-login-form">
|
||||
<view class="phone-login-title">微信一键登录</view>
|
||||
<view class="phone-login-subtitle">授权后将获取您的手机号</view>
|
||||
|
||||
<button
|
||||
class="wechat-phone-btn"
|
||||
:disabled="loading"
|
||||
open-type="getPhoneNumber"
|
||||
@getphonenumber="handleWechatPhoneLogin"
|
||||
>
|
||||
<wd-icon name="weixin" size="24" color="#ffffff" />
|
||||
<text>微信一键登录</text>
|
||||
</button>
|
||||
|
||||
<!-- 切换登录方式 -->
|
||||
<view class="switch-login-type" @click="loginType = 'account'">
|
||||
<text>使用账号密码登录</text>
|
||||
<wd-icon name="arrow-right" size="12" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 其他登录方式 -->
|
||||
<view class="other-login">
|
||||
<view class="other-login-title">
|
||||
<view class="line"></view>
|
||||
<text class="text">其他登录方式</text>
|
||||
<view class="line"></view>
|
||||
</view>
|
||||
|
||||
<view class="wechat-login" @click="handleWechatLogin">
|
||||
<view class="wechat-icon-wrapper">
|
||||
<image src="/static/icons/weixin.png" class="wechat-icon" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部协议 -->
|
||||
<view class="agreement">
|
||||
<text class="text">登录即同意</text>
|
||||
<text class="link" @click="navigateToUserAgreement">《用户协议》</text>
|
||||
<text class="text">和</text>
|
||||
<text class="link" @click="navigateToPrivacy">《隐私政策》</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<wd-toast />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { type LoginData } from "@/api/auth";
|
||||
import { useUserStore } from "@/store/modules/user.store";
|
||||
import { useToast } from "wot-design-uni";
|
||||
import { useWechat } from "@/composables/useWechat";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import { computed, onMounted } from "vue";
|
||||
|
||||
const loginFormRef = ref();
|
||||
const toast = useToast();
|
||||
const loading = ref(false);
|
||||
const userStore = useUserStore();
|
||||
const showPassword = ref(false);
|
||||
const loginType = ref<"account" | "phone">("account");
|
||||
const { authState, getLoginCode, getPhoneNumber } = useWechat();
|
||||
const { theme } = useTheme();
|
||||
|
||||
// 是否暗黑模式
|
||||
const isDarkMode = computed(() => theme.value === "dark");
|
||||
|
||||
// 登录表单数据
|
||||
const loginFormData = ref<LoginData>({
|
||||
username: "admin",
|
||||
password: "123456",
|
||||
});
|
||||
|
||||
// 获取重定向参数
|
||||
const redirect = ref("/pages/index/index");
|
||||
onLoad((options) => {
|
||||
if (options && options.redirect) {
|
||||
redirect.value = decodeURIComponent(options.redirect);
|
||||
}
|
||||
});
|
||||
|
||||
// 强制清除输入框背景色
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
const inputs = document.querySelectorAll("input");
|
||||
inputs.forEach((input) => {
|
||||
input.style.backgroundColor = "transparent";
|
||||
input.style.boxShadow = "none";
|
||||
});
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// 账号密码登录处理
|
||||
const handleAccountLogin = () => {
|
||||
if (loading.value) return;
|
||||
|
||||
// 表单验证
|
||||
if (!loginFormData.value.username) {
|
||||
toast.error("请输入用户名");
|
||||
return;
|
||||
}
|
||||
if (!loginFormData.value.password) {
|
||||
toast.error("请输入密码");
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
userStore
|
||||
.login(loginFormData.value)
|
||||
.then(() => userStore.getInfo())
|
||||
.then(() => {
|
||||
toast.success("登录成功");
|
||||
|
||||
// 账号密码登录直接跳转到重定向页面
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({
|
||||
url: redirect.value,
|
||||
});
|
||||
}, 1000);
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error?.message || "登录失败");
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
// 微信一键登录(通过手机号)
|
||||
const handleWechatPhoneLogin = async (e: any) => {
|
||||
if (loading.value || authState.value.isLogining) return;
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
// 获取手机号加密数据
|
||||
const phoneData = await getPhoneNumber(e);
|
||||
|
||||
// 调用登录接口
|
||||
const result: any = await userStore.loginWithWxPhone(phoneData);
|
||||
|
||||
// 获取用户信息
|
||||
await userStore.getInfo();
|
||||
toast.success("登录成功");
|
||||
|
||||
// 检查是否为新用户或信息不完整
|
||||
if (result.isNewUser || !userStore.isUserInfoComplete()) {
|
||||
// 跳转到完善信息页面
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mine/profile/complete-profile?redirect=${encodeURIComponent(redirect.value)}`,
|
||||
});
|
||||
}, 1000);
|
||||
} else {
|
||||
// 跳转到重定向页面
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({
|
||||
url: redirect.value,
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.message === "用户拒绝授权") {
|
||||
toast.error("您已拒绝授权获取手机号");
|
||||
} else {
|
||||
toast.error(error?.message || "登录失败");
|
||||
}
|
||||
console.error("微信手机号登录失败:", error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 微信授权登录处理
|
||||
const handleWechatLogin = async () => {
|
||||
if (loading.value) return;
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
// #ifdef MP-WEIXIN
|
||||
// 获取微信登录的临时 code
|
||||
const code = await getLoginCode();
|
||||
|
||||
// 尝试使用微信授权登录接口
|
||||
const result: any = await userStore.loginWithWxCode(code);
|
||||
|
||||
// 获取用户信息
|
||||
await userStore.getInfo();
|
||||
toast.success("登录成功");
|
||||
|
||||
// 检查用户信息是否完整
|
||||
if (result.isNewUser || !userStore.isUserInfoComplete()) {
|
||||
// 如果信息不完整,跳转到完善信息页面
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mine/profile/complete-profile?redirect=${encodeURIComponent(redirect.value)}`,
|
||||
});
|
||||
}, 1000);
|
||||
} else {
|
||||
// 否则直接跳转到重定向页面
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({
|
||||
url: redirect.value,
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
toast.error("当前环境不支持微信登录");
|
||||
// #endif
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || "微信登录失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 跳转到用户协议页面
|
||||
const navigateToUserAgreement = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/mine/settings/agreement/index",
|
||||
});
|
||||
};
|
||||
|
||||
// 跳转到隐私政策页面
|
||||
const navigateToPrivacy = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/mine/settings/privacy/index",
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
background-color: var(--wot-color-bg-container);
|
||||
}
|
||||
|
||||
.login-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: 120rpx;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 10rpx;
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 28rpx;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 90%;
|
||||
margin-top: 80rpx;
|
||||
overflow: hidden;
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
.wot-theme-dark & {
|
||||
background-color: rgba(31, 31, 31, 0.95);
|
||||
box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.form-wrap {
|
||||
padding: 40rpx;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx 0;
|
||||
background-color: transparent;
|
||||
|
||||
.wot-theme-dark & {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
/* 强制所有输入元素为透明背景 */
|
||||
input,
|
||||
.form-input,
|
||||
.input-transparent {
|
||||
flex: 1;
|
||||
height: 60rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 60rpx;
|
||||
color: #333;
|
||||
background-color: transparent !important;
|
||||
-webkit-box-shadow: none !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
.wot-theme-dark & {
|
||||
color: #f5f5f5;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 修复webkit浏览器自动填充问题 */
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus,
|
||||
input:-webkit-autofill:active {
|
||||
caret-color: var(--wot-color-text);
|
||||
background-color: transparent !important;
|
||||
-webkit-box-shadow: 0 0 0 1000px transparent inset !important;
|
||||
transition: background-color 5000s;
|
||||
-webkit-text-fill-color: var(--wot-color-text) !important;
|
||||
|
||||
.wot-theme-dark & {
|
||||
-webkit-text-fill-color: #f5f5f5 !important;
|
||||
background-color: transparent !important;
|
||||
-webkit-box-shadow: 0 0 0 1000px rgba(31, 31, 31, 0) inset !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 尝试通过更强的选择器覆盖自动填充 */
|
||||
.form-item input,
|
||||
input.form-input,
|
||||
input.input-transparent {
|
||||
-webkit-appearance: none;
|
||||
background: none !important;
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.clear-icon,
|
||||
.eye-icon {
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
margin: 0;
|
||||
background-color: rgba(0, 0, 0, 0.06);
|
||||
|
||||
.wot-theme-dark & {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
margin-top: 60rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
line-height: 88rpx;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
background-color: var(--wot-color-theme);
|
||||
border: none;
|
||||
border-radius: 44rpx;
|
||||
}
|
||||
|
||||
.switch-login-type {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 30rpx;
|
||||
font-size: 26rpx;
|
||||
color: var(--wot-color-theme);
|
||||
}
|
||||
|
||||
.phone-login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 40rpx 0;
|
||||
}
|
||||
|
||||
.phone-login-title {
|
||||
margin-bottom: 16rpx;
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
|
||||
.wot-theme-dark & {
|
||||
color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
.phone-login-subtitle {
|
||||
margin-bottom: 60rpx;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
|
||||
.wot-theme-dark & {
|
||||
color: #c0c0c0;
|
||||
}
|
||||
}
|
||||
|
||||
.wechat-phone-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
font-size: 32rpx;
|
||||
color: #ffffff;
|
||||
background-color: #07c160;
|
||||
border: none;
|
||||
border-radius: 44rpx;
|
||||
|
||||
text {
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.other-login {
|
||||
margin-top: 60rpx;
|
||||
}
|
||||
|
||||
.other-login-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.line {
|
||||
width: 80rpx;
|
||||
height: 1rpx;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
|
||||
.wot-theme-dark & {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
margin: 0 20rpx;
|
||||
font-size: 26rpx;
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
|
||||
.wot-theme-dark & {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
.wechat-login {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.wechat-icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
background-color: #07c160;
|
||||
border-radius: 50%;
|
||||
|
||||
.wot-theme-dark & {
|
||||
box-shadow: 0 4rpx 12rpx rgba(7, 193, 96, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.wechat-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
.agreement {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 60rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: var(--wot-color-theme);
|
||||
}
|
||||
|
||||
.input-placeholder {
|
||||
color: rgba(0, 0, 0, 0.3);
|
||||
|
||||
.wot-theme-dark & {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
/* 加强输入框透明度 - 暗黑模式特别处理 */
|
||||
.wot-theme-dark {
|
||||
:deep(input) {
|
||||
background: none !important;
|
||||
background-color: transparent !important;
|
||||
-webkit-box-shadow: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.form-input,
|
||||
input {
|
||||
background: none !important;
|
||||
background-color: transparent !important;
|
||||
background-image: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 修复Android Chrome输入框背景色问题 */
|
||||
@supports (-webkit-appearance: none) {
|
||||
input {
|
||||
-webkit-appearance: none;
|
||||
background: transparent !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<view class="app-container">
|
||||
<wd-navbar title="关于我们" left-arrow @click-left="handleBack" />
|
||||
|
||||
<!-- 顶部信息区域 -->
|
||||
<wd-card custom-style="margin-top: 20rpx">
|
||||
<view class="flex items-center p-2">
|
||||
<wd-img width="60px" height="60px" src="/static/logo.png" mode="aspectFit" class="mr-4" />
|
||||
<view class="flex-1">
|
||||
<text class="text-lg font-bold block mb-1">vue-uniapp-template</text>
|
||||
<text class="text-xs text-gray-500">版本 {{ version }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</wd-card>
|
||||
|
||||
<!-- 公司信息区域 -->
|
||||
<wd-card custom-style="margin-top: 20rpx">
|
||||
<view class="p-4">
|
||||
<view class="flex items-center mb-3">
|
||||
<wd-icon name="company" size="18" class="mr-2" />
|
||||
<text class="text-base font-medium">有来开源组织</text>
|
||||
</view>
|
||||
<text class="text-sm text-gray-500 block pl-6">专注于快速构建和高效开发的应用解决方案</text>
|
||||
</view>
|
||||
</wd-card>
|
||||
|
||||
<!-- 项目列表 -->
|
||||
<wd-card title="优质项目" custom-style="margin-top: 20rpx">
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="vue3-element-admin" icon="desktop">
|
||||
<view slot="label" class="text-xs text-gray-500 mt-1">
|
||||
基于 Vue3 + Vite5 + TypeScript5 + Element-Plus + Pinia 构建的中后台管理模板
|
||||
</view>
|
||||
</wd-cell>
|
||||
|
||||
<wd-cell title="vue-uniapp-template" icon="mobile">
|
||||
<view slot="label" class="text-xs text-gray-500 mt-1">
|
||||
基于 uni-app + Vue 3 + TypeScript,集成多种工具的移动端应用模板
|
||||
</view>
|
||||
</wd-cell>
|
||||
|
||||
<wd-cell title="youlai-boot" icon="server">
|
||||
<view slot="label" class="text-xs text-gray-500 mt-1">
|
||||
基于 Spring Boot 3 + Vue 3 构建的前后端分离单体权限管理系统
|
||||
</view>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</wd-card>
|
||||
|
||||
<!-- 联系方式 -->
|
||||
<wd-card title="联系我们" custom-style="margin-top: 20rpx">
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="官方网站" value="www.youlai.tech" icon="link" />
|
||||
<wd-cell title="GitHub" value="github.com/youlaitech" icon="github" />
|
||||
<wd-cell title="联系邮箱" value="youlaitech@163.com" icon="mail" />
|
||||
</wd-cell-group>
|
||||
</wd-card>
|
||||
|
||||
<!-- 底部版权信息 -->
|
||||
<view class="text-center py-4">
|
||||
<text class="block text-xs text-gray-400">Copyright © {{ getYear() }} 有来开源组织</text>
|
||||
<text class="block text-xs text-gray-400 mt-1">All Rights Reserved</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const version = ref("1.0.0");
|
||||
|
||||
const handleBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
const getYear = () => {
|
||||
return new Date().getFullYear();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// #ifdef MP-WEIXIN
|
||||
version.value = uni.getSystemInfoSync().appVersion;
|
||||
// #endif
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.wd-card__header) {
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: var(--wot-color-text, #333);
|
||||
}
|
||||
|
||||
:deep(.wd-cell__icon) {
|
||||
font-size: 36rpx;
|
||||
color: var(--wot-color-theme, #409eff);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<view class="faq-container">
|
||||
<view class="wechat">
|
||||
<view class="tips">
|
||||
<text>长按关注「有来技术」公众号,获取交流群二维码。</text>
|
||||
</view>
|
||||
<view class="flex-center">
|
||||
<image
|
||||
class="w-158px h-158px"
|
||||
:show-menu-by-longpress="true"
|
||||
src="/static/images/qrcode-official.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
<view>
|
||||
<text>如果交流群的二维码过期,请加微信(</text>
|
||||
<text :user-select="true" :selectable="true">haoxianrui</text>
|
||||
<text>)并备注「前端」、「后端」或「全栈」以获取最新二维码。</text>
|
||||
</view>
|
||||
<view>
|
||||
<text>为确保交流群质量,防止营销广告人群混入,我们采取了此措施。望各位理解!</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<wd-collapse v-model="value">
|
||||
<wd-collapse-item title="开源项目issues" name="item1">
|
||||
<!-- #ifdef H5 -->
|
||||
<a href="https://gitee.com/youlaiorg/vue-uniapp-template/issues">#issues</a>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<text :user-select="true">https://gitee.com/youlaiorg/vue-uniapp-template/issues</text>
|
||||
<!-- #endif -->
|
||||
</wd-collapse-item>
|
||||
<wd-collapse-item title="小程序分包" name="item2">
|
||||
<view>
|
||||
<text>
|
||||
分包主要是因为小程序平台对主包大小有限制(微信小程序的规则是主包不超过2M,每个分包不超过2M,总体积一共不能超过20M),
|
||||
分包不需要按照业务模块来分,可以将多个业务模块放入一个分包中,直到这个分包达到小程序的大小限制才考虑下一个分包。
|
||||
uniapp的用法与微信官方文档一样,具体参见:
|
||||
</text>
|
||||
<!-- #ifdef H5 -->
|
||||
<a
|
||||
href="https://developers.weixin.qq.com/miniprogram/dev/framework/subpackages/basic.html"
|
||||
>
|
||||
微信官方文档-分包
|
||||
</a>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<text :user-select="true" :selectable="true">
|
||||
https://developers.weixin.qq.com/miniprogram/dev/framework/subpackages/basic.html
|
||||
</text>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
<view class="mt-15rpx">
|
||||
<text>
|
||||
以下是一个简单示例。以下示例中创建了两个分包,分包a中包含两个页面,分包b中包含一个页面。
|
||||
</text>
|
||||
<text class="mt-15rpx">
|
||||
请注意,如果想把分包页面中使用的组件打包到分包中,则需要将组件放入对应的分包目录下,否则组件会被打包到主包中。
|
||||
</text>
|
||||
</view>
|
||||
<view class="mt-15rpx">
|
||||
<text>目录结构:</text>
|
||||
</view>
|
||||
<rich-text :nodes="subListStr" />
|
||||
<view class="mt-15rpx">
|
||||
<text>在pages.json文件中声明分包结构:</text>
|
||||
</view>
|
||||
<rich-text :nodes="pagesStr" />
|
||||
</wd-collapse-item>
|
||||
</wd-collapse>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const value = ref<string[]>(["item1"]);
|
||||
const subListStr = ref<string>(`
|
||||
<pre style="background-color: #f9f9fa"><code>
|
||||
|-- components //主包组件目录
|
||||
|-- pages //主包页面目录
|
||||
| |-- index
|
||||
|-- sub-pkg-a
|
||||
| |-- components //分包组件目录
|
||||
| |-- pages //分包页面目录
|
||||
| | |-- cat
|
||||
| | |-- dog
|
||||
|-- sub-pkg-b
|
||||
| |-- components //分包组件目录
|
||||
| |-- pages //分包页面目录
|
||||
| | |-- apple</code></pre>
|
||||
`);
|
||||
|
||||
const pagesStr = ref<string>(`<pre style="background-color: #f9f9fa"><code>
|
||||
{
|
||||
"pages":[
|
||||
{
|
||||
"path": "pages/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "主页"
|
||||
}
|
||||
}
|
||||
],
|
||||
"subPackages": [
|
||||
{
|
||||
"root": "sub-pkg-a",
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/cat",
|
||||
"style": {
|
||||
"navigationBarTitleText": "cat"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/dog",
|
||||
"style": {
|
||||
"navigationBarTitleText": "dog"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "sub-pkg-b",
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/apple",
|
||||
"style": {
|
||||
"navigationBarTitleText": "apple"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}</code></pre>
|
||||
`);
|
||||
onMounted(() => {});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.faq-container {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
.wechat {
|
||||
padding: 30rpx;
|
||||
margin: 20px 0;
|
||||
font-size: 14px;
|
||||
color: var(--wot-card-content-color, rgba(0, 0, 0, 0.45));
|
||||
background-color: #fff;
|
||||
.tips {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<view class="app-container">
|
||||
<wd-navbar title="意见反馈" left-arrow @click-left="handleBack" />
|
||||
|
||||
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-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="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-form>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<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(() => {
|
||||
if (!checkLogin()) return;
|
||||
});
|
||||
|
||||
// 问题类型选项
|
||||
const feedbackTypes = [
|
||||
{ label: "功能异常", value: "bug" },
|
||||
{ label: "优化建议", value: "suggestion" },
|
||||
{ label: "其他问题", value: "other" },
|
||||
];
|
||||
|
||||
// 表单数据
|
||||
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: Record<string, any>) => {
|
||||
// 验证文件类型
|
||||
const validTypes = ["image/jpeg", "image/png", "image/gif"];
|
||||
if (!validTypes.includes(file.type)) {
|
||||
toast.error("请上传 jpg、png 或 gif 格式的图片");
|
||||
return false;
|
||||
}
|
||||
// 验证文件大小(限制为 5MB)
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
toast.error("图片大小不能超过 5MB");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// 删除图片
|
||||
const handleDelete = (detail: { index: number }) => {
|
||||
const index = detail.index;
|
||||
formData.fileList.splice(index, 1);
|
||||
};
|
||||
|
||||
// 提交反馈
|
||||
const handleSubmit = async () => {
|
||||
// 表单验证
|
||||
try {
|
||||
const { valid } = await formRef.value.validate();
|
||||
|
||||
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>
|
||||
:deep(.wd-form-item) {
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
margin: 40rpx 30rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,563 @@
|
||||
<template>
|
||||
<view class="app-container">
|
||||
<!-- 用户信息卡片 -->
|
||||
<view class="user-profile">
|
||||
<view class="blur-bg"></view>
|
||||
<view class="user-info">
|
||||
<view class="avatar-container" @click="navigateToProfile">
|
||||
<image
|
||||
class="avatar"
|
||||
:src="isLogin ? userInfo!.avatar : defaultAvatar"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
</view>
|
||||
<view class="user-details">
|
||||
<block v-if="isLogin">
|
||||
<view class="nickname">{{ userInfo!.nickname || "匿名用户" }}</view>
|
||||
<view class="user-id">ID: {{ userInfo?.username || "0000000" }}</view>
|
||||
</block>
|
||||
<block v-else>
|
||||
<view class="login-prompt">立即登录获取更多功能</view>
|
||||
<wd-button
|
||||
custom-class="btn-login"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="navigateToLoginPage"
|
||||
>
|
||||
登录/注册
|
||||
</wd-button>
|
||||
</block>
|
||||
</view>
|
||||
<view class="actions">
|
||||
<view class="action-btn" @click="navigateToSettings">
|
||||
<wd-icon name="setting1" size="22" color="#333" />
|
||||
</view>
|
||||
<view v-if="isLogin" class="action-btn" @click="navigateToSection('messages')">
|
||||
<wd-icon name="notification" size="22" color="#333" />
|
||||
<view v-if="true" class="badge">2</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 数据统计 -->
|
||||
<view class="stats-container">
|
||||
<view class="stat-item" @click="navigateToSection('wallet')">
|
||||
<view class="stat-value">0.00</view>
|
||||
<view class="stat-label">我的余额</view>
|
||||
</view>
|
||||
<view class="divider"></view>
|
||||
<view class="stat-item" @click="navigateToSection('favorites')">
|
||||
<view class="stat-value">0</view>
|
||||
<view class="stat-label">我的收藏</view>
|
||||
</view>
|
||||
<view class="divider"></view>
|
||||
<view class="stat-item" @click="navigateToSection('history')">
|
||||
<view class="stat-value">0</view>
|
||||
<view class="stat-label">浏览历史</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 常用工具 -->
|
||||
<view class="card-container">
|
||||
<view class="card-header">
|
||||
<view class="card-title">
|
||||
<wd-icon name="tools" size="18" :color="currentThemeColor" />
|
||||
<text>常用工具</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="tools-grid">
|
||||
<view class="tool-item" @click="navigateToProfile">
|
||||
<view class="tool-icon">
|
||||
<wd-icon name="user" size="24" :color="currentThemeColor" />
|
||||
</view>
|
||||
<view class="tool-label">个人资料</view>
|
||||
</view>
|
||||
|
||||
<view class="tool-item" @click="navigateToFAQ">
|
||||
<view class="tool-icon">
|
||||
<wd-icon name="help-circle" size="24" :color="currentThemeColor" />
|
||||
</view>
|
||||
<view class="tool-label">常见问题</view>
|
||||
</view>
|
||||
<view class="tool-item" @click="handleQuestionFeedback">
|
||||
<view class="tool-icon">
|
||||
<wd-icon name="check-circle" size="24" :color="currentThemeColor" />
|
||||
</view>
|
||||
<view class="tool-label">问题反馈</view>
|
||||
</view>
|
||||
<view class="tool-item" @click="navigateToAbout">
|
||||
<view class="tool-icon">
|
||||
<wd-icon name="info-circle" size="24" :color="currentThemeColor" />
|
||||
</view>
|
||||
<view class="tool-label">关于我们</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 推荐服务 -->
|
||||
<view class="card-container">
|
||||
<view class="card-header">
|
||||
<view class="card-title">
|
||||
<wd-icon name="star" size="18" :color="currentThemeColor" />
|
||||
<text>推荐服务</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="services-list">
|
||||
<view class="service-item" @click="navigateToSection('services', 'vip')">
|
||||
<view class="service-left">
|
||||
<view class="service-icon">
|
||||
<wd-icon name="dong" size="22" :color="currentThemeColor" />
|
||||
</view>
|
||||
<view class="service-info">
|
||||
<view class="service-name">会员中心</view>
|
||||
<view class="service-desc">解锁更多特权</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-icon name="arrow-right" size="14" color="#999" />
|
||||
</view>
|
||||
<view class="service-item" @click="navigateToSection('services', 'coupon')">
|
||||
<view class="service-left">
|
||||
<view class="service-icon">
|
||||
<wd-icon name="discount" size="22" :color="currentThemeColor" />
|
||||
</view>
|
||||
<view class="service-info">
|
||||
<view class="service-name">优惠券</view>
|
||||
<view class="service-desc">查看我的优惠券</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-icon name="arrow-right" size="14" color="#999" />
|
||||
</view>
|
||||
<view class="service-item" @click="navigateToSection('services', 'invite')">
|
||||
<view class="service-left">
|
||||
<view class="service-icon">
|
||||
<wd-icon name="share" size="22" :color="currentThemeColor" />
|
||||
</view>
|
||||
<view class="service-info">
|
||||
<view class="service-name">邀请有礼</view>
|
||||
<view class="service-desc">邀请好友得奖励</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-icon name="arrow-right" size="14" color="#999" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 退出登录按钮 -->
|
||||
<view v-if="isLogin" class="logout-btn-wrap">
|
||||
<wd-button
|
||||
class="w-full h-80rpx rounded-40rpx font-bold text-32rpx"
|
||||
plain
|
||||
@click="handleLogout"
|
||||
>
|
||||
退出登录
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<wd-toast />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onShow } from "@dcloudio/uni-app";
|
||||
import { useToast } from "wot-design-uni";
|
||||
import { useUserStore } from "@/store/modules/user.store";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import { computed } from "vue";
|
||||
|
||||
const toast = useToast();
|
||||
const userStore = useUserStore();
|
||||
const { currentThemeColor } = useTheme();
|
||||
const userInfo = computed(() => userStore.userInfo);
|
||||
const isLogin = computed(() => !!userInfo.value);
|
||||
const defaultAvatar = "/static/images/default-avatar.png";
|
||||
|
||||
// 登录
|
||||
const navigateToLoginPage = () => {
|
||||
const pages = getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
const currentPagePath = `/${currentPage.route}`;
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages/login/index?redirect=${encodeURIComponent(currentPagePath)}`,
|
||||
});
|
||||
};
|
||||
|
||||
// 退出登录
|
||||
const handleLogout = () => {
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: "确认退出登录吗?",
|
||||
success: function (res) {
|
||||
if (res.confirm) {
|
||||
userStore.logout();
|
||||
toast.show("已退出登录");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 个人信息
|
||||
const navigateToProfile = () => {
|
||||
if (!isLogin.value) {
|
||||
navigateToLoginPage();
|
||||
return;
|
||||
}
|
||||
uni.navigateTo({ url: "/pages/mine/profile/index" });
|
||||
};
|
||||
|
||||
// 常见问题
|
||||
const navigateToFAQ = () => {
|
||||
uni.navigateTo({ url: "/pages/mine/faq/index" });
|
||||
};
|
||||
|
||||
// 关于我们
|
||||
const navigateToAbout = () => {
|
||||
uni.navigateTo({ url: "/pages/mine/about/index" });
|
||||
};
|
||||
|
||||
// 设置
|
||||
const navigateToSettings = () => {
|
||||
uni.navigateTo({ url: "/pages/mine/settings/index" });
|
||||
};
|
||||
|
||||
// 问题反馈
|
||||
const handleQuestionFeedback = () => {
|
||||
uni.navigateTo({ url: "/pages/mine/feedback/index" });
|
||||
};
|
||||
|
||||
// 导航到各个板块
|
||||
const navigateToSection = (section: string, subSection?: string) => {
|
||||
console.log(`导航到: ${section}${subSection ? ` - ${subSection}` : ""}`);
|
||||
// 这里可以根据需要实现具体的导航逻辑
|
||||
uni.showToast({
|
||||
title: "功能开发中",
|
||||
icon: "none",
|
||||
});
|
||||
};
|
||||
|
||||
onShow(() => {
|
||||
// 确保 tabbar 状态正确
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length > 0) {
|
||||
const currentPage = pages[pages.length - 1];
|
||||
if (currentPage.route === "pages/mine/index") {
|
||||
// 通过事件通知 tabbar 布局更新状态
|
||||
uni.$emit("updateTabbar", "mine");
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"name": "mine",
|
||||
"style": { "navigationStyle": "custom" },
|
||||
"layout": "tabbar"
|
||||
}
|
||||
</route>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 用户信息卡片
|
||||
.user-profile {
|
||||
position: relative;
|
||||
padding: 30rpx;
|
||||
overflow: hidden;
|
||||
|
||||
.blur-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
height: 240rpx;
|
||||
background: linear-gradient(to bottom, var(--wot-color-theme), var(--primary-color-light));
|
||||
}
|
||||
|
||||
.user-info {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.avatar-container {
|
||||
position: relative;
|
||||
|
||||
.avatar {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border: 4rpx solid rgba(255, 255, 255, 0.8);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.user-details {
|
||||
flex: 1;
|
||||
margin-left: 24rpx;
|
||||
|
||||
.nickname {
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 34rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.user-id {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.login-prompt {
|
||||
margin-bottom: 16rpx;
|
||||
font-size: 28rpx;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
|
||||
.action-btn {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 70rpx;
|
||||
height: 70rpx;
|
||||
margin-left: 16rpx;
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: -6rpx;
|
||||
right: -6rpx;
|
||||
z-index: 2;
|
||||
min-width: 32rpx;
|
||||
height: 32rpx;
|
||||
padding: 0 6rpx;
|
||||
font-size: 20rpx;
|
||||
line-height: 32rpx;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
background-color: var(--wot-color-danger);
|
||||
border: 2rpx solid #fff;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 数据统计
|
||||
.stats-container {
|
||||
display: flex;
|
||||
padding: 30rpx 20rpx;
|
||||
margin: 20rpx 30rpx;
|
||||
background: var(--wot-color-bg-container);
|
||||
border-radius: 16rpx;
|
||||
box-shadow: var(--wot-card-shadow);
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.stat-value {
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: var(--wot-color-text);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 26rpx;
|
||||
color: var(--wot-color-text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 1px;
|
||||
margin: 0 20rpx;
|
||||
background-color: var(--wot-color-border);
|
||||
}
|
||||
}
|
||||
|
||||
// 卡片容器通用样式
|
||||
.card-container {
|
||||
margin: 24rpx 30rpx;
|
||||
overflow: hidden;
|
||||
background: var(--wot-color-bg-container);
|
||||
border-radius: 16rpx;
|
||||
box-shadow: var(--wot-card-shadow);
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 24rpx;
|
||||
border-bottom: 1rpx solid var(--wot-color-border);
|
||||
|
||||
.card-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
text {
|
||||
margin-left: 12rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: var(--wot-color-text);
|
||||
}
|
||||
}
|
||||
|
||||
.card-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
text {
|
||||
margin-right: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 订单状态
|
||||
.order-status {
|
||||
display: flex;
|
||||
padding: 30rpx 0 20rpx;
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.status-icon {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
.status-badge {
|
||||
position: absolute;
|
||||
top: -10rpx;
|
||||
right: -10rpx;
|
||||
z-index: 2;
|
||||
min-width: 32rpx;
|
||||
height: 32rpx;
|
||||
padding: 0 6rpx;
|
||||
font-size: 20rpx;
|
||||
line-height: 32rpx;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
background-color: #ff4d4f;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 工具网格
|
||||
.tools-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 20rpx 0 10rpx;
|
||||
|
||||
.tool-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 25%;
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.tool-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 90rpx;
|
||||
height: 90rpx;
|
||||
margin-bottom: 12rpx;
|
||||
background-color: var(--wot-color-bg-light);
|
||||
border-radius: 18rpx;
|
||||
transition: transform 0.2s;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.tool-label {
|
||||
font-size: 24rpx;
|
||||
color: var(--wot-color-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 服务列表
|
||||
.services-list {
|
||||
.service-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx;
|
||||
border-bottom: 1rpx solid var(--wot-color-border);
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:active {
|
||||
background-color: var(--wot-color-bg-light);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.service-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.service-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
margin-right: 20rpx;
|
||||
background-color: var(--wot-color-bg-light);
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.service-info {
|
||||
.service-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: var(--wot-color-text);
|
||||
}
|
||||
|
||||
.service-desc {
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: var(--wot-color-text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 退出登录按钮
|
||||
.logout-btn-wrap {
|
||||
padding: 30rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,640 @@
|
||||
<template>
|
||||
<view class="app-container">
|
||||
<wd-navbar title="完善个人信息" left-arrow @click-left="handleBack" />
|
||||
|
||||
<!-- 头部标题 -->
|
||||
<view class="header">
|
||||
<view class="title">完善个人信息</view>
|
||||
<view class="subtitle">为了给您提供更好的服务,请完善以下信息</view>
|
||||
</view>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view class="form-container">
|
||||
<wd-form ref="profileFormRef" :model="profileForm">
|
||||
<!-- 微信小程序专用头像昵称组件 -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<view class="form-section">
|
||||
<view class="section-title">基本信息</view>
|
||||
|
||||
<!-- 内联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 -->
|
||||
|
||||
<!-- 其他平台的头像上传 -->
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<!-- 头像上传 -->
|
||||
<view class="form-section">
|
||||
<view class="section-title">头像</view>
|
||||
<view class="avatar-upload" @click="chooseAvatar">
|
||||
<view v-if="!profileForm.avatar" class="avatar-placeholder">
|
||||
<wd-icon name="camera" size="40" color="#999" />
|
||||
<text class="placeholder-text">点击上传头像</text>
|
||||
</view>
|
||||
<image v-else :src="profileForm.avatar" class="avatar-preview" mode="aspectFill" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 昵称输入 -->
|
||||
<view class="form-section">
|
||||
<view class="section-title">
|
||||
昵称
|
||||
<text class="required">*</text>
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="profileForm.nickname"
|
||||
placeholder="请输入昵称"
|
||||
prop="nickname"
|
||||
:rules="rules.nickname"
|
||||
custom-class="nickname-input"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 性别选择 -->
|
||||
<view class="form-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>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- 手机号授权 -->
|
||||
<view class="form-section">
|
||||
<view class="section-title">
|
||||
手机号
|
||||
<text class="required">*</text>
|
||||
</view>
|
||||
<view v-if="!profileForm.mobile" class="phone-auth">
|
||||
<button
|
||||
class="phone-auth-btn"
|
||||
open-type="getPhoneNumber"
|
||||
:disabled="phoneAuthLoading"
|
||||
@getphonenumber="onGetPhoneNumber"
|
||||
>
|
||||
<wd-icon name="phone" size="20" color="#165DFF" />
|
||||
<text class="auth-text">{{ phoneAuthLoading ? "授权中..." : "授权获取手机号" }}</text>
|
||||
</button>
|
||||
</view>
|
||||
<view v-else class="phone-display">
|
||||
<wd-icon name="phone" size="20" color="#52c41a" />
|
||||
<text class="phone-number">{{ formatPhoneNumber(profileForm.mobile) }}</text>
|
||||
<text class="change-phone" @click="changePhone">更换</text>
|
||||
</view>
|
||||
</view>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<view class="footer">
|
||||
<wd-button
|
||||
class="complete-btn"
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
:disabled="!canComplete || loading"
|
||||
:loading="loading"
|
||||
@click="handleComplete"
|
||||
>
|
||||
完成
|
||||
</wd-button>
|
||||
<view class="skip-btn" @click="handleSkip">
|
||||
<text>暂时跳过</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 头像裁剪 -->
|
||||
<wd-img-cropper
|
||||
v-model="cropperVisible"
|
||||
:img-src="originalImageSrc"
|
||||
@confirm="handleAvatarConfirm"
|
||||
/>
|
||||
|
||||
<wd-toast />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
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";
|
||||
|
||||
const toast = useToast();
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 页面参数
|
||||
const redirect = ref("/pages/index/index");
|
||||
|
||||
// 表单数据
|
||||
const profileForm = reactive<UserProfileForm & { mobile?: string }>({
|
||||
nickname: "",
|
||||
avatar: "",
|
||||
gender: 1,
|
||||
mobile: "",
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
nickname: [
|
||||
{ required: true, message: "请输入昵称" },
|
||||
{ required: true, min: 2, max: 20, message: "昵称长度为2-20个字符" },
|
||||
],
|
||||
};
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false);
|
||||
const phoneAuthLoading = ref(false);
|
||||
const cropperVisible = ref(false);
|
||||
const originalImageSrc = ref("");
|
||||
const profileFormRef = ref();
|
||||
|
||||
// 计算属性
|
||||
const canComplete = computed(() => {
|
||||
return profileForm.nickname && profileForm.mobile;
|
||||
});
|
||||
|
||||
// 页面加载
|
||||
onLoad((options: any) => {
|
||||
if (options?.redirect) {
|
||||
redirect.value = decodeURIComponent(options.redirect);
|
||||
}
|
||||
|
||||
// 如果用户已有部分信息,预填充
|
||||
const userInfo = userStore.userInfo;
|
||||
if (userInfo) {
|
||||
profileForm.nickname = userInfo.nickname || "";
|
||||
profileForm.avatar = userInfo.avatar || "";
|
||||
// 注意:UserInfo接口可能没有gender字段,这里需要安全访问
|
||||
profileForm.gender = (userInfo as any).gender || 1;
|
||||
}
|
||||
});
|
||||
|
||||
// 微信小程序头像选择处理
|
||||
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
|
||||
// 微信小程序使用头像昵称填写能力
|
||||
uni.chooseMedia({
|
||||
count: 1,
|
||||
mediaType: ["image"],
|
||||
sourceType: ["album", "camera"],
|
||||
success: (res) => {
|
||||
originalImageSrc.value = res.tempFiles[0].tempFilePath;
|
||||
cropperVisible.value = true;
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error("选择图片失败:", err);
|
||||
toast.error("选择图片失败");
|
||||
},
|
||||
});
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sourceType: ["album", "camera"],
|
||||
success: (res) => {
|
||||
originalImageSrc.value = res.tempFilePaths[0];
|
||||
cropperVisible.value = true;
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error("选择图片失败:", err);
|
||||
toast.error("选择图片失败");
|
||||
},
|
||||
});
|
||||
// #endif
|
||||
};
|
||||
|
||||
// 头像裁剪确认
|
||||
const handleAvatarConfirm = async (event: any) => {
|
||||
try {
|
||||
const { tempFilePath } = event;
|
||||
toast.loading("上传中...");
|
||||
|
||||
const fileInfo: FileInfo = await FileAPI.upload(tempFilePath);
|
||||
profileForm.avatar = fileInfo.url;
|
||||
|
||||
toast.success("头像上传成功");
|
||||
} catch (error) {
|
||||
console.error("头像上传失败:", error);
|
||||
toast.error("头像上传失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 获取手机号授权
|
||||
const onGetPhoneNumber = async (e: any) => {
|
||||
console.log("手机号授权回调:", e);
|
||||
|
||||
if (e.detail.errMsg === "getPhoneNumber:ok") {
|
||||
phoneAuthLoading.value = true;
|
||||
|
||||
try {
|
||||
// 调用后端接口解析手机号
|
||||
const phoneData = await UserAPI.getPhoneNumber({
|
||||
code: e.detail.code,
|
||||
encryptedData: e.detail.encryptedData,
|
||||
iv: e.detail.iv,
|
||||
});
|
||||
|
||||
profileForm.mobile = phoneData.phoneNumber;
|
||||
toast.success("手机号授权成功");
|
||||
} catch (error: any) {
|
||||
console.error("手机号授权失败:", error);
|
||||
toast.error(error?.message || "手机号授权失败");
|
||||
} finally {
|
||||
phoneAuthLoading.value = false;
|
||||
}
|
||||
} else {
|
||||
toast.error("手机号授权失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 更换手机号
|
||||
const changePhone = () => {
|
||||
profileForm.mobile = "";
|
||||
};
|
||||
|
||||
// 格式化手机号显示
|
||||
const formatPhoneNumber = (phone: string) => {
|
||||
if (!phone) return "";
|
||||
return phone.replace(/(\d{3})\d{4}(\d{4})/, "$1****$2");
|
||||
};
|
||||
|
||||
// 完成信息填写
|
||||
const handleComplete = async () => {
|
||||
try {
|
||||
// 表单验证
|
||||
const { valid } = await profileFormRef.value.validate();
|
||||
if (!valid) return;
|
||||
|
||||
if (!profileForm.mobile) {
|
||||
toast.error("请先授权获取手机号");
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
// 更新用户信息
|
||||
await UserAPI.updateProfile({
|
||||
nickname: profileForm.nickname,
|
||||
avatar: profileForm.avatar,
|
||||
gender: profileForm.gender,
|
||||
});
|
||||
|
||||
// 如果有手机号,绑定手机号
|
||||
if (profileForm.mobile) {
|
||||
await UserAPI.bindMobile({
|
||||
mobile: profileForm.mobile,
|
||||
});
|
||||
}
|
||||
|
||||
// 重新获取用户信息
|
||||
await userStore.getInfo();
|
||||
|
||||
toast.success("信息完善成功");
|
||||
|
||||
// 跳转到目标页面
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({
|
||||
url: redirect.value,
|
||||
});
|
||||
}, 1000);
|
||||
} catch (error: any) {
|
||||
console.error("完善信息失败:", error);
|
||||
toast.error(error?.message || "完善信息失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 跳过完善信息
|
||||
const handleSkip = () => {
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: "跳过信息完善可能会影响部分功能使用,确定要跳过吗?",
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.reLaunch({
|
||||
url: redirect.value,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 返回
|
||||
function handleBack() {
|
||||
uni.navigateBack();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header {
|
||||
margin-bottom: 60rpx;
|
||||
text-align: center;
|
||||
|
||||
.title {
|
||||
margin-bottom: 20rpx;
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 28rpx;
|
||||
line-height: 1.5;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.form-container {
|
||||
padding: 40rpx;
|
||||
margin-bottom: 40rpx;
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin-bottom: 20rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
|
||||
.required {
|
||||
color: #ff4757;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-upload {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.avatar-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
background: #f8f9fa;
|
||||
border: 2rpx dashed #ddd;
|
||||
border-radius: 50%;
|
||||
|
||||
.placeholder-text {
|
||||
margin-top: 10rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-preview {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border: 4rpx solid #f0f0f0;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.nickname-input {
|
||||
:deep(.wd-input__inner) {
|
||||
padding: 24rpx 20rpx;
|
||||
background: #f8f9fa;
|
||||
border: 1rpx solid #e9ecef;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.gender-group {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
|
||||
.gender-radio {
|
||||
flex: 1;
|
||||
|
||||
:deep(.wd-radio) {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.phone-auth {
|
||||
.phone-auth-btn {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
font-size: 28rpx;
|
||||
color: #fff;
|
||||
background: linear-gradient(90deg, #165dff, #4080ff);
|
||||
border: none;
|
||||
border-radius: 12rpx;
|
||||
|
||||
&[disabled] {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.auth-text {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.phone-display {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
align-items: center;
|
||||
padding: 24rpx 20rpx;
|
||||
background: #f0f9ff;
|
||||
border: 1rpx solid #bae6fd;
|
||||
border-radius: 12rpx;
|
||||
|
||||
.phone-number {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.change-phone {
|
||||
font-size: 26rpx;
|
||||
color: #165dff;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
.complete-btn {
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
:deep(.wd-button) {
|
||||
height: 88rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 44rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.skip-btn {
|
||||
text-align: center;
|
||||
|
||||
text {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 内联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>
|
||||
@@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<view class="app-container">
|
||||
<wd-navbar title="个人信息" left-arrow @click-left="handleBack" />
|
||||
|
||||
<wd-card v-if="userProfile" custom-style="margin-top: 20rpx">
|
||||
<wd-cell-group border>
|
||||
<wd-cell class="avatar-cell" title="头像" center is-link>
|
||||
<view class="avatar">
|
||||
<view v-if="!userProfile.avatar" class="img" @click="avatarUpload">
|
||||
<wd-icon name="fill-camera" custom-class="img-icon" />
|
||||
</view>
|
||||
<wd-img
|
||||
v-if="userProfile.avatar"
|
||||
round
|
||||
width="80px"
|
||||
height="80px"
|
||||
:src="userProfile.avatar"
|
||||
mode="aspectFit"
|
||||
custom-class="profile-img"
|
||||
@click="avatarUpload"
|
||||
/>
|
||||
</view>
|
||||
</wd-cell>
|
||||
<wd-cell title="昵称" :value="userProfile.nickname" is-link @click="handleOpenDialog()" />
|
||||
<wd-cell
|
||||
title="性别"
|
||||
:value="userProfile.gender === 1 ? '男' : userProfile.gender === 2 ? '女' : '未知'"
|
||||
is-link
|
||||
@click="handleOpenDialog()"
|
||||
/>
|
||||
<wd-cell title="用户名" :value="userProfile.username" />
|
||||
<wd-cell title="部门" :value="userProfile.deptName" />
|
||||
<wd-cell title="角色" :value="userProfile.roleNames" />
|
||||
<wd-cell title="创建日期" :value="userProfile.createTime" />
|
||||
</wd-cell-group>
|
||||
</wd-card>
|
||||
|
||||
<!--头像裁剪-->
|
||||
<wd-img-cropper v-model="avatarShow" :img-src="originalSrc" @confirm="handleAvatarConfirm" />
|
||||
|
||||
<!--用户信息编辑弹出框-->
|
||||
<wd-popup v-model="dialog.visible" position="bottom">
|
||||
<wd-form ref="userProfileFormRef" :model="userProfileForm" custom-class="edit-form">
|
||||
<wd-cell-group border>
|
||||
<wd-input
|
||||
v-model="userProfileForm.nickname"
|
||||
label="昵称"
|
||||
label-width="160rpx"
|
||||
placeholder="请输入昵称"
|
||||
prop="nickname"
|
||||
:rules="rules.nickname"
|
||||
/>
|
||||
<wd-cell title="性别" title-width="160rpx" center prop="gender" :rules="rules.gender">
|
||||
<wd-radio-group v-model="userProfileForm.gender" shape="button" class="ef-radio-group">
|
||||
<wd-radio :value="1">男</wd-radio>
|
||||
<wd-radio :value="2">女</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
<view class="p-6">
|
||||
<wd-button type="primary" size="large" block @click="handleSubmit">提交</wd-button>
|
||||
</view>
|
||||
</wd-form>
|
||||
</wd-popup>
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import UserAPI, { type UserProfileVO, UserProfileForm } from "@/api/user";
|
||||
import FileAPI, { type FileInfo } from "@/api/file";
|
||||
import { checkLogin } from "@/utils/auth";
|
||||
|
||||
const originalSrc = ref<string>(""); //选取的原图路径
|
||||
const avatarShow = ref<boolean>(false); //显示头像裁剪
|
||||
const userProfile = ref<UserProfileVO>(); //用户信息
|
||||
|
||||
/** 加载用户信息 */
|
||||
const loadUserProfile = async () => {
|
||||
userProfile.value = await UserAPI.getProfile();
|
||||
};
|
||||
|
||||
// 头像选择
|
||||
function avatarUpload() {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
success: (res) => {
|
||||
originalSrc.value = res.tempFilePaths[0];
|
||||
avatarShow.value = true;
|
||||
},
|
||||
});
|
||||
}
|
||||
// 头像裁剪完成
|
||||
function handleAvatarConfirm(event: any) {
|
||||
const { tempFilePath } = event;
|
||||
FileAPI.upload(tempFilePath).then((fileInfo: FileInfo) => {
|
||||
const avatarForm: UserProfileForm = {
|
||||
avatar: fileInfo.url,
|
||||
};
|
||||
// 头像路径保存至后端
|
||||
UserAPI.updateProfile(avatarForm).then(() => {
|
||||
uni.showToast({ title: "头像上传成功", icon: "none" });
|
||||
loadUserProfile();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 本页面中所有的校验规则
|
||||
const rules = reactive({
|
||||
nickname: [{ required: true, message: "请填写昵称" }],
|
||||
gender: [{ required: true, message: "请选择性别" }],
|
||||
});
|
||||
|
||||
const dialog = reactive({
|
||||
visible: false,
|
||||
});
|
||||
|
||||
const userProfileForm = reactive<UserProfileForm>({});
|
||||
const userProfileFormRef = ref();
|
||||
|
||||
/**
|
||||
* 打开弹窗
|
||||
* @param type 弹窗类型 ACCOUNT: 账号资料 PASSWORD: 修改密码 MOBILE: 绑定手机 EMAIL: 绑定邮箱
|
||||
*/
|
||||
const handleOpenDialog = () => {
|
||||
dialog.visible = true;
|
||||
// 初始化表单数据
|
||||
userProfileForm.nickname = userProfile.value?.nickname;
|
||||
userProfileForm.gender = userProfile.value?.gender;
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
function handleSubmit() {
|
||||
userProfileFormRef.value.validate().then(({ valid }: { valid: boolean }) => {
|
||||
if (valid) {
|
||||
UserAPI.updateProfile(userProfileForm).then(() => {
|
||||
uni.showToast({ title: "账号资料修改成功", icon: "none" });
|
||||
dialog.visible = false;
|
||||
loadUserProfile();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 检查登录状态
|
||||
onLoad(() => {
|
||||
if (!checkLogin()) return;
|
||||
|
||||
// #ifdef H5
|
||||
document.addEventListener("touchstart", touchstartListener, { passive: false });
|
||||
document.addEventListener("touchmove", touchmoveListener, { passive: false });
|
||||
// #endif
|
||||
loadUserProfile();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 在onMounted中不再重复检查登录状态和加载用户信息
|
||||
// 如果需要检查登录状态和加载用户信息,请使用onLoad中的逻辑
|
||||
});
|
||||
|
||||
// 页面销毁前移除事件监听
|
||||
onBeforeUnmount(() => {
|
||||
// #ifdef H5
|
||||
document.removeEventListener("touchstart", touchstartListener);
|
||||
document.removeEventListener("touchmove", touchmoveListener);
|
||||
// #endif
|
||||
});
|
||||
// 禁用浏览器双指缩放,使头像裁剪时双指缩放能够起作用
|
||||
function touchstartListener(event: TouchEvent) {
|
||||
if (event.touches.length > 1) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
// 禁用浏览器下拉刷新,使头像裁剪时能够移动图片
|
||||
function touchmoveListener(event: TouchEvent) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
uni.navigateBack();
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.avatar-cell {
|
||||
:deep(.wd-cell__body) {
|
||||
align-items: center;
|
||||
}
|
||||
.avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: right;
|
||||
.img {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
border-radius: 50%;
|
||||
.img-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit-form {
|
||||
padding-top: 40rpx;
|
||||
.ef-radio-group {
|
||||
line-height: 1;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,329 @@
|
||||
<template>
|
||||
<view class="app-container">
|
||||
<wd-navbar title="账号和安全" left-arrow @click-left="handleBack" />
|
||||
|
||||
<wd-card custom-style="margin-top: 20rpx">
|
||||
<wd-cell-group border>
|
||||
<wd-cell
|
||||
title="账户密码"
|
||||
label="定期修改密码有助于保护账户安全"
|
||||
value="修改"
|
||||
is-link
|
||||
@click="handleOpenDialog(DialogType.PASSWORD)"
|
||||
/>
|
||||
<wd-cell
|
||||
title="绑定手机"
|
||||
:value="userProfile?.mobile"
|
||||
is-link
|
||||
@click="handleOpenDialog(DialogType.MOBILE)"
|
||||
/>
|
||||
<wd-cell
|
||||
title="绑定邮箱"
|
||||
:value="userProfile?.email ? userProfile.email : '未绑定邮箱'"
|
||||
is-link
|
||||
@click="handleOpenDialog(DialogType.EMAIL)"
|
||||
/>
|
||||
</wd-cell-group>
|
||||
</wd-card>
|
||||
|
||||
<!--用户信息编辑弹出框-->
|
||||
<wd-popup v-model="dialog.visible" position="bottom">
|
||||
<wd-form
|
||||
v-if="dialog.type === DialogType.PASSWORD"
|
||||
ref="passwordChangeFormRef"
|
||||
:model="passwordChangeForm"
|
||||
custom-class="edit-form"
|
||||
>
|
||||
<wd-cell-group border>
|
||||
<wd-input
|
||||
v-model="passwordChangeForm.oldPassword"
|
||||
label="原密码"
|
||||
label-width="160rpx"
|
||||
show-password
|
||||
clearable
|
||||
placeholder="请输入原密码"
|
||||
prop="oldPassword"
|
||||
:rules="rules.oldPassword"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="passwordChangeForm.newPassword"
|
||||
label="新密码"
|
||||
label-width="160rpx"
|
||||
show-password
|
||||
clearable
|
||||
placeholder="请输入新密码"
|
||||
prop="newPassword"
|
||||
:rules="rules.newPassword"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="passwordChangeForm.confirmPassword"
|
||||
label="确认密码"
|
||||
label-width="160rpx"
|
||||
show-password
|
||||
clearable
|
||||
placeholder="请确认新密码"
|
||||
prop="confirmPassword"
|
||||
:rules="rules.confirmPassword"
|
||||
/>
|
||||
</wd-cell-group>
|
||||
<view class="p-6">
|
||||
<wd-button type="primary" size="large" block @click="handleSubmit">提交</wd-button>
|
||||
</view>
|
||||
</wd-form>
|
||||
<wd-form
|
||||
v-if="dialog.type === DialogType.MOBILE"
|
||||
ref="mobileBindingFormRef"
|
||||
:model="mobileBindingForm"
|
||||
custom-class="edit-form"
|
||||
>
|
||||
<wd-cell-group border>
|
||||
<wd-input
|
||||
v-model="mobileBindingForm.mobile"
|
||||
label="手机号码"
|
||||
label-width="160rpx"
|
||||
clearable
|
||||
placeholder="请输入手机号码"
|
||||
prop="mobile"
|
||||
:rules="rules.mobile"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="mobileBindingForm.code"
|
||||
label="验证码"
|
||||
label-width="160rpx"
|
||||
clearable
|
||||
placeholder="请输入验证码"
|
||||
prop="code"
|
||||
:rules="rules.code"
|
||||
>
|
||||
<template #suffix>
|
||||
<wd-button
|
||||
plain
|
||||
:disabled="mobileCountdown > 0"
|
||||
@click="handleSendVerificationCode('MOBILE')"
|
||||
>
|
||||
{{ mobileCountdown > 0 ? `${mobileCountdown}s后重新发送` : "发送验证码" }}
|
||||
</wd-button>
|
||||
</template>
|
||||
</wd-input>
|
||||
</wd-cell-group>
|
||||
<view class="p-6">
|
||||
<wd-button type="primary" size="large" block @click="handleSubmit">提交</wd-button>
|
||||
</view>
|
||||
</wd-form>
|
||||
<wd-form
|
||||
v-if="dialog.type === DialogType.EMAIL"
|
||||
ref="emailBindingFormRef"
|
||||
:model="emailBindingForm"
|
||||
custom-class="edit-form"
|
||||
>
|
||||
<wd-cell-group border>
|
||||
<wd-input
|
||||
v-model="emailBindingForm.email"
|
||||
label="邮箱"
|
||||
label-width="160rpx"
|
||||
clearable
|
||||
placeholder="请输入邮箱"
|
||||
prop="email"
|
||||
:rules="rules.email"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="emailBindingForm.code"
|
||||
label="验证码"
|
||||
label-width="160rpx"
|
||||
clearable
|
||||
placeholder="请输入验证码"
|
||||
prop="code"
|
||||
:rules="rules.code"
|
||||
>
|
||||
<template #suffix>
|
||||
<wd-button
|
||||
plain
|
||||
:disabled="emailCountdown > 0"
|
||||
@click="handleSendVerificationCode('EMAIL')"
|
||||
>
|
||||
{{ emailCountdown > 0 ? `${emailCountdown}s后重新发送` : "发送验证码" }}
|
||||
</wd-button>
|
||||
</template>
|
||||
</wd-input>
|
||||
</wd-cell-group>
|
||||
<view class="p-6">
|
||||
<wd-button type="primary" size="large" block @click="handleSubmit">提交</wd-button>
|
||||
</view>
|
||||
</wd-form>
|
||||
</wd-popup>
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import UserAPI, {
|
||||
PasswordChangeForm,
|
||||
MobileBindingForm,
|
||||
EmailBindingForm,
|
||||
UserProfileVO,
|
||||
} from "@/api/user";
|
||||
|
||||
const validatorConfirmPassword = (value: string) => {
|
||||
if (!value) {
|
||||
return Promise.reject("请确认密码");
|
||||
} else {
|
||||
if (value !== passwordChangeForm.newPassword) {
|
||||
return Promise.reject("两次输入的密码不一致");
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
};
|
||||
// 本页面中所有的校验规则
|
||||
const rules = reactive({
|
||||
oldPassword: [{ required: true, message: "请填写原密码" }],
|
||||
newPassword: [{ required: true, message: "请填写新密码" }],
|
||||
confirmPassword: [{ required: true, message: "请确认密码", validator: validatorConfirmPassword }],
|
||||
mobile: [{ required: true, pattern: /^1[3-9]\d{9}$/, message: "请填写正确的手机号码" }],
|
||||
code: [{ required: true, message: "请填写验证码" }],
|
||||
email: [
|
||||
{
|
||||
required: true,
|
||||
pattern: /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/,
|
||||
message: "请填写正确的邮箱地址",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
enum DialogType {
|
||||
PASSWORD = "password",
|
||||
MOBILE = "mobile",
|
||||
EMAIL = "email",
|
||||
}
|
||||
|
||||
const dialog = reactive({
|
||||
visible: false,
|
||||
type: "" as DialogType, // 修改账号资料,修改密码、绑定手机、绑定邮箱
|
||||
});
|
||||
|
||||
const userProfile = ref<UserProfileVO>(); //用户信息
|
||||
const passwordChangeForm = reactive<PasswordChangeForm>({});
|
||||
const mobileBindingForm = reactive<MobileBindingForm>({});
|
||||
const emailBindingForm = reactive<EmailBindingForm>({});
|
||||
const passwordChangeFormRef = ref();
|
||||
const mobileBindingFormRef = ref();
|
||||
const emailBindingFormRef = ref();
|
||||
|
||||
const mobileCountdown = ref(0);
|
||||
const mobileTimer = ref<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
const emailCountdown = ref(0);
|
||||
const emailTimer = ref<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
// 处理返回按钮点击
|
||||
const handleBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
/** 加载用户信息 */
|
||||
const loadUserProfile = async () => {
|
||||
userProfile.value = await UserAPI.getProfile();
|
||||
};
|
||||
|
||||
/**
|
||||
* 打开弹窗
|
||||
* @param type 弹窗类型 ACCOUNT: 账号资料 PASSWORD: 修改密码 MOBILE: 绑定手机 EMAIL: 绑定邮箱
|
||||
*/
|
||||
const handleOpenDialog = (type: DialogType) => {
|
||||
dialog.type = type;
|
||||
dialog.visible = true;
|
||||
switch (type) {
|
||||
case DialogType.PASSWORD:
|
||||
passwordChangeForm.oldPassword = "";
|
||||
passwordChangeForm.newPassword = "";
|
||||
passwordChangeForm.confirmPassword = "";
|
||||
break;
|
||||
case DialogType.MOBILE:
|
||||
mobileBindingForm.mobile = "";
|
||||
mobileBindingForm.code = "";
|
||||
break;
|
||||
case DialogType.EMAIL:
|
||||
emailBindingForm.email = "";
|
||||
emailBindingForm.code = "";
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 发送验证码
|
||||
*
|
||||
* @param contactType 联系方式类型 MOBILE: 手机号码 EMAIL: 邮箱
|
||||
*/
|
||||
const handleSendVerificationCode = async (contactType: string) => {
|
||||
if (contactType === "MOBILE") {
|
||||
mobileBindingFormRef.value.validate("mobile").then(({ valid }: { valid: boolean }) => {
|
||||
if (valid) {
|
||||
UserAPI.sendVerificationCode(mobileBindingForm.mobile!, "MOBILE").then(() => {
|
||||
uni.showToast({ title: "验证码已发送", icon: "none" });
|
||||
mobileCountdown.value = 60;
|
||||
mobileTimer.value = setInterval(() => {
|
||||
if (mobileCountdown.value > 0) {
|
||||
mobileCountdown.value -= 1;
|
||||
} else {
|
||||
clearInterval(mobileTimer.value!);
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (contactType === "EMAIL") {
|
||||
emailBindingFormRef.value.validate("email").then(({ valid }: { valid: boolean }) => {
|
||||
if (valid) {
|
||||
UserAPI.sendVerificationCode(emailBindingForm.email!, "EMAIL").then(() => {
|
||||
uni.showToast({ title: "验证码已发送", icon: "none" });
|
||||
emailCountdown.value = 60;
|
||||
emailTimer.value = setInterval(() => {
|
||||
if (emailCountdown.value > 0) {
|
||||
emailCountdown.value -= 1;
|
||||
} else {
|
||||
clearInterval(emailTimer.value!);
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
function handleSubmit() {
|
||||
if (dialog.type === DialogType.PASSWORD) {
|
||||
passwordChangeFormRef.value.validate().then(({ valid }: { valid: boolean }) => {
|
||||
if (valid) {
|
||||
UserAPI.changePassword(passwordChangeForm).then(() => {
|
||||
uni.showToast({ title: "密码修改成功", icon: "none" });
|
||||
dialog.visible = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (dialog.type === DialogType.MOBILE) {
|
||||
mobileBindingFormRef.value.validate().then(({ valid }: { valid: boolean }) => {
|
||||
if (valid) {
|
||||
UserAPI.bindMobile(mobileBindingForm).then(() => {
|
||||
uni.showToast({ title: "手机号绑定成功", icon: "none" });
|
||||
dialog.visible = false;
|
||||
loadUserProfile();
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (dialog.type === DialogType.EMAIL) {
|
||||
emailBindingFormRef.value.validate().then(({ valid }: { valid: boolean }) => {
|
||||
if (valid) {
|
||||
UserAPI.bindEmail(emailBindingForm).then(() => {
|
||||
uni.showToast({ title: "邮箱绑定成功", icon: "none" });
|
||||
dialog.visible = false;
|
||||
loadUserProfile();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadUserProfile();
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<view class="app-container">
|
||||
<wd-navbar title="用户协议" left-arrow @click-left="handleBack" />
|
||||
|
||||
<wd-card custom-style="margin-top: 20rpx">
|
||||
<view class="flex-col-center py-4">
|
||||
<text class="text-xl font-bold mb-2">用户协议</text>
|
||||
<text class="text-sm text-gray-500">更新日期:2024年3月15日</text>
|
||||
</view>
|
||||
</wd-card>
|
||||
|
||||
<wd-collapse v-model="activeNames" accordion>
|
||||
<wd-collapse-item
|
||||
v-for="(section, index) in agreementContent"
|
||||
:key="index"
|
||||
:title="section.title"
|
||||
:name="String(index)"
|
||||
>
|
||||
<view class="py-3 px-4">
|
||||
<text class="text-base leading-relaxed text-gray-600">{{ section.content }}</text>
|
||||
</view>
|
||||
</wd-collapse-item>
|
||||
</wd-collapse>
|
||||
|
||||
<view class="mt-6 px-4">
|
||||
<wd-button type="primary" block @click="handleAgree">我已阅读并同意</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const activeNames = ref(["0"]); // 默认展开第一项
|
||||
|
||||
const agreementContent = [
|
||||
{
|
||||
title: "1. 协议的范围",
|
||||
content:
|
||||
"本协议是您与我们之间关于使用本应用服务所订立的协议。您在使用本应用服务时,须完全接受本协议所有条款。",
|
||||
},
|
||||
{
|
||||
title: "2. 服务内容",
|
||||
content:
|
||||
"本应用向您提供以下服务:网络状态检测、网络性能测试以及其他相关服务。我们将持续优化和更新服务内容,为您提供更好的使用体验。",
|
||||
},
|
||||
{
|
||||
title: "3. 用户隐私",
|
||||
content:
|
||||
"我们重视用户的隐私保护,收集信息仅用于提供网络测试服务、改善用户体验和必要的系统维护。我们承诺对您的信息进行严格保密。",
|
||||
},
|
||||
{
|
||||
title: "4. 用户行为规范",
|
||||
content:
|
||||
"您在使用本服务时必须遵守中华人民共和国相关法律法规。您不得利用本服务从事违法违规活动。如发现违规行为,我们有权终止服务。",
|
||||
},
|
||||
{
|
||||
title: "5. 免责声明",
|
||||
content:
|
||||
"由于网络服务的特殊性,本应用不保证服务一定能满足用户的所有要求。对于因网络状态、通信线路等不可控因素导致的服务中断或其他缺陷,本应用不承担任何责任。",
|
||||
},
|
||||
{
|
||||
title: "6. 协议修改",
|
||||
content:
|
||||
"我们保留随时修改本协议的权利。协议修改后,如果您继续使用本应用服务,即视为您已接受修改后的协议。我们建议您定期查看本协议以了解任何变更。",
|
||||
},
|
||||
];
|
||||
|
||||
// 返回
|
||||
const handleBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
// 同意协议
|
||||
const handleAgree = () => {
|
||||
uni.showToast({
|
||||
title: "感谢您的支持",
|
||||
icon: "success",
|
||||
});
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1500);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,252 @@
|
||||
<template>
|
||||
<view class="app-container">
|
||||
<wd-navbar title="设置" left-arrow @click-left="handleBack" />
|
||||
|
||||
<wd-cell-group custom-style="margin-top: 20rpx">
|
||||
<wd-cell v-if="isLogin" title="个人资料" icon="user" is-link @click="navigateToProfile" />
|
||||
<wd-cell
|
||||
v-if="isLogin"
|
||||
title="账号和安全"
|
||||
icon="secured"
|
||||
is-link
|
||||
@click="navigateToAccount"
|
||||
/>
|
||||
<wd-cell title="主题设置" icon="setting1" is-link @click="navigateToTheme" />
|
||||
<wd-cell title="用户协议" icon="user" is-link @click="navigateToUserAgreement" />
|
||||
<wd-cell title="关于我们" icon="info-circle" is-link @click="navigateToAbout" />
|
||||
</wd-cell-group>
|
||||
|
||||
<wd-cell-group custom-style="margin-top:40rpx">
|
||||
<wd-cell title="网络测试" icon="wifi" is-link @click="navigateToNetworkTest" />
|
||||
<wd-cell
|
||||
title="清空缓存"
|
||||
icon="delete1"
|
||||
:value="cacheSize"
|
||||
clickable
|
||||
@click="handleClearCache"
|
||||
/>
|
||||
</wd-cell-group>
|
||||
|
||||
<view v-if="isLogin" class="logout-section">
|
||||
<wd-button class="logout-btn" @click="handleLogout">退出登录</wd-button>
|
||||
</view>
|
||||
|
||||
<!-- 使用wot-design-uni的Loading组件 -->
|
||||
<wd-loading
|
||||
v-if="clearing"
|
||||
v-model="clearing"
|
||||
text="正在清理..."
|
||||
mask
|
||||
custom-class="loading-center"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useUserStore } from "@/store/modules/user.store";
|
||||
import { checkLogin } from "@/utils/auth";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
|
||||
const userStore = useUserStore();
|
||||
const isLogin = computed(() => !!userStore.userInfo);
|
||||
|
||||
// 个人资料
|
||||
const navigateToProfile = () => {
|
||||
if (checkLogin()) {
|
||||
uni.navigateTo({
|
||||
url: "/pages/mine/profile/index",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 账号和安全
|
||||
const navigateToAccount = () => {
|
||||
if (checkLogin()) {
|
||||
uni.navigateTo({
|
||||
url: "/pages/mine/settings/account/index",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 主题设置
|
||||
const navigateToTheme = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/mine/settings/theme/index",
|
||||
});
|
||||
};
|
||||
|
||||
// 用户协议
|
||||
const navigateToUserAgreement = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/mine/settings/agreement/index",
|
||||
});
|
||||
};
|
||||
|
||||
// 关于我们
|
||||
const navigateToAbout = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/mine/about/index",
|
||||
});
|
||||
};
|
||||
|
||||
// 网络测试
|
||||
const navigateToNetworkTest = () => {
|
||||
uni.navigateTo({ url: "/pages/mine/settings/network/index" });
|
||||
};
|
||||
|
||||
// 是否正在清理
|
||||
const clearing = ref(false);
|
||||
// 缓存大小
|
||||
const cacheSize = ref<any>("计算中...");
|
||||
// 获取缓存大小
|
||||
const getCacheSize = async () => {
|
||||
try {
|
||||
// #ifdef MP-WEIXIN
|
||||
const res = await uni.getStorageInfo();
|
||||
cacheSize.value = formatSize(res.currentSize);
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
cacheSize.value = formatSize(
|
||||
Object.keys(localStorage).reduce((size, key) => size + localStorage[key].length, 0)
|
||||
);
|
||||
// #endif
|
||||
if (!cacheSize.value) {
|
||||
cacheSize.value = "0B";
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取缓存大小失败:", error);
|
||||
cacheSize.value = "获取失败";
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化存储大小
|
||||
const formatSize = (size: number) => {
|
||||
if (size < 1024) {
|
||||
return size + "B";
|
||||
} else if (size < 1024 * 1024) {
|
||||
return (size / 1024).toFixed(2) + "KB";
|
||||
} else {
|
||||
return (size / 1024 / 1024).toFixed(2) + "MB";
|
||||
}
|
||||
};
|
||||
|
||||
// 处理清除缓存
|
||||
const handleClearCache = async () => {
|
||||
if (cacheSize.value === "获取失败") {
|
||||
uni.showToast({
|
||||
title: "获取缓存信息失败,请稍后重试",
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (cacheSize.value === "0B") {
|
||||
uni.showToast({
|
||||
title: "暂无缓存需要清理",
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (clearing.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
clearing.value = true;
|
||||
// 模拟清理过程
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
// 清除缓存
|
||||
await uni.clearStorage();
|
||||
// 更新缓存大小显示
|
||||
await getCacheSize();
|
||||
// 提示清理成功
|
||||
uni.showToast({
|
||||
title: "清理成功",
|
||||
icon: "success",
|
||||
});
|
||||
} catch {
|
||||
uni.showToast({
|
||||
title: "清理失败",
|
||||
icon: "error",
|
||||
});
|
||||
} finally {
|
||||
clearing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 退出登录
|
||||
const handleLogout = () => {
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: "确定要退出登录吗?",
|
||||
success: function (res) {
|
||||
if (res.confirm) {
|
||||
userStore.logout();
|
||||
uni.showToast({
|
||||
title: "已退出登录",
|
||||
icon: "success",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 返回
|
||||
const handleBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
// 检查登录状态
|
||||
onLoad(() => {
|
||||
getCacheSize();
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.logout-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 20rpx;
|
||||
margin-top: 60rpx;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 90%;
|
||||
height: 90rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
background-color: var(--wot-color-theme, var(--primary-color));
|
||||
border: none;
|
||||
border-radius: 45rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(22, 93, 255, 0.3);
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:active {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.loading-center) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
:deep(.loading-center .wd-loading__spinner) {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
:deep(.loading-center .wd-loading__text) {
|
||||
margin-top: 20rpx;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,229 @@
|
||||
<template>
|
||||
<view class="app-container">
|
||||
<wd-navbar title="网络测试" left-arrow @click-left="handleBack" />
|
||||
|
||||
<!-- 网络状态展示 -->
|
||||
<wd-card title="网络状态" custom-style="margin: 20rpx">
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="网络状态">
|
||||
<wd-tag :type="networkType ? 'success' : 'danger'" size="small">
|
||||
{{ networkType ? "在线" : "离线" }}
|
||||
</wd-tag>
|
||||
</wd-cell>
|
||||
<wd-cell title="网络类型" :value="networkType || '未知'" />
|
||||
<wd-cell title="网络强度" :value="signalStrength" />
|
||||
</wd-cell-group>
|
||||
</wd-card>
|
||||
|
||||
<!-- 网络测试 -->
|
||||
<wd-card title="网络测试" custom-style="margin: 20rpx">
|
||||
<view slot="extra">
|
||||
<text class="text-gray-500 text-sm">测试服务器连接情况</text>
|
||||
</view>
|
||||
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="延迟">
|
||||
<view class="flex items-center">
|
||||
<text class="mr-10">{{ pingResult.delay }}ms</text>
|
||||
<wd-tag v-if="getPingStatus" :type="getPingStatusType" size="small">
|
||||
{{ pingResult.status }}
|
||||
</wd-tag>
|
||||
</view>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
|
||||
<wd-progress
|
||||
v-if="testing"
|
||||
:percentage="progress"
|
||||
stroke-width="4"
|
||||
custom-style="margin: 30rpx 0"
|
||||
/>
|
||||
|
||||
<wd-button block type="primary" :loading="testing" @click="startTest">
|
||||
{{ testing ? "测试中..." : "开始测试" }}
|
||||
</wd-button>
|
||||
</wd-card>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import request from "@/utils/request";
|
||||
|
||||
interface PingResult {
|
||||
delay: string | number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
// 声明全局 wx 对象
|
||||
declare const wx: any;
|
||||
|
||||
// 响应式状态
|
||||
const networkType = ref("");
|
||||
const signalStrength = ref("获取中...");
|
||||
const testing = ref(false);
|
||||
const progress = ref(0);
|
||||
const pingResult = ref<PingResult>({
|
||||
delay: "--",
|
||||
status: "未测试",
|
||||
});
|
||||
const networkListener = ref<any>(null);
|
||||
|
||||
// 计算属性
|
||||
const getPingStatus = computed(() => {
|
||||
if (pingResult.value.delay === "--") return "";
|
||||
// 将 delay 转换为数字进行比较
|
||||
const delay = Number(pingResult.value.delay);
|
||||
if (isNaN(delay)) return "";
|
||||
if (delay < 100) return "good";
|
||||
if (delay < 300) return "normal";
|
||||
return "bad";
|
||||
});
|
||||
|
||||
// 计算状态对应的Tag类型
|
||||
const getPingStatusType = computed(() => {
|
||||
const status = getPingStatus.value;
|
||||
if (status === "good") return "success";
|
||||
if (status === "normal") return "warning";
|
||||
if (status === "bad") return "danger";
|
||||
return "primary";
|
||||
});
|
||||
|
||||
// 方法
|
||||
const getNetworkType = async () => {
|
||||
try {
|
||||
const res = await uni.getNetworkType();
|
||||
networkType.value = res.networkType;
|
||||
|
||||
// 微信小程序支持获取信号强度
|
||||
// #ifdef MP-WEIXIN
|
||||
if (wx?.getNetworkWeakness) {
|
||||
const weaknessRes = await wx.getNetworkWeakness();
|
||||
signalStrength.value = `${weaknessRes.weaknessLevel}%`;
|
||||
} else {
|
||||
signalStrength.value = "不支持";
|
||||
}
|
||||
// #endif
|
||||
|
||||
// H5环境
|
||||
// #ifdef H5
|
||||
signalStrength.value = (navigator as any).connection
|
||||
? `${(navigator as any).connection.effectiveType || "未知"}`
|
||||
: "不支持";
|
||||
// #endif
|
||||
} catch {
|
||||
networkType.value = "获取失败";
|
||||
signalStrength.value = "获取失败";
|
||||
}
|
||||
};
|
||||
|
||||
// 监听网络状态变化
|
||||
const listenNetworkStatus = () => {
|
||||
// #ifdef MP-WEIXIN
|
||||
networkListener.value = wx?.onNetworkStatusChange((res: any) => {
|
||||
networkType.value = res.networkType;
|
||||
getNetworkType();
|
||||
});
|
||||
// #endif
|
||||
|
||||
// #ifdef H5
|
||||
window.addEventListener("online", getNetworkType);
|
||||
window.addEventListener("offline", () => {
|
||||
networkType.value = "";
|
||||
signalStrength.value = "离线";
|
||||
});
|
||||
// #endif
|
||||
};
|
||||
|
||||
// 开始网络测试
|
||||
const startTest = async () => {
|
||||
if (testing.value) return;
|
||||
|
||||
testing.value = true;
|
||||
progress.value = 0;
|
||||
pingResult.value.delay = "--";
|
||||
pingResult.value.status = "测试中";
|
||||
|
||||
const progressTimer = setInterval(() => {
|
||||
if (progress.value < 90) {
|
||||
progress.value += 10;
|
||||
}
|
||||
}, 200);
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
// #ifdef H5
|
||||
await uni.request({
|
||||
url: "/api/v1/auth/captcha",
|
||||
timeout: 5000,
|
||||
});
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
await request({
|
||||
url: "/api/v1/auth/captcha",
|
||||
timeout: 5000,
|
||||
});
|
||||
// #endif
|
||||
const endTime = Date.now();
|
||||
const delay = endTime - startTime;
|
||||
|
||||
pingResult.value.delay = delay;
|
||||
pingResult.value.status = delay < 300 ? "正常" : "较慢";
|
||||
} catch {
|
||||
pingResult.value.delay = "--";
|
||||
pingResult.value.status = "连接失败";
|
||||
} finally {
|
||||
clearInterval(progressTimer);
|
||||
progress.value = 100;
|
||||
setTimeout(() => {
|
||||
testing.value = false;
|
||||
progress.value = 0;
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
// 返回
|
||||
const handleBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
getNetworkType();
|
||||
listenNetworkStatus();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// #ifdef MP-WEIXIN
|
||||
if (networkListener.value?.clear) {
|
||||
networkListener.value.clear();
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef H5
|
||||
window.removeEventListener("online", getNetworkType);
|
||||
window.removeEventListener("offline", getNetworkType);
|
||||
// #endif
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mr-10 {
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.text-gray-500 {
|
||||
color: #9e9e9e;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,231 @@
|
||||
<template>
|
||||
<!-- 内容区域 -->
|
||||
<view class="app-container">
|
||||
<wd-navbar title="主题设置" left-arrow @click-left="handleBack" />
|
||||
|
||||
<!-- 页面标题 -->
|
||||
<wd-card custom-class="page-header">
|
||||
<text class="page-title">主题设置</text>
|
||||
<view class="page-subtitle">个性化您的应用外观</view>
|
||||
</wd-card>
|
||||
|
||||
<!-- 暗黑模式设置 -->
|
||||
<wd-card class="mb-3">
|
||||
<view class="flex-between py-2">
|
||||
<text>暗黑模式</text>
|
||||
<wd-switch :model-value="theme === 'dark'" @change="toggleTheme" />
|
||||
</view>
|
||||
</wd-card>
|
||||
<!-- 主题色选择 -->
|
||||
<wd-card title="主题色" class="mb-3">
|
||||
<view class="color-grid">
|
||||
<view
|
||||
v-for="item in colorColumns"
|
||||
:key="item.value"
|
||||
class="color-item"
|
||||
:class="{ active: currentThemeColor === item.value }"
|
||||
@click="setThemeColor(item.value)"
|
||||
>
|
||||
<view class="color-box" :style="{ backgroundColor: item.value }">
|
||||
<wd-icon v-if="currentThemeColor === item.value" name="check" size="16" color="#fff" />
|
||||
</view>
|
||||
<text class="color-label">{{ item.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</wd-card>
|
||||
<!-- 自定义颜色 -->
|
||||
<wd-card class="mb-3">
|
||||
<view class="flex-between items-center py-2" @click="showCustomColorPopup = true">
|
||||
<view class="flex-start gap-2 items-center">
|
||||
<wd-icon name="edit" size="20" :color="currentThemeColor" />
|
||||
<text>自定义颜色</text>
|
||||
</view>
|
||||
<view class="flex-start gap-2 items-center">
|
||||
<view class="color-box small" :style="{ backgroundColor: currentThemeColor }"></view>
|
||||
<text class="text-sm text-gray-500">{{ currentThemeColor }}</text>
|
||||
<wd-icon name="arrow-right" size="14" color="#999" />
|
||||
</view>
|
||||
</view>
|
||||
</wd-card>
|
||||
<!-- 预览效果 -->
|
||||
<wd-card title="预览效果" class="mb-3">
|
||||
<view class="py-2">
|
||||
<view class="flex-start gap-2">
|
||||
<wd-button type="primary" size="small">主要按钮</wd-button>
|
||||
<wd-button type="primary" plain size="small">次要按钮</wd-button>
|
||||
<wd-tag type="primary">标签</wd-tag>
|
||||
</view>
|
||||
</view>
|
||||
</wd-card>
|
||||
<!-- 重置按钮 -->
|
||||
<view class="mt-5 mx-3">
|
||||
<wd-button plain block @click="handleReset">恢复默认</wd-button>
|
||||
</view>
|
||||
|
||||
<!-- 自定义颜色弹窗 -->
|
||||
<wd-popup v-model="showCustomColorPopup" position="bottom" closeable>
|
||||
<view class="custom-color-popup">
|
||||
<view class="text-center mb-5"><text class="text-lg font-bold">自定义主题色</text></view>
|
||||
<view class="mb-5">
|
||||
<view class="color-preview-large" :style="{ backgroundColor: customColor }"></view>
|
||||
<wd-input v-model="customColor" placeholder="请输入颜色值,如 #FF6B6B" clearable />
|
||||
<text class="input-tip">支持 HEX 格式颜色值</text>
|
||||
</view>
|
||||
<view class="flex gap-2">
|
||||
<wd-button type="info" block @click="showCustomColorPopup = false">取消</wd-button>
|
||||
<wd-button type="primary" block @click="applyCustomColor">应用</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</view>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onShow, onLoad } from "@dcloudio/uni-app";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
|
||||
const { theme, currentThemeColor, colorColumns, toggleTheme, setThemeColor, resetTheme } =
|
||||
useTheme();
|
||||
|
||||
// 自定义颜色相关
|
||||
const showCustomColorPopup = ref(false);
|
||||
const customColor = ref(currentThemeColor.value);
|
||||
|
||||
// 动态设置页面标题
|
||||
onLoad(() => {
|
||||
uni.setNavigationBarTitle({
|
||||
title: "主题设置",
|
||||
});
|
||||
});
|
||||
|
||||
// 应用自定义颜色
|
||||
const applyCustomColor = () => {
|
||||
const colorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
|
||||
|
||||
if (!colorRegex.test(customColor.value)) {
|
||||
uni.showToast({
|
||||
title: "请输入正确的颜色格式",
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setThemeColor(customColor.value);
|
||||
showCustomColorPopup.value = false;
|
||||
uni.showToast({
|
||||
title: "主题色已更新",
|
||||
icon: "success",
|
||||
});
|
||||
};
|
||||
|
||||
// 重置主题
|
||||
const handleReset = () => {
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: "确定要恢复默认主题吗?",
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
resetTheme();
|
||||
customColor.value = currentThemeColor.value;
|
||||
uni.showToast({
|
||||
title: "已恢复默认",
|
||||
icon: "success",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 处理返回按钮点击
|
||||
const handleBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
// 页面显示时更新自定义颜色值
|
||||
onShow(() => {
|
||||
customColor.value = currentThemeColor.value;
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.page-header {
|
||||
padding: 40rpx 20rpx;
|
||||
margin-top: 20rpx;
|
||||
text-align: center;
|
||||
background: linear-gradient(135deg, var(--wot-color-theme) 0%, var(--primary-color-light) 100%);
|
||||
|
||||
.page-title {
|
||||
display: block;
|
||||
margin-bottom: 10rpx;
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.color-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 24rpx 20rpx;
|
||||
}
|
||||
|
||||
.color-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
align-items: center;
|
||||
padding: 8rpx;
|
||||
cursor: pointer;
|
||||
|
||||
&.active .color-box {
|
||||
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.15);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.color-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 12rpx;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.small {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.color-label {
|
||||
font-size: 22rpx;
|
||||
color: var(--wot-color-text-secondary);
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-color-popup {
|
||||
padding: 40rpx 30rpx;
|
||||
background-color: var(--wot-color-bg-container);
|
||||
|
||||
.color-preview-large {
|
||||
width: 100%;
|
||||
height: 120rpx;
|
||||
margin-bottom: 30rpx;
|
||||
border: 2rpx solid var(--wot-color-border);
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.input-tip {
|
||||
display: block;
|
||||
margin-top: 15rpx;
|
||||
font-size: 24rpx;
|
||||
color: var(--wot-color-text-placeholder);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<view class="app-container">
|
||||
<wd-status-tip type="search" tip="建设中..." />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"name": "work",
|
||||
"style": {
|
||||
"navigationBarTitleText": "工作台"
|
||||
},
|
||||
"meta": {
|
||||
"requireAuth": true
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
||||
<style lang="scss"></style>
|
||||
@@ -0,0 +1,69 @@
|
||||
import { pages, subPackages } from "virtual:uni-pages";
|
||||
import { isLoggedIn } from "@/utils/auth";
|
||||
import { createRouter } from "uni-mini-router";
|
||||
|
||||
// 生成路由配置
|
||||
function generateRoutes() {
|
||||
const routes = pages.map((page: { path: string; [key: string]: any }) => {
|
||||
const newPath = `/${page.path}`;
|
||||
return { ...page, path: newPath };
|
||||
});
|
||||
|
||||
// 处理分包路由
|
||||
if (subPackages && subPackages.length > 0) {
|
||||
subPackages.forEach((subPackage: { root: string; pages: any[] }) => {
|
||||
const subRoutes = subPackage.pages.map((page: any) => {
|
||||
const newPath = `/${subPackage.root}/${page.path}`;
|
||||
return { ...page, path: newPath };
|
||||
});
|
||||
routes.push(...subRoutes);
|
||||
});
|
||||
}
|
||||
return routes;
|
||||
}
|
||||
|
||||
// 创建路由实例
|
||||
const router = createRouter({
|
||||
routes: generateRoutes(),
|
||||
});
|
||||
|
||||
// 全局前置守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
// 检查页面是否需要登录
|
||||
if (to.meta && to.meta.requireAuth && !isLoggedIn()) {
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: "该功能需要登录后使用",
|
||||
confirmText: "去登录",
|
||||
cancelText: "返回",
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 记住原来要去的页面
|
||||
uni.setStorageSync("redirect", to.fullPath);
|
||||
// 使用 uni 原生导航而不是 router
|
||||
uni.navigateTo({
|
||||
url: "/pages/login/index",
|
||||
});
|
||||
} else {
|
||||
// 取消则返回首页
|
||||
uni.switchTab({
|
||||
url: "/pages/index/index",
|
||||
});
|
||||
}
|
||||
},
|
||||
// 确保在取消弹窗时也能调用 next
|
||||
fail: () => {
|
||||
next(false);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// 继续导航
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
router.afterEach((to) => {
|
||||
console.log("路由跳转完成:", to.path);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,6 @@
|
||||
export {};
|
||||
|
||||
declare module "vue" {
|
||||
type Hooks = App.AppInstance & Page.PageInstance;
|
||||
interface ComponentCustomOptions extends Hooks {}
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
@@ -0,0 +1,54 @@
|
||||
<svg width="100%" height="100%" viewBox="0 0 1000 1000" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- 背景渐变 -->
|
||||
<defs>
|
||||
<!-- 主背景渐变 - 蓝色系 -->
|
||||
<linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#94BFFF" />
|
||||
<stop offset="100%" stop-color="#165DFF" />
|
||||
</linearGradient>
|
||||
|
||||
<!-- 图形渐变 - 白色半透明 -->
|
||||
<linearGradient id="shapeGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.3" />
|
||||
<stop offset="100%" stop-color="#ffffff" stop-opacity="0.15" />
|
||||
</linearGradient>
|
||||
|
||||
<!-- 底部波浪渐变 -->
|
||||
<linearGradient id="waveGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#FFFFFF" stop-opacity="0.05" />
|
||||
<stop offset="100%" stop-color="#FFFFFF" stop-opacity="0.1" />
|
||||
</linearGradient>
|
||||
|
||||
<!-- 为暗黑模式提供的额外效果 -->
|
||||
<filter id="softGlow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur stdDeviation="15" result="blur" />
|
||||
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- 顶部背景 -->
|
||||
<rect width="100%" height="100%" fill="#1A1A1A" opacity="0" class="dark-mode-bg" />
|
||||
<rect width="100%" height="50%" fill="url(#bgGradient)" />
|
||||
|
||||
<!-- 左侧装饰图形 -->
|
||||
<rect x="100" y="150" width="120" height="120" rx="15" fill="url(#shapeGradient)" transform="rotate(-10, 160, 210)" opacity="0.7" />
|
||||
<rect x="190" y="90" width="80" height="80" rx="10" fill="url(#shapeGradient)" transform="rotate(15, 230, 130)" opacity="0.6" />
|
||||
<rect x="60" y="250" width="100" height="100" rx="10" fill="url(#shapeGradient)" transform="rotate(-5, 110, 300)" opacity="0.5" />
|
||||
|
||||
<!-- 右侧装饰图形 -->
|
||||
<circle cx="750" cy="150" r="60" fill="url(#shapeGradient)" opacity="0.7" />
|
||||
<circle cx="820" cy="230" r="90" fill="url(#shapeGradient)" opacity="0.5" />
|
||||
<circle cx="690" cy="250" r="40" fill="url(#shapeGradient)" opacity="0.6" />
|
||||
|
||||
<!-- 底部波浪形状,适配深色和浅色模式 -->
|
||||
<path d="M0,900 C200,800 350,950 550,870 C750,790 850,900 1000,850 L1000,1000 L0,1000 Z" fill="url(#waveGradient)" class="wave-light" />
|
||||
<path d="M0,900 C200,800 350,950 550,870 C750,790 850,900 1000,850 L1000,1000 L0,1000 Z" fill="#1A1A1A" opacity="0" class="wave-dark" />
|
||||
|
||||
<style>
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark-mode-bg { opacity: 1; }
|
||||
.wave-light { opacity: 0; }
|
||||
.wave-dark { opacity: 0.7; }
|
||||
}
|
||||
</style>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
@@ -0,0 +1,13 @@
|
||||
import type { App } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
|
||||
const store = createPinia();
|
||||
|
||||
// 全局注册 store
|
||||
export function setupStore(app: App<Element>) {
|
||||
app.use(store);
|
||||
}
|
||||
|
||||
export * from "./modules/user.store";
|
||||
export * from "./modules/theme.store";
|
||||
export { store };
|
||||
@@ -0,0 +1,49 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { applyThemeToMiniProgram } from "@/utils/theme";
|
||||
|
||||
// 从缓存获取主题色
|
||||
const getThemeColor = (): string => {
|
||||
const savedColor = uni.getStorageSync("themeColor");
|
||||
return savedColor || "#165DFF"; // 默认Arco蓝色
|
||||
};
|
||||
|
||||
// 保存主题色到缓存
|
||||
const setThemeColorCache = (color: string) => {
|
||||
uni.setStorageSync("themeColor", color);
|
||||
};
|
||||
|
||||
export const useThemeStore = defineStore("theme", () => {
|
||||
// 主题色
|
||||
const primaryColor = ref<string>(getThemeColor());
|
||||
|
||||
// 设置主题色
|
||||
const setPrimaryColor = (color: string) => {
|
||||
primaryColor.value = color;
|
||||
setThemeColorCache(color);
|
||||
|
||||
// 检测运行环境,区分处理
|
||||
if (typeof document !== "undefined") {
|
||||
// H5环境
|
||||
document.documentElement.style.setProperty("--primary-color", color);
|
||||
|
||||
// 设置简单的衍生色(不依赖外部工具函数)
|
||||
document.documentElement.style.setProperty("--primary-color-light", color + "80"); // 添加透明度
|
||||
document.documentElement.style.setProperty("--primary-color-dark", color);
|
||||
} else {
|
||||
// 小程序环境
|
||||
applyThemeToMiniProgram(color);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化,应用主题色
|
||||
const initTheme = () => {
|
||||
setPrimaryColor(primaryColor.value);
|
||||
};
|
||||
|
||||
return {
|
||||
primaryColor,
|
||||
setPrimaryColor,
|
||||
initTheme,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
import { defineStore } from "pinia";
|
||||
import AuthAPI, { type LoginData, type WxLoginData } from "@/api/auth";
|
||||
import UserAPI, { type UserInfo } from "@/api/user";
|
||||
import { setAccessToken, clearTokens } from "@/utils/auth";
|
||||
import { getUserInfo, setUserInfo } from "@/utils/storage";
|
||||
import { USER_INFO_KEY } from "@/constants";
|
||||
import { Storage } from "@/utils/storage";
|
||||
|
||||
export const useUserStore = defineStore("user", () => {
|
||||
const userInfo = ref<UserInfo | undefined>(getUserInfo());
|
||||
|
||||
// 账号密码登录
|
||||
const login = (data: LoginData) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
AuthAPI.login(data)
|
||||
.then((data) => {
|
||||
setAccessToken(data.accessToken);
|
||||
resolve(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("登录失败", error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 微信基础授权登录
|
||||
const loginWithWxCode = (code: string) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
AuthAPI.loginByWxMiniAppCode(code)
|
||||
.then((data) => {
|
||||
setAccessToken(data.accessToken);
|
||||
resolve(data);
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.error("微信授权登录失败", error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 微信手机号授权登录
|
||||
const loginWithWxPhone = (data: WxLoginData): Promise<any> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
AuthAPI.loginByWxMiniAppPhone(data)
|
||||
.then((result: any) => {
|
||||
setAccessToken(result.accessToken);
|
||||
resolve(result);
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.error("微信手机号登录失败", error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 检查会话状态
|
||||
const checkSession = (): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
AuthAPI.checkSession()
|
||||
.then((result) => {
|
||||
resolve(result.valid);
|
||||
})
|
||||
.catch(() => {
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 获取用户信息
|
||||
const getInfo = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
UserAPI.getUserInfo()
|
||||
.then((data) => {
|
||||
setUserInfo(data);
|
||||
userInfo.value = data;
|
||||
resolve(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("获取用户信息失败", error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 登出
|
||||
const logout = async () => {
|
||||
try {
|
||||
await AuthAPI.logout(); // 调用后台注销接口
|
||||
} catch (error) {
|
||||
console.error("登出失败", error);
|
||||
} finally {
|
||||
clearTokens(); // 清除本地的 token
|
||||
Storage.remove(USER_INFO_KEY); // 清除用户信息缓存
|
||||
userInfo.value = undefined; // 清空用户信息
|
||||
|
||||
// 跳转到登录页面
|
||||
uni.reLaunch({
|
||||
url: "/pages/login/index",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 判断用户信息是否完整
|
||||
const isUserInfoComplete = (): boolean => {
|
||||
if (!userInfo.value) return false;
|
||||
|
||||
return !!(userInfo.value.nickname && userInfo.value.avatar);
|
||||
};
|
||||
|
||||
return {
|
||||
userInfo,
|
||||
login,
|
||||
loginWithWxCode,
|
||||
loginWithWxPhone,
|
||||
logout,
|
||||
getInfo,
|
||||
checkSession,
|
||||
isUserInfoComplete,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
padding: 0 10rpx;
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
const CommonUtil: typeof import('wot-design-uni')['CommonUtil']
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const Storage: typeof import('../utils/storage')['Storage']
|
||||
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
|
||||
const applyThemeOnPageShow: typeof import('../utils/theme')['applyThemeOnPageShow']
|
||||
const applyThemeToMiniProgram: typeof import('../utils/theme')['applyThemeToMiniProgram']
|
||||
const auth: typeof import('../api/auth')['default']
|
||||
const checkLogin: typeof import('../utils/auth')['checkLogin']
|
||||
const clearAll: typeof import('../utils/storage')['clearAll']
|
||||
const clearTokens: typeof import('../utils/auth')['clearTokens']
|
||||
const colorColumns: typeof import('../composables/useTheme')['colorColumns']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const createPinia: typeof import('pinia')['createPinia']
|
||||
const createRouter: typeof import('uni-mini-router')['createRouter']
|
||||
const currentThemeColor: typeof import('../composables/useTheme')['currentThemeColor']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const debounce: typeof import('../utils/index')['debounce']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const defineStore: typeof import('pinia')['defineStore']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const file: typeof import('../api/file')['default']
|
||||
const getAccessToken: typeof import('../utils/auth')['getAccessToken']
|
||||
const getActivePinia: typeof import('pinia')['getActivePinia']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const getRefreshToken: typeof import('../utils/auth')['getRefreshToken']
|
||||
const getToken: typeof import('../utils/storage')['getToken']
|
||||
const getUserInfo: typeof import('../utils/storage')['getUserInfo']
|
||||
const guessSerializerType: typeof import('@uni-helper/uni-use')['guessSerializerType']
|
||||
const h: typeof import('vue')['h']
|
||||
const initTheme: typeof import('../composables/useTheme')['initTheme']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isLoggedIn: typeof import('../utils/auth')['isLoggedIn']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const mapActions: typeof import('pinia')['mapActions']
|
||||
const mapGetters: typeof import('pinia')['mapGetters']
|
||||
const mapState: typeof import('pinia')['mapState']
|
||||
const mapStores: typeof import('pinia')['mapStores']
|
||||
const mapWritableState: typeof import('pinia')['mapWritableState']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onAddToFavorites: typeof import('@dcloudio/uni-app')['onAddToFavorites']
|
||||
const onBackPress: typeof import('@dcloudio/uni-app')['onBackPress']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeRouteLeave: (typeof import("vue-router"))["onBeforeRouteLeave"]
|
||||
const onBeforeRouteUpdate: (typeof import("vue-router"))["onBeforeRouteUpdate"]
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onError: typeof import('@dcloudio/uni-app')['onError']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onHide: typeof import('@dcloudio/uni-app')['onHide']
|
||||
const onLaunch: typeof import('@dcloudio/uni-app')['onLaunch']
|
||||
const onLoad: typeof import('@dcloudio/uni-app')['onLoad']
|
||||
const onMounted: typeof import('vue')['onMounted']
|
||||
const onNavigationBarButtonTap: typeof import('@dcloudio/uni-app')['onNavigationBarButtonTap']
|
||||
const onNavigationBarSearchInputChanged: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputChanged']
|
||||
const onNavigationBarSearchInputClicked: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputClicked']
|
||||
const onNavigationBarSearchInputConfirmed: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputConfirmed']
|
||||
const onNavigationBarSearchInputFocusChanged: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputFocusChanged']
|
||||
const onPageNotFound: typeof import('@dcloudio/uni-app')['onPageNotFound']
|
||||
const onPageScroll: typeof import('@dcloudio/uni-app')['onPageScroll']
|
||||
const onPullDownRefresh: typeof import('@dcloudio/uni-app')['onPullDownRefresh']
|
||||
const onReachBottom: typeof import('@dcloudio/uni-app')['onReachBottom']
|
||||
const onReady: typeof import('@dcloudio/uni-app')['onReady']
|
||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||
const onResize: typeof import('@dcloudio/uni-app')['onResize']
|
||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onShareAppMessage: typeof import('@dcloudio/uni-app')['onShareAppMessage']
|
||||
const onShareTimeline: typeof import('@dcloudio/uni-app')['onShareTimeline']
|
||||
const onShow: typeof import('@dcloudio/uni-app')['onShow']
|
||||
const onTabItemTap: typeof import('@dcloudio/uni-app')['onTabItemTap']
|
||||
const onThemeChange: typeof import('@dcloudio/uni-app')['onThemeChange']
|
||||
const onUnhandledRejection: typeof import('@dcloudio/uni-app')['onUnhandledRejection']
|
||||
const onUnload: typeof import('@dcloudio/uni-app')['onUnload']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const publicRequest: typeof import('../utils/request')['publicRequest']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const request: typeof import('../utils/request')['default']
|
||||
const requireLogin: typeof import('../utils/auth')['requireLogin']
|
||||
const resetTheme: typeof import('../composables/useTheme')['resetTheme']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const setAccessToken: typeof import('../utils/auth')['setAccessToken']
|
||||
const setActivePinia: typeof import('pinia')['setActivePinia']
|
||||
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
|
||||
const setRefreshToken: typeof import('../utils/auth')['setRefreshToken']
|
||||
const setThemeColor: typeof import('../composables/useTheme')['setThemeColor']
|
||||
const setToken: typeof import('../utils/storage')['setToken']
|
||||
const setUserInfo: typeof import('../utils/storage')['setUserInfo']
|
||||
const setupStore: typeof import('../store/index')['setupStore']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const store: typeof import('../store/index')['store']
|
||||
const storeToRefs: typeof import('pinia')['storeToRefs']
|
||||
const theme: typeof import('../composables/useTheme')['theme']
|
||||
const themeColorOptions: typeof import('../composables/useTheme')['themeColorOptions']
|
||||
const themeVars: typeof import('../composables/useTheme')['themeVars']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const toValue: typeof import('vue')['toValue']
|
||||
const toggleTheme: typeof import('../composables/useTheme')['toggleTheme']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const tryOnBackPress: typeof import('@uni-helper/uni-use')['tryOnBackPress']
|
||||
const tryOnHide: typeof import('@uni-helper/uni-use')['tryOnHide']
|
||||
const tryOnInit: typeof import('@uni-helper/uni-use')['tryOnInit']
|
||||
const tryOnLoad: typeof import('@uni-helper/uni-use')['tryOnLoad']
|
||||
const tryOnReady: typeof import('@uni-helper/uni-use')['tryOnReady']
|
||||
const tryOnScopeDispose: typeof import('@uni-helper/uni-use')['tryOnScopeDispose']
|
||||
const tryOnShow: typeof import('@uni-helper/uni-use')['tryOnShow']
|
||||
const tryOnUnload: typeof import('@uni-helper/uni-use')['tryOnUnload']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const useActionSheet: typeof import('@uni-helper/uni-use')['useActionSheet']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useClipboardData: typeof import('@uni-helper/uni-use')['useClipboardData']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useDownloadFile: typeof import('@uni-helper/uni-use')['useDownloadFile']
|
||||
const useGlobalData: typeof import('@uni-helper/uni-use')['useGlobalData']
|
||||
const useId: typeof import('vue')['useId']
|
||||
const useInterceptor: typeof import('@uni-helper/uni-use')['useInterceptor']
|
||||
const useLink: (typeof import("vue-router"))["useLink"]
|
||||
const useLoading: typeof import('@uni-helper/uni-use')['useLoading']
|
||||
const useMessage: typeof import('wot-design-uni')['useMessage']
|
||||
const useModal: typeof import('@uni-helper/uni-use')['useModal']
|
||||
const useModel: typeof import('vue')['useModel']
|
||||
const useNetwork: typeof import('@uni-helper/uni-use')['useNetwork']
|
||||
const useNotify: typeof import('wot-design-uni')['useNotify']
|
||||
const useOnline: typeof import('@uni-helper/uni-use')['useOnline']
|
||||
const usePage: typeof import('@uni-helper/uni-use')['usePage']
|
||||
const usePageScroll: typeof import('@uni-helper/uni-use')['usePageScroll']
|
||||
const usePages: typeof import('@uni-helper/uni-use')['usePages']
|
||||
const usePreferredDark: typeof import('@uni-helper/uni-use')['usePreferredDark']
|
||||
const usePreferredLanguage: typeof import('@uni-helper/uni-use')['usePreferredLanguage']
|
||||
const usePrevPage: typeof import('@uni-helper/uni-use')['usePrevPage']
|
||||
const usePrevRoute: typeof import('@uni-helper/uni-use')['usePrevRoute']
|
||||
const useProvider: typeof import('@uni-helper/uni-use')['useProvider']
|
||||
const useRequest: typeof import('@uni-helper/uni-use')['useRequest']
|
||||
const useRoute: typeof import('uni-mini-router')['useRoute']
|
||||
const useRouter: typeof import('uni-mini-router')['useRouter']
|
||||
const useScanCode: typeof import('@uni-helper/uni-use')['useScanCode']
|
||||
const useScreenBrightness: typeof import('@uni-helper/uni-use')['useScreenBrightness']
|
||||
const useSelectorQuery: typeof import('@uni-helper/uni-use')['useSelectorQuery']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const useSocket: typeof import('@uni-helper/uni-use')['useSocket']
|
||||
const useStomp: typeof import('../composables/useStomp')['useStomp']
|
||||
const useStorage: typeof import('@uni-helper/uni-use')['useStorage']
|
||||
const useStorageAsync: typeof import('@uni-helper/uni-use')['useStorageAsync']
|
||||
const useStorageSync: typeof import('@uni-helper/uni-use')['useStorageSync']
|
||||
const useTabbar: typeof import('../composables/useTabbar')['useTabbar']
|
||||
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||
const useTheme: typeof import('../composables/useTheme')['useTheme']
|
||||
const useThemeStore: typeof import('../store/modules/theme.store')['useThemeStore']
|
||||
const useToast: typeof import('wot-design-uni')['useToast']
|
||||
const useUploadFile: typeof import('@uni-helper/uni-use')['useUploadFile']
|
||||
const useUserStore: typeof import('../store/modules/user.store')['useUserStore']
|
||||
const useVisible: typeof import('@uni-helper/uni-use')['useVisible']
|
||||
const useWechat: typeof import('../composables/useWechat')['useWechat']
|
||||
const user: typeof import('../api/user')['default']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
||||
|
||||
// for vue template auto import
|
||||
import { UnwrapRef } from 'vue'
|
||||
declare module 'vue' {
|
||||
interface GlobalComponents {}
|
||||
interface ComponentCustomProperties {
|
||||
readonly CommonUtil: UnwrapRef<typeof import('wot-design-uni')['CommonUtil']>
|
||||
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
||||
readonly Storage: UnwrapRef<typeof import('../utils/storage')['Storage']>
|
||||
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
|
||||
readonly applyThemeOnPageShow: UnwrapRef<typeof import('../utils/theme')['applyThemeOnPageShow']>
|
||||
readonly applyThemeToMiniProgram: UnwrapRef<typeof import('../utils/theme')['applyThemeToMiniProgram']>
|
||||
readonly auth: UnwrapRef<typeof import('../api/auth')['default']>
|
||||
readonly checkLogin: UnwrapRef<typeof import('../utils/auth')['checkLogin']>
|
||||
readonly clearAll: UnwrapRef<typeof import('../utils/storage')['clearAll']>
|
||||
readonly clearTokens: UnwrapRef<typeof import('../utils/auth')['clearTokens']>
|
||||
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
|
||||
readonly createPinia: UnwrapRef<typeof import('pinia')['createPinia']>
|
||||
readonly createRouter: UnwrapRef<typeof import('uni-mini-router')['createRouter']>
|
||||
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
|
||||
readonly debounce: UnwrapRef<typeof import('../utils/index')['debounce']>
|
||||
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
|
||||
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
|
||||
readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
|
||||
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
|
||||
readonly file: UnwrapRef<typeof import('../api/file')['default']>
|
||||
readonly getAccessToken: UnwrapRef<typeof import('../utils/auth')['getAccessToken']>
|
||||
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
|
||||
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
||||
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||
readonly getRefreshToken: UnwrapRef<typeof import('../utils/auth')['getRefreshToken']>
|
||||
readonly getToken: UnwrapRef<typeof import('../utils/storage')['getToken']>
|
||||
readonly getUserInfo: UnwrapRef<typeof import('../utils/storage')['getUserInfo']>
|
||||
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||
readonly isLoggedIn: UnwrapRef<typeof import('../utils/auth')['isLoggedIn']>
|
||||
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
|
||||
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
||||
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
|
||||
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
|
||||
readonly mapState: UnwrapRef<typeof import('pinia')['mapState']>
|
||||
readonly mapStores: UnwrapRef<typeof import('pinia')['mapStores']>
|
||||
readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']>
|
||||
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
||||
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
||||
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
|
||||
readonly onAddToFavorites: UnwrapRef<typeof import('@dcloudio/uni-app')['onAddToFavorites']>
|
||||
readonly onBackPress: UnwrapRef<typeof import('@dcloudio/uni-app')['onBackPress']>
|
||||
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
|
||||
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
|
||||
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
|
||||
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
|
||||
readonly onError: UnwrapRef<typeof import('@dcloudio/uni-app')['onError']>
|
||||
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
|
||||
readonly onHide: UnwrapRef<typeof import('@dcloudio/uni-app')['onHide']>
|
||||
readonly onLaunch: UnwrapRef<typeof import('@dcloudio/uni-app')['onLaunch']>
|
||||
readonly onLoad: UnwrapRef<typeof import('@dcloudio/uni-app')['onLoad']>
|
||||
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
|
||||
readonly onNavigationBarButtonTap: UnwrapRef<typeof import('@dcloudio/uni-app')['onNavigationBarButtonTap']>
|
||||
readonly onNavigationBarSearchInputChanged: UnwrapRef<typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputChanged']>
|
||||
readonly onNavigationBarSearchInputClicked: UnwrapRef<typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputClicked']>
|
||||
readonly onNavigationBarSearchInputConfirmed: UnwrapRef<typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputConfirmed']>
|
||||
readonly onNavigationBarSearchInputFocusChanged: UnwrapRef<typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputFocusChanged']>
|
||||
readonly onPageNotFound: UnwrapRef<typeof import('@dcloudio/uni-app')['onPageNotFound']>
|
||||
readonly onPageScroll: UnwrapRef<typeof import('@dcloudio/uni-app')['onPageScroll']>
|
||||
readonly onPullDownRefresh: UnwrapRef<typeof import('@dcloudio/uni-app')['onPullDownRefresh']>
|
||||
readonly onReachBottom: UnwrapRef<typeof import('@dcloudio/uni-app')['onReachBottom']>
|
||||
readonly onReady: UnwrapRef<typeof import('@dcloudio/uni-app')['onReady']>
|
||||
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
|
||||
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
|
||||
readonly onResize: UnwrapRef<typeof import('@dcloudio/uni-app')['onResize']>
|
||||
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
|
||||
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
|
||||
readonly onShareAppMessage: UnwrapRef<typeof import('@dcloudio/uni-app')['onShareAppMessage']>
|
||||
readonly onShareTimeline: UnwrapRef<typeof import('@dcloudio/uni-app')['onShareTimeline']>
|
||||
readonly onShow: UnwrapRef<typeof import('@dcloudio/uni-app')['onShow']>
|
||||
readonly onTabItemTap: UnwrapRef<typeof import('@dcloudio/uni-app')['onTabItemTap']>
|
||||
readonly onThemeChange: UnwrapRef<typeof import('@dcloudio/uni-app')['onThemeChange']>
|
||||
readonly onUnhandledRejection: UnwrapRef<typeof import('@dcloudio/uni-app')['onUnhandledRejection']>
|
||||
readonly onUnload: UnwrapRef<typeof import('@dcloudio/uni-app')['onUnload']>
|
||||
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
|
||||
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
|
||||
readonly onWatcherCleanup: UnwrapRef<typeof import('vue')['onWatcherCleanup']>
|
||||
readonly provide: UnwrapRef<typeof import('vue')['provide']>
|
||||
readonly publicRequest: UnwrapRef<typeof import('../utils/request')['publicRequest']>
|
||||
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
|
||||
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
|
||||
readonly ref: UnwrapRef<typeof import('vue')['ref']>
|
||||
readonly request: UnwrapRef<typeof import('../utils/request')['default']>
|
||||
readonly requireLogin: UnwrapRef<typeof import('../utils/auth')['requireLogin']>
|
||||
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
||||
readonly setAccessToken: UnwrapRef<typeof import('../utils/auth')['setAccessToken']>
|
||||
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
|
||||
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
|
||||
readonly setRefreshToken: UnwrapRef<typeof import('../utils/auth')['setRefreshToken']>
|
||||
readonly setToken: UnwrapRef<typeof import('../utils/storage')['setToken']>
|
||||
readonly setUserInfo: UnwrapRef<typeof import('../utils/storage')['setUserInfo']>
|
||||
readonly setupStore: UnwrapRef<typeof import('../store/index')['setupStore']>
|
||||
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
|
||||
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
|
||||
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
||||
readonly store: UnwrapRef<typeof import('../store/index')['store']>
|
||||
readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']>
|
||||
readonly themeColorOptions: UnwrapRef<typeof import('../composables/useTheme')['themeColorOptions']>
|
||||
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
|
||||
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
|
||||
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
|
||||
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
|
||||
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
|
||||
readonly unref: UnwrapRef<typeof import('vue')['unref']>
|
||||
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
|
||||
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
|
||||
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
|
||||
readonly useId: UnwrapRef<typeof import('vue')['useId']>
|
||||
readonly useMessage: UnwrapRef<typeof import('wot-design-uni')['useMessage']>
|
||||
readonly useModel: UnwrapRef<typeof import('vue')['useModel']>
|
||||
readonly useNotify: UnwrapRef<typeof import('wot-design-uni')['useNotify']>
|
||||
readonly useRoute: UnwrapRef<typeof import('uni-mini-router')['useRoute']>
|
||||
readonly useRouter: UnwrapRef<typeof import('uni-mini-router')['useRouter']>
|
||||
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
||||
readonly useStomp: UnwrapRef<typeof import('../composables/useStomp')['useStomp']>
|
||||
readonly useTabbar: UnwrapRef<typeof import('../composables/useTabbar')['useTabbar']>
|
||||
readonly useTemplateRef: UnwrapRef<typeof import('vue')['useTemplateRef']>
|
||||
readonly useTheme: UnwrapRef<typeof import('../composables/useTheme')['useTheme']>
|
||||
readonly useThemeStore: UnwrapRef<typeof import('../store/modules/theme.store')['useThemeStore']>
|
||||
readonly useToast: UnwrapRef<typeof import('wot-design-uni')['useToast']>
|
||||
readonly useUserStore: UnwrapRef<typeof import('../store/modules/user.store')['useUserStore']>
|
||||
readonly useWechat: UnwrapRef<typeof import('../composables/useWechat')['useWechat']>
|
||||
readonly user: UnwrapRef<typeof import('../api/user')['default']>
|
||||
readonly watch: UnwrapRef<typeof import('vue')['watch']>
|
||||
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
|
||||
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
|
||||
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// src/types/env.d.ts
|
||||
/**
|
||||
* 环境变量类型声明
|
||||
*/
|
||||
interface ImportMetaEnv {
|
||||
/**
|
||||
* 应用端口
|
||||
*/
|
||||
VITE_APP_PORT: number;
|
||||
/** API基础路径 */
|
||||
readonly VITE_APP_BASE_API: string;
|
||||
/** API服务器URL */
|
||||
readonly VITE_APP_API_URL: string;
|
||||
|
||||
/**
|
||||
* WebSocket 端点
|
||||
*/
|
||||
readonly VITE_APP_WS_ENDPOINT?: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
declare global {
|
||||
/**
|
||||
* 分页查询参数
|
||||
*/
|
||||
interface PageQuery {
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页响应对象
|
||||
*/
|
||||
interface PageResult<T> {
|
||||
/** 数据列表 */
|
||||
list: T;
|
||||
/** 总数 */
|
||||
total: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件数据源
|
||||
*/
|
||||
interface OptionType {
|
||||
/** 值 */
|
||||
value: string | number;
|
||||
/** 文本 */
|
||||
label: string;
|
||||
/** 子列表 */
|
||||
children?: OptionType[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应数据
|
||||
*/
|
||||
interface ResponseData<T = any> {
|
||||
code: string;
|
||||
data: T;
|
||||
msg: string;
|
||||
}
|
||||
}
|
||||
export {};
|
||||
@@ -0,0 +1,12 @@
|
||||
import "uni-mini-router";
|
||||
|
||||
declare module "uni-mini-router" {
|
||||
interface Route {
|
||||
path: string;
|
||||
fullPath: string;
|
||||
meta?: {
|
||||
requireAuth?: boolean;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
declare module "virtual:uni-pages" {
|
||||
interface Page {
|
||||
path: string;
|
||||
style?: Record<string, any>;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface SubPackage {
|
||||
root: string;
|
||||
pages: Page[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const pages: Page[];
|
||||
export const subPackages: SubPackage[];
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* 这里是uni-app内置的常用样式变量
|
||||
*
|
||||
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
|
||||
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
|
||||
*
|
||||
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
|
||||
*/
|
||||
|
||||
/* 颜色变量 */
|
||||
|
||||
/* 行为相关颜色 */
|
||||
$uni-color-primary: #007aff;
|
||||
$uni-color-success: #4cd964;
|
||||
$uni-color-warning: #f0ad4e;
|
||||
$uni-color-error: #dd524d;
|
||||
|
||||
/* 文字基本颜色 */
|
||||
$uni-text-color: #333; // 基本色
|
||||
$uni-text-color-inverse: #fff; // 反色
|
||||
$uni-text-color-grey: #999; // 辅助灰色,如加载更多的提示信息
|
||||
$uni-text-color-placeholder: #808080;
|
||||
$uni-text-color-disable: #c0c0c0;
|
||||
|
||||
/* 背景颜色 */
|
||||
$uni-bg-color: #fff;
|
||||
$uni-bg-color-grey: #f8f8f8;
|
||||
$uni-bg-color-hover: #f1f1f1; // 点击状态颜色
|
||||
$uni-bg-color-mask: rgba(0, 0, 0, 0.4); // 遮罩颜色
|
||||
|
||||
/* 边框颜色 */
|
||||
$uni-border-color: #c8c7cc;
|
||||
|
||||
/* 尺寸变量 */
|
||||
|
||||
/* 文字尺寸 */
|
||||
$uni-font-size-sm: 12px;
|
||||
$uni-font-size-base: 14px;
|
||||
$uni-font-size-lg: 16;
|
||||
|
||||
/* 图片尺寸 */
|
||||
$uni-img-size-sm: 20px;
|
||||
$uni-img-size-base: 26px;
|
||||
$uni-img-size-lg: 40px;
|
||||
|
||||
/* Border Radius */
|
||||
$uni-border-radius-sm: 2px;
|
||||
$uni-border-radius-base: 3px;
|
||||
$uni-border-radius-lg: 6px;
|
||||
$uni-border-radius-circle: 50%;
|
||||
|
||||
/* 水平间距 */
|
||||
$uni-spacing-row-sm: 5px;
|
||||
$uni-spacing-row-base: 10px;
|
||||
$uni-spacing-row-lg: 15px;
|
||||
|
||||
/* 垂直间距 */
|
||||
$uni-spacing-col-sm: 4px;
|
||||
$uni-spacing-col-base: 8px;
|
||||
$uni-spacing-col-lg: 12px;
|
||||
|
||||
/* 透明度 */
|
||||
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
|
||||
|
||||
/* 文章场景相关 */
|
||||
$uni-color-title: #2c405a; // 文章标题颜色
|
||||
$uni-font-size-title: 20px;
|
||||
$uni-color-subtitle: #555; // 二级标题颜色
|
||||
$uni-font-size-subtitle: 18px;
|
||||
$uni-color-paragraph: #3f536e; // 文章段落颜色
|
||||
$uni-font-size-paragraph: 15px;
|
||||
|
||||
/* 暗黑模式全局样式 */
|
||||
.wot-theme-dark {
|
||||
color: #f5f5f5 !important;
|
||||
background-color: #1a1a1a !important;
|
||||
|
||||
/* 页面背景 */
|
||||
page {
|
||||
color: #f5f5f5 !important;
|
||||
background-color: #1a1a1a !important;
|
||||
}
|
||||
|
||||
/* H5 环境 body 样式 */
|
||||
body {
|
||||
color: #f5f5f5 !important;
|
||||
background-color: #1a1a1a !important;
|
||||
}
|
||||
|
||||
/* 通用组件暗黑模式适配 */
|
||||
.uni-page-wrapper {
|
||||
color: #f5f5f5 !important;
|
||||
background-color: #1a1a1a !important;
|
||||
}
|
||||
|
||||
/* 导航栏暗黑模式 */
|
||||
.uni-navbar {
|
||||
color: #f5f5f5 !important;
|
||||
background-color: #2a2a2a !important;
|
||||
border-bottom-color: #404040 !important;
|
||||
}
|
||||
|
||||
/* 标签栏暗黑模式 */
|
||||
.uni-tabbar {
|
||||
background-color: #2a2a2a !important;
|
||||
border-top-color: #404040 !important;
|
||||
}
|
||||
|
||||
/* 卡片组件暗黑模式 */
|
||||
.uni-card {
|
||||
color: #f5f5f5 !important;
|
||||
background-color: #2a2a2a !important;
|
||||
}
|
||||
|
||||
/* 列表项暗黑模式 */
|
||||
.uni-list-item {
|
||||
color: #f5f5f5 !important;
|
||||
background-color: #2a2a2a !important;
|
||||
border-bottom-color: #404040 !important;
|
||||
}
|
||||
|
||||
/* 输入框暗黑模式 */
|
||||
.uni-input {
|
||||
color: #f5f5f5 !important;
|
||||
background-color: #404040 !important;
|
||||
border-color: #606060 !important;
|
||||
}
|
||||
|
||||
/* 按钮暗黑模式适配 */
|
||||
.uni-button {
|
||||
&.uni-button-default {
|
||||
color: #f5f5f5 !important;
|
||||
background-color: #404040 !important;
|
||||
border-color: #606060 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { useUserStore } from "@/store/modules/user.store";
|
||||
import { Storage } from "./storage";
|
||||
import { ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY } from "@/constants";
|
||||
|
||||
/**
|
||||
* 认证工具函数
|
||||
*
|
||||
* 使用示例:
|
||||
*
|
||||
* 1. 检查登录状态并自动跳转:
|
||||
* if (!checkLogin()) return; // 未登录会自动跳转到登录页
|
||||
*
|
||||
* 2. 静默检查登录状态:
|
||||
* if (!isLoggedIn()) {
|
||||
* // 处理未登录逻辑,不会自动跳转
|
||||
* }
|
||||
*
|
||||
* 3. 强制要求登录:
|
||||
* requireLogin(); // 清除无效状态并跳转到登录页
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取访问令牌
|
||||
* @returns 返回访问令牌,如果不存在则返回null
|
||||
*/
|
||||
export function getAccessToken(): string | null {
|
||||
return Storage.get<string>(ACCESS_TOKEN_KEY) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置访问令牌
|
||||
* @param token 访问令牌
|
||||
*/
|
||||
export function setAccessToken(token: string): void {
|
||||
Storage.set(ACCESS_TOKEN_KEY, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取刷新令牌
|
||||
* @returns 返回刷新令牌,如果不存在则返回null
|
||||
*/
|
||||
export function getRefreshToken(): string | null {
|
||||
return Storage.get<string>(REFRESH_TOKEN_KEY) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置刷新令牌
|
||||
* @param token 刷新令牌
|
||||
*/
|
||||
export function setRefreshToken(token: string): void {
|
||||
Storage.set(REFRESH_TOKEN_KEY, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有令牌
|
||||
*/
|
||||
export function clearTokens(): void {
|
||||
Storage.remove(ACCESS_TOKEN_KEY);
|
||||
Storage.remove(REFRESH_TOKEN_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户登录状态,未登录则跳转到登录页面
|
||||
* @param silent 是否静默检查,不跳转登录页面
|
||||
* @returns 返回用户是否已登录
|
||||
*/
|
||||
export function checkLogin(silent: boolean = false): boolean {
|
||||
const userStore = useUserStore();
|
||||
const accessToken = getAccessToken();
|
||||
|
||||
// 检查 token 和用户信息是否都存在
|
||||
const isLoggedIn = !!(accessToken && userStore.userInfo);
|
||||
|
||||
if (!isLoggedIn && !silent) {
|
||||
try {
|
||||
// 获取当前页面路径
|
||||
let currentPagePath = "/pages/index/index"; // 默认路径
|
||||
|
||||
const pages = getCurrentPages();
|
||||
if (pages && pages.length > 0) {
|
||||
const currentPage = pages[pages.length - 1];
|
||||
if (currentPage && currentPage.route) {
|
||||
currentPagePath = `/${currentPage.route}`;
|
||||
|
||||
// 处理页面参数 - 使用类型断言
|
||||
const pageOptions = (currentPage as any).options;
|
||||
if (pageOptions && Object.keys(pageOptions).length > 0) {
|
||||
const params = new URLSearchParams(pageOptions as Record<string, string>);
|
||||
currentPagePath += `?${params.toString()}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到登录页面
|
||||
uni.navigateTo({
|
||||
url: `/pages/login/index?redirect=${encodeURIComponent(currentPagePath)}`,
|
||||
fail: (error) => {
|
||||
console.error("跳转登录页面失败:", error);
|
||||
// 如果 navigateTo 失败,尝试使用 reLaunch
|
||||
uni.reLaunch({
|
||||
url: "/pages/login/index",
|
||||
});
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("检查登录状态时发生错误:", error);
|
||||
// 发生错误时,尝试直接跳转到登录页
|
||||
uni.reLaunch({
|
||||
url: "/pages/login/index",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return isLoggedIn;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否已登录(静默检查,不跳转)
|
||||
* @returns 返回用户是否已登录
|
||||
*/
|
||||
export function isLoggedIn(): boolean {
|
||||
return checkLogin(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制用户登录,清除无效的登录状态
|
||||
*/
|
||||
export function requireLogin(): void {
|
||||
const userStore = useUserStore();
|
||||
const accessToken = getAccessToken();
|
||||
|
||||
if (!accessToken || !userStore.userInfo) {
|
||||
// 清除可能存在的无效状态
|
||||
clearTokens();
|
||||
userStore.logout();
|
||||
|
||||
// 跳转到登录页
|
||||
uni.reLaunch({
|
||||
url: "/pages/login/index",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 防抖函数
|
||||
* @param fn 函数
|
||||
* @param delay 延迟时间
|
||||
* @returns
|
||||
*/
|
||||
const debounce = <T extends (...args: any[]) => any>(fn: T, delay: number) => {
|
||||
let timer: number | null = null;
|
||||
return function (this: any, ...args: Parameters<T>) {
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
fn.apply(this, args);
|
||||
}, delay);
|
||||
};
|
||||
};
|
||||
|
||||
export { debounce };
|
||||
@@ -0,0 +1,93 @@
|
||||
import { getAccessToken } from "./auth";
|
||||
|
||||
// 请求配置
|
||||
interface RequestOptions<T = any> {
|
||||
url: string;
|
||||
method: "GET" | "POST" | "PUT" | "DELETE";
|
||||
data?: T;
|
||||
header?: Record<string, string>;
|
||||
timeout?: number;
|
||||
responseType?: "text" | "arraybuffer";
|
||||
skipAuth?: boolean; // 标记是否跳过认证
|
||||
}
|
||||
|
||||
// 请求函数
|
||||
function request<T = any>(options: RequestOptions): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
// 构建请求头
|
||||
const header = Object.assign({}, options.header || {});
|
||||
|
||||
// 检查是否需要添加认证令牌
|
||||
if (!options.skipAuth) {
|
||||
const token = getAccessToken();
|
||||
if (token) {
|
||||
header["Authorization"] = `Bearer ${token}`;
|
||||
} else {
|
||||
// 需要认证但没有令牌,跳转到登录页
|
||||
uni.navigateTo({
|
||||
url: "/pages/login/index",
|
||||
});
|
||||
return reject(new Error("请先登录"));
|
||||
}
|
||||
}
|
||||
|
||||
// 根据平台决定URL前缀
|
||||
let requestUrl = options.url;
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信小程序环境,使用完整URL
|
||||
requestUrl = `${import.meta.env.VITE_APP_API_URL}${options.url}`;
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
// 非微信小程序环境,使用代理前缀
|
||||
requestUrl = `${import.meta.env.VITE_APP_BASE_API}${options.url}`;
|
||||
// #endif
|
||||
|
||||
// 统一处理请求
|
||||
uni.request({
|
||||
url: requestUrl,
|
||||
method: options.method,
|
||||
data: options.data,
|
||||
header,
|
||||
timeout: options.timeout || 30000,
|
||||
responseType: options.responseType,
|
||||
success: (res: any) => {
|
||||
// 请求成功
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(res.data.data);
|
||||
}
|
||||
// 未授权错误
|
||||
else if (res.statusCode === 401) {
|
||||
// 如果需要认证且未授权,跳转到登录页
|
||||
if (!options.skipAuth) {
|
||||
uni.navigateTo({
|
||||
url: "/pages/login/index",
|
||||
});
|
||||
}
|
||||
reject(new Error(res.data.message || "未授权,请重新登录"));
|
||||
}
|
||||
// 其他错误
|
||||
else {
|
||||
const errorMsg = res.data.message || `请求失败: ${res.statusCode}`;
|
||||
reject(new Error(errorMsg));
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(new Error(err.errMsg || "网络请求失败"));
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 无需认证的请求
|
||||
* @param options 请求配置
|
||||
*/
|
||||
export function publicRequest<T = any>(options: RequestOptions): Promise<T> {
|
||||
return request<T>({
|
||||
...options,
|
||||
skipAuth: true,
|
||||
});
|
||||
}
|
||||
|
||||
export default request;
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* 存储工具类
|
||||
* 提供localStorage和sessionStorage操作方法
|
||||
*/
|
||||
|
||||
/**
|
||||
* localStorage 存储
|
||||
*/
|
||||
function set(key: string, value: any): void {
|
||||
uni.setStorageSync(key, JSON.stringify(value));
|
||||
}
|
||||
|
||||
function get<T>(key: string, defaultValue?: T): T {
|
||||
const value = uni.getStorageSync(key);
|
||||
if (!value) return defaultValue as T;
|
||||
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
// 如果解析失败,返回原始字符串
|
||||
return value as unknown as T;
|
||||
}
|
||||
}
|
||||
|
||||
function remove(key: string): void {
|
||||
uni.removeStorageSync(key);
|
||||
}
|
||||
|
||||
export const Storage = {
|
||||
set,
|
||||
get,
|
||||
remove,
|
||||
};
|
||||
|
||||
// 为了向后兼容,导出具体的函数
|
||||
import { ACCESS_TOKEN_KEY, USER_INFO_KEY } from "@/constants";
|
||||
|
||||
/**
|
||||
* 获取令牌
|
||||
*/
|
||||
export function getToken(): string | null {
|
||||
return Storage.get<string>(ACCESS_TOKEN_KEY) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置令牌
|
||||
*/
|
||||
export function setToken(token: string): void {
|
||||
Storage.set(ACCESS_TOKEN_KEY, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*/
|
||||
export function getUserInfo<T = any>(): T | undefined {
|
||||
return Storage.get<T>(USER_INFO_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置用户信息
|
||||
*/
|
||||
export function setUserInfo(userInfo: any): void {
|
||||
Storage.set(USER_INFO_KEY, userInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有数据
|
||||
*/
|
||||
export function clearAll(): void {
|
||||
Storage.remove(ACCESS_TOKEN_KEY);
|
||||
Storage.remove(USER_INFO_KEY);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 小程序主题工具类
|
||||
* 用于解决小程序环境中CSS变量不能动态设置的问题
|
||||
*/
|
||||
|
||||
// 注入小程序环境的全局样式
|
||||
export function applyThemeToMiniProgram(primaryColor: string) {
|
||||
// 确保在小程序环境中执行
|
||||
if (typeof document !== "undefined") return;
|
||||
|
||||
try {
|
||||
// 设置TabBar样式
|
||||
uni.setTabBarStyle({
|
||||
color: "#000000",
|
||||
selectedColor: primaryColor,
|
||||
backgroundColor: "#ffffff",
|
||||
borderStyle: "black",
|
||||
});
|
||||
|
||||
console.log("小程序主题色已应用:", primaryColor);
|
||||
} catch (error) {
|
||||
console.error("应用小程序主题色失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// 在页面展示时应用主题
|
||||
export function applyThemeOnPageShow(primaryColor: string) {
|
||||
// 各平台小程序可能需要不同处理
|
||||
const platform = uni.getSystemInfoSync().platform;
|
||||
console.log(`当前平台: ${platform}, 应用主题色: ${primaryColor}`);
|
||||
|
||||
// 某些平台可能需要特定处理
|
||||
}
|
||||