Lufaneng 4 months ago
commit 4e60dcbbff

@ -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,161 @@
{
//组件自动引入规则 https://uniapp.dcloud.net.cn/collocation/pages.html#easycom
"easycom": {
"autoscan": true,
"custom": {
"^x-(.*)": "@/components/x-$1/index.vue"
}
},
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "电梯",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8",
"enablePullDownRefresh": false
},
"pages": [
//pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "主页",
"navigationStyle": "custom"
}
},
{
"path" : "pages/appointment/appointment",
"style" :
{
"navigationBarTitleText" : "预约服务",
"navigationStyle": "custom"
}
},
{
"path" : "pages/mine/mine",
"style" :
{
"navigationBarTitleText" : "我的",
"navigationStyle": "custom"
}
}
],
"subPackages": [
{
"root": "pagesA",
"pages": [
{
"path": "ProductDetails/ProductDetails",
"style": {
"navigationBarTitleText": "产品详情",
"navigationStyle": "custom"
// "enablePullDownRefresh": true
}
// "needLogin": true
},
{
"path" : "choose-adress/choose-adress",
"style" :
{
"navigationBarTitleText" : "选择位置",
"navigationStyle": "custom"
}
},
{
"path" : "privacy-policy/privacy-policy",
"style" :
{
"navigationBarTitleText" : "隐私政策",
"navigationStyle": "custom"
}
},
{
"path" : "user-agreement/user-agreement",
"style" :
{
"navigationBarTitleText" : "用户协议",
"navigationStyle": "custom"
}
},
{
"path" : "JoiningUs/JoiningUs",
"style" :
{
"navigationBarTitleText" : "加盟卡叔",
"navigationStyle": "custom"
}
},
{
"path" : "StoreManage/StoreList",
"style" :
{
"navigationBarTitleText" : "企业门店列表",
"navigationStyle": "custom"
}
},
{
"path" : "StoreManage/StoreReview",
"style" :
{
"navigationBarTitleText" : "企业门店审核",
"navigationStyle": "custom"
}
},
{
"path" : "StoreManage/AddStore",
"style" :
{
"navigationStyle": "custom"
}
},
{
"path" : "StoreManage/businessEdit",
"style" :
{
"navigationStyle": "custom"
}
}
// {
// "path" : "search-address/search-address",
// "style" :
// {
// "navigationBarTitleText" : "搜索地址",
// "navigationStyle": "custom"
// }
// }
]
}
],
"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/appointment/appointment",
"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,317 @@
<template>
<view class="container">
<HeaderCustom title='预约服务'></HeaderCustom>
<div class="titleBox">
<div class="title">预约服务</div>
<div class="line"></div>
<div class="desc">
请填写以下相关信息,我们的销售工程师会尽快与您联系,并提供报价单
</div>
</div>
<div class="formBox">
<uv-form labelPosition="left" :model="formField" labelWidth="100" :rules="rules" ref="form">
<uv-form-item required label="姓名" prop="name">
<uv-input v-model="formField.name" border="none" placeholder="请输入姓名">
</uv-input>
</uv-form-item>
<uv-form-item required label="手机号码" prop="phone">
<uv-input v-model="formField.phone" border="none" placeholder="请输入手机号码">
</uv-input>
</uv-form-item>
<uv-form-item required label="邮箱" prop="email">
<uv-input v-model="formField.email" border="none" placeholder="请输入邮箱">
</uv-input>
</uv-form-item>
<uv-form-item required label="地址" prop="adress" @click="openAddressPicker">
<uv-input v-model="formField.adress" readonly border="none" placeholder="请选择地址">
</uv-input>
<uv-picker ref="addressPicker" :columns="addressList" keyName="name" @change="addressChange"
@confirm="confirm">
</uv-picker>
</uv-form-item>
<uv-form-item labelPosition='top' required label="咨询业务类型" prop="appointment_type">
<!-- <uv-input v-model="formField.type" readonly border="none" placeholder="请选择咨询业务类型">
</uv-input> -->
<div class="type_select_box">
<div class="item" :class="{selected:formField.appointment_type.includes('别墅梯采购')}"
@click="pickType('别墅梯采购')">
<img class="img" src="/static/imgs/bstcg.png" alt="">
别墅梯采购
</div>
<div class="item" :class="{selected:formField.appointment_type.includes('旧梯换新')}"
@click="pickType('旧梯换新')">
<img class="img" src="/static/imgs/jthx.png" alt="">旧梯换新
</div>
<div class="item" :class="{selected:formField.appointment_type.includes('新梯采购')}"
@click="pickType('新梯采购')">
<img class="img" src="/static/imgs/xtcg.png" alt="">新梯采购
</div>
<div class="item" :class="{selected:formField.appointment_type.includes('旧楼加装')}"
@click="pickType('旧楼加装')">
<img class="img" src="/static/imgs/jljz.png" alt="">旧楼加装
</div>
</div>
</uv-form-item>
<uv-form-item label="需求补充" prop="remark">
<uv-input v-model="formField.remark" border="none" placeholder="请输入需求补充">
</uv-input>
</uv-form-item>
<uv-button type="primary" text="提交" customStyle="margin-top: 10px" @click="submit"></uv-button>
</uv-form>
</div>
</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()
onShow(() => {
if (!authStore?.isLoggedIn) {
uni.switchTab({
url: `/pages/index/index`,
});
uni.showToast({
title: "请先登录",
icon: "none",
})
}
})
const form = ref(null)
const formField = ref({
name: '',
phone: '',
email: '',
adress: '',
appointment_type: [],
remark: '',
})
const pickType = type => {
const index = formField.value.appointment_type.findIndex(item => item === type)
if (index >= 0) {
formField.value.appointment_type.splice(index, 1)
} else {
formField.value.appointment_type.push(type)
}
}
const rules = {
'name': {
type: 'string',
required: true,
message: '请填写姓名',
trigger: ['blur', 'change']
},
'phone': {
type: 'string',
required: true,
message: '请填写手机号码',
trigger: ['blur', 'change']
},
'email': {
type: 'string',
required: true,
message: '请填写邮箱',
trigger: ['blur', 'change']
},
'adress': {
type: 'string',
required: true,
message: '请选择地址',
trigger: ['blur', 'change']
},
'appointment_type': {
type: 'array',
required: true,
message: '请选择咨询业务类型',
trigger: ['blur', 'change']
},
}
const submit = () => {
// 如果有错误,会在catch中返回报错信息数组,校验通过则在then中返回true
form.value.validate().then(res => {
const params = {
...formField.value,
appointment_type: formField.value.appointment_type.join(',')
}
RPC.post('/appointment/create',params).then(() => {
uni.showToast({
icon: 'success',
title: '预约成功'
})
formField.value = {
name: '',
phone: '',
email: '',
adress: '',
appointment_type: [],
remark: '',
}
})
}).catch(errors => {
})
}
const provinces = ref([])
const citys = ref([])
const areas = ref([])
const addressList = computed(() => [provinces.value, citys.value, areas.value])
const addressPicker = ref(null)
const pickerValue = ref(['0', '0', '0'])
const defaultValue = ref(['3442', '1', '2'])
const addressChange = e => {
const {
columnIndex,
index,
indexs
} = e
// 改变了省
if (columnIndex === 0) {
citys.value = provinces.value[index]?.children || []
areas.value = citys.value[0]?.children || []
addressPicker.value.setIndexs([index, 0, 0], true)
} else if (columnIndex === 1) {
areas.value = citys.value[index]?.children || []
addressPicker.value.setIndexs(indexs, true)
}
}
const confirm = e => {
// console.log('确认选择的地区:', e);
// uni.showToast({
// icon: 'none',
// title: `${e.value[0].name}/${e.value[1].name}/${e.value[2].name}`
// })
formField.value.adress = `${e.value[0].name}/${e.value[1].name}/${e.value[2].name}`
}
const handlePickValueDefault = () => {
pickerValue.value[0] = provinces.value.findIndex(item => Number(item.id) == defaultValue.value[0]);
// 设置市
citys.value = provinces.value[pickerValue.value[0]]?.children || [];
pickerValue.value[1] = citys.value.findIndex(item => Number(item.id) == defaultValue.value[1]);
// 设置区
areas.value = citys.value[pickerValue.value[1]]?.children || [];
pickerValue.value[2] = areas.value.findIndex(item => Number(item.id) == defaultValue.value[2]);
// 重置下位置
addressPicker.value.setIndexs([pickerValue.value[0], pickerValue.value[1], pickerValue.value[2]], true);
}
const openAddressPicker = () => {
addressPicker.value.open()
handlePickValueDefault()
}
const getData = () => {
provinces.value = regions.sort((left, right) => (Number(left.code) > Number(right.code) ?
1 : -1));
handlePickValueDefault()
}
onMounted(() => {
getData()
})
</script>
<style lang="scss" scoped>
.container {
font-size: 14px;
min-height: 100vh;
background-color: #f8f8f8;
}
.titleBox {
background-color: #fff;
padding: 20px 4vw;
.title {
font-size: 16px;
font-weight: 700;
}
.line {
width: 5vw;
height: 2px;
background-color: #3F70FF;
margin-top: 4px;
margin-bottom: 8px;
}
.desc {
font-size: 12px;
font-weight: 500;
line-height: 18px;
}
}
.formBox {
padding: 10px;
:deep(.uv-form) {
background-color: #fff;
padding: 10px;
border-radius: 10px;
.uv-form-item {
background-color: #F4F4F4;
margin-bottom: 10px;
border-radius: 10px;
padding-left: 20px;
}
}
.type_select_box {
display: flex;
flex-wrap: wrap;
gap: 15px;
padding-right: 20px;
margin-top: 10px;
.item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: #fff;
font-size: 12px;
width: 90px;
height: 70px;
border-radius: 6px;
.img {
width: 20px;
height: 20px;
margin-bottom: 6px;
}
&.selected {
border: 1px solid #3F70FF;
background-color: rgba(63, 112, 255, 0.1);
}
}
}
}
</style>

File diff suppressed because one or more lines are too long

