Lufaneng 1 year ago
commit 198738cff2

@ -0,0 +1,93 @@
module.exports = {
// 继承的规则
extends: ["@commitlint/config-conventional"],
// 自定义规则
rules: {
// @see https://commitlint.js.org/#/reference-rules
// 提交类型枚举,git提交type必须是以下类型
"type-enum": [
2,
"always",
[
"feat", // 新增功能
"fix", // 修复缺陷
"docs", // 文档变更
"style", // 代码格式(不影响功能,例如空格、分号等格式修正)
"refactor", // 代码重构(不包括 bug 修复、功能新增)
"perf", // 性能优化
"test", // 添加疏漏测试或已有测试改动
"build", // 构建流程、外部依赖变更(如升级 npm 包、修改 webpack 配置等)
"ci", // 修改 CI 配置、脚本
"revert", // 回滚 commit
"chore", // 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)
],
],
"subject-case": [0], // subject大小写不做校验
},
prompt: {
messages: {
type: "选择你要提交的类型 :",
scope: "选择一个提交范围(可选):",
customScope: "请输入自定义的提交范围 :",
subject: "填写简短精炼的变更描述 :\n",
body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n',
breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n',
footerPrefixesSelect: "选择关联issue前缀(可选):",
customFooterPrefix: "输入自定义issue前缀 :",
footer: "列举关联issue (可选) 例如: #31, #I3244 :\n",
generatingByAI: "正在通过 AI 生成你的提交简短描述...",
generatedSelectByAI: "选择一个 AI 生成的简短描述:",
confirmCommit: "是否提交或修改commit ?",
},
// prettier-ignore
types: [
{ value: "feat", name: "特性: ✨ 新增功能", emoji: ":sparkles:" },
{ value: "fix", name: "修复: 🐛 修复缺陷", emoji: ":bug:" },
{ value: "docs", name: "文档: 📝 文档变更(更新README文件,或者注释)", emoji: ":memo:" },
{ value: "style", name: "格式: 🌈 代码格式(空格、格式化、缺失的分号等)", emoji: ":lipstick:" },
{ value: "refactor", name: "重构: 🔄 代码重构(不修复错误也不添加特性的代码更改)", emoji: ":recycle:" },
{ value: "perf", name: "性能: 🚀 性能优化", emoji: ":zap:" },
{ value: "test", name: "测试: 🧪 添加疏漏测试或已有测试改动", emoji: ":white_check_mark:"},
{ value: "build", name: "构建: 📦️ 构建流程、外部依赖变更(如升级 npm 包、修改 vite 配置等)", emoji: ":package:"},
{ value: "ci", name: "集成: ⚙️ 修改 CI 配置、脚本", emoji: ":ferris_wheel:"},
{ value: "revert", name: "回退: ↩️ 回滚 commit",emoji: ":rewind:"},
{ value: "chore", name: "其他: 🛠️ 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)", emoji: ":hammer:"},
],
useEmoji: true,
emojiAlign: "center",
useAI: false,
aiNumber: 1,
themeColorCode: "",
scopes: [],
allowCustomScopes: true,
allowEmptyScopes: true,
customScopesAlign: "bottom",
customScopesAlias: "custom",
emptyScopesAlias: "empty",
upperCaseSubject: false,
markBreakingChangeMode: false,
allowBreakingChanges: ["feat", "fix"],
breaklineNumber: 100,
breaklineChar: "|",
skipQuestions: [],
issuePrefixes: [
{ value: "closed", name: "closed: ISSUES has been processed" },
],
customIssuePrefixAlign: "top",
emptyIssuePrefixAlias: "skip",
customIssuePrefixAlias: "custom",
allowCustomIssuePrefix: true,
allowEmptyIssuePrefix: true,
confirmColorize: true,
maxHeaderLength: Infinity,
maxSubjectLength: Infinity,
minSubjectLength: 0,
scopeOverrides: undefined,
defaultBody: "",
defaultIssues: "",
defaultScope: "",
defaultSubject: "",
},
};

@ -0,0 +1,15 @@
# http://editorconfig.org
root = true
# 表示所有文件适用
[*]
charset = utf-8 # 设置文件字符集为 utf-8
end_of_line = lf # 控制换行类型(lf | cr | crlf)
indent_style = space # 缩进风格(tab | space)
indent_size = 2 # 缩进大小
insert_final_newline = true # 始终在文件末尾插入一个新行
# 表示仅 md 文件适用以下规则
[*.md]
max_line_length = off # 关闭最大行长度限制
trim_trailing_whitespace = false # 关闭末尾空格修剪

@ -0,0 +1,8 @@
# H5应用端口
VITE_APP_PORT = 3000
# H5代理前缀
VITE_APP_PROXY_PREFIX = "/dev-api"
# 接口地址
VITE_APP_BASE_URL = "http://localhost:8080"

@ -0,0 +1,2 @@
# 接口地址
VITE_APP_BASE_URL = "http://prod-api"

9
.gitignore vendored

@ -0,0 +1,9 @@
.idea
.vscode
.hbuilderx
.project
.DS_Store
node_modules
unpackage
package-lock.json
pnpm-lock.yaml

@ -0,0 +1 @@
npx --no-install commitlint --edit $1

@ -0,0 +1 @@
npm run lint:lint-staged

@ -0,0 +1,3 @@
module.exports = {
"*.{js,cjs,ts,json,tsx,css,less,scss,vue,html,md}": ["prettier --write"],
};

@ -0,0 +1,10 @@
unpackage
node_modules
uni_modules
static
.husky
.hbuilderx
.vscode
.idea
*.sh
*.md

@ -0,0 +1,47 @@
// Prettier配置文档:https://prettier.io/docs/en/options.html
module.exports = {
// (x)=>{},单个参数箭头函数是否显示小括号。(always:始终显示;avoid:省略括号。默认:always)
arrowParens: "always",
// 开始标签的右尖括号是否跟随在最后一行属性末尾,默认false
bracketSameLine: false,
// 对象字面量的括号之间打印空格 (true - Example: { foo: bar } ; false - Example: {foo:bar})
bracketSpacing: true,
// 是否格式化一些文件中被嵌入的代码片段的风格(auto|off;默认auto)
embeddedLanguageFormatting: "auto",
// 指定 HTML 文件的空格敏感度 (css|strict|ignore;默认css)
htmlWhitespaceSensitivity: "ignore",
// 当文件已经被 Prettier 格式化之后,是否会在文件顶部插入一个特殊的 @format 标记,默认false
insertPragma: false,
// 在 JSX 中使用单引号替代双引号,默认false
jsxSingleQuote: false,
// 每行最多字符数量,超出换行(默认80)
printWidth: 80,
// 超出打印宽度 (always | never | preserve )
proseWrap: "preserve",
// 对象属性是否使用引号(as-needed | consistent | preserve;默认as-needed:对象的属性需要加引号才添加;)
quoteProps: "as-needed",
// 是否只格式化在文件顶部包含特定注释(@prettier| @format)的文件,默认false
requirePragma: false,
// 结尾添加分号
semi: true,
// 使用单引号 (true:单引号;false:双引号)
singleQuote: false,
// 缩进空格数,默认2个空格
tabWidth: 2,
// 元素末尾是否加逗号,默认es5: ES5中的 objects, arrays 等会添加逗号,TypeScript 中的 type 后不加逗号
trailingComma: "es5",
// 指定缩进方式,空格或tab,默认false,即使用空格
useTabs: false,
// vue 文件中是否缩进 <style> 和 <script> 标签,默认 false
vueIndentScriptAndStyle: false,
endOfLine: "auto",
overrides: [
{
files: "*.html",
options: {
parser: "html",
},
},
],
};

@ -0,0 +1,46 @@
<script setup>
import {
onLaunch,
onShow,
onHide,
onError,
onPageNotFound,
} from "@dcloudio/uni-app";
import { checkForUpdate } from "@/utils";
onLaunch(() => {
console.log("App Launch");
// #ifdef MP-WEIXIN
checkForUpdate();
// #endif
});
onShow(() => {
console.log("App Show");
});
onHide(() => {
console.log("App Hide");
});
onError((error) => {
console.error("App Error", error);
});
onPageNotFound((res) => {
console.error("PageNotFound", res);
uni.redirectTo({
url: "/pages/404/404",
});
});
</script>
<style lang="scss">
/* 每个页面公共css */
page {
height: 100%;
}
/* uv-ui基础样式 */
@import "@/uni_modules/uv-ui-tools/index.scss";
</style>

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 白開水
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1 @@
vue3+uniapp+uv-ui

@ -0,0 +1,4 @@
import AuthAPI from "./modules/auth";
import UserAPI from "./modules/user";
export { AuthAPI, UserAPI };

@ -0,0 +1,30 @@
import http from "@/utils/request";
class AuthAPI {
// 注册
static signUp(data) {
return http.post("/auth/signup", data, {
custom: {
loading: true,
},
});
}
// 登录
static signIn(data) {
return http.post("/auth/signin", data, {
custom: {
loading: true,
},
});
}
// 刷新token
static refreshToken(header = {}) {
return http.post("/auth/refresh", undefined, {
header,
});
}
}
export default AuthAPI;

@ -0,0 +1,16 @@
import http from "@/utils/request";
class UserAPI {
// test get api
static getUserList(params = {}) {
return http.get("/user/list", {
params,
custom: {
auth: true,
loading: true,
},
});
}
}
export default UserAPI;

File diff suppressed because one or more lines are too long

@ -0,0 +1,36 @@
<template>
<text
:class="`iconfont icon-${name}`"
:style="{
color: color,
'font-size': size + 'px',
}"
@click="handleClick"
/>
</template>
<script setup>
const emit = defineEmits(["click"]);
const props = defineProps({
name: {
type: String,
required: true,
},
color: {
type: String,
default: "#333333",
},
size: {
type: [Number, String],
default: 16,
},
});
function handleClick() {
emit("click");
}
</script>
<style>
@import "./iconfont.css";
</style>

@ -0,0 +1,19 @@
@font-face {
font-family: "iconfont"; /* Project id 4657887 */
src:
url("iconfont.woff2?t=1724050101808") format("woff2"),
url("iconfont.woff?t=1724050101808") format("woff"),
url("iconfont.ttf?t=1724050101808") format("truetype");
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-uniapp:before {
content: "\e624";
}

@ -0,0 +1,16 @@
{
"id": "4657887",
"name": "vue3-uniapp-template",
"font_family": "iconfont",
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "19317486",
"name": "uniapp",
"font_class": "uniapp",
"unicode": "e624",
"unicode_decimal": 58916
}
]
}

Binary file not shown.

Binary file not shown.

@ -0,0 +1,58 @@
<template>
<view>
<picker
range-key="label"
:range="langOptions"
:value="langIndex"
@change="handleLangChange"
>
<slot>
<image
:style="langStyle"
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAACWCAMAAAAL34HQAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAJMUExURUdwTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALTIN/sAAADDdFJOUwAU0qz4D0aAQPoBOWx5n+AHLfs4Asfa8BP8GfQs7HPC3RY9ohUIBmKmycFllp4i/gTG/WvmH1OR+el4M97czb7rBVdC7RxorWCT1jUS5RqrcQyb9XdQEAkX8Vxjw3DPkiTX9+HvPHuN0Bvi515qINW1CkeluFWXmIQeQQM2HU5U86FIf9u2mcRWxdQycva8T1qPnUw0h9nouSE7t3Z9fqiVzMtDtI58b4hu04xno00RPkSndDeUspzOKaBtX/J6qe5LPwdbu1UAAAUcSURBVHja7Zr3XxQ5GMaDwAKLKyzrimIDQSwoIoKCnDQVG9i7nr1jv6J3Z+/lzt57773cWc46/5gsouSdSTLJTDaOfvL8mOSdPJv5buZ9M4OQlpaWlpaWlkMlYEr0ji0DU4K2pW1pW9qWtqVt/ZC2EozoSNvStrQtbUvb0ra+N1uJCW60CndSjvd8y8RizRvc1kGv5Dv9cFeVrT3iyp+J27rllcVaCyAv9oqt1birnl5xld8Ht7XcK7YG4K7ap3rE1ZoJuK3fvbJYNwDwxzziqrY97qqFJ8nyDPArk3BXmVHf4adU4vPFbaQMewIW61D01+FnMOFO8qAOhtrFQii3CEzZnzioAoypVp4XGPVTCEO2gSFzldCcOx9Mutg6ousgjgWVrjwwabugZcDib7BYCBXA5bpi7h8FntHGJFWb0mZYLsxg3sIl6jbLA2DiBZNB53Vo+pnCVArs4ca/jFu4TuWzZT1ckYnUW3hynEpbqcvA5IXJX3t+gYaPqn0Ub4Kzl2Y0tdfA9jGMSyzCqtnpUakfDKNvQWNrCEKXPpV+AR945MdLspUxDfq6G2nMKoWNeYwLxIKRLWUt12+mw44HCP0Vhk2sIiwGrqu89PUV9LBlDpoNW0pWMqIDpl+VLMvWuJ/ghVvMMM00kBWdaRrcW9pyhQqZR1g9WcmfzzxaGvQIbWe5qqtF3MBLhR61DtNdJe1H/MBLrtlGl1Jt5TEDA4SIZHm+Qu0ortgE+1MIIa0kPoRqyK7Kq5hRPlKMROgR6k2aYchodlAs8be0lGirioB94SN2DAF46QcVWXWW6z+1CQlQgJQIPerS0XL5YaniwEuG/m/Sf3HJUGHgpUKfepE8QccuwsBLhH7oC+oM/ahBU8E4cKKxKlpYNSs7lxI1EOSvH0FQSIKrm2nMDOJyDgfwI1AbudBnnLJ7OVc53R74IHotE3p/zTSOt4YV+TbA90UoPk4e9HNmkv5+hLZF5sgc0P1/Q0srvOGOG1cT5xEcVLftTGgdGWLs8OmRffeMpJ0+WE6Yvr6soecw6U6uAMGgAJjd2DRSBvSnt5L29fDnv111ibXrMQv4iG67h/6PI8RdIbugqb+syNyV4mcBH5Fr6JfuIqYkSdh9ygmzyjKY0gxvas12ld5kXCohPwAB1QXZ0HIMG/iIFrqAPmvAPPLmtM+cLtzDlySWtcN/kdOdvmrSsDiyqbTj1uQqOKu532cHvOXNCDf0oV5DaPv4rCCxgOw0wR74cHO7A+iLL+ygPlz+OUcr6Iubtta39sBHdF8Y+l/pj7zO+Yy44e8jxQYH8I0pkjD0Y2mm0vNs/iPvGrJ6HuAdQf+S4upalm3oh6K1PMBbfjwX9MtIpnYP5gEAXn4MYYf/ooecGXezxhOOrv508NyiFK1OC9n+FtIHO3maUotWh4Xs5D4yTNGLVqeF7F5s/MyrTrM0n8g3ejzQn/96EPO8zHlGGyv07SDHTr/h81vWs3tyXeTZAsBzQu9fYBgnAkvdlUoBQ0wc0I//b6HrsjJT0Ja8c3ppwEs/spQEvOwjS0nAq/q2KmCIKzn6tkDRWkH92DglWuf0PMB3oI7rpBZ6AHwP+ri2aSqhh8B3Y4zsrhL6APcXqIkKoYcpTS/m2DbqoOcFXjH0vMCrhZ4feKXQ8wOvEnoR4BVCLwK8QuhFgFcHvRjwyqAXA14V9KLAK4JeFHhF0IsCrwZ6ceCVQB+TiKsr7wsAEIW0tLS0tLS0tLS0vKZPHNE+dYS3Jg4AAAAASUVORK5CYII="
></image>
</slot>
</picker>
</view>
</template>
<script setup>
import { computed } from "vue";
import { useI18n } from "vue-i18n";
const emit = defineEmits(["change"]);
const props = defineProps({
size: {
type: Number,
default: 40,
required: false,
},
});
const { locale, t } = useI18n();
const langStyle = {
width: props.size + "rpx",
height: props.size + "rpx",
};
const langOptions = computed(() => {
return [
{ label: t("locale.en"), value: "en" },
{ label: t("locale.zh-hans"), value: "zh-Hans" },
{ label: t("locale.zh-hant"), value: "zh-Hant" },
{ label: t("locale.ja"), value: "ja" },
];
});
const langIndex = computed(() => {
return langOptions.value.findIndex((item) => {
return item.value === locale.value;
});
});
function handleLangChange(event) {
const lang = langOptions.value[event.detail.value].value;
locale.value = lang;
uni.setLocale(lang);
emit("change", lang);
}
</script>
<style lang="scss" scoped></style>

@ -0,0 +1,307 @@
<template>
<view class="login-overlay">
<view class="login-container">
<!-- 关闭按钮 -->
<view class="close-btn" @click="closeLogin">
<uv-icon name="close" color="#FFFFFF" size="24"></uv-icon>
</view>
<!-- 登录类型切换 -->
<view class="login-type">
<view
class="type-item"
:class="{ active: loginType === 'personal' }"
@click="loginType = 'personal'"
>
个人
</view>
<view
class="type-item"
:class="{ active: loginType === 'enterprise' }"
@click="loginType = 'enterprise'"
>
企业
</view>
</view>
<!-- 登录表单 -->
<view class="login-form">
<!-- 用户名输入 -->
<view class="input-item">
<view class="icon">
<uv-icon name="account" color="#AAAAAA" size="20"></uv-icon>
</view>
<input
class="input"
type="text"
v-model="username"
placeholder="请输入8位数字或英文字母"
maxlength="20"
/>
</view>
<!-- 密码输入 -->
<view class="input-item">
<view class="icon">
<uv-icon name="eye" color="#AAAAAA" size="20"></uv-icon>
</view>
<!-- <input
class="input"
:type="showPassword ? 'text' : 'password'"
v-model="password"
placeholder="请输入六密码"
maxlength="20"
/> -->
<uv-input class="input" placeholder="请输入密码" :type="showPassword ? 'text' : 'password'" border="none" v-model="password"></uv-input>
<view class="eye-icon" @click="togglePasswordVisibility">
<uv-icon
:name="showPassword ? 'eye-fill' : 'eye-off'"
color="#AAAAAA"
size="20"
></uv-icon>
</view>
</view>
<!-- 登录按钮 -->
<view class="login-btn" @click="handleLogin">登录</view>
<!-- 用户协议 -->
<view class="agreement">
<checkbox
:checked="agreePolicy"
@click="agreePolicy = !agreePolicy"
color="#4080FF"
style="transform: scale(0.7)"
/>
<text class="agreement-text">
阅读
<text class="link" @click.stop="openUserPolicy">用户政策</text>
和
<text class="link" @click.stop="openPrivacyPolicy">隐私政策</text>
</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from "vue";
import { useAuthStore } from "@/store";
// 登录类型:个人/企业
const loginType = ref("personal");
// 用户名和密码
const username = ref("");
const password = ref("");
// 是否显示密码
const showPassword = ref(false);
// 是否同意用户协议
const agreePolicy = ref(false);
// 切换密码可见性
const togglePasswordVisibility = () => {
showPassword.value = !showPassword.value;
};
// 处理登录
const handleLogin = () => {
// 表单验证
if (!username.value) {
return uni.showToast({
title: "请输入用户名",
icon: "none",
});
}
if (!password.value) {
return uni.showToast({
title: "请输入密码",
icon: "none",
});
}
if (!agreePolicy.value) {
return uni.showToast({
title: "请阅读并同意用户政策和隐私政策",
icon: "none",
});
}
// 登录逻辑
const authStore = useAuthStore();
// 显示加载中
uni.showLoading({
title: "登录中...",
});
// 模拟登录请求
setTimeout(() => {
uni.hideLoading();
// 这里应该调用实际的登录API
// 示例:模拟登录成功
authStore.signIn({
token: "sample_token_123456",
userInfo: {
id: "1",
username: username.value,
type: loginType.value,
},
});
// 登录成功提示
uni.showToast({
title: "登录成功",
icon: "success",
});
// 关闭登录弹窗
emit("close");
}, 1500);
};
// 打开用户政策
const openUserPolicy = () => {
uni.navigateTo({
url: "/pages/policy/user-policy",
});
};
// 打开隐私政策
const openPrivacyPolicy = () => {
uni.navigateTo({
url: "/pages/policy/privacy-policy",
});
};
// 关闭登录弹窗
const emit = defineEmits(["close"]);
const closeLogin = () => {
emit("close");
};
</script>
<style lang="scss" scoped>
.login-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
}
.login-container {
width: 85vw;
background-color: #f0f4ff;
border-radius: 20rpx;
position: relative;
padding: 40rpx 0;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
}
.close-btn {
position: absolute;
top: 0;
right: 0;
width: 80rpx;
height: 80rpx;
background-color: #a0b4ff;
border-radius: 0 20rpx 0 20rpx;
display: flex;
justify-content: center;
align-items: center;
}
.login-type {
display: flex;
justify-content: center;
margin-bottom: 40rpx;
.type-item {
font-size: 32rpx;
padding: 10rpx 40rpx;
margin: 0 20rpx;
color: #999999;
position: relative;
&.active {
color: #333333;
font-weight: bold;
&::after {
content: "";
position: absolute;
bottom: -10rpx;
left: 40rpx;
right: 40rpx;
height: 6rpx;
background-color: #4080ff;
border-radius: 3rpx;
}
}
}
}
.login-form {
padding: 0 40rpx;
.input-item {
background-color: #ffffff;
border-radius: 50rpx;
height: 90rpx;
display: flex;
align-items: center;
padding: 0 30rpx;
margin-bottom: 30rpx;
.icon {
margin-right: 20rpx;
}
.input {
flex: 1;
height: 90rpx;
font-size: 28rpx;
}
.eye-icon {
padding: 0 10rpx;
}
}
.login-btn {
background-color: #4080ff;
color: #ffffff;
height: 90rpx;
border-radius: 50rpx;
display: flex;
justify-content: center;
align-items: center;
font-size: 32rpx;
font-weight: bold;
margin: 40rpx 0;
}
.agreement {
display: flex;
align-items: center;
justify-content: center;
.agreement-text {
font-size: 24rpx;
color: #666666;
.link {
color: #4080ff;
}
}
}
}
</style>

