This commit is contained in:
Your Name
2026-03-01 14:17:59 +08:00
parent 65aa12f146
commit a07e844c47
6071 changed files with 777651 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
# 请求域名
VITE_APP_BASE_URL=''
+3
View File
@@ -0,0 +1,3 @@
# 请求域名
VITE_APP_BASE_URL=''
+39
View File
@@ -0,0 +1,39 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
ignorePatterns: ['src/uni_modules/'],
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript/recommended',
'@vue/eslint-config-prettier'
],
rules: {
'prettier/prettier': [
'warn',
{
semi: false,
singleQuote: true,
printWidth: 100,
proseWrap: 'preserve',
bracketSameLine: false,
endOfLine: 'auto',
tabWidth: 4,
useTabs: false,
trailingComma: 'none'
}
],
'vue/multi-word-component-names': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'no-undef': 'off',
'vue/prefer-import-from-vue': 'off',
'no-prototype-builtins': 'off',
'prefer-spread': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off'
},
globals: {}
}
+32
View File
@@ -0,0 +1,32 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.hbuilderx
# .env
.env.development
.env.production
+5
View File
@@ -0,0 +1,5 @@
.vscode
.idea
dist/
node_modules/
src/uni_modules
+11
View File
@@ -0,0 +1,11 @@
{
"semi": false,
"singleQuote": true,
"printWidth": 100,
"proseWrap": "preserve",
"bracketSameLine": false,
"endOfLine": "lf",
"tabWidth": 4,
"useTabs": false,
"trailingComma": "none"
}
+3
View File
@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}
+11
View File
@@ -0,0 +1,11 @@
{
"editor.detectIndentation": false,
"editor.tabSize": 4,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"css.validate": false,
"less.validate": false,
"scss.validate": false
}
+20
View File
@@ -0,0 +1,20 @@
<!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="/src/main.ts"></script>
</body>
</html>
+15381
View File
File diff suppressed because it is too large Load Diff
+93
View File
@@ -0,0 +1,93 @@
{
"name": "uni-preset-vue",
"version": "0.0.0",
"scripts": {
"dev": "node scripts/develop.js",
"dev:app": "uni -p app",
"dev:custom": "uni -p",
"dev:h5": "uni",
"dev:h5:ssr": "uni --ssr",
"dev:mp-alipay": "uni -p mp-alipay",
"dev:mp-baidu": "uni -p mp-baidu",
"dev:mp-kuaishou": "uni -p mp-kuaishou",
"dev:mp-lark": "uni -p mp-lark",
"dev:mp-qq": "uni -p mp-qq",
"dev:mp-toutiao": "uni -p mp-toutiao",
"dev:mp-weixin": "uni -p mp-weixin",
"dev:quickapp-webview": "uni -p quickapp-webview",
"dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
"dev:quickapp-webview-union": "uni -p quickapp-webview-union",
"build": "node scripts/publish.js",
"build:app": "uni build -p app",
"build:custom": "uni build -p",
"build:h5": "uni build && node scripts/release.mjs",
"build:h5:ssr": "uni build --ssr",
"build:mp-alipay": "uni build -p mp-alipay",
"build:mp-baidu": "uni build -p mp-baidu",
"build:mp-kuaishou": "uni build -p mp-kuaishou",
"build:mp-lark": "uni build -p mp-lark",
"build:mp-qq": "uni build -p mp-qq",
"build:mp-toutiao": "uni build -p mp-toutiao",
"build:mp-weixin": "uni build -p mp-weixin",
"build:quickapp-webview": "uni build -p quickapp-webview",
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
"build:quickapp-webview-union": "uni build -p quickapp-webview-union",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
},
"dependencies": {
"@dcloudio/uni-app": "3.0.0-3070920230324001",
"@dcloudio/uni-app-plus": "3.0.0-3070920230324001",
"@dcloudio/uni-components": "3.0.0-3070920230324001",
"@dcloudio/uni-h5": "3.0.0-3070920230324001",
"@dcloudio/uni-mp-alipay": "3.0.0-3070920230324001",
"@dcloudio/uni-mp-baidu": "3.0.0-3070920230324001",
"@dcloudio/uni-mp-jd": "3.0.0-3070920230324001",
"@dcloudio/uni-mp-kuaishou": "3.0.0-3070920230324001",
"@dcloudio/uni-mp-lark": "3.0.0-3070920230324001",
"@dcloudio/uni-mp-qq": "3.0.0-3070920230324001",
"@dcloudio/uni-mp-toutiao": "3.0.0-3070920230324001",
"@dcloudio/uni-mp-weixin": "3.0.0-3070920230324001",
"@dcloudio/uni-quickapp-webview": "3.0.0-3070920230324001",
"@dcloudio/uni-webview-js": "0.0.3",
"css-color-function": "1.3.3",
"lodash-es": "4.17.21",
"pinia": "2.0.20",
"uniapp-router-next": "1.2.7",
"uniapp-router-next-zm": "1.0.1",
"vconsole": "3.14.6",
"vue": "3.2.45",
"vue-i18n": "9.1.9",
"weixin-js-sdk": "1.6.0",
"z-paging": "2.7.6"
},
"devDependencies": {
"@dcloudio/types": "3.3.2",
"@dcloudio/uni-automator": "3.0.0-3070920230324001",
"@dcloudio/uni-cli-shared": "3.0.0-3070920230324001",
"@dcloudio/uni-stacktracey": "3.0.0-3070920230324001",
"@dcloudio/vite-plugin-uni": "3.0.0-3070920230324001",
"@rushstack/eslint-patch": "1.1.4",
"@types/lodash-es": "4.17.6",
"@types/node": "18.7.16",
"@vue/eslint-config-prettier": "7.0.0",
"@vue/eslint-config-typescript": "11.0.0",
"autoprefixer": "10.4.8",
"eslint": "8.22.0",
"eslint-plugin-vue": "9.4.0",
"execa": "6.1.0",
"fs-extra": "10.1.0",
"postcss": "8.4.16",
"postcss-rem-to-responsive-pixel": "5.1.3",
"prettier": "2.7.1",
"sass": "1.54.5",
"tailwindcss": "3.3.2",
"typescript": "4.7.4",
"unplugin-uni-router": "1.2.7",
"vite": "4.1.4",
"weapp-tailwindcss-webpack-plugin": "1.12.8"
},
"engines": {
"node": ">=18.19.0"
},
"engine-strict": true
}
+105
View File
@@ -0,0 +1,105 @@
const { spawn } = require('child_process')
const readline = require('readline')
class DevelopClientScript {
constructor() {
if (DevelopClientScript.instance) {
return DevelopClientScript.instance
}
DevelopClientScript.instance = this
}
promptUser(question) {
return new Promise((resolve) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
rl.question(question, (res) => {
resolve(res)
rl.close()
})
})
}
async runClient() {
console.error('请选择你需要运行的客户端(回复数字后回车)')
console.error('0.取消')
console.error('1.微信小程序')
console.error('2.公众号或者H5')
const runClientRes = await this.promptUser('请输入运行的客户端:')
switch (runClientRes) {
case '0':
break
case '1':
await this.runNpmScript('dev:mp-weixin')
break
case '2':
await this.runNpmScript('dev:h5')
break
default:
await this.runClient()
break
}
}
runNpmScript(scriptName) {
return new Promise((resolve, reject) => {
const isWindows = process.platform === 'win32'
const command = isWindows ? 'cmd.exe' : 'npm'
const args = isWindows
? ['/c', 'npm', 'run', scriptName]
: ['run', scriptName]
const runProcess = spawn(command, args)
runProcess.stdout.on('data', (data) => {
console.log(data.toString())
})
runProcess.stderr.on('data', (data) => {
console.error(data.toString())
})
runProcess.on('close', (code) => {
if (code !== 0) {
reject(
new Error(
`运行错误,请查看以下报错信息寻找解决方法: ${error.message}`
)
)
} else {
resolve()
}
})
})
}
async run(targetVersion) {
const currentVersion = process.versions.node
if (currentVersion < targetVersion) {
throw new Error(
`你的当前node版本为(${currentVersion}),需要安装目标版本为 ${targetVersion} 以上!!`
)
}
await this.runClient()
}
static getInstance() {
if (!DevelopClientScript.instance) {
DevelopClientScript.instance = new DevelopClientScript()
}
return DevelopClientScript.instance
}
}
;(async () => {
const develop = DevelopClientScript.getInstance()
try {
await develop.run('16.16.0')
} catch (error) {
console.error(error.message)
}
})()
+1
View File
@@ -0,0 +1 @@
const { spawn } = require('child_process')
+35
View File
@@ -0,0 +1,35 @@
import path from 'path'
import fsExtra from 'fs-extra'
const { existsSync, remove, copy } = fsExtra
const cwd = process.cwd()
//打包发布路径,谨慎改动
const releaseRelativePath = '../server/public/mobile'
const distPath = path.resolve(cwd, 'dist/build/h5')
const releasePath = path.resolve(cwd, releaseRelativePath)
async function build() {
if (existsSync(releasePath)) {
await remove(releasePath)
}
console.log(`文件正在复制 ==> ${releaseRelativePath}`)
try {
await copyFile(distPath, releasePath)
} catch (error) {
console.log(`\n ${error}`)
}
console.log(`文件已复制 ==> ${releaseRelativePath}`)
}
function copyFile(sourceDir, targetDir) {
return new Promise((resolve, reject) => {
copy(sourceDir, targetDir, (err) => {
if (err) {
reject(err)
} else {
resolve()
}
})
})
}
build()
+55
View File
@@ -0,0 +1,55 @@
<script setup lang="ts">
import { onLaunch } from '@dcloudio/uni-app'
import { useAppStore } from './stores/app'
import { useUserStore } from './stores/user'
import { useThemeStore } from './stores/theme'
import { useRoute, useRouter } from 'uniapp-router-next'
const appStore = useAppStore()
const { getUser } = useUserStore()
const { getTheme } = useThemeStore()
const router = useRouter()
const route = useRoute()
//#ifdef H5
const setH5WebIcon = () => {
const config = appStore.getWebsiteConfig
let favicon: HTMLLinkElement = document.querySelector('link[rel="icon"]')!
if (favicon) {
favicon.href = config.h5_favicon
return
}
favicon = document.createElement('link')
favicon.rel = 'icon'
favicon.href = config.h5_favicon
document.head.appendChild(favicon)
}
//#endif
const getConfig = async () => {
await appStore.getConfig()
//#ifdef H5
setH5WebIcon()
//#endif
const { status, page_status, page_url } = appStore.getH5Config
if (route.meta.webview) return
//处理关闭h5渠道
//#ifdef H5
if (status == 0) {
if (page_status == 1) return (location.href = page_url)
router.reLaunch('/pages/empty/empty')
}
//#endif
}
onLaunch(async () => {
getTheme()
getConfig()
//#ifdef H5
setH5WebIcon()
//#endif
await getUser()
})
</script>
<style lang="scss">
//
</style>
+40
View File
@@ -0,0 +1,40 @@
import { client } from '@/utils/client'
import request from '@/utils/request'
// 登录
export function login(data: Record<string, any>) {
return request.post({ url: '/login/account', data: { ...data, terminal: client } })
}
//注册
export function register(data: Record<string, any>) {
return request.post({ url: '/login/register', data: { ...data, channel: client } })
}
//向微信请求code的链接
export function getWxCodeUrl(data: Record<string, any>) {
return request.get({ url: '/login/codeUrl', data })
}
export function OALogin(data: Record<string, any>) {
return request.post({ url: '/login/oaLogin', data })
}
export function mnpLogin(data: Record<string, any>) {
return request.post({ url: '/login/mnpLogin', data })
}
//更新微信小程序头像昵称
export function updateUser(data: Record<string, any>, header: any) {
return request.post({ url: '/login/updateUser', data, header })
}
//小程序绑定微信
export function mnpAuthBind(data: Record<string, any>) {
return request.post({ url: '/login/mnpAuthBind', data })
}
//公众号绑定微信
export function oaAuthBind(data: Record<string, any>) {
return request.post({ url: '/login/oaAuthBind', data })
}
+30
View File
@@ -0,0 +1,30 @@
import request from '@/utils/request'
//发送短信
export function smsSend(data: any) {
return request.post({ url: '/sms/sendCode', data: data })
}
export function getConfig() {
return request.get({ url: '/index/config' })
}
export function getPolicy(data: any) {
return request.get({ url: '/index/policy', data: data })
}
export function uploadImage(file: any, token?: string) {
return request.uploadFile({
url: '/upload/image',
filePath: file,
name: 'file',
header: {
token
},
fileType: 'image'
})
}
export function wxJsConfig(data: any) {
return request.get({ url: '/wechat/jsConfig', data })
}
+52
View File
@@ -0,0 +1,52 @@
import request from '@/utils/request'
/**
* @description 获取文章分类
* @return { Promise }
*/
export function getArticleCate() {
return request.get({ url: '/article/cate' })
}
/**
* @description 获取文章列表
* @return { Promise }
*/
export function getArticleList(data: Record<string, any>) {
return request.get({ url: '/article/lists', data: data })
}
/**
* @description 获取文章详情
* @param { number } id
* @return { Promise }
*/
export function getArticleDetail(data: { id: number }) {
return request.get({ url: '/article/detail', data: data })
}
/**
* @description 加入收藏
* @param { number } id
* @return { Promise }
*/
export function addCollect(data: { id: number }) {
return request.post({ url: '/article/addCollect', data: data }, { isAuth: true })
}
/**
* @description 取消收藏
* @param { number } id
* @return { Promise }
*/
export function cancelCollect(data: { id: number }) {
return request.post({ url: '/article/cancelCollect', data: data }, { isAuth: true })
}
/**
* @description 获取收藏列表
* @return { Promise }
*/
export function getCollect() {
return request.get({ url: '/article/collect' })
}
+16
View File
@@ -0,0 +1,16 @@
import request from '@/utils/request'
//支付方式
export function getPayWay(data: any) {
return request.get({ url: '/pay/payWay', data }, { isAuth: true })
}
// 预支付
export function prepay(data: any) {
return request.post({ url: '/pay/prepay', data }, { isAuth: true })
}
// 预支付
export function getPayResult(data: any) {
return request.get({ url: '/pay/payStatus', data }, { isAuth: true })
}
+16
View File
@@ -0,0 +1,16 @@
import request from '@/utils/request'
//充值
export function recharge(data: any) {
return request.post({ url: '/recharge/recharge', data }, { isAuth: true })
}
//充值记录
export function rechargeRecord(data: any) {
return request.get({ url: '/recharge/lists', data }, { isAuth: true })
}
// 充值配置
export function rechargeConfig() {
return request.get({ url: '/recharge/config' }, { isAuth: true })
}
+19
View File
@@ -0,0 +1,19 @@
import request from '@/utils/request'
//首页数据
export function getIndex() {
return request.get({ url: '/index/index' })
}
// 装修页面
export function getDecorate(data: any) {
return request.get({ url: '/index/decorate', data }, { ignoreCancel: true })
}
/**
* @description 热门搜索
* @return { Promise }
*/
export function getHotSearch() {
return request.get({ url: '/search/hotLists' })
}
+41
View File
@@ -0,0 +1,41 @@
import request from '@/utils/request'
export function getUserCenter(header?: any) {
return request.get({ url: '/user/center', header }, { ignoreCancel: true })
}
// 个人信息
export function getUserInfo() {
return request.get({ url: '/user/info' }, { isAuth: true })
}
// 个人编辑
export function userEdit(data: any) {
return request.post({ url: '/user/setInfo', data }, { isAuth: true })
}
// 绑定手机
export function userBindMobile(data: any, header?: any) {
return request.post({ url: '/user/bindMobile', data, header }, { isAuth: true })
}
// 微信电话
export function userMnpMobile(data: any) {
return request.post({ url: '/user/getMobileByMnp', data }, { isAuth: true })
}
// 更改手机号
export function userChangePwd(data: any) {
return request.post({ url: '/user/changePassword', data }, { isAuth: true })
}
//忘记密码
export function forgotPassword(data: Record<string, any>) {
return request.post({ url: '/user/resetPassword', data })
}
//余额明细
export function accountLog(data: any) {
return request.get({ url: '/account_log/lists', data })
}
@@ -0,0 +1,108 @@
<template>
<button
class="avatar-upload p-0 m-0 rounded"
:style="styles"
hover-class="none"
open-type="chooseAvatar"
@click="chooseAvatar"
@chooseavatar="chooseAvatar"
>
<image class="w-full h-full" mode="heightFix" :src="modelValue" v-if="modelValue" />
<slot v-else>
<div
:style="styles"
class="border border-dotted border-light flex w-full h-full flex-col items-center justify-center text-muted text-xs box-border rounded"
>
<u-icon name="plus" :size="36" />
添加图片
</div>
</slot>
</button>
</template>
<script lang="ts" setup>
import { uploadImage } from '@/api/app'
import { useUserStore } from '@/stores/user'
import { addUnit } from '@/utils/util'
import { isBoolean } from 'lodash'
import { computed, CSSProperties, onUnmounted } from 'vue'
const props = defineProps({
modelValue: {
type: String
},
fileKey: {
type: String,
default: 'uri'
},
size: {
type: [String, Number],
default: 120
},
round: {
type: [Boolean, String, Number],
default: false
},
border: {
type: Boolean,
default: true
}
})
const emit = defineEmits<{
(event: 'update:modelValue', value: string): void
}>()
const userStore = useUserStore()
const styles = computed<CSSProperties>(() => {
const size = addUnit(props.size)
return {
width: size,
height: size,
borderRadius: isBoolean(props.round) ? (props.round ? '50%' : '') : addUnit(props.round)
}
})
const chooseAvatar = (e: any) => {
// #ifndef MP-WEIXIN
uni.navigateTo({
url: '/uni_modules/vk-uview-ui/components/u-avatar-cropper/u-avatar-cropper?destWidth=300&rectWidth=200&fileType=jpg'
})
// #endif
// #ifdef MP-WEIXIN
const path = e.detail?.avatarUrl
if (path) {
uploadImageIng(path)
}
// #endif
}
const uploadImageIng = async (file: string) => {
uni.showLoading({
title: '正在上传中...'
})
try {
const res: any = await uploadImage(file, userStore.temToken!)
uni.hideLoading()
console.log(res)
emit('update:modelValue', res[props.fileKey])
} catch (error) {
uni.hideLoading()
uni.$u.toast(error)
}
}
// 监听从裁剪页发布的事件,获得裁剪结果
uni.$on('uAvatarCropper', (path) => {
uploadImageIng(path)
})
onUnmounted(() => {
uni.$off('uAvatarCropper')
})
</script>
<style lang="scss" scoped>
.avatar-upload {
background: #fff;
overflow: hidden;
&::after {
border: none;
}
}
</style>
@@ -0,0 +1,97 @@
<template>
<u-swiper
v-if="lists.length"
:list="lists"
:mode="mode"
:height="height"
:effect3d="effect3d"
:indicator-pos="indicatorPos"
:autoplay="autoplay"
:interval="interval"
:duration="duration"
:circular="circular"
:borderRadius="borderRadius"
:current="current"
:name="name"
:bg-color="bgColor"
@click="handleClick"
@change="handleChange"
>
</u-swiper>
</template>
<script setup lang="ts">
import {useAppStore} from "@/stores/app";
import {navigateTo, navigateToMiniProgram, LinkTypeEnum} from "@/utils/util";
import {watchEffect, computed} from "vue";
import {useRouter} from "uniapp-router-next";
const emit = defineEmits(["change"]);
const props = withDefaults(
defineProps<{
content?: any; // 轮播图数据
mode?: string; // 指示器模式 rect / dot / number / none
height?: string; // 轮播图组件高度
indicatorPos?: string; // 指示器的位置 topLeft / topCenter / topRight / bottomLeft / bottomRight
effect3d?: boolean; // 是否开启3D效果
autoplay?: boolean; // 是否自动播放
interval?: number | string; // 自动轮播时间间隔,单位ms
duration?: number | string; // 切换一张轮播图所需的时间,单位ms
circular?: boolean; // 是否衔接播放
current?: number; // 默认显示第几项
name?: string; // 显示的属性
borderRadius?: string; //轮播图圆角值,单位rpx
bgColor?: string; // 背景颜色
}>(),
{
content: {
data: [],
},
mode: "round",
indicatorPos: "bottomCenter",
height: "340",
effect3d: false,
autoplay: true,
interval: "2500",
duration: 300,
circular: true,
current: 0,
name: "image",
borderRadius: "0",
bgColor: "#f3f4f6",
}
);
const {getImageUrl} = useAppStore();
watchEffect(() => {
try {
const content = props?.content;
const len = content?.data?.length;
if (!len) return;
for (let i = 0; i < len; i++) {
const item = content.data[i];
item.image = getImageUrl(item.image);
}
emit("change", 0);
} catch (error) {
//TODO handle the exception
console.log("轮播图数据错误", error);
}
});
const lists = computed(() => props.content.data || []);
const router = useRouter();
const handleClick = (index: number) => {
const link = props.content.data[index]?.link;
if (!link) return
navigateTo(link);
};
const handleChange = (index: number) => {
emit("change", index);
};
</script>
<style></style>
@@ -0,0 +1,93 @@
<template>
<view>
<u-popup v-model="showPopup" mode="bottom" border-radius="14" :mask-close-able="false">
<view class="h-[1000rpx] p-[40rpx]">
<view class="flex items-center">
<image
class="w-[100rpx] h-[100rpx] rounded"
mode="heightFix"
:src="logo"
></image>
<text class="text-3xl ml-5 font-bold">{{ title }}</text>
</view>
<view class="mt-5 text-muted">
建议使用您的微信头像和昵称以便获得更好的体验
</view>
<view class="mt-[30rpx]">
<form @submit="handleSubmit">
<u-form-item required label="头像" :labelWidth="120">
<view class="flex-1">
<avatar-upload v-model="avatar"></avatar-upload>
</view>
</u-form-item>
<u-form-item required label="昵称" :labelWidth="120">
<input
class="flex-1 h-[60rpx]"
name="nickname"
type="nickname"
placeholder="请输入昵称"
/>
</u-form-item>
<view class="mt-[80rpx]">
<button
class="bg-primary rounded-full text-white text-lg h-[80rpx] leading-[80rpx]"
hover-class="none"
form-type="submit"
>
确定
</button>
</view>
<view class="flex justify-center mt-[60rpx]">
<view class="text-muted" @click="showPopup = false">暂不登录</view>
</view>
</form>
</view>
</view>
</u-popup>
</view>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue'
const props = defineProps({
show: {
type: Boolean
},
logo: {
type: String
},
title: {
type: String
}
})
const emit = defineEmits<{
(event: 'update:show', show: boolean): void
(event: 'update', value: any): void
}>()
const showPopup = computed({
get() {
return props.show
},
set(val) {
emit('update:show', val)
}
})
const avatar = ref()
const handleSubmit = (e: any) => {
const { nickname } = e.detail.value
if (!avatar.value)
return uni.$u.toast('请添加头像')
if (!nickname)
return uni.$u.toast('请输入昵称')
emit('update', {
avatar: avatar.value,
nickname
})
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,65 @@
<template>
<navigator :url="`/pages/news_detail/news_detail?id=${newsId}`">
<view class="news-card flex bg-white px-[20rpx] py-[32rpx]">
<view class="mr-[20rpx]" v-if="item.image">
<u-image :src="item.image" width="240" height="180"></u-image>
</view>
<view class="news-card-content flex flex-col justify-between flex-1">
<view class="news-card-content-title text-base">{{ item.title }}</view>
<view class="news-card-content-intro text-gray-400 text-sm mt-[16rpx]">
{{ item.desc }}
</view>
<view class="text-muted text-xs w-full flex justify-between mt-[12rpx]">
<view>{{ item.create_time }}</view>
<view class="flex items-center">
<image
src="/static/images/icon/icon_visit.png"
class="w-[30rpx] h-[30rpx]"
></image>
<view class="ml-[10rpx]">{{ item.click }}</view>
</view>
</view>
</view>
</view>
</navigator>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const props = withDefaults(
defineProps<{
item: any
newsId: number
}>(),
{
item: {},
newsId: ''
}
)
</script>
<style lang="scss" scoped>
.news-card {
border-bottom: 1px solid #f8f8f8;
&-content {
&-title {
-webkit-line-clamp: 2;
overflow: hidden;
word-break: break-all;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
}
&-intro {
-webkit-line-clamp: 1;
overflow: hidden;
word-break: break-all;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
}
}
}
</style>
@@ -0,0 +1,62 @@
<template>
<view
class="page-status"
v-if="status !== PageStatusEnum['NORMAL']"
:class="{ 'page-status--fixed': fixed }"
>
<!-- Loading -->
<template v-if="status === PageStatusEnum['LOADING']">
<slot name="loading">
<u-loading :size="60" mode="flower" />
</slot>
</template>
<!-- Error -->
<template v-if="status === PageStatusEnum['ERROR']">
<slot name="error"></slot>
</template>
<!-- Empty -->
<template v-if="status === PageStatusEnum['EMPTY']">
<slot name="empty"></slot>
</template>
</view>
<template v-else>
<slot> </slot>
</template>
</template>
<script lang="ts" setup>
import { PageStatusEnum } from '@/enums/appEnums'
const props = defineProps({
status: {
type: String,
default: PageStatusEnum['LOADING']
},
fixed: {
type: Boolean,
default: true
}
})
</script>
<style lang="scss" scoped>
.page-status {
height: 100%;
width: 100%;
min-height: 100%;
padding: 0;
flex: 1;
display: flex;
justify-content: center;
align-items: center;
background-color: #ffffff;
&--fixed {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 900;
}
}
</style>
+322
View File
@@ -0,0 +1,322 @@
<template>
<u-popup
v-model="showPay"
mode="bottom"
safe-area-inset-bottom
:mask-close-able="false"
border-radius="14"
closeable
@close="handleClose"
>
<view class="h-[900rpx]">
<page-status :status="popupStatus" :fixed="false">
<template #error>
<u-empty text="订单信息错误,无法查询到订单信息" mode="order"></u-empty>
</template>
<template #default>
<view class="payment h-full w-full flex flex-col">
<view class="header py-[50rpx] flex flex-col items-center">
<price
:content="payData.order_amount"
mainSize="44rpx"
minorSize="40rpx"
fontWeight="500"
color="#333"
></price>
</view>
<view class="main flex-1 mx-[20rpx]">
<view>
<view class="payway-lists">
<u-radio-group v-model="payWay" class="w-full">
<view
class="p-[20rpx] flex items-center w-full payway-item"
v-for="(item, index) in payData.lists"
:key="index"
@click="selectPayWay(item.pay_way)"
>
<u-icon
class="flex-none"
:size="48"
:name="item.icon"
></u-icon>
<view class="mx-[16rpx] flex-1">
<view class="payway-item--name flex-1">
{{ item.name }}
</view>
<view class="text-muted text-xs">{{
item.extra
}}</view>
</view>
<u-radio activeColor="var(--color-primary)" class="mr-[-20rpx]" :name="item.pay_way">
</u-radio>
</view>
</u-radio-group>
</view>
</view>
</view>
<view class="submit-btn p-[20rpx]">
<u-button
@click="handlePay"
shape="circle"
type="primary"
:loading="isLock"
>
立即支付
</u-button>
</view>
</view>
</template>
</page-status>
</view>
</u-popup>
<u-popup
class="pay-popup"
v-model="showCheckPay"
round
mode="center"
borderRadius="10"
:maskCloseAble="false"
>
<view class="content bg-white w-[560rpx] p-[40rpx]">
<view class="text-2xl font-medium text-center"> 支付确认 </view>
<view class="pt-[30rpx] pb-[40rpx]">
<view> 请在微信内完成支付如果您已支付成功请点击`已完成支付`按钮 </view>
</view>
<view class="flex">
<view class="flex-1 mr-[20rpx]">
<u-button
shape="circle"
type="primary"
plain
size="medium"
hover-class="none"
:customStyle="{ width: '100%' }"
@click="queryPayResult(false)"
>
重新支付
</u-button>
</view>
<view class="flex-1">
<u-button
shape="circle"
type="primary"
size="medium"
hover-class="none"
:customStyle="{ width: '100%' }"
@click="queryPayResult()"
>
已完成支付
</u-button>
</view>
</view>
</view>
</u-popup>
</template>
<script lang="ts" setup>
import { pay, PayWayEnum } from '@/utils/pay'
import { getPayWay, prepay, getPayResult } from '@/api/pay'
import { computed, ref, watch } from 'vue'
import { useLockFn } from '@/hooks/useLockFn'
import { series } from '@/utils/util'
import { ClientEnum, PageStatusEnum, PayStatusEnum } from '@/enums/appEnums'
import { useUserStore } from '@/stores/user'
import { client } from '@/utils/client'
/*
页面参数 orderId:订单idfrom:订单来源
*/
const props = defineProps({
show: {
type: Boolean,
required: true
},
showCheck: {
type: Boolean
},
// 订单id
orderId: {
type: Number,
required: true
},
//订单来源
from: {
type: String,
required: true
},
//h5微信支付回跳路径,一般为拉起支付的页面路径
redirect: {
type: String
}
})
const emit = defineEmits(['update:showCheck', 'update:show', 'close', 'success', 'fail'])
const payWay = ref()
const popupStatus = ref(PageStatusEnum.LOADING)
const payData = ref<any>({
order_amount: '',
lists: []
})
const showCheckPay = computed({
get() {
return props.showCheck
},
set(value) {
emit('update:showCheck', value)
}
})
const showPay = computed({
get() {
return props.show
},
set(value) {
emit('update:show', value)
}
})
const handleClose = () => {
showPay.value = false
emit('close')
}
const getPayData = async () => {
popupStatus.value = PageStatusEnum.LOADING
try {
payData.value = await getPayWay({
order_id: props.orderId,
from: props.from
})
popupStatus.value = PageStatusEnum.NORMAL
const checkPay =
payData.value.lists.find((item: any) => item.is_default) || payData.value.lists[0]
payWay.value = checkPay?.pay_way
} catch (error) {
popupStatus.value = PageStatusEnum.ERROR
}
}
const userStore = useUserStore()
const selectPayWay = (pay: number) => {
payWay.value = pay
}
const payment = (() => {
// 查询是否绑定微信
const checkIsBindWx = async () => {
if (
userStore.userInfo.is_auth == 0 &&
[ClientEnum.OA_WEIXIN, ClientEnum.MP_WEIXIN].includes(client) &&
payWay.value == PayWayEnum.WECHAT
) {
const res: any = await uni.showModal({
title: '温馨提示',
content: '当前账号未绑定微信,无法完成支付',
confirmText: '去绑定'
})
if (res.confirm) {
uni.navigateTo({
url: '/pages/user_set/user_set'
})
}
return Promise.reject()
}
}
// 调用预支付
const prepayTask = async () => {
uni.showLoading({
title: '正在支付中'
})
const data = await prepay({
order_id: props.orderId,
from: props.from,
pay_way: payWay.value,
redirect: props.redirect
})
return data
}
//拉起支付
const payTask = async (data: any) => {
try {
const res = await pay.payment(data.pay_way, data.config)
return res
} catch (error) {
return Promise.reject(error)
}
}
return series(checkIsBindWx, prepayTask, payTask)
})()
const { isLock, lockFn: handlePay } = useLockFn(async () => {
try {
const res: PayStatusEnum = await payment()
handlePayResult(res)
uni.hideLoading()
} catch (error) {
uni.hideLoading()
console.log(error)
}
})
const handlePayResult = (status: PayStatusEnum) => {
switch (status) {
case PayStatusEnum.SUCCESS:
emit('success')
break
case PayStatusEnum.FAIL:
emit('fail')
break
}
}
const queryPayResult = async (confirm = true) => {
const res = await getPayResult({
order_id: props.orderId,
from: props.from
})
if (res.pay_status === 0) {
if (confirm == true) {
uni.$u.toast('您的订单还未支付,请重新支付')
}
showPay.value = true
handlePayResult(PayStatusEnum.FAIL)
} else {
if (confirm == false) {
uni.$u.toast('您的订单已经支付,请勿重新支付')
}
handlePayResult(PayStatusEnum.SUCCESS)
}
showCheckPay.value = false
}
watch(
() => props.show,
(value) => {
if (value) {
if (!props.orderId) {
popupStatus.value = PageStatusEnum.ERROR
return
}
getPayData()
}
},
{
immediate: true
}
)
</script>
<style lang="scss">
.payway-lists {
.payway-item {
border-bottom: 1px solid;
@apply border-page;
}
}
</style>
+126
View File
@@ -0,0 +1,126 @@
<template>
<view class="price-container">
<view
:class="['price-wrap', { 'price-wrap--disabled': lineThrough }]"
:style="{ color: color }"
>
<!-- Prefix -->
<view class="fix-pre" :style="{ fontSize: minorSize }">
<slot name="prefix">{{ prefix }}</slot>
</view>
<!-- Content -->
<view :style="{ 'font-weight': fontWeight }">
<!-- Integer -->
<text :style="{ fontSize: mainSize }">{{ integer }}</text>
<!-- Decimals -->
<text :style="{ fontSize: minorSize }">{{ decimals }}</text>
</view>
<!-- Suffix -->
<view class="fix-suf" :style="{ fontSize: minorSize }">
<slot name="suffix">{{ suffix }}</slot>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
/**
* @description 价格展示,适用于有前后缀,小数样式不一
* @property {String|Number} content 价格 (必填项)
* @property {Number} prec 小数位 (默认: 2)
* @property {Boolean} autoPrec 自动小数位【注:以prec为最大小数位】 (默认: true)
* @property {String} color 颜色 (默认: 'unset')
* @property {String} mainSize 主要内容字体大小 (默认: 46rpx)
* @property {String} minorSize 主要内容字体大小 (默认: 32rpx)
* @property {Boolean} lineThrough 贯穿线 (默认: false)
* @property {String|Number} fontWeight 字重 (默认: normal)
* @property {String} prefix 前缀 (默认: ¥)
* @property {String} suffix 后缀
* @example <price content="100" suffix="\/元" />
*/
import { computed } from 'vue'
import { formatPrice } from '@/utils/util'
/** Props Start **/
const props = withDefaults(
defineProps<{
content: string | number // 标题
prec?: number // 小数数量
autoPrec?: boolean // 动态小数
color?: string // 颜色
mainSize?: string // 主要内容字体大小
minorSize?: string // 次要内容字体大小
lineThrough?: boolean // 贯穿线
fontWeight?: string // 字重
prefix?: string // 前缀
suffix?: string // 后缀
}>(),
{
content: '',
prec: 2,
autoPrec: true,
color: '#FA8919',
mainSize: '36rpx',
minorSize: '28rpx',
lineThrough: false,
fontWeight: 'normal',
prefix: '¥',
suffix: ''
}
)
/** Props End **/
/** Computed Start **/
/**
* @description 金额主体部分
*/
const integer = computed(() => {
return formatPrice({
price: props.content,
take: 'int'
})
})
/**
* @description 金额小数部分
*/
const decimals = computed(() => {
let decimals = formatPrice({
price: props.content,
take: 'dec',
prec: props.prec
})
// 小数余十不能是 .10||.20||.30以此类推,
decimals = decimals % 10 == 0 ? decimals.substr(0, decimals.length - 1) : decimals
return props.autoPrec ? (decimals * 1 ? '.' + decimals : '') : props.prec ? '.' + decimals : ''
})
/** Computed End **/
</script>
<style lang="scss" scoped>
.price-container {
display: inline-block;
}
.price-wrap {
display: flex;
align-items: baseline;
&--disabled {
position: relative;
&::before {
position: absolute;
left: 0;
top: 50%;
right: 0;
transform: translateY(-50%);
display: block;
content: '';
height: 0.05em;
background-color: currentColor;
}
}
}
</style>
+84
View File
@@ -0,0 +1,84 @@
<template>
<view
:class="{ active, inactive: !active, tab: true }"
:style="shouldShow ? '' : 'display: none;'"
>
<slot v-if="shouldRender"></slot>
</view>
</template>
<script lang="ts" setup>
import { ref, provide, inject, watch, computed, onMounted, getCurrentInstance } from 'vue'
const props = withDefaults(
defineProps<{
dot?: boolean | string
name?: boolean | string
info?: any
}>(),
{
dot: false,
name: ''
}
)
const active = ref<boolean>(false)
const shouldShow = ref<boolean>(false)
const shouldRender = ref<boolean>(false)
const inited = ref(undefined)
const updateTabs: any = inject('updateTabs')
const handleChange: any = inject('handleChange')
const updateRender = (value) => {
inited.value = inited.value || value
active.value = value
shouldRender.value = inited.value!
shouldShow.value = value
}
const update = () => {
if (updateTabs) {
updateTabs()
}
}
const instance = getCurrentInstance()
console.log(instance)
handleChange(instance?.props, updateRender)
onMounted(() => {
update()
})
const changeData = computed(() => {
const { dot, info } = props
return {
dot,
info
}
})
watch(
() => changeData.value,
() => {
update()
}
)
watch(
() => props.name,
(val) => {
update()
}
)
</script>
<style>
.tab.active {
height: auto;
}
.tab.inactive {
height: 0;
overflow: visible;
}
</style>
+55
View File
@@ -0,0 +1,55 @@
<template>
<u-tabbar
v-if="showTabbar"
v-model="current"
v-bind="tabbarStyle"
:list="tabbarList"
@change="handleChange"
:hide-tab-bar="true"
></u-tabbar>
</template>
<script lang="ts" setup>
import { useAppStore } from '@/stores/app'
import { navigateTo } from '@/utils/util'
import { computed, ref } from 'vue'
const current = ref()
const appStore = useAppStore()
const tabbarList = computed(() => {
return appStore.getTabbarConfig
?.filter((item: any) => item.is_show == 1)
.map((item: any) => {
return {
iconPath: item.unselected,
selectedIconPath: item.selected,
text: item.name,
link: item.link,
pagePath: item.link.path
}
})
})
const showTabbar = computed(() => {
const currentPages = getCurrentPages()
const currentPage = currentPages[currentPages.length - 1]
const current = tabbarList.value.findIndex((item: any) => {
return item.pagePath === '/' + currentPage.route
})
return current >= 0
})
const tabbarStyle = computed(() => ({
activeColor: appStore.getStyleConfig.selected_color,
inactiveColor: appStore.getStyleConfig.default_color
}))
const nativeTabbar = [
'/pages/index/index',
'/pages/news/news',
'/pages/user/user'
]
const handleChange = (index: number) => {
const selectTab = tabbarList.value[index]
const navigateType = nativeTabbar.includes(selectTab.link.path) ? 'switchTab' : 'reLaunch'
navigateTo(selectTab.link, navigateType)
}
</script>
@@ -0,0 +1,72 @@
import { reactive } from 'vue'
/**
* @description 触碰屏幕钩子函数
* @return { Function } 暴露钩子
*/
export function useTouch() {
// 最小移动距离
const MIN_DISTANCE = 10
const touch = reactive<any>({
direction: '',
deltaX: 0,
deltaY: 0,
offsetX: 0,
offsetY: 0
})
/**
* @description 计算距离
* @return { string } 空字符串
*/
const getDirection = (x: number, y: number) => {
if (x > y && x > MIN_DISTANCE) {
return 'horizontal'
}
if (y > x && y > MIN_DISTANCE) {
return 'vertical'
}
return ''
}
/**
* @description 重置参数
*/
const resetTouchStatus = () => {
touch.direction = ''
touch.deltaX = 0
touch.deltaY = 0
touch.offsetX = 0
touch.offsetY = 0
}
/**
* @description 触发
*/
const touchStart = (event: any) => {
resetTouchStatus()
const events = event.touches[0]
touch.startX = events.clientX
touch.startY = events.clientY
}
/**
* @description 移动
*/
const touchMove = (event: any) => {
const events = event.touches[0]
touch.deltaX = events.clientX - touch.startX
touch.deltaY = events.clientY - touch.startY
touch.offsetX = Math.abs(touch.deltaX)
touch.offsetY = Math.abs(touch.deltaY)
touch.direction = touch.direction || getDirection(touch.offsetX, touch.offsetY)
}
return {
touch,
resetTouchStatus,
touchStart,
touchMove
}
}
+437
View File
@@ -0,0 +1,437 @@
<template>
<view class="tabs">
<u-sticky :enable="isFixed" :bg-color="stickyBgColor" :offset-top="top" :h5-nav-height="0">
<view
:id="id"
:style="{
background: bgColor
}"
>
<scroll-view
:style="{ height: height + 'rpx' }"
scroll-x
class="scroll-view"
:scroll-left="scrollLeft"
scroll-with-animation
>
<view class="scroll-box" :class="{ 'tabs-scorll-flex': !isScroll }">
<view
v-for="(item, index) in list"
class="tab-item line1"
:id="'tab-item-' + index"
:key="index"
@tap="clickTab(index)"
:style="[tabItemStyle(index)]"
>
<u-badge
:count="item[count] || item['dot'] || 0"
:offset="offset"
size="mini"
></u-badge>
{{ item[name] || item['name'] }}
</view>
<view v-if="showBar" class="tab-bar" :style="[tabBarStyle]"></view>
</view>
</scroll-view>
</view>
</u-sticky>
<view
class="tab-content"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchcancel="onTouchEnd"
@touchend="onTouchEnd"
>
<!-- <view class="tab-track" :class="{'tab-animated': animated}" :style="[trackStyle]"> -->
<view>
<slot></slot>
</view>
<!-- </view> -->
</view>
</view>
</template>
<script lang="ts" setup>
import { getRect } from '@/utils/util'
import {
ref,
reactive,
computed,
watch,
provide,
nextTick,
onMounted,
getCurrentInstance
} from 'vue'
import { useTouch } from './hooks/useTouch'
// Touch 钩子
const { touch, resetTouchStatus, touchStart, touchMove } = useTouch()
const emit = defineEmits<{
(event: 'change', value: number): void
}>()
const props = withDefaults(
defineProps<{
isScroll?: boolean // 导航菜单是否需要滚动,如只有2或者3个的时候,就不需要滚动了,此时使用flex平分tab的宽度
current?: number | string // 当前活动tab的索引
height?: number | string // 导航栏的高度和行高
fontSize?: number | string // 字体大小
duration?: number | string // 过渡动画时长, 单位ms
activeColor?: number | string // 选中项的主题颜色
inactiveColor?: number | string // 未选中项的颜色
barWidth?: number | string // 菜单底部移动的bar的宽度,单位rpx
barHeight?: number // 移动bar的高度
gutter?: number | string // 单个tab的左或有内边距(左右相同)
bgColor?: number | string // 导航栏的背景颜色
name?: string // 读取传入的数组对象的属性(tab名称)
count?: string // 读取传入的数组对象的属性(徽标数)
offset?: number[] // 徽标数位置偏移
bold?: boolean // 活动tab字体是否加粗
activeItemStyle?: any // 当前活动tab item的样式
showBar?: boolean // 是否显示底部的滑块
barStyle?: any // 底部滑块的自定义样式
itemWidth?: string // 标签的宽度
isFixed?: boolean // 吸顶是否固定
top?: number | string // 吸顶顶部距离
stickyBgColor?: string // 吸顶颜色
swipeable?: boolean // 是否允许滑动切换
// animated: boolean // 切换动画
}>(),
{
isScroll: true,
current: 0,
height: 80,
fontSize: 28,
duration: 0.3,
activeColor: 'var(--color-primary)',
inactiveColor: '#333',
barWidth: 40,
barHeight: 4,
gutter: 30,
bgColor: '#FFFFFF',
name: 'name',
count: 'count',
offset: [5, 20],
bold: true,
activeItemStyle: {},
showBar: true,
barStyle: {},
itemWidth: 'auto',
isFixed: false,
top: 0,
stickyBgColor: '#FFFFFF',
swipeable: true
// animated: true
}
)
const list = ref<any>([])
const childrens = ref<any>([])
const scrollLeft = ref<number>(0) // 滚动scroll-view的左边滚动距离
const tabQueryInfo = ref<any>([]) // 存放对tab菜单查询后的节点信息
const componentWidth = ref<number>(0) // 屏幕宽度,单位为px
const scrollBarLeft = ref<number>(0) // 移动bar需要通过translateX()移动的距离
const parentLeft = ref<number>(0) // 父元素(tabs组件)到屏幕左边的距离
const id = ref<string>('cu-tab') // id值
const currentIndex = ref<any>(props.current)
const barFirstTimeMove = ref<boolean>(true) // 滑块第一次移动时(页面刚生成时),无需动画,否则给人怪异的感觉
const swiping = ref<boolean>(false)
//@ts-ignore
const ctx = getCurrentInstance()
// 监听tab的变化,重新计算tab菜单的布局信息,因为实际使用中菜单可能是通过
// 后台获取的(如新闻app顶部的菜单),获取返回需要一定时间,所以list变化时,重新获取布局信息
watch(
() => list.value,
async (n, o) => {
// list变动时,重制内部索引,否则可能导致超出数组边界的情况
if (!barFirstTimeMove.value && n.length !== o.length) {
currentIndex.value = 0
}
// 用$nextTick等待视图更新完毕后再计算tab的局部信息,否则可能因为tab还没生成就获取,就会有问题
await nextTick()
init()
}
)
watch(
() => props.current,
(nVal, oVal) => {
// 视图更新后再执行移动操作、
nextTick(() => {
currentIndex.value = nVal
scrollByIndex()
})
},
{ immediate: true }
)
// 移动bar的样式
const tabBarStyle = computed(() => {
const style = {
width: props.barWidth + 'rpx',
transform: `translate(${scrollBarLeft.value}px, -100%)`,
// 滑块在页面渲染后第一次滑动时,无需动画效果
'transition-duration': `${barFirstTimeMove.value ? 0 : props.duration}s`,
'background-color': props.activeColor,
height: props.barHeight + 'rpx',
opacity: barFirstTimeMove.value ? 0 : 1,
// 设置一个很大的值,它会自动取能用的最大值,不用高度的一半,是因为高度可能是单数,会有小数出现
'border-radius': `${props.barHeight / 2}px`
}
Object.assign(style, props.barStyle)
return style
})
// tab的样式
const tabItemStyle = computed(() => {
return (index) => {
let style: any = {
height: props.height + 'rpx',
'line-height': props.height + 'rpx',
'font-size': props.fontSize + 'rpx',
padding: props.isScroll ? `0 ${props.gutter}rpx` : '',
flex: props.isScroll ? 'auto' : '1',
width: `${props.itemWidth}rpx`
}
// 字体加粗
if (index == currentIndex.value && props.bold) style.fontWeight = 'bold'
if (index == currentIndex.value) {
style.color = props.activeColor
// 给选中的tab item添加外部自定义的样式
style = Object.assign(style, props.activeItemStyle)
} else {
style.color = props.inactiveColor
}
return style
}
})
// const trackStyle = computed(() => {
// if (!props.animated) return ''
// return {
// left: -100 * currentIndex.value + '%',
// 'transition-duration': props.duration + 's',
// '-webkit-transition-duration': props.duration + 's',
// }
// })
const updateTabs = () => {
list.value = childrens.value.map((item) => {
const { name, dot, active, inited } = item.event
const { updateRender } = item
return {
name,
dot,
active,
inited,
updateRender
}
})
// nextTick(() => {
// init()
// })
}
// 设置一个init方法,方便多处调用
const init = async () => {
// 获取tabs组件的尺寸信息
const tabRect = await getRect('#' + id.value, false, ctx)
// tabs组件距离屏幕左边的宽度
parentLeft.value = tabRect.left
// tabs组件的宽度
componentWidth.value = tabRect.width
getTabRect()
}
// 点击某一个tab菜单
const clickTab = (index) => {
// 点击当前活动tab,不触发事件
if (index == currentIndex.value) return
nextTick(() => {
currentIndex.value = index
scrollByIndex()
})
// 发送事件给父组件
emit('change', index)
}
// 查询tab的布局信息
const getTabRect = () => {
// 创建节点查询
const query: any = uni.createSelectorQuery().in(ctx)
// 历遍所有tab,这里是执行了查询,最终使用exec()会一次性返回查询的数组结果
for (let i = 0; i < list.value.length; i++) {
// 只要size和rect两个参数
query.select(`#tab-item-${i}`).fields({
size: true,
rect: true
})
}
// 执行查询,一次性获取多个结果
query.exec((res) => {
tabQueryInfo.value = res
// 初始化滚动条和移动bar的位置
scrollByIndex()
})
}
// 滚动scroll-view,让活动的tab处于屏幕的中间位置
const scrollByIndex = () => {
// 当前活动tab的布局信息,有tab菜单的width和left(为元素左边界到父元素左边界的距离)等信息
const tabInfo = tabQueryInfo.value[currentIndex.value]
if (!tabInfo) return
// 活动tab的宽度
const tabWidth = tabInfo.width
// 活动item的左边到tabs组件左边的距离,用item的left减去tabs的left
const offsetLeft = tabInfo.left - parentLeft.value
// 将活动的tabs-item移动到屏幕正中间,实际上是对scroll-view的移动
const scrollLefts = offsetLeft - (componentWidth.value - tabWidth) / 2
scrollLeft.value = scrollLefts < 0 ? 0 : scrollLefts
// 当前活动item的中点点到左边的距离减去滑块宽度的一半,即可得到滑块所需的移动距离
const left = tabInfo.left + tabInfo.width / 2 - parentLeft.value
// 计算当前活跃item到组件左边的距离
scrollBarLeft.value = left - uni.upx2px(props.barWidth) / 2
// 第一次移动滑块的时候,barFirstTimeMove为true,放到延时中将其设置false
// 延时是因为scrollBarLeft作用于computed计算时,需要一个过程需,否则导致出错
if (barFirstTimeMove.value == true) {
setTimeout(() => {
barFirstTimeMove.value = false
}, 100)
}
// 更新子组件的显示
childrens.value.forEach((item, ind) => {
const active = ind === currentIndex.value
if (active !== item.event.active || !item.event.inited) {
item.updateRender(active)
}
})
}
// 子组件调用此函数而产生的事件通信
const handleChange = (event, updateRender) => {
childrens.value.push({ event: event, updateRender })
}
// 手指触摸
const onTouchStart = (event) => {
if (!props.swipeable) return
swiping.value = true
touchStart(event)
}
// 手指滑动
const onTouchMove = (event) => {
if (!props.swipeable || !swiping.value) return
touchMove(event)
}
// 手指滑动结束
const onTouchEnd = () => {
if (!props.swipeable || !swiping.value) return
const minSwipeDistance = 50
if (touch.direction === 'horizontal' && touch.offsetX >= minSwipeDistance) {
let index,
len = list.value.length,
curIndex = currentIndex.value
if (touch.deltaX <= 0) {
curIndex >= len - 1 ? (index = 0) : (index = curIndex + 1)
} else {
curIndex <= 0 ? (index = len - 1) : (index = curIndex - 1)
}
nextTick(() => {
currentIndex.value = index
scrollByIndex()
})
// 发送事件给父组件
emit('change', index)
}
swiping.value = false
}
onMounted(() => {
updateTabs()
})
provide('handleChange', handleChange)
provide('updateTabs', updateTabs)
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
::-webkit-scrollbar,
::-webkit-scrollbar,
::-webkit-scrollbar {
display: none;
width: 0 !important;
height: 0 !important;
-webkit-appearance: none;
background: transparent;
}
/* #endif */
.scroll-box {
height: 100%;
position: relative;
/* #ifdef MP-TOUTIAO */
white-space: nowrap;
/* #endif */
}
.tab-fixed {
position: sticky;
top: 0;
width: 100%;
}
/* #ifdef H5 */
// 通过样式穿透,隐藏H5下,scroll-view下的滚动条
scroll-view ::v-deep ::-webkit-scrollbar {
display: none;
width: 0 !important;
height: 0 !important;
-webkit-appearance: none;
background: transparent;
}
/* #endif */
.scroll-view {
width: 100%;
white-space: nowrap;
position: relative;
}
.tab-item {
position: relative;
/* #ifndef APP-NVUE */
display: inline-block;
/* #endif */
text-align: center;
transition-property: background-color, color;
}
.tab-bar {
position: absolute;
bottom: 6rpx;
}
.tabs-scorll-flex {
display: flex;
justify-content: space-between;
}
// .tab-content {
// overflow: hidden;
// .tab-track {
// position: relative;
// width: 100%;
// height: 100%;
// }
// .tab-animated {
// display: flex;
// transition-property: left;
// }
// }
</style>
@@ -0,0 +1,43 @@
<template>
<view
class="banner translate-y-0"
:class="{ 'px-[20rpx]': !isLargeScreen }"
v-if="content.data.length && content.enabled"
>
<LSwiper
:content="content"
:height="isLargeScreen ? '1100' : '321'"
:circular="true"
:effect3d="false"
:border-radius="isLargeScreen ? '0' : '14'"
interval="7000"
bgColor="transparent"
@change="handleChange"
></LSwiper>
</view>
</template>
<script setup lang="ts">
import LSwiper from '@/components/l-swiper/l-swiper.vue'
import {useAppStore} from "@/stores/app";
const emit = defineEmits(['change'])
const props = defineProps({
content: {
type: Object,
default: () => ({})
},
styles: {
type: Object,
default: () => ({})
},
isLargeScreen: {
type: Boolean
}
})
const {getImageUrl} = useAppStore();
const handleChange = (index: number) => {
emit('change', getImageUrl(props['content'].data[index].bg))
}
</script>
@@ -0,0 +1,65 @@
<template>
<view class="bg-white p-[30rpx] flex text-[#101010] font-medium text-lg">
联系我们
</view>
<view
class="customer-service bg-white flex flex-col justify-center items-center mx-[36rpx] mt-[30rpx] rounded-[20rpx] px-[20rpx] pb-[100rpx]"
>
<view
class="w-full border-solid border-0 border-b border-[#f5f5f5] p-[30rpx] text-center text-[#101010] text-base font-medium">
{{ content.title }}
</view>
<view class="mt-[60rpx]">
<!-- 这样渲染是为了能在小程序中长按识别二维码 -->
<u-parse :html="richTxt"></u-parse>
<!-- <u-image width="200" height="200" border-radius="10rpx" :src="getImageUrl(content.qrcode)"/>-->
</view>
<view v-if="content.remark" class="text-sm mt-[40rpx] font-medium">{{ content.remark }}</view>
<view v-if="content.mobile" class="text-sm mt-[24rpx] flex flex-wrap">
<!-- #ifdef H5 -->
<a class="ml-[10rpx] phone text-primary underline" :href="'tel:' + content.mobile">
{{ content.mobile }}
</a>
<!-- #endif -->
<!-- #ifndef H5 -->
<view class="ml-[10rpx] phone text-primary underline" @click="handleCall">
{{ content.mobile }}
</view>
<!-- #endif -->
</view>
<view v-if="content.time" class="text-muted text-sm mt-[30rpx]">
服务时间{{ content.time }}
</view>
</view>
</template>
<script lang="ts" setup>
import {useAppStore} from '@/stores/app'
import { computed } from 'vue'
const props = defineProps({
content: {
type: Object,
default: () => ({})
},
styles: {
type: Object,
default: () => ({})
}
})
const {getImageUrl} = useAppStore()
const richTxt = computed(() => {
const src = getImageUrl(props.content.qrcode)
return `<img src="${src}" style="width: 100px;height: 100px; border-radius: 8px" />`
})
const handleCall = () => {
uni.makePhoneCall({
phoneNumber: String(props.content.mobile)
})
}
</script>
<style lang="scss"></style>
@@ -0,0 +1,54 @@
<template>
<view
class="banner h-[200rpx] mx-[20rpx] mt-[20rpx] translate-y-0"
v-if="showList.length && content.enabled"
>
<swiper
class="swiper h-full"
:indicator-dots="showList.length > 1"
indicator-active-color="#4173ff"
:autoplay="true"
>
<swiper-item
v-for="(item, index) in showList"
:key="index"
@click="handleClick(item.link)"
>
<u-image
mode="widthFix"
width="100%"
height="100%"
:src="getImageUrl(item.image)"
:border-radius="14"
/>
</swiper-item>
</swiper>
</view>
</template>
<script setup lang="ts">
import { useAppStore } from '@/stores/app'
import { navigateTo } from '@/utils/util'
import { computed } from 'vue'
const props = defineProps({
content: {
type: Object,
default: () => ({})
},
styles: {
type: Object,
default: () => ({})
}
})
const handleClick = (link: any) => {
navigateTo(link)
}
const { getImageUrl } = useAppStore()
const showList = computed(() => {
return props.content.data?.filter((item: any) => item.is_show == '1') || []
})
</script>
<style></style>
@@ -0,0 +1,61 @@
<template>
<div class="my-service bg-white mx-[20rpx] mt-[20rpx] rounded-lg p-[30rpx]">
<div
v-if="content.title"
class="title font-medium text-lg"
>
<div>{{ content.title }}</div>
</div>
<div v-if="content.style == 1" class="grid grid-cols-4 gap-x-9 gap-y-7">
<div
v-for="(item, index) in showList"
:key="index"
class="flex flex-col items-center pt-[40rpx]"
@click="handleClick(item.link)"
>
<u-image width="52" height="52" :src="getImageUrl(item.image)" alt="" />
<div class="mt-[22rpx] text-sm">{{ item.name }}</div>
</div>
</div>
<div v-if="content.style == 2">
<div
v-for="(item, index) in showList"
:key="index"
class="flex items-center border-light border-solid border-0 border-b h-[100rpx] px-[24rpx]"
@click="handleClick(item.link)"
>
<u-image width="48" height="48" :src="getImageUrl(item.image)" alt="" />
<div class="ml-[20rpx] flex-1 text-sm">{{ item.name }}</div>
<div class="text-muted">
<u-icon name="arrow-right" />
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useAppStore } from '@/stores/app'
import { navigateTo } from '@/utils/util'
import { computed } from 'vue'
const props = defineProps({
content: {
type: Object,
default: () => ({})
},
styles: {
type: Object,
default: () => ({})
}
})
const { getImageUrl } = useAppStore()
const handleClick = (link: any) => {
navigateTo(link)
}
const showList = computed(() => {
return props.content.data?.filter((item: any) => item.is_show == '1') || []
})
</script>
<style lang="scss"></style>
+75
View File
@@ -0,0 +1,75 @@
<template>
<div class="relative mx-[20rpx] mt-[20rpx]">
<swiper
class="py-[20rpx] bg-white rounded-lg"
:style="{
height: navList[0].length > content.per_line ? '288rpx' : '132rpx'
}"
:autoplay="false"
:indicator-dots="false"
@change="swiperChange"
>
<swiper-item v-for="(sItem, sIndex) in navList" :key="sIndex">
<view class="nav" v-if="navList.length && content.enabled">
<view
class="grid grid-rows-auto gap-y-3 w-full"
:style="{ 'grid-template-columns': `repeat(${content.per_line}, 1fr)` }"
>
<view
v-for="(item, index) in sItem"
:key="index"
class="flex flex-col items-center"
@click="handleClick(item.link)"
>
<u-image width="82" height="82" :src="getImageUrl(item.image)" alt=""/>
<view class="mt-[14rpx] text-xs">{{ item.name }}</view>
</view>
</view>
</view>
</swiper-item>
</swiper>
</div>
</template>
<script setup lang="ts">
import {ref, watch, computed} from 'vue'
import {useAppStore} from '@/stores/app'
import {navigateTo, sliceArray} from '@/utils/util'
const props = defineProps({
content: {
type: Object,
default: () => ({})
},
styles: {
type: Object,
default: () => ({})
}
})
const {getImageUrl} = useAppStore()
const swiperCurrent = ref<number>(0)
const navList = ref<Record<string, any>>([])
const pagesNum = computed<number>(() => {
return props.content.per_line * props.content.show_line
})
watch(
() => props.content.data,
(val) => {
const num = props.content.style === 1 ? val.length : pagesNum.value
navList.value = sliceArray(val, num)
console.log(navList.value)
},
{deep: true, immediate: true}
)
const handleClick = (link: any) => {
navigateTo(link)
}
const swiperChange = (e: any) => {
swiperCurrent.value = e.detail.current
}
</script>
@@ -0,0 +1,93 @@
<template>
<!-- #ifndef H5 -->
<u-sticky h5-nav-height="0" bg-color="transparent">
<u-navbar
:class="{ 'fixed top-0 z-10': isLargeScreen }"
:is-back="false"
:is-fixed="true"
:title="metaData.title"
:custom-title="metaData.title_type == 2"
:border-bottom="false"
:title-bold="true"
:background="{ background: 'rgba(256,256, 256, 0)' }"
:title-color="percent > 0.5 ? '#000' : metaData.text_color == 1 ? '#fff' : '#000'"
>
<template #default>
<navigator
url="/pages/search/search"
class="mini-search"
hover-class="none"
:style="{ opacity: isLargeScreen ? 1 : percent }"
>
<u-icon name="search"></u-icon>
</navigator>
</template>
<template #title>
<image
class="!h-[54rpx]"
:src="metaData.title_img"
mode="widthFix"
></image>
</template>
</u-navbar>
</u-sticky>
<!-- #endif -->
<navigator
v-if="!isLargeScreen"
url="/pages/search/search"
class="px-[24rpx] mt-[24rpx] mb-[30rpx]"
:style="{ opacity: 1 - percent }"
hover-class="none"
>
<u-search
placeholder="请输入关键词搜索"
:height="72"
:disabled="true"
:show-action="false"
bgColor="#ffffff"
></u-search>
</navigator>
</template>
<script setup lang="ts">
import {computed} from "vue";
const props = defineProps({
pageMeta: {
type: Object,
default: () => []
},
content: {
type: Object,
default: () => ({})
},
styles: {
type: Object,
default: () => ({})
},
isLargeScreen: {
type: Boolean
},
percent: {
type: Number
}
})
const metaData: any = computed(() => {
return props.pageMeta[0].content
})
</script>
<style scoped>
.mini-search {
display: flex;
align-items: center;
justify-content: center;
width: 60rpx;
height: 60rpx;
margin-left: 20rpx;
background-color: #ffffff;
border-radius: 50%;
box-shadow: 0 0 6px rgba(0, 0, 0, 0.2);
}
</style>
@@ -0,0 +1,54 @@
<template>
<view
class="banner h-[200rpx] mx-[20rpx] mt-[20rpx] translate-y-0"
v-if="showList.length && content.enabled"
>
<swiper
class="swiper h-full"
:indicator-dots="showList.length > 1"
indicator-active-color="#4173ff"
:autoplay="true"
>
<swiper-item
v-for="(item, index) in showList"
:key="index"
@click="handleClick(item.link)"
>
<u-image
mode="widthFix"
width="100%"
height="100%"
:src="getImageUrl(item.image)"
:border-radius="14"
/>
</swiper-item>
</swiper>
</view>
</template>
<script setup lang="ts">
import { useAppStore } from '@/stores/app'
import { navigateTo } from '@/utils/util'
import { computed } from 'vue'
const props = defineProps({
content: {
type: Object,
default: () => ({})
},
styles: {
type: Object,
default: () => ({})
}
})
const handleClick = (link: any) => {
navigateTo(link)
}
const { getImageUrl } = useAppStore()
const showList = computed(() => {
return props.content.data?.filter((item: any) => item.is_show == '1') || []
})
</script>
<style></style>
@@ -0,0 +1,98 @@
<template>
<view class="user-info mb-[0rpx]">
<!-- #ifndef H5 -->
<u-sticky
h5-nav-height="0"
bg-color="transparent"
>
<u-navbar
:is-back="false"
:is-fixed="false"
:title="metaData.title"
:custom-title="metaData.title_type == 2"
:border-bottom="false"
:title-bold="true"
:background="{ background: 'rgba(256,256, 256, 0)' }"
:title-color="$theme.navColor"
>
<template #title>
<image
class="!h-[54rpx]"
:src="metaData.title_img"
mode="widthFix"
></image>
</template>
</u-navbar>
</u-sticky>
<!-- #endif -->
<view class="flex items-center justify-between px-[50rpx] pb-[50rpx] pt-[40rpx]">
<view
v-if="isLogin"
class="flex items-center"
@click="navigateTo('/pages/user_data/user_data')"
>
<u-avatar :src="user.avatar" :size="120"></u-avatar>
<view class="text-white ml-[20rpx]">
<view class="text-2xl">{{ user.nickname }}</view>
<view class="text-xs mt-[18rpx]" @click.stop="copy(user.account)">
账号{{ user.account }}
</view>
</view>
</view>
<navigator v-else class="flex items-center" hover-class="none" url="/pages/login/login">
<u-avatar src="/static/images/user/default_avatar.png" :size="120"></u-avatar>
<view class="text-white text-3xl ml-[20rpx]">未登录</view>
</navigator>
<navigator v-if="isLogin" hover-class="none" url="/pages/user_set/user_set">
<u-icon name="setting" color="#fff" :size="48"></u-icon>
</navigator>
</view>
</view>
</template>
<script lang="ts" setup>
import { useCopy } from '@/hooks/useCopy'
import {computed} from "vue";
const props = defineProps({
pageMeta: {
type: Object,
default: () => []
},
content: {
type: Object,
default: () => ({})
},
styles: {
type: Object,
default: () => ({})
},
user: {
type: Object,
default: () => ({})
},
isLogin: {
type: Boolean
}
})
const { copy } = useCopy()
const metaData: any = computed(() => {
return props.pageMeta[0].content
})
const navigateTo = (url: string) => {
uni.navigateTo({
url
})
}
</script>
<style lang="scss" scoped>
.user-info {
background: url(../../../static/images/user/my_topbg.png),
linear-gradient(90deg, $u-type-primary, $u-minor-color);
background-repeat: no-repeat;
background-position: bottom;
background-size: 100%;
}
</style>
+23
View File
@@ -0,0 +1,23 @@
import { isDevMode } from "@/utils/env";
const envBaseUrl = import.meta.env.VITE_APP_BASE_URL || "";
let baseUrl = `${envBaseUrl}/`;
/*
* 微信小程序在`VITE_APP_BASE_URL`存在或`dev`模式下
* 使用`VITE_APP_BASE_URL`的值
* 其他情况使用`[baseUrl]`,方便服务端替换
*/
//#ifdef MP-WEIXIN
baseUrl = isDevMode() || envBaseUrl ? baseUrl : "[baseUrl]";
//#endif
const config = {
version: "1.9.0", //版本号
baseUrl, //请求接口域名
urlPrefix: "api", //请求默认前缀
timeout: 60 * 1000, //请求超时时长
};
export default config;
+5
View File
@@ -0,0 +1,5 @@
//菜单主题类型
export enum AgreementEnum {
PRIVACY = 'privacy',
SERVICE = 'service'
}
+49
View File
@@ -0,0 +1,49 @@
//菜单主题类型
export enum ThemeEnum {
LIGHT = 'light',
DARK = 'dark'
}
// 客户端
export enum ClientEnum {
MP_WEIXIN = 1, // 微信-小程序
OA_WEIXIN = 2, // 微信-公众号
H5 = 3, // H5
IOS = 5, //苹果
ANDROID = 6 //安卓
}
export enum SMSEnum {
LOGIN = 'YZMDL',
BIND_MOBILE = 'BDSJHM',
CHANGE_MOBILE = 'BGSJHM',
FIND_PASSWORD = 'ZHDLMM'
}
export enum SearchTypeEnum {
HISTORY = 'history'
}
// 用户资料
export enum FieldType {
NONE = '',
AVATAR = 'avatar',
USERNAME = 'account',
NICKNAME = 'nickname',
SEX = 'sex'
}
// 支付结果
export enum PayStatusEnum {
SUCCESS = 'success',
FAIL = 'fail',
PENDING = 'pending'
}
// 页面状态
export enum PageStatusEnum {
LOADING = 'loading', // 加载中
NORMAL = 'normal', // 正常
ERROR = 'error', // 异常
EMPTY = 'empty' // 为空
}
+11
View File
@@ -0,0 +1,11 @@
// 本地缓冲key
//token
export const TOKEN_KEY = 'token'
// 搜索历史记录
export const HISTORY = 'history'
export const BACK_URL = 'back_url'
export const PAY_STATUS_EVENT = 'event:payStatus'
+22
View File
@@ -0,0 +1,22 @@
export enum ContentTypeEnum {
// json
JSON = 'application/json;charset=UTF-8',
// form-data 上传资源(图片,视频)
FORM_DATA = 'multipart/form-data;charset=UTF-8'
}
export enum RequestMethodsEnum {
GET = 'GET',
POST = 'POST'
}
export enum RequestCodeEnum {
SUCCESS = 1, //成功
FAILED = 0, // 失败
TOKEN_INVALID = -1 // TOKEN参数无效
}
export enum RequestErrMsgEnum {
ABORT = 'request:fail abort',
TIMEOUT = 'request:fail timeout'
}
+10
View File
@@ -0,0 +1,10 @@
export function useCopy() {
const copy = (text: string) => {
uni.setClipboardData({
data: String(text)
})
}
return {
copy
}
}
+21
View File
@@ -0,0 +1,21 @@
import { ref } from 'vue'
export function useLockFn(fn: (...args: any[]) => Promise<any>) {
const isLock = ref(false)
const lockFn = async (...args: any[]) => {
if (isLock.value) return
isLock.value = true
try {
const res = await fn(...args)
isLock.value = false
return res
} catch (e) {
isLock.value = false
throw e
}
}
return {
isLock,
lockFn
}
}
+15
View File
@@ -0,0 +1,15 @@
import { createSSRApp } from 'vue'
import App from './App.vue'
import plugins from './plugins'
import router from './router'
import './styles/index.scss'
import { setupMixin } from './mixins'
export function createApp() {
const app = createSSRApp(App)
setupMixin(app)
app.use(plugins)
app.use(router)
return {
app
}
}
+82
View File
@@ -0,0 +1,82 @@
{
"name" : "",
"appid" : "",
"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\"/>"
]
},
/* ios */
"ios" : {},
/* SDK */
"sdkConfigs" : {}
}
},
/* */
"quickapp" : {},
/* */
"mp-weixin" : {
"appid" : "wx386a75e518b38935",
"setting" : {
"urlCheck" : false,
"es6" : true,
"minified" : true
},
"__usePrivacyCheck__": true,
"usingComponents" : true
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"uniStatistics" : {
"enable" : false
},
"vueVersion" : "3",
"h5" : {
"router" : {
"mode" : "history",
"base" : "/mobile/"
},
"title" : "加载中"
}
}
+5
View File
@@ -0,0 +1,5 @@
import { App } from 'vue'
import theme from './theme'
export function setupMixin(app: App) {
app.mixin(theme)
}
+18
View File
@@ -0,0 +1,18 @@
import { useThemeStore } from '@/stores/theme'
import { useAppStore } from '@/stores/app'
export default {
computed: {
$theme() {
const themeStore = useThemeStore()
const appStore = useAppStore()
return {
primaryColor: themeStore.primaryColor,
pageStyle: themeStore.vars,
navColor: themeStore.navColor,
navBgColor: themeStore.navBgColor,
title: appStore.getWebsiteConfig.shop_name
}
}
}
}
+24
View File
@@ -0,0 +1,24 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar
:front-color="$theme.navColor"
:background-color="$theme.navBgColor"
/>
<!-- #endif -->
</page-meta>
<view class="h-screen flex flex-col justify-center items-center">
<view>
<u-empty text="对不起,您访问的页面不存在" mode="data"></u-empty>
</view>
<view class="w-full px-[100rpx] mt-[40rpx]">
<router-navigate
class="bg-primary rounded-full text-btn-text leading-[80rpx] text-center"
to="/"
nav-type="reLaunch"
>
返回首页
</router-navigate>
</view>
</view>
</template>
@@ -0,0 +1,109 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar
:front-color="$theme.navColor"
:background-color="$theme.navBgColor"
/>
<!-- #endif -->
</page-meta>
<view class="recharge p-[20rpx]">
<view class="bg-white rounded-[14rpx] p-[40rpx]">
<view class="text-content">充值金额</view>
<view class="border-0 border-b border-solid border-light">
<input
v-model="money"
class="text-[60rpx] h-[60rpx] py-[24rpx]"
placeholder="0.00"
type="digit"
/>
</view>
<view class="mt-[20rpx] text-xs text-muted">
当前可用余额
<text class="text-primary"> {{ wallet.user_money }}</text>
</view>
</view>
<view class="mt-[40rpx]">
<u-button :loading="isLock" type="primary" shape="circle" @click="rechargeLock">
立即充值
</u-button>
</view>
<view class="flex justify-center m-[60rpx]">
<navigator url="/packages/pages/recharge_record/recharge_record" hover-class="none">
<text class="text-content text-sm">充值记录</text>
</navigator>
</view>
<payment
v-model:show="payState.showPay"
v-model:show-check="payState.showCheck"
:order-id="payState.orderId"
:from="payState.from"
:redirect="payState.redirect"
@success="handlePaySuccess"
@fail="handlePayFail"
/>
</view>
</template>
<script lang="ts" setup>
import { recharge, rechargeConfig } from '@/api/recharge'
import { useLockFn } from '@/hooks/useLockFn'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { reactive, ref } from 'vue'
const money = ref('')
const payState = reactive({
orderId: '',
from: '',
showPay: false,
showCheck: false,
redirect: '/packages/pages/recharge/recharge'
})
const wallet = reactive({
user_money: '',
min_amount: 0
})
const { isLock, lockFn: rechargeLock } = useLockFn(async () => {
const minNum = wallet.min_amount
if (!money.value) return uni.$u.toast('请输入充值金额')
if (minNum == 0 && Number(money.value) == minNum) {
return uni.$u.toast(`充值金额必须大于0`)
}
if (Number(money.value) < minNum) return uni.$u.toast(`最低充值金额${minNum}`)
const data = await recharge({
money: money.value
})
payState.orderId = data.order_id
payState.from = data.from
payState.showPay = true
})
const handlePaySuccess = async () => {
payState.showPay = false
payState.showCheck = false
uni.navigateTo({
url: `/pages/payment_result/payment_result?id=${payState.orderId}&from=${payState.from}`
})
}
const handlePayFail = async () => {
uni.$u.toast('支付失败')
}
const getWallet = async () => {
const data = await rechargeConfig()
Object.assign(wallet, data)
}
onLoad((options: any) => {
// h5支付用于弹起手动确认支付弹窗
if (options?.checkPay) {
payState.orderId = options.id
payState.from = options.from
payState.showCheck = true
}
})
onShow(() => {
getWallet()
})
</script>
@@ -0,0 +1,52 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar
:front-color="$theme.navColor"
:background-color="$theme.navBgColor"
/>
<!-- #endif -->
</page-meta>
<z-paging
ref="paging"
v-model="dataList"
@query="queryList"
:show-loading-more-when-reload="true"
>
<view class="pt-2.5">
<view
v-for="item in dataList"
:key="item.id"
class="bg-white border-solid border-b border-0 border-light px-[26rpx] py-[24rpx]"
>
<view class="flex justify-between">
<view class="mr-2">{{ item.tips }}</view>
<view class="text-lg text-primary"> +{{ item.order_amount }} </view>
</view>
<view class="text-sm text-muted mr-1">{{ item.create_time }}</view>
</view>
</view>
</z-paging>
</template>
<script lang="ts" setup>
import { ref, shallowRef } from 'vue'
import { rechargeRecord } from '@/api/recharge'
const paging = shallowRef()
const dataList = ref<any[]>([])
const queryList = async (pageNo: number, pageSize: number) => {
try {
const data = await rechargeRecord({
page_no: pageNo,
page_size: pageSize
})
paging.value.complete(data.lists)
} catch (error) {
paging.value.complete(false)
}
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,120 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar
:front-color="$theme.navColor"
:background-color="$theme.navBgColor"
/>
<!-- #endif -->
</page-meta>
<z-paging
ref="paging"
v-model="dataList"
@query="queryList"
:show-loading-more-when-reload="true"
>
<view class="user-wallet">
<view class="p-[20rpx]">
<view
class="bg-primary rounded-[14rpx] flex items-center justify-between pl-[44rpx] py-[54rpx] text-white"
>
<view>
<view class="text-sm">钱包余额</view>
<view class="text-[60rpx]">{{ wallet.user_money }}</view>
</view>
<navigator
v-if="wallet.status"
url="/packages/pages/recharge/recharge"
hover-class="none"
>
<view class="text-primary px-[30rpx] py-[15rpx] bg-white rounded-l-full">
去充值
</view>
</navigator>
</view>
</view>
<u-tabs
:list="tabList"
:is-scroll="false"
v-model="current"
activeColor="var(--color-primary)"
@change="changeType"
></u-tabs>
<view class="pt-2.5">
<view
v-for="item in dataList"
:key="item.id"
class="bg-white border-solid border-b border-0 border-light px-[26rpx] py-[24rpx]"
>
<view class="flex justify-between">
<view class="mr-2">{{ item.type_desc }}</view>
<view
class="text-lg"
:class="{
'text-primary': item.action == 1
}"
>
{{ item.change_amount_desc }}
</view>
</view>
<view class="text-sm text-muted mr-1">{{ item.create_time }}</view>
</view>
</view>
</view>
</z-paging>
</template>
<script lang="ts" setup>
import { ref, shallowRef } from 'vue'
import { accountLog } from '@/api/user'
import { rechargeConfig } from '@/api/recharge'
import { onShow } from '@dcloudio/uni-app'
const tabList = ref([
{
name: '全部',
type: ''
},
{
name: '收入',
type: 1
},
{
name: '支出',
type: 2
}
])
const paging = shallowRef()
const dataList = ref<any[]>([])
const current = ref(0)
const changeType = (index: number) => {
current.value = index
paging.value.reload()
}
const queryList = async (pageNo: number, pageSize: number) => {
try {
const action = tabList.value[current.value].type
const data = await accountLog({
action,
type: 'um',
page_no: pageNo,
page_size: pageSize
})
paging.value.complete(data.lists)
} catch (error) {
paging.value.complete(false)
}
}
const wallet = ref<any>({})
const getWallet = async () => {
wallet.value = await rechargeConfig()
}
onShow(() => {
getWallet()
})
</script>
<style lang="scss" scoped></style>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