@ -0,0 +1,131 @@
<template>
<view class="container">
<HeaderCustom title="我的"></HeaderCustom>
<div class="titleBox">
<div class="avatar">
<img class="img" src="/static/imgs/avatar.png" alt="">
</div>
<div class="name" v-if="authStore?.isLoggedIn">{{userStore.userInfo.nickname}}</div>
<div class="name" v-else @click="handleLogin">登录/注册</div>
</div>
<div class="cellBox">
<uv-cell-group :customStyle="{backgroundColor:'#fff'}">
<uv-cell icon="server-fill" title="联系客服" isLink></uv-cell>
<uv-cell icon="more-circle-fill" title="加盟卡叔" url="/pagesA/JoiningUs/JoiningUs" isLink></uv-cell>
<uv-cell v-if="userStore.userInfo.identitys && userStore.userInfo.identitys.some(item => item.code==='superadmin' || item.code==='enterprise')" icon="order" title="企业门店列表" url="/pagesA/StoreManage/StoreList" isLink></uv-cell>
<uv-cell v-if="userStore.userInfo.identitys && userStore.userInfo.identitys.some(item => item.code==='superadmin' || item.code==='enterprise') && userStore.userInfo.jurisdictions && userStore.userInfo.jurisdictions.some(item => item.code==='superadmin')" icon="calendar" title="企业门店审核" url="/pagesA/StoreManage/StoreReview" isLink></uv-cell>
<!-- <uv-cell icon="edit-pen-fill" title="公司简介" isLink></uv-cell> -->
<!-- <uv-cell icon="file-text-fill" title="文件管理" isLink></uv-cell> -->
<uv-cell icon="empty-news" title="隐私政策" url="/pagesA/privacy-policy/privacy-policy" isLink></uv-cell>
<uv-cell icon="empty-order" title="用户协议" url="/pagesA/user-agreement/user-agreement" isLink></uv-cell>
<uv-cell icon="chat-fill" title="关于我们" isLink></uv-cell>
</uv-cell-group>
</div>
<uv-button v-if="authStore?.isLoggedIn" type="error" text="退出登录" customStyle="margin-top: 20px" @click="() => modal.open()"></uv-button>
<uv-modal ref="modal" showCancelButton content='确认退出登录?' @confirm="logout"></uv-modal>
</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 logout = () => {
authStore.signOut()
userStore.setUserInfo({username: "",})
modal.value.close()
}
const handleLogin = () => {
uni.showLoading({
title: "登录中...",
});
uni.login({
success: function(loginRes) {
console.log('loginRes', loginRes);
RPC.post('/account/miniprogram/phonelogin', {
code: loginRes.code
}).then(res => {
console.log('phonelogin', res)
uni.showToast({
title: "登录成功",
icon: "success",
});
authStore.setAccessToken(res.token)
userStore.setUserInfo(res)
}).catch(() => {
uni.showToast({
title: "登录失败",
icon: "error",
});
})
},
fail: () => {
uni.hideLoading();
}
})
}
</script>
<style lang="scss" scoped>
.container {
font-size: 14px;
min-height: 100vh;
background-color: #f8f8f8;
}
.titleBox {
height: 90px;
display: flex;
align-items: center;
padding-left: 5vw;
background-color: #fff;
margin-bottom: 10px;
.avatar {
width: 50px;
height: 50px;
border-radius: 50%;
margin-right: 3vw;
background-color: #f8f8f8;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
.img {
width: 80%;
height: 80%;
}
}
.name {
font-size: 20px;
font-weight: 700;
}
}
.cellBox {
:deep(.uv-cell__body) {
padding: 20px 5vw;
}
:deep(.uv-cell__title-text) {
color: #000;
font-weight: 400;
margin-left: 10px;
}
}
</style>

@ -0,0 +1,369 @@
<template>
<view class="container">
<HeaderCustom title='加盟卡叔' :showBack="true"></HeaderCustom>
<div class="titleBox">
<div class="title">加盟卡叔</div>
<div class="line"></div>
<div class="desc">
请填写以下相关信息,我们会尽快与您联系
</div>
</div>
<div class="formBox">
<uv-form labelPosition="left" :model="formField" labelWidth="100" :rules="rules" ref="form">
<uv-form-item required label="企业名称" prop="company_name">
<uv-input :disabled="!!enterprise" v-model="formField.company_name" border="none"
placeholder="请输入企业名称">
</uv-input>
</uv-form-item>
<uv-form-item required label="姓名" prop="contact_person">
<uv-input :disabled="!!enterprise" v-model="formField.contact_person" border="none"
placeholder="请输入姓名">
</uv-input>
</uv-form-item>
<uv-form-item required label="手机号码" prop="contact_phone">
<uv-input :disabled="!!enterprise" v-model="formField.contact_phone" border="none"
placeholder="请输入手机号码">
</uv-input>
</uv-form-item>
<uv-form-item required label="邮箱" prop="contact_email">
<uv-input :disabled="!!enterprise" v-model="formField.contact_email" border="none"
placeholder="请输入邮箱">
</uv-input>
</uv-form-item>
<uv-form-item required label="地址" prop="address" @click="openAddressPicker">
<uv-input :disabled="!!enterprise" v-model="formField.address" readonly border="none"
placeholder="请选择地址">
</uv-input>
<uv-picker ref="addressPicker" :columns="addressList" keyName="name" @change="addressChange"
@confirm="confirm">
</uv-picker>
</uv-form-item>
<uv-button type="primary" v-if="!enterprise" text="提交" customStyle="margin-top: 10px"
@click="submit"></uv-button>
</uv-form>
<div class="infoBox" v-if="!!enterprise">
<div class="line">
<div class="key">状态</div>
<div class="value">
<div v-if="enterprise.status == 1">审核中</div>
<div v-if="enterprise.status == 2">通过</div>
<div v-if="enterprise.status == 3">驳回</div>
</div>
</div>
<template v-if="enterprise.status == 2">
<div class="line">
<div class="key">账号</div>
<div class="value" @click="copyText(enterprise.username)">{{enterprise.username}}</div>
</div>
<div class="line">
<div class="key">密码</div>
<div class="value" @click="copyText(enterprise.plain_password)">{{enterprise.plain_password}}
</div>
</div>
</template>
</div>
</div>
</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 enterprise = ref(null)
function copyText(text) {
uni.setClipboardData({
data: text,
success: function() {
uni.showToast({
title: '复制成功',
icon: 'success',
});
},
fail: function() {
uni.showToast({
title: '复制失败',
icon: 'none',
});
},
})
}
onShow(() => {
if (!authStore?.isLoggedIn) {
uni.switchTab({
url: `/pages/index/index`,
});
uni.showToast({
title: "请先登录",
icon: "none",
})
return
}
RPC.post('/enterprise/profile').then(res => {
console.log('/enterprise/profile', res)
enterprise.value = res.enterprise
if (res.enterprise) {
formField.value = {
...res.enterprise
}
}
})
})
const form = ref(null)
const formField = ref({
contact_person: '',
phone: '',
email: '',
address: '',
company_name: '',
})
const pickType = type => {
const index = formField.value.type.findIndex(item => item === type)
if (index >= 0) {
formField.value.type.splice(index, 1)
} else {
formField.value.type.push(type)
}
}
const rules = {
'company_name': {
type: 'string',
required: true,
message: '请填写企业名称',
trigger: ['blur', 'change']
},
'name': {
type: 'string',
required: true,
message: '请填写姓名',
trigger: ['blur', 'change']
},
'contact_phone': {
type: 'string',
required: true,
message: '请填写手机号码',
trigger: ['blur', 'change']
},
'contact_email': {
type: 'string',
required: true,
message: '请填写邮箱',
trigger: ['blur', 'change']
},
'address': {
type: 'string',
required: true,
message: '请选择地址',
trigger: ['blur', 'change']
}
}
const submit = () => {
// 如果有错误,会在catch中返回报错信息数组,校验通过则在then中返回true
form.value.validate().then(res => {
// uni.showToast({
// icon: 'success',
// title: '校验通过'
// })
RPC.post('/enterprise/apply', formField.value).then(res => {
uni.showToast({
icon: 'success',
title: '操作成功'
})
setTimeout(() => {
uni.navigateBack()
}, 1000)
})
}).catch(errors => {
// uni.showToast({
// icon: 'error',
// title: '校验失败'
// })
})
}
const provinces = ref([])
const citys = ref([])
const areas = ref([])
const addressList = computed(() => [provinces.value, citys.value, areas.value])
const addressPicker = ref(null)
const pickerValue = ref(['0', '0', '0'])
const defaultValue = ref(['3442', '1', '2'])
const addressChange = e => {
const {
columnIndex,
index,
indexs
} = e
// 改变了省
if (columnIndex === 0) {
citys.value = provinces.value[index]?.children || []
areas.value = citys.value[0]?.children || []
addressPicker.value.setIndexs([index, 0, 0], true)
} else if (columnIndex === 1) {
areas.value = citys.value[index]?.children || []
addressPicker.value.setIndexs(indexs, true)
}
}
const confirm = e => {
// console.log('确认选择的地区:', e);
// uni.showToast({
// icon: 'none',
// title: `${e.value[0].name}/${e.value[1].name}/${e.value[2].name}`
// })
formField.value.address = `${e.value[0].name}/${e.value[1].name}/${e.value[2].name}`
}
const handlePickValueDefault = () => {
pickerValue.value[0] = provinces.value.findIndex(item => Number(item.id) == defaultValue.value[0]);
// 设置市
citys.value = provinces.value[pickerValue.value[0]]?.children || [];
pickerValue.value[1] = citys.value.findIndex(item => Number(item.id) == defaultValue.value[1]);
// 设置区
areas.value = citys.value[pickerValue.value[1]]?.children || [];
pickerValue.value[2] = areas.value.findIndex(item => Number(item.id) == defaultValue.value[2]);
// 重置下位置
addressPicker.value.setIndexs([pickerValue.value[0], pickerValue.value[1], pickerValue.value[2]], true);
}
const openAddressPicker = () => {
if (!!enterprise.value) {
return
}
addressPicker.value.open()
handlePickValueDefault()
}
const getData = () => {
provinces.value = regions.sort((left, right) => (Number(left.code) > Number(right.code) ?
1 : -1));
handlePickValueDefault()
}
onMounted(() => {
getData()
})
</script>
<style lang="scss" scoped>
.container {
font-size: 14px;
min-height: 100vh;
background-color: #f8f8f8;
}
.titleBox {
background-color: #fff;
padding: 20px 4vw;
.title {
font-size: 16px;
font-weight: 700;
}
.line {
width: 5vw;
height: 2px;
background-color: #3F70FF;
margin-top: 4px;
margin-bottom: 8px;
}
.desc {
font-size: 12px;
font-weight: 500;
line-height: 18px;
}
}
.formBox {
padding: 10px;
:deep(.uv-form) {
background-color: #fff;
padding: 10px;
border-radius: 10px;
.uv-form-item {
background-color: #F4F4F4;
margin-bottom: 10px;
border-radius: 10px;
padding-left: 20px;
}
}
.type_select_box {
display: flex;
flex-wrap: wrap;
gap: 15px;
padding-right: 20px;
margin-top: 10px;
.item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: #fff;
font-size: 12px;
width: 90px;
height: 70px;
border-radius: 6px;
.img {
width: 20px;
height: 20px;
margin-bottom: 6px;
}
&.selected {
border: 1px solid #3F70FF;
background-color: rgba(63, 112, 255, 0.1);
}
}
}
}
.infoBox {
margin-top: 20px;
padding: 10px 5vw;
background-color: #fff;
border-radius: 10px;
font-size: 16px;
.line {
display: flex;
margin: 10px 0;
.key {
width: 70px;
font-weight: 700;
}
}
}
</style>