@ -0,0 +1,426 @@
<template>
<view class="upload-file">
<view class="upload-area" @click="chooseFile">
<view v-if="!uploading" class="upload-placeholder">
<text class="upload-icon">{{ getUploadIcon() }}</text>
<text class="upload-text">{{ getUploadText() }}</text>
</view>
<view v-if="uploading" class="uploading">
<text class="upload-icon">⏳</text>
<text class="upload-text">上传中...</text>
<progress :percent="uploadProgress" show-info />
</view>
<!-- <view v-if="fileInfo && !uploading" class="file-info">
<text class="file-name">{{ fileInfo.name }}</text>
<text class="file-size">{{ formatFileSize(fileInfo.size) }}</text>
</view> -->
</view>
<!-- <view v-if="error" class="error-message">
<text>{{ error }}</text>
</view> -->
</view>
</template>
<script setup>
import { ref } from "vue";
import {
useAuthStore
} from "@/store/modules/auth";
// 定义 props
const props = defineProps({
// 允许的文件类型
accept: {
type: String,
default: "*",
},
// 最大文件大小 (MB)
maxSize: {
type: Number,
default: 10,
},
// 是否禁用
disabled: {
type: Boolean,
default: false,
},
});
// 定义 emits
const emit = defineEmits(["onSuccess", "onError", "onProgress"]);
// 响应式数据
const uploading = ref(false);
const uploadProgress = ref(0);
const fileInfo = ref(null);
const error = ref(null);
// 选择文件
const chooseFile = () => {
if (props.disabled || uploading.value) return;
error.value = null;
// 根据 accept 属性判断文件类型并选择对应的方法
const fileType = getFileTypeFromAccept();
if (fileType === "image") {
chooseImageFile();
} else if (fileType === "video") {
chooseVideoFile();
} else {
chooseMessageFile();
}
};
// 根据 accept 属性判断文件类型
const getFileTypeFromAccept = () => {
if (props.accept === "*") return "file";
const accept = props.accept.toLowerCase();
// 检查是否为图片类型
const imageTypes = [
".jpg",
".jpeg",
".png",
".gif",
".bmp",
".webp",
"image/",
];
if (imageTypes.some((type) => accept.includes(type))) {
return "image";
}
// 检查是否为视频类型
const videoTypes = [
".mp4",
".avi",
".mov",
".wmv",
".flv",
".webm",
"video/",
];
if (videoTypes.some((type) => accept.includes(type))) {
return "video";
}
// 其他文件类型
return "file";
};
// 选择图片
const chooseImageFile = () => {
uni.chooseImage({
count: 1,
sourceType: ["album", "camera"],
success: (res) => {
console.log("选择图片成功:", res);
if (res.tempFilePaths && res.tempFilePaths.length > 0) {
// 获取文件信息
uni.getFileInfo({
filePath: res.tempFilePaths[0],
success: (fileInfo) => {
const file = {
name: "image_" + Date.now() + ".jpg",
size: fileInfo.size,
path: res.tempFilePaths[0],
};
validateAndUpload(file);
},
fail: () => {
// 如果获取文件信息失败,使用默认值
const file = {
name: "image_" + Date.now() + ".jpg",
size: 0,
path: res.tempFilePaths[0],
};
validateAndUpload(file);
},
});
} else {
error.value = "未选择到图片";
}
},
fail: (err) => {
console.error("选择图片失败:", err);
error.value = "选择图片失败: " + (err.errMsg || "请重试");
emit("onError", err);
},
});
};
// 选择视频
const chooseVideoFile = () => {
uni.chooseVideo({
sourceType: ["album", "camera"],
maxDuration: 60,
camera: "back",
success: (res) => {
console.log("选择视频成功:", res);
const file = {
name: "video_" + Date.now() + ".mp4",
size: res.size || 0,
path: res.tempFilePath,
};
validateAndUpload(file);
},
fail: (err) => {
console.error("选择视频失败:", err);
error.value = "选择视频失败: " + (err.errMsg || "请重试");
emit("onError", err);
},
});
};
// 选择文件
const chooseMessageFile = () => {
uni.chooseMessageFile({
count: 1,
type: "all",
success: (res) => {
console.log("选择文件成功:", res);
if (res.tempFiles && res.tempFiles.length > 0) {
const file = res.tempFiles[0];
validateAndUpload(file);
} else {
error.value = "未选择到文件";
}
},
fail: (err) => {
console.error("选择文件失败:", err);
error.value = "选择文件失败: " + (err.errMsg || "请重试");
emit("onError", err);
},
});
};
// 验证文件类型
const validateFileType = (fileName) => {
if (props.accept === "*") return true;
const acceptTypes = props.accept.split(",").map((type) => type.trim());
const fileExt = "." + fileName.split(".").pop().toLowerCase();
return acceptTypes.some((type) => {
if (type.startsWith(".")) {
return type.toLowerCase() === fileExt;
}
return fileName.toLowerCase().includes(type.toLowerCase());
});
};
// 验证并上传文件
const validateAndUpload = (file) => {
error.value = null;
// 验证文件大小
if (file.size > props.maxSize * 1024 * 1024) {
error.value = `文件大小不能超过 ${props.maxSize}MB`;
return;
}
// 验证文件类型
if (props.accept !== "*" && !validateFileType(file.name)) {
error.value = "文件类型不支持";
return;
}
fileInfo.value = {
name: file.name,
size: file.size,
path: file.path,
};
uploadFile(file);
};
// 处理上传错误
const handleUploadError = (message) => {
uploading.value = false;
uploadProgress.value = 0;
error.value = message;
emit("onError", message);
};
// 上传文件
const uploadFile = (file) => {
uploading.value = true;
uploadProgress.value = 0;
console.log("开始上传文件:", file);
const uploadTask = uni.uploadFile({
url: "https://lift-dev.ruixininfo.com/filestransfer/upload",
filePath: file.path,
name: "file",
header:{
session:useAuthStore().accessToken
},
// formData: {
// fileName: file.name,
// },
success: (res) => {
console.log("上传成功:", res);
try {
const data = JSON.parse(res.data);
uploading.value = false;
uploadProgress.value = 100;
// 将接口返回信息传递给父组件的 onSuccess 方法
emit("onSuccess", {
response: data,
fileInfo: fileInfo.value,
});
} catch (err) {
console.error("解析响应失败:", err);
handleUploadError("响应数据解析失败");
}
},
fail: (err) => {
console.error("上传失败:", err);
handleUploadError(err.errMsg || "上传失败");
},
});
// 监听上传进度
uploadTask.onProgressUpdate((res) => {
console.log("上传进度:", res.progress);
uploadProgress.value = res.progress;
emit("onProgress", res.progress);
});
};
// 格式化文件大小
const formatFileSize = (bytes) => {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
};
// 获取上传图标
const getUploadIcon = () => {
const fileType = getFileTypeFromAccept();
switch (fileType) {
case "image":
return "🖼️";
case "video":
return "🎬";
default:
return "📁";
}
};
// 获取上传文字
const getUploadText = () => {
const fileType = getFileTypeFromAccept();
switch (fileType) {
case "image":
return "点击选择图片";
case "video":
return "点击选择视频";
default:
return "点击选择文件";
}
};
// 重置组件状态
const reset = () => {
uploading.value = false;
uploadProgress.value = 0;
fileInfo.value = null;
error.value = null;
};
// 暴露方法给父组件使用
defineExpose({
reset,
});
</script>
<style scoped>
.upload-file {
width: 100%;
}
.upload-area {
border: 2px dashed #dcdfe6;
border-radius: 8px;
padding: 0 20px;
text-align: center;
cursor: pointer;
transition: border-color 0.3s;
background-color: #fafafa;
}
.upload-area:hover {
border-color: #409eff;
}
.upload-placeholder {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.upload-icon {
font-size: 24px;
}
.upload-text {
color: #606266;
font-size: 14px;
}
.uploading {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.file-info {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
}
.file-name {
font-size: 16px;
font-weight: bold;
color: #303133;
}
.file-size {
font-size: 12px;
color: #909399;
}
.error-message {
margin-top: 10px;
padding: 8px 12px;
background-color: #fef0f0;
border: 1px solid #fbc4c4;
border-radius: 4px;
color: #f56c6c;
font-size: 12px;
text-align: center;
}
progress {
width: 100%;
height: 100%;
margin-top: 10px;
}
</style>

@ -0,0 +1,191 @@
<template>
<view class="upload-example">
<text class="title">文件上传示例</text>
<!-- 图片上传 -->
<view class="upload-section">
<text class="section-title">图片上传</text>
<UploadFile
ref="imageUploadRef"
:accept="'.jpg,.jpeg,.png,.gif'"
:maxSize="5"
@onSuccess="handleImageUploadSuccess"
@onError="handleUploadError"
@onProgress="handleUploadProgress"
/>
</view>
<!-- 视频上传 -->
<view class="upload-section">
<text class="section-title">视频上传</text>
<UploadFile
ref="videoUploadRef"
:accept="'.mp4,.avi,.mov'"
:maxSize="50"
@onSuccess="handleVideoUploadSuccess"
@onError="handleUploadError"
@onProgress="handleUploadProgress"
/>
</view>
<!-- 文档上传 -->
<view class="upload-section">
<text class="section-title">文档上传</text>
<UploadFile
ref="fileUploadRef"
:accept="'.pdf,.doc,.docx,.txt'"
:maxSize="10"
@onSuccess="handleFileUploadSuccess"
@onError="handleUploadError"
@onProgress="handleUploadProgress"
/>
</view>
<!-- 显示上传结果 -->
<view v-if="uploadResult" class="result">
<text class="result-title">{{ uploadResult.type }}上传成功!</text>
<text class="result-info">文件名: {{ uploadResult.fileInfo.name }}</text>
<text class="result-info">文件大小: {{ uploadResult.fileInfo.size }} 字节</text>
<text class="result-info">
服务器响应: {{ JSON.stringify(uploadResult.response) }}
</text>
</view>
<!-- 重置按钮 -->
<button @click="resetAllUploads" class="reset-btn">重置所有</button>
</view>
</template>
<script setup>
import { ref } from "vue";
import UploadFile from "./UploadFile.vue";
// 响应式数据
const uploadResult = ref(null);
const imageUploadRef = ref(null);
const videoUploadRef = ref(null);
const fileUploadRef = ref(null);
// 处理图片上传成功
const handleImageUploadSuccess = (result) => {
console.log("图片上传成功:", result);
uploadResult.value = { ...result, type: '图片' };
uni.showToast({
title: "图片上传成功",
icon: "success",
});
};
// 处理视频上传成功
const handleVideoUploadSuccess = (result) => {
console.log("视频上传成功:", result);
uploadResult.value = { ...result, type: '视频' };
uni.showToast({
title: "视频上传成功",
icon: "success",
});
};
// 处理文档上传成功
const handleFileUploadSuccess = (result) => {
console.log("文档上传成功:", result);
uploadResult.value = { ...result, type: '文档' };
uni.showToast({
title: "文档上传成功",
icon: "success",
});
};
// 处理上传错误
const handleUploadError = (error) => {
console.error("上传失败:", error);
uni.showToast({
title: "上传失败",
icon: "error",
});
};
// 处理上传进度
const handleUploadProgress = (progress) => {
console.log("上传进度:", progress + "%");
};
// 重置所有上传组件
const resetAllUploads = () => {
uploadResult.value = null;
imageUploadRef.value?.reset();
videoUploadRef.value?.reset();
fileUploadRef.value?.reset();
};
</script>
<style scoped>
.upload-example {
padding: 20px;
}
.title {
font-size: 18px;
font-weight: bold;
margin-bottom: 20px;
display: block;
}
.result {
margin-top: 20px;
padding: 15px;
background-color: #f0f9ff;
border: 1px solid #b3d8ff;
border-radius: 8px;
}
.result-title {
font-size: 16px;
font-weight: bold;
color: #409eff;
display: block;
margin-bottom: 10px;
}
.result-info {
font-size: 14px;
color: #606266;
display: block;
margin-bottom: 5px;
}
.reset-btn {
margin-top: 15px;
padding: 10px 20px;
background-color: #409eff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.reset-btn:hover {
background-color: #337ecc;
}
.upload-section {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #e4e7ed;
border-radius: 8px;
background-color: #fafafa;
}
.section-title {
font-size: 16px;
font-weight: bold;
color: #303133;
display: block;
margin-bottom: 15px;
}
</style>
</template>

@ -0,0 +1,10 @@
import { hasPerm } from "@/plugins/permission";
import { currentRoute } from "@/router";
// 对某些特殊场景需要在页面onShow生命周期中校验权限:
// 1.微信小程序端点击tabbar的底层逻辑不触发uni.switchTab
// 2.h5在浏览器地址栏输入url后跳转不触发uni的路由api
// 3.首次启动加载的页面不触发uni的路由api
export async function usePermission() {
return hasPerm(currentRoute());
}

@ -0,0 +1,24 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script>
var coverSupport =
"CSS" in window &&
typeof CSS.supports === "function" &&
(CSS.supports("top: env(a)") || CSS.supports("top: constant(a)"));
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ", viewport-fit=cover" : "") +
'" />'
);
</script>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/main.js"></script>
</body>
</html>

@ -0,0 +1,8 @@
{
"locale.auto": "System",
"locale.en": "English",
"locale.zh-hans": "Simplified Chinese",
"locale.zh-hant": "Traditional Chinese",
"locale.ja": "Japanese",
"index.demo": "uni-app internationalization demo"
}

@ -0,0 +1,23 @@
import { createI18n } from "vue-i18n"; // v9.x
import en from "./en.json";
import ja from "./ja.json";
import zhHans from "./zh-Hans.json";
import zhHant from "./zh-Hant.json";
const i18n = createI18n({
legacy: false, // 必须设置false才能使用Composition API
globalInjection: true, // 为每个组件注入$为前缀的全局属性和函数
locale: uni.getLocale(),
messages: {
en,
"zh-Hans": zhHans,
"zh-Hant": zhHant,
ja,
},
});
export function setupI18n(app) {
app.use(i18n);
}
export { i18n };

@ -0,0 +1,8 @@
{
"locale.auto": "システム",
"locale.en": "英語",
"locale.zh-hans": "簡体字中国語",
"locale.zh-hant": "繁体字中国語",
"locale.ja": "日本語",
"index.demo": "uni-app 国際化デモ"
}

@ -0,0 +1,36 @@
{
"common": {
"uni.app.quit": "もう一度押すと、アプリケーションが終了します",
"uni.async.error": "サーバーへの接続がタイムアウトしました。画面をクリックして再試行してください",
"uni.showActionSheet.cancel": "キャンセル",
"uni.showToast.unpaired": "使用するには、showToastとhideToastをペアにする必要があることに注意してください",
"uni.showLoading.unpaired": "使用するには、showLoadingとhideLoadingをペアにする必要があることに注意してください",
"uni.showModal.cancel": "キャンセル",
"uni.showModal.confirm": "OK",
"uni.chooseImage.cancel": "キャンセル",
"uni.chooseImage.sourceType.album": "アルバムから選択",
"uni.chooseImage.sourceType.camera": "カメラ",
"uni.chooseVideo.cancel": "キャンセル",
"uni.chooseVideo.sourceType.album": "アルバムから選択",
"uni.chooseVideo.sourceType.camera": "カメラ",
"uni.previewImage.cancel": "キャンセル",
"uni.previewImage.button.save": "画像を保存",
"uni.previewImage.save.success": "画像をアルバムに正常に保存します",
"uni.previewImage.save.fail": "画像をアルバムに保存できませんでした",
"uni.setClipboardData.success": "コンテンツがコピーされました",
"uni.scanCode.title": "スキャンコード",
"uni.scanCode.album": "アルバム",
"uni.scanCode.fail": "認識に失敗しました",
"uni.scanCode.flash.on": "タッチして点灯",
"uni.scanCode.flash.off": "タップして閉じる",
"uni.startSoterAuthentication.authContent": "指紋認識...",
"uni.picker.done": "完了",
"uni.picker.cancel": "キャンセル",
"uni.video.danmu": "「弾幕」",
"uni.video.volume": "ボリューム",
"uni.button.feedback.title": "質問のフィードバック",
"uni.button.feedback.send": "送信"
},
"ios": {},
"android": {}
}

@ -0,0 +1,8 @@
{
"locale.auto": "系统",
"locale.en": "英语",
"locale.zh-hans": "简体中文",
"locale.zh-hant": "繁体中文",
"locale.ja": "日语",
"index.demo": "uni-app 国际化演示"
}

@ -0,0 +1,8 @@
{
"locale.auto": "系統",
"locale.en": "英語",
"locale.zh-hans": "簡體中文",
"locale.zh-hant": "繁體中文",
"locale.ja": "日語",
"index.demo": "uni-app 國際化演示"
}

@ -0,0 +1,11 @@
import App from "./App";
import { createSSRApp } from "vue";
import setupPlugins from "@/plugins";
export function createApp() {
const app = createSSRApp(App);
app.use(setupPlugins);
return {
app,
};
}

@ -0,0 +1,100 @@
{
"name" : "卡叔电梯",
"appid" : "__UNI__3A16C1C",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : "100",
"transformPx" : false,
/* 5+App特有相关 */
"app-plus" : {
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
},
/* 模块配置 */
"modules" : {},
/* 应用发布信息 */
"distribute" : {
/* android打包配置 */
"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\"/>"
],
"minSdkVersion" : 21
},
/* ios打包配置 */
"ios" : {},
/* SDK配置 */
"sdkConfigs" : {}
}
},
/* 快应用特有相关 */
"quickapp" : {},
/* 小程序特有相关 */
"mp-weixin" : {
"appid" : "wx2c04ebae2099b3d2",
"setting" : {
"urlCheck" : false,
"es6" : true,
"postcss" : true,
"minified" : true
},
"usingComponents" : true,
"permission" : {
"scope.userLocation" : {
"desc" : "获取当前位置"
},
"scope.userFuzzyLocation" : {
"desc" : "您的位置信息将用于小程序位置接口的效果展示"
}
},
/* 地理位置相关接口配置项 @see https://developers.weixin.qq.com/community/develop/doc/000a02f2c5026891650e7f40351c01 */
"requiredPrivateInfos" : [ "getLocation", "chooseLocation" ]
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"uniStatistics" : {
"enable" : false
},
"vueVersion" : "3",
"h5" : {
"optimization" : {
"treeShaking" : {
"enable" : false
}
},
"devServer" : {
"https" : false
},
"router" : {
"mode" : "history"
}
},
"locale" : "auto"
}

@ -0,0 +1,38 @@
{
"name": "lift-wechat",
"version": "1.0.0",
"description": "电梯图纸生成",
"main": "main.js",
"scripts": {
"prepare": "husky",
"lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,tsx,css,less,scss,vue,html,md}\"",
"lint:lint-staged": "lint-staged",
"commit": "git-cz"
},
"keywords": [
"卡叔电梯"
],
"author": "Lufaneng",
"license": "MIT",
"dependencies": {
"crypto-js": "^4.2.0",
"dayjs": "^1.11.18",
"three": "^0.178.0",
"vue-draggable-plus": "^0.6.0"
},
"devDependencies": {
"@commitlint/cli": "^19.1.0",
"@commitlint/config-conventional": "^19.1.0",
"@dcloudio/types": "^3.4.11",
"commitizen": "^4.3.0",
"cz-git": "^1.9.0",
"husky": "^9.0.11",
"lint-staged": "^15.2.2",
"prettier": "^3.2.5"
},
"config": {
"commitizen": {
"path": "node_modules/cz-git"
}
}
}

@ -0,0 +1,120 @@
{
//组件自动引入规则 https://uniapp.dcloud.net.cn/collocation/pages.html#easycom
"easycom": {
"autoscan": true,
"custom": {
"^x-(.*)": "@/components/x-$1/index.vue"
}
},
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "电梯图纸生成",
"navigationBarBackgroundColor": "#fff",
"backgroundColor": "#F8F8F8",
"enablePullDownRefresh": false
},
"pages": [
//pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/login/login",
"style": {
"navigationBarTitleText": "登录"
}
},
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "电梯图纸生成平台"
}
},
{
"path" : "pages/order/order",
"style" :
{
"navigationBarTitleText" : "订单"
}
},
{
"path" : "pages/consumption/consumption",
"style" :
{
"navigationBarTitleText" : "消费"
}
},
{
"path" : "pages/mine/mine",
"style" :
{
"navigationBarTitleText" : "我的"
}
}
],
"subPackages": [
{
"root": "pagesA",
"pages": [
{
"path" : "user-agreement/user-agreement",
"style" :
{
"navigationBarTitleText" : "用户协议",
"navigationStyle": "custom"
}
},
{
"path" : "generate-record/generate-record",
"style" :
{
"navigationBarTitleText" : "图纸生成记录"
}
},
{
"path" : "edit-profile/edit-profile",
"style" :
{
"navigationBarTitleText" : "编辑资料"
}
}
]
}
],
"preloadRule": {
"pages/index/index": {
"network": "wifi",
"packages": ["pagesA"]
}
},
"tabBar": {
"color": "#999999",
"selectedColor": "#3F70FF",
"backgroundColor": "#fff",
"position": "bottom",
"borderStyle": "black",
"list": [
{
"pagePath": "pages/index/index",
"iconPath": "static/tabbar/index.png",
"selectedIconPath": "static/tabbar/index_HL.png",
"text": "首页"
},
{
"pagePath": "pages/order/order",
"iconPath": "static/tabbar/appointment.png",
"selectedIconPath": "static/tabbar/appointment_HL.png",
"text": "订单"
},
{
"pagePath": "pages/consumption/consumption",
"iconPath": "static/tabbar/appointment.png",
"selectedIconPath": "static/tabbar/appointment_HL.png",
"text": "消费"
},
{
"pagePath": "pages/mine/mine",
"iconPath": "static/tabbar/mine.png",
"selectedIconPath": "static/tabbar/mine_HL.png",
"text": "我的"
}
]
}
}