+247
View File
@@ -0,0 +1,247 @@
{
"pages": [
{
"path": "pages/index/index",
"style": {
// #ifndef H5
"navigationStyle": "custom",
// #endif
// #ifdef H5
"navigationBarTitleText": "首页"
// #endif
}
},
{
"path": "pages/news/news",
"style": {
// #ifndef H5
"navigationStyle": "custom",
// #endif
// #ifdef H5
"navigationBarTitleText": "资讯",
// #endif
"disableScroll": true
}
},
{
"path": "pages/user/user",
"style": {
// #ifndef H5
"navigationStyle": "custom",
// #endif
// #ifdef H5
"navigationBarTitleText": "个人中心"
// #endif
}
},
{
"path": "pages/login/login",
"style": {
"navigationBarTitleText": "登录"
},
"meta": {
"white": true
}
},
{
"path": "pages/register/register",
"style": {
"navigationBarTitleText": "注册"
},
"meta": {
"white": true
}
},
{
"path": "pages/forget_pwd/forget_pwd",
"style": {
"navigationBarTitleText": "忘记密码"
},
"meta": {
"white": true
}
},
{
"path": "pages/customer_service/customer_service",
"style": {
"navigationBarTitleText": "联系客服"
},
"meta": {
"white": true
}
},
{
"path": "pages/news_detail/news_detail",
"style": {
"navigationBarTitleText": "详情"
}
},
{
"path": "pages/user_set/user_set",
"style": {
"navigationBarTitleText": "个人设置"
},
"meta": {
"auth": true
}
},
{
"path": "pages/collection/collection",
"style": {
"navigationBarTitleText": "我的收藏"
},
"meta": {
"auth": true
}
},
{
"path": "pages/as_us/as_us",
"style": {
"navigationBarTitleText": "关于我们"
}
},
{
"path": "pages/agreement/agreement",
"style": {
"navigationBarTitleText": "协议"
}
},
{
"path": "pages/change_password/change_password",
"style": {
"navigationBarTitleText": "修改密码"
},
"meta": {
"auth": true
}
},
{
"path": "pages/user_data/user_data",
"style": {
"navigationBarTitleText": "个人资料"
},
"meta": {
"auth": true
}
},
{
"path": "pages/search/search",
"style": {
"navigationBarTitleText": "搜索"
}
},
{
"path": "pages/webview/webview"
},
{
"path": "pages/bind_mobile/bind_mobile",
"style": {
"navigationBarTitleText": "绑定手机号"
}
},
{
"path": "pages/empty/empty",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/payment_result/payment_result",
"style": {
"navigationBarTitleText": "支付结果"
},
"meta": {
"auth": true
}
},
{
"path": "uni_modules/vk-uview-ui/components/u-avatar-cropper/u-avatar-cropper",
"style": {
"navigationBarTitleText": "头像裁剪",
"navigationBarBackgroundColor": "#000000"
}
}
],
"subPackages": [{
"root": "packages",
"pages": [
{
"path": "pages/404/404",
"style": {
"navigationBarTitleText": "404"
},
"name": "404",
"meta": {
"white": true
}
},
{
"path": "pages/user_wallet/user_wallet",
"style": {
"navigationBarTitleText": "我的钱包"
},
"meta": {
"auth": true
}
},
{
"path": "pages/recharge/recharge",
"style": {
"navigationBarTitleText": "充值"
},
"meta": {
"auth": true
}
},
{
"path": "pages/recharge_record/recharge_record",
"style": {
"navigationBarTitleText": "充值记录"
},
"meta": {
"auth": true
}
}
]
}],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "商城",
"navigationBarBackgroundColor": "#FFFFFF",
"backgroundColor": "#F8F8F8",
"h5": {
"navigationStyle": "custom"
}
},
"tabBar": {
"custom": true,
"iconWidth": "20px",
"list": [
{
"iconPath": "static/images/tabbar/home.png",
"selectedIconPath": "static/images/tabbar/home_s.png",
"pagePath": "pages/index/index",
"text": "首页"
},
{
"iconPath": "static/images/tabbar/news.png",
"selectedIconPath": "static/images/tabbar/news_s.png",
"pagePath": "pages/news/news",
"text": "资讯"
},
{
"iconPath": "static/images/tabbar/user.png",
"selectedIconPath": "static/images/tabbar/user_s.png",
"pagePath": "pages/user/user",
"text": "我的"
}
]
},
"easycom": {
"custom": {
"router-navigate": "uniapp-router-next/components/router-navigate/router-navigate.vue",
"^(?!z-paging-refresh|z-paging-load-more)z-paging(.*)": "z-paging/components/z-paging$1/z-paging$1.vue",
"^w-(.*)": "@/components/widgets/$1/$1.vue"
}
}
}
+31
View File
@@ -0,0 +1,31 @@
<template>
<view class="p-[30rpx]">
<u-parse :html="agreementContent"></u-parse>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getPolicy } from '@/api/app'
let agreementType = ref('') // 协议类型
const agreementContent = ref('') // 协议内容
const getData = async (type) => {
const res = await getPolicy({ type })
agreementContent.value = res.content
uni.setNavigationBarTitle({
title: String(res.title)
})
}
onLoad((options: any) => {
if (options.type) {
agreementType = options.type
getData(agreementType)
}
})
</script>
<style lang="scss" scoped></style>
+30
View File
@@ -0,0 +1,30 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar
:front-color="$theme.navColor"
:background-color="$theme.navBgColor"
/>
<!-- #endif -->
</page-meta>
<view class="as-us flex flex-1 flex-col items-center">
<image :src="appStore.getWebsiteConfig.shop_logo" mode="" class="img"></image>
<view class="text-content mt-[20rpx]">当前版本{{ appStore.config.version }}</view>
</view>
</template>
<script setup lang="ts">
import { useAppStore } from '@/stores/app'
const appStore = useAppStore()
</script>
<style lang="scss" scoped>
.as-us {
.img {
width: 160rpx;
height: 160rpx;
border-radius: 20rpx;
margin-top: 96rpx;
}
}
</style>
@@ -0,0 +1,112 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar
:front-color="$theme.navColor"
:background-color="$theme.navBgColor"
/>
<!-- #endif -->
</page-meta>
<view class="bg-white min-h-full flex flex-col items-center px-[40rpx] pt-[40rpx] box-border">
<view class="w-full">
<view class="text-2xl font-medium mb-[60rpx]">绑定手机号</view>
<view
class="px-[18rpx] border border-solid border-lightc border-light rounded-[10rpx] h-[100rpx] items-center flex"
>
<u-input
class="flex-1"
v-model="formData.mobile"
:border="false"
placeholder="请输入手机号码"
/>
</view>
<view
class="px-[18rpx] border border-solid border-lightc border-light rounded-[10rpx] h-[100rpx] items-center flex mt-[40rpx]"
>
<u-input
class="flex-1"
v-model="formData.code"
placeholder="请输入验证码"
:border="false"
/>
<view
class="border-l border-solid border-0 border-light pl-3 leading-4 ml-3 w-[180rpx]"
@click="sendSms"
>
<u-verification-code
ref="uCodeRef"
:seconds="60"
@change="codeChange"
change-text="x秒"
/>
<text :class="formData.mobile ? 'text-primary' : 'text-muted'">
{{ codeTips }}
</text>
</view>
</view>
<view class="mt-[40rpx]">
<u-button
type="primary"
hover-class="none"
:customStyle="{
height: '100rpx',
opacity:
formData.mobile && formData.code
? '1'
: '0.5'
}"
@click="handleConfirm"
>
确定
</u-button>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { userBindMobile } from '@/api/user'
import { smsSend } from '@/api/app'
import { SMSEnum } from '@/enums/appEnums'
import { reactive, ref, shallowRef } from 'vue'
import { useUserStore } from '@/stores/user'
const uCodeRef = shallowRef()
const codeTips = ref('')
const userStore = useUserStore()
const codeChange = (text: string) => {
codeTips.value = text
}
const formData = reactive({
type: 'bind',
mobile: '',
code: ''
})
const sendSms = async () => {
if (!formData.mobile) return uni.$u.toast('请输入手机号码')
if (uCodeRef.value?.canGetCode) {
await smsSend({
scene: SMSEnum.BIND_MOBILE,
mobile: formData.mobile
})
uni.$u.toast('发送成功')
uCodeRef.value?.start()
}
}
const handleConfirm = async () => {
if (!formData.mobile) return uni.$u.toast('请输入手机号码')
if (!formData.code) return uni.$u.toast('请输入验证码')
await userBindMobile(formData, { token: userStore.temToken })
uni.$u.toast('绑定成功')
userStore.login(userStore.temToken!)
uni.navigateBack()
}
</script>
<style lang="scss">
page {
height: 100%;
}
</style>
@@ -0,0 +1,90 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar
:front-color="$theme.navColor"
:background-color="$theme.navBgColor"
/>
<!-- #endif -->
</page-meta>
<view
class="register bg-white min-h-full flex flex-col items-center px-[40rpx] pt-[100rpx] box-border"
>
<view class="w-full">
<view class="text-2xl font-medium mb-[60rpx]">
{{ type == 'set' ? '设置登录密码' : '修改登录密码' }}
</view>
<u-form borderBottom :label-width="150">
<u-form-item label="原密码" borderBottom v-if="type != 'set'">
<u-input
class="flex-1"
type="password"
v-model="formData.old_password"
:border="false"
placeholder="请输入原来的密码"
/>
</u-form-item>
<u-form-item label="新密码" borderBottom>
<u-input
class="flex-1"
type="password"
v-model="formData.password"
placeholder="6-20位数字+字母或符号组合"
:border="false"
/>
</u-form-item>
<u-form-item label="确认密码" borderBottom>
<u-input
class="flex-1"
type="password"
v-model="formData.password_confirm"
placeholder="再次输入新密码"
:border="false"
/>
</u-form-item>
</u-form>
<view class="mt-[100rpx]">
<u-button type="primary" shape="circle" @click="handleConfirm"> 确定 </u-button>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { userChangePwd } from '@/api/user'
import { onLoad } from '@dcloudio/uni-app'
import { reactive, ref } from 'vue'
const type = ref('')
const formData = reactive<any>({
password: '',
password_confirm: ''
})
const handleConfirm = async () => {
if (!formData.old_password && type.value != 'set') return uni.$u.toast('请输入原来的密码')
if (!formData.password) return uni.$u.toast('请输入密码')
if (!formData.password_confirm) return uni.$u.toast('请输入确认密码')
if (formData.password != formData.password_confirm) return uni.$u.toast('两次输入的密码不一致')
await userChangePwd(formData)
uni.$u.toast('操作成功')
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
onLoad((options) => {
type.value = options.type || ''
if (type.value == 'set') {
uni.setNavigationBarTitle({
title: '设置登录密码'
})
}
})
</script>
<style lang="scss">
page {
height: 100%;
}
</style>
@@ -0,0 +1,70 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar
:front-color="$theme.navColor"
:background-color="$theme.navBgColor"
/>
<!-- #endif -->
</page-meta>
<z-paging
ref="paging"
v-model="collectData"
@query="queryList"
:fixed="false"
height="100%"
use-page-scroll
>
<u-swipe-action
:show="item.show"
:index="index"
v-for="(item, index) in collectData"
:key="item.id"
@click="handleCollect"
:options="options"
btn-width="120"
>
<news-card :item="item" :newsId="item.article_id"></news-card>
</u-swipe-action>
</z-paging>
</template>
<script lang="ts" setup>
import { ref, reactive, shallowRef } from 'vue'
import { getCollect, cancelCollect } from '@/api/news'
const paging = shallowRef()
const options = reactive([
{
text: '取消收藏',
style: {
color: '#FFFFFF',
backgroundColor: '#FF2C3C'
}
}
])
const collectData: any = ref([])
const queryList = async (pageNo, pageSize) => {
const { lists } = await getCollect()
lists.forEach((item: any) => {
item.show = false
})
collectData.value = lists
paging.value.complete(lists)
}
const handleCollect = async (index: number): Promise<void> => {
try {
const article_id: number = collectData.value[index].article_id
await cancelCollect({ id: article_id })
uni.$u.toast('已取消收藏')
paging.value.reload()
} catch (err) {
//TODO handle the exception
console.log('取消收藏报错=>', err)
}
}
</script>
<style scoped></style>
@@ -0,0 +1,34 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar
:front-color="$theme.navColor"
:background-color="$theme.navBgColor"
/>
<!-- #endif -->
</page-meta>
<view class="customer-service">
<view v-for="(item, index) in state.pages" :key="index">
<template v-if="item.name == 'customer-service'">
<w-customer-service :content="item.content" :styles="item.styles" />
</template>
</view>
</view>
</template>
<script setup lang="ts">
import { getDecorate } from '@/api/shop'
import { reactive } from 'vue'
const state = reactive<{
pages: any[]
}>({
pages: []
})
const getData = async () => {
const data = await getDecorate({ id: 3 })
state.pages = JSON.parse(data.data)
}
getData()
</script>
<style></style>
+7
View File
@@ -0,0 +1,7 @@
<template>
<div></div>
</template>
<script setup lang="ts"></script>
<style></style>
+119
View File
@@ -0,0 +1,119 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar
:front-color="$theme.navColor"
:background-color="$theme.navBgColor"
/>
<!-- #endif -->
</page-meta>
<view
class="register bg-white min-h-full flex flex-col items-center px-[40rpx] pt-[100rpx] box-border"
>
<view class="w-full">
<view class="text-2xl font-medium mb-[60rpx]">忘记登录密码</view>
<u-form borderBottom :label-width="150">
<u-form-item label="手机号" borderBottom>
<u-input
class="flex-1"
v-model="formData.mobile"
:border="false"
placeholder="请输入手机号码"
/>
</u-form-item>
<u-form-item label="验证码" borderBottom>
<u-input
class="flex-1"
v-model="formData.code"
placeholder="请输入验证码"
:border="false"
/>
<view
class="border-l border-solid border-0 border-light pl-3 text-muted leading-4 ml-3 w-[180rpx]"
@click="sendSms"
>
<u-verification-code
ref="uCodeRef"
:seconds="60"
@change="codeChange"
change-text="x秒"
/>
<text :class="formData.mobile ? 'text-primary' : 'text-muted'">
{{ codeTips }}
</text>
</view>
</u-form-item>
<u-form-item label="新密码" borderBottom>
<u-input
class="flex-1"
type="password"
v-model="formData.password"
placeholder="6-20位数字+字母或符号组合"
:border="false"
/>
</u-form-item>
<u-form-item label="确认密码" borderBottom>
<u-input
class="flex-1"
type="password"
v-model="formData.password_confirm"
placeholder="再次输入新密码"
:border="false"
/>
</u-form-item>
</u-form>
<view class="mt-[100rpx]">
<u-button type="primary" shape="circle" @click="handleConfirm"> 确定 </u-button>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { smsSend } from '@/api/app'
import { forgotPassword } from '@/api/user'
import { SMSEnum } from '@/enums/appEnums'
import { reactive, ref, shallowRef } from 'vue'
const uCodeRef = shallowRef()
const codeTips = ref('')
const formData = reactive({
mobile: '',
code: '',
password: '',
password_confirm: ''
})
const codeChange = (text: string) => {
codeTips.value = text
}
const sendSms = async () => {
if (!formData.mobile) return
if (uCodeRef.value?.canGetCode) {
await smsSend({
scene: SMSEnum.FIND_PASSWORD,
mobile: formData.mobile
})
uni.$u.toast('发送成功')
uCodeRef.value?.start()
}
}
const handleConfirm = async () => {
if (!formData.mobile) return uni.$u.toast('请输入手机号码')
if (!formData.password) return uni.$u.toast('请输入密码')
if (!formData.password_confirm) return uni.$u.toast('请输入确认密码')
if (formData.password != formData.password_confirm) return uni.$u.toast('两次输入的密码不一致')
await forgotPassword(formData)
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
</script>
<style lang="scss">
page {
height: 100%;
}
</style>
@@ -0,0 +1,158 @@
<template>
<!-- modal:隐私授权弹窗-->
<view v-if="show" class="modal-box" @tap.stop>
<view class="dialog" @tap.stop>
<view class="title">隐私政策提示</view>
<view class="content">
欢迎使用{{
appStore.getWebsiteConfig.shop_name
}}小程序请您在使用前点击
<text
class="text-[#243245]"
hover-class="hover"
@click="openContract"
>
{{ name }}
</text>
并仔细阅读如您同意全部内容请点击同意开始使用我们的服务
</view>
<view class="btn-box">
<button
class="btn disagree"
hover-class="hover"
@click="disagreePrivacy"
>
不同意
</button>
<button
class="btn bg-primary text-white"
hover-class="hover"
id="agree-btn"
open-type="agreePrivacyAuthorization"
@agreeprivacyauthorization="agreePrivacy"
>
同意
</button>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { useAppStore } from '@/stores/app'
const appStore = useAppStore()
const name = ref<string>('')
const show = ref<boolean>(false)
interface PrivacyRes {
errMsg: string
privacyContractName: string
needAuthorization: boolean
}
if (wx.getPrivacySetting) {
wx.getPrivacySetting({
success(res: PrivacyRes) {
console.log(res)
name.value = res.privacyContractName
show.value = res.needAuthorization
}
})
}
const openContract = () => {
wx.openPrivacyContract({
success: () => {},
fail: () => {}
})
}
const disagreeHandle = () => {
// 用户点击拒绝后
show.value = false
}
const disagreePrivacy = () => {
uni.$u.toast('同意隐私政策后可继续使用')
// wx.exitMiniProgram()
}
const agreePrivacy = () => {
show.value = false
}
</script>
<style scoped>
.modal-box {
height: 100vh;
width: 100vw;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 99999;
}
.modal-box .dialog {
box-sizing: border-box;
position: absolute;
bottom: 0;
width: 100%;
padding: 40rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: #ffffff;
border-radius: 20rpx 20rpx 0 0;
}
.modal-box .title {
text-align: center;
color: #333;
font-weight: bold;
font-size: 34rpx;
}
.modal-box .content {
display: block;
font-size: 28rpx;
color: #666;
margin-top: 20rpx;
text-align: justify;
line-height: 1.6;
padding: 10rpx 20rpx;
}
.modal-box .btn-box {
margin-top: 50rpx;
padding: 0 30rpx;
padding-bottom: 30rpx;
display: flex;
text-align: center;
}
.modal-box .btn::after {
border: none;
display: none;
}
.modal-box .btn-box .btn {
width: 50%;
height: 76rpx;
line-height: 76rpx;
margin: 0 10rpx;
padding: 0;
align-items: center;
justify-content: center;
border-radius: 60px;
font-size: 28rpx;
font-weight: 500;
}
.modal-box .disagree {
color: #0f0f0f;
background: #f5f5f5;
}
</style>
+183
View File
@@ -0,0 +1,183 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar
:front-color="$theme.navColor"
:background-color="$theme.navBgColor"
/>
<!-- #endif -->
</page-meta>
<view class="index" :style="pageStyle">
<!-- 组件 -->
<template
v-for="(item, index) in state.pages"
:key="index"
>
<template v-if="item.name == 'search'">
<w-search
:pageMeta="state.meta"
:content="item.content"
:styles="item.styles"
:percent="percent"
:isLargeScreen="isLargeScreen"
/>
</template>
<template v-if="item.name == 'banner'">
<w-banner
:content="item.content"
:styles="item.styles"
:isLargeScreen="isLargeScreen"
@change="handleBanner"
/>
</template>
<template v-if="item.name == 'nav'">
<w-nav :content="item.content" :styles="item.styles"/>
</template>
<template v-if="item.name == 'middle-banner'">
<w-middle-banner :content="item.content" :styles="item.styles" />
</template>
</template>
<view class="article" v-if="state.article.length">
<view
class="flex items-center article-title mx-[20rpx] my-[30rpx] text-lg font-medium"
>
最新资讯
</view>
<news-card
v-for="item in state.article"
:key="item.id"
:news-id="item.id"
:item="item"
/>
</view>
<!-- #ifdef H5 -->
<view class="text-center py-4 mb-12">
<router-navigate
class="mx-1 text-xs text-[#495770]"
:to="{
path: '/pages/webview/webview',
query: {
url: item.value
}
}"
v-for="item in appStore.getCopyrightConfig"
:key="item.key"
>
{{ item.key }}
</router-navigate>
</view>
<!-- #endif -->
<!-- 返回顶部按钮 -->
<u-back-top
:scroll-top="scrollTop"
:top="100"
:customStyle="{
backgroundColor: '#FFF',
color: '#000',
boxShadow: '0px 3px 6px rgba(0, 0, 0, 0.1)'
}"
>
</u-back-top>
<!-- #ifdef MP -->
<!-- 微信小程序隐私弹窗 -->
<MpPrivacyPopup></MpPrivacyPopup>
<!-- #endif -->
<tabbar/>
</view>
</template>
<script setup lang="ts">
import {getIndex} from '@/api/shop'
import {onLoad, onPageScroll} from "@dcloudio/uni-app";
import {computed, reactive, ref} from 'vue'
import {useAppStore} from '@/stores/app'
// #ifdef MP
import MpPrivacyPopup from './component/mp-privacy-popup.vue'
// #endif
const appStore = useAppStore()
const state = reactive<{
pages: any[]
meta: any[]
article: any[]
bannerImage: string
}>({
pages: [],
meta: [],
article: [],
bannerImage: ''
})
const scrollTop = ref<number>(0)
const percent = ref<number>(0)
// 是否联动背景图
const isLinkage = computed(() => {
return state.pages.find((item: any) => item.name === 'banner')?.content.bg_style === 1
})
// 是否大屏banner
const isLargeScreen = computed(() => {
return state.pages.find((item: any) => item.name === 'banner')?.content.style === 2
})
// 根页面样式
const pageStyle = computed(() => {
const {bg_type, bg_color, bg_image} = state.meta[0]?.content ?? {}
if (!isLinkage.value) {
return bg_type == 1 ?
{'background-color': bg_color} :
{'background-image': `url(${bg_image})`}
}
else return {'background-image': `url(${state.bannerImage})`}
})
const handleBanner = (url: string) => {
state.bannerImage = url
}
const getData = async () => {
const data = await getIndex()
state.pages = JSON.parse(data?.page?.data)
state.meta = JSON.parse(data?.page?.meta)
state.article = data.article
uni.setNavigationBarTitle({
title: state.meta[0].content.title
})
}
onPageScroll((event: any) => {
scrollTop.value = event.scrollTop
const top = uni.upx2px(100)
percent.value = event.scrollTop / top > 1 ? 1 : event.scrollTop / top
})
onLoad(() => { getData() })
</script>
<style lang="scss" scoped>
.index {
position: relative;
background-repeat: no-repeat;
background-size: 100% auto;
overflow: hidden;
width: 100%;
transition: all 1s;
min-height: calc(100vh - env(safe-area-inset-bottom));
}
.article-title {
&::before {
content: '';
width: 8rpx;
height: 34rpx;
display: block;
margin-right: 10rpx;
@apply bg-primary;
}
}
</style>
+480
View File
@@ -0,0 +1,480 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar :front-color="$theme.navColor" :background-color="$theme.navBgColor" />
<!-- #endif -->
</page-meta>
<view
class="bg-white login min-h-full flex flex-col items-center px-[40rpx] pt-[120rpx] box-border"
>
<view>
<image
:src="appStore.getWebsiteConfig.shop_logo"
mode="widthFix"
class="w-[160rpx] h-[160rpx] rounded-full"
/>
</view>
<view class="w-full mt-[140rpx] pb-[60rpx]">
<block v-if="!phoneLogin">
<!-- #ifdef MP-WEIXIN || H5 -->
<view v-if="isOpenOtherAuth && isWeixin && inWxAuth">
<u-button
type="primary"
@click="wxLogin"
:customStyle="{ height: '100rpx' }"
hover-class="none"
>
用户一键登录
</u-button>
</view>
<!-- #endif -->
<view class="mt-[40rpx]">
<u-button
@click="phoneLogin = !phoneLogin"
:customStyle="{ height: '100rpx' }"
hover-class="none"
>
手机号登录
</u-button>
</view>
</block>
<block v-if="phoneLogin">
<!-- 密码登录 -->
<template
v-if="
formData.scene == LoginWayEnum.ACCOUNT &&
includeLoginWay(LoginWayEnum.ACCOUNT)
"
>
<view
class="px-[18rpx] border border-solid border-lightc border-light rounded-[10rpx] h-[100rpx] items-center flex"
>
<u-input
class="flex-1"
v-model="formData.account"
:border="false"
placeholder="输入账号"
/>
</view>
<view
class="px-[18rpx] py-[10rpx] border border-solid border-light rounded-[10rpx] flex h-[100rpx] items-center mt-[40rpx]"
>
<u-input
class="flex-1"
v-model="formData.password"
type="password"
placeholder="输入密码"
:border="false"
/>
<navigator url="/pages/forget_pwd/forget_pwd" hover-class="none">
<view
class="border-l border-solid border-0 border-light pl-3 text-muted leading-4 ml-3"
>
忘记密码?
</view>
</navigator>
</view>
</template>
<!-- 验证码登录 -->
<template
v-if="
formData.scene == LoginWayEnum.MOBILE &&
includeLoginWay(LoginWayEnum.MOBILE)
"
>
<view
class="px-[18rpx] border border-solid border-lightc border-light rounded-[10rpx] h-[100rpx] items-center flex"
>
<u-input
class="flex-1"
v-model="formData.account"
:border="false"
placeholder="请输入手机号码"
/>
</view>
<view
class="px-[18rpx] border border-solid border-lightc border-light rounded-[10rpx] h-[100rpx] items-center flex mt-[40rpx]"
>
<u-input
class="flex-1"
v-model="formData.code"
placeholder="请输入验证码"
:border="false"
/>
<view
class="border-l border-solid border-0 border-light pl-3 leading-4 ml-3 w-[180rpx]"
@click="sendSms"
>
<u-verification-code
ref="uCodeRef"
:seconds="60"
@change="codeChange"
change-text="x秒"
/>
<text :class="formData.account ? 'text-primary' : 'text-muted'">
{{ codeTips }}
</text>
</view>
</view>
</template>
</block>
<view class="mt-[40rpx]" v-if="isOpenAgreement">
<u-checkbox v-model="isCheckAgreement" shape="circle">
<view class="text-xs flex">
已阅读并同意
<view @click.stop>
<navigator
class="text-primary"
hover-class="none"
url="/pages/agreement/agreement?type=service"
>
《服务协议》
</navigator>
</view>
<view @click.stop>
<navigator
class="text-primary"
hover-class="none"
url="/pages/agreement/agreement?type=privacy"
>
《隐私协议》
</navigator>
</view>
</view>
</u-checkbox>
</view>
<block v-if="phoneLogin">
<view class="mt-[60rpx]">
<u-button
type="primary"
@click="handleLogin(formData.scene)"
:customStyle="{
height: '100rpx',
opacity: DisableStyle ? '1' : '0.5'
}"
hover-class="none"
>
登录
</u-button>
</view>
<view class="flex justify-between mt-[40rpx]">
<view
>已有账号,使用
<span
class="text-primary"
@click="changeLoginWay(LoginWayEnum.ACCOUNT)"
v-if="
formData.scene == LoginWayEnum.MOBILE &&
includeLoginWay(LoginWayEnum.ACCOUNT)
"
>密码登录</span
>
<span
class="text-primary"
@click="changeLoginWay(LoginWayEnum.MOBILE)"
v-if="
formData.scene == LoginWayEnum.ACCOUNT &&
includeLoginWay(LoginWayEnum.MOBILE)
"
>验证码登录</span
>
</view>
<navigator url="/pages/register/register" hover-class="none"
>注册账号</navigator
>
</view>
</block>
</view>
<!-- 协议弹框 -->
<u-modal
v-model="showModel"
show-cancel-button
:show-title="false"
confirm-color="var(--color-primary)"
@confirm=";(isCheckAgreement = true), (showModel = false)"
@cancel="showModel = false"
>
<view class="text-center px-[70rpx] py-[60rpx]">
<view> 请先阅读并同意 </view>
<view class="flex justify-center">
<navigator data-theme="" url="/pages/agreement/agreement?type=service">
<view class="text-primary">《服务协议》</view>
</navigator>
<navigator url="/pages/agreement/agreement?type=privacy">
<view class="text-primary">《隐私协议》</view>
</navigator>
</view>
</view>
</u-modal>
<!-- #ifdef MP-WEIXIN -->
<mplogin-popup
v-model:show="showLoginPopup"
:logo="websiteConfig.shop_logo"
:title="websiteConfig.shop_name"
@update="handleUpdateUser"
/>
<!-- #endif -->
</view>
</template>
<script setup lang="ts">
import { login, mnpLogin, updateUser, OALogin } from '@/api/account'
import { smsSend } from '@/api/app'
import { SMSEnum } from '@/enums/appEnums'
import { BACK_URL } from '@/enums/constantEnums'
import { useLockFn } from '@/hooks/useLockFn'
import { useAppStore } from '@/stores/app'
import { useUserStore } from '@/stores/user'
import { useRouter, useRoute } from 'uniapp-router-next'
import cache from '@/utils/cache'
import { isWeixinClient } from '@/utils/client'
// #ifdef H5
import wechatOa, { UrlScene } from '@/utils/wechat'
// #endif
import { onLoad, onShow } from '@dcloudio/uni-app'
import { computed, reactive, ref, shallowRef, watch } from 'vue'
enum LoginWayEnum {
ACCOUNT = 1,
MOBILE = 2
}
const isWeixin = ref(true)
// #ifdef H5
isWeixin.value = isWeixinClient()
// #endif
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const appStore = useAppStore()
const showModel = ref(false)
const uCodeRef = shallowRef()
const codeTips = ref('')
const showLoginPopup = ref(false)
const isCheckAgreement = ref(false)
const formData = reactive({
scene: 1,
account: '',
password: '',
code: ''
})
const phoneLogin = ref(false)
const loginData = ref()
const codeChange = (text: string) => {
codeTips.value = text
}
const websiteConfig = computed(() => appStore.getWebsiteConfig)
const sendSms = async () => {
if (!formData.account) return
if (uCodeRef.value?.canGetCode) {
await smsSend({
scene: SMSEnum.LOGIN,
mobile: formData.account
})
uni.$u.toast('发送成功')
uCodeRef.value?.start()
}
}
const changeLoginWay = (way: LoginWayEnum) => {
formData.scene = way
}
const includeLoginWay = (way: LoginWayEnum) => {
return appStore.getLoginConfig.login_way?.includes(String(way))
}
const inWxAuth = computed(() => {
return appStore.getLoginConfig.wechat_auth
})
const isOpenAgreement = computed(() => appStore.getLoginConfig.login_agreement == 1)
const isOpenOtherAuth = computed(() => appStore.getLoginConfig.third_auth == 1)
const isForceBindMobile = computed(() => appStore.getLoginConfig.coerce_mobile == 1)
const loginFun = async () => {
if (!isCheckAgreement.value && isOpenAgreement.value) return (showModel.value = true)
if (formData.scene == LoginWayEnum.ACCOUNT) {
if (!formData.account) return uni.$u.toast('请输入账号/手机号码')
if (!formData.password) return uni.$u.toast('请输入密码')
}
if (formData.scene == LoginWayEnum.MOBILE) {
if (!formData.account) return uni.$u.toast('请输入手机号码')
if (!formData.code) return uni.$u.toast('请输入验证码')
}
uni.showLoading({
title: '请稍后...'
})
try {
const data = await login(formData)
loginHandle(data)
} catch (error: any) {
uni.hideLoading()
uni.$u.toast(error)
}
}
const loginHandle = async (data: any) => {
const { token, mobile } = data
if (!mobile && isForceBindMobile.value) {
userStore.temToken = token
router.navigateTo('/pages/bind_mobile/bind_mobile')
uni.hideLoading()
return
}
userStore.login(data.token)
await userStore.getUser()
uni.$u.toast('登录成功')
uni.hideLoading()
const pages = getCurrentPages()
if (pages.length > 1) {
const prevPage = pages[pages.length - 2]
await router.navigateBack()
// @ts-ignore
const { onLoad, options } = prevPage
// 刷新上一个页面
onLoad && onLoad(options)
} else if (cache.get(BACK_URL)) {
try {
router.redirectTo(cache.get(BACK_URL))
} catch (error) {
router.switchTab(cache.get(BACK_URL))
}
} else {
router.reLaunch('/pages/index/index')
}
cache.remove(BACK_URL)
}
const { lockFn: handleLogin } = useLockFn(loginFun)
const oaLogin = async (options: any = { getUrl: true }) => {
const { code, getUrl } = options
if (getUrl) {
await wechatOa.getUrl(UrlScene.LOGIN)
} else {
const data = await OALogin({
code
})
return data
}
return Promise.reject()
}
const wxLogin = async () => {
if (!isCheckAgreement.value && isOpenAgreement.value) {
showModel.value = true
console.log(showModel.value)
return
}
// #ifdef MP-WEIXIN
uni.showLoading({
title: '请稍后...'
})
try {
const { code }: any = await uni.login({
provider: 'weixin'
})
const data = await mnpLogin({
code: code
})
loginData.value = data
if (data.is_new_user) {
uni.hideLoading()
userStore.temToken = data.token
showLoginPopup.value = true
return
}
loginHandle(data)
} catch (error: any) {
uni.hideLoading()
uni.$u.toast(error)
}
// #endif
// #ifdef H5
if (isWeixin.value) {
oaLogin()
}
// #endif
}
const handleUpdateUser = async (value: any) => {
await updateUser(value, { token: userStore.temToken })
showLoginPopup.value = false
loginHandle(loginData.value)
}
watch(
() => appStore.getLoginConfig,
(value) => {
if (value.login_way) {
formData.scene = value.login_way[0]
}
},
{
immediate: true
}
)
const DisableStyle = computed(() => {
if (formData.scene == 1 && formData.account && formData.password) {
return true
} else if (formData.scene == 2 && formData.account && formData.code) {
return true
} else {
return false
}
})
const removeWxQuery = () => {
const options = route.query
if (options.code && options.state) {
delete options.code
delete options.state
router.redirectTo({ path: route.path, query: options })
}
}
onLoad(async () => {
//#ifdef H5
const options = wechatOa.getAuthData()
try {
if (options.code && options.scene === UrlScene.LOGIN) {
uni.showLoading({
title: '请稍后...'
})
const data = await oaLogin(options)
if (data) {
loginData.value = data
loginHandle(loginData.value)
}
}
} catch (error) {
removeWxQuery()
} finally {
uni.hideLoading()
//清除保存的授权数据
wechatOa.setAuthData()
}
//#endif
})
</script>
<style lang="scss">
page {
height: 100%;
}
</style>
@@ -0,0 +1,65 @@
<template>
<z-paging
auto-show-back-to-top
:auto="i == index"
ref="paging"
v-model="dataList"
:data-key="i"
@query="queryList"
:fixed="false"
height="100%"
>
<block v-for="(newsItem, newsIndex) in dataList" :key="newsIndex">
<news-card :item="newsItem" :newsId="newsItem.id"></news-card>
</block>
</z-paging>
</template>
<script lang="ts" setup>
import { ref, watch, nextTick, shallowRef } from 'vue'
import { getArticleList } from '@/api/news'
const props = withDefaults(
defineProps<{
cid: number
i: number
index: number
}>(),
{
cid: 0
}
)
const paging = shallowRef<any>(null)
const dataList = ref([])
const isFirst = ref<boolean>(true)
watch(
() => props.index,
async () => {
await nextTick()
if (props.i == props.index && isFirst.value) {
isFirst.value = false
paging.value?.reload()
}
},
{ immediate: true }
)
const queryList = async (page_no, page_size) => {
try {
const { lists } = await getArticleList({
cid: props.cid,
page_no,
page_size
})
paging.value.complete(lists)
} catch (e) {
console.log('报错=>', e)
//TODO handle the exception
paging.value.complete(false)
}
}
</script>
<style scoped></style>
+85
View File
@@ -0,0 +1,85 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar
:front-color="$theme.navColor"
:background-color="$theme.navBgColor"
/>
<!-- #endif -->
</page-meta>
<!-- #ifndef H5 -->
<u-sticky
h5-nav-height="0"
bg-color="transparent"
>
<u-navbar
:is-back="false"
:is-fixed="false"
title="资讯"
:border-bottom="false"
:title-bold="true"
:title-color="$theme.navColor"
:background="{ background: $theme.navBgColor }"
>
</u-navbar>
</u-sticky>
<!-- #endif -->
<view class="news">
<!-- 搜索 -->
<navigator class="news-search px-[24rpx] py-[14rpx] bg-white" url="/pages/search/search">
<u-search placeholder="请输入关键词搜索" disabled :show-action="false"></u-search>
</navigator>
<!-- 内容 -->
<tabs
:current="current"
@change="handleChange"
height="80"
bar-width="60"
:barStyle="{ bottom: '0' }"
>
<tab v-for="(item, i) in tabList" :key="i" :name="item.name">
<view class="news-list pt-[20rpx]">
<news-list :cid="item.id" :i="i" :index="current"></news-list>
</view>
</tab>
</tabs>
<tabbar />
</view>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import { onLoad, onShow, onReady } from '@dcloudio/uni-app'
import NewsList from './component/news-list.vue'
import { getArticleCate } from '@/api/news'
const tabList = ref<any>([])
const current = ref<number>(0)
const handleChange = (index: number) => {
console.log(index)
current.value = Number(index)
}
const getData = async () => {
const data = await getArticleCate()
tabList.value = [{ name: '全部', id: '' }].concat(data)
}
onLoad((options) => {
getData()
})
</script>
<style lang="scss">
.news {
&-search {
margin-bottom: 2rpx;
}
&-list {
height: calc(100vh - 272rpx - env(safe-area-inset-bottom));
}
}
</style>
@@ -0,0 +1,108 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar
:front-color="$theme.navColor"
:background-color="$theme.navBgColor"
/>
<!-- #endif -->
</page-meta>
<view class="news-detail bg-white">
<!-- 标题信心 -->
<view class="news-detail-header py-[20rpx] px-[30rpx]">
<view class="text-3xl font-medium">{{ newsData.title }}</view>
<view class="flex mt-[20rpx] text-xs">
<view class="mr-[40rpx]" v-if="newsData.author">作者: {{ newsData.author }}</view>
<view class="text-muted mr-[40rpx] flex-1">{{ newsData.create_time }}</view>
<view class="flex items-center text-muted flex-none">
<image
src="/static/images/icon/icon_visit.png"
class="w-[30rpx] h-[30rpx]"
></image>
<view class="ml-[10rpx]">{{ newsData.click }}</view>
</view>
</view>
</view>
<!-- 咨询内容 -->
<view class="news-detail-section bg-white p-[24rpx]">
<!-- 摘要 -->
<view class="summary p-[20rpx] text-base" v-if="newsData.abstract">
<text class="font-medium">摘要: </text> {{ newsData.abstract }}
</view>
<!-- 内容 -->
<view class="mt-[20rpx]">
<u-parse :html="newsData.content"></u-parse>
</view>
</view>
<view class="panel-btn flex items-center px-[34rpx]" @click="handleAddCollect(newsData.id)">
<u-icon
:name="newsData.collect ? 'star-fill' : 'star'"
size="40"
:color="newsData.collect ? '#F7BA47' : '#333'"
></u-icon>
<text class="ml-[10rpx]">收藏</text>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getArticleDetail, addCollect, cancelCollect } from '@/api/news'
const newsData = ref<any>({})
let newsId = ''
const getData = async (id) => {
newsData.value = await getArticleDetail({ id })
}
const handleAddCollect = async (id: number) => {
try {
if (newsData.value.collect) {
await cancelCollect({ id })
uni.$u.toast('已取消收藏')
} else {
await addCollect({ id })
uni.$u.toast('收藏成功')
}
getData(newsId)
} catch (e) {
//TODO handle the exception
}
}
onLoad((options: any) => {
newsId = options.id
getData(newsId)
})
</script>
<style lang="scss">
.news-detail {
height: 100%;
&-header {
border-bottom: 2rpx solid #f8f8f8;
}
&-section {
.summary {
border-radius: 12rpx;
background-color: #f7f7f7;
}
}
.panel-btn {
position: fixed;
right: 30rpx;
height: 80rpx;
bottom: 80rpx;
border-radius: 40rpx;
background: rgba(255, 255, 255, 0.95);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.16);
}
}
</style>
@@ -0,0 +1,168 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar
:front-color="$theme.navColor"
:background-color="$theme.navBgColor"
/>
<!-- #endif -->
</page-meta>
<!-- 页面状态 -->
<page-status :status="status">
<template #error>
<u-empty text="订单不存在" mode="order"></u-empty>
</template>
<template #default>
<view class="payment-result p-[20rpx]">
<view class="result bg-white p-[20rpx] rounded-md">
<view class="flex flex-col items-center my-[40rpx]">
<!-- 支付状态图片 -->
<u-image
class="status-image"
:src="paymentStatus['image']"
width="100"
height="100"
shape="circle"
/>
<!-- 支付状态文字 -->
<text class="text-2xl font-medium mt-[20rpx]">{{
paymentStatus['text']
}}
</text>
<view class="text-3xl font-medium mt-[20rpx]">
¥ {{ orderInfo.order.order_amount }}
</view>
</view>
<!-- 支付信息 -->
<view class="result-info">
<view class="result-info__item">
<text>订单编号</text>
<text>{{ orderInfo.order.order_sn }}</text>
</view>
<view class="result-info__item">
<text>付款时间</text>
<text>{{ orderInfo.order.pay_time }}</text>
</view>
<view class="result-info__item">
<text>支付方式</text>
<template v-if="orderInfo.pay_status">
<text>{{ orderInfo.order.pay_way || '-' }}</text>
</template>
<template v-else>
<text>未支付</text>
</template>
</view>
</view>
</view>
<view class="mt-[40rpx]">
<view class="mb-[20rpx]">
<u-button
v-if="pageOptions.from == 'recharge'"
type="primary"
shape="circle"
hover-class="none"
@click="goOrder"
>
继续充值
</u-button>
</view>
<view class="mb-[20rpx]">
<u-button
type="primary"
plain
shape="circle"
hover-class="none"
@click="goHome"
>
返回首页
</u-button>
</view>
</view>
</view>
</template>
</page-status>
</template>
<script lang="ts" setup>
import {getPayResult} from '@/api/pay'
import {PageStatusEnum} from '@/enums/appEnums'
import {onLoad} from '@dcloudio/uni-app'
import {computed, reactive, ref} from 'vue'
import {useRouter} from "uniapp-router-next";
const router = useRouter()
const mapStatus = {
succeed: {
text: '支付成功',
image: '/static/images/payment/icon_succeed.png'
},
waiting: {
text: '等待支付',
image: '/static/images/payment/icon_waiting.png'
}
}
const status = ref(PageStatusEnum['LOADING'])
const pageOptions = ref({
id: '',
from: ''
})
const orderInfo = reactive<any>({
order: {}
})
const paymentStatus = computed(() => {
const status = !!orderInfo.pay_status
return mapStatus[status ? 'succeed' : 'waiting']
})
const initPageData = () => {
return new Promise((resolve, reject) => {
getPayResult({
order_id: pageOptions.value.id,
from: pageOptions.value.from
})
.then((data) => {
Object.assign(orderInfo, data)
resolve(data)
})
.catch((err) => {
reject(err)
})
})
}
const goHome = () => {
router.reLaunch('/pages/index/index')
}
const goOrder = () => {
switch (pageOptions.value.from) {
case 'recharge':
router.navigateBack()
break
}
}
onLoad(async (options: any) => {
try {
if (!options.id) throw new Error('订单不存在')
pageOptions.value = options
await initPageData()
status.value = PageStatusEnum['NORMAL']
} catch (err) {
console.log(err)
status.value = PageStatusEnum['ERROR']
}
})
</script>
<style lang="scss" scoped>
.result-info {
.result-info__item {
display: flex;
justify-content: space-between;
margin-bottom: 20rpx;
}
}
</style>
+150
View File
@@ -0,0 +1,150 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar
:front-color="$theme.navColor"
:background-color="$theme.navBgColor"
/>
<!-- #endif -->
</page-meta>
<view
class="register bg-white min-h-full flex flex-col items-center px-[40rpx] pt-[40rpx] box-border"
>
<view class="w-full">
<view class="text-2xl font-medium mb-[60rpx]">注册新账号</view>
<view
class="px-[18rpx] border border-solid border-lightc border-light rounded-[10rpx] h-[100rpx] items-center flex"
>
<u-input
class="flex-1"
v-model="formData.account"
:border="false"
placeholder="请输入账号"
/>
</view>
<view
class="px-[18rpx] border border-solid border-lightc border-light rounded-[10rpx] h-[100rpx] items-center flex mt-[40rpx]"
>
<u-input
class="flex-1"
type="password"
v-model="formData.password"
placeholder="请输入密码"
:border="false"
/>
</view>
<view
class="px-[18rpx] border border-solid border-lightc border-light rounded-[10rpx] h-[100rpx] items-center flex mt-[40rpx]"
>
<u-input
class="flex-1"
type="password"
v-model="formData.password_confirm"
placeholder="请再次输入密码"
:border="false"
/>
</view>
<view class="mt-[40rpx]" v-if="isOpenAgreement">
<u-checkbox v-model="isCheckAgreement" shape="circle">
<view class="text-xs flex">
已阅读并同意
<view @click.stop>
<router-navigate
class="text-primary"
hover-class="none"
to="/pages/agreement/agreement?type=service"
>
服务协议
</router-navigate>
</view>
<view @click.stop>
<router-navigate
class="text-primary"
hover-class="none"
to="/pages/agreement/agreement?type=privacy"
>
隐私协议
</router-navigate>
</view>
</view>
</u-checkbox>
</view>
<view class="mt-[60rpx]">
<u-button
type="primary"
hover-class="none"
@click="accountRegister"
:customStyle="{
height: '100rpx',
opacity:
formData.account && formData.password && formData.password_confirm
? '1'
: '0.5'
}"
>
注册
</u-button>
</view>
</view>
</view>
<!-- 协议弹框 -->
<u-modal
v-model="showModel"
show-cancel-button
:show-title="false"
@confirm=";(isCheckAgreement = true), (showModel = false)"
@cancel="showModel = false"
confirm-color="var(--color-primary)"
>
<view class="text-center px-[70rpx] py-[60rpx]">
<view> 请先阅读并同意</view>
<view class="flex justify-center">
<router-navigate data-theme="" to="/pages/agreement/agreement?type=service">
<view class="text-primary">服务协议</view>
</router-navigate>
<router-navigate to="/pages/agreement/agreement?type=privacy">
<view class="text-primary">隐私协议</view>
</router-navigate>
</view>
</view>
</u-modal>
</template>
<script setup lang="ts">
import {register} from '@/api/account'
import {useAppStore} from '@/stores/app'
import {computed, reactive, ref} from 'vue'
const isCheckAgreement = ref(false)
const appStore = useAppStore()
const isOpenAgreement = computed(() => appStore.getLoginConfig.login_agreement == 1)
const formData = reactive({
account: '',
password: '',
password_confirm: ''
})
const showModel = ref(false)
const accountRegister = async () => {
if (!formData.account) return uni.$u.toast('请输入账号')
if (!formData.password) return uni.$u.toast('请输入密码')
if (!formData.password_confirm) return uni.$u.toast('请输入确认密码')
if (!isCheckAgreement.value && isOpenAgreement.value) return (showModel.value = true)
if (formData.password != formData.password_confirm) return uni.$u.toast('两次输入的密码不一致')
await register(formData)
// uni.navigateBack()
setTimeout(function () {
uni.navigateBack()
}, 1000)
}
</script>
<style lang="scss">
page {
height: 100%;
}
</style>
@@ -0,0 +1,87 @@
<template>
<view class="suggest bg-white">
<!-- 热门搜索 -->
<view class="hot" v-if="hot_search.status == 1 && searchData.length">
<view class="font-medium pl-[24rpx] pt-[26rpx] pb-[6rpx] text-lg">热门搜索</view>
<view class="w-full px-[24rpx]">
<block v-for="(hotItem, index) in searchData" :key="index">
<view
class="keyword truncate max-w-full"
@click="handleHistoreSearch(hotItem.name)"
>
{{ hotItem.name }}
</view>
</block>
</view>
</view>
<view
class="mx-[24rpx] my-[40rpx] border-b border-solid border-0 border-light"
v-if="hot_search.status == 1 && searchData.length && his_search.length"
></view>
<!-- 历史搜索 -->
<view class="history" v-if="his_search.length">
<view class="flex justify-between px-[24rpx] pb-[6rpx] pt-[26rpx]">
<view class="text-lg font-medium">历史搜索</view>
<view class="text-xs text-muted" @click="() => emit('clear')">清空</view>
</view>
<view class="w-full px-[24rpx]">
<block v-for="(hisItem, index) in his_search" :key="index">
<view class="keyword truncate" @click="handleHistoreSearch(hisItem)">{{
hisItem
}}</view>
</block>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
const emit = defineEmits<{
(event: 'search', value: string): void
(event: 'clear', value: void): void
}>()
const props = withDefaults(
defineProps<{
hot_search?: {
data: any[]
status: number
}
his_search?: string[]
}>(),
{
hot_search: () => ({
data: [],
status: 0
}),
his_search: () => []
}
)
const searchData = computed(() => {
return props.hot_search.data.filter((item) => item.name)
})
const handleHistoreSearch = (text: string) => {
emit('search', text)
}
</script>
<style lang="scss" scoped>
.suggest {
height: 100%;
.keyword {
display: inline-block;
margin: 24rpx 16rpx 0 0;
padding: 8rpx 24rpx;
border-radius: 26rpx;
background-color: #f4f4f4;
}
}
</style>
+142
View File
@@ -0,0 +1,142 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar
:front-color="$theme.navColor"
:background-color="$theme.navBgColor"
/>
<!-- #endif -->
</page-meta>
<view class="search">
<!-- 搜索框 -->
<view class="px-[24rpx] py-[14rpx] bg-white">
<u-search
v-model="keyword"
placeholder="请输入关键词搜索"
height="72"
@search="handleSearch"
@custom="handleSearch"
@clear="search.searching = false"
></u-search>
</view>
<!-- 搜索 -->
<view class="search-content">
<!-- -->
<suggest
v-show="!search.searching"
@search="handleSearch"
@clear="handleClear"
:hot_search="search.hot_search"
:his_search="search.his_search"
></suggest>
<!-- -->
<view class="search-content-s pt-[20rpx]" v-show="search.searching">
<z-paging
ref="paging"
v-model="search.result"
@query="queryList"
:fixed="false"
height="100%"
>
<block v-for="item in search.result" :key="item.id">
<news-card :item="item" :newsId="item.id"></news-card>
</block>
</z-paging>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, reactive, shallowRef } from 'vue'
import Suggest from './component/suggest.vue'
import { HISTORY } from '@/enums/constantEnums'
import { getHotSearch } from '@/api/shop'
import cache from '@/utils/cache'
import { getArticleList } from '@/api/news'
interface Search {
hot_search: {
data: any[]
status: number
}
his_search: string[]
result: any
searching: boolean
}
const search = reactive<Search>({
hot_search: {
data: [],
status: 1
},
his_search: [],
result: [],
searching: false
})
const keyword = ref<string>('')
const paging = shallowRef()
const handleSearch = (value: string) => {
keyword.value = value
if (keyword.value) {
if (!search.his_search.includes(keyword.value)) {
search.his_search.unshift(keyword.value)
cache.set(HISTORY, search.his_search)
}
}
paging.value.reload()
search.searching = true
}
const getHotSearchFunc = async () => {
try {
search.hot_search = await getHotSearch()
} catch (e) {
//TODO handle the exception
console.log('获取热门搜索失败=>', e)
}
}
const handleClear = async (): Promise<void> => {
const resModel: any = await uni.showModal({
title: '温馨提示',
content: '是否清空历史记录?'
})
if (resModel.confirm) {
cache.set(HISTORY, '')
search.his_search = []
}
}
const queryList = async (page_no: number, page_size: number) => {
try {
const { lists } = await getArticleList({
keyword: keyword.value,
page_no,
page_size
})
paging.value.complete(lists)
} catch (e) {
console.log('报错=>', e)
//TODO handle the exception
paging.value.complete(false)
}
}
getHotSearchFunc()
search.his_search = cache.get(HISTORY) || []
</script>
<style lang="scss" scoped>
.search {
&-content {
height: calc(100vh - 46px - env(safe-area-inset-bottom));
&-s {
height: 100%;
}
}
}
</style>
+61
View File
@@ -0,0 +1,61 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar
:front-color="$theme.navColor"
:background-color="$theme.navBgColor"
/>
<!-- #endif -->
</page-meta>
<view class="user">
<view v-for="(item, index) in state.pages" :key="index">
<template v-if="item.name == 'user-info'">
<w-user-info
:pageMeta="state.meta"
:content="item.content"
:styles="item.styles"
:user="userInfo"
:is-login="isLogin"
/>
</template>
<template v-if="item.name == 'my-service'">
<w-my-service :content="item.content" :styles="item.styles" />
</template>
<template v-if="item.name == 'user-banner'">
<w-user-banner :content="item.content" :styles="item.styles" />
</template>
</view>
<tabbar />
</view>
</template>
<script setup lang="ts">
import { getDecorate } from '@/api/shop'
import { useUserStore } from '@/stores/user'
import { onShow } from '@dcloudio/uni-app'
import { storeToRefs } from 'pinia'
import { reactive } from 'vue'
const state = reactive<{
meta: any[]
pages: any[]
}>({
meta: [],
pages: []
})
const getData = async () => {
const data = await getDecorate({ id: 2 })
state.meta = JSON.parse(data.meta)
state.pages = JSON.parse(data.data)
uni.setNavigationBarTitle({
title: state.meta[0].content.title
})
}
const userStore = useUserStore()
const { userInfo, isLogin } = storeToRefs(userStore)
onShow(() => {
userStore.getUser()
})
getData()
</script>
<style></style>
+365
View File
@@ -0,0 +1,365 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar
:front-color="$theme.navColor"
:background-color="$theme.navBgColor"
/>
<!-- #endif -->
</page-meta>
<!-- Main Start -->
<!-- 头部修改头像 -->
<view class="header bg-white pt-[30rpx]">
<view class="flex justify-center pb-5">
<avatar-upload
:modelValue="userInfo?.avatar"
file-key="url"
:round="true"
@update:modelValue="handleAvatarChange"
>
</avatar-upload>
</view>
</view>
<!-- 用户ID -->
<view
class="item text-nr flex justify-between"
@click=";(showUserName = true), (newUsername = userInfo?.username)"
>
<view class="label">账号</view>
<view class="content">{{ userInfo?.account }}</view>
<u-icon name="arrow-right" size="22" color="#666"></u-icon>
</view>
<!-- 昵称 -->
<view
class="item text-nr flex justify-between"
@click=";(showNickName = true), (newNickname = userInfo?.nickname)"
>
<view class="label">昵称</view>
<view class="content">{{ userInfo?.nickname }}</view>
<u-icon name="arrow-right" size="22" color="#666"></u-icon>
</view>
<!-- 性别 -->
<view class="item text-nr flex justify-between" @click="changeSex">
<view class="label">性别</view>
<view class="content">{{ userInfo?.sex }}</view>
<u-icon name="arrow-right" size="22" color="#666"></u-icon>
</view>
<!-- 手机号 -->
<view class="item text-nr flex justify-between">
<view class="label">手机号</view>
<view class="content">{{
userInfo?.mobile == '' ? '未绑定手机号' : userInfo?.mobile
}}</view>
<!-- #ifdef MP-WEIXIN -->
<u-button
open-type="getPhoneNumber"
@getphonenumber="getPhoneNumber"
type="primary"
shape="circle"
size="mini"
:plain="true"
>
{{ userInfo?.mobile == '' ? '绑定手机号' : '更换手机号' }}
</u-button>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<u-button
@click="showMobilePop = true"
size="mini"
type="primary"
shape="circle"
:plain="true"
>
{{ userInfo?.mobile == '' ? '绑定手机号' : '更换手机号' }}
</u-button>
<!-- #endif -->
</view>
<!-- 注册时间 -->
<view class="item text-nr flex justify-between">
<view class="label">注册时间</view>
<view class="content">{{ userInfo?.create_time }}</view>
</view>
<!-- 昵称修改组件 -->
<u-popup
v-model="showNickName"
:closeable="true"
mode="center"
:maskCloseAble="false"
border-radius="20"
>
<view class="px-[50rpx] py-[40rpx] bg-white" style="width: 85vw">
<form @submit="changeNameConfirm">
<view class="mb-[70rpx] text-xl text-center">修改昵称</view>
<u-form-item borderBottom>
<input
class="nr h-[60rpx] w-full"
:value="userInfo.nickname"
name="nickname"
type="nickname"
placeholder="请输入昵称"
/>
</u-form-item>
<view class="mt-[80rpx]">
<button
class="bg-primary text-white w-full h-[80rpx] !text-lg !leading-[80rpx] rounded-full"
form-type="submit"
size="mini"
hover-class="none"
>
确定
</button>
</view>
</form>
</view>
</u-popup>
<!-- 账号修改组件 -->
<u-popup v-model="showUserName" :closeable="true" mode="center" border-radius="20">
<view class="px-[50rpx] py-[40rpx] bg-white" style="width: 85vw">
<view class="mb-[70rpx] text-xl text-center">修改账号</view>
<u-form-item borderBottom>
<u-input
class="flex-1"
v-model="newUsername"
placeholder="请输入账号"
:border="false"
/>
</u-form-item>
<view class="mt-[80rpx]">
<u-button @click="changeUserNameConfirm" type="primary" shape="circle">
确定
</u-button>
</view>
</view>
</u-popup>
<!-- 性别修改组件 -->
<u-picker
mode="selector"
v-model="showPicker"
confirm-color="#4173FF"
:default-selector="[0]"
:range="sexList"
@confirm="changeSexConfirm"
/>
<!-- 账号修改组件 -->
<u-popup v-model="showMobilePop" :closeable="true" mode="center" border-radius="20">
<view class="px-[50rpx] py-[40rpx] bg-white" style="width: 85vw">
<view class="mb-[70rpx] text-xl text-center">{{ userInfo?.mobile == '' ? '绑定手机号' : '更换手机号' }}</view>
<u-form-item borderBottom>
<u-input
class="flex-1"
v-model="newMobile"
placeholder="请输入新的手机号码"
:border="false"
/>
</u-form-item>
<u-form-item borderBottom>
<u-input
class="flex-1"
v-model="mobileCode"
placeholder="请输入验证码"
:border="false"
/>
<view
class="border-l border-solid border-0 border-light pl-3 text-muted leading-4 ml-3 w-[180rpx]"
@click="sendSms"
>
<u-verification-code
ref="uCodeRef"
:seconds="60"
@change="codeChange"
change-text="x秒"
/>
{{ codeTips }}
</view>
</u-form-item>
<view class="mt-[80rpx]">
<u-button @click="changeCodeMobile" type="primary" shape="circle"> 确定 </u-button>
</view>
</view>
</u-popup>
</template>
<script lang="ts" setup>
import { ref, shallowRef } from 'vue'
import { onShow, onUnload } from '@dcloudio/uni-app'
import { getUserInfo, userEdit, userBindMobile, userMnpMobile } from '@/api/user'
import { smsSend } from '@/api/app'
import { FieldType, SMSEnum } from '@/enums/appEnums'
// 用户信息
const userInfo = ref<any>({})
// 用户信息的枚举
const fieldType = ref(FieldType.NONE)
//选择性别数据
const sexList = ref<Array<string> | null>(['男', '女'])
//显示昵称弹窗
const showNickName = ref<boolean | null>(false)
//显示账户弹窗
const showUserName = ref<boolean | null>(false)
//显示性别选择弹窗
const showPicker = ref<boolean | null>(false)
// 显示手机号验证码调整弹窗 非小程序才需要
const showMobilePop = ref<boolean | null>(false)
//新昵称
const newNickname = ref<string>('')
//新账号
const newUsername = ref<string>('')
//新的手机号码
const newMobile = ref<string>('')
//修改手机验证码
const mobileCode = ref<string>('')
const codeTips = ref('')
const uCodeRef = shallowRef()
// 获取用户信息
const getUser = async (): Promise<void> => {
userInfo.value = await getUserInfo()
}
// 获取验证码显示字段
const codeChange = (text: string) => {
codeTips.value = text
}
// 发送验证码
const sendSms = async () => {
if (!newMobile.value) return uni.$u.toast('请输入新的手机号码')
if (uCodeRef.value?.canGetCode) {
await smsSend({
scene: userInfo.value.mobile ? SMSEnum.CHANGE_MOBILE : SMSEnum.BIND_MOBILE,
mobile: newMobile.value
})
uni.$u.toast('发送成功')
uCodeRef.value?.start()
}
}
const handleAvatarChange = (value) => {
fieldType.value = FieldType.AVATAR
setUserInfoFun(value)
}
// 验证码修改手机号-非微信小程序
const changeCodeMobile = async () => {
await userBindMobile({
type: userInfo.value.mobile ? 'change' : 'bind',
mobile: newMobile.value,
code: mobileCode.value
})
uni.$u.toast('操作成功')
showMobilePop.value = false
getUser()
}
// 修改用户信息
const setUserInfoFun = async (value: string): Promise<void> => {
await userEdit({
field: fieldType.value,
value: value
})
uni.$u.toast('操作成功')
getUser()
}
// 显示修改用户性别弹窗
const changeSex = () => {
showPicker.value = true
fieldType.value = FieldType.SEX
}
// 修改用户性别
const changeSexConfirm = (value) => {
setUserInfoFun(value[0] + 1)
showPicker.value = false
}
// 修改用户账号
const changeUserNameConfirm = () => {
if (newUsername.value == '') return uni.$u.toast('账号不能为空')
if (newUsername.value.length > 10) return uni.$u.toast('账号长度不得超过十位数')
fieldType.value = FieldType.USERNAME
setUserInfoFun(newUsername.value)
showUserName.value = false
}
// 修改用户昵称
const changeNameConfirm = async (e: any) => {
newNickname.value = e.detail.value.nickname
if (newNickname.value == '') return uni.$u.toast('昵称不能为空')
if (newNickname.value.length > 10) return uni.$u.toast('昵称长度不得超过十位数')
fieldType.value = FieldType.NICKNAME
await setUserInfoFun(newNickname.value)
showNickName.value = false
}
// 微信小程序 绑定||修改用户手机号
const getPhoneNumber = async (e): Promise<void> => {
const { encryptedData, iv, code } = e.detail
const data = {
code,
encrypted_data: encryptedData,
iv
}
if (encryptedData) {
await userMnpMobile({
...data
})
uni.$u.toast('操作成功')
getUser()
}
}
const goPage = (url: string) => {
uni.navigateTo({
url: url
})
}
onShow(async () => {
getUser()
})
onUnload(() => {})
</script>
<style lang="scss">
.header {
width: 100%;
image {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
}
}
.item {
margin-top: 2rpx;
padding: 30rpx;
background-color: #ffffff;
.label {
width: 150rpx;
}
.content {
flex: 1;
width: 80%;
}
}
</style>
+248
View File
@@ -0,0 +1,248 @@
<template>
<page-meta :page-style="$theme.pageStyle">
<!-- #ifndef H5 -->
<navigation-bar
:front-color="$theme.navColor"
:background-color="$theme.navBgColor"
/>
<!-- #endif -->
</page-meta>
<view class="user-set">
<navigator :url="`/pages/user_data/user_data`">
<view class="item flex bg-white mt-[20rpx]">
<u-avatar :src="userInfo.avatar" shape="square" :size="100"></u-avatar>
<view class="ml-[20rpx] flex flex-1 justify-between items-center">
<view>
<view class="mb-[15rpx] text-xl font-medium">{{ userInfo.nickname }}</view>
<view class="text-content text-xs">账号{{ userInfo.account }}</view>
</view>
<u-icon name="arrow-right" color="#666"></u-icon>
</view>
</view>
</navigator>
<view
class="item bg-white mt-[20rpx] btn-border flex flex-1 justify-between"
@click="handlePwd"
>
<view class="">登录密码</view>
<u-icon name="arrow-right" color="#666"></u-icon>
</view>
<!-- #ifdef H5 || MP-WEIXIN -->
<view
v-if="isWeixin"
class="item bg-white flex flex-1 justify-between"
@click="bindWechatLock"
>
<view class="">绑定微信</view>
<view class="flex justify-between">
<view class="text-muted mr-[20rpx]">
{{ userInfo.is_auth ? '已绑定' : '未绑定' }}
</view>
<u-icon v-if="userInfo.is_auth == 0" name="arrow-right" color="#666"></u-icon>
</view>
</view>
<!-- #endif -->
<navigator :url="`/pages/agreement/agreement?type=${AgreementEnum.PRIVACY}`">
<view class="item bg-white mt-[20rpx] btn-border flex flex-1 justify-between">
<view class="">隐私政策</view>
<u-icon name="arrow-right" color="#666"></u-icon>
</view>
</navigator>
<navigator :url="`/pages/agreement/agreement?type=${AgreementEnum.SERVICE}`">
<view class="item bg-white btn-border flex flex-1 justify-between">
<view class="">服务协议</view>
<u-icon name="arrow-right" color="#666"></u-icon>
</view>
</navigator>
<navigator url="/pages/as_us/as_us">
<view class="item bg-white flex flex-1 justify-between">
<view class="">关于我们</view>
<view class="flex justify-between">
<view class="text-muted mr-[20rpx]">
{{ appStore.config.version }}
</view>
<u-icon name="arrow-right" color="#666"></u-icon>
</view>
</view>
</navigator>
<view class="mt-[60rpx] mx-[26rpx]">
<u-button type="primary" shape="circle" @click="showLogout = true"> 退出登录</u-button>
</view>
<u-action-sheet
:list="list"
v-model="show"
@click="handleClick"
:safe-area-inset-bottom="true"
></u-action-sheet>
<u-popup
class="pay-popup"
v-model="showLogout"
round
mode="center"
borderRadius="10"
:maskCloseAble="false"
>
<view class="content bg-white w-[560rpx] p-[40rpx]">
<view class="text-2xl font-medium text-center"> 温馨提示</view>
<view class="pt-[30rpx] pb-[40rpx]">
<view> 是否清除当前登录信息退出登录</view>
</view>
<view class="flex">
<view class="flex-1 mr-[20rpx]">
<u-button
shape="circle"
type="primary"
plain
size="medium"
hover-class="none"
:customStyle="{ width: '100%' }"
@click="showLogout = false"
>
取消
</u-button>
</view>
<view class="flex-1">
<u-button
shape="circle"
type="primary"
size="medium"
hover-class="none"
:customStyle="{ width: '100%' }"
@click="logoutHandle"
>
确认
</u-button>
</view>
</view>
</view>
</u-popup>
</view>
</template>
<script setup lang="ts">
import {onLoad, onShow} from '@dcloudio/uni-app'
import {computed, ref} from 'vue'
import {useAppStore} from '@/stores/app'
import {useUserStore} from '@/stores/user'
import {AgreementEnum} from '@/enums/agreementEnums'
import {isWeixinClient} from '@/utils/client'
import {mnpAuthBind, oaAuthBind} from '@/api/account'
import {useLockFn} from '@/hooks/useLockFn'
import {useRouter} from "uniapp-router-next";
// #ifdef H5
import wechatOa from '@/utils/wechat'
// #endif
const router = useRouter()
const appStore = useAppStore()
const userStore = useUserStore()
const userInfo = computed(() => userStore.userInfo)
const list = ref([
{
text: '修改密码'
},
{
text: '忘记密码'
}
])
const isWeixin = ref(true)
// #ifdef H5
isWeixin.value = isWeixinClient()
// #endif
const show = ref(false)
const showLogout = ref(false)
// 修改/忘记密码
const handleClick = (index: number) => {
switch (index) {
case 0:
router.navigateTo('/pages/change_password/change_password')
break
case 1:
router.navigateTo('/pages/forget_pwd/forget_pwd')
break
}
}
const handlePwd = () => {
if (!userInfo.value.has_password)
return router.navigateTo('/pages/change_password/change_password?type=set')
show.value = true
}
// 退出登录
const logoutHandle = () => {
userStore.logout()
router.redirectTo('/pages/login/login')
}
const bindWechat = async () => {
if (userInfo.value.is_auth) return
try {
uni.showLoading({
title: '请稍后...'
})
// #ifdef MP-WEIXIN
const {code}: any = await uni.login({
provider: 'weixin'
})
await mnpAuthBind({
code: code
})
//#endif
// #ifdef H5
if (isWeixin.value) {
wechatOa.getUrl()
}
// #endif
await userStore.getUser()
uni.hideLoading()
} catch (e) {
uni.hideLoading()
uni.$u.toast(e)
}
}
const {lockFn: bindWechatLock} = useLockFn(bindWechat)
onShow(() => {
userStore.getUser()
})
onLoad(async (options) => {
// #ifdef H5
const {code} = options
if (!isWeixin.value) return
if (code) {
uni.showLoading({
title: '请稍后...'
})
try {
await oaAuthBind({code})
await userStore.getUser()
} catch (error) {
}
//用于清空code
router.redirectTo('/pages/user_set/user_set')
}
// #endif
})
</script>
<style lang="scss" scoped>
.user-set {
.item {
padding: 30rpx;
}
.btn-border {
border-bottom: 2rpx solid #f8f8f8;
}
}
</style>
+16
View File
@@ -0,0 +1,16 @@
<template>
<web-view :src="url" />
</template>
<script setup lang="ts">
import { onLoad } from '@dcloudio/uni-app'
import { ref } from 'vue'
const url = ref('')
onLoad((options) => {
url.value = decodeURIComponent(options.url!)
})
</script>
<style></style>
+12
View File
@@ -0,0 +1,12 @@
import { isFunction } from '@vue/shared'
import { App } from 'vue'
const modules = import.meta.globEager('./modules/**/*.ts')
export default {
install: (app: App) => {
for (const module of Object.values(modules)) {
const fun = module.default
isFunction(fun) && fun(app)
}
}
}
+6
View File
@@ -0,0 +1,6 @@
import { App } from 'vue'
import { createPinia } from 'pinia'
const pinia = createPinia()
export default (app: App) => {
app.use(pinia)
}
+7
View File
@@ -0,0 +1,7 @@
import { App } from 'vue'
import uView from '@/uni_modules/vk-uview-ui'
export default (app: App) => {
// 使用 uView UI
app.use(uView)
}
+18
View File
@@ -0,0 +1,18 @@
// url中携带参数vconsole == vconsoleMd5时打开Vconsole,可以用于手机浏览器中调试
// 如/mobile/pages/user/user?vconsole=47b1e3a9d33e6064e58cc4796c708447,此时在个人中心页面打开vconsole
const vconsoleMd5 = '47b1e3a9d33e6064e58cc4796c708447'
export default async () => {
// #ifdef H5
const url = new URL(location.href)
const searchParams = new URLSearchParams(url.search)
const vconsole = searchParams.get('vconsole')
if (vconsole == vconsoleMd5) {
const module: any = await import('vconsole')
const Vconsole = module.default
const vConsole = new Vconsole()
return vConsole
}
// #endif
}
+91
View File
@@ -0,0 +1,91 @@
import routes from 'uni-router-routes'
import { createRouter } from 'uniapp-router-next'
import { ClientEnum } from '@/enums/appEnums'
import { useUserStore } from '@/stores/user'
import { client } from '@/utils/client'
// #ifdef H5
import wechatOa from '@/utils/wechat'
// #endif
import cache from '@/utils/cache'
import { BACK_URL } from '@/enums/constantEnums'
const router = createRouter({
routes: [
...routes,
{
path: '*',
redirect() {
return {
name: '404'
}
}
}
],
debug: import.meta.env.DEV,
//@ts-ignore
platform: process.env.UNI_PLATFORM,
h5: {}
})
//存储登陆前的页面
let isFirstEach = true
router.beforeEach(async (to, from) => {
//保存第一次进来时的页面路径(需要登陆才能访问的页面)
if (isFirstEach) {
const userStore = useUserStore()
if (!userStore.isLogin && !to.meta.white) {
cache.set(BACK_URL, to.fullPath)
}
isFirstEach = false
}
})
router.afterEach((to, from) => {
const userStore = useUserStore()
if (!userStore.isLogin && !to.meta.white) {
cache.set(BACK_URL, to.fullPath)
}
})
// 登录拦截
router.beforeEach(async (to, from) => {
const userStore = useUserStore();
if (!userStore.isLogin && to.meta.auth) {
return '/pages/login/login'
}
})
// #ifdef H5
//用于收集微信公众号的授权的code,并清除路径上微信带的参数
router.beforeEach(async (to, form) => {
const { code, state, scene } = to.query
if (code && state && scene) {
wechatOa.setAuthData({
code,
scene
})
//收集完删除路径上的参数
delete to.query.code
delete to.query.state
return {
path: to.path,
force: true,
navType: 'reLaunch',
query: to.query
}
}
})
// #endif
// #ifdef H5
router.afterEach((to, from) => {
setTimeout(async () => {
if (client == ClientEnum.OA_WEIXIN && !to.meta.webview) {
// jssdk配置
await wechatOa.config()
}
})
})
// #endif
export default router
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 679 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 972 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

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