@ -0,0 +1,158 @@
<template>
<view class="container" ref="backTop">
<HeaderCustom title="产品详情" :showBack="true"></HeaderCustom>
<!-- <uv-sticky bgColor="#fff" offsetTop="90">
<uv-tabs :list="list" @click="clickTab"></uv-tabs>
</uv-sticky> -->
<div class="contentBox">
<video v-if="videoUrl" class="video" :src="videoUrl" controls></video>
<image class="img" v-for="item in product_images_list" :key="item" :src="item" mode="widthFix" />
<image class="img" v-for="item in qualification_images_list" :key="item" :src="item" mode="widthFix" />
</div>
<div class="pdf_box">
<div class="pdf_item" v-for="(item,index) in product_pdfs_list" :key="item.url" @click="openPdf(item)">
<div class="pdf_icon"><uv-icon name="file-text-fill" color="#2979ff" size="48"></uv-icon></div>
<div class="nameBox">
<div class="name">{{item.name}}</div>
<div class="pdfInfoBox">
<div class="size">{{item.size}}MB</div>
<div class="date">{{item.date}}</div>
<!-- <div class="time">{{item.time}}</div> -->
</div>
</div>
</div>
</div>
<uv-back-top :scroll-top="scrollTop"></uv-back-top>
</view>
</template>
<script setup>
import {
ref
} from "vue";
import {
onPageScroll,
onShow
} from "@dcloudio/uni-app";
import RPC from "@/utils/request";
import HeaderCustom from '/components/HeaderCustom/HeaderCustom.vue'
const props = defineProps({
id: [String]
});
console.log('props', props.id)
const list = ref([])
const videoUrl = ref('')
const images = ref([])
const clickTab = item => {
// console.log(item)
videoUrl.value = item.video || ''
images.value = item.images || []
}
const product_images_list = ref([])
const qualification_images_list = ref([])
const product_pdfs_list = ref([])
const openPdf = (item) => {
uni.downloadFile({
url: item.url,
success: function (res) {
var filePath = res.tempFilePath;
uni.openDocument({
filePath: filePath,
showMenu: true,
success: function (res) {
console.log('打开文档成功');
}
});
}
});
}
const getDetail = () => {
RPC.post('/store/detail', {
id: Number(props.id)
}).then(res => {
console.log('/store/detail', res)
// list.value = res.categories?.map(item => {
// return {
// ...item,
// images:item.images ? JSON.parse(item.images) : []
// }
// })
videoUrl.value = res?.store?.video_intro || ''
product_images_list.value = res?.store?.product_images ? res?.store?.product_images.split(',') : []
qualification_images_list.value = res?.store?.qualification_images ? res?.store?.qualification_images.split(',') : []
product_pdfs_list.value = res?.store?.product_pdfs ? JSON.parse(res?.store?.product_pdfs) : []
})
}
onShow(() => getDetail())
const scrollTop = ref(0)
onPageScroll((e) => {
scrollTop.value = e.scrollTop;
})
</script>
<style lang="scss" scoped>
.container {
font-size: 14px;
min-height: 100vh;
background-color: #f8f8f8;
}
.contentBox {
.video {
width: 100%;
}
.img {
width: 100%;
height: auto;
}
}
.pdf_box {
background-color: #fff;
padding: 10px;
border-radius: 10px;
margin-bottom: 14px;
.pdf_item {
display: flex;
border-bottom: 1px solid #ccc;
display: flex;
justify-content: center;
align-items: center;
padding: 10px 0;
position: relative;
.pdf_icon {
display: flex;
align-items: center;
justify-content: center;
width: 16%;
margin-right: 10px;
}
.nameBox {
flex:1;
.name {
font-weight: 700;
margin-bottom: 4px;
}
.pdfInfoBox {
font-size: 12px;
color: #aaa;
display: flex;
align-items: center;
.date {
margin: 0 14px;
}
}
}
}
}
</style>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,362 @@
<template>
<view class="container" ref="backTop">
<HeaderCustom title="门店企业列表" :showBack="true"></HeaderCustom>
<uv-sticky bgColor="#fff" offsetTop="90">
<div class="tabBox">
<div class="tabItem" :class="{selected:active==='门店'}" @click="() => active = '门店'">门店</div>
<div class="tabItem" :class="{selected:active==='企业'}" @click="() => active = '企业'">企业</div>
</div>
</uv-sticky>
<div class="contentBox" v-show="active==='门店'">
<div class="contentItem" v-for="(item,index) in storeList" :key="item.id" @click="goDetail(item.id)">
<div class="statusBox" v-if="item.status == 2">
<uv-icon name="checkmark-circle-fill" color="#6AD307" size="12"></uv-icon>
已通过
</div>
<div class="statusBox reject" v-if="item.status == 3">
<uv-icon name="close-circle-fill" color="#F40404" size="12"></uv-icon>
已驳回
</div>
<div class="statusBox pedding" v-if="item.status == 1">
<uv-icon name="clock-fill" color="#F49843" size="12"></uv-icon>
待审核
</div>
<div class="title">{{item.store_name}}</div>
<div class="infoBox">
<div class="line">
<div class="infoItem">地区:{{item.location}}</div>
<div class="infoItem">地址详情:{{item.address}}</div>
</div>
<div class="line">
<div class="infoItem">电话:{{item.contact_phone}}</div>
<div class="infoItem">工作时间:{{item.business_hours}}</div>
</div>
<div class="line">
<div class="infoItem">业务类型:{{item.tags}}</div>
</div>
<div class="line">
<div class="infoItem">
注册日期:{{item.created_at ? dayjs(item.created_at * 1000).format('YYYY-MM-DD HH:mm:ss') : ''}}
</div>
<div class="infoItem" v-if="item.status == 3">驳回原因:{{item.review_note}}</div>
</div>
</div>
<div class="buttonBox">
<uv-button v-if="item.status != 1" size="small" shape="circle" text="删除" @click="delStore(item)"></uv-button>
<uv-button @click="goAdd(item.id)" customStyle="margin-left:10px" size="small" type="primary" shape="circle" text="修改"
v-if="item.status != 1"></uv-button>
</div>
</div>
<uv-load-more @loadmore="storeGetMore" :status="storeloadingStatus" />
</div>
<div class="contentBox" v-show="active==='企业'">
<div class="contentItem" v-for="(item,index) in businessList" :key="item.id">
<div class="statusBox" v-if="item.status == 2">
<uv-icon name="checkmark-circle-fill" color="#6AD307" size="12"></uv-icon>
已通过
</div>
<div class="statusBox reject" v-if="item.status == 3">
<uv-icon name="close-circle-fill" color="#F40404" size="12"></uv-icon>
已驳回
</div>
<div class="statusBox pedding" v-if="item.status == 1">
<uv-icon name="clock-fill" color="#F49843" size="12"></uv-icon>
待审核
</div>
<div class="title">{{item.company_name}}</div>
<div class="infoBox">
<div class="line">
<div class="infoItem">姓名:{{item.contact_person}}</div>
<div class="infoItem">邮箱:{{item.contact_email}}</div>
</div>
<div class="line">
<div class="infoItem">手机号码:{{item.contact_phone}}</div>
<div class="infoItem">地址:{{item.address}}</div>
</div>
<div class="line">
<div class="infoItem">更新日期:{{item.updated_at ? dayjs(item.updated_at * 1000).format('YYYY-MM-DD HH:mm:ss') : ''}}</div>
<div class="infoItem" v-if="item.status == 3">驳回原因:{{item.review_note}}</div>
</div>
</div>
<div class="buttonBox">
<uv-button size="small" shape="circle" v-if="item.status != 1" text="删除" @click="delStore(item)"></uv-button>
<uv-button v-if="item.status != 1" customStyle="margin-left:10px" size="small" type="primary" shape="circle"
text="修改" @click="goEdit(item.id)"></uv-button>
</div>
</div>
<uv-load-more @loadmore="businessGetMore" :status="businessloadingStatus" />
</div>
<div class="addBox" @click="goAdd(null)" v-if="active==='门店'"><uv-button iconColor="#fff" iconSize="14" icon="plus" type="primary"
shape="circle" text="新增"></uv-button></div>
<uv-modal showCancelButton ref="modal" content='确认删除?' :asyncClose="true" @confirm="confirmDel"></uv-modal>
</view>
</template>
<script setup>
import {
ref
} from "vue";
import dayjs from 'dayjs';
import {
onShow,
onReachBottom
} from "@dcloudio/uni-app";
import RPC from "@/utils/request";
import HeaderCustom from '/components/HeaderCustom/HeaderCustom.vue'
import {
useAuthStore
} from "@/store";
const authStore = useAuthStore()
// const props = defineProps({
// id: [String]
// });
// console.log('props', props.id)
const goAdd = (id) => {
uni.$uv.route({
url: '/pagesA/StoreManage/AddStore',
params: {
id: id,
}
});
}
const goEdit = (id) => {
uni.$uv.route({
url: '/pagesA/StoreManage/businessEdit',
params: {
id: id,
}
});
}
const goDetail = (id) => {
uni.$uv.route({
url: '/pagesA/ProductDetails/ProductDetails',
params: {
id: id,
}
});
};
const active = ref('门店')
const modal = ref(null)
const storeList = ref([])
const storeloadingStatus = ref('loadmore')
const storePage = ref(1)
const storeCount = ref(0)
const businessList = ref([])
const businessloadingStatus = ref('loadmore')
const businessPage = ref(1)
const businessCount = ref(0)
const confirmDel = () => {
if (active.value === '门店') {
RPC.post('/store/delete', {
id: currentItem.value.id
}).then((res) => {
storeList.value = storeList.value.filter(item => item.id !== currentItem.value.id)
uni.showToast({
icon: 'success',
title: '删除成功'
})
modal.value.close()
}).catch(() => {
modal.value.closeLoading()
})
}
if (active.value === '企业') {
RPC.post('/enterprise/delete', {
id: currentItem.value.id
}).then((res) => {
businessList.value = businessList.value.filter(item => item.id !== currentItem.value.id)
uni.showToast({
icon: 'success',
title: '删除成功'
})
modal.value.close()
}).catch(() => {
modal.value.closeLoading()
})
}
}
const currentItem = ref({})
const delStore = item => {
currentItem.value = item
modal.value.open();
}
const getStoreList = () => {
storeloadingStatus.value === 'loading'
RPC.post('/store/list', {
page: storePage.value,
limit: 20
}).then((res) => {
storeList.value = [...storeList.value, ...res.rows || []]
storeCount.value = res.count || 0
if (storeList.value.length >= storeCount.value) {
storeloadingStatus.value = 'nomore'
} else {
storeloadingStatus.value = 'loadmore'
}
}).catch(() => {
storeloadingStatus.value = 'loadmore'
})
}
const storeGetMore = () => {
if (storeList.value.length >= storeCount.value || storeloadingStatus.value === 'loading') return
storeloadingStatus.value = 'loading';
storePage.value = ++storePage.value;
getStoreList()
}
// getStoreList()
const getBusinessList = () => {
businessloadingStatus.value === 'loading'
RPC.post('/enterprise/list', {
page: businessPage.value,
limit: 20
}).then((res) => {
businessList.value = [...businessList.value, ...res.rows || []]
businessCount.value = res.count || 0
if (businessList.value.length >= businessCount.value) {
businessloadingStatus.value = 'nomore'
} else {
businessloadingStatus.value = 'loadmore'
}
}).catch(() => {
businessloadingStatus.value = 'loadmore'
})
}
const businessGetMore = () => {
if (businessList.value.length >= businessCount.value || businessloadingStatus.value === 'loading') return
businessloadingStatus.value = 'loading';
businessPage.value = ++businessPage.value;
getBusinessList()
}
getBusinessList()
onReachBottom(() => {
console.log('shangladaodi')
if (active.value === '门店') {
storeGetMore()
}
if (active.value === '企业') {
businessGetMore()
}
})
onShow(() => {
if (!authStore?.isLoggedIn) {
uni.switchTab({
url: `/pages/index/index`,
});
uni.showToast({
title: "请先登录",
icon: "none",
})
return
}
storeList.value = []
storePage.value = 1
getStoreList()
})
</script>
<style lang="scss" scoped>
.container {
font-size: 14px;
min-height: 100vh;
background-color: #f8f8f8;
}
.tabBox {
height: 50px;
background-color: #fff;
display: flex;
.tabItem {
height: 100%;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
flex: 1;
&.selected {
color: #5479ED;
border-bottom: 1px solid #5479ED;
}
}
}
.contentBox {
padding: 10px 14px 54px;
.contentItem {
position: relative;
background-color: #fff;
padding: 8px 16px;
border-radius: 8px;
margin-bottom: 10px;
box-shadow: 0px 2px 23px 0px rgba(217, 217, 217, 0.5);
.statusBox {
display: flex;
align-items: center;
font-size: 12px;
justify-content: space-between;
padding: 4px 8px 4px 12px;
width: 56px;
color: #6AD307;
background-color: #E4FBE9;
position: absolute;
top: 0;
right: 0;
border-radius: 0 0 0 20px;
&.reject {
color: #F40404;
background-color: #FAE6E3;
}
&.pedding {
color: #F49843;
background-color: #FBF3E5;
}
}
.title {
font-weight: 700;
margin-bottom: 10px;
}
.infoBox {
.line {
display: flex;
margin-bottom: 6px;
.infoItem {
flex: 1;
font-size: 12px;
color: #A7A7A7;
}
}
}
.buttonBox {
display: flex;
justify-content: flex-end;
}
}
}
.addBox {
position: fixed;
width: calc(100% - 28px);
left: 14px;
bottom: 14px;
}
</style>