@ -0,0 +1,380 @@
<template>
<view class="container">
<!-- 顶部筛选标签 -->
<view class="filter-tabs">
<view
class="tab-item"
:class="{ active: activeTab === 'all' }"
@click="switchTab('all')"
>
全部
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'generate' }"
@click="switchTab('generate')"
>
生成
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'member' }"
@click="switchTab('member')"
>
会员
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'other' }"
@click="switchTab('other')"
>
其他
</view>
</view>
<!-- 消费记录列表 -->
<view class="consumption-list">
<view
class="consumption-item"
v-for="item in filteredConsumptions"
:key="item.id"
>
<!-- 消费项目和金额 -->
<view class="item-header">
<view class="item-title">{{ item.title }}</view>
<view
class="item-amount"
:class="{
positive: item.amount > 0,
negative: item.amount < 0,
}"
>
{{ item.amount > 0 ? "+" : "" }}¥{{
Math.abs(item.amount).toFixed(2)
}}
</view>
</view>
<!-- 消费详情 -->
<view class="item-details">
<view class="detail-row">
<text class="detail-label">消费编号:</text>
<text class="detail-value">{{ item.consumptionNumber }}</text>
</view>
<view class="detail-row">
<text class="detail-label">消费时间:</text>
<text class="detail-value">{{ item.consumptionTime }}</text>
</view>
<view class="balance-row">
<view class="balance-item">
<text class="balance-label">账前金额:</text>
<text class="balance-value">
¥{{ item.beforeBalance.toFixed(2) }}
</text>
</view>
<view class="balance-item">
<text class="balance-label">账后金额:</text>
<text class="balance-value">
¥{{ item.afterBalance.toFixed(2) }}
</text>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="filteredConsumptions.length === 0" class="empty-state">
<u-icon name="file-text" size="60" color="#ccc"></u-icon>
<text class="empty-text">暂无消费记录</text>
</view>
<!-- 加载更多 -->
<view v-if="hasMore && filteredConsumptions.length > 0" class="load-more">
<u-button
type="default"
size="small"
:loading="loading"
@click="loadMore"
>
{{ loading ? "加载中..." : "加载更多" }}
</u-button>
</view>
</view>
</template>
<script setup>
import { computed, onMounted, ref } from "vue";
import { onShow } from "@dcloudio/uni-app";
import { useAuthStore, useUserStore } from "@/store";
import { regions } from "/static/json/regions.js";
import HeaderCustom from "/components/HeaderCustom/HeaderCustom.vue";
import RPC from "@/utils/request";
const authStore = useAuthStore();
const userStore = useUserStore();
// 响应式数据
const activeTab = ref("all");
const loading = ref(false);
const hasMore = ref(true);
const consumptions = ref([
{
id: 1,
title: "乘客电梯图纸生成",
amount: -30.0,
consumptionNumber: "CONS20230516024",
consumptionTime: "2025-05-16 14:30:22",
beforeBalance: 1250.0,
afterBalance: 1220.0,
type: "generate",
},
{
id: 2,
title: "货运电梯图纸生成",
amount: -45.0,
consumptionNumber: "CONS20230516024",
consumptionTime: "2025-05-16 14:30:22",
beforeBalance: 1295.0,
afterBalance: 1250.0,
type: "generate",
},
{
id: 3,
title: "包年会员订阅",
amount: -45.0,
consumptionNumber: "CONS20230516024",
consumptionTime: "2025-05-16 14:30:22",
beforeBalance: 1295.0,
afterBalance: 1250.0,
type: "member",
},
{
id: 4,
title: "账户充值",
amount: 1000.0,
consumptionNumber: "CONS20230516024",
consumptionTime: "2025-05-16 14:30:22",
beforeBalance: 1795.0,
afterBalance: 2795.0,
type: "other",
},
]);
// 计算过滤后的消费记录
const filteredConsumptions = computed(() => {
if (activeTab.value === "all") {
return consumptions.value;
}
return consumptions.value.filter((item) => item.type === activeTab.value);
});
// 切换标签
const switchTab = (tab) => {
activeTab.value = tab;
};
// 加载更多数据
const loadMore = async () => {
if (loading.value) return;
loading.value = true;
try {
// 模拟API调用
// const result = await RPC.get('/api/consumptions', {
// page: currentPage.value + 1,
// type: activeTab.value
// })
// 模拟加载延迟
await new Promise((resolve) => setTimeout(resolve, 1000));
// 模拟没有更多数据
hasMore.value = false;
uni.showToast({
title: "没有更多数据了",
icon: "none",
});
} catch (error) {
console.error("加载更多失败:", error);
uni.showToast({
title: "加载失败,请重试",
icon: "none",
});
} finally {
loading.value = false;
}
};
// 加载消费记录
const loadConsumptions = async () => {
try {
// 调用获取消费记录API
// const result = await RPC.get('/api/consumptions', {
// type: activeTab.value
// })
// consumptions.value = result.data
} catch (error) {
console.error("加载消费记录失败:", error);
uni.showToast({
title: "加载失败,请重试",
icon: "none",
});
}
};
// 下拉刷新
const onRefresh = async () => {
hasMore.value = true;
await loadConsumptions();
};
onMounted(() => {
loadConsumptions();
});
onShow(() => {
// 页面显示时刷新数据
loadConsumptions();
});
</script>
<style lang="scss" scoped>
.container {
font-size: 14px;
min-height: 100vh;
background-color: #f8f8f8;
}
.filter-tabs {
display: flex;
background: white;
padding: 0 16px;
border-bottom: 1px solid #e5e5e5;
.tab-item {
flex: 1;
text-align: center;
padding: 16px 0;
color: #666;
font-size: 14px;
position: relative;
&.active {
color: #4285f4;
font-weight: 500;
&::after {
content: "";
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 20px;
height: 2px;
background: #4285f4;
border-radius: 1px;
}
}
}
}
.consumption-list {
padding: 16px;
.consumption-item {
background: white;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
.item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
.item-title {
font-size: 16px;
color: #333;
font-weight: 500;
}
.item-amount {
font-size: 16px;
font-weight: bold;
&.positive {
color: #52c41a;
}
&.negative {
color: #ff4d4f;
}
}
}
.item-details {
.detail-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
.detail-label {
color: #999;
font-size: 13px;
}
.detail-value {
color: #666;
font-size: 13px;
}
}
.balance-row {
display: flex;
justify-content: space-between;
margin-top: 8px;
.balance-item {
display: flex;
align-items: center;
gap: 4px;
.balance-label {
color: #999;
font-size: 13px;
}
.balance-value {
color: #333;
font-size: 13px;
font-weight: 500;
}
}
}
}
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
.empty-text {
color: #999;
font-size: 14px;
margin-top: 16px;
}
}
.load-more {
padding: 20px;
text-align: center;
}
</style>

@ -0,0 +1,342 @@
<template>
<view class="container">
<!-- 顶部图片和标题 -->
<view class="header-section">
<image
class="header-image"
src="/static/imgs/indexImg.png"
mode="aspectFill"
></image>
<view class="header-overlay">
<view class="header-title">生成电梯图纸</view>
<view class="header-subtitle">
选择模板并设置参数,快速生成专业电梯施工图
</view>
</view>
</view>
<!-- 选择图纸模板 -->
<view class="section">
<view class="section-title">选择图纸模板</view>
<view class="template-grid">
<view
class="template-item"
:class="{ active: selectedTemplate === 'passenger' }"
@click="selectTemplate('passenger')"
>
<u-icon name="edit-pen" color="#4285f4" size="24"></u-icon>
<view class="template-name">乘客电梯标准图纸</view>
<view class="template-desc">适用于住宅、商业建筑</view>
</view>
<view
class="template-item"
:class="{ active: selectedTemplate === 'freight' }"
@click="selectTemplate('freight')"
>
<u-icon name="car" color="#4285f4" size="24"></u-icon>
<view class="template-name">货运电梯专用图纸</view>
<view class="template-desc">适用于仓库、工厂</view>
</view>
<view
class="template-item"
:class="{ active: selectedTemplate === 'medical' }"
@click="selectTemplate('medical')"
>
<u-icon name="plus-circle" color="#4285f4" size="24"></u-icon>
<view class="template-name">医用电梯专业图纸</view>
<view class="template-desc">适用于医院、医疗场所</view>
</view>
<view
class="template-item"
:class="{ active: selectedTemplate === 'sightseeing' }"
@click="selectTemplate('sightseeing')"
>
<u-icon name="eye" color="#4285f4" size="24"></u-icon>
<view class="template-name">观光电梯定制图纸</view>
<view class="template-desc">适用于商场、酒店</view>
</view>
</view>
</view>
<!-- 设置参数 -->
<view class="section">
<view class="section-title">设置参数</view>
<view class="form">
<uv-form labelAlign="left" labelWidth="auto" labelPosition="left" :model="formField" :rules="rules"
ref="form">
<uv-form-item prop="phone" label="电梯类型">
<uv-input v-model="formField.phone" color="#000" border="none"
:placeholderStyle="{color:'#666778'}" placeholder="请输入">
</uv-input>
</uv-form-item>
<uv-form-item prop="password" label="载重(kg)">
<uv-input v-model="formField.password" color="#000" border="none"
:placeholderStyle="{color:'#666778'}" placeholder="请输入">
</uv-input>
</uv-form-item>
<uv-form-item prop="password" label="井道宽度(mm)">
<uv-input type="digit" v-model="formField.password" color="#000" border="none"
:placeholderStyle="{color:'#666778'}" placeholder="请输入">
</uv-input>
</uv-form-item>
<uv-form-item prop="password" label="井道深度(mm)">
<uv-input type="digit" v-model="formField.password" color="#000" border="none"
:placeholderStyle="{color:'#666778'}" placeholder="请输入">
</uv-input>
</uv-form-item>
<uv-form-item prop="password" label="顶层高度(mm)">
<uv-input type="digit" v-model="formField.password" color="#000" border="none"
:placeholderStyle="{color:'#666778'}" placeholder="请输入">
</uv-input>
</uv-form-item>
<uv-form-item prop="password" label="底坑深度(mm)">
<uv-input type="digit" v-model="formField.password" color="#000" border="none"
:placeholderStyle="{color:'#666778'}" placeholder="请输入">
</uv-input>
</uv-form-item>
<uv-form-item prop="password">
<div style="margin-bottom: 4px;">备注说明</div>
<uv-textarea v-model="formField.password" placeholder="请输入"></uv-textarea>
</uv-form-item>
</uv-form>
</view>
</view>
<!-- 底部按钮 -->
<view class="bottom-actions">
<uv-button :loading="generating" @click="viewHistory" text="生成图纸" color="#3F70FF"></uv-button>
<uv-button @click="viewHistory" text="查看记录"></uv-button>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from "vue";
import {
onShow,
onLoad,
onShareAppMessage,
onShareTimeline,
onReachBottom,
} from "@dcloudio/uni-app";
import { useAuthStore, useUserStore } from "@/store";
import RPC from "@/utils/request";
const authStore = useAuthStore();
const userStore = useUserStore();
const generating = ref(false)
// 响应式数据
const selectedTemplate = ref("passenger");
// 选择模板
const selectTemplate = (template) => {
selectedTemplate.value = template;
};
const form = ref(null)
const formField = ref({
phone: '',
password: '',
})
const rules = ref({
// 'phone': {
// required: true,
// message: '请填写手机号',
// trigger: ['blur', 'change']
// },
// 'password': {
// required: true,
// message: '请填写验证码',
// trigger: ['blur', 'change']
// }
})
// 生成图纸
const generateDrawing = async () => {
if (!selectedTemplate.value) {
uni.showToast({
title: "请选择图纸模板",
icon: "none",
});
return;
}
generating.value = true;
try {
const params = {
};
// 这里调用生成图纸的API
// const result = await RPC.post('/api/generate-drawing', params)
uni.showToast({
title: "图纸生成中...",
icon: "loading",
duration: 2000,
});
// 模拟生成过程
setTimeout(() => {
uni.showToast({
title: "图纸生成成功",
icon: "success",
});
generating.value = false;
}, 2000);
} catch (error) {
console.error("生成图纸失败:", error);
uni.showToast({
title: "生成失败,请重试",
icon: "none",
});
generating.value = false;
}
};
// 查看历史记录
const viewHistory = () => {
console.log(111)
uni.navigateTo({
url: "/pagesA/generate-record/generate-record",
});
};
// 分享给朋友
onShareAppMessage((data) => {
console.log("🚀 ~ onShareAppMessage ~ data:", data);
return {
title: "电梯图纸生成平台",
path: "/pages/index/index",
};
});
// 分享到朋友圈
onShareTimeline((data) => {
console.log("🚀 ~ onShareTimeline ~ data:", data);
return {
title: "电梯图纸生成平台",
query: "a=1&b=2",
};
});
onLoad(() => {
console.log("index-onLoad");
});
</script>
<style lang="scss" scoped>
.container {
font-size: 14px;
min-height: 100vh;
background-color: #f8f8f8;
padding-bottom: 52px;
}
.header-section {
position: relative;
height: 200px;
overflow: hidden;
.header-image {
width: 100%;
height: 100%;
}
.header-overlay {
position: absolute;
top: 50%;
right: 0;
background: rgba(0, 0, 0, 0.5);
color: white;
width:70%;
transform: translate(0, -50%);
padding:20px 10px;
.header-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 8px;
color: #BD9E85 ;
}
.header-subtitle {
font-size: 12px;
}
}
}
.section {
margin: 16px;
background: white;
border-radius: 12px;
padding: 16px;
.section-title {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 16px;
}
}
.template-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
.template-item {
border: 1px solid #e5e5e5;
border-radius: 8px;
padding: 16px 12px;
text-align: center;
transition: all 0.3s;
&.active {
border-color: #4285f4;
background-color: #f0f7ff;
}
.template-name {
font-size: 14px;
font-weight: 500;
color: #333;
margin: 8px 0 4px;
}
.template-desc {
font-size: 12px;
color: #999;
}
}
}
.bottom-actions {
z-index: 999;
position: fixed;
bottom: 0;
left: 0;
background: #fff;
height: 50px;
border-top: 1px solid #e5e5e5;
display: flex;
align-items: center;
gap: 12px;
justify-content: space-between;
padding: 0 16px;
box-sizing: border-box;
width: 100%;
}
</style>
<style lang="scss">
.bottom-actions {
.uv-button-wrapper {
width: 48%;
}
}
</style>

@ -0,0 +1,517 @@
<template>
<view class="container">
<!-- 顶部图片和标题 -->
<view class="header-section">
<image
class="header-image"
src="/static/imgs/indexImg.png"
mode="aspectFill"
></image>
<view class="header-overlay">
<view class="header-title">生成电梯图纸</view>
<view class="header-subtitle">
选择模板并设置参数,快速生成专业电梯施工图
</view>
</view>
</view>
<!-- 选择图纸模板 -->
<view class="section">
<view class="section-title">选择图纸模板</view>
<view class="template-grid">
<view
class="template-item"
:class="{ active: selectedTemplate === 'passenger' }"
@click="selectTemplate('passenger')"
>
<u-icon name="edit-pen" color="#4285f4" size="24"></u-icon>
<view class="template-name">乘客电梯标准图纸</view>
<view class="template-desc">适用于住宅、商业建筑</view>
</view>
<view
class="template-item"
:class="{ active: selectedTemplate === 'freight' }"
@click="selectTemplate('freight')"
>
<u-icon name="car" color="#4285f4" size="24"></u-icon>
<view class="template-name">货运电梯专用图纸</view>
<view class="template-desc">适用于仓库、工厂</view>
</view>
<view
class="template-item"
:class="{ active: selectedTemplate === 'medical' }"
@click="selectTemplate('medical')"
>
<u-icon name="plus-circle" color="#4285f4" size="24"></u-icon>
<view class="template-name">医用电梯专业图纸</view>
<view class="template-desc">适用于医院、医疗场所</view>
</view>
<view
class="template-item"
:class="{ active: selectedTemplate === 'sightseeing' }"
@click="selectTemplate('sightseeing')"
>
<u-icon name="eye" color="#4285f4" size="24"></u-icon>
<view class="template-name">观光电梯定制图纸</view>
<view class="template-desc">适用于商场、酒店</view>
</view>
</view>
</view>
<!-- 设置参数 -->
<view class="section">
<view class="section-title">设置参数</view>
<!-- 电梯类型 -->
<view class="param-item">
<view class="param-label">电梯类型</view>
<u-picker
:show="showTypePicker"
:columns="elevatorTypes"
@confirm="onTypeConfirm"
@cancel="showTypePicker = false"
></u-picker>
<view class="param-value" @click="showTypePicker = true">
{{ selectedType }}
<u-icon name="arrow-down" size="16"></u-icon>
</view>
</view>
<!-- 载重 -->
<view class="param-item">
<view class="param-label">载重(kg)</view>
<view class="param-control">
<u-icon
name="minus-circle"
@click="decreaseWeight"
color="#999"
></u-icon>
<input class="param-input" v-model="weight" type="number" />
<u-icon
name="plus-circle"
@click="increaseWeight"
color="#4285f4"
></u-icon>
</view>
</view>
<!-- 井道宽度 -->
<view class="param-item">
<view class="param-label">井道宽度(mm)</view>
<view class="param-control">
<u-icon
name="minus-circle"
@click="decreaseWidth"
color="#999"
></u-icon>
<input class="param-input" v-model="shaftWidth" type="number" />
<u-icon
name="plus-circle"
@click="increaseWidth"
color="#4285f4"
></u-icon>
</view>
</view>
<!-- 井道深度 -->
<view class="param-item">
<view class="param-label">井道深度(mm)</view>
<view class="param-control">
<u-icon
name="minus-circle"
@click="decreaseDepth"
color="#999"
></u-icon>
<input class="param-input" v-model="shaftDepth" type="number" />
<u-icon
name="plus-circle"
@click="increaseDepth"
color="#4285f4"
></u-icon>
</view>
</view>
<!-- 顶层高度 -->
<view class="param-item">
<view class="param-label">顶层高度(mm)</view>
<view class="param-control">
<u-icon
name="minus-circle"
@click="decreaseTopHeight"
color="#999"
></u-icon>
<input class="param-input" v-model="topHeight" type="number" />
<u-icon
name="plus-circle"
@click="increaseTopHeight"
color="#4285f4"
></u-icon>
</view>
</view>
<!-- 底坑深度 -->
<view class="param-item">
<view class="param-label">底坑深度(mm)</view>
<view class="param-control">
<u-icon
name="minus-circle"
@click="decreasePitDepth"
color="#999"
></u-icon>
<input class="param-input" v-model="pitDepth" type="number" />
<u-icon
name="plus-circle"
@click="increasePitDepth"
color="#4285f4"
></u-icon>
</view>
</view>
</view>
<!-- 备注说明 -->
<view class="section">
<view class="section-title">备注说明</view>
<u-textarea
v-model="remarks"
placeholder="如有特殊需要请备注"
:maxlength="200"
count
height="120"
></u-textarea>
</view>
<!-- 底部按钮 -->
<view class="bottom-actions">
<u-button
type="primary"
size="large"
@click="generateDrawing"
:loading="generating"
>
生成图纸
</u-button>
<u-button type="default" size="large" @click="viewHistory" plain>
查看记录
</u-button>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from "vue";
import {
onShow,
onLoad,
onShareAppMessage,
onShareTimeline,
onReachBottom,
} from "@dcloudio/uni-app";
import { useAuthStore, useUserStore } from "@/store";
import RPC from "@/utils/request";
const authStore = useAuthStore();
const userStore = useUserStore();
// 响应式数据
const selectedTemplate = ref("passenger");
const showTypePicker = ref(false);
const selectedType = ref("曳引式电梯");
const weight = ref(1000);
const shaftWidth = ref(2100);
const shaftDepth = ref(2100);
const topHeight = ref(4500);
const pitDepth = ref(4500);
const remarks = ref("");
const generating = ref(false);
// 电梯类型选项
const elevatorTypes = ref([["曳引式电梯", "液压式电梯", "螺杆式电梯"]]);
// 选择模板
const selectTemplate = (template) => {
selectedTemplate.value = template;
};
// 电梯类型选择确认
const onTypeConfirm = (e) => {
selectedType.value = e.value[0];
showTypePicker.value = false;
};
// 载重控制
const decreaseWeight = () => {
if (weight.value > 100) {
weight.value -= 100;
}
};
const increaseWeight = () => {
weight.value += 100;
};
// 井道宽度控制
const decreaseWidth = () => {
if (shaftWidth.value > 100) {
shaftWidth.value -= 100;
}
};
const increaseWidth = () => {
shaftWidth.value += 100;
};
// 井道深度控制
const decreaseDepth = () => {
if (shaftDepth.value > 100) {
shaftDepth.value -= 100;
}
};
const increaseDepth = () => {
shaftDepth.value += 100;
};
// 顶层高度控制
const decreaseTopHeight = () => {
if (topHeight.value > 100) {
topHeight.value -= 100;
}
};
const increaseTopHeight = () => {
topHeight.value += 100;
};
// 底坑深度控制
const decreasePitDepth = () => {
if (pitDepth.value > 100) {
pitDepth.value -= 100;
}
};
const increasePitDepth = () => {
pitDepth.value += 100;
};
// 生成图纸
const generateDrawing = async () => {
if (!selectedTemplate.value) {
uni.showToast({
title: "请选择图纸模板",
icon: "none",
});
return;
}
generating.value = true;
try {
const params = {
template: selectedTemplate.value,
type: selectedType.value,
weight: weight.value,
shaftWidth: shaftWidth.value,
shaftDepth: shaftDepth.value,
topHeight: topHeight.value,
pitDepth: pitDepth.value,
remarks: remarks.value,
};
// 这里调用生成图纸的API
// const result = await RPC.post('/api/generate-drawing', params)
uni.showToast({
title: "图纸生成中...",
icon: "loading",
duration: 2000,
});
// 模拟生成过程
setTimeout(() => {
uni.showToast({
title: "图纸生成成功",
icon: "success",
});
generating.value = false;
}, 2000);
} catch (error) {
console.error("生成图纸失败:", error);
uni.showToast({
title: "生成失败,请重试",
icon: "none",
});
generating.value = false;
}
};
// 查看历史记录
const viewHistory = () => {
uni.navigateTo({
url: "/pagesA/generate-record/generate-record",
});
};
// 分享给朋友
onShareAppMessage((data) => {
console.log("🚀 ~ onShareAppMessage ~ data:", data);
return {
title: "电梯图纸生成平台",
path: "/pages/index/index",
};
});
// 分享到朋友圈
onShareTimeline((data) => {
console.log("🚀 ~ onShareTimeline ~ data:", data);
return {
title: "电梯图纸生成平台",
query: "a=1&b=2",
};
});
onLoad(() => {
console.log("index-onLoad");
});
</script>
<style lang="scss" scoped>
.container {
font-size: 14px;
min-height: 100vh;
background-color: #f8f8f8;
padding-bottom: 120px;
}
.header-section {
position: relative;
height: 200px;
overflow: hidden;
.header-image {
width: 100%;
height: 100%;
}
.header-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: white;
.header-title {
font-size: 24px;
font-weight: bold;
margin-bottom: 8px;
}
.header-subtitle {
font-size: 14px;
opacity: 0.9;
text-align: center;
padding: 0 20px;
}
}
}
.section {
margin: 16px;
background: white;
border-radius: 12px;
padding: 16px;
.section-title {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 16px;
}
}
.template-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
.template-item {
border: 1px solid #e5e5e5;
border-radius: 8px;
padding: 16px 12px;
text-align: center;
transition: all 0.3s;
&.active {
border-color: #4285f4;
background-color: #f0f7ff;
}
.template-name {
font-size: 14px;
font-weight: 500;
color: #333;
margin: 8px 0 4px;
}
.template-desc {
font-size: 12px;
color: #999;
}
}
}
.param-item {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
.param-label {
font-size: 14px;
color: #333;
flex-shrink: 0;
}
.param-value {
display: flex;
align-items: center;
color: #666;
font-size: 14px;
gap: 4px;
}
.param-control {
display: flex;
align-items: center;
gap: 12px;
.param-input {
width: 80px;
text-align: center;
border: 1px solid #e5e5e5;
border-radius: 4px;
padding: 6px 8px;
font-size: 14px;
}
}
}
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
padding: 16px;
border-top: 1px solid #e5e5e5;
display: flex;
gap: 12px;
.u-button {
flex: 1;
}
}
</style>

@ -0,0 +1,214 @@
<template>
<view class="container">
<view class="header">
<div class="line">您好,</div>
<div class="line">欢迎使用电梯图纸生成平台</div>
</view>
<div class="main">
<view class="form">
<uv-form labelAlign="right" labelWidth="50" labelPosition="top" :model="userInfo" :rules="rules"
ref="form">
<uv-form-item prop="phone">
<div class="formLabel">
<img class="img" src="/static/imgs/phone.png" alt="">
手机号
</div>
<uv-input v-model="userInfo.phone" color="#000" border="bottom"
:placeholderStyle="{color:'#666778'}" placeholder="请输入用户名">
</uv-input>
</uv-form-item>
<uv-form-item prop="password">
<div class="formLabel">
<img class="img" src="/static/imgs/code.png" alt="">
验证码
</div>
<uv-input v-model="userInfo.password" color="#000" border="bottom"
:placeholderStyle="{color:'#666778'}" placeholder="请输入验证码">
<template v-slot:suffix>
<uv-code ref="codeRef" @change="codeChange" seconds="20" changeText="X秒重新获取"></uv-code>
<text style="color:#4D82FD" @click="getCode">{{tips}}</text>
</template>
</uv-input>
</uv-form-item>
</uv-form>
</view>
<uv-button @click="login" customStyle="margin-top:30px" text="登录" color="#3F70FF"></uv-button>
<div class="wechatBox">
<uv-divider text="第三方登录"></uv-divider>
<uv-icon name="weixin-circle-fill" color="#5fc157" size="50"></uv-icon>
</div>
</div>
</view>
</template>
<script setup>
import {
ref
} from "vue";
import {
HOME_PATH,
LOGIN_PATH,
isTabBarPath,
removeQueryString,
} from "@/router";
import {
useAuthStore,
useUserStore
} from "@/store";
import {
onLoad
} from "@dcloudio/uni-app";
import RPC from "@/utils/request"
const authStore = useAuthStore()
const userStore = useUserStore()
const tips = ref('获取验证码')
const codeChange = (text) => {
tips.value = text
}
const codeRef = ref(null)
const getCode = () => {
if(!userInfo.phone) {
uni.showToast({
title:'请填写手机号',
icon:'none'
});
return
}
if (codeRef.value.canGetCode) {
// 模拟向后端请求验证码
uni.showLoading({
title: '正在获取验证码'
})
setTimeout(() => {
uni.hideLoading();
// 这里此提示会被this.start()方法中的提示覆盖
uni.showToast({
title:'验证码已发送',
icon:'none'
});
// 通知验证码组件内部开始倒计时
codeRef.value.start();
}, 2000);
} else {
uni.showToast({
title:'倒计时结束后再发送',
icon:'none'
})
}
}
const form = ref(null)
const userInfo = ref({
phone: '',
password: '',
})
const rules = ref({
'phone': {
required: true,
message: '请填写手机号',
trigger: ['blur', 'change']
},
'password': {
required: true,
message: '请填写验证码',
trigger: ['blur', 'change']
}
})
let redirect = HOME_PATH;
onLoad((options) => {
if (options.redirect && removeQueryString(options.redirect) !== LOGIN_PATH) {
redirect = decodeURIComponent(options.redirect)
}
});
const login = async () => {
await form.value.validate()
console.log(userInfo.value)
// RPC.post('/account/login', {
// username: userInfo.value.name,
// password: userInfo.value.password,
// }, {
// custom: {
// loading: true,
// loadingText: '正在登录...'
// },
// }).then(res => {
// authStore.setAccessToken(res.token)
// userStore.setUserInfo(res)
// uni.$uv.route({
// type: isTabBarPath(redirect) ? "switchTab" : "redirectTo",
// url: redirect,
// });
// })
authStore.setAccessToken('accessToken')
setTimeout(() => {
uni.$uv.route({
type: isTabBarPath(redirect) ? "switchTab" : "redirectTo",
url: redirect,
});
}, 800);
}
</script>
<style lang="scss" scoped>
.container {
height: 100vh;
display: flex;
flex-direction: column;
background: linear-gradient(124.92deg, rgba(77, 130, 253, 1) 7.66%, rgba(85, 184, 254, 1) 89.55%);
.header {
height: 15vh;
color: #fff;
display: flex;
flex-direction: column;
padding-left: 7vw;
font-size: 16px;
justify-content: center;
.line {
margin: 6px 0;
}
}
.main {
flex: 1;
background-color: #fff;
border-radius: 20px 20px 0 0;
padding: 7vw;
.formLabel {
display: flex;
align-items: center;
margin-bottom: 4px;
.img {
width: 20px;
height: 20px;
margin-right: 6px;
}
}
.wechatBox {
position: fixed;
width: 100%;
left: 0;
bottom: 15vh;
display: flex;
flex-direction: column;
align-items: center;
}
}
}
</style>
<style lang="scss">
.wechatBox {
.uv-divider {
margin-bottom: 30px;
width: 90%;
}
}
</style>

@ -0,0 +1,457 @@
<template>
<view class="container">
<!-- 顶部标题 -->
<view class="header">
<view class="header-title">管理您的账户信息和服务</view>
</view>
<!-- 用户信息卡片 -->
<view class="user-card">
<view class="user-info">
<image
class="avatar"
src="/static/imgs/avatar.png"
mode="aspectFill"
></image>
<view class="user-details">
<view class="username">
张三
<u-icon
name="checkmark-circle-fill"
color="#ffa500"
size="16"
></u-icon>
</view>
<view class="user-id">用户ID: U12345678</view>
<view class="membership">
会员等级: 年会员 (有效期至: 2026-05-15)
</view>
</view>
</view>
<view class="edit-btn" @click="editProfile">
<text>点击编辑资料</text>
</view>
</view>
<!-- 账户余额 -->
<view class="balance-card">
<view class="balance-header">
<view class="balance-label">账户余额</view>
<u-button type="default" size="small" shape="round" @click="recharge">
充值
</u-button>
</view>
<view class="balance-amount">¥50.00</view>
</view>
<!-- 会员套餐 -->
<view class="membership-section">
<view class="membership-grid">
<view
class="membership-item"
:class="{ active: selectedPlan === 'monthly' }"
@click="selectPlan('monthly')"
>
<view class="tag new">新人特惠</view>
<view class="plan-name">包月会员</view>
<view class="plan-price">¥120</view>
</view>
<view
class="membership-item"
:class="{ active: selectedPlan === 'season' }"
@click="selectPlan('season')"
>
<view class="tag popular">最受欢迎</view>
<view class="plan-name">包季会员</view>
<view class="plan-price">¥300</view>
<view class="plan-discount">立减¥60</view>
</view>
<view
class="membership-item"
:class="{ active: selectedPlan === 'yearly' }"
@click="selectPlan('yearly')"
>
<view class="tag yearly">年度首享</view>
<view class="plan-name">包年会员</view>
<view class="plan-price">¥1000</view>
<view class="plan-discount">立减¥440</view>
</view>
</view>
</view>
<!-- 支付方式 -->
<view class="payment-section">
<view
class="payment-item"
:class="{ active: selectedPayment === 'alipay' }"
@click="selectPayment('alipay')"
>
<view class="payment-info">
<u-icon name="alipay-circle-fill" color="#1677ff" size="24"></u-icon>
<text class="payment-name">支付宝</text>
</view>
<u-icon
:name="
selectedPayment === 'alipay'
? 'checkmark-circle-fill'
: 'radio-button-off'
"
:color="selectedPayment === 'alipay' ? '#ff3333' : '#ccc'"
size="20"
></u-icon>
</view>
<view
class="payment-item"
:class="{ active: selectedPayment === 'wechat' }"
@click="selectPayment('wechat')"
>
<view class="payment-info">
<u-icon name="weixin-fill" color="#07c160" size="24"></u-icon>
<text class="payment-name">支付宝</text>
</view>
<u-icon
:name="
selectedPayment === 'wechat'
? 'checkmark-circle-fill'
: 'radio-button-off'
"
:color="selectedPayment === 'wechat' ? '#ff3333' : '#ccc'"
size="20"
></u-icon>
</view>
</view>
<!-- 底部按钮 -->
<view class="bottom-section">
<u-button
type="primary"
size="large"
@click="confirmPayment"
:loading="paying"
>
确认支付
</u-button>
<view class="agreement">
购买前请仔细阅读
<text class="link" @click="showAgreement">《会员服务协议》</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from "vue";
import HeaderCustom from "/components/HeaderCustom/HeaderCustom.vue";
import { useAuthStore, useUserStore } from "@/store";
import RPC from "@/utils/request";
const modal = ref(null);
const authStore = useAuthStore();
const userStore = useUserStore();
// 响应式数据
const selectedPlan = ref("season"); // 默认选中包季会员
const selectedPayment = ref("alipay"); // 默认选中支付宝
const paying = ref(false);
// 选择会员套餐
const selectPlan = (plan) => {
selectedPlan.value = plan;
};
// 选择支付方式
const selectPayment = (payment) => {
selectedPayment.value = payment;
};
// 编辑资料
const editProfile = () => {
uni.navigateTo({
url: "/pagesA/edit-profile/edit-profile",
});
};
// 充值
const recharge = () => {
uni.navigateTo({
url: "/pages/recharge/recharge",
});
};
// 确认支付
const confirmPayment = async () => {
if (!selectedPlan.value) {
uni.showToast({
title: "请选择会员套餐",
icon: "none",
});
return;
}
if (!selectedPayment.value) {
uni.showToast({
title: "请选择支付方式",
icon: "none",
});
return;
}
paying.value = true;
try {
const params = {
plan: selectedPlan.value,
paymentMethod: selectedPayment.value,
};
// 这里调用支付API
// const result = await RPC.post('/api/payment', params)
uni.showToast({
title: "正在跳转支付...",
icon: "loading",
duration: 2000,
});
// 模拟支付过程
setTimeout(() => {
uni.showToast({
title: "支付成功",
icon: "success",
});
paying.value = false;
}, 2000);
} catch (error) {
console.error("支付失败:", error);
uni.showToast({
title: "支付失败,请重试",
icon: "none",
});
paying.value = false;
}
};
// 显示协议
const showAgreement = () => {
uni.navigateTo({
url: "/pages/agreement/agreement",
});
};
</script>
<style lang="scss" scoped>
.container {
font-size: 14px;
min-height: 100vh;
background: linear-gradient(180deg, #7b9cdb 0%, #f8f8f8 40%);
padding-bottom: 120px;
}
.header {
padding: 20px 16px 16px;
.header-title {
color: white;
font-size: 18px;
font-weight: 500;
}
}
.user-card {
margin: 0 16px 16px;
background: white;
border-radius: 12px;
padding: 16px;
.user-info {
display: flex;
align-items: center;
margin-bottom: 12px;
.avatar {
width: 60px;
height: 60px;
border-radius: 50%;
margin-right: 12px;
}
.user-details {
flex: 1;
.username {
display: flex;
align-items: center;
gap: 4px;
font-size: 18px;
font-weight: bold;
color: #333;
margin-bottom: 4px;
}
.user-id {
font-size: 12px;
color: #999;
margin-bottom: 2px;
}
.membership {
font-size: 12px;
color: #999;
}
}
}
.edit-btn {
text-align: right;
color: #4285f4;
font-size: 14px;
}
}
.balance-card {
margin: 0 16px 16px;
background: linear-gradient(135deg, #4285f4 0%, #6fa8f5 100%);
border-radius: 12px;
padding: 20px;
color: white;
.balance-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.balance-label {
font-size: 14px;
opacity: 0.9;
}
}
.balance-amount {
font-size: 32px;
font-weight: bold;
}
}
.membership-section {
margin: 0 16px 24px;
.membership-grid {
display: flex;
gap: 8px;
.membership-item {
flex: 1;
background: white;
border-radius: 12px;
padding: 16px 12px;
text-align: center;
position: relative;
border: 2px solid transparent;
transition: all 0.3s;
&.active {
border-color: #4285f4;
background-color: #f0f7ff;
}
.tag {
position: absolute;
top: -6px;
left: 50%;
transform: translateX(-50%);
padding: 2px 8px;
border-radius: 8px;
font-size: 10px;
color: white;
&.new {
background: #ff6b6b;
}
&.popular {
background: #ff3333;
}
&.yearly {
background: #ff3333;
}
}
.plan-name {
font-size: 14px;
color: #333;
margin: 12px 0 8px;
}
.plan-price {
font-size: 20px;
font-weight: bold;
color: #ff3333;
margin-bottom: 4px;
}
.plan-discount {
font-size: 12px;
color: #999;
}
}
}
}
.payment-section {
margin: 0 16px 24px;
.payment-item {
display: flex;
align-items: center;
justify-content: space-between;
background: white;
border-radius: 12px;
padding: 16px;
margin-bottom: 8px;
border: 1px solid #e5e5e5;
transition: all 0.3s;
&.active {
border-color: #4285f4;
background-color: #f0f7ff;
}
&:last-child {
margin-bottom: 0;
}
.payment-info {
display: flex;
align-items: center;
gap: 12px;
.payment-name {
font-size: 14px;
color: #333;
}
}
}
}
.bottom-section {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
padding: 16px;
border-top: 1px solid #e5e5e5;
.agreement {
text-align: center;
font-size: 12px;
color: #999;
margin-top: 12px;
.link {
color: #4285f4;
}
}
}
</style>

@ -0,0 +1,528 @@
<template>
<view class="container">
<!-- 顶部筛选标签 -->
<view class="filter-tabs">
<view
class="tab-item"
:class="{ active: activeTab === 'all' }"
@click="switchTab('all')"
>
全部
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'recharge' }"
@click="switchTab('recharge')"
>
充值
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'monthly' }"
@click="switchTab('monthly')"
>
包月
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'season' }"
@click="switchTab('season')"
>
包季
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'yearly' }"
@click="switchTab('yearly')"
>
包年
</view>
</view>
<!-- 订单列表 -->
<view class="order-list">
<view
class="order-item"
v-for="order in filteredOrders"
:key="order.id"
>
<!-- 订单头部 -->
<view class="order-header">
<view class="order-info">
<text class="order-number">订单编号: {{ order.orderNumber }}</text>
<view class="order-type-tag">{{ order.typeLabel }}</view>
</view>
<view
class="order-status"
:class="order.status"
>
{{ order.statusText }}
</view>
</view>
<!-- 订单详情 -->
<view class="order-details">
<view class="detail-row">
<text class="label">用户名称:</text>
<text class="value">{{ order.userName }}</text>
</view>
<view class="detail-row">
<text class="label">用户ID:</text>
<text class="value">{{ order.userId }}</text>
</view>
<view class="detail-row">
<text class="label">订单类型:</text>
<text class="value">{{ order.orderType }}</text>
</view>
<view class="detail-row">
<text class="label">订单金额:</text>
<text class="value price">¥{{ order.amount }}</text>
</view>
<view class="detail-row" v-if="order.discount > 0">
<text class="label">折扣金额:</text>
<text class="value discount">-¥{{ order.discount }}</text>
</view>
<view class="detail-row">
<text class="label">实付金额:</text>
<text class="value final-price">¥{{ order.finalAmount }}</text>
</view>
<view class="detail-row">
<text class="label">{{ order.status === 'pending' ? '创建时间:' : '支付时间:' }}</text>
<text class="value">{{ order.payTime }}</text>
</view>
</view>
<!-- 订单操作 -->
<view class="order-actions">
<u-button
v-if="order.status === 'pending'"
type="default"
size="small"
@click="cancelOrder(order)"
>
取消订单
</u-button>
<u-button
type="default"
size="small"
@click="deleteOrder(order)"
>
删除订单
</u-button>
<u-button
type="primary"
size="small"
@click="viewOrderDetail(order)"
>
查看详情
</u-button>
</view>
<!-- 待支付订单底部 -->
<view v-if="order.status === 'pending'" class="pending-footer">
<view class="countdown">
<text class="countdown-text">支付剩余时间: {{ order.countdown }}</text>
</view>
<view class="pending-actions">
<u-button
type="default"
size="large"
@click="cancelOrder(order)"
>
取消订单
</u-button>
<u-button
type="error"
size="large"
@click="payOrder(order)"
>
去支付
</u-button>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="filteredOrders.length === 0" class="empty-state">
<u-icon name="file-text" size="60" color="#ccc"></u-icon>
<text class="empty-text">暂无订单记录</text>
</view>
</view>
</template>
<script setup>
import {
computed,
onMounted,
ref
} from "vue";
import {
onShow
} from "@dcloudio/uni-app";
import {
useAuthStore,
useUserStore
} from "@/store";
import {
regions
} from '/static/json/regions.js'
import HeaderCustom from '/components/HeaderCustom/HeaderCustom.vue'
import RPC from "@/utils/request";
const authStore = useAuthStore()
const userStore = useUserStore()
// 响应式数据
const activeTab = ref('all')
const orders = ref([
{
id: 1,
orderNumber: 'ORD20230515001',
typeLabel: '续费',
status: 'paid',
statusText: '已支付',
userName: '张三',
userId: 'U12345678',
orderType: '包年会员',
amount: '1200.00',
discount: 200.00,
finalAmount: '1000.00',
payTime: '2025-05-15 10:30',
type: 'yearly'
},
{
id: 2,
orderNumber: 'ORD20230515002',
typeLabel: '续费',
status: 'paid',
statusText: '已支付',
userName: '李四',
userId: 'U12345666',
orderType: '充值',
amount: '500.00',
discount: 0,
finalAmount: '500.00',
payTime: '2025-06-15 11:30',
type: 'recharge'
},
{
id: 3,
orderNumber: 'ORD20230515332',
typeLabel: '续费',
status: 'pending',
statusText: '待支付',
userName: '小客',
userId: 'U12345555',
orderType: '包月会员',
amount: '120.00',
discount: 12.00,
finalAmount: '108.00',
payTime: '2025-08-12 15:20',
countdown: '30分钟',
type: 'monthly'
}
])
// 计算过滤后的订单
const filteredOrders = computed(() => {
if (activeTab.value === 'all') {
return orders.value
}
return orders.value.filter(order => order.type === activeTab.value)
})
// 切换标签
const switchTab = (tab) => {
activeTab.value = tab
}
// 查看订单详情
const viewOrderDetail = (order) => {
uni.navigateTo({
url: `/pages/order-detail/order-detail?id=${order.id}`
})
}
// 取消订单
const cancelOrder = (order) => {
uni.showModal({
title: '确认取消',
content: '确定要取消这个订单吗?',
success: async (res) => {
if (res.confirm) {
try {
// 调用取消订单API
// await RPC.post('/api/order/cancel', { orderId: order.id })
// 更新订单状态
const index = orders.value.findIndex(item => item.id === order.id)
if (index !== -1) {
orders.value[index].status = 'cancelled'
orders.value[index].statusText = '已取消'
}
uni.showToast({
title: '订单已取消',
icon: 'success'
})
} catch (error) {
console.error('取消订单失败:', error)
uni.showToast({
title: '取消失败,请重试',
icon: 'none'
})
}
}
}
})
}
// 删除订单
const deleteOrder = (order) => {
uni.showModal({
title: '确认删除',
content: '确定要删除这个订单吗?删除后无法恢复。',
success: async (res) => {
if (res.confirm) {
try {
// 调用删除订单API
// await RPC.post('/api/order/delete', { orderId: order.id })
// 从列表中移除订单
const index = orders.value.findIndex(item => item.id === order.id)
if (index !== -1) {
orders.value.splice(index, 1)
}
uni.showToast({
title: '订单已删除',
icon: 'success'
})
} catch (error) {
console.error('删除订单失败:', error)
uni.showToast({
title: '删除失败,请重试',
icon: 'none'
})
}
}
}
})
}
// 支付订单
const payOrder = (order) => {
uni.navigateTo({
url: `/pages/payment/payment?orderId=${order.id}`
})
}
// 加载订单数据
const loadOrders = async () => {
try {
// 调用获取订单列表API
// const result = await RPC.get('/api/orders')
// orders.value = result.data
} catch (error) {
console.error('加载订单失败:', error)
uni.showToast({
title: '加载失败,请重试',
icon: 'none'
})
}
}
onMounted(() => {
loadOrders()
})
onShow(() => {
// 页面显示时刷新订单列表
loadOrders()
})
</script>
<style lang="scss" scoped>
.container {
font-size: 14px;
min-height: 100vh;
background-color: #f8f8f8;
}
.filter-tabs {
display: flex;
background: white;
padding: 0 16px;
border-bottom: 1px solid #e5e5e5;
.tab-item {
flex: 1;
text-align: center;
padding: 16px 0;
color: #666;
font-size: 14px;
position: relative;
&.active {
color: #4285f4;
font-weight: 500;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 20px;
height: 2px;
background: #4285f4;
border-radius: 1px;
}
}
}
}
.order-list {
padding: 16px;
.order-item {
background: white;
border-radius: 12px;
margin-bottom: 16px;
overflow: hidden;
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 16px 0;
.order-info {
display: flex;
align-items: center;
gap: 8px;
.order-number {
font-size: 14px;
color: #333;
}
.order-type-tag {
background: #f0f0f0;
color: #666;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
}
}
.order-status {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
&.paid {
background: #e8f5e8;
color: #52c41a;
}
&.pending {
background: #fff7e6;
color: #fa8c16;
}
&.cancelled {
background: #fff1f0;
color: #ff4d4f;
}
}
}
.order-details {
padding: 16px;
.detail-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
&:last-child {
margin-bottom: 0;
}
.label {
color: #666;
font-size: 14px;
}
.value {
color: #333;
font-size: 14px;
&.price {
color: #333;
font-weight: 500;
}
&.discount {
color: #ff4d4f;
}
&.final-price {
color: #333;
font-weight: bold;
}
}
}
}
.order-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 0 16px 16px;
border-top: 1px solid #f0f0f0;
padding-top: 16px;
}
.pending-footer {
border-top: 1px solid #f0f0f0;
padding: 16px;
.countdown {
margin-bottom: 12px;
.countdown-text {
color: #ff4d4f;
font-size: 12px;
}
}
.pending-actions {
display: flex;
gap: 12px;
.u-button {
flex: 1;
}
}
}
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
.empty-text {
color: #999;
font-size: 14px;
margin-top: 16px;
}
}
</style>