@ -0,0 +1,363 @@
<template>
<view class="container" ref="backTop">
<HeaderCustom title="门店新增" :showBack="true"></HeaderCustom>
<uv-sticky bgColor="#fff" offsetTop="90">
<div class="tabBox">
<div class="tabItem" :class="{selected:active==='门店'}" @click="() => active = '门店'">门店</div>
<div class="tabItem" :class="{selected:active==='企业'}" @click="() => active = '企业'">企业</div>
</div>
</uv-sticky>
<div class="contentBox" v-show="active==='门店'">
<div class="contentItem" v-for="(item,index) in storeList" :key="item.id">
<div class="title">{{item.store_name}}</div>
<div class="infoBox">
<div class="line">
<div class="infoItem">地区:{{item.location}}</div>
<div class="infoItem">地址详情:{{item.address}}</div>
</div>
<div class="line">
<div class="infoItem">电话:{{item.contact_phone}}</div>
<div class="infoItem">工作时间:{{item.business_hours}}</div>
</div>
<div class="line">
<div class="infoItem">业务类型:{{item.tags}}</div>
</div>
<div class="line">
<div class="infoItem">
注册日期:{{item.created_at ? dayjs(item.created_at * 1000).format('YYYY-MM-DD HH:mm:ss') : ''}}
</div>
</div>
</div>
<div class="buttonBox">
<uv-button size="small" type="error" shape="circle" text="驳回" @click="handleReject(item)"></uv-button>
<uv-button customStyle="margin-left:10px" size="small" type="primary" shape="circle" text="通过" @click="handlePass(item)"></uv-button>
</div>
</div>
<uv-load-more @loadmore="storeGetMore" :status="storeloadingStatus" />
</div>
<div class="contentBox" v-show="active==='企业'">
<div class="contentItem" v-for="(item,index) in businessList" :key="item.id">
<div class="title">{{item.company_name}}</div>
<div class="infoBox">
<div class="line">
<div class="infoItem">姓名:{{item.contact_person}}</div>
<div class="infoItem">邮箱:{{item.contact_email}}</div>
</div>
<div class="line">
<div class="infoItem">手机号码:{{item.contact_phone}}</div>
<div class="infoItem">地址:{{item.address}}</div>
</div>
<div class="line">
<div class="infoItem">更新日期:{{item.updated_at ? dayjs(item.updated_at * 1000).format('YYYY-MM-DD HH:mm:ss') : ''}}</div>
<div class="infoItem" v-if="item.status == 3">驳回原因:{{item.review_note}}</div>
</div>
</div>
<div class="buttonBox">
<uv-button size="small" type="error" shape="circle" text="驳回" @click="handleReject(item)"></uv-button>
<uv-button customStyle="margin-left:10px" size="small" type="primary" shape="circle"
text="通过" @click="handlePass(item)"></uv-button>
</div>
</div>
<uv-load-more @loadmore="businessGetMore" :status="businessloadingStatus" />
</div>
<uv-modal showCancelButton ref="modal" content='确认通过?' :asyncClose="true" @confirm="confirmPass"></uv-modal>
<uv-modal showCancelButton ref="reject_modal" :asyncClose="true" @confirm="confirmReject">
<view class="slot-content">
<uv-input placeholder="请输入驳回原因" border="surround" v-model="reject_note"></uv-input>
</view>
</uv-modal>
</view>
</template>
<script setup>
import {
ref
} from "vue";
import dayjs from 'dayjs';
import {
onShow,
onReachBottom
} from "@dcloudio/uni-app";
import RPC from "@/utils/request";
import HeaderCustom from '/components/HeaderCustom/HeaderCustom.vue'
import {
useAuthStore
} from "@/store";
const authStore = useAuthStore()
// const props = defineProps({
// id: [String]
// });
// console.log('props', props.id)
const active = ref('门店')
const modal = ref(null)
const reject_note = ref('')
const reject_modal = ref(null)
const storeList = ref([])
const storeloadingStatus = ref('loadmore')
const storePage = ref(1)
const storeCount = ref(0)
const businessList = ref([])
const businessloadingStatus = ref('loadmore')
const businessPage = ref(1)
const businessCount = ref(0)
const confirmPass = () => {
if (active.value === '门店') {
RPC.post('/store/review', {
id: currentItem.value.id,
status:2
}).then((res) => {
storeList.value = storeList.value.filter(item => item.id !== currentItem.value.id)
uni.showToast({
icon: 'success',
title: '通过成功'
})
modal.value.close()
}).catch(() => {
modal.value.closeLoading()
})
}
if (active.value === '企业') {
RPC.post('/enterprise/review', {
id: currentItem.value.id,
status:2
}).then((res) => {
businessList.value = businessList.value.filter(item => item.id !== currentItem.value.id)
uni.showToast({
icon: 'success',
title: '通过成功'
})
modal.value.close()
}).catch(() => {
modal.value.closeLoading()
})
}
}
const confirmReject = () => {
if(!reject_note.value) {
uni.showToast({
icon: 'none',
title: '请填写驳回原因'
})
reject_modal.value.closeLoading()
return
}
if (active.value === '门店') {
RPC.post('/store/review', {
id: currentItem.value.id,
status:3,
note:reject_note.value
}).then((res) => {
storeList.value = storeList.value.filter(item => item.id !== currentItem.value.id)
uni.showToast({
icon: 'success',
title: '驳回成功',
})
reject_modal.value.close()
}).catch(() => {
reject_modal.value.closeLoading()
})
}
if (active.value === '企业') {
RPC.post('/enterprise/review', {
id: currentItem.value.id,
status:3,
note:reject_note.value
}).then((res) => {
businessList.value = businessList.value.filter(item => item.id !== currentItem.value.id)
uni.showToast({
icon: 'success',
title: '驳回成功'
})
reject_modal.value.close()
}).catch(() => {
reject_modal.value.closeLoading()
})
}
}
const currentItem = ref({})
const handlePass = item => {
currentItem.value = item
modal.value.open();
}
const handleReject = item => {
reject_note.value = ''
currentItem.value = item
reject_modal.value.open();
}
const getStoreList = () => {
storeloadingStatus.value === 'loading'
RPC.post('/store/pending_list', {
page: storePage.value,
limit: 20
}).then((res) => {
storeList.value = [...storeList.value, ...res.rows || []]
storeCount.value = res.count || 0
if (storeList.value.length >= storeCount.value) {
storeloadingStatus.value = 'nomore'
} else {
storeloadingStatus.value = 'loadmore'
}
}).catch(() => {
storeloadingStatus.value = 'loadmore'
})
}
const storeGetMore = () => {
if (storeList.value.length >= storeCount.value || storeloadingStatus.value === 'loading') return
storeloadingStatus.value = 'loading';
storePage.value = ++storePage.value;
getStoreList()
}
getStoreList()
const getBusinessList = () => {
businessloadingStatus.value === 'loading'
RPC.post('/enterprise/pending', {
page: businessPage.value,
limit: 20
}).then((res) => {
businessList.value = [...businessList.value, ...res.rows || []]
businessCount.value = res.count || 0
if (businessList.value.length >= businessCount.value) {
businessloadingStatus.value = 'nomore'
} else {
businessloadingStatus.value = 'loadmore'
}
}).catch(() => {
businessloadingStatus.value = 'loadmore'
})
}
const businessGetMore = () => {
if (businessList.value.length >= businessCount.value || businessloadingStatus.value === 'loading') return
businessloadingStatus.value = 'loading';
businessPage.value = ++businessPage.value;
getBusinessList()
}
getBusinessList()
onReachBottom(() => {
console.log('shangladaodi')
if (active.value === '门店') {
storeGetMore()
}
if (active.value === '企业') {
businessGetMore()
}
})
onShow(() => {
if (!authStore?.isLoggedIn) {
uni.switchTab({
url: `/pages/index/index`,
});
uni.showToast({
title: "请先登录",
icon: "none",
})
return
}
})
</script>
<style lang="scss" scoped>
.container {
font-size: 14px;
min-height: 100vh;
background-color: #f8f8f8;
}
.tabBox {
height: 50px;
background-color: #fff;
display: flex;
.tabItem {
height: 100%;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
flex: 1;
&.selected {
color: #5479ED;
border-bottom: 1px solid #5479ED;
}
}
}
.contentBox {
padding: 10px 14px 54px;
.contentItem {
position: relative;
background-color: #fff;
padding: 8px 16px;
border-radius: 8px;
margin-bottom: 10px;
box-shadow: 0px 2px 23px 0px rgba(217, 217, 217, 0.5);
.statusBox {
display: flex;
align-items: center;
font-size: 12px;
justify-content: space-between;
padding: 4px 8px 4px 12px;
width: 56px;
color: #6AD307;
background-color: #E4FBE9;
position: absolute;
top: 0;
right: 0;
border-radius: 0 0 0 20px;
&.reject {
color: #F40404;
background-color: #FAE6E3;
}
&.pedding {
color: #F49843;
background-color: #FBF3E5;
}
}
.title {
font-weight: 700;
margin-bottom: 10px;
}
.infoBox {
.line {
display: flex;
margin-bottom: 6px;
.infoItem {
flex: 1;
font-size: 12px;
color: #A7A7A7;
}
}
}
.buttonBox {
display: flex;
justify-content: flex-end;
}
}
}
.addBox {
position: fixed;
width: calc(100% - 28px);
left: 14px;
bottom: 14px;
}
</style>