@ -0,0 +1,460 @@
<template>
<view class="container">
<!-- 个人信息设置 -->
<view class="profile-section">
<!-- 头像 -->
<view class="profile-item" @click="changeAvatar">
<view class="item-label">头像</view>
<view class="item-content">
<image class="avatar" :src="userInfo.avatar" mode="aspectFill"></image>
<u-icon name="arrow-right" size="16" color="#ccc"></u-icon>
</view>
</view>
<!-- 用户名 -->
<view class="profile-item" @click="editUsername">
<view class="item-label">用户名</view>
<view class="item-content">
<text class="item-value">{{ userInfo.username }}</text>
<u-icon name="arrow-right" size="16" color="#ccc"></u-icon>
</view>
</view>
<!-- 电话 -->
<view class="profile-item" @click="editPhone">
<view class="item-label">电话</view>
<view class="item-content">
<text class="item-value">{{ userInfo.phone }}</text>
<u-icon name="arrow-right" size="16" color="#ccc"></u-icon>
</view>
</view>
<!-- 邮箱 -->
<view class="profile-item" @click="editEmail">
<view class="item-label">邮箱</view>
<view class="item-content">
<text class="item-value">{{ userInfo.email }}</text>
<u-icon name="arrow-right" size="16" color="#ccc"></u-icon>
</view>
</view>
</view>
<!-- 应用设置 -->
<view class="app-section">
<!-- 缓存 -->
<!-- <view class="profile-item" @click="clearCache">
<view class="item-label">缓存</view>
<view class="item-content">
<text class="item-value">{{ cacheSize }}</text>
<u-icon name="arrow-right" size="16" color="#ccc"></u-icon>
</view>
</view> -->
<!-- 用户协议 -->
<view class="profile-item" @click="viewUserAgreement">
<view class="item-label">用户协议</view>
<view class="item-content">
<u-icon name="arrow-right" size="16" color="#ccc"></u-icon>
</view>
</view>
<!-- 关于我们 -->
<view class="profile-item" @click="viewAbout">
<view class="item-label">关于我们</view>
<view class="item-content">
<u-icon name="arrow-right" size="16" color="#ccc"></u-icon>
</view>
</view>
</view>
<!-- 退出登录按钮 -->
<view class="logout-section">
<u-button type="primary" size="large" @click="logout" :loading="logoutLoading">
退出登陆
</u-button>
</view>
<!-- 编辑弹窗 -->
<u-popup :show="showEditModal" mode="center" @close="closeEditModal" :round="10">
<view class="edit-modal">
<view class="modal-title">
编辑{{
editType === "username"
? "用户名"
: editType === "phone"
? "电话"
: "邮箱"
}}
</view>
<u-input v-model="editValue" :placeholder="getPlaceholder()" :type="
editType === 'phone'
? 'number'
: editType === 'email'
? 'email'
: 'text'
" border="bottom" clearable></u-input>
<view class="modal-actions">
<u-button type="default" @click="closeEditModal">取消</u-button>
<u-button type="primary" @click="saveEdit">保存</u-button>
</view>
</view>
</u-popup>
<!-- 头像选择弹窗 -->
<u-action-sheet :show="showAvatarSheet" :actions="avatarActions" @close="showAvatarSheet = false"
@select="onAvatarSelect"></u-action-sheet>
</view>
</template>
<script setup>
import {
ref,
onMounted
} from "vue";
import {
onShow
} from "@dcloudio/uni-app";
import {
useAuthStore,
useUserStore
} from "@/store";
const authStore = useAuthStore()
const userStore = useUserStore()
// 响应式数据
const userInfo = ref({
avatar: "/static/imgs/avatar.png",
username: "张三",
phone: "15825556676",
email: "235382903@qq.com",
});
const cacheSize = ref("36MB");
const logoutLoading = ref(false);
const showEditModal = ref(false);
const showAvatarSheet = ref(false);
const editType = ref("");
const editValue = ref("");
// 头像选择选项
const avatarActions = ref([{
name: "拍照",
value: "camera",
},
{
name: "从相册选择",
value: "album",
},
]);
// 更换头像
const changeAvatar = () => {
showAvatarSheet.value = true;
};
// 头像选择回调
const onAvatarSelect = (item) => {
showAvatarSheet.value = false;
uni.chooseImage({
count: 1,
sizeType: ["compressed"],
sourceType: item.value === "camera" ? ["camera"] : ["album"],
success: (res) => {
const tempFilePath = res.tempFilePaths[0];
// 这里应该上传图片到服务器
// uploadAvatar(tempFilePath)
// 临时更新头像显示
userInfo.value.avatar = tempFilePath;
uni.showToast({
title: "头像更新成功",
icon: "success",
});
},
fail: () => {
uni.showToast({
title: "选择图片失败",
icon: "none",
});
},
});
};
// 编辑用户名
const editUsername = () => {
editType.value = "username";
editValue.value = userInfo.value.username;
showEditModal.value = true;
};
// 编辑电话
const editPhone = () => {
editType.value = "phone";
editValue.value = userInfo.value.phone;
showEditModal.value = true;
};
// 编辑邮箱
const editEmail = () => {
editType.value = "email";
editValue.value = userInfo.value.email;
showEditModal.value = true;
};
// 获取输入框占位符
const getPlaceholder = () => {
switch (editType.value) {
case "username":
return "请输入用户名";
case "phone":
return "请输入手机号";
case "email":
return "请输入邮箱地址";
default:
return "请输入";
}
};
// 关闭编辑弹窗
const closeEditModal = () => {
showEditModal.value = false;
editType.value = "";
editValue.value = "";
};
// 保存编辑
const saveEdit = async () => {
if (!editValue.value.trim()) {
uni.showToast({
title: "请输入内容",
icon: "none",
});
return;
}
// 验证格式
if (editType.value === "phone" && !/^1[3-9]\d{9}$/.test(editValue.value)) {
uni.showToast({
title: "请输入正确的手机号",
icon: "none",
});
return;
}
if (
editType.value === "email" &&
!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(editValue.value)
) {
uni.showToast({
title: "请输入正确的邮箱地址",
icon: "none",
});
return;
}
try {
// 调用更新用户信息API
// await RPC.post('/api/user/update', {
// [editType.value]: editValue.value
// })
// 更新本地数据
userInfo.value[editType.value] = editValue.value;
uni.showToast({
title: "保存成功",
icon: "success",
});
closeEditModal();
} catch (error) {
console.error("保存失败:", error);
uni.showToast({
title: "保存失败,请重试",
icon: "none",
});
}
};
// 清除缓存
const clearCache = () => {
uni.showModal({
title: "清除缓存",
content: "确定要清除应用缓存吗?",
success: (res) => {
if (res.confirm) {
uni.showLoading({
title: "清除中...",
});
// 模拟清除缓存
setTimeout(() => {
uni.hideLoading();
cacheSize.value = "0MB";
uni.showToast({
title: "缓存清除成功",
icon: "success",
});
}, 1500);
}
},
});
};
// 查看用户协议
const viewUserAgreement = () => {
uni.navigateTo({
url: "/pagesA/user-agreement/user-agreement",
});
};
// 查看关于我们
const viewAbout = () => {
uni.navigateTo({
url: "/pagesA/about/about",
});
};
// 退出登录
const logout = () => {
uni.showModal({
title: "退出登录",
content: "确定要退出登录吗?",
success: (res) => {
if (res.confirm) {
logoutLoading.value = true;
// 模拟退出登录
setTimeout(() => {
authStore.signOut()
logoutLoading.value = false;
// 清除用户信息和token
// authStore.logout()
uni.showToast({
title: "已退出登录",
icon: "success",
});
// 跳转到登录页
uni.reLaunch({
url: "/pages/login/login",
});
}, 1000);
}
},
});
};
// 加载用户信息
const loadUserInfo = async () => {
try {
// 调用获取用户信息API
// const result = await RPC.get('/api/user/info')
// userInfo.value = result.data
} catch (error) {
console.error("加载用户信息失败:", error);
}
};
// 计算缓存大小
const calculateCacheSize = () => {
// 实际项目中应该计算真实的缓存大小
// const size = await getCacheSize()
// cacheSize.value = formatSize(size)
};
onMounted(() => {
loadUserInfo();
calculateCacheSize();
});
onShow(() => {
// 页面显示时刷新用户信息
loadUserInfo();
});
</script>
<style lang="scss" scoped>
.container {
font-size: 14px;
min-height: 100vh;
background-color: #f8f8f8;
padding: 16px;
}
.profile-section,
.app-section {
background: white;
border-radius: 12px;
margin-bottom: 16px;
overflow: hidden;
.profile-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.item-label {
font-size: 16px;
color: #333;
font-weight: 500;
}
.item-content {
display: flex;
align-items: center;
gap: 8px;
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
}
.item-value {
font-size: 14px;
color: #666;
}
}
}
}
.logout-section {
margin-top: 32px;
}
.edit-modal {
width: 300px;
padding: 24px;
.modal-title {
font-size: 18px;
font-weight: bold;
color: #333;
text-align: center;
margin-bottom: 20px;
}
.modal-actions {
display: flex;
gap: 12px;
margin-top: 20px;
.u-button {
flex: 1;
}
}
}
</style>