@ -0,0 +1,341 @@
<template>
<view class="container">
<HeaderCustom title='企业修改' :showBack="true"></HeaderCustom>
<div class="formBox">
<uv-form labelPosition="left" :model="formField" labelWidth="100" :rules="rules" ref="form">
<uv-form-item required label="企业名称" prop="company_name">
<uv-input v-model="formField.company_name" border="none"
placeholder="请输入企业名称">
</uv-input>
</uv-form-item>
<uv-form-item required label="姓名" prop="contact_person">
<uv-input v-model="formField.contact_person" border="none"
placeholder="请输入姓名">
</uv-input>
</uv-form-item>
<uv-form-item required label="手机号码" prop="contact_phone">
<uv-input v-model="formField.contact_phone" border="none"
placeholder="请输入手机号码">
</uv-input>
</uv-form-item>
<uv-form-item required label="邮箱" prop="contact_email">
<uv-input v-model="formField.contact_email" border="none"
placeholder="请输入邮箱">
</uv-input>
</uv-form-item>
<uv-form-item required label="地址" prop="address" @click="openAddressPicker">
<uv-input v-model="formField.address" readonly border="none"
placeholder="请选择地址">
</uv-input>
<uv-picker ref="addressPicker" :columns="addressList" keyName="name" @change="addressChange"
@confirm="confirm">
</uv-picker>
</uv-form-item>
<uv-button type="primary" text="提交" customStyle="margin-top: 10px"
@click="submit"></uv-button>
</uv-form>
</div>
</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 props = defineProps({
id: [String],
});
if (props.id && props.id !== 'null') {
RPC.post("/enterprise/detail", {
id: Number(props.id),
}).then((res) => {
formField.value = {
...res.enterprise || {}
};
});
}
function copyText(text) {
uni.setClipboardData({
data: text,
success: function() {
uni.showToast({
title: '复制成功',
icon: 'success',
});
},
fail: function() {
uni.showToast({
title: '复制失败',
icon: 'none',
});
},
})
}
onShow(() => {
if (!authStore?.isLoggedIn) {
uni.switchTab({
url: `/pages/index/index`,
});
uni.showToast({
title: "请先登录",
icon: "none",
})
return
}
})
const form = ref(null)
const formField = ref({
contact_person: '',
phone: '',
email: '',
address: '',
company_name: '',
})
const pickType = type => {
const index = formField.value.type.findIndex(item => item === type)
if (index >= 0) {
formField.value.type.splice(index, 1)
} else {
formField.value.type.push(type)
}
}
const rules = {
'company_name': {
type: 'string',
required: true,
message: '请填写企业名称',
trigger: ['blur', 'change']
},
'name': {
type: 'string',
required: true,
message: '请填写姓名',
trigger: ['blur', 'change']
},
'contact_phone': {
type: 'string',
required: true,
message: '请填写手机号码',
trigger: ['blur', 'change']
},
'contact_email': {
type: 'string',
required: true,
message: '请填写邮箱',
trigger: ['blur', 'change']
},
'address': {
type: 'string',
required: true,
message: '请选择地址',
trigger: ['blur', 'change']
}
}
const submit = () => {
// 如果有错误,会在catch中返回报错信息数组,校验通过则在then中返回true
form.value.validate().then(res => {
// uni.showToast({
// icon: 'success',
// title: '校验通过'
// })
RPC.post('/enterprise/update', formField.value).then(res => {
uni.showToast({
icon: 'success',
title: '操作成功'
})
setTimeout(() => {
uni.navigateBack()
}, 1000)
})
}).catch(errors => {
// uni.showToast({
// icon: 'error',
// title: '校验失败'
// })
})
}
const provinces = ref([])
const citys = ref([])
const areas = ref([])
const addressList = computed(() => [provinces.value, citys.value, areas.value])
const addressPicker = ref(null)
const pickerValue = ref(['0', '0', '0'])
const defaultValue = ref(['3442', '1', '2'])
const addressChange = e => {
const {
columnIndex,
index,
indexs
} = e
// 改变了省
if (columnIndex === 0) {
citys.value = provinces.value[index]?.children || []
areas.value = citys.value[0]?.children || []
addressPicker.value.setIndexs([index, 0, 0], true)
} else if (columnIndex === 1) {
areas.value = citys.value[index]?.children || []
addressPicker.value.setIndexs(indexs, true)
}
}
const confirm = e => {
// console.log('确认选择的地区:', e);
// uni.showToast({
// icon: 'none',
// title: `${e.value[0].name}/${e.value[1].name}/${e.value[2].name}`
// })
formField.value.address = `${e.value[0].name}/${e.value[1].name}/${e.value[2].name}`
}
const handlePickValueDefault = () => {
pickerValue.value[0] = provinces.value.findIndex(item => Number(item.id) == defaultValue.value[0]);
// 设置市
citys.value = provinces.value[pickerValue.value[0]]?.children || [];
pickerValue.value[1] = citys.value.findIndex(item => Number(item.id) == defaultValue.value[1]);
// 设置区
areas.value = citys.value[pickerValue.value[1]]?.children || [];
pickerValue.value[2] = areas.value.findIndex(item => Number(item.id) == defaultValue.value[2]);
// 重置下位置
addressPicker.value.setIndexs([pickerValue.value[0], pickerValue.value[1], pickerValue.value[2]], true);
}
const openAddressPicker = () => {
addressPicker.value.open()
handlePickValueDefault()
}
const getData = () => {
provinces.value = regions.sort((left, right) => (Number(left.code) > Number(right.code) ?
1 : -1));
handlePickValueDefault()
}
onMounted(() => {
getData()
})
</script>
<style lang="scss" scoped>
.container {
font-size: 14px;
min-height: 100vh;
background-color: #f8f8f8;
}
.titleBox {
background-color: #fff;
padding: 20px 4vw;
.title {
font-size: 16px;
font-weight: 700;
}
.line {
width: 5vw;
height: 2px;
background-color: #3F70FF;
margin-top: 4px;
margin-bottom: 8px;
}
.desc {
font-size: 12px;
font-weight: 500;
line-height: 18px;
}
}
.formBox {
padding: 10px;
:deep(.uv-form) {
background-color: #fff;
padding: 10px;
border-radius: 10px;
.uv-form-item {
background-color: #F4F4F4;
margin-bottom: 10px;
border-radius: 10px;
padding-left: 20px;
}
}
.type_select_box {
display: flex;
flex-wrap: wrap;
gap: 15px;
padding-right: 20px;
margin-top: 10px;
.item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: #fff;
font-size: 12px;
width: 90px;
height: 70px;
border-radius: 6px;
.img {
width: 20px;
height: 20px;
margin-bottom: 6px;
}
&.selected {
border: 1px solid #3F70FF;
background-color: rgba(63, 112, 255, 0.1);
}
}
}
}
.infoBox {
margin-top: 20px;
padding: 10px 5vw;
background-color: #fff;
border-radius: 10px;
font-size: 16px;
.line {
display: flex;
margin: 10px 0;
.key {
width: 70px;
font-weight: 700;
}
}
}
</style>