@ -0,0 +1,351 @@
<template>
<view class="container">
<!-- 生成记录列表 -->
<view class="record-list">
<view class="record-item" v-for="record in records" :key="record.id">
<!-- 记录信息 -->
<view class="record-info">
<view class="record-title">{{ record.title }}</view>
<view class="record-time">生成时间: {{ record.generateTime }}</view>
</view>
<!-- 状态标签 -->
<view class="status-tag" :class="record.status">
{{ record.statusText }}
</view>
<!-- 操作按钮 -->
<view class="record-actions">
<u-button
v-if="record.status === 'completed'"
type="primary"
size="small"
@click="downloadRecord(record)"
>
<u-icon
name="download"
size="14"
color="white"
style="margin-right: 4px"
></u-icon>
下载
</u-button>
<u-button v-else type="info" size="small" disabled>
<u-icon
name="download"
size="14"
color="white"
style="margin-right: 4px"
></u-icon>
下载
</u-button>
<u-button
v-if="record.status === 'failed'"
type="error"
size="small"
@click="deleteRecord(record)"
style="margin-left: 8px"
>
<u-icon
name="trash"
size="14"
color="white"
style="margin-right: 4px"
></u-icon>
删除
</u-button>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="records.length === 0" class="empty-state">
<u-icon name="file-text" size="60" color="#ccc"></u-icon>
<text class="empty-text">暂无生成记录</text>
</view>
<!-- 加载更多 -->
<view v-if="hasMore && records.length > 0" class="load-more">
<u-button
type="default"
size="small"
:loading="loading"
@click="loadMore"
>
{{ loading ? "加载中..." : "加载更多" }}
</u-button>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { onShow } from "@dcloudio/uni-app";
// 响应式数据
const loading = ref(false);
const hasMore = ref(true);
const records = ref([
{
id: 1,
title: "乘客电梯标准图纸",
generateTime: "2025-05-15 14:30",
status: "completed",
statusText: "已完成",
downloadUrl: "/static/files/passenger-elevator.pdf",
},
{
id: 2,
title: "货运电梯专用图纸",
generateTime: "2025-05-15 14:30",
status: "failed",
statusText: "未完成",
downloadUrl: null,
},
{
id: 3,
title: "医用电梯专业图纸",
generateTime: "2025-05-15 14:30",
status: "completed",
statusText: "已完成",
downloadUrl: "/static/files/medical-elevator.pdf",
},
{
id: 4,
title: "观光电梯定制图纸",
generateTime: "2025-05-15 14:30",
status: "completed",
statusText: "已完成",
downloadUrl: "/static/files/sightseeing-elevator.pdf",
},
{
id: 5,
title: "货运电梯专用图纸",
generateTime: "2025-05-15 14:30",
status: "failed",
statusText: "未完成",
downloadUrl: null,
},
]);
// 下载记录
const downloadRecord = (record) => {
if (record.status !== "completed" || !record.downloadUrl) {
uni.showToast({
title: "文件未准备好",
icon: "none",
});
return;
}
uni.showLoading({
title: "准备下载...",
});
// 模拟下载过程
setTimeout(() => {
uni.hideLoading();
// 在实际项目中,这里应该调用下载API或打开文件
uni.showToast({
title: "下载成功",
icon: "success",
});
// 实际下载逻辑
// uni.downloadFile({
// url: record.downloadUrl,
// success: (res) => {
// if (res.statusCode === 200) {
// uni.saveFile({
// tempFilePath: res.tempFilePath,
// success: () => {
// uni.showToast({
// title: '下载成功',
// icon: 'success'
// })
// }
// })
// }
// },
// fail: () => {
// uni.showToast({
// title: '下载失败',
// icon: 'none'
// })
// }
// })
}, 1500);
};
// 删除记录
const deleteRecord = (record) => {
uni.showModal({
title: "确认删除",
content: "确定要删除这条生成记录吗?删除后无法恢复。",
success: (res) => {
if (res.confirm) {
// 从列表中移除记录
const index = records.value.findIndex((item) => item.id === record.id);
if (index !== -1) {
records.value.splice(index, 1);
}
uni.showToast({
title: "删除成功",
icon: "success",
});
// 实际项目中调用删除API
// try {
// await RPC.post('/api/generate-record/delete', { id: record.id })
// } catch (error) {
// console.error('删除失败:', error)
// }
}
},
});
};
// 加载更多数据
const loadMore = async () => {
if (loading.value) return;
loading.value = true;
try {
// 模拟API调用
await new Promise((resolve) => setTimeout(resolve, 1000));
// 模拟没有更多数据
hasMore.value = false;
uni.showToast({
title: "没有更多数据了",
icon: "none",
});
} catch (error) {
console.error("加载更多失败:", error);
uni.showToast({
title: "加载失败,请重试",
icon: "none",
});
} finally {
loading.value = false;
}
};
// 加载生成记录
const loadRecords = async () => {
try {
// 调用获取生成记录API
// const result = await RPC.get('/api/generate-records')
// records.value = result.data
} catch (error) {
console.error("加载生成记录失败:", error);
uni.showToast({
title: "加载失败,请重试",
icon: "none",
});
}
};
// 刷新数据
const onRefresh = async () => {
hasMore.value = true;
await loadRecords();
};
onMounted(() => {
loadRecords();
});
onShow(() => {
// 页面显示时刷新数据
loadRecords();
});
</script>
<style lang="scss" scoped>
.container {
font-size: 14px;
min-height: 100vh;
background-color: #f8f8f8;
padding: 16px;
}
.record-list {
.record-item {
background: white;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 12px;
.record-info {
flex: 1;
.record-title {
font-size: 16px;
color: #333;
font-weight: 500;
margin-bottom: 6px;
}
.record-time {
font-size: 13px;
color: #999;
}
}
.status-tag {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
&.completed {
background: #e8f5e8;
color: #52c41a;
}
&.failed {
background: #fff1f0;
color: #ff4d4f;
}
&.processing {
background: #fff7e6;
color: #fa8c16;
}
}
.record-actions {
display: flex;
align-items: center;
flex-shrink: 0;
}
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
.empty-text {
color: #999;
font-size: 14px;
margin-top: 16px;
}
}
.load-more {
padding: 20px;
text-align: center;
}
</style>

@ -0,0 +1,47 @@
<template>
<view class="container">
<HeaderCustom title="用户协议" :showBack="true"></HeaderCustom>
<div class="contentBox">
<div class="part">
一、总则
</div>
<div class="part">
<div class="line">1、用户在注册及使用前请认真阅读本协议,确保充分理解本协议中所有条款。除非您接受本协议所有条款,否则您无权注册、登录或使用本协议所涉服务。您的注册、登录、使用等行为将视为无条件接受本协议所有条款的约束。</div>
<div class="line">2、除非另有明确规定,本产品所推出的新功能、新服务均无条件的使用本协议。</div>
<div class="line">3、我公司保留在任何时候修改本协议条款的权利,且无需另行通知。在我公司修改协议条款后,如果您不接受修改后的条款,请立即停止使用本产品提供的服务,继续使用本产品提供的服务将被视为接受修改后的协议。</div>
</div>
<div class="part">
二、免责声明
</div>
<div class="part">
<div class="line">1、对于经由本产品服务而传送的内容,我公司不保证前述内容的正确性、完整性或品质。用户在接受有关服务时,有可能会接触到令人不快、不适当或令人厌恶的内容。在任何情况下,我公司均不对任何内容负责,包括但不限于任何内容发生任何错误或纰漏以及衍生的任何损失或损害。用户使用上述内容、应自行承担风险。</div>
<div class="line">2、用户明确同意其使用本产品所存在的风险及其后果将完全由其自己承担,我公司对用户不承担任何责任。如因用户违反有关法律、法规或本协议项下的任何条款而给任何其他第三人造成损失,用户同意承担由此造成的损害赔偿责任。</div>
<div class="line">3、我公司尊重并保护用户的个人隐私权。但因恶意的网络攻击等行为及其他无法控制的情形,导致用户隐私信息泄露的,用户同意我公司不承担任何责任。</div>
<div class="line">4、对于因电信系统或互联网网络故障、计算机故障、计算机系统问题或其它任何不可抗力原因而产生损失,我公司不承担任何责任,但将尽力减少因此给用户造成的损失和影响。</div>
</div>
</div>
</view>
</template>
<script setup>
import {
ref
} from "vue";
import HeaderCustom from '/components/HeaderCustom/HeaderCustom.vue'
</script>
<style lang="scss" scoped>
.container {
font-size: 14px;
}
.contentBox {
padding: 20px;
color: #000;
font-weight: 400;
line-height: 22px;
.part {
margin-bottom: 20px;
}
}
</style>

@ -0,0 +1,17 @@
import { setupI18n } from "@/locale";
import { setupStore } from "@/store";
import { setupPermission } from "./permission";
import { setupUI } from "./ui";
export default {
install(app) {
// UI扩展配置
app.use(setupUI);
// 状态管理
app.use(setupStore);
// 国际化
app.use(setupI18n);
// 路由拦截
app.use(setupPermission);
},
};

@ -0,0 +1,56 @@
import {
ERROR404_PATH,
isPathExists,
LOGIN_PATH,
removeQueryString,
routes,
} from "@/router";
import { useAuthStore } from "@/store/modules/auth";
// 白名单路由
const whiteList = ["/"];
routes.forEach((item) => {
if (item.needLogin !== true) {
whiteList.push(item.path);
}
});
/**
* 权限校验
* @param {String} path
* @returns
*/
export function hasPerm(path = "") {
if (!isPathExists(path)) {
uni.redirectTo({
url: ERROR404_PATH,
});
return false;
}
const authStore = useAuthStore();
// 在白名单中或有token,直接放行
const hasPermission =
whiteList.includes(removeQueryString(path)) || authStore.isLoggedIn;
if (!hasPermission) {
// 将用户的目标路径传递过去,这样可以实现用户登录之后,直接跳转到目标页面
uni.redirectTo({
url: `/pages/index/index`,
});
}
return hasPermission;
}
export function setupPermission() {
// 注意:拦截uni.switchTab本身没有问题。但是在微信小程序端点击tabbar的底层逻辑并不是触发uni.switchTab。
// 所以误认为拦截无效,此类场景的解决方案是在tabbar页面的页面生命周期onShow中处理。
["navigateTo", "redirectTo", "reLaunch", "switchTab"].forEach((item) => {
// https://uniapp.dcloud.net.cn/api/interceptor.html
uni.addInterceptor(item, {
// 页面跳转前进行拦截, invoke根据返回值进行判断是否继续执行跳转
invoke(args) {
// args为所拦截api中的参数,比如拦截的是uni.redirectTo(OBJECT),则args对应的是OBJECT参数
return hasPerm(args.url);
},
});
});
}

@ -0,0 +1,28 @@
import uvUI from "@/uni_modules/uv-ui-tools";
export function setupUI(app) {
// UV扩展配置 https://www.uvui.cn/components/setting.html
app.use(uvUI);
// 调用setConfig方法,方法内部会进行对象属性深度合并,可以放心嵌套配置
uni.$uv.setConfig({
// 修改$uv.config对象的属性
config: {
// 修改默认单位为rpx,相当于执行 uni.$uv.config.unit = 'rpx'
// 用于在传入组件大小参数为数值时,指定默认单位
unit: "px",
},
// 修改$uv.props对象的属性
props: {
// 修改uv-text组件的size参数的默认值,注意:默认值都要用default声明
text: {
color: {
default: "#303133",
},
},
// 其他组件属性配置,具体的参数名称可以去每个组件的props.js中进行查看
// ......
},
});
}
export const $uv = uni.$uv;

@ -0,0 +1,91 @@
import pagesJson from "@/pages.json";
// 路径常量
export const HOME_PATH = "/pages/index/index";
export const LOGIN_PATH = "/pages/login/login";
export const ERROR404_PATH = "/pages/404/404";
/**
* 解析路由地址
* @param {object} pagesJson
* @returns [{"path": "/pages/index/index","needLogin": false},...]
*/
function parseRoutes(pagesJson = {}) {
if (!pagesJson.pages) {
pagesJson.pages = [];
}
if (!pagesJson.subPackages) {
pagesJson.subPackages = [];
}
function parsePages(pages = [], rootPath = "") {
const routes = [];
for (let i = 0; i < pages.length; i++) {
routes.push({
path: rootPath ? `/${rootPath}/${pages[i].path}` : `/${pages[i].path}`,
needLogin: pages[i].needLogin === true,
});
}
return routes;
}
function parseSubPackages(subPackages = []) {
const routes = [];
for (let i = 0; i < subPackages.length; i++) {
routes.push(...parsePages(subPackages[i].pages, subPackages[i].root));
}
return routes;
}
return [
...parsePages(pagesJson.pages),
...parseSubPackages(pagesJson.subPackages),
];
}
export const routes = parseRoutes(pagesJson);
/**
* 当前路由
* @returns {String}
*/
export function currentRoute() {
const pages = getCurrentPages();
if (pages.length === 0) {
return;
}
const currentPage = pages[pages.length - 1];
return currentPage?.$page?.fullPath || currentPage.route;
}
/**
* 去除查询字符串
* @param {String} path
* @returns
*/
export function removeQueryString(path = "") {
return path.split("?")[0];
}
/**
* 路径是否存在
* @param {String} path
* @returns
*/
export function isPathExists(path = "") {
const cleanPath = removeQueryString(path);
return routes.some((item) => item.path === cleanPath);
}
/**
* 是否是tabbar页面路径
* @param {String} path
* @returns
*/
export function isTabBarPath(path = "") {
const cleanPath = removeQueryString(path);
return (
pagesJson.tabBar?.list?.some(
(item) => `/${item.pagePath}` === cleanPath
) === true
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 B

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

@ -0,0 +1,12 @@
import { createPinia } from "pinia";
const store = createPinia();
export function setupStore(app) {
app.use(store);
}
export { store };
export * from "./modules/user";
export * from "./modules/auth";

@ -0,0 +1,69 @@
import AuthAPI from "@/api/modules/auth";
import { store } from "@/store";
import { cache } from "@/utils/cache";
import { defineStore } from "pinia";
import { computed, ref, watch } from "vue";
export const useAuthStore = defineStore("auth", () => {
const ACCESS_TOKEN_KEY = "accessToken";
const REFRESH_TOKEN_KEY = "refreshToken";
const accessToken = ref(cache(ACCESS_TOKEN_KEY) ?? "");
const refreshToken = ref(cache(REFRESH_TOKEN_KEY) ?? "");
const isLoggedIn = computed(() => !!accessToken.value);
watch(accessToken, (newValue) => cache(ACCESS_TOKEN_KEY, newValue || null));
watch(refreshToken, (newValue) => cache(REFRESH_TOKEN_KEY, newValue || null));
// 注册
async function signUp(payload) {
const response = await AuthAPI.signUp(payload);
const prefix = response.tokenType ? `${response.tokenType} ` : "";
accessToken.value = prefix + response.accessToken;
refreshToken.value = prefix + response.refreshToken;
return response;
}
// 登录
async function signIn(payload) {
const response = await AuthAPI.signIn(payload);
const prefix = response.tokenType ? `${response.tokenType} ` : "";
accessToken.value = prefix + response.accessToken;
refreshToken.value = prefix + response.refreshToken;
return response;
}
// 登出
function signOut() {
accessToken.value = "";
refreshToken.value = "";
}
// 刷新token
async function refresh(payload) {
const response = await AuthAPI.refreshToken(payload);
const prefix = response.tokenType ? `${response.tokenType} ` : "";
accessToken.value = prefix + response.accessToken;
return response;
}
function setAccessToken(payload) {
accessToken.value = payload;
}
return {
accessToken,
refreshToken,
isLoggedIn,
signUp,
signIn,
signOut,
refresh,
setAccessToken
};
});
// 手动提供给 useStore() 函数 pinia 实例
// https://pinia.vuejs.org/zh/core-concepts/outside-component-usage.html#using-a-store-outside-of-a-component
export function useAuthStoreHook() {
return useAuthStore(store);
}

@ -0,0 +1,32 @@
import { defineStore } from "pinia";
import { ref, watch } from "vue";
import { cache } from "@/utils/cache";
export const useUserStore = defineStore("user", () => {
const userInfo = ref(cache('USERINFO_KEY') ? JSON.parse(cache('USERINFO_KEY')) : {
username: "",
});
const adressInfo = ref(cache('ADRESS_KEY') ? JSON.parse(cache('ADRESS_KEY')) : {
latitude: "",
longitude: "",
adress: "",
});
watch(userInfo, (newValue) => cache('USERINFO_KEY', newValue ? JSON.stringify(newValue) : null));
watch(adressInfo, (newValue) => cache('ADRESS_KEY', newValue ? JSON.stringify(newValue) : null));
function setUserInfo(payload) {
userInfo.value = payload;
}
function setAdressInfo(payload) {
adressInfo.value = payload;
}
return {
userInfo,
setUserInfo,
adressInfo,
setAdressInfo
};
});

@ -0,0 +1,33 @@
{
"compilerOptions": {
"allowJs": true,
"allowSyntheticDefaultImports": true,
"baseUrl": ".",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"importHelpers": true,
"jsx": "preserve",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment",
"jsxImportSource": "vue",
"lib": ["esnext", "dom"],
"module": "esnext",
"moduleResolution": "bundler",
"noEmit": true,
"noImplicitThis": true,
"noLib": false,
"paths": {
"@/*": ["./*"]
},
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"target": "esnext",
"types": ["@dcloudio/types"],
"useDefineForClassFields": true,
"verbatimModuleSyntax": true
},
"exclude": ["node_modules", "uni_modules", "unpackage", "**/*.js"],
"include": ["**/*.{ts,vue}"]
}

@ -0,0 +1,79 @@
/**
* 这里是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: #ffffff;
$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: 16px;
/* 图片尺寸 */
$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: #555555; // 二级标题颜色
$uni-font-size-subtitle: 26px;
$uni-color-paragraph: #3f536e; // 文章段落颜色
$uni-font-size-paragraph: 15px;
/* uv-ui主题 */
@import "@/uni_modules/uv-ui-tools/theme.scss";

@ -0,0 +1,7 @@
## 1.0.2(2023-07-02)
uv-action-sheet 由于弹出层uv-popup的修改,打开和关闭方法更改,详情参考文档:https://www.uvui.cn/components/actionSheet.html
## 1.0.1(2023-05-16)
1. 优化组件依赖,修改后无需全局引入,组件导入即可使用
2. 优化部分功能
## 1.0.0(2023-05-10)
uv-action-sheet 底部操作菜单

@ -0,0 +1,50 @@
export default {
props: {
// 标题,有值则显示,同时会显示关闭按钮
title: {
type: String,
default: ''
},
// 选项上方的描述信息
description: {
type: String,
default: ''
},
// 数据
actions: {
type: Array,
default: () => []
},
// 取消按钮的文字,不为空时显示按钮
cancelText: {
type: String,
default: ''
},
// 点击某个菜单项时是否关闭弹窗
closeOnClickAction: {
type: Boolean,
default: true
},
// 处理底部安全区(默认true)
safeAreaInsetBottom: {
type: Boolean,
default: true
},
// 小程序的打开方式
openType: {
type: String,
default: ''
},
// 点击遮罩是否允许关闭 (默认true)
closeOnClickOverlay: {
type: Boolean,
default: true
},
// 圆角值
round: {
type: [Boolean, String, Number],
default: 0
},
...uni.$uv?.props?.actionSheet
}
}

@ -0,0 +1,280 @@
<template>
<uv-popup
ref="popup"
mode="bottom"
:safeAreaInsetBottom="safeAreaInsetBottom"
:round="round"
:close-on-click-overlay="closeOnClickOverlay"
@change="popupChange"
>
<view class="uv-action-sheet">
<view
class="uv-action-sheet__header"
v-if="title"
>
<text class="uv-action-sheet__header__title uv-line-1">{{title}}</text>
<view
class="uv-action-sheet__header__icon-wrap"
@tap.stop="cancel"
>
<uv-icon
name="close"
size="17"
color="#c8c9cc"
bold
></uv-icon>
</view>
</view>
<text
class="uv-action-sheet__description"
:style="[{
marginTop: `${title && description ? 0 : '18px'}`
}]"
v-if="description"
>{{description}}</text>
<slot>
<uv-line v-if="description"></uv-line>
<view class="uv-action-sheet__item-wrap">
<view v-for="(item, index) in actions" :key="index">
<!-- #ifdef MP -->
<button
class="uv-reset-button"
:openType="item.openType"
@getuserinfo="onGetUserInfo"
@contact="onContact"
@getphonenumber="onGetPhoneNumber"
@error="onError"
@launchapp="onLaunchApp"
@opensetting="onOpenSetting"
:lang="lang"
:session-from="sessionFrom"
:send-message-title="sendMessageTitle"
:send-message-path="sendMessagePath"
:send-message-img="sendMessageImg"
:show-message-card="showMessageCard"
:app-parameter="appParameter"
@tap="selectHandler(index)"
:hover-class="!item.disabled && !item.loading ? 'uv-action-sheet--hover' : ''"
>
<!-- #endif -->
<view
class="uv-action-sheet__item-wrap__item"
@tap.stop="selectHandler(index)"
:hover-class="!item.disabled && !item.loading ? 'uv-action-sheet--hover' : ''"
:hover-stay-time="150"
>
<template v-if="!item.loading">
<text
class="uv-action-sheet__item-wrap__item__name"
:style="[itemStyle(index)]"
>{{ item.name }}</text>
<text
v-if="item.subname"
class="uv-action-sheet__item-wrap__item__subname"
>{{ item.subname }}</text>
</template>
<uv-loading-icon
v-else
custom-class="van-action-sheet__loading"
size="18"
mode="circle"
/>
</view>
<!-- #ifdef MP -->
</button>
<!-- #endif -->
<uv-line v-if="index !== actions.length - 1"></uv-line>
</view>
</view>
</slot>
<uv-gap
bgColor="#eaeaec"
height="6"
v-if="cancelText"
></uv-gap>
<view hover-class="uv-action-sheet--hover">
<text
@touchmove.stop.prevent
:hover-stay-time="150"
v-if="cancelText"
class="uv-action-sheet__cancel-text"
@tap="cancel"
>{{cancelText}}</text>
</view>
</view>
</uv-popup>
</template>
<script>
import mpMixin from '@/uni_modules/uv-ui-tools/libs/mixin/mpMixin.js'
import mixin from '@/uni_modules/uv-ui-tools/libs/mixin/mixin.js'
import button from '@/uni_modules/uv-ui-tools/libs/mixin/button.js'
import openType from '@/uni_modules/uv-ui-tools/libs/mixin/openType.js'
import props from './props.js';
/**
* ActionSheet 操作菜单
* @description 本组件用于从底部弹出一个操作菜单,供用户选择并返回结果。本组件功能类似于uni的uni.showActionSheetAPI,配置更加灵活,所有平台都表现一致。
* @tutorial https://www.uvui.cn/components/actionSheet.html
* @property {Boolean} show 操作菜单是否展示 (默认 false )
* @property {String} title 操作菜单标题
* @property {String} description 选项上方的描述信息
* @property {Array<Object>} actions 按钮的文字数组,见官方文档示例
* @property {String} cancelText 取消按钮的提示文字,不为空时显示按钮
* @property {Boolean} closeOnClickAction 点击某个菜单项时是否关闭弹窗 (默认 true )
* @property {Boolean} safeAreaInsetBottom 处理底部安全区 (默认 true )
* @property {String} openType 小程序的打开方式 (contact | launchApp | getUserInfo | openSetting |getPhoneNumber |error )
* @property {Boolean} closeOnClickOverlay 点击遮罩是否允许关闭 (默认 true )
* @property {String} lang 指定返回用户信息的语言,zh_CN 简体中文,zh_TW 繁体中文,en 英文
* @property {String} sessionFrom 会话来源,openType="contact"时有效
* @property {String} sendMessageTitle 会话内消息卡片标题,openType="contact"时有效
* @property {String} sendMessagePath 会话内消息卡片点击跳转小程序路径,openType="contact"时有效
* @property {String} sendMessageImg 会话内消息卡片图片,openType="contact"时有效
* @property {Boolean} showMessageCard 是否显示会话内消息卡片,设置此参数为 true,用户进入客服会话会在右下角显示"可能要发送的小程序"提示,用户点击后可以快速发送小程序消息,openType="contact"时有效 (默认 false )
* @property {String} appParameter 打开 APP 时,向 APP 传递的参数,openType=launchApp 时有效
*
* @event {Function} select 点击ActionSheet列表项时触发
* @event {Function} close 点击取消按钮时触发
* @event {Function} getuserinfo 用户点击该按钮时,会返回获取到的用户信息,回调的 detail 数据与 wx.getUserInfo 返回的一致,openType="getUserInfo"时有效
* @event {Function} contact 客服消息回调,openType="contact"时有效
* @event {Function} getphonenumber 获取用户手机号回调,openType="getPhoneNumber"时有效
* @event {Function} error 当使用开放能力时,发生错误的回调,openType="error"时有效
* @event {Function} launchapp 打开 APP 成功的回调,openType="launchApp"时有效
* @event {Function} opensetting 在打开授权设置页后回调,openType="openSetting"时有效
* @example <uv-action-sheet ref="actionSheet" :actions="list" :title="title" ></uv-action-sheet>
*/
export default {
name: "uv-action-sheet",
mixins: [openType, button, mpMixin , mixin, props],
emits: ['close', 'select'],
computed: {
// 操作项目的样式
itemStyle() {
return (index) => {
let style = {};
if (this.actions[index].color) style.color = this.actions[index].color
if (this.actions[index].fontSize) style.fontSize = this.$uv.addUnit(this.actions[index].fontSize)
// 选项被禁用的样式
if (this.actions[index].disabled) style.color = '#c0c4cc'
return style;
}
},
},
methods: {
open() {
this.$refs.popup.open();
},
close() {
this.$refs.popup.close();
},
popupChange(e) {
if(!e.show) this.$emit('close');
},
// 点击取消按钮
cancel() {
this.close();
},
selectHandler(index) {
const item = this.actions[index]
if (item && !item.disabled && !item.loading) {
this.$emit('select', item)
if (this.closeOnClickAction) {
this.close();
}
}
},
}
}
</script>
<style lang="scss" scoped>
$show-lines: 1;
$show-reset-button: 1;
@import '@/uni_modules/uv-ui-tools/libs/css/variable.scss';
@import '@/uni_modules/uv-ui-tools/libs/css/components.scss';
@import '@/uni_modules/uv-ui-tools/libs/css/color.scss';
$uv-action-sheet-reset-button-width:100% !default;
$uv-action-sheet-title-font-size: 16px !default;
$uv-action-sheet-title-padding: 12px 30px !default;
$uv-action-sheet-title-color: $uv-main-color !default;
$uv-action-sheet-header-icon-wrap-right:15px !default;
$uv-action-sheet-header-icon-wrap-top:15px !default;
$uv-action-sheet-description-font-size:13px !default;
$uv-action-sheet-description-color:14px !default;
$uv-action-sheet-description-margin: 18px 15px !default;
$uv-action-sheet-item-wrap-item-padding:15px !default;
$uv-action-sheet-item-wrap-name-font-size:16px !default;
$uv-action-sheet-item-wrap-subname-font-size:13px !default;
$uv-action-sheet-item-wrap-subname-color: #c0c4cc !default;
$uv-action-sheet-item-wrap-subname-margin-top:10px !default;
$uv-action-sheet-cancel-text-font-size:16px !default;
$uv-action-sheet-cancel-text-color:$uv-content-color !default;
$uv-action-sheet-cancel-text-font-size:15px !default;
$uv-action-sheet-cancel-text-hover-background-color:rgb(242, 243, 245) !default;
.uv-reset-button {
width: $uv-action-sheet-reset-button-width;
}
.uv-action-sheet {
text-align: center;
&__header {
position: relative;
padding: $uv-action-sheet-title-padding;
&__title {
font-size: $uv-action-sheet-title-font-size;
color: $uv-action-sheet-title-color;
font-weight: bold;
text-align: center;
}
&__icon-wrap {
position: absolute;
right: $uv-action-sheet-header-icon-wrap-right;
top: $uv-action-sheet-header-icon-wrap-top;
}
}
&__description {
font-size: $uv-action-sheet-description-font-size;
color: $uv-tips-color;
margin: $uv-action-sheet-description-margin;
text-align: center;
}
&__item-wrap {
&__item {
padding: $uv-action-sheet-item-wrap-item-padding;
@include flex;
align-items: center;
justify-content: center;
flex-direction: column;
&__name {
font-size: $uv-action-sheet-item-wrap-name-font-size;
color: $uv-main-color;
text-align: center;
}
&__subname {
font-size: $uv-action-sheet-item-wrap-subname-font-size;
color: $uv-action-sheet-item-wrap-subname-color;
margin-top: $uv-action-sheet-item-wrap-subname-margin-top;
text-align: center;
}
}
}
&__cancel-text {
font-size: $uv-action-sheet-cancel-text-font-size;
color: $uv-action-sheet-cancel-text-color;
text-align: center;
padding: $uv-action-sheet-cancel-text-font-size;
}
&--hover {
background-color: $uv-action-sheet-cancel-text-hover-background-color;
}
}
</style>

@ -0,0 +1,92 @@
{
"id": "uv-action-sheet",
"displayName": "uv-action-sheet 底部操作菜单 全面兼容小程序、nvue、vue2、vue3等多端",
"version": "1.0.2",
"description": "该组件用于从底部弹出一个操作菜单,供用户选择并返回结果。本组件功能类似于uni的uni.showActionSheet API,配置更加灵活,所有平台都表现一致。",
"keywords": [
"action-sheet",
"uvui",
"uv-ui",
"操作菜单",
"菜单选择"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [
"uv-ui-tools",
"uv-popup",
"uv-icon",
"uv-line",
"uv-loading-icon",
"uv-gap"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"Vue": {
"vue2": "y",
"vue3": "y"
},
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

@ -0,0 +1,13 @@
## ActionSheet 操作菜单
> **组件名:uv-action-sheet**
本组件用于从底部弹出一个操作菜单,供用户选择并返回结果。
本组件功能类似于uni的uni.showActionSheet API,配置更加灵活,所有平台都表现一致。
### <a href="https://www.uvui.cn/components/actionSheet.html" target="_blank">查看文档</a>
### [完整示例项目下载 | 关注更多组件](https://ext.dcloud.net.cn/plugin?name=uv-ui)
#### 如使用过程中有任何问题,或者您对uv-ui有一些好的建议,欢迎加入 uv-ui 交流群:<a href="https://ext.dcloud.net.cn/plugin?id=12287" target="_blank">uv-ui</a>、<a href="https://www.uvui.cn/components/addQQGroup.html" target="_blank">官方QQ群</a>

@ -0,0 +1,10 @@
## 1.0.4(2023-12-06)
1. 阻止事件冒泡处理
## 1.0.3(2023-10-23)
1. 修复报错的BUG
## 1.0.2(2023-10-23)
1. 修复设置singleSize、multipleSize、space等值带单位,存在不显示的BUG
## 1.0.1(2023-09-13)
1. 添加依赖
## 1.0.0(2023-08-30)
1. 新增uv-album相册组件

@ -0,0 +1,312 @@
<template>
<view class="uv-album">
<view
class="uv-album__row"
ref="uv-album__row"
v-for="(arr, index) in showUrls"
:forComputedUse="albumWidth"
:key="index"
>
<view
class="uv-album__row__wrapper"
v-for="(item, index1) in arr"
:key="index1"
:style="[imageStyle(index + 1, index1 + 1)]"
@tap.stop="previewFullImage ? onPreviewTap(getSrc(item)) : ''"
>
<image
:src="getSrc(item)"
:mode="
urls.length === 1
? imageHeight > 0
? singleMode
: 'widthFix'
: multipleMode
"
:style="[
{
width: imageWidth,
height: imageHeight
}
]"
></image>
<view
v-if="
showMore &&
urls.length > rowCount * showUrls.length &&
index === showUrls.length - 1 &&
index1 === showUrls[showUrls.length - 1].length - 1
"
class="uv-album__row__wrapper__text"
>
<uv-text
:text="`+${urls.length - maxCount}`"
color="#fff"
:size="$uv.getPx(multipleSize) * 0.3"
align="center"
customStyle="justify-content: center"
></uv-text>
</view>
</view>
</view>
</view>
</template>
<script>
import mpMixin from '@/uni_modules/uv-ui-tools/libs/mixin/mpMixin.js'
import mixin from '@/uni_modules/uv-ui-tools/libs/mixin/mixin.js'
// #ifdef APP-NVUE
// 由于weex为阿里的KPI业绩考核的产物,所以不支持百分比单位,这里需要通过dom查询组件的宽度
const dom = uni.requireNativePlugin('dom')
// #endif
/**
* Album 相册
* @description 本组件提供一个类似相册的功能,让开发者开发起来更加得心应手。减少重复的模板代码
* @tutorial https://www.uvui.cn/components/album.html
* @property {Array} urls 图片地址列表 Array<String>|Array<Object>形式
* @property {String} keyName 指定从数组的对象元素中读取哪个属性作为图片地址
* @property {String | Number} singleSize 单图时,图片长边的长度 (默认 180 )
* @property {String | Number} multipleSize 多图时,图片边长 (默认 70 )
* @property {String | Number} space 多图时,图片水平和垂直之间的间隔 (默认 6 )
* @property {String} singleMode 单图时,图片缩放裁剪的模式 (默认 'scaleToFill' )
* @property {String} multipleMode 多图时,图片缩放裁剪的模式 (默认 'aspectFill' )
* @property {String | Number} maxCount 取消按钮的提示文字 (默认 9 )
* @property {Boolean} previewFullImage 是否可以预览图片 (默认 true )
* @property {String | Number} rowCount 每行展示图片数量,如设置,singleSize和multipleSize将会无效 (默认 3 )
* @property {Boolean} showMore 超出maxCount时是否显示查看更多的提示 (默认 true )
*
* @event {Function} albumWidth 某些特殊的情况下,需要让文字与相册的宽度相等,这里事件的形式对外发送 (回调参数 width )
* @example <uv-album :urls="urls2" @albumWidth="width => albumWidth = width" multipleSize="68" ></uv-album>
*/
export default {
name: 'uv-album',
mixins: [mpMixin, mixin],
emits: ['albumWidth'],
props: {
// 图片地址,Array<String>|Array<Object>形式
urls: {
type: Array,
default: () => []
},
// 指定从数组的对象元素中读取哪个属性作为图片地址
keyName: {
type: String,
default: ''
},
// 单图时,图片长边的长度
singleSize: {
type: [String, Number],
default: 180
},
// 多图时,图片边长
multipleSize: {
type: [String, Number],
default: 70
},
// 多图时,图片水平和垂直之间的间隔
space: {
type: [String, Number],
default: 6
},
// 单图时,图片缩放裁剪的模式
singleMode: {
type: String,
default: 'scaleToFill'
},
// 多图时,图片缩放裁剪的模式
multipleMode: {
type: String,
default: 'aspectFill'
},
// 最多展示的图片数量,超出时最后一个位置将会显示剩余图片数量
maxCount: {
type: [String, Number],
default: 9
},
// 是否可以预览图片
previewFullImage: {
type: Boolean,
default: true
},
// 每行展示图片数量,如设置,singleSize和multipleSize将会无效
rowCount: {
type: [String, Number],
default: 3
},
// 超出maxCount时是否显示查看更多的提示
showMore: {
type: Boolean,
default: true
},
...uni.$uv?.props?.album
},
data() {
return {
// 单图的宽度
singleWidth: 0,
// 单图的高度
singleHeight: 0,
// 单图时,如果无法获取图片的尺寸信息,让图片宽度默认为容器的一定百分比
singlePercent: 0.6
}
},
watch: {
urls: {
immediate: true,
handler(newVal) {
if (newVal.length === 1) {
this.getImageRect()
}
}
}
},
computed: {
imageStyle() {
return (index1, index2) => {
const { space, rowCount, multipleSize, urls } = this;
const rowLen = this.showUrls.length;
const allLen = this.urls.length;
const style = {
marginRight: this.$uv.addUnit(space),
marginBottom: this.$uv.addUnit(space)
}
// 如果为最后一行,则每个图片都无需下边框
if (index1 === rowLen) style.marginBottom = 0
// 每行的最右边一张和总长度的最后一张无需右边框
if (
index2 === rowCount ||
(index1 === rowLen &&
index2 === this.showUrls[index1 - 1].length)
)
style.marginRight = 0
return style
}
},
// 将数组划分为二维数组
showUrls() {
const arr = []
this.urls.map((item, index) => {
// 限制最大展示数量
if (index + 1 <= this.maxCount) {
// 计算该元素为第几个素组内
const itemIndex = Math.floor(index / this.rowCount)
// 判断对应的索引是否存在
if (!arr[itemIndex]) {
arr[itemIndex] = []
}
arr[itemIndex].push(item)
}
})
return arr
},
imageWidth() {
return this.$uv.addUnit(
this.urls.length === 1 ? this.singleWidth : this.multipleSize
)
},
imageHeight() {
return this.$uv.addUnit(
this.urls.length === 1 ? this.singleHeight : this.multipleSize
)
},
// 此变量无实际用途,仅仅是为了利用computed特性,让其在urls长度等变化时,重新计算图片的宽度
// 因为用户在某些特殊的情况下,需要让文字与相册的宽度相等,所以这里事件的形式对外发送
albumWidth() {
let width = 0
if (this.urls.length === 1) {
width = this.singleWidth
} else {
width =
this.showUrls[0].length * this.$uv.getPx(this.multipleSize) +
this.$uv.getPx(this.space) * (this.showUrls[0].length - 1)
}
this.$emit('albumWidth', width)
return width
}
},
methods: {
// 预览图片
onPreviewTap(url) {
const urls = this.urls.map((item) => {
return this.getSrc(item)
})
uni.previewImage({
current: url,
urls
})
},
// 获取图片的路径
getSrc(item) {
return this.$uv.test.object(item) ?
(this.keyName && item[this.keyName]) || item.src :
item
},
// 单图时,获取图片的尺寸
// 在小程序中,需要将网络图片的的域名添加到小程序的download域名才可能获取尺寸
// 在没有添加的情况下,让单图宽度默认为盒子的一定宽度(singlePercent)
getImageRect() {
const src = this.getSrc(this.urls[0])
uni.getImageInfo({
src,
success: (res) => {
// 判断图片横向还是竖向展示方式
const isHorizotal = res.width >= res.height
this.singleWidth = isHorizotal ?
this.singleSize :
(res.width / res.height) * this.$uv.getPx(this.singleSize)
this.singleHeight = !isHorizotal ?
this.singleSize :
(res.height / res.width) * this.singleWidth
},
fail: () => {
this.getComponentWidth()
}
})
},
// 获取组件的宽度
async getComponentWidth() {
// 延时一定时间,以获取dom尺寸
await this.$uv.sleep(30)
// #ifndef APP-NVUE
this.$uGetRect('.uv-album__row').then((size) => {
this.singleWidth = size.width * this.singlePercent
})
// #endif
// #ifdef APP-NVUE
// 这里ref="uv-album__row"所在的标签为通过for循环出来,导致this.$refs['uv-album__row']是一个数组
const ref = this.$refs['uv-album__row'][0]
ref &&
dom.getComponentRect(ref, (res) => {
this.singleWidth = res.size.width * this.singlePercent
})
// #endif
}
}
}
</script>
<style lang="scss" scoped>
@import '@/uni_modules/uv-ui-tools/libs/css/components.scss';
.uv-album {
@include flex(column);
&__row {
@include flex(row);
flex-wrap: wrap;
&__wrapper {
position: relative;
&__text {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.3);
@include flex(row);
justify-content: center;
align-items: center;
}
}
}
}
</style>

@ -0,0 +1,88 @@
{
"id": "uv-album",
"displayName": "uv-album 相册 全面兼容vue3+2、app、h5、小程序等多端",
"version": "1.0.4",
"description": "本组件提供一个类似相册的功能,让开发者开发起来更加得心应手,功能齐全,灵活配置可以,开箱即用。减少重复的模板代码",
"keywords": [
"album",
"uv-ui",
"uvui",
"相册",
"图片"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [
"uv-ui-tools",
"uv-text"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"Vue": {
"vue2": "y",
"vue3": "y"
},
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

@ -0,0 +1,21 @@
# Album 相册
> **组件名:uv-album**
本组件提供一个类似相册的功能,让开发者开发起来更加得心应手。
功能齐全,灵活配置可以,开箱即用。减少重复的模板代码。
# <a href="https://www.uvui.cn/components/album.html" target="_blank">查看文档</a>
## [下载完整示例项目](https://ext.dcloud.net.cn/plugin?name=uv-ui) <small>(请不要 下载插件ZIP)</small>
### [更多插件,请关注uv-ui组件库](https://ext.dcloud.net.cn/plugin?name=uv-ui)
<a href="https://ext.dcloud.net.cn/plugin?name=uv-ui" target="_blank">
![image](https://mp-a667b617-c5f1-4a2d-9a54-683a67cff588.cdn.bspapp.com/uv-ui/banner.png)
</a>
#### 如使用过程中有任何问题反馈,或者您对uv-ui有一些好的建议,欢迎加入uv-ui官方交流群:<a href="https://www.uvui.cn/components/addQQGroup.html" target="_blank">官方QQ群</a>

@ -0,0 +1,7 @@
## 1.0.2(2023-06-01)
1. 修复点击触发两次实践的BUG
## 1.0.1(2023-05-16)
1. 优化组件依赖,修改后无需全局引入,组件导入即可使用
2. 优化部分功能
## 1.0.0(2023-05-10)
uv-alert 警告提示组件

@ -0,0 +1,45 @@
export default {
props: {
// 显示文字
title: {
type: String,
default: ''
},
// 主题,success/warning/info/error
type: {
type: String,
default: 'warning'
},
// 辅助性文字
description: {
type: String,
default: ''
},
// 是否可关闭
closable: {
type: Boolean,
default: false
},
// 是否显示图标
showIcon: {
type: Boolean,
default: false
},
// 浅或深色调,light-浅色,dark-深色
effect: {
type: String,
default: 'light'
},
// 文字是否居中
center: {
type: Boolean,
default: false
},
// 字体大小
fontSize: {
type: [String, Number],
default: 14
},
...uni.$uv?.props?.alert
}
}

@ -0,0 +1,246 @@
<template>
<uv-transition
mode="fade"
:show="show"
>
<view
class="uv-alert"
:class="[`uv-alert--${type}--${effect}`]"
@tap.stop="clickHandler"
:style="[$uv.addStyle(customStyle)]"
>
<view
class="uv-alert__icon"
v-if="showIcon"
>
<uv-icon
:name="iconName"
size="18"
:color="iconColor"
></uv-icon>
</view>
<view
class="uv-alert__content"
:style="[{
paddingRight: closable ? '20px' : 0
}]"
>
<text
class="uv-alert__content__title"
v-if="title"
:style="[{
fontSize: $uv.addUnit(fontSize),
textAlign: center ? 'center' : 'left'
}]"
:class="[effect === 'dark' ? 'uv-alert__text--dark' : `uv-alert__text--${type}--light`]"
>{{ title }}</text>
<text
class="uv-alert__content__desc"
v-if="description"
:style="[{
fontSize: $uv.addUnit(fontSize),
textAlign: center ? 'center' : 'left'
}]"
:class="[effect === 'dark' ? 'uv-alert__text--dark' : `uv-alert__text--${type}--light`]"
>{{ description }}</text>
</view>
<view
class="uv-alert__close"
v-if="closable"
@tap.stop="closeHandler"
>
<uv-icon
name="close"
:color="iconColor"
size="15"
></uv-icon>
</view>
</view>
</uv-transition>
</template>
<script>
import mpMixin from '@/uni_modules/uv-ui-tools/libs/mixin/mpMixin.js'
import mixin from '@/uni_modules/uv-ui-tools/libs/mixin/mixin.js'
import props from './props.js';
/**
* Alert 警告提示
* @description 警告提示,展现需要关注的信息。
* @tutorial https://www.uvui.cn/components/alertTips.html
*
* @property {String} title 显示的文字
* @property {String} type 使用预设的颜色 (默认 'warning' )
* @property {String} description 辅助性文字,颜色比title浅一点,字号也小一点,可选
* @property {Boolean} closable 关闭按钮(默认为叉号icon图标) (默认 false )
* @property {Boolean} showIcon 是否显示左边的辅助图标 ( 默认 false )
* @property {String} effect 多图时,图片缩放裁剪的模式 (默认 'light' )
* @property {Boolean} center 文字是否居中 (默认 false )
* @property {String | Number} fontSize 字体大小 (默认 14 )
* @property {Object} customStyle 定义需要用到的外部样式
* @event {Function} click 点击组件时触发
* @example <uv-alert :title="title" type = "warning" :closable="closable" :description = "description"></uv-alert>
*/
export default {
name: 'uv-alert',
mixins: [mpMixin, mixin, props],
emits: ['click'],
data() {
return {
show: true
}
},
computed: {
iconColor() {
return this.effect === 'light' ? this.type : '#fff'
},
// 不同主题对应不同的图标
iconName() {
switch (this.type) {
case 'success':
return 'checkmark-circle-fill';
break;
case 'error':
return 'close-circle-fill';
break;
case 'warning':
return 'error-circle-fill';
break;
case 'info':
return 'info-circle-fill';
break;
case 'primary':
return 'more-circle-fill';
break;
default:
return 'error-circle-fill';
}
}
},
methods: {
// 点击内容
clickHandler() {
this.$emit('click')
},
// 点击关闭按钮
closeHandler() {
this.show = false
}
}
}
</script>
<style lang="scss" scoped>
@import '@/uni_modules/uv-ui-tools/libs/css/components.scss';
@import '@/uni_modules/uv-ui-tools/libs/css/color.scss';
.uv-alert {
position: relative;
background-color: $uv-primary;
padding: 8px 10px;
@include flex(row);
align-items: center;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
&--primary--dark {
background-color: $uv-primary;
}
&--primary--light {
background-color: #ecf5ff;
}
&--error--dark {
background-color: $uv-error;
}
&--error--light {
background-color: #FEF0F0;
}
&--success--dark {
background-color: $uv-success;
}
&--success--light {
background-color: #f5fff0;
}
&--warning--dark {
background-color: $uv-warning;
}
&--warning--light {
background-color: #FDF6EC;
}
&--info--dark {
background-color: $uv-info;
}
&--info--light {
background-color: #f4f4f5;
}
&__icon {
margin-right: 5px;
}
&__content {
@include flex(column);
flex: 1;
&__title {
color: $uv-main-color;
font-size: 14px;
font-weight: bold;
color: #fff;
margin-bottom: 2px;
}
&__desc {
color: $uv-main-color;
font-size: 14px;
flex-wrap: wrap;
color: #fff;
}
}
&__title--dark,
&__desc--dark {
color: #FFFFFF;
}
&__text--primary--light,
&__text--primary--light {
color: $uv-primary;
}
&__text--success--light,
&__text--success--light {
color: $uv-success;
}
&__text--warning--light,
&__text--warning--light {
color: $uv-warning;
}
&__text--error--light,
&__text--error--light {
color: $uv-error;
}
&__text--info--light,
&__text--info--light {
color: $uv-info;
}
&__close {
position: absolute;
top: 11px;
right: 10px;
}
}
</style>

@ -0,0 +1,88 @@
{
"id": "uv-alert",
"displayName": "uv-alert 警告提示 全面兼容小程序、nvue、vue2、vue3等多端",
"version": "1.0.2",
"description": "uv-alert 警告提示,展现需要关注的信息。灵活配置,功能齐全,兼容全端",
"keywords": [
"alert",
"uvui",
"uv-ui",
"警告提示"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [
"uv-ui-tools",
"uv-transition",
"uv-icon"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"Vue": {
"vue2": "y",
"vue3": "y"
},
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

@ -0,0 +1,15 @@
## Alert 警告提示
> **组件名:uv-alert**
警告提示,展现需要关注的信息。
当某个页面需要向用户显示警告的信息时。
非浮层的静态展现形式,始终展现,不会自动消失,用户可以点击关闭。
### <a href="https://www.uvui.cn/components/alert.html" target="_blank">查看文档</a>
### [完整示例项目下载 | 关注更多组件](https://ext.dcloud.net.cn/plugin?name=uv-ui)
#### 如使用过程中有任何问题,或者您对uv-ui有一些好的建议,欢迎加入 uv-ui 交流群:<a href="https://ext.dcloud.net.cn/plugin?id=12287" target="_blank">uv-ui</a>、<a href="https://www.uvui.cn/components/addQQGroup.html" target="_blank">官方QQ群</a>

@ -0,0 +1,13 @@
## 1.0.5(2023-12-06)
1. 优化
## 1.0.4(2023-12-06)
1. 优化
## 1.0.3(2023-12-06)
1. 阻止事件冒泡处理,单个头像模式
## 1.0.2(2023-12-06)
1. 阻止事件冒泡处理
## 1.0.1(2023-05-16)
1. 优化组件依赖,修改后无需全局引入,组件导入即可使用
2. 优化部分功能
## 1.0.0(2023-05-10)
uv-avatar 头像组件

@ -0,0 +1,53 @@
export default {
props: {
// 头像图片组
urls: {
type: Array,
default: () => []
},
// 最多展示的头像数量
maxCount: {
type: [String, Number],
default: 5
},
// 头像形状
shape: {
type: String,
default: 'circle'
},
// 图片裁剪模式
mode: {
type: String,
default: 'scaleToFill'
},
// 超出maxCount时是否显示查看更多的提示
showMore: {
type: Boolean,
default: true
},
// 头像大小
size: {
type: [String, Number],
default: 40
},
// 指定从数组的对象元素中读取哪个属性作为图片地址
keyName: {
type: String,
default: ''
},
// 头像之间的遮挡比例
gap: {
type: [String, Number],
validator(value) {
return value >= 0 && value <= 1
},
default: 0.5
},
// 需额外显示的值
extraValue: {
type: [Number, String],
default: 0
},
...uni.$uv?.props?.avatarGroup
}
}

@ -0,0 +1,106 @@
<template>
<view class="uv-avatar-group">
<view
class="uv-avatar-group__item"
v-for="(item, index) in showUrl"
:key="index"
:style="{
marginLeft: index === 0 ? 0 : $uv.addUnit(-size * gap)
}"
>
<uv-avatar
:size="size"
:shape="shape"
:mode="mode"
:src="$uv.test.object(item) ? keyName && item[keyName] || item.url : item"
></uv-avatar>
<view
class="uv-avatar-group__item__show-more"
v-if="showMore && index === showUrl.length - 1 && (urls.length > maxCount || extraValue > 0)"
@tap="clickHandler"
>
<uv-text
color="#ffffff"
:size="size * 0.4"
:text="`+${extraValue || urls.length - showUrl.length}`"
align="center"
customStyle="justify-content: center"
></uv-text>
</view>
</view>
</view>
</template>
<script>
import mpMixin from '@/uni_modules/uv-ui-tools/libs/mixin/mpMixin.js'
import mixin from '@/uni_modules/uv-ui-tools/libs/mixin/mixin.js'
import props from './props.js';
/**
* AvatarGroup 头像组
* @description 本组件一般用于展示头像的地方,如个人中心,或者评论列表页的用户头像展示等场所。
* @tutorial https://www.uvui.cn/components/avatar.html
*
* @property {Array} urls 头像图片组 (默认 [] )
* @property {String | Number} maxCount 最多展示的头像数量 ( 默认 5 )
* @property {String} shape 头像形状( 'circle' (默认) | 'square' )
* @property {String} mode 图片裁剪模式(默认 'scaleToFill' )
* @property {Boolean} showMore 超出maxCount时是否显示查看更多的提示 (默认 true )
* @property {String | Number} size 头像大小 (默认 40 )
* @property {String} keyName 指定从数组的对象元素中读取哪个属性作为图片地址
* @property {String | Number} gap 头像之间的遮挡比例(0.4代表遮挡40%) (默认 0.5 )
* @property {String | Number} extraValue 需额外显示的值
* @event {Function} showMore 头像组更多点击
* @example <uv-avatar-group:urls="urls" size="35" gap="0.4" ></uv-avatar-group:urls=>
*/
export default {
name: 'uv-avatar-group',
mixins: [mpMixin, mixin, props],
data() {
return {
}
},
computed: {
showUrl() {
return this.urls.slice(0, this.maxCount)
}
},
methods: {
clickHandler() {
this.$emit('showMore')
}
},
}
</script>
<style lang="scss" scoped>
@import '@/uni_modules/uv-ui-tools/libs/css/components.scss';
@import '@/uni_modules/uv-ui-tools/libs/css/color.scss';
.uv-avatar-group {
@include flex;
&__item {
margin-left: -10px;
position: relative;
&--no-indent {
// 如果你想质疑作者不会使用:first-child,说明你太年轻,因为nvue不支持
margin-left: 0;
}
&__show-more {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.3);
@include flex;
align-items: center;
justify-content: center;
border-radius: 100px;
}
}
}
</style>

@ -0,0 +1,80 @@
import { range } from '@/uni_modules/uv-ui-tools/libs/function/test.js'
export default {
props: {
// 头像图片路径(不能为相对路径)
src: {
type: String,
default: ''
},
// 头像形状,circle-圆形,square-方形
shape: {
type: String,
default: 'circle'
},
// 头像尺寸
size: {
type: [String, Number],
default: 40
},
// 裁剪模式
mode: {
type: String,
default: 'scaleToFill'
},
// 显示的文字
text: {
type: String,
default: ''
},
// 背景色
bgColor: {
type: String,
default: '#c0c4cc'
},
// 文字颜色
color: {
type: String,
default: '#fff'
},
// 文字大小
fontSize: {
type: [String, Number],
default: 18
},
// 显示的图标
icon: {
type: String,
default: ''
},
// 显示小程序头像,只对百度,微信,QQ小程序有效
mpAvatar: {
type: Boolean,
default: false
},
// 是否使用随机背景色
randomBgColor: {
type: Boolean,
default: false
},
// 加载失败的默认头像(组件有内置默认图片)
defaultUrl: {
type: String,
default: ''
},
// 如果配置了randomBgColor为true,且配置了此值,则从默认的背景色数组中取出对应索引的颜色值,取值0-19之间
colorIndex: {
type: [String, Number],
// 校验参数规则,索引在0-19之间
validator(n) {
return range(n, [0, 19]) || n === ''
},
default: ''
},
// 组件标识符
name: {
type: String,
default: ''
},
...uni.$uv?.props?.avatar
}
}

@ -0,0 +1,175 @@
<template>
<view
class="uv-avatar"
:class="[`uv-avatar--${shape}`]"
:style="[{
backgroundColor: (text || icon) ? (randomBgColor ? colors[colorIndex !== '' ? colorIndex : $uv.random(0, 19)] : bgColor) : 'transparent',
width: $uv.addUnit(size),
height: $uv.addUnit(size),
}, $uv.addStyle(customStyle)]"
@tap="clickHandler"
>
<slot>
<!-- #ifdef MP-WEIXIN || MP-QQ || MP-BAIDU -->
<open-data
v-if="mpAvatar && allowMp"
type="userAvatarUrl"
:style="[{
width: $uv.addUnit(size),
height: $uv.addUnit(size)
}]"
/>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN && MP-QQ && MP-BAIDU -->
<template v-if="mpAvatar && allowMp"></template>
<!-- #endif -->
<uv-icon
v-else-if="icon"
:name="icon"
:size="fontSize"
:color="color"
></uv-icon>
<uv-text
v-else-if="text"
:text="text"
:size="fontSize"
:color="color"
align="center"
customStyle="justify-content: center"
></uv-text>
<image
class="uv-avatar__image"
v-else
:class="[`uv-avatar__image--${shape}`]"
:src="avatarUrl || defaultUrl"
:mode="mode"
@error="errorHandler"
:style="[{
width: $uv.addUnit(size),
height: $uv.addUnit(size)
}]"
></image>
</slot>
</view>
</template>
<script>
import mpMixin from '@/uni_modules/uv-ui-tools/libs/mixin/mpMixin.js'
import mixin from '@/uni_modules/uv-ui-tools/libs/mixin/mixin.js'
import props from './props.js';
const base64Avatar =
"data:image/jpg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAA8AAD/4QMraHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjMtYzAxMSA2Ni4xNDU2NjEsIDIwMTIvMDIvMDYtMTQ6NTY6MjcgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDUzYgKFdpbmRvd3MpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjREMEQwRkY0RjgwNDExRUE5OTY2RDgxODY3NkJFODMxIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjREMEQwRkY1RjgwNDExRUE5OTY2RDgxODY3NkJFODMxIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NEQwRDBGRjJGODA0MTFFQTk5NjZEODE4Njc2QkU4MzEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NEQwRDBGRjNGODA0MTFFQTk5NjZEODE4Njc2QkU4MzEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7/7gAOQWRvYmUAZMAAAAAB/9sAhAAGBAQEBQQGBQUGCQYFBgkLCAYGCAsMCgoLCgoMEAwMDAwMDBAMDg8QDw4MExMUFBMTHBsbGxwfHx8fHx8fHx8fAQcHBw0MDRgQEBgaFREVGh8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx//wAARCADIAMgDAREAAhEBAxEB/8QAcQABAQEAAwEBAAAAAAAAAAAAAAUEAQMGAgcBAQAAAAAAAAAAAAAAAAAAAAAQAAIBAwICBgkDBQAAAAAAAAABAhEDBCEFMVFBYXGREiKBscHRMkJSEyOh4XLxYjNDFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A/fAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHbHFyZ/Dam+yLA+Z2L0Pjtyj2poD4AAAAAAAAAAAAAAAAAAAAAAAAKWFs9y6lcvvwQeqj8z9wFaziY1n/HbUX9XF97A7QAGXI23EvJ1goyfzR0YEfN269jeZ+a03pNe0DIAAAAAAAAAAAAAAAAAAAACvtO3RcVkXlWutuL9YFYAAAAAOJRjKLjJVi9GmB5/csH/mu1h/in8PU+QGMAAAAAAAAAAAAAAAAAAaMDG/6MmMH8C80+xAelSSVFolwQAAAAAAAHVlWI37ErUulaPk+hgeYnCUJuElSUXRrrQHAAAAAAAAAAAAAAAAABa2Oz4bM7r4zdF2ICmAAAAAAAAAg7zZ8GX41wuJP0rRgYAAAAAAAAAAAAAAAAAD0m2R8ODaXU33tsDSAAAAAAAAAlb9HyWZcnJd9PcBHAAAAAAAAAAAAAAAAAPS7e64Vn+KA0AAAAAAAAAJm+v8Ftf3ewCKAAAAAAAAAAAAAAAAAX9muqeGo9NttP06+0DcAAAAAAAAAjb7dTu2ra+VOT9P8AQCWAAAAAAAAAAAAAAAAAUNmyPt5Ltv4bui/kuAF0AAAAAAADiUlGLlJ0SVW+oDzOXfd/Ind6JPRdS0QHSAAAAAAAAAAAAAAAAAE2nVaNcGB6Lbs6OTao9LsF51z60BrAAAAAABJ3jOVHjW3r/sa9QEgAAAAAAAAAAAAAAAAAAAPu1duWriuW34ZR4MC9hbnZyEoy8l36XwfYBsAAADaSq9EuLAlZ+7xSdrGdW9Hc5dgEdtt1erfFgAAAAAAAAAAAAAAAAADVjbblX6NR8MH80tEBRs7HYivyzlN8lovaBPzduvY0m6eK10TXtAyAarO55lpJK54orolr+4GqO/Xaea1FvqbXvA+Z77kNeW3GPbV+4DJfzcm/pcm3H6Vou5AdAFLC2ed2Pjv1txa8sV8T6wOL+yZEKu1JXFy4MDBOE4ScZxcZLinoB8gAAAAAAAAAAAB242LeyJ+C3GvN9C7QLmJtePYpKS+5c+p8F2IDYAANJqj1T4oCfk7Nj3G5Wn9qXJax7gJ93Z82D8sVNc4v30A6Xg5i42Z+iLfqARwcyT0sz9MWvWBps7LlTf5Grce9/oBTxdtxseklHxT+uWr9AGoAB138ezfj4bsFJdD6V2MCPm7RdtJzs1uW1xXzL3gTgAAAAAAAAADRhYc8q74I6RWs5ckB6GxYtWLat21SK731sDsAAAAAAAAAAAAAAAASt021NO/YjrxuQXT1oCOAAAAAAABzGLlJRSq26JAelwsWONYjbXxcZvmwO8AAAAAAAAAAAAAAAAAAef3TEWPkVivx3NY9T6UBiAAAAAABo2+VmGXblddIJ8eivRUD0oAAAAAAAAAAAAAAAAAAAYt4tKeFKVNYNSXfRgefAAAAAAAAr7VuSSWPedKaW5v1MCsAAAAAAAAAAAAAAAAAAIe6bj96Ts2n+JPzSXzP3ATgAAAAAAAAFbbt1UUrOQ9FpC4/UwK6aaqtU+DAAAAAAAAAAAAAAA4lKMIuUmoxWrb4ARNx3R3q2rLpa4Sl0y/YCcAAAAAAAAAAANmFud7G8r89r6X0dgFvGzLGRGtuWvTF6NAdwAAAAAAAAAAAy5W442PVN+K59EePp5ARMvOv5MvO6QXCC4AZwAAAAAAAAAAAAAcxlKLUotprg1owN+PvORborq+7Hnwl3gUbO74VzRydt8pKn68ANcJwmqwkpLmnUDkAAAAfNy9atqtyagut0AxXt5xIV8Fbj6lRd7Am5G65V6qUvtwfyx94GMAAAAAAAAAAAAAAAAAAAOU2nVOj5gdsc3LiqRvTpyqwOxbnnrhdfpSfrQB7pnv/AGvuS9gHXPMy5/Fem1yq0v0A6W29XqwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf//Z";
/**
* Avatar 头像
* @description 本组件一般用于展示头像的地方,如个人中心,或者评论列表页的用户头像展示等场所。
* @tutorial https://www.uvui.cn/components/avatar.html
*
* @property {String} src 头像路径,如加载失败,将会显示默认头像(不能为相对路径)
* @property {String} shape 头像形状 ( circle (默认) | square)
* @property {String | Number} size 头像尺寸,可以为指定字符串(large, default, mini),或者数值 (默认 40 )
* @property {String} mode 头像图片的裁剪类型,与uni的image组件的mode参数一致,如效果达不到需求,可尝试传widthFix值 (默认 'scaleToFill' )
* @property {String} text 用文字替代图片,级别优先于src
* @property {String} bgColor 背景颜色,一般显示文字时用 (默认 '#c0c4cc' )
* @property {String} color 文字颜色 (默认 '#ffffff' )
* @property {String | Number} fontSize 文字大小 (默认 18 )
* @property {String} icon 显示的图标
* @property {Boolean} mpAvatar 显示小程序头像,只对百度,微信,QQ小程序有效 (默认 false )
* @property {Boolean} randomBgColor 是否使用随机背景色 (默认 false )
* @property {String} defaultUrl 加载失败的默认头像(组件有内置默认图片)
* @property {String | Number} colorIndex 如果配置了randomBgColor为true,且配置了此值,则从默认的背景色数组中取出对应索引的颜色值,取值0-19之间
* @property {String} name 组件标识符 (默认 'level' )
* @property {Object} customStyle 定义需要用到的外部样式
*
* @event {Function} click 点击组件时触发 index: 用户传递的标识符
* @example <uv-avatar :src="src" mode="square"></uv-avatar>
*/
export default {
name: 'uv-avatar',
emits: ['click'],
mixins: [mpMixin, mixin, props],
data() {
return {
// 如果配置randomBgColor参数为true,在图标或者文字的模式下,会随机从中取出一个颜色值当做背景色
colors: ['#ffb34b', '#f2bba9', '#f7a196', '#f18080', '#88a867', '#bfbf39', '#89c152', '#94d554', '#f19ec2',
'#afaae4', '#e1b0df', '#c38cc1', '#72dcdc', '#9acdcb', '#77b1cc', '#448aca', '#86cefa', '#98d1ee',
'#73d1f1',
'#80a7dc'
],
avatarUrl: this.src,
allowMp: false
}
},
watch: {
// 监听头像src的变化,赋值给内部的avatarUrl变量,因为图片加载失败时,需要修改图片的src为默认值
// 而组件内部不能直接修改props的值,所以需要一个中间变量
src: {
immediate: true,
handler(newVal) {
this.avatarUrl = newVal
// 如果没有传src,则主动触发error事件,用于显示默认的头像,否则src为''空字符等的时候,会无内容展示
if(!newVal) {
this.errorHandler()
}
}
}
},
computed: {
imageStyle() {
const style = {}
return style
}
},
created() {
this.init()
},
methods: {
init() {
// 目前只有这几个小程序平台具有open-data标签
// 其他平台可以通过uni.getUserInfo类似接口获取信息,但是需要弹窗授权(首次),不合符组件逻辑
// 故目前自动获取小程序头像只支持这几个平台
// #ifdef MP-WEIXIN || MP-QQ || MP-BAIDU
this.allowMp = true
// #endif
},
// 判断传入的name属性,是否图片路径,只要带有"/"均认为是图片形式
isImg() {
return this.src.indexOf('/') !== -1
},
// 图片加载时失败时触发
errorHandler() {
this.avatarUrl = this.defaultUrl || base64Avatar
},
clickHandler() {
this.$emit('click', this.name)
}
}
}
</script>
<style lang="scss" scoped>
@import '@/uni_modules/uv-ui-tools/libs/css/components.scss';
.uv-avatar {
@include flex;
align-items: center;
justify-content: center;
&--circle {
border-radius: 100px;
}
&--square {
border-radius: 4px;
}
&__image {
&--circle {
border-radius: 100px;
}
&--square {
border-radius: 4px;
}
}
}
</style>