@ -0,0 +1,457 @@
<template>
<view class="container">
<!-- 顶部导航栏 -->
<view class="nav-bar">
<view class="back-btn" @click="goBack">
<uv-icon name="arrow-left" color="#333" size="20"></uv-icon>
</view>
<view class="title">新增地址</view>
<view class="right-placeholder"></view>
</view>
<!-- 表单内容 -->
<view class="form-content">
<!-- 联系人信息 -->
<view class="form-section">
<view class="form-item">
<text class="label">联系人</text>
<input class="input" v-model="formData.name" placeholder="请输入联系人姓名" />
</view>
<view class="form-item">
<text class="label">手机号码</text>
<input class="input" v-model="formData.phone" type="number" placeholder="请输入手机号码" maxlength="11" />
</view>
</view>
<!-- 地址信息 -->
<view class="form-section">
<view class="form-item location-item" @click="chooseLocation">
<text class="label">地址位置</text>
<view class="location-value">
<text v-if="formData.address">{{ formData.address }}</text>
<text v-else class="placeholder">点击选择位置</text>
<uv-icon name="arrow-right" color="#CCCCCC" size="16"></uv-icon>
</view>
</view>
<view class="form-item">
<text class="label">门牌号</text>
<input class="input" v-model="formData.addressDetail" placeholder="详细地址,如:1号楼1单元101室" />
</view>
</view>
<!-- 地址标签 -->
<view class="form-section">
<view class="label-title">地址标签</view>
<view class="tag-list">
<view
class="tag-item"
:class="{ active: formData.tag === tag }"
v-for="(tag, index) in tags"
:key="index"
@click="selectTag(tag)"
>
{{ tag }}
</view>
<view
class="tag-item custom-tag"
:class="{ active: isCustomTag }"
@click="showCustomTagInput"
>
<text v-if="!isCustomTag">自定义</text>
<input
v-else
class="custom-tag-input"
v-model="formData.customTag"
placeholder="自定义标签"
focus
@blur="onCustomTagBlur"
@confirm="onCustomTagConfirm"
/>
</view>
</view>
</view>
<!-- 默认地址设置 -->
<view class="form-section">
<view class="form-item switch-item">
<text class="label">设为默认地址</text>
<switch
:checked="formData.isDefault"
color="#4080FF"
@change="onDefaultChange"
/>
</view>
</view>
</view>
<!-- 底部保存按钮 -->
<view class="footer">
<view class="save-btn" @click="saveAddress">保存</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue';
import { useUserStore } from "@/store";
// 表单数据
const formData = ref({
name: '',
phone: '',
address: '',
addressDetail: '',
latitude: '',
longitude: '',
tag: '家',
customTag: '',
isDefault: false
});
// 预设标签
const tags = ref(['家', '公司', '学校']);
// 是否使用自定义标签
const isCustomTag = computed(() => {
return !tags.value.includes(formData.value.tag);
});
// 选择标签
const selectTag = (tag) => {
formData.value.tag = tag;
};
// 显示自定义标签输入框
const showCustomTagInput = () => {
if (!isCustomTag.value) {
formData.value.customTag = '';
formData.value.tag = '';
}
};
// 自定义标签输入框失焦
const onCustomTagBlur = () => {
if (formData.value.customTag) {
formData.value.tag = formData.value.customTag;
} else {
formData.value.tag = '家'; // 默认回到第一个标签
}
};
// 自定义标签输入确认
const onCustomTagConfirm = () => {
if (formData.value.customTag) {
formData.value.tag = formData.value.customTag;
}
};
// 默认地址切换
const onDefaultChange = (e) => {
formData.value.isDefault = e.detail.value;
};
// 选择位置
const chooseLocation = () => {
uni.chooseLocation({
success: (res) => {
console.log('选择位置成功:', res);
formData.value.address = res.name || res.address;
formData.value.latitude = res.latitude;
formData.value.longitude = res.longitude;
},
fail: (err) => {
console.error('选择位置失败:', err);
// 检查是否是因为权限问题
if (err.errMsg.includes('auth deny')) {
uni.showModal({
title: '提示',
content: '需要您授权使用位置信息',
confirmText: '去设置',
success: (res) => {
if (res.confirm) {
uni.openSetting({
success: (settingRes) => {
console.log('设置结果:', settingRes);
}
});
}
}
});
} else {
uni.showToast({
title: '选择位置失败',
icon: 'none'
});
}
}
});
};
// 保存地址
const saveAddress = () => {
// 表单验证
if (!formData.value.name) {
return uni.showToast({
title: '请输入联系人姓名',
icon: 'none'
});
}
if (!formData.value.phone) {
return uni.showToast({
title: '请输入手机号码',
icon: 'none'
});
}
// 简单的手机号验证
if (!/^1\d{10}$/.test(formData.value.phone)) {
return uni.showToast({
title: '请输入正确的手机号码',
icon: 'none'
});
}
if (!formData.value.address) {
return uni.showToast({
title: '请选择地址位置',
icon: 'none'
});
}
if (!formData.value.addressDetail) {
return uni.showToast({
title: '请输入详细地址',
icon: 'none'
});
}
// 构建地址对象
const addressObj = {
id: Date.now().toString(), // 使用时间戳作为临时ID
name: formData.value.name,
phone: formData.value.phone,
address: formData.value.address,
addressDetail: formData.value.addressDetail,
latitude: formData.value.latitude,
longitude: formData.value.longitude,
tag: formData.value.tag,
isDefault: formData.value.isDefault
};
// 获取已保存的地址列表
let addressList = [];
try {
const savedAddresses = uni.getStorageSync('userAddressList');
if (savedAddresses) {
addressList = JSON.parse(savedAddresses);
}
} catch (e) {
console.error('获取地址列表失败', e);
}
// 如果设置为默认地址,需要将其他地址设为非默认
if (addressObj.isDefault) {
addressList.forEach(item => {
item.isDefault = false;
});
}
// 添加新地址
addressList.push(addressObj);
// 保存地址列表
try {
uni.setStorageSync('userAddressList', JSON.stringify(addressList));
// 如果是默认地址,更新当前使用的地址
if (addressObj.isDefault) {
useUserStore().setAdressInfo({
latitude: addressObj.latitude,
longitude: addressObj.longitude,
adress: addressObj.address,
addressDetail: addressObj.addressDetail
});
}
uni.showToast({
title: '保存成功',
icon: 'success'
});
// 延迟返回上一页
setTimeout(() => {
uni.navigateBack({
delta: 1
});
}, 1500);
} catch (e) {
console.error('保存地址失败', e);
uni.showToast({
title: '保存失败,请重试',
icon: 'none'
});
}
};
// 返回上一页
const goBack = () => {
uni.navigateBack({
delta: 1
});
};
</script>
<style lang="scss" scoped>
.container {
font-size: 14px;
min-height: 100vh;
background-color: #f8f8f8;
display: flex;
flex-direction: column;
}
.nav-bar {
display: flex;
justify-content: space-between;
align-items: center;
height: 90rpx;
background-color: #FFFFFF;
padding: 0 30rpx;
position: relative;
.back-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
}
.title {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
font-size: 32rpx;
font-weight: bold;
}
.right-placeholder {
width: 60rpx;
}
}
.form-content {
flex: 1;
padding-bottom: 120rpx;
}
.form-section {
background-color: #FFFFFF;
margin-top: 20rpx;
padding: 0 30rpx;
.label-title {
padding: 30rpx 0 20rpx;
font-size: 28rpx;
color: #333333;
}
.form-item {
display: flex;
align-items: center;
min-height: 100rpx;
border-bottom: 1px solid #F5F5F5;
&:last-child {
border-bottom: none;
}
.label {
width: 160rpx;
font-size: 28rpx;
color: #333333;
}
.input {
flex: 1;
height: 100rpx;
font-size: 28rpx;
}
&.location-item {
.location-value {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 28rpx;
.placeholder {
color: #999999;
}
}
}
&.switch-item {
justify-content: space-between;
}
}
.tag-list {
display: flex;
flex-wrap: wrap;
padding: 20rpx 0 30rpx;
.tag-item {
padding: 10rpx 30rpx;
background-color: #F5F5F5;
border-radius: 30rpx;
margin-right: 20rpx;
margin-bottom: 20rpx;
font-size: 26rpx;
color: #666666;
&.active {
background-color: #E6F2FF;
color: #4080FF;
border: 1px solid #4080FF;
}
&.custom-tag {
display: flex;
align-items: center;
justify-content: center;
min-width: 120rpx;
.custom-tag-input {
width: 120rpx;
text-align: center;
height: 40rpx;
}
}
}
}
}
.footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #FFFFFF;
padding: 20rpx 30rpx;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
.save-btn {
height: 80rpx;
background-color: #4080FF;
color: #FFFFFF;
border-radius: 40rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
}
}
</style>