@ -0,0 +1,89 @@
{
"id": "uv-avatar",
"displayName": "uv-avatar 头像 全面兼容小程序、nvue、vue2、vue3等多端",
"version": "1.0.5",
"description": "uv-avatar 本组件一般用于展示头像的地方,如个人中心,或者评论列表页的用户头像展示等场所。",
"keywords": [
"uv-avatar",
"uvui",
"uv-ui",
"avatar",
"头像"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [
"uv-ui-tools",
"uv-icon",
"uv-text"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"Vue": {
"vue2": "y",
"vue3": "y"
},
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

@ -0,0 +1,11 @@
## Avatar 头像
> **组件名:uv-avatar**
本组件一般用于展示头像的地方,如个人中心,或者评论列表页的用户头像展示等场所。
### <a href="https://www.uvui.cn/components/avatar.html" target="_blank">查看文档</a>
### [完整示例项目下载 | 关注更多组件](https://ext.dcloud.net.cn/plugin?name=uv-ui)
#### 如使用过程中有任何问题,或者您对uv-ui有一些好的建议,欢迎加入 uv-ui 交流群:<a href="https://ext.dcloud.net.cn/plugin?id=12287" target="_blank">uv-ui</a>、<a href="https://www.uvui.cn/components/addQQGroup.html" target="_blank">官方QQ群</a>

@ -0,0 +1,8 @@
## 1.0.2(2023-07-03)
1. 优化插槽自定义内容部分
2. 增加backToTop方法说明
## 1.0.1(2023-05-16)
1. 优化组件依赖,修改后无需全局引入,组件导入即可使用
2. 优化部分功能
## 1.0.0(2023-05-10)
uv-back-top 返回顶部

@ -0,0 +1,58 @@
export default {
props: {
// 返回顶部的形状,circle-圆形,square-方形
mode: {
type: String,
default: 'circle'
},
// 自定义图标
icon: {
type: String,
default: 'arrow-upward'
},
// 提示文字
text: {
type: String,
default: ''
},
// 返回顶部滚动时间
duration: {
type: [String, Number],
default: 100
},
// 滚动距离
scrollTop: {
type: [String, Number],
default: 0
},
// 距离顶部多少距离显示,单位px
top: {
type: [String, Number],
default: 400
},
// 返回顶部按钮到底部的距离,单位px
bottom: {
type: [String, Number],
default: 100
},
// 返回顶部按钮到右边的距离,单位px
right: {
type: [String, Number],
default: 20
},
// 层级
zIndex: {
type: [String, Number],
default: 9
},
// 图标的样式,对象形式
iconStyle: {
type: Object,
default: () => ({
color: '#909399',
fontSize: '19px'
})
},
...uni.$uv?.props?.backtop
}
}

@ -0,0 +1,116 @@
<template>
<uv-transition mode="fade" :customStyle="backTopStyle" :show="show">
<slot>
<view class="uv-back-top" :style="[contentStyle]" @click="backToTop">
<uv-icon :name="icon" :custom-style="iconStyle"></uv-icon>
<text v-if="text" class="uv-back-top__text">{{text}}</text>
</view>
</slot>
</uv-transition>
</template>
<script>
import mpMixin from '@/uni_modules/uv-ui-tools/libs/mixin/mpMixin.js'
import mixin from '@/uni_modules/uv-ui-tools/libs/mixin/mixin.js'
import props from './props.js';
// #ifdef APP-NVUE
const dom = weex.requireModule('dom')
// #endif
/**
* backTop 返回顶部
* @description 本组件一个用于长页面,滑动一定距离后,出现返回顶部按钮,方便快速返回顶部的场景。
* @tutorial https://www.uvui.cn/components/backTop.html
* @property {String} mode 返回顶部的形状,circle-圆形,square-方形 (默认 'circle' )
* @property {String} icon 自定义图标 (默认 'arrow-upward' ) 见官方文档示例
* @property {String} text 提示文字
* @property {String | Number} duration 返回顶部滚动时间 (默认 100)
* @property {String | Number} scrollTop 滚动距离 (默认 0 )
* @property {String | Number} top 距离顶部多少距离显示,单位px (默认 400 )
* @property {String | Number} bottom 返回顶部按钮到底部的距离,单位px (默认 100 )
* @property {String | Number} right 返回顶部按钮到右边的距离,单位px (默认 20 )
* @property {String | Number} zIndex 层级 (默认 9 )
* @property {Object<Object>} iconStyle 图标的样式,对象形式 (默认 {color: '#909399',fontSize: '19px'})
* @property {Object} customStyle 定义需要用到的外部样式
*
* @example <uv-back-top :scrollTop="scrollTop"></uv-back-top>
*/
export default {
name: 'uv-back-top',
emits: ['click'],
mixins: [mpMixin, mixin, props],
computed: {
backTopStyle() {
// 动画组件样式
const style = {
bottom: this.$uv.addUnit(this.bottom),
right: this.$uv.addUnit(this.right),
width: '40px',
height: '40px',
position: 'fixed',
zIndex: 10,
}
return style
},
show() {
return this.$uv.getPx(this.scrollTop) > this.$uv.getPx(this.top)
},
contentStyle() {
const style = {}
let radius = 0
// 是否圆形
if (this.mode === 'circle') {
radius = '100px'
} else {
radius = '4px'
}
// 为了兼容安卓nvue,只能这么分开写
style.borderTopLeftRadius = radius
style.borderTopRightRadius = radius
style.borderBottomLeftRadius = radius
style.borderBottomRightRadius = radius
return this.$uv.deepMerge(style, this.$uv.addStyle(this.customStyle))
}
},
methods: {
backToTop() {
// #ifdef APP-NVUE
if (!this.$parent.$refs['uv-back-top']) {
this.$uv.error(`nvue页面需要给页面最外层元素设置"ref='uv-back-top'`)
}
dom.scrollToElement(this.$parent.$refs['uv-back-top'], {
offset: 0
})
// #endif
// #ifndef APP-NVUE
uni.pageScrollTo({
scrollTop: 0,
duration: this.duration
});
// #endif
this.$emit('click')
}
}
}
</script>
<style lang="scss" scoped>
@import '@/uni_modules/uv-ui-tools/libs/css/components.scss';
$uv-back-top-flex: 1 !default;
$uv-back-top-height: 100% !default;
$uv-back-top-background-color: #E1E1E1 !default;
$uv-back-top-tips-font-size: 12px !default;
.uv-back-top {
@include flex;
flex-direction: column;
align-items: center;
flex: $uv-back-top-flex;
height: $uv-back-top-height;
justify-content: center;
background-color: $uv-back-top-background-color;
&__tips {
font-size: $uv-back-top-tips-font-size;
transform: scale(0.8);
}
}
</style>

@ -0,0 +1,89 @@
{
"id": "uv-back-top",
"displayName": "uv-back-top 返回顶部 全面兼容小程序、nvue、vue2、vue3等多端",
"version": "1.0.2",
"description": "返回顶部 组件一个用于长页面,滑动一定距离后,出现返回顶部按钮,方便快速返回顶部的场景。",
"keywords": [
"uv-back-top",
"uvui",
"uv-ui",
"avatar",
"返回顶部"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [
"uv-ui-tools",
"uv-icon",
"uv-transition"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"Vue": {
"vue2": "y",
"vue3": "y"
},
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

@ -0,0 +1,11 @@
## BackTop 返回顶部
> **组件名:uv-back-top**
该组件一个用于长页面,滑动一定距离后,出现返回顶部按钮,方便快速返回顶部的场景。
### <a href="https://www.uvui.cn/components/backTop.html" target="_blank">查看文档</a>
### [完整示例项目下载 | 关注更多组件](https://ext.dcloud.net.cn/plugin?name=uv-ui)
#### 如使用过程中有任何问题,或者您对uv-ui有一些好的建议,欢迎加入 uv-ui 交流群:<a href="https://ext.dcloud.net.cn/plugin?id=12287" target="_blank">uv-ui</a>、<a href="https://www.uvui.cn/components/addQQGroup.html" target="_blank">官方QQ群</a>

@ -0,0 +1,7 @@
## 1.0.2(2023-06-04)
1. 修复type等属性为null的时候不显示徽标的BUG
## 1.0.1(2023-05-16)
1. 优化组件依赖,修改后无需全局引入,组件导入即可使用
2. 优化部分功能
## 1.0.0(2023-05-10)
uv-badge 徽标数,数字角标

@ -0,0 +1,73 @@
export default {
props: {
// 是否显示圆点
isDot: {
type: Boolean,
default: false
},
// 显示的内容
value: {
type: [Number, String],
default: ''
},
// 是否显示
show: {
type: Boolean,
default: true
},
// 最大值,超过最大值会显示 '{max}+'
max: {
type: [Number, String],
default: 999
},
// 主题类型,error|warning|success|primary
type: {
type: [String,undefined,null],
default: 'error'
},
// 当数值为 0 时,是否展示 Badge
showZero: {
type: Boolean,
default: false
},
// 背景颜色,优先级比type高,如设置,type参数会失效
bgColor: {
type: [String, null],
default: null
},
// 字体颜色
color: {
type: [String, null],
default: null
},
// 徽标形状,circle-四角均为圆角,horn-左下角为直角
shape: {
type: [String,undefined,null],
default: 'circle'
},
// 设置数字的显示方式,overflow|ellipsis|limit
// overflow会根据max字段判断,超出显示`${max}+`
// ellipsis会根据max判断,超出显示`${max}...`
// limit会依据1000作为判断条件,超出1000,显示`${value/1000}K`,比如2.2k、3.34w,最多保留2位小数
numberType: {
type: [String,undefined,null],
default: 'overflow'
},
// 设置badge的位置偏移,格式为 [x, y],也即设置的为top和right的值,absolute为true时有效
offset: {
type: Array,
default: () => []
},
// 是否反转背景和字体颜色
inverted: {
type: Boolean,
default: false
},
// 是否绝对定位
absolute: {
type: Boolean,
default: false
},
...uni.$uv?.props?.badge
}
}

@ -0,0 +1,176 @@
<template>
<text
v-if="show && ((Number(value) === 0 ? showZero : true) || isDot)"
:class="[isDot ? 'uv-badge--dot' : 'uv-badge--not-dot', inverted && 'uv-badge--inverted', shape === 'horn' && 'uv-badge--horn', `uv-badge--${propsType}${inverted ? '--inverted' : ''}`]"
:style="[$uv.addStyle(customStyle), badgeStyle]"
class="uv-badge"
>{{ isDot ? '' :showValue }}</text>
</template>
<script>
import mpMixin from '@/uni_modules/uv-ui-tools/libs/mixin/mpMixin.js'
import mixin from '@/uni_modules/uv-ui-tools/libs/mixin/mixin.js'
import props from './props.js';
/**
* badge 徽标数
* @description 该组件一般用于图标右上角显示未读的消息数量,提示用户点击,有圆点和圆包含文字两种形式。
* @tutorial https://www.uvui.cn/components/badge.html
*
* @property {Boolean} isDot 是否显示圆点 (默认 false )
* @property {String | Number} value 显示的内容
* @property {Boolean} show 是否显示 (默认 true )
* @property {String | Number} max 最大值,超过最大值会显示 '{max}+' (默认999)
* @property {String} type 主题类型,error|warning|success|primary (默认 'error' )
* @property {Boolean} showZero 当数值为 0 时,是否展示 Badge (默认 false )
* @property {String} bgColor 背景颜色,优先级比type高,如设置,type参数会失效
* @property {String} color 字体颜色 (默认 '#ffffff' )
* @property {String} shape 徽标形状,circle-四角均为圆角,horn-左下角为直角 (默认 'circle' )
* @property {String} numberType 设置数字的显示方式,overflow|ellipsis|limit (默认 'overflow' )
* @property {Array}} offset 设置badge的位置偏移,格式为 [x, y],也即设置的为top和right的值,absolute为true时有效
* @property {Boolean} inverted 是否反转背景和字体颜色(默认 false )
* @property {Boolean} absolute 是否绝对定位(默认 false )
* @property {Object} customStyle 定义需要用到的外部样式
* @example <uv-badge :type="type" :count="count"></uv-badge>
*/
export default {
name: 'uv-badge',
mixins: [mpMixin, mixin, props],
computed: {
// 是否将badge中心与父组件右上角重合
boxStyle() {
let style = {};
return style;
},
// 整个组件的样式
badgeStyle() {
const style = {}
if(this.color) {
style.color = this.color
}
if (this.bgColor && !this.inverted) {
style.backgroundColor = this.bgColor
}
if (this.absolute) {
style.position = 'absolute'
// 如果有设置offset参数
if(this.offset.length) {
// top和right分为为offset的第一个和第二个值,如果没有第二个值,则right等于top
const top = this.offset[0]
const right = this.offset[1] || top
style.top = this.$uv.addUnit(top)
style.right = this.$uv.addUnit(right)
}
}
return style
},
showValue() {
switch (this.numberType) {
case "overflow":
return Number(this.value) > Number(this.max) ? this.max + "+" : this.value
break;
case "ellipsis":
return Number(this.value) > Number(this.max) ? "..." : this.value
break;
case "limit":
return Number(this.value) > 999 ? Number(this.value) >= 9999 ?
Math.floor(this.value / 1e4 * 100) / 100 + "w" : Math.floor(this.value /
1e3 * 100) / 100 + "k" : this.value
break;
default:
return Number(this.value)
}
},
propsType(){
return this.type || 'error'
}
}
}
</script>
<style lang="scss" scoped>
@import '@/uni_modules/uv-ui-tools/libs/css/components.scss';
@import '@/uni_modules/uv-ui-tools/libs/css/color.scss';
$uv-badge-primary: $uv-primary !default;
$uv-badge-error: $uv-error !default;
$uv-badge-success: $uv-success !default;
$uv-badge-info: $uv-info !default;
$uv-badge-warning: $uv-warning !default;
$uv-badge-dot-radius: 100px !default;
$uv-badge-dot-size: 8px !default;
$uv-badge-dot-right: 4px !default;
$uv-badge-dot-top: 0 !default;
$uv-badge-text-font-size: 11px !default;
$uv-badge-text-right: 10px !default;
$uv-badge-text-padding: 2px 5px !default;
$uv-badge-text-align: center !default;
$uv-badge-text-color: #FFFFFF !default;
.uv-badge {
border-top-right-radius: $uv-badge-dot-radius;
border-top-left-radius: $uv-badge-dot-radius;
border-bottom-left-radius: $uv-badge-dot-radius;
border-bottom-right-radius: $uv-badge-dot-radius;
@include flex;
line-height: $uv-badge-text-font-size;
text-align: $uv-badge-text-align;
font-size: $uv-badge-text-font-size;
color: $uv-badge-text-color;
&--dot {
height: $uv-badge-dot-size;
width: $uv-badge-dot-size;
}
&--inverted {
font-size: 13px;
}
&--not-dot {
padding: $uv-badge-text-padding;
}
&--horn {
border-bottom-left-radius: 0;
}
&--primary {
background-color: $uv-badge-primary;
}
&--primary--inverted {
color: $uv-badge-primary;
}
&--error {
background-color: $uv-badge-error;
}
&--error--inverted {
color: $uv-badge-error;
}
&--success {
background-color: $uv-badge-success;
}
&--success--inverted {
color: $uv-badge-success;
}
&--info {
background-color: $uv-badge-info;
}
&--info--inverted {
color: $uv-badge-info;
}
&--warning {
background-color: $uv-badge-warning;
}
&--warning--inverted {
color: $uv-badge-warning;
}
}
</style>

@ -0,0 +1,87 @@
{
"id": "uv-badge",
"displayName": "uv-badge 徽标数,数字角标 全面兼容小程序、nvue、vue2、vue3等多端",
"version": "1.0.2",
"description": "徽标数一般用于图标右上角显示未读的消息数量,提示用户点击,有圆点和圆包含文字两种形式。",
"keywords": [
"uv-badge",
"uvui",
"uv-ui",
"徽标数",
"数字角标"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [
"uv-ui-tools"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"Vue": {
"vue2": "y",
"vue3": "y"
},
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

@ -0,0 +1,11 @@
## Badge 徽标数
> **组件名:uv-badge**
该组件一般用于图标右上角显示未读的消息数量,提示用户点击,有圆点和圆包含文字两种形式。
### <a href="https://www.uvui.cn/components/badge.html" target="_blank">查看文档</a>
### [完整示例项目下载 | 关注更多组件](https://ext.dcloud.net.cn/plugin?name=uv-ui)
#### 如使用过程中有任何问题,或者您对uv-ui有一些好的建议,欢迎加入 uv-ui 交流群:<a href="https://ext.dcloud.net.cn/plugin?id=12287" target="_blank">uv-ui</a>、<a href="https://www.uvui.cn/components/addQQGroup.html" target="_blank">官方QQ群</a>

@ -0,0 +1,33 @@
## 1.0.15(2023-12-20)
1. 优化
## 1.0.14(2023-12-06)
1. 优化
## 1.0.13(2023-12-06)
1. 阻止事件冒泡处理
## 1.0.12(2023-10-19)
1. 增加后置插槽
## 1.0.11(2023-09-21)
1. 修复通过customStyle修改按钮宽度,组件中最外层节点不改变的问题
## 1.0.10(2023-09-15)
1. 按钮支持open-type="agreePrivacyAuthorization"
## 1.0.9(2023-09-11)
1. 增加参数iconSize,用于控制图标的大小
## 1.0.8(2023-09-10)
1. 修复多个按钮在一行宽度不正常的BUG
## 1.0.7(2023-09-07)
1. 修复warning颜色对应错误的BUG
## 1.0.6(2023-07-25)
1. 增加customTextStyle属性,方便自定义文字样式
## 1.0.5(2023-07-20)
1. 解决微信小程序动态设置hover-class点击态不消失的BUG
## 1.0.4(2023-06-29)
1. 修改上次更新出现nvue报错异常
## 1.0.3(2023-06-28)
修复:设置open-type="chooseAvatar"等值不生效的BUG
## 1.0.2(2023-06-01)
1. 修复按钮点击触发两次的BUG
## 1.0.1(2023-05-16)
1. 优化组件依赖,修改后无需全局引入,组件导入即可使用
2. 优化部分功能
## 1.0.0(2023-05-10)
uv-button 按钮

@ -0,0 +1,46 @@
$uv-button-active-opacity:0.75 !default;
$uv-button-loading-text-margin-left:4px !default;
$uv-button-text-color: #FFFFFF !default;
$uv-button-text-plain-error-color:$uv-error !default;
$uv-button-text-plain-warning-color:$uv-warning !default;
$uv-button-text-plain-success-color:$uv-success !default;
$uv-button-text-plain-info-color:$uv-info !default;
$uv-button-text-plain-primary-color:$uv-primary !default;
.uv-button {
&--active {
opacity: $uv-button-active-opacity;
}
&--active--plain {
background-color: rgb(217, 217, 217);
}
&__loading-text {
margin-left:$uv-button-loading-text-margin-left;
}
&__text,
&__loading-text {
color:$uv-button-text-color;
}
&__text--plain--error {
color:$uv-button-text-plain-error-color;
}
&__text--plain--warning {
color:$uv-button-text-plain-warning-color;
}
&__text--plain--success{
color:$uv-button-text-plain-success-color;
}
&__text--plain--info {
color:$uv-button-text-plain-info-color;
}
&__text--plain--primary {
color:$uv-button-text-plain-primary-color;
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save