@ -0,0 +1,501 @@
<template>
<view class="container">
<HeaderCustom title="选择位置" :showBack="true"></HeaderCustom>
<!-- 搜索栏 -->
<view class="search-bar">
<view class="search-input" @click="addNewAddress">
<uv-icon name="search" color="#999" size="16"></uv-icon>
<text class="placeholder">请输入地址</text>
</view>
</view>
<!-- 当前地址 -->
<view class="section">
<view class="section-header">
<uv-icon name="map" color="#AAAAAA" size="18"></uv-icon>
<text class="section-title">当前地址</text>
</view>
<view class="current-location">
<text class="location-name">{{ useUserStore().adressInfo?.adress }}</text>
<view class="relocate-btn" @click="relocate">
<uv-icon name="reload" color="#4080FF" size="16"></uv-icon>
<text class="relocate-text">重新定位</text>
</view>
</view>
</view>
<!-- 我的服务地址 -->
<view class="section">
<view class="section-header">
<uv-icon name="home" color="#AAAAAA" size="18"></uv-icon>
<text class="section-title">我的服务地址</text>
</view>
<view class="address-list">
<view class="address-item" v-for="(item, index) in myAdress" :key="index" @click="selectAddress(item)">
<view class="address-icon">
<uv-icon name="map-fill" color="#CCCCCC" size="20"></uv-icon>
</view>
<view class="address-info">
<view class="address-name">{{ item.name }}</view>
<view class="address-detail">{{ item.address }}</view>
</view>
<div class="delBox" @click.stop="delAdress(item)">删除</div>
<!-- <view class="address-distance">{{ item.distance }}</view> -->
</view>
</view>
<view class="address-item add-address" @click="addNewAddress('add')">
<text class="add-text">新增地址</text>
<uv-icon name="arrow-right" color="#CCCCCC" size="16"></uv-icon>
</view>
</view>
<!-- 附近地址 -->
<!-- <view class="section">
<view class="section-header">
<uv-icon name="map" color="#AAAAAA" size="18"></uv-icon>
<text class="section-title">附近地址</text>
</view>
<view class="address-list">
<view class="address-item" v-for="(item, index) in nearbyAddresses" :key="index"
@click="selectAddress(item)">
<view class="address-icon">
<uv-icon name="map-fill" color="#CCCCCC" size="20"></uv-icon>
</view>
<view class="address-info">
<view class="address-name">{{ item.name }}</view>
<view class="address-detail">{{ item.address }}</view>
</view>
<view class="address-distance">{{ item.distance }}</view>
</view>
</view>
</view> -->
</view>
</template>
<script setup>
import {
ref,
onMounted
} from "vue";
import {
onShow,
onPageScroll
} from "@dcloudio/uni-app";
import {
useUserStore
} from "@/store";
import HeaderCustom from '/components/HeaderCustom/HeaderCustom.vue'
import RPC from "@/utils/request";
// 当前位置
// const currentLocation = ref(useUserStore().adressInfo.address);
// 附近地址列表
const nearbyAddresses = ref([{
"name": "三江半岛",
"address": "四川省绵阳市涪城区滨河北路东段298号",
latitude: 31.45467,
longitude: 104.767,
"distance": '942m'
}, {
"name": "建设街1号院",
"address": "四川省绵阳市涪城区建设街1号",
latitude: 31.454278,
longitude: 104.756637,
"distance": '88m'
}, {
"name": "四川省绵阳第一中学",
"address": "四川省绵阳市涪城区警钟街78号",
latitude: 31.457096,
longitude: 104.758555,
"distance": '174m'
}]);
const myAdress = ref([])
// 重新定位
function relocate() {
uni.showLoading({
title: "定位中...",
});
uni.getLocation({
type: "wgs84",
isHighAccuracy: true,
success: function(res) {
console.log("当前位置的经度:" + res.longitude);
console.log("当前位置的纬度:" + res.latitude);
// 调用腾讯地图API进行逆地址解析
uni.request({
url: `https://apis.map.qq.com/ws/geocoder/v1/`,
method: "GET",
data: {
location: `${res.latitude},${res.longitude}`,
key: "N6LBZ-JLDCT-SQNXQ-LQUWR-6PQX7-TQBQN",
},
success: (res1) => {
uni.hideLoading();
if (res1.data.status === 0) {
const addressComponent = res1.data.result.address_component;
// 更新用户存储的地址信息
useUserStore().setAdressInfo({
latitude: res.latitude,
longitude: res.longitude,
adress: addressComponent.street_number || addressComponent
.street,
});
// 获取附近地址
getNearbyAddresses(res.latitude, res.longitude);
} else {
uni.showToast({
title: "定位失败",
icon: "none",
});
console.error("逆解析失败", res1.data.message);
}
},
fail: (err) => {
uni.hideLoading();
uni.showToast({
title: "网络错误",
icon: "none",
});
console.error("网络错误", err);
},
});
},
fail: function(err) {
uni.hideLoading();
uni.showToast({
title: "定位失败,请检查定位权限",
icon: "none",
});
console.error("定位失败", err);
},
});
}
// 获取附近地址
function getNearbyAddresses(latitude, longitude) {
// 这里可以调用地图API获取附近的地址
console.log("获取附近地址:", latitude, longitude);
// 模拟获取附近地址
// 实际应用中应该调用地图POI搜索API
uni.request({
url: 'https://apis.map.qq.com/ws/place/v1/explore',
method: 'GET',
data: {
key: 'N6LBZ-JLDCT-SQNXQ-LQUWR-6PQX7-TQBQN',
boundary: `nearby(${latitude},${longitude},1000,1)`,
},
success: (res) => {
if (res.data.status === 0) {
const pois = res.data.data
console.log('附近地点列表:', res)
nearbyAddresses.value = res?.data?.data?.map(item => {
return {
name: item.title,
address: item.address,
distance: item._distance + 'm',
latitude: item.location.lat,
longitude: item.location.lng
}
})
// pois 是一个包含附近地点的数组
} else {
console.error('搜索失败', res.data.message)
}
},
fail: (err) => {
console.error('请求失败', err)
}
})
}
// 选择地址
function selectAddress(item) {
// 将选择的地址保存到store
// useUserStore().setAdressInfo({
// adress: address.name,
// addressDetail: address.address,
// // 这里应该有经纬度信息,示例中省略
// });
// console.log(item)
useUserStore().setAdressInfo({
latitude: item.latitude,
longitude: item.longitude,
adress: item.name,
});
// // 返回上一页
uni.navigateBack();
}
const getMyAdress = () => {
RPC.post('/address/list',{limit:-1}).then(res => {
myAdress.value = res.rows || []
})
}
getMyAdress()
const delAdress = item => {
RPC.post('/address/delete',{id:item.id}).then(res => {
uni.showToast({
title:"删除成功"
})
getMyAdress()
})
}
// 添加新地址
function addNewAddress(type) {
uni.chooseLocation({
success: (res) => {
console.log('选择位置成功:', res);
if (type === 'add') {
const params = {
"name": res.name,
"address": res.address,
latitude: res.latitude,
longitude: res.longitude,
}
RPC.post('/address/create',params).then(() => {
getMyAdress()
})
} else {
useUserStore().setAdressInfo({
latitude: res.latitude,
longitude: res.longitude,
adress: res.name,
});
}
},
fail: (err) => {
console.error('选择位置失败:', err);
// 检查是否是因为权限问题
if (err.errMsg.includes('auth deny')) {
uni.showModal({
title: '提示',
content: '需要您授权使用位置信息',
confirmText: '去设置',
success: (res) => {
if (res.confirm) {
uni.openSetting({
success: (settingRes) => {
console.log('设置结果:', settingRes);
}
});
}
}
});
} else {
uni.showToast({
title: '选择位置失败',
icon: 'none'
});
}
}
});
}
// 跳转到搜索页面
function goToSearch() {
uni.navigateTo({
url: "/pagesA/search-address/search-address",
});
}
// 页面加载时获取当前位置
onShow(() => {
const adressInfo = useUserStore().adressInfo;
console.log("onShow adressInfo:", adressInfo);
if (!adressInfo?.adress) {
relocate();
} else {
if (nearbyAddresses.value.length <= 0) {
getNearbyAddresses(adressInfo.latitude, adressInfo.longitude)
}
}
});
</script>
<style lang="scss" scoped>
.container {
font-size: 14px;
min-height: 100vh;
background-color: #f8f8f8;
padding-bottom: 30rpx;
box-sizing: border-box;
}
.search-bar {
background-color: #eeeeee;
padding: 20rpx 30rpx;
display: flex;
align-items: center;
border-radius: 40rpx;
margin: 20rpx;
.city-selector {
display: flex;
align-items: center;
padding-right: 20rpx;
border-right: 1px solid #dddddd;
margin-right: 20rpx;
text {
font-size: 28rpx;
color: #333333;
margin-right: 10rpx;
}
.arrow-down {
width: 0;
height: 0;
border-left: 10rpx solid transparent;
border-right: 10rpx solid transparent;
border-top: 10rpx solid #666666;
}
}
.search-input {
flex: 1;
display: flex;
align-items: center;
.placeholder {
color: #999999;
margin-left: 10rpx;
font-size: 28rpx;
}
}
}
.section {
background-color: #ffffff;
margin-bottom: 20rpx;
.section-header {
display: flex;
align-items: center;
padding: 20rpx 30rpx;
border-bottom: 1px solid #f5f5f5;
.section-title {
color: #999999;
margin-left: 10rpx;
font-size: 28rpx;
}
}
.current-location {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
.location-name {
font-size: 32rpx;
font-weight: bold;
color: #333333;
}
.relocate-btn {
display: flex;
align-items: center;
color: #4080ff;
.relocate-text {
margin-left: 10rpx;
font-size: 28rpx;
}
}
}
.address-item {
display: flex;
align-items: center;
padding: 30rpx;
border-bottom: 1px solid #f5f5f5;
&.add-address {
color: #4080ff;
justify-content: space-between;
.add-text {
font-size: 30rpx;
}
}
.address-icon {
margin-right: 20rpx;
}
.address-info {
flex: 1;
.address-name {
font-size: 30rpx;
color: #333333;
margin-bottom: 10rpx;
}
.address-detail {
font-size: 26rpx;
color: #999999;
}
}
.delBox {
color: #f12323;
font-size: 26rpx;
}
.address-distance {
color: #999999;
font-size: 26rpx;
}
}
}
.city-picker {
background-color: #ffffff;
border-radius: 20rpx 20rpx 0 0;
padding-bottom: 30rpx;
.city-picker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1px solid #f5f5f5;
text {
font-size: 32rpx;
font-weight: bold;
}
}
.city-list {
padding: 20rpx;
display: flex;
flex-wrap: wrap;
.city-item {
width: 25%;
text-align: center;
padding: 20rpx 0;
font-size: 28rpx;
}
}
}
</style>

@ -0,0 +1,44 @@
<template>
<view class="container">
<HeaderCustom title="隐私政策" :showBack="true"></HeaderCustom>
<div class="contentBox">
<div class="part">
<div class="line">
本应用非常重视用户隐私政策并严格遵守相关的法律规定。请您仔细阅读《隐私政策》后再继续使用。
</div>
<div class="line">
如果您继续使用我们的服务,表示您已经充分阅读和理解我们协议的全部内容。
</div>
</div>
<div class="part">
<div class="line">本小程序尊重并保护所有使用服务用户的个人隐私权。</div>
<div class="line">为了给您提供更准确、更优质的服务,本应用会按照本隐私权政策的规定使用和披露您的个人信息。</div>
<div class="line">除本隐私权政策另有规定外,在未征得您事先许可的情况下,本应用不会将这些信息对外披露或向第三方提供</div>
<div class="line">本应用会不时更新本隐私权政策。您在同意本应用服务使用协议之时,即视为您已经同意本隐私权政策全部内容。</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,453 @@
<template>
<view class="container">
<!-- 搜索栏 -->
<view class="search-header">
<view class="search-box">
<uv-icon name="search" color="#999" size="16"></uv-icon>
<input
class="search-input"
v-model="searchKeyword"
placeholder="请输入地址"
confirm-type="search"
focus
@input="onSearchInput"
@confirm="onSearchConfirm"
/>
<uv-icon
v-if="searchKeyword"
name="close"
color="#999"
size="16"
@click="clearSearch"
></uv-icon>
</view>
<view class="cancel-btn" @click="goBack">取消</view>
</view>
<!-- 搜索历史 -->
<view class="section" v-if="!searchKeyword && searchHistory.length > 0">
<view class="section-header">
<text class="section-title">搜索历史</text>
<uv-icon
name="trash"
color="#999"
size="16"
@click="clearHistory"
></uv-icon>
</view>
<view class="history-list">
<view
class="history-item"
v-for="(item, index) in searchHistory"
:key="index"
@click="useHistoryItem(item)"
>
<uv-icon name="time" color="#999" size="16"></uv-icon>
<text class="history-text">{{ item }}</text>
</view>
</view>
</view>
<!-- 搜索结果 -->
<view class="search-results" v-if="searchKeyword">
<view class="loading-box" v-if="isSearching">
<uv-loading-icon></uv-loading-icon>
<text>搜索中...</text>
</view>
<view
v-else-if="searchResults.length === 0 && searchKeyword"
class="no-result"
>
<text>没有找到相关地址</text>
</view>
<view v-else class="result-list">
<view
class="result-item"
v-for="(item, index) in searchResults"
:key="index"
@click="selectAddress(item)"
>
<view class="result-icon">
<uv-icon name="map-fill" color="#4080FF" size="20"></uv-icon>
</view>
<view class="result-info">
<view class="result-name">{{ item.name }}</view>
<view class="result-address">{{ item.address }}</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, watch,onMounted } from "vue";
import { useUserStore } from "@/store";
// 搜索关键词
const searchKeyword = ref("");
// 搜索历史
const searchHistory = ref([]);
// 搜索结果
const searchResults = ref([]);
// 是否正在搜索
const isSearching = ref(false);
// 从本地存储加载搜索历史
const loadSearchHistory = () => {
try {
const history = uni.getStorageSync("searchAddressHistory");
if (history) {
searchHistory.value = JSON.parse(history);
}
} catch (e) {
console.error("加载搜索历史失败", e);
}
};
// 保存搜索历史到本地存储
const saveSearchHistory = () => {
try {
uni.setStorageSync(
"searchAddressHistory",
JSON.stringify(searchHistory.value)
);
} catch (e) {
console.error("保存搜索历史失败", e);
}
};
// 添加搜索历史
const addToHistory = (keyword) => {
// 如果已存在,先移除
const index = searchHistory.value.indexOf(keyword);
if (index !== -1) {
searchHistory.value.splice(index, 1);
}
// 添加到最前面
searchHistory.value.unshift(keyword);
// 限制历史记录数量
if (searchHistory.value.length > 10) {
searchHistory.value.pop();
}
// 保存到本地存储
saveSearchHistory();
};
// 清空搜索历史
const clearHistory = () => {
uni.showModal({
title: "提示",
content: "确定要清空搜索历史吗?",
success: (res) => {
if (res.confirm) {
searchHistory.value = [];
saveSearchHistory();
}
},
});
};
// 使用历史记录项
const useHistoryItem = (keyword) => {
searchKeyword.value = keyword;
searchAddress(keyword);
};
// 清空搜索框
const clearSearch = () => {
searchKeyword.value = "";
searchResults.value = [];
};
// 搜索地址
const searchAddress = (keyword) => {
if (!keyword.trim()) {
searchResults.value = [];
return;
}
isSearching.value = true;
// 这里应该调用地图API进行地址搜索
// 示例使用模拟数据
setTimeout(() => {
// 模拟搜索结果
if (keyword.includes("上海") || keyword.includes("天河")) {
searchResults.value = [
{
name: "通力电梯(上海旗舰店)",
address: "上海天河区珠江城洋村路11号",
latitude: 31.2304,
longitude: 121.4737,
},
{
name: "上海中心大厦",
address: "上海市浦东新区陆家嘴环路479号",
latitude: 31.2338,
longitude: 121.5079,
},
{
name: "上海天河城",
address: "上海市徐汇区天钥桥路123号",
latitude: 31.1898,
longitude: 121.4326,
},
];
} else if (keyword.includes("北京")) {
searchResults.value = [
{
name: "北京通力电梯有限公司",
address: "北京市朝阳区建国路88号",
latitude: 39.9042,
longitude: 116.4074,
},
{
name: "北京国贸中心",
address: "北京市朝阳区建国门外大街1号",
latitude: 39.9088,
longitude: 116.4614,
},
];
} else {
searchResults.value = [];
}
isSearching.value = false;
// 添加到搜索历史
if (searchResults.value.length > 0) {
addToHistory(keyword);
}
}, 800);
// 实际应用中应该使用类似下面的代码调用地图API
/*
uni.request({
url: 'https://apis.map.qq.com/ws/place/v1/search',
method: 'GET',
data: {
keyword: keyword,
boundary: 'region(上海,0)', // 可以根据当前城市动态设置
key: 'YOUR_MAP_KEY'
},
success: (res) => {
isSearching.value = false;
if (res.data.status === 0 && res.data.data.length > 0) {
searchResults.value = res.data.data.map(item => ({
name: item.title,
address: item.address,
latitude: item.location.lat,
longitude: item.location.lng
}));
// 添加到搜索历史
addToHistory(keyword);
} else {
searchResults.value = [];
}
},
fail: (err) => {
isSearching.value = false;
console.error('搜索地址失败', err);
uni.showToast({
title: '搜索失败,请重试',
icon: 'none'
});
}
});
*/
};
// 监听搜索输入
const onSearchInput = () => {
if (searchKeyword.value) {
// 可以实现输入时实时搜索
// 这里为了性能考虑,不实时搜索,而是在用户停止输入一段时间后搜索
clearTimeout(window.searchTimeout);
window.searchTimeout = setTimeout(() => {
searchAddress(searchKeyword.value);
}, 500);
} else {
searchResults.value = [];
}
};
// 搜索确认
const onSearchConfirm = () => {
if (searchKeyword.value.trim()) {
searchAddress(searchKeyword.value);
}
};
// 选择地址
const selectAddress = (address) => {
// 将选择的地址保存到store
useUserStore().setAdressInfo({
latitude: address.latitude,
longitude: address.longitude,
adress: address.name,
addressDetail: address.address,
});
// 返回上一页
uni.navigateBack({
delta: 1,
});
};
// 返回上一页
const goBack = () => {
uni.navigateBack({
delta: 1,
});
};
// 页面加载时
onMounted(() => {
loadSearchHistory();
});
</script>
<style lang="scss" scoped>
.container {
font-size: 14px;
min-height: 100vh;
background-color: #f8f8f8;
}
.search-header {
display: flex;
align-items: center;
padding: 20rpx;
background-color: #ffffff;
.search-box {
flex: 1;
display: flex;
align-items: center;
background-color: #f5f5f5;
border-radius: 30rpx;
padding: 10rpx 20rpx;
.search-input {
flex: 1;
height: 60rpx;
margin: 0 20rpx;
font-size: 28rpx;
}
}
.cancel-btn {
padding: 0 20rpx;
font-size: 28rpx;
color: #333333;
}
}
.section {
background-color: #ffffff;
margin-top: 20rpx;
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 30rpx;
border-bottom: 1px solid #f5f5f5;
.section-title {
font-size: 28rpx;
color: #333333;
}
}
.history-list {
padding: 0 30rpx;
.history-item {
display: flex;
align-items: center;
padding: 20rpx 0;
border-bottom: 1px solid #f5f5f5;
.history-text {
margin-left: 20rpx;
font-size: 28rpx;
color: #333333;
}
&:last-child {
border-bottom: none;
}
}
}
}
.search-results {
background-color: #ffffff;
margin-top: 20rpx;
min-height: 200rpx;
.loading-box {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40rpx 0;
text {
margin-top: 20rpx;
font-size: 28rpx;
color: #999999;
}
}
.no-result {
display: flex;
justify-content: center;
align-items: center;
padding: 60rpx 0;
color: #999999;
font-size: 28rpx;
}
.result-list {
.result-item {
display: flex;
padding: 20rpx 30rpx;
border-bottom: 1px solid #f5f5f5;
.result-icon {
margin-right: 20rpx;
display: flex;
align-items: center;
}
.result-info {
flex: 1;
.result-name {
font-size: 30rpx;
color: #333333;
margin-bottom: 10rpx;
}
.result-address {
font-size: 26rpx;
color: #999999;
}
}
&:last-child {
border-bottom: none;
}
}
}
}
</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: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

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 返回顶部

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

Loading…
Cancel
Save